walletpress/wp-plugin/assets/js/walletpress.js
RMI Admin ba8d1f1261 Initial commit: WalletPress v1.0.0-beta
Self-hosted wallet management platform for WordPress:
- WP plugin: vault, generate, token gates, payments, wallet login, 15 admin pages
- CLI: 17 commands covering all backend feature groups
- Landing pages + PDF documentation
- Connects to rmi-backend API (86+ endpoints, 29+ chains)
2026-06-27 17:12:49 +07:00

396 lines
13 KiB
JavaScript

/**
* WalletPress — Self-Hosted Web3 WordPress Plugin
* Frontend: wallet connect, token gating, crypto payments
*/
const walletpress = (() => {
const state = {
provider: null,
address: null,
chain: null,
connected: false,
};
const $ = (sel, ctx = document) => ctx.querySelector(sel);
const $$ = (sel, ctx = document) => [...ctx.querySelectorAll(sel)];
function log(...args) {
if (window.walletpress?.debug) console.log('[WalletPress]', ...args);
}
function el(tag, attrs = {}, children = []) {
const e = document.createElement(tag);
for (const [k, v] of Object.entries(attrs)) {
if (k.startsWith('on')) e.addEventListener(k.slice(2).toLowerCase(), v);
else if (k === 'className') e.className = v;
else if (k === 'innerHTML') e.innerHTML = v;
else e.setAttribute(k, v);
}
for (const c of children) e.append(c);
return e;
}
// ─── API ──────────────────────────────────────────────────────────────────
async function api(method, endpoint, body) {
const url = `${window.walletpress?.rest || '/wp-json/walletpress/v1'}${endpoint}`;
const opts = {
method,
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': window.walletpress?.nonce || '' },
};
if (body) opts.body = JSON.stringify(body);
const res = await fetch(url, opts);
return res.json();
}
// ─── Wallet Detection ──────────────────────────────────────────────────────
function hasMetaMask() {
return typeof window.ethereum !== 'undefined' && window.ethereum.isMetaMask;
}
function hasPhantom() {
return typeof window.solana !== 'undefined' && window.solana.isPhantom;
}
function hasWalletConnect() {
return typeof window.ethereum !== 'undefined';
}
// ─── MetaMask ──────────────────────────────────────────────────────────────
async function connectMetaMask() {
if (!hasMetaMask()) {
window.open('https://metamask.io/download', '_blank');
throw new Error('MetaMask not installed');
}
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
state.provider = 'metamask';
state.address = accounts[0];
state.chain = chainIdToName(chainId);
state.connected = true;
log('MetaMask connected', state.address, state.chain);
return state;
}
function chainIdToName(chainId) {
const map = {
'0x1': 'ethereum',
'0x5': 'ethereum',
'0x89': 'polygon',
'0x38': 'bsc',
'0x2105': 'base',
'0xa': 'optimism',
'0xa4b1': 'arbitrum',
};
return map[chainId?.toLowerCase()] || 'ethereum';
}
async function signMessageMetaMask(message) {
if (!state.address) throw new Error('Not connected');
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [message, state.address],
});
return signature;
}
// ─── Phantom ──────────────────────────────────────────────────────────────
async function connectPhantom() {
if (!hasPhantom()) {
window.open('https://phantom.app/download', '_blank');
throw new Error('Phantom not installed');
}
try {
const resp = await window.solana.connect();
state.provider = 'phantom';
state.address = resp.publicKey.toString();
state.chain = 'solana';
state.connected = true;
log('Phantom connected', state.address);
return state;
} catch (e) {
throw new Error('Phantom connection rejected');
}
}
async function signMessagePhantom(message) {
if (!state.address || !window.solana) throw new Error('Not connected');
const encoded = new TextEncoder().encode(message);
const signed = await window.solana.signMessage(encoded, 'utf8');
const signature = Buffer.from(signed.signature).toString('hex');
return signature;
}
// ─── Auth Flow ────────────────────────────────────────────────────────────
async function getNonce() {
const data = await api('GET', '/auth/nonce');
if (!data?.nonce) throw new Error('Failed to get nonce');
return data;
}
async function authenticate(provider) {
try {
let address, signature, message, nonceHash, chain;
const nonceData = await getNonce();
const nonce = nonceData.nonce;
nonceHash = nonceData.hash;
if (provider === 'metamask') {
const conn = await connectMetaMask();
address = conn.address;
chain = conn.chain;
message = `WalletPress Login\n\nAddress: ${address}\nNonce: ${nonce}\nChain: ${chain}\n\nThis signature does not cost gas.`;
signature = await signMessageMetaMask(message);
} else if (provider === 'phantom') {
const conn = await connectPhantom();
address = conn.address;
chain = conn.chain;
message = `WalletPress Login\n\nAddress: ${address}\nNonce: ${nonce}\nChain: ${chain}\n\nThis signature does not cost gas.`;
signature = await signMessagePhantom(message);
} else {
throw new Error('Unsupported wallet provider');
}
const result = await api('POST', '/auth/verify', {
address,
signature,
message,
chain,
nonce_hash: nonceHash,
});
if (result?.success) {
window.location.reload();
} else {
throw new Error(result?.error || 'Authentication failed');
}
} catch (e) {
log('Auth error:', e);
showError(e.message);
}
}
// ─── Token Gating ─────────────────────────────────────────────────────────
async function verifyOwnership(tokenAddress, chain, minAmount = 1) {
const user = window.walletpress?.user;
if (!user?.wallet) return false;
try {
const result = await api('POST', '/verify-ownership', {
address: user.wallet,
contract: tokenAddress,
chain: chain || user.chain || 'solana',
min_amount: minAmount,
});
return result?.owns === true;
} catch {
return false;
}
}
function setupGates() {
for (const gate of $$('[data-wallet-gate]')) {
const token = gate.dataset.token;
const chain = gate.dataset.chain;
const min = parseFloat(gate.dataset.min || '1');
const fallback = gate.querySelector('[data-gate-fallback]');
verifyOwnership(token, chain, min).then((owns) => {
if (owns) {
gate.querySelector('[data-gate-content]')?.classList.remove('hidden');
fallback?.classList.add('hidden');
} else {
gate.querySelector('[data-gate-content]')?.classList.add('hidden');
fallback?.classList.remove('hidden');
}
});
}
}
// ─── Payments ────────────────────────────────────────────────────────────
async function submitPayment(amount, token, chain, to, product, successUrl) {
try {
let provider = state.provider;
if (!state.connected) {
if (hasPhantom()) {
await connectPhantom();
provider = 'phantom';
} else if (hasMetaMask()) {
await connectMetaMask();
provider = 'metamask';
} else {
throw new Error('No wallet detected');
}
}
const from = state.address;
const createResult = await api('POST', '/payments/create', {
from, to, amount, token, chain,
});
let txHash;
if (chain === 'solana' && provider === 'phantom') {
txHash = await sendSolanaPayment(from, to, amount, token);
} else if (provider === 'metamask') {
txHash = await sendEVMPayment(to, amount, token);
} else {
throw new Error(`Unsupported chain/wallet combo: ${chain}/${provider}`);
}
const verifyResult = await api('POST', '/payments/verify', {
tx_hash: txHash,
chain,
product,
});
if (verifyResult?.unlocked || verifyResult?.confirmed) {
if (successUrl) window.location.href = successUrl;
showSuccess('Payment confirmed!');
} else {
showStatus('Payment sent. Waiting for confirmation...');
}
return { txHash, verifyResult };
} catch (e) {
log('Payment error:', e);
showError(e.message);
}
}
async function sendSolanaPayment(from, to, amount, token) {
if (!window.solana) throw new Error('Phantom not available');
const connection = new solanaWeb3.Connection(
solanaWeb3.clusterApiUrl('mainnet-beta')
);
const transaction = new solanaWeb3.Transaction().add(
solanaWeb3.SystemProgram.transfer({
fromPubkey: new solanaWeb3.PublicKey(from),
toPubkey: new solanaWeb3.PublicKey(to),
lamports: amount * solanaWeb3.LAMPORTS_PER_SOL,
})
);
const { signature } = await window.solana.signAndSendTransaction(transaction);
return signature;
}
async function sendEVMPayment(to, amount, token) {
if (!window.ethereum) throw new Error('MetaMask not available');
const wei = window.ethereum.utils
? window.ethereum.utils.toWei(amount.toString(), 'ether')
: amount;
const txHash = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [{ to, value: wei?.toString?.() || wei }],
});
return txHash;
}
// ─── UI ──────────────────────────────────────────────────────────────────
function showError(msg) {
for (const el of $$('.walletpress-error')) {
el.textContent = msg;
el.style.display = 'block';
setTimeout(() => { el.style.display = 'none'; }, 5000);
}
}
function showSuccess(msg) {
for (const el of $$('.walletpress-status')) {
el.textContent = msg;
el.className = 'walletpress-status walletpress-success';
el.style.display = 'block';
}
}
function showStatus(msg) {
for (const el of $$('.walletpress-status')) {
el.textContent = msg;
el.className = 'walletpress-status';
el.style.display = 'block';
}
}
// ─── Event Binding ────────────────────────────────────────────────────────
function bindConnectButtons() {
for (const btn of $$('.walletpress-btn-metamask')) {
btn.addEventListener('click', () => authenticate('metamask'));
}
for (const btn of $$('.walletpress-btn-phantom')) {
btn.addEventListener('click', () => authenticate('phantom'));
}
for (const btn of $$('.walletpress-btn-walletconnect')) {
btn.addEventListener('click', () => authenticate('walletconnect'));
}
}
function bindPaymentButtons() {
for (const container of $$('.walletpress-payment')) {
const btn = container.querySelector('.walletpress-btn-pay');
if (!btn) continue;
btn.addEventListener('click', () => {
submitPayment(
container.dataset.amount,
container.dataset.token,
container.dataset.chain,
container.dataset.to,
container.dataset.product,
container.dataset.success,
);
});
}
}
function bindDisconnect() {
for (const btn of $$('.walletpress-disconnect')) {
btn.addEventListener('click', async () => {
const form = new FormData();
form.append('action', 'walletpress_disconnect');
form.append('nonce', window.walletpress?.nonce || '');
try {
await fetch(window.walletpress?.ajax_url || '/wp-admin/admin-ajax.php', {
method: 'POST',
body: form,
});
window.location.reload();
} catch {
showError('Disconnect failed');
}
});
}
}
// ─── Init ──────────────────────────────────────────────────────────────────
function init() {
bindConnectButtons();
bindPaymentButtons();
bindDisconnect();
setupGates();
if (window.ethereum) {
window.ethereum.on('accountsChanged', () => window.location.reload());
window.ethereum.on('chainChanged', () => window.location.reload());
}
if (window.solana) {
window.solana.on('disconnect', () => window.location.reload());
}
log('WalletPress initialized');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
return { connectMetaMask, connectPhantom, authenticate, verifyOwnership, submitPayment, log, state };
})();