diff --git a/.cursor/rules/reference-code.mdc b/.cursor/rules/reference-code.mdc new file mode 100644 index 00000000..dead2b11 --- /dev/null +++ b/.cursor/rules/reference-code.mdc @@ -0,0 +1,8 @@ +--- +description: Location of fc-app reference code +alwaysApply: true +--- + +# Reference Code + +fc-app / reference code is located in `contrib/star-atlas-tech/`. Use this as reference when implementing features or understanding patterns. diff --git a/.cursor/rules/solana-dev.mdc b/.cursor/rules/solana-dev.mdc new file mode 100644 index 00000000..d77e4687 --- /dev/null +++ b/.cursor/rules/solana-dev.mdc @@ -0,0 +1,15 @@ +--- +description: Always use solana-dev-skill for Solana development +alwaysApply: true +--- + +# Solana Development + +When working on Solana code, ALWAYS read and follow the solana-dev-skill at `contrib/solana-dev-skill/skill/SKILL.md` FIRST before doing anything else. + +Read the progressive disclosure files as needed: +- `frontend-framework-kit.md` - UI + wallet + hooks +- `kit-web3-interop.md` - Kit ↔ web3.js boundary +- `testing.md` - Testing strategy +- `security.md` - Security checklist +- `resources.md` - Reference links diff --git a/.cursor/rules/wait-for-instructions.mdc b/.cursor/rules/wait-for-instructions.mdc new file mode 100644 index 00000000..2246b0b5 --- /dev/null +++ b/.cursor/rules/wait-for-instructions.mdc @@ -0,0 +1,13 @@ +--- +description: Only act on explicit user instructions +alwaysApply: true +--- + +# Wait for Explicit Instructions + +NEVER do anything the user did not explicitly ask for. + +- Do NOT proactively make changes +- Do NOT start implementing before getting clear instructions +- Questions ≠ requests to change code +- Always answer questions FIRST, then wait for explicit instruction to act diff --git a/.github/actions/docker-buildx/action.yml b/.github/actions/docker-buildx/action.yml index fd088610..c545be33 100644 --- a/.github/actions/docker-buildx/action.yml +++ b/.github/actions/docker-buildx/action.yml @@ -40,6 +40,9 @@ inputs: cache-strategy: default: 'registry' description: 'Cache strategy: gha or registry' + npm-token: + default: '' + description: 'NPM token for private registry access during build' runs: using: "composite" steps: @@ -77,4 +80,6 @@ runs: cache-to: ${{ inputs.cache-enabled == 'true' && (inputs.cache-strategy == 'gha' && 'type=gha,mode=max' || format('type=registry,ref={0}/{1}:cache,mode=max', inputs.repository, inputs.app-name)) || '' }} build-args: ${{ inputs.build-args }} ssh: ${{ inputs.ssh-enabled == 'true' && 'default' || '' }} - secrets: ${{ inputs.extra-secrets }} + secrets: | + ${{ inputs.extra-secrets }} + ${{ inputs.npm-token != '' && format('npmrc=//registry.npmjs.org/:_authToken={0}', inputs.npm-token) || '' }} diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index 852f36c0..a753e084 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -1,6 +1,12 @@ name: 'Install cached modules' description: 'Run pnpm install with cached modules' +inputs: + npm-token: + description: 'NPM token for private registry' + required: false + default: '' + runs: using: 'composite' steps: @@ -28,6 +34,12 @@ runs: restore-keys: | pnpm-store- + - name: Configure npm auth + shell: bash + if: ${{ inputs.npm-token != '' }} + run: | + echo "//registry.npmjs.org/:_authToken=${{ inputs.npm-token }}" > ~/.npmrc + - name: Install dependencies shell: bash run: pnpm install --frozen-lockfile diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 85b6da53..84ef7ec3 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -12,6 +12,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: ./.github/actions/install + with: + npm-token: ${{ secrets.NPM_TOKEN }} - run: pnpm run build lint: @@ -19,6 +21,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: ./.github/actions/install + with: + npm-token: ${{ secrets.NPM_TOKEN }} - run: pnpm run lint release: @@ -26,6 +30,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: ./.github/actions/install + with: + npm-token: ${{ secrets.NPM_TOKEN }} - run: pnpm run build - run: pnpm install --frozen-lockfile --ignore-scripts --prod - uses: ./.github/actions/docker-buildx @@ -36,4 +42,5 @@ jobs: dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} dockerhub-password: ${{ secrets.DOCKERHUB_TOKEN }} dockerfile: 'docker/Dockerfile' + npm-token: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 058bac83..ac97cc2d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,10 @@ /node_modules .env* !.env.example +.npmrc *.iml dist/ src/main/basedbot/lib/sage/ships.json package-lock.json +contrib/star-atlas-tech/ diff --git a/contrib/solana-dev-skill b/contrib/solana-dev-skill new file mode 160000 index 00000000..dcc633a3 --- /dev/null +++ b/contrib/solana-dev-skill @@ -0,0 +1 @@ +Subproject commit dcc633a384c033bb1ee20c26325caec6c611ecae diff --git a/docker/Dockerfile b/docker/Dockerfile index 83608ff6..cc4faa47 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -12,7 +12,7 @@ RUN apk add --no-cache --virtual .build-deps \ WORKDIR /app RUN corepack enable -COPY package.json pnpm-lock.yaml pnpm-workspace.yaml* ./ +COPY package.json pnpm-lock.yaml ./ COPY tsup.config.ts ./ COPY tsconfig.json ./ COPY src ./src @@ -37,7 +37,6 @@ ADD https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.s COPY --from=builder /app/node_modules /app/node_modules COPY --from=builder /app/dist/airdrop/index.js /app/airdrop.js -COPY --from=builder /app/dist/fleetbot/index.js /app/fleetbot.js COPY --from=builder /app/dist/basedbot/index.js /app/basedbot.js COPY --from=builder /app/dist/migrate/index.js /app/migrate.js COPY src/db/db-data-source.ts /app/db/db-data-source.ts diff --git a/docker/bin/fleetbot.sh b/docker/bin/fleetbot.sh deleted file mode 100755 index 2e60bb78..00000000 --- a/docker/bin/fleetbot.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -migrate-db.sh - -/app/fleetbot.js diff --git a/eslint.config.mjs b/eslint.config.mjs index 31e93b67..8a01805f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,7 +5,7 @@ import globals from 'globals' export default [ { - ignores: ['node_modules/', 'dist/'], + ignores: ['node_modules/', 'dist/', 'contrib/'], }, { files: ['src/**/*.ts', 'test/**/*.ts'], diff --git a/package.json b/package.json index a0c92c0a..953f17c4 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "type": "module", "scripts": { "type-check": "tsc --noEmit", - "start:fleetbot": "tsx src/main/fleetbot/index.ts", "start:basedbot": "tsx src/main/basedbot/index.ts", + "dev:basedbot": "tsx --watch src/main/basedbot/index.ts", "start:airdrop": "tsx src/main/airdrop/index.ts", "build": "pnpm run type-check && tsup", "build:docker": "docker build -t fleetbot -f docker/app/Dockerfile .", @@ -30,26 +30,19 @@ }, "homepage": "https://github.com/mindrunner/fleetbot#readme", "dependencies": { - "@coral-xyz/anchor": "^0.29.0", "@faker-js/faker": "^9.7.0", "@sentry/integrations": "^7.114.0", "@sentry/node": "^9.17.0", "@sentry/tracing": "^7.120.3", - "@solana/spl-token": "^0.4.13", - "@solana/web3.js": "^1.98.2", - "@staratlas/atlas-prime": "^0.13.1", - "@staratlas/cargo": "^1.1.0", - "@staratlas/claim-stake": "^0.12.0", - "@staratlas/crafting": "^1.1.0", - "@staratlas/data-source": "^0.9.0", - "@staratlas/factory": "^0.7.1", - "@staratlas/player-profile": "^0.11.0", - "@staratlas/points": "^1.1.0", - "@staratlas/profile-faction": "^0.6.0", - "@staratlas/sage": "^1.8.10", + "@solana-program/compute-budget": "^0.9.0", + "@solana-program/system": "^0.9.0", + "@solana-program/token": "^0.9.0", + "@solana/kit": "^5.4.0", + "@staratlas/dev-player-profile": "^0.40.0", + "@staratlas/dev-profile-faction": "^0.40.0", + "@staratlas/dev-sage": "^0.40.0", "big.js": "^7.0.1", "bip39": "^3.1.0", - "bn.js": "^5.2.2", "bs58": "^6.0.0", "chance": "^1.1.12", "cron": "^4.3.0", @@ -62,6 +55,7 @@ "pretty-error": "^4.0.0", "punycode": "^2.3.1", "reflect-metadata": "^0.2.2", + "solana-kite": "^2.1.0", "superagent": "^10.2.1", "telegraf": "^4.16.3", "typeorm": "^0.3.23", @@ -70,7 +64,6 @@ }, "devDependencies": { "@types/big.js": "^6.2.2", - "@types/bn.js": "^5.1.6", "@types/chance": "^1.1.6", "@types/module-alias": "^2.0.4", "@types/node": "^22.15.29", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 566d4f05..dc5f7132 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,6 @@ importers: .: dependencies: - '@coral-xyz/anchor': - specifier: ^0.29.0 - version: 0.29.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) '@faker-js/faker': specifier: ^9.7.0 version: 9.8.0 @@ -23,51 +20,33 @@ importers: '@sentry/tracing': specifier: ^7.120.3 version: 7.120.3 - '@solana/spl-token': - specifier: ^0.4.13 - version: 0.4.13(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@solana/web3.js': - specifier: ^1.98.2 - version: 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/atlas-prime': - specifier: ^0.13.1 - version: 0.13.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/cargo': - specifier: ^1.1.0 - version: 1.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/claim-stake': - specifier: ^0.12.0 - version: 0.12.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/crafting': - specifier: ^1.1.0 - version: 1.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/data-source': + '@solana-program/compute-budget': specifier: ^0.9.0 - version: 0.9.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/factory': - specifier: ^0.7.1 - version: 0.7.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/player-profile': - specifier: ^0.11.0 - version: 0.11.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/points': - specifier: ^1.1.0 - version: 1.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/profile-faction': - specifier: ^0.6.0 - version: 0.6.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/sage': - specifier: ^1.8.10 - version: 1.8.10(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + version: 0.9.0(@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@solana-program/system': + specifier: ^0.9.0 + version: 0.9.1(@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@solana-program/token': + specifier: ^0.9.0 + version: 0.9.0(@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@solana/kit': + specifier: ^5.4.0 + version: 5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@staratlas/dev-player-profile': + specifier: ^0.40.0 + version: 0.40.0(@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@staratlas/dev-profile-faction': + specifier: ^0.40.0 + version: 0.40.0(@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@staratlas/dev-sage': + specifier: ^0.40.0 + version: 0.40.0(@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)) big.js: specifier: ^7.0.1 version: 7.0.1 bip39: specifier: ^3.1.0 version: 3.1.0 - bn.js: - specifier: ^5.2.2 - version: 5.2.2 bs58: specifier: ^6.0.0 version: 6.0.0 @@ -104,6 +83,9 @@ importers: reflect-metadata: specifier: ^0.2.2 version: 0.2.2 + solana-kite: + specifier: ^2.1.0 + version: 2.1.0(@solana/sysvars@5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) superagent: specifier: ^10.2.1 version: 10.2.1 @@ -123,9 +105,6 @@ importers: '@types/big.js': specifier: ^6.2.2 version: 6.2.2 - '@types/bn.js': - specifier: ^5.1.6 - version: 5.1.6 '@types/chance': specifier: ^1.1.6 version: 1.1.6 @@ -336,10 +315,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.27.1': - resolution: {integrity: sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==} - engines: {node: '>=6.9.0'} - '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -359,16 +334,6 @@ packages: resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} - '@coral-xyz/anchor@0.29.0': - resolution: {integrity: sha512-eny6QNG0WOwqV0zQ7cs/b1tIuzZGmP7U7EcH+ogt4Gdbl8HDmIYVMh/9aTmYZPaFWjtUaI8qSn73uYEXWfATdA==} - engines: {node: '>=11'} - - '@coral-xyz/borsh@0.29.0': - resolution: {integrity: sha512-s7VFVa3a0oqpkuRloWVPdCK7hMbAMY270geZOGfCnaqexrP5dTIpbEHL33req6IYPPJ0hYa71cdvJ1h6V55/oQ==} - engines: {node: '>=10'} - peerDependencies: - '@solana/web3.js': ^1.68.0 - '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -564,15 +529,6 @@ packages: resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@ethereumjs/rlp@4.0.1': - resolution: {integrity: sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==} - engines: {node: '>=14'} - hasBin: true - - '@ethereumjs/util@8.1.0': - resolution: {integrity: sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==} - engines: {node: '>=14'} - '@faker-js/faker@9.8.0': resolution: {integrity: sha512-U9wpuSrJC93jZBxx/Qq2wPjCuYISBueyVUGK7qqdmj7r/nxaxwW8AQDCLeRO7wZnjj94sh3p246cAYjUKuqgfg==} engines: {node: '>=18.0.0', npm: '>=9.0.0'} @@ -696,129 +652,6 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@metaplex-foundation/beet-solana@0.4.1': - resolution: {integrity: sha512-/6o32FNUtwK8tjhotrvU/vorP7umBuRFvBZrC6XCk51aKidBHe5LPVPA5AjGPbV3oftMfRuXPNd9yAGeEqeCDQ==} - - '@metaplex-foundation/beet@0.7.2': - resolution: {integrity: sha512-K+g3WhyFxKPc0xIvcIjNyV1eaTVJTiuaHZpig7Xx0MuYRMoJLLvhLTnUXhFdR5Tu2l2QSyKwfyXDgZlzhULqFg==} - - '@metaplex-foundation/digital-asset-standard-api@1.0.6': - resolution: {integrity: sha512-lgequ4N69A7QQjyVt0ViqdeXrnviihXZR+Y9rzAD79JtM3NdbKJ3/BTASgYdCm8oAsVBeLw1Wbf5NjCYdfgS8w==} - peerDependencies: - '@metaplex-foundation/umi': '>= 0.8.2 <= 1' - - '@metaplex-foundation/mpl-bubblegum@4.4.0': - resolution: {integrity: sha512-FiRlInn3ZAcdrJHKEWmVOrcQDI4CaME1w9DcxrlbYxbbpsSnO6of6g5zZ8SsimebGjDR+oxSkYO6LkcCPagvHQ==} - peerDependencies: - '@metaplex-foundation/umi': '>= 0.8.9 <= 1' - - '@metaplex-foundation/mpl-token-metadata@3.2.1': - resolution: {integrity: sha512-26W1NhQwDWmLOg/pBRYut7x/vEs/5kFS2sWVEY5/X0f2jJOLhnd4NaZQcq+5u+XZsXvm1jq2AtrRGPNK43oqWQ==} - peerDependencies: - '@metaplex-foundation/umi': '>= 0.8.2 < 1' - - '@metaplex-foundation/mpl-token-metadata@3.4.0': - resolution: {integrity: sha512-AxBAYCK73JWxY3g9//z/C9krkR0t1orXZDknUPS4+GjwGH2vgPfsk04yfZ31Htka2AdS9YE/3wH7sMUBHKn9Rg==} - peerDependencies: - '@metaplex-foundation/umi': '>= 0.8.2 <= 1' - - '@metaplex-foundation/mpl-toolbox@0.10.0': - resolution: {integrity: sha512-84KD1L5cFyw5xnntHwL4uPwfcrkKSiwuDeypiVr92qCUFuF3ZENa2zlFVPu+pQcjTlod2LmEX3MhBmNjRMpdKg==} - peerDependencies: - '@metaplex-foundation/umi': '>= 0.8.2 <= 1' - - '@metaplex-foundation/mpl-toolbox@0.9.4': - resolution: {integrity: sha512-fd6JxfoLbj/MM8FG2x91KYVy1U6AjBQw4qjt7+Da3trzQaWnSaYHDcYRG/53xqfvZ9qofY1T2t53GXPlD87lnQ==} - peerDependencies: - '@metaplex-foundation/umi': '>= 0.8.2 < 1' - - '@metaplex-foundation/umi-bundle-defaults@0.9.2': - resolution: {integrity: sha512-kV3tfvgvRjVP1p9OFOtH+ibOtN9omVJSwKr0We4/9r45e5LTj+32su0V/rixZUkG1EZzzOYBsxhtIE0kIw/Hrw==} - peerDependencies: - '@metaplex-foundation/umi': ^0.9.2 - '@solana/web3.js': ^1.72.0 - - '@metaplex-foundation/umi-downloader-http@0.9.2': - resolution: {integrity: sha512-tzPT9hBwenzTzAQg07rmsrqZfgguAXELbcJrsYMoASp5VqWFXYIP00g94KET6XLjWUXH4P1J2zoa6hGennPXHA==} - peerDependencies: - '@metaplex-foundation/umi': ^0.9.2 - - '@metaplex-foundation/umi-eddsa-web3js@0.9.2': - resolution: {integrity: sha512-hhPCxXbYIp4BC4z9gK78sXpWLkNSrfv4ndhF5ruAkdIp7GcRVYKj0QnOUO6lGYGiIkNlw20yoTwOe1CT//OfTQ==} - peerDependencies: - '@metaplex-foundation/umi': ^0.9.2 - '@solana/web3.js': ^1.72.0 - - '@metaplex-foundation/umi-http-fetch@0.9.2': - resolution: {integrity: sha512-YCZuBu24T9ZzEDe4+w12LEZm/fO9pkyViZufGgASC5NX93814Lvf6Ssjn/hZzjfA7CvZbvLFbmujc6CV3Q/m9Q==} - peerDependencies: - '@metaplex-foundation/umi': ^0.9.2 - - '@metaplex-foundation/umi-options@0.8.9': - resolution: {integrity: sha512-jSQ61sZMPSAk/TXn8v8fPqtz3x8d0/blVZXLLbpVbo2/T5XobiI6/MfmlUosAjAUaQl6bHRF8aIIqZEFkJiy4A==} - - '@metaplex-foundation/umi-program-repository@0.9.2': - resolution: {integrity: sha512-g3+FPqXEmYsBa8eETtUE2gb2Oe3mqac0z3/Ur1TvAg5TtIy3mzRzOy/nza+sgzejnfcxcVg835rmpBaxpBnjDA==} - peerDependencies: - '@metaplex-foundation/umi': ^0.9.2 - - '@metaplex-foundation/umi-public-keys@0.8.9': - resolution: {integrity: sha512-CxMzN7dgVGOq9OcNCJe2casKUpJ3RmTVoOvDFyeoTQuK+vkZ1YSSahbqC1iGuHEtKTLSjtWjKvUU6O7zWFTw3Q==} - - '@metaplex-foundation/umi-rpc-chunk-get-accounts@0.9.2': - resolution: {integrity: sha512-YRwVf6xH0jPBAUgMhEPi+UbjioAeqTXmjsN2TnmQCPAmHbrHrMRj0rlWYwFLWAgkmoxazYrXP9lqOFRrfOGAEA==} - peerDependencies: - '@metaplex-foundation/umi': ^0.9.2 - - '@metaplex-foundation/umi-rpc-web3js@0.9.2': - resolution: {integrity: sha512-MqcsBz8B4wGl6jxsf2Jo/rAEpYReU9VCSR15QSjhvADHMmdFxCIZCCAgE+gDE2Vuanfl437VhOcP3g5Uw8C16Q==} - peerDependencies: - '@metaplex-foundation/umi': ^0.9.2 - '@solana/web3.js': ^1.72.0 - - '@metaplex-foundation/umi-serializer-data-view@0.9.2': - resolution: {integrity: sha512-5vGptadJxUxvUcyrwFZxXlEc6Q7AYySBesizCtrBFUY8w8PnF2vzmS45CP1MLySEATNH6T9mD4Rs0tLb87iQyA==} - peerDependencies: - '@metaplex-foundation/umi': ^0.9.2 - - '@metaplex-foundation/umi-serializers-core@0.8.9': - resolution: {integrity: sha512-WT82tkiYJ0Qmscp7uTj1Hz6aWQPETwaKLAENAUN5DeWghkuBKtuxyBKVvEOuoXerJSdhiAk0e8DWA4cxcTTQ/w==} - - '@metaplex-foundation/umi-serializers-encodings@0.8.9': - resolution: {integrity: sha512-N3VWLDTJ0bzzMKcJDL08U3FaqRmwlN79FyE4BHj6bbAaJ9LEHjDQ9RJijZyWqTm0jE7I750fU7Ow5EZL38Xi6Q==} - - '@metaplex-foundation/umi-serializers-numbers@0.8.9': - resolution: {integrity: sha512-NtBf1fnVNQJHFQjLFzRu2i9GGnigb9hOm/Gfrk628d0q0tRJB7BOM3bs5C61VAs7kJs4yd+pDNVAERJkknQ7Lg==} - - '@metaplex-foundation/umi-serializers@0.9.0': - resolution: {integrity: sha512-hAOW9Djl4w4ioKeR4erDZl5IG4iJdP0xA19ZomdaCbMhYAAmG/FEs5khh0uT2mq53/MnzWcXSUPoO8WBN4Q+Vg==} - - '@metaplex-foundation/umi-transaction-factory-web3js@0.9.2': - resolution: {integrity: sha512-fR1Kf21uylMFd1Smkltmj4jTNxhqSWf416owsJ+T+cvJi2VCOcOwq/3UFzOrpz78fA0RhsajKYKj0HYsRnQI1g==} - peerDependencies: - '@metaplex-foundation/umi': ^0.9.2 - '@solana/web3.js': ^1.72.0 - - '@metaplex-foundation/umi-web3js-adapters@0.9.2': - resolution: {integrity: sha512-RQqUTtHYY9fmEMnq7s3Hiv/81flGaoI0ZVVoafnFVaQLnxU6QBKxtboRZHk43XtD9CiFh5f9izrMJX7iK7KlOA==} - peerDependencies: - '@metaplex-foundation/umi': ^0.9.2 - '@solana/web3.js': ^1.72.0 - - '@metaplex-foundation/umi@0.9.2': - resolution: {integrity: sha512-9i4Acm4pruQfJcpRrc2EauPBwkfDN0I9QTvJyZocIlKgoZwD6A6wH0PViH1AjOVG5CQCd1YI3tJd5XjYE1ElBw==} - - '@noble/curves@1.4.2': - resolution: {integrity: sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==} - - '@noble/curves@1.9.0': - resolution: {integrity: sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg==} - engines: {node: ^14.21.3 || >=16} - - '@noble/hashes@1.4.0': - resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} - engines: {node: '>= 16'} - '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} @@ -1039,12 +872,6 @@ packages: peerDependencies: '@opentelemetry/api': ^1.8 - '@project-serum/borsh@0.2.5': - resolution: {integrity: sha512-UmeUkUoKdQ7rhx6Leve1SssMR/Ghv8qrEiyywyxSWg7ooV7StdpPBhciiy5eB3T0qU1BXvdRNC8TdrkxK7WC5Q==} - engines: {node: '>=10'} - peerDependencies: - '@solana/web3.js': ^1.2.0 - '@rollup/rollup-android-arm-eabi@4.41.1': resolution: {integrity: sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==} cpu: [arm] @@ -1120,11 +947,6 @@ packages: cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.40.2': - resolution: {integrity: sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==} - cpu: [x64] - os: [linux] - '@rollup/rollup-linux-x64-gnu@4.41.1': resolution: {integrity: sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==} cpu: [x64] @@ -1153,15 +975,6 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@scure/base@1.1.9': - resolution: {integrity: sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==} - - '@scure/bip32@1.4.0': - resolution: {integrity: sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==} - - '@scure/bip39@1.3.0': - resolution: {integrity: sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==} - '@sentry-internal/tracing@7.120.3': resolution: {integrity: sha512-Ausx+Jw1pAMbIBHStoQ6ZqDZR60PsCByvHdw/jdH9AqPrNE9xlBSf9EwcycvmrzwyKspSLaB52grlje2cRIUMg==} engines: {node: '>=8'} @@ -1226,157 +1039,433 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} - '@solana/buffer-layout-utils@0.2.0': - resolution: {integrity: sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==} - engines: {node: '>= 10'} + '@solana-program/compute-budget@0.11.0': + resolution: {integrity: sha512-7f1ePqB/eURkTwTOO9TNIdUXZcyrZoX3Uy2hNo7cXMfNhPFWp9AVgIyRNBc2jf15sdUa9gNpW+PfP2iV8AYAaw==} + peerDependencies: + '@solana/kit': ^5.0 + + '@solana-program/compute-budget@0.9.0': + resolution: {integrity: sha512-on7Cs1V48X9E2x1yVmfM6N6Xv0r4oGruXPcWnI50D3D3CIsHNWJ4gsvL4qZ4iey7zAP73FdM21K2CZBi1a/jzg==} + peerDependencies: + '@solana/kit': ^3.0 - '@solana/buffer-layout@4.0.1': - resolution: {integrity: sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==} - engines: {node: '>=5.10'} + '@solana-program/memo@0.10.0': + resolution: {integrity: sha512-1FvQFenL3lzl5SpxhWV4QJCOLU/nvAOXGXjKjS7dprvG+0u971xoanApN7bM/a4NFZolp6S+lP2xVl6vTVIxbg==} + peerDependencies: + '@solana/kit': ^5.0 - '@solana/codecs-core@2.0.0-rc.1': - resolution: {integrity: sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==} + '@solana-program/system@0.10.0': + resolution: {integrity: sha512-Go+LOEZmqmNlfr+Gjy5ZWAdY5HbYzk2RBewD9QinEU/bBSzpFfzqDRT55JjFRBGJUvMgf3C2vfXEGT4i8DSI4g==} peerDependencies: - typescript: '>=5' + '@solana/kit': ^5.0 - '@solana/codecs-core@2.1.0': - resolution: {integrity: sha512-SR7pKtmJBg2mhmkel2NeHA1pz06QeQXdMv8WJoIR9m8F/hw80K/612uaYbwTt2nkK0jg/Qn/rNSd7EcJ4SBGjw==} + '@solana-program/system@0.9.1': + resolution: {integrity: sha512-2N30CgYJw0qX8jKU8vW808yLmx5oRoDSM+FC6tqhsLQiph7agK9eRXJlnrq6OUfTAZd5yCYQHQvGtx0S8I9SAA==} + peerDependencies: + '@solana/kit': ^5.0 + + '@solana-program/token-2022@0.6.1': + resolution: {integrity: sha512-Ex02cruDMGfBMvZZCrggVR45vdQQSI/unHVpt/7HPt/IwFYB4eTlXtO8otYZyqV/ce5GqZ8S6uwyRf0zy6fdbA==} + peerDependencies: + '@solana/kit': ^5.0 + '@solana/sysvars': ^5.0 + + '@solana-program/token@0.9.0': + resolution: {integrity: sha512-vnZxndd4ED4Fc56sw93cWZ2djEeeOFxtaPS8SPf5+a+JZjKA/EnKqzbE1y04FuMhIVrLERQ8uR8H2h72eZzlsA==} + peerDependencies: + '@solana/kit': ^5.0 + + '@solana/accounts@5.4.0': + resolution: {integrity: sha512-qHtAtwCcCFTXcya6JOOG1nzYicivivN/JkcYNHr10qOp9b4MVRkfW1ZAAG1CNzjMe5+mwtEl60RwdsY9jXNb+Q==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: '>=5' + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@solana/codecs-data-structures@2.0.0-rc.1': - resolution: {integrity: sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog==} + '@solana/addresses@5.4.0': + resolution: {integrity: sha512-YRHiH30S8qDV4bZ+mtEk589PGfBuXHzD/fK2Z+YI5f/+s+yi/5le/fVw7PN6LxnnmVQKiRCDUiNF+WmFFKi6QQ==} + engines: {node: '>=20.18.0'} peerDependencies: - typescript: '>=5' + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@solana/codecs-numbers@2.0.0-rc.1': - resolution: {integrity: sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==} + '@solana/assertions@3.0.3': + resolution: {integrity: sha512-2qspxdbWp2y62dfCIlqeWQr4g+hE8FYSSwcaP6itwMwGRb8393yDGCJfI/znuzJh6m/XVWhMHIgFgsBwnevCmg==} + engines: {node: '>=20.18.0'} peerDependencies: - typescript: '>=5' + typescript: '>=5.3.3' - '@solana/codecs-numbers@2.1.0': - resolution: {integrity: sha512-XMu4yw5iCgQnMKsxSWPPOrGgtaohmupN3eyAtYv3K3C/MJEc5V90h74k5B1GUCiHvcrdUDO9RclNjD9lgbjFag==} + '@solana/assertions@5.4.0': + resolution: {integrity: sha512-8EP7mkdnrPc9y67FqWeAPzdWq2qAOkxsuo+ZBIXNWtIixDtXIdHrgjZ/wqbWxLgSTtXEfBCjpZU55Xw2Qfbwyg==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: '>=5' + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@solana/codecs-strings@2.0.0-rc.1': - resolution: {integrity: sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g==} + '@solana/codecs-core@5.4.0': + resolution: {integrity: sha512-rQ5jXgiDe2vIU+mYCHDjgwMd9WdzZfh4sc5H6JgYleAUjeTUX6mx8hTV2+pcXvvn27LPrgrt9jfxswbDb8O8ww==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/codecs-data-structures@5.4.0': + resolution: {integrity: sha512-LVssbdQ1GfY6upnxW3mufYsNfvTWKnHNk5Hx2gHuOYJhm3HZlp+Y8zvuoY65G1d1xAXkPz5YVGxaSeVIRWLGWg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/codecs-numbers@5.4.0': + resolution: {integrity: sha512-z6LMkY+kXWx1alrvIDSAxexY5QLhsso638CjM7XI1u6dB7drTLWKhifyjnm1vOQc1VPVFmbYxTgKKpds8TY8tg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/codecs-strings@5.4.0': + resolution: {integrity: sha512-w0trrjfQDhkCVz7O1GTmHBk9m+MkljKx2uNBbQAD3/yW2Qn9dYiTrZ1/jDVq0/+lPPAUkbT3s3Yo7HUZ2QFmHw==} + engines: {node: '>=20.18.0'} peerDependencies: fastestsmallesttextencoderdecoder: ^1.0.22 - typescript: '>=5' + typescript: ^5.0.0 + peerDependenciesMeta: + fastestsmallesttextencoderdecoder: + optional: true + typescript: + optional: true - '@solana/codecs@2.0.0-rc.1': - resolution: {integrity: sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ==} + '@solana/codecs@5.4.0': + resolution: {integrity: sha512-IbDCUvNX0MrkQahxiXj9rHzkd/fYfp1F2nTJkHGH8v+vPfD+YPjl007ZBM38EnCeXj/Xn+hxqBBivPvIHP29dA==} + engines: {node: '>=20.18.0'} peerDependencies: - typescript: '>=5' + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@solana/errors@2.0.0-rc.1': - resolution: {integrity: sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==} + '@solana/errors@3.0.3': + resolution: {integrity: sha512-1l84xJlHNva6io62PcYfUamwWlc0eM95nHgCrKX0g0cLoC6D6QHYPCEbEVkR+C5UtP9JDgyQM8MFiv+Ei5tO9Q==} + engines: {node: '>=20.18.0'} hasBin: true peerDependencies: - typescript: '>=5' + typescript: '>=5.3.3' - '@solana/errors@2.1.0': - resolution: {integrity: sha512-l+GxAv0Ar4d3c3PlZdA9G++wFYZREEbbRyAFP8+n8HSg0vudCuzogh/13io6hYuUhG/9Ve8ARZNamhV7UScKNw==} + '@solana/errors@5.4.0': + resolution: {integrity: sha512-hNoAOmlZAszaVBrAy1Jf7amHJ8wnUnTU0BqhNQXknbSvirvsYr81yEud2iq18YiCqhyJ9SuQ5kWrSAT0x7S0oA==} engines: {node: '>=20.18.0'} hasBin: true peerDependencies: - typescript: '>=5' + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@solana/options@2.0.0-rc.1': - resolution: {integrity: sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA==} + '@solana/fast-stable-stringify@5.4.0': + resolution: {integrity: sha512-KB7PUL7yalPvbWCezzyUDVRDp39eHLPH7OJ6S8VFT8YNIFUANwwj5ctui50Fim76kvSYDdYJOclXV45O2gfQ8Q==} + engines: {node: '>=20.18.0'} peerDependencies: - typescript: '>=5' + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@solana/spl-account-compression@0.2.0': - resolution: {integrity: sha512-nHpa+hTUpjLdV9x4LXlp7k0WIkr8kUGjY/SPh+vuTUy4SEIIDjrGJ6/B0hUdd8+mFfrq2x4j/tgJvPsm4K5AJw==} - engines: {node: '>=16'} + '@solana/functional@5.4.0': + resolution: {integrity: sha512-32ghHO0bg6GgX/7++0/7Lps6RgeXD2gKF1okiuyEGuVfKENIapgaQdcGhUwb3q6D6fv6MRAVn/Yve4jopGVNMQ==} + engines: {node: '>=20.18.0'} peerDependencies: - '@solana/web3.js': ^1.50.1 + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@solana/spl-token-group@0.0.7': - resolution: {integrity: sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug==} - engines: {node: '>=16'} + '@solana/instruction-plans@5.4.0': + resolution: {integrity: sha512-5xbJ+I/pP2aWECmK75bEM1zCnIITlohAK83dVN+t5X2vBFrr6M9gifo8r4Opdnibsgo6QVVkKPxRo5zow5j0ig==} + engines: {node: '>=20.18.0'} peerDependencies: - '@solana/web3.js': ^1.95.3 + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@solana/spl-token-metadata@0.1.6': - resolution: {integrity: sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA==} - engines: {node: '>=16'} + '@solana/instructions@5.4.0': + resolution: {integrity: sha512-//a7jpHbNoAgTqy3YyqG1X6QhItJLKzJa6zuYJGCwaAAJye7BxS9pxJBgb2mUt7CGidhUksf+U8pmLlxCNWYyg==} + engines: {node: '>=20.18.0'} peerDependencies: - '@solana/web3.js': ^1.95.3 + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@solana/spl-token@0.3.11': - resolution: {integrity: sha512-bvohO3rIMSVL24Pb+I4EYTJ6cL82eFpInEXD/I8K8upOGjpqHsKUoAempR/RnUlI1qSFNyFlWJfu6MNUgfbCQQ==} - engines: {node: '>=16'} + '@solana/keys@5.4.0': + resolution: {integrity: sha512-zQVbAwdoXorgXjlhlVTZaymFG6N8n1zn2NT+xI6S8HtbrKIB/42xPdXFh+zIihGzRw+9k8jzU7Axki/IPm6qWQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/kit@5.4.0': + resolution: {integrity: sha512-aVjN26jOEzJA6UBYxSTQciZPXgTxWnO/WysHrw+yeBL/5AaTZnXEgb4j5xV6cUFzOlVxhJBrx51xtoxSqJ0u3g==} + engines: {node: '>=20.18.0'} peerDependencies: - '@solana/web3.js': ^1.88.0 + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@solana/spl-token@0.4.13': - resolution: {integrity: sha512-cite/pYWQZZVvLbg5lsodSovbetK/eA24gaR0eeUeMuBAMNrT8XFCwaygKy0N2WSg3gSyjjNpIeAGBAKZaY/1w==} - engines: {node: '>=16'} + '@solana/nominal-types@5.4.0': + resolution: {integrity: sha512-h4dTRQwTerzksE5B1WmObN6TvLo8dYUd7kpUUynGd8WJjK0zz3zkDhq0MkA3aF6A1C2C82BSGqSsN9EN0E6Exg==} + engines: {node: '>=20.18.0'} peerDependencies: - '@solana/web3.js': ^1.95.5 + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@solana/web3.js@1.98.2': - resolution: {integrity: sha512-BqVwEG+TaG2yCkBMbD3C4hdpustR4FpuUFRPUmqRZYYlPI9Hg4XMWxHWOWRzHE9Lkc9NDjzXFX7lDXSgzC7R1A==} + '@solana/offchain-messages@5.4.0': + resolution: {integrity: sha512-DjdlYJCcKfgh4dkdk+owH1bP+Q4BRqCs55mgWWp9PTwm/HHy/a5vcMtCi1GyIQXfhtNNvKBLbXrUE0Fxej8qlg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@sqltools/formatter@1.2.5': - resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} + '@solana/options@5.4.0': + resolution: {integrity: sha512-h4vTWRChEXPhaHo9i1pCyQBWWs+NqYPQRXSAApqpUYvHb9Kct/C6KbHjfyaRMyqNQnDHLcJCX7oW9tk0iRDzIg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/plugin-core@5.4.0': + resolution: {integrity: sha512-e1aLGLldW7C5113qTOjFYSGq95a4QC9TWb77iq+8l6h085DcNj+195r4E2zKaINrevQjQTwvxo00oUyHP7hSJA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/programs@5.4.0': + resolution: {integrity: sha512-Sc90WK9ZZ7MghOflIvkrIm08JwsFC99yqSJy28/K+hDP2tcx+1x+H6OFP9cumW9eUA1+JVRDeKAhA8ak7e/kUA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/promises@3.0.3': + resolution: {integrity: sha512-K+UflGBVxj30XQMHTylHHZJdKH5QG3oj5k2s42GrZ/Wbu72oapVJySMBgpK45+p90t8/LEqV6rRPyTXlet9J+Q==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/promises@5.4.0': + resolution: {integrity: sha512-23mfgNBbuP6Q+4vsixGy+GkyZ7wBLrxTBNXqrG/XWrJhjuuSkjEUGaK4Fx5o7LIrBi6KGqPknKxmTlvqnJhy2Q==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/rpc-api@5.4.0': + resolution: {integrity: sha512-FJL6KaAsQ4DhfhLKKMcqbTpToNFwHlABCemIpOunE3OSqJFDrmc/NbsEaLIoeHyIg3d1Imo49GIUOn2TEouFUA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@staratlas/anchor@0.25.1': - resolution: {integrity: sha512-IDRKejAq+B+pZE+qnIj0qYbF8TBOyKofeY/zo6/Lk+PIAetareEZrF+avlDvkTeKIQS1t9fd0DOPcvbsV5ZS9A==} - engines: {node: '>=11'} + '@solana/rpc-parsed-types@5.4.0': + resolution: {integrity: sha512-IRQuSzx+Sj1A3XGiIzguNZlMjMMybXTTjV/RnTwBgnJQPd/H4us4pfPD94r+/yolWDVfGjJRm04hnKVMjJU8Rg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@staratlas/atlas-prime@0.13.1': - resolution: {integrity: sha512-fFMjy0dFUgB+b9m+SZIIMSrrvB+8SVq/wJTFNVzDenr0YrXehbgtkfZWaxpffEAXdp433hbkP9yAArlf+weMPg==} + '@solana/rpc-spec-types@5.4.0': + resolution: {integrity: sha512-JU9hC5/iyJx30ym17gpoXDtT9rCbO6hLpB6UDhSFFoNeirxtTVb4OdnKtsjJDfXAiXsynJRsZRwfj3vGxRLgQw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@staratlas/cargo@1.1.0': - resolution: {integrity: sha512-7td0gxru4dkygO+xv+RQs9bkizKj6+blAMRSPP5ozOZ9QQ35HwCBIDJXB6Kk8sfVcq2JOHiWyQtyjcAD6GHArQ==} + '@solana/rpc-spec@5.4.0': + resolution: {integrity: sha512-XMhxBb1GuZ3Kaeu5WNHB5KteCQ/aVuMByZmUKPqaanD+gs5MQZr0g62CvN7iwRlFU7GC18Q73ROWR3/JjzbXTA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@staratlas/claim-stake@0.12.0': - resolution: {integrity: sha512-HbE2mJOMkMj+gNIQINkLj35elX0Oj772iegu+JcUIpsJRl/hRJh8F6NQt9iwnSYyrYQwBG+U4dpTpLgKAeQeHA==} + '@solana/rpc-subscriptions-api@5.4.0': + resolution: {integrity: sha512-euAFIG6ruEsqK+MsrL1tGSMbbOumm8UAyGzlD/kmXsAqqhcVsSeZdv5+BMIHIBsQ93GHcloA8UYw1BTPhpgl9w==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@staratlas/crafting@1.1.0': - resolution: {integrity: sha512-lxvdGBZatVBL63o1EHcUAxk1k1jHwEv6dZsulpgXpsOfnBP7Btzt8Ky1knBD7HvM/gBQplxMKopolRGFCQekXQ==} + '@solana/rpc-subscriptions-channel-websocket@5.4.0': + resolution: {integrity: sha512-kWCmlW65MccxqXwKsIz+LkXUYQizgvBrrgYOkyclJHPa+zx4gqJjam87+wzvO9cfbDZRer3wtJBaRm61gTHNbw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@staratlas/crew@0.7.0': - resolution: {integrity: sha512-tr8RDDs1waOSVwVwR9WlD6Z6iDvRHprqQu4XQmSSy6oy5Sv2FNofFWENsC0dmysUl/n0OLzKfT8/Q4D3hKRlTA==} + '@solana/rpc-subscriptions-spec@5.4.0': + resolution: {integrity: sha512-ELaV9Z39GtKyUO0++he00ymWleb07QXYJhSfA0e1N5Q9hXu/Y366kgXHDcbZ/oUJkT3ylNgTupkrsdtiy8Ryow==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@staratlas/data-source@0.7.7': - resolution: {integrity: sha512-DQfR/a9MXr1Gds/YTR9RylQZlx5gbCe4NjynQ/45je3rnnblNfhma0WoHrSHz/r/lWwJwN7yRwSJkfaDW8QhuQ==} + '@solana/rpc-subscriptions@5.4.0': + resolution: {integrity: sha512-051t1CEjjAzM9ohjj2zb3ED70yeS3ZY8J5wSytL6tthTGImw/JB2a0D9DWMOKriFKt496n95IC+IdpJ35CpBWA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@staratlas/data-source@0.8.3': - resolution: {integrity: sha512-iM7EYTKUKLYUD90Xai2FufPdL/4raOizdfbJy1RWfZlzR77Jp0BRTi1Z92iSF1WMEXqRqWR2ZqU6hK0/8N3jyg==} + '@solana/rpc-transformers@5.4.0': + resolution: {integrity: sha512-dZ8keYloLW+eRAwAPb471uWCFs58yHloLoI+QH0FulYpsSJ7F2BNWYcdnjSS/WiggsNcU6DhpWzYAzlEY66lGQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@staratlas/data-source@0.9.0': - resolution: {integrity: sha512-WLS6/Abq+0VJFZFQIE0pi2BsSDwU6OQ3c0BsQySIN/SZ38VGeBQLC0gbo0gktYomKDtawYB4qIB2JmrmgvkmQQ==} + '@solana/rpc-transport-http@5.4.0': + resolution: {integrity: sha512-vidA+Qtqrnqp3QSVumWHdWJ/986yCr5+qX3fbc9KPm9Ofoto88OMWB/oLJvi2Tfges1UBu/jl+lJdsVckCM1bA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@staratlas/factory@0.7.1': - resolution: {integrity: sha512-agUbdiD6DsmMHOFA33mZp6rAvcjpFGeYWXHimi04++cVZK7N9cQ0S3YJsQOA0zCfgMbIoB6K5a6bcIl51gVVOA==} + '@solana/rpc-types@5.4.0': + resolution: {integrity: sha512-+C4N4/5AYzBdt3Y2yzkScknScy/jTx6wfvuJIY9XjOXtdDyZ8TmrnMwdPMTZPGLdLuHplJwlwy1acu/4hqmrBQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@staratlas/player-profile@0.11.0': - resolution: {integrity: sha512-SQYysx0s0lD6J9Ln4vJQDa9TS/1rSy/FeKCexwzNiKm+S0fUFv6LKOKIPsGDQDQ2MIVfVqilVtIB2Nf3DuaXgg==} + '@solana/rpc@5.4.0': + resolution: {integrity: sha512-S6GRG+usnubDs0JSpgc0ZWEh9IPL5KPWMuBoD8ggGVOIVWntp53FpvhYslNzbxWBXlTvJecr2todBipGVM/AqQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@staratlas/player-profile@0.9.1': - resolution: {integrity: sha512-bqPXn6fGl+gg5zZBEK4+/Cv9oriVzrNOTW1Z+6TMFSB/vMsE51DsdyN7BWMkvwfBg2XdHfL9VntlU8j8OYD8jQ==} + '@solana/signers@5.4.0': + resolution: {integrity: sha512-s+fZxpi6UPr6XNk2pH/R84WjNRoSktrgG8AGNfsj/V8MJ++eKX7hhIf4JsHZtnnQXXrHmS3ozB2oHlc8yEJvCQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@staratlas/points-store@1.1.0': - resolution: {integrity: sha512-Rrbsnku5flephKSNkb6OYkUxGWmQAsHH76ZQKISBViXSPcQkpZ3lIYaqYaztEDx6rXSY1J5ftkJEAnxMVtkJ4A==} + '@solana/subscribable@5.4.0': + resolution: {integrity: sha512-72LmfNX7UENgA24sn/xjlWpPAOsrxkWb9DQhuPZxly/gq8rl/rvr7Xu9qBkvFF2po9XpdUrKlccqY4awvfpltA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@staratlas/points@1.1.0': - resolution: {integrity: sha512-9XyG5i3xz4Om6Blq/HxYqnGGAnklyCLlokIXpLAxYWEU53JT7ldhLiXCgKuA8n/aWi0Y3YCOnmPuuQ5JwD+cZw==} + '@solana/sysvars@5.4.0': + resolution: {integrity: sha512-A5NES7sOlFmpnsiEts5vgyL3NXrt/tGGVSEjlEGvsgwl5EDZNv+xWnNA400uMDqd9O3a5PmH7p/6NsgR+kUzSg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@staratlas/profile-faction@0.6.0': - resolution: {integrity: sha512-OLENOQ12cG12g8xkmZsf6oFvtCkTlsI/ljGGwpOzic0iCD282bydVStJ//N0nRxBfDBGYCyRI9TBkR3uF69D2g==} + '@solana/transaction-confirmation@5.4.0': + resolution: {integrity: sha512-EdSDgxs84/4gkjQw2r7N+Kgus8x9U+NFo0ufVG+48V8Hzy2t0rlBuXgIxwx0zZwUuTIgaKhpIutJgVncwZ5koA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@staratlas/profile-vault@0.9.1': - resolution: {integrity: sha512-nY6t1VTI/kRd/bQLSEeRTqwJ6QWRAC1du6vFN6zufJQyumuuZpmevKNZF1DkrSjZqhe3/2HgyR/ud7d62KGFxg==} + '@solana/transaction-messages@5.4.0': + resolution: {integrity: sha512-qd/3kZDaPiHM0amhn3vXnupfcsFTVz6CYuHXvq9HFv/fq32+5Kp1FMLnmHwoSxQxdTMDghPdOhC4vhNhuWmuVQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@staratlas/sage@1.8.10': - resolution: {integrity: sha512-e7UvoTXyF94HO3d4hjS04WlrUMChGow4/eW9Snw707snBHkmnNsiBZaLHQs+xU1knbISF2LZ3EtsvEfuy+DXCQ==} + '@solana/transactions@5.4.0': + resolution: {integrity: sha512-OuY4M4x/xna8KZQIrz8tSrI9EEul9Od97XejqFmGGkEjbRsUOfJW8705TveTW8jU3bd5RGecFYscPgS2F+m7jQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@sqltools/formatter@1.2.5': + resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} + + '@staratlas/dev-player-profile@0.40.0': + resolution: {integrity: sha512-Gu4ohvGh3VQrYT3qJCuVxk3aHSRCNzW93m8LB624p0RxFn5D/Nw3HEUjZZlsTTe02xjKvwb2G4NrABENfdECtg==} + peerDependencies: + '@solana/kit': ^5.0.0 + + '@staratlas/dev-profile-faction@0.40.0': + resolution: {integrity: sha512-runFmksgl9Zqqdy/A//G4BdQehC2n5yavm3z2pt0KNxJhhMfmQxlBgqBkBj6SyF+xtSyMz7B6t+goV0eUkpY1g==} + peerDependencies: + '@solana/kit': ^5.0.0 + + '@staratlas/dev-sage@0.40.0': + resolution: {integrity: sha512-8BadSxmj2e01b3rjTDCm8UPsA2lPwe24zbHW9owJxe+/8QvfVusE5MoABuhz+gZJlfdUMF61W//lJ0gDvOqu5A==} + peerDependencies: + '@solana/kit': ^5.0.0 '@swc/core-darwin-arm64@1.11.29': resolution: {integrity: sha512-whsCX7URzbuS5aET58c75Dloby3Gtj/ITk2vc4WW6pSDQKSPDuONsIcZ7B2ng8oz0K6ttbi4p3H/PNPQLJ4maQ==} @@ -1486,9 +1575,6 @@ packages: '@types/big.js@6.2.2': resolution: {integrity: sha512-e2cOW9YlVzFY2iScnGBBkplKsrn2CsObHQ2Hiw4V1sSyiGbgWL8IyqE3zFi1Pt5o1pdAtYkDAIsF3KKUPjdzaA==} - '@types/bn.js@5.1.6': - resolution: {integrity: sha512-Xh8vSwUeMKeYYrj3cX4lGQgFSF/N03r+tv4AiLl1SucqV+uTQpxRcnM8AkXKHwYP9ZPXOYXRr2KPXpVlIvqh9w==} - '@types/chance@1.1.6': resolution: {integrity: sha512-V+pm3stv1Mvz8fSKJJod6CglNGVqEQ6OyuqitoDkWywEODM/eJd1eSuIp9xt6DrX8BWZ2eDSIzbw1tPCUTvGbQ==} @@ -1519,9 +1605,6 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/lodash@4.17.16': - resolution: {integrity: sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==} - '@types/luxon@3.6.2': resolution: {integrity: sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==} @@ -1534,12 +1617,6 @@ packages: '@types/mysql@2.15.26': resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} - '@types/node@12.20.55': - resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - - '@types/node@20.17.46': - resolution: {integrity: sha512-0PQHLhZPWOxGW4auogW0eOQAuNIlCYvibIpG67ja0TOJ6/sehu+1en7sfceUn+QQtx4Rk3GxbLNwPh0Cav7TWw==} - '@types/node@22.15.29': resolution: {integrity: sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==} @@ -1567,15 +1644,6 @@ packages: '@types/triple-beam@1.3.5': resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} - '@types/uuid@8.3.4': - resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==} - - '@types/ws@7.4.7': - resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==} - - '@types/ws@8.18.1': - resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -1662,10 +1730,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - agentkeepalive@4.6.0: - resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} - engines: {node: '>= 8.0.0'} - ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -1693,9 +1757,6 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} - ansicolors@0.3.2: - resolution: {integrity: sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==} - ansis@3.17.0: resolution: {integrity: sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==} engines: {node: '>=14'} @@ -1747,9 +1808,6 @@ packages: asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - assert@2.1.0: - resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} - async-function@1.0.0: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} @@ -1764,9 +1822,6 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axios@1.9.0: - resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==} - babel-jest@29.7.0: resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1795,49 +1850,21 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - base-x@3.0.11: - resolution: {integrity: sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==} - - base-x@4.0.1: - resolution: {integrity: sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==} - base-x@5.0.1: resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - big.js@6.2.2: - resolution: {integrity: sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==} - big.js@7.0.1: resolution: {integrity: sha512-iFgV784tD8kq4ccF1xtNMZnXeZzVuXWWM+ERFzKQjv+A5G9HC8CY3DuV45vgzFFcW+u2tIvmF95+AzWgs6BjCg==} - bigint-buffer@1.1.5: - resolution: {integrity: sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==} - engines: {node: '>= 10.0.0'} - - bignumber.js@9.3.0: - resolution: {integrity: sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==} - - bindings@1.5.0: - resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - bip39@3.1.0: resolution: {integrity: sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==} - bn.js@4.11.6: - resolution: {integrity: sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==} - - bn.js@5.2.2: - resolution: {integrity: sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==} - boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - borsh@0.7.0: - resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==} - brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -1857,12 +1884,6 @@ packages: resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} engines: {node: '>= 6'} - bs58@4.0.1: - resolution: {integrity: sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==} - - bs58@5.0.0: - resolution: {integrity: sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==} - bs58@6.0.0: resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} @@ -1881,13 +1902,6 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - buffer-layout@1.2.2: - resolution: {integrity: sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA==} - engines: {node: '>=4.5'} - - buffer-reverse@1.0.1: - resolution: {integrity: sha512-M87YIUBsZ6N924W57vDwT/aOu8hw7ZgdByz6ijksLjmHJELBASmYTTlNHRgjE+pTsT9oJXGaDSgqqwfdHotDUg==} - buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -1929,14 +1943,6 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - camelcase@7.0.1: - resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} - engines: {node: '>=14.16'} - - camelcase@8.0.0: - resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} - engines: {node: '>=16'} - caniuse-lite@1.0.30001720: resolution: {integrity: sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g==} @@ -1944,8 +1950,8 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chalk@5.4.1: - resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} chance@1.1.13: @@ -2007,16 +2013,13 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} - commander@12.1.0: - resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} - engines: {node: '>=18'} - - commander@13.1.0: - resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} - engines: {node: '>=18'} + commander@14.0.0: + resolution: {integrity: sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==} + engines: {node: '>=20'} - commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + commander@14.0.2: + resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} + engines: {node: '>=20'} commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} @@ -2059,20 +2062,10 @@ packages: resolution: {integrity: sha512-7x7DoEOxV11t3OPWWMjj1xrL1PGkTV5RV+/54IJTZD7gStiaMploY43EkeBSkDZTLRbUwk+OISbQ0TR133oXyA==} engines: {node: '>=18.x'} - cross-fetch@3.2.0: - resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - crypto-hash@1.3.0: - resolution: {integrity: sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg==} - engines: {node: '>=8'} - - crypto-js@4.2.0: - resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} - css-select@4.3.0: resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} @@ -2144,10 +2137,6 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} - delay@5.0.0: - resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==} - engines: {node: '>=10'} - delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -2187,9 +2176,6 @@ packages: domutils@2.8.0: resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} - dot-case@3.0.4: - resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} - dotenv@16.5.0: resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} engines: {node: '>=12'} @@ -2259,12 +2245,6 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - es6-promise@4.2.8: - resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} - - es6-promisify@5.0.0: - resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==} - esbuild@0.25.5: resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} engines: {node: '>=18'} @@ -2394,26 +2374,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - ethereum-bloom-filters@1.2.0: - resolution: {integrity: sha512-28hyiE7HVsWubqhpVLVmZXFd4ITeHi+BUu05o9isf0GUpMtzBUi+8/gFrGaGYzvGAJQmJ3JKj77Mk9G98T84rA==} - - ethereum-cryptography@2.2.1: - resolution: {integrity: sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==} - - ethjs-unit@0.1.6: - resolution: {integrity: sha512-/Sn9Y0oKl0uqQuvgFk/zQgR7aw1g36qX/jzSQ5lSwlO0GigPymk4eGQfeNTD03w1dPOqfz8V77Cy43jH56pagw==} - engines: {node: '>=6.5.0', npm: '>=3'} - event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} - eventemitter3@4.0.7: - resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - - eventemitter3@5.0.1: - resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} - execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -2426,10 +2390,6 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - eyes@0.1.8: - resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==} - engines: {node: '> 0.1.90'} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2449,9 +2409,6 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fast-stable-stringify@1.0.0: - resolution: {integrity: sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==} - fastestsmallesttextencoderdecoder@1.0.22: resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==} @@ -2476,9 +2433,6 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} - file-uri-to-path@1.0.0: - resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} @@ -2507,15 +2461,6 @@ packages: fn.name@1.1.0: resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} - follow-redirects@1.15.9: - resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -2598,7 +2543,7 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} @@ -2667,9 +2612,6 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} - humanize-ms@1.2.1: - resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} - ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -2711,10 +2653,6 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} - is-arguments@1.2.0: - resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} - engines: {node: '>= 0.4'} - is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -2777,18 +2715,10 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - is-hex-prefixed@1.0.0: - resolution: {integrity: sha512-WvtOiug1VFrE9v1Cydwm+FnXd3+w9GaeVUss5W4v/SLy3UW00vP+6iNF2SdnfiBoLy4bTqVdkftNGTUeOFVsbA==} - engines: {node: '>=6.5.0', npm: '>=3'} - is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} - is-nan@1.3.2: - resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} - engines: {node: '>= 0.4'} - is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -2843,11 +2773,6 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isomorphic-ws@4.0.1: - resolution: {integrity: sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==} - peerDependencies: - ws: '*' - istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -2880,11 +2805,6 @@ packages: engines: {node: '>=10'} hasBin: true - jayson@4.2.0: - resolution: {integrity: sha512-VfJ9t1YLwacIubLhONk0KFeosUBwstRWQ0IRT1KDjEjnVnSOVHC3uwugyV7L0c7R9lpVyrUGT2XWiBA1UTtpyg==} - engines: {node: '>=8'} - hasBin: true - jest-changed-files@29.7.0: resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -3018,12 +2938,6 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} - js-sha256@0.9.0: - resolution: {integrity: sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==} - - js-sha3@0.8.0: - resolution: {integrity: sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==} - js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3052,9 +2966,6 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - json-stringify-safe@5.0.1: - resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -3135,9 +3046,6 @@ packages: resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} engines: {node: '>= 12.0.0'} - lower-case@2.0.2: - resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} - lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -3175,17 +3083,10 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - merkletreejs@0.3.11: - resolution: {integrity: sha512-LJKTl4iVNTndhL+3Uz/tfkjD0klIWsHlUzgtuNnNrsf7bAlXR30m+xYB7lHr5Z/l6e/yAIsr26Dabx6Buo4VGQ==} - engines: {node: '>= 7.6.0'} - methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} - micro-ftch@0.3.1: - resolution: {integrity: sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==} - micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -3228,14 +3129,6 @@ packages: mlly@1.7.4: resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} - mobx-utils@6.1.0: - resolution: {integrity: sha512-P3qUVDFp3Kv5HXD7EIGJn3zlgJJnN+/ZpFHWQ+u6YNN1xDxY53iMvsQ9fM8kauTVdDmt7ulDgDQtDrOxb1NS9Q==} - peerDependencies: - mobx: ^6.0.0 - - mobx@6.13.7: - resolution: {integrity: sha512-aChaVU/DO5aRPmk1GX8L+whocagUUpBQqoPtJk+cm7UOXUk87J4PeWCh6nNmTTIfEhiR9DI/+FnA8dln/hTK7g==} - module-alias@2.2.3: resolution: {integrity: sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==} @@ -3255,16 +3148,6 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - neverthrow@6.2.2: - resolution: {integrity: sha512-POR1FACqdK9jH0S2kRPzaZEvzT11wsOxLW520PQV/+vKi9dQe+hXq19EiOvYx7lSRaF5VB9lYGsPInynrnN05w==} - - neverthrow@8.2.0: - resolution: {integrity: sha512-kOCT/1MCPAxY5iUV3wytNFUMUolzuwd/VF/1KCx7kf6CutrOsTie+84zTGTpgQycjvfLdBBdvBvFLqFD2c0wkQ==} - engines: {node: '>=18'} - - no-case@3.0.4: - resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} - node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -3295,10 +3178,6 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - number-to-bn@1.7.0: - resolution: {integrity: sha512-wsJ9gfSz1/s4ZsJN01lyonwuxA1tml6X1yBDnfpMglypcBRFZZkus26EdPSlqS5GJfYddVZa22p3VNb3z5m5Ig==} - engines: {node: '>=6.5.0', npm: '>=3'} - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3307,10 +3186,6 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} - object-is@1.1.6: - resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} - engines: {node: '>= 0.4'} - object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} @@ -3376,9 +3251,6 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - pako@2.1.0: - resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} - parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -3535,9 +3407,6 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3552,9 +3421,6 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -3624,9 +3490,6 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rpc-websockets@9.1.1: - resolution: {integrity: sha512-1IXGM/TfPT6nfYMIXkJdzn+L4JEsmb0FL1O2OBjaH03V3yuUDdKFulGLMFG6ErV+8pZ5HVC0limve01RyO+saA==} - run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -3725,8 +3588,8 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - snake-case@3.0.4: - resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + solana-kite@2.1.0: + resolution: {integrity: sha512-I2wts94RErTUcRiYzK+ygNzniWH4lIeXZVqieXMUYgTS3tI1pGZ4FHh8yPGp+viNkVnesEEtuMaciO+GLRaihA==} source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} @@ -3757,12 +3620,6 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} - stream-chain@2.2.5: - resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==} - - stream-json@1.9.1: - resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==} - string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} @@ -3810,10 +3667,6 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} - strip-hex-prefix@1.0.0: - resolution: {integrity: sha512-q8d4ue7JGEiVcypji1bALTos+0pWtyGlivAWyPuTkHzuTCJqrK9sWxYQZUq6Nq3cuyv3bm734IhHvHtGGURU6A==} - engines: {node: '>=6.5.0', npm: '>=3'} - strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -3827,13 +3680,6 @@ packages: resolution: {integrity: sha512-O+PCv11lgTNJUzy49teNAWLjBZfc+A1enOwTpLlH6/rsvKcTwcdTT8m9azGkVqM7HBl5jpyZ7KTPhHweokBcdg==} engines: {node: '>=14.18.0'} - superstruct@0.15.5: - resolution: {integrity: sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==} - - superstruct@2.0.2: - resolution: {integrity: sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==} - engines: {node: '>=14.0.0'} - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -3859,9 +3705,6 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} - text-encoding-utf-8@1.0.2: - resolution: {integrity: sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==} - text-hex@1.0.0: resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} @@ -3886,9 +3729,6 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - toml@3.0.0: - resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} - tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -3899,10 +3739,6 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - treeify@1.1.0: - resolution: {integrity: sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==} - engines: {node: '>=0.6'} - triple-beam@1.4.1: resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} engines: {node: '>= 14.0.0'} @@ -4078,9 +3914,6 @@ packages: typeorm-aurora-data-api-driver: optional: true - typescript-collections@1.3.3: - resolution: {integrity: sha512-7sI4e/bZijOzyURng88oOFZCISQPTHozfE2sUu5AviFYk5QV7fYGb6YiDl+vKjF/pICA354JImBImL9XJWUvdQ==} - typescript@5.8.3: resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} engines: {node: '>=14.17'} @@ -4093,12 +3926,12 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} - undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici@7.10.0: resolution: {integrity: sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==} engines: {node: '>=20.18.1'} @@ -4116,15 +3949,9 @@ packages: resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} engines: {node: '>=6.14.2'} - utf8@3.0.0: - resolution: {integrity: sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==} - util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - util@0.12.5: - resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} - utila@0.4.0: resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} @@ -4132,10 +3959,6 @@ packages: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true - uuid@8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - hasBin: true - v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -4146,10 +3969,6 @@ packages: walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} - web3-utils@1.10.4: - resolution: {integrity: sha512-tsu8FiKJLk2PzhDl9fXbGUWTkkVXYhtTA+SmEFkKft+9BgwLxfCRpU96sWv7ICC8zixBNd3JURVoiR3dUXgP8A==} - engines: {node: '>=8.0.0'} - webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -4210,20 +4029,8 @@ packages: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - ws@7.5.10: - resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} - engines: {node: '>=8.3.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - ws@8.18.2: - resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==} + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -4430,8 +4237,6 @@ snapshots: '@babel/core': 7.27.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/runtime@7.27.1': {} - '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -4459,34 +4264,6 @@ snapshots: '@colors/colors@1.6.0': {} - '@coral-xyz/anchor@0.29.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)': - dependencies: - '@coral-xyz/borsh': 0.29.0(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@noble/hashes': 1.8.0 - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - bn.js: 5.2.2 - bs58: 4.0.1 - buffer-layout: 1.2.2 - camelcase: 6.3.0 - cross-fetch: 3.2.0 - crypto-hash: 1.3.0 - eventemitter3: 4.0.7 - pako: 2.1.0 - snake-case: 3.0.4 - superstruct: 0.15.5 - toml: 3.0.0 - transitivePeerDependencies: - - bufferutil - - encoding - - typescript - - utf-8-validate - - '@coral-xyz/borsh@0.29.0(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))': - dependencies: - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - bn.js: 5.2.2 - buffer-layout: 1.2.2 - '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -4617,14 +4394,6 @@ snapshots: '@eslint/core': 0.14.0 levn: 0.4.1 - '@ethereumjs/rlp@4.0.1': {} - - '@ethereumjs/util@8.1.0': - dependencies: - '@ethereumjs/rlp': 4.0.1 - ethereum-cryptography: 2.2.1 - micro-ftch: 0.3.1 - '@faker-js/faker@9.8.0': {} '@humanfs/core@0.19.1': {} @@ -4844,162 +4613,6 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.0 optional: true - '@metaplex-foundation/beet-solana@0.4.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)': - dependencies: - '@metaplex-foundation/beet': 0.7.2 - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - bs58: 5.0.0 - debug: 4.4.1 - transitivePeerDependencies: - - bufferutil - - encoding - - supports-color - - typescript - - utf-8-validate - - '@metaplex-foundation/beet@0.7.2': - dependencies: - ansicolors: 0.3.2 - assert: 2.1.0 - bn.js: 5.2.2 - debug: 4.4.1 - transitivePeerDependencies: - - supports-color - - '@metaplex-foundation/digital-asset-standard-api@1.0.6(@metaplex-foundation/umi@0.9.2)': - dependencies: - '@metaplex-foundation/umi': 0.9.2 - - '@metaplex-foundation/mpl-bubblegum@4.4.0(@metaplex-foundation/umi@0.9.2)': - dependencies: - '@metaplex-foundation/digital-asset-standard-api': 1.0.6(@metaplex-foundation/umi@0.9.2) - '@metaplex-foundation/mpl-token-metadata': 3.2.1(@metaplex-foundation/umi@0.9.2) - '@metaplex-foundation/mpl-toolbox': 0.10.0(@metaplex-foundation/umi@0.9.2) - '@metaplex-foundation/umi': 0.9.2 - '@noble/hashes': 1.8.0 - merkletreejs: 0.3.11 - - '@metaplex-foundation/mpl-token-metadata@3.2.1(@metaplex-foundation/umi@0.9.2)': - dependencies: - '@metaplex-foundation/mpl-toolbox': 0.9.4(@metaplex-foundation/umi@0.9.2) - '@metaplex-foundation/umi': 0.9.2 - - '@metaplex-foundation/mpl-token-metadata@3.4.0(@metaplex-foundation/umi@0.9.2)': - dependencies: - '@metaplex-foundation/mpl-toolbox': 0.10.0(@metaplex-foundation/umi@0.9.2) - '@metaplex-foundation/umi': 0.9.2 - - '@metaplex-foundation/mpl-toolbox@0.10.0(@metaplex-foundation/umi@0.9.2)': - dependencies: - '@metaplex-foundation/umi': 0.9.2 - - '@metaplex-foundation/mpl-toolbox@0.9.4(@metaplex-foundation/umi@0.9.2)': - dependencies: - '@metaplex-foundation/umi': 0.9.2 - - '@metaplex-foundation/umi-bundle-defaults@0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))': - dependencies: - '@metaplex-foundation/umi': 0.9.2 - '@metaplex-foundation/umi-downloader-http': 0.9.2(@metaplex-foundation/umi@0.9.2) - '@metaplex-foundation/umi-eddsa-web3js': 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@metaplex-foundation/umi-http-fetch': 0.9.2(@metaplex-foundation/umi@0.9.2) - '@metaplex-foundation/umi-program-repository': 0.9.2(@metaplex-foundation/umi@0.9.2) - '@metaplex-foundation/umi-rpc-chunk-get-accounts': 0.9.2(@metaplex-foundation/umi@0.9.2) - '@metaplex-foundation/umi-rpc-web3js': 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@metaplex-foundation/umi-serializer-data-view': 0.9.2(@metaplex-foundation/umi@0.9.2) - '@metaplex-foundation/umi-transaction-factory-web3js': 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - transitivePeerDependencies: - - encoding - - '@metaplex-foundation/umi-downloader-http@0.9.2(@metaplex-foundation/umi@0.9.2)': - dependencies: - '@metaplex-foundation/umi': 0.9.2 - - '@metaplex-foundation/umi-eddsa-web3js@0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))': - dependencies: - '@metaplex-foundation/umi': 0.9.2 - '@metaplex-foundation/umi-web3js-adapters': 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@noble/curves': 1.9.0 - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - - '@metaplex-foundation/umi-http-fetch@0.9.2(@metaplex-foundation/umi@0.9.2)': - dependencies: - '@metaplex-foundation/umi': 0.9.2 - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding - - '@metaplex-foundation/umi-options@0.8.9': {} - - '@metaplex-foundation/umi-program-repository@0.9.2(@metaplex-foundation/umi@0.9.2)': - dependencies: - '@metaplex-foundation/umi': 0.9.2 - - '@metaplex-foundation/umi-public-keys@0.8.9': - dependencies: - '@metaplex-foundation/umi-serializers-encodings': 0.8.9 - - '@metaplex-foundation/umi-rpc-chunk-get-accounts@0.9.2(@metaplex-foundation/umi@0.9.2)': - dependencies: - '@metaplex-foundation/umi': 0.9.2 - - '@metaplex-foundation/umi-rpc-web3js@0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))': - dependencies: - '@metaplex-foundation/umi': 0.9.2 - '@metaplex-foundation/umi-web3js-adapters': 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - - '@metaplex-foundation/umi-serializer-data-view@0.9.2(@metaplex-foundation/umi@0.9.2)': - dependencies: - '@metaplex-foundation/umi': 0.9.2 - - '@metaplex-foundation/umi-serializers-core@0.8.9': {} - - '@metaplex-foundation/umi-serializers-encodings@0.8.9': - dependencies: - '@metaplex-foundation/umi-serializers-core': 0.8.9 - - '@metaplex-foundation/umi-serializers-numbers@0.8.9': - dependencies: - '@metaplex-foundation/umi-serializers-core': 0.8.9 - - '@metaplex-foundation/umi-serializers@0.9.0': - dependencies: - '@metaplex-foundation/umi-options': 0.8.9 - '@metaplex-foundation/umi-public-keys': 0.8.9 - '@metaplex-foundation/umi-serializers-core': 0.8.9 - '@metaplex-foundation/umi-serializers-encodings': 0.8.9 - '@metaplex-foundation/umi-serializers-numbers': 0.8.9 - - '@metaplex-foundation/umi-transaction-factory-web3js@0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))': - dependencies: - '@metaplex-foundation/umi': 0.9.2 - '@metaplex-foundation/umi-web3js-adapters': 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - - '@metaplex-foundation/umi-web3js-adapters@0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))': - dependencies: - '@metaplex-foundation/umi': 0.9.2 - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - buffer: 6.0.3 - - '@metaplex-foundation/umi@0.9.2': - dependencies: - '@metaplex-foundation/umi-options': 0.8.9 - '@metaplex-foundation/umi-public-keys': 0.8.9 - '@metaplex-foundation/umi-serializers': 0.9.0 - - '@noble/curves@1.4.2': - dependencies: - '@noble/hashes': 1.4.0 - - '@noble/curves@1.9.0': - dependencies: - '@noble/hashes': 1.8.0 - - '@noble/hashes@1.4.0': {} - '@noble/hashes@1.8.0': {} '@nodelib/fs.scandir@2.1.5': @@ -5272,12 +4885,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@project-serum/borsh@0.2.5(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))': - dependencies: - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - bn.js: 5.2.2 - buffer-layout: 1.2.2 - '@rollup/rollup-android-arm-eabi@4.41.1': optional: true @@ -5323,9 +4930,6 @@ snapshots: '@rollup/rollup-linux-s390x-gnu@4.41.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.40.2': - optional: true - '@rollup/rollup-linux-x64-gnu@4.41.1': optional: true @@ -5343,19 +4947,6 @@ snapshots: '@rtsao/scc@1.1.0': {} - '@scure/base@1.1.9': {} - - '@scure/bip32@1.4.0': - dependencies: - '@noble/curves': 1.4.2 - '@noble/hashes': 1.4.0 - '@scure/base': 1.1.9 - - '@scure/bip39@1.3.0': - dependencies: - '@noble/hashes': 1.4.0 - '@scure/base': 1.1.9 - '@sentry-internal/tracing@7.120.3': dependencies: '@sentry/core': 7.120.3 @@ -5456,464 +5047,491 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 - '@solana/buffer-layout-utils@0.2.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)': + '@solana-program/compute-budget@0.11.0(@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10))': dependencies: - '@solana/buffer-layout': 4.0.1 - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - bigint-buffer: 1.1.5 - bignumber.js: 9.3.0 - transitivePeerDependencies: - - bufferutil - - encoding - - typescript - - utf-8-validate + '@solana/kit': 5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@solana/buffer-layout@4.0.1': + '@solana-program/compute-budget@0.9.0(@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10))': dependencies: - buffer: 6.0.3 + '@solana/kit': 5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@solana/codecs-core@2.0.0-rc.1(typescript@5.8.3)': + '@solana-program/memo@0.10.0(@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10))': dependencies: - '@solana/errors': 2.0.0-rc.1(typescript@5.8.3) - typescript: 5.8.3 + '@solana/kit': 5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + + '@solana-program/system@0.10.0(@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10))': + dependencies: + '@solana/kit': 5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + + '@solana-program/system@0.9.1(@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10))': + dependencies: + '@solana/kit': 5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + + '@solana-program/token-2022@0.6.1(@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10))(@solana/sysvars@5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3))': + dependencies: + '@solana/kit': 5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@solana/sysvars': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + + '@solana-program/token@0.9.0(@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10))': + dependencies: + '@solana/kit': 5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@solana/codecs-core@2.1.0(typescript@5.8.3)': + '@solana/accounts@5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': dependencies: - '@solana/errors': 2.1.0(typescript@5.8.3) + '@solana/addresses': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/codecs-core': 5.4.0(typescript@5.8.3) + '@solana/codecs-strings': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/errors': 5.4.0(typescript@5.8.3) + '@solana/rpc-spec': 5.4.0(typescript@5.8.3) + '@solana/rpc-types': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + optionalDependencies: typescript: 5.8.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder - '@solana/codecs-data-structures@2.0.0-rc.1(typescript@5.8.3)': + '@solana/addresses@5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': dependencies: - '@solana/codecs-core': 2.0.0-rc.1(typescript@5.8.3) - '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.8.3) - '@solana/errors': 2.0.0-rc.1(typescript@5.8.3) + '@solana/assertions': 5.4.0(typescript@5.8.3) + '@solana/codecs-core': 5.4.0(typescript@5.8.3) + '@solana/codecs-strings': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/errors': 5.4.0(typescript@5.8.3) + '@solana/nominal-types': 5.4.0(typescript@5.8.3) + optionalDependencies: typescript: 5.8.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder - '@solana/codecs-numbers@2.0.0-rc.1(typescript@5.8.3)': + '@solana/assertions@3.0.3(typescript@5.8.3)': dependencies: - '@solana/codecs-core': 2.0.0-rc.1(typescript@5.8.3) - '@solana/errors': 2.0.0-rc.1(typescript@5.8.3) + '@solana/errors': 3.0.3(typescript@5.8.3) typescript: 5.8.3 - '@solana/codecs-numbers@2.1.0(typescript@5.8.3)': + '@solana/assertions@5.4.0(typescript@5.8.3)': dependencies: - '@solana/codecs-core': 2.1.0(typescript@5.8.3) - '@solana/errors': 2.1.0(typescript@5.8.3) + '@solana/errors': 5.4.0(typescript@5.8.3) + optionalDependencies: typescript: 5.8.3 - '@solana/codecs-strings@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': + '@solana/codecs-core@5.4.0(typescript@5.8.3)': dependencies: - '@solana/codecs-core': 2.0.0-rc.1(typescript@5.8.3) - '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.8.3) - '@solana/errors': 2.0.0-rc.1(typescript@5.8.3) - fastestsmallesttextencoderdecoder: 1.0.22 + '@solana/errors': 5.4.0(typescript@5.8.3) + optionalDependencies: typescript: 5.8.3 - '@solana/codecs@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': + '@solana/codecs-data-structures@5.4.0(typescript@5.8.3)': dependencies: - '@solana/codecs-core': 2.0.0-rc.1(typescript@5.8.3) - '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@5.8.3) - '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.8.3) - '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) - '@solana/options': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/codecs-core': 5.4.0(typescript@5.8.3) + '@solana/codecs-numbers': 5.4.0(typescript@5.8.3) + '@solana/errors': 5.4.0(typescript@5.8.3) + optionalDependencies: typescript: 5.8.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - '@solana/errors@2.0.0-rc.1(typescript@5.8.3)': + '@solana/codecs-numbers@5.4.0(typescript@5.8.3)': dependencies: - chalk: 5.4.1 - commander: 12.1.0 + '@solana/codecs-core': 5.4.0(typescript@5.8.3) + '@solana/errors': 5.4.0(typescript@5.8.3) + optionalDependencies: typescript: 5.8.3 - '@solana/errors@2.1.0(typescript@5.8.3)': + '@solana/codecs-strings@5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': dependencies: - chalk: 5.4.1 - commander: 13.1.0 + '@solana/codecs-core': 5.4.0(typescript@5.8.3) + '@solana/codecs-numbers': 5.4.0(typescript@5.8.3) + '@solana/errors': 5.4.0(typescript@5.8.3) + optionalDependencies: + fastestsmallesttextencoderdecoder: 1.0.22 typescript: 5.8.3 - '@solana/options@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': + '@solana/codecs@5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': dependencies: - '@solana/codecs-core': 2.0.0-rc.1(typescript@5.8.3) - '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@5.8.3) - '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.8.3) - '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) - '@solana/errors': 2.0.0-rc.1(typescript@5.8.3) + '@solana/codecs-core': 5.4.0(typescript@5.8.3) + '@solana/codecs-data-structures': 5.4.0(typescript@5.8.3) + '@solana/codecs-numbers': 5.4.0(typescript@5.8.3) + '@solana/codecs-strings': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/options': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/spl-account-compression@0.2.0(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)': + '@solana/errors@3.0.3(typescript@5.8.3)': dependencies: - '@metaplex-foundation/beet': 0.7.2 - '@metaplex-foundation/beet-solana': 0.4.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - bn.js: 5.2.2 - borsh: 0.7.0 - js-sha3: 0.8.0 - typescript-collections: 1.3.3 - transitivePeerDependencies: - - bufferutil - - encoding - - supports-color - - typescript - - utf-8-validate + chalk: 5.6.2 + commander: 14.0.0 + typescript: 5.8.3 - '@solana/spl-token-group@0.0.7(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': + '@solana/errors@5.4.0(typescript@5.8.3)': dependencies: - '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - typescript + chalk: 5.6.2 + commander: 14.0.2 + optionalDependencies: + typescript: 5.8.3 + + '@solana/fast-stable-stringify@5.4.0(typescript@5.8.3)': + optionalDependencies: + typescript: 5.8.3 + + '@solana/functional@5.4.0(typescript@5.8.3)': + optionalDependencies: + typescript: 5.8.3 - '@solana/spl-token-metadata@0.1.6(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': + '@solana/instruction-plans@5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': dependencies: - '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@solana/errors': 5.4.0(typescript@5.8.3) + '@solana/instructions': 5.4.0(typescript@5.8.3) + '@solana/keys': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/promises': 5.4.0(typescript@5.8.3) + '@solana/transaction-messages': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/transactions': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - - typescript - '@solana/spl-token@0.3.11(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': + '@solana/instructions@5.4.0(typescript@5.8.3)': dependencies: - '@solana/buffer-layout': 4.0.1 - '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@solana/spl-token-metadata': 0.1.6(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - buffer: 6.0.3 - transitivePeerDependencies: - - bufferutil - - encoding - - fastestsmallesttextencoderdecoder - - typescript - - utf-8-validate + '@solana/codecs-core': 5.4.0(typescript@5.8.3) + '@solana/errors': 5.4.0(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 - '@solana/spl-token@0.4.13(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': + '@solana/keys@5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': dependencies: - '@solana/buffer-layout': 4.0.1 - '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@solana/spl-token-group': 0.0.7(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) - '@solana/spl-token-metadata': 0.1.6(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - buffer: 6.0.3 + '@solana/assertions': 5.4.0(typescript@5.8.3) + '@solana/codecs-core': 5.4.0(typescript@5.8.3) + '@solana/codecs-strings': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/errors': 5.4.0(typescript@5.8.3) + '@solana/nominal-types': 5.4.0(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 transitivePeerDependencies: - - bufferutil - - encoding - fastestsmallesttextencoderdecoder - - typescript - - utf-8-validate - '@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)': - dependencies: - '@babel/runtime': 7.27.1 - '@noble/curves': 1.9.0 - '@noble/hashes': 1.8.0 - '@solana/buffer-layout': 4.0.1 - '@solana/codecs-numbers': 2.1.0(typescript@5.8.3) - agentkeepalive: 4.6.0 - bn.js: 5.2.2 - borsh: 0.7.0 - bs58: 4.0.1 - buffer: 6.0.3 - fast-stable-stringify: 1.0.0 - jayson: 4.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - node-fetch: 2.7.0 - rpc-websockets: 9.1.1 - superstruct: 2.0.2 + '@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/accounts': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/addresses': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/codecs': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/errors': 5.4.0(typescript@5.8.3) + '@solana/functional': 5.4.0(typescript@5.8.3) + '@solana/instruction-plans': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/instructions': 5.4.0(typescript@5.8.3) + '@solana/keys': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/offchain-messages': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/plugin-core': 5.4.0(typescript@5.8.3) + '@solana/programs': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/rpc': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/rpc-api': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/rpc-parsed-types': 5.4.0(typescript@5.8.3) + '@solana/rpc-spec-types': 5.4.0(typescript@5.8.3) + '@solana/rpc-subscriptions': 5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@solana/rpc-types': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/signers': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/sysvars': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/transaction-confirmation': 5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@solana/transaction-messages': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/transactions': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 transitivePeerDependencies: - bufferutil - - encoding - - typescript + - fastestsmallesttextencoderdecoder - utf-8-validate - '@sqltools/formatter@1.2.5': {} + '@solana/nominal-types@5.4.0(typescript@5.8.3)': + optionalDependencies: + typescript: 5.8.3 - '@staratlas/anchor@0.25.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)': + '@solana/offchain-messages@5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': dependencies: - '@project-serum/borsh': 0.2.5(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - base64-js: 1.5.1 - bn.js: 5.2.2 - bs58: 4.0.1 - buffer-layout: 1.2.2 - camelcase: 5.3.1 - cross-fetch: 3.2.0 - crypto-hash: 1.3.0 - eventemitter3: 4.0.7 - js-sha256: 0.9.0 - pako: 2.1.0 - snake-case: 3.0.4 - superstruct: 0.15.5 - toml: 3.0.0 - transitivePeerDependencies: - - bufferutil - - encoding - - typescript - - utf-8-validate - - '@staratlas/atlas-prime@0.13.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': - dependencies: - '@solana/spl-token': 0.3.11(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/anchor': 0.25.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/data-source': 0.7.7(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/player-profile': 0.9.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/profile-vault': 0.9.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@types/node': 20.17.46 - axios: 1.9.0 - bs58: 5.0.0 - neverthrow: 6.2.2 + '@solana/addresses': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/codecs-core': 5.4.0(typescript@5.8.3) + '@solana/codecs-data-structures': 5.4.0(typescript@5.8.3) + '@solana/codecs-numbers': 5.4.0(typescript@5.8.3) + '@solana/codecs-strings': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/errors': 5.4.0(typescript@5.8.3) + '@solana/keys': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/nominal-types': 5.4.0(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 transitivePeerDependencies: - - bufferutil - - debug - - encoding - fastestsmallesttextencoderdecoder - - typescript - - utf-8-validate - '@staratlas/cargo@1.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': + '@solana/options@5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': dependencies: - '@solana/spl-token': 0.4.13(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/anchor': 0.25.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/data-source': 0.8.3(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/player-profile': 0.11.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@solana/codecs-core': 5.4.0(typescript@5.8.3) + '@solana/codecs-data-structures': 5.4.0(typescript@5.8.3) + '@solana/codecs-numbers': 5.4.0(typescript@5.8.3) + '@solana/codecs-strings': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/errors': 5.4.0(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 transitivePeerDependencies: - - bufferutil - - encoding - fastestsmallesttextencoderdecoder - - typescript - - utf-8-validate - '@staratlas/claim-stake@0.12.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': + '@solana/plugin-core@5.4.0(typescript@5.8.3)': + optionalDependencies: + typescript: 5.8.3 + + '@solana/programs@5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': dependencies: - '@solana/spl-token': 0.4.13(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/anchor': 0.25.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/data-source': 0.8.3(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@solana/addresses': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/errors': 5.4.0(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 transitivePeerDependencies: - - bufferutil - - encoding - fastestsmallesttextencoderdecoder - - typescript - - utf-8-validate - '@staratlas/crafting@1.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': + '@solana/promises@3.0.3(typescript@5.8.3)': dependencies: - '@solana/spl-token': 0.4.13(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/anchor': 0.25.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/data-source': 0.8.3(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/player-profile': 0.11.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@types/lodash': 4.17.16 - lodash: 4.17.21 - transitivePeerDependencies: - - bufferutil - - encoding - - fastestsmallesttextencoderdecoder - - typescript - - utf-8-validate + typescript: 5.8.3 + + '@solana/promises@5.4.0(typescript@5.8.3)': + optionalDependencies: + typescript: 5.8.3 - '@staratlas/crew@0.7.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': - dependencies: - '@metaplex-foundation/mpl-bubblegum': 4.4.0(@metaplex-foundation/umi@0.9.2) - '@metaplex-foundation/mpl-token-metadata': 3.4.0(@metaplex-foundation/umi@0.9.2) - '@metaplex-foundation/umi': 0.9.2 - '@metaplex-foundation/umi-bundle-defaults': 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@metaplex-foundation/umi-web3js-adapters': 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@project-serum/anchor': '@staratlas/anchor@0.25.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)' - '@solana/spl-account-compression': 0.2.0(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@solana/spl-token': 0.4.13(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/anchor': 0.25.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/data-source': 0.8.3(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/player-profile': 0.11.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/profile-faction': 0.6.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@solana/rpc-api@5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': + dependencies: + '@solana/addresses': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/codecs-core': 5.4.0(typescript@5.8.3) + '@solana/codecs-strings': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/errors': 5.4.0(typescript@5.8.3) + '@solana/keys': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/rpc-parsed-types': 5.4.0(typescript@5.8.3) + '@solana/rpc-spec': 5.4.0(typescript@5.8.3) + '@solana/rpc-transformers': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/rpc-types': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/transaction-messages': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/transactions': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 transitivePeerDependencies: - - bufferutil - - encoding - fastestsmallesttextencoderdecoder - - supports-color - - typescript - - utf-8-validate - '@staratlas/data-source@0.7.7(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': + '@solana/rpc-parsed-types@5.4.0(typescript@5.8.3)': + optionalDependencies: + typescript: 5.8.3 + + '@solana/rpc-spec-types@5.4.0(typescript@5.8.3)': + optionalDependencies: + typescript: 5.8.3 + + '@solana/rpc-spec@5.4.0(typescript@5.8.3)': dependencies: - '@noble/curves': 1.9.0 - '@solana/spl-token': 0.3.11(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/anchor': 0.25.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - bs58: 5.0.0 - camelcase: 7.0.1 - lodash: 4.17.21 - neverthrow: 6.2.2 + '@solana/errors': 5.4.0(typescript@5.8.3) + '@solana/rpc-spec-types': 5.4.0(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 + + '@solana/rpc-subscriptions-api@5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': + dependencies: + '@solana/addresses': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/keys': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/rpc-subscriptions-spec': 5.4.0(typescript@5.8.3) + '@solana/rpc-transformers': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/rpc-types': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/transaction-messages': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/transactions': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 transitivePeerDependencies: - - bufferutil - - encoding - fastestsmallesttextencoderdecoder - - typescript - - utf-8-validate - '@staratlas/data-source@0.8.3(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': + '@solana/rpc-subscriptions-channel-websocket@5.4.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)': dependencies: - '@noble/curves': 1.9.0 - '@solana/spl-token': 0.4.13(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/anchor': 0.25.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - bs58: 6.0.0 - camelcase: 8.0.0 - lodash: 4.17.21 - neverthrow: 8.2.0 + '@solana/errors': 5.4.0(typescript@5.8.3) + '@solana/functional': 5.4.0(typescript@5.8.3) + '@solana/rpc-subscriptions-spec': 5.4.0(typescript@5.8.3) + '@solana/subscribable': 5.4.0(typescript@5.8.3) + ws: 8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.8.3 transitivePeerDependencies: - bufferutil - - encoding - - fastestsmallesttextencoderdecoder - - typescript - utf-8-validate - '@staratlas/data-source@0.9.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': + '@solana/rpc-subscriptions-spec@5.4.0(typescript@5.8.3)': dependencies: - '@noble/curves': 1.9.0 - '@solana/spl-token': 0.4.13(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/anchor': 0.25.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - bs58: 6.0.0 - camelcase: 8.0.0 - lodash: 4.17.21 - neverthrow: 8.2.0 + '@solana/errors': 5.4.0(typescript@5.8.3) + '@solana/promises': 5.4.0(typescript@5.8.3) + '@solana/rpc-spec-types': 5.4.0(typescript@5.8.3) + '@solana/subscribable': 5.4.0(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 + + '@solana/rpc-subscriptions@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/errors': 5.4.0(typescript@5.8.3) + '@solana/fast-stable-stringify': 5.4.0(typescript@5.8.3) + '@solana/functional': 5.4.0(typescript@5.8.3) + '@solana/promises': 5.4.0(typescript@5.8.3) + '@solana/rpc-spec-types': 5.4.0(typescript@5.8.3) + '@solana/rpc-subscriptions-api': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/rpc-subscriptions-channel-websocket': 5.4.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@solana/rpc-subscriptions-spec': 5.4.0(typescript@5.8.3) + '@solana/rpc-transformers': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/rpc-types': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/subscribable': 5.4.0(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 transitivePeerDependencies: - bufferutil - - encoding - fastestsmallesttextencoderdecoder - - typescript - utf-8-validate - '@staratlas/factory@0.7.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': + '@solana/rpc-transformers@5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': dependencies: - '@coral-xyz/anchor': 0.29.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@solana/spl-token': 0.4.13(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - big.js: 6.2.2 - lodash: 4.17.21 - mobx: 6.13.7 - mobx-utils: 6.1.0(mobx@6.13.7) + '@solana/errors': 5.4.0(typescript@5.8.3) + '@solana/functional': 5.4.0(typescript@5.8.3) + '@solana/nominal-types': 5.4.0(typescript@5.8.3) + '@solana/rpc-spec-types': 5.4.0(typescript@5.8.3) + '@solana/rpc-types': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 transitivePeerDependencies: - - bufferutil - - encoding - fastestsmallesttextencoderdecoder - - typescript - - utf-8-validate - '@staratlas/player-profile@0.11.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': + '@solana/rpc-transport-http@5.4.0(typescript@5.8.3)': + dependencies: + '@solana/errors': 5.4.0(typescript@5.8.3) + '@solana/rpc-spec': 5.4.0(typescript@5.8.3) + '@solana/rpc-spec-types': 5.4.0(typescript@5.8.3) + undici-types: 7.18.2 + optionalDependencies: + typescript: 5.8.3 + + '@solana/rpc-types@5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': dependencies: - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/anchor': 0.25.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/data-source': 0.8.3(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@solana/addresses': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/codecs-core': 5.4.0(typescript@5.8.3) + '@solana/codecs-numbers': 5.4.0(typescript@5.8.3) + '@solana/codecs-strings': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/errors': 5.4.0(typescript@5.8.3) + '@solana/nominal-types': 5.4.0(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 transitivePeerDependencies: - - bufferutil - - encoding - fastestsmallesttextencoderdecoder - - typescript - - utf-8-validate - '@staratlas/player-profile@0.9.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': - dependencies: - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/anchor': 0.25.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/data-source': 0.7.7(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@solana/rpc@5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': + dependencies: + '@solana/errors': 5.4.0(typescript@5.8.3) + '@solana/fast-stable-stringify': 5.4.0(typescript@5.8.3) + '@solana/functional': 5.4.0(typescript@5.8.3) + '@solana/rpc-api': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/rpc-spec': 5.4.0(typescript@5.8.3) + '@solana/rpc-spec-types': 5.4.0(typescript@5.8.3) + '@solana/rpc-transformers': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/rpc-transport-http': 5.4.0(typescript@5.8.3) + '@solana/rpc-types': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 transitivePeerDependencies: - - bufferutil - - encoding - fastestsmallesttextencoderdecoder - - typescript - - utf-8-validate - '@staratlas/points-store@1.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': - dependencies: - '@solana/spl-token': 0.4.13(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/anchor': 0.25.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/data-source': 0.8.3(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/player-profile': 0.11.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/points': 1.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/profile-faction': 0.6.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@solana/signers@5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': + dependencies: + '@solana/addresses': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/codecs-core': 5.4.0(typescript@5.8.3) + '@solana/errors': 5.4.0(typescript@5.8.3) + '@solana/instructions': 5.4.0(typescript@5.8.3) + '@solana/keys': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/nominal-types': 5.4.0(typescript@5.8.3) + '@solana/offchain-messages': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/transaction-messages': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/transactions': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 transitivePeerDependencies: - - bufferutil - - encoding - fastestsmallesttextencoderdecoder - - typescript - - utf-8-validate - '@staratlas/points@1.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': + '@solana/subscribable@5.4.0(typescript@5.8.3)': + dependencies: + '@solana/errors': 5.4.0(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 + + '@solana/sysvars@5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': dependencies: - '@solana/spl-token': 0.4.13(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/anchor': 0.25.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/data-source': 0.8.3(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/player-profile': 0.11.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@solana/accounts': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/codecs': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/errors': 5.4.0(typescript@5.8.3) + '@solana/rpc-types': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 transitivePeerDependencies: - - bufferutil - - encoding - fastestsmallesttextencoderdecoder - - typescript - - utf-8-validate - '@staratlas/profile-faction@0.6.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': - dependencies: - '@solana/spl-token': 0.4.13(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/anchor': 0.25.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/data-source': 0.8.3(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/player-profile': 0.11.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@solana/transaction-confirmation@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/addresses': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/codecs-strings': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/errors': 5.4.0(typescript@5.8.3) + '@solana/keys': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/promises': 5.4.0(typescript@5.8.3) + '@solana/rpc': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/rpc-subscriptions': 5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@solana/rpc-types': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/transaction-messages': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/transactions': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 transitivePeerDependencies: - bufferutil - - encoding - fastestsmallesttextencoderdecoder - - typescript - utf-8-validate - '@staratlas/profile-vault@0.9.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': - dependencies: - '@solana/spl-token': 0.3.11(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/anchor': 0.25.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/data-source': 0.7.7(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/player-profile': 0.9.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@solana/transaction-messages@5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': + dependencies: + '@solana/addresses': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/codecs-core': 5.4.0(typescript@5.8.3) + '@solana/codecs-data-structures': 5.4.0(typescript@5.8.3) + '@solana/codecs-numbers': 5.4.0(typescript@5.8.3) + '@solana/errors': 5.4.0(typescript@5.8.3) + '@solana/functional': 5.4.0(typescript@5.8.3) + '@solana/instructions': 5.4.0(typescript@5.8.3) + '@solana/nominal-types': 5.4.0(typescript@5.8.3) + '@solana/rpc-types': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 transitivePeerDependencies: - - bufferutil - - encoding - fastestsmallesttextencoderdecoder - - typescript - - utf-8-validate - '@staratlas/sage@1.8.10(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': - dependencies: - '@metaplex-foundation/mpl-bubblegum': 4.4.0(@metaplex-foundation/umi@0.9.2) - '@metaplex-foundation/umi': 0.9.2 - '@metaplex-foundation/umi-eddsa-web3js': 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@metaplex-foundation/umi-program-repository': 0.9.2(@metaplex-foundation/umi@0.9.2) - '@metaplex-foundation/umi-web3js-adapters': 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@solana/spl-token': 0.4.13(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/anchor': 0.25.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/cargo': 1.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/crafting': 1.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/crew': 0.7.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/data-source': 0.8.3(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/player-profile': 0.11.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/points': 1.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/points-store': 1.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@staratlas/profile-faction': 0.6.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@types/lodash': 4.17.16 - lodash: 4.17.21 + '@solana/transactions@5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': + dependencies: + '@solana/addresses': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/codecs-core': 5.4.0(typescript@5.8.3) + '@solana/codecs-data-structures': 5.4.0(typescript@5.8.3) + '@solana/codecs-numbers': 5.4.0(typescript@5.8.3) + '@solana/codecs-strings': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/errors': 5.4.0(typescript@5.8.3) + '@solana/functional': 5.4.0(typescript@5.8.3) + '@solana/instructions': 5.4.0(typescript@5.8.3) + '@solana/keys': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/nominal-types': 5.4.0(typescript@5.8.3) + '@solana/rpc-types': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/transaction-messages': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 transitivePeerDependencies: - - bufferutil - - encoding - fastestsmallesttextencoderdecoder - - supports-color - - typescript - - utf-8-validate + + '@sqltools/formatter@1.2.5': {} + + '@staratlas/dev-player-profile@0.40.0(@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10))': + dependencies: + '@solana/kit': 5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + + '@staratlas/dev-profile-faction@0.40.0(@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10))': + dependencies: + '@solana/kit': 5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@staratlas/dev-player-profile': 0.40.0(@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)) + + '@staratlas/dev-sage@0.40.0(@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10))': + dependencies: + '@solana/kit': 5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@staratlas/dev-player-profile': 0.40.0(@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@staratlas/dev-profile-faction': 0.40.0(@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)) '@swc/core-darwin-arm64@1.11.29': optional: true @@ -5969,6 +5587,7 @@ snapshots: '@swc/helpers@0.5.17': dependencies: tslib: 2.8.1 + optional: true '@swc/types@0.1.21': dependencies: @@ -6012,10 +5631,6 @@ snapshots: '@types/big.js@6.2.2': {} - '@types/bn.js@5.1.6': - dependencies: - '@types/node': 22.15.29 - '@types/chance@1.1.6': {} '@types/connect@3.4.38': @@ -6044,8 +5659,6 @@ snapshots: '@types/json5@0.0.29': {} - '@types/lodash@4.17.16': {} - '@types/luxon@3.6.2': {} '@types/methods@1.1.4': {} @@ -6056,12 +5669,6 @@ snapshots: dependencies: '@types/node': 22.15.29 - '@types/node@12.20.55': {} - - '@types/node@20.17.46': - dependencies: - undici-types: 6.19.8 - '@types/node@22.15.29': dependencies: undici-types: 6.21.0 @@ -6099,16 +5706,6 @@ snapshots: '@types/triple-beam@1.3.5': {} - '@types/uuid@8.3.4': {} - - '@types/ws@7.4.7': - dependencies: - '@types/node': 22.15.29 - - '@types/ws@8.18.1': - dependencies: - '@types/node': 22.15.29 - '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.33': @@ -6226,10 +5823,6 @@ snapshots: acorn@8.14.1: {} - agentkeepalive@4.6.0: - dependencies: - humanize-ms: 1.2.1 - ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -6253,8 +5846,6 @@ snapshots: ansi-styles@6.2.1: {} - ansicolors@0.3.2: {} - ansis@3.17.0: {} any-promise@1.3.0: {} @@ -6325,14 +5916,6 @@ snapshots: asap@2.0.6: {} - assert@2.1.0: - dependencies: - call-bind: 1.0.8 - is-nan: 1.3.2 - object-is: 1.1.6 - object.assign: 4.1.7 - util: 0.12.5 - async-function@1.0.0: {} async@3.2.6: {} @@ -6343,14 +5926,6 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - axios@1.9.0: - dependencies: - follow-redirects: 1.15.9 - form-data: 4.0.2 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - babel-jest@29.7.0(@babel/core@7.27.4): dependencies: '@babel/core': 7.27.4 @@ -6408,46 +5983,18 @@ snapshots: balanced-match@1.0.2: {} - base-x@3.0.11: - dependencies: - safe-buffer: 5.2.1 - - base-x@4.0.1: {} - base-x@5.0.1: {} base64-js@1.5.1: {} - big.js@6.2.2: {} - big.js@7.0.1: {} - bigint-buffer@1.1.5: - dependencies: - bindings: 1.5.0 - - bignumber.js@9.3.0: {} - - bindings@1.5.0: - dependencies: - file-uri-to-path: 1.0.0 - bip39@3.1.0: dependencies: '@noble/hashes': 1.8.0 - bn.js@4.11.6: {} - - bn.js@5.2.2: {} - boolbase@1.0.0: {} - borsh@0.7.0: - dependencies: - bn.js: 5.2.2 - bs58: 4.0.1 - text-encoding-utf-8: 1.0.2 - brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -6472,14 +6019,6 @@ snapshots: dependencies: fast-json-stable-stringify: 2.1.0 - bs58@4.0.1: - dependencies: - base-x: 3.0.11 - - bs58@5.0.0: - dependencies: - base-x: 4.0.1 - bs58@6.0.0: dependencies: base-x: 5.0.1 @@ -6499,10 +6038,6 @@ snapshots: buffer-from@1.1.2: {} - buffer-layout@1.2.2: {} - - buffer-reverse@1.0.1: {} - buffer@6.0.3: dependencies: base64-js: 1.5.1 @@ -6543,10 +6078,6 @@ snapshots: camelcase@6.3.0: {} - camelcase@7.0.1: {} - - camelcase@8.0.0: {} - caniuse-lite@1.0.30001720: {} chalk@4.1.2: @@ -6554,7 +6085,7 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chalk@5.4.1: {} + chalk@5.6.2: {} chance@1.1.13: {} @@ -6614,11 +6145,9 @@ snapshots: dependencies: delayed-stream: 1.0.0 - commander@12.1.0: {} - - commander@13.1.0: {} + commander@14.0.0: {} - commander@2.20.3: {} + commander@14.0.2: {} commander@4.1.1: {} @@ -6674,22 +6203,12 @@ snapshots: '@types/luxon': 3.6.2 luxon: 3.6.1 - cross-fetch@3.2.0: - dependencies: - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 - crypto-hash@1.3.0: {} - - crypto-js@4.2.0: {} - css-select@4.3.0: dependencies: boolbase: 1.0.0 @@ -6750,8 +6269,6 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 - delay@5.0.0: {} - delayed-stream@1.0.0: {} detect-newline@3.1.0: {} @@ -6792,11 +6309,6 @@ snapshots: domelementtype: 2.3.0 domhandler: 4.3.1 - dot-case@3.0.4: - dependencies: - no-case: 3.0.4 - tslib: 2.8.1 - dotenv@16.5.0: {} dunder-proto@1.0.1: @@ -6911,12 +6423,6 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - es6-promise@4.2.8: {} - - es6-promisify@5.0.0: - dependencies: - es6-promise: 4.2.8 - esbuild@0.25.5: optionalDependencies: '@esbuild/aix-ppc64': 0.25.5 @@ -7093,28 +6599,8 @@ snapshots: esutils@2.0.3: {} - ethereum-bloom-filters@1.2.0: - dependencies: - '@noble/hashes': 1.8.0 - - ethereum-cryptography@2.2.1: - dependencies: - '@noble/curves': 1.4.2 - '@noble/hashes': 1.4.0 - '@scure/bip32': 1.4.0 - '@scure/bip39': 1.3.0 - - ethjs-unit@0.1.6: - dependencies: - bn.js: 4.11.6 - number-to-bn: 1.7.0 - event-target-shim@5.0.1: {} - eventemitter3@4.0.7: {} - - eventemitter3@5.0.1: {} - execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -7137,8 +6623,6 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 - eyes@0.1.8: {} - fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -7157,9 +6641,8 @@ snapshots: fast-safe-stringify@2.1.1: {} - fast-stable-stringify@1.0.0: {} - - fastestsmallesttextencoderdecoder@1.0.22: {} + fastestsmallesttextencoderdecoder@1.0.22: + optional: true fastq@1.19.1: dependencies: @@ -7179,8 +6662,6 @@ snapshots: dependencies: flat-cache: 4.0.1 - file-uri-to-path@1.0.0: {} - filelist@1.0.4: dependencies: minimatch: 5.1.6 @@ -7214,8 +6695,6 @@ snapshots: fn.name@1.1.0: {} - follow-redirects@1.15.9: {} - for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -7376,10 +6855,6 @@ snapshots: human-signals@2.1.0: {} - humanize-ms@1.2.1: - dependencies: - ms: 2.1.3 - ieee754@1.2.1: {} ignore@5.3.2: {} @@ -7420,11 +6895,6 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 - is-arguments@1.2.0: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -7490,15 +6960,8 @@ snapshots: dependencies: is-extglob: 2.1.1 - is-hex-prefixed@1.0.0: {} - is-map@2.0.3: {} - is-nan@1.3.2: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - is-number-object@1.1.1: dependencies: call-bound: 1.0.4 @@ -7551,10 +7014,6 @@ snapshots: isexe@2.0.0: {} - isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)): - dependencies: - ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) - istanbul-lib-coverage@3.2.2: {} istanbul-lib-instrument@5.2.1: @@ -7609,24 +7068,6 @@ snapshots: filelist: 1.0.4 minimatch: 3.1.2 - jayson@4.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): - dependencies: - '@types/connect': 3.4.38 - '@types/node': 12.20.55 - '@types/ws': 7.4.7 - commander: 2.20.3 - delay: 5.0.0 - es6-promisify: 5.0.0 - eyes: 0.1.8 - isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - json-stringify-safe: 5.0.1 - stream-json: 1.9.1 - uuid: 8.3.2 - ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) - transitivePeerDependencies: - - bufferutil - - utf-8-validate - jest-changed-files@29.7.0: dependencies: execa: 5.1.1 @@ -7938,10 +7379,6 @@ snapshots: joycon@3.1.1: {} - js-sha256@0.9.0: {} - - js-sha3@0.8.0: {} - js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -7963,8 +7400,6 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} - json-stringify-safe@5.0.1: {} - json5@1.0.2: dependencies: minimist: 1.2.8 @@ -8033,10 +7468,6 @@ snapshots: safe-stable-stringify: 2.5.0 triple-beam: 1.4.1 - lower-case@2.0.2: - dependencies: - tslib: 2.8.1 - lru-cache@10.4.3: {} lru-cache@5.1.1: @@ -8071,18 +7502,8 @@ snapshots: merge2@1.4.1: {} - merkletreejs@0.3.11: - dependencies: - bignumber.js: 9.3.0 - buffer-reverse: 1.0.1 - crypto-js: 4.2.0 - treeify: 1.1.0 - web3-utils: 1.10.4 - methods@1.1.2: {} - micro-ftch@0.3.1: {} - micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -8121,12 +7542,6 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.1 - mobx-utils@6.1.0(mobx@6.13.7): - dependencies: - mobx: 6.13.7 - - mobx@6.13.7: {} - module-alias@2.2.3: {} module-details-from-path@1.0.4: {} @@ -8143,17 +7558,6 @@ snapshots: natural-compare@1.4.0: {} - neverthrow@6.2.2: {} - - neverthrow@8.2.0: - optionalDependencies: - '@rollup/rollup-linux-x64-gnu': 4.40.2 - - no-case@3.0.4: - dependencies: - lower-case: 2.0.2 - tslib: 2.8.1 - node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 @@ -8175,20 +7579,10 @@ snapshots: dependencies: boolbase: 1.0.0 - number-to-bn@1.7.0: - dependencies: - bn.js: 4.11.6 - strip-hex-prefix: 1.0.0 - object-assign@4.1.1: {} object-inspect@1.13.4: {} - object-is@1.1.6: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - object-keys@1.1.1: {} object.assign@4.1.7: @@ -8269,8 +7663,6 @@ snapshots: package-json-from-dist@1.0.1: {} - pako@2.1.0: {} - parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -8396,8 +7788,6 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 - proxy-from-env@1.1.0: {} - punycode@2.3.1: {} pure-rand@6.1.0: {} @@ -8408,10 +7798,6 @@ snapshots: queue-microtask@1.2.3: {} - randombytes@2.1.0: - dependencies: - safe-buffer: 5.2.1 - react-is@18.3.1: {} readable-stream@3.6.2: @@ -8513,19 +7899,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.41.1 fsevents: 2.3.3 - rpc-websockets@9.1.1: - dependencies: - '@swc/helpers': 0.5.17 - '@types/uuid': 8.3.4 - '@types/ws': 8.18.1 - buffer: 6.0.3 - eventemitter3: 5.0.1 - uuid: 8.3.2 - ws: 8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) - optionalDependencies: - bufferutil: 4.0.9 - utf-8-validate: 5.0.10 - run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -8638,10 +8011,26 @@ snapshots: slash@3.0.0: {} - snake-case@3.0.4: - dependencies: - dot-case: 3.0.4 - tslib: 2.8.1 + solana-kite@2.1.0(@solana/sysvars@5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10): + dependencies: + '@solana-program/compute-budget': 0.11.0(@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@solana-program/memo': 0.10.0(@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@solana-program/system': 0.10.0(@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@solana-program/token': 0.9.0(@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@solana-program/token-2022': 0.6.1(@solana/kit@5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10))(@solana/sysvars@5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)) + '@solana/assertions': 3.0.3(typescript@5.8.3) + '@solana/errors': 3.0.3(typescript@5.8.3) + '@solana/kit': 5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@solana/promises': 3.0.3(typescript@5.8.3) + '@solana/transaction-confirmation': 5.4.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + dotenv: 16.5.0 + ws: 8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - '@solana/sysvars' + - bufferutil + - fastestsmallesttextencoderdecoder + - typescript + - utf-8-validate source-map-support@0.5.13: dependencies: @@ -8666,12 +8055,6 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 - stream-chain@2.2.5: {} - - stream-json@1.9.1: - dependencies: - stream-chain: 2.2.5 - string-length@4.0.2: dependencies: char-regex: 1.0.2 @@ -8730,10 +8113,6 @@ snapshots: strip-final-newline@2.0.0: {} - strip-hex-prefix@1.0.0: - dependencies: - is-hex-prefixed: 1.0.0 - strip-json-comments@3.1.1: {} sucrase@3.35.0: @@ -8760,10 +8139,6 @@ snapshots: transitivePeerDependencies: - supports-color - superstruct@0.15.5: {} - - superstruct@2.0.2: {} - supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -8798,8 +8173,6 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 - text-encoding-utf-8@1.0.2: {} - text-hex@1.0.0: {} thenify-all@1.6.0: @@ -8823,8 +8196,6 @@ snapshots: dependencies: is-number: 7.0.0 - toml@3.0.0: {} - tr46@0.0.3: {} tr46@1.0.1: @@ -8833,8 +8204,6 @@ snapshots: tree-kill@1.2.2: {} - treeify@1.1.0: {} - triple-beam@1.4.1: {} ts-api-utils@2.1.0(typescript@5.8.3): @@ -8998,8 +8367,6 @@ snapshots: - babel-plugin-macros - supports-color - typescript-collections@1.3.3: {} - typescript@5.8.3: {} ufo@1.6.1: {} @@ -9011,10 +8378,10 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - undici-types@6.19.8: {} - undici-types@6.21.0: {} + undici-types@7.18.2: {} + undici@7.10.0: {} update-browserslist-db@1.1.3(browserslist@4.25.0): @@ -9032,24 +8399,12 @@ snapshots: node-gyp-build: 4.8.4 optional: true - utf8@3.0.0: {} - util-deprecate@1.0.2: {} - util@0.12.5: - dependencies: - inherits: 2.0.4 - is-arguments: 1.2.0 - is-generator-function: 1.1.0 - is-typed-array: 1.1.15 - which-typed-array: 1.1.19 - utila@0.4.0: {} uuid@11.1.0: {} - uuid@8.3.2: {} - v8-compile-cache-lib@3.0.1: optional: true @@ -9063,17 +8418,6 @@ snapshots: dependencies: makeerror: 1.0.12 - web3-utils@1.10.4: - dependencies: - '@ethereumjs/util': 8.1.0 - bn.js: 5.2.2 - ethereum-bloom-filters: 1.2.0 - ethereum-cryptography: 2.2.1 - ethjs-unit: 0.1.6 - number-to-bn: 1.7.0 - randombytes: 2.1.0 - utf8: 3.0.0 - webidl-conversions@3.0.1: {} webidl-conversions@4.0.2: {} @@ -9175,12 +8519,7 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 - ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10): - optionalDependencies: - bufferutil: 4.0.9 - utf-8-validate: 5.0.10 - - ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10): + ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): optionalDependencies: bufferutil: 4.0.9 utf-8-validate: 5.0.10 diff --git a/src/config/config.ts b/src/config/config.ts index 0a7afdd2..3420a44f 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -14,6 +14,10 @@ export interface Config { basedCleanup: boolean fleetCount: number autoCreateProfile: boolean + /** Default faction for auto-created profiles: 'MUD' | 'ONI' | 'UST' */ + defaultFaction: 'MUD' | 'ONI' | 'UST' + /** Default character name for auto-created profiles */ + defaultCharacterName: string } bot: { telegramToken: string @@ -49,12 +53,7 @@ export interface Config { rpcEndpoint: string wsEndpoint: string fleetAddress: string - atlasMint: string marketAddress: string - toolMint: string - foodMint: string - fuelMint: string - ammoMint: string feeLimit: number } } @@ -71,6 +70,12 @@ export const config: Config = { basedCleanup: env.getOptional('BASED_CLEANUP') === 'true', fleetCount: Number(env.getOptional('FLEET_COUNT') ?? 10), autoCreateProfile: env.getOptional('AUTO_CREATE_PROFILE') === 'true', + defaultFaction: (env.getOptional('DEFAULT_FACTION') ?? 'MUD') as + | 'MUD' + | 'ONI' + | 'UST', + defaultCharacterName: + env.getOptional('DEFAULT_CHARACTER_NAME') ?? 'Fleetbot', }, bot: { telegramToken: env.get('TELEGRAM_TOKEN'), @@ -102,11 +107,6 @@ export const config: Config = { wsEndpoint: env.get('WS_ENDPOINT'), fleetAddress: env.get('FLEET_ADDRESS'), marketAddress: env.get('MARKET_ADDRESS'), - atlasMint: env.get('ATLAS_MINT'), - toolMint: env.get('TOOL_MINT'), - foodMint: env.get('FOOD_MINT'), - fuelMint: env.get('FUEL_MINT'), - ammoMint: env.get('AMMO_MINT'), feeLimit: Number(env.get('FEE_LIMIT')), }, cron: { diff --git a/src/db/entities/tx-cache.ts b/src/db/entities/tx-cache.ts index fb548754..b51536fe 100644 --- a/src/db/entities/tx-cache.ts +++ b/src/db/entities/tx-cache.ts @@ -1,11 +1,14 @@ -import type { ParsedTransactionWithMeta } from '@solana/web3.js' import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm' +/** + * Legacy R4 transaction cache entity + * Kept for database compatibility - no longer actively used + */ @Entity() export class TxCache extends BaseEntity { @PrimaryColumn({ type: 'varchar' }) id!: string @Column('jsonb', { nullable: false }) - tx!: ParsedTransactionWithMeta + tx!: Record } diff --git a/src/lib/airdrop.ts b/src/lib/airdrop.ts deleted file mode 100644 index e7fc6881..00000000 --- a/src/lib/airdrop.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import superagent from 'superagent' -import { logger } from '../logger.js' - -export const airdrop = async ( - baseUrl: string, - token: string, - address: PublicKey, -) => { - logger.info(`Airdropping to: ${address.toBase58()}`) - - const res = await superagent.get(`${baseUrl}/airdrop/sage`).query({ - token, - address: address.toBase58(), - }) - - logger.info(`Airdropped to ${address.toBase58()}: ${res.statusCode}`) -} - -export const airdropCrew = async ( - baseUrl: string, - token: string, - address: PublicKey, - amount: number, -) => { - logger.info(`Airdropping Crew to: ${address.toBase58()}`) - - const res = await superagent.get(`${baseUrl}/airdrop/crew`).query({ - token, - address: address.toBase58(), - amount, - }) - - logger.info(`Airdropped Crew to ${address.toBase58()}: ${res.statusCode}`) -} - -export const airdropSol = async ( - baseUrl: string, - token: string, - address: PublicKey, - amount: number, -) => { - logger.info(`Airdropping SOL to: ${address.toBase58()}`) - - const res = await superagent.get(`${baseUrl}/airdrop/sol`).query({ - token, - address: address.toBase58(), - amount, - }) - - logger.info(`Airdropped SOL to ${address.toBase58()}: ${res.statusCode}`) -} diff --git a/src/lib/check-atlas-transactions.ts b/src/lib/check-atlas-transactions.ts deleted file mode 100644 index c797b2b6..00000000 --- a/src/lib/check-atlas-transactions.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { getAssociatedTokenAddressSync } from '@solana/spl-token' -import { - ParsedInstruction, - ParsedTransactionWithMeta, - SignaturesForAddressOptions, -} from '@solana/web3.js' - -import dayjs from '../dayjs.js' -import { Transaction } from '../db/entities/transaction.js' -import { logger } from '../logger.js' -import { connection } from '../service/sol/index.js' -import { keyPair, resource } from '../service/wallet/index.js' - -import { ensureWallet } from './ensure-wallet.js' - -export const checkAtlasTransactions = async ( - options?: SignaturesForAddressOptions, -): Promise => { - const atlasTokenAccount = getAssociatedTokenAddressSync( - resource.atlas, - keyPair.publicKey, - true, - ) - const signatureList = await connection.getSignaturesForAddress( - atlasTokenAccount, - options, - ) - - const transactionList: ParsedTransactionWithMeta[] = [] - - for (const signature of signatureList) { - // https://docs.solana.com/developing/versioned-transactions#max-supported-transaction-version - const parsedSignature = await connection.getParsedTransaction( - signature.signature, - { - maxSupportedTransactionVersion: 0, - }, - ) - - if (parsedSignature) { - transactionList.push(parsedSignature) - } - } - - const txList: ParsedTransactionWithMeta[] = transactionList.filter( - (tx): tx is ParsedTransactionWithMeta => tx !== null, - ) - - const transferList = txList.filter( - (tx) => tx.meta?.postTokenBalances?.length === 2, - ) - - await Promise.all( - transferList.map((tx) => - tx.transaction.message.instructions.map(async (instr) => { - const instruction: ParsedInstruction = - instr as ParsedInstruction - - if ( - instruction.program === 'spl-token' && - instruction.parsed.info.mint === resource.atlas.toString() - ) { - const { info } = instruction.parsed - - const sender = info.authority ?? info.multisigAuthority - const amount = info.tokenAmount.uiAmount - const blockTime = tx.blockTime || 0 - const time = dayjs.unix(blockTime).toDate() - const [signature] = tx.transaction.signatures - - const transaction = await Transaction.findOneBy({ - signature, - }) - const log = transaction ? logger.debug : logger.info - - if (sender === keyPair.publicKey.toString()) { - const receiver = tx.meta?.postTokenBalances?.filter( - (tb) => - tb.owner?.toString() !== - keyPair.publicKey.toString(), - )[0].owner as string - - log( - `${receiver} -${amount} ATLAS ${dayjs.duration(dayjs().diff(time)).humanize(false)} ago`, - ) - const wallet = await ensureWallet(receiver) - - const tr = await Transaction.findOneBy({ - signature, - resource: 'ATLAS', - }) - - if (!tr) { - await Transaction.create({ - wallet, - amount: -amount, - signature, - time, - originalAmount: amount, - resource: 'ATLAS', - }).save() - } - } else { - log( - `${sender} +${amount} ATLAS ${dayjs.duration(dayjs().diff(time)).humanize(false)} ago`, - ) - - const wallet = await ensureWallet(sender) - - if ( - wallet.telegramId && - !wallet.authed && - dayjs().isBefore(wallet.authExpire) && - !transaction - ) { - if (wallet.authTxAmount === amount) { - wallet.authed = true - - logger.info( - `Successfully assigned ${wallet.telegramId} to ${wallet.publicKey}`, - ) - } else { - logger.warn( - `Amount mismatch, got ${amount}, expected ${wallet.authTxAmount}`, - ) - } - } - const tr = await Transaction.findOneBy({ - signature, - resource: 'ATLAS', - }) - - if (!tr) { - await Transaction.create({ - wallet, - amount, - signature, - time, - originalAmount: amount, - resource: 'ATLAS', - }).save() - } - } - } - }), - ), - ) -} diff --git a/src/lib/check-r4-transactions.ts b/src/lib/check-r4-transactions.ts deleted file mode 100644 index 3df90ad4..00000000 --- a/src/lib/check-r4-transactions.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { getAssociatedTokenAddressSync } from '@solana/spl-token' -import { - ParsedInstruction, - PublicKey, - SignaturesForAddressOptions, -} from '@solana/web3.js' -import Big from 'big.js' - -import dayjs from '../dayjs.js' -import { Transaction } from '../db/entities/transaction.js' -import { Wallet } from '../db/entities/wallet.js' -import { TxCache } from '../db/entities/tx-cache.js' -import { logger } from '../logger.js' -import { Amounts } from '../service/fleet/const/index.js' -import { AD, connection } from '../service/sol/index.js' -import { keyPair } from '../service/wallet/index.js' - -import { ensureWallet } from './ensure-wallet.js' -import { getPrice } from './get-price.js' - -type ResourceName = 'food' | 'tool' | 'fuel' | 'ammo' - -export const checkR4Transactions = async ( - wallet: Wallet, - resourceName: ResourceName, - resource: PublicKey, - prices: Amounts, - options?: SignaturesForAddressOptions, -): Promise => { - const sourceTokenAccount = getAssociatedTokenAddressSync( - resource, - new PublicKey(wallet.publicKey), - true, - ) - const destTokenAccount = getAssociatedTokenAddressSync( - resource, - new PublicKey(keyPair.publicKey), - true, - ) - - const signatureList = await connection.getSignaturesForAddress( - sourceTokenAccount, - options, - ) - - logger.info( - `${signatureList.length} transactions found for ${resourceName} on ${wallet.publicKey}`, - ) - - for (const signatureInfo of signatureList) { - const { signature } = signatureInfo - - let tx = (await TxCache.findOneBy({ id: signature }))?.tx ?? null - - if (!tx) { - // https://docs.solana.com/developing/versioned-transactions#max-supported-transaction-version - tx = await connection.getParsedTransaction(signature, { - maxSupportedTransactionVersion: 0, - }) - if (tx) { - await TxCache.create({ id: signature, tx }).save() - } - } - - if (!tx) { - throw new Error('tx is null') - } - - const balanceChangeLength = tx?.meta?.postTokenBalances?.length - - if (balanceChangeLength && balanceChangeLength >= 2) { - for (const instr of tx.transaction.message.instructions) { - const instruction: ParsedInstruction = - instr as ParsedInstruction - - if ( - instruction.program === 'spl-token' && - instruction.parsed.info.source === - sourceTokenAccount.toBase58() && - instruction.parsed.info.destination === - destTokenAccount.toBase58() - ) { - const { info } = instruction.parsed - - const sender = info.authority ?? info.multisigAuthority - const originalAmount = - info.tokenAmount?.uiAmount || info.amount - const blockTime = tx.blockTime || 0 - const time = dayjs.unix(blockTime).toDate() - - const transaction = await Transaction.findOneBy({ - signature, - resource: resourceName.toUpperCase(), - }) - const log = transaction ? logger.debug : logger.info - - const amounts: Amounts = { - ammo: Big(0), - food: Big(0), - fuel: Big(0), - tool: Big(0), - } - - amounts[resourceName] = Big(originalAmount) - - const price = getPrice(amounts, prices) - - if (sender === keyPair.publicKey.toString()) { - const receiver = tx.meta?.postTokenBalances?.filter( - (tb) => - tb.owner?.toString() !== - keyPair.publicKey.toString(), - )[0].owner as string - - log( - `${receiver} -${originalAmount} ${resourceName.toUpperCase()} worth ${price.toFixed(AD)} ATLAS ${dayjs.duration(dayjs().diff(time)).humanize(false)} ago`, - ) - - if (!transaction) { - await Transaction.create({ - wallet: await ensureWallet(receiver), - amount: price.mul(-1).toNumber(), - originalAmount: Big(originalAmount) - .mul(-1) - .toNumber(), - resource: resourceName.toUpperCase(), - signature, - time, - }).save() - } - } else { - log( - `${sender} +${originalAmount} ${resourceName.toUpperCase()} worth ${price.toFixed(AD)} ATLAS ${dayjs.duration(dayjs().diff(time)).humanize(false)} ago`, - ) - - if (!transaction) { - await Transaction.create({ - wallet: await ensureWallet(sender), - amount: price.toNumber(), - originalAmount, - resource: resourceName.toUpperCase(), - signature, - time, - }).save() - } - } - } - } - } - } -} diff --git a/src/lib/check-transactions.ts b/src/lib/check-transactions.ts deleted file mode 100644 index 8ce63cdc..00000000 --- a/src/lib/check-transactions.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { LessThan, MoreThan } from 'typeorm' - -import dayjs from '../dayjs.js' -import { Transaction } from '../db/entities/transaction.js' -import { Wallet } from '../db/entities/wallet.js' -import { logger } from '../logger.js' -import { - getBalanceAtlas, - getResourceBalances, - getResourcePrices, -} from '../service/gm/index.js' -import { AD } from '../service/sol/index.js' -import { keyPair, resource } from '../service/wallet/index.js' - -import { checkAtlasTransactions } from './check-atlas-transactions.js' -import { checkR4Transactions } from './check-r4-transactions.js' - -export const checkTransactions = async (): Promise => { - const atlasBalance = await getBalanceAtlas(keyPair.publicKey) - const prices = getResourcePrices() - const resources = await getResourceBalances(keyPair.publicKey) - - logger.info(`ATLAS balance: ${atlasBalance.toFixed(AD)}`) - logger.info( - `FOOD balance: ${resources.food} (${resources.food.mul(prices.food).toFixed(AD)} ATLAS)`, - ) - logger.info( - `TOOL balance: ${resources.tool} (${resources.tool.mul(prices.tool).toFixed(AD)} ATLAS)`, - ) - logger.info( - `FUEL balance: ${resources.fuel} (${resources.fuel.mul(prices.fuel).toFixed(AD)} ATLAS)`, - ) - logger.info( - `AMMO balance: ${resources.ammo} (${resources.ammo.mul(prices.ammo).toFixed(AD)} ATLAS)`, - ) - - const total = atlasBalance - .add(resources.food.mul(prices.food)) - .add(resources.ammo.mul(prices.ammo)) - .add(resources.fuel.mul(prices.fuel)) - .add(resources.tool.mul(prices.tool)) - - const [[lastInTx], [lastOutTx]] = await Promise.all([ - Transaction.find({ - where: { amount: MoreThan(0) }, - order: { time: 'DESC' }, - take: 1, - }), - Transaction.find({ - where: { amount: LessThan(0) }, - order: { time: 'DESC' }, - take: 1, - }), - ]) - - let getSigOptions - - if (lastInTx && lastOutTx) { - const until = dayjs(lastInTx.time).isBefore(lastOutTx.time) - ? lastInTx.signature - : lastOutTx.signature - - getSigOptions = { until } - } - - logger.info(`Total balance: ${total.toFixed(AD)} ATLAS`) - - await checkAtlasTransactions(getSigOptions) - - const wallets = await Wallet.findBy({ enabled: true }) - - for (const wallet of wallets) { - await checkR4Transactions( - wallet, - 'tool', - resource.tool, - prices, - getSigOptions, - ) - - await checkR4Transactions( - wallet, - 'ammo', - resource.ammo, - prices, - getSigOptions, - ) - - await checkR4Transactions( - wallet, - 'food', - resource.food, - prices, - getSigOptions, - ) - - await checkR4Transactions( - wallet, - 'fuel', - resource.fuel, - prices, - getSigOptions, - ) - } -} diff --git a/src/lib/const/fleet-refill.ts b/src/lib/const/fleet-refill.ts deleted file mode 100644 index 14bd89f4..00000000 --- a/src/lib/const/fleet-refill.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ShipStakingInfo } from '@staratlas/factory' -import Big from 'big.js' - -import { Amounts } from '../../service/fleet/const/index.js' - -export interface FleetRefill { - shipStakingInfo: ShipStakingInfo - amount: Amounts - price: Big -} diff --git a/src/lib/const/index.ts b/src/lib/const/index.ts deleted file mode 100644 index e644cd82..00000000 --- a/src/lib/const/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './fleet-refill.js' -export * from './max.js' diff --git a/src/lib/const/max.ts b/src/lib/const/max.ts deleted file mode 100644 index 3fd992bd..00000000 --- a/src/lib/const/max.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Big from 'big.js' - -export const max = (a: Big, b: Big): Big => (a.gte(b) ? a : b) diff --git a/src/lib/ensure-wallet.ts b/src/lib/ensure-wallet.ts deleted file mode 100644 index ebe69bc5..00000000 --- a/src/lib/ensure-wallet.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Wallet } from '../db/entities/wallet.js' - -export const ensureWallet = async (publicKey: string): Promise => { - await Wallet.upsert( - { publicKey: publicKey.toString() }, - { conflictPaths: ['publicKey'], skipUpdateIfNoValuesChanged: true }, - ) - - return Wallet.findOneOrFail({ where: { publicKey: publicKey.toString() } }) -} diff --git a/src/lib/fleet-depletion-info.ts b/src/lib/fleet-depletion-info.ts deleted file mode 100644 index cc5fb1a8..00000000 --- a/src/lib/fleet-depletion-info.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { getScoreVarsShipInfo, ShipStakingInfo } from '@staratlas/factory' - -import dayjs from '../dayjs.js' -import { getFleetRemainingResources } from '../service/fleet/index.js' -import { connection, fleetProgram } from '../service/sol/index.js' - -type FleetDepletionInfo = { - seconds: number - human: string -} - -export const fleetDepletionInfo = async ( - shipStakingInfo: ShipStakingInfo, -): Promise => { - const info = await getScoreVarsShipInfo( - connection, - fleetProgram, - shipStakingInfo.shipMint, - ) - const remainingResources = getFleetRemainingResources(info, shipStakingInfo) - - const secondsLeft = Math.min( - remainingResources.food.secondsLeft, - remainingResources.ammo.secondsLeft, - remainingResources.fuel.secondsLeft, - remainingResources.tool.secondsLeft, - ) - - return { - seconds: secondsLeft, - human: dayjs.duration(secondsLeft, 'seconds').humanize(false), - } -} diff --git a/src/lib/get-price.ts b/src/lib/get-price.ts deleted file mode 100644 index 27d0b7e8..00000000 --- a/src/lib/get-price.ts +++ /dev/null @@ -1,17 +0,0 @@ -import Big from 'big.js' - -import { Amounts } from '../service/fleet/const/index.js' -import { getResourcePrices } from '../service/gm/index.js' - -export const getPrice = (amount: Amounts, price?: Amounts): Big => { - const p: Amounts = price ? price : getResourcePrices() - const totalFuelPrice = amount.fuel.mul(p.fuel) - const totalFoodPrice = amount.food.mul(p.food) - const totalAmmoPrice = amount.ammo.mul(p.ammo) - const totalToolPrice = amount.tool.mul(p.tool) - - return totalFuelPrice - .add(totalFoodPrice) - .add(totalAmmoPrice) - .add(totalToolPrice) -} diff --git a/src/lib/index.ts b/src/lib/index.ts deleted file mode 100644 index b4fb8da0..00000000 --- a/src/lib/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './check-transactions.js' -export * from './const/index.js' -export * from './fleet-depletion-info.js' -export * from './get-price.js' -export * from './refill.js' -export * from './refill-player.js' -export * from './refill-strategy/index.js' -export * from './stock-resources.js' -export * from './telegram/index.js' diff --git a/src/lib/refill-player.ts b/src/lib/refill-player.ts deleted file mode 100644 index cf960f63..00000000 --- a/src/lib/refill-player.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { getAllFleetsForUserPublicKey } from '@staratlas/factory' -import Big from 'big.js' - -import { Sentry } from '../sentry.js' - -import dayjs from '../dayjs.js' -import { Refill } from '../db/entities/refill.js' -import { Wallet } from '../db/entities/wallet.js' -import { logger } from '../logger.js' -import { refillFleet } from '../service/fleet/index.js' -import { AD, connection, fleetProgram } from '../service/sol/index.js' - -import { FleetRefill } from './const/index.js' -import { fleetDepletionInfo } from './fleet-depletion-info.js' -import { RefillStrategy } from './refill-strategy/index.js' -import { getShipName } from './stock-resources.js' - -export const refillPlayer = async ( - player: PublicKey, - strategy: RefillStrategy, -): Promise => { - const wallet = await Wallet.findOneByOrFail({ - publicKey: player.toString(), - }) - const shipStakingInfos = await getAllFleetsForUserPublicKey( - connection, - player, - fleetProgram, - ) - - if (shipStakingInfos.length === 0) { - logger.warn(`${player.toString()} has no staked ships`) - - return [] - } - - await Promise.all( - shipStakingInfos.map(async (shipStakingInfo) => - logger.info( - `${player.toString()} ${await getShipName(shipStakingInfo)} depleting in ${(await fleetDepletionInfo(shipStakingInfo)).human}`, - ), - ), - ) - - const refills: FleetRefill[] = await strategy(shipStakingInfos) - - const totalCost = refills.reduce((acc, curr) => acc.add(curr.price), Big(0)) - const playerBalance = await wallet.getBalance() - - if (playerBalance.lt(totalCost)) { - logger.warn( - `${player.toString()} credits insufficient! Has ${playerBalance.toFixed(AD)}, need ${totalCost.toFixed(AD)}`, - ) - await Wallet.update( - { publicKey: wallet.publicKey }, - { nextRefill: dayjs().add(1, 'day').toDate() }, - ) - - return [] - } - - const fleetRefills = await Promise.all( - refills - .filter( - (refill) => - refill.amount.tool.gt(0) || - refill.amount.fuel.gt(0) || - refill.amount.ammo.gt(0) || - refill.amount.food.gt(0), - ) - .map(async (refill) => { - const shipName = await getShipName(refill.shipStakingInfo) - - logger.warn( - `${player.toString()}: Refilling ${JSON.stringify(refill.amount)} for ${shipName} costs ${refill.price.toFixed(AD)} ATLAS`, - ) - - try { - const tx = await refillFleet( - player, - refill.shipStakingInfo, - refill.amount, - ) - - 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( - `Error refilling fleet: ${(e as Error).message}`, - ) - - return null - } - }), - ) - - const depletionInfos = await Promise.all( - shipStakingInfos.map(async (shipStakingInfo) => { - const depletionInfo = await fleetDepletionInfo(shipStakingInfo) - - logger.info( - `${player.toString()} ${await getShipName(shipStakingInfo)} depleting in ${depletionInfo.human}`, - ) - - return depletionInfo - }), - ) - - const secondsLeft = depletionInfos.reduce( - (acc, cur) => Math.min(cur.seconds, acc), - Number.MAX_SAFE_INTEGER, - ) - - const nextRefill = dayjs() - .add( - Big(secondsLeft).minus(Big(secondsLeft).div(4)).toNumber(), - 'second', - ) - .toDate() - - await Wallet.update({ publicKey: wallet.publicKey }, { nextRefill }) - - return fleetRefills.filter((f): f is Refill[] => f !== null).flat() -} diff --git a/src/lib/refill-strategy/full-refill-strategy.ts b/src/lib/refill-strategy/full-refill-strategy.ts deleted file mode 100644 index d9a4d7ed..00000000 --- a/src/lib/refill-strategy/full-refill-strategy.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { getScoreVarsShipInfo } from '@staratlas/factory' -import Big from 'big.js' - -import { getFleetRemainingResources } from '../../service/fleet/index.js' -import { Amounts } from '../../service/fleet/const/index.js' -import { connection, fleetProgram } from '../../service/sol/index.js' -import { FleetRefill } from '../const/index.js' -import { getPrice } from '../get-price.js' - -import { RefillStrategy } from './refill-strategy.js' - -export const fullRefillStrategy: RefillStrategy = (shipStakingInfos) => - Promise.all( - shipStakingInfos.map(async (shipStakingInfo) => { - const info = await getScoreVarsShipInfo( - connection, - fleetProgram, - shipStakingInfo.shipMint, - ) - - const remainingResources = getFleetRemainingResources( - info, - shipStakingInfo, - ) - - const amount: Amounts = { - food: Big(remainingResources.food.maxUnits) - .minus(remainingResources.food.unitsLeft) - .round(0, Big.roundDown), - ammo: Big(remainingResources.ammo.maxUnits) - .minus(remainingResources.ammo.unitsLeft) - .round(0, Big.roundDown), - fuel: Big(remainingResources.fuel.maxUnits) - .minus(remainingResources.fuel.unitsLeft) - .round(0, Big.roundDown), - tool: Big(remainingResources.tool.maxUnits) - .minus(remainingResources.tool.unitsLeft) - .round(0, Big.roundDown), - } - - return { - shipStakingInfo, - amount, - price: getPrice(amount), - } as FleetRefill - }), - ) diff --git a/src/lib/refill-strategy/index.ts b/src/lib/refill-strategy/index.ts deleted file mode 100644 index f0883129..00000000 --- a/src/lib/refill-strategy/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './full-refill-strategy.js' -export * from './optimal-refill-strategy.js' -export * from './refill-strategy.js' diff --git a/src/lib/refill-strategy/optimal-refill-strategy.ts b/src/lib/refill-strategy/optimal-refill-strategy.ts deleted file mode 100644 index f40abb4d..00000000 --- a/src/lib/refill-strategy/optimal-refill-strategy.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { getScoreVarsShipInfo } from '@staratlas/factory' -import Big from 'big.js' - -import { getFleetRemainingResources } from '../../service/fleet/index.js' -import { Amounts } from '../../service/fleet/const/index.js' -import { connection, fleetProgram } from '../../service/sol/index.js' -import { FleetRefill, max } from '../const/index.js' -import { getPrice } from '../get-price.js' - -import { RefillStrategy } from './refill-strategy.js' - -export const optimalRefillStrategy: RefillStrategy = async ( - shipStakingInfos, -) => { - const targets = await Promise.all( - shipStakingInfos.map(async (shipStakingInfo) => { - const info = await getScoreVarsShipInfo( - connection, - fleetProgram, - shipStakingInfo.shipMint, - ) - const remainingResources = getFleetRemainingResources( - info, - shipStakingInfo, - ) - - return Math.min( - remainingResources.food.maxSeconds, - remainingResources.ammo.maxSeconds, - remainingResources.fuel.maxSeconds, - remainingResources.tool.maxSeconds, - ) - }), - ) - const target = targets.reduce( - (acc, cur) => Math.min(acc, cur), - Number.MAX_SAFE_INTEGER, - ) - - return Promise.all( - shipStakingInfos.map(async (shipStakingInfo) => { - const info = await getScoreVarsShipInfo( - connection, - fleetProgram, - shipStakingInfo.shipMint, - ) - const remainingResources = getFleetRemainingResources( - info, - shipStakingInfo, - ) - - const amount: Amounts = { - food: max( - Big(target) - .minus(remainingResources.food.secondsLeft) - .mul(remainingResources.food.burnRatePerFleet) - .round(), - Big(0), - ), - ammo: max( - Big(target) - .minus(remainingResources.ammo.secondsLeft) - .mul(remainingResources.ammo.burnRatePerFleet) - .round(), - Big(0), - ), - fuel: max( - Big(target) - .minus(remainingResources.fuel.secondsLeft) - .mul(remainingResources.fuel.burnRatePerFleet) - .round(), - Big(0), - ), - tool: max( - Big(target) - .minus(remainingResources.tool.secondsLeft) - .mul(remainingResources.tool.burnRatePerFleet) - .round(), - Big(0), - ), - } - - return { - shipStakingInfo, - amount, - price: getPrice(amount), - } as FleetRefill - }), - ) -} diff --git a/src/lib/refill-strategy/refill-strategy.ts b/src/lib/refill-strategy/refill-strategy.ts deleted file mode 100644 index 4dff1ce6..00000000 --- a/src/lib/refill-strategy/refill-strategy.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ShipStakingInfo } from '@staratlas/factory' - -import { FleetRefill } from '../const/index.js' - -export type RefillStrategy = ( - shipStakingInfos: ShipStakingInfo[], -) => Promise diff --git a/src/lib/refill.ts b/src/lib/refill.ts deleted file mode 100644 index e5570f47..00000000 --- a/src/lib/refill.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { getAllFleetsForUserPublicKey } from '@staratlas/factory' -import Big from 'big.js' - -import dayjs from '../dayjs.js' -import { Wallet } from '../db/entities/wallet.js' -import { logger } from '../logger.js' -import { getResourcePrices } from '../service/gm/index.js' -import { AD, connection, fleetProgram } from '../service/sol/index.js' - -import { max } from './const/index.js' -import { refillPlayer } from './refill-player.js' -import { optimalRefillStrategy } from './refill-strategy/index.js' -import { getDailyBurnRate } from './stock-resources.js' - -export const refill = async (): Promise => { - const players = await Wallet.findBy({ enabled: true }) - - for (const player of players) { - if (dayjs().isAfter(player.nextRefill)) { - await refillPlayer( - new PublicKey(player.publicKey), - optimalRefillStrategy, - ) - } - - const fleets = await getAllFleetsForUserPublicKey( - connection, - new PublicKey(player.publicKey), - fleetProgram, - ) - const burnRate = await getDailyBurnRate(fleets) - const price = getResourcePrices() - const balance = await player.getBalance() - - const burnPerDay = max( - burnRate.food - .mul(price.food) - .add(burnRate.fuel.mul(price.fuel)) - .add(burnRate.tool.mul(price.tool)) - .add(burnRate.fuel.mul(price.ammo)), - Big(0.00000001), - ) - - const balanceTime = balance.div(burnPerDay) - - await player.reload() - - logger.info('-----------------------------------------------------') - logger.info( - `${player.publicKey} next refill in ${dayjs.duration(dayjs().diff(player.nextRefill, 'second'), 'second').humanize()}`, - ) - logger.info( - `Balance: ${balance.toFixed(AD)} ATLAS / Burn ${burnPerDay.toFixed(AD)} ATLAS per day / Credit for ${dayjs.duration(balanceTime.toNumber(), 'day').humanize(false)}`, - ) - logger.info(`Total Tipped: ${(await player.totalTipped()).toFixed(AD)}`) - logger.info('-----------------------------------------------------') - } -} diff --git a/src/lib/stock-resources.ts b/src/lib/stock-resources.ts deleted file mode 100644 index 33cc3ca9..00000000 --- a/src/lib/stock-resources.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { AnchorProvider, Idl, Program } from '@coral-xyz/anchor' -import { Connection, PublicKey } from '@solana/web3.js' -import { - ShipStakingInfo, - getScoreIDL, - getScoreVarsShipInfo, - getShipStakingAccount, -} from '@staratlas/factory' -import Big from 'big.js' -import superagent from 'superagent' - -import dayjs from '../dayjs.js' -import { ShipInfo } from '../db/entities/ship-info.js' -import { Wallet } from '../db/entities/wallet.js' -import { logger } from '../logger.js' -import { - getFleetRemainingResources, - getTimePass, -} from '../service/fleet/index.js' -import { Amounts } from '../service/fleet/const/index.js' -import { - buyResources, - getResourceBalances, - getResourcePrices, -} from '../service/gm/index.js' -import { AD, connection, fleetProgram } from '../service/sol/index.js' -import { keyPair } from '../service/wallet/index.js' - -/** - * Returns a list of player deployed fleets to the SCORE program - * - * @param conn - web3.Connection object - * @param playerPublicKey - Player's public key - * @param programId - Deployed program ID for the SCORE program - * @returns - [Ship Staking Account Infos] - */ -const getAllFleetsForUserPublicKey = async ( - conn: Connection, - playerPublicKey: PublicKey, - programId: PublicKey, -): Promise => { - const idl = getScoreIDL(programId) - // @ts-expect-error 123 - const provider = new AnchorProvider(conn, null, null) - const program = new Program(idl, programId, provider) - - const shipsRegistered = await program.account.scoreVarsShip.all() - - const playerShipStakingAccounts = [] - - for (const ship of shipsRegistered) { - const [playerShipStakingAccount] = await getShipStakingAccount( - programId, - ship.account.shipMint as PublicKey, - playerPublicKey, - ) - - playerShipStakingAccounts.push(playerShipStakingAccount) - } - - const playerFleets: ShipStakingInfo[] = [] - - for (const acc of playerShipStakingAccounts) { - try { - const fleet = await program.account.shipStaking.fetchNullable(acc) - - if (fleet) { - playerFleets.push(fleet) - } - } catch (e) { - logger.error(e) - } - } - logger.info( - `Found ${playerFleets.length} fleets for ${playerPublicKey.toString()}`, - ) - - return playerFleets -} - -export const getShipName = async ( - shipStakingInfo: ShipStakingInfo, -): Promise => { - const mint = shipStakingInfo.shipMint.toString() - let shipInfo = await ShipInfo.findOneBy({ mint }) - - if (!shipInfo) { - try { - const shipNftInfo = await superagent.get( - `https://galaxy.staratlas.com/nfts/${mint}`, - ) - const urlSplit = shipNftInfo.body.image.slice(0, -4).split('/') - const imageName = urlSplit[urlSplit.length - 1] - - shipInfo = await ShipInfo.create({ - mint, - name: shipNftInfo.body.name, - imageName, - }).save() - } catch { - return 'n/a' - } - } - - return shipInfo.name -} - -export const getPendingRewards = async ( - fleets: ShipStakingInfo[], -): Promise => { - const fleetInfos = await Promise.all( - fleets.map(async (fleet) => { - const shipInfo = await getScoreVarsShipInfo( - connection, - fleetProgram, - fleet.shipMint, - ) - - return { - info: shipInfo, - fleet, - } - }), - ) - - return fleetInfos - .reduce((sum, fleetInfo) => { - const { fleet } = fleetInfo - const { info } = fleetInfo - const timePass = getTimePass(fleet) - const pendingReward = - Number(fleet.shipQuantityInEscrow) * - (Number(fleet.totalTimeStaked) - - Number(fleet.stakedTimePaid) + - timePass) * - Number(info.rewardRatePerSecond) - - return sum.add(pendingReward) - }, Big(0)) - .div(100000000) -} - -export const getDailyBurnRate = async ( - fleets: ShipStakingInfo[], -): Promise => { - const dayInSeconds = 86400 - - const resourcePerDay: Amounts = { - food: Big(0), - tool: Big(0), - ammo: Big(0), - fuel: Big(0), - } - - await Promise.all( - fleets.map(async (shipStakingInfo) => { - const info = await getScoreVarsShipInfo( - connection, - fleetProgram, - shipStakingInfo.shipMint, - ) - const remaining = getFleetRemainingResources(info, shipStakingInfo) - - resourcePerDay.food = resourcePerDay.food.add( - Big(remaining.food.burnRatePerFleet).mul(dayInSeconds), - ) - resourcePerDay.tool = resourcePerDay.tool.add( - Big(remaining.tool.burnRatePerFleet).mul(dayInSeconds), - ) - resourcePerDay.ammo = resourcePerDay.ammo.add( - Big(remaining.ammo.burnRatePerFleet).mul(dayInSeconds), - ) - resourcePerDay.fuel = resourcePerDay.fuel.add( - Big(remaining.fuel.burnRatePerFleet).mul(dayInSeconds), - ) - }), - ) - - return resourcePerDay -} - -const logStats = (balance: Amounts, dailyBurn: Amounts) => { - logger.info( - `TOOL balance: ${balance.tool}, burning ${dailyBurn.tool.toFixed(0)} per day, last for ${dailyBurn.tool.eq(0) ? 0 : dayjs.duration(balance.tool.div(dailyBurn.tool).toNumber(), 'day').humanize()}`, - ) - logger.info( - `AMMO balance: ${balance.ammo}, burning ${dailyBurn.ammo.toFixed(0)} per day, last for ${dailyBurn.ammo.eq(0) ? 0 : dayjs.duration(balance.ammo.div(dailyBurn.ammo).toNumber(), 'day').humanize()}`, - ) - logger.info( - `FOOD balance: ${balance.food}, burning ${dailyBurn.food.toFixed(0)} per day, last for ${dailyBurn.food.eq(0) ? 0 : dayjs.duration(balance.food.div(dailyBurn.food).toNumber(), 'day').humanize()}`, - ) - logger.info( - `FUEL balance: ${balance.fuel}, burning ${dailyBurn.fuel.toFixed(0)} per day, last for ${dailyBurn.fuel.eq(0) ? 0 : dayjs.duration(balance.fuel.div(dailyBurn.fuel).toNumber(), 'day').humanize()}`, - ) -} - -export const stockResources = async (): Promise => { - const wallets = await Wallet.findBy({ enabled: true }) - - const dailyBurn = ( - await Promise.all( - wallets.map(async (wallet) => - getDailyBurnRate( - await getAllFleetsForUserPublicKey( - connection, - new PublicKey(wallet.publicKey), - fleetProgram, - ), - ), - ), - ) - ).reduce( - (acc, cur) => ({ - tool: acc.tool.add(cur.tool), - ammo: acc.ammo.add(cur.ammo), - food: acc.food.add(cur.food), - fuel: acc.fuel.add(cur.fuel), - }), - { ammo: Big(0), food: Big(0), fuel: Big(0), tool: Big(0) } as Amounts, - ) - - const balance = await getResourceBalances(keyPair.publicKey) - - logStats(balance, dailyBurn) - - const amount: Amounts = { - food: balance.food.lt(dailyBurn.food.mul(14)) - ? dailyBurn.food.mul(21) - : Big(0), - ammo: balance.ammo.lt(dailyBurn.ammo.mul(14)) - ? dailyBurn.ammo.mul(21) - : Big(0), - fuel: balance.fuel.lt(dailyBurn.fuel.mul(14)) - ? dailyBurn.fuel.mul(21) - : Big(0), - tool: balance.tool.lt(dailyBurn.tool.mul(14)) - ? dailyBurn.tool.mul(21) - : Big(0), - } - - if ( - amount.food.gt(0) || - amount.ammo.gt(0) || - amount.fuel.gt(0) || - amount.tool.gt(0) - ) { - const price = getResourcePrices() - const totalFuelPrice = amount.fuel.mul(price.fuel) - const totalFoodPrice = amount.food.mul(price.food) - const totalAmmoPrice = amount.ammo.mul(price.ammo) - const totalToolPrice = amount.tool.mul(price.tool) - const totalPrice = totalFuelPrice - .add(totalFoodPrice) - .add(totalAmmoPrice) - .add(totalToolPrice) - - logger.info( - `Buying Resources...${JSON.stringify(amount)} for ${totalPrice.toFixed(AD)} ATLAS`, - ) - - const txs = await buyResources(amount) - - txs.forEach((tx) => logger.info(tx)) - logStats(await getResourceBalances(keyPair.publicKey), dailyBurn) - } -} diff --git a/src/lib/telegram/commands/disable-notify.ts b/src/lib/telegram/commands/disable-notify.ts deleted file mode 100644 index abadf592..00000000 --- a/src/lib/telegram/commands/disable-notify.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Telegraf } from 'telegraf' - -import { ContextMessageUpdate } from '../context-message-update.js' -import { unauthorized } from '../response/index.js' - -export const disableNotify = (bot: Telegraf): void => { - bot.command(['disable-notify'], async (ctx) => { - await ctx.persistentChatAction('typing', async () => { - if (!ctx.user || !ctx.authed) { - await unauthorized(ctx) - - return - } - - ctx.user.notify = false - await ctx.user.save() - await ctx.reply( - 'I will stay silent unless there is something urgent happening.', - ) - }) - }) -} diff --git a/src/lib/telegram/commands/disable.ts b/src/lib/telegram/commands/disable.ts deleted file mode 100644 index 0404631f..00000000 --- a/src/lib/telegram/commands/disable.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Telegraf } from 'telegraf' - -import { ContextMessageUpdate } from '../context-message-update.js' -import { unauthorized } from '../response/index.js' - -export const disable = (bot: Telegraf): void => { - bot.command(['disable'], async (ctx) => { - await ctx.persistentChatAction('typing', async () => { - if (!ctx.user || !ctx.authed) { - await unauthorized(ctx) - - return - } - - ctx.user.enabled = false - await ctx.user.save() - await ctx.reply('Refilling is disabled!') - }) - }) -} diff --git a/src/lib/telegram/commands/enable-notify.ts b/src/lib/telegram/commands/enable-notify.ts deleted file mode 100644 index 456b4b6f..00000000 --- a/src/lib/telegram/commands/enable-notify.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Telegraf } from 'telegraf' - -import { ContextMessageUpdate } from '../context-message-update.js' -import { unauthorized } from '../response/index.js' - -export const enableNotify = (bot: Telegraf): void => { - bot.command(['enable-notify'], async (ctx) => { - await ctx.persistentChatAction('typing', async () => { - if (!ctx.user || !ctx.authed) { - await unauthorized(ctx) - - return - } - - ctx.user.notify = true - await ctx.user.save() - await ctx.reply('I will notify you after I refilled your fleets.') - }) - }) -} diff --git a/src/lib/telegram/commands/enable.ts b/src/lib/telegram/commands/enable.ts deleted file mode 100644 index df10c613..00000000 --- a/src/lib/telegram/commands/enable.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Telegraf } from 'telegraf' - -import { ContextMessageUpdate } from '../context-message-update.js' -import { unauthorized } from '../response/index.js' - -export const enable = (bot: Telegraf): void => { - bot.command(['enable'], async (ctx) => { - await ctx.persistentChatAction('typing', async () => { - if (!ctx.user || !ctx.authed) { - await unauthorized(ctx) - - return - } - - ctx.user.enabled = true - await ctx.user.save() - await ctx.reply('Refilling is enabled!') - }) - }) -} diff --git a/src/lib/telegram/commands/help.ts b/src/lib/telegram/commands/help.ts deleted file mode 100644 index 74810be8..00000000 --- a/src/lib/telegram/commands/help.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Telegraf } from 'telegraf' - -import { config } from '../../../config/index.js' -import { ContextMessageUpdate } from '../context-message-update.js' - -export const help = (bot: Telegraf): void => { - bot.command(['start', 'help'], async (ctx: ContextMessageUpdate) => { - await ctx.persistentChatAction('typing', async () => { - await ctx.replyWithHTML(` -Welcome to fleetbot! -I can automatically refill your Star Atlas enlisted fleets. - -Prerequisites: -- Your wallet has enlisted Ships to the Faction Fleet. You can do that on https://play.staratlas.com -- You have refilled your ships at least once. This creates escrow accounts for the supplies. Unfortunately, I cannot do that for you. - -Last but not least, you have to send me some ATLAS which I will use as your credits for the supplies. - -You can send it to one of the following addresses: -- ${config.user.address1} -- ${config.user.address2} - -As soon as I receive the ATLAS, I will start refilling your ships in regular intervals. I use an optimized refilling strategy, so that you are always covered. -However, to save you on commission, I will not fully load the ship. You can always refill your fleet by yourself. I will only jump in if you are running low. - -Since it is now possible to stake your claim stakes and generate R4, I decided to accept this as a payment method as well. Please first set everything up -by using ATLAS as explained above. As soon as everything is set, you can also send me your R4 (tool, fuel, ammo, food). I am not keeping track of individual R4 account balances. -Instead, each R4 you send me will credit your internal ATLAS balance by the current market price. - -Withdrawals are only possible in ATLAS. - -For the hard work of refilling all the fleets, I keep 15% of the ATLAS I spend for myself. -You can change the commission at any time to any value. - -Bare in mind that I check the ledger in 10 minute intervals. So your deposits will be recognized by me a little bit delayed. Dont' panic. :) -If something does not work as expected, you can always use the /support command to get a human fixing the issues. - -Commands: - -/help Prints this message -/support Talk to a human -/verify {publicKey} Connect your Telegram Account to a wallet - -Commands for verified users: - -/enable Enable automatic fleet refilling -/disable Disable automatic fleet refilling -/refill Trigger an immediate refill -/stats Query some stats -/transactions Query transactions associated to your wallet -/refills Query refill activity -/withdraw {amount} Withdraw ATLAS back to your wallet -/logout Disconnects Telegram Account from wallet -/tip Set or query tip setting (default 15%) - `) - }) - }) -} diff --git a/src/lib/telegram/commands/index.ts b/src/lib/telegram/commands/index.ts deleted file mode 100644 index 29d8f607..00000000 --- a/src/lib/telegram/commands/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -export * from './disable.js' -export * from './disable-notify.js' -export * from './enable.js' -export * from './enable-notify.js' -export * from './help.js' -export * from './logout.js' -export * from './meme/index.js' -export * from './refill.js' -export * from './refills.js' -export * from './stats.js' -export * from './support.js' -export * from './tip.js' -export * from './transactions.js' -export * from './verify.js' -export * from './withdraw.js' diff --git a/src/lib/telegram/commands/logout.ts b/src/lib/telegram/commands/logout.ts deleted file mode 100644 index 43bf618c..00000000 --- a/src/lib/telegram/commands/logout.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Telegraf } from 'telegraf' - -import { Wallet } from '../../../db/entities/wallet.js' -import { ContextMessageUpdate } from '../context-message-update.js' -import { unauthorized } from '../response/index.js' - -export const logout = (bot: Telegraf): void => { - bot.command(['logout'], async (ctx) => { - await ctx.persistentChatAction('typing', async () => { - if (!ctx.user || !ctx.authed) { - await unauthorized(ctx) - - return - } - - if (ctx.user.enabled) { - await ctx.reply( - 'Your wallet is currently enabled, toggle with /disable command', - ) - - return - } - - ctx.user.authed = false - ctx.user.telegramId = null - ctx.user.authTxAmount = null - ctx.user.authExpire = null - - await Wallet.save(ctx.user) - - ctx.reply('Logged out successfully') - }) - }) -} diff --git a/src/lib/telegram/commands/meme/index.ts b/src/lib/telegram/commands/meme/index.ts deleted file mode 100644 index 9664b7fc..00000000 --- a/src/lib/telegram/commands/meme/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './kitten.js' -export * from './porn.js' -export * from './wen.js' diff --git a/src/lib/telegram/commands/meme/kitten.ts b/src/lib/telegram/commands/meme/kitten.ts deleted file mode 100644 index 5303511a..00000000 --- a/src/lib/telegram/commands/meme/kitten.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { faker } from '@faker-js/faker' -import { Telegraf } from 'telegraf' - -import { logger } from '../../../../logger.js' -import { ContextMessageUpdate } from '../../context-message-update.js' - -export const kitten = (bot: Telegraf): void => { - bot.command(['kitten'], async (ctx) => { - await ctx.persistentChatAction('upload_photo', async () => { - const x = faker.number.int({ min: 128, max: 2048 }) - - try { - await ctx.replyWithPhoto(`https://placekitten.com/${x}/${x}`) - } catch (e: any) { - logger.error(`Cannot send Photo: ${e.message}`) - await ctx.reply('Meow!') - } - }) - }) -} diff --git a/src/lib/telegram/commands/meme/porn.ts b/src/lib/telegram/commands/meme/porn.ts deleted file mode 100644 index 83d621bf..00000000 --- a/src/lib/telegram/commands/meme/porn.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Telegraf } from 'telegraf' - -import { ShipInfo } from '../../../../db/entities/ship-info.js' -import { logger } from '../../../../logger.js' -import { ContextMessageUpdate } from '../../context-message-update.js' - -export const porn = (bot: Telegraf): void => { - bot.command(['porn'], async (ctx) => { - await ctx.persistentChatAction('upload_photo', async () => { - try { - const randomShip = await ShipInfo.getRepository() - .createQueryBuilder('ship_info') - .select() - .orderBy('RANDOM()') - .getOne() - - const imageName = randomShip?.imageName || '' - const imageUrl = `https://storage.googleapis.com/nft-assets/items/${imageName}.jpg` - - await ctx.replyWithPhoto(imageUrl) - } catch (e: any) { - logger.error(`Cannot send Photo: ${e.message}`) - await ctx.reply(':-*') - } - }) - }) -} diff --git a/src/lib/telegram/commands/meme/wen.ts b/src/lib/telegram/commands/meme/wen.ts deleted file mode 100644 index f7e2bef2..00000000 --- a/src/lib/telegram/commands/meme/wen.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Telegraf } from 'telegraf' - -import { logger } from '../../../../logger.js' -import { ContextMessageUpdate } from '../../context-message-update.js' - -export const wen = (bot: Telegraf): void => { - bot.command(['wen'], async (ctx) => { - await ctx.persistentChatAction('upload_photo', async () => { - try { - await ctx.replyWithAnimation( - 'http://2damnfunny.com/wp-content/uploads/2013/06/Very-Thoon-Husky-Dog-Meme-Gif.gif', - ) - } catch (e: any) { - logger.error(`Cannot send Photo: ${e.message}`) - await ctx.reply('Thoon!') - } - }) - }) -} diff --git a/src/lib/telegram/commands/refill.ts b/src/lib/telegram/commands/refill.ts deleted file mode 100644 index 3f41f580..00000000 --- a/src/lib/telegram/commands/refill.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { Telegraf } from 'telegraf' - -import { AD } from '../../../service/sol/index.js' -import { refillPlayer } from '../../refill-player.js' -import { - fullRefillStrategy, - optimalRefillStrategy, -} from '../../refill-strategy/index.js' -import { ContextMessageUpdate } from '../context-message-update.js' -import { unauthorized } from '../response/index.js' - -export const refill = (bot: Telegraf): void => { - bot.command(['refill'], async (ctx: ContextMessageUpdate) => { - await ctx.persistentChatAction('typing', async () => { - if (!ctx.user || !ctx.authed) { - await unauthorized(ctx) - - return - } - - if (!ctx.user.enabled) { - await ctx.reply( - 'Your wallet is currently disabled, toggle with /enable command', - ) - - return - } - - const strategy = - ctx.params.length === 1 && ctx.params[0] === 'full' - ? fullRefillStrategy - : optimalRefillStrategy - const strategyName = - ctx.params.length === 1 && ctx.params[0] === 'full' - ? 'full' - : 'optimal' - - await ctx.reply( - 'Just saying... There should not be any reason to do this now, but as you wish, I am going to refill your fleets, ser!', - ) - await ctx.reply( - `Using ${strategyName} refill strategy. Give me a moment please...`, - ) - const userRefills = await refillPlayer( - new PublicKey(ctx.user.publicKey), - strategy, - ) - - for (const userRefill of userRefills) { - await ctx.replyWithHTML(` -Signature: click -Time: ${userRefill.time.toLocaleDateString()} ${userRefill.time.toLocaleTimeString()} -Fleet: ${userRefill.fleet} -Food: ${userRefill.food} -Tool: ${userRefill.tool} -Fuel: ${userRefill.fuel} -Ammo ${userRefill.ammo} -Price: ${userRefill.price.toFixed(AD)} ATLAS`) - } - - if (strategyName === 'optimal') { - await ctx.reply( - "Pro Tip: Trigger a full refill with '/refill full'", - ) - } - }) - }) -} diff --git a/src/lib/telegram/commands/refills.ts b/src/lib/telegram/commands/refills.ts deleted file mode 100644 index ffe77f46..00000000 --- a/src/lib/telegram/commands/refills.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Telegraf } from 'telegraf' - -import { Refill } from '../../../db/entities/refill.js' -import { AD } from '../../../service/sol/index.js' -import { ContextMessageUpdate } from '../context-message-update.js' -import { unauthorized } from '../response/index.js' - -export const refills = (bot: Telegraf): void => { - bot.command(['refills'], async (ctx: ContextMessageUpdate) => { - await ctx.persistentChatAction('typing', async () => { - if (!ctx.user || !ctx.authed) { - await unauthorized(ctx) - - return - } - - if (!ctx.user.enabled) { - await ctx.reply( - 'Your wallet is currently disabled, toggle with /enable command', - ) - - return - } - - const take = 10 - - await ctx.reply(`Showing the last ${take} refills`) - const userRefills = await Refill.find({ - where: { walletPublicKey: ctx.user.publicKey }, - take, - order: { time: 'DESC' }, - }) - - for (const refill of userRefills) { - await ctx.replyWithHTML(` -Signature: click -Time: ${refill.time.toLocaleDateString()} ${refill.time.toLocaleTimeString()} -Fleet: ${refill.fleet} -Food: ${refill.food} -Tool: ${refill.tool} -Fuel: ${refill.fuel} -Ammo ${refill.ammo} -Price: ${refill.price.toFixed(AD)} ATLAS`) - } - }) - }) -} diff --git a/src/lib/telegram/commands/stats.ts b/src/lib/telegram/commands/stats.ts deleted file mode 100644 index 0e7fefde..00000000 --- a/src/lib/telegram/commands/stats.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { getAllFleetsForUserPublicKey } from '@staratlas/factory' -import Big from 'big.js' -import { Telegraf } from 'telegraf' -import { Between, MoreThanOrEqual } from 'typeorm' - -import dayjs from '../../../dayjs.js' -import { Refill } from '../../../db/entities/refill.js' -import { getResourcePrices } from '../../../service/gm/index.js' -import { AD, connection, fleetProgram } from '../../../service/sol/index.js' -import { getDailyBurnRate, getPendingRewards } from '../../stock-resources.js' -import { ContextMessageUpdate } from '../context-message-update.js' -import { unauthorized } from '../response/index.js' - -export const stats = (bot: Telegraf): void => { - bot.command(['stats'], async (ctx: ContextMessageUpdate) => { - if (!ctx.user || !ctx.authed) { - await unauthorized(ctx) - - return - } - - if (!ctx.user.enabled) { - await ctx.reply( - 'Your wallet is currently disabled, toggle with /enable command', - ) - - return - } - - await ctx.reply('Gathering statistics, hold on...') - await ctx.persistentChatAction('typing', async () => { - const player = ctx.user - - if (player) { - const fleets = await getAllFleetsForUserPublicKey( - connection, - new PublicKey(player.publicKey), - fleetProgram, - ) - const [burnRate, price, playerBalance, pendingRewards] = - await Promise.all([ - getDailyBurnRate(fleets), - getResourcePrices(), - player.getBalance(), - getPendingRewards(fleets), - ]) - const burnPerDay = burnRate.food - .mul(price.food) - .add(burnRate.fuel.mul(price.fuel)) - .add(burnRate.tool.mul(price.tool)) - .add(burnRate.fuel.mul(price.ammo)) - const balanceTime = burnPerDay.gt(0) - ? playerBalance.div(burnPerDay) - : Big(0) - - const diffToNextRefill = dayjs().diff( - player.nextRefill, - 'second', - ) - - const todayRefills = await Refill.findBy({ - walletPublicKey: player.publicKey, - time: MoreThanOrEqual(dayjs().startOf('day').toDate()), - }) - const burnToday = todayRefills.reduce( - (curr, acc) => curr.add(acc.price), - Big(0), - ) - - const yesterdayRefills = await Refill.findBy({ - walletPublicKey: player.publicKey, - time: Between( - dayjs().subtract(1, 'day').startOf('day').toDate(), - dayjs().subtract(1, 'day').endOf('day').toDate(), - ), - }) - const burnYesterday = yesterdayRefills.reduce( - (curr, acc) => curr.add(acc.price), - Big(0), - ) - - const refills7d = await Refill.findBy({ - walletPublicKey: player.publicKey, - time: MoreThanOrEqual( - dayjs().subtract(7, 'day').startOf('day').toDate(), - ), - }) - const burn7d = refills7d.reduce( - (curr, acc) => curr.add(acc.price), - Big(0), - ) - - const refills30d = await Refill.findBy({ - walletPublicKey: player.publicKey, - time: MoreThanOrEqual( - dayjs().subtract(30, 'day').startOf('day').toDate(), - ), - }) - const burn30d = refills30d.reduce( - (curr, acc) => curr.add(acc.price), - Big(0), - ) - - const avg = (b: Big[]) => - b.reduce((c, a) => c.add(a), Big(0)).div(b.length) - - const burnAvg7 = burn7d.div(7) - const burnAvg30 = burn30d.div(30) - - const burnAvg = avg([burnAvg7, burnAvg30]) - - const drift = burnAvg.sub(burnPerDay) - - await ctx.replyWithMarkdownV2( - ` -*Next refill in:* ${dayjs.duration(diffToNextRefill, 'second').humanize()} -*Balance:* ${playerBalance.toFixed(AD)} ATLAS -*Burn \\(estimate\\):* ${burnPerDay.toFixed(AD)} ATLAS per day -*Burn \\(today\\):* ${burnToday.toFixed(AD)} ATLAS \\(${todayRefills.length} refills\\) -*Burn \\(yesterday\\):* ${burnYesterday.toFixed(AD)} ATLAS \\(${yesterdayRefills.length} refills\\) -*Burn \\(7d\\):* ${burn7d.toFixed(AD)} ATLAS \\(${refills7d.length} refills\\) \\[avg.: ${burnAvg7.toFixed(AD)} / day\\] -*Burn \\(30d\\):* ${burn30d.toFixed(AD)} ATLAS \\(${refills30d.length} refills\\) \\[avg.: ${burnAvg30.toFixed(AD)} / day\\] -*Drift:* ${drift.toFixed(AD).toString().replace('-', '\\-')} ATLAS -*Pending Rewards:* ${pendingRewards.toFixed(AD)} ATLAS -*Credit for:* ${dayjs.duration(balanceTime.toNumber(), 'day').humanize(false)} -*Current Tips:* ${player.tip * 100} %`.replace(/\./g, '\\.'), - ) - } - }) - }) -} diff --git a/src/lib/telegram/commands/support.ts b/src/lib/telegram/commands/support.ts deleted file mode 100644 index dedb1afe..00000000 --- a/src/lib/telegram/commands/support.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Telegraf } from 'telegraf' - -import { config } from '../../../config/index.js' -import { ContextMessageUpdate } from '../context-message-update.js' - -export const support = (bot: Telegraf): void => { - bot.command(['support'], async (ctx) => { - await ctx.persistentChatAction('typing', async () => { - await ctx.reply( - `I really don't know why you would need that, but just in case you want to talk to a human, please contact ${config.bot.owner}`, - ) - }) - }) -} diff --git a/src/lib/telegram/commands/tip.ts b/src/lib/telegram/commands/tip.ts deleted file mode 100644 index bb5a24e4..00000000 --- a/src/lib/telegram/commands/tip.ts +++ /dev/null @@ -1,42 +0,0 @@ -import Big from 'big.js' -import { Telegraf } from 'telegraf' - -import { ContextMessageUpdate } from '../context-message-update.js' -import { unauthorized } from '../response/index.js' - -export const tip = (bot: Telegraf): void => { - bot.command(['tip'], async (ctx) => { - await ctx.persistentChatAction('typing', async () => { - if (!ctx.user || !ctx.authed) { - await unauthorized(ctx) - - return - } - - if (ctx.params.length !== 1) { - await ctx.reply( - `Tip is currently set to ${Big(ctx.user.tip).mul(100).toFixed()} %`, - ) - await ctx.reply('To update setting, use /tip {percentage}') - - return - } - - let [tipPercent] = ctx.params - - tipPercent = tipPercent.replace('%', '') - try { - ctx.user.tip = Big(tipPercent).div(100).abs().toNumber() - await ctx.user.save() - } catch { - await ctx.reply('Tip amount must be be a positive number!') - - return - } - - await ctx.reply( - `Tip set to ${Big(ctx.user.tip).mul(100).toFixed()} %`, - ) - }) - }) -} diff --git a/src/lib/telegram/commands/transactions.ts b/src/lib/telegram/commands/transactions.ts deleted file mode 100644 index 105fa5ea..00000000 --- a/src/lib/telegram/commands/transactions.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Telegraf } from 'telegraf' - -import { Transaction } from '../../../db/entities/transaction.js' -import { AD } from '../../../service/sol/index.js' -import { ContextMessageUpdate } from '../context-message-update.js' -import { unauthorized } from '../response/index.js' - -export const transactions = (bot: Telegraf): void => { - bot.command(['transactions'], async (ctx: ContextMessageUpdate) => { - await ctx.persistentChatAction('typing', async () => { - if (!ctx.user || !ctx.authed) { - await unauthorized(ctx) - - return - } - - if (!ctx.user.enabled) { - await ctx.reply( - 'Your wallet is currently disabled, toggle with /enable command', - ) - - return - } - - const take = 10 - - await ctx.reply(`Showing the last ${take} transactions`) - const userTransactions = await Transaction.find({ - where: { walletPublicKey: ctx.user.publicKey }, - take, - order: { time: 'DESC' }, - }) - - for (const transaction of userTransactions) { - await ctx.replyWithHTML(` -Signature: click -Time: ${transaction.time.toLocaleDateString()} ${transaction.time.toLocaleTimeString()} -Amount: ${transaction.amount.toFixed(AD)} ATLAS`) - } - }) - }) -} diff --git a/src/lib/telegram/commands/verify.ts b/src/lib/telegram/commands/verify.ts deleted file mode 100644 index 9c602358..00000000 --- a/src/lib/telegram/commands/verify.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { faker } from '@faker-js/faker' -import dayjs from 'dayjs' -import { Telegraf } from 'telegraf' - -import { Wallet } from '../../../db/entities/wallet.js' -import { ContextMessageUpdate } from '../context-message-update.js' -import { - alreadyRegistered, - authPending, - unknownWallet, - wrongParamCount, -} from '../response/index.js' - -export const verify = (bot: Telegraf): void => { - bot.command(['verify'], async (ctx) => { - await ctx.persistentChatAction('typing', async () => { - if (ctx.user) { - if (ctx.authed) { - await alreadyRegistered(ctx) - - return - } - - const telegramId = ctx.from.id - const wallet = await Wallet.findOneBy({ telegramId }) - - if (wallet && dayjs().isBefore(wallet.authExpire)) { - await authPending(ctx, wallet) - - return - } - } - - if (ctx.params.length !== 1) { - await wrongParamCount(ctx, 'Usage: /verify {publicKey}') - - return - } - const [publicKey] = ctx.params - const wallet = await Wallet.findOneBy({ publicKey }) - - if (!wallet) { - await unknownWallet(ctx) - - return - } - - ctx.reply( - "Alright! To verify this wallet belongs to you, I need you to send a small amount of ATLAS to me. Don't worry. I will add this to your balance of course.", - ) - wallet.telegramId = ctx.from.id - wallet.authExpire = dayjs().add(1, 'hour').toDate() - wallet.authTxAmount = faker.number.float({ - min: 0.1, - max: 0.999, - fractionDigits: 3, - }) - await wallet.save() - await authPending(ctx, wallet) - ctx.reply( - 'This may take a moment. You can check the status with /verify command', - ) - }) - }) -} diff --git a/src/lib/telegram/commands/withdraw.ts b/src/lib/telegram/commands/withdraw.ts deleted file mode 100644 index 87a10d94..00000000 --- a/src/lib/telegram/commands/withdraw.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import Big from 'big.js' -import { Telegraf } from 'telegraf' - -import dayjs from '../../../dayjs.js' -import { Transaction } from '../../../db/entities/transaction.js' -import { sendAtlas } from '../../../service/gm/index.js' -import { AD } from '../../../service/sol/index.js' -import { ContextMessageUpdate } from '../context-message-update.js' -import { unauthorized, wrongParamCount } from '../response/index.js' - -export const withdraw = (bot: Telegraf): void => { - bot.command(['withdraw'], async (ctx) => { - await ctx.persistentChatAction('typing', async () => { - if (!ctx.user || !ctx.authed) { - await unauthorized(ctx) - - return - } - - if (ctx.params.length !== 1) { - await wrongParamCount(ctx, 'Usage: /withdraw {amount}|all') - - return - } - - const wallet = ctx.user - const userBalance = await wallet.getBalance() - const withdrawAmount = - ctx.params[0] === 'all' ? userBalance : Big(ctx.params[0]).abs() - - if (withdrawAmount.gt(userBalance)) { - await ctx.reply( - `Amount ${withdrawAmount.toFixed(AD)} exceeds user balance ${userBalance.toFixed(AD)}`, - ) - - return - } - - await ctx.reply( - `Sending ${withdrawAmount} ATLAS to ${ctx.user.publicKey}`, - ) - const signatures = await sendAtlas( - new PublicKey(ctx.user.publicKey), - withdrawAmount.toNumber(), - ) - - const amount = -withdrawAmount - - 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/lib/telegram/context-message-update.ts b/src/lib/telegram/context-message-update.ts deleted file mode 100644 index ee768fb2..00000000 --- a/src/lib/telegram/context-message-update.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Context } from 'telegraf' -import { Update } from 'telegraf/types' -import { Wallet } from '../../db/entities/wallet' - -export interface ContextMessageUpdate extends Context { - user: Wallet | null - authed: boolean - params: string[] -} diff --git a/src/lib/telegram/index.ts b/src/lib/telegram/index.ts deleted file mode 100644 index 3d0ae021..00000000 --- a/src/lib/telegram/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './context-message-update.js' -export * from './telegram-bot.js' diff --git a/src/lib/telegram/middleware/auth.ts b/src/lib/telegram/middleware/auth.ts deleted file mode 100644 index d41c5d15..00000000 --- a/src/lib/telegram/middleware/auth.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Middleware } from 'telegraf' -import { Wallet } from '../../../db/entities/wallet.js' - -import { ContextMessageUpdate } from '../context-message-update.js' - -export const auth: Middleware = async ( - ctx: ContextMessageUpdate, - next: any, -) => { - if (ctx.from?.id) { - const wallet = await Wallet.findOneBy({ telegramId: ctx.from.id }) - - ctx.authed = wallet?.authed || false - ctx.user = wallet - } - - await next() -} diff --git a/src/lib/telegram/middleware/index.ts b/src/lib/telegram/middleware/index.ts deleted file mode 100644 index 9401a10c..00000000 --- a/src/lib/telegram/middleware/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './auth.js' diff --git a/src/lib/telegram/middleware/params.ts b/src/lib/telegram/middleware/params.ts deleted file mode 100644 index 4e6c4a04..00000000 --- a/src/lib/telegram/middleware/params.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Middleware } from 'telegraf' - -import { ContextMessageUpdate } from '../context-message-update.js' - -export const params: Middleware = ( - ctx: ContextMessageUpdate, - next: any, -) => { - if (ctx.message && 'text' in ctx.message) { - ctx.params = ctx.message.text.split(' ').splice(1) - } - - return next() -} diff --git a/src/lib/telegram/response/already-registered.ts b/src/lib/telegram/response/already-registered.ts deleted file mode 100644 index 019e88f5..00000000 --- a/src/lib/telegram/response/already-registered.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Message } from 'telegraf/types' - -import { ContextMessageUpdate } from '../context-message-update.js' - -export const alreadyRegistered = ( - ctx: ContextMessageUpdate, -): Promise => - ctx.reply(` -Looks like you are already registered and ready to go! -`) diff --git a/src/lib/telegram/response/auth-pending.ts b/src/lib/telegram/response/auth-pending.ts deleted file mode 100644 index 215b3cec..00000000 --- a/src/lib/telegram/response/auth-pending.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Message } from 'telegraf/types' - -import dayjs from '../../../dayjs.js' -import { Wallet } from '../../../db/entities/wallet' -import { ContextMessageUpdate } from '../context-message-update.js' - -export const authPending = ( - ctx: ContextMessageUpdate, - wallet: Wallet, -): Promise => { - const authExpire = dayjs().diff(wallet.authExpire, 'second') - - return ctx.reply(` -Please send exactly ${wallet.authTxAmount} ATLAS to fleetbot.sol. -You have ${dayjs.duration(authExpire, 'seconds').humanize(false)} left! -`) -} diff --git a/src/lib/telegram/response/index.ts b/src/lib/telegram/response/index.ts deleted file mode 100644 index 74ca5ee7..00000000 --- a/src/lib/telegram/response/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './already-registered.js' -export * from './auth-pending.js' -export * from './unauthorized.js' -export * from './unknown-wallet.js' -export * from './wrong-param-count.js' diff --git a/src/lib/telegram/response/unauthorized.ts b/src/lib/telegram/response/unauthorized.ts deleted file mode 100644 index e18e371c..00000000 --- a/src/lib/telegram/response/unauthorized.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Message } from 'telegraf/types' - -import { ContextMessageUpdate } from '../context-message-update.js' - -export const unauthorized = ( - ctx: ContextMessageUpdate, -): Promise => - ctx.reply(` -Huh, who are you? Please authorize first! -`) diff --git a/src/lib/telegram/response/unknown-wallet.ts b/src/lib/telegram/response/unknown-wallet.ts deleted file mode 100644 index a9dc9f84..00000000 --- a/src/lib/telegram/response/unknown-wallet.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Message } from 'telegraf/types' - -import { ContextMessageUpdate } from '../context-message-update.js' - -export const unknownWallet = ( - ctx: ContextMessageUpdate, -): Promise => - ctx.reply(` -Could not find wallet. Please send some ATLAS first! -`) diff --git a/src/lib/telegram/response/wrong-param-count.ts b/src/lib/telegram/response/wrong-param-count.ts deleted file mode 100644 index e3cd080a..00000000 --- a/src/lib/telegram/response/wrong-param-count.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Message } from 'telegraf/types' - -import { ContextMessageUpdate } from '../context-message-update.js' - -export const wrongParamCount = ( - ctx: ContextMessageUpdate, - message: string, -): Promise => ctx.reply(message) diff --git a/src/lib/telegram/telegram-bot.ts b/src/lib/telegram/telegram-bot.ts deleted file mode 100644 index ab14b8d1..00000000 --- a/src/lib/telegram/telegram-bot.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Telegraf } from 'telegraf' - -import { config } from '../../config/index.js' - -import * as commands from './commands/index.js' -import { ContextMessageUpdate } from './context-message-update.js' -import { auth } from './middleware/index.js' -import { params } from './middleware/params.js' - -const telegramBot: Telegraf = new Telegraf( - config.bot.telegramToken, - { handlerTimeout: 360_000 }, -) - -telegramBot.use(auth) -telegramBot.use(params) - -for (const command of Object.values(commands)) { - command(telegramBot) -} - -process.once('SIGINT', () => telegramBot.stop('SIGINT')) -process.once('SIGTERM', () => telegramBot.stop('SIGTERM')) - -export { telegramBot } diff --git a/src/main/airdrop/airdrop.ts b/src/main/airdrop/airdrop.ts index a61832d8..631a5fc6 100644 --- a/src/main/airdrop/airdrop.ts +++ b/src/main/airdrop/airdrop.ts @@ -1,24 +1,46 @@ -import { Keypair, PublicKey } from '@solana/web3.js' -import { readAllFromRPC } from '@staratlas/data-source' -import { PlayerProfile } from '@staratlas/player-profile' +/** + * Airdrop module for C4 devnet testing + * Requests airdrops from Star Atlas devnet faucet + */ +import { address, type Address, type KeyPairSigner } from '@solana/kit' import { CronJob } from 'cron' import * as fs from 'node:fs' import path from 'node:path' +import superagent from 'superagent' import { config } from '../../config/index.js' -import { airdrop } from '../../lib/airdrop.js' import { logger } from '../../logger.js' -import { connection } from '../../service/sol/index.js' -import { loadKeypairFromFile } from '../../service/wallet' -import { programs } from '../basedbot/lib/programs.js' -import { sageGame } from '../basedbot/lib/sage/state/game.js' -import { Faction } from '../basedbot/lib/util/galaxy-sectors-data' -import { createAndInitializeCharacter } from '../basedbot/lib/util/profile' +import { loadKeypairFromFile } from '../../service/wallet/init-keypair.js' +import { getPlayerContext } from '../basedbot/lib/sage/state/user-account.js' let airdropCronJob: CronJob | undefined -const findKeyFiles = (dirPath: string): Map => { - let keyMap = new Map() +/** + * Request airdrop from Star Atlas devnet faucet + */ +const requestAirdrop = async ( + baseUrl: string, + token: string, + address: Address, +): Promise => { + logger.info(`Airdropping to: ${address}`) + + const res = await superagent.get(`${baseUrl}/airdrop/sage`).query({ + token, + address, + }) + + logger.info(`Airdropped to ${address}: ${res.statusCode}`) +} + +/** + * Find key files in a directory + */ +const findKeyFiles = async ( + dirPath: string, +): Promise> => { + const keyMap = new Map() + if (!fs.existsSync(dirPath)) { logger.warn(`${dirPath} does not exist`) return keyMap @@ -31,18 +53,21 @@ const findKeyFiles = (dirPath: string): Map => { const fullPath = path.join(dirPath, entry.name) if (entry.isDirectory()) { - keyMap = new Map([...keyMap, ...findKeyFiles(fullPath)]) + const subKeys = await findKeyFiles(fullPath) + for (const [addr, signer] of subKeys) { + keyMap.set(addr, signer) + } } else if (entry.isFile() && entry.name.endsWith('.json')) { try { - const keyPair = loadKeypairFromFile(fullPath) - keyMap.set(keyPair.publicKey.toBase58(), keyPair) + const signer = await loadKeypairFromFile(fullPath) + keyMap.set(signer.address, signer) } catch { - logger.warn(`${entry.name} is not a Key`) + logger.warn(`${entry.name} is not a valid key file`) } } } } catch (error) { - console.error(`Error reading directory: ${dirPath}`, error) + logger.error(`Error reading directory: ${dirPath}`, error) } return keyMap @@ -63,61 +88,54 @@ export const stop = async (): Promise => { } } -const airdropOrCreateProfile = async (user: PublicKey): Promise => { - const myProfiles = await readAllFromRPC( - connection, - programs.playerProfile, - PlayerProfile, - 'processed', - [ - { - memcmp: { - offset: PlayerProfile.MIN_DATA_SIZE + 2, - bytes: user.toBase58(), - }, - }, - ], - ) - const game = await sageGame() +/** + * Airdrop to a wallet, creating profile if needed + */ +const airdropOrCreateProfile = async ( + walletAddress: Address, + keys: Map, +): Promise => { + const signer = keys.get(walletAddress) + + if (!signer) { + logger.warn(`No key found for ${walletAddress}, skipping`) + return + } + + try { + // Check if player has a profile + const player = await getPlayerContext(walletAddress, signer) - if (myProfiles.length > 0) { logger.info( - `Airdropping to ${user.toBase58()} with profile ${myProfiles[0].key.toBase58()}`, + `Airdropping to ${walletAddress} with profile ${player.profile.key}`, ) - await airdrop( + await requestAirdrop( config.app.airdropUrl, config.app.airdropToken, - new PublicKey(user), + walletAddress, + ) + } catch { + // No profile - skip (profile creation not yet implemented for C4) + logger.warn( + `No profile for ${walletAddress}, skipping. Create profile via Star Atlas app first.`, ) - } else { - logger.info(`Creating profile for ${user.toBase58()}`) - const keyDir = process.env.KEY_DIR || '/tmp/keys' - logger.info(`Looking for keys in ${path.dirname(keyDir)}`) - const keys = findKeyFiles(keyDir) - - const keyPair = keys.get(user.toBase58()) - - if (keyPair) { - const pubStr = keyPair.publicKey.toBase58() - await createAndInitializeCharacter( - game, - `${pubStr.slice(0, 4)}...${pubStr.slice(-4)}`, - Faction.ONI, - keyPair, - ) - } else { - logger.warn( - `No key found for ${user.toBase58()}, cannot create profile`, - ) - } } } const airdropTick = async (): Promise => { + const keyDir = process.env.KEY_DIR || '/tmp/keys' + logger.info(`Looking for keys in ${keyDir}`) + const keys = await findKeyFiles(keyDir) + await Promise.all( - config.app.airdropWallets.map((w) => - airdropOrCreateProfile(new PublicKey(w)), - ), + config.app.airdropWallets.map((w) => { + try { + return airdropOrCreateProfile(address(w), keys) + } catch { + logger.warn(`Invalid wallet address: ${w}, skipping`) + return Promise.resolve() + } + }), ) } diff --git a/src/main/basedbot/basedbot.ts b/src/main/basedbot/basedbot.ts index fc32ac81..7170af81 100644 --- a/src/main/basedbot/basedbot.ts +++ b/src/main/basedbot/basedbot.ts @@ -1,29 +1,27 @@ -import { - getAssociatedTokenAddressSync, - TOKEN_PROGRAM_ID, -} from '@solana/spl-token' -import { PublicKey } from '@solana/web3.js' -import { - getParsedTokenAccountsByOwner, - ixReturnsToIxs, -} from '@staratlas/data-source' -import { Fleet, Game, Starbase } from '@staratlas/sage' -import BN from 'bn.js' -import { config } from '../../config/index.js' +import type { Address } from '@solana/kit' +import type { Fleet, Game } from '@staratlas/dev-sage' +import { config } from '../../config/index.js' import { logger } from '../../logger.js' import { Sentry } from '../../sentry.js' import { sleep } from '../../service/sleep.js' -import { connection } from '../../service/sol/index.js' -import { sendAndConfirmInstructions } from '../../service/sol/send-and-confirm-tx.js' -import { keyPair } from '../../service/wallet/index.js' +import { keyPairPromise } from '../../service/wallet/index.js' import { StrategyConfig } from './fleet-strategies/strategy-config.js' import { createInfoStrategy } from './fsm/info.js' -import { programs } from './lib/programs.js' -import { createFleet, FleetShip } from './lib/sage/act/create-fleet.js' -import { depositCargo } from './lib/sage/act/deposit-cargo.js' +import { fetchRegionTracker } from './lib/account-fetcher.js' +import { createFleet, type FleetShip } from './lib/sage/act/create-fleet.js' +import { + depositCargo, + getWalletCargoBalance, + type CargoDeposit, +} from './lib/sage/act/deposit-cargo.js' +import { + listAllResearchNodes, + getUnlockedNodeIds, + unlockResearchNode, + hasUnlockedNode, +} from './lib/sage/act/unlock-research.js' import { ensureShips } from './lib/sage/act/deposit-ship.js' -import { getCargoStatsDefinition } from './lib/sage/state/cargo-stats-definition.js' import { sageGame } from './lib/sage/state/game.js' import { settleFleet } from './lib/sage/state/settle-fleet.js' import { getStarbasePlayer } from './lib/sage/state/starbase-player.js' @@ -36,10 +34,7 @@ import { } from './lib/sage/state/user-fleets.js' import { getMapContext, WorldMap } from './lib/sage/state/world-map.js' import { getName } from './lib/sage/util.js' -import { - getCleanPodsByStarbasePlayerAccounts, - getPodCleanupInstructions, -} from './lib/util/pod-cleanup.js' +import type { AccountWithKey } from './lib/types.js' import { getFleetStrategy } from './fleet-strategies/get-fleet-strategy.js' export const create = async (): Promise => { @@ -73,66 +68,105 @@ const applyStrategy = ( return strategy.apply(fleetInfo) } +/** + * Get token balance for an account + */ export const getTokenBalance = async ( - account: PublicKey, - mint: PublicKey, -): Promise => { - const allTokenAccounts = await getParsedTokenAccountsByOwner( - connection, - account, - TOKEN_PROGRAM_ID, - ) + account: Address, + mint: Address, +): Promise => { + try { + const { findAssociatedTokenPda, TOKEN_PROGRAM_ADDRESS } = await import( + '@solana-program/token' + ) + const { connection } = await import('../../service/sol/index.js') - const sourceTokenAccount = getAssociatedTokenAddressSync( - mint, - account, - true, - ) - const [mintTokenAccount] = allTokenAccounts.filter((it) => - it.address.equals(sourceTokenAccount), - ) + // Derive ATA address + const [ata] = await findAssociatedTokenPda({ + owner: account, + mint, + tokenProgram: TOKEN_PROGRAM_ADDRESS, + }) - if (!mintTokenAccount) { - logger.debug('Token account not found, assuming empty balance.') - } + // Fetch token account balance via RPC + const result = await connection.rpc.getTokenAccountBalance(ata).send() - return new BN(mintTokenAccount ? mintTokenAccount.amount.toString() : 0) + return BigInt(result.value.amount) + } catch (e) { + // Account doesn't exist or has no balance + logger.debug(`Token balance query failed: ${(e as Error).message}`) + return 0n + } } -const importR4 = async (player: Player, game: Game): Promise => { - await Promise.all( - [ - game.data.mints.food, - game.data.mints.ammo, - game.data.mints.fuel, - game.data.mints.repairKit, - ].map(async (mint) => { - const amountAtOrigin = await getTokenBalance( - player.signer.publicKey(), - mint, - ) +/** + * Import R4 resources (fuel, ammo) from wallet to home starbase + * Gets cargo IDs from game's cargo definitions + */ +const importR4 = async ( + player: Player, + game: AccountWithKey, + fleets: Array>, +): Promise => { + // Get fuel and ammo cargo IDs from first fleet (if available) + // These are consistent across all fleets in the game + if (fleets.length === 0) { + logger.debug('No fleets available to determine R4 cargo IDs') + return + } - if (amountAtOrigin.gtn(0)) { - logger.info( - `Importing R4 for ${mint.toBase58()}: ${amountAtOrigin}`, - ) + const fleet = fleets[0] + const fuelCargoId = fleet.fuelTank?.cargoId as number | undefined + const ammoCargoId = fleet.ammoBank?.cargoId as number | undefined - await depositCargo( - player, - game, - player.homeStarbase, - mint, - amountAtOrigin, - ) - } - }), - ) + if (!fuelCargoId && !ammoCargoId) { + logger.debug('Could not determine fuel/ammo cargo IDs from fleet') + return + } + + // Get mints for these cargo IDs + const r4Mints: Address[] = [] + for (const [mint, id] of game.cargoDefinitions.mintToId) { + if (id === fuelCargoId || id === ammoCargoId) { + r4Mints.push(mint as Address) + } + } + + logger.debug(`Checking wallet for R4 (${r4Mints.length} mints)`) + + const deposits: CargoDeposit[] = [] + + for (const mint of r4Mints) { + const walletBalance = await getWalletCargoBalance( + player.signer.address, + mint, + ) + if (walletBalance > 0n) { + const cargoId = game.cargoDefinitions.mintToId.get(mint) + deposits.push({ mint, amount: walletBalance }) + logger.debug( + `Found ${walletBalance} of cargoId ${cargoId} (${mint}) in wallet`, + ) + } + } + + if (deposits.length > 0) { + logger.info( + `Importing R4: ${deposits.map((d) => `${d.amount} of ${d.mint}`).join(', ')}`, + ) + await depositCargo(player, game, player.homeSystem, deposits) + } else { + logger.debug('No R4 in wallet to import') + } } +/** + * Ensure required fleets exist + */ const ensureFleets = async ( player: Player, - game: Game, - fleets: Array, + game: AccountWithKey, + fleets: Array>, fleetStrategies: StrategyConfig, ): Promise => { const existingFleets = fleets.map(getName) @@ -142,33 +176,57 @@ const ensureFleets = async ( if (neededFleets.length > 0) { logger.info('Creating fleets:', neededFleets) + + // Debug: List research nodes and character's unlocked nodes + const unlockedIds = getUnlockedNodeIds(player.character) + logger.info( + `Character has unlocked research nodes: [${unlockedIds.join(', ')}]`, + ) + listAllResearchNodes(game) + + // Unlock God Mode (node 9999) if not already unlocked + const GOD_MODE_NODE_ID = 9999 + if (hasUnlockedNode(player.character, GOD_MODE_NODE_ID)) { + logger.info('God Mode already unlocked') + } else { + logger.info( + `Attempting to unlock God Mode (node ${GOD_MODE_NODE_ID})...`, + ) + const starbasePlayer = await getStarbasePlayer( + player, + player.homeSystem, + ) + if (starbasePlayer) { + await unlockResearchNode( + player, + game, + player.homeSystem, + starbasePlayer, + GOD_MODE_NODE_ID, + ) + } else { + logger.warn('No StarbasePlayer - cannot unlock God Mode') + } + } } - const neededShips = new Map() + // Aggregate ships by escrow id + const neededShips = new Map() neededFleets.forEach((fleetName) => { const fleetStrategy = fleetStrategies.map.get(fleetName)! - fleetStrategy.fleet?.forEach((fleetShip) => { - const curr = neededShips.get(fleetShip.shipMint.toBase58()) ?? 0 - - neededShips.set( - fleetShip.shipMint.toBase58(), - curr + fleetShip.count, - ) + fleetStrategy.fleet?.forEach((fleetShip: FleetShip) => { + const curr = neededShips.get(fleetShip.id) ?? 0 + neededShips.set(fleetShip.id, curr + fleetShip.amount) }) }) - const shipMints = Array.from(neededShips.keys()) - .map((mint) => [ - { - count: neededShips.get(mint) ?? 0, - shipMint: new PublicKey(mint), - } as FleetShip, - ]) - .flat() + const shipsToEnsure: FleetShip[] = Array.from(neededShips.entries()).map( + ([id, amount]) => ({ id, amount }), + ) - await ensureShips(player, game, player.homeStarbase, shipMints) + await ensureShips(player, game, player.homeSystem, shipsToEnsure) await Promise.all( neededFleets.map((fleetName) => { @@ -176,14 +234,13 @@ const ensureFleets = async ( if (!fleetStrategy.fleet) { logger.info('Cannot ensure fleet without config.') - return Promise.resolve() } return createFleet( player, game, - player.homeStarbase, + player.homeSystem, fleetStrategy.fleet!, fleetName, ) @@ -191,54 +248,16 @@ 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( - game.data.cargo.statsDefinition, - ) - - 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, - ) - - logger.info(`Pod Cleanup Instructions: ${ixs.length}`) - - await sendAndConfirmInstructions([keyPair])( - await ixReturnsToIxs(ixs, player.signer), - ) -} - const basedbot = async (botConfig: BotConfig) => { logger.info( '-------------------------------------------------------------------------------------', ) - const { player, map } = botConfig - const [fleets, disbandedFleets, game] = await Promise.all([ + const { player } = botConfig + const [fleets, disbandedFleets, game, regionTracker] = await Promise.all([ getUserFleets(player), getUserDisbandedFleets(player), sageGame(), + fetchRegionTracker(), ]) if (disbandedFleets.length > 0) { @@ -247,42 +266,61 @@ const basedbot = async (botConfig: BotConfig) => { ) } + // getFleetInfo now takes only fleet arg const fleetInfos = ( - await Promise.all(fleets.map((f) => getFleetInfo(f, player, map))) + await Promise.all(fleets.map((f) => getFleetInfo(f))) ).filter((fn) => config.app.fleetFilter ? fn.fleetName.includes(config.app.fleetFilter) : true, ) - await cleanupPods(player, game, player.homeStarbase) - await Promise.all([ - importR4(player, game), + importR4(player, game, fleets), ensureFleets(player, game, fleets, botConfig.fleetStrategies), ]) await Promise.all( - fleetInfos.map((fleetInfo) => settleFleet(fleetInfo, player, game)), + fleetInfos.map((fleetInfo) => + settleFleet(fleetInfo, player, game, regionTracker), + ), ) await Promise.all( fleetInfos.map((fleetInfo) => applyStrategy(fleetInfo, botConfig.fleetStrategies), ), ) - // for (const fleetInfo of fleetInfos) { - // await applyStrategy(fleetInfo, botConfig.fleetStrategies) - // } logger.info( '-------------------------------------------------------------------------------------', ) } export const start = async (): Promise => { - const player = await getPlayerContext(keyPair.publicKey, keyPair) + // C4: Use signer.address instead of keypair.publicKey + const keyPair = await keyPairPromise + const player = await getPlayerContext(keyPair.address, keyPair) + logger.debug('Got player context, getting game...') const game = await sageGame() - const map = await getMapContext(game) - const fleetStrategies = getFleetStrategy(map, player, game) + logger.debug('Got game, getting map context...') + // C4: getMapContext takes no args + const map = await getMapContext() + logger.debug('Got map context, fetching existing fleets...') + // Fetch existing fleets and their info (including locations) to create strategies + const existingFleets = await getUserFleets(player) + const existingFleetInfos = await Promise.all( + existingFleets.map((f) => getFleetInfo(f)), + ) + logger.info( + `Found ${existingFleetInfos.length} existing fleets with locations`, + ) + logger.debug('Getting fleet strategy...') + const fleetStrategies = getFleetStrategy( + map, + player, + game, + existingFleetInfos, + ) + logger.debug('Got fleet strategies, starting main loop...') while (true) { try { diff --git a/src/main/basedbot/fleet-strategies/atlasnet-fc-strategy.ts b/src/main/basedbot/fleet-strategies/atlasnet-fc-strategy.ts index 89c07441..2535f4ef 100644 --- a/src/main/basedbot/fleet-strategies/atlasnet-fc-strategy.ts +++ b/src/main/basedbot/fleet-strategies/atlasnet-fc-strategy.ts @@ -1,16 +1,18 @@ -import { Game } from '@staratlas/sage' +import type { Game } from '@staratlas/dev-sage' import { Chance } from 'chance' +import { logger } from '../../../logger.js' import { mine } from '../fsm/configs/mine/mine.js' import { createInfoStrategy } from '../fsm/info.js' import { createMiningStrategy } from '../fsm/mine.js' import { createTransportStrategy, transport } from '../fsm/transport.js' import { Player } from '../lib/sage/state/user-account.js' -import { WorldMap } from '../lib/sage/state/world-map.js' -import { - galaxySectorsData, - SectorInfo, -} from '../lib/util/galaxy-sectors-data.js' +import { FleetInfo } from '../lib/sage/state/user-fleets.js' +import type { AccountWithKey } from '../lib/types.js' +import { Coordinates } from '../lib/util/coordinates.js' +import { getMineableSystems, WorldMap } from '../lib/sage/state/world-map.js' +import { getR4MintSet } from '../lib/sage/state/game-resources.js' +import type { SectorInfo } from '../lib/util/galaxy-sectors-data.js' import { getRandomFleet } from '../lib/util/get-random-fleet.js' import { nameMapMatcher } from './name-map-matcher.js' @@ -24,27 +26,141 @@ import { const randomSector = (chance: Chance.Chance, sectors: Array) => sectors[chance.integer({ min: 0, max: sectors.length - 1 })].coordinates +/** + * Find the closest sector to a given location + */ +const findClosestSector = ( + location: Coordinates, + sectors: SectorInfo[], +): SectorInfo => { + let closest = sectors[0] + let minDist = Infinity + + for (const sector of sectors) { + const dx = sector.coordinates.x - location.x + const dy = sector.coordinates.y - location.y + const dist = Math.sqrt(dx * dx + dy * dy) + if (dist < minDist) { + minDist = dist + closest = sector + } + } + + return closest +} + export const atlasnetFcStrategy = (count: number) => ( map: WorldMap, player: Player, - game: Game, + game: AccountWithKey, seed: string = 'basedbot', + existingFleets: FleetInfo[] = [], ): StrategyConfig => { const strategyMap: StrategyMap = makeStrategyMap() const chance = new Chance(seed) - const sectors = galaxySectorsData() - .filter((sector) => sector.closestFaction === player.faction) - .sort((a, b) => a.name.localeCompare(b.name)) + const allMineableSectors = getMineableSystems(map) + + if (allMineableSectors.length === 0) { + throw new Error( + `No mineable sectors found. Systems: ${map.systems.length}, Resources: ${map.resources.size}`, + ) + } + + // Try faction filter; fallback to all if empty (devnet has no faction data) + const factionSectors = allMineableSectors.filter( + (sector) => sector.closestFaction === player.faction, + ) + const sectors = ( + factionSectors.length > 0 ? factionSectors : allMineableSectors + ).sort((a, b) => a.name.localeCompare(b.name)) + + // Separate miners and transports based on fleet characteristics + // Use ship stats: miners have high cargo capacity, transports have high mobility + // For now, just alternate but use actual locations + const miners: FleetInfo[] = [] + const transports: FleetInfo[] = [] + + for (let i = 0; i < existingFleets.length; i++) { + if (i % 2 === 0) { + miners.push(existingFleets[i]) + } else { + transports.push(existingFleets[i]) + } + } + + // Create mining strategies for miners using their ACTUAL locations + const minerLocations: Map = new Map() + for (const miner of miners) { + const location = miner.location + // Use fleet's current location as home base + const closestSector = findClosestSector(location, sectors) + const targetSector = + sectors[(sectors.indexOf(closestSector) + 1) % sectors.length] + + logger.debug( + `Assigning mining strategy to ${miner.fleetName} at ${location} ` + + `(home=${closestSector.name}, target=${targetSector.name})`, + ) + + strategyMap.set(miner.fleetName, { + fleet: getRandomFleet(player, 'mine', game), + strategy: createMiningStrategy( + mine( + map, + closestSector.coordinates, + targetSector.coordinates, + chance, + ), + player, + game, + ), + }) + + // Record this miner's home for transport assignment + minerLocations.set(miner.fleetName, closestSector.coordinates) + } + + // Create transport strategies - deliver to miners' actual locations + const minerEntries = Array.from(minerLocations.entries()) + for (let i = 0; i < transports.length; i++) { + const transport_ = transports[i] + // Match transport to miner by index + const [minerName, minerHome] = minerEntries[i % minerEntries.length] + + logger.debug( + `Assigning transport strategy to ${transport_.fleetName} ` + + `(CSS -> ${minerName}'s home at ${minerHome})`, + ) - for (let i = 0; i < count; i++) { + strategyMap.set(transport_.fleetName, { + fleet: getRandomFleet(player, 'transport', game), + strategy: createTransportStrategy( + transport( + map, + player.homeCoordinates, + minerHome, + getR4MintSet(game), + ), + player, + game, + ), + }) + } + + // Create additional random fleets if count > existing + const fleetsToCreate = Math.max( + 0, + count - Math.ceil(existingFleets.length / 2), + ) + for (let i = 0; i < fleetsToCreate; i++) { const home = randomSector(chance, sectors) const target = randomSector(chance, sectors) const name = getRandomFleetName(chance, 32, player.faction) strategyMap.set(name, { - fleet: getRandomFleet(player, 'mine'), + fleet: getRandomFleet(player, 'mine', game), strategy: createMiningStrategy( mine(map, home, target, chance), player, @@ -55,18 +171,13 @@ export const atlasnetFcStrategy = if (!home.equals(player.homeCoordinates)) { const name = getRandomFleetName(chance, 32, player.faction) strategyMap.set(name, { - fleet: getRandomFleet(player, 'transport'), + fleet: getRandomFleet(player, 'transport', game), strategy: createTransportStrategy( transport( map, player.homeCoordinates, home, - new Set([ - game.data.mints.fuel, - game.data.mints.ammo, - game.data.mints.food, - game.data.mints.repairKit, - ]), + getR4MintSet(game), ), player, game, @@ -75,6 +186,11 @@ export const atlasnetFcStrategy = } } + logger.info( + `Strategy assigned: ${miners.length} miners, ${transports.length} transports ` + + `(${existingFleets.length} existing, ${fleetsToCreate * 2} to create)`, + ) + return { match: nameMapMatcher(createInfoStrategy()), map: strategyMap, diff --git a/src/main/basedbot/fleet-strategies/atlasnet-lu-strategy.ts b/src/main/basedbot/fleet-strategies/atlasnet-lu-strategy.ts index 6437c73c..bb93df1f 100644 --- a/src/main/basedbot/fleet-strategies/atlasnet-lu-strategy.ts +++ b/src/main/basedbot/fleet-strategies/atlasnet-lu-strategy.ts @@ -1,5 +1,6 @@ -import { Game } from '@staratlas/sage' +import type { Game } from '@staratlas/dev-sage' +import type { AccountWithKey } from '../lib/types.js' import { mineBiomass } from '../fsm/configs/mine/mine-biomass.js' import { mineCarbon } from '../fsm/configs/mine/mine-carbon.js' import { mineCopperOre } from '../fsm/configs/mine/mine-copper-ore.js' @@ -21,7 +22,7 @@ import { StrategyConfig } from './strategy-config.js' export const atlasnetLuStrategy = ( map: WorldMap, player: Player, - game: Game, + game: AccountWithKey, ): StrategyConfig => { return { match: nameMapMatcher(createInfoStrategy()), diff --git a/src/main/basedbot/fleet-strategies/atlasnet-qt-strategy.ts b/src/main/basedbot/fleet-strategies/atlasnet-qt-strategy.ts index 76320eb5..1a9201a2 100644 --- a/src/main/basedbot/fleet-strategies/atlasnet-qt-strategy.ts +++ b/src/main/basedbot/fleet-strategies/atlasnet-qt-strategy.ts @@ -1,4 +1,4 @@ -import { Game } from '@staratlas/sage' +import type { Game } from '@staratlas/dev-sage' import { Chance } from 'chance' import { mine } from '../fsm/configs/mine/mine.js' @@ -6,7 +6,9 @@ import { createInfoStrategy } from '../fsm/info.js' import { createMiningStrategy } from '../fsm/mine.js' import { createTransportStrategy, transport } from '../fsm/transport.js' import { Player } from '../lib/sage/state/user-account.js' +import type { AccountWithKey } from '../lib/types.js' import { WorldMap } from '../lib/sage/state/world-map.js' +import { getR4MintSet } from '../lib/sage/state/game-resources.js' import { galaxySectorsData, SectorInfo, @@ -29,7 +31,7 @@ export const atlasnetQtStrategy = ( map: WorldMap, player: Player, - game: Game, + game: AccountWithKey, seed: string = 'basedbot', ): StrategyConfig => { const strategyMap: StrategyMap = makeStrategyMap() @@ -59,7 +61,7 @@ export const atlasnetQtStrategy = const target = randomSector(chance, sectors) strategyMap.set(getRandomFleetName(chance, 32, player.faction), { - fleet: getRandomFleet(player, 'mine'), + fleet: getRandomFleet(player, 'mine', game), strategy: createMiningStrategy( mine(map, home, target, chance), player, @@ -71,18 +73,13 @@ export const atlasnetQtStrategy = strategyMap.set( getRandomFleetName(chance, 32, player.faction), { - fleet: getRandomFleet(player, 'transport'), + fleet: getRandomFleet(player, 'transport', game), strategy: createTransportStrategy( transport( map, player.homeCoordinates, home, - new Set([ - game.data.mints.fuel, - game.data.mints.ammo, - game.data.mints.food, - game.data.mints.repairKit, - ]), + getR4MintSet(game), ), player, game, diff --git a/src/main/basedbot/fleet-strategies/destruct-all-strategy.ts b/src/main/basedbot/fleet-strategies/destruct-all-strategy.ts index 29808969..2a29e4f6 100644 --- a/src/main/basedbot/fleet-strategies/destruct-all-strategy.ts +++ b/src/main/basedbot/fleet-strategies/destruct-all-strategy.ts @@ -1,7 +1,8 @@ -import { Game } from '@staratlas/sage' +import type { Game } from '@staratlas/dev-sage' import { createDestructStrategy, destructConfig } from '../fsm/destruct.js' import { Player } from '../lib/sage/state/user-account.js' +import type { AccountWithKey } from '../lib/types.js' import { WorldMap } from '../lib/sage/state/world-map.js' import { nameMapMatcher } from './name-map-matcher.js' @@ -10,7 +11,7 @@ import { makeStrategyMap, StrategyConfig } from './strategy-config.js' export const destructAllStrategy = ( worldMap: WorldMap, player: Player, - game: Game, + game: AccountWithKey, ): StrategyConfig => { return { match: nameMapMatcher( diff --git a/src/main/basedbot/fleet-strategies/disband-all-strategy.ts b/src/main/basedbot/fleet-strategies/disband-all-strategy.ts index f7f4c97c..137ac0cc 100644 --- a/src/main/basedbot/fleet-strategies/disband-all-strategy.ts +++ b/src/main/basedbot/fleet-strategies/disband-all-strategy.ts @@ -1,8 +1,9 @@ -import { Game } from '@staratlas/sage' +import type { Game } from '@staratlas/dev-sage' import { disbandConfig } from '../fsm/configs/disband-config.js' import { createDisbandStrategy } from '../fsm/disband.js' import { Player } from '../lib/sage/state/user-account.js' +import type { AccountWithKey } from '../lib/types.js' import { WorldMap } from '../lib/sage/state/world-map.js' import { nameMapMatcher } from './name-map-matcher.js' @@ -11,7 +12,7 @@ import { makeStrategyMap, StrategyConfig } from './strategy-config.js' export const disbandAllStrategy = ( worldMap: WorldMap, player: Player, - game: Game, + game: AccountWithKey, ): StrategyConfig => { return { match: nameMapMatcher( diff --git a/src/main/basedbot/fleet-strategies/get-fleet-strategy.ts b/src/main/basedbot/fleet-strategies/get-fleet-strategy.ts index 57126623..f24af3aa 100644 --- a/src/main/basedbot/fleet-strategies/get-fleet-strategy.ts +++ b/src/main/basedbot/fleet-strategies/get-fleet-strategy.ts @@ -1,7 +1,9 @@ -import { Game } from '@staratlas/sage' +import type { Game } from '@staratlas/dev-sage' import { config } from '../../../config/index.js' import { Player } from '../lib/sage/state/user-account.js' +import { FleetInfo } from '../lib/sage/state/user-fleets.js' +import type { AccountWithKey } from '../lib/types.js' import { WorldMap } from '../lib/sage/state/world-map.js' import { atlasnetFcStrategy } from './atlasnet-fc-strategy.js' @@ -14,9 +16,10 @@ import { StrategyConfig } from './strategy-config.js' export const getFleetStrategy = ( map: WorldMap, player: Player, - game: Game, + game: AccountWithKey, + existingFleets: FleetInfo[] = [], ): StrategyConfig => { - switch (player.publicKey.toString()) { + switch (player.address) { case '4GZeR3hQdQXgoaEG22Gj4egAPX3db7So41rsHtBhHBk8': return mainnetGellsnStrategy(map, player, game) case 'k49Y5xwN7Nyi19TqDR4zbCFuAt8kgy6qMaJ6Kj1wHrn': @@ -30,7 +33,8 @@ export const getFleetStrategy = ( map, player, game, - player.profile.key.toBase58(), + player.character.playerProfile, + existingFleets, ) } } diff --git a/src/main/basedbot/fleet-strategies/mainnet-gellsn-strategy.ts b/src/main/basedbot/fleet-strategies/mainnet-gellsn-strategy.ts index ac5e24af..1f218f97 100644 --- a/src/main/basedbot/fleet-strategies/mainnet-gellsn-strategy.ts +++ b/src/main/basedbot/fleet-strategies/mainnet-gellsn-strategy.ts @@ -1,9 +1,10 @@ -import { Game } from '@staratlas/sage' +import type { Game } from '@staratlas/dev-sage' import { mineHydrogen } from '../fsm/configs/mine/mine-hydrogen.js' import { createInfoStrategy } from '../fsm/info.js' import { createMiningStrategy } from '../fsm/mine.js' import { Player } from '../lib/sage/state/user-account.js' +import type { AccountWithKey } from '../lib/types.js' import { WorldMap } from '../lib/sage/state/world-map.js' import { nameMapMatcher } from './name-map-matcher.js' @@ -12,7 +13,7 @@ import { StrategyConfig } from './strategy-config.js' export const mainnetGellsnStrategy = ( worldMap: WorldMap, player: Player, - game: Game, + game: AccountWithKey, ): StrategyConfig => { return { match: nameMapMatcher(createInfoStrategy()), diff --git a/src/main/basedbot/fleet-strategies/mainnet-lu-strategy.ts b/src/main/basedbot/fleet-strategies/mainnet-lu-strategy.ts index 648b019a..34f177e4 100644 --- a/src/main/basedbot/fleet-strategies/mainnet-lu-strategy.ts +++ b/src/main/basedbot/fleet-strategies/mainnet-lu-strategy.ts @@ -1,10 +1,11 @@ -import { Game } from '@staratlas/sage' +import type { Game } from '@staratlas/dev-sage' import { mineCarbon } from '../fsm/configs/mine/mine-carbon.js' import { mineHydrogen } from '../fsm/configs/mine/mine-hydrogen.js' import { createInfoStrategy } from '../fsm/info.js' import { createMiningStrategy } from '../fsm/mine.js' import { Player } from '../lib/sage/state/user-account.js' +import type { AccountWithKey } from '../lib/types.js' import { WorldMap } from '../lib/sage/state/world-map.js' import { nameMapMatcher } from './name-map-matcher.js' @@ -13,7 +14,7 @@ import { StrategyConfig } from './strategy-config.js' export const mainnetLuStrategy = ( worldMap: WorldMap, player: Player, - game: Game, + game: AccountWithKey, ): StrategyConfig => { return { match: nameMapMatcher(createInfoStrategy()), diff --git a/src/main/basedbot/fleet-strategies/strategy-config.ts b/src/main/basedbot/fleet-strategies/strategy-config.ts index d9e71ac2..a791668a 100644 --- a/src/main/basedbot/fleet-strategies/strategy-config.ts +++ b/src/main/basedbot/fleet-strategies/strategy-config.ts @@ -1,8 +1,8 @@ import { Strategy } from '../fsm/strategy.js' -import { FleetShips } from '../lib/sage/act/create-fleet.js' +import type { FleetShip } from '../lib/sage/act/create-fleet.js' export type FleetStrategy = { - fleet: FleetShips | null + fleet: FleetShip[] | null strategy: Strategy } export type StrategyMap = Map diff --git a/src/main/basedbot/fsm/configs/mine/mine-biomass.ts b/src/main/basedbot/fsm/configs/mine/mine-biomass.ts index 80c9542d..2234e9af 100644 --- a/src/main/basedbot/fsm/configs/mine/mine-biomass.ts +++ b/src/main/basedbot/fsm/configs/mine/mine-biomass.ts @@ -1,19 +1,23 @@ import { mineableByCoordinates, - WorldMap, + type WorldMap, } from '../../../lib/sage/state/world-map.js' import { Coordinates } from '../../../lib/util/coordinates.js' -import { MineConfig, mineConfig } from './mine-config.js' +import { type MineConfig, mineConfig } from './mine-config.js' -export const mineBiomass = (map: WorldMap): MineConfig => - mineConfig({ +export const mineBiomass = (map: WorldMap): MineConfig => { + const targetBase = Coordinates.fromNumber(-42, 35) + const resource = mineableByCoordinates(map, targetBase, 'Biomass') + + if (!resource) { + throw new Error(`No Biomass mineable found`) + } + + return mineConfig({ homeBase: Coordinates.fromNumber(-40, 30), - targetBase: Coordinates.fromNumber(-42, 35), - resource: mineableByCoordinates( - map, - Coordinates.fromNumber(-30, 30), - 'Biomass', - ), + targetBase, + resource, worldMap: map, }) +} diff --git a/src/main/basedbot/fsm/configs/mine/mine-carbon.ts b/src/main/basedbot/fsm/configs/mine/mine-carbon.ts index e4cd28ce..13cc85ea 100644 --- a/src/main/basedbot/fsm/configs/mine/mine-carbon.ts +++ b/src/main/basedbot/fsm/configs/mine/mine-carbon.ts @@ -1,19 +1,23 @@ import { mineableByCoordinates, - WorldMap, + type WorldMap, } from '../../../lib/sage/state/world-map.js' import { Coordinates } from '../../../lib/util/coordinates.js' -import { MineConfig, mineConfig } from './mine-config.js' +import { type MineConfig, mineConfig } from './mine-config.js' -export const mineCarbon = (map: WorldMap): MineConfig => - mineConfig({ +export const mineCarbon = (map: WorldMap): MineConfig => { + const targetBase = Coordinates.fromNumber(-30, 30) + const resource = mineableByCoordinates(map, targetBase, 'Carbon') + + if (!resource) { + throw new Error(`No Carbon mineable found at ${targetBase}`) + } + + return mineConfig({ homeBase: Coordinates.fromNumber(-40, 30), - targetBase: Coordinates.fromNumber(-30, 30), - resource: mineableByCoordinates( - map, - Coordinates.fromNumber(-30, 30), - 'Carbon', - ), + targetBase, + resource, worldMap: map, }) +} diff --git a/src/main/basedbot/fsm/configs/mine/mine-copper-ore.ts b/src/main/basedbot/fsm/configs/mine/mine-copper-ore.ts index f222dd77..0bb3d74e 100644 --- a/src/main/basedbot/fsm/configs/mine/mine-copper-ore.ts +++ b/src/main/basedbot/fsm/configs/mine/mine-copper-ore.ts @@ -1,19 +1,23 @@ import { mineableByCoordinates, - WorldMap, + type WorldMap, } from '../../../lib/sage/state/world-map.js' import { Coordinates } from '../../../lib/util/coordinates.js' -import { MineConfig, mineConfig } from './mine-config.js' +import { type MineConfig, mineConfig } from './mine-config.js' -export const mineCopperOre = (map: WorldMap): MineConfig => - mineConfig({ +export const mineCopperOre = (map: WorldMap): MineConfig => { + const targetBase = Coordinates.fromNumber(-47, 30) + const resource = mineableByCoordinates(map, targetBase, 'Copper Ore') + + if (!resource) { + throw new Error(`No Copper Ore mineable found at ${targetBase}`) + } + + return mineConfig({ homeBase: Coordinates.fromNumber(-40, 30), - targetBase: Coordinates.fromNumber(-47, 30), - resource: mineableByCoordinates( - map, - Coordinates.fromNumber(-47, 30), - 'Copper Ore', - ), + targetBase, + resource, worldMap: map, }) +} diff --git a/src/main/basedbot/fsm/configs/mine/mine-diamond.ts b/src/main/basedbot/fsm/configs/mine/mine-diamond.ts index 630877be..695e7206 100644 --- a/src/main/basedbot/fsm/configs/mine/mine-diamond.ts +++ b/src/main/basedbot/fsm/configs/mine/mine-diamond.ts @@ -1,19 +1,23 @@ import { mineableByCoordinates, - WorldMap, + type WorldMap, } from '../../../lib/sage/state/world-map.js' import { Coordinates } from '../../../lib/util/coordinates.js' -import { MineConfig, mineConfig } from './mine-config.js' +import { type MineConfig, mineConfig } from './mine-config.js' -export const mineDiamond = (map: WorldMap): MineConfig => - mineConfig({ +export const mineDiamond = (map: WorldMap): MineConfig => { + const targetBase = Coordinates.fromNumber(-16, 0) + const resource = mineableByCoordinates(map, targetBase, 'Diamond') + + if (!resource) { + throw new Error(`No Diamond mineable found at ${targetBase}`) + } + + return mineConfig({ homeBase: Coordinates.fromNumber(-40, 30), - targetBase: Coordinates.fromNumber(-16, 0), - resource: mineableByCoordinates( - map, - Coordinates.fromNumber(-16, 0), - 'Diamond', - ), + targetBase, + resource, worldMap: map, }) +} diff --git a/src/main/basedbot/fsm/configs/mine/mine-hydrogen.ts b/src/main/basedbot/fsm/configs/mine/mine-hydrogen.ts index a9df04c9..2fdd4cdb 100644 --- a/src/main/basedbot/fsm/configs/mine/mine-hydrogen.ts +++ b/src/main/basedbot/fsm/configs/mine/mine-hydrogen.ts @@ -1,19 +1,23 @@ import { mineableByCoordinates, - WorldMap, + type WorldMap, } from '../../../lib/sage/state/world-map.js' import { Coordinates } from '../../../lib/util/coordinates.js' -import { MineConfig, mineConfig } from './mine-config.js' +import { type MineConfig, mineConfig } from './mine-config.js' -export const mineHydrogen = (map: WorldMap): MineConfig => - mineConfig({ +export const mineHydrogen = (map: WorldMap): MineConfig => { + const targetBase = Coordinates.fromNumber(-40, 30) + const resource = mineableByCoordinates(map, targetBase, 'Hydrogen') + + if (!resource) { + throw new Error(`No Hydrogen mineable found at ${targetBase}`) + } + + return mineConfig({ homeBase: Coordinates.fromNumber(-40, 30), - targetBase: Coordinates.fromNumber(-40, 30), - resource: mineableByCoordinates( - map, - Coordinates.fromNumber(-40, 30), - 'Hydrogen', - ), + targetBase, + resource, worldMap: map, }) +} diff --git a/src/main/basedbot/fsm/configs/mine/mine-iron-ore.ts b/src/main/basedbot/fsm/configs/mine/mine-iron-ore.ts index 3e81f47b..38173c97 100644 --- a/src/main/basedbot/fsm/configs/mine/mine-iron-ore.ts +++ b/src/main/basedbot/fsm/configs/mine/mine-iron-ore.ts @@ -1,19 +1,23 @@ import { mineableByCoordinates, - WorldMap, + type WorldMap, } from '../../../lib/sage/state/world-map.js' import { Coordinates } from '../../../lib/util/coordinates.js' -import { MineConfig, mineConfig } from './mine-config.js' +import { type MineConfig, mineConfig } from './mine-config.js' -export const mineIronOre = (map: WorldMap): MineConfig => - mineConfig({ +export const mineIronOre = (map: WorldMap): MineConfig => { + const targetBase = Coordinates.fromNumber(-38, 25) + const resource = mineableByCoordinates(map, targetBase, 'Iron Ore') + + if (!resource) { + throw new Error(`No Iron Ore mineable found at ${targetBase}`) + } + + return mineConfig({ homeBase: Coordinates.fromNumber(-40, 30), - targetBase: Coordinates.fromNumber(-38, 25), - resource: mineableByCoordinates( - map, - Coordinates.fromNumber(-38, 25), - 'Iron Ore', - ), + targetBase, + resource, worldMap: map, }) +} diff --git a/src/main/basedbot/fsm/configs/mine/mine-lumanite.ts b/src/main/basedbot/fsm/configs/mine/mine-lumanite.ts index 94dc3d6a..580714ff 100644 --- a/src/main/basedbot/fsm/configs/mine/mine-lumanite.ts +++ b/src/main/basedbot/fsm/configs/mine/mine-lumanite.ts @@ -1,19 +1,23 @@ import { mineableByCoordinates, - WorldMap, + type WorldMap, } from '../../../lib/sage/state/world-map.js' import { Coordinates } from '../../../lib/util/coordinates.js' -import { MineConfig, mineConfig } from './mine-config.js' +import { type MineConfig, mineConfig } from './mine-config.js' -export const mineLumanite = (map: WorldMap): MineConfig => - mineConfig({ +export const mineLumanite = (map: WorldMap): MineConfig => { + const targetBase = Coordinates.fromNumber(-23, 4) + const resource = mineableByCoordinates(map, targetBase, 'Lumanite') + + if (!resource) { + throw new Error(`No Lumanite mineable found at ${targetBase}`) + } + + return mineConfig({ homeBase: Coordinates.fromNumber(-40, 30), - targetBase: Coordinates.fromNumber(-23, 4), - resource: mineableByCoordinates( - map, - Coordinates.fromNumber(-23, 4), - 'Lumanite', - ), + targetBase, + resource, worldMap: map, }) +} diff --git a/src/main/basedbot/fsm/configs/mine/mine-nitrogen.ts b/src/main/basedbot/fsm/configs/mine/mine-nitrogen.ts index 27a1e387..e8861bba 100644 --- a/src/main/basedbot/fsm/configs/mine/mine-nitrogen.ts +++ b/src/main/basedbot/fsm/configs/mine/mine-nitrogen.ts @@ -1,19 +1,23 @@ import { mineableByCoordinates, - WorldMap, + type WorldMap, } from '../../../lib/sage/state/world-map.js' import { Coordinates } from '../../../lib/util/coordinates.js' -import { MineConfig, mineConfig } from './mine-config.js' +import { type MineConfig, mineConfig } from './mine-config.js' -export const mineNitrogen = (map: WorldMap): MineConfig => - mineConfig({ +export const mineNitrogen = (map: WorldMap): MineConfig => { + const targetBase = Coordinates.fromNumber(-45, 15) + const resource = mineableByCoordinates(map, targetBase, 'Nitrogen') + + if (!resource) { + throw new Error(`No Nitrogen mineable found at ${targetBase}`) + } + + return mineConfig({ homeBase: Coordinates.fromNumber(-40, 30), - targetBase: Coordinates.fromNumber(-45, 15), - resource: mineableByCoordinates( - map, - Coordinates.fromNumber(-45, 15), - 'Nitrogen', - ), + targetBase, + resource, worldMap: map, }) +} diff --git a/src/main/basedbot/fsm/configs/mine/mine-rochinol.ts b/src/main/basedbot/fsm/configs/mine/mine-rochinol.ts index db17b9b4..780c75c7 100644 --- a/src/main/basedbot/fsm/configs/mine/mine-rochinol.ts +++ b/src/main/basedbot/fsm/configs/mine/mine-rochinol.ts @@ -1,19 +1,23 @@ import { mineableByCoordinates, - WorldMap, + type WorldMap, } from '../../../lib/sage/state/world-map.js' import { Coordinates } from '../../../lib/util/coordinates.js' -import { MineConfig, mineConfig } from './mine-config.js' +import { type MineConfig, mineConfig } from './mine-config.js' -export const mineRochinol = (map: WorldMap): MineConfig => - mineConfig({ +export const mineRochinol = (map: WorldMap): MineConfig => { + const targetBase = Coordinates.fromNumber(0, 16) + const resource = mineableByCoordinates(map, targetBase, 'Rochinol') + + if (!resource) { + throw new Error(`No Rochinol mineable found at ${targetBase}`) + } + + return mineConfig({ homeBase: Coordinates.fromNumber(-40, 30), - targetBase: Coordinates.fromNumber(0, 16), - resource: mineableByCoordinates( - map, - Coordinates.fromNumber(0, 16), - 'Rochinol', - ), + targetBase, + resource, worldMap: map, }) +} diff --git a/src/main/basedbot/fsm/configs/mine/mine-silicia.ts b/src/main/basedbot/fsm/configs/mine/mine-silicia.ts index a08fc06c..3157fc61 100644 --- a/src/main/basedbot/fsm/configs/mine/mine-silicia.ts +++ b/src/main/basedbot/fsm/configs/mine/mine-silicia.ts @@ -1,19 +1,23 @@ import { mineableByCoordinates, - WorldMap, + type WorldMap, } from '../../../lib/sage/state/world-map.js' import { Coordinates } from '../../../lib/util/coordinates.js' -import { MineConfig, mineConfig } from './mine-config.js' +import { type MineConfig, mineConfig } from './mine-config.js' -export const mineSilicia = (map: WorldMap): MineConfig => - mineConfig({ +export const mineSilicia = (map: WorldMap): MineConfig => { + const targetBase = Coordinates.fromNumber(-22, 32) + const resource = mineableByCoordinates(map, targetBase, 'Silicia') + + if (!resource) { + throw new Error(`No Silicia mineable found at ${targetBase}`) + } + + return mineConfig({ homeBase: Coordinates.fromNumber(-40, 30), - targetBase: Coordinates.fromNumber(-22, 32), - resource: mineableByCoordinates( - map, - Coordinates.fromNumber(-22, 32), - 'Silicia', - ), + targetBase, + resource, worldMap: map, }) +} diff --git a/src/main/basedbot/fsm/configs/mine/mine-titanium-ore.ts b/src/main/basedbot/fsm/configs/mine/mine-titanium-ore.ts index 96aa4ddd..5458d03b 100644 --- a/src/main/basedbot/fsm/configs/mine/mine-titanium-ore.ts +++ b/src/main/basedbot/fsm/configs/mine/mine-titanium-ore.ts @@ -1,19 +1,23 @@ import { mineableByCoordinates, - WorldMap, + type WorldMap, } from '../../../lib/sage/state/world-map.js' import { Coordinates } from '../../../lib/util/coordinates.js' -import { MineConfig, mineConfig } from './mine-config.js' +import { type MineConfig, mineConfig } from './mine-config.js' -export const mineTitaniumOre = (map: WorldMap): MineConfig => - mineConfig({ +export const mineTitaniumOre = (map: WorldMap): MineConfig => { + const targetBase = Coordinates.fromNumber(-8, 35) + const resource = mineableByCoordinates(map, targetBase, 'Titanium Ore') + + if (!resource) { + throw new Error(`No Titanium Ore mineable found at ${targetBase}`) + } + + return mineConfig({ homeBase: Coordinates.fromNumber(-40, 30), - targetBase: Coordinates.fromNumber(-8, 35), - resource: mineableByCoordinates( - map, - Coordinates.fromNumber(-8, 35), - 'Titanium Ore', - ), + targetBase, + resource, worldMap: map, }) +} diff --git a/src/main/basedbot/fsm/configs/mine/mine.ts b/src/main/basedbot/fsm/configs/mine/mine.ts index f71e1e0d..2187505a 100644 --- a/src/main/basedbot/fsm/configs/mine/mine.ts +++ b/src/main/basedbot/fsm/configs/mine/mine.ts @@ -1,25 +1,33 @@ import { mineablesByCoordinates, - WorldMap, + type WorldMap, } from '../../../lib/sage/state/world-map.js' import { getName } from '../../../lib/sage/util.js' -import { Coordinates } from '../../../lib/util/coordinates.js' +import type { Coordinates } from '../../../lib/util/coordinates.js' -import { mineConfig, MineConfig } from './mine-config.js' +import { mineConfig, type MineConfig } from './mine-config.js' export const mine = ( map: WorldMap, homeBase: Coordinates, targetBase: Coordinates, chance: Chance.Chance, -): MineConfig => - mineConfig({ +): MineConfig => { + const mineables = mineablesByCoordinates(map, targetBase) + + if (mineables.length === 0) { + throw new Error(`No mineables found at ${targetBase}`) + } + + // C4: Use celestialBody instead of mineItem + const sortedMineables = mineables.sort((m, n) => + getName(m.celestialBody).localeCompare(getName(n.celestialBody)), + ) + + return mineConfig({ homeBase, targetBase, - resource: chance.pickone( - Array.from(mineablesByCoordinates(map, targetBase)).sort((m, n) => - getName(m.mineItem).localeCompare(getName(n.mineItem)), - ), - ), + resource: chance.pickone(sortedMineables), worldMap: map, }) +} diff --git a/src/main/basedbot/fsm/destruct.ts b/src/main/basedbot/fsm/destruct.ts index 2caf41c6..c1a4e95f 100644 --- a/src/main/basedbot/fsm/destruct.ts +++ b/src/main/basedbot/fsm/destruct.ts @@ -1,133 +1,144 @@ -import { Game } from '@staratlas/sage' +import type { Game } from '@staratlas/dev-sage' import dayjs from 'dayjs' import { now } from '../../../dayjs.js' import { logger } from '../../../logger.js' import { disbandFleet } from '../lib/sage/act/disband-fleet.js' -import { dock } from '../lib/sage/act/dock.js' -import { endMine } from '../lib/sage/act/end-mine.js' -import { endMove } from '../lib/sage/act/end-move.js' -import { selfDestruct } from '../lib/sage/act/self-destruct.js' -import { stopSubwarp } from '../lib/sage/act/stop-subwarp.js' -import { undock } from '../lib/sage/act/undock.js' import { starbaseByCoordinates } from '../lib/sage/state/starbase-by-coordinates.js' -import { Player } from '../lib/sage/state/user-account.js' -import { FleetInfo } from '../lib/sage/state/user-fleets.js' -import { mineableByCoordinates, WorldMap } from '../lib/sage/state/world-map.js' +import type { Player } from '../lib/sage/state/user-account.js' +import type { FleetInfo } from '../lib/sage/state/user-fleets.js' +import { + mineableByCoordinates, + type WorldMap, +} from '../lib/sage/state/world-map.js' import { getName } from '../lib/sage/util.js' - -import { DisbandConfig } from './configs/disband-config.js' -import { Strategy } from './strategy.js' +import type { AccountWithKey } from '../lib/types.js' + +import type { DisbandConfig } from './configs/disband-config.js' +import type { Strategy } from './strategy.js' +import { + fsmDock, + fsmEndMine, + fsmEndMove, + fsmSelfDestruct, + fsmStopSubwarp, + fsmUndock, +} from './fsm-actions.js' const transition = async ( fleetInfo: FleetInfo, player: Player, - game: Game, + game: AccountWithKey, config: DestructConfig, ): Promise => { const currentStarbase = await starbaseByCoordinates(fleetInfo.location) const { fleetName, location } = fleetInfo const homeBase = player.homeCoordinates - const isAtHomeBase = homeBase.equals(location) + // Use nearEquals for fuzzy comparison (within 1 unit) to handle coordinate precision issues + const isAtHomeBase = homeBase.nearEquals(location) - switch (fleetInfo.fleetState.type) { + switch (fleetInfo.fleetState.kind) { case 'Idle': { logger.info( - `${fleetName} is idle at ${fleetInfo.fleetState.data.sector} [Starbase: ${currentStarbase ? getName(currentStarbase) : 'N/A'}]`, + `${fleetName} is idle at ${fleetInfo.fleetState.data.sector} [Starbase: ${currentStarbase ? getName(currentStarbase.system) : 'N/A'}]`, ) if (isAtHomeBase) { logger.info(`${fleetName} is at home base, docking to disband`) - - return dock(fleetInfo, location, player, game) + await fsmDock(fleetInfo, location, player, game) + return } - return selfDestruct(fleetInfo, player, game) + return fsmSelfDestruct(fleetInfo, player, game) } - case 'StarbaseLoadingBay': { + case 'Docked': { + const dockedSystem = fleetInfo.fleetState.data.system logger.info( - `${fleetInfo.fleetName} is in the loading bay at ${getName(fleetInfo.fleetState.data.starbase)}`, + `${fleetInfo.fleetName} is docked at ${getName(dockedSystem)}`, ) if (isAtHomeBase) { logger.info( `${fleetInfo.fleetName} is at home base, disbanding...`, ) - - return disbandFleet( - player, - game, - player.homeStarbase, - fleetInfo, - ) + // Use actual docked system, not player.homeSystem (may differ due to fuzzy matching) + return disbandFleet(player, game, dockedSystem, fleetInfo) } logger.info( `${fleetInfo.fleetName} is at ${location}, undocking...`, ) - - return undock(fleetInfo.fleet, fleetInfo.location, player, game) + await fsmUndock(fleetInfo, fleetInfo.location, player, game) + return } case 'MoveWarp': { - const { fromSector, toSector, warpFinish } = + const { fromCoordinates, toCoordinates, arrivalTime } = fleetInfo.fleetState.data - if (!homeBase.equals(toSector)) { + if (!homeBase.nearEquals(toCoordinates)) { logger.info(`Stopping fleet ${fleetInfo.fleetName}`) - - return endMove(fleetInfo, player, game) + return fsmEndMove(fleetInfo, player, game) } - if (warpFinish.isBefore(now())) { + if (arrivalTime.isBefore(now())) { logger.info( - `${fleetInfo.fleetName} has arrived at ${fleetInfo.fleetState.data.toSector}`, + `${fleetInfo.fleetName} has arrived at ${toCoordinates}`, ) } else { logger.info( - `${fleetInfo.fleetName} warping from ${fromSector} to ${toSector}. Arrival in ${dayjs.duration(warpFinish.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, + `${fleetInfo.fleetName} warping from ${fromCoordinates} to ${toCoordinates}. Arrival in ${dayjs.duration(arrivalTime.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, ) } break } case 'MoveSubwarp': { - const { fromSector, toSector, arrivalTime } = + const { fromCoordinates, toCoordinates, arrivalTime } = fleetInfo.fleetState.data - if (!homeBase.equals(toSector)) { + if (!homeBase.nearEquals(toCoordinates)) { logger.info(`Stopping fleet ${fleetInfo.fleetName}`) - - return stopSubwarp(fleetInfo, player, game) + return fsmStopSubwarp(fleetInfo, player, game) } if (arrivalTime.isBefore(now())) { logger.info( - `${fleetInfo.fleetName} has arrived at ${fleetInfo.fleetState.data.toSector}`, + `${fleetInfo.fleetName} has arrived at ${toCoordinates}`, ) } else { logger.info( - `${fleetInfo.fleetName} subwarping from ${fromSector} to ${toSector}. Arrival in ${dayjs.duration(arrivalTime.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, + `${fleetInfo.fleetName} subwarping from ${fromCoordinates} to ${toCoordinates}. Arrival in ${dayjs.duration(arrivalTime.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, ) } break } case 'MineAsteroid': { - const { mineItem, end, amountMined } = fleetInfo.fleetState.data + const { celestialBody, end, amountMined } = + fleetInfo.fleetState.data + const celestialBodyName = getName(celestialBody) if (end.isBefore(now())) { logger.info( - `${fleetInfo.fleetName} has finished mining ${getName(mineItem)} for ${amountMined}`, + `${fleetInfo.fleetName} has finished mining ${celestialBodyName} for ${amountMined}`, ) } logger.info( - `${fleetInfo.fleetName} mining ${getName(mineItem)} for ${amountMined}. Ending...`, + `${fleetInfo.fleetName} mining ${celestialBodyName} for ${amountMined}. Ending...`, ) - const resource = mineableByCoordinates( + + const mineable = mineableByCoordinates( config.worldMap, fleetInfo.location, - getName(mineItem), + celestialBodyName, ) - return endMine(fleetInfo, player, game, resource) + if (!mineable) { + logger.error( + `No mineable found at ${fleetInfo.location} for ${celestialBodyName}`, + ) + return Promise.resolve() + } + + return fsmEndMine(fleetInfo, player, game, mineable) } case 'Respawn': { const { destructionTime, ETA } = fleetInfo.fleetState.data @@ -142,10 +153,7 @@ const transition = async ( break } default: - logger.info( - `${fleetInfo.fleetName} is ${fleetInfo.fleetState.type}`, - ) - + logger.info(`${fleetInfo.fleetName} is in unknown state`) return Promise.resolve() } } @@ -165,7 +173,7 @@ export const destructConfig = ( export const createDestructStrategy = ( config: DestructConfig, player: Player, - game: Game, + game: AccountWithKey, ): Strategy => { return { apply: (fleetInfo: FleetInfo): Promise => diff --git a/src/main/basedbot/fsm/disband.ts b/src/main/basedbot/fsm/disband.ts index b9df4f08..561ecc83 100644 --- a/src/main/basedbot/fsm/disband.ts +++ b/src/main/basedbot/fsm/disband.ts @@ -1,148 +1,162 @@ -import { Game } from '@staratlas/sage' +import type { Game } from '@staratlas/dev-sage' import dayjs from 'dayjs' import { now } from '../../../dayjs.js' import { logger } from '../../../logger.js' import { disbandFleet } from '../lib/sage/act/disband-fleet.js' -import { dock } from '../lib/sage/act/dock.js' -import { endMine } from '../lib/sage/act/end-mine.js' -import { endMove } from '../lib/sage/act/end-move.js' -import { move } from '../lib/sage/act/move.js' -import { selfDestruct } from '../lib/sage/act/self-destruct.js' -import { stopSubwarp } from '../lib/sage/act/stop-subwarp.js' -import { undock } from '../lib/sage/act/undock.js' import { starbaseByCoordinates } from '../lib/sage/state/starbase-by-coordinates.js' -import { Player } from '../lib/sage/state/user-account.js' -import { FleetInfo } from '../lib/sage/state/user-fleets.js' +import type { Player } from '../lib/sage/state/user-account.js' +import type { FleetInfo } from '../lib/sage/state/user-fleets.js' import { mineableByCoordinates } from '../lib/sage/state/world-map.js' import { getName } from '../lib/sage/util.js' - -import { DisbandConfig } from './configs/disband-config.js' -import { Strategy } from './strategy.js' +import type { AccountWithKey } from '../lib/types.js' + +import type { DisbandConfig } from './configs/disband-config.js' +import type { Strategy } from './strategy.js' +import { + fsmDock, + fsmEndMine, + fsmEndMove, + fsmMove, + fsmSelfDestruct, + fsmStopSubwarp, + fsmUndock, +} from './fsm-actions.js' const transition = async ( fleetInfo: FleetInfo, player: Player, - game: Game, + game: AccountWithKey, config: DisbandConfig, ): Promise => { - const cargoLevelFuel = fleetInfo.cargoLevels.fuel + const cargoLevelFuel = Number(fleetInfo.cargoLevels.fuel) const currentStarbase = await starbaseByCoordinates(fleetInfo.location) const { fleetName, location } = fleetInfo const { homeBase, warpMode } = config - const isAtHomeBase = homeBase.equals(location) + // Use nearEquals for fuzzy comparison (within 1 unit) to handle coordinate precision issues + const isAtHomeBase = homeBase.nearEquals(location) - switch (fleetInfo.fleetState.type) { + switch (fleetInfo.fleetState.kind) { case 'Idle': { logger.info( - `${fleetName} is idle at ${fleetInfo.fleetState.data.sector} [Starbase: ${currentStarbase ? getName(currentStarbase) : 'N/A'}]`, + `${fleetName} is idle at ${fleetInfo.fleetState.data.sector} [Starbase: ${currentStarbase ? getName(currentStarbase.system) : 'N/A'}]`, ) if (!currentStarbase && cargoLevelFuel < 1) { logger.warn( `${fleetName} is out of fuel and not at a starbase, need self destruction`, ) - - return selfDestruct(fleetInfo, player, game) + return fsmSelfDestruct(fleetInfo, player, game) } if (isAtHomeBase) { logger.info(`${fleetName} is at home base, docking to disband`) - - return dock(fleetInfo, location, player, game) + await fsmDock(fleetInfo, location, player, game) + return } logger.info(`${fleetName} is at ${location} warping home`) - - return move(fleetInfo, homeBase, player, game, warpMode) + await fsmMove( + fleetInfo, + homeBase, + player, + game, + config.worldMap, + warpMode, + ) + return } - case 'StarbaseLoadingBay': { + case 'Docked': { + const dockedSystem = fleetInfo.fleetState.data.system logger.info( - `${fleetInfo.fleetName} is in the loading bay at ${getName(fleetInfo.fleetState.data.starbase)}`, + `${fleetInfo.fleetName} is docked at ${getName(dockedSystem)}`, ) if (isAtHomeBase) { logger.info( `${fleetInfo.fleetName} is at home base, disbanding...`, ) - - return disbandFleet( - player, - game, - player.homeStarbase, - fleetInfo, - ) + // Use actual docked system, not player.homeSystem (may differ due to fuzzy matching) + return disbandFleet(player, game, dockedSystem, fleetInfo) } logger.info( `${fleetInfo.fleetName} is at ${location}, undocking...`, ) - - return undock(fleetInfo.fleet, fleetInfo.location, player, game) + await fsmUndock(fleetInfo, fleetInfo.location, player, game) + return } case 'MoveWarp': { - const { fromSector, toSector, warpFinish } = + const { fromCoordinates, toCoordinates, arrivalTime } = fleetInfo.fleetState.data - if (!homeBase.equals(toSector)) { + if (!homeBase.nearEquals(toCoordinates)) { logger.info( `Wrong direction, stopping fleet ${fleetInfo.fleetName}`, ) - - return endMove(fleetInfo, player, game) + return fsmEndMove(fleetInfo, player, game) } - if (warpFinish.isBefore(now())) { + if (arrivalTime.isBefore(now())) { logger.info( - `${fleetInfo.fleetName} has arrived at ${fleetInfo.fleetState.data.toSector}`, + `${fleetInfo.fleetName} has arrived at ${toCoordinates}`, ) } else { logger.info( - `${fleetInfo.fleetName} warping from ${fromSector} to ${toSector}. Arrival in ${dayjs.duration(warpFinish.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, + `${fleetInfo.fleetName} warping from ${fromCoordinates} to ${toCoordinates}. Arrival in ${dayjs.duration(arrivalTime.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, ) } break } case 'MoveSubwarp': { - const { fromSector, toSector, arrivalTime } = + const { fromCoordinates, toCoordinates, arrivalTime } = fleetInfo.fleetState.data - if (!homeBase.equals(toSector)) { + if (!homeBase.nearEquals(toCoordinates)) { logger.info( `Wrong direction, stopping fleet ${fleetInfo.fleetName}`, ) - - return stopSubwarp(fleetInfo, player, game) + return fsmStopSubwarp(fleetInfo, player, game) } if (arrivalTime.isBefore(now())) { logger.info( - `${fleetInfo.fleetName} has arrived at ${fleetInfo.fleetState.data.toSector}`, + `${fleetInfo.fleetName} has arrived at ${toCoordinates}`, ) } else { logger.info( - `${fleetInfo.fleetName} subwarping from ${fromSector} to ${toSector}. Arrival in ${dayjs.duration(arrivalTime.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, + `${fleetInfo.fleetName} subwarping from ${fromCoordinates} to ${toCoordinates}. Arrival in ${dayjs.duration(arrivalTime.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, ) } break } case 'MineAsteroid': { - const { mineItem, end, amountMined } = fleetInfo.fleetState.data + const { celestialBody, end, amountMined } = + fleetInfo.fleetState.data + const celestialBodyName = getName(celestialBody) if (end.isBefore(now())) { logger.info( - `${fleetInfo.fleetName} has finished mining ${getName(mineItem)} for ${amountMined}`, + `${fleetInfo.fleetName} has finished mining ${celestialBodyName} for ${amountMined}`, ) } logger.info( - `${fleetInfo.fleetName} mining ${getName(mineItem)} for ${amountMined}. Ending...`, + `${fleetInfo.fleetName} mining ${celestialBodyName} for ${amountMined}. Ending...`, ) - const resource = mineableByCoordinates( + + const mineable = mineableByCoordinates( config.worldMap, fleetInfo.location, - getName(mineItem), + celestialBodyName, ) - return endMine(fleetInfo, player, game, resource) + if (!mineable) { + logger.error( + `No mineable found at ${fleetInfo.location} for ${celestialBodyName}`, + ) + return Promise.resolve() + } + + return fsmEndMine(fleetInfo, player, game, mineable) } case 'Respawn': { const { destructionTime, ETA } = fleetInfo.fleetState.data @@ -157,10 +171,7 @@ const transition = async ( break } default: - logger.info( - `${fleetInfo.fleetName} is ${fleetInfo.fleetState.type}`, - ) - + logger.info(`${fleetInfo.fleetName} is in unknown state`) return Promise.resolve() } } @@ -168,7 +179,7 @@ const transition = async ( export const createDisbandStrategy = ( config: DisbandConfig, player: Player, - game: Game, + game: AccountWithKey, ): Strategy => { return { apply: (fleetInfo: FleetInfo): Promise => diff --git a/src/main/basedbot/fsm/fsm-actions.ts b/src/main/basedbot/fsm/fsm-actions.ts new file mode 100644 index 00000000..7777ef80 --- /dev/null +++ b/src/main/basedbot/fsm/fsm-actions.ts @@ -0,0 +1,962 @@ +/** + * FSM Action Adapters for C4 + * Bridges FSM layer to C4 action layer with proper params construction + */ +import type { Address } from '@solana/kit' +import type { Game } from '@staratlas/dev-sage' + +import { logger } from '../../../logger.js' +import type { AccountWithKey, Fleet } from '../lib/types.js' +import type { Coordinates } from '../lib/util/coordinates.js' +import type { Player } from '../lib/sage/state/user-account.js' +import type { FleetInfo } from '../lib/sage/state/user-fleets.js' +import { + type Mineable, + type WorldMap, + findSystemByCoordinates, + getSystemRegionId, +} from '../lib/sage/state/world-map.js' +import { + getExactSystemCoordinates, + starbaseByCoordinates, +} from '../lib/sage/state/starbase-by-coordinates.js' +import { findNearestCompatibleStarbase } from '../lib/sage/state/starbases.js' +import { getStarbasePlayer } from '../lib/sage/state/starbase-player.js' +import { fetchRegionTracker } from '../lib/account-fetcher.js' + +import { dock as dockAction } from '../lib/sage/act/dock.js' +import { undock as undockAction } from '../lib/sage/act/undock.js' +import { move as moveAction, type WarpMode } from '../lib/sage/act/move.js' +import { mine as mineAction } from '../lib/sage/act/mine.js' +import { endMine as endMineAction } from '../lib/sage/act/end-mine.js' +// Note: C4 has no self-destruct instruction - fleets must be docked to disband +import { endMove as endMoveAction } from '../lib/sage/act/end-move.js' +import { stopSubwarp as stopSubwarpAction } from '../lib/sage/act/stop-subwarp.js' +import { + loadCargo as loadCargoAction, + getFuelCargoId, + getAmmoCargoId, + type CargoTransfer, +} from '../lib/sage/act/load-cargo.js' +import { unloadAllCargo as unloadAllCargoAction } from '../lib/sage/act/unload-all-cargo.js' +import { unloadCargo as unloadCargoAction } from '../lib/sage/act/unload-cargo.js' + +/** + * FSM dock adapter - bridges old signature to new params + * Returns true if dock was executed, false if validation failed + * Checks faction compatibility before attempting to dock + * If fleet is at approximate (integer) coordinates, moves to exact coordinates first + */ +export async function fsmDock( + fleetInfo: FleetInfo, + _location: Coordinates, + player: Player, + game: AccountWithKey, +): Promise { + // Check if already docked - state may be stale + if (fleetInfo.fleetState.kind === 'Docked') { + logger.debug( + `${fleetInfo.fleetName}: Already docked, skipping dock action`, + ) + return true // Already docked is success + } + + const starbaseData = await starbaseByCoordinates(fleetInfo.location) + if (!starbaseData) { + // Not at a starbase - can't dock + logger.debug( + `${fleetInfo.fleetName}: Cannot dock - not at a starbase location (${fleetInfo.location.x},${fleetInfo.location.y})`, + ) + return false + } + + // Check if starbase player already exists + const existingStarbasePlayer = await getStarbasePlayer( + player, + starbaseData.system, + ) + + // Check faction compatibility for REGISTRATION (not docking) + // Can dock at any starbase if already registered there + // Can only REGISTER at starbases owned by player's faction + if (!existingStarbasePlayer) { + // Need to register - check faction compatibility + const systemName = starbaseData.system.name ?? 'unknown' + const factionNames = ['Unknown', 'MUD', 'ONI', 'UST'] + const playerFactionName = + factionNames[player.faction] ?? `faction-${player.faction}` + const ownerFactionName = + starbaseData.owner === null + ? 'Unaligned' + : (factionNames[starbaseData.owner] ?? + `faction-${starbaseData.owner}`) + + logger.debug( + `${fleetInfo.fleetName}: Checking dock at ${systemName} - ` + + `owner=${ownerFactionName} (raw=${starbaseData.ownerSdk}), player=${playerFactionName}`, + ) + + if ( + starbaseData.owner !== null && + starbaseData.owner !== player.faction + ) { + logger.warn( + `${fleetInfo.fleetName}: Cannot dock at ${systemName} - ` + + `owned by ${ownerFactionName}, player is ${playerFactionName} (registration required)`, + ) + return false + } + + if (starbaseData.owner === null) { + // Unaligned starbase - cannot register with faction profile + // This is likely a configuration issue - fleet should not be routed to Unaligned starbases + logger.warn( + `${fleetInfo.fleetName}: Cannot dock at ${systemName} (Unaligned starbase) - ` + + `${playerFactionName} faction cannot register here. ` + + `Change fleet config to use faction-owned starbase.`, + ) + return false + } + } + + // Get exact starbase coordinates + const exactCoords = getExactSystemCoordinates(starbaseData.system) + + // Check if fleet is at exact coordinates (compare raw values) + // Also check precise distance as a fallback for floating point edge cases + const fleetAtExact = + fleetInfo.location.xRaw === exactCoords.xRaw && + fleetInfo.location.yRaw === exactCoords.yRaw + const distanceToExact = fleetInfo.location.preciseDistanceFrom(exactCoords) + + // Skip move if either: raw coords match OR distance is negligible (< 0.01 units) + if (!fleetAtExact && distanceToExact >= 0.01) { + // Fleet is at approximate (integer) coordinates, need to move to exact first + logger.info( + `${fleetInfo.fleetName}: At approximate coordinates [${fleetInfo.location.x},${fleetInfo.location.y}], moving to exact starbase coordinates (distance: ${distanceToExact.toFixed(4)})`, + ) + + // Move to exact coordinates using subwarp (very short distance) + const regionTracker = await fetchRegionTracker() + const sourceRegionId = getSystemRegionId(starbaseData.system) + + const moveResult = await moveAction({ + game, + fleet: fleetInfo.fleet, + character: player.character, + regionTracker, + destination: exactCoords.toRawTuple(), + signer: player.signer, + keyIndex: player.keyIndex, + warpMode: 'subwarp', // Short distance, use subwarp + startRegionId: sourceRegionId, + destRegionId: sourceRegionId, // Same system + }) + + if (!moveResult) { + logger.warn( + `${fleetInfo.fleetName}: Failed to move to exact coordinates`, + ) + return false + } + + // Movement started - will dock next cycle after arrival + return true + } + + const starbasePlayer = await getStarbasePlayer(player, starbaseData.system) + + const result = await dockAction({ + game, + fleet: fleetInfo.fleet, + profileFaction: player.profileFaction, + character: player.character, + system: starbaseData.system, + starbasePlayer: starbasePlayer?.key, + signer: player.signer, + keyIndex: player.keyIndex, + }) + + return result.success +} + +/** + * Result of recovery attempt + */ +export type RecoveryResult = { + action: 'moving' | 'self_destruct' | 'stuck' | 'none_needed' + message: string + targetStarbase?: string +} + +/** + * Attempt to recover a fleet that's stuck at an incompatible starbase + * Returns what action was taken (or should be taken) + * + * Recovery strategy: + * 1. If fleet is docked -> undock first + * 2. If fleet has fuel -> find nearest compatible starbase and move there + * 3. If fleet has no fuel -> self-destruct for redeployment + * 4. If no compatible starbase found -> stuck (should not happen normally) + */ +export async function fsmRecoverFromIncompatibleStarbase( + fleetInfo: FleetInfo, + player: Player, + game: AccountWithKey, + map: WorldMap, +): Promise { + logger.debug( + `${fleetInfo.fleetName}: Attempting recovery from incompatible starbase (state: ${fleetInfo.fleetState.kind})`, + ) + + const fuelLevel = Number(fleetInfo.cargoLevels.fuel) + const starbaseData = await starbaseByCoordinates(fleetInfo.location) + + if (!starbaseData) { + logger.debug(`${fleetInfo.fleetName}: Recovery - not at a starbase`) + return { action: 'none_needed', message: 'Not at a starbase' } + } + + // Check if starbase is actually incompatible + const existingStarbasePlayer = await getStarbasePlayer( + player, + starbaseData.system, + ) + + if (existingStarbasePlayer) { + // Already registered - no recovery needed + logger.debug( + `${fleetInfo.fleetName}: Recovery - already registered at ${starbaseData.system.name}`, + ) + return { + action: 'none_needed', + message: 'Already registered at starbase', + } + } + + const isCompatible = + starbaseData.owner !== null && starbaseData.owner === player.faction + + if (isCompatible) { + // Starbase is compatible - no recovery needed (will register on dock) + logger.debug( + `${fleetInfo.fleetName}: Recovery - starbase ${starbaseData.system.name} is compatible`, + ) + return { action: 'none_needed', message: 'Starbase is compatible' } + } + + const systemName = starbaseData.system.name ?? 'unknown' + const factionNames = ['Unknown', 'MUD', 'ONI', 'UST'] + const playerFactionName = factionNames[player.faction] ?? 'unknown' + + // If fleet is docked, undock first before we can move + if (fleetInfo.fleetState.kind === 'Docked') { + logger.info( + `${fleetInfo.fleetName}: Undocking from incompatible starbase ${systemName} before recovery`, + ) + + const starbasePlayer = await getStarbasePlayer( + player, + starbaseData.system, + ) + + const undockResult = await undockAction({ + game, + fleet: fleetInfo.fleet, + profileFaction: player.profileFaction, + character: player.character, + system: starbaseData.system, + starbasePlayer: starbasePlayer?.key, + signer: player.signer, + keyIndex: player.keyIndex, + }) + + if (undockResult.success) { + // Undocked successfully - will continue recovery next cycle when fleet is Idle + return { + action: 'moving', + message: `Undocking from ${systemName} - will move next cycle`, + } + } else { + logger.warn( + `${fleetInfo.fleetName}: Failed to undock from ${systemName}`, + ) + return { + action: 'stuck', + message: `Cannot undock from ${systemName}`, + } + } + } + + // Fleet must be Idle to move - if not, skip + if (fleetInfo.fleetState.kind !== 'Idle') { + logger.debug( + `${fleetInfo.fleetName}: Recovery - fleet is ${fleetInfo.fleetState.kind}, waiting for Idle state`, + ) + return { + action: 'none_needed', + message: `Fleet is ${fleetInfo.fleetState.kind}, waiting`, + } + } + + // Starbase is incompatible and fleet is Idle - attempt recovery + if (fuelLevel > 0) { + // Has fuel - find nearest compatible starbase and move there + const nearestCompatible = await findNearestCompatibleStarbase( + starbaseData.system, + player.faction, + ) + + if (nearestCompatible) { + const targetName = nearestCompatible.system.name ?? 'unknown' + const targetCoords = getExactSystemCoordinates( + nearestCompatible.system, + ) + + logger.info( + `${fleetInfo.fleetName}: Recovering from incompatible starbase ${systemName} ` + + `-> moving to ${targetName} (${targetCoords.x},${targetCoords.y})`, + ) + + // Move to the compatible starbase + const regionTracker = await fetchRegionTracker() + const sourceSystem = findSystemByCoordinates( + map, + fleetInfo.location, + ) + const startRegionId = sourceSystem + ? getSystemRegionId(sourceSystem) + : null + const destRegionId = getSystemRegionId(nearestCompatible.system) + + const moved = await moveAction({ + game, + fleet: fleetInfo.fleet, + character: player.character, + regionTracker, + destination: targetCoords.toRawTuple(), + signer: player.signer, + keyIndex: player.keyIndex, + warpMode: 'auto', + startRegionId, + destRegionId, + }) + + if (moved) { + return { + action: 'moving', + message: `Moving to compatible starbase ${targetName}`, + targetStarbase: targetName, + } + } + } + + // No compatible starbase found or move failed - fleet is stuck + // C4 has no self-destruct instruction. Fleet will remain stuck until: + // 1. Starbase is conquered by player's faction, OR + // 2. Fleet is destroyed in combat + logger.warn( + `${fleetInfo.fleetName}: STUCK at incompatible starbase ${systemName} - no ${playerFactionName} starbase reachable. ` + + `Fleet will remain stuck until starbase is conquered or fleet is destroyed in combat.`, + ) + + return { + action: 'stuck', + message: `Stuck at ${systemName} - no compatible starbase reachable`, + } + } + + // No fuel - fleet is stuck + // C4 has no self-destruct instruction + logger.warn( + `${fleetInfo.fleetName}: STUCK at incompatible starbase ${systemName} with no fuel. ` + + `Fleet will remain stuck until starbase is conquered or fleet is destroyed in combat.`, + ) + + return { + action: 'stuck', + message: `Stuck at ${systemName} - no fuel`, + } +} + +/** + * FSM undock adapter + * Returns true if undock was executed, false if validation failed + */ +export async function fsmUndock( + fleetInfo: FleetInfo, + _location: Coordinates, + player: Player, + game: AccountWithKey, +): Promise { + if (fleetInfo.fleetState.kind !== 'Docked') { + // Not docked - nothing to do + return false + } + + const system = fleetInfo.fleetState.data.system + + const starbasePlayer = await getStarbasePlayer(player, system) + + const result = await undockAction({ + game, + fleet: fleetInfo.fleet, + profileFaction: player.profileFaction, + character: player.character, + system, + starbasePlayer: starbasePlayer?.key, + signer: player.signer, + keyIndex: player.keyIndex, + }) + + return result.success +} + +/** + * FSM move adapter + * Returns true if move was executed, false if requirements not met + * Automatically looks up exact starbase coordinates when destination is a star system + */ +export async function fsmMove( + fleetInfo: FleetInfo, + destination: Coordinates, + player: Player, + game: AccountWithKey, + map: WorldMap, + warpMode: WarpMode = 'auto', +): Promise { + const regionTracker = await fetchRegionTracker() + + // Get source system region ID from fleet's current location + const sourceSystem = findSystemByCoordinates(map, fleetInfo.location) + const startRegionId = sourceSystem ? getSystemRegionId(sourceSystem) : null + + // Get destination system region ID and exact coordinates + const destSystem = findSystemByCoordinates(map, destination) + const destRegionId = destSystem ? getSystemRegionId(destSystem) : null + + // Use exact system coordinates if destination is a star system (for docking precision) + // This ensures we move to the exact starbase location instead of rounded coordinates + let exactDestination = destination + if (destSystem && !destination.isRaw) { + exactDestination = getExactSystemCoordinates(destSystem) + logger.debug( + `Using exact starbase coordinates: ${exactDestination.x},${exactDestination.y} (raw)`, + ) + } + + return moveAction({ + game, + fleet: fleetInfo.fleet, + character: player.character, + regionTracker, + destination: exactDestination.toRawTuple(), + signer: player.signer, + keyIndex: player.keyIndex, + warpMode, + startRegionId, + destRegionId, + }) +} + +/** + * FSM mine adapter + */ +export async function fsmMine( + fleetInfo: FleetInfo, + player: Player, + game: AccountWithKey, + mineable: Mineable, +): Promise { + const regionTracker = await fetchRegionTracker() + + return mineAction({ + game, + fleet: fleetInfo.fleet, + character: player.character, + regionTracker, + system: mineable.system, + asteroid: mineable.celestialBody, + signer: player.signer, + keyIndex: player.keyIndex, + }) +} + +/** + * FSM end mine adapter + */ +export async function fsmEndMine( + fleetInfo: FleetInfo, + player: Player, + game: AccountWithKey, + mineable: Mineable, +): Promise { + return endMineAction({ + game, + fleet: fleetInfo.fleet, + character: player.character, + asteroid: mineable.celestialBody, + signer: player.signer, + keyIndex: player.keyIndex, + }) +} + +/** + * FSM self destruct adapter + * NOTE: C4 does not have a self-destruct instruction. + * Fleets can only be disbanded when docked at a friendly starbase. + */ +export async function fsmSelfDestruct( + fleetInfo: FleetInfo, + _player: Player, + _game: AccountWithKey, +): Promise { + logger.warn( + `${fleetInfo.fleetName}: Cannot self-destruct - C4 has no self-destruct instruction. ` + + `Fleet must be docked at a friendly starbase to disband.`, + ) +} + +/** + * FSM end move adapter + */ +export async function fsmEndMove( + fleetInfo: FleetInfo, + player: Player, + game: AccountWithKey, +): Promise { + return endMoveAction({ + game, + fleet: fleetInfo.fleet, + signer: player.signer, + character: player.character, + }) +} + +/** + * FSM stop subwarp adapter + */ +export async function fsmStopSubwarp( + fleetInfo: FleetInfo, + player: Player, + game: AccountWithKey, +): Promise { + const regionTracker = await fetchRegionTracker() + + return stopSubwarpAction({ + game, + fleet: fleetInfo.fleet, + character: player.character, + regionTracker, + signer: player.signer, + keyIndex: player.keyIndex, + }) +} + +/** + * Get cargo ID for a cargo type string or mint address + * Uses game data to resolve mints to cargo IDs + */ +function resolveCargoId( + cargoType: string, + fleetInfo: FleetInfo, + game: AccountWithKey, +): number { + // Get R4 cargo IDs from game.resources + const fuelCargoId = game.resources?.fuel ?? getFuelCargoId(fleetInfo.fleet) + const ammoCargoId = game.resources?.ammo ?? getAmmoCargoId(fleetInfo.fleet) + const foodCargoId = game.resources?.food ?? 2 + const toolCargoId = game.resources?.repairKit ?? 3 + + // Check if cargoType is a known string + switch (cargoType) { + case 'fuel': + return fuelCargoId + case 'ammo': + return ammoCargoId + case 'food': + return foodCargoId + case 'tool': + return toolCargoId + } + + // Try to parse as number + const parsed = parseInt(cargoType, 10) + if (!isNaN(parsed)) { + return parsed + } + + // Try as mint address - look up cargo ID from game + const cargoId = lookupCargoIdFromMint(game, cargoType as Address) + if (cargoId !== undefined) { + // Check if it matches known R4 cargo IDs + if (cargoId === fuelCargoId || cargoId === ammoCargoId) { + return cargoId + } + if (cargoId === foodCargoId || cargoId === toolCargoId) { + return cargoId + } + // Unknown cargo type but found in game + return cargoId + } + + logger.warn(`Unknown cargo type: ${cargoType}, defaulting to cargo ID 0`) + return 0 +} + +/** + * Look up cargo ID from mint address + */ +function lookupCargoIdFromMint( + game: AccountWithKey, + mint: Address, +): number | undefined { + const mintToId = game.cargoDefinitions?.mintToId + if (!mintToId) { + logger.debug( + `lookupCargoIdFromMint: no mintToId in game.cargoDefinitions`, + ) + return undefined + } + + // Handle both Map and array of tuples + if (mintToId instanceof Map) { + const id = mintToId.get(mint) + if (id !== undefined) { + logger.debug( + `lookupCargoIdFromMint: ${mint.slice(0, 8)}... -> cargoId ${id}`, + ) + } + return id + } + // Array of tuples + for (const [m, id] of mintToId as [string, number][]) { + if (m === mint) { + logger.debug( + `lookupCargoIdFromMint: ${mint.slice(0, 8)}... -> cargoId ${id}`, + ) + return id + } + } + logger.debug(`lookupCargoIdFromMint: mint ${mint.slice(0, 8)}... not found`) + return undefined +} + +/** + * FSM load cargo adapter + * Returns true if cargo was loaded, false if not available + */ +export async function fsmLoadCargo( + fleetInfo: FleetInfo, + player: Player, + game: AccountWithKey, + cargoType: string, + amount: number, + _maxAmount?: boolean, +): Promise { + if (fleetInfo.fleetState.kind !== 'Docked') { + throw new Error('Fleet must be docked to load cargo') + } + + const system = fleetInfo.fleetState.data.system + const starbasePlayer = await getStarbasePlayer(player, system) + const cargoId = resolveCargoId(cargoType, fleetInfo, game) + + const result = await loadCargoAction({ + game, + fleet: fleetInfo.fleet, + character: player.character, + profileFaction: player.profileFaction, + system, + starbasePlayer: starbasePlayer?.key, + signer: player.signer, + keyIndex: player.keyIndex, + transfers: [{ cargoId, amount }], + player, // Enable auto-deposit from wallet + fleetName: fleetInfo.fleetName, + }) + + return result.loaded +} + +/** + * FSM unload all cargo adapter + */ +export async function fsmUnloadAllCargo( + fleetInfo: FleetInfo, + _location: Coordinates, + player: Player, + game: AccountWithKey, +): Promise { + if (fleetInfo.fleetState.kind !== 'Docked') { + throw new Error('Fleet must be docked to unload cargo') + } + + const system = fleetInfo.fleetState.data.system + const starbasePlayer = await getStarbasePlayer(player, system) + + return unloadAllCargoAction({ + game, + fleet: fleetInfo.fleet, + character: player.character, + profileFaction: player.profileFaction, + system, + starbasePlayer: starbasePlayer?.key, + signer: player.signer, + keyIndex: player.keyIndex, + }) +} + +// Re-export for FSM files to import +export { type WarpMode } from '../lib/sage/act/move.js' + +// ============================================================================ +// R4 Cargo Helpers (fuel, ammo, food, toolkit) +// ============================================================================ + +/** + * Get cargo IDs for R4 resources (fuel, ammo, food, toolkit) + * Uses game.resources for the cargo IDs + */ +export function getR4CargoIds( + game: AccountWithKey, + fleet?: AccountWithKey, +): Set { + // Get R4 cargo IDs from game.resources + const fuelCargoId = game.resources?.fuel ?? fleet?.fuelTank?.cargoId ?? 0 + const ammoCargoId = game.resources?.ammo ?? fleet?.ammoBank?.cargoId ?? 1 + const foodCargoId = game.resources?.food ?? 2 + const toolCargoId = game.resources?.repairKit ?? 3 + + return new Set([fuelCargoId, ammoCargoId, foodCargoId, toolCargoId]) +} + +/** + * FSM unload R4 cargo only (fuel, ammo, food, toolkit from cargo hold) + * Used by transport to drop off supplies at mining base + * + * In C4, R4 resources are determined by the fleet's fuelTank and ammoBank cargo IDs. + * Food and toolkit share the same cargo hold. + */ +export async function fsmUnloadR4Cargo( + fleetInfo: FleetInfo, + player: Player, + game: AccountWithKey, +): Promise { + if (fleetInfo.fleetState.kind !== 'Docked') { + throw new Error('Fleet must be docked to unload cargo') + } + + // Get R4 cargo IDs from fleet (more reliable than game cargo definitions) + const fuelCargoId = getFuelCargoId(fleetInfo.fleet) + const ammoCargoId = getAmmoCargoId(fleetInfo.fleet) + const r4Ids = new Set([fuelCargoId, ammoCargoId]) + + // Also include food and tool IDs if defined + // In C4, cargoHold stores these as cargo IDs + const r4IdsFromGame = getR4CargoIds(game) + for (const id of r4IdsFromGame) { + r4Ids.add(id) + } + + // Build transfers from actual source compartments: + // - Fuel/ammo: use actual tank/bank amounts (not cargoHold.amounts which may be stale) + // - Food/toolkit: use cargoHold.amounts (they live in the cargo hold) + const transfers: CargoTransfer[] = [] + + // Fuel from fuel tank + const fuelAmount = Number(fleetInfo.fleet.fuelTank?.amount ?? 0n) + if (fuelAmount > 0) { + transfers.push({ cargoId: fuelCargoId, amount: fuelAmount }) + } + + // Ammo from ammo bank + const ammoAmount = Number(fleetInfo.fleet.ammoBank?.amount ?? 0n) + if (ammoAmount > 0) { + transfers.push({ cargoId: ammoCargoId, amount: ammoAmount }) + } + + // Food/toolkit from cargo hold + const cargoAmounts = fleetInfo.fleet.cargoHold?.amounts + if (cargoAmounts) { + for (const [cargoId, amount] of cargoAmounts) { + const id = Array.isArray(cargoId) + ? (cargoId[0] as number) + : Number(cargoId) + // Skip fuel/ammo (already handled from tanks above) + if (id === fuelCargoId || id === ammoCargoId) continue + if (r4Ids.has(id) && Number(amount) > 0) { + transfers.push({ cargoId: id, amount: Number(amount) }) + } + } + } + + if (transfers.length === 0) { + logger.debug(`${fleetInfo.fleetName}: No R4 cargo to unload`) + return true + } + + const system = fleetInfo.fleetState.data.system + const starbasePlayer = await getStarbasePlayer(player, system) + + logger.info( + `${fleetInfo.fleetName}: Unloading R4 cargo: ${transfers.map((t) => `${t.cargoId}x${t.amount}`).join(', ')}`, + ) + + return unloadCargoAction({ + game, + fleet: fleetInfo.fleet, + character: player.character, + profileFaction: player.profileFaction, + system, + starbasePlayer: starbasePlayer?.key, + signer: player.signer, + keyIndex: player.keyIndex, + transfers, + }) +} + +/** + * FSM unload non-R4 cargo (mined resources) + * Used by transport to drop off mined resources at CSS + */ +export async function fsmUnloadNonR4Cargo( + fleetInfo: FleetInfo, + player: Player, + game: AccountWithKey, +): Promise { + if (fleetInfo.fleetState.kind !== 'Docked') { + throw new Error('Fleet must be docked to unload cargo') + } + + const r4Ids = getR4CargoIds(game) + const cargoAmounts = fleetInfo.fleet.cargoHold?.amounts + if (!cargoAmounts || cargoAmounts.size === 0) { + return true + } + + // Filter to only non-R4 cargo (mined resources) + const transfers: CargoTransfer[] = [] + for (const [cargoId, amount] of cargoAmounts) { + if (!r4Ids.has(Number(cargoId)) && Number(amount) > 0) { + transfers.push({ cargoId: Number(cargoId), amount: Number(amount) }) + } + } + + if (transfers.length === 0) { + return true + } + + const system = fleetInfo.fleetState.data.system + const starbasePlayer = await getStarbasePlayer(player, system) + + logger.info( + `${fleetInfo.fleetName}: Unloading mined resources: ${transfers.map((t) => `${t.cargoId}x${t.amount}`).join(', ')}`, + ) + + return unloadCargoAction({ + game, + fleet: fleetInfo.fleet, + character: player.character, + profileFaction: player.profileFaction, + system, + starbasePlayer: starbasePlayer?.key, + signer: player.signer, + keyIndex: player.keyIndex, + transfers, + fromCargoHold: true, + }) +} + +/** + * FSM load mined cargo (non-R4 resources from starbase) + * Used by transport to pick up mined resources at mining base + * Returns true if cargo was loaded + */ +export async function fsmLoadMinedCargo( + fleetInfo: FleetInfo, + player: Player, + game: AccountWithKey, +): Promise { + if (fleetInfo.fleetState.kind !== 'Docked') { + throw new Error('Fleet must be docked to load cargo') + } + + const r4Ids = getR4CargoIds(game) + const system = fleetInfo.fleetState.data.system + const starbasePlayer = await getStarbasePlayer(player, system) + + if (!starbasePlayer) { + return false + } + + // Get cargo at starbase + const starbaseCargo = starbasePlayer.cargoPod?.amounts + if (!starbaseCargo || starbaseCargo.size === 0) { + return false + } + + // Filter to only non-R4 cargo (mined resources) + const transfers: CargoTransfer[] = [] + for (const [cargoId, amount] of starbaseCargo) { + if (!r4Ids.has(Number(cargoId)) && Number(amount) > 0) { + transfers.push({ cargoId: Number(cargoId), amount: Number(amount) }) + } + } + + if (transfers.length === 0) { + return false + } + + // Get available cargo capacity + const cargoCapacity = Number(fleetInfo.cargoStats.cargoCapacity) + const currentCargo = Array.from( + fleetInfo.cargoLevels.cargo.values(), + ).reduce((acc, curr) => acc + Number(curr), 0) + const availableSpace = cargoCapacity - currentCargo + + if (availableSpace <= 0) { + return false + } + + // Limit transfers to available space + let totalToLoad = 0 + const limitedTransfers: CargoTransfer[] = [] + for (const transfer of transfers) { + const remaining = availableSpace - totalToLoad + if (remaining <= 0) break + + const amountToLoad = Math.min(transfer.amount, remaining) + if (amountToLoad > 0) { + limitedTransfers.push({ + cargoId: transfer.cargoId, + amount: amountToLoad, + }) + totalToLoad += amountToLoad + } + } + + if (limitedTransfers.length === 0) { + return false + } + + logger.info( + `${fleetInfo.fleetName}: Loading mined resources: ${limitedTransfers.map((t) => `${t.cargoId}x${t.amount}`).join(', ')}`, + ) + + const result = await loadCargoAction({ + game, + fleet: fleetInfo.fleet, + character: player.character, + profileFaction: player.profileFaction, + system, + starbasePlayer: starbasePlayer?.key, + signer: player.signer, + keyIndex: player.keyIndex, + transfers: limitedTransfers, + player, // Enable availability check + fleetName: fleetInfo.fleetName, + }) + + return result.loaded +} diff --git a/src/main/basedbot/fsm/info.ts b/src/main/basedbot/fsm/info.ts index 75203f21..ba03fe5a 100644 --- a/src/main/basedbot/fsm/info.ts +++ b/src/main/basedbot/fsm/info.ts @@ -10,7 +10,7 @@ import { getName } from '../lib/sage/util.js' import { Strategy } from './strategy.js' const transition = async (fleetInfo: FleetInfo): Promise => { - switch (fleetInfo.fleetState.type) { + switch (fleetInfo.fleetState.kind) { case 'Idle': { const baseStation = await starbaseByCoordinates(fleetInfo.location) const planets = await planetsByCoordinates(fleetInfo.location) @@ -20,61 +20,61 @@ const transition = async (fleetInfo: FleetInfo): Promise => { ) break } - case 'StarbaseLoadingBay': + case 'Docked': logger.info( - `${fleetInfo.fleetName} is in the loading bay at ${getName(fleetInfo.fleetState.data.starbase)}`, + `${fleetInfo.fleetName} is docked at ${getName(fleetInfo.fleetState.data.system)}`, ) break case 'MoveWarp': { - const { fromSector, toSector, warpFinish } = + const { fromCoordinates, toCoordinates, arrivalTime } = fleetInfo.fleetState.data - if (warpFinish.isBefore(now())) { + if (arrivalTime.isBefore(now())) { logger.info( - `${fleetInfo.fleetName} has arrived at ${fleetInfo.fleetState.data.toSector}`, + `${fleetInfo.fleetName} has arrived at ${fleetInfo.fleetState.data.toCoordinates}`, ) } else { logger.info( - `${fleetInfo.fleetName} warping from ${fromSector} to ${toSector}. Arrival in ${dayjs.duration(warpFinish.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, + `${fleetInfo.fleetName} warping from ${fromCoordinates} to ${toCoordinates}. Arrival in ${dayjs.duration(arrivalTime.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, ) } break } case 'MoveSubwarp': { - const { fromSector, toSector, arrivalTime } = + const { fromCoordinates, toCoordinates, arrivalTime } = fleetInfo.fleetState.data if (arrivalTime.isBefore(now())) { logger.info( - `${fleetInfo.fleetName} has arrived at ${fleetInfo.fleetState.data.toSector}`, + `${fleetInfo.fleetName} has arrived at ${fleetInfo.fleetState.data.toCoordinates}`, ) } else { logger.info( - `${fleetInfo.fleetName} subwarping from ${fromSector} to ${toSector}. Arrival in ${dayjs.duration(arrivalTime.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, + `${fleetInfo.fleetName} subwarping from ${fromCoordinates} to ${toCoordinates}. Arrival in ${dayjs.duration(arrivalTime.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, ) } break } case 'MineAsteroid': { - const { mineItem, end, amountMined, endReason } = + const { celestialBody, end, amountMined, endReason } = fleetInfo.fleetState.data if (end.isBefore(now())) { logger.info( - `${fleetInfo.fleetName} has finished mining ${getName(mineItem)} for ${amountMined}`, + `${fleetInfo.fleetName} has finished mining ${getName(celestialBody)} for ${amountMined}`, ) } else { const log = endReason === 'FULL' ? logger.info : logger.warn log( - `${fleetInfo.fleetName} mining ${getName(mineItem)} for ${amountMined}. Time remaining: ${dayjs.duration(end.diff(now())).humanize(false)} until ${endReason}`, + `${fleetInfo.fleetName} mining ${getName(celestialBody)} for ${amountMined}. Time remaining: ${dayjs.duration(end.diff(now())).humanize(false)} until ${endReason}`, ) } break } default: logger.info( - `${fleetInfo.fleetName} is ${fleetInfo.fleetState.type}`, + `${fleetInfo.fleetName} is ${fleetInfo.fleetState.kind}`, ) } diff --git a/src/main/basedbot/fsm/mine.ts b/src/main/basedbot/fsm/mine.ts index 2571c983..d18fe800 100644 --- a/src/main/basedbot/fsm/mine.ts +++ b/src/main/basedbot/fsm/mine.ts @@ -1,78 +1,79 @@ -import { Game } from '@staratlas/sage' +import type { Game } from '@staratlas/dev-sage' import dayjs from 'dayjs' import { now } from '../../../dayjs.js' import { logger } from '../../../logger.js' -import { dock } from '../lib/sage/act/dock.js' -import { endMine } from '../lib/sage/act/end-mine.js' -import { loadCargo } from '../lib/sage/act/load-cargo.js' -import { mine } from '../lib/sage/act/mine.js' -import { move } from '../lib/sage/act/move.js' -import { selfDestruct } from '../lib/sage/act/self-destruct.js' -import { undock } from '../lib/sage/act/undock.js' -import { unloadAllCargo } from '../lib/sage/act/unload-all-cargo.js' import { starbaseByCoordinates } from '../lib/sage/state/starbase-by-coordinates.js' -import { Player } from '../lib/sage/state/user-account.js' -import { FleetInfo } from '../lib/sage/state/user-fleets.js' +import type { Player } from '../lib/sage/state/user-account.js' +import type { FleetInfo } from '../lib/sage/state/user-fleets.js' import { mineableByCoordinates } from '../lib/sage/state/world-map.js' import { getName } from '../lib/sage/util.js' - -import { MineConfig } from './configs/mine/mine-config.js' -import { Strategy } from './strategy.js' +import type { AccountWithKey } from '../lib/types.js' + +import type { MineConfig } from './configs/mine/mine-config.js' +import type { Strategy } from './strategy.js' +import { + fsmDock, + fsmEndMine, + fsmLoadCargo, + fsmMine, + fsmMove, + fsmRecoverFromIncompatibleStarbase, + fsmSelfDestruct, + fsmUndock, + fsmUnloadAllCargo, +} from './fsm-actions.js' const transition = async ( fleetInfo: FleetInfo, player: Player, - game: Game, + game: AccountWithKey, config: MineConfig, ): Promise => { - const cargoLoad = player.cargoTypes - .filter((ct) => !ct.data.mint.equals(game.data.mints.food)) - .reduce((acc, cargoType) => { - const load = - fleetInfo.cargoLevels.cargo.get( - cargoType.data.mint.toBase58(), - ) ?? 0 + // C4: cargo levels are bigint, convert to number for comparisons + const cargoEntries = Array.from(fleetInfo.cargoLevels.cargo.entries()) + const cargoLoad = cargoEntries.reduce( + (acc, [_cargoId, amount]) => acc + Number(amount), + 0, + ) - return acc + load - }, 0) const { homeBase, targetBase, resource, warpMode } = config - const { cargoCapacity } = fleetInfo.cargoStats - const cargoLevelFood = fleetInfo.cargoLevels.food - const cargoLevelAmmo = fleetInfo.cargoLevels.ammo - const cargoLevelFuel = fleetInfo.cargoLevels.fuel - const desiredFood = cargoCapacity / 20 - const fuelReserve = fleetInfo.cargoStats.fuelCapacity + const { cargoCapacity, fuelCapacity, ammoCapacity } = fleetInfo.cargoStats + const cargoLevelFood = 0 // TODO: Get food level from cargo by cargoId + const cargoLevelAmmo = Number(fleetInfo.cargoLevels.ammo) + const cargoLevelFuel = Number(fleetInfo.cargoLevels.fuel) + const desiredFood = Math.floor(Number(cargoCapacity) / 20) + const fuelReserve = Number(fuelCapacity) const toLoad = desiredFood - cargoLevelFood const hasEnoughFood = toLoad <= 10 - const hasEnoughAmmo = - cargoLevelAmmo >= fleetInfo.cargoStats.ammoCapacity - 100 + const hasEnoughAmmo = cargoLevelAmmo >= Number(ammoCapacity) - 100 const hasEnoughFuel = cargoLevelFuel >= fuelReserve - 100 const hasCargo = cargoLoad > 0 const currentStarbase = await starbaseByCoordinates(fleetInfo.location) const { fleetName, location } = fleetInfo - const resourceName = getName(resource.mineItem) - const isAtHomeBase = homeBase.equals(location) - const isAtTargetBase = targetBase.equals(location) + + const resourceName = getName(resource.celestialBody) + // Use nearEquals for fuzzy comparison (within 1 unit) to handle coordinate precision issues + const isAtHomeBase = homeBase.nearEquals(location) + const isAtTargetBase = targetBase.nearEquals(location) const isSameBase = homeBase.equals(targetBase) - logger.info( - `${fleetName} is mining ${getName(config.resource.mineItem)} resources from ${config.targetBase} to ${config.homeBase}`, + logger.debug( + `${fleetName} [mine ${resourceName}] state=${fleetInfo.fleetState.kind}`, ) - switch (fleetInfo.fleetState.type) { + switch (fleetInfo.fleetState.kind) { case 'Idle': { logger.info( - `${fleetName} is idle at ${fleetInfo.fleetState.data.sector} [Starbase: ${currentStarbase ? getName(currentStarbase) : 'N/A'}]`, + `${fleetName} is idle at ${fleetInfo.fleetState.data.sector} [Starbase: ${currentStarbase ? getName(currentStarbase.system) : 'N/A'}]`, ) if (!currentStarbase && cargoLevelFuel < 1) { logger.warn( `${fleetName} is out of fuel and not at a starbase, need self destruction`, ) - - return selfDestruct(fleetInfo, player, game) + return fsmSelfDestruct(fleetInfo, player, game) } if (isAtHomeBase) { logger.info(`${fleetName} is at home base`) @@ -80,26 +81,60 @@ const transition = async ( logger.info( `${fleetName} has ${cargoLoad} ${resourceName}, docking to unload`, ) - - return dock(fleetInfo, location, player, game) + const docked = await fsmDock( + fleetInfo, + location, + player, + game, + ) + if (!docked) { + // Home base became incompatible - recover + await fsmRecoverFromIncompatibleStarbase( + fleetInfo, + player, + game, + config.worldMap, + ) + } + return } if (!hasEnoughFood || !hasEnoughFuel || !hasEnoughAmmo) { logger.info( `${fleetName} doesn't have enough resources, docking to resupply`, ) - - return dock(fleetInfo, location, player, game) + const docked = await fsmDock( + fleetInfo, + location, + player, + game, + ) + if (!docked) { + // Home base became incompatible - recover + await fsmRecoverFromIncompatibleStarbase( + fleetInfo, + player, + game, + config.worldMap, + ) + } + return } if (isSameBase) { logger.info(`${fleetName} is at home/target base, mining`) - - return mine(fleetInfo, player, game, resource) + return fsmMine(fleetInfo, player, game, resource) } logger.info( `${fleetName} is at home base, moving to target base`, ) - - return move(fleetInfo, targetBase, player, game, warpMode) + await fsmMove( + fleetInfo, + targetBase, + player, + game, + config.worldMap, + warpMode, + ) + return } if (isAtTargetBase && !isSameBase) { @@ -108,150 +143,259 @@ const transition = async ( logger.info( `${fleetName} doesn't have enough fuel, docking`, ) - return dock(fleetInfo, location, player, game) + const docked = await fsmDock( + fleetInfo, + location, + player, + game, + ) + if (!docked) { + // Target base became incompatible - recover + await fsmRecoverFromIncompatibleStarbase( + fleetInfo, + player, + game, + config.worldMap, + ) + } + return } if (hasCargo) { logger.info( `${fleetName} has ${cargoLoad} ${resourceName}, returning home`, ) - - return move(fleetInfo, homeBase, player, game, warpMode) + await fsmMove( + fleetInfo, + homeBase, + player, + game, + config.worldMap, + warpMode, + ) + return } if (hasEnoughFood) { logger.info(`${fleetName} has enough food, mining`) - - return mine(fleetInfo, player, game, resource) + return fsmMine(fleetInfo, player, game, resource) } logger.info( `${fleetName} doesn't have enough food, returning home`, ) - - return move(fleetInfo, homeBase, player, game, warpMode) + await fsmMove( + fleetInfo, + homeBase, + player, + game, + config.worldMap, + warpMode, + ) + return } - logger.info(`${fleetName} is at ${location}`) + // Not at home or target - move to appropriate location or recover + if (currentStarbase) { + // At some other starbase - try to dock or recover + const docked = await fsmDock(fleetInfo, location, player, game) + if (!docked && cargoLevelFuel < 1) { + // Can't dock and no fuel - recover + await fsmRecoverFromIncompatibleStarbase( + fleetInfo, + player, + game, + config.worldMap, + ) + return + } + if (docked) { + return // Will handle refuel/unload on next cycle + } + } - return move( + // In space or at compatible starbase with fuel - move to destination + logger.info( + `${fleetName} is at ${location}, moving to ${hasCargo || !hasEnoughFood ? 'home' : 'target'}`, + ) + await fsmMove( fleetInfo, hasCargo || !hasEnoughFood ? homeBase : targetBase, player, game, + config.worldMap, warpMode, ) + return } - case 'StarbaseLoadingBay': { + case 'Docked': { logger.info( - `${fleetInfo.fleetName} is in the loading bay at ${getName(fleetInfo.fleetState.data.starbase)}`, + `${fleetInfo.fleetName} is docked at ${getName(fleetInfo.fleetState.data.system)}`, ) if (hasCargo) { logger.info( `${fleetInfo.fleetName} has ${cargoLoad} cargo, unloading`, ) - - return unloadAllCargo( + const success = await fsmUnloadAllCargo( fleetInfo, fleetInfo.location, player, game, ) + if (!success) { + logger.warn( + `${fleetInfo.fleetName}: Unload cargo failed, will retry next tick`, + ) + } + return } if (!hasEnoughFuel) { - const neededFuel = - fleetInfo.cargoStats.fuelCapacity - cargoLevelFuel - logger.info(`${fleetInfo.fleetName} is refueling ${neededFuel}`) + // Before trying to refuel, check if starbase is compatible + // If at an incompatible starbase (wrong faction), no fuel will ever arrive + const currentStarbase = await starbaseByCoordinates( + fleetInfo.location, + ) + if (currentStarbase) { + const isCompatible = + currentStarbase.owner !== null && + currentStarbase.owner === player.faction + if (!isCompatible) { + const ownerName = + currentStarbase.owner === null + ? 'Unaligned' + : ['Unknown', 'MUD', 'ONI', 'UST'][ + currentStarbase.owner + ] + const myFaction = ['Unknown', 'MUD', 'ONI', 'UST'][ + player.faction + ] + logger.warn( + `${fleetInfo.fleetName}: Docked at incompatible ${ownerName} starbase ` + + `(we are ${myFaction}) - triggering recovery`, + ) + await fsmRecoverFromIncompatibleStarbase( + fleetInfo, + player, + game, + config.worldMap, + ) + return + } + } - return loadCargo( + const neededFuel = + Number(fleetInfo.cargoStats.fuelCapacity) - cargoLevelFuel + const fuelLoaded = await fsmLoadCargo( fleetInfo, player, game, - game.data.mints.fuel, + 'fuel', neededFuel, ) + if (!fuelLoaded) { + logger.warn( + `${fleetInfo.fleetName}: Waiting for fuel at ${fleetInfo.fleetState.data.system.name}`, + ) + } + return } if (!hasEnoughAmmo && isAtHomeBase) { const neededAmmo = - fleetInfo.cargoStats.ammoCapacity - cargoLevelAmmo - logger.info(`${fleetInfo.fleetName} is rearming ${neededAmmo}`) - - return loadCargo( + Number(fleetInfo.cargoStats.ammoCapacity) - cargoLevelAmmo + const ammoLoaded = await fsmLoadCargo( fleetInfo, player, game, - game.data.mints.ammo, + 'ammo', neededAmmo, ) + if (!ammoLoaded) { + logger.warn( + `${fleetInfo.fleetName}: Waiting for ammo at ${fleetInfo.fleetState.data.system.name}`, + ) + } + return } if (!hasEnoughFood && isAtHomeBase) { - logger.info( - `${fleetInfo.fleetName} is loading ${desiredFood - cargoLevelFood} food`, - ) - - return loadCargo( + const foodLoaded = await fsmLoadCargo( fleetInfo, player, game, - game.data.mints.food, + 'food', toLoad, ) + if (!foodLoaded) { + logger.warn( + `${fleetInfo.fleetName}: Waiting for food at ${fleetInfo.fleetState.data.system.name}`, + ) + } + return } - return undock(fleetInfo.fleet, fleetInfo.location, player, game) + await fsmUndock(fleetInfo, fleetInfo.location, player, game) + return } case 'MoveWarp': { - const { fromSector, toSector, warpFinish } = + const { fromCoordinates, toCoordinates, arrivalTime } = fleetInfo.fleetState.data - if (warpFinish.isBefore(now())) { + if (arrivalTime.isBefore(now())) { logger.info( - `${fleetInfo.fleetName} has arrived at ${fleetInfo.fleetState.data.toSector}`, + `${fleetInfo.fleetName} has arrived at ${toCoordinates}`, ) } else { logger.info( - `${fleetInfo.fleetName} warping from ${fromSector} to ${toSector}. Arrival in ${dayjs.duration(warpFinish.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, + `${fleetInfo.fleetName} warping from ${fromCoordinates} to ${toCoordinates}. Arrival in ${dayjs.duration(arrivalTime.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, ) } break } case 'MoveSubwarp': { - const { fromSector, toSector, arrivalTime } = + const { fromCoordinates, toCoordinates, arrivalTime } = fleetInfo.fleetState.data if (arrivalTime.isBefore(now())) { logger.info( - `${fleetInfo.fleetName} has arrived at ${fleetInfo.fleetState.data.toSector}`, + `${fleetInfo.fleetName} has arrived at ${toCoordinates}`, ) } else { logger.info( - `${fleetInfo.fleetName} subwarping from ${fromSector} to ${toSector}. Arrival in ${dayjs.duration(arrivalTime.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, + `${fleetInfo.fleetName} subwarping from ${fromCoordinates} to ${toCoordinates}. Arrival in ${dayjs.duration(arrivalTime.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, ) } break } case 'MineAsteroid': { - const { mineItem, end, amountMined, endReason } = + const { celestialBody, end, amountMined, endReason } = fleetInfo.fleetState.data + const celestialBodyName = getName(celestialBody) + if (end.isBefore(now())) { logger.info( - `${fleetInfo.fleetName} has finished mining ${getName(mineItem)} for ${amountMined}`, + `${fleetInfo.fleetName} has finished mining ${celestialBodyName} for ${amountMined}`, ) - const resource = mineableByCoordinates( + const mineable = mineableByCoordinates( config.worldMap, fleetInfo.location, - getName(mineItem), + celestialBodyName, ) - return endMine(fleetInfo, player, game, resource) + if (!mineable) { + logger.error( + `No mineable found at ${fleetInfo.location} for ${celestialBodyName}`, + ) + return Promise.resolve() + } + + return fsmEndMine(fleetInfo, player, game, mineable) } const log = endReason === 'FULL' ? logger.info : logger.warn log( - `${fleetInfo.fleetName} mining ${getName(mineItem)} for ${amountMined}. Time remaining: ${dayjs.duration(end.diff(now())).humanize(false)} until ${endReason}`, + `${fleetInfo.fleetName} mining ${celestialBodyName} for ${amountMined}. Time remaining: ${dayjs.duration(end.diff(now())).humanize(false)} until ${endReason}`, ) break @@ -269,10 +413,7 @@ const transition = async ( break } default: - logger.info( - `${fleetInfo.fleetName} is ${fleetInfo.fleetState.type}`, - ) - + logger.info(`${fleetInfo.fleetName} is in unknown state`) return Promise.resolve() } } @@ -280,7 +421,7 @@ const transition = async ( export const createMiningStrategy = ( miningConfig: MineConfig, player: Player, - game: Game, + game: AccountWithKey, ): Strategy => { const config = miningConfig diff --git a/src/main/basedbot/fsm/transport.ts b/src/main/basedbot/fsm/transport.ts index be809553..5205c1a3 100644 --- a/src/main/basedbot/fsm/transport.ts +++ b/src/main/basedbot/fsm/transport.ts @@ -1,97 +1,114 @@ -import { PublicKey } from '@solana/web3.js' -import { Game } from '@staratlas/sage' -import BN from 'bn.js' +import type { Game } from '@staratlas/dev-sage' import dayjs from 'dayjs' import { now } from '../../../dayjs.js' import { logger } from '../../../logger.js' -import { Resource } from '../../../service/wallet/index.js' -import { getTokenBalance } from '../basedbot.js' -import { dock } from '../lib/sage/act/dock.js' -import { loadCargo } from '../lib/sage/act/load-cargo.js' -import { move, WarpMode } from '../lib/sage/act/move.js' -import { selfDestruct } from '../lib/sage/act/self-destruct.js' -import { undock } from '../lib/sage/act/undock.js' -import { getHold, unloadCargo } from '../lib/sage/act/unload-cargo.js' +import type { Address } from '@solana/kit' import { starbaseByCoordinates } from '../lib/sage/state/starbase-by-coordinates.js' -import { Player } from '../lib/sage/state/user-account.js' -import { FleetInfo } from '../lib/sage/state/user-fleets.js' -import { WorldMap } from '../lib/sage/state/world-map.js' +import type { Player } from '../lib/sage/state/user-account.js' +import type { FleetInfo } from '../lib/sage/state/user-fleets.js' +import type { WorldMap } from '../lib/sage/state/world-map.js' import { getName } from '../lib/sage/util.js' -import { Coordinates } from '../lib/util/coordinates.js' +import type { AccountWithKey } from '../lib/types.js' +import type { Coordinates } from '../lib/util/coordinates.js' import { getFuelConsumption } from '../lib/util/fuel-consumption.js' -import { Strategy } from './strategy.js' +import type { Strategy } from './strategy.js' +import { + fsmDock, + fsmLoadCargo, + fsmLoadMinedCargo, + fsmMove, + fsmRecoverFromIncompatibleStarbase, + fsmUndock, + fsmUnloadNonR4Cargo, + fsmUnloadR4Cargo, + type WarpMode, +} from './fsm-actions.js' const transition = async ( fleetInfo: FleetInfo, player: Player, - game: Game, + game: AccountWithKey, config: TransportConfig, ): Promise => { - const cargoLoad = player.cargoTypes.reduce((acc, cargoType) => { - const load = - fleetInfo.cargoLevels.cargo.get(cargoType.data.mint.toBase58()) ?? 0 - - return acc + load - }, 0) + // C4: Convert bigint to number for comparisons + const cargoEntries = Array.from(fleetInfo.cargoLevels.cargo.entries()) + const cargoLoad = cargoEntries.reduce( + (acc, [_cargoId, amount]) => acc + Number(amount), + 0, + ) const { homeBase, targetBase, resources, warpMode } = config const fuelConsumption = getFuelConsumption(homeBase, targetBase, fleetInfo) - const { cargoCapacity } = fleetInfo.cargoStats - const cargoLevelFuel = fleetInfo.cargoLevels.fuel - const cargoLevelAmmo = fleetInfo.cargoLevels.ammo + const cargoCapacity = Number(fleetInfo.cargoStats.cargoCapacity) + const cargoLevelFuel = Number(fleetInfo.cargoLevels.fuel) + const cargoLevelAmmo = Number(fleetInfo.cargoLevels.ammo) const cargoLevel = Array.from(fleetInfo.cargoLevels.cargo.values()).reduce( - (acc, curr) => acc + curr, + (acc, curr) => acc + Number(curr), 0, ) const fuelReserve = Math.ceil(fuelConsumption.auto * 1.1) - const ammoReserve = fleetInfo.cargoStats.ammoCapacity + const ammoReserve = Number(fleetInfo.cargoStats.ammoCapacity) const hasEnoughFuel = cargoLevelFuel >= fuelReserve const hasEnoughAmmo = cargoLevelAmmo >= ammoReserve const hasCargo = cargoLoad > 0 const currentStarbase = await starbaseByCoordinates(fleetInfo.location) const { fleetName, location } = fleetInfo - const isAtHomeBase = homeBase.equals(location) - const isAtTargetBase = targetBase.equals(location) + // Use nearEquals for fuzzy comparison (within 1 unit) to handle coordinate precision issues + const isAtHomeBase = homeBase.nearEquals(location) + const isAtTargetBase = targetBase.nearEquals(location) const isSameBase = homeBase.equals(targetBase) - logger.info( - `${fleetName} is transporting ${config.resources.size} resources from ${config.homeBase} to ${config.targetBase}`, - ) + logger.debug(`${fleetName} [transport] state=${fleetInfo.fleetState.kind}`) - switch (fleetInfo.fleetState.type) { + switch (fleetInfo.fleetState.kind) { case 'Idle': { logger.info( - `${fleetName} is idle at ${fleetInfo.fleetState.data.sector} [Starbase: ${currentStarbase ? getName(currentStarbase) : 'N/A'}]`, + `${fleetName} is idle at ${fleetInfo.fleetState.data.sector} [Starbase: ${currentStarbase ? getName(currentStarbase.system) : 'N/A'}]`, ) if (!currentStarbase && cargoLevelFuel < 1) { logger.warn( - `${fleetName} is out of fuel and not at a starbase, need self destruction`, + `${fleetName}: STUCK - out of fuel and not at a starbase. ` + + `Fleet will remain stuck until destroyed in combat.`, ) - - return selfDestruct(fleetInfo, player, game) + return } if (isSameBase) { logger.warn( `${fleetName} is configured as transport fleet with home and target being the same. Idling....`, ) - return Promise.resolve() } if (isAtHomeBase) { logger.info(`${fleetName} is at home base`) if (hasEnoughAmmo && hasEnoughFuel && hasCargo) { logger.info('Ready to go! Moving to target base') - - return move(fleetInfo, targetBase, player, game, warpMode) + await fsmMove( + fleetInfo, + targetBase, + player, + game, + config.map, + warpMode, + ) + return } logger.info(`${fleetName} is docking to resupply`) - - return dock(fleetInfo, location, player, game) + const docked = await fsmDock(fleetInfo, location, player, game) + if (!docked) { + // Home base became incompatible (conquered) - recover + await fsmRecoverFromIncompatibleStarbase( + fleetInfo, + player, + game, + config.map, + ) + } + return } if (isAtTargetBase) { @@ -101,226 +118,316 @@ const transition = async ( logger.info( `${fleetName} docking to unload ${cargoLoad} cargo`, ) - return dock(fleetInfo, location, player, game) + const docked = await fsmDock( + fleetInfo, + location, + player, + game, + ) + if (!docked) { + // Target base became incompatible - recover + await fsmRecoverFromIncompatibleStarbase( + fleetInfo, + player, + game, + config.map, + ) + } + return } if (!hasEnoughFuel) { logger.info(`${fleetName} docking to refuel.`) - - return dock(fleetInfo, location, player, game) + const docked = await fsmDock( + fleetInfo, + location, + player, + game, + ) + if (!docked) { + // Target base became incompatible - recover + await fsmRecoverFromIncompatibleStarbase( + fleetInfo, + player, + game, + config.map, + ) + } + return } if (hasEnoughFuel && !hasCargo) { - logger.info('Ready to go! Moving to target base') + logger.info('Ready to go! Moving home') + await fsmMove( + fleetInfo, + homeBase, + player, + game, + config.map, + warpMode, + ) + return + } + } - return move(fleetInfo, homeBase, player, game, warpMode) + // Fleet is at neither home nor target base - redirect to correct destination + // This handles fleets that ended up at wrong coordinates due to rounding + if (currentStarbase) { + if (hasCargo) { + // Has cargo - deliver to target + logger.info( + `${fleetName} is at wrong starbase (${location.x},${location.y}), redirecting to target (${targetBase.x},${targetBase.y})`, + ) + await fsmMove( + fleetInfo, + targetBase, + player, + game, + config.map, + warpMode, + ) + return } + + if (hasEnoughFuel) { + // No cargo but has fuel - return home to pick up cargo + logger.info( + `${fleetName} is at wrong starbase (${location.x},${location.y}), redirecting to home (${homeBase.x},${homeBase.y})`, + ) + await fsmMove( + fleetInfo, + homeBase, + player, + game, + config.map, + warpMode, + ) + return + } + + // At wrong starbase without enough fuel - try to dock to refuel + const docked = await fsmDock(fleetInfo, location, player, game) + if (!docked) { + // Can't dock - attempt recovery (find compatible starbase) + const recovery = await fsmRecoverFromIncompatibleStarbase( + fleetInfo, + player, + game, + config.map, + ) + if (recovery.action === 'stuck') { + logger.warn( + `${fleetName}: ${recovery.message} - fleet needs manual intervention`, + ) + } + } + return } - logger.warn(`${fleetName} doesn't know what to do`) + // Not at any starbase - move home + if (hasEnoughFuel) { + logger.info( + `${fleetName} is in space at (${location.x},${location.y}), moving home`, + ) + await fsmMove( + fleetInfo, + homeBase, + player, + game, + config.map, + warpMode, + ) + return + } - return Promise.resolve() + // Stranded with no fuel - cannot self-destruct in C4 + logger.warn( + `${fleetName}: STUCK at (${location.x},${location.y}) with no fuel. ` + + `Fleet will remain stuck until destroyed in combat.`, + ) + return } - case 'StarbaseLoadingBay': { + case 'Docked': { logger.info( - `${fleetInfo.fleetName} is in the loading bay at ${getName(fleetInfo.fleetState.data.starbase)}`, + `${fleetInfo.fleetName} is docked at ${getName(fleetInfo.fleetState.data.system)}`, ) if (isAtHomeBase) { - if (!hasEnoughFuel) { - const neededFuel = - fleetInfo.cargoStats.fuelCapacity - cargoLevelFuel + // First: unload any mined resources we brought back + if (hasCargo) { logger.info( - `${fleetInfo.fleetName} is refueling ${neededFuel}`, + `${fleetInfo.fleetName} unloading mined resources at CSS`, ) + const success = await fsmUnloadNonR4Cargo( + fleetInfo, + player, + game, + ) + if (!success) { + logger.warn( + `${fleetInfo.fleetName}: Unload mined resources failed, will retry next tick`, + ) + } + // Re-evaluate state after unload + return + } - await loadCargo( + // Then: refuel if needed + if (!hasEnoughFuel) { + const neededFuel = + Number(fleetInfo.cargoStats.fuelCapacity) - + cargoLevelFuel + const loaded = await fsmLoadCargo( fleetInfo, player, game, - game.data.mints.fuel, + 'fuel', neededFuel, ) + if (!loaded) { + logger.warn( + `${fleetName}: Waiting for fuel at home base`, + ) + } + return } + + // Then: rearm if needed if (!hasEnoughAmmo) { const neededAmmo = - fleetInfo.cargoStats.ammoCapacity - cargoLevelAmmo - logger.info( - `${fleetInfo.fleetName} is rearming ${neededAmmo}`, - ) - - await loadCargo( + Number(fleetInfo.cargoStats.ammoCapacity) - + cargoLevelAmmo + const loaded = await fsmLoadCargo( fleetInfo, player, game, - game.data.mints.ammo, + 'ammo', neededAmmo, ) + if (!loaded) { + logger.warn( + `${fleetName}: Waiting for ammo at home base`, + ) + } + return } - if (!hasCargo) { - logger.info(`Loading ${Array.from(resources).length} cargo`) - const cargoResources = Array.from(resources).filter( - (resource) => !resource.equals(game.data.mints.fuel), + // Then: load R4 supplies for delivery + const cargoResources = Array.from(resources) + let anyLoaded = false + + for (const resource of cargoResources) { + const count = Math.floor( + (cargoCapacity - cargoLevel) / cargoResources.length, ) + if (count > 0) { + const loaded = await fsmLoadCargo( + fleetInfo, + player, + game, + String(resource), + count, + true, + ) + if (loaded) anyLoaded = true + } + } - await Promise.all( - cargoResources.map((resource) => { - const count = Math.floor( - (cargoCapacity - cargoLevel) / - Array.from(cargoResources).length, - ) - - logger.info( - `Loading ${count} ${resource.toBase58()}`, - ) - - return loadCargo( - fleetInfo, - player, - game, - resource, - count, - true, - ) - }), + if (!anyLoaded) { + logger.warn( + `${fleetName}: No R4 supplies available at home base`, ) + return } logger.info(`${fleetName} is undocking...`) - - return undock(fleetInfo.fleet, fleetInfo.location, player, game) + await fsmUndock(fleetInfo, fleetInfo.location, player, game) + return } if (isAtTargetBase) { + // First: unload R4 supplies we brought if (hasCargo) { - logger.info( - `Unloading ${Array.from(resources).length} cargo`, + const success = await fsmUnloadR4Cargo( + fleetInfo, + player, + game, ) - - //Unload Fuel - - const unload = async (mint: PublicKey): Promise => { - const fleetCargoPod = getHold(mint, game, fleetInfo) - const amount = await getTokenBalance( - fleetCargoPod, - mint, - ) - - const toUnload = amount.sub(new BN(fuelReserve)) - - if (toUnload.lte(new BN(0))) { - return Promise.resolve() - } - - logger.info(`Unloading ${toUnload} ${mint}`) - - await unloadCargo( - fleetInfo, - player, - game, - mint, - toUnload, + if (!success) { + logger.warn( + `${fleetInfo.fleetName}: Unload R4 cargo failed, will retry next tick`, ) } - - await Promise.all([ - unload(game.data.mints.ammo), - unload(game.data.mints.fuel), - ]) - - const cargoResources = Array.from(resources).filter( - (resource) => !resource.equals(game.data.mints.fuel), - ) - - await Promise.all( - Array.from(cargoResources).map(async (resource) => { - const fleetCargoPod = fleetInfo.fleet.data.cargoHold - const amount = await getTokenBalance( - fleetCargoPod, - resource, - ) - - logger.info(`Unloading ${amount} ${resource}`) - - if (amount.eq(new BN(0))) { - return Promise.resolve() - } - - logger.info( - `Unloading ${amount} ${resource.toBase58()}`, - ) - - await unloadCargo( - fleetInfo, - player, - game, - resource, - amount, - true, - ) - }), - ) + // Re-evaluate state after unload + return } + // Then: load any mined resources at this starbase + await fsmLoadMinedCargo(fleetInfo, player, game) + + // Then: refuel if needed for return trip if (!hasEnoughFuel) { const neededFuel = fuelReserve - cargoLevelFuel - logger.info( - `${fleetInfo.fleetName} is refueling ${neededFuel}`, - ) - - await loadCargo( + const loaded = await fsmLoadCargo( fleetInfo, player, game, - game.data.mints.fuel, + 'fuel', neededFuel, ) + if (!loaded) { + logger.warn( + `${fleetName}: Waiting for fuel at mining base`, + ) + } + return } - } - logger.info(`${fleetName} is undocking for take off`) + // Ready to return home + await fsmUndock(fleetInfo, fleetInfo.location, player, game) + return + } - return undock(fleetInfo.fleet, fleetInfo.location, player, game) + // Unknown location while docked + logger.warn(`${fleetName} is docked at unknown location`) + await fsmUndock(fleetInfo, fleetInfo.location, player, game) + return } case 'MoveWarp': { - const { fromSector, toSector, warpFinish } = + const { fromCoordinates, toCoordinates, arrivalTime } = fleetInfo.fleetState.data - if (warpFinish.isBefore(now())) { + if (arrivalTime.isBefore(now())) { logger.info( - `${fleetInfo.fleetName} has arrived at ${fleetInfo.fleetState.data.toSector}`, + `${fleetInfo.fleetName} has arrived at ${toCoordinates}`, ) } else { logger.info( - `${fleetInfo.fleetName} warping from ${fromSector} to ${toSector}. Arrival in ${dayjs.duration(warpFinish.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, + `${fleetInfo.fleetName} warping from ${fromCoordinates} to ${toCoordinates}. Arrival in ${dayjs.duration(arrivalTime.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, ) } break } case 'MoveSubwarp': { - const { fromSector, toSector, arrivalTime } = + const { fromCoordinates, toCoordinates, arrivalTime } = fleetInfo.fleetState.data if (arrivalTime.isBefore(now())) { logger.info( - `${fleetInfo.fleetName} has arrived at ${fleetInfo.fleetState.data.toSector}`, + `${fleetInfo.fleetName} has arrived at ${toCoordinates}`, ) } else { logger.info( - `${fleetInfo.fleetName} subwarping from ${fromSector} to ${toSector}. Arrival in ${dayjs.duration(arrivalTime.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, + `${fleetInfo.fleetName} subwarping from ${fromCoordinates} to ${toCoordinates}. Arrival in ${dayjs.duration(arrivalTime.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, ) } break } case 'MineAsteroid': { - //TODO: Gather 'Mineable' in order to call `endMine` - // return endMine(fleetInfo, player, game, config.resource) logger.warn( `${fleetInfo.fleetName} is currently mining, need to end mine manually.`, ) - return Promise.resolve() } case 'Respawn': { @@ -336,10 +443,7 @@ const transition = async ( break } default: - logger.info( - `${fleetInfo.fleetName} is ${fleetInfo.fleetState.type}`, - ) - + logger.info(`${fleetInfo.fleetName} is in unknown state`) return Promise.resolve() } } @@ -348,7 +452,7 @@ export type TransportConfig = { map: WorldMap homeBase: Coordinates targetBase: Coordinates - resources: Set + resources: Set
warpMode: WarpMode } @@ -357,7 +461,7 @@ export const transportConfig = ( map: WorldMap homeBase: Coordinates targetBase: Coordinates - resources: Set + resources: Set
}, ): TransportConfig => ({ map: config.map, @@ -371,7 +475,7 @@ export const transport = ( map: WorldMap, homeBase: Coordinates, targetBase: Coordinates, - resources: Set, + resources: Set
, ): TransportConfig => transportConfig({ map, @@ -384,7 +488,7 @@ export const transport = ( export const createTransportStrategy = ( config: TransportConfig, player: Player, - game: Game, + game: AccountWithKey, ): Strategy => { return { apply: (fleetInfo: FleetInfo): Promise => diff --git a/src/main/basedbot/lib/account-fetcher.ts b/src/main/basedbot/lib/account-fetcher.ts new file mode 100644 index 00000000..0744997d --- /dev/null +++ b/src/main/basedbot/lib/account-fetcher.ts @@ -0,0 +1,94 @@ +import { decodeAccount, fetchEncodedAccount, type Address } from '@solana/kit' + +import { connection } from '../../../service/sol/index.js' +import { + ACCOUNT_REGISTRY, + type AccountConfig, + type AccountTypeMap, + type AccountTypeName, +} from './account-registry.js' +import type { AccountWithKey } from './types.js' + +/** + * Fetch all accounts of a specific type using discriminator-based filtering + */ +export async function fetchAllAccounts( + accountType: T, +): Promise[]> { + const config = ACCOUNT_REGISTRY[accountType] as AccountConfig< + AccountTypeMap[T] + > + + const factoryFn = connection.getAccountsFactory( + config.programAddress, + config.discriminator, + config.decoder(), + ) + + const accounts = await factoryFn() + + return accounts + .filter((acc) => acc.exists) + .map((acc) => ({ + ...(acc.data as AccountTypeMap[T]), + key: acc.address as Address, + })) +} + +/** + * Fetch a single account by address using getAccountInfo (single RPC call) + */ +export async function fetchAccount( + accountType: T, + accountAddress: Address, +): Promise | null> { + const config = ACCOUNT_REGISTRY[accountType] as AccountConfig< + AccountTypeMap[T] + > + + const maybeAccount = await fetchEncodedAccount( + connection.rpc, + accountAddress, + ) + + if (!maybeAccount.exists) { + return null + } + + const decoded = decodeAccount(maybeAccount, config.decoder()) + + return { + ...(decoded.data as AccountTypeMap[T]), + key: accountAddress, + } +} + +/** + * Fetch the Game account (singleton) + */ +export async function fetchGame(): Promise< + AccountWithKey +> { + const games = await fetchAllAccounts('Game') + + if (games.length === 0) { + throw new Error('No Game account found') + } + + return games[0] +} + +/** + * Fetch RegionTracker account (singleton) + */ +export async function fetchRegionTracker(): Promise< + AccountWithKey +> { + const trackers = await fetchAllAccounts('RegionTracker') + + if (trackers.length === 0) { + throw new Error('No RegionTracker account found') + } + + return trackers[0] +} diff --git a/src/main/basedbot/lib/account-registry.ts b/src/main/basedbot/lib/account-registry.ts new file mode 100644 index 00000000..4c86bd64 --- /dev/null +++ b/src/main/basedbot/lib/account-registry.ts @@ -0,0 +1,123 @@ +import type { Address, Decoder } from '@solana/kit' +import { + PLAYER_NAME_DISCRIMINATOR, + PLAYER_PROFILE_PROGRAM_ADDRESS, + PROFILE_DISCRIMINATOR, + type PlayerName, + type Profile, + getPlayerNameDecoder, + getProfileDecoder, +} from '@staratlas/dev-player-profile' +import { + PROFILE_FACTION_ACCOUNT_DISCRIMINATOR, + PROFILE_FACTION_PROGRAM_ADDRESS, + type ProfileFactionAccount, + getProfileFactionAccountDecoder, +} from '@staratlas/dev-profile-faction' +import { + CELESTIAL_BODY_DISCRIMINATOR, + CHARACTER_DISCRIMINATOR, + FLEET_DISCRIMINATOR, + GAME_DISCRIMINATOR, + REGION_TRACKER_DISCRIMINATOR, + SAGE_PROGRAM_ADDRESS, + STARBASE_PLAYER_DISCRIMINATOR, + STAR_SYSTEM_DISCRIMINATOR, + type CelestialBody, + type Character, + type Fleet, + type Game, + type RegionTracker, + type StarbasePlayer, + type StarSystem, + getCelestialBodyDecoder, + getCharacterDecoder, + getFleetDecoder, + getGameDecoder, + getRegionTrackerDecoder, + getStarbasePlayerDecoder, + getStarSystemDecoder, +} from '@staratlas/dev-sage' + +export interface AccountConfig { + programAddress: Address + discriminator: Uint8Array + decoder: () => Decoder +} + +export const ACCOUNT_REGISTRY = { + Profile: { + programAddress: PLAYER_PROFILE_PROGRAM_ADDRESS, + discriminator: PROFILE_DISCRIMINATOR, + decoder: getProfileDecoder, + }, + + PlayerName: { + programAddress: PLAYER_PROFILE_PROGRAM_ADDRESS, + discriminator: PLAYER_NAME_DISCRIMINATOR, + decoder: getPlayerNameDecoder, + }, + + ProfileFactionAccount: { + programAddress: PROFILE_FACTION_PROGRAM_ADDRESS, + discriminator: PROFILE_FACTION_ACCOUNT_DISCRIMINATOR, + decoder: getProfileFactionAccountDecoder, + }, + + Character: { + programAddress: SAGE_PROGRAM_ADDRESS, + discriminator: CHARACTER_DISCRIMINATOR, + decoder: getCharacterDecoder, + }, + + StarbasePlayer: { + programAddress: SAGE_PROGRAM_ADDRESS, + discriminator: STARBASE_PLAYER_DISCRIMINATOR, + decoder: getStarbasePlayerDecoder, + }, + + Game: { + programAddress: SAGE_PROGRAM_ADDRESS, + discriminator: GAME_DISCRIMINATOR, + decoder: getGameDecoder, + }, + + RegionTracker: { + programAddress: SAGE_PROGRAM_ADDRESS, + discriminator: REGION_TRACKER_DISCRIMINATOR, + decoder: getRegionTrackerDecoder, + }, + + StarSystem: { + programAddress: SAGE_PROGRAM_ADDRESS, + discriminator: STAR_SYSTEM_DISCRIMINATOR, + decoder: getStarSystemDecoder, + }, + + CelestialBody: { + programAddress: SAGE_PROGRAM_ADDRESS, + discriminator: CELESTIAL_BODY_DISCRIMINATOR, + decoder: getCelestialBodyDecoder, + }, + + Fleet: { + programAddress: SAGE_PROGRAM_ADDRESS, + discriminator: FLEET_DISCRIMINATOR, + decoder: getFleetDecoder, + }, +} as const + +export type AccountTypeMap = { + Profile: Profile + PlayerName: PlayerName + ProfileFactionAccount: ProfileFactionAccount + Character: Character + StarbasePlayer: StarbasePlayer + Game: Game + RegionTracker: RegionTracker + StarSystem: StarSystem + CelestialBody: CelestialBody + Fleet: Fleet +} + +export type AccountTypeName = keyof typeof ACCOUNT_REGISTRY diff --git a/src/main/basedbot/lib/fleet-state/fleet-state.ts b/src/main/basedbot/lib/fleet-state/fleet-state.ts index c4f72c5b..e2fff28d 100644 --- a/src/main/basedbot/lib/fleet-state/fleet-state.ts +++ b/src/main/basedbot/lib/fleet-state/fleet-state.ts @@ -1,168 +1,111 @@ -import { CargoStats, Fleet, MiscStats } from '@staratlas/sage' -import Big from 'big.js' -import BN from 'bn.js' +import type { Fleet } from '@staratlas/dev-sage' import { now } from '../../../../dayjs.js' -import { FleetCargo } from '../sage/state/fleet-cargo.js' -import { planetByKey } from '../sage/state/planet-by-key.js' -import { starbaseByKey } from '../sage/state/starbase-by-key.js' -import { WorldMap } from '../sage/state/world-map.js' +import { logger } from '../../../../logger.js' +import { fetchAccount } from '../account-fetcher.js' +import type { AccountWithKey } from '../types.js' import { Coordinates } from '../util/coordinates.js' import { transformSector } from './transform/transform-sector.js' import { transformTime } from './transform/transform-time.js' -import { isIdleData } from './type-guard/idle.js' -import { isMineAsteroidData } from './type-guard/mine-asteroid.js' -import { isMoveSubWarpData } from './type-guard/move-sub-warp.js' -import { isMoveWarpData } from './type-guard/move-warp.js' -import { isRespawnData } from './type-guard/respawn.js' -import { isStarbaseLoadingBayData } from './type-guard/starbase-loading-bay.js' import { - EndReason, - FleetState, - FleetStateType, - RawMineAsteroidData, + type EndReason, + type FleetState, + type FleetStateKind, } from './types.js' -const toBig = (bn: BN): Big => new Big(bn.toString()) -const toBN = (bigInt: Big): BN => new BN(bigInt.toString()) +/** + * Safely convert a value to BigInt, handling floating point numbers + * by truncating to integer first. C4 SDK sometimes returns floats for timestamps. + */ +function toBigInt(value: bigint | number): bigint { + if (typeof value === 'bigint') { + return value + } + // Truncate to integer before converting to BigInt + return BigInt(Math.trunc(value)) +} +/** + * Calculate current position during movement + */ const calculateCurrentPosition = ( startPos: Coordinates, destPos: Coordinates, - startTime: BN, - endTime: BN, - currentTime: BN, + startTime: bigint, + endTime: bigint, + currentTime: bigint, ): Coordinates => { - if (currentTime.gte(endTime)) { + if (currentTime >= endTime) { return destPos - } else if (currentTime.lte(startTime)) { + } else if (currentTime <= startTime) { return startPos } - const totalTime = toBig(endTime.sub(startTime)) - const elapsedTime = toBig(currentTime.sub(startTime)) - const ratio = elapsedTime.div(totalTime) + const totalTime = Number(endTime - startTime) + const elapsedTime = Number(currentTime - startTime) + const ratio = elapsedTime / totalTime - const xDifference = toBig(destPos.xBN.sub(startPos.xBN)) - const yDifference = toBig(destPos.yBN.sub(startPos.yBN)) + const xDiff = destPos.x - startPos.x + const yDiff = destPos.y - startPos.y - const xt = toBig(startPos.xBN).add(xDifference.mul(ratio)) - const yt = toBig(startPos.yBN).add(yDifference.mul(ratio)) + const newX = Math.round(startPos.x + xDiff * ratio) + const newY = Math.round(startPos.y + yDiff * ratio) - return Coordinates.fromBN(toBN(xt.round(0, 1)), toBN(yt.round(0, 1))) + return Coordinates.fromNumber(newX, newY) } -type MiningStats = { - startTime: BN - endTime: BN - cargoLevel: number - miningRate: number - amountMined: number - ammoRequired: number - foodRequired: number - ammoConsumptionRate: number - foodConsumptionRate: number - ammoLevel: number - foodLevel: number - endReason: EndReason - maxMiningDuration: number - isMining: boolean -} - -const getMiningStats = ( - fleet: Fleet, - cargoLevels: FleetCargo, - mineAsteroidData: RawMineAsteroidData, -): MiningStats => { - const cargoStats = fleet.data.stats.cargoStats as unknown as CargoStats - const { - miningRate, - cargoCapacity, - foodConsumptionRate, - ammoConsumptionRate, - } = cargoStats - - const startTime = mineAsteroidData.start - - let cargoLevel = 0 - - for (const [_, value] of cargoLevels.cargo) { - cargoLevel += value - } - - const cargoSpace = cargoCapacity - cargoLevel - - const miningRatePerSecond = miningRate / 10000 - const ammoConsumptionRatePerSecond = ammoConsumptionRate / 10000 - const foodConsumptionRatePerSecond = foodConsumptionRate / 10000 - - const durationToFull = cargoSpace / miningRatePerSecond - const durationToammoDepletion = - cargoLevels.ammo / ammoConsumptionRatePerSecond - const durationToFoodDepletion = - cargoLevels.food / foodConsumptionRatePerSecond - - const maxMiningDuration = Math.min( - durationToFull, - durationToammoDepletion, - durationToFoodDepletion, - ) - - const endReason = - maxMiningDuration === durationToFull - ? 'FULL' - : maxMiningDuration === durationToammoDepletion - ? 'AMMO' - : 'FOOD' - - const n = new BN(now().unix()) - const miningDuration = n.sub(startTime).toNumber() - - const realMiningDuration = Math.min(miningDuration, maxMiningDuration) - - const amountMined = miningRatePerSecond * realMiningDuration - const ammoConsumed = ammoConsumptionRatePerSecond * realMiningDuration - const foodConsumed = foodConsumptionRatePerSecond * realMiningDuration - - return { - startTime, - endTime: startTime.add(new BN(maxMiningDuration)), - cargoLevel, - miningRate, - amountMined, - ammoRequired: ammoConsumed, - foodRequired: foodConsumed, - ammoConsumptionRate, - foodConsumptionRate, - ammoLevel: cargoLevels.ammo - ammoConsumed, - foodLevel: cargoLevels.food - foodConsumed, - endReason, - maxMiningDuration, - isMining: miningDuration < maxMiningDuration, +/** + * Get the fleet state kind from C4 __kind discriminator + */ +function getStateKind(fleet: Fleet): FleetStateKind { + const kind = fleet.state.__kind + // Map C4 state names to our normalized names + switch (kind) { + case 'Idle': + case 'Docked': + case 'MoveWarp': + case 'MoveSubwarp': + case 'MineAsteroid': + case 'Respawn': + return kind + default: + throw new Error(`Unknown fleet state kind: ${kind}`) } } +/** + * Get processed fleet state from C4 Fleet account + */ export const getFleetState = async ( - fleet: Fleet, - map: WorldMap, - cargoLevels: FleetCargo, + fleet: AccountWithKey, ): Promise => { - const fleetStateKeys = Object.keys(fleet.state) as Array - const miscStats = fleet.data.stats.miscStats as unknown as MiscStats - - if (fleetStateKeys.length === 0) { - throw new Error('Fleet state is empty') + const kind = getStateKind(fleet) + + // Get state fields - handle different C4 state structures + const rawFields = fleet.state.fields + let stateFields: Record + + if (Array.isArray(rawFields) && rawFields.length > 0) { + stateFields = rawFields[0] as Record + } else if (typeof rawFields === 'object' && rawFields !== null) { + stateFields = rawFields as unknown as Record + } else { + stateFields = {} } - const [type] = fleetStateKeys - const data = fleet.state[type as keyof typeof fleet.state] - - if (!data) { - throw new Error('Data is empty') + // Debug log for movement states with coordinate issues + if (kind === 'MoveSubwarp' || kind === 'MoveWarp') { + const journey = stateFields.journey || stateFields + logger.debug( + `Fleet ${fleet.key} state ${kind}: ` + + `stateFields keys: [${Object.keys(stateFields).join(', ')}], ` + + `journey keys: [${typeof journey === 'object' && journey ? Object.keys(journey as object).join(', ') : 'N/A'}]`, + ) } - const warpCooldownExpiry = transformTime(fleet.data.warpCooldownExpiresAt) - const scanCoolDownExpiry = transformTime(fleet.data.scanCooldownExpiresAt) + + const warpCooldownExpiry = transformTime(fleet.warpCooldownExpiresAt) + const scanCoolDownExpiry = transformTime(fleet.scanCooldownExpiresAt) const baseData = { warpCooldownExpiry, @@ -171,116 +114,267 @@ export const getFleetState = async ( scanCooldown: scanCoolDownExpiry.isAfter(now()), } - switch (type) { - case 'Idle': - if (isIdleData(data)) { - return { - type, - data: { - sector: transformSector(data.sector), - ...baseData, - }, - } + const currentUnix = BigInt(now().unix()) + + switch (kind) { + case 'Idle': { + // C4 Idle state has no fields - coordinates are in fleet.location + const location = fleet.location as + | Array<{ raw: bigint }> + | undefined + if (!location || location.length < 2) { + throw new Error( + `Fleet ${fleet.key} is Idle but has no location data`, + ) } - break - case 'StarbaseLoadingBay': - if (isStarbaseLoadingBayData(data)) { - const starbase = await starbaseByKey(data.starbase) - - return { - type, - data: { - starbase, - lastUpdate: data.lastUpdate, - sector: transformSector(starbase.data.sector), - ...baseData, - }, - } + return { + kind: 'Idle', + data: { + sector: transformSector([location[0], location[1]]), + ...baseData, + }, } - break - case 'MineAsteroid': - if (isMineAsteroidData(data)) { - const planet = await planetByKey(data.asteroid) - - const miningStats = getMiningStats(fleet, cargoLevels, data) - - return { - type, - data: { - sector: transformSector(planet.data.sector), - lastUpdate: transformTime(data.lastUpdate), - amountMined: new BN(miningStats.amountMined), - asteroid: data.asteroid, - end: transformTime(miningStats.endTime), - resource: data.resource, - mineItem: map.mineItems.get(data.resource.toBase58())!, - start: transformTime(data.start), - endReason: miningStats.endReason, - ...baseData, - }, - } + } + + case 'Docked': { + const systemKey = stateFields.system as string + const system = await fetchAccount('StarSystem', systemKey as never) + if (!system) { + throw new Error(`StarSystem not found: ${systemKey}`) } - break - case 'MoveWarp': - if (isMoveWarpData(data)) { - return { - type, - data: { - fromSector: transformSector(data.fromSector), - toSector: transformSector(data.toSector), - warpStart: transformTime(data.warpStart), - warpFinish: transformTime(data.warpFinish), - sector: calculateCurrentPosition( - transformSector(data.fromSector), - transformSector(data.toSector), - data.warpStart, - data.warpFinish, - new BN(now().unix()), - ), - ...baseData, - }, - } + + if (!system.coordinates || system.coordinates.length < 2) { + throw new Error(`StarSystem ${system.key} has no coordinates`) } - break - case 'MoveSubwarp': - if (isMoveSubWarpData(data)) { - return { - type, - data: { - fromSector: transformSector(data.fromSector), - toSector: transformSector(data.toSector), - departureTime: transformTime(data.departureTime), - arrivalTime: transformTime(data.arrivalTime), - fuelExpenditure: data.fuelExpenditure, - lastUpdate: transformTime(data.lastUpdate), - sector: calculateCurrentPosition( - transformSector(data.fromSector), - transformSector(data.toSector), - data.departureTime, - data.arrivalTime, - new BN(now().unix()), - ), - ...baseData, - }, - } + + // Ensure BigInt conversion (C4 may return numbers/floats) + const lastUpdateRaw = stateFields.lastUpdate as + | bigint + | number + | undefined + const lastUpdate = + lastUpdateRaw === undefined ? 0n : toBigInt(lastUpdateRaw) + + return { + kind: 'Docked', + data: { + system, + lastUpdate, + sector: transformSector([ + system.coordinates[0], + system.coordinates[1], + ]), + ...baseData, + }, } - break - case 'Respawn': - if (isRespawnData(data)) { - return { - type, - data: { - sector: transformSector(data.sector), - destructionTime: transformTime(data.start), - ETA: transformTime( - data.start.add(new BN(miscStats.respawnTime)), - ), - ...baseData, - }, - } + } + + case 'MoveWarp': { + const journey = (stateFields.journey || stateFields) as Record< + string, + unknown + > + // C4 uses 'from'/'to', legacy uses 'fromCoordinates'/'toCoordinates' + const from = (journey.from || journey.fromCoordinates) as + | [{ raw: bigint }, { raw: bigint }] + | undefined + const to = (journey.to || journey.toCoordinates) as + | [{ raw: bigint }, { raw: bigint }] + | undefined + + if (!from || !to) { + throw new Error( + `MoveWarp fleet ${fleet.key} missing coordinates. ` + + `stateFields keys: ${Object.keys(stateFields).join(', ')}, ` + + `journey keys: ${Object.keys(journey).join(', ')}`, + ) + } + + // Ensure BigInt conversion for all time values (C4 may return numbers/floats) + const departureTime = toBigInt( + journey.departureTime as bigint | number, + ) + // C4 uses 'duration' instead of 'arrivalTime' + const durationRaw = journey.duration as bigint | number | undefined + const arrivalTime = + durationRaw === undefined + ? toBigInt(journey.arrivalTime as bigint | number) + : departureTime + toBigInt(durationRaw) + + const fromCoords = transformSector(from) + const toCoords = transformSector(to) + + return { + kind: 'MoveWarp', + data: { + fromCoordinates: fromCoords, + toCoordinates: toCoords, + departureTime: transformTime(departureTime), + arrivalTime: transformTime(arrivalTime), + sector: calculateCurrentPosition( + fromCoords, + toCoords, + departureTime, + arrivalTime, + currentUnix, + ), + ...baseData, + }, + } + } + + case 'MoveSubwarp': { + const journey = (stateFields.journey || stateFields) as Record< + string, + unknown + > + // C4 uses 'from'/'to', legacy uses 'fromCoordinates'/'toCoordinates' + const from = (journey.from || journey.fromCoordinates) as + | [{ raw: bigint }, { raw: bigint }] + | undefined + const to = (journey.to || journey.toCoordinates) as + | [{ raw: bigint }, { raw: bigint }] + | undefined + + if (!from || !to) { + throw new Error( + `MoveSubwarp fleet ${fleet.key} missing coordinates. ` + + `stateFields keys: ${Object.keys(stateFields).join(', ')}, ` + + `journey keys: ${Object.keys(journey).join(', ')}`, + ) + } + + // Ensure BigInt conversion for all time values (C4 may return numbers/floats) + const departureTime = toBigInt( + journey.departureTime as bigint | number, + ) + // C4 uses 'duration' instead of 'arrivalTime' + const durationRaw = journey.duration as bigint | number | undefined + const arrivalTime = + durationRaw === undefined + ? toBigInt(journey.arrivalTime as bigint | number) + : departureTime + toBigInt(durationRaw) + const fuelExpenditure = + (journey.fuelExpenditure as bigint) ?? + (stateFields.totalFuelExpenditure as bigint) ?? + 0n + const lastUpdateRaw = journey.lastUpdate as + | bigint + | number + | undefined + const lastUpdate = + lastUpdateRaw === undefined + ? departureTime + : toBigInt(lastUpdateRaw) + + const fromCoords = transformSector(from) + const toCoords = transformSector(to) + + return { + kind: 'MoveSubwarp', + data: { + fromCoordinates: fromCoords, + toCoordinates: toCoords, + departureTime: transformTime(departureTime), + arrivalTime: transformTime(arrivalTime), + fuelExpenditure, + lastUpdate: transformTime(lastUpdate), + sector: calculateCurrentPosition( + fromCoords, + toCoords, + departureTime, + arrivalTime, + currentUnix, + ), + ...baseData, + }, + } + } + + case 'MineAsteroid': { + const asteroidKey = stateFields.asteroid as string + const celestialBody = await fetchAccount( + 'CelestialBody', + asteroidKey as never, + ) + if (!celestialBody) { + throw new Error(`CelestialBody not found: ${asteroidKey}`) + } + + // Get coordinates from the parent star system + const systemKey = celestialBody.system + const system = await fetchAccount('StarSystem', systemKey as never) + if (!system) { + throw new Error(`StarSystem not found: ${systemKey}`) + } + + // Ensure BigInt conversion (C4 may return numbers/floats) + const start = toBigInt(stateFields.start as bigint | number) + const lastUpdate = toBigInt( + stateFields.lastUpdate as bigint | number, + ) + + // Calculate mining end time based on resources + // TODO: Implement proper mining calculation with fleet stats + const miningDuration = 3600n // Placeholder: 1 hour + const end = start + miningDuration + const endReason: EndReason = 'FULL' // Placeholder + + if (!system.coordinates || system.coordinates.length < 2) { + throw new Error( + `StarSystem ${system.key} has no coordinates for MineAsteroid`, + ) } - break - } - throw new Error('Data does not match expected type for the fleet state') + return { + kind: 'MineAsteroid', + data: { + asteroid: asteroidKey as never, + celestialBody, + start: transformTime(start), + end: transformTime(end), + amountMined: (stateFields.amountMined as bigint) ?? 0n, + lastUpdate: transformTime(lastUpdate), + endReason, + sector: transformSector([ + system.coordinates[0], + system.coordinates[1], + ]), + ...baseData, + }, + } + } + + case 'Respawn': { + // Ensure BigInt conversion (C4 may return numbers/floats) + const start = toBigInt(stateFields.start as bigint | number) + // C4 uses respawnTimeWithoutFee for base respawn time + const respawnTimeRaw = ( + fleet.stats.miscStats as Record + ).respawnTimeWithoutFee as bigint | number | undefined + const respawnTime = + respawnTimeRaw === undefined ? 0n : toBigInt(respawnTimeRaw) + const eta = start + respawnTime + + // Get coordinates from respawn location + const coordinates = stateFields.coordinates as + | [{ raw: bigint }, { raw: bigint }] + | undefined + + if (!coordinates || coordinates.length < 2) { + throw new Error(`Respawn fleet ${fleet.key} has no coordinates`) + } + const sector = transformSector(coordinates) + + return { + kind: 'Respawn', + data: { + destructionTime: transformTime(start), + ETA: transformTime(eta), + sector, + ...baseData, + }, + } + } + } } diff --git a/src/main/basedbot/lib/fleet-state/transform/transform-sector.ts b/src/main/basedbot/lib/fleet-state/transform/transform-sector.ts index 9b22f9a2..beedfc62 100644 --- a/src/main/basedbot/lib/fleet-state/transform/transform-sector.ts +++ b/src/main/basedbot/lib/fleet-state/transform/transform-sector.ts @@ -1,6 +1,124 @@ -import BN from 'bn.js' - import { Coordinates } from '../../util/coordinates.js' -export const transformSector = (sector: BN[]): Coordinates => - Coordinates.fromBN(sector[0], sector[1]) +// C4 coordinate with fixed-point representation (full object with toNumber method) +type C4Coord = { + raw: bigint + fractionalBits: number + toNumber: () => number +} + +// Raw-only coordinate (from fleet state data, missing toNumber method) +type RawCoord = { + raw: bigint +} + +type CoordInput = any + +/** + * Extract raw bigint from a coordinate element + * Returns the raw bigint value (already scaled by 2^56) + */ +function extractRawCoord(coord: unknown): bigint | null { + if (coord === null || coord === undefined) { + return null + } + + // Object with raw bigint (preferred - preserves full precision) + if (typeof coord === 'object' && 'raw' in coord) { + return (coord as RawCoord).raw + } + + // Direct bigint (already raw) + if (typeof coord === 'bigint') { + return coord + } + + // Direct number - convert to raw (scale by 2^56) + if (typeof coord === 'number') { + const scale = 2n ** 56n + return BigInt(Math.round(coord)) * scale + } + + return null +} + +/** + * Extract display integer from a coordinate element (for logging/comparisons) + * Handles FixedPoint objects, raw objects, and bigints + */ +function extractIntValue(coord: unknown): number | null { + if (coord === null || coord === undefined) { + return null + } + + // FixedPoint or object with toNumber method + if ( + typeof coord === 'object' && + 'toNumber' in coord && + typeof (coord as C4Coord).toNumber === 'function' + ) { + return Math.round((coord as C4Coord).toNumber()) + } + + // Object with raw bigint (fleet state format) + if (typeof coord === 'object' && 'raw' in coord) { + const raw = (coord as RawCoord).raw + // Assume 56 fractional bits for I8F56 format + const scale = 1n << 56n + return Number(raw / scale) + } + + // Direct bigint + if (typeof coord === 'bigint') { + // Assume 56 fractional bits + const scale = 1n << 56n + return Number(coord / scale) + } + + // Direct number + if (typeof coord === 'number') { + return Math.round(coord) + } + + return null +} + +/** + * Transform C4 coordinate array to Coordinates + * C4 uses [{raw, fractionalBits, toNumber()}, ...] format + * Fleet state data may only have [{raw}, {raw}] without toNumber method + * Preserves full precision using raw bigint values for accurate docking + */ +export const transformSector = (coords: CoordInput): Coordinates => { + // Handle null/undefined + if (!coords) { + throw new Error('transformSector: coords is null/undefined') + } + + // Handle array format + if (Array.isArray(coords) && coords.length >= 2) { + // Try to get raw coordinates first (preserves full precision) + const rawX = extractRawCoord(coords[0]) + const rawY = extractRawCoord(coords[1]) + + if (rawX !== null && rawY !== null) { + // Use raw coordinates for full precision (needed for docking) + return Coordinates.fromRaw(rawX, rawY) + } + + // Fall back to integer coordinates if raw not available + const x = extractIntValue(coords[0]) + const y = extractIntValue(coords[1]) + + if (x !== null && y !== null) { + return Coordinates.fromNumber(x, y) + } + } + + // Log the problematic format for debugging + throw new Error( + `Invalid coordinate format: ${JSON.stringify(coords, (_, v) => + typeof v === 'bigint' ? v.toString() : v, + )}`, + ) +} diff --git a/src/main/basedbot/lib/fleet-state/transform/transform-time.ts b/src/main/basedbot/lib/fleet-state/transform/transform-time.ts index 650789ac..1e288277 100644 --- a/src/main/basedbot/lib/fleet-state/transform/transform-time.ts +++ b/src/main/basedbot/lib/fleet-state/transform/transform-time.ts @@ -1,6 +1,10 @@ -import BN from 'bn.js' +import dayjs, { type Dayjs } from '../../../../../dayjs.js' -import dayjs from '../../../../../dayjs.js' - -export const transformTime = (time: BN): dayjs.Dayjs => - dayjs.unix(time.toNumber()) +/** + * Transform C4 timestamp (bigint seconds) to Dayjs + */ +export const transformTime = (timestamp: bigint | number): Dayjs => { + const seconds = + typeof timestamp === 'bigint' ? Number(timestamp) : timestamp + return dayjs.unix(seconds) +} diff --git a/src/main/basedbot/lib/fleet-state/type-guard/idle.ts b/src/main/basedbot/lib/fleet-state/type-guard/idle.ts deleted file mode 100644 index e68afc00..00000000 --- a/src/main/basedbot/lib/fleet-state/type-guard/idle.ts +++ /dev/null @@ -1,12 +0,0 @@ -import BN from 'bn.js' - -import { RawIdleData } from '../types.js' - -export const isIdleData = (data: unknown): data is RawIdleData => - data !== undefined && - data instanceof Object && - 'sector' in data && - Array.isArray(data.sector) && - data.sector.length === 2 && - data.sector[0] instanceof BN && - data.sector[1] instanceof BN diff --git a/src/main/basedbot/lib/fleet-state/type-guard/mine-asteroid.ts b/src/main/basedbot/lib/fleet-state/type-guard/mine-asteroid.ts deleted file mode 100644 index 2cdaf723..00000000 --- a/src/main/basedbot/lib/fleet-state/type-guard/mine-asteroid.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import BN from 'bn.js' - -import { RawMineAsteroidData } from '../types.js' - -export const isMineAsteroidData = ( - data: unknown, -): data is RawMineAsteroidData => - data !== undefined && - data instanceof Object && - 'asteroid' in data && - 'resource' in data && - 'start' in data && - 'end' in data && - 'amountMined' in data && - 'lastUpdate' in data && - data.asteroid instanceof PublicKey && - data.resource instanceof PublicKey && - data.start instanceof BN && - data.end instanceof BN && - data.amountMined instanceof BN && - data.lastUpdate instanceof BN diff --git a/src/main/basedbot/lib/fleet-state/type-guard/move-sub-warp.ts b/src/main/basedbot/lib/fleet-state/type-guard/move-sub-warp.ts deleted file mode 100644 index c598fa4e..00000000 --- a/src/main/basedbot/lib/fleet-state/type-guard/move-sub-warp.ts +++ /dev/null @@ -1,18 +0,0 @@ -import BN from 'bn.js' - -import { RawMoveSubwarpData } from '../types.js' - -// TODO: Add all the fields that are required to be present in the data -export const isMoveSubWarpData = (data: unknown): data is RawMoveSubwarpData => - data !== undefined && - data instanceof Object && - 'fromSector' in data && - 'toSector' in data && - Array.isArray(data.fromSector) && - data.fromSector.length === 2 && - data.fromSector[0] instanceof BN && - data.fromSector[1] instanceof BN && - Array.isArray(data.toSector) && - data.toSector.length === 2 && - data.toSector[0] instanceof BN && - data.toSector[1] instanceof BN diff --git a/src/main/basedbot/lib/fleet-state/type-guard/move-warp.ts b/src/main/basedbot/lib/fleet-state/type-guard/move-warp.ts deleted file mode 100644 index f14cea6a..00000000 --- a/src/main/basedbot/lib/fleet-state/type-guard/move-warp.ts +++ /dev/null @@ -1,18 +0,0 @@ -import BN from 'bn.js' - -import { RawMoveWarpData } from '../types.js' - -// TODO: Add all the fields that are required to be present in the data -export const isMoveWarpData = (data: unknown): data is RawMoveWarpData => - data !== undefined && - data instanceof Object && - 'fromSector' in data && - 'toSector' in data && - Array.isArray(data.fromSector) && - data.fromSector.length === 2 && - data.fromSector[0] instanceof BN && - data.fromSector[1] instanceof BN && - Array.isArray(data.toSector) && - data.toSector.length === 2 && - data.toSector[0] instanceof BN && - data.toSector[1] instanceof BN diff --git a/src/main/basedbot/lib/fleet-state/type-guard/respawn.ts b/src/main/basedbot/lib/fleet-state/type-guard/respawn.ts deleted file mode 100644 index 085324d2..00000000 --- a/src/main/basedbot/lib/fleet-state/type-guard/respawn.ts +++ /dev/null @@ -1,12 +0,0 @@ -import BN from 'bn.js' - -import { RawRespawnData } from '../types.js' - -export const isRespawnData = (data: unknown): data is RawRespawnData => - data !== undefined && - data instanceof Object && - 'sector' in data && - Array.isArray(data.sector) && - data.sector.length === 2 && - data.sector[0] instanceof BN && - data.sector[1] instanceof BN diff --git a/src/main/basedbot/lib/fleet-state/type-guard/starbase-loading-bay.ts b/src/main/basedbot/lib/fleet-state/type-guard/starbase-loading-bay.ts deleted file mode 100644 index 998a4eaf..00000000 --- a/src/main/basedbot/lib/fleet-state/type-guard/starbase-loading-bay.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import BN from 'bn.js' - -import { RawStarbaseLoadingBayData } from '../types.js' - -export const isStarbaseLoadingBayData = ( - data: unknown, -): data is RawStarbaseLoadingBayData => - data !== undefined && - data instanceof Object && - 'starbase' in data && - 'lastUpdate' in data && - data.starbase instanceof PublicKey && - data.lastUpdate instanceof BN diff --git a/src/main/basedbot/lib/fleet-state/types.ts b/src/main/basedbot/lib/fleet-state/types.ts index 2af1db4c..2b6f37b0 100644 --- a/src/main/basedbot/lib/fleet-state/types.ts +++ b/src/main/basedbot/lib/fleet-state/types.ts @@ -1,118 +1,108 @@ -import { PublicKey } from '@solana/web3.js' -import { MineItem, Starbase } from '@staratlas/sage' -import BN from 'bn.js' +import type { Address } from '@solana/kit' +import type { CelestialBody, StarSystem } from '@staratlas/dev-sage' -import dayjs from '../../../../dayjs.js' -import { Coordinates } from '../util/coordinates.js' +import type { Dayjs } from '../../../../dayjs.js' +import type { AccountWithKey } from '../types.js' +import type { Coordinates } from '../util/coordinates.js' export type EndReason = 'FULL' | 'AMMO' | 'FOOD' + +/** + * Base data included in all fleet states + */ type BaseData = { sector: Coordinates - warpCooldownExpiry: dayjs.Dayjs - scanCoolDownExpiry: dayjs.Dayjs + warpCooldownExpiry: Dayjs + scanCoolDownExpiry: Dayjs warpCooldown: boolean scanCooldown: boolean } -// Raw data types for incoming data -export type RawIdleData = { - sector: [BN, BN] -} +/** + * Fleet state types matching C4 __kind discriminators + */ +export type FleetStateKind = + | 'Idle' + | 'Docked' + | 'MoveWarp' + | 'MoveSubwarp' + | 'MineAsteroid' + | 'Respawn' +/** + * Idle state data + */ export type IdleData = BaseData -export type RawStarbaseLoadingBayData = { starbase: PublicKey; lastUpdate: BN } -export type StarbaseLoadingBayData = { - starbase: Starbase - lastUpdate: BN +/** + * Docked state data (C4: was StarbaseLoadingBay) + */ +export type DockedData = { + system: AccountWithKey + lastUpdate: bigint } & BaseData -export type RawMineAsteroidData = { - asteroid: PublicKey - resource: PublicKey - start: BN - end: BN - amountMined: BN - lastUpdate: BN - sector: BN[] -} +/** + * Mining state data + */ export type MineAsteroidData = { - asteroid: PublicKey - mineItem: MineItem - resource: PublicKey - start: dayjs.Dayjs - end: dayjs.Dayjs - amountMined: BN - lastUpdate: dayjs.Dayjs + asteroid: Address + celestialBody: AccountWithKey + start: Dayjs + end: Dayjs + amountMined: bigint + lastUpdate: Dayjs endReason: EndReason } & BaseData -export type RawMoveWarpData = { - fromSector: BN[] - toSector: BN[] - warpStart: BN - warpFinish: BN -} +/** + * Warp movement state data + */ export type MoveWarpData = { - fromSector: Coordinates - toSector: Coordinates - warpStart: dayjs.Dayjs - warpFinish: dayjs.Dayjs + fromCoordinates: Coordinates + toCoordinates: Coordinates + departureTime: Dayjs + arrivalTime: Dayjs } & BaseData -export type RawMoveSubwarpData = { - fromSector: BN[] - toSector: BN[] - currentSector: BN[] - departureTime: BN - arrivalTime: BN - fuelExpenditure: BN - lastUpdate: BN -} +/** + * Subwarp movement state data + */ export type MoveSubwarpData = { - fromSector: Coordinates - toSector: Coordinates - departureTime: dayjs.Dayjs - arrivalTime: dayjs.Dayjs - fuelExpenditure: BN - lastUpdate: dayjs.Dayjs + fromCoordinates: Coordinates + toCoordinates: Coordinates + departureTime: Dayjs + arrivalTime: Dayjs + fuelExpenditure: bigint + lastUpdate: Dayjs } & BaseData -export type RawRespawnData = { - sector: [BN, BN] - start: BN -} +/** + * Respawn state data + */ export type RespawnData = { - destructionTime: dayjs.Dayjs - ETA: dayjs.Dayjs + destructionTime: Dayjs + ETA: Dayjs } & BaseData -export type StarbaseUpgradeData = BaseData -export type StarbaseRepairData = BaseData - -export type FleetStateType = - | 'StarbaseLoadingBay' - | 'Idle' - | 'MineAsteroid' - | 'MoveWarp' - | 'MoveSubwarp' - | 'Respawn' - | 'StarbaseUpgrade' - | 'StarbaseRepair' +/** + * Map of state kinds to their data types + */ export type FleetStateDataMap = { - StarbaseLoadingBay: StarbaseLoadingBayData Idle: IdleData - MineAsteroid: MineAsteroidData + Docked: DockedData MoveWarp: MoveWarpData MoveSubwarp: MoveSubwarpData + MineAsteroid: MineAsteroidData Respawn: RespawnData - StarbaseUpgrade: StarbaseUpgradeData - StarbaseRepair: StarbaseRepairData } +/** + * Discriminated union of all fleet states + */ export type FleetState = { - [K in FleetStateType]: { - type: K + [K in FleetStateKind]: { + kind: K data: FleetStateDataMap[K] } -}[FleetStateType] +}[FleetStateKind] diff --git a/src/main/basedbot/lib/programs.ts b/src/main/basedbot/lib/programs.ts deleted file mode 100644 index e5ad0c5c..00000000 --- a/src/main/basedbot/lib/programs.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { Idl } from '@coral-xyz/anchor' -import { PublicKey } from '@solana/web3.js' -import { CargoProgram } from '@staratlas/cargo' -import { Cargo } from '@staratlas/cargo/dist/src/idl/cargo' -import { CraftingProgram } from '@staratlas/crafting' -import { Crafting } from '@staratlas/crafting/dist/src/idl/crafting' -import { ProgramMethods } from '@staratlas/data-source' -import { PlayerProfileProgram } from '@staratlas/player-profile' -import { PlayerProfile } from '@staratlas/player-profile/dist/src/idl/player_profile' -import { PointsProgram } from '@staratlas/points' -import { Points } from '@staratlas/points/dist/src/idl/points' -import { ProfileFactionProgram } from '@staratlas/profile-faction' -import { ProfileFaction } from '@staratlas/profile-faction/dist/src/idl/profile_faction' -import { SageProgram } from '@staratlas/sage' -import { Sage } from '@staratlas/sage/dist/src/idl/sage' - -import { config } from '../../../config/index.js' -import { anchorProvider } from '../../../service/sol/anchor.js' - -// @ts-expect-error -export type StarAtlasProgram = ProgramMethods - -export const xpCategoryIds = - config.sol.rpcEndpoint.includes('devnet') || - config.sol.rpcEndpoint.includes('validator') || - config.sol.rpcEndpoint.includes('universe') || - config.sol.rpcEndpoint.includes('localhost') - ? { - dataRunningXpCategory: - 'DXPsKQPMyaDtunxDWqiKTGWbQga3Wihck8zb8iSLATJQ', - councilRankXpCategory: - 'CRXPW3csNpkEYU5U4DUp6Ln6aEEWq4PSUAwV8v6Ygcqg', - pilotingXpCategory: 'PXPfCZwu5Vuuj6aFdEUAXbxudDGeXVktTo6imwhZ5nC', - miningXpCategory: 'MXPkuZz7yXvqdEB8pGtyNknqhxbCzJNQzqixoEiW4Q7', - craftingXpCategory: - 'CXPukKpixXCFPrfQmEUGR9VqnDvkUsKfPPLfdd4sKSH8', - loyalityCategory: 'LPpdwMuXRuGMz298EMbNcUioaARN8CUU6dA2qyq46g8', - } - : { - dataRunningXpCategory: - 'DataJpxFgHhzwu4zYJeHCnAv21YqWtanEBphNxXBHdEY', - councilRankXpCategory: - 'XPneyd1Wvoay3aAa24QiKyPjs8SUbZnGg5xvpKvTgN9', - pilotingXpCategory: 'PiLotBQoUBUvKxMrrQbuR3qDhqgwLJctWsXj3uR7fGs', - miningXpCategory: 'MineMBxARiRdMh7s1wdStSK4Ns3YfnLjBfvF5ZCnzuw', - craftingXpCategory: - 'CraftndAV62acibnaW7TiwEYwu8MmJZBdyrfyN54nre7', - loyalityCategory: '', - } - -const programIds = - config.sol.rpcEndpoint.includes('devnet') || - config.sol.rpcEndpoint.includes('validator') || - config.sol.rpcEndpoint.includes('universe') || - config.sol.rpcEndpoint.includes('localhost') - ? { - sage: 'sAgezwJpDb1aHvzNr3o24cKjsETmFEKghBEyJ1askDi', - profile: 'PprofUW1pURCnMW2si88GWPXEEK3Bvh9Tksy8WtnoYJ', - cargo: 'CArGoi989iv3VL3xArrJXmYYDNhjwCX5ey5sY5KKwMG', - profileFaction: 'pFACzkX2eSpAjDyEohD6i3VRJvREtH9ynbtM1DwVFsj', - crafting: 'CRAFtUSjCW74gQtCS6LyJH33rhhVhdPhZxbPegE4Qwfq', - points: 'PointJfvuHi8DgGsPCy97EaZkQ6NvpghAAVkuquLf3w', - } - : { - sage: 'SAGE2HAwep459SNq61LHvjxPk4pLPEJLoMETef7f7EE', - profile: 'pprofELXjL5Kck7Jn5hCpwAL82DpTkSYBENzahVtbc9', - cargo: 'Cargo2VNTPPTi9c1vq1Jw5d3BWUNr18MjRtSupAghKEk', - profileFaction: 'pFACSRuobDmvfMKq1bAzwj27t6d2GJhSCHb1VcfnRmq', - crafting: 'CRAFT2RPXPJWCEix4WpJST3E7NLf79GTqZUL75wngXo5', - points: 'Point2iBvz7j5TMVef8nEgpmz4pDr7tU7v3RjAfkQbM', - } - -export type StarAtlasPrograms = { - sage: StarAtlasProgram - points: StarAtlasProgram - playerProfile: StarAtlasProgram - cargo: StarAtlasProgram - profileFaction: StarAtlasProgram - crafting: StarAtlasProgram -} - -export const programs: StarAtlasPrograms = { - sage: SageProgram.buildProgram( - new PublicKey(programIds.sage), - anchorProvider, - ), - points: PointsProgram.buildProgram( - new PublicKey(programIds.points), - anchorProvider, - ), - playerProfile: PlayerProfileProgram.buildProgram( - new PublicKey(programIds.profile), - anchorProvider, - ), - cargo: CargoProgram.buildProgram( - new PublicKey(programIds.cargo), - anchorProvider, - ), - profileFaction: ProfileFactionProgram.buildProgram( - new PublicKey(programIds.profileFaction), - anchorProvider, - ), - crafting: CraftingProgram.buildProgram( - new PublicKey(programIds.crafting), - anchorProvider, - ), -} diff --git a/src/main/basedbot/lib/sage/act/create-fleet.ts b/src/main/basedbot/lib/sage/act/create-fleet.ts index 06b8991e..423ee91b 100644 --- a/src/main/basedbot/lib/sage/act/create-fleet.ts +++ b/src/main/basedbot/lib/sage/act/create-fleet.ts @@ -1,143 +1,343 @@ -import { PublicKey } from '@solana/web3.js' -import { InstructionReturn, ixReturnsToIxs } from '@staratlas/data-source' -import { +/** + * Create fleet action for C4 + */ +import type { Address, Instruction, KeyPairSigner } from '@solana/kit' +import type { Game, Ship, - Starbase, StarbasePlayer, - WrappedShipEscrow, -} from '@staratlas/sage' + StarSystem, +} from '@staratlas/dev-sage' +import { + findFleetPda, + getAddShipToFleetInstruction, + getCreateFleetInstruction, + getLoadFleetCrewInstruction, +} from '@staratlas/dev-sage' import { logger } from '../../../../../logger.js' import { sendAndConfirmInstructions } from '../../../../../service/sol/send-and-confirm-tx.js' -import { keyPair } from '../../../../../service/wallet' -import { programs } from '../../programs.js' -import { addShipToFleetIx } from '../ix/add-ship-to-fleet.js' -import { createFleetIx } from '../ix/create-fleet.js' -import { getCargoStatsDefinition } from '../state/cargo-stats-definition.js' -import { getShipByMint, getStarbasePlayer } from '../state/starbase-player.js' -import { Player } from '../state/user-account.js' - -export type FleetShips = Array +import type { AccountWithKey, Character } from '../../types.js' +import { getStarbasePlayer } from '../state/starbase-player.js' +import type { Player } from '../state/user-account.js' +import { + findNodesGrantingTag, + hasRequiredResearchTags, + listAllResearchNodes, +} from './unlock-research.js' + export type FleetShip = { - shipMint: PublicKey - count: number + /** Ship type ID (also used as escrow index) */ + id: number + /** Number of ships */ + amount: number } -const getShipEscrowIndex = ( - starbasePlayer: StarbasePlayer, - shipKey: PublicKey, -) => { - const pred = (key: PublicKey) => (v: WrappedShipEscrow) => - v.ship.equals(key) - const index = starbasePlayer.wrappedShipEscrows.findIndex(pred(shipKey)) +export type CreateFleetParams = { + game: AccountWithKey + character: AccountWithKey + system: AccountWithKey + ships: FleetShip[] + fleetName: string + signer: KeyPairSigner + keyIndex?: number +} + +/** + * Check if player has required research for ships + * Returns missing research tags if any + */ +function checkResearchRequirements( + ships: FleetShip[], + game: AccountWithKey, + player: Player, +): { canUse: boolean; missingTags: number[] } { + const shipDefs = game.shipDefinitions.ships.unsizedList + const allMissingTags: Set = new Set() - if (index === -1) { - throw new Error('Ship not found') + for (const ship of ships) { + const shipDef = shipDefs.find((s: Ship) => s.id === ship.id) + if (shipDef && shipDef.researchRequirements.length > 0) { + const { missing } = hasRequiredResearchTags( + player.character, + game, + shipDef.researchRequirements, + ) + for (const tag of missing) { + allMissingTags.add(tag) + } + } } - return index + return { + canUse: allMissingTags.size === 0, + missingTags: [...allMissingTags], + } } -type ShipMintMap = { - mint: PublicKey - ship: Ship +/** + * Calculate total crew required for a set of ships + */ +function calculateRequiredCrew( + ships: FleetShip[], + game: AccountWithKey, +): number { + const shipDefs = game.shipDefinitions.ships.unsizedList + let totalCrew = 0 + + logger.debug( + `Ship definitions available: ${shipDefs.length}, looking for IDs: ${ships.map((s) => s.id).join(', ')}`, + ) + + for (const ship of ships) { + // Find ship definition by ID + const shipDef = shipDefs.find((s: Ship) => s.id === ship.id) + if (shipDef) { + const crewNeeded = + shipDef.stats.miscStats.requiredCrew * ship.amount + logger.debug( + `Ship ${ship.id} (${shipDef.name}): ${shipDef.stats.miscStats.requiredCrew} crew x ${ship.amount} = ${crewNeeded}`, + ) + logger.debug( + `Ship ${ship.id} requirements: research=${JSON.stringify(shipDef.researchRequirements)}, components=${JSON.stringify([...shipDef.componentRequirements.entries()])}`, + ) + totalCrew += crewNeeded + } else { + logger.warn(`Ship definition not found for ID ${ship.id}`) + } + } + + return totalCrew } -export const createFleet = async ( - player: Player, - game: Game, - starbase: Starbase, - fleetShips: FleetShips, - name: string, -): Promise => { - const instructions: InstructionReturn[] = [] - - const shipMints = ( - await Promise.all( - fleetShips.map(async (fleetShip) => { - return { - mint: fleetShip.shipMint, - ship: await getShipByMint( - fleetShip.shipMint, - game, - programs, - ), - } as ShipMintMap +/** + * Convert string to fixed-size byte array for fleet label + */ +function stringToByteArray(input: string, forceSize?: number): number[] { + const nameBytes = new TextEncoder().encode(input) + if (forceSize === undefined) { + return Array.from(nameBytes) + } + if (nameBytes.length > forceSize) { + throw new Error('name too long') + } + return Array.from(nameBytes).concat( + new Array(forceSize - nameBytes.length).fill(0), + ) +} + +/** + * Add ships to a fleet from escrow + * fc-app StarbaseActions.ts lines 395-435 + */ +function addShipsToFleet({ + signer, + game, + character, + system, + starbasePlayer, + fleetKey, + ships, + keyIndex = 0, +}: { + signer: KeyPairSigner + game: AccountWithKey + character: AccountWithKey + system: AccountWithKey + starbasePlayer: AccountWithKey + fleetKey: Address + ships: { id: number; amount: number }[] + keyIndex: number +}): Instruction[] { + const instructions: Instruction[] = [] + + for (const { id, amount } of ships) { + instructions.push( + getAddShipToFleetInstruction({ + game: game.key, + character: character.key, + profileValidationProfile: character.playerProfile, + profileValidationSigner: signer, + systemAndStarbasePlayerSystem: system.key, + systemAndStarbasePlayerStarbasePlayer: starbasePlayer.key, + fleet: fleetKey, + fleetShipInfoIndex: id, + shipEscrowIndex: id, + ships: [{ id, amount }], + keyIndex, }), ) - ).reduce( - (acc, curr) => acc.set(curr.mint.toBase58(), curr.ship), - new Map(), - ) + } - const [starbasePlayer, cargoStatsDefinition] = await Promise.all([ - getStarbasePlayer(player, starbase, programs), - getCargoStatsDefinition(game.data.cargo.statsDefinition), - ]) - - const [head, ...tail] = fleetShips.sort( - (a, b) => - getShipEscrowIndex( - starbasePlayer, - shipMints.get(a.shipMint.toBase58())!.key, - ) - - getShipEscrowIndex( - starbasePlayer, - shipMints.get(b.shipMint.toBase58())!.key, - ), - ) + return instructions +} - const shipKey = shipMints.get(head.shipMint.toBase58())?.key +/** + * Execute create fleet action + */ +export async function createFleet( + player: Player, + game: AccountWithKey, + system: AccountWithKey, + ships: FleetShip[], + fleetName: string, +): Promise { + if (ships.length === 0) { + logger.debug(`No ships to create fleet "${fleetName}"`) + return + } - if (!shipKey) throw new Error('No ship found') + const starbasePlayer = await getStarbasePlayer(player, system) + if (!starbasePlayer) { + logger.warn( + `No StarbasePlayer found at system ${system.key}, cannot create fleet`, + ) + return + } - const escrowIndex = getShipEscrowIndex(starbasePlayer, shipKey) + // Check if ships are in escrow (Map) + const shipsInEscrow = starbasePlayer.shipsInEscrow + if (!shipsInEscrow || shipsInEscrow.size === 0) { + logger.warn( + `No ships in escrow at system ${system.key}, cannot create fleet. Deposit ships first.`, + ) + return + } - logger.debug(`Escrow index ${escrowIndex} for ${head.shipMint.toBase58()}`) + // Build ships list - fc-app pattern: just verify ships exist in escrow + const actualShips: { id: number; amount: number }[] = [] - const createFleetReturn = createFleetIx( - player, + for (const requestedShip of ships) { + const availableAmount = shipsInEscrow.get(requestedShip.id) + if (!availableAmount || availableAmount <= 0n) { + logger.warn( + `Requested ship ${requestedShip.id} not in escrow, skipping`, + ) + continue + } + + const amountToUse = Math.min( + requestedShip.amount, + Number(availableAmount), + ) + if (amountToUse > 0) { + actualShips.push({ id: requestedShip.id, amount: amountToUse }) + } + } + + if (actualShips.length === 0) { + logger.warn(`No ships available to create fleet "${fleetName}"`) + return + } + + // Check research requirements + const { canUse, missingTags } = checkResearchRequirements( + actualShips, game, - starbase, - starbasePlayer, - programs, - shipKey, - cargoStatsDefinition.key, - head.count, - name, - escrowIndex, + player, + ) + if (!canUse) { + logger.warn( + `Missing research tags to use ships: [${missingTags.join(', ')}]`, + ) + // Show which nodes grant these tags + for (const tag of missingTags) { + const nodes = findNodesGrantingTag(game, tag) + if (nodes.length > 0) { + logger.info( + ` Tag ${tag} can be unlocked by: ${nodes.map((n) => `[${n.id}] ${n.node.name}`).join(', ')}`, + ) + } + } + // List all available research nodes for debugging + listAllResearchNodes(game) + return + } + + // Calculate required crew for the ships + const requiredCrew = calculateRequiredCrew(actualShips, game) + + // Check StarbasePlayer has enough crew + const availableCrew = starbasePlayer.totalCrew - starbasePlayer.busyCrew + if (availableCrew < requiredCrew) { + logger.warn( + `Not enough crew at starbase: need ${requiredCrew}, have ${availableCrew}. ` + + `Deposit crew to the starbase to create fleets.`, + ) + return + } + + logger.debug( + `Ships in escrow: ${shipsInEscrow.size} types. Using: ${actualShips.map((s) => `${s.id}x${s.amount}`).join(', ')}. Crew: ${requiredCrew}/${availableCrew}`, ) - instructions.push(createFleetReturn.instructions) + const keyIndex = player.keyIndex + const signer = player.signer + const character = player.character - for (const fleetShip of tail) { - const shipKey2 = shipMints.get(fleetShip.shipMint.toBase58())?.key + // Derive fleet PDA + const fleetLabelPart1 = stringToByteArray(fleetName, 32) + const fleetLabelPart2 = stringToByteArray('', 32) - if (!shipKey2) throw new Error('No ship found') + const [fleetPda] = await findFleetPda({ + gameId: game.key, + ownerProfile: character.playerProfile, + fleetLabelPart1, + fleetLabelPart2, + }) - const escrowIndex2 = getShipEscrowIndex(starbasePlayer, shipKey2) + logger.info(`Creating fleet "${fleetName}" at ${fleetPda}`) - logger.info( - `Escrow index ${escrowIndex2} for ${fleetShip.shipMint.toBase58()}`, - ) + const instructions: Instruction[] = [] + + // Create fleet instruction + instructions.push( + getCreateFleetInstruction({ + profileValidationSigner: signer, + game: game.key, + character: character.key, + profileValidationProfile: character.playerProfile, + profileFaction: player.profileFaction.key, + systemAndStarbasePlayerSystem: system.key, + systemAndStarbasePlayerStarbasePlayer: starbasePlayer.key, + fleet: fleetPda, + fleetLabel: fleetName, + keyIndex, + }), + ) + + // Add ships to fleet - fc-app pattern + instructions.push( + ...addShipsToFleet({ + signer, + game, + character, + system, + starbasePlayer, + fleetKey: fleetPda, + ships: actualShips, + keyIndex, + }), + ) + // Load crew onto fleet + if (requiredCrew > 0) { instructions.push( - addShipToFleetIx( - player, - game, - starbase, - starbasePlayer, - programs, - createFleetReturn.fleetKey[0], - shipKey2, - fleetShip.count, - escrowIndex2, - ), + getLoadFleetCrewInstruction({ + profileValidationSigner: signer, + profileValidationProfile: character.playerProfile, + fleet: fleetPda, + game: game.key, + systemAndStarbasePlayerSystem: system.key, + systemAndStarbasePlayerStarbasePlayer: starbasePlayer.key, + count: requiredCrew, + keyIndex, + }), ) } - await sendAndConfirmInstructions([keyPair])( - await ixReturnsToIxs(instructions, player.signer), - ) + // Send transaction + await sendAndConfirmInstructions(signer)(instructions) + + logger.info(`Fleet "${fleetName}" created successfully`) } diff --git a/src/main/basedbot/lib/sage/act/deposit-cargo.ts b/src/main/basedbot/lib/sage/act/deposit-cargo.ts index d4599b4e..d9e27372 100644 --- a/src/main/basedbot/lib/sage/act/deposit-cargo.ts +++ b/src/main/basedbot/lib/sage/act/deposit-cargo.ts @@ -1,83 +1,316 @@ -import { getAssociatedTokenAddressSync } from '@solana/spl-token' -import { PublicKey } from '@solana/web3.js' +/** + * Deposit cargo action for C4 + * + * In C4, cargo must be deposited to StarbasePlayer before it can be + * transferred to a fleet. Uses getDepositCargoToGameInstruction. + * + * CARGO ATA DERIVATION (from fc-app reference): + * - Cargo token account owner: `game.cargoDefinitions.cache` (CargoCache PDA) + * - Profile fields: `character.playerProfile` (Profile address stored in Character) + * - Derive with: findAssociatedTokenPda({ owner: game.cargoDefinitions.cache, mint, tokenProgram }) + */ +import type { Address, Instruction } from '@solana/kit' import { - createAssociatedTokenAccountIdempotent, - InstructionReturn, - ixReturnsToIxs, -} from '@staratlas/data-source' -import { Game, Starbase } from '@staratlas/sage' -import BN from 'bn.js' + fetchMaybeToken, + findAssociatedTokenPda, + getCreateAssociatedTokenIdempotentInstruction, + TOKEN_PROGRAM_ADDRESS, +} from '@solana-program/token' +import type { Game, StarSystem } from '@staratlas/dev-sage' +import { + findStarbasePlayerPda, + getDepositCargoToGameInstruction, + getRegisterStarbasePlayerInstruction, +} from '@staratlas/dev-sage' +import { logger } from '../../../../../logger.js' +import { connection } from '../../../../../service/sol/index.js' import { sendAndConfirmInstructions } from '../../../../../service/sol/send-and-confirm-tx.js' -import { keyPair } from '../../../../../service/wallet' -import { getTokenBalance } from '../../../basedbot.js' -import { programs } from '../../programs.js' -import { depositCargoIx } from '../ix/deposit-cargo.js' -import { getCargoType } from '../state/cargo-types.js' -import { - getCargoPodsForStarbasePlayer, - getStarbasePlayer, -} from '../state/starbase-player.js' -import { Player } from '../state/user-account.js' +import type { AccountWithKey } from '../../types.js' +import { getStarbasePlayer } from '../state/starbase-player.js' +import type { Player } from '../state/user-account.js' + +export type CargoDeposit = { + /** Cargo mint address */ + mint: Address + /** Amount to deposit */ + amount: bigint +} -export const depositCargo = async ( +/** + * Deposit cargo from wallet to starbase + * + * NOTE: Deposits can only be made at the main CSS starbase. + * + * Uses game.cargoDefinitions.cache as the destination ATA owner. + * Reference: fc-app StarbaseActions.ts lines 88-101 + */ +export async function depositCargo( player: Player, - game: Game, - starbase: Starbase, - mint: PublicKey, - amount: BN, -): Promise => { - const instructions: InstructionReturn[] = [] + game: AccountWithKey, + system: AccountWithKey, + deposits: CargoDeposit[], +): Promise { + if (deposits.length === 0) { + return + } - const starbasePlayer = await getStarbasePlayer(player, starbase, programs) + // Deposits can only be made at CSS + if (system.key !== player.homeSystem.key) { + logger.warn( + `Cannot deposit cargo: not at home CSS. Current: ${system.key}, CSS: ${player.homeSystem.key}`, + ) + return + } - const sourceTokenAccount = getAssociatedTokenAddressSync( - mint, - player.signer.publicKey(), - ) - - const cargoPodTo = await getCargoPodsForStarbasePlayer( - starbasePlayer, - programs, - ) - const destinationTokenAccount = getAssociatedTokenAddressSync( - mint, - cargoPodTo.key, - true, - ) + const signer = player.signer + const character = player.character + const keyIndex = player.keyIndex + const instructions: Instruction[] = [] + + // Get or register starbase player + const starbasePlayer = await getStarbasePlayer(player, system) + let starbasePlayerAddress: Address + + if (starbasePlayer) { + starbasePlayerAddress = starbasePlayer.key + } else { + // Derive and register + const [pda] = await findStarbasePlayerPda({ + system: system.key, + character: character.key, + }) + starbasePlayerAddress = pda + + logger.info(`Registering new starbase player at system: ${system.key}`) + instructions.push( + getRegisterStarbasePlayerInstruction({ + game: game.key, + character: character.key, + profileFaction: player.profileFaction.key, + funder: signer, + system: system.key, + starbasePlayer: starbasePlayerAddress, + }), + ) + } + + const cargoCache = game.cargoDefinitions.cache + + for (const { mint, amount } of deposits) { + if (amount <= 0n) { + logger.debug(`Skipping cargo ${mint} with amount ${amount}`) + continue + } + + // Source: wallet ATA (owned by signer) + const [sourceAta] = await findAssociatedTokenPda({ + owner: signer.address, + mint, + tokenProgram: TOKEN_PROGRAM_ADDRESS, + }) + + // Check wallet balance + const sourceToken = await fetchMaybeToken(connection.rpc, sourceAta) + if (!sourceToken.exists) { + logger.debug(`No wallet token account for mint ${mint}`) + continue + } + const walletBalance = sourceToken.data.amount + if (walletBalance === 0n) { + logger.debug(`Zero balance for mint ${mint}`) + continue + } + + // Deposit at most what's in wallet + const depositAmount = amount > walletBalance ? walletBalance : amount + + // Destination: cargo cache ATA (owned by game.cargoDefinitions.cache) + const [destAta] = await findAssociatedTokenPda({ + owner: cargoCache, + mint, + tokenProgram: TOKEN_PROGRAM_ADDRESS, + }) + + // Create destination ATA if needed (idempotent) + instructions.push( + getCreateAssociatedTokenIdempotentInstruction({ + payer: signer, + owner: cargoCache, + mint, + ata: destAta, + }), + ) + + // Deposit instruction + instructions.push( + getDepositCargoToGameInstruction({ + game: game.key, + character: character.key, + systemAndStarbasePlayerSystem: system.key, + systemAndStarbasePlayerStarbasePlayer: starbasePlayerAddress, + cargoCache, + profileProfile: character.playerProfile, + owner: signer, + profileSigner: signer, + source: sourceAta, + destination: destAta, + amount: depositAmount, + keyIndex, + }), + ) + + logger.debug(`Prepared cargo deposit: ${depositAmount} of mint ${mint}`) + } + + if (instructions.length > 0) { + logger.info( + `Depositing cargo to starbase (${instructions.length} instructions)`, + ) + await sendAndConfirmInstructions(signer)(instructions) + } +} - instructions.push( - createAssociatedTokenAccountIdempotent(mint, cargoPodTo.key, true) - .instructions, - ) +/** + * Get mint address for a cargo ID from game config + */ +export function getCargoMint( + game: AccountWithKey, + cargoId: number, +): Address | undefined { + // mintToId is Map, we need reverse lookup + for (const [mint, id] of game.cargoDefinitions.mintToId) { + if (id === cargoId) { + return mint + } + } + return undefined +} - const cargoType = getCargoType(player.cargoTypes, game, mint) +/** + * Check starbase cargo balance for a specific cargo type + * Reads from starbasePlayer.cargoPod.amounts + */ +export async function getStarbaseCargoBalance( + player: Player, + system: AccountWithKey, + cargoId: number, +): Promise { + const starbasePlayer = await getStarbasePlayer(player, system) + if (!starbasePlayer) { + return 0n + } - const amountAtOrigin = await getTokenBalance( - player.signer.publicKey(), + // CargoPod.amounts is Map + const amounts = starbasePlayer.cargoPod?.amounts + if (!amounts) { + return 0n + } + + return amounts.get(cargoId) ?? 0n +} + +/** + * Check wallet balance for a cargo mint + */ +export async function getWalletCargoBalance( + walletAddress: Address, + mint: Address, +): Promise { + const [walletAta] = await findAssociatedTokenPda({ + owner: walletAddress, mint, - ) + tokenProgram: TOKEN_PROGRAM_ADDRESS, + }) + const token = await fetchMaybeToken(connection.rpc, walletAta) + return token.exists ? token.data.amount : 0n +} + +/** + * Check if the given system is the player's home CSS + * Deposits can only be made at the main CSS starbase + */ +function isHomeCSS( + player: Player, + system: AccountWithKey, +): boolean { + return system.key === player.homeSystem.key +} + +/** + * Ensure cargo is available at starbase for fleet transfer. + * Deposits from wallet if needed. + * + * NOTE: Deposits can only be made at the main CSS starbase. + * If not at CSS, will skip deposit and log a warning. + * + * @param player - Player account + * @param game - Game account + * @param system - Current star system + * @param cargoNeeds - Array of { cargoId, amount } needed + * @returns true if cargo is available or was deposited, false if insufficient + */ +export async function ensureCargo( + player: Player, + game: AccountWithKey, + system: AccountWithKey, + cargoNeeds: Array<{ cargoId: number; amount: number }>, +): Promise { + if (cargoNeeds.length === 0) { + return true + } + + // Deposits can only be made at the main CSS + if (!isHomeCSS(player, system)) { + logger.debug( + `Not at home CSS (${player.homeSystem.key}), skipping cargo deposit`, + ) + return true // Don't block the transfer attempt + } + + const deposits: CargoDeposit[] = [] + + for (const { cargoId, amount } of cargoNeeds) { + if (amount <= 0) continue + + const mint = getCargoMint(game, cargoId) + if (!mint) { + logger.warn(`Unknown cargoId ${cargoId}, cannot deposit`) + continue + } + + // Check wallet balance + const walletBalance = await getWalletCargoBalance( + player.signer.address, + mint, + ) + + if (walletBalance === 0n) { + logger.debug( + `No wallet balance for cargoId ${cargoId} (mint ${mint})`, + ) + continue + } + + // Deposit what we can (floor amount to integer for BigInt conversion) + const amountInt = BigInt(Math.floor(amount)) + const depositAmount = + walletBalance >= amountInt ? amountInt : walletBalance + + deposits.push({ mint, amount: depositAmount }) + logger.debug( + `Will deposit ${depositAmount} of cargoId ${cargoId} to starbase`, + ) + } - if (amountAtOrigin.lt(new BN(amount))) { - throw new Error('Not enough cargo available at origin') + if (deposits.length > 0) { + await depositCargo(player, game, system, deposits) + return true } - instructions.push( - depositCargoIx( - player, - game, - starbase, - starbasePlayer, - cargoPodTo.key, - sourceTokenAccount, - destinationTokenAccount, - cargoType.key, - programs, - amount, - ), - ) - - await sendAndConfirmInstructions([keyPair])( - await ixReturnsToIxs(instructions, player.signer), - ) + // No deposits needed or possible + return cargoNeeds.every((need) => { + const mint = getCargoMint(game, need.cargoId) + return mint !== undefined + }) } diff --git a/src/main/basedbot/lib/sage/act/deposit-ship.ts b/src/main/basedbot/lib/sage/act/deposit-ship.ts index e78c3965..3e0e10ad 100644 --- a/src/main/basedbot/lib/sage/act/deposit-ship.ts +++ b/src/main/basedbot/lib/sage/act/deposit-ship.ts @@ -1,119 +1,346 @@ -import { getAssociatedTokenAddressSync } from '@solana/spl-token' +/** + * Deposit/ensure ships action for C4 + * + * In C4, ships must be deposited to StarbasePlayer escrow before + * they can be added to a fleet. + * + * ESCROW ATA DERIVATION (from fc-app reference): + * - Ship escrow token account owner: `character.key` (Character PDA) + * - Profile fields: `character.playerProfile` (Profile address stored in Character) + * - Derive with: findAssociatedTokenPda({ owner: character.key, mint, tokenProgram }) + */ +import type { Address, Instruction, KeyPairSigner } from '@solana/kit' import { - createAssociatedTokenAccountIdempotent, - InstructionReturn, - ixReturnsToIxs, -} from '@staratlas/data-source' + fetchMaybeToken, + findAssociatedTokenPda, + getCreateAssociatedTokenIdempotentInstruction, + TOKEN_PROGRAM_ADDRESS, +} from '@solana-program/token' +import type { Game, StarSystem } from '@staratlas/dev-sage' import { - Game, - SagePlayerProfile, - Ship, - Starbase, - WrappedShipEscrow, -} from '@staratlas/sage' -import BN from 'bn.js' + findStarbasePlayerPda, + getAddShipEscrowInstruction, + getRegisterStarbasePlayerInstruction, +} from '@staratlas/dev-sage' +import { logger } from '../../../../../logger.js' import { connection } from '../../../../../service/sol/index.js' import { sendAndConfirmInstructions } from '../../../../../service/sol/send-and-confirm-tx.js' -import { keyPair } from '../../../../../service/wallet' -import { programs } from '../../programs.js' -import { addShipEscrowIx } from '../ix/add-ship-escrow.js' -import { getShipByMint, getStarbasePlayer } from '../state/starbase-player.js' -import { Player } from '../state/user-account.js' +import type { AccountWithKey, Character } from '../../types.js' +import { getStarbasePlayer } from '../state/starbase-player.js' +import type { Player } from '../state/user-account.js' +import type { FleetShip } from './create-fleet.js' -import { FleetShips } from './create-fleet.js' +export type DepositShipParams = { + game: AccountWithKey + character: AccountWithKey + system: AccountWithKey + /** Ship type id */ + id: number + /** Number of ships to deposit */ + amount: number + signer: KeyPairSigner + keyIndex?: number +} + +export type ShipTransfer = { + /** Ship type id */ + id: number + /** Amount to deposit (positive) or withdraw (negative) */ + amount: number + /** Source token account (wallet ATA for deposits) */ + source: Address + /** Destination token account (escrow ATA for deposits) */ + destination: Address + /** Ship mint address (for creating escrow ATA) */ + mint?: Address +} -export const depositShip = async ( +/** + * Deposit ships to StarbasePlayer escrow + * @param starbasePlayerKey - Optional starbase player key. If not provided, will be fetched. + */ +export async function depositShips( player: Player, - game: Game, - starbase: Starbase, - ship: Ship, - amount: BN, -): Promise => { - const instructions: InstructionReturn[] = [] + game: AccountWithKey, + system: AccountWithKey, + transfers: ShipTransfer[], + starbasePlayerKey?: Address, +): Promise { + if (transfers.length === 0) { + return + } - const { mint } = ship.data - const starbasePlayer = await getStarbasePlayer(player, starbase, programs) + // Use provided key or fetch from cache + let sbpKey = starbasePlayerKey + if (!sbpKey) { + const starbasePlayer = await getStarbasePlayer(player, system) + if (!starbasePlayer) { + logger.warn( + `No StarbasePlayer found at system ${system.key}, cannot deposit ships`, + ) + return + } + sbpKey = starbasePlayer.key + } - const sourceTokenAccount = getAssociatedTokenAddressSync( - mint, - player.signer.publicKey(), - ) + const signer = player.signer + const character = player.character + const keyIndex = player.keyIndex + const instructions: Instruction[] = [] - const [sagePlayerProfile] = SagePlayerProfile.findAddress( - programs.sage, - player.profile.key, - game.key, - ) - const shipEscrowTokenAccountResult = createAssociatedTokenAccountIdempotent( - mint, - sagePlayerProfile, - true, - ) + for (const { id, amount, source, destination, mint } of transfers) { + if (amount <= 0) { + logger.debug(`Skipping ship id ${id} with amount ${amount}`) + continue + } - instructions.push(shipEscrowTokenAccountResult.instructions) + // Create escrow ATA if it doesn't exist (idempotent) + // Owner is character.key (Character PDA) - NOT player.profile.key! + // See fc-app: StarbaseMenu.tsx line 1459 + if (mint) { + instructions.push( + getCreateAssociatedTokenIdempotentInstruction({ + payer: signer, + owner: character.key, + mint, + ata: destination, + }), + ) + } - let uiAmount = 0 - try { - const info = await connection.getTokenAccountBalance(sourceTokenAccount) - if (info.value.uiAmount === null) uiAmount = 0 - else uiAmount = info.value.uiAmount - } catch (_) { - uiAmount = 0 + // Profile fields use character.playerProfile (matching fc-app) + instructions.push( + getAddShipEscrowInstruction({ + game: game.key, + starSystem: system.key, + starbasePlayer: sbpKey, + character: character.key, + profile: character.playerProfile, + profileSignerProfile: character.playerProfile, + profileSignerSigner: signer, + key: signer, + id, + originTokenAccount: source, + shipEscrowTokenAccount: destination, + shipAmount: amount, + keyIndex, + }), + ) } - const amountAtOrigin = new BN(uiAmount) + if (instructions.length > 0) { + // Batch deposits to stay under Solana's 1232 byte tx limit + // Each ship type = 2 instructions (ATA + deposit), ~4-5 ship types fit per tx + const BATCH_SIZE = 10 // instructions per batch (5 ship types) + const batches: Instruction[][] = [] + for (let i = 0; i < instructions.length; i += BATCH_SIZE) { + batches.push(instructions.slice(i, i + BATCH_SIZE)) + } - if (amountAtOrigin.lt(new BN(amount))) { - throw new Error( - `Not enough ships available at origin ${ship.data.mint.toBase58()}`, + logger.info( + `Depositing ${transfers.length} ship type(s) to escrow in ${batches.length} batch(es)`, ) + + for (let i = 0; i < batches.length; i++) { + logger.debug(`Deposit batch ${i + 1}/${batches.length}`) + await sendAndConfirmInstructions(signer)(batches[i]) + } } +} - const pred = (v: WrappedShipEscrow) => v.ship.equals(ship.key) - // const shipEscrow = starbasePlayer.wrappedShipEscrows.find(pred) - const index = starbasePlayer.wrappedShipEscrows.findIndex(pred) - - instructions.push( - addShipEscrowIx( - player, - game, - starbase, - starbasePlayer, - sagePlayerProfile, - programs, - sourceTokenAccount, - ship.key, - shipEscrowTokenAccountResult.address, - amount, - index === -1 ? null : index, - ), - ) +/** + * Get ship mint address from game config by ship ID + * + * Uses ships field: + * - ships.offsetList[i].key = shipId + * - ships.unsizedList[i] = Ship definition (contains mint field) + */ +export function getShipMint( + game: AccountWithKey, + shipId: number, +): Address | undefined { + const { ships } = game.shipDefinitions + + // Find the ship definition by ID + // ships.offsetList[i].key = ShipId, ships.unsizedList[i] = Ship + for (let i = 0; i < ships.offsetList.length; i++) { + const id = ships.offsetList[i].key + if (id === shipId) { + const ship = ships.unsizedList[i] + if (ship && 'mint' in ship) { + logger.debug(`getShipMint: found mint for shipId ${shipId}`) + return ship.mint + } + break + } + } - await sendAndConfirmInstructions([keyPair])( - await ixReturnsToIxs(instructions, player.signer), + logger.warn( + `getShipMint: no mint found for shipId ${shipId} (${ships.offsetList.length} ships in config)`, ) + return undefined } -export const ensureShips = async ( + +/** + * Check if current system is the home CSS (deposits only work there) + */ +function isHomeCSS( player: Player, - game: Game, - starbase: Starbase, - fleetShips: FleetShips, -): Promise => { - const starbasePlayer = await getStarbasePlayer(player, starbase, programs) - - for (const fleetShip of fleetShips) { - const desiredAmount = new BN(fleetShip.count) - - const ship = await getShipByMint(fleetShip.shipMint, game, programs) - const pred = (v: WrappedShipEscrow) => v.ship.equals(ship.key) - const shipEscrow = starbasePlayer.wrappedShipEscrows.find(pred) - const needed = shipEscrow - ? desiredAmount.sub(shipEscrow.amount) - : desiredAmount - - if (needed.gt(new BN(0))) { - await depositShip(player, game, starbase, ship, needed) + system: AccountWithKey, +): boolean { + return system.key === player.homeSystem.key +} + +/** + * Check wallet balance for a ship mint + */ +async function getWalletShipBalance( + walletAddress: Address, + mint: Address, +): Promise { + const [walletAta] = await findAssociatedTokenPda({ + owner: walletAddress, + mint, + tokenProgram: TOKEN_PROGRAM_ADDRESS, + }) + const token = await fetchMaybeToken(connection.rpc, walletAta) + return token.exists ? token.data.amount : 0n +} + +/** + * Ensure ships are available in escrow for fleet creation. + * Auto-deposits from wallet if at home CSS. + */ +export async function ensureShips( + player: Player, + game: AccountWithKey, + system: AccountWithKey, + ships: FleetShip[], +): Promise { + logger.debug( + `ensureShips called with ${ships.length} ship types: ${JSON.stringify(ships)}`, + ) + + if (ships.length === 0) { + logger.debug(`No ships to ensure`) + return + } + + // Check if at home CSS - deposits only work there + if (!isHomeCSS(player, system)) { + logger.debug( + `Not at home CSS (system=${system.key}, home=${player.homeSystem.key}), skipping`, + ) + return + } + + logger.debug(`At home CSS, checking ships...`) + + let starbasePlayer = await getStarbasePlayer(player, system) + const shipsInEscrow = starbasePlayer?.shipsInEscrow ?? new Map() + + // Check what's needed vs available + const transfers: ShipTransfer[] = [] + + for (const { id, amount } of ships) { + const inEscrow = Number(shipsInEscrow.get(id) ?? 0n) + const needed = amount - inEscrow + + if (needed <= 0) { + continue + } + + const mint = getShipMint(game, id) + if (!mint) { + logger.warn(`Unknown ship ID ${id}, cannot deposit`) + continue + } + + // Check wallet balance + const walletBalance = await getWalletShipBalance( + player.signer.address, + mint, + ) + if (walletBalance === 0n) { + logger.warn( + `No ships of type ${id} (mint ${mint}) in wallet. ` + + `Need ${needed} more for fleet.`, + ) + continue + } + + const depositAmount = Math.min(needed, Number(walletBalance)) + + // Derive ATAs + const [sourceAta] = await findAssociatedTokenPda({ + owner: player.signer.address, + mint, + tokenProgram: TOKEN_PROGRAM_ADDRESS, + }) + const [destAta] = await findAssociatedTokenPda({ + owner: player.character.key, // Character PDA is escrow owner + mint, + tokenProgram: TOKEN_PROGRAM_ADDRESS, + }) + + transfers.push({ + id, + amount: depositAmount, + source: sourceAta, + destination: destAta, + mint, + }) + + logger.debug( + `Will deposit ${depositAmount} ships of type ${id} to escrow`, + ) + } + + if (transfers.length > 0) { + let sbpKey: Address + + // Need to ensure starbase player exists + if (starbasePlayer) { + sbpKey = starbasePlayer.key + } else { + const [pda] = await findStarbasePlayerPda({ + system: system.key, + character: player.character.key, + }) + sbpKey = pda + + logger.info(`Registering starbase player at ${system.key}`) + await sendAndConfirmInstructions(player.signer)([ + getRegisterStarbasePlayerInstruction({ + game: game.key, + character: player.character.key, + profileFaction: player.profileFaction.key, + funder: player.signer, + system: system.key, + starbasePlayer: pda, + }), + ]) } + + // Pass the key directly to avoid cache miss + await depositShips(player, game, system, transfers, sbpKey) + } + + // Final check + const updatedStarbasePlayer = await getStarbasePlayer(player, system) + const updatedEscrow = updatedStarbasePlayer?.shipsInEscrow ?? new Map() + const totalNeeded = ships.reduce((sum, s) => sum + s.amount, 0) + const totalInEscrow = [...updatedEscrow.values()].reduce( + (sum, n) => sum + Number(n), + 0, + ) + + if (totalInEscrow < totalNeeded) { + logger.warn( + `Insufficient ships in escrow after deposit: ${totalInEscrow}/${totalNeeded}`, + ) + } else { + logger.debug(`Ships in escrow: ${totalInEscrow}/${totalNeeded} - OK`) } } diff --git a/src/main/basedbot/lib/sage/act/disband-fleet.ts b/src/main/basedbot/lib/sage/act/disband-fleet.ts index 032ea455..d8c41181 100644 --- a/src/main/basedbot/lib/sage/act/disband-fleet.ts +++ b/src/main/basedbot/lib/sage/act/disband-fleet.ts @@ -1,92 +1,72 @@ -import { InstructionReturn, ixReturnsToIxs } from '@staratlas/data-source' -import { Game, Starbase, WrappedShipEscrow } from '@staratlas/sage' +/** + * Disband fleet action for C4 + */ +import type { KeyPairSigner } from '@solana/kit' +import type { Game, StarSystem } from '@staratlas/dev-sage' +import { getDisbandFleetInstruction } from '@staratlas/dev-sage' -import dayjs from '../../../../../dayjs.js' import { logger } from '../../../../../logger.js' import { sendAndConfirmInstructions } from '../../../../../service/sol/send-and-confirm-tx.js' -import { keyPair } from '../../../../../service/wallet' -import { programs } from '../../programs.js' -import { closeDisbandedFleetIx } from '../ix/close-disbanded-fleet.js' -import { disbandFleetIx } from '../ix/disband-fleet.js' -import { disbandedFleetToEscrowIx } from '../ix/disbanded-fleet-to-escrow.js' -import { getFleetShips } from '../state/get-fleet-ships.js' +import type { AccountWithKey, Character, Fleet } from '../../types.js' import { getStarbasePlayer } from '../state/starbase-player.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' -import { getName } from '../util.js' +import type { FleetInfo } from '../state/user-fleets.js' +import type { Player } from '../state/user-account.js' -export const disbandFleet = async ( +export type DisbandFleetParams = { + game: AccountWithKey + fleet: AccountWithKey + character: AccountWithKey + system: AccountWithKey + signer: KeyPairSigner + keyIndex?: number +} + +/** + * Execute disband fleet action + * + * Note: Fleet must be in Docked state to be disbanded. + * Ships will be returned to the StarbasePlayer escrow. + */ +export async function disbandFleet( player: Player, - game: Game, - starbase: Starbase, + game: AccountWithKey, + system: AccountWithKey, fleetInfo: FleetInfo, -): Promise => { - const ixs: InstructionReturn[] = [] - const starbasePlayer = await getStarbasePlayer(player, starbase, programs) - const { fleet } = fleetInfo +): Promise { + const fleet = fleetInfo.fleet + const signer = player.signer + const keyIndex = player.keyIndex - if (fleetInfo.fleetState.data.warpCooldown) { - const timeLeft = dayjs.duration( - dayjs().diff(fleetInfo.fleetState.data.warpCooldownExpiry), + // Fleet must be docked to disband + if (fleet.state.__kind !== 'Docked') { + logger.warn( + `Cannot disband fleet "${fleetInfo.fleetName}" - not docked (state: ${fleet.state.__kind})`, ) + return + } + const starbasePlayer = await getStarbasePlayer(player, system) + if (!starbasePlayer) { logger.warn( - `Fleet is on warp cooldown, cannot disband. Retry in: ${timeLeft.humanize()}`, + `No StarbasePlayer at system ${system.key}, cannot disband fleet`, ) - return } - const { disbandedFleetKey, instructions } = disbandFleetIx( - player, - game, - starbase, - starbasePlayer, - programs, - fleet, - ) + logger.info(`Disbanding fleet "${fleetInfo.fleetName}"`) - ixs.push(instructions) - - const [fleetShips] = await getFleetShips(fleet) - - for (let i = fleetShips.fleetShips.length - 1; i >= 0; --i) { - const fleetShipInfo = fleetShips.fleetShips[i] - - const pred = (v: WrappedShipEscrow) => v.ship.equals(fleetShipInfo.ship) - const shipEscrowIndex = - starbasePlayer.wrappedShipEscrows.findIndex(pred) - - ixs.push( - disbandedFleetToEscrowIx( - player, - game, - starbase, - starbasePlayer, - programs, - shipEscrowIndex === -1 ? null : shipEscrowIndex, - i, - disbandedFleetKey[0], - fleet.data.fleetShips, - fleetShipInfo.ship, - fleetShipInfo.amount, - ), - ) - } + const instruction = getDisbandFleetInstruction({ + profileValidationSigner: signer, + game: game.key, + character: player.character.key, + profileValidationProfile: player.character.playerProfile, + systemAndStarbasePlayerSystem: system.key, + systemAndStarbasePlayerStarbasePlayer: starbasePlayer.key, + fleet: fleet.key, + keyIndex, + }) - ixs.push( - closeDisbandedFleetIx( - player, - programs, - disbandedFleetKey[0], - fleet.data.fleetShips, - ), - ) - logger.debug( - `Added ${ixs.length} ixs for disbanding fleet ${getName(fleet)}`, - ) + await sendAndConfirmInstructions(signer)([instruction]) - await sendAndConfirmInstructions([keyPair])( - await ixReturnsToIxs(ixs, player.signer), - ) + logger.info(`Fleet "${fleetInfo.fleetName}" disbanded successfully`) } diff --git a/src/main/basedbot/lib/sage/act/dock.ts b/src/main/basedbot/lib/sage/act/dock.ts index 610706a6..42fb019c 100644 --- a/src/main/basedbot/lib/sage/act/dock.ts +++ b/src/main/basedbot/lib/sage/act/dock.ts @@ -1,39 +1,176 @@ -import { ixReturnsToIxs } from '@staratlas/data-source' -import { Game } from '@staratlas/sage' - -import { sendAndConfirmInstructions } from '../../../../../service/sol/send-and-confirm-tx.js' -import { keyPair } from '../../../../../service/wallet' -import { programs } from '../../programs.js' -import { Coordinates } from '../../util/coordinates.js' -import { dockIx } from '../ix/dock.js' -import { starbaseByCoordinates } from '../state/starbase-by-coordinates.js' -import { getStarbasePlayer } from '../state/starbase-player.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' - -export const dock = async ( - fleetInfo: FleetInfo, - coordinates: Coordinates, - player: Player, - game: Game, -): Promise => { - const starbase = await starbaseByCoordinates(coordinates) - - if (!starbase) { - throw new Error(`No starbase found at ${coordinates}`) +/** + * Dock fleet action - C4 migration + * Transitions fleet from Idle to Docked state + */ +import type { Address, Instruction, KeyPairSigner } from '@solana/kit' +import { + findStarbasePlayerPda, + getIdleToDockedInstruction, + getRegisterStarbasePlayerInstruction, +} from '@staratlas/dev-sage' + +import { logger } from '../../../../../logger.js' +import { + sendAndConfirmInstructions, + SimulationProgramError, +} from '../../../../../service/sol/send-and-confirm-tx.js' +import type { + AccountWithKey, + Character, + Fleet, + Game, + ProfileFactionAccount, + StarSystem, +} from '../../types.js' + +export type DockResult = { + success: boolean + reason?: string +} + +export type DockParams = { + game: AccountWithKey + fleet: AccountWithKey + profileFaction: AccountWithKey + character: AccountWithKey + system: AccountWithKey + starbasePlayer: Address | undefined + signer: KeyPairSigner + keyIndex?: number +} + +/** + * Validate fleet can dock and create instructions + * Returns null if fleet cannot dock (with logging) + */ +export async function validateAndCreateDockInstructions({ + game, + fleet, + profileFaction, + character, + system, + starbasePlayer, + signer, + keyIndex = 0, +}: DockParams): Promise { + const instructions: Instruction[] = [] + + // Validate fleet state - must be Idle to dock + const state = fleet.state.__kind + if (state !== 'Idle') { + if (state === 'Docked') { + logger.debug(`Fleet already docked, skipping dock action`) + } else { + logger.warn(`Cannot dock: fleet is in ${state} state, must be Idle`) + } + return null + } + + // Validate fleet is at a starbase (has matching system) + // Fleet location should match system coordinates + const fleetLocation = fleet.location as Array<{ raw: bigint }> | undefined + if (!fleetLocation || fleetLocation.length < 2) { + logger.warn(`Cannot dock: fleet has no location data`) + return null + } + + let starbasePlayerAddress = starbasePlayer + + // Register starbase player if needed + if (!starbasePlayerAddress) { + const [pda] = await findStarbasePlayerPda({ + system: system.key, + character: character.key, + }) + starbasePlayerAddress = pda + + logger.info(`Registering new starbase player at system: ${system.key}`) + + instructions.push( + getRegisterStarbasePlayerInstruction({ + game: game.key, + character: character.key, + profileFaction: profileFaction.key, + funder: signer, + system: system.key, + starbasePlayer: starbasePlayerAddress, + }), + ) } - const starbasePlayer = await getStarbasePlayer(player, starbase, programs) - - const ix = dockIx( - fleetInfo, - player, - game, - starbase, - starbasePlayer, - programs, + + // Add dock instruction + instructions.push( + getIdleToDockedInstruction({ + game: game.key, + systemAndStarbasePlayerSystem: system.key, + systemAndStarbasePlayerStarbasePlayer: starbasePlayerAddress, + fleet: fleet.key, + profileValidationSigner: signer, + profileValidationProfile: character.playerProfile, + keyIndex, + }), ) - const instructions = await ixReturnsToIxs(ix, player.signer) + return instructions +} - await sendAndConfirmInstructions([keyPair])(instructions) +/** + * Creates dock instruction (Idle -> Docked) + * @deprecated Use validateAndCreateDockInstructions for proper validation + */ +export async function createDockInstructions( + params: DockParams, +): Promise { + const instructions = await validateAndCreateDockInstructions(params) + if (!instructions) { + throw new Error('Dock validation failed - check logs for details') + } + return instructions +} + +/** + * Execute dock action - sends transaction + * Returns result indicating success or failure reason + */ +export async function dock(params: DockParams): Promise { + const instructions = await validateAndCreateDockInstructions(params) + + if (!instructions) { + return { success: false, reason: 'validation_failed' } + } + + try { + await sendAndConfirmInstructions(params.signer)(instructions) + return { success: true } + } catch (e) { + // Handle simulation errors gracefully + if (e instanceof SimulationProgramError) { + const logsStr = e.logs.join('\n') + + // Fleet already docked - stale local state, treat as success + if ( + logsStr.includes('Status mismatch') && + logsStr.includes('actual_state=Docked') + ) { + return { success: true } + } + + // Starbase faction mismatch + if (logsStr.includes('System mismatch')) { + return { success: false, reason: 'incompatible_starbase' } + } + + // Any other simulation error + logger.warn( + `Dock simulation failed: ${e.message}\nLogs: ${e.logs.join('\n')}`, + ) + return { success: false, reason: 'transaction_failed' } + } + + // Any other error + logger.warn( + `Dock failed: ${e instanceof Error ? e.message : String(e)}`, + ) + return { success: false, reason: 'transaction_failed' } + } } diff --git a/src/main/basedbot/lib/sage/act/end-mine.ts b/src/main/basedbot/lib/sage/act/end-mine.ts index 35e54a5d..5a03e32a 100644 --- a/src/main/basedbot/lib/sage/act/end-mine.ts +++ b/src/main/basedbot/lib/sage/act/end-mine.ts @@ -1,100 +1,139 @@ +/** + * End mining action - C4 migration + * Transitions fleet from MineAsteroid to Idle state + */ +import type { Instruction, KeyPairSigner } from '@solana/kit' import { - createAssociatedTokenAccountIdempotent, - ixReturnsToIxs, -} from '@staratlas/data-source' -import { Game } from '@staratlas/sage' + getStopMiningAsteroidInstruction, + getTheOneFleetStateHandlerInstruction, +} from '@staratlas/dev-sage' import { logger } from '../../../../../logger.js' import { sendAndConfirmInstructions } from '../../../../../service/sol/send-and-confirm-tx.js' -import { keyPair } from '../../../../../service/wallet' -import { programs } from '../../programs.js' -import { miningHandlerIx } from '../ix/fleet-state-handler.js' -import { stopMiningIx } from '../ix/stop-mining.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' -import { Mineable } from '../state/world-map.js' - -export const endMine = async ( - fleetInfo: FleetInfo, - player: Player, - game: Game, - mineable: Mineable, -): Promise => { - const { fleet } = fleetInfo - - if (!fleet.state.MineAsteroid) { - logger.warn('Fleet is not mining, cannot End Mine') - - return +import type { + AccountWithKey, + CelestialBody, + Character, + Fleet, + Game, +} from '../../types.js' + +export type EndMineParams = { + game: AccountWithKey + fleet: AccountWithKey + character: AccountWithKey + asteroid: AccountWithKey + signer: KeyPairSigner + keyIndex?: number +} + +/** + * Creates stop mining instruction + */ +export function createStopMiningInstruction({ + game, + fleet, + character, + asteroid, + signer, + keyIndex = 0, +}: EndMineParams): Instruction { + // Check fleet state + if (fleet.state.__kind !== 'MineAsteroid') { + throw new Error( + `Cannot stop mining: fleet is in ${fleet.state.__kind} state, must be MineAsteroid`, + ) + } + + return getStopMiningAsteroidInstruction({ + profileValidationSigner: signer, + profileValidationProfile: character.playerProfile, + character: character.key, + fleet: fleet.key, + asteroid: asteroid.key, + game: game.key, + keyIndex, + }) +} + +/** + * Creates mining handler instruction (processes mining rewards) + * This is typically called before stopMining to collect resources + */ +export function createMiningHandlerInstruction({ + game, + fleet, + asteroid, + signer, +}: Pick): Instruction { + return getTheOneFleetStateHandlerInstruction({ + game: game.key, + fleet: fleet.key, + asteroid: asteroid.key, + funder: signer, + }) +} + +/** + * Execute end mining action - sends transaction + * Stops mining and returns fleet to Idle state + */ +export async function endMine(params: EndMineParams): Promise { + const instruction = createStopMiningInstruction(params) + + logger.info(`Stopping mining for fleet ${params.fleet.key}`) + await sendAndConfirmInstructions(params.signer)([instruction]) +} + +/** + * Execute full mining cycle completion: + * 1. Process mining rewards (handler) + * 2. Stop mining + */ +export async function completeMining(params: EndMineParams): Promise { + const handlerIx = createMiningHandlerInstruction(params) + const stopIx = createStopMiningInstruction(params) + + logger.info(`Completing mining cycle for fleet ${params.fleet.key}`) + + // Send handler first to process rewards + await sendAndConfirmInstructions(params.signer)([handlerIx]) + + // Then stop mining + await sendAndConfirmInstructions(params.signer)([stopIx]) +} + +/** + * Check if mining is complete (end time reached) + */ +export function isMiningComplete(fleet: AccountWithKey): boolean { + const state = fleet.state + + if (state.__kind === 'MineAsteroid') { + const mineState = state.fields[0] as unknown as { end: bigint } + return BigInt(Date.now() / 1000) >= mineState.end } - const [ - foodToken, - ammoToken, - resourceFromToken, - resourceToToken, - fuelToken, - ] = [ - createAssociatedTokenAccountIdempotent( - game.data.mints.food, - fleet.data.cargoHold, - true, - ), - createAssociatedTokenAccountIdempotent( - game.data.mints.ammo, - fleet.data.ammoBank, - true, - ), - createAssociatedTokenAccountIdempotent( - mineable.mineItem.data.mint, - mineable.resource.data.mineItem, - true, - ), - createAssociatedTokenAccountIdempotent( - mineable.mineItem.data.mint, - fleet.data.cargoHold, - true, - ), - createAssociatedTokenAccountIdempotent( - game.data.mints.fuel, - fleet.data.fuelTank, - true, - ), - ] - - await ixReturnsToIxs( - [ - foodToken.instructions, - ammoToken.instructions, - resourceFromToken.instructions, - resourceToToken.instructions, - miningHandlerIx( - fleetInfo, - player, - mineable, - foodToken.address, - ammoToken.address, - resourceFromToken.address, - resourceToToken.address, - programs, - game, - ), - ], - player.signer, - ).then(sendAndConfirmInstructions([keyPair])) - - await ixReturnsToIxs( - [ - fuelToken.instructions, - stopMiningIx( - fleetInfo, - player, - game, - mineable, - fuelToken.address, - programs, - ), - ], - player.signer, - ).then(sendAndConfirmInstructions([keyPair])) + return false } + +/** + * Get remaining mining time in seconds (0 if complete) + */ +export function getRemainingMiningTime(fleet: AccountWithKey): number { + const state = fleet.state + + if (state.__kind === 'MineAsteroid') { + const mineState = state.fields[0] as unknown as { end: bigint } + const endTime = Number(mineState.end) + const now = Math.floor(Date.now() / 1000) + return Math.max(0, endTime - now) + } + + return 0 +} + +/** + * Alias for endMine() + */ +export const stopMining = endMine diff --git a/src/main/basedbot/lib/sage/act/end-move.ts b/src/main/basedbot/lib/sage/act/end-move.ts index 999c1d18..79be0422 100644 --- a/src/main/basedbot/lib/sage/act/end-move.ts +++ b/src/main/basedbot/lib/sage/act/end-move.ts @@ -1,44 +1,110 @@ -import { - createAssociatedTokenAccountIdempotent, - ixReturnsToIxs, -} from '@staratlas/data-source' -import { Game } from '@staratlas/sage' +/** + * End movement action - C4 migration + * Completes warp or subwarp movement (MoveWarp/MoveSubwarp -> Idle) + */ +import type { Instruction, KeyPairSigner } from '@solana/kit' +import { getTheOneFleetStateHandlerInstruction } from '@staratlas/dev-sage' import { logger } from '../../../../../logger.js' import { sendAndConfirmInstructions } from '../../../../../service/sol/send-and-confirm-tx.js' -import { keyPair } from '../../../../../service/wallet' -import { programs } from '../../programs.js' -import { movementSubwarpHandlerIx } from '../ix/movement-subwarp-handler.js' -import { stopWarpIx } from '../ix/stop-warp.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' - -export const endMove = async ( - fleetInfo: FleetInfo, - player: Player, - game: Game, -): Promise => { - const { fleet } = fleetInfo - - if (!fleet.state.MoveWarp && !fleet.state.MoveSubwarp) { - logger.warn('Fleet is not moving, cannot End Move') - - return +import type { AccountWithKey, Character, Fleet, Game } from '../../types.js' + +export type EndMoveParams = { + game: AccountWithKey + fleet: AccountWithKey + signer: KeyPairSigner + character: AccountWithKey +} + +/** + * Creates end movement instruction + * Uses TheOneFleetStateHandler to transition from movement states + */ +export function createEndMoveInstruction({ + game, + fleet, + signer, + character, +}: EndMoveParams): Instruction { + // Check fleet state + if ( + fleet.state.__kind !== 'MoveWarp' && + fleet.state.__kind !== 'MoveSubwarp' + ) { + throw new Error( + `Cannot end move: fleet is in ${fleet.state.__kind} state, must be MoveWarp or MoveSubwarp`, + ) } - const fuelTokenAccount = createAssociatedTokenAccountIdempotent( - game.data.mints.fuel, - fleet.data.fuelTank, - true, - ) - - const ix = ( - fleet.state.MoveSubwarp ? movementSubwarpHandlerIx : stopWarpIx - )(fleetInfo, player, game, fuelTokenAccount.address, programs) - - const instructions = await ixReturnsToIxs( - [fuelTokenAccount.instructions, ix], - player.signer, - ) - - await sendAndConfirmInstructions([keyPair])(instructions) + + // In C4, character is required for ending movement + if (!character?.key) { + throw new Error('Character account required for ending movement in C4') + } + + return getTheOneFleetStateHandlerInstruction({ + game: game.key, + fleet: fleet.key, + funder: signer, + character: character.key, + }) +} + +/** + * Execute end move action - sends transaction + */ +export async function endMove(params: EndMoveParams): Promise { + const instruction = createEndMoveInstruction(params) + + logger.info(`Ending movement for fleet ${params.fleet.key}`) + await sendAndConfirmInstructions(params.signer)([instruction]) +} + +/** + * Check if fleet movement is complete (arrival time passed) + */ +export function isMovementComplete(fleet: AccountWithKey): boolean { + const state = fleet.state + + if (state.__kind === 'MoveWarp') { + const moveState = state.fields[0] as unknown as { + journey?: { arrivalTime: bigint } + arrivalTime?: bigint + } + const arrivalTime = + moveState.journey?.arrivalTime ?? moveState.arrivalTime ?? 0n + return BigInt(Date.now() / 1000) >= arrivalTime + } + + if (state.__kind === 'MoveSubwarp') { + const moveState = state.fields[0] as unknown as { + journey?: { arrivalTime: bigint } + arrivalTime?: bigint + } + const arrivalTime = + moveState.journey?.arrivalTime ?? moveState.arrivalTime ?? 0n + return BigInt(Date.now() / 1000) >= arrivalTime + } + + return false +} + +/** + * Get remaining movement time in seconds (0 if complete) + */ +export function getRemainingMovementTime(fleet: AccountWithKey): number { + const state = fleet.state + + if (state.__kind === 'MoveWarp' || state.__kind === 'MoveSubwarp') { + const moveState = state.fields[0] as unknown as { + journey?: { arrivalTime: bigint } + arrivalTime?: bigint + } + const arrivalTime = Number( + moveState.journey?.arrivalTime ?? moveState.arrivalTime ?? 0n, + ) + const now = Math.floor(Date.now() / 1000) + return Math.max(0, arrivalTime - now) + } + + return 0 } diff --git a/src/main/basedbot/lib/sage/act/exit-respawn.ts b/src/main/basedbot/lib/sage/act/exit-respawn.ts deleted file mode 100644 index 0cf62f14..00000000 --- a/src/main/basedbot/lib/sage/act/exit-respawn.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { getAssociatedTokenAddressSync } from '@solana/spl-token' -import { PublicKey } from '@solana/web3.js' -import { CargoType } from '@staratlas/cargo' -import { InstructionReturn, ixReturnsToIxs } from '@staratlas/data-source' -import { Game, Starbase } from '@staratlas/sage' - -import { logger } from '../../../../../logger.js' -import { connection } from '../../../../../service/sol/index.js' -import { sendAndConfirmInstructions } from '../../../../../service/sol/send-and-confirm-tx.js' -import { keyPair } from '../../../../../service/wallet' -import { programs } from '../../programs.js' -import { exitRespawnIx } from '../ix/exit-respawn.js' -import { forceDropFleetCargoIx } from '../ix/force-drop-fleet-cargo.js' -import { getCargoStatsDefinition } from '../state/cargo-stats-definition.js' -import { getCargoType } from '../state/cargo-types.js' -import { getStarbasePlayer } from '../state/starbase-player.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' - -import { getFleetCargoHold } from './load-cargo.js' - -export const exitRespawn = async ( - fleetInfo: FleetInfo, - starbase: Starbase, - player: Player, - game: Game, -): Promise => { - const { fleet } = fleetInfo - - if (!fleet.state.Respawn) { - logger.warn('Fleet is not respawning, cannot Exit Respawn') - - return - } - - const starbasePlayer = await getStarbasePlayer(player, starbase, programs) - - const cargoStatsDefinition = await getCargoStatsDefinition( - game.data.cargo.statsDefinition, - ) - - const ixs: Array = [] - - const cargoMints = player.cargoTypes.map((ct) => ct.data.mint) - const uniqPublicKeys = (keys: PublicKey[]): PublicKey[] => { - const uniqueStrings = [...new Set(keys.map((key) => key.toString()))] - - return uniqueStrings.map((str) => new PublicKey(str)) - } - - for (const key of uniqPublicKeys(cargoMints)) { - const mint = new PublicKey(key) - let cargoType: CargoType | undefined - try { - cargoType = getCargoType(player.cargoTypes, game, mint) - } catch (_) { - continue - } - - const cargoPod = getFleetCargoHold(mint, game, fleetInfo) - const tokenFrom = getAssociatedTokenAddressSync(mint, cargoPod, true) - - const accountInfo = await connection.getAccountInfo(tokenFrom) - - if (accountInfo && cargoType) { - ixs.push( - forceDropFleetCargoIx( - fleetInfo, - game, - cargoStatsDefinition, - cargoPod, - cargoType.key, - tokenFrom, - mint, - programs, - ), - ) - } - } - ixs.push( - exitRespawnIx( - fleetInfo, - player, - game, - starbase, - starbasePlayer, - programs, - ), - ) - await ixReturnsToIxs(ixs, player.signer).then( - sendAndConfirmInstructions([keyPair]), - ) -} diff --git a/src/main/basedbot/lib/sage/act/load-cargo.ts b/src/main/basedbot/lib/sage/act/load-cargo.ts index c87021b1..f07fbac8 100644 --- a/src/main/basedbot/lib/sage/act/load-cargo.ts +++ b/src/main/basedbot/lib/sage/act/load-cargo.ts @@ -1,120 +1,347 @@ -import { getAssociatedTokenAddressSync } from '@solana/spl-token' -import { PublicKey } from '@solana/web3.js' +/** + * Load cargo to fleet action - C4 migration + * Transfers cargo from starbase to fleet using cargoId system + * + * Cargo flow: Wallet -> StarbasePlayer -> Fleet + * This module handles StarbasePlayer -> Fleet transfers. + * If player is provided, will auto-deposit from wallet first. + */ +import type { Address, Instruction, KeyPairSigner } from '@solana/kit' import { - createAssociatedTokenAccountIdempotent, - ixReturnsToIxs, -} from '@staratlas/data-source' -import { Game } from '@staratlas/sage' -import BN from 'bn.js' + findStarbasePlayerPda, + getRegisterStarbasePlayerInstruction, + getTransferCargoToFleetInstruction, +} from '@staratlas/dev-sage' import { logger } from '../../../../../logger.js' -import { sendAndConfirmInstructions } from '../../../../../service/sol/send-and-confirm-tx.js' -import { keyPair } from '../../../../../service/wallet' -import { getTokenBalance } from '../../../basedbot.js' -import { programs } from '../../programs.js' -import { loadCargoIx } from '../ix/load-cargo.js' -import { getCargoType } from '../state/cargo-types.js' -import { starbaseByCoordinates } from '../state/starbase-by-coordinates.js' import { - getCargoPodsForStarbasePlayer, - getStarbasePlayer, -} from '../state/starbase-player.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' -import { getName } from '../util.js' - -export const getFleetCargoHold = ( - mint: PublicKey, - game: Game, - fleetInfo: FleetInfo, -): PublicKey => { - switch (mint.toBase58()) { - case game.data.mints.fuel.toBase58(): - return fleetInfo.fleet.data.fuelTank - case game.data.mints.ammo.toBase58(): - return fleetInfo.fleet.data.ammoBank - default: - return fleetInfo.fleet.data.cargoHold - } + sendAndConfirmInstructions, + SimulationProgramError, + TransactionFailedError, +} from '../../../../../service/sol/send-and-confirm-tx.js' +import type { + AccountWithKey, + Character, + Fleet, + Game, + ProfileFactionAccount, + StarSystem, +} from '../../types.js' +import type { Player } from '../state/user-account.js' + +import { + ensureCargo, + getCargoMint, + getStarbaseCargoBalance, +} from './deposit-cargo.js' + +export type CargoTransfer = { + cargoId: number + amount: number // positive to load, negative to unload } -export const loadCargo = async ( - fleetInfo: FleetInfo, - player: Player, - game: Game, - mint: PublicKey, - amount: number, - forceCargoHold: boolean = false, -): Promise => { - if (amount < 1) { - logger.warn(`Cannot load amount less than 1 (${amount})`) - return +export type LoadCargoParams = { + game: AccountWithKey + fleet: AccountWithKey + character: AccountWithKey + profileFaction: AccountWithKey + system: AccountWithKey + starbasePlayer: Address | undefined + signer: KeyPairSigner + transfers: CargoTransfer[] + keyIndex?: number + /** If provided, will auto-deposit cargo from wallet to starbase first */ + player?: Player + /** Fleet name for logging */ + fleetName?: string +} + +/** + * Get fuel tank cargo ID from fleet + */ +export function getFuelCargoId(fleet: AccountWithKey): number { + return fleet.fuelTank?.cargoId ?? 1 +} + +/** + * Get ammo bank cargo ID from fleet + */ +export function getAmmoCargoId(fleet: AccountWithKey): number { + return fleet.ammoBank?.cargoId ?? 2 +} + +/** + * Creates transfer cargo instruction + * Handles routing to fuel tank, ammo bank, or cargo hold automatically + */ +export async function createTransferCargoInstructions({ + game, + fleet, + character, + profileFaction, + system, + starbasePlayer, + signer, + transfers, + keyIndex = 0, +}: LoadCargoParams): Promise { + const instructions: Instruction[] = [] + + // Check fleet state - must be docked + if (fleet.state.__kind !== 'Docked') { + throw new Error( + `Cannot transfer cargo: fleet is in ${fleet.state.__kind} state, must be Docked`, + ) } - const starbase = await starbaseByCoordinates(fleetInfo.location) - const hold = forceCargoHold - ? fleetInfo.fleet.data.cargoHold - : getFleetCargoHold(mint, game, fleetInfo) + let starbasePlayerAddress = starbasePlayer + + // Register starbase player if needed + if (!starbasePlayerAddress) { + const [pda] = await findStarbasePlayerPda({ + system: system.key, + character: character.key, + }) + starbasePlayerAddress = pda - if (!starbase) { - throw new Error(`No starbase found at ${fleetInfo.location}`) + logger.info(`Registering new starbase player at system: ${system.key}`) + + instructions.push( + getRegisterStarbasePlayerInstruction({ + game: game.key, + character: character.key, + profileFaction: profileFaction.key, + funder: signer, + system: system.key, + starbasePlayer: starbasePlayerAddress, + }), + ) } - const cargoType = getCargoType(player.cargoTypes, game, mint) - const fleetCargoTokenResult = createAssociatedTokenAccountIdempotent( - mint, - hold, - true, - ) + // Extract fuel and ammo cargo IDs from fleet + const fuelCargoId = getFuelCargoId(fleet) + const ammoCargoId = getAmmoCargoId(fleet) - const starbasePlayer = await getStarbasePlayer(player, starbase, programs) - const cargoPodFrom = await getCargoPodsForStarbasePlayer( - starbasePlayer, - programs, - ) + logger.debug(`Cargo transfer setup:`) + logger.debug(` fuelCargoId: ${fuelCargoId}, ammoCargoId: ${ammoCargoId}`) + logger.debug(` starbasePlayer: ${starbasePlayerAddress}`) + logger.debug(` transfers: ${JSON.stringify(transfers)}`) + + // Get current amounts and capacities + const currentFuel = Number(fleet.fuelTank?.amount ?? 0n) + const currentAmmo = Number(fleet.ammoBank?.amount ?? 0n) + const fuelCapacity = Number(fleet.stats?.cargoStats?.fuelCapacity ?? 0) + const ammoCapacity = Number(fleet.stats?.cargoStats?.ammoCapacity ?? 0) + + logger.debug(` currentFuel: ${currentFuel}/${fuelCapacity}`) + logger.debug(` currentAmmo: ${currentAmmo}/${ammoCapacity}`) - const cargoTokenAccountAddress = getAssociatedTokenAddressSync( - mint, - cargoPodFrom.key, - true, + let fuelTank: bigint | null = null + let ammoBank: bigint | null = null + const cargoHoldToLoad: Array = [] + const cargoHoldToUnload: Array = [] + + for (const { cargoId, amount } of transfers) { + if (amount === 0) continue + + const isWithdrawal = amount < 0 + // Floor to ensure integer for BigInt conversion + const absAmount = Math.floor(Math.abs(amount)) + if (absAmount === 0) continue + + if (cargoId === fuelCargoId) { + if (isWithdrawal) { + fuelTank = BigInt(-absAmount) + } else { + // Fill tank first, overflow to cargo hold + const spaceInTank = fuelCapacity - currentFuel + const toTank = Math.min(absAmount, spaceInTank) + const overflow = absAmount - toTank + + if (toTank > 0) { + fuelTank = BigInt(toTank) + } + if (overflow > 0) { + cargoHoldToLoad.push([cargoId, BigInt(overflow)] as const) + } + } + } else if (cargoId === ammoCargoId) { + if (isWithdrawal) { + ammoBank = BigInt(-absAmount) + } else { + // Fill bank first, overflow to cargo hold + const spaceInBank = ammoCapacity - currentAmmo + const toBank = Math.min(absAmount, spaceInBank) + const overflow = absAmount - toBank + + if (toBank > 0) { + ammoBank = BigInt(toBank) + } + if (overflow > 0) { + cargoHoldToLoad.push([cargoId, BigInt(overflow)] as const) + } + } + } else { + // Regular cargo goes to/from cargo hold + if (isWithdrawal) { + cargoHoldToUnload.push([cargoId, BigInt(absAmount)] as const) + } else { + cargoHoldToLoad.push([cargoId, BigInt(absAmount)] as const) + } + } + } + + // Create transfer instruction + instructions.push( + getTransferCargoToFleetInstruction({ + game: game.key, + character: character.key, + systemAndStarbasePlayerStarbasePlayer: starbasePlayerAddress, + systemAndStarbasePlayerSystem: system.key, + fleet: fleet.key, + profileValidationProfile: character.playerProfile, + profileValidationSigner: signer, + fuelTank, + ammoBank, + cargoHold: { toLoad: cargoHoldToLoad, toUnload: cargoHoldToUnload }, + keyIndex, + }), ) - const cargoAmountAtOrigin = await getTokenBalance(cargoPodFrom.key, mint) - const toLoad = cargoAmountAtOrigin.lt(new BN(amount)) - ? cargoAmountAtOrigin - : new BN(amount) + return instructions +} + +export type LoadCargoResult = { + loaded: boolean + reason?: + | 'no_cargo_available' + | 'partial_load' + | 'simulation_failed' + | 'success' + | 'transaction_failed' +} + +/** + * Execute load cargo action - sends transaction + * + * If player is provided, will auto-deposit cargo from wallet first. + * Checks cargo availability before attempting transfer to fail gracefully. + */ +export async function loadCargo( + params: LoadCargoParams, +): Promise { + const { player, game, system, transfers } = params - if (toLoad.eq(new BN(0))) { - logger.warn(`No ${mint} available at ${getName(starbase)}...`) + // Auto-deposit from wallet if player provided (only works at CSS) + if (player) { + const cargoNeeds = transfers + .filter((t) => t.amount > 0) + .map((t) => ({ cargoId: t.cargoId, amount: t.amount })) - return + if (cargoNeeds.length > 0) { + logger.debug( + `Ensuring cargo availability for ${cargoNeeds.length} cargo types`, + ) + await ensureCargo(player, game, system, cargoNeeds) + } } - if (cargoAmountAtOrigin.lt(new BN(amount))) { - logger.warn( - `Not enough cargo available at origin Starbase, loading ${cargoAmountAtOrigin} instead of ${amount}`, - ) + + // Check cargo availability and adjust transfers to available amounts + // Instead of skipping entirely, we transfer what's available + let adjustedTransfers = transfers + + if (player) { + const adjustedList: CargoTransfer[] = [] + + for (const { cargoId, amount } of transfers) { + if (amount <= 0) { + // Withdrawals pass through unchanged + adjustedList.push({ cargoId, amount }) + continue + } + + const available = await getStarbaseCargoBalance( + player, + system, + cargoId, + ) + + logger.debug( + `Cargo availability check: cargoId=${cargoId}, requested=${amount}, available=${available}`, + ) + + if (available === 0n) { + // Skip - no cargo available at this starbase + continue + } + + const adjustedAmount = Math.min(amount, Number(available)) + if (adjustedAmount < amount) { + const mint = getCargoMint(game, cargoId) + logger.info( + `Partial cargo available: cargoId=${cargoId} (mint=${mint}), ` + + `requested=${amount}, available=${available}, transferring=${adjustedAmount}`, + ) + } + adjustedList.push({ cargoId, amount: adjustedAmount }) + } + + adjustedTransfers = adjustedList } - const ix = loadCargoIx( - fleetInfo, - player, - game, - starbase, - starbasePlayer, - cargoPodFrom.key, - hold, - cargoTokenAccountAddress, - fleetCargoTokenResult.address, - mint, - cargoType.key, - programs, - toLoad, - ) + // Skip if no transfers remaining after adjustment - nothing available + if (adjustedTransfers.length === 0) { + return { loaded: false, reason: 'no_cargo_available' } + } - const instructions = await ixReturnsToIxs( - [fleetCargoTokenResult.instructions, ix], - player.signer, - ) + const instructions = await createTransferCargoInstructions({ + ...params, + transfers: adjustedTransfers, + }) + + try { + await sendAndConfirmInstructions(params.signer)(instructions) + return { loaded: true, reason: 'success' } + } catch (e) { + if (e instanceof SimulationProgramError) { + logger.warn( + `Load cargo simulation failed: ${e.message}\nLogs: ${e.logs.join('\n')}`, + ) + return { loaded: false, reason: 'simulation_failed' } + } + + if (e instanceof TransactionFailedError) { + logger.warn(`Load cargo transaction failed: ${e.message}`) + return { loaded: false, reason: 'transaction_failed' } + } + + // Re-throw unknown errors + throw e + } +} + +/** + * Convenience function: load specific amount of fuel + */ +export async function loadFuel( + params: Omit & { amount: number }, +): Promise { + const fuelCargoId = getFuelCargoId(params.fleet) + return loadCargo({ + ...params, + transfers: [{ cargoId: fuelCargoId, amount: params.amount }], + }) +} - await sendAndConfirmInstructions([keyPair])(instructions) +/** + * Convenience function: load specific amount of ammo + */ +export async function loadAmmo( + params: Omit & { amount: number }, +): Promise { + const ammoCargoId = getAmmoCargoId(params.fleet) + return loadCargo({ + ...params, + transfers: [{ cargoId: ammoCargoId, amount: params.amount }], + }) } diff --git a/src/main/basedbot/lib/sage/act/mine.ts b/src/main/basedbot/lib/sage/act/mine.ts index ae486b1d..44ef86f3 100644 --- a/src/main/basedbot/lib/sage/act/mine.ts +++ b/src/main/basedbot/lib/sage/act/mine.ts @@ -1,75 +1,126 @@ -import { - createAssociatedTokenAccountIdempotent, - ixReturnsToIxs, -} from '@staratlas/data-source' -import { Game } from '@staratlas/sage' +/** + * Start mining action - C4 migration + * Transitions fleet from Idle to MineAsteroid state + */ +import type { Instruction, KeyPairSigner } from '@solana/kit' +import { getStartMiningAsteroidInstruction } from '@staratlas/dev-sage' import { logger } from '../../../../../logger.js' import { sendAndConfirmInstructions } from '../../../../../service/sol/send-and-confirm-tx.js' -import { keyPair } from '../../../../../service/wallet' -import { programs } from '../../programs.js' -import { startMiningIx } from '../ix/start-mining.js' -import { getStarbasePlayer } from '../state/starbase-player.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' -import { Mineable } from '../state/world-map.js' +import type { + AccountWithKey, + CelestialBody, + Character, + Fleet, + Game, + RegionTracker, + StarSystem, +} from '../../types.js' -import { undock } from './undock.js' - -export const mine = async ( - fleetInfo: FleetInfo, - player: Player, - game: Game, - mineable: Mineable, -): Promise => { - const { fleet } = fleetInfo +export type MineParams = { + game: AccountWithKey + fleet: AccountWithKey + character: AccountWithKey + regionTracker: AccountWithKey + system: AccountWithKey + asteroid: AccountWithKey + signer: KeyPairSigner + keyIndex?: number + /** Resource cargo IDs to mine (e.g., [0, 1, 2] for specific resources) */ + resources?: number[] +} - // TOOD: Check fuel cost for mining +/** + * Get resource cargo IDs from asteroid + * Returns array of cargo IDs for all mineable resources on the asteroid (max 8) + */ +function getAsteroidResourceIds( + asteroid: AccountWithKey, +): number[] { + // In C4, asteroid resources are in celestialBodyType.fields[0].resources + // as a Map + const cbType = asteroid.celestialBodyType as unknown as { + __kind: string + fields?: unknown[] + } - if (fleet.state.MineAsteroid) { - logger.warn('Fleet is already mining') + if (cbType.__kind !== 'Asteroid' || !cbType.fields?.[0]) { + return [] + } - return + const asteroidData = cbType.fields[0] as { + resources?: Map | unknown } - if (fleet.state.StarbaseLoadingBay) { - logger.info( - `${fleetInfo.fleetName} is in the loading bay at ${fleet.state.StarbaseLoadingBay.starbase}, undocking...`, - ) + const resourcesMap = asteroidData.resources - await undock(fleet, fleetInfo.location, player, game) + if (resourcesMap instanceof Map) { + // Return up to 8 resource IDs (C4 limit) + return Array.from(resourcesMap.keys()).slice(0, 8) } - if (fleet.state.MoveSubwarp || fleet.state.MoveWarp) { - logger.info(`${fleetInfo.fleetName} is moving, cannot mine`) + // Fallback: try treating as object + if (resourcesMap && typeof resourcesMap === 'object') { + const keys = Object.keys(resourcesMap as Record) + .map((k) => Number(k)) + .filter((k) => !isNaN(k)) + return keys.slice(0, 8) + } + + return [] +} - return +/** + * Creates start mining instruction + */ +export function createStartMiningInstruction({ + game, + fleet, + character, + regionTracker, + system, + asteroid, + signer, + keyIndex = 0, + resources, +}: MineParams): Instruction { + // Check fleet state + if (fleet.state.__kind !== 'Idle') { + throw new Error( + `Cannot start mining: fleet is in ${fleet.state.__kind} state, must be Idle`, + ) } - const starbasePlayer = await getStarbasePlayer( - player, - mineable.starbase, - programs, - ) - const fuelTokenAccount = createAssociatedTokenAccountIdempotent( - game.data.mints.fuel, - fleet.data.fuelTank, - true, - ) - const ix = startMiningIx( - fleetInfo, - player, - game, - mineable, - starbasePlayer, - fuelTokenAccount.address, - programs, - ) + // Get resources to mine - use provided list or extract from asteroid + const resourceIds = resources ?? getAsteroidResourceIds(asteroid) - const instructions = await ixReturnsToIxs( - [fuelTokenAccount.instructions, ix], - player.signer, - ) + return getStartMiningAsteroidInstruction({ + game: game.key, + asteroid: asteroid.key, + starSystem: system.key, + fleet: fleet.key, + profileValidationSigner: signer, + profileValidationProfile: character.playerProfile, + regionTracker: regionTracker.key, + character: character.key, + keyIndex, + resources: resourceIds, + }) +} + +/** + * Execute start mining action - sends transaction + */ +export async function mine(params: MineParams): Promise { + const instruction = createStartMiningInstruction(params) - await sendAndConfirmInstructions([keyPair])(instructions) + logger.info( + `Starting mining at asteroid ${params.asteroid.key} for fleet ${params.fleet.key}`, + ) + await sendAndConfirmInstructions(params.signer)([instruction]) } + +/** + * Alias for mine() + */ +export const startMining = mine diff --git a/src/main/basedbot/lib/sage/act/move.ts b/src/main/basedbot/lib/sage/act/move.ts index 94cc4bf3..a26c387c 100644 --- a/src/main/basedbot/lib/sage/act/move.ts +++ b/src/main/basedbot/lib/sage/act/move.ts @@ -1,133 +1,405 @@ +/** + * Movement actions - C4 migration + * Warp and subwarp fleet movement + */ +import type { Instruction, KeyPairSigner } from '@solana/kit' import { - createAssociatedTokenAccountIdempotent, - ixReturnsToIxs, -} from '@staratlas/data-source' -import { Game } from '@staratlas/sage' + getStartSubwarpInstruction, + getWarpToCoordinateInstruction, +} from '@staratlas/dev-sage' -import dayjs from '../../../../../dayjs.js' import { logger } from '../../../../../logger.js' -import { sendAndConfirmInstructions } from '../../../../../service/sol/send-and-confirm-tx.js' -import { keyPair } from '../../../../../service/wallet' -import { programs } from '../../programs.js' -import { Coordinates } from '../../util/coordinates.js' -import { getFuelConsumption } from '../../util/fuel-consumption.js' -import { subWarpIx } from '../ix/subwarp.js' -import { warpIx } from '../ix/warp.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' - -import { undock } from './undock.js' +import { + sendAndConfirmInstructions, + SimulationProgramError, +} from '../../../../../service/sol/send-and-confirm-tx.js' +import type { + AccountWithKey, + Character, + Fleet, + Game, + RegionTracker, +} from '../../types.js' -export type WarpMode = 'warp' | 'subwarp' | 'auto' +// C4 uses 56 fractional bits for coordinates +const COORDINATE_SCALE = 2n ** 56n -export const move = async ( - fleetInfo: FleetInfo, - coordinates: Coordinates, - player: Player, - game: Game, - warpMode: WarpMode = 'auto', -): Promise => { - const { fleet } = fleetInfo +export type WarpMode = 'warp' | 'subwarp' | 'auto' - if (fleet.state.MoveWarp || fleet.state.MoveSubwarp) { - logger.warn('Fleet is already moving') +export type MoveParams = { + game: AccountWithKey + fleet: AccountWithKey + character: AccountWithKey + regionTracker: AccountWithKey + destination: [bigint, bigint] + signer: KeyPairSigner + keyIndex?: number + warpMode?: WarpMode + /** Starting region ID (null if not in a region) */ + startRegionId?: number | null + /** Destination region ID (null if not in a region) */ + destRegionId?: number | null +} - return +/** + * Extract number from FixedPoint-like object + */ +function extractNumber(value: unknown): number { + if (value === null || value === undefined) return 0 + if (typeof value === 'number') return value + if (typeof value === 'bigint') return Number(value) + if (typeof value === 'object' && 'raw' in value) { + const raw = (value as { raw: bigint }).raw + // Assume 56 fractional bits + return Number(raw) / Number(COORDINATE_SCALE) } - - if (fleet.state.StarbaseLoadingBay) { - logger.info( - `${fleetInfo.fleetName} is in the loading bay at ${fleet.state.StarbaseLoadingBay.starbase}, undocking...`, - ) - - await undock(fleet, fleetInfo.location, player, game) + if (typeof value === 'object' && 'toNumber' in value) { + return (value as { toNumber: () => number }).toNumber() } + return 0 +} - if (fleet.state.MineAsteroid) { - logger.info(`${fleetInfo.fleetName} is mining an asteroid, cannot move`) +/** + * Calculate distance between two raw coordinate tuples + */ +function calculateDistance( + from: [bigint, bigint], + to: [bigint, bigint], +): number { + const dx = Number(to[0] - from[0]) / Number(COORDINATE_SCALE) + const dy = Number(to[1] - from[1]) / Number(COORDINATE_SCALE) + return Math.sqrt(dx * dx + dy * dy) +} - return - } +/** + * Get fuel level from fleet's fuel tank + */ +function getFleetFuelLevel(fleet: AccountWithKey): bigint { + // In C4, fuel is stored in fleet.fuelTank.amount (not cargoHold) + const fuelTank = fleet.fuelTank as { amount?: bigint } | undefined + return fuelTank?.amount ?? 0n +} - const { maxWarpDistance } = fleetInfo.movementStats +/** + * Calculate fuel required for subwarp movement + */ +function calculateSubwarpFuel( + fleet: AccountWithKey, + distance: number, +): number { + const rate = extractNumber( + fleet.stats?.movementStats?.subwarpFuelConsumptionRate, + ) + // Rate is per 10000 units of distance, distance is in game units + return Math.ceil((distance * 100 * rate) / 10000) +} - const desiredDistance = fleetInfo.location.distanceFrom(coordinates) * 100 +/** + * Calculate fuel required for warp movement + */ +function calculateWarpFuel( + fleet: AccountWithKey, + distance: number, +): number { + const rate = extractNumber( + fleet.stats?.movementStats?.warpFuelConsumptionRate, + ) + return Math.ceil((distance * 100 * rate) / 10000) +} - const fuelConsumption = getFuelConsumption( - fleetInfo.location, - coordinates, - fleetInfo, +/** + * Calculate if fleet can warp given distance and stats + */ +function canWarpDistance( + fleet: AccountWithKey, + fromCoords: [bigint, bigint], + toCoords: [bigint, bigint], +): boolean { + const maxWarpDistance = extractNumber( + fleet.stats?.movementStats?.maxWarpDistance, ) + const distance = calculateDistance(fromCoords, toCoords) + + // maxWarpDistance is already in game units + return distance <= maxWarpDistance +} + +/** + * Get current fleet coordinates from fleet.location + * In C4, fleet location is stored in fleet.location: Array + */ +function getCurrentCoordinates(fleet: AccountWithKey): [bigint, bigint] { + // C4 stores location directly on fleet, not in state + const location = fleet.location as Array<{ raw: bigint }> | undefined - const subWarpFuelConsumptionRatePerSecond = fuelConsumption.subwarp / 1000 - const warpFuelConsumptionRatePerSecond = fuelConsumption.warp / 1000 + if (!location || location.length < 2) { + throw new Error( + `Fleet has no location data. State: ${fleet.state.__kind}`, + ) + } - logger.info(`Distance to Travel: ${desiredDistance}`) - logger.info( - `Subwarp Fuel Consumption per sec: ${subWarpFuelConsumptionRatePerSecond}`, + // Extract raw bigint from FixedPoint objects + const x = + typeof location[0] === 'object' && 'raw' in location[0] + ? location[0].raw + : BigInt(0) + const y = + typeof location[1] === 'object' && 'raw' in location[1] + ? location[1].raw + : BigInt(0) + + logger.debug( + `getCurrentCoordinates: fleet=${fleet.key}, ` + + `rawLocation=${JSON.stringify(location, (_, v) => (typeof v === 'bigint' ? v.toString() : v))}, ` + + `extracted=[${x}, ${y}]`, ) - logger.info( - `Warp Fuel Consumption per sec: ${warpFuelConsumptionRatePerSecond}`, + + return [x, y] +} + +/** + * Creates warp instruction + */ +export function createWarpInstruction({ + game, + fleet, + character, + regionTracker, + destination, + signer, + keyIndex = 0, + startRegionId = null, + destRegionId = null, +}: Omit): Instruction { + return getWarpToCoordinateInstruction({ + game: game.key, + destination, + fleet: fleet.key, + profileValidationSigner: signer, + profileValidationProfile: character.playerProfile, + character: character.key, + regionTracker: regionTracker.key, + keyIndex, + startRegionId, + destRegionId, + }) +} + +/** + * Creates subwarp instruction + */ +export function createSubwarpInstruction({ + game, + fleet, + character, + regionTracker, + destination, + signer, + keyIndex = 0, + startRegionId = null, + destRegionId = null, +}: Omit): Instruction { + logger.debug( + `createSubwarpInstruction: fleet=${fleet.key}, ` + + `fleetState=${fleet.state.__kind}, ` + + `dest=[${destination[0]}, ${destination[1]}], ` + + `game=${game.key}, character=${character.key}, ` + + `regionTracker=${regionTracker.key}`, ) - logger.info(`Fuel level: ${fleetInfo.cargoLevels.fuel}`) + return getStartSubwarpInstruction({ + game: game.key, + toCoordinate: destination, + fleet: fleet.key, + profileValidationSigner: signer, + profileValidationProfile: character.playerProfile, + character: character.key, + regionTracker: regionTracker.key, + keyIndex, + startRegionId, + destRegionId, + }) +} - const canWarp = desiredDistance <= maxWarpDistance - const warp = warpMode === 'warp' || (warpMode === 'auto' && canWarp) +/** + * Validate move requirements and return instruction if valid + * Returns null if requirements not met (with appropriate logging) + */ +export function validateAndCreateMoveInstruction( + params: MoveParams, +): Instruction | null { + const { fleet, destination, warpMode = 'auto' } = params - if (warp && fleetInfo.fleetState.data.warpCooldown) { - const timeLeft = dayjs.duration( - dayjs().diff(fleetInfo.fleetState.data.warpCooldownExpiry), + // Check fleet state + if (fleet.state.__kind !== 'Idle') { + logger.warn( + `Cannot move: fleet is in ${fleet.state.__kind} state, must be Idle`, ) + return null + } + // Debug: log full state and ship counts for troubleshooting + const shipCounts = fleet.shipCounts as + | { + total?: number + xxSmall?: number + xSmall?: number + small?: number + medium?: number + large?: number + capital?: number + commander?: number + titan?: number + } + | undefined + logger.debug( + `validateAndCreateMoveInstruction: fleet=${fleet.key}, ` + + `state=${fleet.state.__kind}, ` + + `shipCounts=${JSON.stringify(shipCounts)}`, + ) + + const currentCoords = getCurrentCoordinates(fleet) + const distance = calculateDistance(currentCoords, destination) + const canWarp = canWarpDistance(fleet, currentCoords, destination) + + // Determine movement type + const useWarp = warpMode === 'warp' || (warpMode === 'auto' && canWarp) + + if (useWarp && !canWarp) { logger.warn( - `Fleet is on warp cooldown, cannot warp. Retry in: ${timeLeft.humanize()}`, + `Cannot warp: distance ${distance.toFixed(2)} exceeds max warp distance`, ) + return null + } + + // Check fuel levels + const fuelLevel = Number(getFleetFuelLevel(fleet)) + const fuelRequired = useWarp + ? calculateWarpFuel(fleet, distance) + : calculateSubwarpFuel(fleet, distance) - return + if (fuelLevel < fuelRequired) { + logger.warn( + `Cannot move: insufficient fuel. Have ${fuelLevel}, need ${fuelRequired} for ${distance.toFixed(2)} distance`, + ) + return null } - const estimatedConsumption = warp - ? fuelConsumption.warp - : fuelConsumption.subwarp + // Check warp cooldown if warping + if (useWarp) { + const cooldownExpires = fleet.warpCooldownExpiresAt as bigint + const now = BigInt(Math.floor(Date.now() / 1000)) + if (cooldownExpires > now) { + const remaining = Number(cooldownExpires - now) + logger.warn(`Cannot warp: cooldown active, ${remaining}s remaining`) + return null + } + } - logger.info(`Estimated fuel consumption: ${estimatedConsumption}`) + // Convert destination to display coordinates for logging + const destX = Number(destination[0]) / Number(COORDINATE_SCALE) + const destY = Number(destination[1]) / Number(COORDINATE_SCALE) + const currX = Number(currentCoords[0]) / Number(COORDINATE_SCALE) + const currY = Number(currentCoords[1]) / Number(COORDINATE_SCALE) - const hasEnoughFuel = fleetInfo.cargoLevels.fuel >= estimatedConsumption + logger.debug( + `Move params: from [${currX}, ${currY}] to [${destX}, ${destY}], distance=${distance.toFixed(2)}, fuel=${fuelLevel}, fuelNeeded=${fuelRequired}`, + ) + logger.debug(`Fleet state: ${fleet.state.__kind}, key=${fleet.key}`) - const hasEnoughFuelForRoundTrip = - fleetInfo.cargoLevels.fuel >= estimatedConsumption * 2 + if (useWarp) { + logger.info( + `Warping fleet to [${destX}, ${destY}] (fuel: ${fuelRequired}/${fuelLevel})`, + ) + return createWarpInstruction(params) + } else { + logger.info( + `Subwarping fleet to [${destX}, ${destY}] (fuel: ${fuelRequired}/${fuelLevel})`, + ) + return createSubwarpInstruction(params) + } +} - if (!hasEnoughFuel) { - logger.warn('Not enough fuel to move') - return +/** + * Creates move instruction (warp or subwarp based on mode/distance) + * @deprecated Use validateAndCreateMoveInstruction for proper validation + */ +export function createMoveInstruction(params: MoveParams): Instruction { + const instruction = validateAndCreateMoveInstruction(params) + if (!instruction) { + throw new Error('Move validation failed - check logs for details') } + return instruction +} - if (!hasEnoughFuelForRoundTrip) { - logger.warn( - 'Not enough fuel for the round trip. Need Fuel at destination Starbase', - ) +/** + * Execute move action - sends transaction + * Returns true if move was executed, false if requirements not met or failed + */ +export async function move(params: MoveParams): Promise { + const instruction = validateAndCreateMoveInstruction(params) + + if (!instruction) { + // Requirements not met - already logged + return false } - const fuelTokenAccount = createAssociatedTokenAccountIdempotent( - game.data.mints.fuel, - fleet.data.fuelTank, - true, - ) + try { + await sendAndConfirmInstructions(params.signer)([instruction]) + return true + } catch (e) { + // Check for simulation errors that indicate stale state or known issues + if (e instanceof SimulationProgramError) { + const logsStr = e.logs.join('\n') - const ix = (warp ? warpIx : subWarpIx)( - fleetInfo, - coordinates, - fuelTokenAccount.address, - player, - game, - programs, - ) + // Fleet already moving - treat as success (stale local state) + if ( + logsStr.includes('Status mismatch') && + (logsStr.includes('actual_state=MoveSubwarp') || + logsStr.includes('actual_state=MoveWarp')) + ) { + // Don't log - this is expected when state is stale + return true + } - const instructions = await ixReturnsToIxs( - [fuelTokenAccount.instructions, ix], - player.signer, - ) + // Character required for movement - game config issue + if (logsStr.includes('character_required_for_move_subwarp')) { + logger.warn( + `Fleet requires character assignment - configure in game UI`, + ) + return false + } + + // Not within range - fleet position doesn't match + if (logsStr.includes('Not within range')) { + logger.warn(`Fleet position mismatch - will retry next cycle`) + return false + } + } + + // Unknown error - log for debugging + const err = e as Error + logger.debug(`Move error: ${err.message}`) + return false + } +} + +/** + * Execute warp action - sends transaction + * Returns true if executed, false if requirements not met + */ +export async function warp( + params: Omit, +): Promise { + return move({ ...params, warpMode: 'warp' }) +} - await sendAndConfirmInstructions([keyPair])(instructions) +/** + * Execute subwarp action - sends transaction + * Returns true if executed, false if requirements not met + */ +export async function subwarp( + params: Omit, +): Promise { + return move({ ...params, warpMode: 'subwarp' }) } diff --git a/src/main/basedbot/lib/sage/act/rearm.ts b/src/main/basedbot/lib/sage/act/rearm.ts index 93766dcf..7046bd52 100644 --- a/src/main/basedbot/lib/sage/act/rearm.ts +++ b/src/main/basedbot/lib/sage/act/rearm.ts @@ -1,91 +1,69 @@ -import { - createAssociatedTokenAccountIdempotent, - getParsedTokenAccountsByOwner, - ixReturnsToIxs, -} from '@staratlas/data-source' -import { Game } from '@staratlas/sage' -import BN from 'bn.js' -import { logger } from '../../../../../logger' +/** + * Rearm fleet action - C4 migration + * Fills fleet ammo bank from starbase inventory + */ +import type { Address, KeyPairSigner } from '@solana/kit' -import { connection } from '../../../../../service/sol/index.js' -import { sendAndConfirmInstructions } from '../../../../../service/sol/send-and-confirm-tx.js' -import { keyPair } from '../../../../../service/wallet' -import { programs } from '../../programs.js' -import { Coordinates } from '../../util/coordinates.js' -import { loadCargoIx } from '../ix/load-cargo.js' -import { getCargoType } from '../state/cargo-types.js' -import { starbaseByCoordinates } from '../state/starbase-by-coordinates.js' -import { - getCargoPodsForStarbasePlayer, - getStarbasePlayer, -} from '../state/starbase-player.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' +import { logger } from '../../../../../logger.js' +import type { + AccountWithKey, + Character, + Fleet, + Game, + ProfileFactionAccount, + StarSystem, +} from '../../types.js' +import { getAmmoCargoId, loadCargo } from './load-cargo.js' -export const rearm = async ( - fleetInfo: FleetInfo, - coordinates: Coordinates, - player: Player, - game: Game, -): Promise => { - const starbase = await starbaseByCoordinates(coordinates) +import type { Player } from '../state/user-account.js' - if (!starbase) { - throw new Error(`No starbase found at ${coordinates}`) - } +export type RearmParams = { + game: AccountWithKey + fleet: AccountWithKey + character: AccountWithKey + profileFaction: AccountWithKey + system: AccountWithKey + starbasePlayer: Address | undefined + signer: KeyPairSigner + keyIndex?: number + /** If provided, will auto-deposit ammo from wallet */ + player?: Player +} - const cargoType = getCargoType( - player.cargoTypes, - game, - game.data.mints.ammo, - ) - const fleetFuelTokenResult = createAssociatedTokenAccountIdempotent( - game.data.mints.ammo, - fleetInfo.fleet.data.ammoBank, - true, - ) +/** + * Calculate ammo needed to fill bank (bigint arithmetic to avoid precision loss) + */ +export function getAmmoNeeded(fleet: AccountWithKey): number { + const currentAmmo = BigInt(fleet.ammoBank?.amount ?? 0n) + const maxAmmo = BigInt(fleet.stats?.cargoStats?.ammoCapacity ?? 0) + const needed = maxAmmo - currentAmmo + return Number(needed > 0n ? needed : 0n) +} - const starbasePlayer = await getStarbasePlayer(player, starbase, programs) - const cargoPodFrom = await getCargoPodsForStarbasePlayer( - starbasePlayer, - programs, - ) +/** + * Rearm fleet - fills ammo bank to capacity + */ +export async function rearm(params: RearmParams): Promise { + const { fleet } = params - const starbaseTokenAccounts = await getParsedTokenAccountsByOwner( - connection, - cargoPodFrom.key, - ) + const ammoNeeded = getAmmoNeeded(fleet) - const currentAmmo = fleetInfo.cargoLevels.ammo - const maxAmmo = fleetInfo.cargoStats.ammoCapacity - const ammoNeeded = maxAmmo - currentAmmo - - logger.info( - `Current Ammo: ${currentAmmo}, Max Ammo: ${maxAmmo}, Ammo Needed: ${ammoNeeded}`, - ) + if (ammoNeeded <= 0) { + logger.info('Fleet ammo bank is already full') + return + } - // TODO: Check if starbase has enough ammo balance + const currentAmmo = fleet.ammoBank?.amount ?? 0n + const maxAmmo = fleet.stats?.cargoStats?.ammoCapacity ?? 0n - const ix = loadCargoIx( - fleetInfo, - player, - game, - starbase, - starbasePlayer, - cargoPodFrom.key, - fleetInfo.fleet.data.ammoBank, - starbaseTokenAccounts[0].address, - fleetFuelTokenResult.address, - game.data.mints.ammo, - cargoType.key, - programs, - new BN(ammoNeeded), + logger.info( + `Rearming: current=${currentAmmo}, max=${maxAmmo}, needed=${ammoNeeded}`, ) - const instructions = await ixReturnsToIxs( - [fleetFuelTokenResult.instructions, ix], - player.signer, - ) + const ammoCargoId = getAmmoCargoId(fleet) - await sendAndConfirmInstructions([keyPair])(instructions) + await loadCargo({ + ...params, + transfers: [{ cargoId: ammoCargoId, amount: ammoNeeded }], + }) } diff --git a/src/main/basedbot/lib/sage/act/refuel.ts b/src/main/basedbot/lib/sage/act/refuel.ts index 955f743e..0ca1ee5b 100644 --- a/src/main/basedbot/lib/sage/act/refuel.ts +++ b/src/main/basedbot/lib/sage/act/refuel.ts @@ -1,91 +1,69 @@ -import { - createAssociatedTokenAccountIdempotent, - getParsedTokenAccountsByOwner, - ixReturnsToIxs, -} from '@staratlas/data-source' -import { Game } from '@staratlas/sage' -import BN from 'bn.js' -import { logger } from '../../../../../logger' +/** + * Refuel fleet action - C4 migration + * Fills fleet fuel tank from starbase inventory + */ +import type { Address, KeyPairSigner } from '@solana/kit' -import { connection } from '../../../../../service/sol/index.js' -import { sendAndConfirmInstructions } from '../../../../../service/sol/send-and-confirm-tx.js' -import { keyPair } from '../../../../../service/wallet' -import { programs } from '../../programs.js' -import { Coordinates } from '../../util/coordinates.js' -import { loadCargoIx } from '../ix/load-cargo.js' -import { getCargoType } from '../state/cargo-types.js' -import { starbaseByCoordinates } from '../state/starbase-by-coordinates.js' -import { - getCargoPodsForStarbasePlayer, - getStarbasePlayer, -} from '../state/starbase-player.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' +import { logger } from '../../../../../logger.js' +import type { + AccountWithKey, + Character, + Fleet, + Game, + ProfileFactionAccount, + StarSystem, +} from '../../types.js' +import { getFuelCargoId, loadCargo } from './load-cargo.js' -export const refuel = async ( - fleetInfo: FleetInfo, - coordinates: Coordinates, - player: Player, - game: Game, -): Promise => { - const starbase = await starbaseByCoordinates(coordinates) +import type { Player } from '../state/user-account.js' - if (!starbase) { - throw new Error(`No starbase found at ${coordinates}`) - } +export type RefuelParams = { + game: AccountWithKey + fleet: AccountWithKey + character: AccountWithKey + profileFaction: AccountWithKey + system: AccountWithKey + starbasePlayer: Address | undefined + signer: KeyPairSigner + keyIndex?: number + /** If provided, will auto-deposit fuel from wallet */ + player?: Player +} - const cargoType = getCargoType( - player.cargoTypes, - game, - game.data.mints.fuel, - ) - const fleetFuelTokenResult = createAssociatedTokenAccountIdempotent( - game.data.mints.fuel, - fleetInfo.fleet.data.fuelTank, - true, - ) +/** + * Calculate fuel needed to fill tank (bigint arithmetic to avoid precision loss) + */ +export function getFuelNeeded(fleet: AccountWithKey): number { + const currentFuel = BigInt(fleet.fuelTank?.amount ?? 0n) + const maxFuel = BigInt(fleet.stats?.cargoStats?.fuelCapacity ?? 0) + const needed = maxFuel - currentFuel + return Number(needed > 0n ? needed : 0n) +} - const starbasePlayer = await getStarbasePlayer(player, starbase, programs) - const cargoPodFrom = await getCargoPodsForStarbasePlayer( - starbasePlayer, - programs, - ) +/** + * Refuel fleet - fills fuel tank to capacity + */ +export async function refuel(params: RefuelParams): Promise { + const { fleet } = params - const starbaseTokenAccounts = await getParsedTokenAccountsByOwner( - connection, - cargoPodFrom.key, - ) + const fuelNeeded = getFuelNeeded(fleet) - const currentFuel = fleetInfo.cargoLevels.fuel - const maxFuel = fleetInfo.cargoStats.fuelCapacity - const fuelNeeded = maxFuel - currentFuel - - logger.info( - `Current Fuel: ${currentFuel}, Max Fuel: ${maxFuel}, Fuel Needed: ${fuelNeeded}`, - ) + if (fuelNeeded <= 0) { + logger.info('Fleet fuel tank is already full') + return + } - // TODO: Check if starbase has enough fuel balance + const currentFuel = fleet.fuelTank?.amount ?? 0n + const maxFuel = fleet.stats?.cargoStats?.fuelCapacity ?? 0n - const ix = loadCargoIx( - fleetInfo, - player, - game, - starbase, - starbasePlayer, - cargoPodFrom.key, - fleetInfo.fleet.data.fuelTank, - starbaseTokenAccounts[0].address, - fleetFuelTokenResult.address, - game.data.mints.fuel, - cargoType.key, - programs, - new BN(fuelNeeded), + logger.info( + `Refueling: current=${currentFuel}, max=${maxFuel}, needed=${fuelNeeded}`, ) - const instructions = await ixReturnsToIxs( - [fleetFuelTokenResult.instructions, ix], - player.signer, - ) + const fuelCargoId = getFuelCargoId(fleet) - await sendAndConfirmInstructions([keyPair])(instructions) + await loadCargo({ + ...params, + transfers: [{ cargoId: fuelCargoId, amount: fuelNeeded }], + }) } diff --git a/src/main/basedbot/lib/sage/act/self-destruct.ts b/src/main/basedbot/lib/sage/act/self-destruct.ts index cc8a6b1f..7a9ef687 100644 --- a/src/main/basedbot/lib/sage/act/self-destruct.ts +++ b/src/main/basedbot/lib/sage/act/self-destruct.ts @@ -1,35 +1,26 @@ -import { getAssociatedTokenAddressSync } from '@solana/spl-token' -import { ixReturnsToIxs } from '@staratlas/data-source' -import { Game } from '@staratlas/sage' +/** + * Self-destruct fleet action - C4 migration stub + * TODO: Implement when needed + */ +import type { KeyPairSigner } from '@solana/kit' +import type { Game } from '@staratlas/dev-sage' import { logger } from '../../../../../logger.js' -import { sendAndConfirmInstructions } from '../../../../../service/sol/send-and-confirm-tx.js' -import { keyPair } from '../../../../../service/wallet' -import { programs } from '../../programs.js' -import { idleToRespawnIx } from '../ix/idle-to-respawn.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' +import type { AccountWithKey, Character, Fleet } from '../../types.js' -export const selfDestruct = async ( - fleetInfo: FleetInfo, - player: Player, - game: Game, -): Promise => { - const { fleet } = fleetInfo - - // TODO: Also support self-destruct for mining fleets - if (!fleet.state.Idle) { - logger.warn('Only Idle Fleets can self destruct') - - return - } - const atlasTokenFrom = getAssociatedTokenAddressSync( - game.data.mints.atlas, - player.signer.publicKey(), - ) +export type SelfDestructParams = { + game: AccountWithKey + fleet: AccountWithKey + character: AccountWithKey + signer: KeyPairSigner + keyIndex?: number +} - await ixReturnsToIxs( - idleToRespawnIx(player, game, fleet, atlasTokenFrom, programs), - player.signer, - ).then(sendAndConfirmInstructions([keyPair])) +/** + * Execute self-destruct action + * TODO: Implement with C4 self-destruct instruction + */ +export async function selfDestruct(_params: SelfDestructParams): Promise { + logger.warn('selfDestruct not yet implemented for C4') + // TODO: Implement using getSelfDestructInstruction from dev-sage } diff --git a/src/main/basedbot/lib/sage/act/stop-subwarp.ts b/src/main/basedbot/lib/sage/act/stop-subwarp.ts index 34a8f9cf..777ddd6a 100644 --- a/src/main/basedbot/lib/sage/act/stop-subwarp.ts +++ b/src/main/basedbot/lib/sage/act/stop-subwarp.ts @@ -1,53 +1,64 @@ -import { - createAssociatedTokenAccountIdempotent, - ixReturnsToIxs, -} from '@staratlas/data-source' -import { Game } from '@staratlas/sage' +/** + * Stop subwarp action - C4 migration + * Stops subwarp movement before arrival (MoveSubwarp -> Idle) + */ +import type { Instruction, KeyPairSigner } from '@solana/kit' +import { getStopSubwarpInstruction } from '@staratlas/dev-sage' import { logger } from '../../../../../logger.js' import { sendAndConfirmInstructions } from '../../../../../service/sol/send-and-confirm-tx.js' -import { keyPair } from '../../../../../service/wallet' -import { programs } from '../../programs.js' -import { stopSubWarpIx } from '../ix/stop-subwarp.js' -import { getCargoStatsDefinition } from '../state/cargo-stats-definition.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' +import type { + AccountWithKey, + Character, + Fleet, + Game, + RegionTracker, +} from '../../types.js' -import { endMove } from './end-move.js' - -export const stopSubwarp = async ( - fleetInfo: FleetInfo, - player: Player, - game: Game, -): Promise => { - const { fleet } = fleetInfo - - if (!fleet.state.MoveSubwarp) { - logger.warn('Fleet is not subwarping, cannot End Subwarp') +export type StopSubwarpParams = { + game: AccountWithKey + fleet: AccountWithKey + character: AccountWithKey + regionTracker: AccountWithKey + signer: KeyPairSigner + keyIndex?: number +} - return +/** + * Creates stop subwarp instruction + */ +export function createStopSubwarpInstruction({ + game, + fleet, + character, + regionTracker, + signer, + keyIndex = 0, +}: StopSubwarpParams): Instruction { + // Check fleet state + if (fleet.state.__kind !== 'MoveSubwarp') { + throw new Error( + `Cannot stop subwarp: fleet is in ${fleet.state.__kind} state, must be MoveSubwarp`, + ) } - const fuelToken = createAssociatedTokenAccountIdempotent( - game.data.mints.fuel, - fleet.data.fuelTank, - true, - ) + return getStopSubwarpInstruction({ + game: game.key, + fleet: fleet.key, + profileValidationSigner: signer, + profileValidationProfile: character.playerProfile, + character: character.key, + regionTracker: regionTracker.key, + keyIndex, + }) +} + +/** + * Execute stop subwarp action - sends transaction + */ +export async function stopSubwarp(params: StopSubwarpParams): Promise { + const instruction = createStopSubwarpInstruction(params) - await ixReturnsToIxs( - [ - fuelToken.instructions, - stopSubWarpIx( - fleetInfo, - player, - game, - await getCargoStatsDefinition(game.data.cargo.statsDefinition), - fuelToken.address, - game.data.mints.fuel, - programs, - ), - ], - player.signer, - ).then(sendAndConfirmInstructions([keyPair])) - await endMove(fleetInfo, player, game) + logger.info(`Stopping subwarp for fleet ${params.fleet.key}`) + await sendAndConfirmInstructions(params.signer)([instruction]) } diff --git a/src/main/basedbot/lib/sage/act/undock.ts b/src/main/basedbot/lib/sage/act/undock.ts index 765821e0..6000ce39 100644 --- a/src/main/basedbot/lib/sage/act/undock.ts +++ b/src/main/basedbot/lib/sage/act/undock.ts @@ -1,42 +1,165 @@ -import { ixReturnsToIxs } from '@staratlas/data-source' -import { Fleet, Game } from '@staratlas/sage' - -import { sendAndConfirmInstructions } from '../../../../../service/sol/send-and-confirm-tx.js' -import { keyPair } from '../../../../../service/wallet' -import { programs } from '../../programs.js' -import { Coordinates } from '../../util/coordinates.js' -import { starbaseByCoordinates } from '../state/starbase-by-coordinates.js' -import { getStarbasePlayer } from '../state/starbase-player.js' -import { Player } from '../state/user-account.js' - -export const undock = async ( - fleet: Fleet, - coordinates: Coordinates, - player: Player, - game: Game, -): Promise => { - const starbase = await starbaseByCoordinates(coordinates) - - if (!starbase) { - throw new Error(`No starbase found at ${coordinates}`) +/** + * Undock fleet action - C4 migration + * Transitions fleet from Docked to Idle state + */ +import type { Address, Instruction, KeyPairSigner } from '@solana/kit' +import { + findStarbasePlayerPda, + getDockedToIdleInstruction, + getRegisterStarbasePlayerInstruction, +} from '@staratlas/dev-sage' + +import { logger } from '../../../../../logger.js' +import { + sendAndConfirmInstructions, + SimulationProgramError, +} from '../../../../../service/sol/send-and-confirm-tx.js' +import type { + AccountWithKey, + Character, + Fleet, + Game, + ProfileFactionAccount, + StarSystem, +} from '../../types.js' + +export type UndockResult = { + success: boolean + reason?: string +} + +export type UndockParams = { + game: AccountWithKey + fleet: AccountWithKey + profileFaction: AccountWithKey + character: AccountWithKey + system: AccountWithKey + starbasePlayer: Address | undefined + signer: KeyPairSigner + keyIndex?: number +} + +/** + * Validate fleet can undock and create instructions + * Returns null if fleet cannot undock (with logging) + */ +export async function validateAndCreateUndockInstructions({ + game, + fleet, + profileFaction, + character, + system, + starbasePlayer, + signer, + keyIndex = 0, +}: UndockParams): Promise { + const instructions: Instruction[] = [] + + // Validate fleet state - must be Docked to undock + const state = fleet.state.__kind + if (state !== 'Docked') { + if (state === 'Idle') { + logger.debug(`Fleet already idle, skipping undock action`) + } else { + logger.warn( + `Cannot undock: fleet is in ${state} state, must be Docked`, + ) + } + return null + } + + let starbasePlayerAddress = starbasePlayer + + // Register starbase player if needed + if (!starbasePlayerAddress) { + const [pda] = await findStarbasePlayerPda({ + system: system.key, + character: character.key, + }) + starbasePlayerAddress = pda + + logger.info(`Registering new starbase player at system: ${system.key}`) + + instructions.push( + getRegisterStarbasePlayerInstruction({ + game: game.key, + character: character.key, + profileFaction: profileFaction.key, + funder: signer, + system: system.key, + starbasePlayer: starbasePlayerAddress, + }), + ) } - const { sage } = programs - const starbasePlayer = await getStarbasePlayer(player, starbase, programs) - - const ix = Fleet.loadingBayToIdle( - sage, - player.signer, - player.profile.key, - player.profileFaction.key, - fleet.key, - starbase.key, - starbasePlayer.key, - game.key, - game.data.gameState, - player.keyIndex, + + // Add undock instruction + instructions.push( + getDockedToIdleInstruction({ + game: game.key, + systemAndStarbasePlayerSystem: system.key, + systemAndStarbasePlayerStarbasePlayer: starbasePlayerAddress, + fleet: fleet.key, + profileValidationSigner: signer, + profileValidationProfile: character.playerProfile, + keyIndex, + }), ) - const instructions = await ixReturnsToIxs(ix, player.signer) + return instructions +} + +/** + * Creates undock instruction (Docked -> Idle) + * @deprecated Use validateAndCreateUndockInstructions for proper validation + */ +export async function createUndockInstructions( + params: UndockParams, +): Promise { + const instructions = await validateAndCreateUndockInstructions(params) + if (!instructions) { + throw new Error('Undock validation failed - check logs for details') + } + return instructions +} + +/** + * Execute undock action - sends transaction + * Returns result indicating success or failure reason + */ +export async function undock(params: UndockParams): Promise { + const instructions = await validateAndCreateUndockInstructions(params) - await sendAndConfirmInstructions([keyPair])(instructions) + if (!instructions) { + return { success: false, reason: 'validation_failed' } + } + + try { + await sendAndConfirmInstructions(params.signer)(instructions) + return { success: true } + } catch (e) { + // Handle simulation errors gracefully + if (e instanceof SimulationProgramError) { + const logsStr = e.logs.join('\n') + + // Fleet already idle - stale local state, treat as success + if ( + logsStr.includes('Status mismatch') && + logsStr.includes('actual_state=Idle') + ) { + return { success: true } + } + + // Any other simulation error + logger.warn( + `Undock simulation failed: ${e.message}\nLogs: ${e.logs.join('\n')}`, + ) + return { success: false, reason: 'transaction_failed' } + } + + // Any other error + logger.warn( + `Undock failed: ${e instanceof Error ? e.message : String(e)}`, + ) + return { success: false, reason: 'transaction_failed' } + } } diff --git a/src/main/basedbot/lib/sage/act/unload-all-cargo.ts b/src/main/basedbot/lib/sage/act/unload-all-cargo.ts index 2141814b..2184e318 100644 --- a/src/main/basedbot/lib/sage/act/unload-all-cargo.ts +++ b/src/main/basedbot/lib/sage/act/unload-all-cargo.ts @@ -1,95 +1,5 @@ -import { - createAssociatedTokenAccountIdempotent, - getParsedTokenAccountsByOwner, - InstructionReturn, - ixReturnsToIxs, -} from '@staratlas/data-source' -import { Game } from '@staratlas/sage' -import BN from 'bn.js' - -import { connection } from '../../../../../service/sol/index.js' -import { sendAndConfirmInstructions } from '../../../../../service/sol/send-and-confirm-tx.js' -import { keyPair } from '../../../../../service/wallet' -import { programs } from '../../programs.js' -import { Coordinates } from '../../util/coordinates.js' -import { unloadCargoIx } from '../ix/unload-cargo.js' -import { getCargoType } from '../state/cargo-types.js' -import { starbaseByCoordinates } from '../state/starbase-by-coordinates.js' -import { - getCargoPodsForStarbasePlayer, - getStarbasePlayer, -} from '../state/starbase-player.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' - -export const unloadAllCargo = async ( - fleetInfo: FleetInfo, - coordinates: Coordinates, - player: Player, - game: Game, -): Promise => { - const starbase = await starbaseByCoordinates(coordinates) - - const hold = fleetInfo.fleet.data.cargoHold - - if (!starbase) { - throw new Error(`No starbase found at ${coordinates}`) - } - const starbasePlayer = await getStarbasePlayer(player, starbase, programs) - const cargoPodTo = await getCargoPodsForStarbasePlayer( - starbasePlayer, - programs, - ) - - const fleetTokenAccounts = await getParsedTokenAccountsByOwner( - connection, - hold, - ) - - const tokenAddresses: string[] = [] - const withdrawInstructions: InstructionReturn[] = [] - - for (let i = 0; i < fleetTokenAccounts.length; i++) { - const fleetTokenAccount = fleetTokenAccounts[i] - const tokenToResult = createAssociatedTokenAccountIdempotent( - fleetTokenAccount.mint, - (await getCargoPodsForStarbasePlayer(starbasePlayer, programs)).key, - true, - ) - - if (!tokenAddresses.includes(tokenToResult.address.toBase58())) { - tokenAddresses.push(tokenToResult.address.toBase58()) - withdrawInstructions.push(tokenToResult.instructions) - } - - const cargoType = getCargoType( - player.cargoTypes, - game, - fleetTokenAccount.mint, - ) - - withdrawInstructions.push( - unloadCargoIx( - fleetInfo, - player, - game, - starbase, - starbasePlayer, - fleetInfo.fleet.data.cargoHold, - cargoPodTo.key, - fleetTokenAccount.address, - tokenToResult.address, - fleetTokenAccount.mint, - cargoType.key, - programs, - new BN(fleetTokenAccount.delegatedAmount.toString()), - ), - ) - } - const instructions = await ixReturnsToIxs( - withdrawInstructions, - player.signer, - ) - - await sendAndConfirmInstructions([keyPair])(instructions) -} +/** + * Unload all cargo action - C4 migration + * Re-exports from unload-cargo.ts for backwards compatibility + */ +export { unloadAllCargo } from './unload-cargo.js' diff --git a/src/main/basedbot/lib/sage/act/unload-cargo.ts b/src/main/basedbot/lib/sage/act/unload-cargo.ts index a232c6f0..68015e65 100644 --- a/src/main/basedbot/lib/sage/act/unload-cargo.ts +++ b/src/main/basedbot/lib/sage/act/unload-cargo.ts @@ -1,114 +1,225 @@ -import { getAssociatedTokenAddressSync } from '@solana/spl-token' -import { PublicKey } from '@solana/web3.js' +/** + * Unload cargo from fleet action - C4 migration + * Transfers cargo from fleet to starbase using cargoId system + */ +import type { Address, Instruction, KeyPairSigner } from '@solana/kit' import { - createAssociatedTokenAccountIdempotent, - ixReturnsToIxs, -} from '@staratlas/data-source' -import { Game } from '@staratlas/sage' -import BN from 'bn.js' + findStarbasePlayerPda, + getRegisterStarbasePlayerInstruction, + getTransferCargoToFleetInstruction, +} from '@staratlas/dev-sage' import { logger } from '../../../../../logger.js' -import { sendAndConfirmInstructions } from '../../../../../service/sol/send-and-confirm-tx.js' -import { keyPair } from '../../../../../service/wallet' -import { getTokenBalance } from '../../../basedbot.js' -import { programs } from '../../programs.js' -import { unloadCargoIx } from '../ix/unload-cargo.js' -import { getCargoType } from '../state/cargo-types.js' -import { starbaseByCoordinates } from '../state/starbase-by-coordinates.js' import { - getCargoPodsForStarbasePlayer, - getStarbasePlayer, -} from '../state/starbase-player.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' -import { getFleetCargoHold } from './load-cargo.js' - -export const getHold = ( - mint: PublicKey, - game: Game, - fleetInfo: FleetInfo, -): PublicKey => { - switch (mint.toBase58()) { - case game.data.mints.fuel.toBase58(): - return fleetInfo.fleet.data.fuelTank - case game.data.mints.ammo.toBase58(): - return fleetInfo.fleet.data.ammoBank - default: - return fleetInfo.fleet.data.cargoHold - } + sendAndConfirmInstructions, + SimulationProgramError, + TransactionFailedError, +} from '../../../../../service/sol/send-and-confirm-tx.js' +import type { + AccountWithKey, + Character, + Fleet, + Game, + ProfileFactionAccount, + StarSystem, +} from '../../types.js' +import { + getFuelCargoId, + getAmmoCargoId, + type CargoTransfer, +} from './load-cargo.js' + +export type UnloadCargoParams = { + game: AccountWithKey + fleet: AccountWithKey + character: AccountWithKey + profileFaction: AccountWithKey + system: AccountWithKey + starbasePlayer: Address | undefined + signer: KeyPairSigner + transfers: CargoTransfer[] + keyIndex?: number + /** + * When true, all transfers go to cargoHold.toUnload regardless of cargoId. + * Use when the transfer amounts come from cargoHold.amounts (not fuel tank/ammo bank). + * When false (default), fuel/ammo cargoIds are routed to fuelTank/ammoBank fields. + */ + fromCargoHold?: boolean } -export const unloadCargo = async ( - fleetInfo: FleetInfo, - player: Player, - game: Game, - mint: PublicKey, - amount: BN, - forceCargoHold: boolean = false, -): Promise => { - const starbase = await starbaseByCoordinates(fleetInfo.location) - - if (!starbase) { - throw new Error(`No starbase found at ${fleetInfo.location}`) +/** + * Creates unload cargo instruction + * Uses negative amounts to transfer from fleet to starbase + */ +export async function createUnloadCargoInstructions({ + game, + fleet, + character, + profileFaction, + system, + starbasePlayer, + signer, + transfers, + keyIndex = 0, + fromCargoHold = false, +}: UnloadCargoParams): Promise { + const instructions: Instruction[] = [] + + // Check fleet state - must be docked + if (fleet.state.__kind !== 'Docked') { + throw new Error( + `Cannot unload cargo: fleet is in ${fleet.state.__kind} state, must be Docked`, + ) } - const cargoType = getCargoType(player.cargoTypes, game, mint) + let starbasePlayerAddress = starbasePlayer - const fleetCargoPod = forceCargoHold - ? fleetInfo.fleet.data.cargoHold - : getFleetCargoHold(mint, game, fleetInfo) + // Register starbase player if needed + if (!starbasePlayerAddress) { + const [pda] = await findStarbasePlayerPda({ + system: system.key, + character: character.key, + }) + starbasePlayerAddress = pda - const starbasePlayer = await getStarbasePlayer(player, starbase, programs) - const cargoPodTo = await getCargoPodsForStarbasePlayer( - starbasePlayer, - programs, - ) + logger.info(`Registering new starbase player at system: ${system.key}`) - const cargoPodTokenAccountAddress = getAssociatedTokenAddressSync( - mint, - cargoPodTo.key, - true, - ) - const cargoFleetTokenAccountAddress = getAssociatedTokenAddressSync( - mint, - fleetCargoPod, - true, - ) - const cargoPodTokenResult = createAssociatedTokenAccountIdempotent( - mint, - cargoPodTo.key, - true, - ) + instructions.push( + getRegisterStarbasePlayerInstruction({ + game: game.key, + character: character.key, + profileFaction: profileFaction.key, + funder: signer, + system: system.key, + starbasePlayer: starbasePlayerAddress, + }), + ) + } - const amountAtOrigin = await getTokenBalance(fleetCargoPod, mint) + const fuelCargoId = getFuelCargoId(fleet) + const ammoCargoId = getAmmoCargoId(fleet) - if (amountAtOrigin.lt(amount)) { - logger.warn( - `Requested ${amount.toNumber()} cargo to unload. can only unload ${amountAtOrigin.toNumber()}`, - ) + let fuelTank: bigint | null = null + let ammoBank: bigint | null = null + const cargoHoldToUnload: Array = [] + + for (const { cargoId, amount } of transfers) { + if (amount === 0) continue + + // Floor to ensure integer for BigInt conversion + const unloadAmount = Math.floor(Math.abs(amount)) + if (unloadAmount === 0) continue + + if (fromCargoHold) { + // All items came from cargoHold.amounts — unload from cargo hold + cargoHoldToUnload.push([cargoId, BigInt(unloadAmount)] as const) + } else if (cargoId === fuelCargoId) { + fuelTank = BigInt(-unloadAmount) + } else if (cargoId === ammoCargoId) { + ammoBank = BigInt(-unloadAmount) + } else { + cargoHoldToUnload.push([cargoId, BigInt(unloadAmount)] as const) + } } - const toUnload = amountAtOrigin.lt(amount) ? amountAtOrigin : amount - - const ix = unloadCargoIx( - fleetInfo, - player, - game, - starbase, - starbasePlayer, - fleetCargoPod, - cargoPodTo.key, - cargoFleetTokenAccountAddress, - cargoPodTokenAccountAddress, - mint, - cargoType.key, - programs, - toUnload, - ) - const instructions = await ixReturnsToIxs( - [cargoPodTokenResult.instructions, ix], - player.signer, + // Create transfer instruction + instructions.push( + getTransferCargoToFleetInstruction({ + game: game.key, + character: character.key, + systemAndStarbasePlayerStarbasePlayer: starbasePlayerAddress, + systemAndStarbasePlayerSystem: system.key, + fleet: fleet.key, + profileValidationProfile: character.playerProfile, + profileValidationSigner: signer, + fuelTank, + ammoBank, + cargoHold: { toLoad: [], toUnload: cargoHoldToUnload }, + keyIndex, + }), ) - await sendAndConfirmInstructions([keyPair])(instructions) + return instructions +} + +/** + * Execute unload cargo action - sends transaction + * Returns true if successful, false if failed + */ +export async function unloadCargo(params: UnloadCargoParams): Promise { + const instructions = await createUnloadCargoInstructions(params) + + try { + await sendAndConfirmInstructions(params.signer)(instructions) + return true + } catch (e) { + if (e instanceof SimulationProgramError) { + logger.warn( + `Unload cargo simulation failed: ${e.message}\nLogs: ${e.logs.join('\n')}`, + ) + return false + } + + if (e instanceof TransactionFailedError) { + logger.warn(`Unload cargo transaction failed: ${e.message}`) + return false + } + + // Re-throw unknown errors + throw e + } +} + +/** + * Unload all cargo from fleet cargo hold + */ +export async function unloadAllCargo( + params: Omit, +): Promise { + const { fleet } = params + + // Get all cargo in cargo hold + const cargoAmounts = fleet.cargoHold?.amounts + if (!cargoAmounts || cargoAmounts.size === 0) { + return true // Nothing to unload is success + } + + const transfers: CargoTransfer[] = [] + for (const [cargoId, amount] of cargoAmounts) { + if (Number(amount) > 0) { + transfers.push({ cargoId: Number(cargoId), amount: Number(amount) }) + } + } + + if (transfers.length === 0) { + return true // Nothing to unload is success + } + + return unloadCargo({ ...params, transfers }) +} + +/** + * Convenience function: unload specific amount of fuel + */ +export async function unloadFuel( + params: Omit & { amount: number }, +): Promise { + const fuelCargoId = getFuelCargoId(params.fleet) + return unloadCargo({ + ...params, + transfers: [{ cargoId: fuelCargoId, amount: params.amount }], + }) +} + +/** + * Convenience function: unload specific amount of ammo + */ +export async function unloadAmmo( + params: Omit & { amount: number }, +): Promise { + const ammoCargoId = getAmmoCargoId(params.fleet) + return unloadCargo({ + ...params, + transfers: [{ cargoId: ammoCargoId, amount: params.amount }], + }) } diff --git a/src/main/basedbot/lib/sage/act/unlock-research.ts b/src/main/basedbot/lib/sage/act/unlock-research.ts new file mode 100644 index 00000000..5e0ac208 --- /dev/null +++ b/src/main/basedbot/lib/sage/act/unlock-research.ts @@ -0,0 +1,236 @@ +/** + * Research node unlock actions for C4 + */ +import type { Instruction } from '@solana/kit' +import { + findAssociatedTokenPda, + getCreateAssociatedTokenIdempotentInstruction, + TOKEN_PROGRAM_ADDRESS, +} from '@solana-program/token' +import type { + Character, + Game, + ResearchNode, + StarbasePlayer, + StarSystem, +} from '@staratlas/dev-sage' +import { + findCurrencyConfigCachePda, + getUnlockResearchNodeInstructionAsync, +} from '@staratlas/dev-sage' + +import { logger } from '../../../../../logger.js' +import { sendAndConfirmInstructions } from '../../../../../service/sol/send-and-confirm-tx.js' +import type { AccountWithKey } from '../../types.js' +import type { Player } from '../state/user-account.js' + +/** + * Get all research nodes from the game + */ +export function getAllResearchNodes( + game: AccountWithKey, +): { id: number; node: ResearchNode }[] { + const nodes = game.researchTreeDefinitions.nodes + + return nodes.offsetList.map((entry, index) => ({ + id: entry.key, + node: nodes.unsizedList[index], + })) +} + +/** + * Get a research node by ID + */ +export function getResearchNodeById( + game: AccountWithKey, + nodeId: number, +): ResearchNode | null { + const nodes = game.researchTreeDefinitions.nodes + const nodeIndex = nodes.offsetList.findIndex( + (entry) => entry.key === nodeId, + ) + if (nodeIndex === -1 || nodeIndex >= nodes.unsizedList.length) return null + return nodes.unsizedList[nodeIndex] +} + +/** + * Check if character has unlocked a specific research node + */ +export function hasUnlockedNode( + character: AccountWithKey, + nodeId: number, +): boolean { + return character.characterModifiers.unlockedNodes.has(nodeId) +} + +/** + * Get all unlocked node IDs from a character + */ +export function getUnlockedNodeIds( + character: AccountWithKey, +): number[] { + return Array.from(character.characterModifiers.unlockedNodes) +} + +/** + * Get research tags the character has from unlocked nodes + */ +export function getCharacterResearchTags( + character: AccountWithKey, + game: AccountWithKey, +): Set { + const tags = new Set() + const unlockedIds = getUnlockedNodeIds(character) + + for (const nodeId of unlockedIds) { + const node = getResearchNodeById(game, nodeId) + if (node && node.modifier.researchTags) { + // Get the research_tags from the node's modifier + for (const tag of node.modifier.researchTags) { + tags.add(tag) + } + } + } + + return tags +} + +/** + * Check if character has all required research tags for a ship + */ +export function hasRequiredResearchTags( + character: AccountWithKey, + game: AccountWithKey, + requiredTags: number[], +): { hasAll: boolean; missing: number[] } { + const characterTags = getCharacterResearchTags(character, game) + const missing: number[] = [] + + for (const tag of requiredTags) { + if (!characterTags.has(tag)) { + missing.push(tag) + } + } + + return { + hasAll: missing.length === 0, + missing, + } +} + +/** + * Find research nodes that grant a specific tag + */ +export function findNodesGrantingTag( + game: AccountWithKey, + tag: number, +): { id: number; node: ResearchNode }[] { + const allNodes = getAllResearchNodes(game) + return allNodes.filter( + (n) => n.node.modifier.researchTags?.has(tag) ?? false, + ) +} + +/** + * Unlock a research node + */ +export async function unlockResearchNode( + player: Player, + game: AccountWithKey, + system: AccountWithKey, + starbasePlayer: AccountWithKey, + nodeId: number, +): Promise { + const signer = player.signer + const character = player.character + const keyIndex = player.keyIndex + + const node = getResearchNodeById(game, nodeId) + if (!node) { + logger.warn(`Research node ${nodeId} not found`) + return + } + + if (hasUnlockedNode(character, nodeId)) { + logger.info(`Research node ${nodeId} already unlocked`) + return + } + + logger.info(`Unlocking research node ${nodeId}: ${node.name}`) + + const instructions: Instruction[] = [] + + // Derive the currency config cache PDA + const [currencyCache] = await findCurrencyConfigCachePda({ + gameId: game.key, + }) + + // Get the game's ATLAS vault ATA + const [vaultAta] = await findAssociatedTokenPda({ + mint: game.currencies.atlas.mint, + owner: currencyCache, + tokenProgram: TOKEN_PROGRAM_ADDRESS, + }) + + // Create vault ATA idempotently + instructions.push( + getCreateAssociatedTokenIdempotentInstruction({ + payer: signer, + ata: vaultAta, + owner: currencyCache, + mint: game.currencies.atlas.mint, + }), + ) + + // Add the unlock research node instruction + instructions.push( + await getUnlockResearchNodeInstructionAsync({ + profileValidationSigner: signer, + profileValidationProfile: character.playerProfile, + character: character.key, + game: game.key, + starbasePlayer: starbasePlayer.key, + system: system.key, + currencyCache, + keyIndex, + nodeId, + }), + ) + + try { + await sendAndConfirmInstructions(signer)(instructions) + logger.info(`Research node ${nodeId} unlocked successfully`) + } catch (error) { + // Check if this is an "already unlocked" error - handle gracefully + const errorStr = String(error) + if ( + errorStr.includes('node_already_unlocked') || + errorStr.includes('Already exists') + ) { + logger.info( + `Research node ${nodeId} was already unlocked (on-chain state newer than cache)`, + ) + // Update the local cache to reflect on-chain state + character.characterModifiers.unlockedNodes.add(nodeId) + return + } + // Re-throw other errors + throw error + } +} + +/** + * List all research nodes (for debugging) + */ +export function listAllResearchNodes(game: AccountWithKey): void { + const nodes = getAllResearchNodes(game) + logger.info(`Available research nodes (${nodes.length}):`) + for (const { id, node } of nodes) { + const tags = node.modifier.researchTags + ? [...node.modifier.researchTags] + : [] + logger.info( + ` [${id}] ${node.name} - grants tags: [${tags.join(', ')}], requires: [${[...node.requiredTags].join(', ')}]`, + ) + } +} diff --git a/src/main/basedbot/lib/sage/act/withdraw-cargo.ts b/src/main/basedbot/lib/sage/act/withdraw-cargo.ts deleted file mode 100644 index 443a2b02..00000000 --- a/src/main/basedbot/lib/sage/act/withdraw-cargo.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { getAssociatedTokenAddressSync } from '@solana/spl-token' -import { PublicKey } from '@solana/web3.js' -import { - createAssociatedTokenAccountIdempotent, - InstructionReturn, - ixReturnsToIxs, -} from '@staratlas/data-source' -import { Game, Starbase } from '@staratlas/sage' -import BN from 'bn.js' - -import { sendAndConfirmInstructions } from '../../../../../service/sol/send-and-confirm-tx.js' -import { keyPair } from '../../../../../service/wallet' -import { getTokenBalance } from '../../../basedbot.js' -import { programs } from '../../programs.js' -import { withdrawCargoIx } from '../ix/withdraw-cargo.js' -import { getCargoType } from '../state/cargo-types.js' -import { - getCargoPodsForStarbasePlayer, - getStarbasePlayer, -} from '../state/starbase-player.js' -import { Player } from '../state/user-account.js' - -export const withdrawCargo = async ( - player: Player, - game: Game, - starbase: Starbase, - mint: PublicKey, - amount: BN, -): Promise => { - const instructions: InstructionReturn[] = [] - - const starbasePlayer = await getStarbasePlayer(player, starbase, programs) - - const destinationTokenAccount = getAssociatedTokenAddressSync( - mint, - player.signer.publicKey(), - ) - - const cargoPodFrom = await getCargoPodsForStarbasePlayer( - starbasePlayer, - programs, - ) - const sourceTokenAccount = getAssociatedTokenAddressSync( - mint, - cargoPodFrom.key, - true, - ) - - instructions.push( - createAssociatedTokenAccountIdempotent( - mint, - destinationTokenAccount, - true, - ).instructions, - ) - - const cargoType = getCargoType(player.cargoTypes, game, mint) - - const amountAtOrigin = await getTokenBalance(cargoPodFrom.key, mint) - - if (amountAtOrigin.lt(new BN(amount))) { - throw new Error('Not enough cargo available at origin') - } - - instructions.push( - withdrawCargoIx( - player, - game, - starbase, - starbasePlayer, - cargoPodFrom.key, - sourceTokenAccount, - destinationTokenAccount, - cargoType.key, - mint, - programs, - amount, - ), - ) - - await sendAndConfirmInstructions([keyPair])( - await ixReturnsToIxs(instructions, player.signer), - ) -} diff --git a/src/main/basedbot/lib/sage/ix/add-ship-escrow.ts b/src/main/basedbot/lib/sage/ix/add-ship-escrow.ts deleted file mode 100644 index 3c60a126..00000000 --- a/src/main/basedbot/lib/sage/ix/add-ship-escrow.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { InstructionReturn } from '@staratlas/data-source' -import { - Game, - SagePlayerProfile, - Starbase, - StarbasePlayer, -} from '@staratlas/sage' -import BN from 'bn.js' - -import { StarAtlasPrograms } from '../../programs.js' -import { Player } from '../state/user-account.js' - -export const addShipEscrowIx = ( - player: Player, - game: Game, - starbase: Starbase, - starbasePlayer: StarbasePlayer, - sagePlayerProfile: PublicKey, - programs: StarAtlasPrograms, - originTokenAccount: PublicKey, - ship: PublicKey, - shipEscrowTokenAccount: PublicKey, - shipAmount: BN, - escrowIndex: number | null, -): InstructionReturn => - SagePlayerProfile.addShipEscrow( - programs.sage, - player.profile.key, - player.profileFaction.key, - sagePlayerProfile, - player.signer, - originTokenAccount, - ship, - shipEscrowTokenAccount, - starbasePlayer.key, - starbase.key, - game.key, - game.data.gameState, - { - shipAmount, - index: escrowIndex, - }, - ) diff --git a/src/main/basedbot/lib/sage/ix/add-ship-to-fleet.ts b/src/main/basedbot/lib/sage/ix/add-ship-to-fleet.ts deleted file mode 100644 index 1a4b31de..00000000 --- a/src/main/basedbot/lib/sage/ix/add-ship-to-fleet.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { InstructionReturn } from '@staratlas/data-source' -import { Fleet, Game, Starbase, StarbasePlayer } from '@staratlas/sage' - -import { StarAtlasPrograms } from '../../programs.js' -import { Player } from '../state/user-account.js' - -export const addShipToFleetIx = ( - player: Player, - game: Game, - starbase: Starbase, - starbasePlayer: StarbasePlayer, - programs: StarAtlasPrograms, - fleet: PublicKey, - ship: PublicKey, - shipAmount: number, - shipEscrowIndex: number, -): InstructionReturn => - Fleet.addShipToFleet( - programs.sage, - player.signer, - player.profile.key, - player.profileFaction.key, - fleet, - ship, - starbasePlayer.key, - starbase.key, - game.key, - game.data.gameState, - { - shipAmount, - shipEscrowIndex, - keyIndex: 0, - fleetShipInfoIndex: null, - }, - ) diff --git a/src/main/basedbot/lib/sage/ix/close-disbanded-fleet.ts b/src/main/basedbot/lib/sage/ix/close-disbanded-fleet.ts deleted file mode 100644 index dc871b58..00000000 --- a/src/main/basedbot/lib/sage/ix/close-disbanded-fleet.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { InstructionReturn } from '@staratlas/data-source' -import { DisbandedFleet } from '@staratlas/sage' - -import { StarAtlasPrograms } from '../../programs.js' -import { Player } from '../state/user-account.js' - -export const closeDisbandedFleetIx = ( - player: Player, - programs: StarAtlasPrograms, - disbandedFleetKey: PublicKey, - fleetShipsKey: PublicKey, -): InstructionReturn => - DisbandedFleet.closeDisbandedFleet( - programs.sage, - player.signer, - player.profile.key, - 'funder', - disbandedFleetKey, - fleetShipsKey, - { keyIndex: 0 }, - ) diff --git a/src/main/basedbot/lib/sage/ix/create-fleet.ts b/src/main/basedbot/lib/sage/ix/create-fleet.ts deleted file mode 100644 index ac40bc51..00000000 --- a/src/main/basedbot/lib/sage/ix/create-fleet.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { InstructionReturn, stringToByteArray } from '@staratlas/data-source' -import { Fleet, Game, Starbase, StarbasePlayer } from '@staratlas/sage' - -import { StarAtlasPrograms } from '../../programs.js' -import { Player } from '../state/user-account.js' - -type CreateFleetReturn = { - fleetKey: [PublicKey, number] - cargoHoldKey: [PublicKey, number] - fuelTankKey: [PublicKey, number] - ammoBankKey: [PublicKey, number] - instructions: InstructionReturn -} - -export const createFleetIx = ( - player: Player, - game: Game, - starbase: Starbase, - starbasePlayer: StarbasePlayer, - programs: StarAtlasPrograms, - ship: PublicKey, - cargoStatsDefinition: PublicKey, - shipAmount: number, - name: string, - shipEscrowIndex: number, -): CreateFleetReturn => - Fleet.createFleet( - programs.sage, - programs.cargo, - player.signer, - player.profile.key, - player.profileFaction.key, - ship, - starbasePlayer.key, - starbase.key, - game.key, - game.data.gameState, - cargoStatsDefinition, - { - shipAmount, - fleetLabel: stringToByteArray(name, 32), - shipEscrowIndex, - keyIndex: 0, - }, - ) diff --git a/src/main/basedbot/lib/sage/ix/deposit-cargo.ts b/src/main/basedbot/lib/sage/ix/deposit-cargo.ts deleted file mode 100644 index d9373846..00000000 --- a/src/main/basedbot/lib/sage/ix/deposit-cargo.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { InstructionReturn } from '@staratlas/data-source' -import { Game, Starbase, StarbasePlayer } from '@staratlas/sage' -import BN from 'bn.js' - -import { StarAtlasPrograms } from '../../programs.js' -import { Player } from '../state/user-account.js' - -export const depositCargoIx = ( - player: Player, - game: Game, - starbase: Starbase, - starbasePlayer: StarbasePlayer, - cargoPodTo: PublicKey, - tokenFrom: PublicKey, - tokenTo: PublicKey, - cargoType: PublicKey, - programs: StarAtlasPrograms, - amount: BN, -): InstructionReturn => - StarbasePlayer.depositCargoToGame( - programs.sage, - programs.cargo, - starbasePlayer.key, - player.signer, - player.profile.key, - player.profileFaction.key, - starbase.key, - cargoPodTo, - cargoType, - game.data.cargo.statsDefinition, - tokenFrom, - tokenTo, - game.key, - game.data.gameState, - { - amount, - keyIndex: 0, - }, - ) diff --git a/src/main/basedbot/lib/sage/ix/disband-fleet.ts b/src/main/basedbot/lib/sage/ix/disband-fleet.ts deleted file mode 100644 index a4592466..00000000 --- a/src/main/basedbot/lib/sage/ix/disband-fleet.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { InstructionReturn } from '@staratlas/data-source' -import { Fleet, Game, Starbase, StarbasePlayer } from '@staratlas/sage' - -import { StarAtlasPrograms } from '../../programs.js' -import { Player } from '../state/user-account.js' - -type DisbandFleetReturn = { - disbandedFleetKey: [PublicKey, number] - instructions: InstructionReturn -} - -export const disbandFleetIx = ( - player: Player, - game: Game, - starbase: Starbase, - starbasePlayer: StarbasePlayer, - programs: StarAtlasPrograms, - fleet: Fleet, -): DisbandFleetReturn => - Fleet.disbandFleet( - programs.sage, - programs.cargo, - player.signer, - player.profile.key, - player.profileFaction.key, - fleet, - starbasePlayer.key, - starbase.key, - game.key, - game.data.gameState, - { - keyIndex: 0, - }, - ) diff --git a/src/main/basedbot/lib/sage/ix/disbanded-fleet-to-escrow.ts b/src/main/basedbot/lib/sage/ix/disbanded-fleet-to-escrow.ts deleted file mode 100644 index 615e0ce3..00000000 --- a/src/main/basedbot/lib/sage/ix/disbanded-fleet-to-escrow.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { InstructionReturn } from '@staratlas/data-source' -import { DisbandedFleet, Game, Starbase, StarbasePlayer } from '@staratlas/sage' -import BN from 'bn.js' - -import { StarAtlasPrograms } from '../../programs.js' -import { Player } from '../state/user-account.js' - -export const disbandedFleetToEscrowIx = ( - player: Player, - game: Game, - starbase: Starbase, - starbasePlayer: StarbasePlayer, - programs: StarAtlasPrograms, - shipEscrowIndex: number | null, - fleetShipInfoIndex: number, - disbandedFleet: PublicKey, - fleetShips: PublicKey, - shipKey: PublicKey, - shipAmount: BN, -): InstructionReturn => - DisbandedFleet.disbandedFleetToEscrow( - programs.sage, - player.signer, - player.profile.key, - player.profileFaction.key, - disbandedFleet, - fleetShips, - shipKey, - starbasePlayer.key, - starbase.key, - game.key, - game.data.gameState, - { - fleetShipInfoIndex, - keyIndex: 0, - shipAmount: shipAmount.toNumber(), - shipEscrowIndex, - }, - ) diff --git a/src/main/basedbot/lib/sage/ix/dock.ts b/src/main/basedbot/lib/sage/ix/dock.ts deleted file mode 100644 index 7175d4f9..00000000 --- a/src/main/basedbot/lib/sage/ix/dock.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { InstructionReturn } from '@staratlas/data-source' -import { Fleet, Game, Starbase, StarbasePlayer } from '@staratlas/sage' - -import { StarAtlasPrograms } from '../../programs.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' - -export const dockIx = ( - fleetInfo: FleetInfo, - player: Player, - game: Game, - starbase: Starbase, - starbasePlayer: StarbasePlayer, - programs: StarAtlasPrograms, -): InstructionReturn => - Fleet.idleToLoadingBay( - programs.sage, - player.signer, - player.profile.key, - player.profileFaction.key, - fleetInfo.fleet.key, - starbase.key, - starbasePlayer.key, - game.key, - game.data.gameState, - player.keyIndex, - ) diff --git a/src/main/basedbot/lib/sage/ix/exit-respawn.ts b/src/main/basedbot/lib/sage/ix/exit-respawn.ts deleted file mode 100644 index 57824e25..00000000 --- a/src/main/basedbot/lib/sage/ix/exit-respawn.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { InstructionReturn } from '@staratlas/data-source' -import { Fleet, Game, Starbase, StarbasePlayer } from '@staratlas/sage' - -import { StarAtlasPrograms } from '../../programs.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' - -export const exitRespawnIx = ( - fleetInfo: FleetInfo, - player: Player, - game: Game, - starbase: Starbase, - starbasePlayer: StarbasePlayer, - programs: StarAtlasPrograms, -): InstructionReturn => - Fleet.respawnToLoadingBay( - programs.sage, - player.signer, - player.profile.key, - player.profileFaction.key, - fleetInfo.fleet.key, - starbase.key, - starbasePlayer.key, - fleetInfo.fleet.data.cargoHold, - fleetInfo.fleet.data.fuelTank, - fleetInfo.fleet.data.ammoBank, - game.key, - game.data.gameState, - { - keyIndex: 0, - }, - ) diff --git a/src/main/basedbot/lib/sage/ix/fleet-state-handler.ts b/src/main/basedbot/lib/sage/ix/fleet-state-handler.ts deleted file mode 100644 index 07c9b32c..00000000 --- a/src/main/basedbot/lib/sage/ix/fleet-state-handler.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { InstructionReturn } from '@staratlas/data-source' -import { Fleet, Game } from '@staratlas/sage' - -import { StarAtlasPrograms } from '../../programs.js' -import { getCargoType } from '../state/cargo-types.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' -import { Mineable } from '../state/world-map.js' - -export const miningHandlerIx = ( - fleetInfo: FleetInfo, - player: Player, - mineable: Mineable, - foodTokenFrom: PublicKey, - ammoTokenFrom: PublicKey, - resourceTokenFrom: PublicKey, - resourceTokenTo: PublicKey, - programs: StarAtlasPrograms, - game: Game, -): InstructionReturn => - Fleet.asteroidMiningHandler( - programs.sage, - programs.cargo, - fleetInfo.fleet.key, - mineable.starbase.key, - mineable.mineItem.key, - mineable.resource.key, - mineable.planet.key, - fleetInfo.fleet.data.cargoHold, - fleetInfo.fleet.data.ammoBank, - player.foodCargoType.key, - player.ammoCargoType.key, - getCargoType(player.cargoTypes, game, mineable.mineItem.data.mint).key, - game.data.cargo.statsDefinition, - game.data.gameState, - game.key, - foodTokenFrom, - ammoTokenFrom, - resourceTokenFrom, - resourceTokenTo, - game.data.mints.food, - game.data.mints.ammo, - ) diff --git a/src/main/basedbot/lib/sage/ix/force-drop-fleet-cargo.ts b/src/main/basedbot/lib/sage/ix/force-drop-fleet-cargo.ts deleted file mode 100644 index 1f73aa09..00000000 --- a/src/main/basedbot/lib/sage/ix/force-drop-fleet-cargo.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { CargoStatsDefinition } from '@staratlas/cargo' -import { InstructionReturn } from '@staratlas/data-source' -import { Fleet, Game } from '@staratlas/sage' - -import { StarAtlasPrograms } from '../../programs.js' -import { FleetInfo } from '../state/user-fleets.js' - -export const forceDropFleetCargoIx = ( - fleetInfo: FleetInfo, - game: Game, - cargoStatsDefinition: CargoStatsDefinition, - cargoPod: PublicKey, - cargoType: PublicKey, - tokenFrom: PublicKey, - tokenMint: PublicKey, - programs: StarAtlasPrograms, -): InstructionReturn => - Fleet.forceDropFleetCargo( - programs.sage, - programs.cargo, - fleetInfo.fleet.key, - cargoPod, - cargoType, - cargoStatsDefinition.key, - game.key, - tokenFrom, - tokenMint, - ) diff --git a/src/main/basedbot/lib/sage/ix/idle-to-respawn.ts b/src/main/basedbot/lib/sage/ix/idle-to-respawn.ts deleted file mode 100644 index 3c165a69..00000000 --- a/src/main/basedbot/lib/sage/ix/idle-to-respawn.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { InstructionReturn } from '@staratlas/data-source' -import { Fleet, Game } from '@staratlas/sage' - -import { StarAtlasPrograms } from '../../programs.js' -import { Player } from '../state/user-account.js' - -export const idleToRespawnIx = ( - player: Player, - game: Game, - fleet: Fleet, - atlasTokenFrom: PublicKey, - programs: StarAtlasPrograms, -): InstructionReturn => - Fleet.idleToRespawn( - programs.sage, - player.signer, - player.profile.key, - player.profileFaction.key, - fleet.key, - atlasTokenFrom, - game.data.vaults.atlas, - game.data.gameState, - game.key, - { keyIndex: 0 }, - ) diff --git a/src/main/basedbot/lib/sage/ix/load-cargo.ts b/src/main/basedbot/lib/sage/ix/load-cargo.ts deleted file mode 100644 index e18c1bcd..00000000 --- a/src/main/basedbot/lib/sage/ix/load-cargo.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { InstructionReturn } from '@staratlas/data-source' -import { Fleet, Game, Starbase, StarbasePlayer } from '@staratlas/sage' -import BN from 'bn.js' - -import { StarAtlasPrograms } from '../../programs.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' - -export const loadCargoIx = ( - fleetInfo: FleetInfo, - player: Player, - game: Game, - starbase: Starbase, - starbasePlayer: StarbasePlayer, - cargoPodFrom: PublicKey, - cargoPodTo: PublicKey, - tokenFrom: PublicKey, - tokenTo: PublicKey, - tokenMint: PublicKey, - cargoType: PublicKey, - programs: StarAtlasPrograms, - amount: BN, -): InstructionReturn => - Fleet.depositCargoToFleet( - programs.sage, - programs.cargo, - player.signer, - player.profile.key, - player.profileFaction.key, - 'funder', - starbase.key, - starbasePlayer.key, - fleetInfo.fleet.key, - cargoPodFrom, - cargoPodTo, - cargoType, - game.data.cargo.statsDefinition, - tokenFrom, - tokenTo, - tokenMint, - game.key, - game.data.gameState, - { - amount, - keyIndex: 0, - }, - ) diff --git a/src/main/basedbot/lib/sage/ix/movement-subwarp-handler.ts b/src/main/basedbot/lib/sage/ix/movement-subwarp-handler.ts deleted file mode 100644 index 9c089f07..00000000 --- a/src/main/basedbot/lib/sage/ix/movement-subwarp-handler.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { InstructionReturn } from '@staratlas/data-source' -import { Fleet, Game } from '@staratlas/sage' - -import { StarAtlasPrograms } from '../../programs.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' - -export const movementSubwarpHandlerIx = ( - fleetInfo: FleetInfo, - player: Player, - game: Game, - fuelTokenAccount: PublicKey, - programs: StarAtlasPrograms, -): InstructionReturn => - Fleet.movementSubwarpHandler( - programs.sage, - programs.cargo, - programs.points, - player.profile.key, - fleetInfo.fleet.key, - fleetInfo.fleet.data.fuelTank, - player.fuelCargoType.key, - game.data.cargo.statsDefinition, - fuelTokenAccount, - game.data.mints.fuel, - player.xpAccounts.piloting.userPointsAccount, - player.xpAccounts.piloting.pointsCategory, - player.xpAccounts.piloting.pointsModifierAccount, - player.xpAccounts.councilRank.userPointsAccount, - player.xpAccounts.councilRank.pointsCategory, - player.xpAccounts.councilRank.pointsModifierAccount, - game.key, - ) diff --git a/src/main/basedbot/lib/sage/ix/start-mining.ts b/src/main/basedbot/lib/sage/ix/start-mining.ts deleted file mode 100644 index 30ebc794..00000000 --- a/src/main/basedbot/lib/sage/ix/start-mining.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { InstructionReturn } from '@staratlas/data-source' -import { Fleet, Game, StarbasePlayer } from '@staratlas/sage' - -import { StarAtlasPrograms } from '../../programs.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' -import { Mineable } from '../state/world-map.js' - -export const startMiningIx = ( - fleetInfo: FleetInfo, - player: Player, - game: Game, - mineable: Mineable, - starbasePlayer: StarbasePlayer, - fuelTokenAccount: PublicKey, - programs: StarAtlasPrograms, -): InstructionReturn => - Fleet.startMiningAsteroid( - programs.sage, - player.signer, - player.profile.key, - player.profileFaction.key, - fleetInfo.fleet.key, - mineable.starbase.key, - starbasePlayer.key, - mineable.mineItem.key, - mineable.resource.key, - mineable.planet.key, - game.data.gameState, - game.key, - fuelTokenAccount, - { - keyIndex: player.keyIndex, - }, - ) diff --git a/src/main/basedbot/lib/sage/ix/stop-mining.ts b/src/main/basedbot/lib/sage/ix/stop-mining.ts deleted file mode 100644 index bb0c4adc..00000000 --- a/src/main/basedbot/lib/sage/ix/stop-mining.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { InstructionReturn } from '@staratlas/data-source' -import { Fleet, Game } from '@staratlas/sage' - -import { StarAtlasPrograms } from '../../programs.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' -import { Mineable } from '../state/world-map.js' - -export const stopMiningIx = ( - fleetInfo: FleetInfo, - player: Player, - game: Game, - mineable: Mineable, - fuelTokenAccount: PublicKey, - programs: StarAtlasPrograms, -): InstructionReturn => - Fleet.stopMiningAsteroid( - programs.sage, - programs.cargo, - programs.points, - player.signer, - player.profile.key, - player.profileFaction.key, - fleetInfo.fleet.key, - mineable.mineItem.key, - mineable.resource.key, - mineable.planet.key, - fleetInfo.fleet.data.fuelTank, - player.fuelCargoType.key, - game.data.cargo.statsDefinition, - player.xpAccounts.mining.userPointsAccount, - player.xpAccounts.mining.pointsCategory, - player.xpAccounts.mining.pointsModifierAccount, - player.xpAccounts.piloting.userPointsAccount, - player.xpAccounts.piloting.pointsCategory, - player.xpAccounts.piloting.pointsModifierAccount, - player.xpAccounts.councilRank.userPointsAccount, - player.xpAccounts.councilRank.pointsCategory, - player.xpAccounts.councilRank.pointsModifierAccount, - game.data.gameState, - game.key, - fuelTokenAccount, - game.data.mints.fuel, - { - keyIndex: player.keyIndex, - }, - ) diff --git a/src/main/basedbot/lib/sage/ix/stop-subwarp.ts b/src/main/basedbot/lib/sage/ix/stop-subwarp.ts deleted file mode 100644 index 96738a3b..00000000 --- a/src/main/basedbot/lib/sage/ix/stop-subwarp.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { CargoStatsDefinition } from '@staratlas/cargo' -import { InstructionReturn } from '@staratlas/data-source' -import { Fleet, Game } from '@staratlas/sage' - -import { StarAtlasPrograms } from '../../programs.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' - -export const stopSubWarpIx = ( - fleetInfo: FleetInfo, - player: Player, - game: Game, - cargoStatsDefinition: CargoStatsDefinition, - fuelTokenAccount: PublicKey, - fuelTokenMint: PublicKey, - programs: StarAtlasPrograms, -): InstructionReturn => - Fleet.stopSubwarp( - programs.sage, - programs.cargo, - programs.points, - player.signer, - player.profile.key, - player.profileFaction.key, - fleetInfo.fleet.key, - fleetInfo.fleet.data.fuelTank, - player.fuelCargoType.key, - cargoStatsDefinition.key, - fuelTokenAccount, - fuelTokenMint, - player.xpAccounts.piloting.userPointsAccount, - player.xpAccounts.piloting.pointsCategory, - player.xpAccounts.piloting.pointsModifierAccount, - player.xpAccounts.councilRank.userPointsAccount, - player.xpAccounts.councilRank.pointsCategory, - player.xpAccounts.councilRank.pointsModifierAccount, - game.key, - game.data.gameState, - { - keyIndex: 0, - }, - ) diff --git a/src/main/basedbot/lib/sage/ix/stop-warp.ts b/src/main/basedbot/lib/sage/ix/stop-warp.ts deleted file mode 100644 index ec2e10a7..00000000 --- a/src/main/basedbot/lib/sage/ix/stop-warp.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { InstructionReturn } from '@staratlas/data-source' -import { Fleet, Game } from '@staratlas/sage' - -import { StarAtlasPrograms } from '../../programs.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' - -export const stopWarpIx = ( - fleetInfo: FleetInfo, - player: Player, - game: Game, - _fuelTokenAccount: PublicKey, - programs: StarAtlasPrograms, -): InstructionReturn => - Fleet.moveWarpHandler( - programs.sage, - programs.points, - player.profile.key, - fleetInfo.fleet.key, - player.xpAccounts.piloting.userPointsAccount, - player.xpAccounts.piloting.pointsCategory, - player.xpAccounts.piloting.pointsModifierAccount, - player.xpAccounts.councilRank.userPointsAccount, - player.xpAccounts.councilRank.pointsCategory, - player.xpAccounts.councilRank.pointsModifierAccount, - game.key, - ) diff --git a/src/main/basedbot/lib/sage/ix/subwarp.ts b/src/main/basedbot/lib/sage/ix/subwarp.ts deleted file mode 100644 index 470334de..00000000 --- a/src/main/basedbot/lib/sage/ix/subwarp.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { InstructionReturn } from '@staratlas/data-source' -import { Fleet, Game } from '@staratlas/sage' - -import { StarAtlasPrograms } from '../../programs.js' -import { Coordinates } from '../../util/coordinates.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' - -export const subWarpIx = ( - fleetInfo: FleetInfo, - coordinates: Coordinates, - _fuelTokenAccount: PublicKey, - player: Player, - game: Game, - programs: StarAtlasPrograms, -): InstructionReturn => - Fleet.startSubwarp( - programs.sage, - player.signer, - player.profile.key, - player.profileFaction.key, - fleetInfo.fleet.key, - game.key, - game.data.gameState, - { - toSector: coordinates.toArray(), - keyIndex: player.keyIndex, - }, - ) diff --git a/src/main/basedbot/lib/sage/ix/undock.ts b/src/main/basedbot/lib/sage/ix/undock.ts deleted file mode 100644 index b15074c6..00000000 --- a/src/main/basedbot/lib/sage/ix/undock.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { InstructionReturn } from '@staratlas/data-source' -import { Fleet, Game, Starbase, StarbasePlayer } from '@staratlas/sage' - -import { StarAtlasPrograms } from '../../programs.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' - -export const undockIx = ( - fleetInfo: FleetInfo, - player: Player, - game: Game, - starbase: Starbase, - starbasePlayer: StarbasePlayer, - programs: StarAtlasPrograms, -): InstructionReturn => - Fleet.loadingBayToIdle( - programs.sage, - player.signer, - player.profile.key, - player.profileFaction.key, - fleetInfo.fleet.key, - starbase.key, - starbasePlayer.key, - game.key, - game.data.gameState, - player.keyIndex, - ) diff --git a/src/main/basedbot/lib/sage/ix/unload-cargo.ts b/src/main/basedbot/lib/sage/ix/unload-cargo.ts deleted file mode 100644 index 727f6ffd..00000000 --- a/src/main/basedbot/lib/sage/ix/unload-cargo.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { InstructionReturn } from '@staratlas/data-source' -import { Fleet, Game, Starbase, StarbasePlayer } from '@staratlas/sage' -import BN from 'bn.js' - -import { StarAtlasPrograms } from '../../programs.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' - -export const unloadCargoIx = ( - fleetInfo: FleetInfo, - player: Player, - game: Game, - starbase: Starbase, - starbasePlayer: StarbasePlayer, - cargoPodFrom: PublicKey, - cargoPodTo: PublicKey, - tokenFrom: PublicKey, - tokenTo: PublicKey, - tokenMint: PublicKey, - cargoType: PublicKey, - programs: StarAtlasPrograms, - amount: BN, -): InstructionReturn => - Fleet.withdrawCargoFromFleet( - programs.sage, - programs.cargo, - player.signer, - 'funder', - player.profile.key, - player.profileFaction.key, - starbase.key, - starbasePlayer.key, - fleetInfo.fleet.key, - cargoPodFrom, - cargoPodTo, - cargoType, - game.data.cargo.statsDefinition, - tokenFrom, - tokenTo, - tokenMint, - game.key, - game.data.gameState, - { - amount, - keyIndex: 0, - }, - ) diff --git a/src/main/basedbot/lib/sage/ix/warp.ts b/src/main/basedbot/lib/sage/ix/warp.ts deleted file mode 100644 index cbccc326..00000000 --- a/src/main/basedbot/lib/sage/ix/warp.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { InstructionReturn } from '@staratlas/data-source' -import { Fleet, Game } from '@staratlas/sage' - -import { StarAtlasPrograms } from '../../programs.js' -import { Coordinates } from '../../util/coordinates.js' -import { Player } from '../state/user-account.js' -import { FleetInfo } from '../state/user-fleets.js' - -export const warpIx = ( - fleetInfo: FleetInfo, - coordinates: Coordinates, - fuelTokenAccount: PublicKey, - player: Player, - game: Game, - programs: StarAtlasPrograms, -): InstructionReturn => - Fleet.warpToCoordinate( - programs.sage, - player.signer, - player.profile.key, - player.profileFaction.key, - fleetInfo.fleet.key, - fleetInfo.fleet.data.fuelTank, - player.fuelCargoType.key, - game.data.cargo.statsDefinition, - fuelTokenAccount, - game.data.mints.fuel, - game.data.gameState, - game.key, - programs.cargo, - { - toSector: coordinates.toArray(), - keyIndex: player.keyIndex, - }, - ) diff --git a/src/main/basedbot/lib/sage/ix/withdraw-cargo.ts b/src/main/basedbot/lib/sage/ix/withdraw-cargo.ts deleted file mode 100644 index db70af72..00000000 --- a/src/main/basedbot/lib/sage/ix/withdraw-cargo.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { InstructionReturn } from '@staratlas/data-source' -import { Game, Starbase, StarbasePlayer } from '@staratlas/sage' -import BN from 'bn.js' - -import { StarAtlasPrograms } from '../../programs.js' -import { Player } from '../state/user-account.js' - -export const withdrawCargoIx = ( - player: Player, - game: Game, - starbase: Starbase, - starbasePlayer: StarbasePlayer, - cargoPodTo: PublicKey, - tokenFrom: PublicKey, - tokenTo: PublicKey, - cargoType: PublicKey, - mint: PublicKey, - programs: StarAtlasPrograms, - amount: BN, -): InstructionReturn => - StarbasePlayer.withdrawCargoFromGame( - programs.sage, - programs.cargo, - starbasePlayer.key, - player.signer, - player.publicKey, - player.profile.key, - player.profileFaction.key, - starbase.key, - cargoPodTo, - cargoType, - game.data.cargo.statsDefinition, - tokenFrom, - tokenTo, - mint, - game.key, - game.data.gameState, - { - amount, - keyIndex: 0, - }, - ) diff --git a/src/main/basedbot/lib/sage/ships.ts b/src/main/basedbot/lib/sage/ships.ts index a932b1bb..731b0c83 100644 --- a/src/main/basedbot/lib/sage/ships.ts +++ b/src/main/basedbot/lib/sage/ships.ts @@ -1,10 +1,14 @@ -import { PublicKey } from '@solana/web3.js' -import { Game, Ship } from '@staratlas/sage' +/** + * Ship data utilities for C4 + * TODO: Implement ship config loading for C4 + * In C4, ship configs may be accessed differently + */ + +import { address, type Address } from '@solana/kit' import superagent from 'superagent' + import { config } from '../../../../config/index.js' import { logger } from '../../../../logger.js' -import { programs } from '../programs.js' -import { getShipByMint } from './state/starbase-player.js' interface ApiItem { _id: string @@ -22,18 +26,6 @@ interface ApiItem { mint: string } -interface ShipData { - role: ShipRole - mint: PublicKey - make: ShipMake - model: ShipModel -} - -export interface ExtShipData extends ShipData { - ship: Ship - size: number -} - type ShipRole = | 'fighter' | 'multi-role' @@ -79,6 +71,19 @@ type ShipModel = | 'MiG' | 'Tree Arrow' +interface ShipData { + name: string + role: ShipRole + mint: Address + make: ShipMake + model: ShipModel | 'Unknown' +} + +export interface ExtShipData extends ShipData { + configId?: number + size: number +} + const problematicMints = new Set([ 'RNGRjeGyFeyFT4k5aTJXKZukVx3GbG215fcSQJxg64G', 'phi4PYgmxeTMLLpGkU87T16VUZ6AjWZESkfT1JGJ635', @@ -92,13 +97,13 @@ const parseShips = (items: ApiItem[]): ShipData[] => { .map((item) => ({ name: item.name, role: item.attributes.spec as ShipRole, - mint: new PublicKey(item.mint), + mint: address(item.mint), make: item.attributes.make as ShipMake, model: (item.attributes.model as ShipModel) || 'Unknown', })) } -export const fetchGalaxyData = async (baseUrl: string) => { +export const fetchGalaxyData = async (baseUrl: string): Promise => { logger.info('Fetching galaxy data') const res = await superagent.get(`${baseUrl}/items`) @@ -107,22 +112,15 @@ export const fetchGalaxyData = async (baseUrl: string) => { return res.body } -export const getShipData = async (game: Game): Promise> => { +/** + * Get ship data from API + * TODO: Enhance with on-chain ship config data for C4 + */ +export const getShipData = async (): Promise => { const shipData = parseShips(await fetchGalaxyData(config.app.airdropUrl)) - return ( - await Promise.all( - shipData.map(async (value): Promise => { - const ship = await getShipByMint(value.mint, game, programs) - const size = ship.data.sizeClass ** 2 - return { - make: value.make, - model: value.model, - role: value.role, - mint: value.mint, - ship: ship, - size, - } - }), - ) - ).sort((a, b) => a.mint.toBase58().localeCompare(b.mint.toBase58())) + + return shipData.map((ship) => ({ + ...ship, + size: 1, // TODO: Get actual size from ship config + })) } diff --git a/src/main/basedbot/lib/sage/state/cargo-stats-definition.ts b/src/main/basedbot/lib/sage/state/cargo-stats-definition.ts deleted file mode 100644 index 25bde93e..00000000 --- a/src/main/basedbot/lib/sage/state/cargo-stats-definition.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { CargoStatsDefinition } from '@staratlas/cargo' -import { readAllFromRPC } from '@staratlas/data-source' - -import { connection } from '../../../../../service/sol/index.js' -import { programs } from '../../programs.js' - -export const getCargoStatsDefinition = async ( - statsDefinitionKey: PublicKey, -): Promise => { - const cargoTypesAccountData = await readAllFromRPC( - connection, - programs.cargo, - CargoStatsDefinition, - ) - - return cargoTypesAccountData - .filter((f) => f.type === 'ok' && 'data' in f) - .map((f) => (f as any).data) - .find((f) => f.key.toString() === statsDefinitionKey.toString()) -} diff --git a/src/main/basedbot/lib/sage/state/cargo-types.ts b/src/main/basedbot/lib/sage/state/cargo-types.ts deleted file mode 100644 index d7de2368..00000000 --- a/src/main/basedbot/lib/sage/state/cargo-types.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { PublicKey } from '@solana/web3.js' -import { CargoType } from '@staratlas/cargo' -import { readAllFromRPC } from '@staratlas/data-source' -import { Game } from '@staratlas/sage' - -import { connection } from '../../../../../service/sol/index.js' -import { programs } from '../../programs.js' - -export const getCargoTypes = async (): Promise> => { - const cargoTypesAccountData = await readAllFromRPC( - connection, - programs.cargo, - CargoType, - ) - - return cargoTypesAccountData - .filter((f) => f.type === 'ok' && 'data' in f) - .map((f) => (f as any).data) -} - -export const getCargoType = ( - cargoTypes: Array, - game: Game, - mint: PublicKey, -): CargoType => { - const cargoType = cargoTypes.find( - (ct) => - game.data.cargo.statsDefinition.equals(ct.data.statsDefinition) && - mint.equals(ct.data.mint), - ) - - if (!cargoType) { - throw new Error(`Cargo type not found for mint ${mint}.`) - } - - return cargoType -} diff --git a/src/main/basedbot/lib/sage/state/fleet-cargo.ts b/src/main/basedbot/lib/sage/state/fleet-cargo.ts index 7eccc3e4..fe12eca0 100644 --- a/src/main/basedbot/lib/sage/state/fleet-cargo.ts +++ b/src/main/basedbot/lib/sage/state/fleet-cargo.ts @@ -1,137 +1,79 @@ -import { - getAssociatedTokenAddressSync, - TOKEN_PROGRAM_ID, -} from '@solana/spl-token' -import { PublicKey } from '@solana/web3.js' -import { - createAssociatedTokenAccountIdempotent, - ixReturnsToIxs, -} from '@staratlas/data-source' -import { Fleet } from '@staratlas/sage' -import BN from 'bn.js' -import bs58 from 'bs58' +/** + * Fleet cargo utilities for C4 + * In C4, cargo amounts are directly on the Fleet struct + */ -import { logger } from '../../../../../logger.js' -import { connection } from '../../../../../service/sol/index.js' -import { sendAndConfirmInstructions } from '../../../../../service/sol/send-and-confirm-tx.js' -import { keyPair } from '../../../../../service/wallet' +import type { Fleet } from '@staratlas/dev-sage' -import { sageGame } from './game.js' -import { Player } from './user-account.js' +import type { AccountWithKey } from '../../types.js' -type PartialTokenAccount = { - amount: number | null - tokenAccount: string - mint: string - owner: string - delegate: string | null - delegatedAmount: number -} - -const getTokenAccountForKey = async ( - key: PublicKey, -): Promise => { - const tokenAccounts = await connection.getTokenAccountsByOwner(key, { - programId: TOKEN_PROGRAM_ID, - }) - - const result: PartialTokenAccount[] = [] - - for (const tokenAccount of tokenAccounts.value) { - const accountKey = tokenAccount.pubkey.toBase58() - const accountData = tokenAccount.account.data - - result.push({ - amount: new BN(accountData.subarray(64, 72), 'le').toNumber(), - mint: bs58.encode(accountData.subarray(0, 32)), - owner: bs58.encode(accountData.subarray(32, 64)), - tokenAccount: accountKey, - delegate: bs58.encode(accountData.subarray(76, 108)), - delegatedAmount: new BN( - accountData.slice(121, 129), - 'le', - ).toNumber(), - }) - } - - return result +export type FleetCargo = { + fuel: bigint + ammo: bigint + /** Map of cargoId -> amount for cargo hold items */ + cargo: Map + /** Total storage cost of cargo hold */ + storageCost: bigint } -const getBalance = async ( - mint: PublicKey, - bank: PublicKey, - player: Player, -): Promise => { - const tokenAccount = getAssociatedTokenAddressSync(mint, bank, true) - - try { - const balance = await connection.getTokenAccountBalance(tokenAccount) - - return balance.value.uiAmount ?? 0 - } catch (e) { - if ((e as Error).message.includes('could not find account')) { - logger.debug( - `No balance found for ${mint.toBase58()} at ${bank.toBase58()} creating new account`, - ) - const fleetFuelTokenResult = createAssociatedTokenAccountIdempotent( - mint, - bank, - true, - ) - - await sendAndConfirmInstructions([keyPair])( - await ixReturnsToIxs( - fleetFuelTokenResult.instructions, - player.signer, - ), - ) - - return 0 +/** + * Get cargo levels directly from Fleet struct (C4) + * No RPC calls needed - data is on the Fleet account + */ +export const getFleetCargoBalance = ( + fleet: AccountWithKey, +): FleetCargo => { + const cargoMap = new Map() + + // In C4, cargoHold.amounts is a Map + const amounts = fleet.cargoHold?.amounts + if (amounts) { + for (const [cargoId, amount] of amounts.entries()) { + // Handle both array keys [number] and number keys + const id = Array.isArray(cargoId) + ? (cargoId[0] as number) + : (cargoId as number) + cargoMap.set(id, amount as bigint) } } - return 0 + return { + fuel: (fleet.fuelTank?.amount as bigint) ?? 0n, + ammo: (fleet.ammoBank?.amount as bigint) ?? 0n, + cargo: cargoMap, + storageCost: (fleet.cargoHold?.storageCost as bigint) ?? 0n, + } } -const getFleetCargoBalances = async ( - fleet: Fleet, -): Promise> => { - const cargoHoldBalances = await getTokenAccountForKey( - new PublicKey(fleet.data.cargoHold), - ) - - return new Map( - cargoHoldBalances.map((tokenAccount) => [ - tokenAccount.mint, - tokenAccount.delegatedAmount, - ]), - ) +/** + * Get cargo amount by cargoId from cargo hold + */ +export const getCargoAmount = (cargo: FleetCargo, cargoId: number): bigint => { + return cargo.cargo.get(cargoId) ?? 0n } -export type FleetCargo = { - ammo: number - cargo: Map - food: number - fuel: number - toolkit: number +/** + * Get fuel/ammo cargo IDs from fleet + */ +export const getFleetCargoIds = ( + fleet: AccountWithKey, +): { fuelCargoId: number; ammoCargoId: number } => { + return { + fuelCargoId: (fleet.fuelTank?.cargoId as number) ?? 0, + ammoCargoId: (fleet.ammoBank?.cargoId as number) ?? 0, + } } -export const getFleetCargoBalance = async ( - fleet: Fleet, - player: Player, -): Promise => { - const game = await sageGame() - const [ammo, fuel, cargo] = await Promise.all([ - getBalance(game.data.mints.ammo, fleet.data.ammoBank, player), - getBalance(game.data.mints.fuel, fleet.data.fuelTank, player), - getFleetCargoBalances(fleet), - ]) - +/** + * Get cargo capacity stats from fleet + */ +export const getFleetCargoCapacity = ( + fleet: AccountWithKey, +): { fuelCapacity: bigint; ammoCapacity: bigint; cargoCapacity: bigint } => { + const stats = fleet.stats?.cargoStats as Record | undefined return { - ammo, - cargo, - food: cargo.get(game.data.mints.food.toBase58()) ?? 0, - fuel, - toolkit: cargo.get(game.data.mints.repairKit.toBase58()) ?? 0, + fuelCapacity: (stats?.fuelCapacity as bigint) ?? 0n, + ammoCapacity: (stats?.ammoCapacity as bigint) ?? 0n, + cargoCapacity: (stats?.cargoCapacity as bigint) ?? 0n, } } diff --git a/src/main/basedbot/lib/sage/state/game-resources.ts b/src/main/basedbot/lib/sage/state/game-resources.ts new file mode 100644 index 00000000..85c61801 --- /dev/null +++ b/src/main/basedbot/lib/sage/state/game-resources.ts @@ -0,0 +1,104 @@ +/** + * Game-based R4 resource mints + * + * Gets fuel, ammo, food, and tool (repairKit) mint addresses from the game's + * resources and cargo definitions. + */ +import type { Address } from '@solana/kit' + +import { logger } from '../../../../../logger.js' +import type { AccountWithKey, Fleet, Game } from '../../types.js' +import { getCargoMint } from '../act/deposit-cargo.js' + +export type GameResources = { + fuel: Address | undefined + ammo: Address | undefined + food: Address | undefined + tool: Address | undefined +} + +// Cached resources from game +let cachedResources: GameResources | null = null + +/** + * Get R4 resource mints from game data + * + * Uses game.resources which contains cargo IDs for fuel, ammo, food, repairKit. + * Then looks up the mint address for each cargo ID. + */ +export function getGameResources( + game: AccountWithKey, + _fleet?: AccountWithKey, +): GameResources { + // Use cached if available + if (cachedResources) { + return cachedResources + } + + // Get cargo IDs from game.resources + const fuelCargoId = game.resources?.fuel ?? 0 + const ammoCargoId = game.resources?.ammo ?? 1 + const foodCargoId = game.resources?.food ?? 2 + const toolCargoId = game.resources?.repairKit ?? 3 + + const resources: GameResources = { + fuel: getCargoMint(game, fuelCargoId), + ammo: getCargoMint(game, ammoCargoId), + food: getCargoMint(game, foodCargoId), + tool: getCargoMint(game, toolCargoId), + } + + // Log for debugging + logger.info( + `Game R4 resources: ` + + `fuel=${resources.fuel?.slice(0, 8) ?? 'none'} (id=${fuelCargoId}), ` + + `ammo=${resources.ammo?.slice(0, 8) ?? 'none'} (id=${ammoCargoId}), ` + + `food=${resources.food?.slice(0, 8) ?? 'none'} (id=${foodCargoId}), ` + + `tool=${resources.tool?.slice(0, 8) ?? 'none'} (id=${toolCargoId})`, + ) + + cachedResources = resources + return resources +} + +/** + * Clear cached resources (useful for testing or game changes) + */ +export function clearGameResourcesCache(): void { + cachedResources = null +} + +/** + * Get all R4 mints as a set (for transport config) + */ +export function getR4MintSet( + game: AccountWithKey, + fleet?: AccountWithKey, +): Set
{ + const resources = getGameResources(game, fleet) + const mints = new Set
() + + if (resources.fuel) mints.add(resources.fuel) + if (resources.ammo) mints.add(resources.ammo) + if (resources.food) mints.add(resources.food) + if (resources.tool) mints.add(resources.tool) + + return mints +} + +/** + * Check if a mint is an R4 resource + */ +export function isR4Mint( + mint: Address, + game: AccountWithKey, + fleet?: AccountWithKey, +): boolean { + const resources = getGameResources(game, fleet) + return ( + mint === resources.fuel || + mint === resources.ammo || + mint === resources.food || + mint === resources.tool + ) +} diff --git a/src/main/basedbot/lib/sage/state/game.ts b/src/main/basedbot/lib/sage/state/game.ts index dc0de0bf..775edee6 100644 --- a/src/main/basedbot/lib/sage/state/game.ts +++ b/src/main/basedbot/lib/sage/state/game.ts @@ -1,21 +1,12 @@ -import { readAllFromRPC } from '@staratlas/data-source' -import { Game } from '@staratlas/sage' +import type { Game } from '@staratlas/dev-sage' -import { connection } from '../../../../../service/sol/index.js' -import { programs } from '../../programs.js' +import { fetchGame } from '../../account-fetcher.js' +import type { AccountWithKey } from '../../types.js' -export const sageGame = async (): Promise => { - const [game] = await readAllFromRPC( - connection, - programs.sage, - Game, - 'processed', - [], - ) - - if (game.type === 'error') { - throw new Error('Error reading game account') - } - - return game.data +/** + * Fetch the SAGE Game account + * Note: This is a singleton - there's only one Game account per program deployment + */ +export const sageGame = async (): Promise> => { + return fetchGame() } diff --git a/src/main/basedbot/lib/sage/state/get-fleet-ships.ts b/src/main/basedbot/lib/sage/state/get-fleet-ships.ts deleted file mode 100644 index 42f32df0..00000000 --- a/src/main/basedbot/lib/sage/state/get-fleet-ships.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { readAllFromRPC } from '@staratlas/data-source' -import { Fleet, FleetShips } from '@staratlas/sage' - -import { connection } from '../../../../../service/sol/index.js' -import { programs } from '../../programs.js' - -export const getFleetShips = async ( - fleet: Fleet, -): Promise> => { - const resources = await readAllFromRPC( - connection, - programs.sage, - FleetShips, - 'processed', - [ - { - memcmp: { - offset: 8 + 1, - bytes: fleet.key.toBase58(), - }, - }, - ], - ) - - return resources - .filter((p) => p.type === 'ok' && 'data' in p) - .map((p) => (p as any).data) -} diff --git a/src/main/basedbot/lib/sage/state/mine-items.ts b/src/main/basedbot/lib/sage/state/mine-items.ts index 671cb04f..836e0b87 100644 --- a/src/main/basedbot/lib/sage/state/mine-items.ts +++ b/src/main/basedbot/lib/sage/state/mine-items.ts @@ -1,26 +1,50 @@ -import { readAllFromRPC } from '@staratlas/data-source' -import { Game, MineItem } from '@staratlas/sage' - -import { connection } from '../../../../../service/sol/index.js' -import { programs } from '../../programs.js' - -export const getMineItems = async (game: Game): Promise> => { - const mineItems = await readAllFromRPC( - connection, - programs.sage, - MineItem, - 'processed', - [ - { - memcmp: { - offset: 8 + 1, - bytes: game.key.toBase58(), - }, - }, - ], - ) - - return mineItems - .filter((p) => p.type === 'ok' && 'data' in p) - .map((p) => (p as any).data) +/** + * Mine items utilities for C4 + * In C4, there are no separate MineItem accounts + * Mining is done directly on CelestialBody (asteroids) + * Resource type is identified by cargoId + */ + +import type { CelestialBody } from '@staratlas/dev-sage' + +import { fetchAllAccounts } from '../../account-fetcher.js' +import type { AccountWithKey } from '../../types.js' + +import { type ResourceInfo, getResourceFromCelestialBody } from './resources.js' + +/** + * Get all minable asteroids + */ +export const getMinableAsteroids = async (): Promise => { + const celestialBodies = await fetchAllAccounts('CelestialBody') + + return celestialBodies + .map((cb) => getResourceFromCelestialBody(cb)) + .filter((r): r is ResourceInfo => r !== null) +} + +/** + * Get minable asteroids at a specific star system + */ +export const getMinableAsteroidsBySystem = async ( + systemKey: string, +): Promise => { + const celestialBodies = await fetchAllAccounts('CelestialBody') + + return celestialBodies + .filter((cb) => cb.system === systemKey) + .map((cb) => getResourceFromCelestialBody(cb)) + .filter((r): r is ResourceInfo => r !== null) +} + +/** + * Find asteroid by cargoId (resource type) + */ +export const findAsteroidByCargoId = async ( + systemKey: string, + cargoId: number, +): Promise | null> => { + const resources = await getMinableAsteroidsBySystem(systemKey) + const match = resources.find((r) => r.cargoId === cargoId) + return match?.celestialBody ?? null } diff --git a/src/main/basedbot/lib/sage/state/planet-by-key.ts b/src/main/basedbot/lib/sage/state/planet-by-key.ts index 9748b71d..d0ef0e38 100644 --- a/src/main/basedbot/lib/sage/state/planet-by-key.ts +++ b/src/main/basedbot/lib/sage/state/planet-by-key.ts @@ -1,26 +1,27 @@ -import { PublicKey } from '@solana/web3.js' -import { readFromRPC } from '@staratlas/data-source' -import { Planet } from '@staratlas/sage' +/** + * CelestialBody by key utility for C4 + */ -import { connection } from '../../../../../service/sol/index.js' -import { programs } from '../../programs.js' +import type { Address } from '@solana/kit' +import type { CelestialBody } from '@staratlas/dev-sage' -export const planetByKey = async (key: PublicKey): Promise => { - const planet = await readFromRPC( - connection, - programs.sage, - key, - Planet, - 'processed', - ) +import { fetchAccount } from '../../account-fetcher.js' +import type { AccountWithKey } from '../../types.js' - if (!planet) { - throw new Error('no planet found') - } +/** + * Get a celestial body by address + */ +export const celestialBodyByKey = async ( + key: Address, +): Promise> => { + const celestialBody = await fetchAccount('CelestialBody', key) - if (planet.type === 'error') { - throw new Error('Error reading planet account') + if (!celestialBody) { + throw new Error(`CelestialBody not found: ${key}`) } - return planet.data + return celestialBody } + +// Legacy alias +export const planetByKey = celestialBodyByKey diff --git a/src/main/basedbot/lib/sage/state/planets-by-coordinates.ts b/src/main/basedbot/lib/sage/state/planets-by-coordinates.ts index 7d32a265..adb9b305 100644 --- a/src/main/basedbot/lib/sage/state/planets-by-coordinates.ts +++ b/src/main/basedbot/lib/sage/state/planets-by-coordinates.ts @@ -1,35 +1,40 @@ -import { readAllFromRPC } from '@staratlas/data-source' -import { Planet } from '@staratlas/sage' +/** + * CelestialBodies by system coordinates for C4 + * In C4, celestial bodies are associated with a StarSystem, not coordinates + */ -import { connection } from '../../../../../service/sol/index.js' -import { programs } from '../../programs.js' -import { Coordinates } from '../../util/coordinates.js' +import type { CelestialBody } from '@staratlas/dev-sage' -export const planetsByCoordinates = async ( +import { fetchAllAccounts } from '../../account-fetcher.js' +import type { AccountWithKey } from '../../types.js' +import type { Coordinates } from '../../util/coordinates.js' + +// C4 coordinate with fixed-point representation +type C4Coord = { toNumber: () => number } + +/** + * Get celestial bodies at a star system matching coordinates + */ +export const celestialBodiesByCoordinates = async ( coordinates: Coordinates, -): Promise> => { - const planets = await readAllFromRPC( - connection, - programs.sage, - Planet, - 'processed', - [ - { - memcmp: { - offset: 105, - bytes: coordinates.xB58, - }, - }, - { - memcmp: { - offset: 113, - bytes: coordinates.yB58, - }, - }, - ], - ) +): Promise[]> => { + // First find the star system at these coordinates + const systems = await fetchAllAccounts('StarSystem') + const system = systems.find((s) => { + const sysCoords = s.coordinates as unknown as [C4Coord, C4Coord] + const sysX = Math.round(sysCoords[0].toNumber()) + const sysY = Math.round(sysCoords[1].toNumber()) + return sysX === coordinates.x && sysY === coordinates.y + }) - return planets - .filter((p) => p.type === 'ok' && 'data' in p) - .map((p) => (p as any).data) + if (!system) { + return [] + } + + // Then get all celestial bodies for that system + const celestialBodies = await fetchAllAccounts('CelestialBody') + return celestialBodies.filter((cb) => cb.system === system.key) } + +// Legacy alias +export const planetsByCoordinates = celestialBodiesByCoordinates diff --git a/src/main/basedbot/lib/sage/state/planets.ts b/src/main/basedbot/lib/sage/state/planets.ts index f8d301ba..5c14c722 100644 --- a/src/main/basedbot/lib/sage/state/planets.ts +++ b/src/main/basedbot/lib/sage/state/planets.ts @@ -1,26 +1,31 @@ -import { readAllFromRPC } from '@staratlas/data-source' -import { Game, Planet } from '@staratlas/sage' +/** + * CelestialBody (planets/asteroids) utilities for C4 + * In C4, Planet is renamed to CelestialBody + */ -import { connection } from '../../../../../service/sol/index.js' -import { programs } from '../../programs.js' +import type { CelestialBody } from '@staratlas/dev-sage' -export const getPlanets = async (game: Game): Promise> => { - const planets = await readAllFromRPC( - connection, - programs.sage, - Planet, - 'processed', - [ - { - memcmp: { - offset: 8 + 1 + 64, - bytes: game.key.toBase58(), - }, - }, - ], - ) +import { fetchAllAccounts } from '../../account-fetcher.js' +import type { AccountWithKey } from '../../types.js' - return planets - .filter((p) => p.type === 'ok' && 'data' in p) - .map((p) => (p as any).data) +/** + * Get all celestial bodies + */ +export const getCelestialBodies = async (): Promise< + AccountWithKey[] +> => { + return fetchAllAccounts('CelestialBody') } + +/** + * Get celestial bodies for a specific star system + */ +export const getCelestialBodiesBySystem = async ( + systemKey: string, +): Promise[]> => { + const all = await fetchAllAccounts('CelestialBody') + return all.filter((cb) => cb.system === systemKey) +} + +// Legacy alias +export const getPlanets = getCelestialBodies diff --git a/src/main/basedbot/lib/sage/state/resources.ts b/src/main/basedbot/lib/sage/state/resources.ts index 3f547388..624f8874 100644 --- a/src/main/basedbot/lib/sage/state/resources.ts +++ b/src/main/basedbot/lib/sage/state/resources.ts @@ -1,26 +1,170 @@ -import { readAllFromRPC } from '@staratlas/data-source' -import { Game, Resource } from '@staratlas/sage' - -import { connection } from '../../../../../service/sol/index.js' -import { programs } from '../../programs.js' - -export const getResources = async (game: Game): Promise> => { - const resources = await readAllFromRPC( - connection, - programs.sage, - Resource, - 'processed', - [ - { - memcmp: { - offset: 8 + 1, - bytes: game.key.toBase58(), - }, - }, - ], - ) - - return resources - .filter((p) => p.type === 'ok' && 'data' in p) - .map((p) => (p as any).data) +/** + * Resource utilities for C4 + * In C4, asteroids have a Map of resources (cargoId -> AsteroidResourceState) + * Fleets can mine up to 8 resources simultaneously from Asteroid celestial bodies + */ + +import type { CelestialBody } from '@staratlas/dev-sage' + +import { fetchAllAccounts } from '../../account-fetcher.js' +import type { AccountWithKey } from '../../types.js' + +/** + * Individual resource state on an asteroid + */ +export type AsteroidResource = { + cargoId: number + richness: number + amountMined: bigint + miners: bigint +} + +/** + * Resource info extracted from CelestialBody (asteroid) + * In C4, asteroids can have multiple resources (up to 8 mineable at once) + */ +export type ResourceInfo = { + celestialBody: AccountWithKey + /** All resources available on this asteroid */ + resources: AsteroidResource[] + /** Legacy: first resource cargoId for backwards compatibility */ + cargoId: number + /** Legacy: first resource richness for backwards compatibility */ + richness: number + /** Legacy: first resource amountMined for backwards compatibility */ + amountMined: bigint +} + +/** + * Extract U16F48 fixed-point value to number + */ +function extractFixedPoint(value: unknown): number { + if (value === null || value === undefined) return 0 + if (typeof value === 'number') return value + if (typeof value === 'bigint') return Number(value) + if (typeof value === 'object' && 'toNumber' in value) { + return (value as { toNumber: () => number }).toNumber() + } + if (typeof value === 'object' && 'raw' in value) { + // U16F48 has 48 fractional bits + const raw = (value as { raw: bigint }).raw + return Number(raw) / 2 ** 48 + } + return 0 +} + +// Debug flag - set to true to enable verbose logging +const DEBUG_RESOURCES = false + +/** + * Extract resource info from a CelestialBody (asteroid) + * Only Asteroid type celestial bodies can be mined by fleets + */ +export const getResourceFromCelestialBody = ( + celestialBody: AccountWithKey, +): ResourceInfo | null => { + const cbType = celestialBody.celestialBodyType as unknown as { + __kind: string + fields?: unknown[] + } + + // Only Asteroid type celestial bodies can be mined by fleets + if (cbType.__kind !== 'Asteroid' || !cbType.fields?.[0]) { + return null + } + + const asteroidData = cbType.fields[0] as { + resources?: Map | unknown + } + + // C4: resources is a Map + const resourcesMap = asteroidData.resources + + if (DEBUG_RESOURCES) { + console.log( + `Asteroid ${celestialBody.key}: resourcesMap type=${typeof resourcesMap}, ` + + `isMap=${resourcesMap instanceof Map}, ` + + `keys=${resourcesMap instanceof Map ? Array.from(resourcesMap.keys()) : Object.keys(resourcesMap ?? {})}`, + ) + } + + if (!resourcesMap) { + return null + } + + const resources: AsteroidResource[] = [] + + // Handle Map type + if (resourcesMap instanceof Map) { + for (const [cargoId, state] of resourcesMap) { + const resourceState = state as { + richness?: unknown + amountMined?: bigint + miners?: bigint + } + resources.push({ + cargoId, + richness: extractFixedPoint(resourceState.richness), + amountMined: resourceState.amountMined ?? 0n, + miners: resourceState.miners ?? 0n, + }) + } + } else if (typeof resourcesMap === 'object') { + // Fallback: try treating it as an object/record + const entries = Object.entries(resourcesMap as Record) + for (const [key, state] of entries) { + const cargoId = Number(key) + if (isNaN(cargoId)) continue + const resourceState = state as { + richness?: unknown + amountMined?: bigint + miners?: bigint + } + resources.push({ + cargoId, + richness: extractFixedPoint(resourceState.richness), + amountMined: resourceState.amountMined ?? 0n, + miners: resourceState.miners ?? 0n, + }) + } + } + + if (resources.length === 0) { + return null + } + + // Return with first resource for backwards compatibility + const firstResource = resources[0] + return { + celestialBody, + resources, + cargoId: firstResource.cargoId, + richness: firstResource.richness, + amountMined: firstResource.amountMined, + } +} + +/** + * Get all resources (asteroids with resource data) + */ +export const getResources = async (): Promise => { + const celestialBodies = await fetchAllAccounts('CelestialBody') + + return celestialBodies + .map((cb) => getResourceFromCelestialBody(cb)) + .filter((r): r is ResourceInfo => r !== null) +} + +/** + * Get resources at a specific star system + */ +export const getResourcesBySystem = async ( + systemKey: string, +): Promise => { + const celestialBodies = await fetchAllAccounts('CelestialBody') + + return celestialBodies + .filter((cb) => cb.system === systemKey) + .map((cb) => getResourceFromCelestialBody(cb)) + .filter((r): r is ResourceInfo => r !== null) } diff --git a/src/main/basedbot/lib/sage/state/settle-fleet.ts b/src/main/basedbot/lib/sage/state/settle-fleet.ts index d78832b5..9b2a8c68 100644 --- a/src/main/basedbot/lib/sage/state/settle-fleet.ts +++ b/src/main/basedbot/lib/sage/state/settle-fleet.ts @@ -1,24 +1,47 @@ -import { Game } from '@staratlas/sage' +/** + * Fleet settlement utilities for C4 + * Handles completing in-progress fleet actions + */ + +import type { Game, RegionTracker } from '@staratlas/dev-sage' import { now } from '../../../../../dayjs.js' import { logger } from '../../../../../logger.js' import { endMove } from '../act/end-move.js' -import { exitRespawn } from '../act/exit-respawn.js' +import { stopSubwarp } from '../act/stop-subwarp.js' +import type { AccountWithKey } from '../../types.js' -import { Player } from './user-account.js' -import { FleetInfo } from './user-fleets.js' +import type { Player } from './user-account.js' +import type { FleetInfo } from './user-fleets.js' +/** + * Settle (complete) any in-progress fleet actions + */ export const settleFleet = async ( fleetInfo: FleetInfo, player: Player, - game: Game, + game: AccountWithKey, + regionTracker: AccountWithKey, ): Promise => { - switch (fleetInfo.fleetState.type) { + switch (fleetInfo.fleetState.kind) { case 'MoveWarp': { - const { warpFinish } = fleetInfo.fleetState.data + const { arrivalTime } = fleetInfo.fleetState.data - if (warpFinish.isBefore(now())) { - await endMove(fleetInfo, player, game) + if (arrivalTime.isBefore(now())) { + logger.info(`${fleetInfo.fleetName} warp complete, settling...`) + try { + await endMove({ + game, + fleet: fleetInfo.fleet, + signer: player.signer, + character: player.character, + }) + } catch (error) { + // Log and continue - fleet may need manual settlement via game UI + logger.warn( + `Failed to settle warp for ${fleetInfo.fleetName}: ${error instanceof Error ? error.message : String(error)}`, + ) + } } break } @@ -26,7 +49,25 @@ export const settleFleet = async ( const { arrivalTime } = fleetInfo.fleetState.data if (arrivalTime.isBefore(now())) { - await endMove(fleetInfo, player, game) + logger.info( + `${fleetInfo.fleetName} subwarp complete, settling...`, + ) + try { + // Use stopSubwarp for MoveSubwarp - requires profile validation + await stopSubwarp({ + game, + fleet: fleetInfo.fleet, + signer: player.signer, + character: player.character, + regionTracker, + keyIndex: player.keyIndex, + }) + } catch (error) { + // Log and continue - fleet may need manual settlement via game UI + logger.warn( + `Failed to settle subwarp for ${fleetInfo.fleetName}: ${error instanceof Error ? error.message : String(error)}`, + ) + } } break } @@ -34,13 +75,16 @@ export const settleFleet = async ( const { ETA } = fleetInfo.fleetState.data if (ETA.isBefore(now())) { - await exitRespawn(fleetInfo, player.homeStarbase, player, game) + // TODO: Implement exitRespawn action + logger.info( + `${fleetInfo.fleetName} respawn complete, needs settlement`, + ) } break } default: logger.debug( - `${fleetInfo.fleetName} is ${fleetInfo.fleetState.type}`, + `${fleetInfo.fleetName} is ${fleetInfo.fleetState.kind}`, ) } } diff --git a/src/main/basedbot/lib/sage/state/show-fleet-cargo-info.ts b/src/main/basedbot/lib/sage/state/show-fleet-cargo-info.ts index a7a86130..95792a2b 100644 --- a/src/main/basedbot/lib/sage/state/show-fleet-cargo-info.ts +++ b/src/main/basedbot/lib/sage/state/show-fleet-cargo-info.ts @@ -1,16 +1,23 @@ +/** + * Fleet cargo info display utilities for C4 + */ + import { logger } from '../../../../../logger.js' -import { FleetInfo } from './user-fleets.js' +import type { FleetInfo } from './user-fleets.js' export const showFleetCargoInfo = (fleetInfo: FleetInfo): void => { - const { ammo, cargo, food, fuel, toolkit } = fleetInfo.cargoLevels + const { fuel, ammo, cargo, storageCost } = fleetInfo.cargoLevels logger.info(`${fleetInfo.fleetName} cargo levels:`) - logger.info(`Ammo: ${ammo}`) - logger.info(`Food: ${food}`) - logger.info(`Fuel: ${fuel}`) - logger.info(`Toolkit: ${toolkit}`) - for (const [mint, amount] of cargo.entries()) { - logger.info(`${mint}: ${amount}`) + logger.info(` Fuel: ${fuel}`) + logger.info(` Ammo: ${ammo}`) + logger.info(` Storage Cost: ${storageCost}`) + + if (cargo.size > 0) { + logger.info(' Cargo Hold:') + for (const [cargoId, amount] of cargo.entries()) { + logger.info(` CargoId ${cargoId}: ${amount}`) + } } } diff --git a/src/main/basedbot/lib/sage/state/show-fleet-info.ts b/src/main/basedbot/lib/sage/state/show-fleet-info.ts index d35c2c23..fd463ca0 100644 --- a/src/main/basedbot/lib/sage/state/show-fleet-info.ts +++ b/src/main/basedbot/lib/sage/state/show-fleet-info.ts @@ -1,77 +1,91 @@ +/** + * Fleet info display utilities for C4 + */ + import dayjs, { now } from '../../../../../dayjs.js' import { logger } from '../../../../../logger.js' -import { getName } from '../util.js' -import { planetsByCoordinates } from './planets-by-coordinates.js' -import { starbaseByCoordinates } from './starbase-by-coordinates.js' -import { FleetInfo } from './user-fleets.js' +import { celestialBodiesByCoordinates } from './planets-by-coordinates.js' +import { systemByCoordinates } from './starbase-by-coordinates.js' +import type { FleetInfo } from './user-fleets.js' export const showFleetInfo = async (fleetInfo: FleetInfo): Promise => { - switch (fleetInfo.fleetState.type) { + switch (fleetInfo.fleetState.kind) { case 'Idle': { - const baseStation = await starbaseByCoordinates(fleetInfo.location) - const planets = await planetsByCoordinates(fleetInfo.location) + const system = await systemByCoordinates(fleetInfo.location) + const celestialBodies = await celestialBodiesByCoordinates( + fleetInfo.location, + ) logger.info( - `${fleetInfo.fleetName} is idle at ${fleetInfo.fleetState.data.sector} [BaseStation: ${baseStation ? getName(baseStation) : 'N/A'} / Planets: ${planets.length}]`, + `${fleetInfo.fleetName} is idle at ${fleetInfo.fleetState.data.sector} [System: ${system?.name ?? 'N/A'} / Bodies: ${celestialBodies.length}]`, ) break } - case 'StarbaseLoadingBay': + case 'Docked': { + const { system } = fleetInfo.fleetState.data logger.info( - `${fleetInfo.fleetName} is in the loading bay at ${getName(fleetInfo.fleetState.data.starbase)}`, + `${fleetInfo.fleetName} is docked at ${system.name ?? system.key}`, ) break + } case 'MoveWarp': { - const { fromSector, toSector, warpFinish } = + const { fromCoordinates, toCoordinates, arrivalTime } = fleetInfo.fleetState.data - if (warpFinish.isBefore(now())) { + if (arrivalTime.isBefore(now())) { logger.info( - `${fleetInfo.fleetName} has arrived at ${fleetInfo.fleetState.data.toSector}`, + `${fleetInfo.fleetName} has arrived at ${toCoordinates}`, ) } else { logger.info( - `${fleetInfo.fleetName} warping from ${fromSector} to ${toSector}. Arrival in ${dayjs.duration(warpFinish.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, + `${fleetInfo.fleetName} warping from ${fromCoordinates} to ${toCoordinates}. Arrival in ${dayjs.duration(arrivalTime.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, ) } break } case 'MoveSubwarp': { - const { fromSector, toSector, arrivalTime } = + const { fromCoordinates, toCoordinates, arrivalTime } = fleetInfo.fleetState.data if (arrivalTime.isBefore(now())) { logger.info( - `${fleetInfo.fleetName} has arrived at ${fleetInfo.fleetState.data.toSector}`, + `${fleetInfo.fleetName} has arrived at ${toCoordinates}`, ) } else { logger.info( - `${fleetInfo.fleetName} subwarping from ${fromSector} to ${toSector}. Arrival in ${dayjs.duration(arrivalTime.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, + `${fleetInfo.fleetName} subwarping from ${fromCoordinates} to ${toCoordinates}. Arrival in ${dayjs.duration(arrivalTime.diff(now())).humanize(false)}. Current Position: ${fleetInfo.location}`, ) } break } case 'MineAsteroid': { - const { mineItem, end, amountMined, endReason } = + const { celestialBody, end, amountMined, endReason } = fleetInfo.fleetState.data if (end.isBefore(now())) { logger.info( - `${fleetInfo.fleetName} has finished mining ${getName(mineItem)} for ${amountMined}`, + `${fleetInfo.fleetName} has finished mining at ${celestialBody.key} for ${amountMined}`, ) } else { const log = endReason === 'FULL' ? logger.info : logger.warn log( - `${fleetInfo.fleetName} mining ${getName(mineItem)} for ${amountMined}. Time remaining: ${dayjs.duration(end.diff(now())).humanize(false)} until ${endReason}`, + `${fleetInfo.fleetName} mining at ${celestialBody.key} for ${amountMined}. Time remaining: ${dayjs.duration(end.diff(now())).humanize(false)} until ${endReason}`, ) } break } + case 'Respawn': { + const { ETA } = fleetInfo.fleetState.data + logger.info( + `${fleetInfo.fleetName} respawning. ETA: ${dayjs.duration(ETA.diff(now())).humanize(false)}`, + ) + break + } default: logger.info( - `${fleetInfo.fleetName} is ${fleetInfo.fleetState.type}`, + `${fleetInfo.fleetName} is ${(fleetInfo.fleetState as { kind: string }).kind}`, ) } } diff --git a/src/main/basedbot/lib/sage/state/starbase-by-coordinates.ts b/src/main/basedbot/lib/sage/state/starbase-by-coordinates.ts index ad3c975a..102d51fe 100644 --- a/src/main/basedbot/lib/sage/state/starbase-by-coordinates.ts +++ b/src/main/basedbot/lib/sage/state/starbase-by-coordinates.ts @@ -1,41 +1,119 @@ -import { readAllFromRPC } from '@staratlas/data-source' -import { Starbase } from '@staratlas/sage' +/** + * Find star system with starbase by coordinates (C4) + */ -import { connection } from '../../../../../service/sol/index.js' -import { programs } from '../../programs.js' -import { Coordinates } from '../../util/coordinates.js' +import type { StarSystem } from '@staratlas/dev-sage' -export const starbaseByCoordinates = async ( +import { fetchAllAccounts } from '../../account-fetcher.js' +import type { AccountWithKey } from '../../types.js' +import { Coordinates, type C4Coord } from '../../util/coordinates.js' + +import { type StarbaseData, getStarbaseFromSystem } from './starbases.js' + +/** Cached StarSystem accounts (fetched once, refreshed on demand) */ +let cachedSystems: AccountWithKey[] | null = null +let cacheTimestamp = 0 +const CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes + +/** + * Get all StarSystem accounts, using a cache to avoid re-fetching on every FSM tick. + */ +async function getCachedSystems(): Promise[]> { + const now = Date.now() + if (!cachedSystems || now - cacheTimestamp > CACHE_TTL_MS) { + cachedSystems = await fetchAllAccounts('StarSystem') + cacheTimestamp = now + } + return cachedSystems +} + +/** Clear the StarSystem cache (e.g. after a game state change) */ +export function invalidateSystemCache(): void { + cachedSystems = null + cacheTimestamp = 0 +} + +/** + * Find a star system by coordinates (using integer comparison) + * Supports exact match or fuzzy match (within tolerance) + */ +export const systemByCoordinates = async ( coordinates: Coordinates, -): Promise => { - const [starbase] = await readAllFromRPC( - connection, - programs.sage, - Starbase, - 'processed', - [ - { - memcmp: { - offset: 41, - bytes: coordinates.xB58, - }, - }, - { - memcmp: { - offset: 49, - bytes: coordinates.yB58, - }, - }, - ], + tolerance: number = 0, +): Promise | null> => { + const systems = await getCachedSystems() + + return ( + systems.find((system) => { + const sysCoords = system.coordinates as unknown as [ + C4Coord, + C4Coord, + ] + const sysX = Math.round(sysCoords[0].toNumber()) + const sysY = Math.round(sysCoords[1].toNumber()) + if (tolerance === 0) { + return sysX === coordinates.x && sysY === coordinates.y + } + // Fuzzy match within tolerance + const dx = Math.abs(sysX - coordinates.x) + const dy = Math.abs(sysY - coordinates.y) + return dx <= tolerance && dy <= tolerance + }) ?? null ) +} - if (!starbase) { - return null - } +/** + * Find a star system near coordinates (within 1 unit tolerance) + * Use this when a fleet might be at slightly off coordinates + */ +export const systemNearCoordinates = async ( + coordinates: Coordinates, +): Promise | null> => { + // Try exact match first + const exact = await systemByCoordinates(coordinates, 0) + if (exact) return exact + // Try fuzzy match within 1 unit + return systemByCoordinates(coordinates, 1) +} + +/** + * Find a starbase by coordinates (returns StarSystem that has a starbase) + * Uses fuzzy matching to handle fleets that are 1 unit away from exact starbase position + */ +export const starbaseByCoordinates = async ( + coordinates: Coordinates, +): Promise => { + // Try exact match first + let system = await systemByCoordinates(coordinates, 0) - if (starbase.type === 'error') { - throw new Error('Error reading starbase account') + // If no exact match, try fuzzy match within 1 unit + if (!system) { + system = await systemByCoordinates(coordinates, 1) } - return starbase.data + if (!system) return null + return getStarbaseFromSystem(system) +} + +/** + * Get exact coordinates from a star system (preserves full precision) + * Use this when moving to a starbase for docking + */ +export const getExactSystemCoordinates = ( + system: AccountWithKey, +): Coordinates => { + const coords = system.coordinates as unknown as [C4Coord, C4Coord] + return Coordinates.fromC4Coords(coords) +} + +/** + * Find exact starbase coordinates by approximate coordinates + * Returns precise coordinates needed for docking, or null if not found + */ +export const getExactStarbaseCoordinates = async ( + approxCoordinates: Coordinates, +): Promise => { + const system = await systemByCoordinates(approxCoordinates) + if (!system) return null + return getExactSystemCoordinates(system) } diff --git a/src/main/basedbot/lib/sage/state/starbase-by-key.ts b/src/main/basedbot/lib/sage/state/starbase-by-key.ts index b392cee9..385af987 100644 --- a/src/main/basedbot/lib/sage/state/starbase-by-key.ts +++ b/src/main/basedbot/lib/sage/state/starbase-by-key.ts @@ -1,26 +1,41 @@ -import { PublicKey } from '@solana/web3.js' -import { readFromRPC } from '@staratlas/data-source' -import { Starbase } from '@staratlas/sage' - -import { connection } from '../../../../../service/sol/index.js' -import { programs } from '../../programs.js' - -export const starbaseByKey = async (key: PublicKey): Promise => { - const starbase = await readFromRPC( - connection, - programs.sage, - key, - Starbase, - 'processed', - ) +/** + * Find star system with starbase by key (C4) + */ - if (!starbase) { - throw new Error('no starbase found') +import type { Address } from '@solana/kit' +import type { StarSystem } from '@staratlas/dev-sage' + +import { fetchAccount } from '../../account-fetcher.js' +import type { AccountWithKey } from '../../types.js' + +import { type StarbaseData, getStarbaseFromSystem } from './starbases.js' + +/** + * Find a star system by address + */ +export const systemByKey = async ( + key: Address, +): Promise> => { + const system = await fetchAccount('StarSystem', key) + + if (!system) { + throw new Error(`StarSystem not found: ${key}`) } - if (starbase.type === 'error') { - throw new Error('Error reading starbase account') + return system +} + +/** + * Get starbase data from a star system by address + * Throws if system doesn't have a starbase + */ +export const starbaseByKey = async (key: Address): Promise => { + const system = await systemByKey(key) + const starbase = getStarbaseFromSystem(system) + + if (!starbase) { + throw new Error(`No starbase at system: ${key}`) } - return starbase.data + return starbase } diff --git a/src/main/basedbot/lib/sage/state/starbase-player.ts b/src/main/basedbot/lib/sage/state/starbase-player.ts index 026e48ea..7db36409 100644 --- a/src/main/basedbot/lib/sage/state/starbase-player.ts +++ b/src/main/basedbot/lib/sage/state/starbase-player.ts @@ -1,195 +1,76 @@ -import { Keypair, PublicKey } from '@solana/web3.js' -import { CargoPod } from '@staratlas/cargo' -import { ixReturnsToIxs, readAllFromRPC } from '@staratlas/data-source' -import { - Game, - SagePlayerProfile, - Ship, - Starbase, - StarbasePlayer, -} from '@staratlas/sage' +/** + * StarbasePlayer utilities for C4 + */ -import { logger } from '../../../../../logger.js' -import { connection } from '../../../../../service/sol/index.js' -import { sendAndConfirmInstructions } from '../../../../../service/sol/send-and-confirm-tx.js' -import { keyPair } from '../../../../../service/wallet' -import { StarAtlasPrograms } from '../../programs.js' +import type { Address } from '@solana/kit' +import type { StarbasePlayer, StarSystem } from '@staratlas/dev-sage' +import { findStarbasePlayerPda } from '@staratlas/dev-sage' -import { sageGame } from './game.js' -import { Player } from './user-account.js' +import { fetchAllAccounts, fetchAccount } from '../../account-fetcher.js' +import type { AccountWithKey } from '../../types.js' -export const getCargoPodsForStarbasePlayer = async ( - starbasePlayer: StarbasePlayer, - programs: StarAtlasPrograms, -): Promise => { - const cargoPods = await readAllFromRPC( - connection, - programs.cargo, - CargoPod, - 'processed', - [ - { - memcmp: { - offset: 8 + 1 + 32, - bytes: starbasePlayer.key.toBase58(), - }, - }, - ], - ) - - if (cargoPods.length > 1) { - logger.warn( - `${starbasePlayer.data.starbase.toBase58()} has ${cargoPods.length} cargo pods`, - ) - } - - const cargoPod = cargoPods[0] - - if (!cargoPod) { - throw new Error('Error reading cargo pods') - } - - if (cargoPod.type === 'error') { - throw new Error('Error reading cargoPods account') - } - - return cargoPod.data -} +import type { Player } from './user-account.js' -export const getShipByMint = async ( - mint: PublicKey, - game: Game, - programs: StarAtlasPrograms, -): Promise => { - const ships = await readAllFromRPC( - connection, - programs.sage, - Ship, - 'processed', - [ - { - memcmp: { - offset: 8 + 1, - bytes: game.key.toString(), - }, - }, - { - memcmp: { - offset: 8 + 1 + 32, - bytes: mint.toString(), - }, - }, - ], - ) - - const ship = ships.find( - (s) => - s.type === 'ok' && - (s.data.data as any).next.key.toString() === - '11111111111111111111111111111111', +/** + * Get StarbasePlayer for a player at a specific system + */ +export const getStarbasePlayer = async ( + player: Player, + system: AccountWithKey, +): Promise | null> => { + const allStarbasePlayers = await fetchAllAccounts('StarbasePlayer') + + return ( + allStarbasePlayers.find( + (sp) => + sp.playerProfile === player.character.playerProfile && + sp.system === system.key, + ) ?? null ) - - if (!ship) { - throw new Error('Error reading ship with mint ' + mint.toBase58()) - } - - if (ship.type === 'error') { - throw new Error('Error reading ship account') - } - - return ship.data } -export const getStarbasePlayer = async ( +/** + * Get all StarbasePlayers for a player + */ +export const getAllStarbasePlayers = async ( player: Player, - starbase: Starbase, - programs: StarAtlasPrograms, -): Promise => { - const starbasePlayers = await readAllFromRPC( - connection, - programs.sage, - StarbasePlayer, - 'processed', - [ - { - memcmp: { - offset: 9, - bytes: player.profile.key.toBase58(), - }, - }, - { - memcmp: { - offset: 73, - bytes: starbase.key.toBase58(), - }, - }, - ], +): Promise[]> => { + const allStarbasePlayers = await fetchAllAccounts('StarbasePlayer') + return allStarbasePlayers.filter( + (sp) => sp.playerProfile === player.character.playerProfile, ) +} - const game = await sageGame() - - if (starbasePlayers.length > 1) { - throw new Error('Multiple starbase players found') - } - const [starbasePlayer] = starbasePlayers - - if (!starbasePlayer) { - const [sageProfileAddress] = SagePlayerProfile.findAddress( - programs.sage, - player.profile.key, - game.key, - ) - const [starbasePlayerAddress] = StarbasePlayer.findAddress( - programs.sage, - starbase.key, - sageProfileAddress, - starbase.data.seqId, - ) - - const instructionReturns = [ - StarbasePlayer.registerStarbasePlayer( - programs.sage, - player.profileFaction.key, - sageProfileAddress, - starbase.key, - game.key, - game.data.gameState, - starbase.data.seqId, - ), - StarbasePlayer.createCargoPod( - programs.sage, - programs.cargo, - starbasePlayerAddress, - player.signer, - player.profile.key, - player.profileFaction.key, - starbase.key, - game.data.cargo.statsDefinition, - game.key, - game.data.gameState, - { - keyIndex: 0, - podSeeds: Array.from( - Keypair.generate().publicKey.toBuffer(), - ), - }, - ), - ] - - logger.warn('Starbase player not found, creating', { - player: player.profile.key.toBase58(), - starbase: starbase.key.toBase58(), - }) - await sendAndConfirmInstructions([keyPair])( - await ixReturnsToIxs(instructionReturns, player.signer), - ) - - return getStarbasePlayer(player, starbase, programs) - } +/** + * Get StarbasePlayer by address + */ +export const getStarbasePlayerByKey = async ( + key: Address, +): Promise | null> => { + return fetchAccount('StarbasePlayer', key) +} - if (starbasePlayer.type === 'error') { - throw new Error('Error reading starbasePlayer account') - } +/** + * Derive StarbasePlayer PDA address + */ +export const deriveStarbasePlayerAddress = async ( + system: AccountWithKey, + player: Player, +): Promise
=> { + const [pda] = await findStarbasePlayerPda({ + system: system.key, + character: player.character.key, + }) + return pda +} - return starbasePlayer.data +/** + * Check if player has a StarbasePlayer at a system + */ +export const hasStarbasePlayerAt = async ( + player: Player, + system: AccountWithKey, +): Promise => { + const sp = await getStarbasePlayer(player, system) + return sp !== null } diff --git a/src/main/basedbot/lib/sage/state/starbases.ts b/src/main/basedbot/lib/sage/state/starbases.ts index 5034afba..a1029c26 100644 --- a/src/main/basedbot/lib/sage/state/starbases.ts +++ b/src/main/basedbot/lib/sage/state/starbases.ts @@ -1,26 +1,207 @@ -import { readAllFromRPC } from '@staratlas/data-source' -import { Game, Starbase } from '@staratlas/sage' - -import { connection } from '../../../../../service/sol/index.js' -import { programs } from '../../programs.js' - -export const getStarbases = async (game: Game): Promise> => { - const starbases = await readAllFromRPC( - connection, - programs.sage, - Starbase, - 'processed', - [ - { - memcmp: { - offset: 8 + 1, - bytes: game.key.toBase58(), - }, - }, - ], +/** + * Starbase utilities for C4 + * In C4, Starbase is embedded in StarSystem as an optional field + */ + +import type { StarSystem } from '@staratlas/dev-sage' +import { Faction as SdkFaction } from '@staratlas/dev-profile-faction' + +import { fetchAllAccounts } from '../../account-fetcher.js' +import type { AccountWithKey } from '../../types.js' +import { Faction } from '../../util/galaxy-sectors-data.js' + +/** + * Starbase data extracted from StarSystem + */ +export type StarbaseData = { + system: AccountWithKey + level: number + /** Starbase faction owner (SDK enum value) */ + ownerSdk: SdkFaction + /** Starbase faction owner (fleetbot enum value) */ + owner: Faction | null + hp: number + pendingHp: number +} + +/** + * Convert SDK Faction enum to fleetbot Faction enum + * SDK: Unaligned=0, Mud=1, Oni=2, Ustur=3 + * Fleetbot: MUD=1, ONI=2, UST=3 + */ +function sdkFactionToFleetbot(sdkFaction: SdkFaction | number): Faction | null { + // Handle both enum and raw number values + const numValue = typeof sdkFaction === 'number' ? sdkFaction : sdkFaction + + // SDK enum values: Unaligned=0, Mud=1, Oni=2, Ustur=3 + if (numValue === SdkFaction.Mud || numValue === 1) { + return Faction.MUD + } + if (numValue === SdkFaction.Oni || numValue === 2) { + return Faction.ONI + } + if (numValue === SdkFaction.Ustur || numValue === 3) { + return Faction.UST + } + if (numValue === SdkFaction.Unaligned || numValue === 0) { + return null // Unaligned starbases are neutral + } + return null +} + +/** + * Get all star systems that have a starbase + */ +export const getStarbases = async (): Promise => { + const systems = await fetchAllAccounts('StarSystem') + + return systems + .filter((system) => { + const starbase = system.starbase as { __option: string } | undefined + return starbase?.__option === 'Some' + }) + .map((system) => { + const starbase = system.starbase as { + __option: 'Some' + value: { + level: number + owner: SdkFaction + hp: number + pendingHp: number + } + } + return { + system, + level: starbase.value.level, + ownerSdk: starbase.value.owner, + owner: sdkFactionToFleetbot(starbase.value.owner), + hp: starbase.value.hp, + pendingHp: starbase.value.pendingHp, + } + }) +} + +/** + * Check if a star system has a starbase + */ +export const hasStarbase = (system: AccountWithKey): boolean => { + const starbase = system.starbase as { __option: string } | undefined + return starbase?.__option === 'Some' +} + +/** + * Get starbase data from a star system (if present) + */ +export const getStarbaseFromSystem = ( + system: AccountWithKey, +): StarbaseData | null => { + const starbase = system.starbase as + | { + __option: 'Some' + value: { + level: number + owner: SdkFaction + hp: number + pendingHp: number + } + } + | { __option: 'None' } + | undefined + + if (!starbase || starbase.__option !== 'Some') { + return null + } + + return { + system, + level: starbase.value.level, + ownerSdk: starbase.value.owner, + owner: sdkFactionToFleetbot(starbase.value.owner), + hp: starbase.value.hp, + pendingHp: starbase.value.pendingHp, + } +} + +/** + * C4 coordinate type for distance calculation + */ +type C4Coord = { + raw: bigint + toNumber: () => number +} + +/** + * Calculate distance between two systems + */ +function systemDistance( + a: AccountWithKey, + b: AccountWithKey, +): number { + const aCoords = a.coordinates as unknown as [C4Coord, C4Coord] + const bCoords = b.coordinates as unknown as [C4Coord, C4Coord] + const dx = aCoords[0].toNumber() - bCoords[0].toNumber() + const dy = aCoords[1].toNumber() - bCoords[1].toNumber() + return Math.sqrt(dx * dx + dy * dy) +} + +/** + * Find the nearest starbase compatible with a faction + * Compatible means: owned by the same faction (can register there) + * Returns null if no compatible starbase found + * + * @param currentSystem - The system the fleet is currently at + * @param playerFaction - The player's faction + * @param minDistance - Minimum distance to consider (default 1.0 to exclude same/nearby starbases) + */ +export const findNearestCompatibleStarbase = async ( + currentSystem: AccountWithKey, + playerFaction: Faction, + minDistance: number = 1.0, +): Promise => { + const allStarbases = await getStarbases() + const currentName = (currentSystem as { name?: string }).name ?? 'unknown' + + // Filter to faction-compatible starbases that are not at the same location + const compatible = allStarbases.filter((sb) => { + if (sb.owner !== playerFaction) return false + // Exclude starbases that are too close (same or nearby location) + const distance = systemDistance(currentSystem, sb.system) + // Also exclude if it's the same system by key + if (sb.system.key === currentSystem.key) return false + return distance >= minDistance + }) + + if (compatible.length === 0) { + // Log what's available for debugging + const factionNames = ['Unknown', 'MUD', 'ONI', 'UST'] + const factionName = factionNames[playerFaction] ?? 'unknown' + const totalCompatible = allStarbases.filter( + (sb) => sb.owner === playerFaction, + ).length + console.warn( + `findNearestCompatibleStarbase: No compatible ${factionName} starbases found ` + + `(total ${factionName} starbases: ${totalCompatible}, ` + + `current location: ${currentName})`, + ) + return null + } + + // Sort by distance and return nearest + compatible.sort( + (a, b) => + systemDistance(currentSystem, a.system) - + systemDistance(currentSystem, b.system), ) - return starbases - .filter((p) => p.type === 'ok' && 'data' in p) - .map((p) => (p as any).data) + return compatible[0] +} + +/** + * Check if a starbase is compatible with a player's faction for registration + */ +export const isStarbaseCompatible = ( + starbase: StarbaseData, + playerFaction: Faction, +): boolean => { + return starbase.owner === playerFaction } diff --git a/src/main/basedbot/lib/sage/state/user-account.ts b/src/main/basedbot/lib/sage/state/user-account.ts index 64660638..d1ac97df 100644 --- a/src/main/basedbot/lib/sage/state/user-account.ts +++ b/src/main/basedbot/lib/sage/state/user-account.ts @@ -1,194 +1,269 @@ -import { Keypair, PublicKey } from '@solana/web3.js' -import { CargoType } from '@staratlas/cargo' -import { - AsyncSigner, - keypairToAsyncSigner, - readAllFromRPC, -} from '@staratlas/data-source' -import { PlayerProfile } from '@staratlas/player-profile' -import { UserPoints } from '@staratlas/points' -import { ProfileFactionAccount } from '@staratlas/profile-faction' -import { SagePointsCategory, Starbase } from '@staratlas/sage' +import type { Address, KeyPairSigner } from '@solana/kit' +import type { Profile } from '@staratlas/dev-player-profile' +import type { ProfileFactionAccount } from '@staratlas/dev-profile-faction' +import type { Character, StarSystem } from '@staratlas/dev-sage' + import { config } from '../../../../../config/index.js' import { logger } from '../../../../../logger.js' - -import { connection } from '../../../../../service/sol/index.js' -import { programs } from '../../programs.js' -import { Coordinates } from '../../util/coordinates.js' +import { getProfileKeyPair } from '../../../../../service/wallet/index.js' +import { fetchAllAccounts, fetchGame } from '../../account-fetcher.js' +import type { AccountWithKey } from '../../types.js' +import { Coordinates, type C4Coord } from '../../util/coordinates.js' import { Faction, galaxySectorsData } from '../../util/galaxy-sectors-data.js' import { createAndInitializeCharacter } from '../../util/profile.js' -import { ExtShipData, getShipData } from '../ships.js' - -import { getCargoType, getCargoTypes } from './cargo-types.js' -import { sageGame } from './game.js' -import { starbaseByCoordinates } from './starbase-by-coordinates.js' - -export type XpAccounts = { - councilRank: XpAccount - dataRunning: XpAccount - piloting: XpAccount - mining: XpAccount - crafting: XpAccount -} - -export type XpAccount = { - userPointsAccount: PublicKey - pointsCategory: PublicKey - pointsModifierAccount: PublicKey -} +import { getStarbaseFromSystem } from './starbases.js' +/** + * Player context for C4 SAGE operations + */ export type Player = { - publicKey: PublicKey + /** Player wallet address */ + address: Address + /** Profile key index */ keyIndex: number - profile: PlayerProfile - profileFaction: ProfileFactionAccount + /** Player profile account */ + profile: AccountWithKey + /** Profile faction account */ + profileFaction: AccountWithKey + /** Character account (required for C4 actions) */ + character: AccountWithKey + /** Faction enum value */ faction: Faction - xpAccounts: XpAccounts - signer: AsyncSigner - homeStarbase: Starbase + /** Signer for transactions */ + signer: KeyPairSigner + /** Home star system */ + homeSystem: AccountWithKey + /** Home coordinates */ homeCoordinates: Coordinates - cargoTypes: Array - fuelCargoType: CargoType - foodCargoType: CargoType - ammoCargoType: CargoType - shipData: Array } -const getXpAccount = ( - playerProfile: PublicKey, - pointsCategory: SagePointsCategory, -): XpAccount => { - const pointsCategoryKey = pointsCategory.category - const pointsModifierAccount = pointsCategory.modifier - const [userPointsAccount] = UserPoints.findAddress( - programs.points, - pointsCategoryKey, - playerProfile, - ) +/** + * Find profiles for a wallet address + */ +async function findProfiles( + walletAddress: Address, +): Promise[]> { + const allProfiles = await fetchAllAccounts('Profile') + return allProfiles.filter((p) => p.profileKeys[0]?.key === walletAddress) +} - return { - userPointsAccount, - pointsModifierAccount, - pointsCategory: pointsCategoryKey, +/** + * Find faction account for a profile + */ +async function findProfileFaction( + profileKey: Address, +): Promise | null> { + const allFactions = await fetchAllAccounts('ProfileFactionAccount') + return allFactions.find((f) => f.profile === profileKey) ?? null +} + +/** + * Find character account for a profile + */ +async function findCharacter( + profileKey: Address, +): Promise | null> { + const allCharacters = await fetchAllAccounts('Character') + return allCharacters.find((c) => c.playerProfile === profileKey) ?? null +} + +/** + * Find star systems + */ +async function findStarSystems(): Promise[]> { + return fetchAllAccounts('StarSystem') +} + +/** + * Convert C4 coordinate to integer (for matching with galaxy sectors data) + */ +function coordToInt(coord: C4Coord): number { + return Math.round(coord.toNumber()) +} + +/** + * Find home star system based on faction + * Prioritizes CSS (Central Space Station) with matching faction starbase, + * falls back to any system with a starbase owned by the player's faction. + */ +function findHomeSystem( + faction: Faction, + systems: AccountWithKey[], +): AccountWithKey | null { + // First, filter systems that have a starbase owned by the player's faction + const systemsWithMatchingFaction = systems.filter((system) => { + const starbase = getStarbaseFromSystem(system) + return starbase && starbase.owner === faction + }) + + if (systemsWithMatchingFaction.length === 0) { + logger.warn( + `No systems found with starbase owned by faction ${faction}`, + ) + return null + } + + // Try to find CSS (Central Space Station) for faction from static data + const sectorInfo = galaxySectorsData() + .filter((sector) => sector.closestFaction === faction) + .find((sector) => sector.name.includes('CSS')) + + if (sectorInfo) { + // Find matching star system by coordinates (using integer comparison) + const targetX = sectorInfo.coordinates.x + const targetY = sectorInfo.coordinates.y + + const cssSystem = systemsWithMatchingFaction.find((system) => { + const coords = system.coordinates as unknown as [C4Coord, C4Coord] + const sysX = coordToInt(coords[0]) + const sysY = coordToInt(coords[1]) + return sysX === targetX && sysY === targetY + }) + + if (cssSystem) { + return cssSystem + } + + logger.warn( + `CSS at [${targetX}, ${targetY}] not owned by faction ${faction}, using fallback`, + ) } + + // Fallback: return first system with matching faction starbase + logger.info( + `Using fallback home system from ${systemsWithMatchingFaction.length} faction-owned starbases`, + ) + return systemsWithMatchingFaction[0] } -const getKeyIndex = (_: PlayerProfile): number => 0 +const getKeyIndex = (_: Profile): number => 0 + +/** + * Map config faction string to Faction enum + */ +function mapConfigFaction(factionStr: 'MUD' | 'ONI' | 'UST'): Faction { + switch (factionStr) { + case 'MUD': + return Faction.MUD + case 'ONI': + return Faction.ONI + case 'UST': + return Faction.UST + default: + return Faction.MUD + } +} +/** + * Get player context for C4 SAGE operations + */ export const getPlayerContext = async ( - user: PublicKey, - signer: Keypair, + walletAddress: Address, + signer: KeyPairSigner, ): Promise => { - const myProfiles = await readAllFromRPC( - connection, - programs.playerProfile, - PlayerProfile, - 'processed', - [ - { - memcmp: { - offset: PlayerProfile.MIN_DATA_SIZE + 2, - bytes: user.toBase58(), - }, - }, - ], - ) - const game = await sageGame() - - const [profile] = - myProfiles.length > 0 - ? myProfiles - : config.app.autoCreateProfile - ? await createAndInitializeCharacter( - game, - 'fleetbot', - Faction.ONI, - signer, - ) - : [] + // Fetch game to ensure we're connected + const game = await fetchGame() - if (!profile) { - throw new Error('no player profile found') - } + // Find player profiles + let myProfiles = await findProfiles(walletAddress) + + if (myProfiles.length === 0) { + if (config.app.autoCreateProfile) { + logger.info('No profile found, auto-creating profile...') + + const faction = mapConfigFaction(config.app.defaultFaction) + const characterName = config.app.defaultCharacterName - if (profile.type === 'error') { - throw new Error('Error reading account') + logger.info( + `Creating profile with faction: ${config.app.defaultFaction}, name: ${characterName}`, + ) + + const profileSigner = await getProfileKeyPair() + await createAndInitializeCharacter( + game, + characterName, + faction, + signer, + profileSigner, + ) + + logger.info('Profile created, re-fetching profiles...') + + // Re-fetch profiles after creation + // Need to wait a bit for the chain to be updated + await new Promise((resolve) => setTimeout(resolve, 2000)) + myProfiles = await findProfiles(walletAddress) + + if (myProfiles.length === 0) { + throw new Error( + 'Profile creation succeeded but profile not found. Try again in a few seconds.', + ) + } + } else { + throw new Error('No player profile found') + } } + const profile = myProfiles[0] logger.info(`Player profile: ${profile.key}`) - const keyIndex = getKeyIndex(profile.data) - - const [profileFaction] = await readAllFromRPC( - connection, - programs.profileFaction as any, - ProfileFactionAccount, - 'processed', - [ - { - memcmp: { - offset: 9, - bytes: profile.key.toBase58(), - }, - }, - ], - ) + const keyIndex = getKeyIndex(profile) - if (profileFaction.type === 'error') { - throw new Error('Error reading faction account') + // Find faction + const profileFaction = await findProfileFaction(profile.key) + if (!profileFaction) { + throw new Error('No faction account found for profile') } - const xpAccounts = { - councilRank: getXpAccount( - profile.key, - game.data.points.councilRankXpCategory, - ), - dataRunning: getXpAccount( - profile.key, - game.data.points.dataRunningXpCategory, - ), - piloting: getXpAccount(profile.key, game.data.points.pilotXpCategory), - mining: getXpAccount(profile.key, game.data.points.miningXpCategory), - crafting: getXpAccount( - profile.key, - game.data.points.craftingXpCategory, - ), + // Find character (required for C4) + const character = await findCharacter(profile.key) + if (!character) { + throw new Error( + 'No character account found. Create character via Star Atlas app first.', + ) } - const cargoTypes = await getCargoTypes() + // Determine faction enum + // C4 ProfileFactionAccount.faction is a number: 1=MUD, 2=ONI, 3=UST + const factionValue = profileFaction.faction as unknown as number + let faction: Faction + if (factionValue === 1) { + faction = Faction.MUD + } else if (factionValue === 2) { + faction = Faction.ONI + } else if (factionValue === 3) { + faction = Faction.UST + } else { + throw new Error(`Unknown faction value: ${factionValue}`) + } - const homeCoordinates = galaxySectorsData() - .filter( - (sector) => - sector.closestFaction === profileFaction.data.data.faction, - ) - .find((sector) => sector.name.includes('CSS'))?.coordinates + // Find home system + const systems = await findStarSystems() + logger.info(`Found ${systems.length} star systems`) - if (!homeCoordinates) { - throw new Error('No home coordinates found') + const homeSystem = findHomeSystem(faction, systems) + if (!homeSystem) { + logger.error(`Faction: ${faction}, no matching home system found`) + throw new Error('No home star system found') } - const homeStarbase = await starbaseByCoordinates(homeCoordinates) + const homeCoords = homeSystem.coordinates as unknown as [C4Coord, C4Coord] + logger.info( + `Home system: [${coordToInt(homeCoords[0])}, ${coordToInt(homeCoords[1])}]`, + ) - if (!homeStarbase) { - throw new Error('No home starbase found') - } - const shipData = await getShipData(game) + // Use fromC4Coords to preserve exact starbase coordinates for docking + const homeCoordinates = Coordinates.fromC4Coords(homeCoords) return { - publicKey: user, - profile: profile.data, - profileFaction: profileFaction.data, - faction: profileFaction.data.data.faction, + address: walletAddress, + profile, + profileFaction, + character, + faction, keyIndex, - xpAccounts, - signer: keypairToAsyncSigner(signer), + signer, + homeSystem, homeCoordinates, - cargoTypes, - homeStarbase, - fuelCargoType: getCargoType(cargoTypes, game, game.data.mints.fuel), - foodCargoType: getCargoType(cargoTypes, game, game.data.mints.food), - ammoCargoType: getCargoType(cargoTypes, game, game.data.mints.ammo), - shipData, } } diff --git a/src/main/basedbot/lib/sage/state/user-fleets.ts b/src/main/basedbot/lib/sage/state/user-fleets.ts index f78236f0..3ac2af3c 100644 --- a/src/main/basedbot/lib/sage/state/user-fleets.ts +++ b/src/main/basedbot/lib/sage/state/user-fleets.ts @@ -1,26 +1,37 @@ -import { readAllFromRPC } from '@staratlas/data-source' -import { DisbandedFleet, Fleet } from '@staratlas/sage' -import BN from 'bn.js' +/** + * User fleet utilities for C4 + */ -import { connection } from '../../../../../service/sol/index.js' +import { + decodeAccount, + getBase58Decoder, + parseBase64RpcAccount, + type Address, +} from '@solana/kit' +import { + FLEET_DISCRIMINATOR, + SAGE_PROGRAM_ADDRESS, + getFleetDecoder, + type Fleet, +} from '@staratlas/dev-sage' + +import { rpcFetch } from '../../../../../service/sol/rpc-fetch.js' import { getFleetState } from '../../fleet-state/fleet-state.js' -import { FleetState } from '../../fleet-state/types.js' -import { programs } from '../../programs.js' -import { Coordinates } from '../../util/coordinates.js' +import type { FleetState } from '../../fleet-state/types.js' +import type { AccountWithKey } from '../../types.js' +import type { Coordinates } from '../../util/coordinates.js' import { getName } from '../util.js' -import { FleetCargo, getFleetCargoBalance } from './fleet-cargo.js' -import { Player } from './user-account.js' -import { WorldMap } from './world-map.js' +import { type FleetCargo, getFleetCargoBalance } from './fleet-cargo.js' +import type { Player } from './user-account.js' type ShipCounts = { total: number - updated: number - xxs: number - xs: number - s: number - m: number - l: number + xxSmall: number + xSmall: number + small: number + medium: number + large: number capital: number commander: number titan: number @@ -37,9 +48,9 @@ type MovementStats = { } type CargoStats = { - cargoCapacity: number - fuelCapacity: number - ammoCapacity: number + cargoCapacity: bigint + fuelCapacity: bigint + ammoCapacity: bigint ammoConsumptionRate: number foodConsumptionRate: number miningRate: number @@ -48,18 +59,19 @@ type CargoStats = { type MiscStats = { crew: number - respawnTime: number + respawnTimeWithoutFee: number + respawnTimeWithFee: number scanCooldown: number scanRepairKitAmount: number } export type FleetInfo = { - fleet: Fleet + fleet: AccountWithKey location: Coordinates fleetName: string shipCounts: ShipCounts - warpCooldownExpiresAt: BN - scanCooldownExpiresAt: BN + warpCooldownExpiresAt: bigint + scanCooldownExpiresAt: bigint movementStats: MovementStats cargoStats: CargoStats miscStats: MiscStats @@ -67,62 +79,116 @@ export type FleetInfo = { cargoLevels: FleetCargo } -export const getUserDisbandedFleets = async ( +/** + * RPC response type for getProgramAccounts with base64 encoding + */ +type GpaAccountResult = { + pubkey: string + account: { + data: [string, string] + executable: boolean + lamports: number + owner: string + rentEpoch: number + space: number + } +} + +/** + * Get all fleets for a player profile using server-side memcmp filtering. + * Fleet layout: [8 discriminator][1 version][1 bump][32 gameId][32 ownerProfile] + * ownerProfile starts at byte offset 42. + */ +export const getUserFleets = async ( player: Player, -): Promise> => { - const fleets = await readAllFromRPC( - connection, - programs.sage, - DisbandedFleet, - 'processed', - [ +): Promise[]> => { + const base58 = getBase58Decoder() + const decoder = getFleetDecoder() + const discriminatorBase58 = base58.decode(FLEET_DISCRIMINATOR) + const ownerProfile = player.character.playerProfile + + const response = await rpcFetch<{ + result: GpaAccountResult[] + }>({ + jsonrpc: '2.0', + id: 1, + method: 'getProgramAccounts', + params: [ + SAGE_PROGRAM_ADDRESS, { - memcmp: { - offset: 8 + 1 + 32, // 8 (discriminator) + 1 (version) + 32 (gameId) - bytes: player.profile.key.toBase58(), - }, + encoding: 'base64', + filters: [ + { + memcmp: { + offset: 0, + bytes: discriminatorBase58, + encoding: 'base58', + }, + }, + { + memcmp: { + offset: 42, + bytes: ownerProfile, + encoding: 'base58', + }, + }, + ], }, ], - ) + }) - return fleets - .filter((f) => f.type === 'ok' && 'data' in f) - .map((f) => (f as any).data) -} + const accounts = response.result.result ?? [] -export const getUserFleets = async (player: Player): Promise> => { - const fleets = await readAllFromRPC( - connection, - programs.sage, - Fleet, - 'processed', - [ + return accounts.map((item) => { + const encoded = parseBase64RpcAccount( + item.pubkey as Address, + item.account as any, + ) + const decoded = decodeAccount( { - memcmp: { - offset: 8 + 1 + 32, // 8 (discriminator) + 1 (version) + 32 (gameId) - bytes: player.profile.key.toBase58(), - }, + ...encoded, + data: Uint8Array.from(encoded.data), + exists: true as const, }, - ], - ) + decoder, + ) + + if (!decoded.exists) { + throw new Error(`Failed to decode Fleet account ${item.pubkey}`) + } - return fleets - .filter((f) => f.type === 'ok' && 'data' in f) - .map((f) => (f as any).data) + return { + ...(decoded.data as Fleet), + key: decoded.address as Address, + } + }) } +/** + * Get disbanded fleets for a player + * TODO: Implement for C4 if DisbandedFleet account type exists + */ +export const getUserDisbandedFleets = async ( + _player: Player, +): Promise => { + // In C4, disbanded fleet tracking may work differently + // Return empty array for now + return [] +} + +/** + * Get fleet info with state and cargo + */ export const getFleetInfo = async ( - fleet: Fleet, - player: Player, - map: WorldMap, + fleet: AccountWithKey, ): Promise => { - const cargoLevels = await getFleetCargoBalance(fleet, player) - const fleetState = await getFleetState(fleet, map, cargoLevels) - const shipCounts = fleet.data.shipCounts as unknown as ShipCounts - const movementStats = fleet.data.stats - .movementStats as unknown as MovementStats - const cargoStats = fleet.data.stats.cargoStats as unknown as CargoStats - const miscStats = fleet.data.stats.miscStats as unknown as MiscStats + const cargoLevels = getFleetCargoBalance(fleet) + const fleetState = await getFleetState(fleet) + + const shipCounts = fleet.shipCounts as unknown as ShipCounts + const movementStats = fleet.stats?.movementStats as unknown as MovementStats + const cargoStats = fleet.stats?.cargoStats as unknown as CargoStats + const miscStats = fleet.stats?.miscStats as unknown as MiscStats const location = fleetState.data.sector const fleetName = getName(fleet) @@ -135,8 +201,8 @@ export const getFleetInfo = async ( fleetName, shipCounts, fleetState, - warpCooldownExpiresAt: fleet.data.warpCooldownExpiresAt, - scanCooldownExpiresAt: fleet.data.scanCooldownExpiresAt, + warpCooldownExpiresAt: fleet.warpCooldownExpiresAt as bigint, + scanCooldownExpiresAt: fleet.scanCooldownExpiresAt as bigint, cargoLevels, } } diff --git a/src/main/basedbot/lib/sage/state/world-map.ts b/src/main/basedbot/lib/sage/state/world-map.ts index 87dc435d..1d0cc645 100644 --- a/src/main/basedbot/lib/sage/state/world-map.ts +++ b/src/main/basedbot/lib/sage/state/world-map.ts @@ -1,136 +1,258 @@ -import { Game, MineItem, Planet, Resource, Starbase } from '@staratlas/sage' +/** + * World map utilities for C4 + * Provides a unified view of the galaxy map + */ + +import type { CelestialBody, StarSystem } from '@staratlas/dev-sage' + +import { fetchAllAccounts, fetchGame } from '../../account-fetcher.js' import { transformSector } from '../../fleet-state/transform/transform-sector.js' -import { Coordinates } from '../../util/coordinates.js' -import { getName } from '../util.js' +import type { AccountWithKey } from '../../types.js' +import type { Coordinates } from '../../util/coordinates.js' +import type { SectorInfo } from '../../util/galaxy-sectors-data.js' -import { getMineItems } from './mine-items.js' -import { getPlanets } from './planets.js' -import { getResources } from './resources.js' -import { getStarbases } from './starbases.js' +import { type ResourceInfo, getResourceFromCelestialBody } from './resources.js' +import { type StarbaseData, getStarbaseFromSystem } from './starbases.js' -export type PlanetId = string -export type ResourceId = string -export type StarbaseId = string +export type SystemId = string +export type CelestialBodyId = string +/** + * World map containing all galaxy data + */ export type WorldMap = { - starbases: Array - planets: Map> - mineItems: Map - resources: Map> + systems: AccountWithKey[] + starbases: StarbaseData[] + celestialBodies: Map[]> + resources: Map } +/** + * Mineable location info + */ export type Mineable = { - starbase: Starbase - planet: Planet - resource: Resource - mineItem: MineItem + system: AccountWithKey + starbase: StarbaseData | null + celestialBody: AccountWithKey + resource: ResourceInfo } -export const planetsByStarbase = ( - planets: Map>, - starbase: Starbase, -): Set => planets.get(starbase.key.toBase58()) ?? new Set() - -export const resourcesByPlanet = ( - resources: Map>, - planet: Planet, -): Set => resources.get(planet.key.toBase58()) ?? new Set() +/** + * Get celestial bodies for a system + */ +export const celestialBodiesBySystem = ( + map: WorldMap, + system: AccountWithKey, +): AccountWithKey[] => { + return map.celestialBodies.get(system.key) ?? [] +} -export const mineItemByResource = ( - mineItems: Map, - resource: Resource, -): MineItem | undefined => mineItems.get(resource.key.toBase58()) +/** + * Get resource info for a celestial body + */ +export const resourceByCelestialBody = ( + map: WorldMap, + celestialBody: AccountWithKey, +): ResourceInfo | undefined => { + return map.resources.get(celestialBody.key) +} +/** + * Get all mineables at a system by coordinates + */ export const mineablesByCoordinates = ( map: WorldMap, coordinates: Coordinates, -): Set => { - const starbase = map.starbases.find((s) => - transformSector(s.data.sector).equals(coordinates), - ) +): Mineable[] => { + const system = map.systems.find((s) => { + if (!s.coordinates || s.coordinates.length < 2) return false + const sysCoords = transformSector([s.coordinates[0], s.coordinates[1]]) + return sysCoords.equals(coordinates) + }) - if (!starbase) { - throw new Error(`No starbase found at ${coordinates}`) + if (!system) { + return [] + } + + const starbase = getStarbaseFromSystem(system) + const celestialBodies = celestialBodiesBySystem(map, system) + const mineables: Mineable[] = [] + + for (const cb of celestialBodies) { + const resource = resourceByCelestialBody(map, cb) + if (resource) { + mineables.push({ + system, + starbase, + celestialBody: cb, + resource, + }) + } } - const planets = planetsByStarbase(map.planets, starbase) - const mineables = new Set() - - planets.forEach((planet) => { - const resources = resourcesByPlanet(map.resources, planet) - - resources.forEach((resource) => { - const mineItem = mineItemByResource(map.mineItems, resource) - - if (mineItem) { - mineables.add({ - starbase, - planet, - resource, - mineItem, - }) - } - }) - }) return mineables } +/** + * Get a specific mineable by coordinates and cargo ID + */ +export const mineableByCoordinatesAndCargoId = ( + map: WorldMap, + coordinates: Coordinates, + cargoId: number, +): Mineable | null => { + const mineables = mineablesByCoordinates(map, coordinates) + return mineables.find((m) => m.resource.cargoId === cargoId) ?? null +} + +/** + * Get a specific mineable by coordinates and name (legacy compatibility) + * In C4, resources are identified by cargoId, so name matching is approximate + */ export const mineableByCoordinates = ( map: WorldMap, coordinates: Coordinates, - name: string, -): Mineable => { + _name: string, +): Mineable | null => { const mineables = mineablesByCoordinates(map, coordinates) + // In C4, return first mineable at location + // TODO: Implement proper name matching with cargoType lookup + return mineables[0] ?? null +} + +/** + * Get all systems that have mineable resources as SectorInfo + * Used to derive dynamic sector list from on-chain data instead of hardcoded coords + * Only includes systems with starbases owned by a specific faction (not Unaligned) + */ +export const getMineableSystems = (map: WorldMap): SectorInfo[] => { + return map.systems + .filter((system) => { + // Filter out systems without valid coordinates + if (!system.coordinates || system.coordinates.length < 2) + return false - const res = Array.from(mineables).find((m) => getName(m.mineItem) === name) - if (!res) { - throw new Error(`No ${name} found at ${coordinates}`) + // Must have mineable resources + const bodies = celestialBodiesBySystem(map, system) + if (!bodies.some((cb) => map.resources.has(cb.key))) { + return false + } + + // Must have a starbase with a faction owner (not Unaligned) + const starbase = getStarbaseFromSystem(system) + if (!starbase || starbase.owner === null) { + return false // Skip Unaligned or no-starbase systems + } + + return true + }) + .map((system) => { + const coords = transformSector([ + system.coordinates[0], + system.coordinates[1], + ]) + const starbase = getStarbaseFromSystem(system) + // Use actual starbase faction (guaranteed to exist from filter) + const faction = starbase?.owner ?? 1 // Fallback to MUD if somehow null + return { + name: (system as { name?: string }).name ?? coords.toString(), + closestFaction: faction, + coordinates: coords, + } + }) +} + +/** + * Get the region ID from a star system + * Returns null if the system is in deep space (no region) + */ +export function getSystemRegionId( + system: AccountWithKey, +): number | null { + const region = system.region as + | { __option: string; value?: number } + | undefined + if (region?.__option === 'Some' && region.value !== undefined) { + return region.value } - return res + return null } -export const getMapContext = async (game: Game): Promise => { - const [starbases, pl, mI, res] = await Promise.all([ - getStarbases(game), - getPlanets(game), - getMineItems(game), - getResources(game), - ]) - - const planets = new Map>() - const resources = new Map>() - const mineItems = new Map() - - starbases.forEach((s) => { - const location = transformSector(s.data.sector) - const planetSet = planets.get(s.key.toBase58()) ?? new Set() - - pl.filter((p) => - transformSector(p.data.sector).equals(location), - ).forEach((p) => { - planetSet.add(p) - }) +/** + * Find a star system by its coordinates + */ +export function findSystemByCoordinates( + map: WorldMap, + coords: Coordinates, +): AccountWithKey | null { + return ( + map.systems.find((system) => { + if (!system.coordinates || system.coordinates.length < 2) { + return false + } + const sysCoords = transformSector([ + system.coordinates[0], + system.coordinates[1], + ]) + return sysCoords.x === coords.x && sysCoords.y === coords.y + }) ?? null + ) +} - planets.set(s.key.toBase58(), planetSet) - }) +/** + * Build the world map from on-chain data + */ +export const getMapContext = async (): Promise => { + console.log('getMapContext: fetching game...') + await fetchGame() // ensure game is loaded + console.log('getMapContext: fetching star systems...') + const systems = await fetchAllAccounts('StarSystem') + console.log( + `getMapContext: got ${systems.length} systems, fetching celestial bodies...`, + ) + const allCelestialBodies = await fetchAllAccounts('CelestialBody') + console.log( + `getMapContext: got ${allCelestialBodies.length} celestial bodies`, + ) - res.forEach((r) => { - const resourceSet = - resources.get(r.data.location.toBase58()) ?? new Set() + // Build starbases from systems + const starbases: StarbaseData[] = systems + .map((s) => getStarbaseFromSystem(s)) + .filter((sb): sb is StarbaseData => sb !== null) - mineItems.set( - r.key.toBase58(), - mI.find((m) => m.key.equals(r.data.mineItem))!, - ) + // Group celestial bodies by system + const celestialBodies = new Map[]>() + for (const cb of allCelestialBodies) { + const systemKey = cb.system as string + const existing = celestialBodies.get(systemKey) ?? [] + existing.push(cb) + celestialBodies.set(systemKey, existing) + } - resourceSet.add(r) - resources.set(r.data.location.toBase58(), resourceSet) - }) + // Extract resource info from celestial bodies (asteroids only) + const resources = new Map() + let asteroidCount = 0 + let planetCount = 0 + for (const cb of allCelestialBodies) { + const cbType = (cb.celestialBodyType as { __kind: string }).__kind + if (cbType === 'Asteroid') { + asteroidCount++ + } else if (cbType === 'Planet') { + planetCount++ + } + const resourceInfo = getResourceFromCelestialBody(cb) + if (resourceInfo) { + resources.set(cb.key, resourceInfo) + } + } + console.log( + `getMapContext: Celestial bodies - ${asteroidCount} asteroids, ${planetCount} planets, ${resources.size} with mineable resources`, + ) return { + systems, starbases, - planets, - mineItems, + celestialBodies, resources, } } diff --git a/src/main/basedbot/lib/sage/util.ts b/src/main/basedbot/lib/sage/util.ts index 7ba34323..3a84bb0e 100644 --- a/src/main/basedbot/lib/sage/util.ts +++ b/src/main/basedbot/lib/sage/util.ts @@ -1,16 +1,70 @@ -import { byteArrayToString } from '@staratlas/data-source' -import { Fleet, MineItem, Planet, Starbase } from '@staratlas/sage' +/** + * SAGE utility functions for C4 + */ -export const getName = (item: Fleet | Starbase | Planet | MineItem): string => { - if (!item?.data) { +import type { CelestialBody, Fleet, StarSystem } from '@staratlas/dev-sage' + +import type { AccountWithKey } from '../types.js' + +/** + * Convert byte array to string + */ +function byteArrayToString(bytes: Uint8Array | number[] | string): string { + if (typeof bytes === 'string') { + return bytes + } + + const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes) + + // Find null terminator + let end = arr.length + for (let i = 0; i < arr.length; i++) { + if (arr[i] === 0) { + end = i + break + } + } + + return new TextDecoder().decode(arr.slice(0, end)) +} + +type NamedAccount = + | AccountWithKey + | AccountWithKey + | AccountWithKey + +/** + * Get name from a named account + */ +export const getName = (item: NamedAccount | unknown): string => { + if (!item || typeof item !== 'object') { return 'N/A' } - const name = 'name' in item.data ? item.data.name : undefined - const fleetLabel = - 'fleetLabel' in item.data ? item.data.fleetLabel : undefined + const obj = item as Record + + // Try fleetLabel for Fleet + if ('fleetLabel' in obj && obj.fleetLabel) { + const label = obj.fleetLabel + if (typeof label === 'string') return label + if (label instanceof Uint8Array || Array.isArray(label)) { + return byteArrayToString(label as Uint8Array) + } + } - const dataToConvert = name ?? fleetLabel + // Try name for StarSystem + if ('name' in obj && obj.name) { + const name = obj.name + if (typeof name === 'string') return name + if (name instanceof Uint8Array || Array.isArray(name)) { + return byteArrayToString(name as Uint8Array) + } + } + + // Try key as fallback + if ('key' in obj && typeof obj.key === 'string') { + return obj.key.slice(0, 8) + '...' + } - return dataToConvert ? byteArrayToString(dataToConvert) : 'N/A' + return 'N/A' } diff --git a/src/main/basedbot/lib/types.ts b/src/main/basedbot/lib/types.ts new file mode 100644 index 00000000..c688869a --- /dev/null +++ b/src/main/basedbot/lib/types.ts @@ -0,0 +1,31 @@ +import type { Address, Lamports } from '@solana/kit' + +/** + * Account with its on-chain address attached + */ +export type AccountWithKey = T & { key: Address } + +/** + * Account with address and lamport balance info + */ +export type AccountWithKeyAndLamports = T & { + key: Address + lamports: Lamports + minLamports: Lamports +} + +/** + * Re-export common types from dev-sage for convenience + */ +export type { + CelestialBody, + Character, + Fleet, + Game, + RegionTracker, + StarbasePlayer, + StarSystem, +} from '@staratlas/dev-sage' + +export type { Profile, PlayerName } from '@staratlas/dev-player-profile' +export type { ProfileFactionAccount } from '@staratlas/dev-profile-faction' diff --git a/src/main/basedbot/lib/util/coordinates.ts b/src/main/basedbot/lib/util/coordinates.ts index edaee26c..ed9f8cda 100644 --- a/src/main/basedbot/lib/util/coordinates.ts +++ b/src/main/basedbot/lib/util/coordinates.ts @@ -1,66 +1,179 @@ -import BN from 'bn.js' import bs58 from 'bs58' +// C4 uses fixed-point coordinates with 56 fractional bits +const COORDINATE_SCALE = 2n ** 56n // 72057594037927936n + +// C4 coordinate with fixed-point representation +export type C4Coord = { + raw: bigint + fractionalBits: number + toNumber: () => number +} + +/** + * Coordinate helper class for C4 + * Can store either display integer values or raw on-chain values with full precision + */ export class Coordinates { - private readonly _x: BN - private readonly _y: BN + private readonly _x: bigint + private readonly _y: bigint + // When true, _x and _y are already in raw on-chain format (scaled by 2^56) + private readonly _isRaw: boolean public static fromString = (str: string): Coordinates => { const [x, y] = str.split(',') - - return new Coordinates(new BN(x, 10), new BN(y, 10)) + return new Coordinates(BigInt(x), BigInt(y), false) } - public static fromBN = (x: BN, y: BN): Coordinates => new Coordinates(x, y) + public static fromBigInt = (x: bigint, y: bigint): Coordinates => + new Coordinates(x, y, false) public static fromNumber = (x: number, y: number): Coordinates => - new Coordinates(x, y) - - private constructor(x: BN | number, y: BN | number) { - this._x = typeof x === 'number' ? new BN(x, 10) : x - this._y = typeof y === 'number' ? new BN(y, 10) : y - // logger.debug('Coordinates', { x: this._x.toNumber(), y: this._y.toNumber() }) + new Coordinates(BigInt(x), BigInt(y), false) + + /** + * Create from C4 coordinate tuple [bigint, bigint] (display values) + */ + public static fromTuple = (coords: [bigint, bigint]): Coordinates => + new Coordinates(coords[0], coords[1], false) + + /** + * Create from raw C4 coordinate values (already scaled by 2^56) + * Use this to preserve exact starbase coordinates for docking + */ + public static fromRaw = (xRaw: bigint, yRaw: bigint): Coordinates => + new Coordinates(xRaw, yRaw, true) + + /** + * Create from C4Coord objects (preserves full precision) + * Use this when getting coordinates from StarSystem accounts + */ + public static fromC4Coords = (coords: [C4Coord, C4Coord]): Coordinates => + new Coordinates(coords[0].raw, coords[1].raw, true) + + private constructor(x: bigint, y: bigint, isRaw: boolean) { + this._x = x + this._y = y + this._isRaw = isRaw } - private static toB58 = (bn: BN): string => - bs58.encode(bn.toTwos(64).toArrayLike(Buffer, 'le', 8)) + private static toB58 = (n: bigint): string => { + // Convert to 8-byte little-endian buffer (twos complement for negative) + const buffer = new ArrayBuffer(8) + const view = new DataView(buffer) + view.setBigInt64(0, n, true) // true = little-endian + return bs58.encode(new Uint8Array(buffer)) + } - get xBN(): BN { - return this._x + /** + * Get display X value (integer) + */ + get xBigInt(): bigint { + return this._isRaw ? this._x / COORDINATE_SCALE : this._x } - get yBN(): BN { - return this._y + /** + * Get display Y value (integer) + */ + get yBigInt(): bigint { + return this._isRaw ? this._y / COORDINATE_SCALE : this._y } get xB58(): string { - return Coordinates.toB58(this._x) + return Coordinates.toB58(this.xBigInt) } get yB58(): string { - return Coordinates.toB58(this._y) + return Coordinates.toB58(this.yBigInt) } + /** + * Get display X as number (integer, for logging/display) + */ get x(): number { - return this._x.toNumber() + return Number(this.xBigInt) } + /** + * Get display Y as number (integer, for logging/display) + */ get y(): number { - return this._y.toNumber() + return Number(this.yBigInt) } public distanceFrom = (other: Coordinates): number => { - const x = this._x.sub(other._x) - const y = this._y.sub(other._y) + // Compare display values (integers) + const dx = Number(this.xBigInt - other.xBigInt) + const dy = Number(this.yBigInt - other.yBigInt) + return Math.sqrt(dx * dx + dy * dy) + } - return Math.sqrt(x.mul(x).add(y.mul(y)).toNumber()) + /** + * Calculate precise distance using full raw coordinate precision + * Use this for docking range checks where sub-unit precision matters + */ + public preciseDistanceFrom = (other: Coordinates): number => { + // Use raw coordinates divided by scale for precise floating-point + const thisX = Number(this.xRaw) / Number(COORDINATE_SCALE) + const thisY = Number(this.yRaw) / Number(COORDINATE_SCALE) + const otherX = Number(other.xRaw) / Number(COORDINATE_SCALE) + const otherY = Number(other.yRaw) / Number(COORDINATE_SCALE) + const dx = thisX - otherX + const dy = thisY - otherY + return Math.sqrt(dx * dx + dy * dy) } public equals = (other: Coordinates): boolean => - this._x.eq(other._x) && this._y.eq(other._y) + this.xBigInt === other.xBigInt && this.yBigInt === other.yBigInt + + /** + * Check if coordinates are approximately equal (within 1 unit) + * Use this for comparing integer display coords with raw coords + */ + public nearEquals = (other: Coordinates): boolean => { + const dx = Math.abs(Number(this.xBigInt - other.xBigInt)) + const dy = Math.abs(Number(this.yBigInt - other.yBigInt)) + return dx <= 1 && dy <= 1 + } + + public toString = (): string => `${this.x},${this.y}` + + /** + * Convert to C4 tuple format for instructions (legacy - display values) + */ + public toTuple = (): [bigint, bigint] => [this.xBigInt, this.yBigInt] + + /** + * Convert to on-chain raw format (scaled by 2^56) + * Use this when sending coordinates in blockchain instructions + */ + public toRawTuple = (): [bigint, bigint] => { + if (this._isRaw) { + // Already in raw format + return [this._x, this._y] + } + // Scale display values to raw + return [this._x * COORDINATE_SCALE, this._y * COORDINATE_SCALE] + } - public toString = (): string => - `${this._x.toNumber()},${this._y.toNumber()}` + /** + * Get raw X coordinate for on-chain use + */ + get xRaw(): bigint { + return this._isRaw ? this._x : this._x * COORDINATE_SCALE + } - public toArray = (): [BN, BN] => [this._x, this._y] + /** + * Get raw Y coordinate for on-chain use + */ + get yRaw(): bigint { + return this._isRaw ? this._y : this._y * COORDINATE_SCALE + } + + /** + * Whether this coordinate preserves full precision (raw format) + */ + get isRaw(): boolean { + return this._isRaw + } } diff --git a/src/main/basedbot/lib/util/get-random-fleet.ts b/src/main/basedbot/lib/util/get-random-fleet.ts index 4fa57fdd..378be586 100644 --- a/src/main/basedbot/lib/util/get-random-fleet.ts +++ b/src/main/basedbot/lib/util/get-random-fleet.ts @@ -1,87 +1,219 @@ +/** + * Fleet selection utilities for C4 + * + * Selects ships from escrow based on faction and role. + */ + +import type { Game, Ship, StarbasePlayer } from '@staratlas/dev-sage' + import { logger } from '../../../../logger.js' -import { FleetShips } from '../sage/act/create-fleet.js' -import { ShipMake } from '../sage/ships.js' -import { Player } from '../sage/state/user-account.js' +import type { FleetShip } from '../sage/act/create-fleet.js' +import type { Player } from '../sage/state/user-account.js' +import type { AccountWithKey } from '../types.js' import { Faction } from './galaxy-sectors-data.js' +/** Ship role inferred from name */ +export type ShipRole = 'miner' | 'transport' | 'multi' | 'other' + +/** Ship info with role */ +interface ShipInfo { + id: number + name: string + role: ShipRole + sizeClass: number + cargoCapacity: bigint +} + +/** + * Infer ship role from name + */ +function inferRoleFromName(name: string): ShipRole { + const lower = name.toLowerCase() + + // Miners: Armstrong IMP series + if (lower.includes('armstrong') && lower.includes('imp')) return 'miner' + + // Transport ships + if ( + lower.includes('lowbie') || + lower.includes('sledbarge') || + lower.includes('packlite') || + lower.includes('niruch') || + lower.includes('jod asteris') || + lower.includes('sunpaa') || + lower.includes('rainbow om') || + lower.includes('rainbow arc') || + lower.includes('bitboat') || + lower.includes('maxhog') + ) + return 'transport' + + // Multi-role ships + if ( + lower.includes('guardian') || + lower.includes('compakt hero') || + lower.includes('default config') + ) + return 'multi' + + return 'other' +} + +/** + * Check if ship belongs to faction based on name prefix + */ +function isFactionShip(name: string, faction: Faction): boolean { + const lower = name.toLowerCase() + + // Neutral ships (all factions can use) + if ( + lower.startsWith('fimbul') || + lower.startsWith('rainbow') || + lower.startsWith('armstrong') || + lower.startsWith('calico') + ) + return true + + // Faction-specific + if (faction === Faction.MUD) { + return lower.startsWith('pearce') || lower.startsWith('opal') + } + if (faction === Faction.ONI) { + return lower.startsWith('busan') || lower.startsWith('ogrika') + } + if (faction === Faction.UST) { + return lower.startsWith('vzus') || lower.startsWith('opal') + } + + return false +} + +/** + * Get ship info from game definitions + */ +function getShipInfo(ship: Ship): ShipInfo { + return { + id: ship.id, + name: ship.name, + role: inferRoleFromName(ship.name), + sizeClass: ship.sizeClass, + cargoCapacity: ship.stats.cargoStats.cargoCapacity, + } +} + +/** + * Get fleet for a player based on mode + * + * @param player - Player context + * @param mode - Fleet purpose (mine or transport) + * @param game - Game config with ship definitions + * @param starbasePlayer - Player's starbase account (optional, for escrow check) + * @returns Array of ships for fleet creation + */ export const getRandomFleet = ( player: Player, mode: 'mine' | 'transport', -): FleetShips => { - const { faction, shipData } = player - - const factionMakes: Record = { - [Faction.MUD]: [ - 'Fimbul', - 'Fimbul BYOS', - 'Fimbul ECOS', - 'Rainbow', - 'Armstrong Industries', - 'Pearce', - 'Calico', - 'Opal', - ], - [Faction.ONI]: [ - 'Fimbul', - 'Fimbul BYOS', - 'Fimbul ECOS', - 'Rainbow', - 'Armstrong Industries', - 'Busan', - 'Calico', - 'Ogrika', - ], - [Faction.UST]: [ - 'Fimbul', - 'Fimbul BYOS', - 'Fimbul ECOS', - 'Rainbow', - 'Armstrong Industries', - 'VZUS', - 'Ogrika', - 'Opal', - ], + game?: AccountWithKey, + starbasePlayer?: AccountWithKey, +): FleetShip[] => { + if (!game) { + logger.warn('getRandomFleet: no game config provided') + return [] } - const factionShips = shipData.filter((ship) => - factionMakes[faction]?.includes(ship.make), - ) + const shipDefs = game.shipDefinitions.ships.unsizedList as Ship[] + const faction = player.faction - // Filter by mode (mining or transport) - const roleShips = factionShips.filter((value) => { - if (mode === 'mine') - return value.role === 'miner' || value.role === 'multi-role' - if (mode === 'transport') - return ( - value.role === 'freighter' || - value.role === 'transport' || - value.role === 'multi-role' - ) - return false - }) - logger.info(`Selecting ${mode} fleet out of ${roleShips.length} options`) + // Get all ship info + const allShips = shipDefs.map(getShipInfo) - let fleetSize = 0 - const fleet: FleetShips = [] + // Filter by faction + const factionShips = allShips.filter((s) => isFactionShip(s.name, faction)) - while (fleetSize < 145 && fleet.length <= 5) { - let shipAdded = false + if (factionShips.length === 0) { + logger.warn(`No faction ships found for ${faction}`) + // Fallback to all ships + factionShips.push(...allShips) + } - const randomIndex = Math.floor(Math.random() * roleShips.length) - const shipData = roleShips[randomIndex] + // Filter by role + const acceptableRoles: ShipRole[] = + mode === 'mine' ? ['miner', 'multi'] : ['transport', 'multi'] - if (fleetSize + shipData.size <= 145) { - fleet.push({ - shipMint: shipData.mint, - count: 1, - }) + let roleShips = factionShips.filter((s) => acceptableRoles.includes(s.role)) - fleetSize += shipData.size - shipAdded = true + if (roleShips.length === 0) { + logger.warn(`No ${mode} ships found, using faction ships`) + roleShips = factionShips + } + + // If we have starbasePlayer, filter by what's actually in escrow + let candidates = roleShips + if (starbasePlayer) { + const escrow = starbasePlayer.shipsInEscrow + if (escrow && escrow.size > 0) { + const escrowIds = new Set([...escrow.keys()]) + const inEscrow = roleShips.filter((s) => escrowIds.has(s.id)) + if (inEscrow.length > 0) { + candidates = inEscrow + logger.debug( + `Found ${inEscrow.length} ${mode} ships in escrow: ${inEscrow.map((s) => s.name).join(', ')}`, + ) + } } + } - if (!shipAdded) break + // Sort by cargo capacity for transport, random for mining + if (mode === 'transport') { + candidates.sort((a, b) => (a.cargoCapacity > b.cargoCapacity ? -1 : 1)) + } else { + // Shuffle for variety + candidates = candidates.sort(() => Math.random() - 0.5) } + // Build fleet up to size limit (game max is ~145, use 100 for safety margin) + const fleet: FleetShip[] = [] + let fleetSize = 0 + const maxSize = 100 + + for (const ship of candidates) { + if (fleet.length >= 5) break // Max 5 ship types per fleet + + const shipSize = ship.sizeClass * ship.sizeClass + if (fleetSize + shipSize <= maxSize) { + // Check how many are in escrow + let amount = 1 + if (starbasePlayer?.shipsInEscrow) { + const available = starbasePlayer.shipsInEscrow.get(ship.id) + if (available && available > 0n) { + // Calculate max that fits + const maxFit = Math.floor((maxSize - fleetSize) / shipSize) + amount = Math.min(maxFit, Number(available)) + } + } + + if (amount > 0) { + fleet.push({ id: ship.id, amount }) + fleetSize += shipSize * amount + logger.debug( + `Selected: ${ship.name} (id=${ship.id}) x${amount}, role=${ship.role}, size=${shipSize}`, + ) + } + } + } + + if (fleet.length === 0) { + // Fallback: just use first available ship + const firstShip = shipDefs[0] + if (firstShip) { + logger.warn(`Fallback: using first ship ${firstShip.name}`) + return [{ id: firstShip.id, amount: 1 }] + } + } + + logger.info( + `Selected ${mode} fleet: ${fleet.map((f) => `${f.id}x${f.amount}`).join(', ')} (size=${fleetSize})`, + ) + return fleet } diff --git a/src/main/basedbot/lib/util/pod-cleanup.ts b/src/main/basedbot/lib/util/pod-cleanup.ts deleted file mode 100644 index 40f9bec8..00000000 --- a/src/main/basedbot/lib/util/pod-cleanup.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { Account, TOKEN_PROGRAM_ID } from '@solana/spl-token' -import { Connection, PublicKey } from '@solana/web3.js' -import { CargoIDLProgram, CargoType } from '@staratlas/cargo' -import { - AsyncSigner, - createAssociatedTokenAccountIdempotent, - getParsedTokenAccountsByOwner, - InstructionReturn, -} from '@staratlas/data-source' -import { - getCargoPodsByAuthority, - SageIDLProgram, - StarbasePlayer, -} from '@staratlas/sage' -import BN from 'bn.js' - -interface PodCleanup { - mainPod: PublicKey - podsAndTokensToClean: [PublicKey, Account[]][] - cargoSeqId: number -} - -export const getCleanPodsByStarbasePlayerAccounts = async ( - connection: Connection, - cargoProgram: CargoIDLProgram, - starbasePlayer: PublicKey, -): Promise => { - const cargoPods = await getCargoPodsByAuthority( - connection, - cargoProgram, - starbasePlayer, - ) - if (cargoPods.length > 1) { - let podsToClean = cargoPods.map((cargoPod) => { - if (cargoPod.type === 'error') - throw new Error('Error reading CargoPod account') - return cargoPod.data - }) - const mainPod = podsToClean.reduce((prev, current) => { - return prev.data.openTokenAccounts > current.data.openTokenAccounts - ? prev - : current - }) - podsToClean = podsToClean.filter((it) => !it.key.equals(mainPod.key)) - const podsAndTokensToClean: Array<[PublicKey, Account[]]> = - await Promise.all( - podsToClean.map(async (thisPod) => { - const podTokenAccounts = - await getParsedTokenAccountsByOwner( - connection, - thisPod.key, - ) - return [thisPod.key, podTokenAccounts] - }), - ) - - return { - mainPod: mainPod.key, - podsAndTokensToClean, - cargoSeqId: mainPod.data.seqId, - } - } - return undefined -} - -export const getPodCleanupInstructions = ( - podCleanup: PodCleanup, - sageProgram: SageIDLProgram, - cargoProgram: CargoIDLProgram, - starbasePlayer: PublicKey, - starbase: PublicKey, - playerProfile: PublicKey, - profileFaction: PublicKey, - cargoStatsDefinition: PublicKey, - gameId: PublicKey, - gameState: PublicKey, - key: AsyncSigner, - keyIndex: number, -) => { - const cleanInstructions: InstructionReturn[] = [] - const newTokenAccounts: string[] = [] - if (podCleanup.podsAndTokensToClean.length > 1) { - for ( - let index2 = 0; - index2 < podCleanup.podsAndTokensToClean.length; - index2++ - ) { - const element = podCleanup.podsAndTokensToClean[index2] - const thisPodKey = element[0] - const podTokenAccounts = element[1] - if (podTokenAccounts.length > 0) { - for ( - let index3 = 0; - index3 < podTokenAccounts.length; - index3++ - ) { - const tokenData = podTokenAccounts[index3] - const cargoType = CargoType.findAddress( - cargoProgram, - cargoStatsDefinition, - tokenData.mint, - podCleanup.cargoSeqId, - )[0] - if (Number(tokenData.delegatedAmount) > 0) { - const tokenTo = createAssociatedTokenAccountIdempotent( - tokenData.mint, - podCleanup.mainPod, - true, - TOKEN_PROGRAM_ID, - ) - const tokenToBase58 = tokenTo.address.toBase58() - if (!newTokenAccounts.includes(tokenToBase58)) { - newTokenAccounts.push(tokenToBase58) - cleanInstructions.push(tokenTo.instructions) - } - const transferIx = - StarbasePlayer.transferCargoAtStarbase( - sageProgram, - cargoProgram, - starbasePlayer, - key, - 'funder', - playerProfile, - profileFaction, - starbase, - thisPodKey, - podCleanup.mainPod, - cargoType, - cargoStatsDefinition, - tokenData.address, - tokenTo.address, - tokenData.mint, - gameId, - gameState, - { - amount: new BN( - tokenData.delegatedAmount.toString(), - ), - keyIndex, - }, - ) - cleanInstructions.push(transferIx) - } else { - const closeIx = - StarbasePlayer.closeStarbaseCargoTokenAccount( - sageProgram, - cargoProgram, - starbasePlayer, - key, - 'funder', - playerProfile, - profileFaction, - starbase, - thisPodKey, - cargoType, - cargoStatsDefinition, - tokenData.address, - tokenData.mint, - gameId, - gameState, - { - keyIndex, - }, - ) - cleanInstructions.push(closeIx) - } - } - } - cleanInstructions.push( - StarbasePlayer.removeCargoPod( - sageProgram, - cargoProgram, - starbasePlayer, - key, - playerProfile, - profileFaction, - 'funder', - starbase, - thisPodKey, - gameId, - gameState, - { - keyIndex, - }, - ), - ) - } - } - return cleanInstructions -} diff --git a/src/main/basedbot/lib/util/profile.ts b/src/main/basedbot/lib/util/profile.ts index 74ffdb00..0994b3b1 100644 --- a/src/main/basedbot/lib/util/profile.ts +++ b/src/main/basedbot/lib/util/profile.ts @@ -1,368 +1,200 @@ -import { Keypair, PublicKey } from '@solana/web3.js' +/** + * Profile utilities for C4 + * Creates player profiles, sets names, chooses factions, and registers characters + */ + +import { getTransferSolInstruction } from '@solana-program/system' +import { type Address, type Instruction, type KeyPairSigner } from '@solana/kit' import { - AsyncSigner, - InstructionReturn, - ixReturnsToIxs, - keypairToAsyncSigner, - readAllFromRPC, -} from '@staratlas/data-source' + findPlayerNamePda, + getCreateProfileInstruction, + getSetNameInstruction, + PLAYER_PROFILE_PROGRAM_ADDRESS, +} from '@staratlas/dev-player-profile' import { - PlayerProfile, - ProfileKeyInput, - ProfilePermissions, -} from '@staratlas/player-profile' -import { UserPoints } from '@staratlas/points' -import { ProfileFactionAccount } from '@staratlas/profile-faction' + Faction as SdkFaction, + findProfileFactionAccountPda, + getChooseFactionInstruction, +} from '@staratlas/dev-profile-faction' import { - Game, - SagePlayerProfile, - Starbase, - StarbasePlayer, -} from '@staratlas/sage' -import { airdrop, airdropCrew, airdropSol } from '../../../../lib/airdrop.js' + findCharacterPda, + getRegisterCharacterInstruction, + type Game, +} from '@staratlas/dev-sage' + import { logger } from '../../../../logger.js' -import { connection } from '../../../../service/sol/index.js' import { sendAndConfirmInstructions } from '../../../../service/sol/send-and-confirm-tx.js' -import { programs, StarAtlasPrograms } from '../programs.js' -import { starbaseByCoordinates } from '../sage/state/starbase-by-coordinates.js' -import { Faction, galaxySectorsData } from './galaxy-sectors-data.js' -import { config } from '../../../../config/index.js' - -export const getStarbasePlayer = async ( - keyPair: Keypair, - game: Game, - playerProfile: PublicKey, - profileFaction: PublicKey, - starbase: Starbase, - programs: StarAtlasPrograms, -): Promise => { - const starbasePlayers = await readAllFromRPC( - connection, - programs.sage, - StarbasePlayer, - 'processed', - [ - { - memcmp: { - offset: 9, - bytes: playerProfile.toBase58(), - }, - }, - { - memcmp: { - offset: 73, - bytes: starbase.key.toBase58(), - }, - }, - ], - ) - - if (starbasePlayers.length > 1) { - throw new Error('Multiple starbase players found') - } - const [starbasePlayer] = starbasePlayers - - if (!starbasePlayer) { - const [sageProfileAddress] = SagePlayerProfile.findAddress( - programs.sage, - playerProfile, - game.key, - ) - const [starbasePlayerAddress] = StarbasePlayer.findAddress( - programs.sage, - starbase.key, - sageProfileAddress, - starbase.data.seqId, - ) - - const instructionReturns = [ - StarbasePlayer.registerStarbasePlayer( - programs.sage, - profileFaction, - sageProfileAddress, - starbase.key, - game.key, - game.data.gameState, - starbase.data.seqId, - ), - StarbasePlayer.createCargoPod( - programs.sage, - programs.cargo, - starbasePlayerAddress, - keypairToAsyncSigner(keyPair), - playerProfile, - profileFaction, - starbase.key, - game.data.cargo.statsDefinition, - game.key, - game.data.gameState, - { - keyIndex: 0, - podSeeds: Array.from( - Keypair.generate().publicKey.toBuffer(), - ), - }, - ), - ] - - logger.warn('Starbase player not found, creating', { - player: playerProfile.toBase58(), - starbase: starbase.key.toBase58(), - }) - await sendAndConfirmInstructions([keyPair])( - await ixReturnsToIxs( - instructionReturns, - keypairToAsyncSigner(keyPair), - ), - ) - - return getStarbasePlayer( - keyPair, - game, - playerProfile, - profileFaction, - starbase, - programs, - ) - } - - if (starbasePlayer.type === 'error') { - throw new Error('Error reading starbasePlayer account') - } - - return starbasePlayer.data +import type { AccountWithKey } from '../types.js' +import { Faction } from './galaxy-sectors-data.js' + +/** + * Convert a string to a byte array for player name + */ +function stringToByteArray(input: string): number[] { + const nameBytes = new TextEncoder().encode(input) + return Array.from(nameBytes) } -const getCategory = (category: any): PublicKey => { - if ('category' in category) { - return new PublicKey(category.category) +/** + * Map fleetbot's Faction enum to SDK's Faction enum + */ +function mapFactionToSdk(faction: Faction): SdkFaction { + switch (faction) { + case Faction.MUD: + return SdkFaction.Mud + case Faction.ONI: + return SdkFaction.Oni + case Faction.UST: + return SdkFaction.Ustur + default: + throw new Error(`Unknown faction: ${faction}`) } - - throw new Error('Category not found') } -const getCreatePlayerPointsAccountsIxs = ( - game: Game, - character: PublicKey, -): InstructionReturn[] => { - const pilotXPAccountIx = createCharacterUserPointAccount( - character, - getCategory(game.data.points.pilotXpCategory), - ) - - const dataRunningXPAccountIx = createCharacterUserPointAccount( - character, - getCategory(game.data.points.dataRunningXpCategory), - ) - - const councilRankXpAccountIx = createCharacterUserPointAccount( - character, - getCategory(game.data.points.councilRankXpCategory), - ) - - const miningXpAccountIx = createCharacterUserPointAccount( - character, - getCategory(game.data.points.miningXpCategory), - ) - - const craftingXpAccountIx = createCharacterUserPointAccount( - character, - getCategory(game.data.points.craftingXpCategory), - ) - - const lpAccountIx = createCharacterUserPointAccount( - character, - getCategory(game.data.points.lpCategory), - ) - - return [ - pilotXPAccountIx.instructions, - dataRunningXPAccountIx.instructions, - councilRankXpAccountIx.instructions, - miningXpAccountIx.instructions, - craftingXpAccountIx.instructions, - lpAccountIx.instructions, - ] -} - -export const createNewCharacter = ( - characterProfileKeypair: AsyncSigner, - signer: AsyncSigner, -): { - character: PublicKey - instruction: InstructionReturn -} => { - logger.info(characterProfileKeypair.publicKey().toBase58()) - logger.info(signer.publicKey().toBase58()) - const instructions = PlayerProfile.createProfile( - programs.playerProfile, - characterProfileKeypair, - [ - { - key: signer, - expireTime: null, - scope: programs.playerProfile.programId, - permissions: ProfilePermissions.all(), - }, - ], - 1, - ) - - return { - character: characterProfileKeypair.publicKey(), - instruction: instructions, - } -} - -export const setCharacterName = ( - signer: AsyncSigner, - character: { - key: PublicKey - index: number - }, - name: string, -) => { - const keyInput: ProfileKeyInput = { - playerProfileProgram: programs.playerProfile, - profileKey: character.key, - key: signer, - keyIndex: character.index, - } - - return PlayerProfile.setName(programs.playerProfile, keyInput, name) -} - -export const setCharacterFaction = ( - signer: AsyncSigner, - character: { - key: PublicKey - index: number - }, - faction: Faction, -) => { - const keyInput: ProfileKeyInput = { - playerProfileProgram: programs.playerProfile, - profileKey: character.key, - key: signer, - keyIndex: character.index, - } - - return ProfileFactionAccount.chooseFaction( - programs.profileFaction, - keyInput, - faction.valueOf(), - ) -} - -export const createCharacterUserPointAccount = ( - player: PublicKey, - category: PublicKey, -) => { - return UserPoints.createUserPointAccount(programs.points, player, category) +/** + * Result of profile creation + */ +export interface ProfileCreationResult { + profileKey: Address + characterKey: Address } +/** + * Create and initialize a new character profile for C4 + * + * This creates: + * 1. A new player profile account + * 2. Sets the player name + * 3. Chooses a faction + * 4. Registers a character for the game + * + * @param game - The game account + * @param name - Character name (max 32 bytes) + * @param faction - Faction to join (MUD, ONI, or UST) + * @param signer - The wallet signer (will be the profile authority) + * @param profileSigner - Deterministic profile keypair (e.g. from getProfileKeyPair()) + * @returns The profile and character addresses + */ export const createAndInitializeCharacter = async ( - game: Game, + game: AccountWithKey, name: string, faction: Faction, - keypair: Keypair, -) => { - await airdropSol( - config.app.airdropUrl, - config.app.airdropToken, - keypair.publicKey, - 10, - ) - const signer = keypairToAsyncSigner(keypair) - const characterKeyPair = Keypair.generate() - const characterProfileSigner = keypairToAsyncSigner(characterKeyPair) - const characterInstruction = createNewCharacter( - characterProfileSigner, - signer, - ) - const character = characterInstruction.character - const nameIx = setCharacterName(signer, { key: character, index: 0 }, name) - const factionIx = setCharacterFaction( - signer, - { key: character, index: 0 }, - faction, - ) - if (!nameIx || !factionIx || !nameIx.instructions.length) { - throw new Error('Failed to create name or faction instruction') - } - const userPointsIxs = getCreatePlayerPointsAccountsIxs(game, character) - const profileIx = SagePlayerProfile.registerSagePlayerProfile( - programs.sage, - character, - game.key, - game.data.gameState, - ) - - await ixReturnsToIxs([characterInstruction.instruction], signer).then( - sendAndConfirmInstructions([keypair, characterKeyPair]), - ) - - await ixReturnsToIxs( - [ - nameIx.instructions, - factionIx.instructions, - profileIx, - ...userPointsIxs, - ], - signer, - ).then(sendAndConfirmInstructions([keypair])) - - const homeCoordinates = galaxySectorsData() - .filter((sector) => sector.closestFaction === faction) - .find((sector) => sector.name.includes('CSS'))?.coordinates - - if (!homeCoordinates) { - throw new Error('No home coordinates found') + signer: KeyPairSigner, + profileSigner: KeyPairSigner, +): Promise => { + logger.info(`Creating new profile for ${signer.address}`) + logger.info(`Character name: ${name}, Faction: ${Faction[faction]}`) + + // Validate name length + const nameBytes = new TextEncoder().encode(name) + if (nameBytes.length > 32) { + throw new Error( + `Character name too long: ${nameBytes.length} bytes (max 32)`, + ) } - - const homeStarbase = await starbaseByCoordinates(homeCoordinates) - - if (!homeStarbase) { - throw new Error('No home starbase found') + if (nameBytes.length === 0) { + throw new Error('Character name cannot be empty') } - const profiles = await readAllFromRPC( - connection, - programs.playerProfile, - PlayerProfile, - 'processed', - [ - { - memcmp: { - offset: PlayerProfile.MIN_DATA_SIZE + 2, - bytes: keypair.publicKey.toBase58(), - }, - }, - ], - ) - const [profile] = profiles + const instructions: Instruction[] = [] - await getStarbasePlayer( - keypair, - game, - profile.key, - factionIx.faction[0], - homeStarbase, - programs, - ) + const profileAddress = profileSigner.address + logger.info(`Using profile address: ${profileAddress}`) - await airdrop( - config.app.airdropUrl, - config.app.airdropToken, - keypair.publicKey, - ) - - await airdropCrew( - config.app.airdropUrl, - config.app.airdropToken, - keypair.publicKey, - 10000, - ) + // 1. Create profile with permissions + // Permissions hardcoded as in reference: [255, 15, 0, 0, 0, 0, 0, 128] = all permissions + instructions.push( + getCreateProfileInstruction({ + funder: signer, + profile: profileSigner, + keyPermissions: [ + { + expireTime: -1, + scope: PLAYER_PROFILE_PROGRAM_ADDRESS, + permissions: [255, 15, 0, 0, 0, 0, 0, 128], + }, + ], + keyThreshold: 1, + remainingAccounts: [signer.address], + }), + ) + + // Derive PDAs + const [characterPda] = await findCharacterPda({ + playerProfile: profileAddress, + gameId: game.key, + }) + const [playerNamePda] = await findPlayerNamePda({ profile: profileAddress }) + const [profileFactionAccount] = await findProfileFactionAccountPda({ + profile: profileAddress, + }) + + logger.info(`Character PDA: ${characterPda}`) + logger.info(`Player name PDA: ${playerNamePda}`) + logger.info(`Profile faction PDA: ${profileFactionAccount}`) + + // 2. Transfer SOL to profile for transaction fees + // Profile account needs SOL for C4 actions + const solAmount = 1_000_000_000n // 1 SOL + instructions.push( + getTransferSolInstruction({ + source: signer, + destination: profileAddress, + amount: solAmount, + }), + ) + logger.info(`Transferring ${Number(solAmount) / 1e9} SOL to profile`) + + // 3. Set player name + instructions.push( + getSetNameInstruction({ + profile: profileAddress, + funder: signer, + key: signer, + name: playerNamePda, + keyIndex: 0, + nameArg: stringToByteArray(name), + remainingAccounts: [signer.address], + }), + ) + + // 4. Choose faction + const sdkFaction = mapFactionToSdk(faction) + instructions.push( + getChooseFactionInstruction({ + faction: sdkFaction, + factionAccount: profileFactionAccount, + profileValidationProfile: profileAddress, + profileValidationSigner: signer, + keyIndex: 0, + }), + ) + + // 5. Register character + instructions.push( + getRegisterCharacterInstruction({ + game: game.key, + profile: profileAddress, + character: characterPda, + funder: signer, + }), + ) + + logger.info( + `Sending profile creation transaction with ${instructions.length} instructions`, + ) + + // Send and confirm the transaction + // The profileSigner needs to sign, so we add it to the transaction + // Note: sendAndConfirmInstructions expects all signers to be embedded in instructions + await sendAndConfirmInstructions(signer)(instructions) + + logger.info(`Profile created successfully!`) + logger.info(`Profile: ${profileAddress}`) + logger.info(`Character: ${characterPda}`) - return profiles + return { + profileKey: profileAddress, + characterKey: characterPda, + } } diff --git a/src/main/fleetbot/fleetbot.ts b/src/main/fleetbot/fleetbot.ts deleted file mode 100644 index 8c6a212b..00000000 --- a/src/main/fleetbot/fleetbot.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { CronJob } from 'cron' - -import { config } from '../../config/index.js' -import * as db from '../../db/index.js' -import { - checkTransactions, - refill, - stockResources, - telegramBot, -} from '../../lib/index.js' -import { logger } from '../../logger.js' -import { initOrderBook } from '../../service/gm/index.js' - -let refillCronJob: CronJob | undefined, - resourcesCronJob: CronJob | undefined, - transactionCronJob: CronJob | undefined - -export const create = async (): Promise => { - logger.info('Starting fleetbot...') - await db.connect() -} - -export const stop = async (): Promise => { - try { - logger.info('Stopping fleetbot...') - if (resourcesCronJob) { - resourcesCronJob.stop() - } - if (transactionCronJob) { - transactionCronJob.stop() - } - if (refillCronJob) { - refillCronJob.stop() - } - await db.close() - } catch (e) { - logger.error(e) - } -} - -export const start = async (): Promise => { - await initOrderBook() - // https://github.com/telegraf/telegraf/issues/1749 - telegramBot.launch().catch((e) => logger.error(e)) - - resourcesCronJob = CronJob.from({ - cronTime: config.cron.resourceInterval, - onTick: stockResources, - runOnInit: config.app.quickstart, - start: true, - }) - refillCronJob = CronJob.from({ - cronTime: config.cron.refillInterval, - onTick: refill, - runOnInit: config.app.quickstart, - start: true, - }) - transactionCronJob = CronJob.from({ - cronTime: config.cron.bookkeeperInterval, - onTick: checkTransactions, - runOnInit: config.app.quickstart, - start: true, - }) -} diff --git a/src/main/fleetbot/index.ts b/src/main/fleetbot/index.ts deleted file mode 100644 index 083cc804..00000000 --- a/src/main/fleetbot/index.ts +++ /dev/null @@ -1,66 +0,0 @@ -import 'reflect-metadata' -import { Sentry } from '../../sentry.js' // import this as early as possible to catch early startup errors - -import { logger } from '../../logger.js' - -import * as app from './fleetbot.js' - -const stop = async (signal?: NodeJS.Signals) => { - logger.info(`Shutting down${signal ? ` (${signal})` : ''}`) - - try { - await app.stop() - } catch (error) { - Sentry.captureException(error) - logger.error('Close failed') - logger.error((error as Error).stack) - - process.exitCode = 1 - } - process.exit() -} - -const start = async () => { - try { - await app.create() - await app.start() - } catch (error) { - Sentry.captureException(error) - logger.error((error as Error).stack) - - process.exitCode = 1 - await stop() - } -} - -process.on( - 'unhandledRejection', - async (reason: any | null | undefined, _promise: Promise) => { - logger.error('Unhandled rejection') - - if (reason) { - const { message }: { message: string } = reason - - if (message.includes('Event listener')) { - return - } - logger.error(message) - } - - Sentry.captureException(reason) - await stop() - }, -) - -process.on('uncaughtException', async (error) => { - Sentry.captureException(error) - logger.error('Uncaught exception') - logger.error(error.stack) - - await stop() -}) - -process.on('SIGINT', stop) -process.on('SIGTERM', stop) - -start() diff --git a/src/service/fleet/const/amounts.ts b/src/service/fleet/const/amounts.ts deleted file mode 100644 index 490d75bd..00000000 --- a/src/service/fleet/const/amounts.ts +++ /dev/null @@ -1,8 +0,0 @@ -import Big from 'big.js' - -export type Amounts = { - food: Big - fuel: Big - ammo: Big - tool: Big -} diff --git a/src/service/fleet/const/index.ts b/src/service/fleet/const/index.ts deleted file mode 100644 index 932fb1d5..00000000 --- a/src/service/fleet/const/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './amounts.js' diff --git a/src/service/fleet/get-remaining-details.ts b/src/service/fleet/get-remaining-details.ts deleted file mode 100644 index 7cbe1958..00000000 --- a/src/service/fleet/get-remaining-details.ts +++ /dev/null @@ -1,275 +0,0 @@ -import { ScoreVarsShipInfo, ShipStakingInfo } from '@staratlas/factory' - -export interface ResourceStats { - food: Stats - tool: Stats - fuel: Stats - ammo: Stats -} - -export interface Stats { - unitsBurnt: number - unitsLeftPct: number - unitsLeft: number - secondsLeft: number - totalSeconds: number - maxSeconds: number - maxUnits: number - burnRatePerShip: number - burnRatePerFleet: number -} - -export const getTimePass = (fleet: ShipStakingInfo): number => { - const now = Date.now() / 1000 - const tripStart = fleet.currentCapacityTimestamp.toNumber() - - return now - tripStart -} -export const getRemainFoodSec = ( - fleet: ShipStakingInfo, - tp: number | undefined = undefined, -): number => { - const timePass = tp === undefined ? getTimePass(fleet) : tp - - return fleet.foodCurrentCapacity.toNumber() - timePass -} - -export const getRemainArmsSec = ( - fleet: ShipStakingInfo, - tp: number | undefined = undefined, -): number => { - const timePass = tp === undefined ? getTimePass(fleet) : tp - - return fleet.armsCurrentCapacity.toNumber() - timePass -} - -export const getRemainFuelSec = ( - fleet: ShipStakingInfo, - tp: number | undefined = undefined, -): number => { - const timePass = tp === undefined ? getTimePass(fleet) : tp - - return fleet.fuelCurrentCapacity.toNumber() - timePass -} - -export const getRemainHealthSec = ( - fleet: ShipStakingInfo, - tp: number | undefined = undefined, -): number => { - const timePass = tp === undefined ? getTimePass(fleet) : tp - - return fleet.healthCurrentCapacity.toNumber() - timePass -} - -export const timePassSinceLastAction = (fleet: ShipStakingInfo): number => { - let timePassSinceStart = getTimePass(fleet) - - const [foodRemainSec, armsRemainSec, fuelRemainSec, healthRemainSec] = [ - getRemainFoodSec(fleet), - getRemainArmsSec(fleet), - getRemainFuelSec(fleet), - getRemainHealthSec(fleet), - ] - - const depletionTime = Math.min( - foodRemainSec, - armsRemainSec, - fuelRemainSec, - healthRemainSec, - ) - - if (depletionTime < 0) { - timePassSinceStart = depletionTime + timePassSinceStart - } - - return timePassSinceStart -} - -export const getRemainFoodDetails = ( - shipInfo: ScoreVarsShipInfo, - fleet: ShipStakingInfo, - timePassSinceStart: number, -): Stats => { - const secondsLeft = getRemainFoodSec(fleet, timePassSinceStart) - const unitsBurnRate = 1 / (shipInfo.millisecondsToBurnOneFood / 1000) // Per Second - const burnRatePerFleet = - 1 / - (shipInfo.millisecondsToBurnOneFood / - fleet.shipQuantityInEscrow.toNumber() / - 1000) - const unitsBurnt = - unitsBurnRate * - timePassSinceStart * - fleet.shipQuantityInEscrow.toNumber() - const unitsLeft = - unitsBurnRate * secondsLeft * fleet.shipQuantityInEscrow.toNumber() - const unitsLeftPct = - unitsLeft / - (shipInfo.foodMaxReserve * fleet.shipQuantityInEscrow.toNumber()) - const totalSeconds = fleet.foodCurrentCapacity.toNumber() - const maxSeconds = - shipInfo.foodMaxReserve * - fleet.shipQuantityInEscrow.toNumber() * - (shipInfo.millisecondsToBurnOneFood / - 1000 / - fleet.shipQuantityInEscrow.toNumber()) - const maxUnits = - shipInfo.foodMaxReserve * fleet.shipQuantityInEscrow.toNumber() - - return { - unitsBurnt, - unitsLeftPct, - unitsLeft, - secondsLeft: Math.max(0, secondsLeft), - totalSeconds, - maxSeconds, - maxUnits, - burnRatePerShip: unitsBurnRate, - burnRatePerFleet, - } -} - -export const getRemainArmsDetails = ( - shipInfo: ScoreVarsShipInfo, - fleet: ShipStakingInfo, - timePassSinceStart: number, -): Stats => { - const secondsLeft = getRemainArmsSec(fleet, timePassSinceStart) - const unitsBurnRate = 1 / (shipInfo.millisecondsToBurnOneArms / 1000) // Per Second - const unitsBurnt = - unitsBurnRate * - timePassSinceStart * - fleet.shipQuantityInEscrow.toNumber() - const burnRatePerFleet = - 1 / - (shipInfo.millisecondsToBurnOneArms / - 1000 / - fleet.shipQuantityInEscrow.toNumber()) - const unitsLeft = - unitsBurnRate * secondsLeft * fleet.shipQuantityInEscrow.toNumber() - const unitsLeftPct = - unitsLeft / - (shipInfo.armsMaxReserve * fleet.shipQuantityInEscrow.toNumber()) - const maxSeconds = - shipInfo.armsMaxReserve * - fleet.shipQuantityInEscrow.toNumber() * - (shipInfo.millisecondsToBurnOneArms / - 1000 / - fleet.shipQuantityInEscrow.toNumber()) - const totalSeconds = fleet.armsCurrentCapacity.toNumber() - const maxUnits = - shipInfo.armsMaxReserve * fleet.shipQuantityInEscrow.toNumber() - - return { - unitsBurnt, - unitsLeftPct, - unitsLeft, - secondsLeft: Math.max(0, secondsLeft), - totalSeconds, - maxSeconds, - maxUnits, - burnRatePerShip: unitsBurnRate, - burnRatePerFleet, - } -} - -export const getRemainFuelDetails = ( - shipInfo: ScoreVarsShipInfo, - fleet: ShipStakingInfo, - timePassSinceStart: number, -): Stats => { - const secondsLeft = getRemainFuelSec(fleet, timePassSinceStart) - const unitsBurnRate = 1 / (shipInfo.millisecondsToBurnOneFuel / 1000) // Per Second - const unitsBurnt = - unitsBurnRate * - timePassSinceStart * - fleet.shipQuantityInEscrow.toNumber() - const burnRatePerFleet = - 1 / - (shipInfo.millisecondsToBurnOneFuel / - fleet.shipQuantityInEscrow.toNumber() / - 1000) - const unitsLeft = - unitsBurnRate * secondsLeft * fleet.shipQuantityInEscrow.toNumber() - const unitsLeftPct = - unitsLeft / - (shipInfo.fuelMaxReserve * fleet.shipQuantityInEscrow.toNumber()) - const totalSeconds = fleet.fuelCurrentCapacity.toNumber() - const maxSeconds = - shipInfo.fuelMaxReserve * - fleet.shipQuantityInEscrow.toNumber() * - (shipInfo.millisecondsToBurnOneFuel / - 1000 / - fleet.shipQuantityInEscrow.toNumber()) - const maxUnits = - shipInfo.fuelMaxReserve * fleet.shipQuantityInEscrow.toNumber() - - return { - unitsBurnt, - unitsLeftPct, - unitsLeft, - secondsLeft: Math.max(0, secondsLeft), - totalSeconds, - maxSeconds, - maxUnits, - burnRatePerShip: unitsBurnRate, - burnRatePerFleet, - } -} - -export const getRemainHealthDetails = ( - shipInfo: ScoreVarsShipInfo, - fleet: ShipStakingInfo, - timePassSinceStart: number, -): Stats => { - const unitsLeftPct = - (fleet.healthCurrentCapacity.toNumber() - timePassSinceStart) / - fleet.healthCurrentCapacity.toNumber() - const secondsLeft = getRemainHealthSec(fleet, timePassSinceStart) - const unitsBurnRate = 1 / (shipInfo.millisecondsToBurnOneToolkit / 1000) - const burnRatePerFleet = - 1 / - (shipInfo.millisecondsToBurnOneToolkit / - fleet.shipQuantityInEscrow.toNumber() / - 1000) - const unitsLeft = - secondsLeft / - (shipInfo.millisecondsToBurnOneToolkit / - 1000 / - fleet.shipQuantityInEscrow.toNumber()) - const totalSeconds = fleet.healthCurrentCapacity.toNumber() - const maxSeconds = - shipInfo.toolkitMaxReserve * - fleet.shipQuantityInEscrow.toNumber() * - (shipInfo.millisecondsToBurnOneToolkit / - 1000 / - fleet.shipQuantityInEscrow.toNumber()) - const maxUnits = - shipInfo.toolkitMaxReserve * fleet.shipQuantityInEscrow.toNumber() - - return { - unitsBurnt: 0, - unitsLeftPct, - secondsLeft: Math.max(0, secondsLeft), - totalSeconds, - maxSeconds, - maxUnits, - unitsLeft, - burnRatePerShip: unitsBurnRate, - burnRatePerFleet, - } -} - -export const getFleetRemainingResources = ( - shipInfo: ScoreVarsShipInfo, - fleet: ShipStakingInfo, -): ResourceStats => { - const timePassSinceStart = timePassSinceLastAction(fleet) - - return { - food: getRemainFoodDetails(shipInfo, fleet, timePassSinceStart), - ammo: getRemainArmsDetails(shipInfo, fleet, timePassSinceStart), - fuel: getRemainFuelDetails(shipInfo, fleet, timePassSinceStart), - tool: getRemainHealthDetails(shipInfo, fleet, timePassSinceStart), - } -} diff --git a/src/service/fleet/index.ts b/src/service/fleet/index.ts deleted file mode 100644 index 734473a8..00000000 --- a/src/service/fleet/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './get-remaining-details.js' -export * from './refill-fleet.js' diff --git a/src/service/fleet/refill-fleet.ts b/src/service/fleet/refill-fleet.ts deleted file mode 100644 index c6e08df5..00000000 --- a/src/service/fleet/refill-fleet.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { PublicKey, TransactionInstruction } from '@solana/web3.js' -import { - ShipStakingInfo, - createRearmInstruction, - createRefeedInstruction, - createRefuelInstruction, - createRepairInstruction, -} from '@staratlas/factory' - -import { connection, fleetProgram, getAccount } from '../sol/index.js' -import { sendAndConfirmInstructions } from '../sol/send-and-confirm-tx.js' -import { keyPair, resource } from '../wallet/index.js' - -import { Amounts } from './const/index.js' - -export const refillFleet = async ( - player: PublicKey, - fleetUnit: ShipStakingInfo, - amounts: Amounts, -): Promise => { - const [foodAccount, fuelAccount, ammoAccount, toolAccount] = - await Promise.all([ - getAccount(keyPair.publicKey, resource.food), - getAccount(keyPair.publicKey, resource.fuel), - getAccount(keyPair.publicKey, resource.ammo), - getAccount(keyPair.publicKey, resource.tool), - ]) - - const instructions: TransactionInstruction[] = [] - - if (amounts.food.gt(0)) { - instructions.push( - new TransactionInstruction( - await createRefeedInstruction( - connection, - keyPair.publicKey, - player, - amounts.food.toNumber(), - fleetUnit.shipMint, - resource.food, - foodAccount, - fleetProgram, - ), - ), - ) - } - if (amounts.fuel.gt(0)) { - instructions.push( - new TransactionInstruction( - await createRefuelInstruction( - connection, - keyPair.publicKey, - player, - amounts.fuel.toNumber(), - fleetUnit.shipMint, - resource.fuel, - fuelAccount, - fleetProgram, - ), - ), - ) - } - if (amounts.ammo.gt(0)) { - instructions.push( - new TransactionInstruction( - await createRearmInstruction( - connection, - keyPair.publicKey, - player, - amounts.ammo.toNumber(), - fleetUnit.shipMint, - resource.ammo, - ammoAccount, - fleetProgram, - ), - ), - ) - } - if (amounts.tool.gt(0)) { - instructions.push( - new TransactionInstruction( - await createRepairInstruction( - connection, - keyPair.publicKey, - player, - amounts.tool.toNumber(), - fleetUnit.shipMint, - resource.tool, - toolAccount, - fleetProgram, - ), - ), - ) - } - - return sendAndConfirmInstructions([keyPair])(instructions) -} diff --git a/src/service/gm/index.ts b/src/service/gm/index.ts deleted file mode 100644 index f21d6252..00000000 --- a/src/service/gm/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './market.js' diff --git a/src/service/gm/market.ts b/src/service/gm/market.ts deleted file mode 100644 index 61ccf3d1..00000000 --- a/src/service/gm/market.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { - createTransferCheckedInstruction, - getAssociatedTokenAddressSync, - getOrCreateAssociatedTokenAccount, -} from '@solana/spl-token' -import { Keypair, PublicKey } from '@solana/web3.js' -import { GmClientService, GmOrderbookService, Order } from '@staratlas/factory' -import Big from 'big.js' - -import { Sentry } from '../../sentry.js' - -import { logger } from '../../logger.js' -import { Amounts } from '../fleet/const/index.js' -import { connection, marketProgram } from '../sol/index.js' -import { sendAndConfirmInstructions } from '../sol/send-and-confirm-tx.js' -import { keyPair, resource } from '../wallet/index.js' - -const gmClientService = new GmClientService() -const gmOrderbookService = new GmOrderbookService(connection, marketProgram) - -const orderSorter = (a: Order, b: Order) => a.price.sub(b.price).toNumber() - -export const getMarketPrice = (res: PublicKey): Big => { - const orders = gmOrderbookService - .getSellOrdersByCurrencyAndItem( - resource.atlas.toString(), - res.toString(), - ) - .sort(orderSorter) - - const [order] = orders - - return Big(order.uiPrice) -} - -export const getResourcePrices = (): Amounts => ({ - food: getMarketPrice(resource.food), - tool: getMarketPrice(resource.tool), - ammo: getMarketPrice(resource.ammo), - fuel: getMarketPrice(resource.fuel), -}) - -export const getBalanceAtlas = async (pubKey: PublicKey): Promise => { - try { - const balance = await getOrCreateAssociatedTokenAccount( - connection, - new Keypair(), - resource.atlas, - pubKey, - ) - - return Big(Number(balance.amount)).div(100000000) - } catch (e) { - Sentry.captureException(e) - logger.error(e) - - return Big(0) - } -} - -export const sendAtlas = ( - receiver: PublicKey, - amount: number, -): Promise => { - const instructions = [ - createTransferCheckedInstruction( - getAssociatedTokenAddressSync( - resource.atlas, - keyPair.publicKey, - true, - ), - resource.atlas, - getAssociatedTokenAddressSync(resource.atlas, receiver, true), - keyPair.publicKey, - Big(amount).mul(100000000).toNumber(), - 8, - [], - ), - ] - - return sendAndConfirmInstructions([keyPair])(instructions) -} - -export const getBalanceMarket = async ( - pubKey: PublicKey, - res: PublicKey, -): Promise => { - const balance = await getOrCreateAssociatedTokenAccount( - connection, - new Keypair(), - res, - pubKey, - ) - - return Big(Number(balance.amount)) -} - -export const getResourceBalances = async ( - player: PublicKey, -): Promise => { - const [tool, food, ammo, fuel] = await Promise.all([ - getBalanceMarket(player, resource.tool), - getBalanceMarket(player, resource.food), - getBalanceMarket(player, resource.ammo), - getBalanceMarket(player, resource.fuel), - ]) - - return { food, tool, ammo, fuel } -} - -export const initOrderBook = async (): Promise => { - await gmOrderbookService.initialize() -} - -export const buyResource = async ( - res: PublicKey, - amount: Big, -): Promise => { - const orders = gmOrderbookService - .getSellOrdersByCurrencyAndItem( - resource.atlas.toString(), - res.toString(), - ) - .sort(orderSorter) - - const [order] = orders - - const exchangeTx = await gmClientService.getCreateExchangeTransaction( - connection, - order, - keyPair.publicKey, - amount.round().toNumber(), - marketProgram, - ) - - logger.info(`Buying ${amount.toFixed(0)} ${res} for ${order.uiPrice} each`) - - return sendAndConfirmInstructions([keyPair])( - 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(''), - ]) - ).flat() - - return res.filter((r) => r !== '') -} diff --git a/src/service/sol/anchor.ts b/src/service/sol/anchor.ts deleted file mode 100644 index 2dc12bb2..00000000 --- a/src/service/sol/anchor.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { AnchorProvider, Wallet } from '@coral-xyz/anchor' - -import { keyPair } from '../wallet/index.js' - -import { connection } from './const/index.js' - -export const anchorProvider = new AnchorProvider( - connection, - new Wallet(keyPair), - {}, -) diff --git a/src/service/sol/const/connection.ts b/src/service/sol/const/connection.ts index ac5dbfb2..f2156212 100644 --- a/src/service/sol/const/connection.ts +++ b/src/service/sol/const/connection.ts @@ -1,14 +1,4 @@ -import { Connection } from '@solana/web3.js' +import { connect } from 'solana-kite' import { config } from '../../../config/index.js' -import { fetchWithRetries } from '../undici-retry.js' -export const connection = new Connection(config.sol.rpcEndpoint, { - wsEndpoint: config.sol.wsEndpoint, - commitment: 'confirmed', - fetch: ( - input: RequestInfo | URL, - init?: RequestInit, - ): Promise => { - return fetchWithRetries(input, init, 5) - }, -}) +export const connection = connect(config.sol.rpcEndpoint, config.sol.wsEndpoint) diff --git a/src/service/sol/const/fleet-program.ts b/src/service/sol/const/fleet-program.ts index 78ec5f10..95c9fc9e 100644 --- a/src/service/sol/const/fleet-program.ts +++ b/src/service/sol/const/fleet-program.ts @@ -1,5 +1,4 @@ -import { PublicKey } from '@solana/web3.js' - +import { address } from '@solana/kit' import { config } from '../../../config/index.js' -export const fleetProgram = new PublicKey(config.sol.fleetAddress) +export const fleetProgram = address(config.sol.fleetAddress) diff --git a/src/service/sol/const/market-program.ts b/src/service/sol/const/market-program.ts index e95b47df..0152bba3 100644 --- a/src/service/sol/const/market-program.ts +++ b/src/service/sol/const/market-program.ts @@ -1,5 +1,4 @@ -import { PublicKey } from '@solana/web3.js' - +import { address } from '@solana/kit' import { config } from '../../../config/index.js' -export const marketProgram = new PublicKey(config.sol.marketAddress) +export const marketProgram = address(config.sol.marketAddress) diff --git a/src/service/sol/get-account.ts b/src/service/sol/get-account.ts index 8270d002..46532fba 100644 --- a/src/service/sol/get-account.ts +++ b/src/service/sol/get-account.ts @@ -1,26 +1,25 @@ -import { TOKEN_PROGRAM_ID } from '@solana/spl-token' -import { PublicKey } from '@solana/web3.js' +import { + findAssociatedTokenPda, + TOKEN_PROGRAM_ADDRESS, +} from '@solana-program/token' +import { type Address } from '@solana/kit' -import { Resource } from '../wallet/index.js' - -const resourceAccounts: Map = new Map() +const resourceAccounts: Map = new Map() export const getAccount = async ( - player: PublicKey, - resource: Resource, -): Promise => { - if (!resourceAccounts.get(resource.toString())) { - const ret = await PublicKey.findProgramAddress( - [ - player.toBuffer(), - TOKEN_PROGRAM_ID.toBuffer(), - resource.toBuffer(), - ], - new PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'), - ) + player: Address, + resource: Address, +): Promise
=> { + const key = `${player}:${resource}` - resourceAccounts.set(resource.toString(), ret[0]) + if (!resourceAccounts.has(key)) { + const [ata] = await findAssociatedTokenPda({ + owner: player, + mint: resource, + tokenProgram: TOKEN_PROGRAM_ADDRESS, + }) + resourceAccounts.set(key, ata) } - return resourceAccounts.get(resource.toString()) as Resource + return resourceAccounts.get(key) as Address } diff --git a/src/service/sol/priority-fee/bloxroute-tip-instruction.ts b/src/service/sol/priority-fee/bloxroute-tip-instruction.ts index 88f44b3b..ce79187d 100644 --- a/src/service/sol/priority-fee/bloxroute-tip-instruction.ts +++ b/src/service/sol/priority-fee/bloxroute-tip-instruction.ts @@ -1,19 +1,17 @@ -import { - PublicKey, - SystemProgram, - TransactionInstruction, -} from '@solana/web3.js' +import { address, type Instruction, type TransactionSigner } from '@solana/kit' +import { getTransferSolInstruction } from '@solana-program/system' -const TRADER_API_TIP_WALLET = 'HWEoBxYs7ssKuudEjzjmpfJVX7Dvi7wescFsVx2L5yoY' -export const createBloxrouteTipInstruction = ( - senderAddress: PublicKey, - tipAmount: number, -): TransactionInstruction => { - const tipAddress = new PublicKey(TRADER_API_TIP_WALLET) +const TRADER_API_TIP_WALLET = address( + 'HWEoBxYs7ssKuudEjzjmpfJVX7Dvi7wescFsVx2L5yoY', +) - return SystemProgram.transfer({ - fromPubkey: senderAddress, - toPubkey: tipAddress, - lamports: tipAmount, +export const createBloxrouteTipInstruction = ( + source: TransactionSigner, + tipAmount: bigint, +): Instruction => { + return getTransferSolInstruction({ + source, + destination: TRADER_API_TIP_WALLET, + amount: tipAmount, }) } diff --git a/src/service/sol/priority-fee/compute-unit-instruction.ts b/src/service/sol/priority-fee/compute-unit-instruction.ts index bba43ab7..380da842 100644 --- a/src/service/sol/priority-fee/compute-unit-instruction.ts +++ b/src/service/sol/priority-fee/compute-unit-instruction.ts @@ -1,54 +1,174 @@ +import { getSetComputeUnitLimitInstruction } from '@solana-program/compute-budget' import { - AddressLookupTableAccount, - ComputeBudgetProgram, - PublicKey, - TransactionInstruction, - TransactionMessage, - VersionedTransaction, -} from '@solana/web3.js' + appendTransactionMessageInstructions, + compileTransaction, + createTransactionMessage, + getBase64EncodedWireTransaction, + pipe, + setTransactionMessageFeePayerSigner, + setTransactionMessageLifetimeUsingBlockhash, + type Instruction, +} from '@solana/kit' +import { config } from '../../../config/index.js' import { logger } from '../../../logger.js' -import { keyPair } from '../../wallet/index.js' import { connection } from '../const/index.js' +import { getKeyPair } from '../../wallet/init-keypair.js' + +type SimulateResponse = { + jsonrpc: string + id: number + result?: { + context: { slot: number } + value: { + err: unknown + logs: string[] + unitsConsumed?: number + } + } + error?: { code: number; message: string } +} + +const DEFAULT_UNITS = 800_000 +const MAX_UNITS = 1_400_000 +const HEADROOM_MULTIPLIER = 1.5 + +// Error that indicates simulation failed with a program error (should not send tx) +export class SimulationProgramError extends Error { + constructor( + message: string, + public readonly err: unknown, + public readonly logs: string[], + public readonly serializedTx: string, + ) { + super(message) + this.name = 'SimulationProgramError' + } +} + +// Check if simulation error is a fatal program error that we should not retry +function isFatalProgramError(err: unknown): boolean { + if (!err || typeof err !== 'object') return false + + // Check for InstructionError + if ('InstructionError' in err) { + const instructionError = (err as { InstructionError: unknown[] }) + .InstructionError + if (Array.isArray(instructionError) && instructionError.length >= 2) { + const errorType = instructionError[1] + // String errors that indicate transaction will fail + if ( + errorType === 'ProgramFailedToComplete' || + errorType === 'InvalidAccountData' || + errorType === 'InvalidArgument' || + errorType === 'InvalidInstructionData' || + errorType === 'UnbalancedInstruction' + ) { + return true + } + // Custom program errors (objects with error codes) + if (typeof errorType === 'object' && errorType !== null) { + return true + } + } + } + return false +} const getSimulationUnits = async ( - instructions: TransactionInstruction[], - payer: PublicKey, - lookupTables: AddressLookupTableAccount[], + instructions: Instruction[], ): Promise => { - const testInstructions = [ - ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 }), - ...instructions, - ] - - const testVersionedTxn = new VersionedTransaction( - new TransactionMessage({ - instructions: testInstructions, - payerKey: payer, - recentBlockhash: PublicKey.default.toString(), - }).compileToV0Message(lookupTables), - ) + try { + const testInstructions = [ + getSetComputeUnitLimitInstruction({ units: MAX_UNITS }), + ...instructions, + ] + + const latestBlockhash = await connection.rpc.getLatestBlockhash().send() + const signer = await getKeyPair() + + const transactionMessage = pipe( + createTransactionMessage({ version: 0 }), + (tx) => setTransactionMessageFeePayerSigner(signer, tx), + (tx) => + setTransactionMessageLifetimeUsingBlockhash( + latestBlockhash.value, + tx, + ), + (tx) => appendTransactionMessageInstructions(testInstructions, tx), + ) + + const compiledTx = compileTransaction(transactionMessage) + const encodedTx = getBase64EncodedWireTransaction(compiledTx) + + // Use direct fetch to bypass solana-kite BigInt serialization issues + const res = await fetch(config.sol.rpcEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'simulateTransaction', + params: [ + encodedTx, + { + encoding: 'base64', + replaceRecentBlockhash: true, + sigVerify: false, + commitment: 'confirmed', + }, + ], + }), + }) + + const json = (await res.json()) as SimulateResponse - const simulation = await connection.simulateTransaction(testVersionedTxn, { - replaceRecentBlockhash: true, - sigVerify: false, - }) + if (json.error) { + logger.debug(`Simulation RPC error: ${json.error.message}`) + return undefined + } - if (simulation.value.err) { + if (json.result?.value.err) { + const errStr = JSON.stringify(json.result.value.err) + const logs = json.result.value.logs || [] + + // Throw for fatal program errors so caller can handle appropriately + if (isFatalProgramError(json.result.value.err)) { + throw new SimulationProgramError( + `Simulation failed: ${errStr}`, + json.result.value.err, + logs, + encodedTx, + ) + } + + return undefined + } + + return json.result?.value.unitsConsumed + ? Number(json.result.value.unitsConsumed) + : undefined + } catch (e) { + // Re-throw SimulationProgramError + if (e instanceof SimulationProgramError) { + throw e + } return undefined } - - return simulation.value.unitsConsumed } export const createComputeUnitInstruction = async ( - instructions: TransactionInstruction[], -): Promise => { - const units = - ((await getSimulationUnits(instructions, keyPair.publicKey, [])) ?? - 200_000) * 1.5 + instructions: Instruction[], +): Promise => { + // This will throw SimulationProgramError if simulation fails with fatal error + const simulated = await getSimulationUnits(instructions) + const units = simulated + ? Math.min(Math.ceil(simulated * HEADROOM_MULTIPLIER), MAX_UNITS) + : DEFAULT_UNITS - logger.debug(`Esitmated Compute Units: ${units}`) + logger.debug( + `Compute Units: ${units}${simulated ? ` (simulated: ${simulated})` : ' (default)'}`, + ) - return ComputeBudgetProgram.setComputeUnitLimit({ units }) + return getSetComputeUnitLimitInstruction({ units }) } diff --git a/src/service/sol/priority-fee/priority-fee-instruction.ts b/src/service/sol/priority-fee/priority-fee-instruction.ts index 09aeaf8f..73d505eb 100644 --- a/src/service/sol/priority-fee/priority-fee-instruction.ts +++ b/src/service/sol/priority-fee/priority-fee-instruction.ts @@ -1,70 +1,20 @@ -import { - AddressLookupTableAccount, - ComputeBudgetProgram, - PublicKey, - TransactionInstruction, - TransactionMessage, - VersionedTransaction, -} from '@solana/web3.js' -import base58 from 'bs58' -import { config } from '../../../config/index.js' +import { getSetComputeUnitPriceInstruction } from '@solana-program/compute-budget' +import type { Instruction } from '@solana/kit' +import { config } from '../../../config/index.js' import { logger } from '../../../logger.js' -import { programs } from '../../../main/basedbot/lib/programs.js' -import { keyPair } from '../../wallet/index.js' +import { fleetProgram } from '../const/fleet-program.js' import { rpcFetch } from '../rpc-fetch.js' -const getDummyTransaction = ( - instructions: TransactionInstruction[], - payer: PublicKey, - lookupTables: AddressLookupTableAccount[], -): VersionedTransaction => { - const testInstructions = [ - ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 }), - ...instructions, - ] - - return new VersionedTransaction( - new TransactionMessage({ - instructions: testInstructions, - payerKey: payer, - recentBlockhash: PublicKey.default.toString(), - }).compileToV0Message(lookupTables), - ) -} - -export const createPriorityFeeInstruction = async ( - instructions: TransactionInstruction[], -): Promise => { - const transaction = getDummyTransaction(instructions, keyPair.publicKey, []) - let encodedTx: string | undefined - try { - encodedTx = base58.encode(transaction.serialize()) - } catch (e) { - logger.error((e as any).message) - } - +export const createPriorityFeeInstruction = async (): Promise => { try { - const result = - config.sol.rpcEndpoint.includes('devnet') || - config.sol.rpcEndpoint.includes('validator') || - config.sol.rpcEndpoint.includes('localhost') - ? await rpcFetch({ - jsonrpc: '2.0', - id: 1, - method: 'getRecentPrioritizationFees', - params: [[programs.sage.programId.toBase58()]], - }) - : await rpcFetch({ - jsonrpc: '2.0', - id: 1, - method: 'getRecentPrioritizationFees', - params: { - transaction: encodedTx, - percentiles: [50, 75, 95, 100], - lookbackSlots: 10, - }, - }) + // Fetch priority fee from RPC, scoped to the fleet program + const result = await rpcFetch({ + jsonrpc: '2.0', + id: 1, + method: 'getRecentPrioritizationFees', + params: [[fleetProgram]], + }) const feeData = (result as any).result.result as Array<{ slot: number @@ -72,26 +22,27 @@ export const createPriorityFeeInstruction = async ( }> const microLamports = - feeData.find((f: any) => f.slot == -1)?.prioritizationFee ?? - Math.max(...feeData.map((f: any) => f.prioritizationFee)) + feeData.length > 0 + ? Math.max(...feeData.map((f) => f.prioritizationFee)) + : 5000 - // const microLamports = 5000 logger.debug(`Priority fee estimates: ${microLamports}`) const feeLimit = config.sol.feeLimit + const finalFee = + feeLimit > 0 ? Math.min(feeLimit, microLamports) : microLamports + if (feeLimit > 0 && microLamports > feeLimit) { logger.debug(`Capping fee at ${feeLimit}`) } - return ComputeBudgetProgram.setComputeUnitPrice({ - microLamports: - feeLimit > 0 - ? Math.min(feeLimit, microLamports) - : microLamports, + + return getSetComputeUnitPriceInstruction({ + microLamports: BigInt(finalFee), }) } catch (e) { - logger.error((e as any).message) - return ComputeBudgetProgram.setComputeUnitPrice({ - microLamports: 5000, + logger.error((e as Error).message) + return getSetComputeUnitPriceInstruction({ + microLamports: 5000n, }) } } diff --git a/src/service/sol/send-and-confirm-tx.ts b/src/service/sol/send-and-confirm-tx.ts index 4458f5ff..f8dcc038 100644 --- a/src/service/sol/send-and-confirm-tx.ts +++ b/src/service/sol/send-and-confirm-tx.ts @@ -1,249 +1,401 @@ import { - Keypair, - LAMPORTS_PER_SOL, - PublicKey, - TransactionInstruction, - TransactionMessage, - VersionedTransaction, -} from '@solana/web3.js' -import { config } from '../../config/index.js' + appendTransactionMessageInstructions, + createTransactionMessage, + getBase64EncodedWireTransaction, + pipe, + setTransactionMessageFeePayerSigner, + setTransactionMessageLifetimeUsingBlockhash, + signTransactionMessageWithSigners, + type Instruction, + type KeyPairSigner, + type Signature, +} from '@solana/kit' +import { lamports } from '@solana/kit' +import { config } from '../../config/index.js' import { logger } from '../../logger.js' - import { connection } from './const/index.js' import { createBloxrouteTipInstruction } from './priority-fee/bloxroute-tip-instruction.js' -import { createComputeUnitInstruction } from './priority-fee/compute-unit-instruction.js' +import { + createComputeUnitInstruction, + SimulationProgramError, +} from './priority-fee/compute-unit-instruction.js' + +// Re-export for callers +export { SimulationProgramError } import { createPriorityFeeInstruction } from './priority-fee/priority-fee-instruction.js' -// 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 +/** + * SAGE program error codes mapping + * From @staratlas/dev-sage error definitions + */ +const SAGE_ERRORS: Record = { + 1367932928: 'PROGRAM_MISMATCH', + 1367932929: 'MUST_BE_ONLY_INSTRUCTION', + 1367932930: 'INVALID_FLEET_SIZE', + 1367932931: 'NOT_FOUND', + 1367932932: 'INVALID_INPUT', + 1367932933: 'TYPE_MISMATCH', + 1367932934: 'CELESTIAL_BODY_TYPE_MISMATCH', + 1367932935: 'STATUS_MISMATCH', + 1367932936: 'INACTIVE_CLAIM_STAKE', + 1367932937: 'CRAFTING_HAB_NOT_EVICTED', + 1367932938: 'STATE_MISMATCH', + 1367932939: 'ID_MISMATCH', + 1367932940: 'GAME_MISMATCH', + 1367932941: 'PROFILE_MISMATCH', + 1367932942: 'FACTION_MISMATCH', + 1367932943: 'SYSTEM_MISMATCH', + 1367932944: 'STARBASE_DESTROYED', + 1367932945: 'STARBASE_NOT_DESTROYED', + 1367932946: 'CAPACITY_EXCEEDED', + 1367932947: 'REQUIREMENTS_NOT_MET', + 1367932948: 'COOLDOWN_ACTIVE', + 1367932949: 'USAGE_LIMIT_EXCEEDED', + 1367932950: 'CRAFTING_JOB_LIMIT_REACHED', + 1367932951: 'HUB_ALREADY_PLACED', + 1367932952: 'ALREADY_EXISTS', + 1367932953: 'DUPLICATE_RESOURCE', + 1367932954: 'NOT_AT_CENTRAL_SPACE_STATION', + 1367932955: 'NOT_WITHIN_RANGE', + 1367932956: 'GLOBAL_CEASEFIRE_ACTIVE', + 1367932957: 'UNAUTHORIZED', + 1367932958: 'UNDER_CONSTRUCTION', + 1367932959: 'INSTANCE_DEACTIVATED', + 1367932960: 'NUMERIC_OVERFLOW', + 1367932961: 'ESCROW_UNDERFLOW', + 1367932962: 'INSUFFICIENT_AMOUNT', + 1367932963: 'WITHDRAWAL_LIMIT_EXCEEDED', + 1367932964: 'WITHDRAW_DISABLED', + 1367932965: 'EPOCH_DIVISION_ERROR', + 1367932966: 'INVALID_TIMESTAMP', + 1367932967: 'REGION_NOT_FOUND', + 1367932968: 'REGION_INVALID', + 1367932969: 'INVALID_PATH', + 1367932970: 'BOUNDING_BOX_FAILED', + 1367932971: 'INVARIANT_VIOLATION', + 1367932972: 'SEQUENCE_ID_MISMATCH', + 1367932973: 'RENTED_FLEET', +} -const sleep = (ms: number) => - new Promise((resolve) => { - setTimeout(resolve, ms) - }) +/** + * Decode a Solana transaction error to a human-readable message + * Handles SAGE program custom errors and standard instruction errors + */ +function decodeTransactionError(err: unknown): string { + if (!err || typeof err !== 'object') { + return JSON.stringify(err) + } -type Blockhash = string -type BlockhashWithExpiryBlockHeight = Readonly<{ - blockhash: Blockhash - lastValidBlockHeight: number -}> + const error = err as Record -const confirmTx = async (txId: string): Promise => { - const res = await connection.getSignatureStatus(txId) + // Handle InstructionError format: {"InstructionError":[index, errorType]} + if ('InstructionError' in error && Array.isArray(error.InstructionError)) { + const [instructionIndex, errorType] = error.InstructionError - // logger.debug(`Signature: ${txId} with status: ${JSON.stringify(res)}`) + if (typeof errorType === 'string') { + return `Instruction ${instructionIndex}: ${errorType}` + } - if (res?.value && 'confirmationStatus' in res.value) { + // Handle Custom error: {"Custom": errorCode} if ( - res.value.confirmationStatus === 'finalized' || - res.value.confirmationStatus === 'confirmed' || - res.value.confirmationStatus === 'processed' + typeof errorType === 'object' && + errorType !== null && + 'Custom' in errorType ) { - const log = res.value.err ? logger.warn : logger.debug + const customError = errorType as { Custom: number } + const errorCode = customError.Custom + const errorName = SAGE_ERRORS[errorCode] || `UNKNOWN_ERROR` + return `Instruction ${instructionIndex}: ${errorName} (${errorCode})` + } - // log(`Transaction ${res.value.confirmationStatus}: ${txId} with status: ${res.value.confirmationStatus}`) - log(`Signature: ${txId} with status: ${JSON.stringify(res)}`) + return `Instruction ${instructionIndex}: ${JSON.stringify(errorType)}` + } - // logger.info(`https://solscan.io/tx/${txId}`) + return JSON.stringify(err) +} - return txId - } +const sleep = (ms: number) => + new Promise((resolve) => { + setTimeout(resolve, ms) + }) + +/** Error thrown when transaction is finalized but failed on-chain */ +export class TransactionFailedError extends Error { + constructor(message: string) { + super(message) + this.name = 'TransactionFailedError' } - throw new Error('Transaction confirmation failed') } -export const sendAndConfirmTx = async ( - transaction: VersionedTransaction, - latestBlockHash?: BlockhashWithExpiryBlockHeight, -): Promise => { - const blockHash = latestBlockHash ?? (await connection.getLatestBlockhash()) - let blockheight = await connection.getBlockHeight() - - let txId: string | undefined - - while (blockheight <= blockHash.lastValidBlockHeight) { - blockheight = await connection.getBlockHeight() - // logger.info( - // `${blockHash.lastValidBlockHeight} - ${blockheight} = ${blockHash.lastValidBlockHeight - blockheight}`, - // ) - try { - txId = await connection.sendRawTransaction( - transaction.serialize(), - { skipPreflight: true }, - ) - } catch (e) { - const message = (e as any).message as string - - const logs = (e as any).logs as string[] - - if (logs) { - logs.filter((log) => log.includes('AnchorError')).forEach( - (log) => { - logger.error(log) - }, - ) - } +type SignatureStatus = { + slot: number + confirmations: number | null + err: unknown | null + confirmationStatus: 'processed' | 'confirmed' | 'finalized' | null +} - if (message.includes('has already been processed') && txId) { - await confirmTx(txId) +type GetSignatureStatusesResponse = { + jsonrpc: string + id: number + result: { + context: { slot: number } + value: Array + } +} - return txId - } - throw e +type GetTransactionResponse = { + jsonrpc: string + id: number + result: { + meta: { + err: unknown | null + logMessages: string[] } + } | null +} - try { - await confirmTx(txId) +/** + * Fetch transaction logs from the chain + */ +async function fetchTransactionLogs(signature: string): Promise { + try { + const res = await fetch(config.sol.rpcEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'getTransaction', + params: [ + signature, + { encoding: 'json', maxSupportedTransactionVersion: 0 }, + ], + }), + }) + const json = (await res.json()) as GetTransactionResponse + return json.result?.meta?.logMessages ?? [] + } catch { + return [] + } +} - return txId - } catch (_e) { - await sleep(500) +/** + * Extract program error from transaction logs + */ +function extractProgramError(logs: string[]): string | null { + for (const log of logs) { + // Look for StarFrameError or other program errors + if (log.includes('Error:') || log.includes('error:')) { + return log + .replace(/^>\s*Program logged:\s*"?/, '') + .replace(/"$/, '') } } - - throw new Error(`Transaction ${txId} failed to confirm`) + return null } -const createAndSignTransaction = ( - instructions: TransactionInstruction[], - blockhash: Blockhash, - signers: Array, -): VersionedTransaction => { - const messageV0 = new TransactionMessage({ - payerKey: signers[0].publicKey, - recentBlockhash: blockhash, - instructions, - }).compileToV0Message() - const transaction = new VersionedTransaction(messageV0) - - transaction.sign(signers) - - return transaction -} +/** + * Confirm transaction using direct JSON-RPC fetch. + * Bypasses solana-kite SDK which has issues with getSignatureStatuses returning null. + */ +const confirmTx = async (signature: Signature): Promise => { + logger.debug(`confirmTx: checking ${signature.slice(0, 20)}...`) + + let json: GetSignatureStatusesResponse + try { + const res = await fetch(config.sol.rpcEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'getSignatureStatuses', + params: [[signature], { searchTransactionHistory: true }], + }), + }) + json = (await res.json()) as GetSignatureStatusesResponse + } catch (e) { + logger.debug(`confirmTx RPC error: ${(e as Error).message}`) + throw e + } -const getInstructionSize = ( - instructions: TransactionInstruction[], - signer: Keypair, -): number => { - const messageV0 = new TransactionMessage({ - payerKey: signer.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[], - maxSize: number, - signer: Keypair, -): TransactionInstruction[] => { - for (let i = 0; i < instructions.length; ++i) { - const instructionSize = getInstructionSize( - instructions.slice(0, i + 1), - signer, - ) - - logger.debug( - `Transaction with ${i + 1} instructions has size ${instructionSize}`, - ) - - if (instructionSize > maxSize) { - return instructions.slice(0, i) + const status = json.result?.value?.[0] + logger.debug(`confirmTx response: ${JSON.stringify(status)}`) + + if (status && status.confirmationStatus) { + if ( + status.confirmationStatus === 'finalized' || + status.confirmationStatus === 'confirmed' || + status.confirmationStatus === 'processed' + ) { + // Check for transaction error - fetch logs and throw non-retryable error + if (status.err) { + const logs = await fetchTransactionLogs(signature) + const programError = extractProgramError(logs) + const baseError = decodeTransactionError(status.err) + + // Log full transaction logs for debugging + if (logs.length > 0) { + logger.error( + `Transaction logs:\n${logs.map((l) => ` ${l}`).join('\n')}`, + ) + } + + const errorMsg = programError + ? `${baseError} - ${programError}` + : baseError + + throw new TransactionFailedError( + `Transaction failed on-chain: ${errorMsg}`, + ) + } + logger.debug(`Signature: ${signature} confirmed`) + return signature } } - - return instructions + // Transaction not found or not yet confirmed + throw new Error( + status + ? `Status: ${JSON.stringify(status)}` + : 'Transaction not found yet', + ) } export const sendAndConfirmInstructions = - (signers: Array) => - async (instructionArray: TransactionInstruction[]): Promise => { + (signer: KeyPairSigner) => + async (instructions: Instruction[]): 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, - signers[0], - ) - - for (let i = 0; i < maxRetries; ++i) { - const [ - latestBlockHash, - priorityFeeInstruction, - computeUnitsInstruction, - ] = await Promise.all([ - connection.getLatestBlockhash(), - createPriorityFeeInstruction(chunk), - createComputeUnitInstruction(chunk), - ]) - - const txInstructions = [ - computeUnitsInstruction, - priorityFeeInstruction, + const results: Signature[] = [] + + // For now, send all instructions in one transaction + // TODO: Add chunking logic for large instruction sets + for (let i = 0; i < maxRetries; ++i) { + try { + const [latestBlockhash, priorityFeeIx, computeUnitsIx] = + await Promise.all([ + connection.rpc.getLatestBlockhash().send(), + createPriorityFeeInstruction(), + createComputeUnitInstruction(instructions), + ]) + + const allInstructions: Instruction[] = [ + computeUnitsIx, + priorityFeeIx, ...(config.sol.bloxroute ? [ createBloxrouteTipInstruction( - signers[0].publicKey, - 0.0001 * LAMPORTS_PER_SOL, + signer, + lamports(100_000n), // 0.0001 SOL ), ] : []), - ...chunk, + ...instructions, ] - const transaction = createAndSignTransaction( - txInstructions, - latestBlockHash.blockhash, - signers, + const transactionMessage = pipe( + createTransactionMessage({ version: 0 }), + (tx) => setTransactionMessageFeePayerSigner(signer, tx), + (tx) => + setTransactionMessageLifetimeUsingBlockhash( + latestBlockhash.value, + tx, + ), + (tx) => + appendTransactionMessageInstructions( + allInstructions, + tx, + ), ) - const rawTransaction = transaction.serialize() + const signedTransaction = + await signTransactionMessageWithSigners(transactionMessage) - if (rawTransaction.length > MAX_TRANSACTION_SIZE) { - throw new Error( - `Transaction too large: ${rawTransaction.length} bytes`, - ) - } + const serialized = + getBase64EncodedWireTransaction(signedTransaction) - try { - logger.debug( - Buffer.from(transaction.serialize()).toString('base64'), - ) - const result = await sendAndConfirmTx( - transaction, - latestBlockHash, - ) + logger.debug(`Transaction: ${serialized}`) - 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.debug(`Sending transaction...`) - logger.error( - `Transaction failed: ${message}, retrying... (${i + 1}/${maxRetries})`, - ) + const signature = await connection.rpc + .sendTransaction(serialized, { + encoding: 'base64', + skipPreflight: true, + }) + .send() + + logger.debug(`Sent transaction: ${signature}`) + + // Poll for confirmation + let confirmed = false + const startSlot = latestBlockhash.value.lastValidBlockHeight + let pollCount = 0 - if (i === maxRetries - 1) { - throw new Error( - `Transaction failed after ${maxRetries} attempts`, + while (!confirmed) { + pollCount++ + const currentSlot = await connection.rpc + .getBlockHeight() + .send() + + if (pollCount % 10 === 1) { + logger.debug( + `Polling confirmation ${pollCount}: slot ${currentSlot}/${startSlot + 150n}`, ) } + + if (currentSlot > startSlot + 150n) { + throw new Error('Transaction expired') + } + + try { + await confirmTx(signature) + confirmed = true + results.push(signature) + logger.info(`Transaction confirmed: ${signature}`) + } catch (e) { + // Re-throw on-chain failures immediately - don't retry + if (e instanceof TransactionFailedError) { + throw e + } + // Otherwise it's "not found yet" - keep polling + await sleep(500) + } + } + + return results + } catch (e) { + const err = e as Error + + // Don't retry on-chain failures - they won't succeed with same data + if (e instanceof TransactionFailedError) { + // Let callers handle the error + throw e + } + + // Don't retry simulation program errors - the instruction is invalid + if (e instanceof SimulationProgramError) { + // Let callers handle the error - they can inspect e.logs + throw e } + + logger.error(`Transaction failed (${i + 1}/${maxRetries}):`) + logger.error( + JSON.stringify(err, Object.getOwnPropertyNames(err), 2), + ) + + if (i === maxRetries - 1) { + throw new Error( + `Transaction failed after ${maxRetries} attempts: ${err.message}`, + ) + } + + await sleep(1000) } } return results } + +// Legacy export for compatibility +export const sendAndConfirmTx = sendAndConfirmInstructions diff --git a/src/service/wallet/index.ts b/src/service/wallet/index.ts index ac050db4..3379bdf9 100644 --- a/src/service/wallet/index.ts +++ b/src/service/wallet/index.ts @@ -1,2 +1 @@ export * from './init-keypair.js' -export * from './resource.js' diff --git a/src/service/wallet/init-keypair.ts b/src/service/wallet/init-keypair.ts index 75cf458e..f412a32d 100644 --- a/src/service/wallet/init-keypair.ts +++ b/src/service/wallet/init-keypair.ts @@ -1,61 +1,106 @@ -import { Keypair, PublicKey } from '@solana/web3.js' +import { + address, + createKeyPairFromBytes, + createKeyPairSignerFromPrivateKeyBytes, + createSignerFromKeyPair, + type Address, + type KeyPairSigner, +} from '@solana/kit' import { mnemonicToSeedSync } from 'bip39' import { derivePath } from 'ed25519-hd-key' +import { createHash } from 'node:crypto' import { readFileSync } from 'node:fs' import { config } from '../../config/index.js' import { logger } from '../../logger.js' -export const loadKeypairFromFile = (filePath: string): Keypair => { +/** Derivation path for the profile keypair (deterministic from mnemonic). */ +const PROFILE_DERIVATION_PATH = "m/44'/501'/0'/1'" + +export const loadKeypairFromFile = async ( + filePath: string, +): Promise => { const loadedKeyBytes = Uint8Array.from( JSON.parse(readFileSync(filePath, 'utf8')), ) - return Keypair.fromSecretKey(loadedKeyBytes) + const keyPair = await createKeyPairFromBytes(loadedKeyBytes) + return createSignerFromKeyPair(keyPair) } -const initKeypairBySecretKey = (key: number[], pubKey: PublicKey): Keypair => { - const keypair = Keypair.fromSecretKey(new Uint8Array(key)) - - if (keypair.publicKey.equals(pubKey)) { - logger.info(`Found keypair for ${pubKey.toBase58()}`) +const initKeypairBySecretKey = async ( + key: number[], + pubKey: Address, +): Promise => { + const keyPair = await createKeyPairFromBytes(new Uint8Array(key)) + const signer = await createSignerFromKeyPair(keyPair) - return keypair + if (signer.address === pubKey) { + logger.info(`Found keypair for ${pubKey}`) + return signer } throw new Error('PubKey does not match Private key') } -const initKeypairByMnemonic = ( +const initKeypairByMnemonic = async ( mnemonic: string, - pubKey: PublicKey, -): Keypair => { + pubKey: Address, +): Promise => { const seed = mnemonicToSeedSync(mnemonic, '') for (let i = 0; i < 1000; ++i) { const path = `m/44'/501'/${i}'/0'` - const keypair = Keypair.fromSeed( - derivePath(path, seed.toString('hex')).key, - ) + const derivedKey = derivePath(path, seed.toString('hex')).key - logger.debug(`${path} => ${keypair.publicKey.toBase58()}`) + const signer = await createKeyPairSignerFromPrivateKeyBytes(derivedKey) - if (keypair.publicKey.equals(pubKey)) { - logger.info(`Found keypair for ${pubKey.toBase58()} at ${path}`) + logger.debug(`${path} => ${signer.address}`) - return keypair + if (signer.address === pubKey) { + logger.info(`Found keypair for ${pubKey} at ${path}`) + return signer } } throw new Error('PubKey not found in derivation Path') } -export const keyPair = - config.user.keyMode === 'mnemonic' - ? initKeypairByMnemonic( - config.user.mnemonic, - new PublicKey(config.user.pubKey), - ) - : initKeypairBySecretKey( - config.user.secretKey, - new PublicKey(config.user.pubKey), - ) +// Initialize keypair - this returns a Promise now +const initKeyPair = async (): Promise => { + const pubKey = address(config.user.pubKey) + + if (config.user.keyMode === 'mnemonic') { + return initKeypairByMnemonic(config.user.mnemonic, pubKey) + } + return initKeypairBySecretKey(config.user.secretKey, pubKey) +} + +// Export as a promise that can be awaited by callers +export const keyPairPromise = initKeyPair() + +// Preferred accessor to avoid race conditions with async initialization +export const getKeyPair = async (): Promise => { + return keyPairPromise +} + +/** + * Returns the deterministic profile keypair derived from the wallet. + * Mnemonic: derives path m/44'/501'/0'/1'. + * Secret key: derives 32-byte seed via SHA256(secretKeyBytes + 'fleetbot-profile'). + */ +export const getProfileKeyPair = async (): Promise => { + if (config.user.keyMode === 'mnemonic') { + const seed = mnemonicToSeedSync(config.user.mnemonic, '') + const derivedKey = derivePath( + PROFILE_DERIVATION_PATH, + seed.toString('hex'), + ).key + return createKeyPairSignerFromPrivateKeyBytes(derivedKey) + } + const secretKeyBytes = new Uint8Array(config.user.secretKey) + const profileSeed = createHash('sha256') + .update(secretKeyBytes) + .update('fleetbot-profile', 'utf8') + .digest() + return createKeyPairSignerFromPrivateKeyBytes(profileSeed) +} diff --git a/src/service/wallet/resource.ts b/src/service/wallet/resource.ts deleted file mode 100644 index 8883674e..00000000 --- a/src/service/wallet/resource.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { PublicKey } from '@solana/web3.js' - -import { config } from '../../config/index.js' - -export type Resource = PublicKey - -export const resource = { - atlas: new PublicKey(config.sol.atlasMint), - food: new PublicKey(config.sol.foodMint), - fuel: new PublicKey(config.sol.fuelMint), - ammo: new PublicKey(config.sol.ammoMint), - tool: new PublicKey(config.sol.toolMint), -} diff --git a/tsup.config.ts b/tsup.config.ts index 2ddfb79b..49394ba0 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -2,7 +2,6 @@ import { defineConfig } from 'tsup'; export default defineConfig({ entry: [ - 'src/main/fleetbot/index.ts', 'src/main/basedbot/index.ts', 'src/main/airdrop/index.ts', 'src/main/migrate/index.ts',