Commit remaining workspace changes
This commit is contained in:
parent
bbe406ac01
commit
890e10de9f
@ -1,5 +1,5 @@
|
|||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
use anchor_lang::solana_program::{program::invoke_signed, system_instruction, system_program};
|
use anchor_lang::solana_program::{program::invoke_signed, system_instruction};
|
||||||
|
|
||||||
/// сдесь коды всех ошибок
|
/// сдесь коды всех ошибок
|
||||||
|
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "shine_payments"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "Payments and investments smart contract"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
crate-type = ["cdylib", "lib"]
|
|
||||||
name = "shine_payments"
|
|
||||||
test = false
|
|
||||||
doctest = false
|
|
||||||
bench = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
anchor-lang = "0.31.1"
|
|
||||||
common = { path = "../common" }
|
|
||||||
|
|
||||||
# ==== добавлено для NFT-функционала ====
|
|
||||||
anchor-spl = { version = "0.31.1", features = ["associated_token", "token"] }
|
|
||||||
mpl-token-metadata = "5.1.1"
|
|
||||||
spl-token = { version = "4.0.0", features = ["no-entrypoint"] }
|
|
||||||
# ======================================
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
no-entrypoint = []
|
|
||||||
no-idl = []
|
|
||||||
no-log-ix-name = []
|
|
||||||
anchor-debug = []
|
|
||||||
custom-heap = []
|
|
||||||
custom-panic = []
|
|
||||||
cpi = []
|
|
||||||
idl-build = ["anchor-lang/idl-build"]
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
# Важно
|
|
||||||
|
|
||||||
Эта папка содержит устаревшую версию `shine_payments`.
|
|
||||||
|
|
||||||
- Не использовать для новых доработок.
|
|
||||||
- Актуальная реализация находится в `programs/shine_payments`.
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Скрипт для копирования dApp на тестовый сервер
|
|
||||||
|
|
||||||
# Настройки
|
|
||||||
LOCAL_FILE="init.html"
|
|
||||||
REMOTE_USER="aidar"
|
|
||||||
REMOTE_HOST="shineup.me"
|
|
||||||
REMOTE_PATH="/home/aidar/Docker_server/site/dApp"
|
|
||||||
|
|
||||||
# Проверка, что файл существует
|
|
||||||
if [ ! -f "$LOCAL_FILE" ]; then
|
|
||||||
echo "Ошибка: файл $LOCAL_FILE не найден."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Копирование файла
|
|
||||||
scp "$LOCAL_FILE" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH}"
|
|
||||||
|
|
||||||
# Проверка результата
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
echo "Файл успешно загружен на сервер."
|
|
||||||
else
|
|
||||||
echo "Ошибка при загрузке файла на сервер."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
#echo
|
|
||||||
#echo "Нажмите Enter, чтобы закрыть..."
|
|
||||||
#read
|
|
||||||
@ -1,404 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ru">
|
|
||||||
<head>
|
|
||||||
<meta name="robots" content="noindex, nofollow">
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>Shine Payments — Phantom demo (devnet, deep logs)</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<style>
|
|
||||||
body { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; padding: 20px; }
|
|
||||||
h1 { font-size: 18px; margin-bottom: 12px; }
|
|
||||||
.row { display: flex; gap: 8px; margin-bottom: 10px; flex-wrap: wrap; }
|
|
||||||
button { padding: 8px 12px; border-radius: 8px; border: 1px solid #ccc; cursor: pointer; }
|
|
||||||
button:hover { background: #f5f5f5; }
|
|
||||||
#log {
|
|
||||||
background: #0a0a0a; color: #d1d5db; padding: 12px; border-radius: 10px;
|
|
||||||
min-height: 220px; max-height: 60vh; overflow: auto; line-height: 1.4; white-space: pre-wrap;
|
|
||||||
box-shadow: inset 0 0 0 1px #222;
|
|
||||||
}
|
|
||||||
.muted { color: #6b7280; }
|
|
||||||
.ok { color: #86efac; }
|
|
||||||
.err { color: #fca5a5; }
|
|
||||||
|
|
||||||
/* ——— banner с логотипом и предупреждением ——— */
|
|
||||||
.banner {
|
|
||||||
display: flex; align-items: center; gap: 14px;
|
|
||||||
padding: 12px 14px; margin: 12px 0 18px;
|
|
||||||
border: 1px solid #f1e5a8; border-radius: 12px;
|
|
||||||
background: #fffbe6; color: #7a5d00;
|
|
||||||
}
|
|
||||||
.banner img { width: 44px; height: 44px; border-radius: 8px; flex: 0 0 auto; }
|
|
||||||
.banner .txt { line-height: 1.35; }
|
|
||||||
.banner .txt b { font-weight: 700; }
|
|
||||||
.banner .en { margin-top: 6px; color: #6b7280; font-size: 13px; }
|
|
||||||
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- DEV BANNER (logo + RU/EN notice) -->
|
|
||||||
<div class="banner" role="note" aria-label="Dev notice">
|
|
||||||
<img src="./shine_nft_logo_256.png" alt="Shine logo">
|
|
||||||
<div class="txt">
|
|
||||||
<div><b>ВНИМАНИЕ:</b> это тестовая страница для внутренней разработки. Я разбираюсь и пишу смарт-контракт и dApp для будущего токена и тестирую его работу в Devnet и подключение кошелька Phantom. Страница не является рабочим продуктом и предназначена только для внутренних тестов.</div>
|
|
||||||
<div class="en"><b>NOTICE:</b> this is a development test page. I’m building a smart contract and dApp for a future token and testing it on Devnet and Phantom wallet connection. This page is not a live product and is intended for internal testing only.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1>Shine Payments — Phantom wallet (devnet)</h1>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<button id="btnConnect">Connect Phantom</button>
|
|
||||||
<button id="btnInfo" disabled>Показать адрес и баланс</button>
|
|
||||||
<button id="btnAirdrop" disabled>Airdrop 1 SOL (devnet)</button>
|
|
||||||
<button id="btnInit" disabled>Выполнить init()</button>
|
|
||||||
<button id="btn-delete-init" disabled>🗑️ Удалить PDA (delete_init)</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="muted">
|
|
||||||
В Phantom выбери сеть <b>Devnet</b>. Логи ниже и в консоли (F12 → Console).
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<pre id="log"></pre>
|
|
||||||
|
|
||||||
<!-- Единственная зависимость: web3.js -->
|
|
||||||
<script src="https://unpkg.com/@solana/web3.js@1.95.0/lib/index.iife.min.js"></script>
|
|
||||||
<script>
|
|
||||||
(async () => {
|
|
||||||
const logEl = document.getElementById("log");
|
|
||||||
const autoScroll = () => { logEl.scrollTop = logEl.scrollHeight; };
|
|
||||||
const log = (...a) => { console.log(...a); logEl.textContent += a.join(" ") + "\n"; autoScroll(); };
|
|
||||||
const logOk = (...a) => log('%c' + a.join(" "), 'color:#86efac');
|
|
||||||
const logErr = (...a) => log('%c' + a.join(" "), 'color:#fca5a5');
|
|
||||||
|
|
||||||
// ===== ПАРАМЕТРЫ ПРОЕКТА =====
|
|
||||||
const RPC_URL = "https://api.devnet.solana.com";
|
|
||||||
const PROGRAM_ID = new solanaWeb3.PublicKey("92sgkgx7KHpbhQu81mNGHaKa7skJB7esArVdPM7paDSW");
|
|
||||||
const STATE_SEED = "shine_investments_state";
|
|
||||||
|
|
||||||
// Лучше "processed" для симуляций + "confirmed" для подтверждений
|
|
||||||
const connection = new solanaWeb3.Connection(RPC_URL, { commitment: "confirmed" });
|
|
||||||
const enc = new TextEncoder();
|
|
||||||
|
|
||||||
let provider = null; // Phantom provider (window.solana)
|
|
||||||
let walletPubkey = null; // PublicKey пользователя из Phantom
|
|
||||||
let logsSubId = null; // id подписки на логи программы
|
|
||||||
|
|
||||||
// ===== УТИЛИТЫ ОШИБОК/ЛОГОВ =====
|
|
||||||
function safeJson(v) {
|
|
||||||
try { return JSON.stringify(v, null, 2); } catch { return String(v); }
|
|
||||||
}
|
|
||||||
|
|
||||||
function printRpcError(prefix, e) {
|
|
||||||
// Структура ошибок Solana/Anchor часто лежит в e, e.message, e.data, e.logs, e.code
|
|
||||||
logErr(prefix);
|
|
||||||
if (!e) return;
|
|
||||||
|
|
||||||
if (e.message) logErr("message:", e.message);
|
|
||||||
if (e.code !== undefined) logErr("code:", e.code);
|
|
||||||
if (e.name) logErr("name:", e.name);
|
|
||||||
|
|
||||||
// web3.js/JSON-RPC иногда кладёт это сюда:
|
|
||||||
if (e.data) {
|
|
||||||
if (e.data.logs) {
|
|
||||||
logErr("logs:");
|
|
||||||
(e.data.logs || []).forEach(l => logErr(" " + l));
|
|
||||||
}
|
|
||||||
if (e.data.err) {
|
|
||||||
logErr("rpc err:", safeJson(e.data.err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Некоторые кошельки/обёртки кладут логи прямо в e.logs
|
|
||||||
if (e.logs) {
|
|
||||||
logErr("logs:");
|
|
||||||
(e.logs || []).forEach(l => logErr(" " + l));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Стек — в конце
|
|
||||||
if (e.stack) {
|
|
||||||
logErr("stack:\n" + e.stack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function simulateAndLog(tx) {
|
|
||||||
// Симуляция перед отправкой — ключ к пониманию, где падает инструкция.
|
|
||||||
try {
|
|
||||||
const sim = await connection.simulateTransaction(tx, {
|
|
||||||
sigVerify: false, // подпись не требуется
|
|
||||||
commitment: "processed"
|
|
||||||
});
|
|
||||||
const v = sim.value;
|
|
||||||
log("🔎 simulate result — err:", safeJson(v.err));
|
|
||||||
if (v.logs?.length) {
|
|
||||||
log("🪵 simulate logs:");
|
|
||||||
v.logs.forEach(l => log(" " + l));
|
|
||||||
}
|
|
||||||
if (v.unitsConsumed !== undefined) {
|
|
||||||
log("⛽ compute units (simulate):", v.unitsConsumed);
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
} catch (e) {
|
|
||||||
printRpcError("❌ Ошибка simulateTransaction:", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function confirmAndLog(signature, blockhashCtx) {
|
|
||||||
try {
|
|
||||||
const { blockhash, lastValidBlockHeight } = blockhashCtx;
|
|
||||||
log("🧱 confirm with blockhash/lastValid:", blockhash, "/", lastValidBlockHeight);
|
|
||||||
const res = await connection.confirmTransaction(
|
|
||||||
{ signature, blockhash, lastValidBlockHeight },
|
|
||||||
"confirmed"
|
|
||||||
);
|
|
||||||
log("📬 confirmation status:", safeJson(res.value));
|
|
||||||
return res.value;
|
|
||||||
} catch (e) {
|
|
||||||
printRpcError("❌ Ошибка confirmTransaction:", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getSigStatus(signature) {
|
|
||||||
try {
|
|
||||||
const st = await connection.getSignatureStatus(signature, { searchTransactionHistory: true });
|
|
||||||
log("🧾 signature status:", safeJson(st?.value));
|
|
||||||
return st?.value;
|
|
||||||
} catch (e) {
|
|
||||||
printRpcError("❌ Ошибка getSignatureStatus:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Anchor discriminator (8 байт) =====
|
|
||||||
async function anchorDiscriminator8(name) {
|
|
||||||
const hash = await crypto.subtle.digest("SHA-256", enc.encode("global:" + name));
|
|
||||||
return new Uint8Array(hash).slice(0, 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== PDA =====
|
|
||||||
async function getStatePda() {
|
|
||||||
const [pda] = await solanaWeb3.PublicKey.findProgramAddress(
|
|
||||||
[enc.encode(STATE_SEED)],
|
|
||||||
PROGRAM_ID
|
|
||||||
);
|
|
||||||
return pda;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Отправка через Phantom с расширенным логированием =====
|
|
||||||
async function sendViaPhantom(tx, blockhashCtx) {
|
|
||||||
// Вариант с signAndSendTransaction вернёт сразу signature,
|
|
||||||
// но иногда теряются preflight-детали. Мы дополнительно логируем simulate.
|
|
||||||
// Обязательно: транзакция без подписей, место под Lighthouse-инструкции.
|
|
||||||
await simulateAndLog(tx); // можно оставить, подпись не требуется
|
|
||||||
if (!provider.signAndSendTransaction) throw new Error("Phantom не поддерживает signAndSendTransaction");
|
|
||||||
const {signature} = await provider.signAndSendTransaction(tx);
|
|
||||||
logOk("✍️ отправлено, сигнатура:", signature);
|
|
||||||
await confirmAndLog(signature, blockhashCtx);
|
|
||||||
await getSigStatus(signature);
|
|
||||||
return signature;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function setButtonsEnabled(connected) {
|
|
||||||
document.getElementById("btnInfo").disabled = !connected;
|
|
||||||
document.getElementById("btnAirdrop").disabled = !connected;
|
|
||||||
document.getElementById("btnInit").disabled = !connected;
|
|
||||||
document.getElementById("btn-delete-init").disabled = !connected;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== CONNECT =====
|
|
||||||
document.getElementById("btnConnect").addEventListener("click", async () => {
|
|
||||||
try {
|
|
||||||
if (!window.solana || !window.solana.isPhantom) {
|
|
||||||
logErr("❌ Phantom не найден. Установи расширение Phantom Wallet.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
provider = window.solana;
|
|
||||||
|
|
||||||
// Подключаемся ТОЛЬКО по клику пользователя:
|
|
||||||
log("🔌 Ожидаем подключение Phantom…");
|
|
||||||
await provider.connect(); // без onlyIfTrusted — это уже явный жест пользователя
|
|
||||||
|
|
||||||
walletPubkey = provider.publicKey;
|
|
||||||
logOk("✅ Подключено:", walletPubkey.toBase58());
|
|
||||||
setButtonsEnabled(true);
|
|
||||||
|
|
||||||
// Подписка на логи программы — помогает увидеть то, что в simulate/confirm могло не попасть
|
|
||||||
try {
|
|
||||||
if (logsSubId) {
|
|
||||||
await connection.removeOnLogsListener(logsSubId);
|
|
||||||
logsSubId = null;
|
|
||||||
}
|
|
||||||
logsSubId = connection.onLogs(PROGRAM_ID, (ev) => {
|
|
||||||
log("🛰 onLogs:", ev.signature, "err:", safeJson(ev.err));
|
|
||||||
(ev.logs || []).forEach(l => log(" " + l));
|
|
||||||
}, "confirmed");
|
|
||||||
log("📡 Подписка на логи программы включена.");
|
|
||||||
} catch (e) {
|
|
||||||
printRpcError("⚠️ Не удалось подписаться на логи программы:", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
provider.on?.("disconnect", async () => {
|
|
||||||
log("🔌 Отключено");
|
|
||||||
setButtonsEnabled(false);
|
|
||||||
walletPubkey = null;
|
|
||||||
if (logsSubId) {
|
|
||||||
try { await connection.removeOnLogsListener(logsSubId); } catch {}
|
|
||||||
logsSubId = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
printRpcError("❌ Connect error:", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===== INFO =====
|
|
||||||
document.getElementById("btnInfo").addEventListener("click", async () => {
|
|
||||||
try {
|
|
||||||
if (!walletPubkey) throw new Error("Сначала подключи Phantom.");
|
|
||||||
const balanceLamports = await connection.getBalance(walletPubkey, "processed");
|
|
||||||
const statePda = await getStatePda();
|
|
||||||
log("👛 Кошелёк:", walletPubkey.toBase58());
|
|
||||||
log("💰 Баланс:", balanceLamports / solanaWeb3.LAMPORTS_PER_SOL, "SOL");
|
|
||||||
log("📦 statePda:", statePda.toBase58());
|
|
||||||
log("🌐 RPC:", RPC_URL);
|
|
||||||
} catch (e) {
|
|
||||||
printRpcError("❌ Ошибка INFO:", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===== AIRDROP (devnet) =====
|
|
||||||
document.getElementById("btnAirdrop").addEventListener("click", async () => {
|
|
||||||
try {
|
|
||||||
if (!walletPubkey) throw new Error("Сначала подключи Phantom.");
|
|
||||||
log("⛽ Запрос airdrop 1 SOL на", walletPubkey.toBase58());
|
|
||||||
const sig = await connection.requestAirdrop(walletPubkey, 1 * solanaWeb3.LAMPORTS_PER_SOL);
|
|
||||||
log("⏳ confirm airdrop…");
|
|
||||||
const { value: ctx } = await connection.getLatestBlockhashAndContext("processed");
|
|
||||||
await confirmAndLog(sig, ctx);
|
|
||||||
logOk("✅ Airdrop tx:", sig);
|
|
||||||
const bal = await connection.getBalance(walletPubkey);
|
|
||||||
log("💰 Новый баланс:", bal / solanaWeb3.LAMPORTS_PER_SOL, "SOL");
|
|
||||||
} catch (e) {
|
|
||||||
printRpcError("❌ Ошибка airdrop:", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===== INIT() =====
|
|
||||||
document.getElementById("btnInit").addEventListener("click", async () => {
|
|
||||||
try {
|
|
||||||
if (!walletPubkey) throw new Error("Сначала подключи Phantom.");
|
|
||||||
|
|
||||||
const statePda = await getStatePda();
|
|
||||||
log("🚀 Вызываем init()");
|
|
||||||
log(" payer: ", walletPubkey.toBase58());
|
|
||||||
log(" statePda: ", statePda.toBase58());
|
|
||||||
log(" programId:", PROGRAM_ID.toBase58());
|
|
||||||
|
|
||||||
// 8 байт дискриминатора Anchor для "global:init"
|
|
||||||
const data = await anchorDiscriminator8("init"); // Uint8Array длиной 8
|
|
||||||
|
|
||||||
// Аккаунты в том порядке, который ожидает on-chain метод
|
|
||||||
const keys = [
|
|
||||||
{ pubkey: walletPubkey, isSigner: true, isWritable: true }, // payer (signer)
|
|
||||||
{ pubkey: statePda, isSigner: false, isWritable: true }, // state PDA
|
|
||||||
{ pubkey: solanaWeb3.SystemProgram.programId, isSigner: false, isWritable: false },
|
|
||||||
];
|
|
||||||
|
|
||||||
const ix = new solanaWeb3.TransactionInstruction({ programId: PROGRAM_ID, keys, data });
|
|
||||||
|
|
||||||
// Получаем блокхеш/lastValidBlockHeight и логируем
|
|
||||||
const { value: ctx } = await connection.getLatestBlockhashAndContext("processed");
|
|
||||||
const { blockhash, lastValidBlockHeight } = ctx;
|
|
||||||
log("⏱ blockhash:", blockhash);
|
|
||||||
log("⏱ lastValidBlockHeight:", lastValidBlockHeight);
|
|
||||||
|
|
||||||
const tx = new solanaWeb3.Transaction({
|
|
||||||
feePayer: walletPubkey,
|
|
||||||
recentBlockhash: blockhash,
|
|
||||||
}).add(ix);
|
|
||||||
|
|
||||||
// 1) Симуляция — сразу покажет логи Anchor/InstructionError
|
|
||||||
await simulateAndLog(tx);
|
|
||||||
|
|
||||||
log("📝 Подписываем в Phantom…");
|
|
||||||
// 2) Отправка + подтверждение с расширенными логами
|
|
||||||
const sig = await sendViaPhantom(tx, { blockhash, lastValidBlockHeight });
|
|
||||||
logOk("✅ Готово! tx:", sig);
|
|
||||||
log("🔗 Explorer:", `https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
|
||||||
} catch (e) {
|
|
||||||
// Печатаем максимально подробно
|
|
||||||
printRpcError("❌ Ошибка init:", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===== DELETE INIT =====
|
|
||||||
document.getElementById("btn-delete-init").addEventListener("click", async () => {
|
|
||||||
try {
|
|
||||||
// 1) Проверка подключения
|
|
||||||
if (!walletPubkey) throw new Error("Сначала подключи Phantom.");
|
|
||||||
|
|
||||||
// 2) PDA — те же сиды, что и в init
|
|
||||||
const statePda = await getStatePda();
|
|
||||||
log("🗑️ Вызываем delete_init()");
|
|
||||||
log(" signer: ", walletPubkey.toBase58());
|
|
||||||
log(" statePda: ", statePda.toBase58());
|
|
||||||
log(" programId:", PROGRAM_ID.toBase58());
|
|
||||||
|
|
||||||
// 3) Дискриминатор Anchor для "global:delete_init"
|
|
||||||
const data = await anchorDiscriminator8("delete_init"); // 8 байт
|
|
||||||
|
|
||||||
// 4) Аккаунты в порядке, который ожидает on-chain метод
|
|
||||||
const keys = [
|
|
||||||
{ pubkey: walletPubkey, isSigner: true, isWritable: true }, // signer (получатель ренты)
|
|
||||||
{ pubkey: statePda, isSigner: false, isWritable: true }, // state_pda
|
|
||||||
{ pubkey: solanaWeb3.SystemProgram.programId, isSigner: false, isWritable: false },
|
|
||||||
];
|
|
||||||
|
|
||||||
const ix = new solanaWeb3.TransactionInstruction({
|
|
||||||
programId: PROGRAM_ID,
|
|
||||||
keys,
|
|
||||||
data, // только 8 байт дискриминатора, т.к. у метода нет аргументов
|
|
||||||
});
|
|
||||||
|
|
||||||
// 5) Блокхеш, формирование транзакции
|
|
||||||
const { value: ctx } = await connection.getLatestBlockhashAndContext("processed");
|
|
||||||
const { blockhash, lastValidBlockHeight } = ctx;
|
|
||||||
log("⏱ blockhash:", blockhash);
|
|
||||||
log("⏱ lastValidBlockHeight:", lastValidBlockHeight);
|
|
||||||
|
|
||||||
const tx = new solanaWeb3.Transaction({
|
|
||||||
feePayer: walletPubkey,
|
|
||||||
recentBlockhash: blockhash,
|
|
||||||
}).add(ix);
|
|
||||||
|
|
||||||
// 6) Симуляция → отправка → подтверждение
|
|
||||||
await simulateAndLog(tx);
|
|
||||||
|
|
||||||
log("📝 Подписываем в Phantom…");
|
|
||||||
const sig = await sendViaPhantom(tx, { blockhash, lastValidBlockHeight });
|
|
||||||
|
|
||||||
logOk("✅ delete_init выполнен. tx:", sig);
|
|
||||||
log("🔗 Explorer:", `https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
|
||||||
alert(`PDA удалён, рента возвращена подписанту.\nTx: ${sig}`);
|
|
||||||
} catch (e) {
|
|
||||||
printRpcError("❌ Ошибка delete_init:", e);
|
|
||||||
alert(`Ошибка delete_init: ${e.message || e}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Больше НИКАКИХ автоконнектов на загрузке страницы.
|
|
||||||
if (window.solana?.isPhantom) {
|
|
||||||
provider = window.solana;
|
|
||||||
log("ℹ️ Phantom найден. Нажми «Connect Phantom», чтобы подключиться.");
|
|
||||||
} else {
|
|
||||||
log("ℹ️ Установи Phantom Wallet: https://phantom.app");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
)();
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,326 +0,0 @@
|
|||||||
use anchor_lang::prelude::*;
|
|
||||||
|
|
||||||
use anchor_lang::solana_program::{program::invoke_signed, system_instruction};
|
|
||||||
use common::utils::*; // тянем общие PDA-хелперы из programs/common
|
|
||||||
|
|
||||||
// === добавлено: используем наш NFT-модуль ===
|
|
||||||
use crate::nft::{CreateNftParams, create_nft_with_freeze};
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
/// Утилита чтения структуры из PDA: читает байты и десериализует.
|
|
||||||
/// Возвращает ошибку, если данных нет/пустые/неверный формат.
|
|
||||||
fn read_state_from_pda(pda: &AccountInfo) -> Result<InvestState> {
|
|
||||||
let raw = safe_read_pda(pda); // ← берём Vec<u8> (или пустой)
|
|
||||||
require!(!raw.is_empty(), ErrCode::EmptyPdaData); // ← пусто — ошибка
|
|
||||||
let st = deserialize_invest_state(&raw)?; // ← десериализуем по формату
|
|
||||||
require!(st.format == INVEST_STATE_FORMAT_V1, ErrCode::UnsupportedFormat); // ← проверяем версию
|
|
||||||
Ok(st)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Утилита записи структуры в PDA: сериализует и пишет.
|
|
||||||
/// Важно: сам аккаунт уже должен существовать и быть #[account(mut)].
|
|
||||||
fn write_state_to_pda(pda: &AccountInfo, s: &InvestState) -> Result<()> {
|
|
||||||
let raw = serialize_invest_state_v1(s); // ← 24 байта
|
|
||||||
write_to_pda(pda, &raw) // ← записываем в начало data
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ==============================================
|
|
||||||
/// Контексты инструкций (минимально необходимые)
|
|
||||||
/// ==============================================
|
|
||||||
|
|
||||||
/// init: создаём PDA и кладём в него PayStateV1 {format=1, coef=10, ...0}
|
|
||||||
#[derive(Accounts)]
|
|
||||||
pub struct Init<'info> {
|
|
||||||
/// Плательщик аренды за PDA; подписант транзакции.
|
|
||||||
#[account(mut)]
|
|
||||||
pub payer: Signer<'info>,
|
|
||||||
|
|
||||||
/// Наш PDA (с произвольным типом, чтобы работать через AccountInfo).
|
|
||||||
/// Проверку адреса делаем в handler (по seed + bump), чтобы избежать подмены.
|
|
||||||
/// CHECK: проверяется вручную по адресу
|
|
||||||
#[account(mut)]
|
|
||||||
pub state_pda: UncheckedAccount<'info>,
|
|
||||||
|
|
||||||
/// Системная программа.
|
|
||||||
pub system_program: Program<'info, System>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Общие аккаунты для invest/add_bonus/claim:
|
|
||||||
/// Везде просто читаем/пишем одно и то же состояние из того же PDA.
|
|
||||||
#[derive(Accounts)]
|
|
||||||
pub struct UseState<'info> {
|
|
||||||
/// Любой платящий/подписант (в реальном коде — свои проверки).
|
|
||||||
pub signer: Signer<'info>,
|
|
||||||
|
|
||||||
/// Тот же PDA с состоянием (должен уже существовать).
|
|
||||||
/// CHECK: проверяется вручную по адресу
|
|
||||||
#[account(mut)]
|
|
||||||
pub state_pda: UncheckedAccount<'info>,
|
|
||||||
|
|
||||||
/// Системная программа (на всякий случай; может не понадобиться).
|
|
||||||
pub system_program: Program<'info, System>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ==============================================
|
|
||||||
/// Программа
|
|
||||||
/// ==============================================
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use anchor_lang::prelude::*;
|
|
||||||
|
|
||||||
|
|
||||||
/// ------------------------------------------
|
|
||||||
/// init: создаёт PDA и записывает в него дефолтное состояние.
|
|
||||||
/// format = 1, coef = 10, остальные поля = 0.
|
|
||||||
/// ------------------------------------------
|
|
||||||
pub fn init(ctx: Context<Init>) -> Result<()> {
|
|
||||||
let program_id = ctx.program_id; // ← адрес этой программы
|
|
||||||
|
|
||||||
// 1. Проверка что вызывает именно разрешённый ключ
|
|
||||||
/* todo пока все могут вызыватьно !! но в итоге будет добавленна проверка что бы только дао могло вызвать эту функцию один раз
|
|
||||||
require_keys_eq!(
|
|
||||||
ctx.accounts.payer.key(),
|
|
||||||
ALLOWED_INIT_CALLER,
|
|
||||||
ErrCode::InvalidSigner
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
|
|
||||||
// 2. Проверка что PDA ещё не создан
|
|
||||||
if ctx.accounts.state_pda.data_len() > 0 && ctx.accounts.state_pda.owner != &System::id() {
|
|
||||||
return Err(error!(ErrCode::PdaAlreadyExists));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Ещё раз Проверка что PDA ещё не создан
|
|
||||||
if ctx.accounts.state_pda.owner != &System::id()
|
|
||||||
|| ctx.accounts.state_pda.lamports() > 0
|
|
||||||
{
|
|
||||||
// Если аккаунт уже создан и не пустой
|
|
||||||
return Err(error!(ErrCode::PdaAlreadyExists));
|
|
||||||
}
|
|
||||||
|
|
||||||
let pda_key_expected = Pubkey::find_program_address(&[crate::PDA_SEED_PREFIX], program_id).0; // ← вычисляем PDA
|
|
||||||
require_keys_eq!(
|
|
||||||
pda_key_expected,
|
|
||||||
ctx.accounts.state_pda.key(),
|
|
||||||
ErrCode::InvalidPdaAddress
|
|
||||||
); // ← убеждаемся, что нам подали именно правильный PDA
|
|
||||||
|
|
||||||
// Конструируем дефолтную структуру состояния.
|
|
||||||
let state = InvestState {
|
|
||||||
format: INVEST_STATE_FORMAT_V1, // ← 1
|
|
||||||
coef: crate::DEFAULT_COEF, // ← 10
|
|
||||||
q1_tokens: 0, // ← нули
|
|
||||||
sum1_bonus: 0,
|
|
||||||
q1_paid_tokens: 0,
|
|
||||||
sum1_paid_bonus: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Сериализуем в 24 байта.
|
|
||||||
let data = serialize_invest_state_v1(&state);
|
|
||||||
|
|
||||||
// Для подписи PDA нужен bump; здесь получим (ключ, bump).
|
|
||||||
let (_pda_key, bump) = Pubkey::find_program_address(&[crate::PDA_SEED_PREFIX], program_id);
|
|
||||||
|
|
||||||
// Сиды для invoke_signed: [seed, bump]
|
|
||||||
let seeds: [&[u8]; 2] = [crate::PDA_SEED_PREFIX, &[bump]];
|
|
||||||
|
|
||||||
// Создаём и сразу записываем, арендный минимум оплачивает payer.
|
|
||||||
create_and_write_pda(
|
|
||||||
&ctx.accounts.state_pda.to_account_info(), // куда пишем
|
|
||||||
&ctx.accounts.payer.to_account_info(), // кто платит
|
|
||||||
&ctx.accounts.system_program.to_account_info(),
|
|
||||||
program_id,
|
|
||||||
&seeds,
|
|
||||||
data,
|
|
||||||
crate::PAY_STATE_SPACE, // резерв с запасом
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ------------------------------------------
|
|
||||||
/// invest: «внос инвестиций».
|
|
||||||
/// По заданию: в начале читаем состояние, в конце сохраняем.
|
|
||||||
/// (Здесь логика модификации не задана — оставляем как заглушку.)
|
|
||||||
/// ------------------------------------------
|
|
||||||
pub fn invest(ctx: Context<UseState>, _amount: u64) -> Result<()> {
|
|
||||||
// 1) читаем
|
|
||||||
let mut st = read_state_from_pda(&ctx.accounts.state_pda.to_account_info())?; // ← PayStateV1
|
|
||||||
|
|
||||||
// --- тут можно модифицировать st по твоей бизнес-логике ---
|
|
||||||
// Например, ничего не меняем сейчас (заглушка).
|
|
||||||
let _ = &mut st; // чтоб компилятор не ругался, если пока не используем
|
|
||||||
|
|
||||||
// 2) сохраняем
|
|
||||||
write_state_to_pda(&ctx.accounts.state_pda.to_account_info(), &st)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ------------------------------------------
|
|
||||||
/// add_bonus: «начисление бонусов» (обычно вызывать от DAO).
|
|
||||||
/// По заданию: читаем в начале, создаём/добавляем NFT в очередь, сохраняем в конце.
|
|
||||||
/// Для операций с NFT используем расширенный контекст AddBonusCtx (см. lib.rs).
|
|
||||||
/// ------------------------------------------
|
|
||||||
pub fn add_bonus(ctx: Context<crate::AddBonusCtx>, investor: Pubkey, amount: u64) -> Result<()> {
|
|
||||||
// 1) читаем состояние
|
|
||||||
let mut st = read_state_from_pda(&ctx.accounts.state_pda.to_account_info())?;
|
|
||||||
|
|
||||||
// 2) создаём/добавляем NFT через модуль nft (создание metadata, mint 1, freeze, master edition, verify)
|
|
||||||
let next_index = st.q1_tokens as u64 + 1;
|
|
||||||
let params = CreateNftParams {
|
|
||||||
name: format!("Bonus #{}", next_index),
|
|
||||||
symbol: "BN".to_string(),
|
|
||||||
uri: "https://example.com/nft.json".to_string(), // заглушка для devnet-теста
|
|
||||||
index: next_index,
|
|
||||||
recipient: investor,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ВАЖНО: mint_pda должен быть создан ТЕСТОМ заранее с decimals=0, mint_authority=signer, freeze_authority=signer.
|
|
||||||
create_nft_with_freeze(&ctx, params)?;
|
|
||||||
|
|
||||||
// 3) обновляем агрегаты очереди (минимально: увеличим счётчик и сумму бонусов)
|
|
||||||
st.q1_tokens = st.q1_tokens.saturating_add(1);
|
|
||||||
let add = u32::try_from(core::cmp::min(amount, u64::from(u32::MAX))).unwrap_or(u32::MAX);
|
|
||||||
st.sum1_bonus = st.sum1_bonus.saturating_add(add);
|
|
||||||
|
|
||||||
// 4) сохраняем
|
|
||||||
write_state_to_pda(&ctx.accounts.state_pda.to_account_info(), &st)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ------------------------------------------
|
|
||||||
/// claim: «выплата».
|
|
||||||
/// По заданию: читаем в начале, сохраняем в конце.
|
|
||||||
/// ------------------------------------------
|
|
||||||
pub fn claim(ctx: Context<UseState>) -> Result<()> {
|
|
||||||
// 1) читаем
|
|
||||||
let mut st = read_state_from_pda(&ctx.accounts.state_pda.to_account_info())?;
|
|
||||||
|
|
||||||
// --- тут твоя логика списаний/выплат ---
|
|
||||||
let _ = &mut st; // заглушка
|
|
||||||
|
|
||||||
// 2) сохраняем
|
|
||||||
write_state_to_pda(&ctx.accounts.state_pda.to_account_info(), &st)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//todo
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// ==============================================
|
|
||||||
/// Коды ошибок (берём из твоего блока; можно расширять)
|
|
||||||
/// ==============================================
|
|
||||||
|
|
||||||
#[error_code]
|
|
||||||
pub enum ErrCode {
|
|
||||||
/// Система уже инициализирована и не может быть инициализирована повторно!
|
|
||||||
#[msg("Система уже инициализирована и не может быть инициализирована повторно!")]
|
|
||||||
SystemAlreadyInitialized = 1000,
|
|
||||||
|
|
||||||
#[msg("PDA не содержит данных или не инициализирован")]
|
|
||||||
EmptyPdaData = 1002,
|
|
||||||
|
|
||||||
#[msg("Пользователь уже зарегистрирован")]
|
|
||||||
UserAlreadyExists = 1003,
|
|
||||||
|
|
||||||
#[msg("Некорректный логин")]
|
|
||||||
InvalidLogin = 1004,
|
|
||||||
|
|
||||||
#[msg("Не совпадает PDA адрес")]
|
|
||||||
InvalidPdaAddress = 1006,
|
|
||||||
|
|
||||||
#[msg("Формат данных не поддерживается")]
|
|
||||||
UnsupportedFormat = 1011,
|
|
||||||
|
|
||||||
#[msg("Ошибка при десериализации")]
|
|
||||||
DeserializationError = 1012,
|
|
||||||
|
|
||||||
/// PDA уже существует, создание невозможно
|
|
||||||
#[msg("PDA-аккаунт уже существует и не может быть создан повторно.")]
|
|
||||||
PdaAlreadyExists = 1009,
|
|
||||||
|
|
||||||
#[msg("Подписавший не совпадает с ожидаемым пользователем (временное ограничение)")]
|
|
||||||
InvalidSigner = 1005,
|
|
||||||
|
|
||||||
/// Не получилось создать пользователя
|
|
||||||
#[msg("Не получилось создать пользователя, система уже перегружена, попробуйте позже!")]
|
|
||||||
NoSuitableIdPda = 1010,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
use anchor_lang::prelude::*;
|
|
||||||
|
|
||||||
/// ================================
|
|
||||||
/// КОНСТАНТЫ ФОРМАТА / ДЛИНЫ ДАННЫХ
|
|
||||||
/// ================================
|
|
||||||
|
|
||||||
/// Версия формата хранения состояния.
|
|
||||||
/// Мы жёстко фиксируем «1», чтобы код мог отличать будущие версии.
|
|
||||||
pub const INVEST_STATE_FORMAT_V1: u32 = 1;
|
|
||||||
|
|
||||||
/// Сырые данные состояния V1 занимают ровно 6 * 4 = 24 байта.
|
|
||||||
pub const INVEST_STATE_RAW_LEN_V1: usize = 24; // байт
|
|
||||||
|
|
||||||
/// ================================
|
|
||||||
/// ОПИСАНИЕ СТРУКТУРЫ СОСТОЯНИЯ (V1)
|
|
||||||
/// ================================
|
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
|
||||||
pub struct InvestState {
|
|
||||||
pub format: u32,
|
|
||||||
pub coef: u32,
|
|
||||||
pub q1_tokens: u32,
|
|
||||||
pub sum1_bonus: u32,
|
|
||||||
pub q1_paid_tokens: u32,
|
|
||||||
pub sum1_paid_bonus: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ========================================
|
|
||||||
/// СЕРИАЛИЗАЦИЯ (структура -> массив байт)
|
|
||||||
/// ========================================
|
|
||||||
pub fn serialize_invest_state_v1(s: &InvestState) -> Vec<u8> {
|
|
||||||
let mut out = Vec::with_capacity(INVEST_STATE_RAW_LEN_V1);
|
|
||||||
out.extend_from_slice(&INVEST_STATE_FORMAT_V1.to_le_bytes()); // [0..4)
|
|
||||||
out.extend_from_slice(&s.coef.to_le_bytes()); // [4..8)
|
|
||||||
out.extend_from_slice(&s.q1_tokens.to_le_bytes()); // [8..12)
|
|
||||||
out.extend_from_slice(&s.sum1_bonus.to_le_bytes()); // [12..16)
|
|
||||||
out.extend_from_slice(&s.q1_paid_tokens.to_le_bytes()); // [16..20)
|
|
||||||
out.extend_from_slice(&s.sum1_paid_bonus.to_le_bytes()); // [20..24)
|
|
||||||
debug_assert_eq!(out.len(), INVEST_STATE_RAW_LEN_V1);
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ===========================================
|
|
||||||
/// ДЕСЕРИАЛИЗАЦИЯ (массив байт -> структура)
|
|
||||||
/// ===========================================
|
|
||||||
pub fn deserialize_invest_state(data: &[u8]) -> Result<InvestState> {
|
|
||||||
if data.len() < INVEST_STATE_RAW_LEN_V1 {
|
|
||||||
return Err(error!(ErrCode::DeserializationError));
|
|
||||||
}
|
|
||||||
fn read_u32_le(slice: &[u8], start: usize) -> u32 {
|
|
||||||
let bytes: [u8; 4] = slice[start..start + 4]
|
|
||||||
.try_into()
|
|
||||||
.expect("slice has enough length due to pre-check");
|
|
||||||
u32::from_le_bytes(bytes)
|
|
||||||
}
|
|
||||||
let format = read_u32_le(data, 0);
|
|
||||||
if format != INVEST_STATE_FORMAT_V1 {
|
|
||||||
return Err(error!(ErrCode::UnsupportedFormat));
|
|
||||||
}
|
|
||||||
let coef = read_u32_le(data, 4);
|
|
||||||
let q1_tokens = read_u32_le(data, 8);
|
|
||||||
let sum1_bonus = read_u32_le(data, 12);
|
|
||||||
let q1_paid_tokens = read_u32_le(data, 16);
|
|
||||||
let sum1_paid_bonus = read_u32_le(data, 20);
|
|
||||||
|
|
||||||
Ok(InvestState { format, coef, q1_tokens, sum1_bonus, q1_paid_tokens, sum1_paid_bonus })
|
|
||||||
}
|
|
||||||
@ -1,158 +0,0 @@
|
|||||||
use anchor_lang::prelude::*;
|
|
||||||
|
|
||||||
declare_id!("qpgnAKhsXgPPaqQWfXhpme7UnG8GyStssuoSjF6Fzy3");
|
|
||||||
|
|
||||||
|
|
||||||
/// Подключаем модуль с полной реализацией.
|
|
||||||
pub mod investments;
|
|
||||||
use investments::*; // импортируем всё в корень
|
|
||||||
|
|
||||||
// === модуль NFT ===
|
|
||||||
pub mod nft;
|
|
||||||
|
|
||||||
// ==============================================
|
|
||||||
// Константы формата / сидов / размеров
|
|
||||||
// ==============================================
|
|
||||||
|
|
||||||
/// Префикс (seed) для PDA, где храним глобальное состояние выплат.
|
|
||||||
/// Важно: сид — это просто набор байт; здесь он фиксированный.
|
|
||||||
pub const PDA_SEED_PREFIX: &[u8] = b"shine_investments_state";
|
|
||||||
|
|
||||||
/// Значение коэффициента «по умолчанию» при инициализации.
|
|
||||||
pub const DEFAULT_COEF: u32 = 10; // ← «коэффициент» = 10 при init
|
|
||||||
|
|
||||||
/// Ровно столько байт резервируем под PDA-данные.
|
|
||||||
/// (Можно добавить запас на будущее, но по заданию — только 28.)
|
|
||||||
pub const PAY_STATE_SPACE: u64 = 50; // просто сделал с запасом
|
|
||||||
|
|
||||||
// ==============================================
|
|
||||||
// Программа
|
|
||||||
// ==============================================
|
|
||||||
|
|
||||||
#[program]
|
|
||||||
pub mod shine_payments {
|
|
||||||
use super::*;
|
|
||||||
// Явно подтягиваем типы и функции, чтобы не было путаницы после предыдущих ошибок парсера
|
|
||||||
use crate::investments::{Init, UseState};
|
|
||||||
use crate::investments::{
|
|
||||||
add_bonus as inv_add_bonus, claim as inv_claim, init as inv_init, invest as inv_invest,
|
|
||||||
ErrCode,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// init — создаёт PDA и кладёт дефолтное состояние.
|
|
||||||
pub fn init(ctx: Context<Init>) -> Result<()> {
|
|
||||||
inv_init(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// invest — в начале читает состояние, в конце сохраняет (логика внутри модуля).
|
|
||||||
pub fn invest(ctx: Context<UseState>, amount: u64) -> Result<()> {
|
|
||||||
inv_invest(ctx, amount)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// add_bonus — начисление бонусов (обычно от DAO).
|
|
||||||
/// Для NFT используем расширенный контекст AddBonusCtx (с аккаунтами коллекции и т.п.).
|
|
||||||
pub fn add_bonus(ctx: Context<AddBonusCtx>, investor: Pubkey, amount: u64) -> Result<()> {
|
|
||||||
inv_add_bonus(ctx, investor, amount)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// claim — выплата.
|
|
||||||
pub fn claim(ctx: Context<UseState>) -> Result<()> {
|
|
||||||
inv_claim(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ВРЕМЕННАЯ ФУНКЦИЯ только для тестов (в итоговой версии её не будет):
|
|
||||||
/// deleteInit — удалить PDA из init и вернуть ренту подписанту.
|
|
||||||
pub fn delete_init(ctx: Context<DeleteInit>) -> Result<()> {
|
|
||||||
let program_id = ctx.program_id;
|
|
||||||
|
|
||||||
// PDA по тем же сид/бамп, что и в init
|
|
||||||
let (expected_pda, _bump) = Pubkey::find_program_address(&[PDA_SEED_PREFIX], program_id);
|
|
||||||
require_keys_eq!(
|
|
||||||
expected_pda,
|
|
||||||
ctx.accounts.state_pda.key(),
|
|
||||||
ErrCode::InvalidPdaAddress
|
|
||||||
);
|
|
||||||
|
|
||||||
// Рента уйдёт на счёт подписанта (signer)
|
|
||||||
common::utils::delete_pda_return_rent(
|
|
||||||
&ctx.accounts.state_pda.to_account_info(),
|
|
||||||
&ctx.accounts.signer.to_account_info(),
|
|
||||||
program_id,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==============================================
|
|
||||||
// Контексты вне #[program]
|
|
||||||
// ==============================================
|
|
||||||
|
|
||||||
/// Контекст для deleteInit (временный для тестов)
|
|
||||||
#[derive(Accounts)]
|
|
||||||
pub struct DeleteInit<'info> {
|
|
||||||
/// Подписант транзакции — ПОЛУЧАТЕЛЬ ренты
|
|
||||||
#[account(mut)]
|
|
||||||
pub signer: Signer<'info>,
|
|
||||||
|
|
||||||
/// Тот самый PDA из init
|
|
||||||
/// CHECK: адрес валидируем в хендлере по сид-у
|
|
||||||
#[account(mut)]
|
|
||||||
pub state_pda: UncheckedAccount<'info>,
|
|
||||||
|
|
||||||
/// Системная программа
|
|
||||||
pub system_program: Program<'info, System>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Контекст для add_bonus: полный набор аккаунтов для операций с NFT и коллекцией.
|
|
||||||
/// (Комменты по стилю проекта оставлены.)
|
|
||||||
#[derive(Accounts)]
|
|
||||||
pub struct AddBonusCtx<'info> {
|
|
||||||
/// Любой платящий/подписант (в реальном коде — свои проверки).
|
|
||||||
#[account(mut)]
|
|
||||||
pub signer: Signer<'info>,
|
|
||||||
|
|
||||||
/// Тот же PDA с состоянием (должен уже существовать).
|
|
||||||
/// CHECK: проверяется вручную по адресу
|
|
||||||
#[account(mut)]
|
|
||||||
pub state_pda: UncheckedAccount<'info>,
|
|
||||||
|
|
||||||
// --- аккаунты минтимого NFT ---
|
|
||||||
/// Mint создаваемого NFT (должен быть создан заранее: decimals=0, mint_authority=signer, freeze_authority=signer)
|
|
||||||
/// CHECK
|
|
||||||
#[account(mut)]
|
|
||||||
pub mint_pda: UncheckedAccount<'info>,
|
|
||||||
|
|
||||||
/// ATA получателя (может быть предсоздан тестом)
|
|
||||||
/// CHECK
|
|
||||||
#[account(mut)]
|
|
||||||
pub recipient_ata: UncheckedAccount<'info>,
|
|
||||||
/// Владелец ATA (инвестор)
|
|
||||||
/// CHECK
|
|
||||||
pub recipient_owner: UncheckedAccount<'info>,
|
|
||||||
|
|
||||||
// --- аккаунты коллекции (уже созданной заранее) ---
|
|
||||||
/// CHECK
|
|
||||||
pub collection_mint: UncheckedAccount<'info>,
|
|
||||||
/// CHECK
|
|
||||||
#[account(mut)]
|
|
||||||
pub collection_metadata_pda: UncheckedAccount<'info>,
|
|
||||||
/// CHECK
|
|
||||||
#[account(mut)]
|
|
||||||
pub collection_master_edition_pda: UncheckedAccount<'info>,
|
|
||||||
/// Апдейтер коллекции (update authority)
|
|
||||||
pub collection_update_authority: Signer<'info>,
|
|
||||||
|
|
||||||
// --- metadata + master edition для создаваемого NFT ---
|
|
||||||
/// CHECK
|
|
||||||
#[account(mut)]
|
|
||||||
pub metadata_pda: UncheckedAccount<'info>,
|
|
||||||
/// CHECK
|
|
||||||
#[account(mut)]
|
|
||||||
pub master_edition_pda: UncheckedAccount<'info>,
|
|
||||||
|
|
||||||
// --- программы ---
|
|
||||||
/// CHECK: проверяется по ID внутри nft.rs
|
|
||||||
pub token_metadata_program: UncheckedAccount<'info>,
|
|
||||||
pub token_program: Program<'info, anchor_spl::token::Token>,
|
|
||||||
pub associated_token_program: Program<'info, anchor_spl::associated_token::AssociatedToken>,
|
|
||||||
pub system_program: Program<'info, System>,
|
|
||||||
}
|
|
||||||
@ -1,190 +0,0 @@
|
|||||||
use anchor_lang::prelude::*;
|
|
||||||
use anchor_lang::solana_program::{program::invoke, program::invoke_signed};
|
|
||||||
use anchor_spl::{associated_token::AssociatedToken, token::Token};
|
|
||||||
|
|
||||||
use mpl_token_metadata::{
|
|
||||||
ID as TM_ID,
|
|
||||||
instructions::{
|
|
||||||
CreateMasterEditionV3Builder,
|
|
||||||
CreateMetadataAccountV3Builder,
|
|
||||||
SetAndVerifySizedCollectionItemBuilder,
|
|
||||||
},
|
|
||||||
types::{Collection, Creator, DataV2, Uses, UseMethod},
|
|
||||||
};
|
|
||||||
|
|
||||||
use spl_token::instruction as spl_ix;
|
|
||||||
|
|
||||||
/// Параметры для минта NFT
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct CreateNftParams {
|
|
||||||
pub name: String,
|
|
||||||
pub symbol: String,
|
|
||||||
pub uri: String,
|
|
||||||
pub index: u64,
|
|
||||||
pub recipient: Pubkey,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Создание metadata, чеканка 1 токена, freeze ATA, создание master edition, verify в коллекции.
|
|
||||||
pub fn create_nft_with_freeze(
|
|
||||||
ctx: &Context<crate::AddBonusCtx>,
|
|
||||||
params: CreateNftParams,
|
|
||||||
) -> Result<()> {
|
|
||||||
let a = &ctx.accounts;
|
|
||||||
|
|
||||||
// Проверяем что это именно программа Metaplex Token Metadata
|
|
||||||
require_keys_eq!(a.token_metadata_program.key(), TM_ID, CustomError::InvalidMetadataProgram);
|
|
||||||
|
|
||||||
// 1) Создание Metadata для нового NFT
|
|
||||||
let creators = Some(vec![Creator {
|
|
||||||
address: a.collection_update_authority.key(),
|
|
||||||
verified: true,
|
|
||||||
share: 100,
|
|
||||||
}]);
|
|
||||||
|
|
||||||
let data = DataV2 {
|
|
||||||
name: truncate(¶ms.name, 32),
|
|
||||||
symbol: truncate(¶ms.symbol, 10),
|
|
||||||
uri: truncate(¶ms.uri, 256),
|
|
||||||
seller_fee_basis_points: 0,
|
|
||||||
creators,
|
|
||||||
collection: Some(Collection {
|
|
||||||
verified: false, // отметим как часть коллекции позже через verify
|
|
||||||
key: a.collection_mint.key(),
|
|
||||||
}),
|
|
||||||
uses: Some(Uses {
|
|
||||||
use_method: UseMethod::Burn,
|
|
||||||
remaining: 1,
|
|
||||||
total: 1,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
// В mpl-token-metadata v5 update_authority(pubkey, is_signer: bool)
|
|
||||||
let ix_md = CreateMetadataAccountV3Builder::new()
|
|
||||||
.metadata(a.metadata_pda.key())
|
|
||||||
.mint(a.mint_pda.key())
|
|
||||||
.mint_authority(a.signer.key())
|
|
||||||
.payer(a.signer.key())
|
|
||||||
.update_authority(a.collection_update_authority.key(), true)
|
|
||||||
.system_program(a.system_program.key())
|
|
||||||
.data(data)
|
|
||||||
.is_mutable(true)
|
|
||||||
.instruction();
|
|
||||||
|
|
||||||
invoke_signed(
|
|
||||||
&ix_md,
|
|
||||||
&[
|
|
||||||
a.metadata_pda.to_account_info(),
|
|
||||||
a.mint_pda.to_account_info(),
|
|
||||||
a.signer.to_account_info(),
|
|
||||||
a.collection_update_authority.to_account_info(),
|
|
||||||
a.system_program.to_account_info(),
|
|
||||||
a.token_metadata_program.to_account_info(),
|
|
||||||
],
|
|
||||||
&[],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// 2) Чеканим 1 токен на ATA получателя
|
|
||||||
let ix_mint_to = spl_ix::mint_to(
|
|
||||||
&a.token_program.key(),
|
|
||||||
&a.mint_pda.key(),
|
|
||||||
&a.recipient_ata.key(),
|
|
||||||
&a.signer.key(),
|
|
||||||
&[],
|
|
||||||
1,
|
|
||||||
)?;
|
|
||||||
invoke(
|
|
||||||
&ix_mint_to,
|
|
||||||
&[
|
|
||||||
a.mint_pda.to_account_info(),
|
|
||||||
a.recipient_ata.to_account_info(),
|
|
||||||
a.signer.to_account_info(),
|
|
||||||
a.token_program.to_account_info(),
|
|
||||||
],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// 3) Замораживаем ATA получателя (freeze authority = signer)
|
|
||||||
let ix_freeze = spl_ix::freeze_account(
|
|
||||||
&a.token_program.key(),
|
|
||||||
&a.recipient_ata.key(),
|
|
||||||
&a.mint_pda.key(),
|
|
||||||
&a.signer.key(),
|
|
||||||
&[],
|
|
||||||
)?;
|
|
||||||
invoke(
|
|
||||||
&ix_freeze,
|
|
||||||
&[
|
|
||||||
a.recipient_ata.to_account_info(),
|
|
||||||
a.mint_pda.to_account_info(),
|
|
||||||
a.signer.to_account_info(),
|
|
||||||
a.token_program.to_account_info(),
|
|
||||||
],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// 4) Создаём Master Edition
|
|
||||||
let ix_me = CreateMasterEditionV3Builder::new()
|
|
||||||
.edition(a.master_edition_pda.key())
|
|
||||||
.mint(a.mint_pda.key())
|
|
||||||
.update_authority(a.collection_update_authority.key())
|
|
||||||
.mint_authority(a.signer.key())
|
|
||||||
.payer(a.signer.key())
|
|
||||||
.metadata(a.metadata_pda.key())
|
|
||||||
.token_program(a.token_program.key())
|
|
||||||
.system_program(a.system_program.key())
|
|
||||||
.max_supply(0)
|
|
||||||
.instruction();
|
|
||||||
|
|
||||||
invoke_signed(
|
|
||||||
&ix_me,
|
|
||||||
&[
|
|
||||||
a.master_edition_pda.to_account_info(),
|
|
||||||
a.mint_pda.to_account_info(),
|
|
||||||
a.collection_update_authority.to_account_info(),
|
|
||||||
a.signer.to_account_info(),
|
|
||||||
a.metadata_pda.to_account_info(),
|
|
||||||
a.token_program.to_account_info(),
|
|
||||||
a.system_program.to_account_info(),
|
|
||||||
a.token_metadata_program.to_account_info(),
|
|
||||||
],
|
|
||||||
&[],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// 5) Verify как часть коллекции
|
|
||||||
// Метод называется collection_master_edition_account(...)
|
|
||||||
let ix_verify = SetAndVerifySizedCollectionItemBuilder::new()
|
|
||||||
.metadata(a.metadata_pda.key())
|
|
||||||
.collection_authority(a.collection_update_authority.key())
|
|
||||||
.payer(a.signer.key())
|
|
||||||
.update_authority(a.collection_update_authority.key())
|
|
||||||
.collection_mint(a.collection_mint.key())
|
|
||||||
.collection(a.collection_metadata_pda.key())
|
|
||||||
.collection_master_edition_account(a.collection_master_edition_pda.key())
|
|
||||||
.instruction();
|
|
||||||
|
|
||||||
invoke_signed(
|
|
||||||
&ix_verify,
|
|
||||||
&[
|
|
||||||
a.metadata_pda.to_account_info(),
|
|
||||||
a.collection_update_authority.to_account_info(),
|
|
||||||
a.signer.to_account_info(),
|
|
||||||
a.collection_update_authority.to_account_info(),
|
|
||||||
a.collection_mint.to_account_info(),
|
|
||||||
a.collection_metadata_pda.to_account_info(),
|
|
||||||
a.collection_master_edition_pda.to_account_info(),
|
|
||||||
a.token_metadata_program.to_account_info(),
|
|
||||||
],
|
|
||||||
&[],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
msg!("NFT создан, заморожен, мастер-издание создано и верифицировано в коллекции (index={})", params.index);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn truncate(s: &str, max: usize) -> String {
|
|
||||||
if s.len() <= max { s.to_string() } else { s.chars().take(max).collect() }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[error_code]
|
|
||||||
pub enum CustomError {
|
|
||||||
#[msg("Invalid Token Metadata program account")]
|
|
||||||
InvalidMetadataProgram,
|
|
||||||
}
|
|
||||||
@ -1,16 +1,290 @@
|
|||||||
import * as anchor from "@coral-xyz/anchor";
|
import * as anchor from "@coral-xyz/anchor";
|
||||||
import { Program } from "@coral-xyz/anchor";
|
import { Program } from "@coral-xyz/anchor";
|
||||||
|
import {
|
||||||
|
Ed25519Program,
|
||||||
|
PublicKey,
|
||||||
|
SYSVAR_INSTRUCTIONS_PUBKEY,
|
||||||
|
SystemProgram,
|
||||||
|
Transaction,
|
||||||
|
} from "@solana/web3.js";
|
||||||
|
import { createHash } from "crypto";
|
||||||
|
import { expect } from "chai";
|
||||||
import { Shine } from "../target/types/shine";
|
import { Shine } from "../target/types/shine";
|
||||||
|
|
||||||
describe("shine", () => {
|
const MAGIC = Buffer.from("SHiNE", "utf8");
|
||||||
// Configure the client to use the local cluster.
|
const FORMAT_MAJOR = 1;
|
||||||
anchor.setProvider(anchor.AnchorProvider.env());
|
const FORMAT_MINOR = 0;
|
||||||
|
const RESERVED = Buffer.from([0, 0, 0, 0, 0]);
|
||||||
|
const ZERO_HASH = Buffer.alloc(32, 0);
|
||||||
|
const KEY_STATUS_CREATED = 0;
|
||||||
|
|
||||||
|
const LIMIT_STEP = 10_000n;
|
||||||
|
const START_BONUS_LIMIT = 100_000n;
|
||||||
|
const FEE_RECEIVER = new PublicKey("9vXFoN9ngfN1gpqQ3HT5n3y9Wp2r7HnSQckirgwVwWwb");
|
||||||
|
|
||||||
|
type MutableFields = {
|
||||||
|
blockchainKey: PublicKey;
|
||||||
|
deviceKey: PublicKey;
|
||||||
|
chainNumber: number;
|
||||||
|
isServer: boolean;
|
||||||
|
serverKey: PublicKey;
|
||||||
|
serverAddress: string;
|
||||||
|
connectionServers: string[];
|
||||||
|
trustedCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UnsignedRecord = {
|
||||||
|
createdAtMs: bigint;
|
||||||
|
updatedAtMs: bigint;
|
||||||
|
version: number;
|
||||||
|
prevHash: Buffer;
|
||||||
|
login: string;
|
||||||
|
rootKeyStatus: number;
|
||||||
|
rootKey: PublicKey;
|
||||||
|
blockchainKeyStatus: number;
|
||||||
|
blockchainKey: PublicKey;
|
||||||
|
deviceKeyStatus: number;
|
||||||
|
deviceKey: PublicKey;
|
||||||
|
chainNumber: number;
|
||||||
|
balance: bigint;
|
||||||
|
isServer: boolean;
|
||||||
|
serverKey: PublicKey;
|
||||||
|
serverAddress: string;
|
||||||
|
connectionServers: string[];
|
||||||
|
trustedCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function u16le(v: number): Buffer {
|
||||||
|
const b = Buffer.alloc(2);
|
||||||
|
b.writeUInt16LE(v, 0);
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function u32le(v: number): Buffer {
|
||||||
|
const b = Buffer.alloc(4);
|
||||||
|
b.writeUInt32LE(v, 0);
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function u64le(v: bigint): Buffer {
|
||||||
|
const b = Buffer.alloc(8);
|
||||||
|
b.writeBigUInt64LE(v, 0);
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeUnsignedRecord(r: UnsignedRecord): Buffer {
|
||||||
|
const loginBytes = Buffer.from(r.login, "utf8");
|
||||||
|
const serverAddressBytes = Buffer.from(r.serverAddress, "utf8");
|
||||||
|
|
||||||
|
const out: Buffer[] = [];
|
||||||
|
out.push(MAGIC);
|
||||||
|
out.push(Buffer.from([FORMAT_MAJOR]));
|
||||||
|
out.push(Buffer.from([FORMAT_MINOR]));
|
||||||
|
out.push(Buffer.alloc(2, 0)); // record_len placeholder
|
||||||
|
|
||||||
|
out.push(u64le(r.createdAtMs));
|
||||||
|
out.push(u64le(r.updatedAtMs));
|
||||||
|
out.push(u32le(r.version));
|
||||||
|
out.push(r.prevHash);
|
||||||
|
|
||||||
|
out.push(Buffer.from([loginBytes.length]));
|
||||||
|
out.push(loginBytes);
|
||||||
|
|
||||||
|
out.push(Buffer.from([r.rootKeyStatus]));
|
||||||
|
out.push(r.rootKey.toBuffer());
|
||||||
|
out.push(Buffer.from([r.blockchainKeyStatus]));
|
||||||
|
out.push(r.blockchainKey.toBuffer());
|
||||||
|
out.push(Buffer.from([r.deviceKeyStatus]));
|
||||||
|
out.push(r.deviceKey.toBuffer());
|
||||||
|
|
||||||
|
out.push(u16le(r.chainNumber));
|
||||||
|
out.push(u64le(r.balance));
|
||||||
|
|
||||||
|
out.push(Buffer.from([r.isServer ? 1 : 0]));
|
||||||
|
if (r.isServer) {
|
||||||
|
out.push(r.serverKey.toBuffer());
|
||||||
|
out.push(Buffer.from([serverAddressBytes.length]));
|
||||||
|
out.push(serverAddressBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push(Buffer.from([r.connectionServers.length]));
|
||||||
|
for (const s of r.connectionServers) {
|
||||||
|
const sb = Buffer.from(s, "utf8");
|
||||||
|
out.push(Buffer.from([sb.length]));
|
||||||
|
out.push(sb);
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push(Buffer.from([r.trustedCount]));
|
||||||
|
out.push(RESERVED);
|
||||||
|
|
||||||
|
const unsigned = Buffer.concat(out);
|
||||||
|
const recordLen = unsigned.length + 64;
|
||||||
|
unsigned.writeUInt16LE(recordLen, 7);
|
||||||
|
return unsigned;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sha256(buf: Buffer): Buffer {
|
||||||
|
return createHash("sha256").update(buf).digest();
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractSigFromEdIx(ixData: Buffer): Buffer {
|
||||||
|
const signatureOffset = ixData.readUInt16LE(2);
|
||||||
|
return ixData.subarray(signatureOffset, signatureOffset + 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("shine_users e2e", () => {
|
||||||
|
anchor.setProvider(anchor.AnchorProvider.env());
|
||||||
|
const provider = anchor.getProvider() as anchor.AnchorProvider;
|
||||||
const program = anchor.workspace.shine as Program<Shine>;
|
const program = anchor.workspace.shine as Program<Shine>;
|
||||||
|
|
||||||
it("Is initialized!", async () => {
|
it("registers user and updates balance/server data", async () => {
|
||||||
// Add your test here.
|
const login = `u${Date.now().toString().slice(-10)}`;
|
||||||
const tx = await program.methods.initialize().rpc();
|
const [userPda] = PublicKey.findProgramAddressSync(
|
||||||
console.log("Your transaction signature", tx);
|
[Buffer.from("login="), Buffer.from(login, "utf8")],
|
||||||
|
program.programId
|
||||||
|
);
|
||||||
|
|
||||||
|
const root = anchor.web3.Keypair.generate();
|
||||||
|
const blockchainKey = anchor.web3.Keypair.generate().publicKey;
|
||||||
|
const deviceKey = anchor.web3.Keypair.generate().publicKey;
|
||||||
|
const serverKey1 = anchor.web3.Keypair.generate().publicKey;
|
||||||
|
const serverKey2 = anchor.web3.Keypair.generate().publicKey;
|
||||||
|
|
||||||
|
const createdAtMs = BigInt(Date.now());
|
||||||
|
const additionalLimitCreate = 20_000n;
|
||||||
|
expect(additionalLimitCreate % LIMIT_STEP).eq(0n);
|
||||||
|
|
||||||
|
const createRecord: UnsignedRecord = {
|
||||||
|
createdAtMs,
|
||||||
|
updatedAtMs: createdAtMs,
|
||||||
|
version: 0,
|
||||||
|
prevHash: ZERO_HASH,
|
||||||
|
login,
|
||||||
|
rootKeyStatus: KEY_STATUS_CREATED,
|
||||||
|
rootKey: root.publicKey,
|
||||||
|
blockchainKeyStatus: KEY_STATUS_CREATED,
|
||||||
|
blockchainKey,
|
||||||
|
deviceKeyStatus: KEY_STATUS_CREATED,
|
||||||
|
deviceKey,
|
||||||
|
chainNumber: 1,
|
||||||
|
balance: START_BONUS_LIMIT + additionalLimitCreate,
|
||||||
|
isServer: true,
|
||||||
|
serverKey: serverKey1,
|
||||||
|
serverAddress: "https://srv-1.local",
|
||||||
|
connectionServers: ["srv_login_1"],
|
||||||
|
trustedCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const createUnsigned = serializeUnsignedRecord(createRecord);
|
||||||
|
const createHash = sha256(createUnsigned);
|
||||||
|
const createEdIx = Ed25519Program.createInstructionWithPrivateKey({
|
||||||
|
privateKey: root.secretKey,
|
||||||
|
message: createHash,
|
||||||
|
});
|
||||||
|
const createSig = extractSigFromEdIx(Buffer.from(createEdIx.data));
|
||||||
|
|
||||||
|
const createIx = await program.methods
|
||||||
|
.createUserPda({
|
||||||
|
login,
|
||||||
|
rootKey: root.publicKey,
|
||||||
|
createdAtMs: new anchor.BN(createdAtMs.toString()),
|
||||||
|
additionalLimit: new anchor.BN(additionalLimitCreate.toString()),
|
||||||
|
fields: {
|
||||||
|
blockchainKey,
|
||||||
|
deviceKey,
|
||||||
|
chainNumber: 1,
|
||||||
|
isServer: true,
|
||||||
|
serverKey: serverKey1,
|
||||||
|
serverAddress: "https://srv-1.local",
|
||||||
|
connectionServers: ["srv_login_1"],
|
||||||
|
trustedCount: 0,
|
||||||
|
},
|
||||||
|
signature: createSig,
|
||||||
|
})
|
||||||
|
.accounts({
|
||||||
|
signer: provider.wallet.publicKey,
|
||||||
|
userPda,
|
||||||
|
systemProgram: SystemProgram.programId,
|
||||||
|
feeReceiver: FEE_RECEIVER,
|
||||||
|
instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
|
||||||
|
})
|
||||||
|
.instruction();
|
||||||
|
|
||||||
|
await provider.sendAndConfirm(new Transaction().add(createEdIx, createIx), []);
|
||||||
|
|
||||||
|
const createAcc = await provider.connection.getAccountInfo(userPda);
|
||||||
|
expect(createAcc).not.eq(null);
|
||||||
|
expect(createAcc!.owner.toBase58()).eq(program.programId.toBase58());
|
||||||
|
|
||||||
|
const additionalLimitUpdate = 30_000n;
|
||||||
|
expect(additionalLimitUpdate % LIMIT_STEP).eq(0n);
|
||||||
|
|
||||||
|
const updateRecord: UnsignedRecord = {
|
||||||
|
createdAtMs,
|
||||||
|
updatedAtMs: createdAtMs + 1_000n,
|
||||||
|
version: 1,
|
||||||
|
prevHash: sha256(createUnsigned),
|
||||||
|
login,
|
||||||
|
rootKeyStatus: KEY_STATUS_CREATED,
|
||||||
|
rootKey: root.publicKey,
|
||||||
|
blockchainKeyStatus: KEY_STATUS_CREATED,
|
||||||
|
blockchainKey: anchor.web3.Keypair.generate().publicKey,
|
||||||
|
deviceKeyStatus: KEY_STATUS_CREATED,
|
||||||
|
deviceKey: anchor.web3.Keypair.generate().publicKey,
|
||||||
|
chainNumber: 1,
|
||||||
|
balance: START_BONUS_LIMIT + additionalLimitCreate + additionalLimitUpdate,
|
||||||
|
isServer: true,
|
||||||
|
serverKey: serverKey2,
|
||||||
|
serverAddress: "https://srv-2.local",
|
||||||
|
connectionServers: ["srv_login_2", "srv_login_3"],
|
||||||
|
trustedCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateUnsigned = serializeUnsignedRecord(updateRecord);
|
||||||
|
const updateHash = sha256(updateUnsigned);
|
||||||
|
const updateEdIx = Ed25519Program.createInstructionWithPrivateKey({
|
||||||
|
privateKey: root.secretKey,
|
||||||
|
message: updateHash,
|
||||||
|
});
|
||||||
|
const updateSig = extractSigFromEdIx(Buffer.from(updateEdIx.data));
|
||||||
|
|
||||||
|
const updateIx = await program.methods
|
||||||
|
.updateUserPda({
|
||||||
|
login,
|
||||||
|
rootKey: root.publicKey,
|
||||||
|
createdAtMs: new anchor.BN(createdAtMs.toString()),
|
||||||
|
updatedAtMs: new anchor.BN((createdAtMs + 1_000n).toString()),
|
||||||
|
version: 1,
|
||||||
|
prevHash: sha256(createUnsigned),
|
||||||
|
additionalLimit: new anchor.BN(additionalLimitUpdate.toString()),
|
||||||
|
fields: {
|
||||||
|
blockchainKey: updateRecord.blockchainKey,
|
||||||
|
deviceKey: updateRecord.deviceKey,
|
||||||
|
chainNumber: 1,
|
||||||
|
isServer: true,
|
||||||
|
serverKey: serverKey2,
|
||||||
|
serverAddress: "https://srv-2.local",
|
||||||
|
connectionServers: ["srv_login_2", "srv_login_3"],
|
||||||
|
trustedCount: 0,
|
||||||
|
},
|
||||||
|
signature: updateSig,
|
||||||
|
})
|
||||||
|
.accounts({
|
||||||
|
signer: provider.wallet.publicKey,
|
||||||
|
userPda,
|
||||||
|
systemProgram: SystemProgram.programId,
|
||||||
|
feeReceiver: FEE_RECEIVER,
|
||||||
|
instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
|
||||||
|
})
|
||||||
|
.instruction();
|
||||||
|
|
||||||
|
await provider.sendAndConfirm(new Transaction().add(updateEdIx, updateIx), []);
|
||||||
|
|
||||||
|
const updatedAcc = await provider.connection.getAccountInfo(userPda);
|
||||||
|
expect(updatedAcc).not.eq(null);
|
||||||
|
const data = updatedAcc!.data;
|
||||||
|
expect(data.subarray(0, 5).toString("utf8")).eq("SHiNE");
|
||||||
|
expect(data[5]).eq(FORMAT_MAJOR);
|
||||||
|
expect(data[6]).eq(FORMAT_MINOR);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user