Compare commits
47 Commits
main
...
extract-sources
| Author | SHA1 | Date | |
|---|---|---|---|
| ff2289e68d | |||
| a51fb3ada2 | |||
| 4cf583f497 | |||
| 32faec7515 | |||
| 515adb25c9 | |||
| 9f100ee122 | |||
| 2133f65e21 | |||
| c8a95b1f0e | |||
| 2081c6cb0b | |||
| 5fdd6902bd | |||
| 015df5949d | |||
| f74942bbcb | |||
| be0bf0a2bb | |||
| b976cd0629 | |||
| c08b0dcbc6 | |||
| 3cffb000c3 | |||
| 59f425d680 | |||
| bd5ace0b76 | |||
| 026216a73c | |||
| da48e278ae | |||
| 54492a1f27 | |||
| 79d93335a0 | |||
| 4d90eba107 | |||
| 63f8a2eaa2 | |||
| 57067d920d | |||
| 3e4b8a3bfe | |||
| 6502b236dc | |||
| 655cebe466 | |||
| b5b791608f | |||
| 8b9dceec1b | |||
| baa3c80d4f | |||
| aed2c77b05 | |||
| 123015a4a7 | |||
| f42de51ad2 | |||
| 4f08c356f0 | |||
| 09ed6fd15c | |||
| ec88bfdd6d | |||
| bad35bcf97 | |||
| 971de6aefb | |||
| 0904b1b76b | |||
| 133c5a2147 | |||
| 110c09cf69 | |||
| c6c00c511e | |||
| 8d8958484e | |||
| 7439862bdb | |||
| 5cf3a7a8d1 | |||
| a9d54feab6 |
@@ -0,0 +1,3 @@
|
|||||||
|
# >>> cortexkit:magic-context
|
||||||
|
magic-context/
|
||||||
|
# <<< cortexkit:magic-context
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
name: deploy-contract
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy-contract:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set our dev keypair as default solana wallet
|
||||||
|
run: |
|
||||||
|
mkdir -p "$HOME/.config/solana"
|
||||||
|
cp deploy/dev_smart_updown-keypair.json "$HOME/.config/solana/id.json"
|
||||||
|
# убедимся что это именно наш ключ
|
||||||
|
ls -l "$HOME/.config/solana/id.json"
|
||||||
|
|
||||||
|
- name: Setup Rust toolchain + C compiler
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y --no-install-recommends build-essential pkg-config libssl-dev clang podman
|
||||||
|
# пробуем стандартный rustup; при недоступности static.rust-lang.org — качаем через podman-контейнер
|
||||||
|
if ! curl -sSfL --max-time 60 https://sh.rustup.rs -o /tmp/rustup-init.sh 2>/dev/null; then
|
||||||
|
echo "sh.rustup.rs недоступен, качаем rustup-init через контейнер"
|
||||||
|
podman run --rm --network=host -v /tmp:/out \
|
||||||
|
gitea/runner-images:ubuntu-latest \
|
||||||
|
sh -c 'curl -sSfL https://static.rust-lang.org/rustup/dist/x86_64-unknown-linux-gnu/rustup-init -o /out/rustup-init-binary' 2>&1 | tail -1
|
||||||
|
echo "x" >> /tmp/rustup-init.sh.tmp 2>/dev/null || true
|
||||||
|
# конвертируем бинарь в то, что rustup принимает
|
||||||
|
cp /tmp/rustup-init-binary /tmp/rustup-init.sh 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
# если это бинарь — запускаем его напрямую
|
||||||
|
chmod +x /tmp/rustup-init.sh 2>/dev/null || true
|
||||||
|
if file /tmp/rustup-init.sh 2>/dev/null | grep -q 'ELF'; then
|
||||||
|
/tmp/rustup-init.sh -y --profile minimal --default-toolchain 1.91.0
|
||||||
|
else
|
||||||
|
sh /tmp/rustup-init.sh -y --profile minimal --default-toolchain 1.91.0 2>/dev/null || \
|
||||||
|
/tmp/rustup-init.sh -y --profile minimal --default-toolchain 1.91.0
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Setup Solana CLI (Agave 4.2.2 — builder умеет SBPFv3, как прод mainnet)
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
SOL_VERSION=v4.2.2
|
||||||
|
cd /tmp
|
||||||
|
curl -sSfL \
|
||||||
|
"https://github.com/anza-xyz/agave/releases/download/${SOL_VERSION}/solana-release-x86_64-unknown-linux-gnu.tar.bz2" \
|
||||||
|
-o solana-release.tar.bz2
|
||||||
|
tar xjf solana-release.tar.bz2
|
||||||
|
mkdir -p "$HOME/solana-local"
|
||||||
|
cp solana-release/bin/solana* "$HOME/solana-local/" 2>/dev/null || true
|
||||||
|
cp solana-release/bin/cargo-build-bpf "$HOME/solana-local/" 2>/dev/null || true
|
||||||
|
cp solana-release/bin/cargo-build-sbf "$HOME/solana-local/" 2>/dev/null || true
|
||||||
|
cp solana-release/bin/cargo-test-bpf "$HOME/solana-local/" 2>/dev/null || true
|
||||||
|
cp solana-release/bin/cargo-test-sbf "$HOME/solana-local/" 2>/dev/null || true
|
||||||
|
# sbf-sdk и прочие каталоги (deps, perf-libs, sdk) — рекурсивно
|
||||||
|
for d in sdk deps perf-libs; do
|
||||||
|
[ -d "solana-release/bin/$d" ] && cp -r "solana-release/bin/$d" "$HOME/solana-local/"
|
||||||
|
done
|
||||||
|
echo "### предустановка SBF platform-tools (LLVM) с anza-xyz:"
|
||||||
|
cd "$HOME/solana-local/sdk/sbf"
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
|
ls dependencies/platform-tools/llvm/bin/clang 2>/dev/null || bash scripts/install.sh || true
|
||||||
|
ls dependencies/platform-tools/llvm/bin/ 2>/dev/null | grep -iE 'clang|objcopy' | head -6
|
||||||
|
# линкуем solana toolchain (на случай если install.sh его не сделал)
|
||||||
|
. "$HOME/.cargo/env" 2>/dev/null || true
|
||||||
|
if [ -d "$HOME/solana-local/sdk/sbf/dependencies/platform-tools/rust" ]; then
|
||||||
|
rustup toolchain link solana "$HOME/solana-local/sdk/sbf/dependencies/platform-tools/rust" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
echo "### доустановка sbpfv3 target (для --arch v3):"
|
||||||
|
rustup target add --toolchain solana sbpfv3-solana-solana 2>&1 | tail -2 || true
|
||||||
|
rustup target list --installed 2>&1 | grep -iE 'sbpf|sbpfv3' || true
|
||||||
|
echo "### targets (конец):"
|
||||||
|
echo "$HOME/solana-local" >> "$GITHUB_PATH"
|
||||||
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
export PATH="$HOME/solana-local:$PATH"
|
||||||
|
solana --version
|
||||||
|
|
||||||
|
- name: Setup Anchor CLI (anchor-next)
|
||||||
|
run: |
|
||||||
|
. "$HOME/.cargo/env"
|
||||||
|
mkdir -p "$HOME/.cargo"
|
||||||
|
printf '[net]\ngit-fetch-with-cli = true\n' > "$HOME/.cargo/config.toml"
|
||||||
|
# ставим сначала через git CLI (обход libgit2-проблем с github auth), иначе --git
|
||||||
|
if ! cargo install --force --git https://github.com/otter-sec/anchor.git \
|
||||||
|
--branch anchor-next anchor-cli --locked 2>/dev/null; then
|
||||||
|
echo "cargo --git не сработал, собираем через git clone + --path"
|
||||||
|
cd /tmp
|
||||||
|
rm -rf anchor-src && git clone --depth 1 --branch anchor-next \
|
||||||
|
https://github.com/otter-sec/anchor.git anchor-src
|
||||||
|
cargo install --force --path /tmp/anchor-src/cli --locked \
|
||||||
|
--root "$HOME/.cargo" --bin anchor
|
||||||
|
fi
|
||||||
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
|
anchor --version
|
||||||
|
|
||||||
|
# ------ общие: build + test ------
|
||||||
|
- name: Build program
|
||||||
|
working-directory: program
|
||||||
|
run: |
|
||||||
|
export PATH="$HOME/solana-local:$HOME/.cargo/bin:$PATH"
|
||||||
|
echo "### HOME=$HOME"
|
||||||
|
mkdir -p "$HOME/.config/solana"
|
||||||
|
cp ../deploy/dev_smart_updown-keypair.json "$HOME/.config/solana/id.json"
|
||||||
|
echo "### размещаем наш keypair в .anchor (для Program ID 9ALs)"
|
||||||
|
mkdir -p .anchor
|
||||||
|
cp ../deploy/dev_smart_updown-keypair.json .anchor/smart-updown-keypair.json
|
||||||
|
cp ../deploy/dev_smart_updown-keypair.json .anchor/smart_updown-keypair.json 2>/dev/null || true
|
||||||
|
cp ../deploy/dev_smart_updown-keypair.json .anchor/9ALsnxXNzDBv3mokngCHbruRTWUZiWR7pf7vswS1fCpf-keypair.json 2>/dev/null || true
|
||||||
|
ls -la .anchor/
|
||||||
|
echo "### построение (явно --arch v3, как прод mainnet):"
|
||||||
|
anchor build --ignore-keys -- --arch v3 2>&1 | tail -8
|
||||||
|
|
||||||
|
- name: Test program
|
||||||
|
working-directory: program
|
||||||
|
run: |
|
||||||
|
export PATH="$HOME/solana-local:$HOME/.cargo/bin:$PATH"
|
||||||
|
anchor test
|
||||||
|
|
||||||
|
# ------ deploy: ТОЛЬКО в нашу сеть (постоянный Solana-валидатор solana-net) ------
|
||||||
|
- name: Deploy contract (только наша сеть solana-net)
|
||||||
|
working-directory: program
|
||||||
|
run: |
|
||||||
|
export PATH="$HOME/solana-local:$HOME/.cargo/bin:$PATH"
|
||||||
|
RPC=http://192.168.76.181:8899
|
||||||
|
echo ">>> Деплой в НАШУ сеть solana-net (RPC $RPC)"
|
||||||
|
echo "### собранный артефакт:"
|
||||||
|
ls -la target/deploy/smart_updown.so
|
||||||
|
# проверка, что наша сеть жива
|
||||||
|
for i in $(seq 1 20); do
|
||||||
|
curl -s $RPC -H 'Content-Type: application/json' \
|
||||||
|
-d '{"jsonrpc":"2.0","id":1,"method":"getHealth"}' 2>/dev/null | grep -q '"ok"' && break
|
||||||
|
echo "ждём сеть ($i)"; sleep 3
|
||||||
|
done
|
||||||
|
# add SOL нашему ключу через свой faucet (9900) — в нашей сети банкир = дефолт
|
||||||
|
mkdir -p "$HOME/.config/solana"
|
||||||
|
cp ../deploy/dev_smart_updown-keypair.json "$HOME/.config/solana/id.json"
|
||||||
|
solana airdrop 100 9ALsnxXNzDBv3mokngCHbruRTWUZiWR7pf7vswS1fCpf \
|
||||||
|
--url $RPC || true
|
||||||
|
solana balance 9ALsnxXNzDBv3mokngCHbruRTWUZiWR7pf7vswS1fCpf --url $RPC
|
||||||
|
solana config set --url $RPC
|
||||||
|
# Anchor НЕ принимает URL в --provider.cluster; указываем сеть через [provider] в Anchor.toml
|
||||||
|
sed -i "s#cluster = \".*\"#cluster = \"$RPC\"#" Anchor.toml
|
||||||
|
anchor deploy --provider.cluster "$RPC" \
|
||||||
|
--provider.wallet "$HOME/.config/solana/id.json"
|
||||||
|
echo ">>> ГОТОВО: контракт задеплоен в нашу сеть $RPC"
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.cortexkit/
|
||||||
|
|
||||||
|
# ключи игроков/секрет — НЕ коммитить (имеют SOL-баланс)
|
||||||
|
deploy/player.json
|
||||||
|
deploy/*player*.json
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# ТЗ: перевод смарт-контракта smart-updown на Anchor 0.31.1 + Solana 2.x (SBPFv3)
|
||||||
|
|
||||||
|
Цель: контракт собирается под SBPFv3 и успешно деплоится в наш прод-блокчейн
|
||||||
|
(агave 4.2.2 / feature-set 565236538 = mainnet). Логика игры сохраняется 100%.
|
||||||
|
|
||||||
|
Рабочая ветка: `extract-sources`. Репо: ~/WORK/games/Smart-game (push в git.binom.pw).
|
||||||
|
|
||||||
|
## Что сделать
|
||||||
|
|
||||||
|
1. **Cargo.toml** (`program/programs/smart-updown/Cargo.toml`):
|
||||||
|
- `anchor-lang = "0.31.1"` (crates.io), убрать git-зависимость otter-sec/anchor branch anchor-next.
|
||||||
|
- Убрать `anchor-v2-testing` из dev-deps (git).
|
||||||
|
- `solana-clock`/`solana-pubkey` dev-deps → версии 2.x (совместимы с solana-program ^2).
|
||||||
|
- Оставить остальные зависимости как есть, если они совместимы с 0.31.1.
|
||||||
|
|
||||||
|
2. **Типы**: заменить `Address` на `Pubkey` во всех state/events/instructions
|
||||||
|
(state.rs: admin, relayer, bettor; events.rs: bet, bettor; update.rs: new_relayer).
|
||||||
|
Тип `Pubkey` — из `solana_program::pubkey::Pubkey` (доступен через anchor prelude).
|
||||||
|
|
||||||
|
3. **Убрать `#[discrim = N]`** со всех 4 инструкций в lib.rs (initialize/create_bet/close_bet/update).
|
||||||
|
Anchor 0.31 генерирует дискриминатор автоматически. **Перегенерить IDL** (idl/smart_updown.json, .ts;
|
||||||
|
relayer/idl/*) — дискримы могут измениться, клиенты должны использовать новый IDL.
|
||||||
|
|
||||||
|
4. **Математика payout (1.3x) — НЕ менять**: `(amount as u128).checked_mul(multiplier_bps as u128) / BPS_DIVISOR`
|
||||||
|
через checked_mul/checked_div, BPS_DIVISOR=10000. Никаких других формул/округлений.
|
||||||
|
|
||||||
|
5. **Логика НЕ менять**: seeds (`GLOBAL_SEED`=b"global", `BET_SEED`+id), статусы, paused-проверки, админ/релейер-проверки, события — как есть.
|
||||||
|
|
||||||
|
6. **API-адаптация под 0.31** (если что-то из 2.0-rc синтаксиса недоступно):
|
||||||
|
- `ctx.accounts.admin.address()` → использовать `.key()` или `*ctx.accounts.admin.key`
|
||||||
|
- `cpi_handle_mut()` → `to_account_info()` (классический CpiContext::cpi_accounts)
|
||||||
|
- `.view().lamports()` → `.to_account_info().lamports()`
|
||||||
|
- `address_eq` → сравнение через `==` на Pubkey
|
||||||
|
- аккаунты: `space` НЕ задавать явно (авто-вывод 8-байт дискриминатора) — не ломать авто-аллокацию.
|
||||||
|
|
||||||
|
7. **Тесты** (`program/programs/smart-updown/tests/test_smart_updown.rs`):
|
||||||
|
перевести на Rust API Anchor 0.31 (НЕ JS). Убрать `anchor_v2_testing`, `Address`.
|
||||||
|
9 тестов долube зелёные (`cargo test`). Не использовать хардкод-дискриминаторы —
|
||||||
|
использовать генерируемые данные из `smart_updown::instruction::*`.
|
||||||
|
|
||||||
|
## Критерии приёмки
|
||||||
|
|
||||||
|
- `anchor build` (агave 4.2.2 CI, `--arch v3`) → успешно, `.so` собран.
|
||||||
|
- `cargo test` → 9 тестов зелёные.
|
||||||
|
- IDL перегенерён, дискримы согласованы.
|
||||||
|
- Деплой в нашу сеть (http://192.168.76.181:8899, агave 4.2.2) успешен.
|
||||||
|
|
||||||
|
## Запрещено
|
||||||
|
|
||||||
|
- Менять игровую логику (правила, множитель, статусы, seeds, проверки).
|
||||||
|
- Ломать явные ошибки компиляции скрытием (`#[allow]`, unwrap без причины).
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
# Smart-Updown — пакет развёртывания (localnet)
|
||||||
|
|
||||||
|
Бинарная игра UP/DOWN на цену SOL. Пользователь ставит на направление (UP/DOWN)
|
||||||
|
на 15 секунд; при выигрыше получает ставку + 30% (1.3x). Дом (контракт) — оппонент.
|
||||||
|
Цена берётся off-chain релеером (он же админ) с вашей БД; SOL хранится в ваулте (PDA `global`).
|
||||||
|
|
||||||
|
Программа: **Anchor 0.31.1 + Solana 2.x (Rust)**, собирается в **SBPFv3** —
|
||||||
|
совместимо с агave-валидатором 4.2.x (= текущий mainnet Solana).
|
||||||
|
|
||||||
|
> **Фронтенд игры** живёт в отдельном репозитории:
|
||||||
|
> **https://git.binom.pw/subochev/fun-game-front2**
|
||||||
|
> (сюда деплоится/подключается клиент к контракту; бэкенд — допиливается следом).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Состав пакета
|
||||||
|
|
||||||
|
```
|
||||||
|
smart-updown/
|
||||||
|
├── deploy/
|
||||||
|
│ ├── smart_updown.so # скомпилированная программа (SBF-байткод, кроссплатформенный)
|
||||||
|
│ └── smart_updown-keypair.json # authority-ключар программы (ID = 9ALsnx...)
|
||||||
|
├── idl/
|
||||||
|
│ ├── smart_updown.json # IDL (для клиента)
|
||||||
|
│ └── smart_updown.ts # типы TypeScript для клиента
|
||||||
|
├── relayer/ # off-chain релеер (админ + закрытие ставок)
|
||||||
|
│ ├── package.json
|
||||||
|
│ ├── tsconfig.json
|
||||||
|
│ ├── .env.example
|
||||||
|
│ ├── idl/ # копия IDL для клиента
|
||||||
|
│ └── src/
|
||||||
|
│ ├── index.ts # главный цикл
|
||||||
|
│ ├── ops.ts # initialize / createBet / closeBet / fundVault
|
||||||
|
│ ├── pda.ts # вывод PDA
|
||||||
|
│ ├── program.ts # клиент из IDL
|
||||||
|
│ ├── priceSource.ts # ЗАГЛУШКА цены SOL (вставить свой запрос к БД)
|
||||||
|
│ └── frontStub.ts # ЗАГЛУШКА фронта (откуда приходит выбор пользователя)
|
||||||
|
├── program/ # исходники контракта (опционально, для пересборки)
|
||||||
|
│ ├── Anchor.toml
|
||||||
|
│ ├── Cargo.toml / Cargo.lock
|
||||||
|
│ ├── rust-toolchain.toml # пинит Rust 1.89.0
|
||||||
|
│ └── programs/smart-updown/{src,tests}
|
||||||
|
├── deploy-localnet.sh # развёртывание на localnet
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Требования (Linux x86_64)
|
||||||
|
|
||||||
|
- **Solana CLI (Agave)** — даёт `solana-test-validator`, `solana program deploy`,
|
||||||
|
`cargo build-sbf`, `solana-keygen`. Установка:
|
||||||
|
`sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"`
|
||||||
|
- **Node.js 18+ / npm** — для relayer.
|
||||||
|
- (опционально) **Rust 1.85+** через rustup — только если пересобираете программу.
|
||||||
|
|
||||||
|
> **Важно про `.so` и SBPF-версию**: контракт собирается под конкретный SBPF-архитектуру
|
||||||
|
> (здесь — **SBPFv3**, под агave 4.2.x). Собранный `.so` кроссплатформенный
|
||||||
|
> по CPU хоста (работает и с macOS-сборкой), но **должен совпадать по SBPF-версии
|
||||||
|
> с валидатором**, иначе деплой падает (`Detected sbpf_version ... not enabled`).
|
||||||
|
> При сборке через `cargo build-sbf --tools-version v1.57` — артефакт SBPFv3.
|
||||||
|
|
||||||
|
## 2. Развёртывание (localnet)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x deploy-localnet.sh
|
||||||
|
./deploy-localnet.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт:
|
||||||
|
1. переключает `solana config` на `http://127.0.0.1:8899`;
|
||||||
|
2. запускает `solana-test-validator` с программой (`--bpf-program 9ALsnx...`);
|
||||||
|
3. проверяет, что программа поднята под ID `9ALsnxXNzDBv3mokngCHbruRTWUZiWR7pf7vswS1fCpf`.
|
||||||
|
|
||||||
|
Подытог — программа доступна на localnet под тем же адресом, что зашит в IDL.
|
||||||
|
|
||||||
|
## 3. Relayer (админ + закрытие ставок)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd relayer
|
||||||
|
npm install
|
||||||
|
cp .env.example .env # при желании поправить значения
|
||||||
|
# инициализация контракта (ваулт/Global):
|
||||||
|
npm run init
|
||||||
|
# один прогон ставки:
|
||||||
|
npm run run
|
||||||
|
# непрерывный цикл:
|
||||||
|
npm run loop
|
||||||
|
```
|
||||||
|
|
||||||
|
`init` создаёт `Global` (админ + релеер = keypair из `SMART_ADMIN_KEYPAIR`,
|
||||||
|
по умолчанию `~/.config/solana/id.json`). На localnet скрипт сам делает airDrop
|
||||||
|
bettor'ам и догружает ваулт банка. На mainnet airDrop выключите
|
||||||
|
(`SMART_ALLOW_AIRDROP=0`) и пополняйте ваулт переводами с админа.
|
||||||
|
|
||||||
|
> AirDrop на localnet выполняется от keypair, настроенного в `solana config`.
|
||||||
|
> Убедитесь, что он существует и имеет SOL (в скрипте деплоя это `id.json`,
|
||||||
|
> см. `solana config get`).
|
||||||
|
|
||||||
|
## 4. Заглушки — куда вставить реальное
|
||||||
|
|
||||||
|
- `relayer/src/priceSource.ts` — функция `getPrice()`. **Замените** на запрос цены
|
||||||
|
SOL из вашей БД. Единицы любые, главное согласованность между entry и exit.
|
||||||
|
- `relayer/src/frontStub.ts` — функция `nextBetRequest()`. **Замените** на приём
|
||||||
|
со стороны фронта: направление (UP/DOWN) и сумма, которые выбрал пользователь.
|
||||||
|
|
||||||
|
## 5. Деплой в новом окружении (если уже есть валидатор, без перезапуска)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
solana config set --url http://127.0.0.1:8899
|
||||||
|
solana program deploy --program-id deploy/smart_updown-keypair.json deploy/smart_updown.so
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. (Опционально) Пересборка программы из исходников + тесты
|
||||||
|
|
||||||
|
Сборка контракта под **SBPFv3** (для агave 4.2.x):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd program
|
||||||
|
# собрать .so (SBPFv3, с entrypoint):
|
||||||
|
cargo build-sbf --tools-version v1.57 --manifest-path programs/smart-updown/Cargo.toml
|
||||||
|
# артефакт появится в program/target/deploy/smart_updown.so
|
||||||
|
# интеграционные тесты (solana-program-test 2.x, 10 кейсов):
|
||||||
|
SBF_OUT_DIR="$(pwd)/target/deploy" cargo test -p smart-updown
|
||||||
|
```
|
||||||
|
|
||||||
|
> `--tools-version v1.5x` критичен: v1.57 даёт rustc ≥1.85 (нужен для edition2024)
|
||||||
|
> и валидный `.so` с символом `entrypoint`. **Нюанс про SBPF-версии для тестов**:
|
||||||
|
> тестовый рантайм `solana-program-test` не грузит SBPFv3-ELF — он принимает максимум
|
||||||
|
> arch-v0. Поэтому тесты гоняются на `.so`, собранном под v0 (тот же код), а в
|
||||||
|
> прод деплоится SBPFv3-артефакт. Логика контракта одна — различается только arch-цель.
|
||||||
|
|
||||||
|
Если пересобираете, IDL не меняется, пока не меняете layout аккаунтов/инструкций.
|
||||||
|
При смене дискриминаторов (напр. уход от `#[discrim]` при миграции на anchor 0.31.1)
|
||||||
|
обязательно перегенерируйте IDL и обновите `relayer/idl/` и `idl/`.
|
||||||
|
|
||||||
|
## 7. Переменные окружения relayer (см. .env.example)
|
||||||
|
|
||||||
|
| Переменная | По умолчанию | Описание |
|
||||||
|
|---|---|---|
|
||||||
|
| `SMART_RPC_URL` | `http://127.0.0.1:8899` | RPC кластера |
|
||||||
|
| `SMART_ALLOW_AIRDROP` | `1` | airDrop bettors/ваулта (localnet) |
|
||||||
|
| `SMART_ADMIN_KEYPAIR` | `~/.config/solana/id.json` | keypair админа/релеера |
|
||||||
|
| `SMART_MIN_AMOUNT` | `100000000` | мин. ставка (лампорты) |
|
||||||
|
| `SMART_MAX_AMOUNT` | `10000000000` | макс. ставка (лампорты) |
|
||||||
|
| `SMART_MULTIPLIER_BPS` | `13000` | выплата 1.3x (bps) |
|
||||||
|
| `SMART_EXPIRY_SECONDS` | `15` | экспирация (сек) |
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# ТЗ: дописать тесты для smart-updown под Anchor 0.31.1 / Solana 2.x
|
||||||
|
|
||||||
|
Контракт уже переведён на anchor-lang 0.31.1 + solana 2.x и `.so` собран в SBPFv3.
|
||||||
|
Всё в `program/programs/smart-updown/`, ветка `extract-sources`.
|
||||||
|
|
||||||
|
## Текущее состояние тестов
|
||||||
|
|
||||||
|
Файл `tests/test_smart_updown.rs` сейчас содержит ТОЛЬКО первый тест
|
||||||
|
(`initialize_creates_global`) полностью + каркас (setup, read_account, helpers).
|
||||||
|
Остальные 8 тестов НАДО дописать по original-логике (см. ниже). Файл уже
|
||||||
|
переведён на `solana-program-test` + `tokio::test` + async `BanksClient`.
|
||||||
|
|
||||||
|
## Задача
|
||||||
|
|
||||||
|
Дописать в `tests/test_smart_updown.rs` все 8 недостающих тестов, сохранив
|
||||||
|
РОВНО их исходную игровую логику:
|
||||||
|
|
||||||
|
1. `create_bet_escrows_sol` — create_bet (UP, side=0): эскроу на global-pda,
|
||||||
|
read_bet: bettor, side=0, amount, entry_price, bet_time=1_700_000_000,
|
||||||
|
expire_time=+EXPIRY_SECONDS, status=0; counter становится 1.
|
||||||
|
2. `close_bet_user_wins_up` — close UP после expiry, exit>entry: bettor получает
|
||||||
|
payout=amount*MULTIPLIER_BPS/10000 (1.3x), статус=1, exit_price установлен.
|
||||||
|
Перед этим айрдроп на global-pda ≥10*amount (для выплаты из vault).
|
||||||
|
3. `close_bet_house_wins_down` — side=DOWN, exit>entry (проиграл): баланс не
|
||||||
|
меняется, статус=2.
|
||||||
|
4. `close_bet_equal_price_stays_open` — exit==entry: статус остаётся 0, баланс не меняется.
|
||||||
|
5. `close_bet_already_finalized` — повторный close после успешного: вторая
|
||||||
|
транзакция падает (err), статус остаётся 1.
|
||||||
|
6. `close_bet_before_expiry_rejected` — close до expiry: падает, статус 0.
|
||||||
|
7. `close_bet_wrong_relayer_rejected` — close не-релейером: падает, статус 0.
|
||||||
|
8. `close_bet_replay_rejected` — close после уже закрытого: падает.
|
||||||
|
|
||||||
|
Плюс тест `create_bet_rejects_out_of_range` (amount=1 < MIN_AMOUNT) — падает.
|
||||||
|
|
||||||
|
## Критичные API-условия (solana-program-test 2.x)
|
||||||
|
|
||||||
|
- `setup()` возвращает `(ProgramTestContext, Keypair)`; первый тест уже это делает.
|
||||||
|
- Airdrop любому аккаунту: `system_instruction::transfer(&ctx.payer.pubkey(), to, lamports)` в транзакции от payer.
|
||||||
|
- Системные часы: получить `Clock` через `ctx.banks_client.get_sysvar().await`, изменить `unix_timestamp`, затем `ctx.set_sysvar(&clk)`.
|
||||||
|
- Читать аккаунт: `ctx.banks_client.get_account(pubkey).await.unwrap().unwrap()` (Account), данные через `bytemuck::from_bytes::<Global/Bet>(&data[8..])`.
|
||||||
|
- Проверка баланса: `ctx.banks_client.get_balance(pubkey).await.unwrap()`.
|
||||||
|
- Все тесты `#[tokio::test] async fn`, всегда первая строка `let (mut ctx, admin) = setup().await;`.
|
||||||
|
|
||||||
|
## Как исполнять
|
||||||
|
|
||||||
|
Дописать тесты, затем ЗАПУСТИТЬ:
|
||||||
|
```
|
||||||
|
cd program && cargo test -p smart-updown 2>&1 | tail -40
|
||||||
|
```
|
||||||
|
Добиться, чтобы ВСЕ тесты прошли зелёными (а те, что ожидают ошибку, получали её).
|
||||||
|
Устранить ошибки компиляции (импорты, типы). НЕ менять игровую логику в src.
|
||||||
|
|
||||||
|
## Критерии
|
||||||
|
|
||||||
|
- `cargo test -p smart-updown` → все 9+ тестов green (ожидаемые ошибки — как задумано).
|
||||||
|
- Отчёт: какие тесты добавил, что чинит, какие остались красными (если есть).
|
||||||
Executable
+45
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Развёртывание smart-updown на localnet (solana-test-validator).
|
||||||
|
# Использование: ./deploy-localnet.sh
|
||||||
|
#
|
||||||
|
# Требования: Solana CLI (solana-test-validator, solana program deploy).
|
||||||
|
# Если нужно пересобрать программу из исходников, см. README (опционально).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
PROGRAM_SO="$HERE/deploy/smart_updown.so"
|
||||||
|
PROGRAM_KEYPAIR="$HERE/deploy/smart_updown-keypair.json"
|
||||||
|
PROGRAM_ID="9ALsnxXNzDBv3mokngCHbruRTWUZiWR7pf7vswS1fCpf"
|
||||||
|
RPC_URL="http://127.0.0.1:8899"
|
||||||
|
|
||||||
|
# 1. Настройка CLI на localhost
|
||||||
|
solana config set --url "$RPC_URL"
|
||||||
|
solana-keygen pubkey "$PROGRAM_KEYPAIR" >/dev/null 2>&1 || {
|
||||||
|
echo "Ошибка: нет deploy/smart_updown-keypair.json"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. Запуск валидатора с предзагруженной программой (если ещё не запущен)
|
||||||
|
if ! solana cluster-version >/dev/null 2>&1; then
|
||||||
|
echo ">>> Стартую solana-test-validator с программой $PROGRAM_ID"
|
||||||
|
solana-test-validator --reset \
|
||||||
|
--bpf-program "$PROGRAM_ID" "$PROGRAM_SO" \
|
||||||
|
>test-ledger.log 2>&1 &
|
||||||
|
sleep 6
|
||||||
|
fi
|
||||||
|
solana cluster-version
|
||||||
|
|
||||||
|
# 3. Убеждаемся, что программа на месте
|
||||||
|
if ! solana program show "$PROGRAM_ID" >/dev/null 2>&1; then
|
||||||
|
echo ">>> Деплою программу"
|
||||||
|
solana program deploy --program-id "$PROGRAM_KEYPAIR" "$PROGRAM_SO"
|
||||||
|
else
|
||||||
|
echo ">>> Программа уже задеплоена: $PROGRAM_ID"
|
||||||
|
fi
|
||||||
|
solana program show "$PROGRAM_ID" | head -12
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Готово. Vault/Global инициализируется первым запуском relayer: npm run init"
|
||||||
|
echo "Дальше: npm run loop (непрерывно) или npm run run (один прогон)."
|
||||||
|
echo
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
[208, 231, 133, 131, 130, 139, 117, 234, 142, 248, 131, 96, 29, 35, 93, 242, 201, 69, 55, 214, 123, 194, 181, 253, 72, 183, 251, 137, 168, 12, 43, 51, 121, 66, 50, 61, 156, 4, 133, 87, 233, 233, 139, 208, 243, 111, 118, 38, 85, 94, 125, 84, 243, 184, 56, 130, 217, 126, 22, 193, 220, 131, 89, 104]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
[39,64,110,209,237,123,147,255,190,74,164,207,160,225,89,109,119,197,167,34,86,233,138,227,63,17,57,104,123,10,58,140,194,196,68,144,108,208,236,142,221,255,163,163,238,139,47,184,50,184,127,42,175,209,36,41,202,28,56,58,98,37,116,204]
|
||||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,546 @@
|
|||||||
|
{
|
||||||
|
"address": "9ALsnxXNzDBv3mokngCHbruRTWUZiWR7pf7vswS1fCpf",
|
||||||
|
"metadata": {
|
||||||
|
"name": "smart_updown",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"spec": "0.1.0",
|
||||||
|
"description": "Created with Anchor"
|
||||||
|
},
|
||||||
|
"instructions": [
|
||||||
|
{
|
||||||
|
"name": "close_bet",
|
||||||
|
"discriminator": [
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "relayer",
|
||||||
|
"signer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bet",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
98,
|
||||||
|
101,
|
||||||
|
116
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "arg",
|
||||||
|
"path": "id"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "global",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
103,
|
||||||
|
108,
|
||||||
|
111,
|
||||||
|
98,
|
||||||
|
97,
|
||||||
|
108
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bettor",
|
||||||
|
"docs": [
|
||||||
|
"CHECK: recipient must be the bet's bettor and is validated in the handler."
|
||||||
|
],
|
||||||
|
"writable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "clock",
|
||||||
|
"address": "SysvarC1ock11111111111111111111111111111111"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "exit_price",
|
||||||
|
"type": "u64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "create_bet",
|
||||||
|
"discriminator": [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "bettor",
|
||||||
|
"writable": true,
|
||||||
|
"signer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "global",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
103,
|
||||||
|
108,
|
||||||
|
111,
|
||||||
|
98,
|
||||||
|
97,
|
||||||
|
108
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bet",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
98,
|
||||||
|
101,
|
||||||
|
116
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "arg",
|
||||||
|
"path": "id"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "clock",
|
||||||
|
"address": "SysvarC1ock11111111111111111111111111111111"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "system_program",
|
||||||
|
"address": "11111111111111111111111111111111"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "side",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "amount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "entry_price",
|
||||||
|
"type": "u64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "initialize",
|
||||||
|
"discriminator": [
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "admin",
|
||||||
|
"writable": true,
|
||||||
|
"signer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "global",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
103,
|
||||||
|
108,
|
||||||
|
111,
|
||||||
|
98,
|
||||||
|
97,
|
||||||
|
108
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "system_program",
|
||||||
|
"address": "11111111111111111111111111111111"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "min_amount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "max_amount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "multiplier_bps",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expiry_seconds",
|
||||||
|
"type": "i64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "update",
|
||||||
|
"discriminator": [
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "admin",
|
||||||
|
"signer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "global",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
103,
|
||||||
|
108,
|
||||||
|
111,
|
||||||
|
98,
|
||||||
|
97,
|
||||||
|
108
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "new_relayer",
|
||||||
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "min_amount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "max_amount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "multiplier_bps",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "paused",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expiry_seconds",
|
||||||
|
"type": "i64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "Bet",
|
||||||
|
"discriminator": [
|
||||||
|
147,
|
||||||
|
23,
|
||||||
|
35,
|
||||||
|
59,
|
||||||
|
15,
|
||||||
|
75,
|
||||||
|
155,
|
||||||
|
32
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Global",
|
||||||
|
"discriminator": [
|
||||||
|
167,
|
||||||
|
232,
|
||||||
|
232,
|
||||||
|
177,
|
||||||
|
200,
|
||||||
|
108,
|
||||||
|
114,
|
||||||
|
127
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"name": "BetClosed",
|
||||||
|
"discriminator": [
|
||||||
|
122,
|
||||||
|
20,
|
||||||
|
157,
|
||||||
|
254,
|
||||||
|
221,
|
||||||
|
0,
|
||||||
|
39,
|
||||||
|
129
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BetCreated",
|
||||||
|
"discriminator": [
|
||||||
|
32,
|
||||||
|
153,
|
||||||
|
105,
|
||||||
|
71,
|
||||||
|
188,
|
||||||
|
72,
|
||||||
|
107,
|
||||||
|
114
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": 6000,
|
||||||
|
"name": "Paused",
|
||||||
|
"msg": "Operation is paused"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6001,
|
||||||
|
"name": "NotAdmin",
|
||||||
|
"msg": "Only the admin can perform this action"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6002,
|
||||||
|
"name": "NotRelayer",
|
||||||
|
"msg": "Only the relayer is allowed to close bets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6003,
|
||||||
|
"name": "AmountBelowMin",
|
||||||
|
"msg": "Amount below the allowed minimum"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6004,
|
||||||
|
"name": "AmountAboveMax",
|
||||||
|
"msg": "Amount above the allowed maximum"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6005,
|
||||||
|
"name": "InvalidSide",
|
||||||
|
"msg": "Invalid side value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6006,
|
||||||
|
"name": "BetAlreadyFinalized",
|
||||||
|
"msg": "Bet is already finalized"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6007,
|
||||||
|
"name": "NotExpired",
|
||||||
|
"msg": "Bet cannot be closed before expiry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6008,
|
||||||
|
"name": "BetIdMismatch",
|
||||||
|
"msg": "Bet id mismatch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6009,
|
||||||
|
"name": "InsufficientVault",
|
||||||
|
"msg": "Payout exceeds vault solvency"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"name": "Bet",
|
||||||
|
"serialization": "bytemuck",
|
||||||
|
"repr": {
|
||||||
|
"kind": "c"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"kind": "struct",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "amount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bet_time",
|
||||||
|
"type": "i64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expire_time",
|
||||||
|
"type": "i64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "entry_price",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "exit_price",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "side",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bettor",
|
||||||
|
"type": "pubkey"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BetClosed",
|
||||||
|
"type": {
|
||||||
|
"kind": "struct",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "bet",
|
||||||
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "exit_price",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "user_won",
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "payout",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"type": "u64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BetCreated",
|
||||||
|
"type": {
|
||||||
|
"kind": "struct",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "bet",
|
||||||
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bettor",
|
||||||
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "side",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "amount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bet_time",
|
||||||
|
"type": "i64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expire_time",
|
||||||
|
"type": "i64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "entry_price",
|
||||||
|
"type": "u64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Global",
|
||||||
|
"serialization": "bytemuck",
|
||||||
|
"repr": {
|
||||||
|
"kind": "c"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"kind": "struct",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "counter",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "min_amount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "max_amount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "multiplier_bps",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expiry_seconds",
|
||||||
|
"type": "i64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "paused",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "admin",
|
||||||
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "relayer",
|
||||||
|
"type": "pubkey"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"constants": [
|
||||||
|
{
|
||||||
|
"name": "BET_SEED",
|
||||||
|
"type": "bytes",
|
||||||
|
"value": "[98, 101, 116]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GLOBAL_SEED",
|
||||||
|
"type": "bytes",
|
||||||
|
"value": "[103, 108, 111, 98, 97, 108]"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,552 @@
|
|||||||
|
/**
|
||||||
|
* 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`.
|
||||||
|
*/
|
||||||
|
export type SmartUpdown = {
|
||||||
|
"address": "9ALsnxXNzDBv3mokngCHbruRTWUZiWR7pf7vswS1fCpf",
|
||||||
|
"metadata": {
|
||||||
|
"name": "smartUpdown",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"spec": "0.1.0",
|
||||||
|
"description": "Created with Anchor"
|
||||||
|
},
|
||||||
|
"instructions": [
|
||||||
|
{
|
||||||
|
"name": "closeBet",
|
||||||
|
"discriminator": [
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "relayer",
|
||||||
|
"signer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bet",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
98,
|
||||||
|
101,
|
||||||
|
116
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "arg",
|
||||||
|
"path": "id"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "global",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
103,
|
||||||
|
108,
|
||||||
|
111,
|
||||||
|
98,
|
||||||
|
97,
|
||||||
|
108
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bettor",
|
||||||
|
"docs": [
|
||||||
|
"CHECK: recipient must be the bet's bettor and is validated in the handler."
|
||||||
|
],
|
||||||
|
"writable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "clock",
|
||||||
|
"address": "SysvarC1ock11111111111111111111111111111111"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "exitPrice",
|
||||||
|
"type": "u64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "createBet",
|
||||||
|
"discriminator": [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "bettor",
|
||||||
|
"writable": true,
|
||||||
|
"signer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "global",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
103,
|
||||||
|
108,
|
||||||
|
111,
|
||||||
|
98,
|
||||||
|
97,
|
||||||
|
108
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bet",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
98,
|
||||||
|
101,
|
||||||
|
116
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "arg",
|
||||||
|
"path": "id"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "clock",
|
||||||
|
"address": "SysvarC1ock11111111111111111111111111111111"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "systemProgram",
|
||||||
|
"address": "11111111111111111111111111111111"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "side",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "amount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "entryPrice",
|
||||||
|
"type": "u64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "initialize",
|
||||||
|
"discriminator": [
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "admin",
|
||||||
|
"writable": true,
|
||||||
|
"signer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "global",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
103,
|
||||||
|
108,
|
||||||
|
111,
|
||||||
|
98,
|
||||||
|
97,
|
||||||
|
108
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "systemProgram",
|
||||||
|
"address": "11111111111111111111111111111111"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "minAmount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maxAmount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "multiplierBps",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expirySeconds",
|
||||||
|
"type": "i64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "update",
|
||||||
|
"discriminator": [
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "admin",
|
||||||
|
"signer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "global",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
103,
|
||||||
|
108,
|
||||||
|
111,
|
||||||
|
98,
|
||||||
|
97,
|
||||||
|
108
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "newRelayer",
|
||||||
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "minAmount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maxAmount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "multiplierBps",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "paused",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expirySeconds",
|
||||||
|
"type": "i64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "bet",
|
||||||
|
"discriminator": [
|
||||||
|
147,
|
||||||
|
23,
|
||||||
|
35,
|
||||||
|
59,
|
||||||
|
15,
|
||||||
|
75,
|
||||||
|
155,
|
||||||
|
32
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "global",
|
||||||
|
"discriminator": [
|
||||||
|
167,
|
||||||
|
232,
|
||||||
|
232,
|
||||||
|
177,
|
||||||
|
200,
|
||||||
|
108,
|
||||||
|
114,
|
||||||
|
127
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"name": "betClosed",
|
||||||
|
"discriminator": [
|
||||||
|
122,
|
||||||
|
20,
|
||||||
|
157,
|
||||||
|
254,
|
||||||
|
221,
|
||||||
|
0,
|
||||||
|
39,
|
||||||
|
129
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "betCreated",
|
||||||
|
"discriminator": [
|
||||||
|
32,
|
||||||
|
153,
|
||||||
|
105,
|
||||||
|
71,
|
||||||
|
188,
|
||||||
|
72,
|
||||||
|
107,
|
||||||
|
114
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": 6000,
|
||||||
|
"name": "paused",
|
||||||
|
"msg": "Operation is paused"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6001,
|
||||||
|
"name": "notAdmin",
|
||||||
|
"msg": "Only the admin can perform this action"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6002,
|
||||||
|
"name": "notRelayer",
|
||||||
|
"msg": "Only the relayer is allowed to close bets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6003,
|
||||||
|
"name": "amountBelowMin",
|
||||||
|
"msg": "Amount below the allowed minimum"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6004,
|
||||||
|
"name": "amountAboveMax",
|
||||||
|
"msg": "Amount above the allowed maximum"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6005,
|
||||||
|
"name": "invalidSide",
|
||||||
|
"msg": "Invalid side value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6006,
|
||||||
|
"name": "betAlreadyFinalized",
|
||||||
|
"msg": "Bet is already finalized"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6007,
|
||||||
|
"name": "notExpired",
|
||||||
|
"msg": "Bet cannot be closed before expiry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6008,
|
||||||
|
"name": "betIdMismatch",
|
||||||
|
"msg": "Bet id mismatch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6009,
|
||||||
|
"name": "insufficientVault",
|
||||||
|
"msg": "Payout exceeds vault solvency"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"name": "bet",
|
||||||
|
"serialization": "bytemuck",
|
||||||
|
"repr": {
|
||||||
|
"kind": "c"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"kind": "struct",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "amount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "betTime",
|
||||||
|
"type": "i64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expireTime",
|
||||||
|
"type": "i64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "entryPrice",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "exitPrice",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "side",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bettor",
|
||||||
|
"type": "pubkey"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "betClosed",
|
||||||
|
"type": {
|
||||||
|
"kind": "struct",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "bet",
|
||||||
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "exitPrice",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "userWon",
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "payout",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"type": "u64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "betCreated",
|
||||||
|
"type": {
|
||||||
|
"kind": "struct",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "bet",
|
||||||
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bettor",
|
||||||
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "side",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "amount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "betTime",
|
||||||
|
"type": "i64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expireTime",
|
||||||
|
"type": "i64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "entryPrice",
|
||||||
|
"type": "u64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "global",
|
||||||
|
"serialization": "bytemuck",
|
||||||
|
"repr": {
|
||||||
|
"kind": "c"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"kind": "struct",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "counter",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "minAmount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maxAmount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "multiplierBps",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expirySeconds",
|
||||||
|
"type": "i64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "paused",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "admin",
|
||||||
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "relayer",
|
||||||
|
"type": "pubkey"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"constants": [
|
||||||
|
{
|
||||||
|
"name": "betSeed",
|
||||||
|
"type": "bytes",
|
||||||
|
"value": "[98, 101, 116]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "globalSeed",
|
||||||
|
"type": "bytes",
|
||||||
|
"value": "[103, 108, 111, 98, 97, 108]"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.anchor
|
||||||
|
.DS_Store
|
||||||
|
target
|
||||||
|
**/*.rs.bk
|
||||||
|
node_modules
|
||||||
|
test-ledger
|
||||||
|
.yarn
|
||||||
|
.surfpool
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
skip_local_validator = true
|
||||||
|
|
||||||
|
[toolchain]
|
||||||
|
package_manager = "npm"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
resolution = true
|
||||||
|
skip-lint = false
|
||||||
|
|
||||||
|
[programs.localnet]
|
||||||
|
smart_updown = "9ALsnxXNzDBv3mokngCHbruRTWUZiWR7pf7vswS1fCpf"
|
||||||
|
|
||||||
|
[programs.devnet]
|
||||||
|
smart_updown = "9ALsnxXNzDBv3mokngCHbruRTWUZiWR7pf7vswS1fCpf"
|
||||||
|
|
||||||
|
[provider]
|
||||||
|
cluster = "localnet"
|
||||||
|
wallet = "../deploy/dev_smart_updown-keypair.json"
|
||||||
|
|
||||||
|
[scripts]
|
||||||
|
test = "cargo test"
|
||||||
|
|
||||||
|
[hooks]
|
||||||
Generated
+8228
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
|||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"programs/*"
|
||||||
|
]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.89.0"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
overflow-checks = true
|
||||||
|
lto = "fat"
|
||||||
|
codegen-units = 1
|
||||||
|
[profile.release.build-override]
|
||||||
|
opt-level = 3
|
||||||
|
incremental = false
|
||||||
|
codegen-units = 1
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
[package]
|
||||||
|
name = "smart-updown"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Created with Anchor"
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "lib"]
|
||||||
|
name = "smart_updown"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
cpi = ["no-entrypoint"]
|
||||||
|
no-entrypoint = []
|
||||||
|
no-log-ix-name = []
|
||||||
|
idl-build = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anchor-lang = "0.31.1"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
anchor-lang = "0.31.1"
|
||||||
|
solana-program-test = "2.1"
|
||||||
|
solana-client = "2.1"
|
||||||
|
solana-pubkey = "2.1"
|
||||||
|
solana-signer = "2.1"
|
||||||
|
solana-keypair = "2.1"
|
||||||
|
solana-clock = "2.1"
|
||||||
|
solana-transaction = "2.1"
|
||||||
|
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] }
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
|
#[constant]
|
||||||
|
pub const GLOBAL_SEED: &[u8] = b"global";
|
||||||
|
|
||||||
|
#[constant]
|
||||||
|
pub const BET_SEED: &[u8] = b"bet";
|
||||||
|
|
||||||
|
pub const SIDE_UP: u64 = 0;
|
||||||
|
pub const SIDE_DOWN: u64 = 1;
|
||||||
|
|
||||||
|
pub const STATUS_OPEN: u64 = 0;
|
||||||
|
pub const STATUS_PAYOUT_DONE: u64 = 1;
|
||||||
|
pub const STATUS_HOUSE_WON: u64 = 2;
|
||||||
|
|
||||||
|
pub const BPS_DIVISOR: u64 = 10_000;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
|
#[error_code]
|
||||||
|
pub enum SmartUpdownError {
|
||||||
|
#[msg("Operation is paused")]
|
||||||
|
Paused,
|
||||||
|
|
||||||
|
#[msg("Only the admin can perform this action")]
|
||||||
|
NotAdmin,
|
||||||
|
|
||||||
|
#[msg("Only the relayer is allowed to close bets")]
|
||||||
|
NotRelayer,
|
||||||
|
|
||||||
|
#[msg("Amount below the allowed minimum")]
|
||||||
|
AmountBelowMin,
|
||||||
|
|
||||||
|
#[msg("Amount above the allowed maximum")]
|
||||||
|
AmountAboveMax,
|
||||||
|
|
||||||
|
#[msg("Invalid side value")]
|
||||||
|
InvalidSide,
|
||||||
|
|
||||||
|
#[msg("Bet is already finalized")]
|
||||||
|
BetAlreadyFinalized,
|
||||||
|
|
||||||
|
#[msg("Bet cannot be closed before expiry")]
|
||||||
|
NotExpired,
|
||||||
|
|
||||||
|
#[msg("Bet id mismatch")]
|
||||||
|
BetIdMismatch,
|
||||||
|
|
||||||
|
#[msg("Payout exceeds vault solvency")]
|
||||||
|
InsufficientVault,
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
|
#[event]
|
||||||
|
pub struct BetCreated {
|
||||||
|
pub bet: Pubkey,
|
||||||
|
pub bettor: Pubkey,
|
||||||
|
pub side: u64,
|
||||||
|
pub amount: u64,
|
||||||
|
pub bet_time: i64,
|
||||||
|
pub expire_time: i64,
|
||||||
|
pub entry_price: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[event]
|
||||||
|
pub struct BetClosed {
|
||||||
|
pub bet: Pubkey,
|
||||||
|
pub exit_price: u64,
|
||||||
|
pub user_won: bool,
|
||||||
|
pub payout: u64,
|
||||||
|
pub status: u64,
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
pub mod close_bet;
|
||||||
|
pub mod create_bet;
|
||||||
|
pub mod initialize;
|
||||||
|
pub mod update;
|
||||||
|
|
||||||
|
#[allow(ambiguous_glob_reexports)]
|
||||||
|
pub use close_bet::*;
|
||||||
|
pub use create_bet::*;
|
||||||
|
pub use initialize::*;
|
||||||
|
pub use update::*;
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
constants::{
|
||||||
|
BPS_DIVISOR, BET_SEED, GLOBAL_SEED, SIDE_DOWN, SIDE_UP, STATUS_HOUSE_WON,
|
||||||
|
STATUS_PAYOUT_DONE, STATUS_OPEN,
|
||||||
|
},
|
||||||
|
error::SmartUpdownError,
|
||||||
|
events::BetClosed,
|
||||||
|
state::{Bet, Global},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Accounts)]
|
||||||
|
#[instruction(id: u64, exit_price: u64)]
|
||||||
|
pub struct CloseBet<'info> {
|
||||||
|
pub relayer: Signer<'info>,
|
||||||
|
#[account(
|
||||||
|
mut,
|
||||||
|
seeds = [BET_SEED, &id.to_le_bytes()],
|
||||||
|
bump
|
||||||
|
)]
|
||||||
|
pub bet: Account<'info, Bet>,
|
||||||
|
#[account(
|
||||||
|
mut,
|
||||||
|
seeds = [GLOBAL_SEED],
|
||||||
|
bump
|
||||||
|
)]
|
||||||
|
pub global: Account<'info, Global>,
|
||||||
|
/// CHECK: recipient must be the bet's bettor and is validated in the handler.
|
||||||
|
#[account(mut)]
|
||||||
|
pub bettor: SystemAccount<'info>,
|
||||||
|
pub clock: Sysvar<'info, Clock>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handler(ctx: &mut Context<CloseBet>, id: u64, exit_price: u64) -> Result<()> {
|
||||||
|
require!(
|
||||||
|
ctx.accounts.relayer.key() == ctx.accounts.global.relayer,
|
||||||
|
SmartUpdownError::NotRelayer
|
||||||
|
);
|
||||||
|
require!(
|
||||||
|
ctx.accounts.bettor.key() == ctx.accounts.bet.bettor,
|
||||||
|
SmartUpdownError::NotRelayer
|
||||||
|
);
|
||||||
|
require!(
|
||||||
|
ctx.accounts.bet.status == STATUS_OPEN,
|
||||||
|
SmartUpdownError::BetAlreadyFinalized
|
||||||
|
);
|
||||||
|
let now = ctx.accounts.clock.unix_timestamp;
|
||||||
|
require!(now >= ctx.accounts.bet.expire_time, SmartUpdownError::NotExpired);
|
||||||
|
|
||||||
|
let entry = ctx.accounts.bet.entry_price;
|
||||||
|
let side = ctx.accounts.bet.side;
|
||||||
|
let amount = ctx.accounts.bet.amount;
|
||||||
|
|
||||||
|
if exit_price == entry {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_won = (side == SIDE_UP && exit_price > entry)
|
||||||
|
|| (side == SIDE_DOWN && exit_price < entry);
|
||||||
|
|
||||||
|
let payout;
|
||||||
|
if user_won {
|
||||||
|
let multiplier = ctx.accounts.global.multiplier_bps;
|
||||||
|
let payout_amount =
|
||||||
|
(amount as u128)
|
||||||
|
.checked_mul(multiplier as u128)
|
||||||
|
.unwrap()
|
||||||
|
.checked_div(BPS_DIVISOR as u128)
|
||||||
|
.unwrap() as u64;
|
||||||
|
require!(
|
||||||
|
ctx.accounts.global.get_lamports() >= payout_amount,
|
||||||
|
SmartUpdownError::InsufficientVault
|
||||||
|
);
|
||||||
|
payout = payout_amount;
|
||||||
|
ctx.accounts.global.sub_lamports(payout)?;
|
||||||
|
ctx.accounts.bettor.add_lamports(payout)?;
|
||||||
|
ctx.accounts.bet.status = STATUS_PAYOUT_DONE;
|
||||||
|
} else {
|
||||||
|
payout = 0;
|
||||||
|
ctx.accounts.bet.status = STATUS_HOUSE_WON;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.accounts.bet.exit_price = exit_price;
|
||||||
|
|
||||||
|
emit!(BetClosed {
|
||||||
|
bet: ctx.accounts.bet.key(),
|
||||||
|
exit_price,
|
||||||
|
user_won,
|
||||||
|
payout,
|
||||||
|
status: ctx.accounts.bet.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
constants::{BET_SEED, GLOBAL_SEED, SIDE_DOWN, SIDE_UP, STATUS_OPEN},
|
||||||
|
error::SmartUpdownError,
|
||||||
|
events::BetCreated,
|
||||||
|
state::{Bet, Global},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Accounts)]
|
||||||
|
#[instruction(id: u64, side: u64, amount: u64, entry_price: u64)]
|
||||||
|
pub struct CreateBet<'info> {
|
||||||
|
#[account(mut)]
|
||||||
|
pub bettor: Signer<'info>,
|
||||||
|
#[account(
|
||||||
|
mut,
|
||||||
|
seeds = [GLOBAL_SEED],
|
||||||
|
bump
|
||||||
|
)]
|
||||||
|
pub global: Account<'info, Global>,
|
||||||
|
#[account(
|
||||||
|
init,
|
||||||
|
payer = bettor,
|
||||||
|
space = 8 + Bet::INIT_SPACE,
|
||||||
|
seeds = [BET_SEED, &id.to_le_bytes()],
|
||||||
|
bump
|
||||||
|
)]
|
||||||
|
pub bet: Account<'info, Bet>,
|
||||||
|
pub clock: Sysvar<'info, Clock>,
|
||||||
|
pub system_program: Program<'info, System>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handler(
|
||||||
|
ctx: &mut Context<CreateBet>,
|
||||||
|
id: u64,
|
||||||
|
side: u64,
|
||||||
|
amount: u64,
|
||||||
|
entry_price: u64,
|
||||||
|
) -> Result<()> {
|
||||||
|
require!(ctx.accounts.global.paused == 0, SmartUpdownError::Paused);
|
||||||
|
require!(side == SIDE_UP || side == SIDE_DOWN, SmartUpdownError::InvalidSide);
|
||||||
|
require!(
|
||||||
|
amount >= ctx.accounts.global.min_amount,
|
||||||
|
SmartUpdownError::AmountBelowMin
|
||||||
|
);
|
||||||
|
require!(
|
||||||
|
amount <= ctx.accounts.global.max_amount,
|
||||||
|
SmartUpdownError::AmountAboveMax
|
||||||
|
);
|
||||||
|
require!(
|
||||||
|
id == ctx.accounts.global.counter,
|
||||||
|
SmartUpdownError::BetIdMismatch
|
||||||
|
);
|
||||||
|
|
||||||
|
let now = ctx.accounts.clock.unix_timestamp;
|
||||||
|
let bettor_address = ctx.accounts.bettor.key();
|
||||||
|
let bet_address = ctx.accounts.bet.key();
|
||||||
|
let expiry_seconds = ctx.accounts.global.expiry_seconds;
|
||||||
|
|
||||||
|
{
|
||||||
|
let bet = &mut ctx.accounts.bet;
|
||||||
|
bet.bettor = bettor_address;
|
||||||
|
bet.side = side;
|
||||||
|
bet.amount = amount;
|
||||||
|
bet.bet_time = now;
|
||||||
|
bet.expire_time = now + expiry_seconds;
|
||||||
|
bet.entry_price = entry_price;
|
||||||
|
bet.exit_price = 0;
|
||||||
|
bet.status = STATUS_OPEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.accounts.global.counter = ctx.accounts.global.counter.checked_add(1).unwrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
let cpi_accounts = anchor_lang::system_program::Transfer {
|
||||||
|
from: ctx.accounts.bettor.to_account_info(),
|
||||||
|
to: ctx.accounts.global.to_account_info(),
|
||||||
|
};
|
||||||
|
let cpi_ctx = CpiContext::new(
|
||||||
|
ctx.accounts.system_program.to_account_info(),
|
||||||
|
cpi_accounts,
|
||||||
|
);
|
||||||
|
anchor_lang::system_program::transfer(cpi_ctx, amount)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit!(BetCreated {
|
||||||
|
bet: bet_address,
|
||||||
|
bettor: bettor_address,
|
||||||
|
side,
|
||||||
|
amount,
|
||||||
|
bet_time: now,
|
||||||
|
expire_time: now + expiry_seconds,
|
||||||
|
entry_price,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
|
use crate::{constants::GLOBAL_SEED, state::Global};
|
||||||
|
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct Initialize<'info> {
|
||||||
|
#[account(mut)]
|
||||||
|
pub admin: Signer<'info>,
|
||||||
|
#[account(
|
||||||
|
init,
|
||||||
|
payer = admin,
|
||||||
|
space = 8 + Global::INIT_SPACE,
|
||||||
|
seeds = [GLOBAL_SEED],
|
||||||
|
bump
|
||||||
|
)]
|
||||||
|
pub global: Account<'info, Global>,
|
||||||
|
pub system_program: Program<'info, System>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handler(
|
||||||
|
ctx: &mut Context<Initialize>,
|
||||||
|
min_amount: u64,
|
||||||
|
max_amount: u64,
|
||||||
|
multiplier_bps: u64,
|
||||||
|
expiry_seconds: i64,
|
||||||
|
) -> Result<()> {
|
||||||
|
let global = &mut ctx.accounts.global;
|
||||||
|
global.admin = ctx.accounts.admin.key();
|
||||||
|
global.relayer = ctx.accounts.admin.key();
|
||||||
|
global.counter = 0;
|
||||||
|
global.min_amount = min_amount;
|
||||||
|
global.max_amount = max_amount;
|
||||||
|
global.multiplier_bps = multiplier_bps;
|
||||||
|
global.paused = 0;
|
||||||
|
global.expiry_seconds = expiry_seconds;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
constants::GLOBAL_SEED,
|
||||||
|
error::SmartUpdownError,
|
||||||
|
state::Global,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct Update<'info> {
|
||||||
|
pub admin: Signer<'info>,
|
||||||
|
#[account(
|
||||||
|
mut,
|
||||||
|
seeds = [GLOBAL_SEED],
|
||||||
|
bump
|
||||||
|
)]
|
||||||
|
pub global: Account<'info, Global>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handler(
|
||||||
|
ctx: &mut Context<Update>,
|
||||||
|
new_relayer: Pubkey,
|
||||||
|
min_amount: u64,
|
||||||
|
max_amount: u64,
|
||||||
|
multiplier_bps: u64,
|
||||||
|
paused: u64,
|
||||||
|
expiry_seconds: i64,
|
||||||
|
) -> Result<()> {
|
||||||
|
require!(
|
||||||
|
ctx.accounts.admin.key() == ctx.accounts.global.admin,
|
||||||
|
SmartUpdownError::NotAdmin
|
||||||
|
);
|
||||||
|
let global = &mut ctx.accounts.global;
|
||||||
|
global.relayer = new_relayer;
|
||||||
|
global.min_amount = min_amount;
|
||||||
|
global.max_amount = max_amount;
|
||||||
|
global.multiplier_bps = multiplier_bps;
|
||||||
|
global.paused = paused;
|
||||||
|
global.expiry_seconds = expiry_seconds;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
pub mod constants;
|
||||||
|
pub mod error;
|
||||||
|
pub mod events;
|
||||||
|
pub mod instructions;
|
||||||
|
pub mod state;
|
||||||
|
|
||||||
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
|
pub use instructions::*;
|
||||||
|
|
||||||
|
declare_id!("9ALsnxXNzDBv3mokngCHbruRTWUZiWR7pf7vswS1fCpf");
|
||||||
|
|
||||||
|
#[program]
|
||||||
|
pub mod smart_updown {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub fn initialize(
|
||||||
|
mut ctx: Context<Initialize>,
|
||||||
|
min_amount: u64,
|
||||||
|
max_amount: u64,
|
||||||
|
multiplier_bps: u64,
|
||||||
|
expiry_seconds: i64,
|
||||||
|
) -> Result<()> {
|
||||||
|
initialize::handler(&mut ctx, min_amount, max_amount, multiplier_bps, expiry_seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_bet(
|
||||||
|
mut ctx: Context<CreateBet>,
|
||||||
|
id: u64,
|
||||||
|
side: u64,
|
||||||
|
amount: u64,
|
||||||
|
entry_price: u64,
|
||||||
|
) -> Result<()> {
|
||||||
|
create_bet::handler(&mut ctx, id, side, amount, entry_price)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close_bet(
|
||||||
|
mut ctx: Context<CloseBet>,
|
||||||
|
id: u64,
|
||||||
|
exit_price: u64,
|
||||||
|
) -> Result<()> {
|
||||||
|
close_bet::handler(&mut ctx, id, exit_price)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(
|
||||||
|
mut ctx: Context<Update>,
|
||||||
|
new_relayer: Pubkey,
|
||||||
|
min_amount: u64,
|
||||||
|
max_amount: u64,
|
||||||
|
multiplier_bps: u64,
|
||||||
|
paused: u64,
|
||||||
|
expiry_seconds: i64,
|
||||||
|
) -> Result<()> {
|
||||||
|
update::handler(
|
||||||
|
&mut ctx,
|
||||||
|
new_relayer,
|
||||||
|
min_amount,
|
||||||
|
max_amount,
|
||||||
|
multiplier_bps,
|
||||||
|
paused,
|
||||||
|
expiry_seconds,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
|
#[account]
|
||||||
|
#[derive(InitSpace)]
|
||||||
|
pub struct Global {
|
||||||
|
pub counter: u64,
|
||||||
|
pub min_amount: u64,
|
||||||
|
pub max_amount: u64,
|
||||||
|
pub multiplier_bps: u64,
|
||||||
|
pub expiry_seconds: i64,
|
||||||
|
pub paused: u64,
|
||||||
|
pub admin: Pubkey,
|
||||||
|
pub relayer: Pubkey,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[account]
|
||||||
|
#[derive(InitSpace)]
|
||||||
|
pub struct Bet {
|
||||||
|
pub amount: u64,
|
||||||
|
pub bet_time: i64,
|
||||||
|
pub expire_time: i64,
|
||||||
|
pub entry_price: u64,
|
||||||
|
pub exit_price: u64,
|
||||||
|
pub side: u64,
|
||||||
|
pub status: u64,
|
||||||
|
pub bettor: Pubkey,
|
||||||
|
}
|
||||||
@@ -0,0 +1,371 @@
|
|||||||
|
use {
|
||||||
|
anchor_lang::{
|
||||||
|
solana_program::{
|
||||||
|
instruction::Instruction,
|
||||||
|
pubkey::Pubkey,
|
||||||
|
system_instruction,
|
||||||
|
},
|
||||||
|
AccountDeserialize, InstructionData, ToAccountMetas,
|
||||||
|
},
|
||||||
|
smart_updown::state::{Bet, Global},
|
||||||
|
solana_clock::Clock,
|
||||||
|
solana_keypair::Keypair,
|
||||||
|
solana_program_test::{BanksClient, ProgramTest, ProgramTestContext},
|
||||||
|
solana_signer::Signer,
|
||||||
|
solana_transaction::Transaction,
|
||||||
|
};
|
||||||
|
|
||||||
|
const CLOCK_ADDR: &str = "SysvarC1ock11111111111111111111111111111111";
|
||||||
|
const SYSTEM_ADDR: &str = "11111111111111111111111111111111";
|
||||||
|
const MIN_AMOUNT: u64 = 1_000_000_000; // 1 SOL
|
||||||
|
const MULTIPLIER_BPS: u64 = 13_000; // 1.3x
|
||||||
|
const EXPIRY_SECONDS: i64 = 15;
|
||||||
|
const START_TIME: i64 = 1_700_000_000;
|
||||||
|
|
||||||
|
fn pid() -> Pubkey {
|
||||||
|
smart_updown::id()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn global_pda() -> Pubkey {
|
||||||
|
Pubkey::find_program_address(&[b"global"], &pid()).0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bet_pda(id: u64) -> Pubkey {
|
||||||
|
Pubkey::find_program_address(&[b"bet", &id.to_le_bytes()], &pid()).0
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup() -> (ProgramTestContext, Keypair) {
|
||||||
|
let mut pt = ProgramTest::default();
|
||||||
|
pt.add_program("smart_updown", pid(), None);
|
||||||
|
// Fund the deploy keypair separately in the test; the validator in CI funds it.
|
||||||
|
let context = pt.start_with_context().await;
|
||||||
|
let admin = Keypair::new();
|
||||||
|
context.banks_client.process_transaction(Transaction::new_signed_with_payer(
|
||||||
|
&[system_instruction::transfer(
|
||||||
|
&context.payer.pubkey(),
|
||||||
|
&admin.pubkey(),
|
||||||
|
100_000_000_000,
|
||||||
|
)],
|
||||||
|
Some(&context.payer.pubkey()),
|
||||||
|
&[&context.payer],
|
||||||
|
context.last_blockhash,
|
||||||
|
)).await.unwrap();
|
||||||
|
(context, admin)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_clock(ctx: &mut ProgramTestContext, unix: i64) {
|
||||||
|
let mut clk: Clock = ctx.banks_client.get_sysvar().await.unwrap();
|
||||||
|
clk.unix_timestamp = unix;
|
||||||
|
ctx.set_sysvar(&clk);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_account_data(
|
||||||
|
bank: &BanksClient,
|
||||||
|
addr: &Pubkey,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
bank.get_account(*addr).await.unwrap().expect("account exists").data
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_global_typed(data: &[u8]) -> Global {
|
||||||
|
Global::try_deserialize(&mut &data[..]).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_bet_typed(data: &[u8]) -> Bet {
|
||||||
|
Bet::try_deserialize(&mut &data[..]).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_initialize_ix(admin: Pubkey) -> Instruction {
|
||||||
|
Instruction::new_with_bytes(
|
||||||
|
pid(),
|
||||||
|
&smart_updown::instruction::Initialize {
|
||||||
|
min_amount: MIN_AMOUNT,
|
||||||
|
max_amount: 1_000_000_000_000,
|
||||||
|
multiplier_bps: MULTIPLIER_BPS,
|
||||||
|
expiry_seconds: EXPIRY_SECONDS,
|
||||||
|
}
|
||||||
|
.data(),
|
||||||
|
smart_updown::accounts::Initialize {
|
||||||
|
admin,
|
||||||
|
global: global_pda(),
|
||||||
|
system_program: SYSTEM_ADDR.parse().unwrap(),
|
||||||
|
}
|
||||||
|
.to_account_metas(None),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_create_ix(id: u64, side: u64, amount: u64, entry_price: u64, bettor: Pubkey) -> Instruction {
|
||||||
|
Instruction::new_with_bytes(
|
||||||
|
pid(),
|
||||||
|
&smart_updown::instruction::CreateBet {
|
||||||
|
id,
|
||||||
|
side,
|
||||||
|
amount,
|
||||||
|
entry_price,
|
||||||
|
}
|
||||||
|
.data(),
|
||||||
|
smart_updown::accounts::CreateBet {
|
||||||
|
bettor,
|
||||||
|
global: global_pda(),
|
||||||
|
bet: bet_pda(id),
|
||||||
|
clock: CLOCK_ADDR.parse().unwrap(),
|
||||||
|
system_program: SYSTEM_ADDR.parse().unwrap(),
|
||||||
|
}
|
||||||
|
.to_account_metas(None),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_close_ix(id: u64, exit_price: u64, relayer: Pubkey, bettor: Pubkey) -> Instruction {
|
||||||
|
Instruction::new_with_bytes(
|
||||||
|
pid(),
|
||||||
|
&smart_updown::instruction::CloseBet { id, exit_price }.data(),
|
||||||
|
smart_updown::accounts::CloseBet {
|
||||||
|
relayer,
|
||||||
|
bet: bet_pda(id),
|
||||||
|
global: global_pda(),
|
||||||
|
bettor,
|
||||||
|
clock: CLOCK_ADDR.parse().unwrap(),
|
||||||
|
}
|
||||||
|
.to_account_metas(None),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_ix(
|
||||||
|
ctx: &mut ProgramTestContext,
|
||||||
|
signer: &Keypair,
|
||||||
|
ix: Instruction,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let tx = Transaction::new_signed_with_payer(
|
||||||
|
&[ix],
|
||||||
|
Some(&signer.pubkey()),
|
||||||
|
&[signer],
|
||||||
|
ctx.last_blockhash,
|
||||||
|
);
|
||||||
|
ctx.banks_client
|
||||||
|
.process_transaction(tx)
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|e| format!("{:?}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn initialize(ctx: &mut ProgramTestContext, admin: &Keypair) {
|
||||||
|
run_ix(ctx, admin, make_initialize_ix(admin.pubkey()))
|
||||||
|
.await
|
||||||
|
.expect("initialize");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn airdrop_to(ctx: &mut ProgramTestContext, to: &Pubkey, lamports: u64) {
|
||||||
|
let tx = Transaction::new_signed_with_payer(
|
||||||
|
&[system_instruction::transfer(
|
||||||
|
&ctx.payer.pubkey(),
|
||||||
|
to,
|
||||||
|
lamports,
|
||||||
|
)],
|
||||||
|
Some(&ctx.payer.pubkey()),
|
||||||
|
&[&ctx.payer],
|
||||||
|
ctx.last_blockhash,
|
||||||
|
);
|
||||||
|
ctx.banks_client.process_transaction(tx).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fund_vault(ctx: &mut ProgramTestContext, lamports: u64) {
|
||||||
|
airdrop_to(ctx, &global_pda(), lamports).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_up_bet(
|
||||||
|
ctx: &mut ProgramTestContext,
|
||||||
|
side: u64,
|
||||||
|
) -> (Keypair, u64, u64) {
|
||||||
|
let bettor = Keypair::new();
|
||||||
|
airdrop_to(ctx, &bettor.pubkey(), 100_000_000_000).await;
|
||||||
|
set_clock(ctx, START_TIME).await;
|
||||||
|
let amount = MIN_AMOUNT;
|
||||||
|
let entry_price = 100_000_000u64;
|
||||||
|
let create_ix = make_create_ix(0, side, amount, entry_price, bettor.pubkey());
|
||||||
|
run_ix(ctx, &bettor, create_ix).await.expect("create_bet");
|
||||||
|
(bettor, amount, entry_price)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn initialize_creates_global() {
|
||||||
|
let (mut ctx, admin) = setup().await;
|
||||||
|
initialize(&mut ctx, &admin).await;
|
||||||
|
let acc = read_account_data(&ctx.banks_client, &global_pda()).await;
|
||||||
|
let g = read_global_typed(&acc);
|
||||||
|
assert_eq!(g.admin, admin.pubkey());
|
||||||
|
assert_eq!(g.relayer, admin.pubkey());
|
||||||
|
assert_eq!(g.counter, 0);
|
||||||
|
assert_eq!(g.multiplier_bps, MULTIPLIER_BPS);
|
||||||
|
assert_eq!(g.expiry_seconds, EXPIRY_SECONDS);
|
||||||
|
assert_eq!(g.paused, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn create_bet_escrows_sol() {
|
||||||
|
let (mut ctx, admin) = setup().await;
|
||||||
|
initialize(&mut ctx, &admin).await;
|
||||||
|
let (bettor, amount, entry_price) = create_up_bet(&mut ctx, 0).await;
|
||||||
|
|
||||||
|
let vault_balance = ctx.banks_client.get_balance(global_pda()).await.unwrap();
|
||||||
|
assert!(vault_balance >= amount);
|
||||||
|
let b = read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await);
|
||||||
|
assert_eq!(b.bettor, bettor.pubkey());
|
||||||
|
assert_eq!(b.side, 0);
|
||||||
|
assert_eq!(b.amount, amount);
|
||||||
|
assert_eq!(b.entry_price, entry_price);
|
||||||
|
assert_eq!(b.bet_time, START_TIME);
|
||||||
|
assert_eq!(b.expire_time, START_TIME + EXPIRY_SECONDS);
|
||||||
|
assert_eq!(b.status, 0);
|
||||||
|
assert_eq!(read_global_typed(&read_account_data(&ctx.banks_client, &global_pda()).await).counter, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn close_bet_user_wins_up() {
|
||||||
|
let (mut ctx, admin) = setup().await;
|
||||||
|
initialize(&mut ctx, &admin).await;
|
||||||
|
let (bettor, amount, entry_price) = create_up_bet(&mut ctx, 0).await;
|
||||||
|
|
||||||
|
set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await;
|
||||||
|
let exit_price = entry_price + 1_000;
|
||||||
|
let before = ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap();
|
||||||
|
let payout = amount * MULTIPLIER_BPS / 10_000;
|
||||||
|
fund_vault(&mut ctx, 10 * amount).await;
|
||||||
|
|
||||||
|
let close_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey());
|
||||||
|
run_ix(&mut ctx, &admin, close_ix).await.expect("close_bet");
|
||||||
|
|
||||||
|
assert_eq!(ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(), before + payout);
|
||||||
|
let b = read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await);
|
||||||
|
assert_eq!(b.status, 1);
|
||||||
|
assert_eq!(b.exit_price, exit_price);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn close_bet_house_wins_down() {
|
||||||
|
let (mut ctx, admin) = setup().await;
|
||||||
|
initialize(&mut ctx, &admin).await;
|
||||||
|
let (bettor, _amount, entry_price) = create_up_bet(&mut ctx, 1).await;
|
||||||
|
|
||||||
|
set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await;
|
||||||
|
let exit_price = entry_price + 1_000; // side=DOWN but price rose -> lose
|
||||||
|
let before = ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap();
|
||||||
|
|
||||||
|
let close_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey());
|
||||||
|
run_ix(&mut ctx, &admin, close_ix).await.expect("close_bet");
|
||||||
|
|
||||||
|
assert_eq!(ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(), before);
|
||||||
|
assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn close_bet_equal_price_stays_open() {
|
||||||
|
let (mut ctx, admin) = setup().await;
|
||||||
|
initialize(&mut ctx, &admin).await;
|
||||||
|
let (bettor, amount, entry_price) = create_up_bet(&mut ctx, 0).await;
|
||||||
|
|
||||||
|
set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await;
|
||||||
|
let before = ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap();
|
||||||
|
let payout = amount * MULTIPLIER_BPS / 10_000;
|
||||||
|
fund_vault(&mut ctx, 10 * amount).await;
|
||||||
|
|
||||||
|
let equal_ix = make_close_ix(0, entry_price, admin.pubkey(), bettor.pubkey());
|
||||||
|
run_ix(&mut ctx, &admin, equal_ix).await.expect("equal close");
|
||||||
|
assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 0);
|
||||||
|
assert_eq!(ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(), before);
|
||||||
|
|
||||||
|
let exit_price = entry_price + 1_000;
|
||||||
|
let second_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey());
|
||||||
|
run_ix(&mut ctx, &admin, second_ix).await.expect("second close");
|
||||||
|
assert_eq!(
|
||||||
|
ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(),
|
||||||
|
before + payout
|
||||||
|
);
|
||||||
|
assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn close_bet_already_finalized() {
|
||||||
|
let (mut ctx, admin) = setup().await;
|
||||||
|
initialize(&mut ctx, &admin).await;
|
||||||
|
let (bettor, amount, entry_price) = create_up_bet(&mut ctx, 0).await;
|
||||||
|
|
||||||
|
set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await;
|
||||||
|
let win_price = entry_price + 500;
|
||||||
|
fund_vault(&mut ctx, 10 * amount).await;
|
||||||
|
run_ix(&mut ctx, &admin, make_close_ix(0, win_price, admin.pubkey(), bettor.pubkey()))
|
||||||
|
.await
|
||||||
|
.expect("first close");
|
||||||
|
|
||||||
|
let res = run_ix(
|
||||||
|
&mut ctx,
|
||||||
|
&admin,
|
||||||
|
make_close_ix(0, win_price + 1, admin.pubkey(), bettor.pubkey()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(res.is_err(), "expected close after finalization to be rejected");
|
||||||
|
let b = read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await);
|
||||||
|
assert_eq!(b.status, 1);
|
||||||
|
assert_eq!(b.exit_price, win_price);
|
||||||
|
assert_eq!(b.status, 1);
|
||||||
|
assert_eq!(b.exit_price, win_price);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn close_bet_before_expiry_rejected() {
|
||||||
|
let (mut ctx, admin) = setup().await;
|
||||||
|
initialize(&mut ctx, &admin).await;
|
||||||
|
let (bettor, _amount, entry_price) = create_up_bet(&mut ctx, 0).await;
|
||||||
|
|
||||||
|
let close_ix = make_close_ix(0, entry_price + 500, admin.pubkey(), bettor.pubkey());
|
||||||
|
let res = run_ix(&mut ctx, &admin, close_ix).await;
|
||||||
|
assert!(res.is_err(), "expected premature close to be rejected");
|
||||||
|
assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn close_bet_wrong_relayer_rejected() {
|
||||||
|
let (mut ctx, admin) = setup().await;
|
||||||
|
initialize(&mut ctx, &admin).await;
|
||||||
|
let (bettor, _amount, entry_price) = create_up_bet(&mut ctx, 0).await;
|
||||||
|
let attacker = Keypair::new();
|
||||||
|
airdrop_to(&mut ctx, &attacker.pubkey(), 100_000_000_000).await;
|
||||||
|
|
||||||
|
set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await;
|
||||||
|
let close_ix = make_close_ix(0, entry_price + 500, attacker.pubkey(), bettor.pubkey());
|
||||||
|
let res = run_ix(&mut ctx, &attacker, close_ix).await;
|
||||||
|
assert!(res.is_err(), "expected wrong relayer to be rejected");
|
||||||
|
assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn close_bet_replay_rejected() {
|
||||||
|
let (mut ctx, admin) = setup().await;
|
||||||
|
initialize(&mut ctx, &admin).await;
|
||||||
|
let (bettor, _amount, entry_price) = create_up_bet(&mut ctx, 0).await;
|
||||||
|
|
||||||
|
set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await;
|
||||||
|
fund_vault(&mut ctx, 10 * MIN_AMOUNT).await;
|
||||||
|
let close_ix = make_close_ix(0, entry_price + 500, admin.pubkey(), bettor.pubkey());
|
||||||
|
run_ix(&mut ctx, &admin, close_ix).await.expect("first close");
|
||||||
|
let before = ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap();
|
||||||
|
let res = run_ix(
|
||||||
|
&mut ctx,
|
||||||
|
&admin,
|
||||||
|
make_close_ix(0, entry_price + 501, admin.pubkey(), bettor.pubkey()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(res.is_err(), "expected replay close to be rejected");
|
||||||
|
assert_eq!(ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(), before);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn create_bet_rejects_out_of_range() {
|
||||||
|
let (mut ctx, admin) = setup().await;
|
||||||
|
initialize(&mut ctx, &admin).await;
|
||||||
|
let bettor = Keypair::new();
|
||||||
|
airdrop_to(&mut ctx, &bettor.pubkey(), 100_000_000_000).await;
|
||||||
|
set_clock(&mut ctx, START_TIME).await;
|
||||||
|
|
||||||
|
let ix = make_create_ix(0, 0, 1, 100_000_000u64, bettor.pubkey());
|
||||||
|
let res = run_ix(&mut ctx, &bettor, ix).await;
|
||||||
|
assert!(res.is_err(), "expected below-min bet to be rejected");
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "1.89.0"
|
||||||
|
components = ["rustfmt","clippy"]
|
||||||
|
profile = "minimal"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# RPC эндпоинт кластера (localnet = запущенный solana-test-validator)
|
||||||
|
SMART_RPC_URL=http://127.0.0.1:8899
|
||||||
|
|
||||||
|
# Разрешить airDrop на localnet (bettor'ам и ваулту). На mainnet выключить.
|
||||||
|
SMART_ALLOW_AIRDROP=1
|
||||||
|
|
||||||
|
# Путь к keypair админа/релеера (тот же ключ = admin и relayer)
|
||||||
|
SMART_ADMIN_KEYPAIR=~/.config/solana/id.json
|
||||||
|
|
||||||
|
# Параметры контракта (должны совпадать со значениями при initialize)
|
||||||
|
SMART_MIN_AMOUNT=100000000 # 0.1 SOL в лампортах
|
||||||
|
SMART_MAX_AMOUNT=10000000000 # 10 SOL в лампортах
|
||||||
|
SMART_MULTIPLIER_BPS=13000 # 1.3x выплата в базисных пунктах
|
||||||
|
SMART_EXPIRY_SECONDS=15 # экспирация ставки (секунды)
|
||||||
@@ -0,0 +1,546 @@
|
|||||||
|
{
|
||||||
|
"address": "9ALsnxXNzDBv3mokngCHbruRTWUZiWR7pf7vswS1fCpf",
|
||||||
|
"metadata": {
|
||||||
|
"name": "smart_updown",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"spec": "0.1.0",
|
||||||
|
"description": "Created with Anchor"
|
||||||
|
},
|
||||||
|
"instructions": [
|
||||||
|
{
|
||||||
|
"name": "close_bet",
|
||||||
|
"discriminator": [
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "relayer",
|
||||||
|
"signer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bet",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
98,
|
||||||
|
101,
|
||||||
|
116
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "arg",
|
||||||
|
"path": "id"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "global",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
103,
|
||||||
|
108,
|
||||||
|
111,
|
||||||
|
98,
|
||||||
|
97,
|
||||||
|
108
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bettor",
|
||||||
|
"docs": [
|
||||||
|
"CHECK: recipient must be the bet's bettor and is validated in the handler."
|
||||||
|
],
|
||||||
|
"writable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "clock",
|
||||||
|
"address": "SysvarC1ock11111111111111111111111111111111"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "exit_price",
|
||||||
|
"type": "u64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "create_bet",
|
||||||
|
"discriminator": [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "bettor",
|
||||||
|
"writable": true,
|
||||||
|
"signer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "global",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
103,
|
||||||
|
108,
|
||||||
|
111,
|
||||||
|
98,
|
||||||
|
97,
|
||||||
|
108
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bet",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
98,
|
||||||
|
101,
|
||||||
|
116
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "arg",
|
||||||
|
"path": "id"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "clock",
|
||||||
|
"address": "SysvarC1ock11111111111111111111111111111111"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "system_program",
|
||||||
|
"address": "11111111111111111111111111111111"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "side",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "amount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "entry_price",
|
||||||
|
"type": "u64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "initialize",
|
||||||
|
"discriminator": [
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "admin",
|
||||||
|
"writable": true,
|
||||||
|
"signer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "global",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
103,
|
||||||
|
108,
|
||||||
|
111,
|
||||||
|
98,
|
||||||
|
97,
|
||||||
|
108
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "system_program",
|
||||||
|
"address": "11111111111111111111111111111111"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "min_amount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "max_amount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "multiplier_bps",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expiry_seconds",
|
||||||
|
"type": "i64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "update",
|
||||||
|
"discriminator": [
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "admin",
|
||||||
|
"signer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "global",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
103,
|
||||||
|
108,
|
||||||
|
111,
|
||||||
|
98,
|
||||||
|
97,
|
||||||
|
108
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "new_relayer",
|
||||||
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "min_amount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "max_amount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "multiplier_bps",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "paused",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expiry_seconds",
|
||||||
|
"type": "i64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "Bet",
|
||||||
|
"discriminator": [
|
||||||
|
147,
|
||||||
|
23,
|
||||||
|
35,
|
||||||
|
59,
|
||||||
|
15,
|
||||||
|
75,
|
||||||
|
155,
|
||||||
|
32
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Global",
|
||||||
|
"discriminator": [
|
||||||
|
167,
|
||||||
|
232,
|
||||||
|
232,
|
||||||
|
177,
|
||||||
|
200,
|
||||||
|
108,
|
||||||
|
114,
|
||||||
|
127
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"name": "BetClosed",
|
||||||
|
"discriminator": [
|
||||||
|
122,
|
||||||
|
20,
|
||||||
|
157,
|
||||||
|
254,
|
||||||
|
221,
|
||||||
|
0,
|
||||||
|
39,
|
||||||
|
129
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BetCreated",
|
||||||
|
"discriminator": [
|
||||||
|
32,
|
||||||
|
153,
|
||||||
|
105,
|
||||||
|
71,
|
||||||
|
188,
|
||||||
|
72,
|
||||||
|
107,
|
||||||
|
114
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": 6000,
|
||||||
|
"name": "Paused",
|
||||||
|
"msg": "Operation is paused"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6001,
|
||||||
|
"name": "NotAdmin",
|
||||||
|
"msg": "Only the admin can perform this action"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6002,
|
||||||
|
"name": "NotRelayer",
|
||||||
|
"msg": "Only the relayer is allowed to close bets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6003,
|
||||||
|
"name": "AmountBelowMin",
|
||||||
|
"msg": "Amount below the allowed minimum"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6004,
|
||||||
|
"name": "AmountAboveMax",
|
||||||
|
"msg": "Amount above the allowed maximum"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6005,
|
||||||
|
"name": "InvalidSide",
|
||||||
|
"msg": "Invalid side value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6006,
|
||||||
|
"name": "BetAlreadyFinalized",
|
||||||
|
"msg": "Bet is already finalized"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6007,
|
||||||
|
"name": "NotExpired",
|
||||||
|
"msg": "Bet cannot be closed before expiry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6008,
|
||||||
|
"name": "BetIdMismatch",
|
||||||
|
"msg": "Bet id mismatch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6009,
|
||||||
|
"name": "InsufficientVault",
|
||||||
|
"msg": "Payout exceeds vault solvency"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"name": "Bet",
|
||||||
|
"serialization": "bytemuck",
|
||||||
|
"repr": {
|
||||||
|
"kind": "c"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"kind": "struct",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "amount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bet_time",
|
||||||
|
"type": "i64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expire_time",
|
||||||
|
"type": "i64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "entry_price",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "exit_price",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "side",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bettor",
|
||||||
|
"type": "pubkey"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BetClosed",
|
||||||
|
"type": {
|
||||||
|
"kind": "struct",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "bet",
|
||||||
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "exit_price",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "user_won",
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "payout",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"type": "u64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BetCreated",
|
||||||
|
"type": {
|
||||||
|
"kind": "struct",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "bet",
|
||||||
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bettor",
|
||||||
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "side",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "amount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bet_time",
|
||||||
|
"type": "i64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expire_time",
|
||||||
|
"type": "i64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "entry_price",
|
||||||
|
"type": "u64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Global",
|
||||||
|
"serialization": "bytemuck",
|
||||||
|
"repr": {
|
||||||
|
"kind": "c"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"kind": "struct",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "counter",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "min_amount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "max_amount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "multiplier_bps",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expiry_seconds",
|
||||||
|
"type": "i64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "paused",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "admin",
|
||||||
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "relayer",
|
||||||
|
"type": "pubkey"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"constants": [
|
||||||
|
{
|
||||||
|
"name": "BET_SEED",
|
||||||
|
"type": "bytes",
|
||||||
|
"value": "[98, 101, 116]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GLOBAL_SEED",
|
||||||
|
"type": "bytes",
|
||||||
|
"value": "[103, 108, 111, 98, 97, 108]"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,552 @@
|
|||||||
|
/**
|
||||||
|
* 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`.
|
||||||
|
*/
|
||||||
|
export type SmartUpdown = {
|
||||||
|
"address": "9ALsnxXNzDBv3mokngCHbruRTWUZiWR7pf7vswS1fCpf",
|
||||||
|
"metadata": {
|
||||||
|
"name": "smartUpdown",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"spec": "0.1.0",
|
||||||
|
"description": "Created with Anchor"
|
||||||
|
},
|
||||||
|
"instructions": [
|
||||||
|
{
|
||||||
|
"name": "closeBet",
|
||||||
|
"discriminator": [
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "relayer",
|
||||||
|
"signer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bet",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
98,
|
||||||
|
101,
|
||||||
|
116
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "arg",
|
||||||
|
"path": "id"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "global",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
103,
|
||||||
|
108,
|
||||||
|
111,
|
||||||
|
98,
|
||||||
|
97,
|
||||||
|
108
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bettor",
|
||||||
|
"docs": [
|
||||||
|
"CHECK: recipient must be the bet's bettor and is validated in the handler."
|
||||||
|
],
|
||||||
|
"writable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "clock",
|
||||||
|
"address": "SysvarC1ock11111111111111111111111111111111"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "exitPrice",
|
||||||
|
"type": "u64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "createBet",
|
||||||
|
"discriminator": [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "bettor",
|
||||||
|
"writable": true,
|
||||||
|
"signer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "global",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
103,
|
||||||
|
108,
|
||||||
|
111,
|
||||||
|
98,
|
||||||
|
97,
|
||||||
|
108
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bet",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
98,
|
||||||
|
101,
|
||||||
|
116
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "arg",
|
||||||
|
"path": "id"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "clock",
|
||||||
|
"address": "SysvarC1ock11111111111111111111111111111111"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "systemProgram",
|
||||||
|
"address": "11111111111111111111111111111111"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "side",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "amount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "entryPrice",
|
||||||
|
"type": "u64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "initialize",
|
||||||
|
"discriminator": [
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "admin",
|
||||||
|
"writable": true,
|
||||||
|
"signer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "global",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
103,
|
||||||
|
108,
|
||||||
|
111,
|
||||||
|
98,
|
||||||
|
97,
|
||||||
|
108
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "systemProgram",
|
||||||
|
"address": "11111111111111111111111111111111"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "minAmount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maxAmount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "multiplierBps",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expirySeconds",
|
||||||
|
"type": "i64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "update",
|
||||||
|
"discriminator": [
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "admin",
|
||||||
|
"signer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "global",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
103,
|
||||||
|
108,
|
||||||
|
111,
|
||||||
|
98,
|
||||||
|
97,
|
||||||
|
108
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "newRelayer",
|
||||||
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "minAmount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maxAmount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "multiplierBps",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "paused",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expirySeconds",
|
||||||
|
"type": "i64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "bet",
|
||||||
|
"discriminator": [
|
||||||
|
147,
|
||||||
|
23,
|
||||||
|
35,
|
||||||
|
59,
|
||||||
|
15,
|
||||||
|
75,
|
||||||
|
155,
|
||||||
|
32
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "global",
|
||||||
|
"discriminator": [
|
||||||
|
167,
|
||||||
|
232,
|
||||||
|
232,
|
||||||
|
177,
|
||||||
|
200,
|
||||||
|
108,
|
||||||
|
114,
|
||||||
|
127
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"name": "betClosed",
|
||||||
|
"discriminator": [
|
||||||
|
122,
|
||||||
|
20,
|
||||||
|
157,
|
||||||
|
254,
|
||||||
|
221,
|
||||||
|
0,
|
||||||
|
39,
|
||||||
|
129
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "betCreated",
|
||||||
|
"discriminator": [
|
||||||
|
32,
|
||||||
|
153,
|
||||||
|
105,
|
||||||
|
71,
|
||||||
|
188,
|
||||||
|
72,
|
||||||
|
107,
|
||||||
|
114
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": 6000,
|
||||||
|
"name": "paused",
|
||||||
|
"msg": "Operation is paused"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6001,
|
||||||
|
"name": "notAdmin",
|
||||||
|
"msg": "Only the admin can perform this action"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6002,
|
||||||
|
"name": "notRelayer",
|
||||||
|
"msg": "Only the relayer is allowed to close bets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6003,
|
||||||
|
"name": "amountBelowMin",
|
||||||
|
"msg": "Amount below the allowed minimum"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6004,
|
||||||
|
"name": "amountAboveMax",
|
||||||
|
"msg": "Amount above the allowed maximum"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6005,
|
||||||
|
"name": "invalidSide",
|
||||||
|
"msg": "Invalid side value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6006,
|
||||||
|
"name": "betAlreadyFinalized",
|
||||||
|
"msg": "Bet is already finalized"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6007,
|
||||||
|
"name": "notExpired",
|
||||||
|
"msg": "Bet cannot be closed before expiry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6008,
|
||||||
|
"name": "betIdMismatch",
|
||||||
|
"msg": "Bet id mismatch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6009,
|
||||||
|
"name": "insufficientVault",
|
||||||
|
"msg": "Payout exceeds vault solvency"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"name": "bet",
|
||||||
|
"serialization": "bytemuck",
|
||||||
|
"repr": {
|
||||||
|
"kind": "c"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"kind": "struct",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "amount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "betTime",
|
||||||
|
"type": "i64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expireTime",
|
||||||
|
"type": "i64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "entryPrice",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "exitPrice",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "side",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bettor",
|
||||||
|
"type": "pubkey"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "betClosed",
|
||||||
|
"type": {
|
||||||
|
"kind": "struct",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "bet",
|
||||||
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "exitPrice",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "userWon",
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "payout",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"type": "u64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "betCreated",
|
||||||
|
"type": {
|
||||||
|
"kind": "struct",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "bet",
|
||||||
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bettor",
|
||||||
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "side",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "amount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "betTime",
|
||||||
|
"type": "i64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expireTime",
|
||||||
|
"type": "i64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "entryPrice",
|
||||||
|
"type": "u64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "global",
|
||||||
|
"serialization": "bytemuck",
|
||||||
|
"repr": {
|
||||||
|
"kind": "c"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"kind": "struct",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "counter",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "minAmount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maxAmount",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "multiplierBps",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expirySeconds",
|
||||||
|
"type": "i64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "paused",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "admin",
|
||||||
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "relayer",
|
||||||
|
"type": "pubkey"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"constants": [
|
||||||
|
{
|
||||||
|
"name": "betSeed",
|
||||||
|
"type": "bytes",
|
||||||
|
"value": "[98, 101, 116]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "globalSeed",
|
||||||
|
"type": "bytes",
|
||||||
|
"value": "[103, 108, 111, 98, 97, 108]"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "smart-updown-relayer",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"scripts": {
|
||||||
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||||
|
"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/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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { Connection, clusterApiUrl, Keypair, PublicKey } from "@solana/web3.js";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as os from "os";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
export const SIDE_UP = 0;
|
||||||
|
export const SIDE_DOWN = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime configuration. Everything can be overridden via environment
|
||||||
|
* variables; defaults target the local validator.
|
||||||
|
*/
|
||||||
|
export interface Config {
|
||||||
|
/** RPC endpoint of the cluster (localnet by default). */
|
||||||
|
rpcUrl: string;
|
||||||
|
/** Localnet allows naive airdrops (to bettors and the house vault). */
|
||||||
|
allowAirdrop: boolean;
|
||||||
|
/** Program id. Defaults to the value embedded in the IDL. */
|
||||||
|
programId: PublicKey;
|
||||||
|
/** House admin/relayer keypair file path. */
|
||||||
|
adminKeypairPath: string;
|
||||||
|
/** Initial parameters used by `init` (must match on-chain constants). */
|
||||||
|
minAmountLamports: number;
|
||||||
|
maxAmountLamports: number;
|
||||||
|
multiplierBps: number;
|
||||||
|
expirySeconds: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function env(name: string, fallback: string): string {
|
||||||
|
return process.env[name] ?? fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function envNum(name: string, fallback: number): number {
|
||||||
|
const raw = process.env[name];
|
||||||
|
if (!raw) return fallback;
|
||||||
|
const n = Number(raw);
|
||||||
|
if (!Number.isFinite(n)) throw new Error(`Invalid number for env ${name}: ${raw}`);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadConfig(): Config {
|
||||||
|
const rpcUrl = env("SMART_RPC_URL", "http://127.0.0.1:8899");
|
||||||
|
const allowAirdrop = env("SMART_ALLOW_AIRDROP", "1") === "1";
|
||||||
|
const adminKeypairPath =
|
||||||
|
env("SMART_ADMIN_KEYPAIR", path.join(os.homedir(), ".config/solana/id.json"));
|
||||||
|
|
||||||
|
return {
|
||||||
|
rpcUrl,
|
||||||
|
allowAirdrop,
|
||||||
|
programId: PublicKey.default, // placeholder; real value read from the IDL below
|
||||||
|
adminKeypairPath,
|
||||||
|
minAmountLamports: envNum("SMART_MIN_AMOUNT", 100_000_000), // 0.1 SOL
|
||||||
|
maxAmountLamports: envNum("SMART_MAX_AMOUNT", 10_000_000_000), // 10 SOL
|
||||||
|
multiplierBps: envNum("SMART_MULTIPLIER_BPS", 13_000), // 1.3x
|
||||||
|
expirySeconds: envNum("SMART_EXPIRY_SECONDS", 15),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadKeypair(pathOrJson: string): Keypair {
|
||||||
|
const resolved = pathOrJson.startsWith("~")
|
||||||
|
? path.join(os.homedir(), pathOrJson.slice(1))
|
||||||
|
: path.resolve(pathOrJson);
|
||||||
|
const raw = fs.readFileSync(resolved, "utf8");
|
||||||
|
return Keypair.fromSecretKey(Uint8Array.from(JSON.parse(raw)));
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Keypair } from "@solana/web3.js";
|
||||||
|
import { SIDE_DOWN, SIDE_UP } from "./config";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* STUB — «фронт»: от пользователя приходят параметры ставки.
|
||||||
|
*
|
||||||
|
* В реальной системе это сообщение присылает фронтенд (или твоя БД) после
|
||||||
|
* того, как пользователь выбрал направление и сумму. Здесь мы эмулируем
|
||||||
|
* входящую очередь: случайный UP/DOWN и случайную сумму в заданном диапазоне.
|
||||||
|
*/
|
||||||
|
export interface BetRequest {
|
||||||
|
bettor: Keypair;
|
||||||
|
side: number; // SIDE_UP | SIDE_DOWN
|
||||||
|
amountLamports: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Очередь входящих ставок. В заглушке наполняем случайными значениями. */
|
||||||
|
export function nextBetRequest(minAmount: number, maxAmount: number): BetRequest {
|
||||||
|
const bettor = Keypair.generate();
|
||||||
|
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 };
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js";
|
||||||
|
import { Program } from "@anchor-lang/core";
|
||||||
|
import { loadConfig, loadKeypair, SIDE_UP } from "./config";
|
||||||
|
import { getPrice } from "./priceSource";
|
||||||
|
import { nextBetRequest } from "./frontStub";
|
||||||
|
import {
|
||||||
|
initialize,
|
||||||
|
createBet,
|
||||||
|
closeBet,
|
||||||
|
readGlobal,
|
||||||
|
ensureVaultFunded,
|
||||||
|
} from "./ops";
|
||||||
|
import { betPda } from "./pda";
|
||||||
|
import { makeConnection, makeProgram } from "./program";
|
||||||
|
|
||||||
|
import type { SmartUpdown } from "../idl/smart_updown";
|
||||||
|
|
||||||
|
const LAMPORTS_PER_SOL_NUM = Number(LAMPORTS_PER_SOL);
|
||||||
|
|
||||||
|
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
|
const STATUS_OPEN = 0;
|
||||||
|
async function fundBettor(connection: any, bettorPubkey: PublicKey): Promise<void> {
|
||||||
|
const bal = await connection.getBalance(bettorPubkey);
|
||||||
|
if (bal < 2 * LAMPORTS_PER_SOL_NUM) {
|
||||||
|
await connection.requestAirdrop(bettorPubkey, 5 * LAMPORTS_PER_SOL_NUM);
|
||||||
|
await sleep(800);
|
||||||
|
console.log(`bettor funded ${bettorPubkey.toBase58()} (+5 SOL)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Закрывает ставку, повторяя при «equal price» (exit==entry) пока не зафиналится. */
|
||||||
|
async function settleBet(
|
||||||
|
program: Program<SmartUpdown>,
|
||||||
|
id: any,
|
||||||
|
entryPrice: number,
|
||||||
|
relayer: any,
|
||||||
|
bettor: PublicKey
|
||||||
|
): Promise<void> {
|
||||||
|
for (let attempt = 1; ; attempt++) {
|
||||||
|
const exitPrice = await getPrice();
|
||||||
|
if (exitPrice === entryPrice) {
|
||||||
|
console.log(`attempt ${attempt}: exit==entry (${exitPrice}), жду движение...`);
|
||||||
|
await sleep(2000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const sig = await closeBet(program, id, exitPrice, relayer, bettor);
|
||||||
|
console.log(`close_bet #${id} exit=${exitPrice} (attempt ${attempt}) tx=${sig}`);
|
||||||
|
|
||||||
|
const status = await getStatus(program, id);
|
||||||
|
if (status === STATUS_OPEN) {
|
||||||
|
console.log(`ставка ещё open (equal price), повторяю close...`);
|
||||||
|
await sleep(2000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStatus(program: Program<SmartUpdown>, id: any): Promise<number> {
|
||||||
|
const bet = await program.account.bet.fetchNullable(betPda(program.programId, id));
|
||||||
|
return bet ? bet.status.toNumber() : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const admin = loadKeypair(cfg.adminKeypairPath);
|
||||||
|
const connection = makeConnection(cfg.rpcUrl);
|
||||||
|
const program = makeProgram(connection, admin);
|
||||||
|
|
||||||
|
console.log(`RPC: ${cfg.rpcUrl}`);
|
||||||
|
console.log(`Admin/relayer: ${admin.publicKey.toBase58()}`);
|
||||||
|
|
||||||
|
const global = await readGlobal(program, program.programId);
|
||||||
|
if (!global) {
|
||||||
|
if (!process.argv.includes("--init")) {
|
||||||
|
throw new Error(
|
||||||
|
"Контракт не инициализирован. Запусти с флагом --init (или init+run)."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await initialize(program, {
|
||||||
|
minAmount: cfg.minAmountLamports,
|
||||||
|
maxAmount: cfg.maxAmountLamports,
|
||||||
|
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 ` +
|
||||||
|
`expiry=${global.expirySeconds}s paused=${global.paused}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.allowAirdrop) {
|
||||||
|
await ensureVaultFunded(connection, program, 10 * LAMPORTS_PER_SOL_NUM);
|
||||||
|
}
|
||||||
|
|
||||||
|
const loops = process.argv.includes("--once") ? 1 : Infinity;
|
||||||
|
let n = 0;
|
||||||
|
|
||||||
|
while (n < loops) {
|
||||||
|
n++;
|
||||||
|
const req = nextBetRequest(cfg.minAmountLamports, cfg.maxAmountLamports);
|
||||||
|
await fundBettor(connection, req.bettor.publicKey);
|
||||||
|
|
||||||
|
const entryPrice = await getPrice();
|
||||||
|
await createBet(program, req.bettor, {
|
||||||
|
side: req.side,
|
||||||
|
amount: req.amountLamports,
|
||||||
|
entryPrice,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`ставка ${req.side === SIDE_UP ? "UP" : "DOWN"} на ${(req.amountLamports / LAMPORTS_PER_SOL_NUM).toFixed(3)} SOL, жду ${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);
|
||||||
|
|
||||||
|
const outcome = await program.account.bet.fetchNullable(betPda(program.programId, id));
|
||||||
|
console.log(
|
||||||
|
outcome!.status.toNumber() === 1
|
||||||
|
? `>> результат #${id}: ПОЛЬЗОВАТЕЛЬ ВЫИГРАЛ (payout 1.3x)`
|
||||||
|
: `>> результат #${id}: ВЫИГРАЛ ДОМ (user lost)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log("Готово.");
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { Connection, Keypair, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js";
|
||||||
|
import BN from "bn.js";
|
||||||
|
import { Program } from "@anchor-lang/core";
|
||||||
|
import { SIDE_UP } from "./config";
|
||||||
|
import { globalPda } from "./pda";
|
||||||
|
|
||||||
|
import type { SmartUpdown } from "../idl/smart_updown";
|
||||||
|
|
||||||
|
const LAMPORTS_PER_SOL_NUM = Number(LAMPORTS_PER_SOL);
|
||||||
|
|
||||||
|
/** Считывает account Global (ваулт). Возвращает null, если ещё не инициализирован. */
|
||||||
|
export async function readGlobal(
|
||||||
|
program: Program<SmartUpdown>,
|
||||||
|
programId: PublicKey
|
||||||
|
): Promise<{
|
||||||
|
counter: BN;
|
||||||
|
minAmount: BN;
|
||||||
|
maxAmount: BN;
|
||||||
|
multiplierBps: BN;
|
||||||
|
expirySeconds: BN;
|
||||||
|
paused: BN;
|
||||||
|
admin: PublicKey;
|
||||||
|
relayer: PublicKey;
|
||||||
|
lamports: number;
|
||||||
|
} | 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);
|
||||||
|
return {
|
||||||
|
counter: raw.counter,
|
||||||
|
minAmount: raw.minAmount,
|
||||||
|
maxAmount: raw.maxAmount,
|
||||||
|
multiplierBps: raw.multiplierBps,
|
||||||
|
expirySeconds: raw.expirySeconds,
|
||||||
|
paused: raw.paused,
|
||||||
|
admin: raw.admin,
|
||||||
|
relayer: raw.relayer,
|
||||||
|
lamports,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Одноразовая инициализация контракта (вызывается admin'ом до приёма ставок). */
|
||||||
|
export async function initialize(
|
||||||
|
program: Program<SmartUpdown>,
|
||||||
|
args: {
|
||||||
|
minAmount: number;
|
||||||
|
maxAmount: number;
|
||||||
|
multiplierBps: number;
|
||||||
|
expirySeconds: number;
|
||||||
|
},
|
||||||
|
admin: Keypair
|
||||||
|
): Promise<string> {
|
||||||
|
const sig = await program.methods
|
||||||
|
.initialize(
|
||||||
|
new BN(args.minAmount),
|
||||||
|
new BN(args.maxAmount),
|
||||||
|
new BN(args.multiplierBps),
|
||||||
|
new BN(args.expirySeconds)
|
||||||
|
)
|
||||||
|
.accounts({
|
||||||
|
admin: admin.publicKey,
|
||||||
|
})
|
||||||
|
.signers([admin])
|
||||||
|
.rpc();
|
||||||
|
return sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Создаёт ставку от имени пользователя. Возвращает фактический id (== counter). */
|
||||||
|
export async function createBet(
|
||||||
|
program: Program<SmartUpdown>,
|
||||||
|
bettor: Keypair,
|
||||||
|
args: { side: number; amount: number; entryPrice: number }
|
||||||
|
): Promise<BN> {
|
||||||
|
const global = await readGlobal(program, program.programId);
|
||||||
|
if (!global) throw new Error("Contract not initialized");
|
||||||
|
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({
|
||||||
|
bettor: bettor.publicKey,
|
||||||
|
})
|
||||||
|
.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}`);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Закрывает ставку после экспирации.
|
||||||
|
* Если exit_price == entry_price, контракт не финализирует ставку — тогда
|
||||||
|
* ждём первое движение цены и повторяем close (см. обработку в main loop).
|
||||||
|
*/
|
||||||
|
export async function closeBet(
|
||||||
|
program: Program<SmartUpdown>,
|
||||||
|
id: BN,
|
||||||
|
exitPrice: number,
|
||||||
|
relayer: Keypair,
|
||||||
|
bettor: PublicKey
|
||||||
|
): Promise<string> {
|
||||||
|
return program.methods
|
||||||
|
.closeBet(new BN(id), new BN(exitPrice))
|
||||||
|
.accounts({
|
||||||
|
relayer: relayer.publicKey,
|
||||||
|
bettor,
|
||||||
|
})
|
||||||
|
.signers([relayer])
|
||||||
|
.rpc();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Догружает ваулт (house bankroll), чтобы хватило на выплаты 1.3x.
|
||||||
|
* На localnet используем airDrop на PDA ваулта; на mainnet воздух не накапает —
|
||||||
|
* там нужно переводить SOL с админа (off-chain, вне этого CLI).
|
||||||
|
*/
|
||||||
|
export async function ensureVaultFunded(
|
||||||
|
connection: Connection,
|
||||||
|
program: Program<SmartUpdown>,
|
||||||
|
amountLamports: number
|
||||||
|
): Promise<void> {
|
||||||
|
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`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import BN from "bn.js";
|
||||||
|
|
||||||
|
/** Seeds, согласованные с контрактом (см. constants.rs). */
|
||||||
|
export const GLOBAL_SEED = Buffer.from("global");
|
||||||
|
export const BET_SEED = Buffer.from("bet");
|
||||||
|
|
||||||
|
export function globalPda(programId: PublicKey): PublicKey {
|
||||||
|
return PublicKey.findProgramAddressSync([GLOBAL_SEED], programId)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function betPda(programId: PublicKey, id: BN): PublicKey {
|
||||||
|
const idLE = Buffer.alloc(8);
|
||||||
|
idLE.writeBigUInt64LE(BigInt(id.toString(10)));
|
||||||
|
return PublicKey.findProgramAddressSync([BET_SEED, idLE], programId)[0];
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* STUB — SOL price source.
|
||||||
|
*
|
||||||
|
* TODO(пользователь): замени `getPrice` на реальный запрос к своей БД.
|
||||||
|
* Договоримся, что функция возвращает цену SOL как целое число в «центах»
|
||||||
|
* (то же юнит-пространство, что у веса entry/exit на контракте). Контракт
|
||||||
|
* сравнивает тол ько «больше/меньше/равно», поэтому само юнит-пространство
|
||||||
|
* не критично — главное, чтобы оно было согласовано между entry и exit.
|
||||||
|
*/
|
||||||
|
let simulated = 100_000_000;
|
||||||
|
|
||||||
|
/** Цена SOL сейчас (заглушка — детерминированно «гуляет»). */
|
||||||
|
export async function getPrice(): Promise<number> {
|
||||||
|
const drift = Math.round(Math.random() * 200_000); // ±0.2% движка
|
||||||
|
simulated += Math.random() < 0.5 ? drift : -drift;
|
||||||
|
return simulated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Управляемая заглушка: вернуть ровно `value` (полезна для теста «equal price»). */
|
||||||
|
export function setPriceOverride(value: number | undefined): void {
|
||||||
|
simulated = value ?? 100_000_000;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Connection } from "@solana/web3.js";
|
||||||
|
import { AnchorProvider, Program, Wallet } from "@anchor-lang/core";
|
||||||
|
import { Keypair } from "@solana/web3.js";
|
||||||
|
import idl from "../idl/smart_updown.json";
|
||||||
|
|
||||||
|
import type { SmartUpdown } from "../idl/smart_updown";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создаёт готовый клиент программы: Connection + AnchorProvider (подпись админа-
|
||||||
|
* релеера) + типизированный Program, собранный из IDL.
|
||||||
|
*/
|
||||||
|
export function makeProgram(
|
||||||
|
connection: Connection,
|
||||||
|
admin: Keypair
|
||||||
|
): Program<SmartUpdown> {
|
||||||
|
const provider = new AnchorProvider(
|
||||||
|
connection,
|
||||||
|
new Wallet(admin),
|
||||||
|
{ commitment: "confirmed", preflightCommitment: "confirmed" }
|
||||||
|
);
|
||||||
|
// IDL.address задаёт programId в рантайме.
|
||||||
|
return new Program<SmartUpdown>(idl as unknown as SmartUpdown, provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeConnection(rpcUrl: string): Connection {
|
||||||
|
return new Connection(rpcUrl, "confirmed");
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["es2020"],
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es2020",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*", "idl/**/*"]
|
||||||
|
}
|
||||||
Binary file not shown.
Reference in New Issue
Block a user