Files
SHiNE-server/shine-solana/shine/programs/shine_payments/web/dao_revoke_vote.html
T

342 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 Mainnet</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 (Mainnet)</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
} 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("https://solana-rpc.publicnode.com", "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>