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