Исправить чтение state в shine_payments и описать e2e тест
This commit is contained in:
parent
89d06d317b
commit
d25c19cdaa
@ -0,0 +1,185 @@
|
||||
# Shine Payments: e2e после переписи без Anchor и добавления Q3
|
||||
|
||||
## Краткое описание
|
||||
|
||||
Нужно вручную и через вспомогательные CLI-проверки подтвердить, что программа `shine_payments` после:
|
||||
|
||||
- переписи на чистый `solana_program`;
|
||||
- отказа от `programs/common`;
|
||||
- добавления очереди `Q3`;
|
||||
- обновления HTML UI;
|
||||
|
||||
корректно работает на devnet с новым `program id`.
|
||||
|
||||
Отличие от финального боевого сценария:
|
||||
|
||||
- вместо DAO-механики используется обычный кошелёк `FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P`, которому даны права DAO на изменение коэффициента и выдачу лимитов менеджеру.
|
||||
|
||||
## Что именно проверять
|
||||
|
||||
### 1. Подготовка окружения
|
||||
|
||||
Проверить и зафиксировать:
|
||||
|
||||
- новый keypair программы `shine_payments`;
|
||||
- новый `program id`;
|
||||
- обновление `program id` в HTML UI и связанных настройках;
|
||||
- наличие deploy authority, которой можно закрыть старый buffer/programdata, если это технически доступно;
|
||||
- адреса тестовых кошельков:
|
||||
- DAO/базовый кошелёк;
|
||||
- менеджер;
|
||||
- покупатель 1;
|
||||
- покупатель 2;
|
||||
- получатели выплат.
|
||||
|
||||
### 2. Очистка/смена старой программы
|
||||
|
||||
Проверить один из сценариев:
|
||||
|
||||
- если возможно, закрыть старый `program buffer/programdata` текущими ключами;
|
||||
- если закрытие невозможно или нецелесообразно, зафиксировать это и продолжить с новым `program id`.
|
||||
|
||||
Отдельно проверить, что старые PDA предыдущей версии не используются новой программой.
|
||||
|
||||
### 3. Деплой и init новой программы
|
||||
|
||||
Проверить:
|
||||
|
||||
- `cargo build-sbf` проходит;
|
||||
- новая программа деплоится на devnet;
|
||||
- `init` выполняется один раз на пустых PDA;
|
||||
- после `init` читаются:
|
||||
- `config`;
|
||||
- `coef_limit`;
|
||||
- `queues`;
|
||||
- `inflow_vault`.
|
||||
|
||||
Сразу после `init` запросить состояние очередей и зафиксировать, что:
|
||||
|
||||
- `Q1`, `Q2`, `Q3` пустые;
|
||||
- `tickets_total = 0`;
|
||||
- `tickets_paid = 0`;
|
||||
- все суммы равны `0`.
|
||||
|
||||
### 4. Проверка покупки билета
|
||||
|
||||
На минимальных суммах проверить:
|
||||
|
||||
1. покупку через `buy_ticket_usd`;
|
||||
2. покупку через `buy_ticket_sol`;
|
||||
3. при необходимости ещё один вызов базового `buy_ticket`.
|
||||
|
||||
После каждой покупки:
|
||||
|
||||
- запросить состояние `Q1`;
|
||||
- убедиться, что создался следующий ticket;
|
||||
- проверить рост:
|
||||
- `q1_tickets_total`;
|
||||
- `q1_sum_total_usd_cents`;
|
||||
- убедиться, что деньги покупки ушли в `dao_wallet`, а не в `inflow_vault`.
|
||||
|
||||
### 5. Проверка DAO-управления
|
||||
|
||||
Проверить:
|
||||
|
||||
1. изменение коэффициента через `update_coef_limit`;
|
||||
2. повторный запрос `coef_limit` и подтверждение нового значения;
|
||||
3. выдачу менеджеру прав через `grant_manager_limits`:
|
||||
- отдельно под `Q1`;
|
||||
- отдельно под `Q2`;
|
||||
- отдельно под `Q3`.
|
||||
|
||||
После выдачи лимитов:
|
||||
|
||||
- считать `manager_allowance_pda`;
|
||||
- убедиться, что лимиты записаны отдельно по трём очередям.
|
||||
|
||||
### 6. Проверка manager_add_ticket
|
||||
|
||||
На минимальных суммах создать менеджерские тикеты:
|
||||
|
||||
1. один ticket в `Q1`;
|
||||
2. один ticket в `Q2`;
|
||||
3. один ticket в `Q3`.
|
||||
|
||||
После каждого добавления:
|
||||
|
||||
- запросить состояние очередей;
|
||||
- проверить рост счётчиков и сумм именно у нужной очереди;
|
||||
- проверить уменьшение соответствующего manager allowance.
|
||||
|
||||
### 7. Проверка приоритета очередей
|
||||
|
||||
Подтвердить очередность `step_payout`:
|
||||
|
||||
1. сначала выплачивается `Q1`;
|
||||
2. затем `Q2`;
|
||||
3. затем `Q3`.
|
||||
|
||||
Для этого:
|
||||
|
||||
- между шагами регулярно читать `queues`;
|
||||
- фиксировать, какой именно ticket был следующим к выплате;
|
||||
- убедиться, что при наличии pending в `Q1` программа не уходит в `Q2` или `Q3`.
|
||||
|
||||
### 8. Проверка частичных выплат
|
||||
|
||||
Перед выплатами пополнять `inflow_vault` только минимально достаточными суммами.
|
||||
|
||||
Нужно проверить:
|
||||
|
||||
1. частичную серию выплат, когда часть тикетов ещё остаётся pending;
|
||||
2. дополнительную покупку билета в промежутке между выплатами;
|
||||
3. повторную проверку приоритета после появления нового билета в `Q1`.
|
||||
|
||||
После каждого `step_payout`:
|
||||
|
||||
- запрашивать состояние очередей;
|
||||
- проверять:
|
||||
- рост `tickets_paid`;
|
||||
- рост `sum_paid_usd_cents`;
|
||||
- `is_paid = true` у погашенного ticket;
|
||||
- правильный DAO multiplier:
|
||||
- `Q1 -> 1x`;
|
||||
- `Q2 -> 2x`;
|
||||
- `Q3 -> 3x`.
|
||||
|
||||
### 9. Проверка финального добора
|
||||
|
||||
После частичных выплат:
|
||||
|
||||
- купить ещё один билет;
|
||||
- допополнить `inflow_vault`;
|
||||
- выполнить оставшиеся `step_payout` до полного погашения всех трёх очередей.
|
||||
|
||||
В конце:
|
||||
|
||||
- все pending ticket должны отсутствовать;
|
||||
- все суммы paid должны совпасть с total по каждой очереди;
|
||||
- если вызвать `step_payout` на пустых очередях, доступный остаток `inflow_vault` должен уйти в `dao_wallet`.
|
||||
|
||||
### 10. Финальный возврат лампортов
|
||||
|
||||
После завершения теста вернуть все доступные остатки, которые можно вернуть текущими полномочиями, на базовый кошелёк:
|
||||
|
||||
- `FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P`
|
||||
|
||||
Отдельно зафиксировать:
|
||||
|
||||
- что именно удалось вернуть;
|
||||
- что именно нельзя вернуть без специальной инструкции закрытия или без deploy authority.
|
||||
|
||||
## Ожидаемый результат
|
||||
|
||||
- `buy_ticket_usd` и `buy_ticket_sol` создают ticket без ошибок чтения state;
|
||||
- `Q3` работает наравне с `Q2`, но с третьим приоритетом;
|
||||
- DAO может менять коэффициент и выдавать лимиты;
|
||||
- менеджер может создавать билеты во все три очереди;
|
||||
- `step_payout` соблюдает порядок `Q1 -> Q2 -> Q3`;
|
||||
- DAO-множитель на выплатах равен `1x/2x/3x` для `Q1/Q2/Q3`;
|
||||
- HTML UI и on-chain программа используют один и тот же актуальный `program id`;
|
||||
- остатки средств после теста по максимуму возвращены на базовый DAO-кошелёк.
|
||||
|
||||
## Статус
|
||||
|
||||
- `pending`
|
||||
@ -250,11 +250,16 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
|
||||
trait StateCodec: Sized {
|
||||
fn encoded_len() -> usize;
|
||||
fn encode(&self) -> Vec<u8>;
|
||||
fn decode(data: &[u8]) -> Result<Self, ProgramError>;
|
||||
}
|
||||
|
||||
impl StateCodec for ConfigState {
|
||||
fn encoded_len() -> usize {
|
||||
1 + 32 + 32
|
||||
}
|
||||
|
||||
fn encode(&self) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(65);
|
||||
out.push(self.version);
|
||||
@ -274,6 +279,10 @@ impl StateCodec for ConfigState {
|
||||
}
|
||||
|
||||
impl StateCodec for CoefLimitState {
|
||||
fn encoded_len() -> usize {
|
||||
1 + 8 + 8 + 8
|
||||
}
|
||||
|
||||
fn encode(&self) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(25);
|
||||
out.push(self.version);
|
||||
@ -295,6 +304,10 @@ impl StateCodec for CoefLimitState {
|
||||
}
|
||||
|
||||
impl StateCodec for QueuesState {
|
||||
fn encoded_len() -> usize {
|
||||
1 + 12 * 8
|
||||
}
|
||||
|
||||
fn encode(&self) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(97);
|
||||
out.push(self.version);
|
||||
@ -339,6 +352,10 @@ impl StateCodec for QueuesState {
|
||||
}
|
||||
|
||||
impl StateCodec for TicketState {
|
||||
fn encoded_len() -> usize {
|
||||
1 + 1 + 8 + 1 + 32 + 8 + 8
|
||||
}
|
||||
|
||||
fn encode(&self) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(59);
|
||||
out.push(self.version);
|
||||
@ -370,6 +387,10 @@ impl StateCodec for TicketState {
|
||||
}
|
||||
|
||||
impl StateCodec for ManagerAllowanceState {
|
||||
fn encoded_len() -> usize {
|
||||
1 + 32 + 8 + 8 + 8
|
||||
}
|
||||
|
||||
fn encode(&self) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(57);
|
||||
out.push(self.version);
|
||||
@ -393,6 +414,10 @@ impl StateCodec for ManagerAllowanceState {
|
||||
}
|
||||
|
||||
impl StateCodec for VaultState {
|
||||
fn encoded_len() -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn encode(&self) -> Vec<u8> {
|
||||
vec![self.version]
|
||||
}
|
||||
@ -1162,9 +1187,9 @@ fn read_state<T: StateCodec>(pda: &AccountInfo) -> Result<T, ProgramError> {
|
||||
require!(!is_uninitialized_account(pda), PaymentsError::EmptyState);
|
||||
require_keys_eq!(*pda.owner, id(), PaymentsError::InvalidPdaAddress);
|
||||
let data = pda.try_borrow_data()?;
|
||||
let used_len = data.iter().rposition(|b| *b != 0).map(|idx| idx + 1).unwrap_or(0);
|
||||
require!(used_len > 0, PaymentsError::EmptyState);
|
||||
T::decode(&data[..used_len])
|
||||
let encoded_len = T::encoded_len();
|
||||
require!(data.len() >= encoded_len, PaymentsError::InvalidAccountData);
|
||||
T::decode(&data[..encoded_len])
|
||||
}
|
||||
|
||||
fn is_uninitialized_account(account: &AccountInfo) -> bool {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user