Удалить устаревший проект solana-shine-client-lib и очистить документацию

This commit is contained in:
AidarKC 2026-05-16 17:31:44 +03:00
parent 890e10de9f
commit deef20c517
84 changed files with 3 additions and 4960 deletions

View File

@ -4,9 +4,9 @@
В проекте есть спецификация пользовательской PDA-записи: В проекте есть спецификация пользовательской PDA-записи:
- `doc/SHINE_USER_PDA_V1.md` - актуальные документы в `doc/`.
Если меняется формат записи, сериализация, правила подписи, `prev_hash`, экономика лимитов или связанные ограничения create/update, этот документ нужно обновлять в том же изменении. Если меняется формат записи, сериализация, правила подписи, `prev_hash`, экономика лимитов или связанные ограничения create/update, соответствующую документацию в `doc/` нужно обновлять в том же изменении.
## Language Rule ## Language Rule
@ -35,6 +35,7 @@ Push выполнять через `http.extraHeader` (Authorization) без в
## Rule: Commit Messages ## Rule: Commit Messages
Текст commit message писать на русском языке. Текст commit message писать на русском языке.
Это обязательное правило для всех новых коммитов в этом репозитории.
## Rule: UI Deploy ## Rule: UI Deploy

View File

@ -1,31 +0,0 @@
Деплой в devnet (по умолчанию у тебя уже devnet):
обе программы сразу:
anchor deploy
или по одной:
anchor deploy --program-name shine_users
anchor deploy --program-name shine_payments
Проверка деплоя
anchor keys list
solana program show 5dFcWDNp42Xn9Vv4oDMJzM4obBJ8hvDuAtPX54fT5L3t // <ID_из_shine_users-keypair.json>
solana program show GcGFR47xF7o7ztXzN4MFThmxzHn4z6VmpmELgNk8smCm // <ID_из_shine_payments-keypair.json>
Апгрейд в будущем
После изменений кода:
anchor build
anchor upgrade --program-name shine_users
anchor upgrade --program-name shine_payments
посмотреть адрес кошелька
solana address -k /home/ai/.config/solana/id.jsonanchor build --program-name shine_payments
anchor deploy --program-name shine_payments
solana program show 92sgkgx7KHpbhQu81mNGHaKa7skJB7esArVdPM7paDSW --url http://127.0.0.1:8899
solana address -k /home/ai/work/SOLANA/shine/target/deploy/shine_payments-keypair.json

View File

@ -1,39 +0,0 @@
# подключаться надо к
JSON RPC URL: http://127.0.0.1:8899
# Запустить саму ноду
solana-test-validator
# Удалить процесс ноды что бы запустить заново
kill -9 $(pgrep -f "solana-test-validator")
или
ps aux | grep solana-test-validator
а потом
kill -9 1052345
# Убивает и логи и всю базу локальной ноды
rm -rf test-ledger
# Удалить все данные с ноды
solana-test-validator --reset
# Что бы запустить просмотр логов ноды
solana logs
# Запустить контракт
anchor deploy
# Cкомпилировать и задеплоить новую версию
anchor build # Скомпилировать контракт и сгенерировать IDL
anchor deploy # Задеплоить контракт в сеть (указанную в Anchor.toml)
Если ты хочешь сразу убедиться, куда он деплоится — проверь Anchor.toml.
[provider]
cluster = "https://api.testnet.solana.com" # или "localnet"
wallet = "~/.config/solana/id.json"
# Создать новый проект
anchor init имя_проекта

View File

@ -1,65 +0,0 @@
https://api.devnet.solana.com
проверить настройки
solana config get
настроить
solana config set --url https://api.devnet.solana.com
или
solana config set --url http://127.0.0.1:8899
потом
solana airdrop 2 --keypair /home/ai/.config/solana/id.json
и
solana balance --keypair /home/ai/.config/solana/id.json
anchor deploy \
--provider.cluster https://api.devnet.solana.com \
--provider.wallet /home/ai/.config/solana/id.json
Шаг 1. Создай новый ключ для новой программы
solana-keygen new --outfile target/deploy/user_registry-testnet-keypair.json
Шаг 2. Укажи новый ID в declare_id!:
declare_id!("НОВЫЙ_PUBKEY_ОТСЮДА"); // получен из предыдущей команды
Чтобы узнать pubkey:
solana address -k target/deploy/user_registry-testnet-keypair.json
Шаг 3. Обнови Anchor.toml:
[programs.testnet]
user_registry = "НОВЫЙ_PUBKEY"
[provider]
cluster = "https://api.testnet.solana.com"
wallet = "~/.config/solana/id.json"
Шаг 4. Компиляция и деплой:
anchor build
anchor deploy --provider.cluster testnet
Шаг 5. Проверка:
solana program show НОВЫЙ_PUBKEY --url https://api.testnet.solana.com

View File

@ -1 +0,0 @@
Просто разные заметки для себя

View File

@ -1,50 +0,0 @@
# SHINE Payments v2: тестовый план (Devnet)
## Цель
Проверить:
1. создание PDA и инициализацию;
2. покупку обычных билетов (очередь 1);
3. выдачу DAO-лимитов менеджеру;
4. создание менеджерских билетов в очередь 1 и 2;
5. корректный приоритет выплат (очередь 1 > очередь 2);
6. перевод средств в DAO и награды вызывающему шаг выплат.
## Сценарий А: один кошелек
1. Открыть `admin_tools`, выполнить `init`.
2. В `buy_ticket` купить несколько билетов в очередь 1.
3. В `dao_tools` (тем же кошельком, если он DAO) выдать лимиты менеджеру.
4. В `manager_tools` создать билеты в очередь 1 и 2.
5. Пополнить inflow-вольт вручную.
6. В `track_ticket` запускать шаг выплат и смотреть, что:
- сначала платится очередь 1;
- после исчерпания очереди 1 платится очередь 2;
- если в процессе снова появился билет в очереди 1, приоритет возвращается к ней.
## Сценарий Б: разные кошельки
1. Кошелек DAO: выдает лимиты менеджерам.
2. Кошелек менеджера: добавляет тикеты через `manager_add_ticket`.
3. Кошельки покупателей: покупают обычные билеты.
4. Любой кошелек: вызывает `step_payout`.
## Проверка результата
1. У получателей тикетов растут балансы.
2. В DAO поступает симметричная сумма `X` при каждом шаге выплаты.
3. Вызывающий шаг выплат получает фиксированную награду.
4. Агрегаты очередей (`total/paid/sum_total/sum_paid`) изменяются ожидаемо.
## Возврат средств после теста
1. Перевести остатки SOL с тестовых кошельков на исходный кошелек.
2. При необходимости закрыть неиспользуемые program-аккаунты и вернуть ренту.
3. Проверить, что ключевые кошельки и devnet-параметры не содержат лишних средств/прав после завершения теста.
## Ограничения текущего этапа
1. DAO пока заменен обычным кошельком.
2. Финальный governance (голосование DAO) не подключен.
3. Расчеты пока в SOL/lamports, переход на USDT по курсу запланирован.

View File

@ -1,121 +0,0 @@
# SHINE Payments v2
## Назначение
`shine_payments` v2 — контракт очереди выплат с двумя очередями:
1. обычная покупка билета (очередь 1);
2. менеджерское добавление билетов (очередь 1 и очередь 2 по лимитам от DAO);
3. пошаговые выплаты из inflow-вольта с приоритетом очереди 1.
Сейчас тестовый этап в Devnet: расчеты и хранение в USD-центах (1 USD = 100), при этом реальные on-chain переводы выполняются в SOL по курсу Pyth SOL/USD.
## PDA
1. `config_pda` (`shine_payments_v3_config`)
- `dao_wallet`
- `manager_wallet` (сервисный параметр, для будущих сценариев)
- `inflow_vault`
2. `coef_limit_pda` (`shine_payments_v3_coef_limit`)
- `coef_ppm` (fixed-point, scale = 1_000_000)
- `limit_usd_cents` (лимит суммарной исторической суммы очереди 1 для обычной покупки)
- `call_reward_lamports` (награда за шаг выплат, максимум 0.01 SOL)
3. `queues_pda` (`shine_payments_v3_queues`)
- очередь 1: `tickets_total`, `tickets_paid`, `sum_total`, `sum_paid`
- очередь 2: `tickets_total`, `tickets_paid`, `sum_total`, `sum_paid`
4. `inflow_vault_pda` (`shine_payments_v3_inflow_vault`)
- входящий PDA-вольт программы для выплат.
5. `ticket_pda`
- очередь 1: `shine_payments_v3_q1_ticket + index_le_u64`
- очередь 2: `shine_payments_v3_q2_ticket + index_le_u64`
- поля тикета:
- `queue_id`
- `index`
- `is_paid`
- `recipient_wallet`
- `payout_usd_cents`
- `debt_before_usd_cents`
6. `manager_allowance_pda` (`shine_p_v3_manager_allow + manager_pubkey`)
- `manager_wallet`
- `q1_available_usd_cents`
- `q2_available_usd_cents`
## Методы
1. `init`
- вызывается один раз (кто угодно);
- создает `config_pda`, `coef_limit_pda`, `queues_pda`, `inflow_vault_pda`.
2. `update_coef_limit` (только `dao_wallet` из config)
- меняет коэффициент, лимит покупки в очередь 1 и награду шага выплат;
- ограничение награды: не более `0.01 SOL`.
3. `grant_manager_limits` (только `dao_wallet` из config)
- DAO выдает/добавляет лимиты менеджеру:
- `add_q1_usd_cents`
- `add_q2_usd_cents`
- если PDA менеджера нет — создается;
- если есть — лимиты увеличиваются.
4. `buy_ticket`
- обратная совместимость: покупка с входом в lamports, но запись тикета в USD-центах;
- использует Pyth SOL/USD и stale-check (не старше 120 секунд).
5. `buy_ticket_usd`
- покупка билета в очередь 1 с суммой в USD-центах;
- сумма SOL к оплате считается в контракте по Pyth;
- есть `max_pay_lamports` для slippage-защиты.
6. `buy_ticket_sol`
- покупка билета в очередь 1 с суммой в lamports;
- USD-объём покупки считается в контракте по Pyth;
- есть `min_expected_usd_cents` для slippage-защиты.
7. `manager_add_ticket`
- менеджер добавляет тикет в очередь 1 или 2;
- без денежного перевода;
- списывает лимит менеджера по выбранной очереди (в USD-центах).
8. `step_payout`
- выбирает очередь по приоритету:
1. сначала очередь 1;
2. если в 1-й нет ожидания — очередь 2.
- перед выплатой получает текущий SOL/USD из Pyth и проверяет stale.
- шаг выплаты:
- очередь 1: `X` получателю тикета + `X` в DAO + `reward` вызывающему;
- очередь 2: `X` получателю тикета + `2X` в DAO + `reward` вызывающему.
- если обе очереди пусты/выплачены:
- переводит весь доступный остаток inflow-вольта в DAO (без reward).
9. `change_ticket_recipient`
- текущий `recipient_wallet` тикета может сменить получателя на другой кошелек;
- тикет должен быть невыплаченным;
- смена запрещена, если этот тикет является следующим к выплате в текущем шаге
(с учетом приоритета очереди 1 над очередью 2).
10. Экономика покупки
- сумма покупки идет в DAO;
- тикет получает выплату `purchase_usd_cents * coef_ppm / 1_000_000`;
- проверка лимита выполняется по `q1_sum_total_usd_cents` (исторически накопленная сумма, без вычета уже выплаченного).
## Стартовые настройки
См. `programs/shine_payments/src/settings.rs`:
- `START_COEF_PPM = 5_000_000` (коэффициент 5.0)
- `START_LIMIT_USD_CENTS = 10_000 USD`
- `START_CALL_REWARD_LAMPORTS = 0.008 SOL`
- `ORACLE_MAX_AGE_SECS = 120`
- `PYTH_SOL_USD_FEED_ID`
- `PYTH_SOL_USD_ACCOUNT`
- `DAO_WALLET`
- `MANAGER_WALLET`
## Тестовый режим
Пока нет финального production-потока пополнения inflow из регистрации/экосистемы, inflow-вольт пополняется вручную в Devnet, после чего выполняются шаги выплат.

View File

@ -1,146 +0,0 @@
# SHiNE User PDA v1.0
## 1. Назначение
`SHiNE User PDA v1.0` — бинарный формат пользовательской записи в PDA Solana.
Хранит:
- логин;
- ключи пользователя;
- номер внутренней сети;
- лимит (баланс);
- серверные данные (опционально);
- список серверов подключения;
- данные восстановления;
- связь с предыдущей версией через `prev_hash`;
- подпись владельца `root_key`.
Размер PDA фиксированный:
`768 bytes`
Полезная длина записи указывается в `record_len`.
## 2. Общие правила кодирования
- Числа: `little-endian`.
- Строки: `UTF-8` с префиксом длины `u8`.
- Padding до 768 байт: нули `0x00`.
- Padding не входит в `record_len`.
## 3. Структура записи
- `magic` (5): `"SHiNE"`
- `format_major` (1): `1`
- `format_minor` (1): `0`
- `record_len` (2): длина от `magic` до `signature` включительно
- `created_at_ms` (8)
- `updated_at_ms` (8)
- `version` (4)
- `prev_hash` (32)
- `login_len` (1)
- `login` (N)
- `root_key` (32)
- `blockchain_key` (32)
- `device_key` (32)
- `chain_number` (2)
- `balance` (8)
- `is_server` (1)
- если `is_server=1`:
- `server_key` (32)
- `server_address_len` (1)
- `server_address` (N)
- `connection_servers_count` (1)
- повтор `count` раз:
- `server_login_len` (1)
- `server_login` (N)
- `trusted_count` (1)
- `reserved` (5) = `0x00 0x00 0x00 0x00 0x00`
- `signature` (64)
- `padding` до 768
## 4. Подпись (v1.0)
В `v1.0` подписывается не сырой блок полей напрямую, а его SHA-256.
1. Формируется `unsigned_bytes`:
- все поля от `magic` до `reserved` включительно;
- поле `signature` не включается;
- padding не включается.
2. Считается `msg_hash = SHA-256(unsigned_bytes)`.
3. `signature = Ed25519.sign(root_private_key, msg_hash)`.
4. Проверка:
- `Ed25519.verify(root_key, msg_hash, signature)`.
- В текущей реализации проверка выполняется через встроенную Solana-инструкцию `Ed25519Program`
(инструкция должна идти в транзакции перед вызовом `create_user_pda` / `update_user_pda`).
## 5. Что входит в `prev_hash`
Для связи версий:
- `prev_hash = SHA-256(previous_unsigned_bytes)`
Где `previous_unsigned_bytes` — предыдущая версия записи от `magic` до `reserved` включительно, без `signature` и без padding.
Для первой версии:
- `prev_hash = 32` нулевых байта.
## 6. Правила create/update в текущей реализации
### Create
- PDA: seed `["login=", login]`.
- Создаётся запись версии `0`.
- `updated_at_ms = created_at_ms`.
- Стартовый лимит:
- `START_BONUS_LIMIT + additional_limit`.
- Оплата:
- регистрационная комиссия;
- пополнение `additional_limit` по курсу;
- рента PDA (плательщик транзакции).
### Update
- Проверка подписи новой записи по `root_key`.
- Проверка:
- `magic`, `format_major`, `format_minor`;
- корректного `record_len`;
- `prev_hash` на соответствие предыдущей версии;
- `version = old_version + 1`;
- неизменяемых полей: `login`, `created_at_ms`, `root_key`.
- `balance` не уменьшается:
- `new_balance = old_balance + additional_limit`.
- При `additional_limit > 0` берётся комиссия пополнения.
## 7. Параметры экономики/размера (settings)
См. `programs/shine_users/src/settings.rs`:
- `USER_PDA_SPACE = 768`
- `REGISTRATION_FEE_RECEIVER`
- `REGISTRATION_FEE_LAMPORTS`
- `LIMIT_STEP`
- `LAMPORTS_PER_LIMIT_STEP`
- `START_BONUS_LIMIT`
## 8. Root Key Rotation (пока не включено)
В `v1.0` `root_key` неизменяем.
Варианты расширения:
1. **Dual-signature rotate tx**:
- отдельный флаг операции rotate;
- запись подписывается и старым, и новым root key;
- контракт проверяет обе подписи.
2. **Two-step commit/confirm**:
- шаг 1: proposal смены root (`old root` подпись);
- шаг 2: confirm (`new root` подпись) в отдельной tx.
3. **Recovery guardians**:
- отдельная PDA для доверенных лиц;
- пороговая схема (например `m-of-n`) для восстановления root.
Для v1.0 решение отложено, но изменение root_key в update запрещено.

View File

@ -1,7 +0,0 @@
. Сделать новые форматы для пользователей что бы там было больше информации
. Протестировать работу и может доработать что бы можно было паралельно регистрировать 5 и более юзеров - за счёт передачи при вызове адресов PDA +1 +2 +3 +4 и т.д.
. - пока не надо - Сделать что бы в файле общей информации добавилась запись для будущей миграции пользователей (хотя можно потом и добавить будет :)))

View File

@ -1,19 +0,0 @@
Как вернуть деньги
Узнаём адрес buffer account:
solana program show <адрес_твоей_программы>
Там будет строчка Buffer: <PUBKEY>.
Закрываем буфер:
solana program close <PUBKEY> --recipient <адресошелька>
💡 --recipient — это куда вернуть SOL (обычно твой же кошелёк из ~/.config/solana/id.json).
Сколько вернётся
Если бинарник весит ~400 KB, с одного буфера вернётся ~0.35 SOL.
У тебя две программы (shine_users, shine_payments), значит можно вернуть ~0.7 SOL.

View File

@ -1,6 +0,0 @@
✅ Чтобы максимально уменьшить .so надо будет включить флаг оптимизации в Cargo.toml:
[profile.release]
opt-level = "z" # Максимальная компрессия
lto = true # Link Time Optimization
codegen-units = 1 # Уменьшает размер бинаря

View File

@ -1,44 +0,0 @@
#!/bin/bash
set -e # Завершаем при ошибке
set -o pipefail
PROGRAM_KEYPAIR="target/deploy/shine-keypair.json" # замени на свой путь
WALLET=$(solana address)
echo "🧹 Удаление старого ledger..."
rm -rf test-ledger
echo "🚀 Запуск solana-test-validator в фоне..."
solana-test-validator --ledger test-ledger --reset > validator.log 2>&1 &
VALIDATOR_PID=$!
# Убедимся, что validator запущен
echo "⏳ Ожидание запуска валидатора..."
until solana cluster-version &>/dev/null; do
sleep 1
done
sleep 2 # На всякий случай немного подождём
echo "💸 Airdrop 10 SOL на $WALLET..."
solana airdrop 10 HMww7YSVfwVm4i8sugqj7wyH26dqzHykzv3wzWwzEvPA
solana airdrop 5 $WALLET
echo "🔨 Сборка контракта..."
anchor build
echo "📦 Деплой контракта..."
anchor deploy
echo "✅ Готово!"
# Не убиваем валидатор, чтобы он оставался запущенным
echo " Валидатор всё ещё работает (PID $VALIDATOR_PID)"
echo " Запускаем логи"
solana logs

