343 lines
11 KiB
HTML
343 lines
11 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>DAO revoke vote — Shine Payments Devnet</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: 1200px; }
|
||
.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; }
|
||
label { display: inline-flex; flex-direction: column; gap: 6px; color: var(--muted); min-width: 280px; }
|
||
input { padding: 9px 10px; 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; }
|
||
.back { color: var(--muted); text-decoration: none; font-size: 18px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
<div style="margin-bottom: 12px;"><a class="back" href="./index.html">← На главную</a></div>
|
||
<h1>DAO: голосование на revoke/burn membership token (Devnet)</h1>
|
||
<div class="muted">Governance program: <code id="govPid"></code></div>
|
||
|
||
<div class="panel">
|
||
<div class="row">
|
||
<button id="connectBtn">Подключить Phantom</button>
|
||
</div>
|
||
<div id="walletInfo" class="muted">Кошелек: не подключен</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="row">
|
||
<label>Realm
|
||
<input id="realm" placeholder="Realm pubkey" />
|
||
</label>
|
||
<label>Governance
|
||
<input id="governance" placeholder="Governance pubkey" />
|
||
</label>
|
||
<label>Community mint
|
||
<input id="mint" placeholder="Mint pubkey" />
|
||
</label>
|
||
</div>
|
||
<div class="row">
|
||
<label>Target owner
|
||
<input id="targetOwner" placeholder="Кого лишаем governance token" />
|
||
</label>
|
||
<label>Amount
|
||
<input id="amount" value="1" />
|
||
</label>
|
||
</div>
|
||
<div class="row">
|
||
<button id="createVoteBtn">Create + SignOff + Vote</button>
|
||
</div>
|
||
<div id="proposalResult" class="muted"></div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="row">
|
||
<label>Proposal
|
||
<input id="proposal" placeholder="Proposal pubkey" />
|
||
</label>
|
||
<label>Proposal transaction
|
||
<input id="proposalTx" placeholder="ProposalTransaction pubkey" />
|
||
</label>
|
||
</div>
|
||
<div class="row">
|
||
<button id="executeBtn">Execute revoke</button>
|
||
</div>
|
||
<div class="muted">Если получите hold-up (`0x20d`) — дождитесь конца voting window/hold-up и повторите execute.</div>
|
||
<div id="executeResult" class="muted"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script type="module">
|
||
import BN from "https://esm.sh/bn.js@5.2.1";
|
||
import {
|
||
Connection,
|
||
PublicKey,
|
||
Transaction,
|
||
clusterApiUrl
|
||
} from "https://esm.sh/@solana/web3.js@1.95.3";
|
||
import {
|
||
PROGRAM_VERSION_V3,
|
||
Vote,
|
||
YesNoVote,
|
||
VoteType,
|
||
InstructionData,
|
||
AccountMetaData,
|
||
withRevokeGoverningTokens,
|
||
withCreateProposal,
|
||
withInsertTransaction,
|
||
withSignOffProposal,
|
||
withCastVote,
|
||
withExecuteTransaction,
|
||
getTokenOwnerRecordAddress
|
||
} from "https://esm.sh/@solana/spl-governance@0.3.28";
|
||
|
||
const GOVERNANCE_PROGRAM_ID = new PublicKey("GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw");
|
||
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
|
||
document.getElementById("govPid").textContent = GOVERNANCE_PROGRAM_ID.toBase58();
|
||
|
||
let wallet = null;
|
||
let walletPubkey = null;
|
||
|
||
function out(id, html, cls = "muted") {
|
||
const el = document.getElementById(id);
|
||
el.className = cls;
|
||
el.innerHTML = html;
|
||
}
|
||
|
||
function mustPubkey(id) {
|
||
const raw = document.getElementById(id).value.trim();
|
||
if (!raw) throw new Error(`Пустое поле: ${id}`);
|
||
return new PublicKey(raw);
|
||
}
|
||
|
||
function toGovernanceInstructionData(ix) {
|
||
return new InstructionData({
|
||
programId: ix.programId,
|
||
accounts: ix.keys.map(
|
||
(k) => new AccountMetaData({
|
||
pubkey: k.pubkey,
|
||
isSigner: !!k.isSigner,
|
||
isWritable: !!k.isWritable,
|
||
})
|
||
),
|
||
data: Uint8Array.from(ix.data),
|
||
});
|
||
}
|
||
|
||
async function connect() {
|
||
if (!window.solana || !window.solana.isPhantom) throw new Error("Phantom не найден");
|
||
wallet = window.solana;
|
||
const res = await wallet.connect();
|
||
walletPubkey = new PublicKey(res.publicKey.toString());
|
||
out("walletInfo", `Кошелек: <code>${walletPubkey.toBase58()}</code>`, "muted");
|
||
}
|
||
|
||
async function sendIxs(ixs) {
|
||
if (!walletPubkey) await connect();
|
||
const tx = new Transaction().add(...ixs);
|
||
tx.feePayer = walletPubkey;
|
||
const bh = await connection.getLatestBlockhash("confirmed");
|
||
tx.recentBlockhash = bh.blockhash;
|
||
const signed = await wallet.signTransaction(tx);
|
||
const sig = await connection.sendRawTransaction(signed.serialize(), { skipPreflight: false });
|
||
await connection.confirmTransaction({ signature: sig, blockhash: bh.blockhash, lastValidBlockHeight: bh.lastValidBlockHeight }, "confirmed");
|
||
return sig;
|
||
}
|
||
|
||
async function createSignVote() {
|
||
out("proposalResult", "Выполняю...", "muted");
|
||
try {
|
||
const realm = mustPubkey("realm");
|
||
const governance = mustPubkey("governance");
|
||
const mint = mustPubkey("mint");
|
||
const targetOwner = mustPubkey("targetOwner");
|
||
const amount = new BN(document.getElementById("amount").value.trim() || "1");
|
||
if (amount.lten(0)) throw new Error("Amount должен быть > 0");
|
||
|
||
const proposerRecord = await getTokenOwnerRecordAddress(
|
||
GOVERNANCE_PROGRAM_ID,
|
||
realm,
|
||
mint,
|
||
walletPubkey
|
||
);
|
||
|
||
const proposalName = `Revoke ${amount.toString()} from ${targetOwner.toBase58().slice(0, 8)}...`;
|
||
const proposalDescription = "https://arweave.net/";
|
||
|
||
const ixCreateProposal = [];
|
||
const proposalPk = await withCreateProposal(
|
||
ixCreateProposal,
|
||
GOVERNANCE_PROGRAM_ID,
|
||
PROGRAM_VERSION_V3,
|
||
realm,
|
||
governance,
|
||
proposerRecord,
|
||
proposalName,
|
||
proposalDescription,
|
||
mint,
|
||
walletPubkey,
|
||
undefined,
|
||
VoteType.SINGLE_CHOICE,
|
||
["Approve"],
|
||
true,
|
||
walletPubkey
|
||
);
|
||
const txCreateProposal = await sendIxs(ixCreateProposal);
|
||
|
||
const ixRawRevoke = [];
|
||
await withRevokeGoverningTokens(
|
||
ixRawRevoke,
|
||
GOVERNANCE_PROGRAM_ID,
|
||
PROGRAM_VERSION_V3,
|
||
realm,
|
||
targetOwner,
|
||
mint,
|
||
governance,
|
||
amount
|
||
);
|
||
const revokeInstructionData = toGovernanceInstructionData(ixRawRevoke[0]);
|
||
|
||
const ixInsert = [];
|
||
const proposalTxPk = await withInsertTransaction(
|
||
ixInsert,
|
||
GOVERNANCE_PROGRAM_ID,
|
||
PROGRAM_VERSION_V3,
|
||
governance,
|
||
proposalPk,
|
||
proposerRecord,
|
||
walletPubkey,
|
||
0,
|
||
0,
|
||
0,
|
||
[revokeInstructionData],
|
||
walletPubkey
|
||
);
|
||
const txInsert = await sendIxs(ixInsert);
|
||
|
||
const ixSignOff = [];
|
||
withSignOffProposal(
|
||
ixSignOff,
|
||
GOVERNANCE_PROGRAM_ID,
|
||
PROGRAM_VERSION_V3,
|
||
realm,
|
||
governance,
|
||
proposalPk,
|
||
walletPubkey,
|
||
undefined,
|
||
proposerRecord
|
||
);
|
||
const txSignOff = await sendIxs(ixSignOff);
|
||
|
||
const ixVote = [];
|
||
const vote = Vote.fromYesNoVote(YesNoVote.Yes);
|
||
const voteRecordPk = await withCastVote(
|
||
ixVote,
|
||
GOVERNANCE_PROGRAM_ID,
|
||
PROGRAM_VERSION_V3,
|
||
realm,
|
||
governance,
|
||
proposalPk,
|
||
proposerRecord,
|
||
proposerRecord,
|
||
walletPubkey,
|
||
mint,
|
||
vote,
|
||
walletPubkey
|
||
);
|
||
const txVote = await sendIxs(ixVote);
|
||
|
||
document.getElementById("proposal").value = proposalPk.toBase58();
|
||
document.getElementById("proposalTx").value = proposalTxPk.toBase58();
|
||
|
||
out(
|
||
"proposalResult",
|
||
`Proposal: <code>${proposalPk.toBase58()}</code><br/>` +
|
||
`ProposalTx: <code>${proposalTxPk.toBase58()}</code><br/>` +
|
||
`VoteRecord: <code>${voteRecordPk.toBase58()}</code><br/>` +
|
||
`Tx create: <code>${txCreateProposal}</code><br/>` +
|
||
`Tx insert: <code>${txInsert}</code><br/>` +
|
||
`Tx signOff: <code>${txSignOff}</code><br/>` +
|
||
`Tx vote: <code>${txVote}</code>`,
|
||
"ok"
|
||
);
|
||
} catch (e) {
|
||
out("proposalResult", String(e?.message || e), "err");
|
||
}
|
||
}
|
||
|
||
async function executeRevoke() {
|
||
out("executeResult", "Выполняю execute...", "muted");
|
||
try {
|
||
const governance = mustPubkey("governance");
|
||
const proposal = mustPubkey("proposal");
|
||
const proposalTx = mustPubkey("proposalTx");
|
||
const realm = mustPubkey("realm");
|
||
const mint = mustPubkey("mint");
|
||
const targetOwner = mustPubkey("targetOwner");
|
||
const amount = new BN(document.getElementById("amount").value.trim() || "1");
|
||
|
||
const ixRawRevoke = [];
|
||
await withRevokeGoverningTokens(
|
||
ixRawRevoke,
|
||
GOVERNANCE_PROGRAM_ID,
|
||
PROGRAM_VERSION_V3,
|
||
realm,
|
||
targetOwner,
|
||
mint,
|
||
governance,
|
||
amount
|
||
);
|
||
const revokeInstructionData = toGovernanceInstructionData(ixRawRevoke[0]);
|
||
|
||
const ixExecute = [];
|
||
await withExecuteTransaction(
|
||
ixExecute,
|
||
GOVERNANCE_PROGRAM_ID,
|
||
PROGRAM_VERSION_V3,
|
||
governance,
|
||
proposal,
|
||
proposalTx,
|
||
[revokeInstructionData]
|
||
);
|
||
const sig = await sendIxs(ixExecute);
|
||
out("executeResult", `Execute success. Tx: <code>${sig}</code>`, "ok");
|
||
} catch (e) {
|
||
const msg = String(e?.message || e);
|
||
out("executeResult", msg, "err");
|
||
}
|
||
}
|
||
|
||
document.getElementById("connectBtn").addEventListener("click", async () => {
|
||
try { await connect(); } catch (e) { out("walletInfo", String(e?.message || e), "err"); }
|
||
});
|
||
document.getElementById("createVoteBtn").addEventListener("click", createSignVote);
|
||
document.getElementById("executeBtn").addEventListener("click", executeRevoke);
|
||
</script>
|
||
</body>
|
||
</html>
|