From fb9a7f4e8b222ccfcc5c8f831e31941bb16a663b Mon Sep 17 00:00:00 2001 From: Caffeine Date: Thu, 10 Sep 2026 00:19:21 +0300 Subject: [PATCH] =?UTF-8?q?feat(relayer):=20migrate=20to=20SPL-token=20(?= =?UTF-8?q?=D1=84=D0=B0=D0=BD=D1=82=D0=B8=D0=BA=D0=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ops: readGlobal читает vaultBalance (getTokenAccountBalance), createBet/closeBet передают bettorToken/vaultToken/tokenProgram, ensureVaultFunded наливает токен (mintTo, allowOwnerOffCurve) - pda: vaultPda([global,vault]), config:.env.new SMART_TOKEN_MINT, суммы в токен-единицах - check.ts: 14 проверок сигнатур создания create/close под новым токеновым IDL - @solana/spl-token dependency; .gitignore + node_modules/dist --- TASK-relayer-token.md | 56 ++ relayer/.env.example | 18 +- relayer/.gitignore | 2 + relayer/idl/smart_updown.ts | 162 ++++- relayer/package-lock.json | 1224 +++++++++++++++++++++++++++++++++++ relayer/package.json | 3 + relayer/src/check.ts | 129 ++++ relayer/src/config.ts | 25 +- relayer/src/frontStub.ts | 5 +- relayer/src/index.ts | 47 +- relayer/src/ops.ts | 116 +++- relayer/src/pda.ts | 10 + 12 files changed, 1726 insertions(+), 71 deletions(-) create mode 100644 TASK-relayer-token.md create mode 100644 relayer/.gitignore create mode 100644 relayer/package-lock.json create mode 100644 relayer/src/check.ts diff --git a/TASK-relayer-token.md b/TASK-relayer-token.md new file mode 100644 index 0000000..b4a0e51 --- /dev/null +++ b/TASK-relayer-token.md @@ -0,0 +1,56 @@ +# TASK — Фаза 2: перевести релейер с SOL на фантики (SPL-токен) + +Репозиторий: `smart-updown-token` (тот, где контракт уже на токене). Работаем ТОЛЬКО в `relayer/`. +Контракт уже переведён (Фаза 1, коммит 45d64d4) — релейер теперь должен звать его токеновые инструкции. + +## Контекст +- Контракт переведён: `create_bet` escrow'ит фантики bettor→vault; `close_bet` платит vault→bettor. +- Новые аккаунты в инструкциях: `bettor_token`, `vault_token`, `token_program`. IDL уже содержит их + (в `relayer/idl/smart_updown.json` — регенерирован). +- Сейчас `relayer/` заточен под SOL: `readGlobal` берёт `getBalance(global)`, `ensureVaultFunded` + airdrop'ит SOL, `createBet/closeBet` не передают токен-аккаунты. + +## Что сделать (в `relayer/src/` и `relayer/`) + +### 1. `pda.ts` +- Добавить `vaultPda(programId): PublicKey` — PDA по седам `[b"global", b"vault"]` (совпадает с контрактом). +- (Убедиться, что `globalPda` и `betPda` соответствуют контракту.) + +### 2. `config.ts` +- Добавить `tokenMint: PublicKey` (mint наших фантиков) в конфиг/cfg, с default-плейсхолдером. +- Проверить `.env.example` — добавить переменную для mint (напр. `TOKEN_MINT`). + +### 3. `ops.ts` +- **`readGlobal`**: вместо `getBalance(global)` (SOL) — читать **баланс `vault_token`** (PDA `vaultPda`), + через connection/getTokenAccountBalance (или через spl-token). Поле, что возвращаем, переименовать/пометить + как `vaultBalance` (в токен-единицах). +- **`createBet`**: в `.accounts` добавить `bettor_token` (ATA bettor), `vault_token` (vaultPda), `token_program`. + `min/amount` — уже в токен-единицах (не lamports), логировать в токене. +- **`closeBet`**: в `.accounts` добавить `bettor_token` (ATA bettor по адресу из bet), `vault_token`, + `token_program`. +- **`ensureVaultFunded`**: теперь не airDrop SOL в global, а **наливать токен в vault_token** — на localnet + можно mint'ить mint-авторити (токен создать/налить). Либо вызывать mint по необходимости. Не SOL. + +### 4. `index.ts` (главный цикл) +- Привести в соответствие: вызовы `readGlobal`/`createBet`/`closeBet`/`ensureVaultFunded` — с новыми сигнатурами. +- Логирование: суммы в токене, а не в SOL. + +### 5. Тесты +- Обновить интеграционный тест релейера на токеновом флоу (если есть в `relayer/`): создать mint/ATA, + налить токен, прогнать create→close. `npm test` / `ts-node` зелёный. + +## Гейты (после каждого шага) +- `npm run build` (или `tsc`) — компилируется без ошибок. +- В конце: релейер собирается, сигнатуры соответствуют новому IDL. + +## ЧЕГО НЕ ДЕЛАТЬ +- Не трогать контракт (`program/`). +- Не коммитить — всё в рабочем дереве до приёмки человеком. +- Не менять `program/` и `deploy/`. +- Логику игры (вверх/вниз, статусы) не менять. + +## Проверка (что релеер готов) +- `npm run build` — 0 ошибок. +- Сигнатуры `createBet`/`closeBet` передают `bettor_token`/`vault_token`/`token_program`. +- `ensureVaultFunded` наливает токен, а не SOL. +- Логи/цикл работают с токен-единицами. \ No newline at end of file diff --git a/relayer/.env.example b/relayer/.env.example index 6beb0e0..5b1d924 100644 --- a/relayer/.env.example +++ b/relayer/.env.example @@ -1,14 +1,22 @@ # RPC эндпоинт кластера (localnet = запущенный solana-test-validator) SMART_RPC_URL=http://127.0.0.1:8899 -# Разрешить airDrop на localnet (bettor'ам и ваулту). На mainnet выключить. +# Mint наших фантиков (SPL-токен, decimals = 6; см. state.rs контракта). +# На localnet — адрес mint, созданный до старта релейера (mint_authority = админ). +# Обязателен: без него ATA/ваулт нельзя привязать к нужному токену. +SMART_TOKEN_MINT= + +# Разрешить «наивное» финансирование на localnet: SOL-airDrop бетторам +# (ренты/комиссии) + налив фантиков в ваулт через mint authority (ensureVaultFunded). +# На mainnet выключить — ваулт там финансируется off-chain. SMART_ALLOW_AIRDROP=1 -# Путь к keypair админа/релеера (тот же ключ = admin и relayer) +# Путь к keypair админа/релеера (тот же ключ = admin и relayer; на localnet ещё и mint authority) SMART_ADMIN_KEYPAIR=~/.config/solana/id.json -# Параметры контракта (должны совпадать со значениями при initialize) -SMART_MIN_AMOUNT=100000000 # 0.1 SOL в лампортах -SMART_MAX_AMOUNT=10000000000 # 10 SOL в лампортах +# Параметры контракта (должны совпадать со значениями при initialize). +# ВАЖНО: теперь это ТОКЕН-юниты (base units фантиков, decimals = 6), а не лампорты! +SMART_MIN_AMOUNT=100000000 # 100 фантиков +SMART_MAX_AMOUNT=10000000000 # 10 000 фантиков SMART_MULTIPLIER_BPS=13000 # 1.3x выплата в базисных пунктах SMART_EXPIRY_SECONDS=15 # экспирация ставки (секунды) diff --git a/relayer/.gitignore b/relayer/.gitignore new file mode 100644 index 0000000..04c01ba --- /dev/null +++ b/relayer/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ \ No newline at end of file diff --git a/relayer/idl/smart_updown.ts b/relayer/idl/smart_updown.ts index bb64b46..a7d58b6 100644 --- a/relayer/idl/smart_updown.ts +++ b/relayer/idl/smart_updown.ts @@ -1,8 +1,7 @@ /** - * Program IDL in camelCase format in order to be used in JS/TS. - * - * Note that this is only a type helper and is not the actual IDL. The original - * IDL can be found at `target/idl/smart_updown.json`. + * Program IDL in camelCase format (see @anchor-lang/core convertIdlToCamelCase). + * Mirrors relayer/idl/smart_updown.json but with names camelCased, exactly as + * the runtime Program instance exposes them (methods/accounts/types). */ export type SmartUpdown = { "address": "9ALsnxXNzDBv3mokngCHbruRTWUZiWR7pf7vswS1fCpf", @@ -16,7 +15,14 @@ export type SmartUpdown = { { "name": "closeBet", "discriminator": [ - 2 + 185, + 206, + 13, + 184, + 176, + 108, + 140, + 107 ], "accounts": [ { @@ -63,15 +69,54 @@ export type SmartUpdown = { } }, { - "name": "bettor", - "docs": [ - "CHECK: recipient must be the bet's bettor and is validated in the handler." - ], - "writable": true + "name": "bettor" }, { "name": "clock", "address": "SysvarC1ock11111111111111111111111111111111" + }, + { + "name": "vaultToken", + "docs": [ + "Program-owned token vault: PDA([GLOBAL_SEED, VAULT_SEED]); payouts are paid out from here." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + }, + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116 + ] + } + ] + } + }, + { + "name": "bettorToken", + "docs": [ + "Bettor's SPL-token account; the payout is sent here." + ], + "writable": true + }, + { + "name": "tokenProgram", + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" } ], "args": [ @@ -88,7 +133,14 @@ export type SmartUpdown = { { "name": "createBet", "discriminator": [ - 1 + 197, + 42, + 153, + 2, + 59, + 63, + 143, + 246 ], "accounts": [ { @@ -142,6 +194,49 @@ export type SmartUpdown = { { "name": "systemProgram", "address": "11111111111111111111111111111111" + }, + { + "name": "bettorToken", + "docs": [ + "Bettor's SPL-token account (our \"fanti\" mint); bet amount is pulled from here." + ], + "writable": true + }, + { + "name": "vaultToken", + "docs": [ + "Program-owned token vault: PDA([GLOBAL_SEED, VAULT_SEED]); bet amounts are escrowed here." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + }, + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116 + ] + } + ] + } + }, + { + "name": "tokenProgram", + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" } ], "args": [ @@ -166,7 +261,14 @@ export type SmartUpdown = { { "name": "initialize", "discriminator": [ - 0 + 175, + 175, + 109, + 31, + 13, + 152, + 155, + 237 ], "accounts": [ { @@ -220,7 +322,14 @@ export type SmartUpdown = { { "name": "update", "discriminator": [ - 3 + 219, + 200, + 88, + 176, + 158, + 63, + 253, + 127 ], "accounts": [ { @@ -386,10 +495,9 @@ export type SmartUpdown = { "types": [ { "name": "bet", - "serialization": "bytemuck", - "repr": { - "kind": "c" - }, + "docs": [ + "Open bet. `amount` is in token base units (decimals = 6), not lamports." + ], "type": { "kind": "struct", "fields": [ @@ -494,10 +602,13 @@ export type SmartUpdown = { }, { "name": "global", - "serialization": "bytemuck", - "repr": { - "kind": "c" - }, + "docs": [ + "Global game state.", + "", + "All money-related fields are denominated in **SPL-token base units**", + "(our \"fanti\" token, decimals = 6), not in lamports:", + "`min_amount`/`max_amount` bound a bet amount in token units." + ], "type": { "kind": "struct", "fields": [ @@ -547,6 +658,15 @@ export type SmartUpdown = { "name": "globalSeed", "type": "bytes", "value": "[103, 108, 111, 98, 97, 108]" + }, + { + "name": "vaultSeed", + "docs": [ + "Vault PDA seed: the token vault lives at PDA([GLOBAL_SEED, VAULT_SEED]).", + "It cannot reuse GLOBAL_SEED alone — that PDA address is the `global` state account itself." + ], + "type": "bytes", + "value": "[118, 97, 117, 108, 116]" } ] }; diff --git a/relayer/package-lock.json b/relayer/package-lock.json new file mode 100644 index 0000000..cba366f --- /dev/null +++ b/relayer/package-lock.json @@ -0,0 +1,1224 @@ +{ + "name": "smart-updown-relayer", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "smart-updown-relayer", + "version": "0.1.0", + "license": "ISC", + "dependencies": { + "@anchor-lang/core": "^1.0.0", + "@solana/spl-token": "^0.4.15", + "@solana/web3.js": "^1.98.0", + "bn.js": "^5.2.0" + }, + "devDependencies": { + "@types/bn.js": "^5.2.0", + "@types/node": "^25.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } + }, + "node_modules/@anchor-lang/borsh": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@anchor-lang/borsh/-/borsh-1.2.0.tgz", + "integrity": "sha512-qUC90JezAXyetwCqhLxkxh/r9ofRCd2D0fkEoEex7Vurw3pGDtf1r779v0cM4fsvOEIskK9nja/GkZOnlvJqYw==", + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.2.3", + "buffer-layout": "^1.2.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@solana/web3.js": "^1.69.1" + } + }, + "node_modules/@anchor-lang/core": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@anchor-lang/core/-/core-1.2.0.tgz", + "integrity": "sha512-GpHdhYnwVSEIvdZIeRCL7Q9LK0oTyoJ259r4mtc+a3W9oM3Iz6qezorzZcd92ASCJJ6MYqqdrM/b2JKDZpIzmw==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "@anchor-lang/borsh": "^1.2.0", + "@anchor-lang/errors": "^1.2.0", + "@noble/hashes": "^1.3.1", + "@solana/web3.js": "^1.69.1", + "bn.js": "^5.2.3", + "bs58": "^4.0.1", + "buffer-layout": "^1.2.2", + "eventemitter3": "^4.0.7", + "pako": "^2.0.3", + "superstruct": "^0.15.4", + "toml": "^3.0.0" + }, + "engines": { + "node": ">=20.18" + } + }, + "node_modules/@anchor-lang/errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@anchor-lang/errors/-/errors-1.2.0.tgz", + "integrity": "sha512-muEmHFs2UDrcmGUuj+IWiYW2/AazI3tMv6f6OT5KNwOuSLG4j9g4zv8Pqvo+Cj9F6deP4P2c6DWhSUBHdYL4+g==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@solana/buffer-layout": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz", + "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", + "license": "MIT", + "dependencies": { + "buffer": "~6.0.3" + }, + "engines": { + "node": ">=5.10" + } + }, + "node_modules/@solana/buffer-layout-utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@solana/buffer-layout-utils/-/buffer-layout-utils-0.3.0.tgz", + "integrity": "sha512-MuQOCC1j0np1xH9yAv0ZWWfwvr7Bt7Sz4LId11Wi4wDdAmJ+lobE+vHg/mZmGcihF0BIkqVBNxGmlv8QE5DrtA==", + "license": "Apache-2.0", + "dependencies": { + "@solana/buffer-layout": "^4.0.0", + "@solana/web3.js": "^1.32.0", + "bigint-buffer": "^1.1.5", + "bignumber.js": "^9.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@solana/codecs": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs/-/codecs-2.0.0-rc.1.tgz", + "integrity": "sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/codecs-data-structures": "2.0.0-rc.1", + "@solana/codecs-numbers": "2.0.0-rc.1", + "@solana/codecs-strings": "2.0.0-rc.1", + "@solana/options": "2.0.0-rc.1" + }, + "peerDependencies": { + "typescript": ">=5" + } + }, + "node_modules/@solana/codecs-core": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.0.0-rc.1.tgz", + "integrity": "sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==", + "license": "MIT", + "dependencies": { + "@solana/errors": "2.0.0-rc.1" + }, + "peerDependencies": { + "typescript": ">=5" + } + }, + "node_modules/@solana/codecs-data-structures": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-rc.1.tgz", + "integrity": "sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/codecs-numbers": "2.0.0-rc.1", + "@solana/errors": "2.0.0-rc.1" + }, + "peerDependencies": { + "typescript": ">=5" + } + }, + "node_modules/@solana/codecs-numbers": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.0.0-rc.1.tgz", + "integrity": "sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/errors": "2.0.0-rc.1" + }, + "peerDependencies": { + "typescript": ">=5" + } + }, + "node_modules/@solana/codecs-strings": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-strings/-/codecs-strings-2.0.0-rc.1.tgz", + "integrity": "sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/codecs-numbers": "2.0.0-rc.1", + "@solana/errors": "2.0.0-rc.1" + }, + "peerDependencies": { + "fastestsmallesttextencoderdecoder": "^1.0.22", + "typescript": ">=5" + } + }, + "node_modules/@solana/errors": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.0.0-rc.1.tgz", + "integrity": "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "commander": "^12.1.0" + }, + "bin": { + "errors": "bin/cli.mjs" + }, + "peerDependencies": { + "typescript": ">=5" + } + }, + "node_modules/@solana/options": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/options/-/options-2.0.0-rc.1.tgz", + "integrity": "sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/codecs-data-structures": "2.0.0-rc.1", + "@solana/codecs-numbers": "2.0.0-rc.1", + "@solana/codecs-strings": "2.0.0-rc.1", + "@solana/errors": "2.0.0-rc.1" + }, + "peerDependencies": { + "typescript": ">=5" + } + }, + "node_modules/@solana/spl-token": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.4.15.tgz", + "integrity": "sha512-3Lof3mNov8NVQ3PalIWb1Jgr/TZ6lYM+/sexv2TLqdhNFVth2OfWmH3d7QucgMjSbokkjNiNlRr6I8Fd269uaw==", + "license": "Apache-2.0", + "dependencies": { + "@solana/buffer-layout": "^4.0.0", + "@solana/buffer-layout-utils": "^0.3.0", + "@solana/spl-token-group": "^0.0.7", + "@solana/spl-token-metadata": "^0.1.6", + "buffer": "^6.0.3" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@solana/web3.js": "^1.95.5" + } + }, + "node_modules/@solana/spl-token-group": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@solana/spl-token-group/-/spl-token-group-0.0.7.tgz", + "integrity": "sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug==", + "license": "Apache-2.0", + "dependencies": { + "@solana/codecs": "2.0.0-rc.1" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@solana/web3.js": "^1.95.3" + } + }, + "node_modules/@solana/spl-token-metadata": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@solana/spl-token-metadata/-/spl-token-metadata-0.1.6.tgz", + "integrity": "sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA==", + "license": "Apache-2.0", + "dependencies": { + "@solana/codecs": "2.0.0-rc.1" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@solana/web3.js": "^1.95.3" + } + }, + "node_modules/@solana/web3.js": { + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.99.0.tgz", + "integrity": "sha512-QZYQ2T1z6xWisoyALPq25i/QZTsRlM02BABtAsfaQ1p8wX4SdTfxrKTRue/ZZqrhNVh5oL7T/DUFiTS9DRgxow==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@noble/curves": "^1.9.7", + "@noble/hashes": "^1.8.0", + "@solana/buffer-layout": "^4.0.1", + "@solana/codecs-numbers": "^5.5.1", + "agentkeepalive": "^4.6.0", + "bn.js": "^5.2.5", + "borsh": "^0.7.0", + "bs58": "^4.0.1", + "buffer": "6.0.3", + "fast-stable-stringify": "^1.0.0", + "jayson": "^4.3.0", + "node-fetch": "^2.7.0", + "rpc-websockets": "^9.0.2", + "superstruct": "^2.0.2" + } + }, + "node_modules/@solana/web3.js/node_modules/@solana/codecs-core": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-5.5.1.tgz", + "integrity": "sha512-TgBt//bbKBct0t6/MpA8ElaOA3sa8eYVvR7LGslCZ84WiAwwjCY0lW/lOYsFHJQzwREMdUyuEyy5YWBKtdh8Rw==", + "license": "MIT", + "dependencies": { + "@solana/errors": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/web3.js/node_modules/@solana/codecs-numbers": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-5.5.1.tgz", + "integrity": "sha512-rllMIZAHqmtvC0HO/dc/21wDuWaD0B8Ryv8o+YtsICQBuiL/0U4AGwH7Pi5GNFySYk0/crSuwfIqQFtmxNSPFw==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "5.5.1", + "@solana/errors": "5.5.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/web3.js/node_modules/@solana/errors": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-5.5.1.tgz", + "integrity": "sha512-vFO3p+S7HoyyrcAectnXbdsMfwUzY2zYFUc2DEe5BwpiE9J1IAxPBGjOWO6hL1bbYdBrlmjNx8DXCslqS+Kcmg==", + "license": "MIT", + "dependencies": { + "chalk": "5.6.2", + "commander": "14.0.2" + }, + "bin": { + "errors": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/web3.js/node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/@solana/web3.js/node_modules/superstruct": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", + "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.13.tgz", + "integrity": "sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.9.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.6.tgz", + "integrity": "sha512-JR6Q/PV5DKFvjrGFVqQJdeG0qvsqQQLDa3TzFrqVwhqRXqwNaxPo2KYCtCQXpOdIulCouKwT7a6in9nFthBAzw==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bigint-buffer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz", + "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "bindings": "^1.3.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bn.js": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.5.tgz", + "integrity": "sha512-Vq886eXykuP5E6HcKSSStP3bJgrE6In5WKxVUvJ8XGpWWYs2xZHWqUwzCtGgEtBcxyd57KBFDPFoUfNzdaHCNg==", + "license": "MIT" + }, + "node_modules/borsh": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.2.0", + "bs58": "^4.0.0", + "text-encoding-utf-8": "^1.0.2" + } + }, + "node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-layout": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/buffer-layout/-/buffer-layout-1.2.2.tgz", + "integrity": "sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA==", + "license": "MIT", + "engines": { + "node": ">=4.5" + } + }, + "node_modules/bufferutil": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", + "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT" + }, + "node_modules/es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "license": "MIT", + "dependencies": { + "es6-promise": "^4.0.3" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "engines": { + "node": "> 0.1.90" + } + }, + "node_modules/fast-stable-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", + "license": "MIT" + }, + "node_modules/fastestsmallesttextencoderdecoder": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", + "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==", + "license": "CC0-1.0", + "peer": true + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/jayson": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.3.0.tgz", + "integrity": "sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ==", + "license": "MIT", + "dependencies": { + "@types/connect": "^3.4.33", + "@types/node": "^12.12.54", + "@types/ws": "^7.4.4", + "commander": "^2.20.3", + "delay": "^5.0.0", + "es6-promisify": "^5.0.0", + "eyes": "^0.1.8", + "isomorphic-ws": "^4.0.1", + "json-stringify-safe": "^5.0.1", + "stream-json": "^1.9.1", + "uuid": "^8.3.2", + "ws": "^7.5.10" + }, + "bin": { + "jayson": "bin/jayson.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jayson/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "license": "MIT" + }, + "node_modules/jayson/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/pako": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz", + "integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "(MIT AND Zlib)" + }, + "node_modules/rpc-websockets": { + "version": "9.3.9", + "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.3.9.tgz", + "integrity": "sha512-2iQDaTB4g5fDB2ihrTFSJSibCEuxaRi1q7qTW7ZO9/M5/TC+ToHA4D9/ffNLEbAoHNNrcdeP05oATNk44SKZXA==", + "license": "LGPL-3.0-only", + "dependencies": { + "@swc/helpers": "^0.5.11", + "@types/uuid": "^10.0.0", + "@types/ws": "^8.2.2", + "buffer": "^6.0.3", + "eventemitter3": "^5.0.1", + "uuid": "^14.0.0", + "ws": "^8.5.0" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/kozjak" + }, + "optionalDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^6.0.0" + } + }, + "node_modules/rpc-websockets/node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/rpc-websockets/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/rpc-websockets/node_modules/utf-8-validate": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.6.tgz", + "integrity": "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/rpc-websockets/node_modules/uuid": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.2.tgz", + "integrity": "sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/rpc-websockets/node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/stream-chain": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", + "license": "BSD-3-Clause" + }, + "node_modules/stream-json": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", + "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", + "license": "BSD-3-Clause", + "dependencies": { + "stream-chain": "^2.2.5" + } + }, + "node_modules/superstruct": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.15.5.tgz", + "integrity": "sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==", + "license": "MIT" + }, + "node_modules/text-encoding-utf-8": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" + }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", + "license": "MIT" + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/ws": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", + "license": "MIT", + "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 + } + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/relayer/package.json b/relayer/package.json index a06805b..91b289e 100644 --- a/relayer/package.json +++ b/relayer/package.json @@ -4,13 +4,16 @@ "private": true, "license": "ISC", "scripts": { + "build": "tsc -p tsconfig.json", "typecheck": "tsc -p tsconfig.json --noEmit", + "test": "ts-node src/check.ts", "init": "ts-node src/index.ts --init --once", "run": "ts-node src/index.ts --once", "loop": "ts-node src/index.ts" }, "dependencies": { "@anchor-lang/core": "^1.0.0", + "@solana/spl-token": "^0.4.15", "@solana/web3.js": "^1.98.0", "bn.js": "^5.2.0" }, diff --git a/relayer/src/check.ts b/relayer/src/check.ts new file mode 100644 index 0000000..0405ee8 --- /dev/null +++ b/relayer/src/check.ts @@ -0,0 +1,129 @@ +/** + * Оффлайн-проверка релейера (без сети): компиляция и сигнатуры под новый + * токеновый IDL. + * + * 1. PDA-деривации (global/bet/vault) совпадают с седами IDL. + * 2. Инструкции create_bet/close_bet содержат bettor_token / vault_token / + * token_program с нужными адресами и writable-флагами. + * + * Запуск: `npm test`. + */ +import { Keypair, PublicKey } from "@solana/web3.js"; +import BN from "bn.js"; +import { getAssociatedTokenAddress, TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import idl from "../idl/smart_updown.json"; +import { globalPda, betPda, vaultPda } from "./pda"; +import { makeConnection, makeProgram } from "./program"; + +const ok = (msg: string) => console.log(`ok: ${msg}`); +let failed = 0; +function check(cond: boolean, msg: string): void { + if (cond) { + ok(msg); + } else { + failed++; + console.error(`FAIL: ${msg}`); + } +} + +/** PDA из собственных (JSON) севов IDL — независимо от pda.ts. */ +function pdaFromIdlSeeds(programId: PublicKey, seeds: ReadonlyArray<{ kind: string; value?: unknown; path?: string }>): PublicKey { + const buffers = seeds.map((s) => { + if (s.kind === "const") return Buffer.from(s.value as number[]); + if (s.kind === "arg" && s.path === "id") return Buffer.alloc(8); // id = 0 + throw new Error(`unsupported seed: ${JSON.stringify(s)}`); + }); + return PublicKey.findProgramAddressSync(buffers as Buffer[], programId)[0]; +} + +function idlIx(name: string) { + const ix = (idl as any).instructions.find((i: any) => i.name === name); + if (!ix) throw new Error(`instruction ${name} not in IDL`); + return ix; +} + +async function main(): Promise { + const programId = new PublicKey((idl as any).address as string); + const createIx = idlIx("create_bet"); + const closeIx = idlIx("close_bet"); + + // 1. PDA-деривации против севов IDL + const acct = (ix: any, name: string) => ix.accounts.find((a: any) => a.name === name); + check( + globalPda(programId).equals(pdaFromIdlSeeds(programId, (acct(createIx, "global") as any).pda.seeds)), + "globalPda совпадает с седами global из IDL" + ); + check( + betPda(programId, new BN(0)).equals(pdaFromIdlSeeds(programId, (acct(createIx, "bet") as any).pda.seeds)), + "betPda(0) совпадает с седами bet из IDL" + ); + check( + vaultPda(programId).equals(pdaFromIdlSeeds(programId, (acct(createIx, "vault_token") as any).pda.seeds)), + "vaultPda совпадает с седами vault_token из IDL" + ); + check( + vaultPda(programId).equals(pdaFromIdlSeeds(programId, (acct(closeIx, "vault_token") as any).pda.seeds)), + "vaultPda совпадает с седами vault_token в close_bet" + ); + + // 2. Сборка инструкций оффлайн и проверка ключей + const connection = makeConnection("http://127.0.0.1:8899"); // не используется сетью + const admin = Keypair.generate(); + const bettor = Keypair.generate(); + const mint = new PublicKey("11111111111111111111111111111111"); // плейсхолдер + const program = makeProgram(connection, admin); + + const vaultToken = vaultPda(program.programId); + const bettorToken = await getAssociatedTokenAddress(mint, bettor.publicKey); + + const create = await program.methods + .createBet(new BN(0), new BN(0), new BN(100), new BN(100_000_000)) + .accountsPartial({ + bettor: bettor.publicKey, + bettorToken: bettorToken, + vaultToken: vaultToken, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .signers([bettor]) + .instruction(); + + check(create.programId.equals(programId), "create_bet: programId"); + const cByName = (p: PublicKey) => create.keys.find((k) => k.pubkey.equals(p)); + check(!!(cByName(bettor.publicKey)!.isSigner && cByName(bettor.publicKey)!.isWritable), "create_bet: bettor — writable signer"); + check(!!(cByName(bettorToken)!.isWritable && !cByName(bettorToken)!.isSigner), "create_bet: bettor_token — writable"); + check(!!(cByName(vaultToken)!.isWritable && !cByName(vaultToken)!.isSigner), "create_bet: vault_token — writable"); + check(cByName(TOKEN_PROGRAM_ID) !== undefined && !cByName(TOKEN_PROGRAM_ID)!.isWritable, "create_bet: token_program присутствует"); + check(create.keys.length === 8, "create_bet: 8 аккаунтов (IDL-состав)"); + + const close = await program.methods + .closeBet(new BN(0), new BN(100_100_000)) + .accountsPartial({ + relayer: admin.publicKey, + bettor: bettor.publicKey, + bettorToken: bettorToken, + vaultToken: vaultToken, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .signers([admin]) + .instruction(); + + check(close.programId.equals(programId), "close_bet: programId"); + const kByName = (p: PublicKey) => close.keys.find((k) => k.pubkey.equals(p)); + check(kByName(admin.publicKey)!.isSigner, "close_bet: relayer — signer"); + check(kByName(bettor.publicKey) !== undefined && !kByName(bettor.publicKey)!.isSigner, "close_bet: bettor — не signer"); + check(!!(kByName(bettorToken)!.isWritable), "close_bet: bettor_token — writable"); + check(!!(kByName(vaultToken)!.isWritable), "close_bet: vault_token — writable"); + check(kByName(TOKEN_PROGRAM_ID) !== undefined && !kByName(TOKEN_PROGRAM_ID)!.isWritable, "close_bet: token_program присутствует"); + check(close.keys.length === 8, "close_bet: 8 аккаунтов (IDL-состав)"); + + if (failed > 0) { + console.error(`\ncheck: ${failed} проверок ПРОВАЛЕНО`); + process.exit(1); + } + console.log("\ncheck: все проверки пройдены — сигнатуры релейера соответствуют новому токеновому IDL."); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/relayer/src/config.ts b/relayer/src/config.ts index f536ae1..242a936 100644 --- a/relayer/src/config.ts +++ b/relayer/src/config.ts @@ -13,15 +13,23 @@ export const SIDE_DOWN = 1; export interface Config { /** RPC endpoint of the cluster (localnet by default). */ rpcUrl: string; - /** Localnet allows naive airdrops (to bettors and the house vault). */ + /** + * Localnet allows naive funding: SOL airdrop to bettors (rent/fees) and + * minting tokens into the vault via the mint authority. Disable on mainnet. + */ allowAirdrop: boolean; /** Program id. Defaults to the value embedded in the IDL. */ programId: PublicKey; - /** House admin/relayer keypair file path. */ + /** + * Mint of our SPL "фантики" (decimals = 6, per the contract). + * All amounts in this relayer are token base units of this mint. + */ + tokenMint: PublicKey; + /** House admin/relayer keypair path (also the localnet mint authority). */ adminKeypairPath: string; - /** Initial parameters used by `init` (must match on-chain constants). */ - minAmountLamports: number; - maxAmountLamports: number; + /** Bet amount bounds in token base units (NOT lamports); must match the on-chain `initialize` values. */ + minAmount: number; + maxAmount: number; multiplierBps: number; expirySeconds: number; } @@ -43,14 +51,17 @@ export function loadConfig(): Config { const allowAirdrop = env("SMART_ALLOW_AIRDROP", "1") === "1"; const adminKeypairPath = env("SMART_ADMIN_KEYPAIR", path.join(os.homedir(), ".config/solana/id.json")); + const tokenMintRaw = env("SMART_TOKEN_MINT", ""); return { rpcUrl, allowAirdrop, programId: PublicKey.default, // placeholder; real value read from the IDL below + // Плейсхолдер: настоящий адрес фантичного mint задаётся через SMART_TOKEN_MINT. + tokenMint: tokenMintRaw ? new PublicKey(tokenMintRaw) : PublicKey.default, adminKeypairPath, - minAmountLamports: envNum("SMART_MIN_AMOUNT", 100_000_000), // 0.1 SOL - maxAmountLamports: envNum("SMART_MAX_AMOUNT", 10_000_000_000), // 10 SOL + minAmount: envNum("SMART_MIN_AMOUNT", 100_000_000), // 100 фантиков (decimals = 6) + maxAmount: envNum("SMART_MAX_AMOUNT", 10_000_000_000), // 10 000 фантиков multiplierBps: envNum("SMART_MULTIPLIER_BPS", 13_000), // 1.3x expirySeconds: envNum("SMART_EXPIRY_SECONDS", 15), }; diff --git a/relayer/src/frontStub.ts b/relayer/src/frontStub.ts index b511219..604b721 100644 --- a/relayer/src/frontStub.ts +++ b/relayer/src/frontStub.ts @@ -11,7 +11,8 @@ import { SIDE_DOWN, SIDE_UP } from "./config"; export interface BetRequest { bettor: Keypair; side: number; // SIDE_UP | SIDE_DOWN - amountLamports: number; + /** Сумма ставки в базовых юнитах SPL-токена (не лампорты). */ + amount: number; } /** Очередь входящих ставок. В заглушке наполняем случайными значениями. */ @@ -20,5 +21,5 @@ export function nextBetRequest(minAmount: number, maxAmount: number): BetRequest const side = Math.random() < 0.5 ? SIDE_UP : SIDE_DOWN; const amount = minAmount + Math.floor(Math.random() * (maxAmount - minAmount + 1)); - return { bettor, side, amountLamports: amount }; + return { bettor, side, amount }; } diff --git a/relayer/src/index.ts b/relayer/src/index.ts index 59fc019..fe4e7a4 100644 --- a/relayer/src/index.ts +++ b/relayer/src/index.ts @@ -1,4 +1,5 @@ import { LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js"; +import { getMint } from "@solana/spl-token"; import { Program } from "@anchor-lang/core"; import { loadConfig, loadKeypair, SIDE_UP } from "./config"; import { getPrice } from "./priceSource"; @@ -9,6 +10,7 @@ import { closeBet, readGlobal, ensureVaultFunded, + ensureBettorTokenAccount, } from "./ops"; import { betPda } from "./pda"; import { makeConnection, makeProgram } from "./program"; @@ -21,6 +23,8 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); const STATUS_OPEN = 0; async function fundBettor(connection: any, bettorPubkey: PublicKey): Promise { + // SOL всё ещё нужен беттору: рент PDA-аккаунта ставки + комиссия за tx. + // Саму ставку (фантики) он закрывает из своего токен-аккаунта (ATA). const bal = await connection.getBalance(bettorPubkey); if (bal < 2 * LAMPORTS_PER_SOL_NUM) { await connection.requestAirdrop(bettorPubkey, 5 * LAMPORTS_PER_SOL_NUM); @@ -29,13 +33,23 @@ async function fundBettor(connection: any, bettorPubkey: PublicKey): Promise "N.NNNNNN" по decimals. */ +function humanize(units: string | number | bigint, decimals: number): string { + const n = BigInt(units); + const sign = n < 0n ? "-" : ""; + const abs = n < 0n ? -n : n; + const pow = 10n ** BigInt(decimals); + return `${sign}${abs / pow}.${(abs % pow).toString().padStart(decimals, "0")}`; +} + /** Закрывает ставку, повторяя при «equal price» (exit==entry) пока не зафиналится. */ async function settleBet( program: Program, id: any, entryPrice: number, relayer: any, - bettor: PublicKey + bettor: PublicKey, + mint: PublicKey ): Promise { for (let attempt = 1; ; attempt++) { const exitPrice = await getPrice(); @@ -44,7 +58,7 @@ async function settleBet( await sleep(2000); continue; } - const sig = await closeBet(program, id, exitPrice, relayer, bettor); + const sig = await closeBet(program, id, exitPrice, relayer, bettor, mint); console.log(`close_bet #${id} exit=${exitPrice} (attempt ${attempt}) tx=${sig}`); const status = await getStatus(program, id); @@ -70,6 +84,15 @@ async function main(): Promise { console.log(`RPC: ${cfg.rpcUrl}`); console.log(`Admin/relayer: ${admin.publicKey.toBase58()}`); + console.log(`Token mint: ${cfg.tokenMint.toBase58()}`); + + // Decimals фантиков (см. state.rs = 6). Только для человекочитаемых логов. + let decimals = 6; + try { + decimals = (await getMint(connection, cfg.tokenMint)).decimals; + } catch { + // mint ещё не создан (плейсхолдер) — дефолт из ТЗ контракта + } const global = await readGlobal(program, program.programId); if (!global) { @@ -79,21 +102,21 @@ async function main(): Promise { ); } await initialize(program, { - minAmount: cfg.minAmountLamports, - maxAmount: cfg.maxAmountLamports, + minAmount: cfg.minAmount, + maxAmount: cfg.maxAmount, multiplierBps: cfg.multiplierBps, expirySeconds: cfg.expirySeconds, }, admin); console.log("Контракт инициализирован."); } else { console.log( - `Global: counter=${global.counter} vault=${(global.lamports / LAMPORTS_PER_SOL_NUM).toFixed(2)} SOL ` + + `Global: counter=${global.counter} vault=${humanize(global.vaultBalance, decimals)} fant ` + `expiry=${global.expirySeconds}s paused=${global.paused}` ); } if (cfg.allowAirdrop) { - await ensureVaultFunded(connection, program, 10 * LAMPORTS_PER_SOL_NUM); + await ensureVaultFunded(connection, program, admin, cfg); } const loops = process.argv.includes("--once") ? 1 : Infinity; @@ -101,24 +124,26 @@ async function main(): Promise { while (n < loops) { n++; - const req = nextBetRequest(cfg.minAmountLamports, cfg.maxAmountLamports); + const req = nextBetRequest(cfg.minAmount, cfg.maxAmount); await fundBettor(connection, req.bettor.publicKey); + // Беттору нужен токен-аккаунт (ATA) — без него create_bet не пройдёт. + await ensureBettorTokenAccount(connection, admin, cfg.tokenMint, req.bettor.publicKey); const entryPrice = await getPrice(); - await createBet(program, req.bettor, { + await createBet(program, cfg.tokenMint, req.bettor, { side: req.side, - amount: req.amountLamports, + amount: req.amount, entryPrice, }); console.log( - `ставка ${req.side === SIDE_UP ? "UP" : "DOWN"} на ${(req.amountLamports / LAMPORTS_PER_SOL_NUM).toFixed(3)} SOL, жду ${cfg.expirySeconds}s...` + `ставка ${req.side === SIDE_UP ? "UP" : "DOWN"} на ${humanize(req.amount, decimals)} fant, жду ${cfg.expirySeconds}s...` ); await sleep((cfg.expirySeconds + 1) * 1000); const g0 = await readGlobal(program, program.programId); const id = g0!.counter.subn(1); - await settleBet(program, id, entryPrice, admin, req.bettor.publicKey); + await settleBet(program, id, entryPrice, admin, req.bettor.publicKey, cfg.tokenMint); const outcome = await program.account.bet.fetchNullable(betPda(program.programId, id)); console.log( diff --git a/relayer/src/ops.ts b/relayer/src/ops.ts index 3660f3e..24e9af0 100644 --- a/relayer/src/ops.ts +++ b/relayer/src/ops.ts @@ -1,14 +1,26 @@ -import { Connection, Keypair, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js"; +import { Connection, Keypair, PublicKey } from "@solana/web3.js"; +import { + TOKEN_PROGRAM_ID, + createAssociatedTokenAccount, + getAccount, + getAssociatedTokenAddress, + getOrCreateAssociatedTokenAccount, + mintTo, +} from "@solana/spl-token"; import BN from "bn.js"; import { Program } from "@anchor-lang/core"; -import { SIDE_UP } from "./config"; -import { globalPda } from "./pda"; +import { Config, SIDE_UP } from "./config"; +import { globalPda, vaultPda } from "./pda"; import type { SmartUpdown } from "../idl/smart_updown"; -const LAMPORTS_PER_SOL_NUM = Number(LAMPORTS_PER_SOL); +const BPS_DIVISOR = 10_000n; -/** Считывает account Global (ваулт). Возвращает null, если ещё не инициализирован. */ +/** + * Считывает account Global + баланс токеного ваулта. + * Возвращает null, если глобальное состояние ещё не инициализировано. + * Суммы — в базовых юнитах SPL-токена (decimals = 6), не в лампортах. + */ export async function readGlobal( program: Program, programId: PublicKey @@ -21,12 +33,21 @@ export async function readGlobal( paused: BN; admin: PublicKey; relayer: PublicKey; - lamports: number; + /** Баланс vaultToken (PDA-ваула) в токен-юнитах; "0", если аккаунт ещё не создан. */ + vaultBalance: string; } | null> { const global = globalPda(programId); const raw = await program.account.global.fetchNullable(global); if (!raw) return null; - const lamports = await program.provider.connection.getBalance(global); + let vaultBalance = "0"; + try { + const bal = await program.provider.connection.getTokenAccountBalance( + vaultPda(programId) + ); + vaultBalance = bal.value.amount || "0"; + } catch { + vaultBalance = "0"; + } return { counter: raw.counter, minAmount: raw.minAmount, @@ -36,7 +57,7 @@ export async function readGlobal( paused: raw.paused, admin: raw.admin, relayer: raw.relayer, - lamports, + vaultBalance, }; } @@ -58,7 +79,7 @@ export async function initialize( new BN(args.multiplierBps), new BN(args.expirySeconds) ) - .accounts({ + .accountsPartial({ admin: admin.publicKey, }) .signers([admin]) @@ -69,6 +90,7 @@ export async function initialize( /** Создаёт ставку от имени пользователя. Возвращает фактический id (== counter). */ export async function createBet( program: Program, + mint: PublicKey, bettor: Keypair, args: { side: number; amount: number; entryPrice: number } ): Promise { @@ -77,15 +99,25 @@ export async function createBet( const id = global.counter; const sig = await program.methods - .createBet(new BN(id), new BN(args.side), new BN(args.amount), new BN(args.entryPrice)) - .accounts({ + .createBet( + new BN(id), + new BN(args.side), + new BN(args.amount), + new BN(args.entryPrice) + ) + .accountsPartial({ bettor: bettor.publicKey, + bettorToken: await getAssociatedTokenAddress(mint, bettor.publicKey), + vaultToken: vaultPda(program.programId), + tokenProgram: TOKEN_PROGRAM_ID, }) .signers([bettor]) .rpc(); - console.log(`create_bet #${id} side=${args.side === SIDE_UP ? "UP" : "DOWN"} ` + - `amount=${(args.amount / LAMPORTS_PER_SOL_NUM).toFixed(3)} SOL entry=${args.entryPrice} tx=${sig}`); + console.log( + `createBet #${id} side=${args.side === SIDE_UP ? "UP" : "DOWN"} ` + + `amount=${args.amount} (token base units) entry=${args.entryPrice} tx=${sig}` + ); return id; } @@ -99,33 +131,67 @@ export async function closeBet( id: BN, exitPrice: number, relayer: Keypair, - bettor: PublicKey + bettor: PublicKey, + mint: PublicKey ): Promise { return program.methods .closeBet(new BN(id), new BN(exitPrice)) - .accounts({ + .accountsPartial({ relayer: relayer.publicKey, bettor, + bettorToken: await getAssociatedTokenAddress(mint, bettor), + vaultToken: vaultPda(program.programId), + tokenProgram: TOKEN_PROGRAM_ID, }) .signers([relayer]) .rpc(); } +/** + * Создаёт ATA беттора, если ещё нет (рент платит админ; сам ATA не требует + * подписи владельца). Без аккаунта createBet не пройдёт: контракт сам + * его не инициализирует. + */ +export async function ensureBettorTokenAccount( + connection: Connection, + admin: Keypair, + mint: PublicKey, + bettor: PublicKey +): Promise { + const ata = await getAssociatedTokenAddress(mint, bettor); + try { + await getAccount(connection, ata); + } catch { + await createAssociatedTokenAccount(connection, admin, mint, bettor); + console.log(`bettor ATA created ${bettor.toBase58()} -> ${ata.toBase58()}`); + } +} + /** * Догружает ваулт (house bankroll), чтобы хватило на выплаты 1.3x. - * На localnet используем airDrop на PDA ваулта; на mainnet воздух не накапает — - * там нужно переводить SOL с админа (off-chain, вне этого CLI). + * Теперь это ТОКЕН, не SOL: на localnet mint-авторитетом выступает admin + * (см. state.rs: decimals = 6), так что наливается через mintTo в PDA-ваулт. + * На mainnet наливается off-chain переводом фантиков (вне этого CLI). */ export async function ensureVaultFunded( connection: Connection, program: Program, - amountLamports: number + admin: Keypair, + cfg: Config ): Promise { - const global = globalPda(program.programId); - const current = await connection.getBalance(global); - const needed = amountLamports; // будущая выплата (до 1.3x), запасаем с запасом - if (current >= needed) return; - const topUp = needed - current + 5 * LAMPORTS_PER_SOL_NUM; // +5 SOL запаса - await connection.requestAirdrop(global, topUp); - console.log(`vault funded +${(topUp / LAMPORTS_PER_SOL_NUM).toFixed(2)} SOL`); + const mint = cfg.tokenMint; + const vaultToken = vaultPda(program.programId); + + // PDA-ваулт (ATA с owner = PDA([global, vault])) может не существовать — + // контракт его не создаёт. ATA не требует подписи owner (allowOwnerOffCurve), + // поэтому создаём её от имени admin. + await getOrCreateAssociatedTokenAccount(connection, admin, mint, vaultToken, true); + + const bal = await connection.getTokenAccountBalance(vaultToken); + const current = BigInt(bal.value.amount || "0"); + const target = (BigInt(cfg.maxAmount) * BigInt(cfg.multiplierBps) / BPS_DIVISOR) * 2n; + if (current >= target) return; + const topUp = target - current; + await mintTo(connection, admin, mint, vaultToken, admin, topUp); + console.log(`vault funded +${topUp} token base units (target ${target})`); } diff --git a/relayer/src/pda.ts b/relayer/src/pda.ts index 06dcce3..a144574 100644 --- a/relayer/src/pda.ts +++ b/relayer/src/pda.ts @@ -4,6 +4,7 @@ import BN from "bn.js"; /** Seeds, согласованные с контрактом (см. constants.rs). */ export const GLOBAL_SEED = Buffer.from("global"); export const BET_SEED = Buffer.from("bet"); +export const VAULT_SEED = Buffer.from("vault"); export function globalPda(programId: PublicKey): PublicKey { return PublicKey.findProgramAddressSync([GLOBAL_SEED], programId)[0]; @@ -14,3 +15,12 @@ export function betPda(programId: PublicKey, id: BN): PublicKey { idLE.writeBigUInt64LE(BigInt(id.toString(10))); return PublicKey.findProgramAddressSync([BET_SEED, idLE], programId)[0]; } + +/** + * PDA токеного ваулта: PDA([GLOBAL_SEED, VAULT_SEED]) — совпадает с контрактом + * (vault_token в create_bet/close_bet). Сам по себе global PDA ([GLOBAL_SEED]) + * ваултом быть не может: это адрес state-аккаунта. + */ +export function vaultPda(programId: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync([GLOBAL_SEED, VAULT_SEED], programId)[0]; +}