206 lines
8.3 KiB
JavaScript
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);
|
|
});
|