shine-solana/shine/scripts/devnet/quick_devnet_e2e.js

286 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const anchor = require("@coral-xyz/anchor");
const {
Connection,
PublicKey,
SystemProgram,
Transaction,
TransactionInstruction,
} = require("@solana/web3.js");
const {
getAssociatedTokenAddress,
createAssociatedTokenAccountInstruction,
TOKEN_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID,
createMint,
getAccount,
} = require("@solana/spl-token");
const crypto = require("crypto");
// Адрес программы метаданных Metaplex (фиксированный)
const METAPLEX_TOKEN_METADATA_PROGRAM_ID = new PublicKey(
"metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
);
// ────────────────────────────────
// Утилиты
// ────────────────────────────────
const BASE58_RE = /[1-9A-HJ-NP-Za-km-z]{32,}/g;
function mustEnv(name) {
const v = (process.env[name] || "").trim();
if (!v) throw new Error(`Переменная окружения ${name} не задана`);
return v;
}
function pickBase58(raw, name) {
const m = (raw || "").toString().match(BASE58_RE);
if (!m) throw new Error(`${name} не найден/невалиден: "${raw}"`);
return m[0];
}
// Anchor discriminator: sha256("global:<ix_name>") первые 8 байт
function disc8(ixName) {
const preimage = `global:${ixName}`;
const h = crypto.createHash("sha256").update(preimage).digest();
return h.subarray(0, 8);
}
function u64le(n) {
const bn = BigInt(n.toString());
const buf = Buffer.alloc(8);
buf.writeBigUInt64LE(bn);
return buf;
}
// Надёжная отправка транзакций с ретраями при «Blockhash not found»
async function sendTx(provider, tx, signers = []) {
const conn = provider.connection;
let lastErr;
for (let attempt = 0; attempt < 3; attempt++) {
try {
const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash(
"confirmed"
);
tx.recentBlockhash = blockhash;
tx.feePayer = provider.wallet.publicKey;
for (const s of signers) tx.partialSign(s);
const signed = await provider.wallet.signTransaction(tx);
const sig = await conn.sendRawTransaction(signed.serialize(), {
skipPreflight: false,
preflightCommitment: "confirmed",
maxRetries: 3,
});
await conn.confirmTransaction(
{ signature: sig, blockhash, lastValidBlockHeight },
"confirmed"
);
return sig;
} catch (e) {
lastErr = e;
const msg = String(e?.message || e).toLowerCase();
if (msg.includes("blockhash not found") || msg.includes("expired")) {
// пробуем ещё раз со свежим blockhash
continue;
}
throw e;
}
}
throw lastErr;
}
// ────────────────────────────────
// Основной сценарий
// ────────────────────────────────
(async () => {
// Провайдер / окружение
const RPC = mustEnv("ANCHOR_PROVIDER_URL");
const WALLET_PATH = mustEnv("ANCHOR_WALLET");
const PROGRAM_ID = new PublicKey(
pickBase58(mustEnv("PROGRAM_ID"), "PROGRAM_ID")
);
const COLLECTION_MINT = new PublicKey(
pickBase58(mustEnv("COLLECTION_MINT"), "COLLECTION_MINT")
);
const provider = anchor.AnchorProvider.env(); // читает из ENV
anchor.setProvider(provider);
const conn = provider.connection;
const wallet = provider.wallet;
console.log("────────────────────────────────────────────────────────");
console.log("RPC :", RPC);
console.log("Wallet :", wallet.publicKey.toBase58());
console.log("Program ID :", PROGRAM_ID.toBase58());
console.log("Collection mint :", COLLECTION_MINT.toBase58());
console.log(
"TokenMetadata PID :",
METAPLEX_TOKEN_METADATA_PROGRAM_ID.toBase58()
);
console.log("ATA Program PID :", ASSOCIATED_TOKEN_PROGRAM_ID.toBase58());
console.log("────────────────────────────────────────────────────────");
// 1) INIT (создаёт PDA состояния)
const [statePda] = PublicKey.findProgramAddressSync(
[Buffer.from("shine_investments_state")],
PROGRAM_ID
);
const initIx = new TransactionInstruction({
programId: PROGRAM_ID,
keys: [
{ pubkey: wallet.publicKey, isSigner: true, isWritable: true }, // payer
{ pubkey: statePda, isSigner: false, isWritable: true }, // state_pda
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
],
data: Buffer.from([...disc8("init")]), // без аргументов
});
try {
const sigInit = await sendTx(provider, new Transaction().add(initIx));
console.log(
"init() tx:",
sigInit,
`https://explorer.solana.com/tx/${sigInit}?cluster=devnet`
);
} catch (e) {
console.log("init(): возможно уже выполнен ->", e.message);
}
// 2) Локально создаём mint нового NFT и ATA получателя
const mintPubkey = await createMint(
conn,
wallet.payer,
wallet.publicKey,
wallet.publicKey,
0
);
console.log("NFT mint:", mintPubkey.toBase58());
const recipientOwner = wallet.publicKey;
const recipientAta = await getAssociatedTokenAddress(
mintPubkey,
recipientOwner
);
const ataInfo = await conn.getAccountInfo(recipientAta);
if (!ataInfo) {
const createAtaIx = createAssociatedTokenAccountInstruction(
wallet.publicKey,
recipientAta,
recipientOwner,
mintPubkey
);
const sigAta = await sendTx(
provider,
new Transaction().add(createAtaIx)
);
console.log("Created ATA:", recipientAta.toBase58(), sigAta);
} else {
console.log("ATA exists:", recipientAta.toBase58());
}
// PDA для metadata/master edition нашего нового NFT
const [metadataPda] = PublicKey.findProgramAddressSync(
[
Buffer.from("metadata"),
METAPLEX_TOKEN_METADATA_PROGRAM_ID.toBuffer(),
mintPubkey.toBuffer(),
],
METAPLEX_TOKEN_METADATA_PROGRAM_ID
);
const [masterEditionPda] = PublicKey.findProgramAddressSync(
[
Buffer.from("metadata"),
METAPLEX_TOKEN_METADATA_PROGRAM_ID.toBuffer(),
mintPubkey.toBuffer(),
Buffer.from("edition"),
],
METAPLEX_TOKEN_METADATA_PROGRAM_ID
);
// PDA коллекции (metadata/master edition)
const [collectionMetadataPda] = PublicKey.findProgramAddressSync(
[
Buffer.from("metadata"),
METAPLEX_TOKEN_METADATA_PROGRAM_ID.toBuffer(),
COLLECTION_MINT.toBuffer(),
],
METAPLEX_TOKEN_METADATA_PROGRAM_ID
);
const [collectionMasterEditionPda] = PublicKey.findProgramAddressSync(
[
Buffer.from("metadata"),
METAPLEX_TOKEN_METADATA_PROGRAM_ID.toBuffer(),
COLLECTION_MINT.toBuffer(),
Buffer.from("edition"),
],
METAPLEX_TOKEN_METADATA_PROGRAM_ID
);
// 3) add_bonus(investor: Pubkey, amount: u64) — raw-инструкция
// Порядок аккаунтов должен совпасть с #[derive(Accounts)] AddBonusCtx:
// signer(Signer), state_pda(mut), mint_pda(mut), recipient_ata(mut), recipient_owner,
// collection_mint, collection_metadata_pda(mut), collection_master_edition_pda(mut),
// collection_update_authority(Signer), metadata_pda(mut), master_edition_pda(mut),
// token_metadata_program, token_program, associated_token_program, system_program
const investor = recipientOwner;
const amount = 123_000_000n; // u64
const addBonusData = Buffer.concat([
disc8("add_bonus"),
investor.toBuffer(),
u64le(amount),
]);
const addBonusIx = new TransactionInstruction({
programId: PROGRAM_ID,
keys: [
{ pubkey: wallet.publicKey, isSigner: true, isWritable: false }, // signer
{ pubkey: statePda, isSigner: false, isWritable: true }, // state_pda
{ pubkey: mintPubkey, isSigner: false, isWritable: true }, // mint_pda
{ pubkey: recipientAta, isSigner: false, isWritable: true }, // recipient_ata
{ pubkey: recipientOwner, isSigner: false, isWritable: false }, // recipient_owner
{ pubkey: COLLECTION_MINT, isSigner: false, isWritable: false }, // collection_mint
{ pubkey: collectionMetadataPda, isSigner: false, isWritable: true }, // collection_metadata_pda
{ pubkey: collectionMasterEditionPda, isSigner: false, isWritable: true }, // collection_master_edition_pda
{ pubkey: wallet.publicKey, isSigner: true, isWritable: false }, // collection_update_authority
{ pubkey: metadataPda, isSigner: false, isWritable: true }, // metadata_pda
{ pubkey: masterEditionPda, isSigner: false, isWritable: true }, // master_edition_pda
{ pubkey: METAPLEX_TOKEN_METADATA_PROGRAM_ID, isSigner: false, isWritable: false },
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
{ pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
],
data: addBonusData,
});
const sig = await sendTx(provider, new Transaction().add(addBonusIx));
console.log(
"add_bonus() tx:",
sig,
`https://explorer.solana.com/tx/${sig}?cluster=devnet`
);
// 4) простые проверки
const acc = await getAccount(conn, recipientAta);
console.log("isFrozen (ATA):", acc.isFrozen);
if (!acc.isFrozen) throw new Error("Ожидали заморозку ATA после add_bonus()");
const mdInfo = await conn.getAccountInfo(metadataPda);
if (!mdInfo || mdInfo.data.length === 0)
throw new Error("Metadata PDA отсутствует или пуст");
console.log(
"Готово: raw-инструкции прошли, NFT создан/верифицирован и ATA заморожен"
);
})().catch((e) => {
console.error("Ошибка e2e:", e);
process.exit(1);
});