View File

@ -1,23 +0,0 @@
#!/bin/bash
set -e # Завершаем при ошибке
set -o pipefail
kill -9 $(pgrep -f "solana-test-validator")
# 🔍 Ищем запущенный solana-test-validator
EXISTING_PID=$(pgrep -f "solana-test-validator")
if [ -n "$EXISTING_PID" ]; then
echo "🛑 Найден работающий solana-test-validator (PID $EXISTING_PID), останавливаем..."
bash kill -9 $(pgrep -f "solana-test-validator")
echo "✅ Пытаюсь остановить старый валидатор..."
# ждём завершения
while kill -0 "$EXISTING_PID" 2>/dev/null; do
sleep 0.5
done
echo "✅ Старый валидатор остановлен."
fi

View File

@ -1,145 +0,0 @@
Функции для работы с PDA:
🧩 create_pda(...)
Создаёт новый PDA, если он ещё не существует.
Проверяет, чтобы не было коллизии.
Без записи данных.
🧩 write_to_pda(...)
Просто записывает байты в существующий PDA.
Без создания.
🧩 create_and_write_pda(...)
Комбинированная функция.
Сначала проверяет, есть ли PDA — если нет, создаёт.
Затем записывает данные.
Очень удобна для инициализации одного PDA в один вызов.
🧩 safe_read_pda(...)
Возвращает Vec<u8> с содержимым PDA.
Никогда не паникует: если PDA не существует или пустой — просто отдаёт Vec::new().
Защита от двойного borrow'а (через try_borrow_data()).
Отличный инструмент для безопасного считывания данных.
💡 Да, с этим ты можешь:
Возможность Функция
📦 Создать PDA create_pda
💾 Записать в PDA write_to_pda
⚡ Создать и записать create_and_write_pda
📖 Безопасно прочитать safe_read_pda
🔧 create_pda(...)
🔹 Назначение:
Создаёт новый PDA-аккаунт, если он ещё не существует.
📥 Аргументы:
pda_account: &AccountInfo — аккаунт, который хотим создать
signer: &AccountInfo — аккаунт плательщика (обычно пользователь)
system_program: &AccountInfo — системная программа
program_id: &Pubkey — адрес текущей программы
seeds: &[&[u8]] — массив сидов, по которым создавался PDA
space: u64 — сколько байт выделить под данные
📤 Возвращает:
Result<()> — Ok если успешно, Err если PDA уже существует или при ошибке создания
🧠 Особенности:
Проверяет, что PDA ещё не создан (через pda_account.owner == Pubkey::default())
Выбрасывает ErrCode::PdaAlreadyExists, если уже существует
🔧 write_to_pda(...)
🔹 Назначение:
Записывает бинарные данные в существующий PDA.
📥 Аргументы:
pda_account: &AccountInfo — аккаунт, в который пишем
data: &[u8] — массив байт, которые нужно записать
📤 Возвращает:
Result<()> — Ok при успехе, Err если не удалось получить доступ к данным
🧠 Особенности:
⚠️ Только пишет, не создаёт PDA
Записывает в начало data-секции аккаунта
🔧 create_and_write_pda(...)
🔹 Назначение:
Если PDA ещё не существует — создаёт, затем сразу записывает данные.
📥 Аргументы:
pda_account: &AccountInfo — аккаунт для создания/записи
signer: &AccountInfo — кто оплачивает создание
system_program: &AccountInfo — системная программа
program_id: &Pubkey — адрес текущей программы
seeds: &[&[u8]] — сиды PDA
data: Vec<u8> — данные для записи
space: u64 — сколько байт выделить (при создании)
📤 Возвращает:
Result<()> — Ok при успехе, Err при ошибке создания или записи
🧠 Особенности:
Безопасно создаёт и пишет за один вызов
Не выбрасывает ошибку, если PDA уже существует — просто пишет
🔧 safe_read_pda(...)
🔹 Назначение:
Безопасно считывает байты из PDA. Никогда не паникует.
📥 Аргументы:
pda_account: &AccountInfo — аккаунт для чтения
📤 Возвращает:
Vec<u8> — массив байт с содержимым PDA
→ Если аккаунт не инициализирован или пустой, возвращает Vec::new()
🧠 Особенности:
Не выбрасывает ошибки — только логирует
Полностью безопасно: подходит для чтения read-only PDA в любой ситуации

View File

@ -1,29 +0,0 @@
#!/bin/bash
# Кол-во адресов для генерации
NUM_KEYS=5
# RPC endpoint (можешь поменять)
#RPC_URL="https://api.testnet.solana.com"
RPC_URL="http://127.0.0.1:8899"
echo "👉 Используем RPC: $RPC_URL"
for i in $(seq 1 $NUM_KEYS); do
KEYPAIR="temp-key-$i.json"
echo "🔐 Генерирую ключ №$i: $KEYPAIR"
solana-keygen new --outfile "$KEYPAIR" --no-bip39-passphrase --silent
PUBKEY=$(solana-keygen pubkey "$KEYPAIR")
echo "🪙 Публичный ключ: $PUBKEY"
echo "💸 Запрашиваю airdrop на $PUBKEY..."
solana airdrop 1 "$PUBKEY" --url "$RPC_URL"
echo "🔍 Проверяю баланс:"
solana balance "$PUBKEY" --url "$RPC_URL"
echo "-----------------------------"
done
echo "✅ Готово. Удаляю временные ключи..."
rm temp-key-*.json

View File

@ -1,81 +0,0 @@
# DEPLOY CONFIG CHECKLIST (Shine Programs)
Документ для подготовки к реальному деплою (mainnet/prod): какие адреса и где заменить.
## 1) Program IDs
1. `shine/programs/shine_payments/src/lib.rs`
- `declare_id!("...")` для `shine_payments`.
2. `shine/programs/shine_users/src/lib.rs`
- `declare_id!("...")` для `shine_users`.
3. `shine/Anchor.toml`
- обновить `programs.devnet` / `programs.localnet` (и при необходимости добавить/обновить секцию под mainnet workflow).
## 2) Shine Payments on-chain settings
Файл: `shine/programs/shine_payments/src/settings.rs`
Обязательные адреса:
1. `DAO_WALLET`
2. `MANAGER_WALLET`
3. `PYTH_SOL_USD_ACCOUNT`
4. `PYTH_SOL_USD_FEED_ID` (идентификатор feed для SOL/USD)
Параметры экономики (по необходимости):
1. `START_COEF_PPM`
2. `START_LIMIT_USD_CENTS`
3. `START_CALL_REWARD_LAMPORTS`
4. `MAX_CALL_REWARD_LAMPORTS`
5. `ORACLE_MAX_AGE_SECS`
## 3) Shine Users on-chain settings
Файл: `shine/programs/shine_users/src/settings.rs`
Обязательные параметры:
1. `REGISTRATION_FEE_RECEIVER` (куда идет комиссия регистрации)
2. `REGISTRATION_FEE_LAMPORTS`
3. при необходимости скорректировать лимитные/бонусные константы:
- `LIMIT_STEP`
- `LAMPORTS_PER_LIMIT_STEP`
- `START_BONUS_LIMIT`
## 4) Web UI constants (hardcoded values)
Проверить и заменить Program ID / Oracle account в HTML:
1. `shine/programs/shine_payments/web/buy_ticket.html`
2. `shine/programs/shine_payments/web/track_ticket.html`
3. `shine/programs/shine_payments/web/admin_tools.html`
4. `shine/programs/shine_payments/web/dao_tools.html`
5. `shine/programs/shine_payments/web/manager_tools.html`
Проверить RPC endpoint для нужной сети в соответствующих страницах.
## 5) Скрипты и окружение
Проверить конфиги и env-файлы, где участвуют адреса:
1. `shine/scripts/**/config.env`
2. `shine/scripts/**/dao.config.env`
3. `shine/scripts/**/governance_token.config.env`
## 6) Проверка перед деплоем
1. `cargo check -p shine_payments`
2. `cargo check -p shine_users`
3. сверить, что `declare_id` совпадает с ключами деплоя программ.
4. убедиться, что `PYTH_SOL_USD_ACCOUNT` читается в выбранной сети.
5. прогнать smoke-тесты UI (buy / track / admin / dao / manager).
## 7) Проверка после деплоя
1. Выполнить `init` для `shine_payments`.
2. Проверить существование PDA:
- `config_pda`
- `coef_limit_pda`
- `queues_pda`
- `inflow_vault_pda`
3. Проверить покупку тикета и шаг выплаты на малой сумме.
4. Проверить `change_ticket_recipient`:
- разрешено для не-next тикета;
- запрещено для next тикета.

View File

@ -1,19 +0,0 @@
# Oracle Check
Мини-страница диагностики оракула Pyth для `SOL/USD`.
Файл:
- `index.html`
Что проверяет:
1. Чтение oracle account через RPC (`devnet`, `mainnet-beta`, `testnet`).
2. Парсинг по текущим оффсетам из UI (`74/90/94`).
3. Альтернативный парсинг (`73/89/93`) для проверки сдвига формата.
4. Сравнение с Hermes API (эталонный источник цены по feed id).
Запуск:
Открыть `index.html` в браузере и нажать кнопку «Проверить все сети».

View File

@ -1,255 +0,0 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Проверка оракула Pyth (Devnet/Mainnet/Testnet)</title>
<style>
:root {
color-scheme: dark;
--bg: #0f1218;
--panel: #171b24;
--text: #e8edf6;
--muted: #97a3b8;
--line: #2a3242;
--ok: #55d48a;
--warn: #ffbf5e;
--err: #ff7d7d;
--btn: #273247;
--btn-hover: #32415c;
--code: #1e2633;
}
* { box-sizing: border-box; }
body { font-family: Arial, sans-serif; margin: 20px; background: var(--bg); color: var(--text); }
.wrap { width: 100%; max-width: 1800px; }
.panel { border: 1px solid var(--line); border-radius: 8px; padding: 14px; margin-bottom: 14px; background: var(--panel); }
.row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 8px 0; }
input { padding: 9px 10px; min-width: 340px; border: 1px solid var(--line); border-radius: 8px; background: #131824; color: var(--text); }
button { padding: 9px 14px; border: 1px solid var(--line); border-radius: 8px; cursor: pointer; background: var(--btn); color: var(--text); }
button:hover { background: var(--btn-hover); }
.muted { color: var(--muted); }
.ok { color: var(--ok); }
.warn { color: var(--warn); }
.err { color: var(--err); white-space: pre-wrap; }
code { background: var(--code); padding: 2px 4px; border-radius: 4px; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
th, td { border: 1px solid var(--line); padding: 6px; text-align: left; vertical-align: top; }
</style>
</head>
<body>
<div class="wrap">
<h1>Диагностика оракула Pyth (SOL/USD)</h1>
<div class="panel">
<div class="muted">
Страница нужна, чтобы проверить, что именно возвращает аккаунт оракула в разных сетях и где ломается парсинг.
</div>
<div class="muted">
Проверяются три сети: <code>devnet</code>, <code>mainnet-beta</code>, <code>testnet</code>.
</div>
</div>
<div class="panel">
<h3>Настройки</h3>
<div class="row">
<label>Feed ID (hex с 0x):<br /><input id="feedId" value="0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d" /></label>
</div>
<div class="row">
<label>Oracle account (devnet):<br /><input id="oracleDevnet" value="7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE" /></label>
</div>
<div class="row">
<label>Oracle account (mainnet-beta):<br /><input id="oracleMainnet" value="7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE" /></label>
</div>
<div class="row">
<label>Oracle account (testnet):<br /><input id="oracleTestnet" value="7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE" /></label>
</div>
<div class="row">
<button id="runBtn">Проверить все сети</button>
</div>
<div class="muted">
Если для сети аккаунт не существует, это тоже покажется в отчёте.
</div>
</div>
<div class="panel">
<h3>Результаты</h3>
<div id="out" class="muted">Нажмите «Проверить все сети».</div>
</div>
</div>
<script>
const NETWORKS = [
{ name: "devnet", rpc: "https://api.devnet.solana.com", inputId: "oracleDevnet" },
{ name: "mainnet-beta", rpc: "https://api.mainnet-beta.solana.com", inputId: "oracleMainnet" },
{ name: "testnet", rpc: "https://api.testnet.solana.com", inputId: "oracleTestnet" },
];
function readU64(data, offset) {
let x = 0n;
for (let i = 0; i < 8; i++) x |= BigInt(data[offset + i]) << (8n * BigInt(i));
return x;
}
function readI64(data, offset) {
let x = readU64(data, offset);
if (x > 0x7fffffffffffffffn) x -= 0x10000000000000000n;
return x;
}
function readU32(data, offset) {
return data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24);
}
function readI32(data, offset) {
let x = readU32(data, offset);
if (x > 0x7fffffff) x -= 0x100000000;
return x;
}
function toHex(bytes) {
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
}
function trimZeros(v) {
return v.replace(/(\.\d*?[1-9])0+$/u, "$1").replace(/\.0+$/u, "").replace(/\.$/u, "");
}
function priceToFloatStr(price, exponent) {
const p = Number(price);
if (!Number.isFinite(p)) return "NaN";
return trimZeros((p * Math.pow(10, exponent)).toFixed(12));
}
function fmtErr(e) {
const s = String(e?.message || e || "unknown error");
return s.length > 350 ? s.slice(0, 350) + "..." : s;
}
function parseWithOffsets(data, priceOffset, exponentOffset, publishOffset) {
const price = readI64(data, priceOffset);
const exponent = readI32(data, exponentOffset);
const publishTime = readI64(data, publishOffset);
return { price, exponent, publishTime, valueStr: priceToFloatStr(price, exponent) };
}
function parseFeedIdFromLikelyPosition(data) {
// Для текущего PriceUpdateV2 в аккаунте receiver:
// 0..7 discriminator, 8..39 write_authority, 40..40 verification enum, 41..72 feed_id.
if (data.length < 73) return null;
const feedRaw = data.slice(41, 73);
return "0x" + toHex(feedRaw);
}
async function rpcGetAccountInfo(rpcUrl, pubkey) {
const body = {
jsonrpc: "2.0",
id: 1,
method: "getAccountInfo",
params: [pubkey, { encoding: "base64", commitment: "confirmed" }],
};
const res = await fetch(rpcUrl, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`RPC HTTP ${res.status}`);
const json = await res.json();
if (json.error) throw new Error(json.error.message || JSON.stringify(json.error));
return json.result?.value || null;
}
async function fetchHermesPrice(feedIdHex) {
const url = `https://hermes.pyth.network/v2/updates/price/latest?ids[]=${encodeURIComponent(feedIdHex)}`;
const res = await fetch(url);
if (!res.ok) throw new Error(`Hermes HTTP ${res.status}`);
const json = await res.json();
const p = json?.parsed?.[0]?.price;
if (!p) throw new Error("Hermes: price not found");
return {
price: BigInt(p.price),
expo: Number(p.expo),
publishTime: BigInt(p.publish_time),
valueStr: priceToFloatStr(BigInt(p.price), Number(p.expo)),
};
}
async function runCheck() {
const out = document.getElementById("out");
out.textContent = "Проверка...";
const feedId = document.getElementById("feedId").value.trim().toLowerCase();
const rows = [];
let hermes = null;
let hermesErr = null;
try {
hermes = await fetchHermesPrice(feedId);
} catch (e) {
hermesErr = fmtErr(e);
}
for (const n of NETWORKS) {
const oracle = document.getElementById(n.inputId).value.trim();
try {
const ai = await rpcGetAccountInfo(n.rpc, oracle);
if (!ai) {
rows.push({
network: n.name,
status: "account not found",
details: "Аккаунт оракула не найден в этой сети",
});
continue;
}
const data = Uint8Array.from(atob(ai.data[0]), c => c.charCodeAt(0));
const feedFromData = parseFeedIdFromLikelyPosition(data);
const parsedCurrentUi = parseWithOffsets(data, 74, 90, 94); // как сейчас в UI проекта
const parsedShiftMinus1 = parseWithOffsets(data, 73, 89, 93); // диагностический вариант
const currentUiValid = parsedCurrentUi.price > 0n;
const shiftValid = parsedShiftMinus1.price > 0n;
const feedMatch = feedFromData === feedId;
rows.push({
network: n.name,
status: "ok",
details: `
<div>RPC: <code>${n.rpc}</code></div>
<div>Account owner: <code>${ai.owner}</code>, data len: <b>${data.length}</b></div>
<div>Feed ID (из аккаунта): <code>${feedFromData || "n/a"}</code> ${feedMatch ? '<span class="ok">совпадает</span>' : '<span class="warn">НЕ совпадает</span>'}</div>
<div>Парсер UI (offsets 74/90/94): price=<code>${parsedCurrentUi.price.toString()}</code>, exp=<code>${parsedCurrentUi.exponent}</code>, value=<b>${parsedCurrentUi.valueStr}</b> ${currentUiValid ? '<span class="ok">valid</span>' : '<span class="err">invalid</span>'}</div>
<div>Альт. парсер (offsets 73/89/93): price=<code>${parsedShiftMinus1.price.toString()}</code>, exp=<code>${parsedShiftMinus1.exponent}</code>, value=<b>${parsedShiftMinus1.valueStr}</b> ${shiftValid ? '<span class="ok">valid</span>' : '<span class="err">invalid</span>'}</div>
${hermes
? `<div>Hermes (эталон): price=<code>${hermes.price.toString()}</code>, exp=<code>${hermes.expo}</code>, value=<b>${hermes.valueStr}</b></div>`
: `<div class="warn">Hermes недоступен: ${hermesErr}</div>`
}
`,
});
} catch (e) {
rows.push({
network: n.name,
status: "error",
details: `<span class="err">${fmtErr(e)}</span>`,
});
}
}
out.innerHTML = `
<table>
<thead>
<tr>
<th>Сеть</th>
<th>Статус</th>
<th>Детали</th>
</tr>
</thead>
<tbody>
${rows.map((r) => `
<tr>
<td><b>${r.network}</b></td>
<td>${r.status === "ok" ? '<span class="ok">ok</span>' : (r.status === "account not found" ? '<span class="warn">not found</span>' : '<span class="err">error</span>')}</td>
<td>${r.details}</td>
</tr>
`).join("")}
</tbody>
</table>
`;
}
document.getElementById("runBtn").addEventListener("click", runCheck);
</script>
</body>
</html>

View File

@ -1,42 +0,0 @@
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

View File

@ -1,10 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Environment-dependent path to Maven home directory
/mavenHomeManager.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/solana-shine-lib" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -1,46 +0,0 @@
plugins {
id 'java'
}
apply plugin: 'java'
group = 'com.shine'
version = '1.0.0'
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
tasks.withType(Jar) {
manifest {
attributes(
'Implementation-Title': 'solana-shine-lib', // или solana-shine-client-lib
'Implementation-Version': version
)
}
}
repositories {
mavenCentral()
flatDir {
dirs 'libs'
}
}
dependencies {
testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
implementation project(':solana-shine-lib')
implementation 'com.google.code.gson:gson:2.10.1'
// implementation "com.mmorrell:solanaj:1.15.1"
}
test {
useJUnitPlatform()
}

View File

@ -1,6 +0,0 @@
#Fri Jun 13 15:16:43 MSK 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -1,234 +0,0 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View File

@ -1,89 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -1,2 +0,0 @@
rootProject.name = 'solana-shine-client-lib'
include 'solana-shine-lib'

View File

@ -1,56 +0,0 @@
plugins {
id 'java'
}
group = 'me.shineup'
version = '1.0'
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
repositories {
mavenCentral()
flatDir {
dirs 'libs'
}
}
dependencies {
// были стандартные
testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
// шифрование нужна
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
// implementation 'com.squareup.okhttp3:okhttp:4.12.0' // запросы по сети
//солана
implementation "com.mmorrell:solanaj:1.15.1"
// implementation 'org.bitcoinj:bitcoinj-core:0.15.10'
// implementation 'com.squareup.moshi:moshi:1.13.0'
// implementation 'com.mmorrell:solanaj:1.20.4' - старые соланы не нужны
// implementation name: 'solanaj-1.20.4' - старые соланы не нужны
// Logging
implementation 'org.slf4j:slf4j-api:1.7.36'
implementation 'ch.qos.logback:logback-classic:1.2.11'
implementation 'com.google.code.gson:gson:2.10.1'
}
test {
useJUnitPlatform()
}

View File

@ -1,9 +0,0 @@
Что ещё надо сделать по библиотеке
1. Сделать более рандомное создание пар ключей. (тк я три из трёх угадал в девнет!!!)
2. Доделат обработку ошибок при вызове функции ( наверно уже в UI надо отлавливать ексепшены которые возвращает нода при препроверки вызова функции)
2.5 метод проверки что транзакция прям точно добавлена в систему. (хотя нужен ли он??. Тк пользователь добавился значет уже всё хорошо :) )
3. Исправить перевод денег (тк он не работает после перехода на старую библиотеку)

