shine-solana/shine/programs/shine_payments/web/dao_revoke_vote.html

343 lines
11 KiB
HTML
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.

<!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>