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

206 lines
8.3 KiB
JavaScript

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");
// Programs
const MPL = new PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s");
// utils
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(`ENV ${name} is required`);
return v;
}
function pickBase58(raw, name) {
const m = (raw || "").toString().match(BASE58_RE);
if (!m) throw new Error(`${name} invalid: "${raw}"`);
return m[0];
}
function disc8(name) {
const preimage = `global:${name}`;
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;
}
async function sendTx(provider, tx, signers = []) {
const conn = provider.connection;
const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash("confirmed");
tx.recentBlockhash = blockhash;
tx.feePayer = provider.wallet.publicKey;
for (const s of signers) tx.partialSign(s);
const raw = await provider.wallet.signTransaction(tx);
const sig = await conn.sendRawTransaction(raw.serialize(), {
skipPreflight: false,
preflightCommitment: "confirmed",
maxRetries: 3,
});
await conn.confirmTransaction({ signature: sig, blockhash, lastValidBlockHeight }, "confirmed");
return sig;
}
(async () => {
const count = Number(process.argv[2] || "3"); // сколько NFT сделать
const RPC = mustEnv("ANCHOR_PROVIDER_URL");
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();
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 :", MPL.toBase58());
console.log("ATA Program PID :", ASSOCIATED_TOKEN_PROGRAM_ID.toBase58());
console.log("Count :", count);
console.log("────────────────────────────────────────────────────────");
// ensure init (если уже есть — просто пропустим)
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);
}
const minted = [];
for (let i = 0; i < count; i++) {
// 1) создаём новый mint (NFT)
const mintPubkey = await createMint(conn, wallet.payer, wallet.publicKey, wallet.publicKey, 0);
console.log(`\n[${i + 1}/${count}] NFT mint:`, mintPubkey.toBase58());
// 2) создаём ATA при необходимости
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(" ATA created:", recipientAta.toBase58(), sigAta);
} else {
console.log(" ATA exists:", recipientAta.toBase58());
}
// PDA для нашего NFT (metadata/master edition)
const [metadataPda] = PublicKey.findProgramAddressSync(
[Buffer.from("metadata"), MPL.toBuffer(), mintPubkey.toBuffer()],
MPL
);
const [masterEditionPda] = PublicKey.findProgramAddressSync(
[Buffer.from("metadata"), MPL.toBuffer(), mintPubkey.toBuffer(), Buffer.from("edition")],
MPL
);
// PDA коллекции
const [collectionMetadataPda] = PublicKey.findProgramAddressSync(
[Buffer.from("metadata"), MPL.toBuffer(), COLLECTION_MINT.toBuffer()],
MPL
);
const [collectionMasterEditionPda] = PublicKey.findProgramAddressSync(
[Buffer.from("metadata"), MPL.toBuffer(), COLLECTION_MINT.toBuffer(), Buffer.from("edition")],
MPL
);
// 3) add_bonus(investor, amount)
const investor = recipientOwner;
// Для наглядности — разные суммы
const amount = BigInt(100_000_000 + i * 10_000_000); // 100M, 110M, 120M...
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 },
{ pubkey: collectionMasterEditionPda, isSigner: false, isWritable: true },
{ pubkey: wallet.publicKey, isSigner: true, isWritable: false }, // collection_update_authority
{ pubkey: metadataPda, isSigner: false, isWritable: true },
{ pubkey: masterEditionPda, isSigner: false, isWritable: true },
{ pubkey: MPL, 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(" ATA frozen:", acc.isFrozen);
minted.push({
mint: mintPubkey.toBase58(),
ata: recipientAta.toBase58(),
addBonusSig: sig,
metadataPda: metadataPda.toBase58(),
});
}
console.log("\n==================== SUMMARY ====================");
console.log("Wallet:", wallet.publicKey.toBase58());
console.log("Collection:", COLLECTION_MINT.toBase58());
console.table(minted);
console.log("Открой каждую сигнатуру (add_bonus tx) и mint в Explorer:");
minted.forEach((m, i) => {
console.log(`[${i + 1}] Mint: https://explorer.solana.com/address/${m.mint}?cluster=devnet`);
console.log(` TX : https://explorer.solana.com/tx/${m.addBonusSig}?cluster=devnet`);
});
console.log("=================================================");
})().catch((e) => {
console.error("Ошибка:", e);
process.exit(1);
});