View File

@ -1,42 +0,0 @@
package me.shineup.solana;
import me.shineup.solana.config.Const;
/**
* Настройки подключения к Solana.
* Позволяет выбирать RPC-сервер (локальный, тестовая сеть или произвольный).
*/
public class SolanaSettings {
/**
* Устанавливает локальный RPC-адрес (например, http://127.0.0.1:8899).
*/
public static void setRpcUrlLocal() {
Const.RPC_URL = Const.LOCAL_RPC_URL;
}
/**
* Устанавливает RPC-адрес тестовой сети Solana (https://api.testnet.solana.com).
*/
public static void setRpcUrlTestNet() {
Const.RPC_URL = "https://api.testnet.solana.com"; // или Const.TESTNET_RPC_URL
}
/**
* Устанавливает пользовательский RPC-адрес.
*
* @param url Строка с адресом RPC-сервера
*/
public static void setRpcUrl(String url) {
Const.RPC_URL = url;
}
/**
* Получает текущий установленный RPC-адрес.
*
* @return строка с текущим RPC-URL
*/
public static String getRpcUrl() {
return Const.RPC_URL;
}
}

View File

@ -1,218 +0,0 @@
package me.shineup.solana;
import me.shineup.solana.internal.utils.resultChecker.TransactionStatusHelper;
import me.shineup.solana.model.TxStatus;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
/**
* <h2>SolanaTxWatcher</h2>
* <p>
* Класс-наблюдатель за одной транзакцией сети Solana.
* Он:
* <ul>
* <li>Хранит подпись (signature) и фиксирует момент создания объекта.</li>
* <li>Через {@link #updateStatus()} опрашивает RPC (используя {@link TransactionStatusHelper})
* и обновляет внутренний статус.</li>
* <li>Через {@link #shouldRetry()} сообщает, надо ли продолжать опрос (с учётом таймаута,
* лимита неудачных попыток и финального статуса).</li>
* <li>Через {@link #isSuccess()} указывает, прошла ли транзакция успешно.</li>
* </ul>
*
* <h3>Пример использования</h3>
*
* <pre>{@code
* SolanaTxWatcher watcher = new SolanaTxWatcher("5QgV...sig");
*
* while (watcher.shouldRetry()) { // проверяем, нужно ли ещё опрашивать
* watcher.updateStatus(); // запрашиваем статус через RPC
* System.out.println(
* watcher.getStatus() + " | " + // печатаем статус
* watcher.getStatusComment());
* Thread.sleep(SolanaTxWatcher.getRetryIntervalMs()); // ждём секунду
* }
*
* if (watcher.isSuccess()) {
* System.out.println("✅ Транзакция прошла успешно!");
* } else {
* System.out.println("⛔ Завершили слежение без успеха.");
* }
* }</pre>
*/
public class SolanaTxWatcher {
/* ---------- НАСТРАИВАЕМЫЕ КОНСТАНТЫ ---------- */
/** Максимальное время слежения (мс). */
private static final long TIMEOUT_MS = 30_000;
/** Допустимое количество подряд статусов UNKNOWN / NETWORK_ERROR. */
private static final int MAX_FAILED_ATTEMPTS = 3;
/** Рекомендуемый интервал (мс) между вызовами {@link #updateStatus()}. */
private static final long RETRY_INTERVAL_MS = 1_000;
/* ---------- ПОЛЯ ЭКЗЕМПЛЯРА ---------- */
/** Подпись (signature) транзакции. */
private final String signature;
/** Время создания объекта (Unix-millis). */
private final long startTimeMs;
/** Кол-во подряд «слабых» ошибок (UNKNOWN / NETWORK_ERROR). */
private int failedAttempts;
/** Флаг: нужно ли ещё опрашивать RPC. */
private boolean needRetry;
/** Флаг: успешна ли транзакция (устанавливается при FINALIZED_SUCCESS). */
private boolean success;
/** Текущий статус из {@link TxStatus}. */
private TxStatus status;
/* ---------- ЧЕЛОВЕКО-ЧИТАЕМЫЕ ОПИСАНИЯ СТАТУСОВ ---------- */
private static final Map<TxStatus, String> COMMENTS = new HashMap<>();
static {
COMMENTS.put(TxStatus.NOT_FOUND,
"Подпись не дошла до RPC — ждём появления.");
COMMENTS.put(TxStatus.PROCESSED,
"Принята в обработку — ожидаем включения в блок.");
COMMENTS.put(TxStatus.CONFIRMED,
"Уже в блоке — ждём финализации.");
COMMENTS.put(TxStatus.FINALIZED_SUCCESS,
"Финализирована успешно.");
COMMENTS.put(TxStatus.FINALIZED_ERROR,
"Финализирована с ошибкой.");
COMMENTS.put(TxStatus.UNKNOWN,
"Неизвестная ошибка RPC/парсинга.");
COMMENTS.put(TxStatus.NETWORK_ERROR,
"Сбой сети или RPC недоступен.");
}
/* ---------- КОНСТРУКТОР ---------- */
/**
* Создаёт watcher для указанной подписи.
*
* @param signature подпись транзакции (Base58).
*/
public SolanaTxWatcher(String signature) {
this.signature = signature;
this.startTimeMs = System.currentTimeMillis();
this.failedAttempts = 0;
this.needRetry = true; // по умолчанию пытаемся
this.success = false; // успех пока не достигнут
this.status = TxStatus.NOT_FOUND; // стартовый
}
/* ---------- ГЕТТЕРЫ ---------- */
/** @return подпись транзакции. */
public String getSignature() { return signature; }
/** @return время создания watcherа (Unix-millis). */
public long getStartTimeMs() { return startTimeMs; }
/** @return текущий статус. */
public TxStatus getStatus() { return status; }
/** @return true, если транзакция финализирована без ошибок. */
public boolean isSuccess() { return success; }
/** @return кол-во подряд неудачных (UNKNOWN/NETWORK_ERROR) попыток. */
public int getFailedAttempts() { return failedAttempts; }
/** @return человеко-читаемый комментарий к текущему статусу. */
public String getStatusComment() {
return COMMENTS.getOrDefault(status, "");
}
/* ---------- ОСНОВНОЙ МЕТОД ОПРОСА ---------- */
/**
* Запрашивает актуальный статус транзакции через {@link TransactionStatusHelper}
* и обновляет внутренние поля.
* <p> При промежуточных статусах (NOT_FOUND / PROCESSED / CONFIRMED)
* счётчик ошибок сбрасывается.<br>
* При {@code FINALIZED_SUCCESS} или {@code FINALIZED_ERROR}
* флаг {@link #needRetry} переводится в {@code false}.<br>
* При {@code UNKNOWN} или {@code NETWORK_ERROR}
* счётчик ошибок увеличивается; если превышен лимит дальнейший опрос прекращается.
*/
public void updateStatus() {
if (!needRetry) return; // уже решено не опрашивать
TxStatus newStatus =
TransactionStatusHelper.getTxStatus(signature);
switch (newStatus) {
case NOT_FOUND:
case PROCESSED:
case CONFIRMED:
failedAttempts = 0; // успешное промежуточное обновление
break;
case FINALIZED_SUCCESS:
success = true;
needRetry = false; // финальный успех
break;
case FINALIZED_ERROR:
success = false;
needRetry = false; // финальный провал
break;
case UNKNOWN:
case NETWORK_ERROR:
failedAttempts++;
if (failedAttempts > MAX_FAILED_ATTEMPTS) {
needRetry = false; // слишком много ошибок прекращаем
}
break;
}
status = newStatus; // сохраняем новый статус
}
/* ---------- РЕШЕНИЕ: НУЖНО ЛИ ПОВТОРЯТЬ ---------- */
/**
* @return {@code true}, если можно и стоит делать ещё один запрос статуса.<br>
* {@code false} если достигнут финальный статус, превышен таймаут
* или лимит неудачных попыток.
*/
public boolean shouldRetry() {
if (!needRetry) return false; // наш флаг запрещает
long elapsed = System.currentTimeMillis() - startTimeMs;
if (elapsed > TIMEOUT_MS) { // вышли за таймаут
needRetry = false;
return false;
}
return true;
}
/* ---------- ВСПОМОГАТЕЛЬНОЕ ---------- */
/** @return рекомендуемую задержку (мс) между опросами. */
public static long getRetryIntervalMs() { return RETRY_INTERVAL_MS; }
@Override
public String toString() {
return String.format("[%s] sig=%s | status=%s | needRetry=%s | success=%s | attempts=%d | %s",
Instant.ofEpochMilli(startTimeMs),
signature,
status,
needRetry,
success,
failedAttempts,
getStatusComment());
}
}

View File

@ -1,88 +0,0 @@
package me.shineup.solana;
import me.shineup.solana.internal.callSolanaFunc.RegisterUser.RegisterUserWithOneDev;
import me.shineup.solana.model.TxStatus;
import me.shineup.solana.model.UserById;
import me.shineup.solana.internal.readFromSolana.userById.UserByIdReader;
import me.shineup.solana.model.UserByLogin;
import me.shineup.solana.internal.readFromSolana.userByLogin.UserByLoginReader;
import me.shineup.solana.internal.standartActions.airDrops.SolanaAirdrop;
import me.shineup.solana.internal.standartActions.balanse.SolanaBalanceChecker;
import me.shineup.solana.internal.standartActions.keysGenerator.KeyPairBase58;
import me.shineup.solana.internal.standartActions.keysGenerator.SolanaKeyGeneratorManual;
import me.shineup.solana.internal.standartActions.transfer.SolanaTransfer;
import me.shineup.solana.config.Const;
import me.shineup.solana.internal.utils.resultChecker.TransactionStatusHelper;
public class SolanaWrapper {
/** Получает баланс по публичному ключу */
public static long getBalance(String publicKey) throws Exception{
return SolanaBalanceChecker.getBalance(publicKey);
}
/** Запрашивает Airdrop на указанный публичный ключ */
public static String requestAirdrop(String publicKey, long lamports) throws Exception{
return SolanaAirdrop.requestAirdrop(publicKey, lamports);
}
/** Обёртка для перевода lamports между двумя публичными ключами (оба ключа — в base58, приватный — отправителя). */
// public static String sendLamports(String fromBase58Secret, String toBase58Pubkey, long lamports) throws Exception {
// return SolanaTransfer.sendSol(fromBase58Secret, toBase58Pubkey, lamports);
// }
/** Генерирует новый Ed25519-кошелёк (ключи Solana) */
public static KeyPairBase58 generateNewWallet() throws Exception{ // todo возмаожно ключи генерируются недостаточно рандомно
return SolanaKeyGeneratorManual.generateKeyPair();
}
/** Генерирует новeую пару ключей X25519 */
public static KeyPairBase58 generateNewKeyPairX25519() throws Exception{ // todo это пока заглушка
return SolanaKeyGeneratorManual.generateKeyPair();
}
/** Проверяет статус транзакции по её подписи (signature) */
public static TxStatus getTransactionStatus(String signature) throws Exception {
return TransactionStatusHelper.getTxStatus(signature);
}
/**
* Обёртка для перевода SOL с одного аккаунта на другой.
*
* @param fromBase58Secret приватный ключ отправителя (в base58)
* @param toAddressBase58 публичный ключ получателя (в base58)
* @param lamports сумма в лампортах (1 SOL = 1_000_000_000 лампортов)
*/
public static String sendSol(String fromBase58Secret, String toAddressBase58, long lamports) throws Exception{
return SolanaTransfer.sendSol(fromBase58Secret, toAddressBase58, lamports);
}
/** Выполняет регистрацию пользователя и одного устройства */
public static String registerUserWithOneDev(
String payerPubkeyB58,
String payerPrivkeyB58,
String login,
String deviceSignPubkeyB58,
String deviceX25519PubkeyB58
) throws Exception {
return RegisterUserWithOneDev.callRegisterUserWithOneDev(
payerPubkeyB58,
payerPrivkeyB58,
login,
deviceSignPubkeyB58,
deviceX25519PubkeyB58
);
}
/** Получить объект UserByLogin по логину */
public static UserByLogin getUserByLogin(String login) throws Exception {
return UserByLoginReader.getUserByLogin(login);
}
/** Получить объект UserById по числовому ID */
public static UserById getUserById(long id) throws Exception {
return UserByIdReader.getUserById(id);
}
}

View File

@ -1,19 +0,0 @@
InitializeUserCounter - инициализирует счётчик пользователь (вызывается один раз)
RegisterUserWithOneDev - регистрирует пользователя с одним устройством
- оплачивает деньги
- решистрирует акаунт (PDA) по ЛОГИНУ и (PDA) по id
- регистрирует ключи пользователя и устройствва
- увеличивает количество пользователей в ситеме ++1
UserByLoginReader - читает данные пользователя по логину
UserCounterReader - читает количество пользователей в системе
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SolanaKeyGeneratorManual - генерирует пары ключей
- - - -
SolanaBalanceChecker - показывает баланс акаунта
SolanaAirdrop - запрашивает Airdrop
SolanaTransfer - перевод со счёта на счёт
-----------------------------------------
Const - хранит константы настройки
ResultChecker(sig) - проверяет результат транзакции

View File

@ -1,76 +0,0 @@
package me.shineup.solana.config;
import me.shineup.solana.internal.utils.KeyPair;
import org.p2p.solanaj.core.PublicKey;
import java.text.DecimalFormat;
public class Const {
/** Program ID из declare_id! */
public static final String PROGRAM_ID_str = "5dFcWDNp42Xn9Vv4oDMJzM4obBJ8hvDuAtPX54fT5L3t"; // shine
public static final String userSeedsPrefix = "u="; // префикс для Seed адреса пользователя по логину
public static String USER_COUNTER_SEED = "user_counter"; // Seed Адрес PDA счётчика пользователей
public static final PublicKey PROGRAM_ID_key = new PublicKey(PROGRAM_ID_str);//"BmCgGmQbSjkE6Zg8WAwhxDMNHiTknMYqTF4ZVMrPdTpz"); // shine
public static PublicKey ADMIN_FEE_ACCOUNT = new PublicKey("6bFc5Gz5qF172GQhK5HpDbWs8F6qcSxdHn5XqAstf1fY");
public static final String LOCAL_RPC_URL = "http://127.0.0.1:8899";
public static final String LOCAL_ANDROID_TEST_RPC_URL = "http://10.0.2.2:8899";
public static final String TESTNET_RPC_URL = "https://api.testnet.solana.com";
public static final String DEVNET_RPC_URL = "https://api.devnet.solana.com";
// RPC URL для используемой ноды Solana
public static String RPC_URL = DEVNET_RPC_URL;
// Запись для хранения ключей
// public record KeyPair(String name, String publicKey, String privateKey) {} не надо больше
// Массив пар ключей
public static final KeyPair[] KEYS = {
new KeyPair("key1", "HMww7YSVfwVm4i8sugqj7wyH26dqzHykzv3wzWwzEvPA", // есть в дев нет!!! --url https://api.devnet.solana.com
"5pbFo9Zq1VsNheHwbEp6AZKa6R62CZHoGkJFZnugpMEtCmkQFjuUP7TgA5hSPqv4NABGmPP62qVnDPHmRqEAwvJc"),
new KeyPair("key2", "E3ZDHbWv1qiFvDTmaRc9wjFCgbQw6UmKJLJYbaTNvjAh",
"5qm1GJGXB1fFJ3YsU5Y3XXgTiQfaimqBWk79oEveFASH9D2of3jqUoT7dumBvS449fW5j5Sw8MgAMH2QBMmFPdry"),
new KeyPair("key3", "6bFc5Gz5qF172GQhK5HpDbWs8F6qcSxdHn5XqAstf1fY",
"3VYfYZZ3ugmgwisiQQAfcimX9T65AE9BmwmYVixAUj4jyneccSE9rzbC3g5twvH7ECZ8xgp7emJo3pR4yQqCwjGn")
};
// Метод для определения ключа
public static String identifyKey(String key) {
for (KeyPair kp : KEYS) {
if (kp.getPublicKey().equals(key)) {
return kp.getName() + "(public)";
}
if (kp.getPrivateKey().equals(key)) {
return kp.getName() + "(private)";
}
}
return key; // если не найдено
}
// Метод для получения KeyPair по имени
public static KeyPair getKeyByName(String name) {
for (KeyPair kp : KEYS) {
if (kp.getName().equals(name)) {
return kp;
}
}
return null; // если не найдено
}
// Метод форматирования лампортов в SOL
public static String lamportsToSol(long lamports) {
double sol = lamports / 1_000_000_000.0;
DecimalFormat df = new DecimalFormat("0.00000000");
return df.format(sol);
}
}

