feat: migrate smart-updown contract to SPL-token (фантики)
- create_bet: escrow фантиков с bettor на vault (PDA[global,vault]) через anchor_spl::token - close_bet: выплата фантиков из vault на bettor (CPI с signer_seeds vault) - state/constants: comments про единицы (token units decimals=6), + VAULT_SEED - IDL регенерирован (idl/ + relayer/idl/) с аккаунтами bettor_token/vault_token/token_program - тесты обновлены под токен (10/10 + doctest)
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
# TASK — перевод смарт-контракта smart-updown на фантики (SPL-токен)
|
||||
|
||||
Репозиторий: `smart-updown-token` (копия `Smart-game`, ветка `main`). Оригинал НЕ трогаем.
|
||||
|
||||
> Работаем ТОЛЬКО `program/programs/smart-updown/src/**`. Остальное (relayer, deploy) не менять
|
||||
> в этом проходе.
|
||||
|
||||
## Цель
|
||||
|
||||
Контракт сейчас принимает/выплачивает **нативный SOL (lamports)** через `system_program` и
|
||||
`add/sub_lamports`. Перевести на **наши фантики = SPL-token** (обычный SPL Token, НЕ Token-2022 —
|
||||
для MVP берём простой стандарт; swap на pump.fun конвертит SOL↔фантики безотносительно стандарта).
|
||||
|
||||
**Логика игры не меняется:** ставка вверх/вниз по цене SOL, `entry_price`/`exit_price` — обычные
|
||||
u64 и сравниваются как раньше. Меняется ТОЛЬКО норма расчётов (валюта): с lamports на токен-единицы.
|
||||
|
||||
## Что изменится
|
||||
|
||||
### 1. `state.rs`
|
||||
- Поля `Global` остаются (counter, min/max_amount, multiplier_bps, expiry_seconds, paused, admin, relayer).
|
||||
`min_amount`/`max_amount` теперь = **токен-единицы**, не lamports. (decimals=6; логика количеств не меняется, просто ценовая шкала).
|
||||
- `Bet` — без изменений (amount = токен-единицы).
|
||||
|
||||
### 2. `create_bet.rs`
|
||||
- Добавить аккаунты (через `anchor_spl::token::Token`):
|
||||
- `bettor_token: Account<'info, TokenAccount>` (ATA игрока, `#[account(mut)]`) — откуда списываем фантики.
|
||||
- `vault_token: Account<'info, TokenAccount>` (ATA на global, `#[account(seeds=[GLOBAL_SEED], bump)]`, `mut`) — куда кладём.
|
||||
- `token_program: Program<'info, Token>`.
|
||||
- Заменить блок `system_program::transfer(...)` на `anchor_spl::token::transfer(...)`:
|
||||
из `bettor_token` в `vault_token`, `amount`.
|
||||
- `bettor` остаётся `Signer`, но теперь просто auth (токен-подпись).
|
||||
|
||||
### 3. `close_bet.rs`
|
||||
- Добавить:
|
||||
- `vault_token: Account<'info, TokenAccount>` (`mut`, seeds GLOBAL_SEED) — источник фантиков.
|
||||
- `bettor_token: Account<'info, TokenAccount>` (`mut`) — куда платим.
|
||||
- `token_program: Program<'info, Token>`.
|
||||
- Заменить ваулт-логику: вместо `add/sub_lamports` у global — `token::transfer` из `vault_token`
|
||||
в `bettor_token` на `payout`.
|
||||
- `global` больше НЕ касса SOL — убрать `SystemAccount`-требование; `bettor` в `close_bet` можно убрать
|
||||
из аккаунтов (адрес в `bet.bettor` уже есть), но для обратной совместимости можно оставить как `SystemAccount` (не мутируя).
|
||||
- Проверка `global.get_lamports() >= payout` заменяется на проверку баланса `vault_token.amount >= payout`
|
||||
(или просто положиться на token::transfer failure — но лучше явно проверить `InsufficientVault`).
|
||||
|
||||
### 4. `initialize.rs` / `update.rs`
|
||||
- Убедиться, что `min/max_amount` интерпретируются как токен-единицы. Если в `initialize` передаются
|
||||
как lamports — просто поменять смысл (или оставить, числа вводим в токен-единицах). Логику не трогаем.
|
||||
|
||||
### 5. Cargo.toml
|
||||
- Добавить зависимость: `anchor-spl = "0.31.1"` (даёт `anchor_spl::token`).
|
||||
|
||||
### 6. IDL (idl/*.json)
|
||||
- Обновить после сборки (`anchor build` регенерит). Новые аккаунты в create_bet/close_bet.
|
||||
|
||||
## Требования (гейты)
|
||||
|
||||
1. Каждый шаг компилируется: `anchor build` (или `cargo build-sbf`) — БЕЗ ошибок.
|
||||
2. В конце: `anchor build` зелёный + обновлённый IDL.
|
||||
3. Семантика: create_bet берёт фантики с bettor на ваулт; close_bet платит фантики с ваулта на bettor.
|
||||
4. Тесты (`program/tests/`) — обновить под токен (создать mint, ATA, задеплоить), прогнать `anchor test`.
|
||||
|
||||
## Порядок работы (шаг за шагом, компиляз после каждого)
|
||||
|
||||
- **Шаг 1** — Cargo.toml: + анchor-spl.
|
||||
- **Шаг 2** — state.rs: только комментарий (min/max = токен-единицы), без изменения полей.
|
||||
- **Шаг 3** — create_bet.rs: добавить токен-аккаунты + `token::transfer`.
|
||||
- **Шаг 4** — close_bet.rs: добавить токен-аккаунты + `token::transfer` из ваулта на bettor.
|
||||
- **Шаг 5** — IDL реген + тесты.
|
||||
- **Шаг 6** — финальный `anchor test` зелёный.
|
||||
|
||||
## ЧЕГО НЕ ДЕЛАТЬ
|
||||
- Не трогать логику «вверх/вниз», entry/exit_price, статусы.
|
||||
- Не менять relayer/скрипты деплоя.
|
||||
- Не коммитить (всё остаётся в рабочем дереве до приёмки человеком).
|
||||
|
||||
## Проверка
|
||||
- `anchor build` — 0 ошибок.
|
||||
- `anchor test` (или `cargo test` в program) — зелёный.
|
||||
- Все новые аккаунты корректно работают с токен-кейсами (mint, ATA, ваулт).
|
||||
+139
-18
@@ -10,7 +10,14 @@
|
||||
{
|
||||
"name": "close_bet",
|
||||
"discriminator": [
|
||||
2
|
||||
185,
|
||||
206,
|
||||
13,
|
||||
184,
|
||||
176,
|
||||
108,
|
||||
140,
|
||||
107
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
@@ -57,15 +64,54 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bettor",
|
||||
"docs": [
|
||||
"CHECK: recipient must be the bet's bettor and is validated in the handler."
|
||||
],
|
||||
"writable": true
|
||||
"name": "bettor"
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"address": "SysvarC1ock11111111111111111111111111111111"
|
||||
},
|
||||
{
|
||||
"name": "vault_token",
|
||||
"docs": [
|
||||
"Program-owned token vault: PDA([GLOBAL_SEED, VAULT_SEED]); payouts are paid out from here."
|
||||
],
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
103,
|
||||
108,
|
||||
111,
|
||||
98,
|
||||
97,
|
||||
108
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
118,
|
||||
97,
|
||||
117,
|
||||
108,
|
||||
116
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bettor_token",
|
||||
"docs": [
|
||||
"Bettor's SPL-token account; the payout is sent here."
|
||||
],
|
||||
"writable": true
|
||||
},
|
||||
{
|
||||
"name": "token_program",
|
||||
"address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
@@ -82,7 +128,14 @@
|
||||
{
|
||||
"name": "create_bet",
|
||||
"discriminator": [
|
||||
1
|
||||
197,
|
||||
42,
|
||||
153,
|
||||
2,
|
||||
59,
|
||||
63,
|
||||
143,
|
||||
246
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
@@ -136,6 +189,49 @@
|
||||
{
|
||||
"name": "system_program",
|
||||
"address": "11111111111111111111111111111111"
|
||||
},
|
||||
{
|
||||
"name": "bettor_token",
|
||||
"docs": [
|
||||
"Bettor's SPL-token account (our \"fanti\" mint); bet amount is pulled from here."
|
||||
],
|
||||
"writable": true
|
||||
},
|
||||
{
|
||||
"name": "vault_token",
|
||||
"docs": [
|
||||
"Program-owned token vault: PDA([GLOBAL_SEED, VAULT_SEED]); bet amounts are escrowed here."
|
||||
],
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
103,
|
||||
108,
|
||||
111,
|
||||
98,
|
||||
97,
|
||||
108
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
118,
|
||||
97,
|
||||
117,
|
||||
108,
|
||||
116
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "token_program",
|
||||
"address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
@@ -160,7 +256,14 @@
|
||||
{
|
||||
"name": "initialize",
|
||||
"discriminator": [
|
||||
0
|
||||
175,
|
||||
175,
|
||||
109,
|
||||
31,
|
||||
13,
|
||||
152,
|
||||
155,
|
||||
237
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
@@ -214,7 +317,14 @@
|
||||
{
|
||||
"name": "update",
|
||||
"discriminator": [
|
||||
3
|
||||
219,
|
||||
200,
|
||||
88,
|
||||
176,
|
||||
158,
|
||||
63,
|
||||
253,
|
||||
127
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
@@ -380,10 +490,9 @@
|
||||
"types": [
|
||||
{
|
||||
"name": "Bet",
|
||||
"serialization": "bytemuck",
|
||||
"repr": {
|
||||
"kind": "c"
|
||||
},
|
||||
"docs": [
|
||||
"Open bet. `amount` is in token base units (decimals = 6), not lamports."
|
||||
],
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
@@ -488,10 +597,13 @@
|
||||
},
|
||||
{
|
||||
"name": "Global",
|
||||
"serialization": "bytemuck",
|
||||
"repr": {
|
||||
"kind": "c"
|
||||
},
|
||||
"docs": [
|
||||
"Global game state.",
|
||||
"",
|
||||
"All money-related fields are denominated in **SPL-token base units**",
|
||||
"(our \"fanti\" token, decimals = 6), not in lamports:",
|
||||
"`min_amount`/`max_amount` bound a bet amount in token units."
|
||||
],
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
@@ -541,6 +653,15 @@
|
||||
"name": "GLOBAL_SEED",
|
||||
"type": "bytes",
|
||||
"value": "[103, 108, 111, 98, 97, 108]"
|
||||
},
|
||||
{
|
||||
"name": "VAULT_SEED",
|
||||
"docs": [
|
||||
"Vault PDA seed: the token vault lives at PDA([GLOBAL_SEED, VAULT_SEED]).",
|
||||
"It cannot reuse GLOBAL_SEED alone — that PDA address is the `global` state account itself."
|
||||
],
|
||||
"type": "bytes",
|
||||
"value": "[118, 97, 117, 108, 116]"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+422
-6
@@ -294,6 +294,7 @@ dependencies = [
|
||||
"anchor-derive-accounts",
|
||||
"anchor-derive-serde",
|
||||
"anchor-derive-space",
|
||||
"anchor-lang-idl",
|
||||
"base64 0.21.7",
|
||||
"bincode",
|
||||
"borsh 0.10.4",
|
||||
@@ -311,6 +312,7 @@ dependencies = [
|
||||
"anchor-lang-idl-spec",
|
||||
"anyhow",
|
||||
"heck 0.3.3",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
@@ -326,6 +328,21 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anchor-spl"
|
||||
version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c08cb5d762c0694f74bd02c9a5b04ea53cefc496e2c27b3234acffca5cd076b"
|
||||
dependencies = [
|
||||
"anchor-lang",
|
||||
"spl-associated-token-account",
|
||||
"spl-pod",
|
||||
"spl-token",
|
||||
"spl-token-2022",
|
||||
"spl-token-group-interface",
|
||||
"spl-token-metadata-interface",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anchor-syn"
|
||||
version = "0.31.1"
|
||||
@@ -334,6 +351,7 @@ checksum = "5dc7a6d90cc643df0ed2744862cdf180587d1e5d28936538c18fc8908489ed67"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bs58",
|
||||
"cargo_toml",
|
||||
"heck 0.3.3",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -892,6 +910,16 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cargo_toml"
|
||||
version = "0.19.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a98356df42a2eb1bd8f1793ae4ee4de48e384dd974ce5eac8eee802edb7492be"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"toml 0.8.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.4.4"
|
||||
@@ -3171,7 +3199,7 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785"
|
||||
dependencies = [
|
||||
"toml",
|
||||
"toml 0.5.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3180,7 +3208,7 @@ version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||
dependencies = [
|
||||
"toml_edit",
|
||||
"toml_edit 0.25.13+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3854,6 +3882,15 @@ dependencies = [
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
@@ -4035,6 +4072,8 @@ name = "smart-updown"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anchor-lang",
|
||||
"anchor-spl",
|
||||
"solana-account",
|
||||
"solana-client",
|
||||
"solana-clock",
|
||||
"solana-keypair",
|
||||
@@ -4042,6 +4081,8 @@ dependencies = [
|
||||
"solana-pubkey",
|
||||
"solana-signer",
|
||||
"solana-transaction",
|
||||
"spl-associated-token-account",
|
||||
"spl-token",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -6046,6 +6087,12 @@ dependencies = [
|
||||
"solana-sdk-ids",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solana-security-txt"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c94a02d486b28f219a4f8f5d7dd93cbfbb93c9f466cb7871c22e50cd5ae9a7a2"
|
||||
|
||||
[[package]]
|
||||
name = "solana-seed-derivable"
|
||||
version = "2.2.1"
|
||||
@@ -6977,6 +7024,81 @@ dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-associated-token-account"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76fee7d65013667032d499adc3c895e286197a35a0d3a4643c80e7fd3e9969e3"
|
||||
dependencies = [
|
||||
"borsh 1.8.1",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"solana-program",
|
||||
"spl-associated-token-account-client",
|
||||
"spl-token",
|
||||
"spl-token-2022",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-associated-token-account-client"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f8349dbcbe575f354f9a533a21f272f3eb3808a49e2fdc1c34393b88ba76cb"
|
||||
dependencies = [
|
||||
"solana-instruction",
|
||||
"solana-pubkey",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-discriminator"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7398da23554a31660f17718164e31d31900956054f54f52d5ec1be51cb4f4b3"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"solana-program-error",
|
||||
"solana-sha256-hasher",
|
||||
"spl-discriminator-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-discriminator-derive"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9e8418ea6269dcfb01c712f0444d2c75542c04448b480e87de59d2865edc750"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"spl-discriminator-syn",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-discriminator-syn"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d1dbc82ab91422345b6df40a79e2b78c7bce1ebb366da323572dd60b7076b67"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"sha2 0.10.9",
|
||||
"syn 2.0.119",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-elgamal-registry"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce0f668975d2b0536e8a8fd60e56a05c467f06021dae037f1d0cfed0de2e231d"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"solana-program",
|
||||
"solana-zk-sdk",
|
||||
"spl-pod",
|
||||
"spl-token-confidential-transfer-proof-extraction",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-generic-token"
|
||||
version = "1.0.1"
|
||||
@@ -6987,6 +7109,250 @@ dependencies = [
|
||||
"solana-pubkey",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-memo"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f09647c0974e33366efeb83b8e2daebb329f0420149e74d3a4bd2c08cf9f7cb"
|
||||
dependencies = [
|
||||
"solana-account-info",
|
||||
"solana-instruction",
|
||||
"solana-msg",
|
||||
"solana-program-entrypoint",
|
||||
"solana-program-error",
|
||||
"solana-pubkey",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-pod"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d994afaf86b779104b4a95ba9ca75b8ced3fdb17ee934e38cb69e72afbe17799"
|
||||
dependencies = [
|
||||
"borsh 1.8.1",
|
||||
"bytemuck",
|
||||
"bytemuck_derive",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"solana-decode-error",
|
||||
"solana-msg",
|
||||
"solana-program-error",
|
||||
"solana-program-option",
|
||||
"solana-pubkey",
|
||||
"solana-zk-sdk",
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-program-error"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d39b5186f42b2b50168029d81e58e800b690877ef0b30580d107659250da1d1"
|
||||
dependencies = [
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"solana-program",
|
||||
"spl-program-error-derive",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-program-error-derive"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6d375dd76c517836353e093c2dbb490938ff72821ab568b545fd30ab3256b3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"sha2 0.10.9",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-tlv-account-resolution"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd99ff1e9ed2ab86e3fd582850d47a739fec1be9f4661cba1782d3a0f26805f3"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"solana-account-info",
|
||||
"solana-decode-error",
|
||||
"solana-instruction",
|
||||
"solana-msg",
|
||||
"solana-program-error",
|
||||
"solana-pubkey",
|
||||
"spl-discriminator",
|
||||
"spl-pod",
|
||||
"spl-program-error",
|
||||
"spl-type-length-value",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-token"
|
||||
version = "7.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed320a6c934128d4f7e54fe00e16b8aeaecf215799d060ae14f93378da6dc834"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"bytemuck",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"num_enum",
|
||||
"solana-program",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-token-2022"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b27f7405010ef816587c944536b0eafbcc35206ab6ba0f2ca79f1d28e488f4f"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"bytemuck",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"num_enum",
|
||||
"solana-program",
|
||||
"solana-security-txt",
|
||||
"solana-zk-sdk",
|
||||
"spl-elgamal-registry",
|
||||
"spl-memo",
|
||||
"spl-pod",
|
||||
"spl-token",
|
||||
"spl-token-confidential-transfer-ciphertext-arithmetic",
|
||||
"spl-token-confidential-transfer-proof-extraction",
|
||||
"spl-token-confidential-transfer-proof-generation",
|
||||
"spl-token-group-interface",
|
||||
"spl-token-metadata-interface",
|
||||
"spl-transfer-hook-interface",
|
||||
"spl-type-length-value",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-token-confidential-transfer-ciphertext-arithmetic"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "170378693c5516090f6d37ae9bad2b9b6125069be68d9acd4865bbe9fc8499fd"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytemuck",
|
||||
"solana-curve25519",
|
||||
"solana-zk-sdk",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-token-confidential-transfer-proof-extraction"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eff2d6a445a147c9d6dd77b8301b1e116c8299601794b558eafa409b342faf96"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"solana-curve25519",
|
||||
"solana-program",
|
||||
"solana-zk-sdk",
|
||||
"spl-pod",
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-token-confidential-transfer-proof-generation"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8627184782eec1894de8ea26129c61303f1f0adeed65c20e0b10bc584f09356d"
|
||||
dependencies = [
|
||||
"curve25519-dalek 4.1.3",
|
||||
"solana-zk-sdk",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-token-group-interface"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d595667ed72dbfed8c251708f406d7c2814a3fa6879893b323d56a10bedfc799"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"solana-decode-error",
|
||||
"solana-instruction",
|
||||
"solana-msg",
|
||||
"solana-program-error",
|
||||
"solana-pubkey",
|
||||
"spl-discriminator",
|
||||
"spl-pod",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-token-metadata-interface"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfb9c89dbc877abd735f05547dcf9e6e12c00c11d6d74d8817506cab4c99fdbb"
|
||||
dependencies = [
|
||||
"borsh 1.8.1",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"solana-borsh",
|
||||
"solana-decode-error",
|
||||
"solana-instruction",
|
||||
"solana-msg",
|
||||
"solana-program-error",
|
||||
"solana-pubkey",
|
||||
"spl-discriminator",
|
||||
"spl-pod",
|
||||
"spl-type-length-value",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-transfer-hook-interface"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4aa7503d52107c33c88e845e1351565050362c2314036ddf19a36cd25137c043"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"bytemuck",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"solana-account-info",
|
||||
"solana-cpi",
|
||||
"solana-decode-error",
|
||||
"solana-instruction",
|
||||
"solana-msg",
|
||||
"solana-program-error",
|
||||
"solana-pubkey",
|
||||
"spl-discriminator",
|
||||
"spl-pod",
|
||||
"spl-program-error",
|
||||
"spl-tlv-account-resolution",
|
||||
"spl-type-length-value",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-type-length-value"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba70ef09b13af616a4c987797870122863cba03acc4284f226a4473b043923f9"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"solana-account-info",
|
||||
"solana-decode-error",
|
||||
"solana-msg",
|
||||
"solana-program-error",
|
||||
"spl-discriminator",
|
||||
"spl-pod",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
@@ -7157,7 +7523,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.4",
|
||||
"getrandom 0.4.3",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -7409,6 +7775,27 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime 0.6.11",
|
||||
"toml_edit 0.22.27",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
@@ -7418,6 +7805,20 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime 0.6.11",
|
||||
"toml_write",
|
||||
"winnow 0.7.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.25.13+spec-1.1.0"
|
||||
@@ -7425,9 +7826,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime",
|
||||
"toml_datetime 1.1.1+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"winnow",
|
||||
"winnow 1.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7436,9 +7837,15 @@ version = "1.1.3+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
"winnow 1.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.3"
|
||||
@@ -8021,6 +8428,15 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.4"
|
||||
|
||||
@@ -14,10 +14,11 @@ default = []
|
||||
cpi = ["no-entrypoint"]
|
||||
no-entrypoint = []
|
||||
no-log-ix-name = []
|
||||
idl-build = []
|
||||
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]
|
||||
|
||||
[dependencies]
|
||||
anchor-lang = "0.31.1"
|
||||
anchor-spl = "0.31.1"
|
||||
|
||||
[dev-dependencies]
|
||||
anchor-lang = "0.31.1"
|
||||
@@ -27,7 +28,10 @@ solana-pubkey = "2.1"
|
||||
solana-signer = "2.1"
|
||||
solana-keypair = "2.1"
|
||||
solana-clock = "2.1"
|
||||
solana-account = "2.1"
|
||||
solana-transaction = "2.1"
|
||||
spl-token = "7.0.0"
|
||||
spl-associated-token-account = "6.0.0"
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
||||
|
||||
[lints.rust]
|
||||
|
||||
@@ -6,6 +6,11 @@ pub const GLOBAL_SEED: &[u8] = b"global";
|
||||
#[constant]
|
||||
pub const BET_SEED: &[u8] = b"bet";
|
||||
|
||||
/// Vault PDA seed: the token vault lives at PDA([GLOBAL_SEED, VAULT_SEED]).
|
||||
/// It cannot reuse GLOBAL_SEED alone — that PDA address is the `global` state account itself.
|
||||
#[constant]
|
||||
pub const VAULT_SEED: &[u8] = b"vault";
|
||||
|
||||
pub const SIDE_UP: u64 = 0;
|
||||
pub const SIDE_DOWN: u64 = 1;
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token::{Token, TokenAccount};
|
||||
|
||||
use crate::{
|
||||
constants::{
|
||||
BPS_DIVISOR, BET_SEED, GLOBAL_SEED, SIDE_DOWN, SIDE_UP, STATUS_HOUSE_WON,
|
||||
STATUS_PAYOUT_DONE, STATUS_OPEN,
|
||||
STATUS_PAYOUT_DONE, STATUS_OPEN, VAULT_SEED,
|
||||
},
|
||||
error::SmartUpdownError,
|
||||
events::BetClosed,
|
||||
@@ -27,9 +28,19 @@ pub struct CloseBet<'info> {
|
||||
)]
|
||||
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>,
|
||||
/// Program-owned token vault: PDA([GLOBAL_SEED, VAULT_SEED]); payouts are paid out from here.
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [GLOBAL_SEED, VAULT_SEED],
|
||||
bump
|
||||
)]
|
||||
pub vault_token: Account<'info, TokenAccount>,
|
||||
/// Bettor's SPL-token account; the payout is sent here.
|
||||
#[account(mut)]
|
||||
pub bettor_token: Account<'info, TokenAccount>,
|
||||
pub token_program: Program<'info, Token>,
|
||||
}
|
||||
|
||||
pub fn handler(ctx: &mut Context<CloseBet>, id: u64, exit_price: u64) -> Result<()> {
|
||||
@@ -69,17 +80,36 @@ let payout;
|
||||
.checked_div(BPS_DIVISOR as u128)
|
||||
.unwrap() as u64;
|
||||
require!(
|
||||
ctx.accounts.global.get_lamports() >= payout_amount,
|
||||
ctx.accounts.vault_token.amount >= payout_amount,
|
||||
SmartUpdownError::InsufficientVault
|
||||
);
|
||||
payout = payout_amount;
|
||||
ctx.accounts.global.sub_lamports(payout)?;
|
||||
ctx.accounts.bettor.add_lamports(payout)?;
|
||||
{
|
||||
// Pay the bettor: transfer the payout from the vault to the bettor's token account.
|
||||
// The vault is owned by the `global` PDA, so the CPI is signed with its seeds.
|
||||
let cpi_accounts = anchor_spl::token::Transfer {
|
||||
from: ctx.accounts.vault_token.to_account_info(),
|
||||
to: ctx.accounts.bettor_token.to_account_info(),
|
||||
authority: ctx.accounts.vault_token.to_account_info(),
|
||||
};
|
||||
let signer_seeds: &[&[u8]] = &[
|
||||
GLOBAL_SEED,
|
||||
VAULT_SEED,
|
||||
&[ctx.bumps.vault_token],
|
||||
];
|
||||
let vault_signer_seeds = [signer_seeds];
|
||||
let cpi_ctx = CpiContext::new_with_signer(
|
||||
ctx.accounts.token_program.to_account_info(),
|
||||
cpi_accounts,
|
||||
&vault_signer_seeds,
|
||||
);
|
||||
anchor_spl::token::transfer(cpi_ctx, payout)?;
|
||||
}
|
||||
ctx.accounts.bet.status = STATUS_PAYOUT_DONE;
|
||||
} else {
|
||||
payout = 0;
|
||||
ctx.accounts.bet.status = STATUS_HOUSE_WON;
|
||||
}
|
||||
payout = 0;
|
||||
ctx.accounts.bet.status = STATUS_HOUSE_WON;
|
||||
}
|
||||
|
||||
ctx.accounts.bet.exit_price = exit_price;
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token::{Token, TokenAccount};
|
||||
|
||||
use crate::{
|
||||
constants::{BET_SEED, GLOBAL_SEED, SIDE_DOWN, SIDE_UP, STATUS_OPEN},
|
||||
constants::{BET_SEED, GLOBAL_SEED, SIDE_DOWN, SIDE_UP, STATUS_OPEN, VAULT_SEED},
|
||||
error::SmartUpdownError,
|
||||
events::BetCreated,
|
||||
state::{Bet, Global},
|
||||
@@ -28,6 +29,17 @@ pub struct CreateBet<'info> {
|
||||
pub bet: Account<'info, Bet>,
|
||||
pub clock: Sysvar<'info, Clock>,
|
||||
pub system_program: Program<'info, System>,
|
||||
/// Bettor's SPL-token account (our "fanti" mint); bet amount is pulled from here.
|
||||
#[account(mut)]
|
||||
pub bettor_token: Account<'info, TokenAccount>,
|
||||
/// Program-owned token vault: PDA([GLOBAL_SEED, VAULT_SEED]); bet amounts are escrowed here.
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [GLOBAL_SEED, VAULT_SEED],
|
||||
bump
|
||||
)]
|
||||
pub vault_token: Account<'info, TokenAccount>,
|
||||
pub token_program: Program<'info, Token>,
|
||||
}
|
||||
|
||||
pub fn handler(
|
||||
@@ -72,15 +84,18 @@ pub fn handler(
|
||||
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(),
|
||||
// Escrow: move the bet amount (token units) from the bettor's account to the vault.
|
||||
// The SPL token program enforces that `bettor_token` is owned by `bettor` (authority).
|
||||
let cpi_accounts = anchor_spl::token::Transfer {
|
||||
from: ctx.accounts.bettor_token.to_account_info(),
|
||||
to: ctx.accounts.vault_token.to_account_info(),
|
||||
authority: ctx.accounts.bettor.to_account_info(),
|
||||
};
|
||||
let cpi_ctx = CpiContext::new(
|
||||
ctx.accounts.system_program.to_account_info(),
|
||||
ctx.accounts.token_program.to_account_info(),
|
||||
cpi_accounts,
|
||||
);
|
||||
anchor_lang::system_program::transfer(cpi_ctx, amount)?;
|
||||
anchor_spl::token::transfer(cpi_ctx, amount)?;
|
||||
}
|
||||
|
||||
emit!(BetCreated {
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
/// Global game state.
|
||||
///
|
||||
/// All money-related fields are denominated in **SPL-token base units**
|
||||
/// (our "fanti" token, decimals = 6), not in lamports:
|
||||
/// `min_amount`/`max_amount` bound a bet amount in token units.
|
||||
#[account]
|
||||
#[derive(InitSpace)]
|
||||
pub struct Global {
|
||||
@@ -13,6 +18,7 @@ pub struct Global {
|
||||
pub relayer: Pubkey,
|
||||
}
|
||||
|
||||
/// Open bet. `amount` is in token base units (decimals = 6), not lamports.
|
||||
#[account]
|
||||
#[derive(InitSpace)]
|
||||
pub struct Bet {
|
||||
|
||||
@@ -2,26 +2,43 @@ use {
|
||||
anchor_lang::{
|
||||
solana_program::{
|
||||
instruction::Instruction,
|
||||
program_option::COption,
|
||||
program_pack::Pack,
|
||||
pubkey::Pubkey,
|
||||
rent::Rent,
|
||||
system_instruction,
|
||||
},
|
||||
AccountDeserialize, InstructionData, ToAccountMetas,
|
||||
},
|
||||
smart_updown::state::{Bet, Global},
|
||||
smart_updown::{constants::VAULT_SEED, state::{Bet, Global}},
|
||||
solana_account::{Account as SolanaAccount},
|
||||
solana_clock::Clock,
|
||||
solana_keypair::Keypair,
|
||||
solana_program_test::{BanksClient, ProgramTest, ProgramTestContext},
|
||||
solana_signer::Signer,
|
||||
solana_transaction::Transaction,
|
||||
spl_associated_token_account,
|
||||
spl_token,
|
||||
spl_token::state::{Account as TokenData, AccountState, Mint},
|
||||
};
|
||||
|
||||
const CLOCK_ADDR: &str = "SysvarC1ock11111111111111111111111111111111";
|
||||
const SYSTEM_ADDR: &str = "11111111111111111111111111111111";
|
||||
const MIN_AMOUNT: u64 = 1_000_000_000; // 1 SOL
|
||||
const MIN_AMOUNT: u64 = 1_000_000_000; // minimum bet, token units
|
||||
const MULTIPLIER_BPS: u64 = 13_000; // 1.3x
|
||||
const EXPIRY_SECONDS: i64 = 15;
|
||||
const START_TIME: i64 = 1_700_000_000;
|
||||
|
||||
// SPL token (фантики) environment
|
||||
const TOKEN_DECIMALS: u8 = 6;
|
||||
const MINT_SIZE: u64 = 82; // classic SPL token mint account size
|
||||
const TOKEN_ACCOUNT_SIZE: u64 = 165; // classic SPL token account size
|
||||
const VAULT_TOKEN_SUPPLY: u64 = 100 * MIN_AMOUNT; // vault seed, covers many payouts
|
||||
const BETTOR_TOKEN_SUPPLY: u64 = 10 * MIN_AMOUNT; // per-bettor token supply
|
||||
/// Fixed фантики mint: mint + vault token accounts are pre-loaded into
|
||||
/// genesis because the vault is a PDA and cannot sign its own create_account.
|
||||
const MINT: Pubkey = Pubkey::new_from_array([3u8; 32]);
|
||||
|
||||
fn pid() -> Pubkey {
|
||||
smart_updown::id()
|
||||
}
|
||||
@@ -30,16 +47,73 @@ fn global_pda() -> Pubkey {
|
||||
Pubkey::find_program_address(&[b"global"], &pid()).0
|
||||
}
|
||||
|
||||
fn vault_pda() -> Pubkey {
|
||||
Pubkey::find_program_address(&[b"global", VAULT_SEED], &pid()).0
|
||||
}
|
||||
|
||||
fn bet_pda(id: u64) -> Pubkey {
|
||||
Pubkey::find_program_address(&[b"bet", &id.to_le_bytes()], &pid()).0
|
||||
}
|
||||
|
||||
/// Boot the program-test context with the фантики mint and the program's
|
||||
/// token vault (PDA([global, vault])) pre-loaded into genesis. The vault is a
|
||||
/// PDA owned by itself, so it can never sign a top-level create_account; the
|
||||
/// account data below matches what the token program would have created.
|
||||
async fn setup() -> (ProgramTestContext, Keypair) {
|
||||
let admin = Keypair::new();
|
||||
let mint_data = {
|
||||
let m = Mint {
|
||||
mint_authority: COption::Some(admin.pubkey()),
|
||||
supply: 0,
|
||||
decimals: TOKEN_DECIMALS,
|
||||
is_initialized: true,
|
||||
freeze_authority: COption::None,
|
||||
};
|
||||
let mut buf = [0u8; MINT_SIZE as usize];
|
||||
Pack::pack(m, &mut buf).expect("pack mint");
|
||||
buf
|
||||
};
|
||||
let vault_data = {
|
||||
let a = TokenData {
|
||||
mint: MINT,
|
||||
owner: vault_pda(),
|
||||
amount: VAULT_TOKEN_SUPPLY,
|
||||
delegate: COption::None,
|
||||
state: AccountState::Initialized,
|
||||
is_native: COption::None,
|
||||
delegated_amount: 0,
|
||||
close_authority: COption::None,
|
||||
};
|
||||
let mut buf = [0u8; TOKEN_ACCOUNT_SIZE as usize];
|
||||
Pack::pack(a, &mut buf).expect("pack vault token account");
|
||||
buf
|
||||
};
|
||||
|
||||
let mut pt = ProgramTest::default();
|
||||
pt.add_program("smart_updown", pid(), None);
|
||||
let rent = Rent::default();
|
||||
pt.add_account(
|
||||
MINT,
|
||||
SolanaAccount {
|
||||
lamports: rent.minimum_balance(MINT_SIZE as usize),
|
||||
data: mint_data.to_vec(),
|
||||
owner: spl_token::ID,
|
||||
executable: false,
|
||||
rent_epoch: 0,
|
||||
},
|
||||
);
|
||||
pt.add_account(
|
||||
vault_pda(),
|
||||
SolanaAccount {
|
||||
lamports: rent.minimum_balance(TOKEN_ACCOUNT_SIZE as usize),
|
||||
data: vault_data.to_vec(),
|
||||
owner: spl_token::ID,
|
||||
executable: false,
|
||||
rent_epoch: 0,
|
||||
},
|
||||
);
|
||||
// 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(),
|
||||
@@ -93,7 +167,14 @@ fn make_initialize_ix(admin: Pubkey) -> Instruction {
|
||||
)
|
||||
}
|
||||
|
||||
fn make_create_ix(id: u64, side: u64, amount: u64, entry_price: u64, bettor: Pubkey) -> Instruction {
|
||||
fn make_create_ix(
|
||||
id: u64,
|
||||
side: u64,
|
||||
amount: u64,
|
||||
entry_price: u64,
|
||||
bettor: Pubkey,
|
||||
bettor_ata: &Pubkey,
|
||||
) -> Instruction {
|
||||
Instruction::new_with_bytes(
|
||||
pid(),
|
||||
&smart_updown::instruction::CreateBet {
|
||||
@@ -109,12 +190,21 @@ fn make_create_ix(id: u64, side: u64, amount: u64, entry_price: u64, bettor: Pub
|
||||
bet: bet_pda(id),
|
||||
clock: CLOCK_ADDR.parse().unwrap(),
|
||||
system_program: SYSTEM_ADDR.parse().unwrap(),
|
||||
bettor_token: *bettor_ata,
|
||||
vault_token: vault_pda(),
|
||||
token_program: spl_token::ID,
|
||||
}
|
||||
.to_account_metas(None),
|
||||
)
|
||||
}
|
||||
|
||||
fn make_close_ix(id: u64, exit_price: u64, relayer: Pubkey, bettor: Pubkey) -> Instruction {
|
||||
fn make_close_ix(
|
||||
id: u64,
|
||||
exit_price: u64,
|
||||
relayer: Pubkey,
|
||||
bettor: Pubkey,
|
||||
bettor_ata: &Pubkey,
|
||||
) -> Instruction {
|
||||
Instruction::new_with_bytes(
|
||||
pid(),
|
||||
&smart_updown::instruction::CloseBet { id, exit_price }.data(),
|
||||
@@ -124,6 +214,9 @@ fn make_close_ix(id: u64, exit_price: u64, relayer: Pubkey, bettor: Pubkey) -> I
|
||||
global: global_pda(),
|
||||
bettor,
|
||||
clock: CLOCK_ADDR.parse().unwrap(),
|
||||
vault_token: vault_pda(),
|
||||
bettor_token: *bettor_ata,
|
||||
token_program: spl_token::ID,
|
||||
}
|
||||
.to_account_metas(None),
|
||||
)
|
||||
@@ -167,22 +260,90 @@ async fn airdrop_to(ctx: &mut ProgramTestContext, to: &Pubkey, lamports: u64) {
|
||||
ctx.banks_client.process_transaction(tx).await.unwrap();
|
||||
}
|
||||
|
||||
async fn fund_vault(ctx: &mut ProgramTestContext, lamports: u64) {
|
||||
airdrop_to(ctx, &global_pda(), lamports).await;
|
||||
/// Create the token mint, the program's token vault (PDA([global, vault]),
|
||||
/// owned by the global PDA), and seed the vault with tokens.
|
||||
// The фантики env (mint + seeded vault) is pre-loaded into genesis by setup();
|
||||
// a PDA vault can never sign its own top-level create_account, so this only
|
||||
// returns the fixed mint the genesis accounts were built around.
|
||||
async fn create_token_env(_ctx: &mut ProgramTestContext, _admin: &Keypair) -> Pubkey {
|
||||
MINT
|
||||
}
|
||||
|
||||
/// Mint tokens to a destination account (admin is the mint authority).
|
||||
async fn mint_to(
|
||||
ctx: &mut ProgramTestContext,
|
||||
admin: &Keypair,
|
||||
mint: &Pubkey,
|
||||
dest: &Pubkey,
|
||||
amount: u64,
|
||||
) {
|
||||
let ix = spl_token::instruction::mint_to(&spl_token::ID, mint, dest, &admin.pubkey(), &[], amount)
|
||||
.expect("mint_to");
|
||||
run_ix(ctx, admin, ix).await.expect("mint_to");
|
||||
}
|
||||
|
||||
/// New bettor: lamports account + associated token account + token supply.
|
||||
async fn create_bettor(
|
||||
ctx: &mut ProgramTestContext,
|
||||
admin: &Keypair,
|
||||
mint: &Pubkey,
|
||||
) -> (Keypair, Pubkey) {
|
||||
let bettor = Keypair::new();
|
||||
airdrop_to(ctx, &bettor.pubkey(), 100_000_000_000).await;
|
||||
let ata = spl_associated_token_account::get_associated_token_address(&bettor.pubkey(), mint);
|
||||
let ix = spl_associated_token_account::instruction::create_associated_token_account(
|
||||
&admin.pubkey(),
|
||||
&bettor.pubkey(),
|
||||
mint,
|
||||
&spl_token::ID,
|
||||
);
|
||||
run_ix(ctx, admin, ix).await.expect("create ata");
|
||||
mint_to(ctx, admin, mint, &ata, BETTOR_TOKEN_SUPPLY).await;
|
||||
(bettor, ata)
|
||||
}
|
||||
|
||||
/// Read the `amount` field of a classic SPL token account (offset 64).
|
||||
async fn read_token_balance(ctx: &mut ProgramTestContext, addr: &Pubkey) -> u64 {
|
||||
let data = read_account_data(&ctx.banks_client, addr).await;
|
||||
u64::from_le_bytes(data[64..72].try_into().expect("amount offset"))
|
||||
}
|
||||
|
||||
/// close_bet pays out by moving tokens out of the vault; the vault PDA signs
|
||||
/// that transfer CPI inside the program, so the transaction needs only the
|
||||
/// relayer's signature.
|
||||
async fn run_close_tx(
|
||||
ctx: &mut ProgramTestContext,
|
||||
signer: &Keypair,
|
||||
ix: &Instruction,
|
||||
) -> Result<(), String> {
|
||||
// The vault PDA signs the payout CPI inside the program (via signer_seeds);
|
||||
// the transaction itself needs only the relayer's signature.
|
||||
let tx = Transaction::new_signed_with_payer(
|
||||
std::slice::from_ref(ix),
|
||||
Some(&signer.pubkey()),
|
||||
&[signer],
|
||||
ctx.last_blockhash,
|
||||
);
|
||||
ctx.banks_client
|
||||
.process_transaction(tx)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
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;
|
||||
mint: &Pubkey,
|
||||
admin: &Keypair,
|
||||
) -> (Keypair, Pubkey, u64, u64) {
|
||||
let (bettor, ata) = create_bettor(ctx, admin, mint).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());
|
||||
let create_ix = make_create_ix(0, side, amount, entry_price, bettor.pubkey(), &ata);
|
||||
run_ix(ctx, &bettor, create_ix).await.expect("create_bet");
|
||||
(bettor, amount, entry_price)
|
||||
(bettor, ata, amount, entry_price)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -200,13 +361,20 @@ async fn initialize_creates_global() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_bet_escrows_sol() {
|
||||
async fn create_bet_escrows_tokens() {
|
||||
let (mut ctx, admin) = setup().await;
|
||||
initialize(&mut ctx, &admin).await;
|
||||
let (bettor, amount, entry_price) = create_up_bet(&mut ctx, 0).await;
|
||||
let mint = create_token_env(&mut ctx, &admin).await;
|
||||
let (bettor, ata, amount, entry_price) = create_up_bet(&mut ctx, 0, &mint, &admin).await;
|
||||
|
||||
let vault_balance = ctx.banks_client.get_balance(global_pda()).await.unwrap();
|
||||
assert!(vault_balance >= amount);
|
||||
assert_eq!(
|
||||
read_token_balance(&mut ctx, &vault_pda()).await,
|
||||
VAULT_TOKEN_SUPPLY + amount
|
||||
);
|
||||
assert_eq!(
|
||||
read_token_balance(&mut ctx, &ata).await,
|
||||
BETTOR_TOKEN_SUPPLY - 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);
|
||||
@@ -222,18 +390,27 @@ async fn create_bet_escrows_sol() {
|
||||
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;
|
||||
let mint = create_token_env(&mut ctx, &admin).await;
|
||||
let (bettor, ata, amount, entry_price) = create_up_bet(&mut ctx, 0, &mint, &admin).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 before = read_token_balance(&mut ctx, &ata).await;
|
||||
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");
|
||||
run_close_tx(
|
||||
&mut ctx,
|
||||
&admin,
|
||||
&make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey(), &ata),
|
||||
)
|
||||
.await
|
||||
.expect("close_bet");
|
||||
|
||||
assert_eq!(ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(), before + payout);
|
||||
assert_eq!(read_token_balance(&mut ctx, &ata).await, before + payout);
|
||||
assert_eq!(
|
||||
read_token_balance(&mut ctx, &vault_pda()).await,
|
||||
VAULT_TOKEN_SUPPLY + amount - 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);
|
||||
@@ -243,16 +420,22 @@ async fn close_bet_user_wins_up() {
|
||||
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;
|
||||
let mint = create_token_env(&mut ctx, &admin).await;
|
||||
let (bettor, ata, _amount, entry_price) = create_up_bet(&mut ctx, 1, &mint, &admin).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 before = read_token_balance(&mut ctx, &ata).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");
|
||||
run_close_tx(
|
||||
&mut ctx,
|
||||
&admin,
|
||||
&make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey(), &ata),
|
||||
)
|
||||
.await
|
||||
.expect("close_bet");
|
||||
|
||||
assert_eq!(ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(), before);
|
||||
assert_eq!(read_token_balance(&mut ctx, &ata).await, before);
|
||||
assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 2);
|
||||
}
|
||||
|
||||
@@ -260,23 +443,23 @@ async fn close_bet_house_wins_down() {
|
||||
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;
|
||||
let mint = create_token_env(&mut ctx, &admin).await;
|
||||
let (bettor, ata, amount, entry_price) = create_up_bet(&mut ctx, 0, &mint, &admin).await;
|
||||
|
||||
set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await;
|
||||
let before = ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap();
|
||||
let before = read_token_balance(&mut ctx, &ata).await;
|
||||
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");
|
||||
let equal_ix = make_close_ix(0, entry_price, admin.pubkey(), bettor.pubkey(), &ata);
|
||||
run_close_tx(&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);
|
||||
assert_eq!(read_token_balance(&mut ctx, &ata).await, 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");
|
||||
let second_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey(), &ata);
|
||||
run_close_tx(&mut ctx, &admin, &second_ix).await.expect("second close");
|
||||
assert_eq!(
|
||||
ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(),
|
||||
read_token_balance(&mut ctx, &ata).await,
|
||||
before + payout
|
||||
);
|
||||
assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 1);
|
||||
@@ -286,37 +469,40 @@ async fn close_bet_equal_price_stays_open() {
|
||||
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;
|
||||
let mint = create_token_env(&mut ctx, &admin).await;
|
||||
let (bettor, ata, _amount, entry_price) = create_up_bet(&mut ctx, 0, &mint, &admin).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(
|
||||
run_close_tx(
|
||||
&mut ctx,
|
||||
&admin,
|
||||
make_close_ix(0, win_price + 1, admin.pubkey(), bettor.pubkey()),
|
||||
&make_close_ix(0, win_price, admin.pubkey(), bettor.pubkey(), &ata),
|
||||
)
|
||||
.await
|
||||
.expect("first close");
|
||||
|
||||
let res = run_close_tx(
|
||||
&mut ctx,
|
||||
&admin,
|
||||
&make_close_ix(0, win_price + 1, admin.pubkey(), bettor.pubkey(), &ata),
|
||||
)
|
||||
.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 mint = create_token_env(&mut ctx, &admin).await;
|
||||
let (bettor, ata, _amount, entry_price) = create_up_bet(&mut ctx, 0, &mint, &admin).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;
|
||||
let close_ix = make_close_ix(0, entry_price + 500, admin.pubkey(), bettor.pubkey(), &ata);
|
||||
let res = run_close_tx(&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);
|
||||
}
|
||||
@@ -325,13 +511,14 @@ async fn close_bet_before_expiry_rejected() {
|
||||
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 mint = create_token_env(&mut ctx, &admin).await;
|
||||
let (bettor, ata, _amount, entry_price) = create_up_bet(&mut ctx, 0, &mint, &admin).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;
|
||||
let close_ix = make_close_ix(0, entry_price + 500, attacker.pubkey(), bettor.pubkey(), &ata);
|
||||
let res = run_close_tx(&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);
|
||||
}
|
||||
@@ -340,32 +527,34 @@ async fn close_bet_wrong_relayer_rejected() {
|
||||
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;
|
||||
let mint = create_token_env(&mut ctx, &admin).await;
|
||||
let (bettor, ata, _amount, entry_price) = create_up_bet(&mut ctx, 0, &mint, &admin).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(
|
||||
let close_ix = make_close_ix(0, entry_price + 500, admin.pubkey(), bettor.pubkey(), &ata);
|
||||
run_close_tx(&mut ctx, &admin, &close_ix).await.expect("first close");
|
||||
let before = read_token_balance(&mut ctx, &ata).await;
|
||||
let res = run_close_tx(
|
||||
&mut ctx,
|
||||
&admin,
|
||||
make_close_ix(0, entry_price + 501, admin.pubkey(), bettor.pubkey()),
|
||||
&make_close_ix(0, entry_price + 501, admin.pubkey(), bettor.pubkey(), &ata),
|
||||
)
|
||||
.await;
|
||||
assert!(res.is_err(), "expected replay close to be rejected");
|
||||
assert_eq!(ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(), before);
|
||||
assert_eq!(read_token_balance(&mut ctx, &ata).await, 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;
|
||||
let mint = create_token_env(&mut ctx, &admin).await;
|
||||
let (bettor, ata) = create_bettor(&mut ctx, &admin, &mint).await;
|
||||
set_clock(&mut ctx, START_TIME).await;
|
||||
|
||||
let ix = make_create_ix(0, 0, 1, 100_000_000u64, bettor.pubkey());
|
||||
let vault_before = read_token_balance(&mut ctx, &vault_pda()).await;
|
||||
let ix = make_create_ix(0, 0, 1, 100_000_000u64, bettor.pubkey(), &ata);
|
||||
let res = run_ix(&mut ctx, &bettor, ix).await;
|
||||
assert!(res.is_err(), "expected below-min bet to be rejected");
|
||||
assert_eq!(read_token_balance(&mut ctx, &vault_pda()).await, vault_before);
|
||||
}
|
||||
+139
-18
@@ -10,7 +10,14 @@
|
||||
{
|
||||
"name": "close_bet",
|
||||
"discriminator": [
|
||||
2
|
||||
185,
|
||||
206,
|
||||
13,
|
||||
184,
|
||||
176,
|
||||
108,
|
||||
140,
|
||||
107
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
@@ -57,15 +64,54 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bettor",
|
||||
"docs": [
|
||||
"CHECK: recipient must be the bet's bettor and is validated in the handler."
|
||||
],
|
||||
"writable": true
|
||||
"name": "bettor"
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"address": "SysvarC1ock11111111111111111111111111111111"
|
||||
},
|
||||
{
|
||||
"name": "vault_token",
|
||||
"docs": [
|
||||
"Program-owned token vault: PDA([GLOBAL_SEED, VAULT_SEED]); payouts are paid out from here."
|
||||
],
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
103,
|
||||
108,
|
||||
111,
|
||||
98,
|
||||
97,
|
||||
108
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
118,
|
||||
97,
|
||||
117,
|
||||
108,
|
||||
116
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bettor_token",
|
||||
"docs": [
|
||||
"Bettor's SPL-token account; the payout is sent here."
|
||||
],
|
||||
"writable": true
|
||||
},
|
||||
{
|
||||
"name": "token_program",
|
||||
"address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
@@ -82,7 +128,14 @@
|
||||
{
|
||||
"name": "create_bet",
|
||||
"discriminator": [
|
||||
1
|
||||
197,
|
||||
42,
|
||||
153,
|
||||
2,
|
||||
59,
|
||||
63,
|
||||
143,
|
||||
246
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
@@ -136,6 +189,49 @@
|
||||
{
|
||||
"name": "system_program",
|
||||
"address": "11111111111111111111111111111111"
|
||||
},
|
||||
{
|
||||
"name": "bettor_token",
|
||||
"docs": [
|
||||
"Bettor's SPL-token account (our \"fanti\" mint); bet amount is pulled from here."
|
||||
],
|
||||
"writable": true
|
||||
},
|
||||
{
|
||||
"name": "vault_token",
|
||||
"docs": [
|
||||
"Program-owned token vault: PDA([GLOBAL_SEED, VAULT_SEED]); bet amounts are escrowed here."
|
||||
],
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
103,
|
||||
108,
|
||||
111,
|
||||
98,
|
||||
97,
|
||||
108
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
118,
|
||||
97,
|
||||
117,
|
||||
108,
|
||||
116
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "token_program",
|
||||
"address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
@@ -160,7 +256,14 @@
|
||||
{
|
||||
"name": "initialize",
|
||||
"discriminator": [
|
||||
0
|
||||
175,
|
||||
175,
|
||||
109,
|
||||
31,
|
||||
13,
|
||||
152,
|
||||
155,
|
||||
237
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
@@ -214,7 +317,14 @@
|
||||
{
|
||||
"name": "update",
|
||||
"discriminator": [
|
||||
3
|
||||
219,
|
||||
200,
|
||||
88,
|
||||
176,
|
||||
158,
|
||||
63,
|
||||
253,
|
||||
127
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
@@ -380,10 +490,9 @@
|
||||
"types": [
|
||||
{
|
||||
"name": "Bet",
|
||||
"serialization": "bytemuck",
|
||||
"repr": {
|
||||
"kind": "c"
|
||||
},
|
||||
"docs": [
|
||||
"Open bet. `amount` is in token base units (decimals = 6), not lamports."
|
||||
],
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
@@ -488,10 +597,13 @@
|
||||
},
|
||||
{
|
||||
"name": "Global",
|
||||
"serialization": "bytemuck",
|
||||
"repr": {
|
||||
"kind": "c"
|
||||
},
|
||||
"docs": [
|
||||
"Global game state.",
|
||||
"",
|
||||
"All money-related fields are denominated in **SPL-token base units**",
|
||||
"(our \"fanti\" token, decimals = 6), not in lamports:",
|
||||
"`min_amount`/`max_amount` bound a bet amount in token units."
|
||||
],
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
@@ -541,6 +653,15 @@
|
||||
"name": "GLOBAL_SEED",
|
||||
"type": "bytes",
|
||||
"value": "[103, 108, 111, 98, 97, 108]"
|
||||
},
|
||||
{
|
||||
"name": "VAULT_SEED",
|
||||
"docs": [
|
||||
"Vault PDA seed: the token vault lives at PDA([GLOBAL_SEED, VAULT_SEED]).",
|
||||
"It cannot reuse GLOBAL_SEED alone — that PDA address is the `global` state account itself."
|
||||
],
|
||||
"type": "bytes",
|
||||
"value": "[118, 97, 117, 108, 116]"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user