View File

@ -1,35 +0,0 @@
Exeption - если что то полетело в коде
SolanaRpcConnectionException - Не удалось подключиться к RPC Solana или получить ответ
SolanaLibLogicException - Базовое исключение всех ошибок, связанных с Solana
Выкидываем во всех не стандартных случаях из библиотеки вместо стандартного Exception
SolanaProgramException - Исключение, выбрасываемое при кастомной ошибке от Solana-программы (смарт контракта). Например: "custom program error: 0x1771"
SolanaInsufficientFundsForFeeException - Недостаточно SOL для оплаты комиссии (InsufficientFundsForFee).
это если вызвали регистрацию без средств
(но получается тоже не надо так как - прога не вызовет её без проверки баланса)
нет такого пользователя
SolanaIncorrectProgramIdException Неверный programId — вызывается не та программа (IncorrectProgramId)
возможно надо что бы быстро находить новый програм Ид
(но по факту он не меняется если кто то не потеряет пароль!!! ) ну или потребуется один раз при переходе на DAO

View File

@ -1,43 +0,0 @@
package me.shineup.solana.exceptions;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
public class SolanaErrorHandler {
public static void handleRpcJsonError(String json) throws SolanaException {
try {
JsonObject obj = JsonParser.parseString(json).getAsJsonObject();
if (!obj.has("error")) return;
JsonObject error = obj.getAsJsonObject("error");
String msg = error.has("message") ? error.get("message").getAsString() : "";
handleSolanaError(msg);
} catch (Exception e) {
// fallback
throw new SolanaException("Ошибка обработки RPC-ошибки", e);
}
}
public static void handleSolanaError(String errorMessage) throws SolanaException {
if (errorMessage == null || errorMessage.isEmpty()) return;
if (errorMessage.contains("custom program error: 0x")) {
String hex = errorMessage.substring(errorMessage.indexOf("0x")).split(" ")[0];
throw new SolanaException_InProgram(hex);
}
if (errorMessage.contains("InsufficientFundsForFee")) {
throw new SolanaException_InsufficientFundsForFee();
}
if (errorMessage.contains("IncorrectProgramId")) {
throw new SolanaException_IncorrectProgramId();
}
throw new SolanaException("Неизвестная ошибка Solana: " + errorMessage);
}
}

View File

@ -1,13 +0,0 @@
package me.shineup.solana.exceptions;
/** Базовое исключение всех ошибок, связанных с Solana */
public class SolanaException extends Exception {
public SolanaException(String message) {
super(message);
}
public SolanaException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -1,41 +0,0 @@
package me.shineup.solana.exceptions;
/**
* Исключение, выбрасываемое при кастомной ошибке от Solana-программы.
* Например: "custom program error: 0x1771" или 10001
*/
public class SolanaException_InProgram extends SolanaException {
private final int errorCode;
/**
* Создаёт исключение на основе шестнадцатеричного кода (например, "0x1771").
*/
public SolanaException_InProgram(String errorCodeHex) {
super("Ошибка от смарт-контракта. Код: " + parseHex(errorCodeHex));
this.errorCode = parseHex(errorCodeHex);
}
/**
* Создаёт исключение на основе десятичного кода (например, 10001).
*/
public SolanaException_InProgram(int errorCodeDecimal) {
super("Ошибка от смарт-контракта. Код: " + errorCodeDecimal);
this.errorCode = errorCodeDecimal;
}
public int getErrorCodeDecimal() {
return errorCode;
}
public String getErrorCodeHex() {
return "0x" + Integer.toHexString(errorCode);
}
private static int parseHex(String hex) {
try {
return Integer.parseInt(hex.replace("0x", ""), 16);
} catch (NumberFormatException e) {
return -1;
}
}
}

View File

@ -1,12 +0,0 @@
package me.shineup.solana.exceptions;
/**
* Неверный programId вызывается не та программа (IncorrectProgramId).
*/
public class SolanaException_IncorrectProgramId extends SolanaException {
public SolanaException_IncorrectProgramId() {
super("Указан неверный programId — не соответствует контракту");
}
}

View File

@ -1,10 +0,0 @@
package me.shineup.solana.exceptions;
/**
* Недостаточно SOL для оплаты комиссии (InsufficientFundsForFee).
*/
public class SolanaException_InsufficientFundsForFee extends SolanaException {
public SolanaException_InsufficientFundsForFee() {
super("Недостаточно средств на балансе для оплаты комиссии (InsufficientFundsForFee)");
}
}

View File

@ -1,21 +0,0 @@
package me.shineup.solana.exceptions;
/**
* Исключение, выбрасываемое вручную при логических проверках в библиотеке.
* К сообщению автоматически добавляется строка вызова (класс и номер строки).
*/
public class SolanaException_LibLogic extends SolanaException {
public SolanaException_LibLogic(String userMessage) {
super(userMessage + getSourceSuffix());
}
private static String getSourceSuffix() {
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
if (stack.length > 3) {
StackTraceElement caller = stack[3];
return " (исходный код: " + caller.getFileName() + ":" + caller.getLineNumber() + ")";
}
return "";
}
}

View File

@ -1,8 +0,0 @@
package me.shineup.solana.exceptions;
/** Не удалось подключиться к RPC или получить ответ */
public class SolanaException_RpcConnection extends SolanaException {
public SolanaException_RpcConnection(String message) {
super(message);
}
}

View File

@ -1,14 +0,0 @@
package me.shineup.solana.exceptions;
/**
* Пользователь с указанным идентификатором не найден в системе.
*/
public class SolanaException_UserNotFound extends SolanaException {
public SolanaException_UserNotFound() {
super("Пользователь не найден в системе.");
}
public SolanaException_UserNotFound(String msg) {
super("Пользователь не найден в системе: " + msg);
}
}

View File

@ -1,98 +0,0 @@
package me.shineup.solana.internal.callSolanaFunc.InitializeUserCounter;
import org.p2p.solanaj.core.AccountMeta;
import org.p2p.solanaj.core.PublicKey;
import org.p2p.solanaj.programs.SystemProgram;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import me.shineup.solana.internal.utils.resultChecker.ResultChecker;
import me.shineup.solana.config.Const;
import me.shineup.solana.internal.utils.SolanaProgramCaller;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Вызывает Anchor-функцию `initialize_user_counter` смарт-контракта на Solana.
*
* Эта функция предназначена для одноразовой инициализации PDA-аккаунта, в котором будет храниться
* счётчик пользователей. Аккаунт создаётся с сидом "user_counter" и содержит 8 байт, представляющих число.
*
* Повторный вызов приведёт к ошибке (если PDA уже существует).
*
*/
public class InitializeUserCounter {
private static final Logger LOG = LoggerFactory.getLogger(InitializeUserCounter.class);
/**
* Вызывает Anchor-функцию `initialize_user_counter` смарт-контракта на Solana.
*
* Эта функция предназначена для одноразовой инициализации PDA-аккаунта, в котором будет храниться
* счётчик пользователей. Аккаунт создаётся с сидом "user_counter" и содержит 8 байт, представляющих число.
* Вызвать её в принципе может кто угодно, кто оплатит создание этого пда
*
* После того как PDA будет создан, любой повторный вызов приведёт к ошибке (если PDA уже существует).
*/
public static String callInitializeUserCounter(
String publicKeyB58,
String privateKeyB58
) {
try {
//
// 1. Генерируем PDA-адрес для сидов ["user_counter"]
//
String seed = "user_counter";
PublicKey counterPda = PublicKey.findProgramAddress(
Collections.singletonList(seed.getBytes(StandardCharsets.UTF_8)),
Const.PROGRAM_ID_key
).getAddress();
//
// 2. Аргументы Anchor-функции: initialize_user_counter не требует входных данных
//
byte[] serializedArgs = new byte[0]; // нет параметров
//
// 3. Список аккаунтов
//
List<AccountMeta> accounts = Arrays.asList(
new AccountMeta(new PublicKey(publicKeyB58), true, true), // payer / signer
new AccountMeta(counterPda, false, true), // pda: user_counter
new AccountMeta(SystemProgram.PROGRAM_ID, false, false) // system_program
);
//
// 4. Вызов Anchor-функции
//
return SolanaProgramCaller.callAnchorFunction(
publicKeyB58,
privateKeyB58,
"initialize_user_counter", // имя функции Anchor
Const.PROGRAM_ID_key,
accounts,
serializedArgs
);
} catch (Exception e) {
LOG.error("❌ Ошибка вызова initialize_user_counter", e);
return null;
}
}
// Для теста
public static void main(String[] args) {
// Const.RPC_URL=Const.DEVNET_RPC_URL;
String sig = InitializeUserCounter.callInitializeUserCounter(
Const.getKeyByName("key1").getPublicKey(),
Const.getKeyByName("key1").getPrivateKey()
);
ResultChecker.check(sig);
}
}

View File

@ -1,104 +0,0 @@
package me.shineup.solana.internal.callSolanaFunc.RegisterUser;
import org.p2p.solanaj.core.AccountMeta;
import org.p2p.solanaj.core.PublicKey;
import org.p2p.solanaj.programs.SystemProgram;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import me.shineup.solana.internal.utils.resultChecker.ResultChecker;
import me.shineup.solana.config.Const;
import me.shineup.solana.internal.utils.SolanaProgramCaller;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Вызывает Anchor-функцию `register_user_step_one`. !!!!!!!!! ЭТО УСТАРЕЛО И БОЛЬШЕ НЕ НАДО
*
* Функция создаёт нового пользователя:
* - проверяет логин,
* - создаёт PDA-аккаунт пользователя,
* - переводит 0.01 SOL на счёт администрации,
* - сохраняет логин, ID, pubkey и статус 0 в PDA,
* - обновляет счётчик пользователей.
*/
public class RegisterUserStepOne {
private static final Logger LOG = LoggerFactory.getLogger(RegisterUserStepOne.class);
public static String callRegisterUserStepOne(
String payerPubkeyB58,
String payerPrivkeyB58,
String login,
String userPubkeyB58
) {
try {
// 1. Адрес получателя комиссии
PublicKey feeReceiver = Const.ADMIN_FEE_ACCOUNT;
// 2. PDA-адрес по логину: seed = "u=" + login
String seed = Const.userSeedsPrefix + login;
PublicKey userPda = PublicKey.findProgramAddress(
Collections.singletonList(seed.getBytes(StandardCharsets.UTF_8)),
Const.PROGRAM_ID_key
).getAddress();
// 3. Адрес PDA счётчика пользователей
String counterSeed = Const.USER_COUNTER_SEED;
PublicKey counterPda = PublicKey.findProgramAddress(
Collections.singletonList(counterSeed.getBytes(StandardCharsets.UTF_8)),
Const.PROGRAM_ID_key
).getAddress();
// 4. Сериализация аргументов Anchor
byte[] loginBytes = login.getBytes(StandardCharsets.UTF_8);
byte[] userPubkeyBytes = new PublicKey(userPubkeyB58).toByteArray();
byte[] serializedArgs = SolanaProgramCaller.encodeAnchorArgs(
Arrays.asList("string", "pubkey"),
Arrays.asList(loginBytes, userPubkeyBytes)
);
// 5. Аккаунты, требуемые для вызова
List<AccountMeta> accounts = Arrays.asList(
new AccountMeta(new PublicKey(payerPubkeyB58), true, true), // signer
new AccountMeta(counterPda, false, true), // user_counter
new AccountMeta(userPda, false, true), // user_by_login_pda
new AccountMeta(SystemProgram.PROGRAM_ID, false, false), // system_program
new AccountMeta(feeReceiver, false, true) // fee_receiver
);
// 6. Вызов Anchor-функции
return SolanaProgramCaller.callAnchorFunction(
payerPubkeyB58,
payerPrivkeyB58,
"register_user_step_one",
Const.PROGRAM_ID_key,
accounts,
serializedArgs
);
} catch (Exception e) {
LOG.error("❌ Ошибка вызова register_user_step_one", e);
return null;
}
}
// Для теста
public static void main(String[] args) {
Const.RPC_URL = Const.LOCAL_RPC_URL;
String sig = RegisterUserStepOne.callRegisterUserStepOne(
Const.getKeyByName("key1").getPublicKey(), // кто платит
Const.getKeyByName("key1").getPrivateKey(), //
"testlogin", // логин пользователя
Const.getKeyByName("key2").getPublicKey() // публичный ключ пользователя
);
ResultChecker.check(sig);
}
}

View File

@ -1,156 +0,0 @@
package me.shineup.solana.internal.callSolanaFunc.RegisterUser;
import me.shineup.solana.SolanaWrapper;
import me.shineup.solana.internal.readFromSolana.userCounter.UserCounterReader;
import me.shineup.solana.config.Const;
import me.shineup.solana.internal.standartActions.keysGenerator.KeyPairBase58;
import me.shineup.solana.internal.utils.SolanaProgramCaller;
import me.shineup.solana.internal.utils.resultChecker.ResultChecker;
import org.p2p.solanaj.core.AccountMeta;
import org.p2p.solanaj.core.PublicKey;
import org.p2p.solanaj.programs.SystemProgram;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static me.shineup.solana.internal.standartActions.keysGenerator.SolanaKeyGeneratorManual.generateKeyPair;
/**
* Вызывает Anchor-функцию `register_user_with_one_dev`.
*
* Выполняет регистрацию пользователя и одного устройства.
*/
public class RegisterUserWithOneDev {
private static final Logger LOG = LoggerFactory.getLogger(RegisterUserWithOneDev.class);
// Пример вызова
public static void main(String[] args) {
// Const.RPC_URL = Const.LOCAL_RPC_URL;
Const.RPC_URL=Const.DEVNET_RPC_URL;
try {
String publicKey = "2fppzT84GoDqQe2RCxuK2gjZUrMhkzKgVTY6BzfLF9RX";
String privateKey = "2qTERJQ2EBPsHWNXxhAWW4pBk1beo1BWtifCPsDbrDhw9z5riNLsUUj6BNQ9UbprJq398Zk3Fv21ZGUjRrAU4T73";
SolanaWrapper.getBalance(Const.getKeyByName("key1").publicKey);
// SolanaWrapper.sendSol(Const.getKeyByName("key1").getPrivateKey(),publicKey, 1000000);
SolanaWrapper.getBalance(publicKey);
// KeyPairBase58 keys = generateKeyPair();
String sig = RegisterUserWithOneDev.callRegisterUserWithOneDev(
Const.getKeyByName("key1").getPublicKey(), // payer
Const.getKeyByName("key1").getPrivateKey(), //
"testlogin", // логин
Const.getKeyByName("key2").getPublicKey(), // подпись устройства
Const.getKeyByName("key2").getPublicKey() // x25519
);
ResultChecker.check(sig);
} catch (Exception e) {
LOG.error(e.getMessage());
}
}
public static String callRegisterUserWithOneDev(
String payerPubkeyB58,
String payerPrivkeyB58,
String login,
// String userPubkeyB58, пока userPubkeyB58=payerPubkeyB58
String deviceSignPubkeyB58,
String deviceX25519PubkeyB58
) throws Exception {
// Адреса
PublicKey payer = new PublicKey(payerPubkeyB58);
/** PublicKey userPub = new PublicKey(userPubkeyB58);
* тут в принципе можно передавать на смарт контракт разные параметры
* и получиться что платит один а создаёт аккаунт на другое имя
* это может актуально при расширении, если дописать додумать и т.д.
* но пока это отключено и кто оплатил тна того и оформляем аккаунт
* пока так работает только за себя и можно отпраавлять транзакции
*/
PublicKey userPub = new PublicKey(payerPubkeyB58); // пока тот кто подписал транзакцию, на него же и аккаунт создаём
PublicKey devSignPub = new PublicKey(deviceSignPubkeyB58);
PublicKey devX25519Pub = new PublicKey(deviceX25519PubkeyB58);
// Читаем текущий ID
long currentId = UserCounterReader.getUserCount();
long newId = currentId + 1;
// Счётчик пользователей
PublicKey counterPda = PublicKey.findProgramAddress(
Arrays.asList("user_counter".getBytes(StandardCharsets.UTF_8)),
Const.PROGRAM_ID_key
).getAddress();
// PDA по логину: ["login=", login]
PublicKey loginPda = PublicKey.findProgramAddress(
Arrays.asList(
"login=".getBytes(StandardCharsets.UTF_8),
login.getBytes(StandardCharsets.UTF_8)
),
Const.PROGRAM_ID_key
).getAddress();
// 5 возможных PDA по ID: ["userId=", String.valueOf(newId)]
String idSeedStr = String.valueOf(newId);
List<PublicKey> idCandidates = new ArrayList<>();
for (int i = 0; i < 5; i++) {
byte[] prefix = "userId=".getBytes(StandardCharsets.UTF_8);
byte[] id = idSeedStr.getBytes(StandardCharsets.UTF_8);
byte[] seed1 = prefix;
byte[] seed2 = id;
PublicKey pda = PublicKey.findProgramAddress(Arrays.asList(seed1, seed2), Const.PROGRAM_ID_key).getAddress();
idCandidates.add(pda);
}
// Комиссионный адрес
PublicKey feeReceiver = Const.ADMIN_FEE_ACCOUNT;
// Аргументы Anchor
byte[] loginBytes = login.getBytes(StandardCharsets.UTF_8);
byte[] pubkeyBytes = userPub.toByteArray();
byte[] devSignBytes = devSignPub.toByteArray();
byte[] devX25519Bytes = devX25519Pub.toByteArray();
byte[] serializedArgs = SolanaProgramCaller.encodeAnchorArgs(
Arrays.asList("string", "pubkey", "pubkey", "pubkey"),
Arrays.asList(loginBytes, pubkeyBytes, devSignBytes, devX25519Bytes)
);
// Список аккаунтов
List<AccountMeta> accounts = new ArrayList<AccountMeta>();
accounts.add(new AccountMeta(payer, true, true));
accounts.add(new AccountMeta(counterPda, false, true));
accounts.add(new AccountMeta(loginPda, false, true));
for (PublicKey idPda : idCandidates) {
accounts.add(new AccountMeta(idPda, false, true));
}
accounts.add(new AccountMeta(SystemProgram.PROGRAM_ID, false, false));
accounts.add(new AccountMeta(feeReceiver, false, true));
// Вызов Anchor-функции
return SolanaProgramCaller.callAnchorFunction(
payerPubkeyB58,
payerPrivkeyB58,
"register_user_with_one_dev",
Const.PROGRAM_ID_key,
accounts,
serializedArgs
);
}
}

View File

@ -1,103 +0,0 @@
package me.shineup.solana.internal.readFromSolana.userById;
import me.shineup.solana.exceptions.SolanaException;
import me.shineup.solana.model.UserById;
import org.bitcoinj.core.Base58;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* Десериализует сырые байты из PDA в объект {@link UserById}.
*
* Сейчас поддерживается только формат 1, но общий метод
* parse(...) готов к расширению версий.
*/
class UserByIdParser {
/** Точка входа: определяем формат по первым 4 байтам (LE u32). */
public static UserById parse(byte[] data) throws Exception {
if (data.length < 4)
throw new SolanaException("Недостаточно данных для чтения format_type");
int fmt = ByteBuffer.wrap(data, 0, 4)
.order(ByteOrder.LITTLE_ENDIAN)
.getInt();
switch (fmt) {
case 1:
return parseFormat1(data);
default:
throw new SolanaException("Неподдерживаемый формат: " + fmt);
}
}
/**
* Формат 1 (см. Rust-комментарии):
* [0..4] format
* [4..12] id (u64 LE)
* [12] len(login) u8
* [13..] login
* [...] pubkey 32 байта
* [...] deviceCount u8
* [...] devices ×65 байт (type + 32 + 32)
*/
private static UserById parseFormat1(byte[] data) throws Exception {
int offset = 4;
// id
if (data.length < offset + 8) throw new Exception("Мало байт для id");
long id = ByteBuffer.wrap(data, offset, 8)
.order(ByteOrder.LITTLE_ENDIAN).getLong();
offset += 8;
// login
int loginLen = data[offset] & 0xFF;
offset += 1;
if (data.length < offset + loginLen) throw new Exception("Мало байт для login");
String login = new String(data, offset, loginLen, StandardCharsets.UTF_8);
offset += loginLen;
// pubkey
if (data.length < offset + 32) throw new Exception("Мало байт для pubkey");
byte[] pubkeyBytes = new byte[32];
System.arraycopy(data, offset, pubkeyBytes, 0, 32);
String pubkey58 = Base58.encode(pubkeyBytes);
offset += 32;
// deviceCount
if (data.length < offset + 1) throw new Exception("Мало байт для deviceCount");
int devCount = data[offset] & 0xFF;
offset += 1;
// devices
List<UserById.DeviceInfo> devices = new ArrayList<>();
for (int i = 0; i < devCount; i++) {
if (data.length < offset + 65)
throw new Exception("Мало байт для devices[" + i + "]");
UserById.DeviceInfo d = new UserById.DeviceInfo();
d.deviceType = data[offset] & 0xFF;
byte[] devPub = new byte[32];
byte[] x25519 = new byte[32];
System.arraycopy(data, offset + 1, devPub, 0, 32);
System.arraycopy(data, offset + 33, x25519, 0, 32);
d.devicePubkey = Base58.encode(devPub);
d.x25519Pubkey = Base58.encode(x25519);
devices.add(d);
offset += 65;
}
// собираем объект
UserById u = new UserById();
u.format = 1;
u.id = id;
u.login = login;
u.pubkey = pubkey58;
u.deviceCount = devCount;
u.devices = devices;
return u;
}
}

View File

@ -1,48 +0,0 @@
package me.shineup.solana.internal.readFromSolana.userById;
import me.shineup.solana.exceptions.SolanaException_UserNotFound;
import me.shineup.solana.model.UserById;
import me.shineup.solana.internal.utils.reader.PdaReader;
import me.shineup.solana.config.Const;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
/**
* Читает из PDA данные пользователя по числовому ID
* и десериализует их в {@link UserById}.
*/
public class UserByIdReader {
private static final Logger LOG = LoggerFactory.getLogger(UserByIdReader.class);
/** Получить UserById по ID. */
public static UserById getUserById(long id) throws Exception{
byte[] seed1 = "userId=".getBytes(StandardCharsets.UTF_8);
byte[] seed2 = Long.toString(id).getBytes(StandardCharsets.UTF_8);
String programId = Const.PROGRAM_ID_str;
byte[] data = PdaReader.readTwoSeeds(seed1, seed2, programId);
if (data == null) {
LOG.warn("⚠️ Нет данных в PDA для id={}", id);
throw new SolanaException_UserNotFound();
}
return UserByIdParser.parse(data); // Передаём данные в парсер
}
/** Быстрый тест чтения. */
public static void main(String[] args) {
long id = 10; // поменять на существующий ID
try {
UserById u = UserByIdReader.getUserById(id);
System.out.println("✅ Найден: " + u);
} catch (SolanaException_UserNotFound e) {
System.out.println("⚠️ Пользователь с id=" + id + " не найден");
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@ -1,73 +0,0 @@
package me.shineup.solana.internal.readFromSolana.userByLogin;
import me.shineup.solana.model.UserByLogin;
/**
* Парсер для десериализации массива байт в объект UserByLogin.
* Поддерживает несколько форматов, определяемых по первым 4 байтам (LE u32).
*/
class UserByLoginParser {
/**
* Основной метод: определяет формат по первым 4 байтам, вызывает нужный парсер.
* @param data байты, полученные из PDA
* @return объект UserByLogin
* @throws Exception если формат неизвестен или ошибка разбора
*/
public static UserByLogin parse(byte[] data) throws Exception {
if (data.length < 4) throw new Exception("Недостаточно данных для чтения format_type");
int format = java.nio.ByteBuffer.wrap(data, 0, 4)
.order(java.nio.ByteOrder.LITTLE_ENDIAN).getInt();
switch (format) {
case 1:
return parseFormat1(data);
default:
throw new Exception("Неподдерживаемый формат данных: " + format);
}
}
/**
* Парсит формат 1:
* [0..4] формат
* [4] длина логина (u8)
* [5..X] логин
* [X..X+8] id (u64 LE)
* [X..X+32] pubkey (32 байта)
* [X+32..X+36] status (u32 LE)
*/
public static UserByLogin parseFormat1(byte[] data) throws Exception {
int offset = 4; // после format_type
int loginLen = data[offset] & 0xFF;
offset += 1;
if (data.length < offset + loginLen + 8 + 32 + 4)
throw new Exception("Недостаточно байт для парсинга format 1");
String login = new String(data, offset, loginLen, java.nio.charset.StandardCharsets.UTF_8);
offset += loginLen;
long id = java.nio.ByteBuffer.wrap(data, offset, 8)
.order(java.nio.ByteOrder.LITTLE_ENDIAN).getLong();
offset += 8;
byte[] pubkeyBytes = new byte[32];
System.arraycopy(data, offset, pubkeyBytes, 0, 32);
String pubkey = org.bitcoinj.core.Base58.encode(pubkeyBytes);
offset += 32;
int status = java.nio.ByteBuffer.wrap(data, offset, 4)
.order(java.nio.ByteOrder.LITTLE_ENDIAN).getInt();
UserByLogin result = new UserByLogin();
result.format = 1;
result.login = login;
result.id = id;
result.pubkey = pubkey;
result.status = status;
return result;
}
}

View File

@ -1,58 +0,0 @@
package me.shineup.solana.internal.readFromSolana.userByLogin;
import me.shineup.solana.exceptions.SolanaException_UserNotFound;
import me.shineup.solana.model.UserByLogin;
import me.shineup.solana.internal.utils.reader.PdaReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import me.shineup.solana.config.Const;
import java.nio.charset.StandardCharsets;
/**
* Класс для получения объекта UserByLogin по логину из PDA
*/
public class UserByLoginReader {
private static final Logger LOG = LoggerFactory.getLogger(UserByLoginReader.class);
/**
* Получает объект UserByLogin по логину
* @param login Логин пользователя (например, "sol_user")
* @return Объект UserByLogin или null, если данных нет
* @throws Exception если ошибка соединения или парсинга
*/
public static UserByLogin getUserByLogin(String login) throws Exception {
byte[] seed1 = "login=".getBytes(StandardCharsets.UTF_8);
byte[] seed2 = login.getBytes(StandardCharsets.UTF_8);
String programId = Const.PROGRAM_ID_str;
byte[] data = PdaReader.readTwoSeeds(seed1, seed2, programId);
if (data == null) {
LOG.warn("⚠️ Нет данных в PDA для логина '{}'", login);
throw new SolanaException_UserNotFound();
}
// Передаём данные в парсер
return UserByLoginParser.parse(data);
}
/**
* Тестовый запуск чтения информации о пользователе
*/
public static void main(String[] args) {
String login = "testlogin3"; // замените на нужный логин
try {
UserByLogin user = UserByLoginReader.getUserByLogin(login);
System.out.println("✅ Найден пользователь: " + login);
} catch (SolanaException_UserNotFound e) {
System.out.println("⚠️ Пользователь с id=" + login + " не найден");
} catch (Exception e) {
System.err.println("❌ Ошибка при получении информации о пользователе: " + e.getMessage());
e.printStackTrace();
}
}
}

View File

@ -1,60 +0,0 @@
package me.shineup.solana.internal.readFromSolana.userCounter;
import me.shineup.solana.exceptions.SolanaException;
import me.shineup.solana.internal.utils.reader.PdaReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import me.shineup.solana.config.Const;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* Утилита для получения значения счётчика пользователей из PDA.
*/
public class UserCounterReader {
private static final Logger LOG = LoggerFactory.getLogger(UserCounterReader.class);
/**
* Считывает текущее количество пользователей из PDA "user_counter".
*
* @return количество пользователей (long), либо -1 если нет данных
* @throws Exception при ошибке подключения или чтения
*/
public static long getUserCount() throws Exception {
String seed = "user_counter";
String programId = Const.PROGRAM_ID_str;
byte[] data = PdaReader.readOneSeed(seed, programId);
if (data == null || data.length < 8) {
throw new SolanaException("⚠️ Не удалось прочитать счётчик пользователей — PDA пуст или слишком короткий"); // этого не должно случаться
}
// Считываем первые 8 байт как u64 (Little Endian)
ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
long count = buffer.getLong();
LOG.debug("👥 На данный момент в системе зарегистрировано: {} пользователей", count);
return count;
}
/**
* Тестовый запуск
*/
public static void main(String[] args) {
try {
long count = getUserCount();
if (count >= 0) {
System.out.println("✅ 👥 Количество пользователей системы на данный момент: " + count);
} else {
System.out.println("⚠️ Счётчик не найден или пуст");
}
} catch (Exception e) {
System.err.println("❌ Ошибка при чтении счётчика пользователей: " + e.getMessage());
e.printStackTrace();
}
}
}

View File

@ -1,68 +0,0 @@
package me.shineup.solana.internal.standartActions.airDrops;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import me.shineup.solana.exceptions.SolanaException_LibLogic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import me.shineup.solana.config.Const;
import me.shineup.solana.internal.utils.SolanaRpcClient;
public class SolanaAirdrop {
private static final Logger log = LoggerFactory.getLogger(SolanaAirdrop.class);
//
public static void main(String[] args) {
// Кол-во лампортов (1 SOL = 1_000_000_000 лампортов)
long lamports = 1_000_000_000L;
try {
// Вызываем airdrop
String trx = SolanaAirdrop.requestAirdrop(Const.getKeyByName("key1").getPublicKey(), lamports);
// Вывод результата
log.info("Баланс должен скоро обновиться. " + trx);
} catch (Exception e) {
log.error("Airdrop не удался." + e.getMessage());
}
}
/** запрашивает AirDrop на счёт */
public static String requestAirdrop(String publicKey, long lamports) throws Exception{
// String requestJson = """
// {
// "jsonrpc": "2.0",
// "id": 1,
// "method": "requestAirdrop",
// "params": ["%s", %d]
// }
// """.formatted(publicKey, lamports);
String requestJson = String.format(
"{\n" +
" \"jsonrpc\": \"2.0\",\n" +
" \"id\": 1,\n" +
" \"method\": \"requestAirdrop\",\n" +
" \"params\": [\"%s\", %d]\n" +
"}", publicKey, lamports);
String response = SolanaRpcClient.getInstance().sendRequest(requestJson);
JsonObject json = JsonParser.parseString(response).getAsJsonObject();
if (json.has("result")) {
String txSignature = json.get("result").getAsString();
log.info("✅ Airdrop успешно запрошен. Tx: " + txSignature);
return txSignature;
}
throw new SolanaException_LibLogic("Неизвестный ответ: " + response);
}
}

View File

@ -1,72 +0,0 @@
package me.shineup.solana.internal.standartActions.balanse;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import me.shineup.solana.config.Const;
import me.shineup.solana.internal.utils.SolanaRpcClient;
public class SolanaBalanceChecker {
// для теста
public static void main(String[] args) {
try {
// Const.RPC_URL = Const.LOCAL_RPC_URL;
Const.RPC_URL = "https://api.devnet.solana.com";
//Long balance1 = SolanaBalanceChecker.getBalance(Const.getKeyByName("key1").getPublicKey());
Long balance1 = SolanaBalanceChecker.getBalance("HMww7YSVfwVm4i8sugqj7wyH26dqzHykzv3wzWwzEvPA");
Long balance2 = SolanaBalanceChecker.getBalance(Const.getKeyByName("key2").getPublicKey());
Long balance3 = SolanaBalanceChecker.getBalance(Const.getKeyByName("key3").getPublicKey());
} catch (Exception e) {
log.error( e.getMessage());
}
}
private static final Logger log = LoggerFactory.getLogger(SolanaBalanceChecker.class);
private final Gson gson = new Gson();
/** показывает баланс счёта */
public static long getBalance(String publicKey) throws Exception {
String requestJson = String.format(
"{\n" +
" \"jsonrpc\": \"2.0\",\n" +
" \"id\": 1,\n" +
" \"method\": \"getBalance\",\n" +
" \"params\": [\"%s\"]\n" +
"}", publicKey);
// try {
String responseJson = SolanaRpcClient.getInstance().sendRequest(requestJson);
// Парсим строку JSON в дерево объектов
JsonObject root = JsonParser.parseString(responseJson).getAsJsonObject();
// if (root.has("error")) {
// log.error("Не удалось получить баланс для " + publicKey);
// log.error("Ошибка от RPC: " + root.get("error"));
// new SolanaLibLogicException() ;
// }
log.debug("📥 Ответ от RPC: " + responseJson);
long balance = root.getAsJsonObject("result").get("value").getAsLong();
double sol = balance / 1_000_000_000.0;
log.info("✅ Баланс кошелька " + Const.identifyKey(publicKey) + ": " + Const.lamportsToSol(balance));// + " SOL или лампортов " + balance );
return balance;
// } catch (Exception e) {
// log.error("Не удалось получить баланс для " + publicKey);
// log.error("Ошибка при получении баланса: " + e.getMessage());
// log.error("Ошибка при получении баланса: " + e.getStackTrace()[0] );
// log.error("Ошибка при получении баланса: " + e.getStackTrace()[1] );
// log.error("Ошибка при получении баланса: " + e.getStackTrace()[2] );
// return -1;
// }
}
}

View File

@ -1,26 +0,0 @@
package me.shineup.solana.internal.standartActions.keysGenerator;
/**
* Объект, представляющий пару ключей Solana в формате Base58:
* - публичный ключ (32 байта)
* - приватный ключ (64 байта, включает публичный)
*/
public class KeyPairBase58 {
public final String publicKey;
public final String privateKey;
public KeyPairBase58(String publicKey, String privateKey) {
this.publicKey = publicKey;
this.privateKey = privateKey;
}
@Override
public String toString() {
return "KeyPairBase58{\n" +
" publicKey='" + publicKey + "',\n" +
" privateKey='" + privateKey + "'\n" +
'}';
}
}

View File

@ -1,101 +0,0 @@
package me.shineup.solana.internal.standartActions.keysGenerator;
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.bouncycastle.crypto.KeyGenerationParameters;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import java.security.SecureRandom;
public class SolanaKeyGeneratorManual {
// Новый метод: возвращает пару ключей (в Base58)
public static KeyPairBase58 generateKeyPair() {
// Генератор ключей Ed25519
Ed25519KeyPairGenerator keyGen = new Ed25519KeyPairGenerator();
keyGen.init(new KeyGenerationParameters(new SecureRandom(), 256));
AsymmetricCipherKeyPair keyPair = keyGen.generateKeyPair();
Ed25519PrivateKeyParameters privateKeyParams = (Ed25519PrivateKeyParameters) keyPair.getPrivate();
Ed25519PublicKeyParameters publicKeyParams = (Ed25519PublicKeyParameters) keyPair.getPublic();
byte[] privateKey = privateKeyParams.getEncoded(); // 32 байта
byte[] publicKey = publicKeyParams.getEncoded(); // 32 байта
byte[] solanaSecretKey = new byte[64];
System.arraycopy(privateKey, 0, solanaSecretKey, 0, 32);
System.arraycopy(publicKey, 0, solanaSecretKey, 32, 32);
return new KeyPairBase58(
Base58.encode(publicKey),
Base58.encode(solanaSecretKey)
);
}
public static void main(String[] args) {
KeyPairBase58 keys = generateKeyPair();
System.out.println("✅ Сгенерирован новый кошелёк Solana:");
System.out.println("Публичный ключ (Base58): " + keys.publicKey);
System.out.println("Приватный ключ (Base58, 64 байта): " + keys.privateKey);
System.out.println();
System.out.println("String publicKey = \"" + keys.publicKey + "\";");
System.out.println("String privateKey = \"" + keys.privateKey + "\";");
}
//
// // 👇 Встроенная реализация Base58 (без bitcoinj)
static class Base58 {
private static final char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray();
private static final int BASE_58 = ALPHABET.length;
public static String encode(byte[] input) {
if (input.length == 0) return "";
// Count leading zeros.
int zeros = 0;
while (zeros < input.length && input[zeros] == 0) {
++zeros;
}
// Convert base-256 digits to base-58 digits (plus conversion to ASCII characters)
int size = input.length * 2;
int[] encoded = new int[size];
int length = 0;
for (byte b : input) {
int carry = b & 0xFF;
int i = 0;
for (int j = size - 1; (carry != 0 || i < length) && j >= 0; j--, i++) {
carry += 256 * encoded[j];
encoded[j] = carry % BASE_58;
carry /= BASE_58;
}
length = i;
}
// Skip leading zeros in encoded.
int encodedStart = size - length;
while (encodedStart < size && encoded[encodedStart] == 0) {
encodedStart++;
}
// Translate the result into a string.
StringBuilder result = new StringBuilder(zeros + size - encodedStart);
for (int i = 0; i < zeros; i++) {
result.append('1');
}
for (int i = encodedStart; i < size; i++) {
result.append(ALPHABET[encoded[i]]);
}
return result.toString();
}
}
}

View File

@ -1,73 +0,0 @@
package me.shineup.solana.internal.standartActions.transfer;
import me.shineup.solana.exceptions.SolanaException_RpcConnection;
import me.shineup.solana.internal.standartActions.balanse.SolanaBalanceChecker;
import org.p2p.solanaj.core.Account;
import org.p2p.solanaj.core.PublicKey;
import org.p2p.solanaj.core.Transaction;
import org.p2p.solanaj.rpc.RpcClient;
import org.p2p.solanaj.rpc.RpcException;
import org.p2p.solanaj.programs.SystemProgram;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import me.shineup.solana.config.Const;
import me.shineup.solana.internal.utils.KeyUtils;
import static me.shineup.solana.internal.utils.SolanaProgramCaller.getLatestBlockhash;
import static me.shineup.solana.internal.utils.SolanaProgramCaller.sendTransactionWithBlockhash;
public class SolanaTransfer {
private static final Logger log = LoggerFactory.getLogger(SolanaTransfer.class);
public static void main(String[] args) {
try {
Const.RPC_URL = Const.LOCAL_RPC_URL;
SolanaBalanceChecker.getBalance(Const.getKeyByName("key1").getPublicKey());
SolanaTransfer.sendSol(Const.getKeyByName("key1").getPrivateKey(), Const.getKeyByName("key2").getPublicKey(),100_000_000);
SolanaBalanceChecker.getBalance(Const.getKeyByName("key1").getPublicKey());
SolanaBalanceChecker.getBalance(Const.getKeyByName("key2").getPublicKey());
} catch (Exception e) {
log.error( e.getMessage());
}
}
/**
* Переводит lamports (1 SOL = 1_000_000_000 лампортов)
* @param fromBase58Secret - приватный ключ в виде массива из 64 байт
* @param toAddressBase58 - публичный ключ получателя
* @param lamports - сумма в лампортах
*/
public static String sendSol(String fromBase58Secret, String toAddressBase58, long lamports) throws Exception{
try {
RpcClient rpc = new RpcClient(Const.RPC_URL);
byte[] senderSecretKey58 = KeyUtils.base58ToBytes(fromBase58Secret);
// Загружаем отправителя
Account from = new Account(senderSecretKey58);
PublicKey to = new PublicKey(toAddressBase58);
// Создаём транзакцию
Transaction transaction = new Transaction();
transaction.addInstruction(
SystemProgram.transfer(from.getPublicKey(), to, lamports)
);
// Получаем blockhash
String recentBlockhash = getLatestBlockhash(rpc);
// отправляем транзакцию
String signature = sendTransactionWithBlockhash(rpc, transaction, recentBlockhash, from);
log.info("✅ Перевод " + Const.lamportsToSol(lamports) + " отправлен с " + Const.identifyKey(fromBase58Secret) + " на " + Const.identifyKey(toAddressBase58) + " . Подпись: " + signature);
return signature;
} catch (RpcException e) {
throw new SolanaException_RpcConnection("❌ Ошибка RPC: " + e.getMessage());
}
}
}

View File

@ -1,55 +0,0 @@
package me.shineup.solana.internal.utils;
public class KeyPair {
public final String name;
public final String publicKey;
public final String privateKey;
public KeyPair(String name, String publicKey, String privateKey) {
this.name = name;
this.publicKey = publicKey;
this.privateKey = privateKey;
}
public String getName() {
return name;
}
public String getPublicKey() {
return publicKey;
}
public String getPrivateKey() {
return privateKey;
}
@Override
public String toString() {
return "KeyPair{" +
"name='" + name + '\'' +
", publicKey='" + publicKey + '\'' +
", privateKey='" + privateKey + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
KeyPair keyPair = (KeyPair) o;
if (!name.equals(keyPair.name)) return false;
if (!publicKey.equals(keyPair.publicKey)) return false;
return privateKey.equals(keyPair.privateKey);
}
@Override
public int hashCode() {
int result = name.hashCode();
result = 31 * result + publicKey.hashCode();
result = 31 * result + privateKey.hashCode();
return result;
}
}

View File

@ -1,39 +0,0 @@
package me.shineup.solana.internal.utils;
import java.util.Arrays;
public class KeyUtils {
private static final String BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
public static byte[] base58ToBytes(String base58) {
// Простейший декодер Base58
int[] indexes = new int[128];
Arrays.fill(indexes, -1);
for (int i = 0; i < BASE58_ALPHABET.length(); i++) {
indexes[BASE58_ALPHABET.charAt(i)] = i;
}
byte[] input = new byte[base58.length()];
for (int i = 0; i < base58.length(); i++) {
input[i] = (byte) indexes[base58.charAt(i)];
}
byte[] result = new byte[64]; // длина ключа
int length = 0;
for (byte b : input) {
int carry = b & 0xFF;
int i = 0;
for (int j = result.length - 1; (carry != 0 || i < length) && j >= 0; j--, i++) {
carry += 58 * (result[j] & 0xFF);
result[j] = (byte) (carry % 256);
carry /= 256;
}
length = i;
}
return result;
}
}

View File

@ -1,378 +0,0 @@
package me.shineup.solana.internal.utils;
import me.shineup.solana.config.Const;
import me.shineup.solana.exceptions.SolanaException;
import me.shineup.solana.exceptions.SolanaException_InProgram;
import me.shineup.solana.exceptions.SolanaException_InsufficientFundsForFee;
import me.shineup.solana.exceptions.SolanaException_RpcConnection;
import org.bitcoinj.core.Base58;
import org.p2p.solanaj.core.*;
import org.p2p.solanaj.rpc.RpcClient;
import org.p2p.solanaj.rpc.RpcException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.*;
import java.util.Map;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class SolanaProgramCaller {
// Логгер для вывода отладочной информации
private static final Logger log = LoggerFactory.getLogger(SolanaProgramCaller.class);
/**
* Универсальный метод вызова Anchor-функции на Solana.
*
* @param publicKeyB58 Публичный ключ вызывающего аккаунта в формате base58
* @param privateKeyB58 Приватный ключ в формате base58 (только первые 32 байта или полный 64-байтный seed)
* @param functionName Имя функции Anchor (например: "register_user")
* @param programId Адрес Anchor-программы, в которую делается вызов
* @param accounts Список аккаунтов, необходимых для вызова (AccountMeta)
* @param serializedArgs Сериализованные аргументы вызова (без дискриминатора)
* @return base58-подпись транзакции или null при ошибке
*/
public static String callAnchorFunction(
String publicKeyB58,
String privateKeyB58,
String functionName,
PublicKey programId,
List<AccountMeta> accounts,
byte[] serializedArgs
) throws Exception {
// Создаём RPC клиент для взаимодействия с Solana (через URL, заданный в конфиге)
RpcClient rpc = new RpcClient(Const.RPC_URL);
// Декодируем публичный ключ вызывающего аккаунта из Base58 строки
PublicKey pubKey = new PublicKey(publicKeyB58);
// Декодируем приватный ключ из Base58 строки (ожидается 32 байта seed или 64 байта полного ключа)
byte[] priv = Base58.decode(privateKeyB58);
// Если длина приватного ключа всего 32 байта (seed), то дополняем его 32 байтами публичного ключа
if (priv.length == 32) {
byte[] full = new byte[64]; // 64-байтный ключ для Ed25519 (32 priv + 32 pub)
System.arraycopy(priv, 0, full, 0, 32); // копируем 32 байта приватного ключа
System.arraycopy(pubKey.toByteArray(), 0, full, 32, 32); // дописываем 32 байта публичного ключа
priv = full; // теперь priv содержит полный 64-байтный ключ
}
// Создаём объект аккаунта для подписи транзакции
Account signer = new Account(priv);
// Логируем вызов функции и аккаунт, от имени которого он будет происходить
log.debug("Вызов Anchor-функции '{}' от аккаунта {}", functionName, Const.identifyKey(pubKey.toBase58()));
// Генерируем Anchor-дискриминатор первые 8 байт SHA-256 хеша строки "global:имя_функции"
byte[] discriminator = MessageDigest
.getInstance("SHA-256") // используем SHA-256
.digest(("global:" + functionName).getBytes(StandardCharsets.UTF_8)); // хешируем
discriminator = Arrays.copyOf(discriminator, 8); // берём только первые 8 байт (Anchor-дискриминатор)
// Формируем payload: сначала дискриминатор, потом сериализованные параметры
byte[] data = new byte[discriminator.length + serializedArgs.length];
System.arraycopy(discriminator, 0, data, 0, discriminator.length); // копируем дискриминатор
System.arraycopy(serializedArgs, 0, data, discriminator.length, serializedArgs.length); // копируем параметры
// Создаём инструкцию Solana-транзакции:
// - указываем ID программы (куда отправляем)
// - список аккаунтов, задействованных в вызове (AccountMeta)
// - подготовленные данные (дискриминатор + параметры)
TransactionInstruction instruction = new TransactionInstruction(
programId, // ID Solana-программы (Anchor)
accounts, // список аккаунтов
data // бинарные данные вызова
);
// Создаём Solana транзакцию и добавляем в неё инструкцию
Transaction tx = new Transaction();
tx.addInstruction(instruction);
// Получаем blockhash
String recentBlockhash = getLatestBlockhash(rpc);
// отправляем транзакцию
String sig = sendTransactionWithBlockhash(rpc, tx, recentBlockhash, signer);
log.info("✅ Tx отправлена: {}", sig);
return sig; // возвращаем base58-подпись транзакции
}
// Вспомогательная склейка массивов
public static byte[] concat(byte[]... arrays) {
int length = 0;
for (byte[] a : arrays) length += a.length;
byte[] result = new byte[length];
int pos = 0;
for (byte[] a : arrays) {
System.arraycopy(a, 0, result, pos, a.length);
pos += a.length;
}
return result;
}
/**
* Сериализует аргументы Anchor/Borsh.
* Сейчас поддерживает: string, pubkey.
* - string 4-байтная длина (LE) + UTF-8
* - pubkey 32 байта
*/
public static byte[] encodeAnchorArgs(List<String> types, List<Object> values) {
if (types.size() != values.size())
throw new IllegalArgumentException("Количество типов и значений должно совпадать");
ByteBuffer buf = ByteBuffer.allocate(1024).order(ByteOrder.LITTLE_ENDIAN);
for (int i = 0; i < types.size(); i++) {
String t = types.get(i);
Object v = values.get(i);
switch (t) {
case "string": {
byte[] s = (byte[]) v; // уже UTF-8 байты
buf.putInt(s.length); // 4-byte length LE
buf.put(s);
break;
}
case "pubkey": {
byte[] pk = (byte[]) v;
if (pk.length != 32)
throw new IllegalArgumentException("Pubkey должен быть 32 байта");
buf.put(pk);
break;
}
default:
throw new UnsupportedOperationException("Неизвестный тип: " + t);
}
}
byte[] out = new byte[buf.position()];
buf.flip();
buf.get(out);
return out;
}
/*------------------------------------------------------------
* 1. Получаем актуальный blockhash
*-----------------------------------------------------------*/
@SuppressWarnings("unchecked")
public static String getLatestBlockhash(RpcClient rpc) throws Exception {
Map<String, Object> commitment = new HashMap<String, Object>();
commitment.put("commitment", "finalized");
Map<String, Object> bhJson;
try {
bhJson = rpc.call(
"getLatestBlockhash",
Collections.<Object>singletonList(commitment),
Map.class
);
// остальной код
} catch (Exception e) {
throw new SolanaException_RpcConnection("Неудаётся соедениться при попытке получить blockhash: " + e.getMessage());
}
// --- проверка ответа + поддержка обоих форматов ---
if (bhJson == null) {
throw new SolanaException("При получении номера последнего блока RPC вернул null");
}
if (bhJson.containsKey("error")) {
throw new SolanaException("При получении номера последнего блока: RPC error: " + bhJson.get("error"));
}
Map<String, Object> value;
if (bhJson.containsKey("result")) {
Map<String, Object> result = (Map<String, Object>) bhJson.get("result");
value = (Map<String, Object>) result.get("value");
} else {
value = (Map<String, Object>) bhJson.get("value");
}
if (value == null) {
throw new SolanaException("При получении номера последнего блока: Поле \"value\" отсутствует. Ответ: " + bhJson);
}
String recentBlockhash = (String) value.get("blockhash");
if (recentBlockhash == null) {
throw new SolanaException("При получении номера последнего блока: Поле \"blockhash\" отсутствует. Ответ: " + bhJson);
}
return recentBlockhash;
}
/*------------------------------------------------------------
* 2. Подписываем и отправляем транзакцию, зная blockhash
*-----------------------------------------------------------*/
public static String sendTransactionWithBlockhash(
RpcClient rpc,
Transaction tx,
String recentBlockhash,
Account... signers
) throws Exception {
/* ---- подставляем hash ---- */
tx.setRecentBlockHash(recentBlockhash);
/* ---- подписываем ---- */
if (signers.length == 0) {
throw new IllegalArgumentException("Нужен хотя бы один подписант");
} else if (signers.length == 1) {
tx.sign(signers[0]);
} else {
tx.sign(Arrays.asList(signers));
}
/** // можно сериализовывать в базу 64 и в базу 58 // пока оставил 58
// // ---- сериализация в база 64 ----
// String b64Tx = Base64.getEncoder()
// .encodeToString(tx.serialize());
//
// // ---- отправка ----
// Map<String, Object> cfg = new HashMap<String, Object>();
// cfg.put("skipPreflight", Boolean.FALSE); //Boolean.TRUE);
//
// cfg.put("encoding", "base64"); // ключевое добавление!
//
//
// return rpc.call(
// "sendTransaction",
// Arrays.asList(b64Tx, cfg),
// String.class
// );
*/
/* ---- сериализация и отправка в база 58 ---- */
byte[] txBytes = tx.serialize();
String base58EncodedTx = Base58.encode(txBytes);
Map<String, Object> cfg = new HashMap<>();
cfg.put("skipPreflight", false); // если false - то транзакцию сразу проверяют на ошибки
try {
return rpc.call(
"sendTransaction",
Arrays.asList(base58EncodedTx, cfg),
String.class
);
} catch (RpcException e) {
/* ---------- низкоуровневые сетевые ошибки (не достучались до RPC) */
Throwable cause = e.getCause();
if (cause instanceof ConnectException) {
throw new SolanaException_RpcConnection("Не удалось подключиться к RPC-узлу: " + cause.getMessage());
}
if (cause instanceof SocketTimeoutException) {
throw new SolanaException_RpcConnection("Время ожидания ответа RPC истекло");
}
/* ---------- парсинг сообщения об ошибке от RPC/симуляции -------- */
String err = Objects.toString(e.getMessage(), "");
/* 1) Стандартные InstructionError-ы */
if (err.contains("\"InstructionError\"")) {
// Чаще всего приходит: "InstructionError": [0,"InsufficientFunds"]
if (err.contains("InsufficientFunds")) {
throw new SolanaException_InsufficientFundsForFee();
}
// if (err.contains("AccountNotFound")) {
// throw new SolanaException_AccountNotFound();
// }
// if (err.contains("InvalidAccountData")) {
// throw new SolanaException_InvalidAccountData();
// }
// if (err.contains("InstructionMissing")) {
// throw new SolanaException_InstructionMissing();
// }
// if (err.contains("UninitializedAccount")) {
// throw new SolanaException_UninitializedAccount();
// }
// if (err.contains("IncorrectProgramId")) {
// throw new SolanaException_IncorrectProgramId();
// }
}
/* ) Отдельная проверка на ошибку дебета с пустого аккаунта */
if (err.contains("insufficient funds for rent") // или такое сообщение если акаунт пуст
|| err.matches("(?i).*custom program error: 0x0*1\\b.*")) { // или такое если денег не хватает
throw new SolanaException_InsufficientFundsForFee();
}
/* 2) Кастомная ошибка из твоей программы: "custom program error: 0xXXXX" */
Pattern p = Pattern.compile("custom program error: (0x[0-9a-fA-F]+)");
Matcher m = p.matcher(err);
if (m.find()) {
String hexCode = m.group(1);
int decimalCode = Integer.parseInt(hexCode.substring(2), 16);
throw new SolanaException_InProgram(decimalCode);
}
/* 3) Прочие симуляционные ошибки (можно расширять по вкусу) */
// if (err.contains("Transaction simulation failed")) {
// throw new SolanaException(
// "Симуляция транзакции завершилась ошибкой: " + err
// );
// }
/* 4) Всё остальное оборачиваем как «неизвестную» */
throw new SolanaException("RPC-ошибка: " + err, e);
}
}
}

View File

@ -1,86 +0,0 @@
package me.shineup.solana.internal.utils;
import me.shineup.solana.config.Const;
import me.shineup.solana.exceptions.SolanaException_RpcConnection;
import me.shineup.solana.exceptions.SolanaErrorHandler;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
public class SolanaRpcClient {
private static final Logger log = LoggerFactory.getLogger(SolanaRpcClient.class);
private static SolanaRpcClient instance;
private final OkHttpClient httpClient;
private final String rpcUrl;
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
private SolanaRpcClient(String rpcUrl) {
this.rpcUrl = rpcUrl;
this.httpClient = new OkHttpClient();
}
public static SolanaRpcClient getInstance() {
if (instance == null) {
instance = new SolanaRpcClient(Const.RPC_URL);
}
return instance;
}
public static SolanaRpcClient withCustomUrl(String customUrl) {
instance = new SolanaRpcClient(customUrl);
return instance;
}
/**
* Выполняет JSON-RPC запрос и возвращает ответ как строку.
* В случае ошибки выбрасывает RpcConnectionException.
*/
public String sendRequest(String jsonRequest) throws SolanaException_RpcConnection {
log.debug("📤 Отправка RPC-запроса: {}", jsonRequest);
RequestBody body = RequestBody.create(jsonRequest, JSON);
Request request = new Request.Builder()
.url(rpcUrl)
.post(body)
.addHeader("Content-Type", "application/json")
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
log.error("❌ RPC ответ с ошибкой: {} {}", response.code(), response.message());
throw new SolanaException_RpcConnection("RPC ответ с ошибкой: " + response.code() + " " + response.message());
}
if (response.body() == null) {
log.error("❌ RPC вернул пустое тело");
throw new SolanaException_RpcConnection("Пустой ответ от RPC");
}
String responseText = response.body().string();
log.debug("📥 Получен ответ от RPC: {}", responseText);
// Обработка ошибок, если в ответе есть error
if (responseText.contains("\"error\"")) {
SolanaErrorHandler.handleRpcJsonError(responseText);
}
return responseText;
} catch (IOException e) {
log.error("❌ Ошибка подключения к RPC: {}", e.toString());
throw new SolanaException_RpcConnection("Ошибка подключения к RPC" + e.toString());
} catch (Exception e) {
log.error("❌ Непредвиденная ошибка при вызове RPC: {}", e.toString());
throw new SolanaException_RpcConnection("Непредвиденная ошибка RPC" + e.toString());
}
}
}

View File

@ -1,19 +0,0 @@
package me.shineup.solana.internal.utils.jsonrpc;
import java.util.List;
/** странная фигня котыль который пришлось добавить при переходе на старую библиотеку solanaj для джава 8 */
public class JsonRpcRequest {
public String jsonrpc = "2.0";
public String method;
public List<Object> params;
public int id = 1;
public JsonRpcRequest(String method, List<Object> params) {
this.method = method;
this.params = params;
}
}

View File

@ -1,157 +0,0 @@
package me.shineup.solana.internal.utils.reader;
import me.shineup.solana.exceptions.SolanaException_RpcConnection;
import me.shineup.solana.exceptions.SolanaException;
import org.p2p.solanaj.core.PublicKey;
import org.p2p.solanaj.rpc.RpcClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import me.shineup.solana.config.Const;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* Чтение произвольного PDA-аккаунта в актуальных кластерах Solana (v1.18+).
*
*
* Solana HTTP RPC: getAccountInfo
*
* {
* "jsonrpc":"2.0",
* "id":1,
* "result":{
* "context":{"slot":123456},
* "value":{
* "data":[ // 0 - base64-строка, 1 - "base64"
* "BASE64_STRING",
* "base64"
* ],
* "executable":false,
* "lamports":2039280,
* "owner":"BPFLoaderUpgradeab1e11111111111111111111111",
* "rentEpoch":"18446744073709551615" // - строка-u64 (v1.18+)
* }
* }
* }
*
* Мы читаем только value.data[0] (Base64) остальное не трогаем,
* поэтому не важен тип rentEpoch (строка или число).
*
*/
public final class PdaReader {
/* --------------------------------------------------------------------- */
/* CONFIG & LOG */
/* --------------------------------------------------------------------- */
private static final Logger LOG = LoggerFactory.getLogger(PdaReader.class);
private static final RpcClient RPC = new RpcClient(Const.RPC_URL); // один клиент на класс
private PdaReader() {} // запретить new
/* --------------------------------------------------------------------- */
/* PUBLIC API */
/* --------------------------------------------------------------------- */
/** Чтение PDA, вычисленного из одного текстового сида. */
public static byte[] readOneSeed(String seed, String programId) throws Exception {
PublicKey pda = derivePda(Collections.singletonList(seed.getBytes(StandardCharsets.UTF_8)),
programId);
return fetchAccountData(pda);
}
/** Чтение PDA, вычисленного из двух произвольных сид-массивов. */
public static byte[] readTwoSeeds(byte[] seed1, byte[] seed2, String programId) throws Exception {
PublicKey pda = derivePda(Arrays.asList(seed1, seed2), programId);
return fetchAccountData(pda);
}
/* --------------------------------------------------------------------- */
/* INTERNALS */
/* --------------------------------------------------------------------- */
/** Высчитываем адрес PDA для списка сидов. */
private static PublicKey derivePda(List<byte[]> seeds, String programId) throws Exception {
PublicKey program = new PublicKey(programId);
PublicKey pda = PublicKey.findProgramAddress(seeds, program).getAddress();
LOG.info("📡 PDA адрес: {}", pda.toBase58());
return pda;
}
/**
* Достаём бинарные данные аккаунта.<br>
* Возвращает <code>null</code>, если аккаунт отсутствует или пуст.
*/
@SuppressWarnings("unchecked")
private static byte[] fetchAccountData(PublicKey pda) throws Exception {
// 1) getAccountInfo c base64-энкодингом
Map<String,Object> cfg = new HashMap<>();
cfg.put("encoding", "base64");
cfg.put("commitment", "confirmed");
// Map<String,Object> resp = RPC.call(
// "getAccountInfo",
// Arrays.asList(pda.toBase58(), cfg),
// Map.class // сырое дерево
// );
Map<String, Object> resp;
try {
resp = RPC.call(
"getAccountInfo",
Arrays.asList(pda.toBase58(), cfg),
Map.class
);
} catch (Exception e) { // solanaj бросает RuntimeException/IOException
throw new SolanaException_RpcConnection("Не удалось выполнить getAccountInfo");
}
// Если RPC вернул стандартное поле error разбираем его централизованно
if (resp.get("error") != null) {
throw new SolanaException("RPC вернул поле error"); //тут можно добавить вывод что за конкретная ошибка случилась
}
// 2) Достаём value data[0]
// Map<String,Object> result = (Map<String,Object>) resp.get("result");
// if (result == null) return null;
Map<String,Object> value = (Map<String,Object>) resp.get("value");//result.get("value");
if (value == null) return null;
List<?> dataArr = (List<?>) value.get("data");
if (dataArr == null || dataArr.isEmpty()) return null;
String b64 = (String) dataArr.get(0); // вот он payload
if (b64 == null || b64.isEmpty()) return null;
return Base64.getDecoder().decode(b64);
}
@SuppressWarnings("unchecked")
private static void debugDumpJson(Object node, int indent) {
String pad = String.join("", Collections.nCopies(indent, " ")); // два пробела × indent
if (node instanceof Map) {
Map<String, Object> map = (Map<String, Object>) node;
for (Map.Entry<String, Object> e : map.entrySet()) {
System.out.println(pad + e.getKey() + ":");
debugDumpJson(e.getValue(), indent + 1);
}
} else if (node instanceof Iterable) {
Iterable<?> it = (Iterable<?>) node;
for (Object val : it) {
debugDumpJson(val, indent + 1);
}
} else {
System.out.println(pad + String.valueOf(node));
}
}
}

View File

@ -1,29 +0,0 @@
package me.shineup.solana.internal.utils.resultChecker;
/**
* Утилита для проверки результата выполнения транзакции по подписи.
*
* Данный класс:
* - выводит подпись транзакции в консоль
* - ждёт подтверждения транзакции через Solana RPC
* - выводит статус транзакции после завершения
*/
public class ResultChecker {
/**
* Проверяет статус транзакции по её подписи.
*
* @param sig Подпись (signature) транзакции в base58-формате.
*/
public static void check(String sig) {
System.out.println("📦 Signature: " + sig);
if (sig != null) {
TransactionStatusChecker.waitForConfirmation(sig);
SolanaTransactionStatusChecker.getTransactionStatus(sig);
} else {
System.out.println("⚠️ Подпись транзакции пуста или null");
}
}
}

View File

@ -1,210 +0,0 @@
package me.shineup.solana.internal.utils.resultChecker;
import me.shineup.solana.config.Const;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import me.shineup.solana.internal.utils.SolanaRpcClient;
public class SolanaTransactionStatusChecker {
private static final Logger log = LoggerFactory.getLogger(SolanaTransactionStatusChecker.class);
private static final Gson gson = new Gson();
public static boolean getTransactionStatus(String signature) {
boolean isOk = false;
String requestJson = String.format(
"{\n" +
" \"jsonrpc\": \"2.0\",\n" +
" \"id\": 1,\n" +
" \"method\": \"getTransaction\",\n" +
" \"params\": [\"%s\", { \"encoding\": \"jsonParsed\", \"commitment\": \"finalized\" }]\n" +
"}", signature);
// String requestJson = """
// {
// "jsonrpc": "2.0",
// "id": 1,
// "method": "getTransaction",
// "params": ["%s", { "encoding": "jsonParsed", "commitment": "finalized" }]
// }
// """.formatted(signature);
try {
String responseJson = SolanaRpcClient.getInstance().sendRequest(requestJson);
JsonObject root = gson.fromJson(responseJson, JsonObject.class);
if (root.has("error")) {
log.error("❌ Ошибка при запросе транзакции: {}", root.get("error"));
return false;
}
JsonElement resultElement = root.get("result");
if (resultElement == null || resultElement.isJsonNull()) {
log.warn("⚠️ Транзакция с сигнатурой {} не найдена или ещё не финализирована", signature);
return false;
}
JsonObject result = root.getAsJsonObject("result");
if (result == null) {
log.warn("⚠️ Транзакция с сигнатурой {} не найдена", signature);
return false;
}
JsonObject meta = result.getAsJsonObject("meta");
if (meta == null) {
log.warn("⚠️ Нет информации о результате выполнения");
return false;
}
JsonElement err = meta.get("err");
if (err == null || err.isJsonNull()) {
log.info("✅ Транзакция завершилась успешно");
isOk = true;
} else {
log.warn("⚠️ Транзакция завершилась с ошибкой. Код ошибки: {}", extractCustomErrorCode(err));
isOk = false;
}
// 💸 Вывод комиссии
if (meta.has("fee")) {
long fee = meta.get("fee").getAsLong();
log.info("💸 Комиссия за транзакцию: {} лампортов", fee);
}
// Получаем message accountKeys
JsonObject message = result.getAsJsonObject("transaction").getAsJsonObject("message");
JsonArray accountKeys = message.getAsJsonArray("accountKeys");
// 🔄 Выводим изменения балансов
logBalanceChanges(meta, accountKeys);
// 💸 Общая сумма списанных лампортов (не только комиссия)
long totalSpent = calculateTotalSpent(meta);
log.info("💸 Общая сумма списанных лампортов (включая переводы и аренду аккаунтов): {}", totalSpent);
// Выводим лог исполнения (если есть)
if (meta.has("logMessages")) {
JsonElement logsElement = meta.get("logMessages");
if (logsElement != null && logsElement.isJsonArray()) {
JsonArray logs = logsElement.getAsJsonArray();
log.info("📝 Логи исполнения:");
for (JsonElement logLine : logs) {
log.info("" + logLine.getAsString());
}
} else if (logsElement != null && logsElement.isJsonNull()) {
log.info("📝 Логи исполнения: отсутствуют (null)");
} else {
log.warn("📝 Логи исполнения: неожиданный формат данных");
}
}
} catch (Exception e) {
log.error("❌ Ошибка при запросе статуса транзакции", e);
return false;
}
return isOk;
}
/**
* Извлекает значение Custom Anchor ошибки из поля "err", если оно есть.
*
* Пример ожидаемого JSON:
* {
* "InstructionError": [0, { "Custom": 10000 }]
* }
*
* @param errJson поле "err" из ответа Solana
* @return числовой код ошибки (например, 10000) или null, если не найден
*/
public static Integer extractCustomErrorCode(JsonElement errJson) {
if (errJson == null || errJson.isJsonNull()) return null;
try {
JsonObject errObj = errJson.getAsJsonObject();
if (errObj.has("InstructionError")) {
JsonArray instrError = errObj.getAsJsonArray("InstructionError");
if (instrError.size() == 2 && instrError.get(1).isJsonObject()) {
JsonObject customObj = instrError.get(1).getAsJsonObject();
if (customObj.has("Custom")) {
return customObj.get("Custom").getAsInt();
}
}
}
} catch (Exception e) {
// безопасно возвращаем null
}
return null;
}
/**
* Логирует изменения балансов всех аккаунтов, участвующих в транзакции.
*
* @param meta JSON-объект "meta" из транзакции (включает preBalances и postBalances)
* @param accountKeys Список accountKeys из message (содержит pubkey для каждого аккаунта)
*/
public static void logBalanceChanges(JsonObject meta, JsonArray accountKeys) {
JsonArray preBalances = meta.getAsJsonArray("preBalances");
JsonArray postBalances = meta.getAsJsonArray("postBalances");
log.info("💰 Изменения балансов по аккаунтам:");
for (int i = 0; i < preBalances.size(); i++) {
long pre = preBalances.get(i).getAsLong();
long post = postBalances.get(i).getAsLong();
long delta = post - pre;
String pubkey = accountKeys.get(i)
.getAsJsonObject()
.get("pubkey")
.getAsString();
double usdValue = delta * 150.0 / 1_000_000_000;
String usdFormatted = String.format("%.5f", usdValue);
log.info("🔄 {}: {} → {} (Δ: {} лампортов) ≈ {} $", Const.identifyKey(pubkey), pre, post, delta, usdFormatted);
}
}
/**
* Считает общую сумму списанных лампортов по всем аккаунтам,
* т.е. где postBalance < preBalance.
*
* @param meta JSON-объект "meta" из транзакции
* @return общее количество списанных лампортов
*/
public static long calculateTotalSpent(JsonObject meta) {
JsonArray preBalances = meta.getAsJsonArray("preBalances");
JsonArray postBalances = meta.getAsJsonArray("postBalances");
long totalSpent = 0;
for (int i = 0; i < preBalances.size(); i++) {
long pre = preBalances.get(i).getAsLong();
long post = postBalances.get(i).getAsLong();
if (post < pre) {
totalSpent += (pre - post);
}
}
return totalSpent;
}
}

View File

@ -1,68 +0,0 @@
package me.shineup.solana.internal.utils.resultChecker;
import org.p2p.solanaj.rpc.RpcClient;
import org.p2p.solanaj.rpc.types.SignatureStatuses;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import me.shineup.solana.config.Const;
import java.util.Collections;
import java.util.List;
public class TransactionStatusChecker {
private static final Logger LOG = LoggerFactory.getLogger(TransactionStatusChecker.class);
/**
* Проверяет статус транзакции по подписи, делает до 10 попыток с паузой.
*
* @param signature Подпись транзакции (Base58)
* @return true, если транзакция прошла успешно, иначе false
*/
public static boolean waitForConfirmation(String signature) {
RpcClient rpc = new RpcClient(Const.RPC_URL);
try {
for (int attempt = 1; attempt <= 10; attempt++) {
LOG.info("🔍 Попытка {} проверки транзакции {}", attempt, signature);
SignatureStatuses statuses = rpc.getApi().getSignatureStatuses(Collections.singletonList(signature), true);
List<SignatureStatuses.Value> infoList = statuses.getValue();
if (infoList != null && !infoList.isEmpty()) {
SignatureStatuses.Value info = infoList.get(0);
if (info != null) {
String status = info.getConfirmationStatus(); // Или getConfirmationStatusString() в других версиях
LOG.info("⏳ Статус: {}", status);
if ("finalized".equals(status)) {
LOG.info("🎉 Финализирована большинством");
//todo
return true;
} else if ("processed".equals(status)) {
LOG.info("Транзакция принята в пул");
} else if ("confirmed".equals(status)) {
LOG.info("Транзакция вошла в блок, но не финализирована");
} else {
LOG.info("Хер его знает ");
}
} else {
LOG.info("Пока такой транзакции нету");
}
} else {
LOG.info("Запрос вообще не удался");
}
Thread.sleep(3000); // Ждать 3 секунды перед следующей попыткой
}
LOG.warn("❌ Транзакция не подтвердилась за 10 попыток: {}", signature);
} catch (Exception e) {
LOG.error("❌ Ошибка при проверке транзакции", e);
}
return false;
}
}

View File

@ -1,126 +0,0 @@
package me.shineup.solana.internal.utils.resultChecker;
// Демонстрационный класс: единая точка проверки статуса транзакции Solana
// ----------------------------------------------------------------------
// Содержит:
// 1. Enum TxStatus перечень возможных состояний
// 2. Статический метод getTxStatus(...) собственно проверка
// 3. Метод main(...) пример использования
// ----------------------------------------------------------------------
import java.util.Collections;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import me.shineup.solana.config.Const;
import me.shineup.solana.internal.utils.SolanaRpcClient;
import me.shineup.solana.model.TxStatus;
import org.p2p.solanaj.rpc.RpcClient;
import org.p2p.solanaj.rpc.types.SignatureStatuses;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class TransactionStatusHelper {
// // --------------------------------------------------
// // 1. Enum возможные состояния транзакции
// // --------------------------------------------------
// public enum TxStatus {
// // Повторять запрос, если лимит времени не исчерпан:
// NOT_FOUND, // RPCузел ещё не видел подпись */
// PROCESSED, // Подпись обработана, но не попала в блок
// CONFIRMED, // Транзакция в блоке, но блок не финализирован
//
// // Не повторять, даже если лимит не исчерпан:
// FINALIZED_SUCCESS, // Блок финализирован и meta.err == null
// FINALIZED_ERROR, // Блок финализирован, но meta.err содержит ошибку
// // Не повторять или повторять ограниченно (13 раза, или N секунд):
// UNKNOWN, // Не удалось определить статус (RPCошибка, исключение и т.д.) */
// NETWORK_ERROR // Не удаётся подключиться по сете
// }
// --------------------------------------------------
// 2. Метод проверки статуса
// --------------------------------------------------
private static final Logger log = LoggerFactory.getLogger(TransactionStatusHelper.class);
/**
* Универсальная проверка статуса транзакции.
* @param signature подпись (transaction signature / id)
* @return текущее состояние {@link TxStatus}
*/
public static TxStatus getTxStatus(String signature) {
RpcClient rpc = new RpcClient(Const.RPC_URL);
try {
// --- 1. Быстрый запрос STATUSES
// Пробуем получить статус транзакции
SignatureStatuses.Value info;
try {
info = rpc.getApi()
.getSignatureStatuses(Collections.singletonList(signature), true)
.getValue()
.get(0);
} catch (Exception e) {
log.error("🔌 Ошибка подключения к RPC или сети", e);
return TxStatus.NETWORK_ERROR; // Тут можно вернуть специальный статус NETWORK_ERROR, если нужно точнее
}
if (info == null) {
return TxStatus.NOT_FOUND; // подпись ещё не дошла до RPC
}
String commit = info.getConfirmationStatus(); // processed / confirmed / finalized
switch (commit) {
case "processed":
return TxStatus.PROCESSED;
case "confirmed":
return TxStatus.CONFIRMED;
case "finalized":
// --- 2. Дошли до финала нужен getTransaction, чтобы узнать err
String reqJson = String.format(
"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getTransaction\"," +
"\"params\":[\"%s\",{\"encoding\":\"json\",\"commitment\":\"finalized\"}]}",
signature);
// SolanaRpcClient ваша внутренняя обёртка. Замените на свой httpклиент.
String raw = SolanaRpcClient.getInstance().sendRequest(reqJson);
JsonObject meta = JsonParser.parseString(raw)
.getAsJsonObject()
.getAsJsonObject("result")
.getAsJsonObject("meta");
return meta.get("err").isJsonNull() ? TxStatus.FINALIZED_SUCCESS : TxStatus.FINALIZED_ERROR;
default:
return TxStatus.UNKNOWN;
}
} catch (Exception e) {
log.error("RPC error while checking {}", signature, e);
return TxStatus.UNKNOWN;
}
}
// --------------------------------------------------
// 3. Пример использования
// --------------------------------------------------
/**
* Точка входа для быстрого ручного теста.
* Запускайте так:
* java TransactionStatusHelper <signature>
* Если аргумент не передан, используется демоподпись.
*/
public static void main(String[] args) {
// Подпись можно передать первым аргументом
String sig = (args.length > 0)
? args[0]
: "4bxeRu4pNk9UzN6QgTPy6Q3DLJ6ZQt3xMkUQnDzohBpxjMVqRyba2Riqm8o7MBYo2YfSfvqbMFxRRWwu1XbbeiKf";
TxStatus status = getTxStatus(sig);
System.out.println("Статус транзакции " + sig + "" + status);
}
}

View File

@ -1,22 +0,0 @@
package me.shineup.solana.model;
/**
* Статус транзакции в Solana.
* Используется для отслеживания состояния по сигнатуре.
*/
// --------------------------------------------------
// 1. Enum возможные состояния транзакции
// --------------------------------------------------
public enum TxStatus {
// Повторять запрос, если лимит времени не исчерпан:
NOT_FOUND, // RPCузел ещё не видел подпись */
PROCESSED, // Подпись обработана, но не попала в блок
CONFIRMED, // Транзакция в блоке, но блок не финализирован
// Не повторять, даже если лимит не исчерпан:
FINALIZED_SUCCESS, // Блок финализирован и meta.err == null
FINALIZED_ERROR, // Блок финализирован, но meta.err содержит ошибку
// Не повторять или повторять ограниченно (13 раза, или N секунд):
UNKNOWN, // Не удалось определить статус (RPCошибка, исключение и т.д.) */
NETWORK_ERROR // Не удаётся подключиться по сете
}

View File

@ -1,52 +0,0 @@
package me.shineup.solana.model;
import java.util.List;
/**
* Java-представление Solana-структуры UserById.
*
* Содержит:
* format версия сериализации;
* id числовой ID пользователя;
* login строковый логин;
* pubkey публичная подпись пользователя (Base58);
* deviceCount сколько устройств хранится;
* devices список устройств (DeviceInfo).
*/
public class UserById {
/* ---------- поля, идущие в сериализации ---------- */
public int format;
public long id;
public String login;
public String pubkey;
public int deviceCount;
public List<DeviceInfo> devices;
/** Вложенный класс описания одного устройства. */
public static class DeviceInfo {
public int deviceType; // 1 байт в on-chain
public String devicePubkey; // 32 байта (Base58)
public String x25519Pubkey; // 32 байта (Base58)
@Override
public String toString() {
return "DeviceInfo{type=" + deviceType +
", devPub=" + devicePubkey +
", x25519=" + x25519Pubkey + '}';
}
}
@Override
public String toString() {
return "UserById{" +
"format=" + format +
", id=" + id +
", login='" + login + '\'' +
", pubkey='" + pubkey + '\'' +
", deviceCount=" + deviceCount +
", devices=" + devices +
'}';
}
}

View File

@ -1,23 +0,0 @@
package me.shineup.solana.model;
/**
* Класс описывающий объект UserByLogin, аналогичный структуре на стороне Solana.
*/
public class UserByLogin {
public int format; // формат сериализации (например, 1)
public String login; // логин
public long id; // числовой ID
public String pubkey; // публичный ключ (base58)
public int status; // статус (например, 0)
@Override
public String toString() {
return "UserByLogin{" +
"format=" + format +
", login='" + login + '\'' +
", id=" + id +
", pubkey='" + pubkey + '\'' +
", status=" + status +
'}';
}
}

View File

@ -1,2 +0,0 @@
./gradlew :solana-shine-lib:build
- команда что бы сбилдить библиотеку

View File

@ -1 +0,0 @@
solana airdrop 1 HMww7YSVfwVm4i8sugqj7wyH26dqzHykzv3wzWwzEvPA --url https://api.devnet.solana.com

View File

@ -1,118 +0,0 @@
package com.shine.solana.test;
import me.shineup.solana.SolanaWrapper;
import me.shineup.solana.internal.standartActions.keysGenerator.KeyPairBase58;
import me.shineup.solana.config.Const;
import me.shineup.solana.SolanaTxWatcher;
import me.shineup.solana.model.TxStatus;
public class AddUserExemple {
public static void main(String[] args) {
try {
// SolanaWrapper.setRPC_URL_testNet();
// SolanaWrapper.requestAirdrop(Const.getKeyByName("key3").getPublicKey(), 1);
SolanaWrapper.getBalance(Const.getKeyByName("key3").getPublicKey());
// long id =1;
// try {
// UserById u = SolanaWrapper.getUserById(id);
// System.out.println(u);
// UserByLogin u2 =SolanaWrapper.getUserByLogin(u.login);
// System.out.println(u2);
// } catch (Exception e) {
// e.printStackTrace();
// }
KeyPairBase58 k = SolanaWrapper.generateNewWallet();
String sig;
// sig = SolanaWrapper.sendSol(Const.getKeyByName("key1").getPrivateKey(), k.publicKey, 100000000);
SolanaWrapper.getBalance(Const.getKeyByName("key1").getPublicKey());
// SolanaWrapper.getBalance(k.publicKey);
// SolanaWrapper.getBalance(k.publicKey);
// SolanaWrapper.getBalance(k.publicKey);
// String sig = SolanaWrapper.registerUserWithOneDev(k.publicKey,k.privateKey,"ivan11263456",k.publicKey, k.publicKey);
sig = SolanaWrapper.registerUserWithOneDev(Const.getKeyByName("key1").getPublicKey(), Const.getKeyByName("key1").getPrivateKey(),
"ivan1261", Const.getKeyByName("key1").getPublicKey(), Const.getKeyByName("key1").getPublicKey());
SolanaTxWatcher watcher = new SolanaTxWatcher(sig);
while (watcher.shouldRetry()) { // проверяем, нужно ли ещё опрашивать
watcher.updateStatus(); // запрашиваем статус через RPC
System.out.println(
watcher.getStatus() + " | " + // печатаем статус
watcher.getStatusComment());
try {
Thread.sleep(SolanaTxWatcher.getRetryIntervalMs()); // ждём секунду
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (watcher.isSuccess()) {
System.out.println("✅ Транзакция прошла успешно!");
} else {
System.out.println("❌ Завершили слежение без успеха.");
}
// TxStatus st = SolanaWrapper.getTransactionStatus(sig);
// System.out.println("Статус транзакции " + sig + "" + st);
// TxStatus result = waitTransactionFinalized(sig);
//
// System.out.println("Итоговый статус: " + result);
// if (result == TxStatus.FINALIZED_SUCCESS) {
// System.out.println("✅ Транзакция прошла успешно!");
// } else if (result == TxStatus.FINALIZED_ERROR) {
// System.out.println("❌ Транзакция завершилась с ошибкой.");
// }
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Ожидает финализации транзакции, проверяя статус каждую секунду.
* Максимальное время ожидания 20 секунд.
*/
public static TxStatus waitTransactionFinalized(String signature) {
final int maxSeconds = 20;
TxStatus st;
for (int i = 0; i < maxSeconds; i++) {
try {
st = SolanaWrapper.getTransactionStatus(signature);
} catch (Exception e) {
throw new RuntimeException(e); //и тут дописывать проверки если надо
}
System.out.println("Статус транзакции " + signature + "" + st);
// Если достигли финального состояния возвращаем
if (st == TxStatus.FINALIZED_SUCCESS || st == TxStatus.FINALIZED_ERROR) {
return st;
}
// Если транзакция не найдена или в неизвестном состоянии тоже выходим
if (st == TxStatus.UNKNOWN) {
System.out.println("Ошибка или подпись не найдена. Прерываем ожидание.");
return st;
}
// Подождать 1 секунду
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Ожидание прервано.");
return TxStatus.UNKNOWN;
}
}
System.out.println("⏱ Превышен лимит ожидания (20 сек).");
return TxStatus.UNKNOWN;
}
}

View File

@ -1,32 +0,0 @@
package com.shine.solana.test;
import me.shineup.solana.SolanaWrapper;
import me.shineup.solana.config.Const;
public class AirDrop {
public static void main(String[] args) {
// Если первый аргумент равен строке "1" запрашиваем airdrop,
// иначе просто выводим баланс.
try {
// SolanaWrapper.setRPC_URL("https://api.testnet.solana.com");
// SolanaWrapper.setRPC_URL("https://api.devnet.solana.com");
boolean needAirdrop = args.length > 0 && "1".equals(args[0]);
// needAirdrop = true;
if (needAirdrop) {
long oneSolLamports = 1_000_000_000L; // 1 SOL в лампортах
SolanaWrapper.requestAirdrop(
Const.getKeyByName("key1").getPublicKey(),
oneSolLamports);
} else {
SolanaWrapper.getBalance(Const.getKeyByName("key1").getPublicKey());
SolanaWrapper.getBalance(Const.getKeyByName("key2").getPublicKey());
SolanaWrapper.getBalance(Const.getKeyByName("key3").getPublicKey());
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@ -1,47 +0,0 @@
package com.shine.solana.test;
import me.shineup.solana.SolanaWrapper;
import me.shineup.solana.config.Const;
import me.shineup.solana.internal.callSolanaFunc.InitializeUserCounter.InitializeUserCounter;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
private static final ExecutorService executorService = Executors.newCachedThreadPool();
public static void main(String[] args) {
// SolanaWrapper.setRPC_URL("https://api.devnet.solana.com");
// InitializeUserCounter
long oneSolLamports = 5_000_000_000L; // 1 SOL в лампортах
try {
SolanaWrapper.requestAirdrop(
"FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P",
oneSolLamports);
System.out.println(SolanaWrapper.getBalance("FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P"));//H6q58ytZk5sd3KQisC57R6urKUjn5PaWKLCv7sNZtj4i"));
System.out.println("Баланс Shine" + SolanaWrapper.getBalance( "jZnbqzrbKaksVomiqAxsKGiz5ct8rHPgcNDiiKyTZDD"));
} catch (Exception e) {
System.out.println(e.getMessage());
}
// 5RpEoxRKSr2norQP3vEnq9XokQGh9EbGN8q8xUUVAdm1M5mTD1vMuyJPYJfViMWFf6c8qT5mj2bt64gLE2zm6VG3 - тестовые в фантом валет
// FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P
//
// executorService.submit(() -> {
// try {
// System.out.println(SolanaWrapper.getUserById(1).login);
// } catch (Exception e) {
// System.out.println(e.getMessage());
// }
// });
}
}

View File

@ -1,58 +0,0 @@
ai@ai-home:~$ ~/.cargo/bin/spl-token --version
spl-token-cli 5.3.0
//создание
ai@ai-home:~$ ~/.cargo/bin/spl-token create-token
Creating token Dt7t2kKXYT8UqdioTQZeH4SRXqDA2kh2dscqZQmfr1Rr under program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
Address: Dt7t2kKXYT8UqdioTQZeH4SRXqDA2kh2dscqZQmfr1Rr
Decimals: 9
Signature: 656MfR7x4K5fZJEsf1nJTk5gorMkuZtSCw2YGzvtAEHLyftDsYhoXspGspEhD1Jto8pNNfkyNVb2jRVZPBg5gi9u
------------------------------------------------------------------
//Эмиссия
ai@ai-home:~$ ~/.cargo/bin/spl-token mint Dt7t2kKXYT8UqdioTQZeH4SRXqDA2kh2dscqZQmfr1Rr 10
Minting 10 tokens
Token: Dt7t2kKXYT8UqdioTQZeH4SRXqDA2kh2dscqZQmfr1Rr
Recipient: AcyCkqNEdR78s6wsuhwEeqfSe4BHGuK1EkzGcUbArifq
Signature: 5SWjE1cHrcWEnArZuUsx4efSVXKWsADxPsuEZL2KbeWfUGy4DqQe4Mdg3AhKtXMLLmRMeQYSSX1mB7VgzLcbEGao
ai@ai-home:~$ ~/.cargo/bin/spl-token balance Dt7t2kKXYT8UqdioTQZeH4SRXqDA2kh2dscqZQmfr1Rr
10
------------------------------------------------------------------------------
✅ 2. Перевод токенов на существующий аккаунт
Шаг 1. Создаём токен-аккаунт получателя (один раз)
Вариант A: вручную
~/.cargo/bin/spl-token create-account <адресокена> --owner <публичный_адрес_получателя>
Вариант B: автоматически при переводе
~/.cargo/bin/spl-token transfer <адресокена> 1 <публичный_адрес_получателя> --fund-recipient
Флаг --fund-recipient автоматически создаст токен-аккаунт получателя и оплатит аренду за счёт твоего кошелька.
-----
ai@ai-home:~$ ~/.cargo/bin/spl-token transfer Dt7t2kKXYT8UqdioTQZeH4SRXqDA2kh2dscqZQmfr1Rr 1 HMww7YSVfwVm4i8sugqj7wyH26dqzHykzv3wzWwzEvPA --fund-recipient
Transfer 1 tokens
Sender: AcyCkqNEdR78s6wsuhwEeqfSe4BHGuK1EkzGcUbArifq
Recipient: HMww7YSVfwVm4i8sugqj7wyH26dqzHykzv3wzWwzEvPA
Recipient associated token account: GJQPLqNXZNXATm4J1sGgtiKKCk9wFced2NXhwGxc5hfA
Funding recipient: GJQPLqNXZNXATm4J1sGgtiKKCk9wFced2NXhwGxc5hfA
Signature: 5R3bGiqC49GBHJFMTGMRvBxNt12nkvmKrwRxUQVcvhyRn23rmr7YAaWxzvedkneuSr3YUmAQXk7UFyorhdr5WvhY
ai@ai-home:~$ ~/.cargo/bin/spl-token accounts
Token Balance
-----------------------------------------------------
Dt7t2kKXYT8UqdioTQZeH4SRXqDA2kh2dscqZQmfr1Rr 9
ai@ai-home:~$ ~/.cargo/bin/spl-token accounts --owner ^C
ai@ai-home:~$ ~/.cargo/bin/spl-token accounts --owner HMww7YSVfwVm4i8sugqj7wyH26dqzHykzv3wzWwzEvPA
Token Balance
-----------------------------------------------------
Dt7t2kKXYT8UqdioTQZeH4SRXqDA2kh2dscqZQmfr1Rr 1

View File

@ -1,110 +0,0 @@
package com.shine.utils;
import java.util.ArrayList;
import java.util.List;
/**
* Класс для конвертации приватного ключа Solana из Base58 в формат JSON-массива чисел,
* который понимает Solana CLI.
*
* Использование:
* 1. Вставьте свой приватный ключ в Base58 в main().
* 2. Запустите программу она выведет строку JSON, которую можно сохранить в файл.
* 3. С помощью этого файла можно подключить кошелек в Solana CLI.
*/
public class SolanaKeyBase58Converter {
//
// 🔹 ТЕСТОВЫЙ ЗАПУСК
//
public static void main(String[] args) {
// Вставьте сюда свой приватный ключ в Base58
String base58Key = "5RpEoxRKSr2norQP3vEnq9XokQGh9EbGN8q8xUUVAdm1M5mTD1vMuyJPYJfViMWFf6c8qT5mj2bt64gLE2zm6VG3"; //"ВСТАВЬ_СЮДА_СВОЙ_ПРИВАТНЫЙ_КЛЮЧ";
// Конвертация в JSON-массив чисел
String jsonKey = base58ToJson(base58Key);
// Вывод в консоль
System.out.println("Скопируйте эту строку и сохраните в файл (например, mywallet.json):");
System.out.println(jsonKey);
}
//
// 🔹 ОСНОВНОЙ МЕТОД КОНВЕРТАЦИИ
//
/**
* Преобразует приватный ключ из Base58 в строку JSON-массива чисел,
* которую можно сохранить в файл для Solana CLI.
*
* @param base58Key приватный ключ в формате Base58 (строка)
* @return строка вида "[25,156,13,48,...,147]"
*/
public static String base58ToJson(String base58Key) {
byte[] bytes = decodeBase58(base58Key);
// Формируем JSON-массив из байт
StringBuilder json = new StringBuilder("[");
for (int i = 0; i < bytes.length; i++) {
json.append(bytes[i] & 0xFF); // "& 0xFF" нужно, чтобы убрать знак и получить 0255
if (i < bytes.length - 1) {
json.append(",");
}
}
json.append("]");
return json.toString();
}
//
// 🔹 ДЕКОДЕР BASE58 БАЙТЫ
//
// Алфавит Base58 (стандарт Solana/Bitcoin)
private static final String BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
/**
* Декодирует строку Base58 в байтовый массив.
*
* @param input приватный ключ в формате Base58
* @return байтовый массив приватного ключа
*/
private static byte[] decodeBase58(String input) {
java.math.BigInteger num = java.math.BigInteger.ZERO;
java.math.BigInteger base = java.math.BigInteger.valueOf(58);
// Переводим Base58 в большое число
for (char c : input.toCharArray()) {
int digit = BASE58_ALPHABET.indexOf(c);
if (digit < 0) {
throw new IllegalArgumentException("Недопустимый символ в Base58: " + c);
}
num = num.multiply(base).add(java.math.BigInteger.valueOf(digit));
}
// Конвертируем большое число в байты
List<Byte> byteList = new ArrayList<>();
byte[] numBytes = num.toByteArray();
// Убираем возможный ведущий 0x00 (для BigInteger)
int startIndex = (numBytes.length > 1 && numBytes[0] == 0) ? 1 : 0;
for (int i = startIndex; i < numBytes.length; i++) {
byteList.add(numBytes[i]);
}
// Добавляем ведущие нули для каждого символа '1' в Base58
for (char c : input.toCharArray()) {
if (c == '1') {
byteList.add(0, (byte) 0);
} else {
break;
}
}
// Преобразуем в обычный массив
byte[] result = new byte[byteList.size()];
for (int i = 0; i < byteList.size(); i++) {
result[i] = byteList.get(i);
}
return result;
}
}

View File

@ -1,37 +0,0 @@
solana/phantomWallet.json
// 5RpEoxRKSr2norQP3vEnq9XokQGh9EbGN8q8xUUVAdm1M5mTD1vMuyJPYJfViMWFf6c8qT5mj2bt64gLE2zm6VG3 - тестовые в фантом валет
// FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P
Баланс
✅ 2. Инструкция: как сохранить и подключить ключ в Ubuntu
1. Сохраняем JSON в файл
После запуска программы скопируй результат, например:
[25,156,13,48,203,...,147]
Сохрани в файл mywallet.json:
echo '[25,156,13,48,203,...,147]' > mywallet.json
2. Временно подключаем кошелёк в Solana CLI
Подключаем как текущий кошелёк:
solana config set --keypair mywallet.json
Проверяем:
solana address
Должен показать публичный ключ, соответствующий этому приватному.
3. Возвращаем обратно свой основной ключ
После работы верни основной ключ:
solana config set --keypair ~/.config/solana/id.json