docs: apply fleet-template (16-artifact scaffold)
Adds missing standard artifacts: - README.md (if missing) - AGENTS.md (AI agent contract) - PLAN.md (current sprint) - STATUS.md (where we are) - DEVELOPMENT.md (dev workflow) - DEPLOYMENT.md (deploy procedure) - TESTING.md (test strategy) - DECISIONS.md (ADR index + templates) - .github/CODEOWNERS - .github/workflows/ci.yml Preserves all existing artifacts. Refs: RugMunchMedia/fleet-template
This commit is contained in:
commit
e13bd4d774
203 changed files with 31140 additions and 0 deletions
98
wp-plugin/assets/css/walletpress.css
Normal file
98
wp-plugin/assets/css/walletpress.css
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/* ─── Layout ────────────────────────────────────────── */
|
||||
.walletpress-cols { display:flex; gap:20px; flex-wrap:wrap; }
|
||||
.walletpress-col { flex:1; min-width:300px; }
|
||||
.walletpress-card { background:#fff; border:1px solid #dcdcde; padding:20px; margin-bottom:20px; border-radius:4px; box-shadow:0 1px 2px rgba(0,0,0,.04); }
|
||||
.walletpress-version { font-size:14px; color:#666; font-weight:400; }
|
||||
|
||||
/* ─── Stats Grid ─────────────────────────────────────── */
|
||||
.walletpress-stats-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:16px; margin:20px 0; }
|
||||
.walletpress-stat { background:#fff; border:1px solid #dcdcde; border-radius:4px; padding:20px; text-align:center; }
|
||||
.walletpress-stat .stat-icon { display:block; font-size:28px; margin-bottom:8px; }
|
||||
.walletpress-stat .stat-value { display:block; font-size:24px; font-weight:700; color:#1d2327; }
|
||||
.walletpress-stat .stat-label { display:block; font-size:13px; color:#646970; margin-top:4px; }
|
||||
.stat-ok { border-left:4px solid #00a32a; }
|
||||
.stat-warn { border-left:4px solid #dba617; }
|
||||
|
||||
/* ─── Setup Wizard Steps ─────────────────────────────── */
|
||||
.walletpress-steps { display:flex; gap:4px; margin:20px 0; }
|
||||
.walletpress-steps .step { flex:1; padding:12px; text-align:center; background:#f0f0f1; color:#646970; font-weight:500; border-radius:4px; }
|
||||
.walletpress-steps .step.active { background:#2271b1; color:#fff; }
|
||||
|
||||
/* ─── Connect Buttons ───────────────────────────────── */
|
||||
.walletpress-connect { display:flex; gap:12px; flex-wrap:wrap; }
|
||||
.walletpress-btn { display:inline-flex; align-items:center; gap:8px; padding:12px 24px; border:2px solid #dcdcde; background:#fff; border-radius:8px; font-size:15px; cursor:pointer; transition:all .15s; }
|
||||
.walletpress-btn:hover { border-color:#2271b1; background:#f0f6fc; }
|
||||
.walletpress-btn:active { transform:scale(.97); }
|
||||
.walletpress-btn-metamask:hover { border-color:#f6851b; background:#fff8f0; }
|
||||
.walletpress-btn-phantom:hover { border-color:#ab9ff2; background:#f5f0ff; }
|
||||
.walletpress-btn-pay { background:#2271b1; color:#fff; border-color:#2271b1; }
|
||||
.walletpress-btn-pay:hover { background:#135e96; color:#fff; }
|
||||
.walletpress-wallet-icon { font-size:20px; }
|
||||
|
||||
/* ─── Profile ────────────────────────────────────────── */
|
||||
.walletpress-profile { padding:20px; background:#fff; border:1px solid #dcdcde; border-radius:4px; }
|
||||
.walletpress-profile p { margin:8px 0; }
|
||||
.walletpress-chain-badge { display:inline-block; padding:2px 8px; background:#2271b1; color:#fff; border-radius:3px; font-size:11px; margin-left:8px; }
|
||||
|
||||
/* ─── Payment ────────────────────────────────────────── */
|
||||
.walletpress-payment { margin:16px 0; }
|
||||
.walletpress-payment-status { margin-top:8px; padding:8px 12px; border-radius:4px; }
|
||||
.walletpress-success { background:#edfaef; color:#00a32a; border:1px solid #b8e6bf; }
|
||||
.walletpress-error { background:#fcf0f1; color:#dc3232; border:1px solid #f1adad; padding:8px 12px; border-radius:4px; margin-top:8px; }
|
||||
|
||||
/* ─── Badge ──────────────────────────────────────────── */
|
||||
.walletpress-badge { display:inline-block; padding:2px 8px; border-radius:3px; font-size:12px; font-weight:500; }
|
||||
.badge-confirmed { background:#edfaef; color:#00a32a; }
|
||||
.badge-pending { background:#fcf9e8; color:#996800; }
|
||||
.badge-failed { background:#fcf0f1; color:#dc3232; }
|
||||
|
||||
/* ─── Token Gating ───────────────────────────────────── */
|
||||
.walletpress-gate-fallback { padding:20px; background:#fcf0f1; border:1px solid #f1adad; border-radius:4px; text-align:center; color:#dc3232; }
|
||||
.hidden { display:none !important; }
|
||||
|
||||
/* ─── Table tweaks ───────────────────────────────────── */
|
||||
.walletpress-admin .wp-list-table code { font-size:12px; }
|
||||
.new-gate input, .new-gate select { width:100%; }
|
||||
|
||||
/* ════════════════════════════════════════════════════════════
|
||||
MOBILE RESPONSIVE — Added for WalletPress v1.1
|
||||
All breakpoints: 320px (small phone), 480px (phone),
|
||||
768px (tablet), 1024px (desktop)
|
||||
════════════════════════════════════════════════════════════ */
|
||||
|
||||
@media(max-width:1024px) {
|
||||
.walletpress-stats-grid { grid-template-columns:repeat(2,1fr); }
|
||||
}
|
||||
|
||||
@media(max-width:768px) {
|
||||
.walletpress-stats-grid { grid-template-columns:1fr 1fr; gap:12px; }
|
||||
.walletpress-cols { flex-direction:column; }
|
||||
.walletpress-col { min-width:auto; }
|
||||
.walletpress-card { padding:16px; }
|
||||
.walletpress-connect { flex-direction:column; }
|
||||
.walletpress-btn { width:100%; justify-content:center; padding:14px; }
|
||||
.walletpress-steps { flex-direction:column; gap:2px; }
|
||||
.walletpress-steps .step { padding:10px; font-size:13px; }
|
||||
.walletpress-stat { padding:16px; }
|
||||
.walletpress-stat .stat-value { font-size:20px; }
|
||||
table.wp-list-table { font-size:13px; }
|
||||
table.wp-list-table th, table.wp-list-table td { padding:6px 8px; }
|
||||
}
|
||||
|
||||
@media(max-width:480px) {
|
||||
.walletpress-stats-grid { grid-template-columns:1fr; gap:8px; }
|
||||
.walletpress-stat { padding:12px; }
|
||||
.walletpress-stat .stat-icon { font-size:22px; }
|
||||
.walletpress-stat .stat-value { font-size:18px; }
|
||||
.walletpress-card { padding:12px; margin-bottom:12px; }
|
||||
.walletpress-card h1 { font-size:20px; }
|
||||
.walletpress-card h2 { font-size:16px; }
|
||||
.walletpress-card h3 { font-size:14px; }
|
||||
.wp-list-table { display:block; overflow-x:auto; -webkit-overflow-scrolling:touch; }
|
||||
}
|
||||
|
||||
@media(max-width:360px) {
|
||||
.walletpress-connect { gap:8px; }
|
||||
.walletpress-btn { font-size:14px; padding:12px; }
|
||||
.walletpress-wallet-icon { font-size:18px; }
|
||||
}
|
||||
10
wp-plugin/assets/js/walletpress-admin.js
Normal file
10
wp-plugin/assets/js/walletpress-admin.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
document.addEventListener('DOMContentLoaded', () => {
|
||||
for (const btn of document.querySelectorAll('.copy-shortcode')) {
|
||||
btn.addEventListener('click', () => {
|
||||
navigator.clipboard.writeText(btn.dataset.code).then(() => {
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(() => { btn.textContent = 'Copy'; }, 2000);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
408
wp-plugin/assets/js/walletpress.js
Normal file
408
wp-plugin/assets/js/walletpress.js
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
/**
|
||||
* WalletPress — Self-Hosted Web3 WordPress Plugin
|
||||
* Frontend: wallet connect, token gating, crypto payments
|
||||
*
|
||||
* Trust: All wallet operations happen client-side. Private keys
|
||||
* NEVER leave the browser unless you explicitly sync them to your
|
||||
* self-hosted backend (encrypted with your passphrase).
|
||||
*
|
||||
* Compatible with: MetaMask, Phantom, and any EIP-1193 provider.
|
||||
* No external SDKs required — uses native browser APIs only.
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// ─── 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';
|
||||
}
|
||||
|
||||
// ─── Encoding helpers (no Buffer dependency) ──────────────────────────────
|
||||
|
||||
function hexEncode(bytes) {
|
||||
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
function base58Encode(bytes) {
|
||||
const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
||||
let num = BigInt('0x' + hexEncode(bytes));
|
||||
let result = '';
|
||||
while (num > 0) {
|
||||
result = ALPHABET[num % 58n] + result;
|
||||
num /= 58n;
|
||||
}
|
||||
return result || ALPHABET[0];
|
||||
}
|
||||
|
||||
// ─── 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', '0xa86a': 'avalanche',
|
||||
};
|
||||
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');
|
||||
|
||||
// Browser-compatible bytes-to-hex (no Buffer dependency)
|
||||
let signature;
|
||||
if (signed.signature instanceof Uint8Array) {
|
||||
signature = hexEncode(signed.signature);
|
||||
} else if (typeof signed.signature === 'string') {
|
||||
signature = signed.signature;
|
||||
} else {
|
||||
signature = hexEncode(new Uint8Array(signed.signature));
|
||||
}
|
||||
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 if (provider === 'walletconnect') {
|
||||
window.open('https://walletconnect.com/explorer', '_blank');
|
||||
throw new Error('WalletConnect: scan QR with your wallet app');
|
||||
} 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) => {
|
||||
const content = gate.querySelector('[data-gate-content]');
|
||||
if (owns) {
|
||||
content?.classList.remove('hidden');
|
||||
fallback?.classList.add('hidden');
|
||||
} else {
|
||||
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. Install Phantom or MetaMask.');
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
} else if (provider === 'metamask') {
|
||||
txHash = await sendEVMPayment(to, amount);
|
||||
} else {
|
||||
throw new Error(`Unsupported chain/wallet: ${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) {
|
||||
if (!window.solana) throw new Error('Phantom not available');
|
||||
const { solana } = window;
|
||||
|
||||
const { SystemProgram, Transaction, PublicKey, LAMPORTS_PER_SOL } = solana;
|
||||
const connection = new solana.Connection('https://api.mainnet-beta.solana.com');
|
||||
|
||||
const transaction = new Transaction().add(
|
||||
SystemProgram.transfer({
|
||||
fromPubkey: new PublicKey(from),
|
||||
toPubkey: new PublicKey(to),
|
||||
lamports: Math.round(parseFloat(amount) * LAMPORTS_PER_SOL),
|
||||
})
|
||||
);
|
||||
const { signature } = await solana.signAndSendTransaction(transaction);
|
||||
return signature;
|
||||
}
|
||||
|
||||
async function sendEVMPayment(to, amount) {
|
||||
if (!window.ethereum) throw new Error('MetaMask not available');
|
||||
const wei = '0x' + (BigInt(Math.round(parseFloat(amount) * 1e18))).toString(16);
|
||||
const txHash = await window.ethereum.request({
|
||||
method: 'eth_sendTransaction',
|
||||
params: [{ to, value: 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 };
|
||||
})();
|
||||
782
wp-plugin/includes/class-walletpress-admin.php
Normal file
782
wp-plugin/includes/class-walletpress-admin.php
Normal file
|
|
@ -0,0 +1,782 @@
|
|||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class WalletPressAdmin {
|
||||
private WalletPress $plugin;
|
||||
|
||||
private array $tabs = [];
|
||||
|
||||
public function __construct(WalletPress $plugin) {
|
||||
$this->plugin = $plugin;
|
||||
add_action('admin_menu', [$this, 'add_menu']);
|
||||
add_action('admin_init', [$this, 'register_settings']);
|
||||
add_action('admin_init', [$this, 'handle_actions']);
|
||||
add_filter('plugin_action_links_' . plugin_basename(WALLETPRESS_FILE), [$this, 'action_links']);
|
||||
}
|
||||
|
||||
public function action_links(array $links): array {
|
||||
$links[] = '<a href="' . admin_url('admin.php?page=walletpress-setup') . '">' . __('Setup', 'walletpress') . '</a>';
|
||||
$links[] = '<a href="' . admin_url('admin.php?page=walletpress-settings') . '">' . __('Settings', 'walletpress') . '</a>';
|
||||
return $links;
|
||||
}
|
||||
|
||||
public function add_menu(): void {
|
||||
$icon = 'data:image/svg+xml;base64,' . base64_encode('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>');
|
||||
add_menu_page('WalletPress', 'WalletPress', 'manage_options', 'walletpress', [$this, 'page_dashboard'], $icon, 30);
|
||||
|
||||
$pages = [
|
||||
'walletpress' => ['Dashboard', [$this, 'page_dashboard']],
|
||||
'walletpress-setup' => ['Setup', [$this, 'page_setup']],
|
||||
'walletpress-vault' => ['Vault', [$this, 'page_vault']],
|
||||
'walletpress-gen' => ['Generate', [$this, 'page_generate']],
|
||||
'walletpress-mnemonic'=> ['Mnemonic', [$this, 'page_mnemonic']],
|
||||
'walletpress-recover' => ['Recovery', [$this, 'page_recovery']],
|
||||
'walletpress-sweep' => ['Sweep', [$this, 'page_sweep']],
|
||||
'walletpress-escrow' => ['Escrow', [$this, 'page_escrow']],
|
||||
'walletpress-gates' => ['Gates', [$this, 'page_gates']],
|
||||
'walletpress-payments'=> ['Payments', [$this, 'page_payments']],
|
||||
'walletpress-keys' => ['API Keys', [$this, 'page_api_keys']],
|
||||
'walletpress-webhooks'=> ['Webhooks', [$this, 'page_webhooks']],
|
||||
'walletpress-alerts' => ['Alerts', [$this, 'page_alerts']],
|
||||
'walletpress-audit' => ['Audit', [$this, 'page_audit']],
|
||||
'walletpress-plugins' => ['Plugins', [$this, 'page_plugins']],
|
||||
'walletpress-settings'=> ['Settings', [$this, 'page_settings']],
|
||||
];
|
||||
foreach ($pages as $slug => [$title, $cb]) {
|
||||
$parent = $slug === 'walletpress' ? null : 'walletpress';
|
||||
add_submenu_page($parent ?: 'walletpress', $title, $title, 'manage_options', $slug, $cb);
|
||||
}
|
||||
}
|
||||
|
||||
public function register_settings(): void {
|
||||
$s = [
|
||||
'walletpress_api_url' => ['esc_url_raw', ''],
|
||||
'walletpress_api_key' => ['sanitize_text_field', ''],
|
||||
'walletpress_merchant_wallet' => ['sanitize_text_field', ''],
|
||||
'walletpress_merchant_chain' => ['sanitize_text_field', 'solana'],
|
||||
'walletpress_default_chain' => ['sanitize_text_field', 'solana'],
|
||||
'walletpress_auto_create_users' => ['rest_sanitize_boolean', '1'],
|
||||
'walletpress_default_role' => ['sanitize_text_field', 'subscriber'],
|
||||
'walletpress_login_enabled' => ['rest_sanitize_boolean', '1'],
|
||||
'walletpress_payments_enabled' => ['rest_sanitize_boolean', '1'],
|
||||
'walletpress_setup_complete' => ['rest_sanitize_boolean', '0'],
|
||||
];
|
||||
foreach ($s as $k => [$cb, $default]) {
|
||||
register_setting('walletpress_settings', $k, ['sanitize_callback' => $cb, 'default' => $default]);
|
||||
}
|
||||
}
|
||||
|
||||
public function handle_actions(): void {
|
||||
if (!isset($_GET['page']) || !current_user_can('manage_options')) return;
|
||||
$page = $_GET['page'];
|
||||
|
||||
// Handle gate creation
|
||||
if ($page === 'walletpress-gates' && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_gate'])) {
|
||||
check_admin_referer('walletpress_save_gate');
|
||||
$gates = get_option('walletpress_gates', []);
|
||||
$gates[] = [
|
||||
'title' => sanitize_text_field($_POST['gate_title']),
|
||||
'token' => sanitize_text_field($_POST['gate_token']),
|
||||
'chain' => sanitize_text_field($_POST['gate_chain']),
|
||||
'min_amount' => floatval($_POST['gate_min_amount']),
|
||||
'created_at' => current_time('mysql'),
|
||||
];
|
||||
update_option('walletpress_gates', $gates);
|
||||
wp_redirect(admin_url('admin.php?page=walletpress-gates&msg=created'));
|
||||
exit;
|
||||
}
|
||||
if ($page === 'walletpress-gates' && isset($_GET['action']) && $_GET['action'] === 'delete' && isset($_GET['id'])) {
|
||||
check_admin_referer('walletpress_delete_gate');
|
||||
$gates = get_option('walletpress_gates', []);
|
||||
$id = (int) $_GET['id'];
|
||||
if (isset($gates[$id])) { unset($gates[$id]); update_option('walletpress_gates', array_values($gates)); }
|
||||
wp_redirect(admin_url('admin.php?page=walletpress-gates&msg=deleted'));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
private function api(): WalletPressAPI { return $this->plugin->api(); }
|
||||
private function notice(string $msg, string $type = 'success'): void {
|
||||
echo '<div class="notice notice-' . esc_attr($type) . ' is-dismissible"><p>' . esc_html($msg) . '</p></div>';
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// DASHBOARD
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
public function page_dashboard(): void {
|
||||
$health = $this->api()->is_configured() ? $this->api()->health() : null;
|
||||
$stats = $this->api()->is_configured() ? $this->api()->stats() : null;
|
||||
$vault = $this->api()->is_configured() ? $this->api()->list_vault() : null;
|
||||
$wallet_count = is_array($vault) ? count($vault) - 1 : 0; // subtract _meta
|
||||
$chains = $this->api()->is_configured() ? $this->api()->list_chains() : null;
|
||||
$chain_count = is_array($chains) ? count($chains) : 0;
|
||||
$setup = get_option('walletpress_setup_complete', '0');
|
||||
?>
|
||||
<div class="wrap walletpress-dashboard">
|
||||
<h1>WalletPress <span class="walletpress-version">v<?php echo WALLETPRESS_VERSION; ?></span>
|
||||
<?php if (!$setup): ?><a href="<?php echo admin_url('admin.php?page=walletpress-setup'); ?>" class="page-title-action"><?php esc_html_e('Run Setup', 'walletpress'); ?></a><?php endif; ?>
|
||||
</h1>
|
||||
<div class="walletpress-stats-grid">
|
||||
<div class="walletpress-stat <?php echo $health ? 'stat-ok' : 'stat-warn'; ?>">
|
||||
<span class="stat-icon">🔌</span>
|
||||
<span class="stat-value"><?php echo $health ? 'Connected' : ($this->api()->is_configured() ? 'Error' : 'Offline'); ?></span>
|
||||
<span class="stat-label">Backend API</span>
|
||||
</div>
|
||||
<div class="walletpress-stat">
|
||||
<span class="stat-icon">🏦</span>
|
||||
<span class="stat-value"><?php echo esc_html($wallet_count); ?></span>
|
||||
<span class="stat-label">Wallets in Vault</span>
|
||||
</div>
|
||||
<div class="walletpress-stat">
|
||||
<span class="stat-icon">⛓️</span>
|
||||
<span class="stat-value"><?php echo esc_html($chain_count ?: '29'); ?></span>
|
||||
<span class="stat-label">Supported Chains</span>
|
||||
</div>
|
||||
<div class="walletpress-stat">
|
||||
<span class="stat-icon">🔐</span>
|
||||
<span class="stat-value"><?php echo esc_html(count(get_option('walletpress_gates', []))); ?></span>
|
||||
<span class="stat-label">Token Gates</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="walletpress-cols">
|
||||
<div class="walletpress-col">
|
||||
<div class="walletpress-card"><h3>Quick Actions</h3>
|
||||
<p><a href="<?php echo admin_url('admin.php?page=walletpress-gen'); ?>" class="button">Generate Wallet</a>
|
||||
<a href="<?php echo admin_url('admin.php?page=walletpress-vault'); ?>" class="button">Browse Vault</a>
|
||||
<a href="<?php echo admin_url('admin.php?page=walletpress-mnemonic'); ?>" class="button">Mnemonic Converter</a></p>
|
||||
<p><a href="<?php echo admin_url('admin.php?page=walletpress-gates'); ?>" class="button">Token Gates</a>
|
||||
<a href="<?php echo admin_url('admin.php?page=walletpress-keys'); ?>" class="button">API Keys</a></p>
|
||||
</div>
|
||||
<div class="walletpress-card"><h3>Shortcodes</h3>
|
||||
<table class="widefat striped">
|
||||
<tr><td><code>[wallet_connect]</code></td><td>Wallet login buttons</td></tr>
|
||||
<tr><td><code>[wallet_gate id="0"]</code></td><td>Token-gated content</td></tr>
|
||||
<tr><td><code>[wallet_pay amount="10"]</code></td><td>Crypto payment button</td></tr>
|
||||
<tr><td><code>[wallet_generate]</code></td><td>Wallet generator (admins)</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="walletpress-col">
|
||||
<div class="walletpress-card"><h3>API Endpoints</h3>
|
||||
<p>The backend exposes <strong>86+ REST endpoints</strong> at <code><?php echo esc_html(get_option('walletpress_api_url', '(not set)')); ?></code></p>
|
||||
<p>OpenAPI docs at <code><?php echo esc_html(get_option('walletpress_api_url', '') . '/docs'); ?></code></p>
|
||||
<p>All endpoints are accessible via the WP plugin admin or directly via curl/httpx.</p>
|
||||
</div>
|
||||
<?php if (is_array($stats)): ?>
|
||||
<div class="walletpress-card"><h3>Vault Stats</h3>
|
||||
<pre style="max-height:200px;overflow:auto;"><?php echo esc_html(json_encode($stats, JSON_PRETTY_PRINT)); ?></pre>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// SETUP WIZARD
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
public function page_setup(): void {
|
||||
$step = isset($_GET['step']) ? (int) $_GET['step'] : 1;
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if ($step === 1) { update_option('walletpress_api_url', esc_url_raw($_POST['walletpress_api_url']));
|
||||
update_option('walletpress_api_key', sanitize_text_field($_POST['walletpress_api_key']));
|
||||
wp_redirect(admin_url('admin.php?page=walletpress-setup&step=2')); exit; }
|
||||
if ($step === 2) { update_option('walletpress_merchant_wallet', sanitize_text_field($_POST['walletpress_merchant_wallet']));
|
||||
update_option('walletpress_merchant_chain', sanitize_text_field($_POST['walletpress_merchant_chain']));
|
||||
wp_redirect(admin_url('admin.php?page=walletpress-setup&step=3')); exit; }
|
||||
if ($step === 3 && isset($_POST['save_gate'])) {
|
||||
check_admin_referer('walletpress_save_gate');
|
||||
$gates = get_option('walletpress_gates', []);
|
||||
$gates[] = ['title' => sanitize_text_field($_POST['gate_title']), 'token' => sanitize_text_field($_POST['gate_token']), 'chain' => sanitize_text_field($_POST['gate_chain']), 'min_amount' => floatval($_POST['gate_min_amount']), 'created_at' => current_time('mysql')];
|
||||
update_option('walletpress_gates', $gates);
|
||||
update_option('walletpress_setup_complete', '1');
|
||||
wp_redirect(admin_url('admin.php?page=walletpress-setup&step=4')); exit;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<div class="wrap walletpress-setup">
|
||||
<h1>WalletPress Setup Wizard</h1>
|
||||
<div class="walletpress-steps">
|
||||
<div class="step <?php echo $step >= 1 ? 'active' : ''; ?>">API Connection</div>
|
||||
<div class="step <?php echo $step >= 2 ? 'active' : ''; ?>">Merchant Wallet</div>
|
||||
<div class="step <?php echo $step >= 3 ? 'active' : ''; ?>">Token Gate</div>
|
||||
<div class="step <?php echo $step >= 4 ? 'active' : ''; ?>">Done</div>
|
||||
</div>
|
||||
<?php if ($step === 1): ?>
|
||||
<form method="post" class="walletpress-card">
|
||||
<h2>Connect Your Backend API</h2>
|
||||
<p>Enter your WalletPress backend API URL and key.</p>
|
||||
<table class="form-table">
|
||||
<tr><th><label for="api_url">API URL</label></th><td><input type="url" id="api_url" name="walletpress_api_url" value="<?php echo esc_attr(get_option('walletpress_api_url', '')); ?>" class="regular-text" placeholder="https://api.yourdomain.com" required></td></tr>
|
||||
<tr><th><label for="api_key">API Key</label></th><td><input type="password" id="api_key" name="walletpress_api_key" value="<?php echo esc_attr(get_option('walletpress_api_key', '') ? '********' : ''); ?>" class="regular-text" placeholder="wp_..." required></td></tr>
|
||||
</table>
|
||||
<?php submit_button('Test & Continue', 'primary'); ?>
|
||||
</form>
|
||||
<?php elseif ($step === 2): ?>
|
||||
<form method="post" class="walletpress-card">
|
||||
<h2>Set Your Merchant Wallet</h2>
|
||||
<p>Where crypto payments will be sent.</p>
|
||||
<table class="form-table">
|
||||
<tr><th><label for="mw">Wallet Address</label></th><td><input type="text" id="mw" name="walletpress_merchant_wallet" value="<?php echo esc_attr(get_option('walletpress_merchant_wallet', '')); ?>" class="regular-text" placeholder="SOL or ETH address" required></td></tr>
|
||||
<tr><th><label for="mc">Chain</label></th><td><select id="mc" name="walletpress_merchant_chain"><?php foreach ($this->plugin->supported_chains() as $k => $c): ?><option value="<?php echo esc_attr($k); ?>" <?php selected(get_option('walletpress_merchant_chain', 'solana'), $k); ?>><?php echo esc_html($c['label']); ?></option><?php endforeach; ?></select></td></tr>
|
||||
</table>
|
||||
<?php submit_button('Continue', 'primary'); ?>
|
||||
</form>
|
||||
<?php elseif ($step === 3): ?>
|
||||
<form method="post" class="walletpress-card">
|
||||
<h2>Create Your First Token Gate</h2>
|
||||
<p>Restrict content to users who hold a specific token.</p>
|
||||
<?php wp_nonce_field('walletpress_save_gate'); ?>
|
||||
<table class="form-table">
|
||||
<tr><th><label for="gt">Gate Name</label></th><td><input type="text" id="gt" name="gate_title" class="regular-text" placeholder="NFT Holders" required></td></tr>
|
||||
<tr><th><label for="ga">Token Contract</label></th><td><input type="text" id="ga" name="gate_token" class="regular-text code" placeholder="Token mint/contract address" required></td></tr>
|
||||
<tr><th><label for="gc">Chain</label></th><td><select id="gc" name="gate_chain"><?php foreach ($this->plugin->supported_chains() as $k => $c): ?><option value="<?php echo esc_attr($k); ?>"><?php echo esc_html($c['label']); ?></option><?php endforeach; ?></select></td></tr>
|
||||
<tr><th><label for="gm">Min Amount</label></th><td><input type="number" id="gm" name="gate_min_amount" value="1" step="0.000001" min="0" class="small-text"></td></tr>
|
||||
</table>
|
||||
<?php submit_button('Save Gate & Finish', 'primary', 'save_gate'); ?>
|
||||
</form>
|
||||
<?php elseif ($step === 4): ?>
|
||||
<div class="walletpress-card">
|
||||
<h2>Setup Complete</h2>
|
||||
<p>WalletPress is ready. What's next:</p>
|
||||
<ul>
|
||||
<li><strong>Vault:</strong> <a href="<?php echo admin_url('admin.php?page=walletpress-gen'); ?>">Generate wallets</a> or <a href="<?php echo admin_url('admin.php?page=walletpress-vault'); ?>">browse your vault</a></li>
|
||||
<li><strong>Gates:</strong> Add <code>[wallet_gate id="0"]</code> to any post to gate content</li>
|
||||
<li><strong>API:</strong> Create <a href="<?php echo admin_url('admin.php?page=walletpress-keys'); ?>">API keys</a> for programmatic access</li>
|
||||
</ul>
|
||||
<a href="<?php echo admin_url('admin.php?page=walletpress'); ?>" class="button button-primary">Go to Dashboard</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// VAULT BROWSER
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
public function page_vault(): void {
|
||||
$vault = $this->api()->list_vault();
|
||||
$search = isset($_GET['s']) ? sanitize_text_field($_GET['s']) : '';
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>Wallet Vault</h1>
|
||||
<form method="get" class="walletpress-card" style="display:flex;gap:8px;">
|
||||
<input type="hidden" name="page" value="walletpress-vault">
|
||||
<input type="text" name="s" value="<?php echo esc_attr($search); ?>" placeholder="Search by address or label..." style="flex:1;">
|
||||
<button class="button">Search</button>
|
||||
<a href="<?php echo admin_url('admin.php?page=walletpress-gen'); ?>" class="button button-primary">Generate New</a>
|
||||
<a href="<?php echo admin_url('admin.php?page=walletpress-mnemonic'); ?>" class="button">From Mnemonic</a>
|
||||
</form>
|
||||
<?php if (!is_array($vault) || empty($vault)): $this->notice('No wallets in vault. Generate one first.', 'info'); return; endif; ?>
|
||||
<table class="wp-list-table widefat fixed striped">
|
||||
<thead><tr><th>ID</th><th>Chain</th><th>Address</th><th>Label</th><th>Balance</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($vault as $id => $w): if ($id === '_meta') continue;
|
||||
$addr = $w['address'] ?? $w['public_key'] ?? '';
|
||||
if ($search && !str_contains(strtolower($addr), strtolower($search)) && !str_contains(strtolower($w['label'] ?? ''), strtolower($search))) continue;
|
||||
?>
|
||||
<tr>
|
||||
<td><code><?php echo esc_html(substr($id, 0, 8)); ?>..</code></td>
|
||||
<td><?php echo esc_html($w['chain'] ?? $w['chain_key'] ?? ''); ?></td>
|
||||
<td><code title="<?php echo esc_attr($addr); ?>"><?php echo esc_html(substr($addr, 0, 12) . '...' . substr($addr, -4)); ?></code></td>
|
||||
<td><?php echo esc_html($w['label'] ?? ''); ?></td>
|
||||
<td><?php echo esc_html($w['balance'] ?? $w['native_balance'] ?? '?'); ?></td>
|
||||
<td>
|
||||
<a href="<?php echo wp_nonce_url(admin_url('admin.php?page=walletpress-vault&action=view&id=' . $id), 'walletpress_view'); ?>" class="button button-small">View</a>
|
||||
<button class="button button-small copy-addr" data-addr="<?php echo esc_attr($addr); ?>">Copy</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// GENERATE
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
public function page_generate(): void {
|
||||
$chains = $this->api()->list_chains();
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>Generate Wallets</h1>
|
||||
<div class="walletpress-cols">
|
||||
<div class="walletpress-col">
|
||||
<div class="walletpress-card">
|
||||
<h2>Single Wallet</h2>
|
||||
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
|
||||
<input type="hidden" name="action" value="walletpress_generate">
|
||||
<?php wp_nonce_field('walletpress_gen'); ?>
|
||||
<table class="form-table">
|
||||
<tr><th>Chain</th><td><select name="chain"><?php if (is_array($chains)) foreach ($chains as $k => $c): ?><option value="<?php echo esc_attr($k); ?>"><?php echo esc_html($c['name'] ?? $k); ?></option><?php endforeach; else: foreach ($this->plugin->supported_chains() as $k => $c): ?><option value="<?php echo esc_attr($k); ?>"><?php echo esc_html($c['label']); ?></option><?php endforeach; endif; ?></select></td></tr>
|
||||
<tr><th>Label</th><td><input type="text" name="label" class="regular-text" placeholder="e.g. My Trading Wallet"></td></tr>
|
||||
</table>
|
||||
<?php submit_button('Generate Wallet', 'primary'); ?>
|
||||
</form>
|
||||
</div>
|
||||
<div class="walletpress-card">
|
||||
<h2>Batch Generate</h2>
|
||||
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
|
||||
<input type="hidden" name="action" value="walletpress_generate_batch">
|
||||
<?php wp_nonce_field('walletpress_gen'); ?>
|
||||
<table class="form-table">
|
||||
<tr><th>Chains (comma-separated)</th><td><input type="text" name="chains" class="regular-text" placeholder="solana,ethereum,base" value="solana,ethereum,base,polygon,bsc"></td></tr>
|
||||
<tr><th>Label Prefix</th><td><input type="text" name="label_prefix" class="regular-text" placeholder="batch-2026"></td></tr>
|
||||
</table>
|
||||
<?php submit_button('Generate Batch', 'primary'); ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="walletpress-col">
|
||||
<div class="walletpress-card">
|
||||
<h2>Generate All Chains</h2>
|
||||
<p>Generate a wallet for every supported chain (55).</p>
|
||||
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
|
||||
<input type="hidden" name="action" value="walletpress_generate_all">
|
||||
<?php wp_nonce_field('walletpress_gen'); ?>
|
||||
<table class="form-table">
|
||||
<tr><th>Label Prefix</th><td><input type="text" name="label_prefix" class="regular-text" placeholder="full-suite"></td></tr>
|
||||
</table>
|
||||
<?php submit_button('Generate All Chains', 'primary'); ?>
|
||||
</form>
|
||||
</div>
|
||||
<div class="walletpress-card">
|
||||
<h2>Import Existing Wallet</h2>
|
||||
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
|
||||
<input type="hidden" name="action" value="walletpress_import">
|
||||
<?php wp_nonce_field('walletpress_gen'); ?>
|
||||
<table class="form-table">
|
||||
<tr><th>Chain</th><td><select name="chain"><?php if (is_array($chains)) foreach ($chains as $k => $c): ?><option value="<?php echo esc_attr($k); ?>"><?php echo esc_html($c['name'] ?? $k); ?></option><?php endforeach; endif; ?></select></td></tr>
|
||||
<tr><th>Private Key</th><td><input type="password" name="private_key" class="regular-text" placeholder="Enter private key"></td></tr>
|
||||
<tr><th>Label</th><td><input type="text" name="label" class="regular-text" placeholder="Imported wallet"></td></tr>
|
||||
</table>
|
||||
<?php submit_button('Import', 'primary'); ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// MNEMONIC CONVERTER
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
public function page_mnemonic(): void {
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>Mnemonic → Multi-Chain Converter</h1>
|
||||
<div class="walletpress-card">
|
||||
<p>Enter a BIP39 mnemonic phrase to derive addresses for all 55 supported chains. Your phrase never leaves your browser — it's sent directly to your self-hosted backend.</p>
|
||||
<form method="post">
|
||||
<table class="form-table">
|
||||
<tr><th><label for="mnemonic">Mnemonic Phrase</label></th><td><textarea id="mnemonic" name="mnemonic" rows="3" class="large-text code" placeholder="word1 word2 word3 ... word12" required></textarea></td></tr>
|
||||
<tr><th><label for="passphrase">Passphrase (optional)</label></th><td><input type="text" id="passphrase" name="passphrase" class="regular-text"></td></tr>
|
||||
</table>
|
||||
<?php submit_button('Derive All Addresses', 'primary'); ?>
|
||||
</form>
|
||||
</div>
|
||||
<?php if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['mnemonic'])):
|
||||
$result = $this->api()->from_mnemonic(sanitize_text_field($_POST['mnemonic']), sanitize_text_field($_POST['passphrase']));
|
||||
if ($result): ?>
|
||||
<div class="walletpress-card">
|
||||
<h2>Results</h2>
|
||||
<table class="wp-list-table widefat fixed striped">
|
||||
<thead><tr><th>Chain</th><th>Address</th><th>Private Key</th></tr></thead>
|
||||
<tbody>
|
||||
<?php $wallets = $result['wallets'] ?? $result['result'] ?? $result; foreach ($wallets as $chain => $data): if (!is_array($data)) continue; ?>
|
||||
<tr><td><strong><?php echo esc_html($chain); ?></strong></td>
|
||||
<td><code><?php echo esc_html($data['address'] ?? $data['public_key'] ?? ''); ?></code></td>
|
||||
<td><code><?php echo esc_html(substr($data['private_key'] ?? $data['wif'] ?? '', 0, 16) . '...'); ?></code></td></tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: $this->notice('Failed to convert mnemonic. Check your API connection.', 'error'); endif; endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// RECOVERY
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
public function page_recovery(): void {
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>Wallet Recovery</h1>
|
||||
<div class="walletpress-card">
|
||||
<p>Recover wallets from partial mnemonics, corrupted seeds, or lost keys. The recovery tool tries multiple BIP39 word combinations.</p>
|
||||
<p class="description">This is resource-intensive on large partial phrases. Run during low-traffic periods.</p>
|
||||
<form method="post">
|
||||
<table class="form-table">
|
||||
<tr><th><label for="partial">Partial Mnemonic</label></th><td><textarea id="partial" name="partial_mnemonic" rows="3" class="large-text code" placeholder="Enter known words, use ? for unknown: abandon ? ? lunar ? city vanish party uncle ? pudding" required></textarea></td></tr>
|
||||
<tr><th>Chain</th><td><select name="chain"><?php foreach ($this->plugin->supported_chains() as $k => $c): ?><option value="<?php echo esc_attr($k); ?>"><?php echo esc_html($c['label']); ?></option><?php endforeach; ?></select></td></tr>
|
||||
</table>
|
||||
<?php submit_button('Attempt Recovery', 'primary'); ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// SWEEP
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
public function page_sweep(): void {
|
||||
$vault = $this->api()->list_vault();
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>Dust Sweep & Rotate</h1>
|
||||
<div class="walletpress-cols">
|
||||
<div class="walletpress-col">
|
||||
<div class="walletpress-card">
|
||||
<h2>Rotate + Sweep</h2>
|
||||
<p>Generate a new wallet key and sweep all funds from the old wallet to the new one.</p>
|
||||
<form method="post">
|
||||
<table class="form-table">
|
||||
<tr><th>Source Wallet</th><td><select name="wallet_id"><?php if (is_array($vault)) foreach ($vault as $id => $w): if ($id === '_meta') continue; ?><option value="<?php echo esc_attr($id); ?>"><?php echo esc_html(($w['chain'] ?? '?') . ' - ' . substr($w['address'] ?? $id, 0, 12)); ?></option><?php endforeach; endif; ?></select></td></tr>
|
||||
<tr><th>Destination Address</th><td><input type="text" name="to_address" class="regular-text code" placeholder="Target wallet address" required></td></tr>
|
||||
</table>
|
||||
<?php submit_button('Rotate & Sweep', 'primary'); ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="walletpress-col">
|
||||
<div class="walletpress-card">
|
||||
<h2>Simple Sweep</h2>
|
||||
<p>Sweep funds from a vault wallet to an external address (no key rotation).</p>
|
||||
<form method="post">
|
||||
<table class="form-table">
|
||||
<tr><th>From Wallet</th><td><select name="from_wallet_id"><?php if (is_array($vault)) foreach ($vault as $id => $w): if ($id === '_meta') continue; ?><option value="<?php echo esc_attr($id); ?>"><?php echo esc_html(($w['chain'] ?? '?') . ' - ' . substr($w['address'] ?? $id, 0, 12)); ?></option><?php endforeach; endif; ?></select></td></tr>
|
||||
<tr><th>To Address</th><td><input type="text" name="to_address" class="regular-text code" placeholder="External address" required></td></tr>
|
||||
</table>
|
||||
<?php submit_button('Sweep', 'primary'); ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// ESCROW
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
public function page_escrow(): void {
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>Pay-to-Release Escrow</h1>
|
||||
<div class="walletpress-card">
|
||||
<p>Create time-locked escrow wallets. Funds are released when conditions are met.</p>
|
||||
<form method="post">
|
||||
<table class="form-table">
|
||||
<tr><th>Deposit Address</th><td><input type="text" name="deposit_address" class="regular-text code" placeholder="Wallet address receiving deposit" required></td></tr>
|
||||
<tr><th>Amount (USD)</th><td><input type="number" name="amount" step="0.01" class="small-text" required></td></tr>
|
||||
<tr><th>Platform</th><td><select name="platform"><option value="solana">Solana</option><option value="ethereum">Ethereum</option></select></td></tr>
|
||||
<tr><th>Conditions (JSON)</th><td><textarea name="conditions" rows="4" class="large-text code" placeholder='{"release_after": "2026-12-31", "approved_by": ["wallet1", "wallet2"]}'></textarea></td></tr>
|
||||
</table>
|
||||
<?php submit_button('Create Escrow', 'primary'); ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// TOKEN GATES
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
public function page_gates(): void {
|
||||
if (isset($_GET['msg'])) $this->notice($_GET['msg'] === 'created' ? 'Gate created.' : 'Gate deleted.');
|
||||
$gates = get_option('walletpress_gates', []);
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>Token Gates</h1>
|
||||
<table class="wp-list-table widefat fixed striped">
|
||||
<thead><tr><th>Name</th><th>Token</th><th>Chain</th><th>Min</th><th>Shortcode</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
<?php if (empty($gates)): ?><tr><td colspan="6">No gates yet. Create one below.</td></tr>
|
||||
<?php else: foreach ($gates as $id => $g): ?>
|
||||
<tr><td><?php echo esc_html($g['title']); ?></td><td><code><?php echo esc_html(substr($g['token'], 0, 16) . '...'); ?></code></td><td><?php echo esc_html($g['chain']); ?></td><td><?php echo esc_html($g['min_amount']); ?></td>
|
||||
<td><code>[wallet_gate id="<?php echo esc_attr($id); ?>"]</code> <button class="button button-small copy-shortcode" data-code='[wallet_gate id="<?php echo esc_attr($id); ?>"]'>Copy</button></td>
|
||||
<td><a href="<?php echo wp_nonce_url(admin_url('admin.php?page=walletpress-gates&action=delete&id=' . $id), 'walletpress_delete_gate'); ?>" class="button button-small">Delete</a></td></tr>
|
||||
<?php endforeach; endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<form method="post" class="walletpress-card" style="margin-top:20px;">
|
||||
<h2>New Gate</h2>
|
||||
<?php wp_nonce_field('walletpress_save_gate'); ?>
|
||||
<table class="form-table">
|
||||
<tr><th>Gate Name</th><td><input type="text" name="gate_title" class="regular-text" placeholder="e.g. NFT Holders Only" required></td></tr>
|
||||
<tr><th>Token Contract</th><td><input type="text" name="gate_token" class="regular-text code" placeholder="So11111111111111111111111111111111111111112" required></td></tr>
|
||||
<tr><th>Chain</th><td><select name="gate_chain"><?php foreach ($this->plugin->supported_chains() as $k => $c): ?><option value="<?php echo esc_attr($k); ?>"><?php echo esc_html($c['label']); ?></option><?php endforeach; ?></select></td></tr>
|
||||
<tr><th>Min Amount</th><td><input type="number" name="gate_min_amount" value="1" step="0.000001" min="0" class="small-text"></td></tr>
|
||||
</table>
|
||||
<?php submit_button('Create Gate', 'primary', 'save_gate'); ?>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// PAYMENTS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
public function page_payments(): void {
|
||||
$payments = get_option('walletpress_payments_log', []);
|
||||
$total = array_sum(array_column($payments, 'amount_usd'));
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>Payments</h1>
|
||||
<div class="walletpress-stats-grid" style="margin-bottom:20px;">
|
||||
<div class="walletpress-stat"><span class="stat-value">$<?php echo number_format($total, 2); ?></span><span class="stat-label">Total Revenue</span></div>
|
||||
<div class="walletpress-stat"><span class="stat-value"><?php echo count($payments); ?></span><span class="stat-label">Transactions</span></div>
|
||||
<div class="walletpress-stat"><span class="stat-value"><?php echo count(array_filter($payments, fn($p) => ($p['status'] ?? '') === 'confirmed')); ?></span><span class="stat-label">Confirmed</span></div>
|
||||
</div>
|
||||
<table class="wp-list-table widefat fixed striped">
|
||||
<thead><tr><th>Date</th><th>From</th><th>Amount</th><th>Token</th><th>Status</th><th>TX</th></tr></thead>
|
||||
<tbody>
|
||||
<?php if (empty($payments)): ?><tr><td colspan="6">No payments yet.</td></tr>
|
||||
<?php else: foreach (array_reverse($payments) as $p): ?>
|
||||
<tr><td><?php echo esc_html($p['created_at'] ?? ''); ?></td><td><code><?php echo esc_html(substr($p['from'] ?? '', 0, 8) . '...'); ?></code></td><td>$<?php echo number_format($p['amount_usd'] ?? 0, 2); ?></td><td><?php echo esc_html($p['token'] ?? 'SOL'); ?></td><td><span class="walletpress-badge badge-<?php echo esc_attr($p['status'] ?? 'pending'); ?>"><?php echo esc_html($p['status'] ?? 'pending'); ?></span></td><td><code><?php echo esc_html(substr($p['tx_hash'] ?? '', 0, 12) . '...'); ?></code></td></tr>
|
||||
<?php endforeach; endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// API KEYS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
public function page_api_keys(): void {
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_key'])) {
|
||||
check_admin_referer('walletpress_key');
|
||||
$result = $this->api()->create_api_key(sanitize_text_field($_POST['key_label']), array_map('sanitize_text_field', $_POST['key_scopes'] ?? []));
|
||||
if ($result && isset($result['api_key'])): $this->notice("Key created: {$result['api_key']} — save this now, it won't be shown again."); endif;
|
||||
}
|
||||
$keys = $this->api()->list_api_keys();
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>API Keys</h1>
|
||||
<form method="post" class="walletpress-card">
|
||||
<?php wp_nonce_field('walletpress_key'); ?>
|
||||
<h2>Create New Key</h2>
|
||||
<table class="form-table">
|
||||
<tr><th>Label</th><td><input type="text" name="key_label" class="regular-text" placeholder="e.g. Production" required></td></tr>
|
||||
<tr><th>Scopes</th><td><label><input type="checkbox" name="key_scopes[]" value="vault.read" checked> vault.read</label><br>
|
||||
<label><input type="checkbox" name="key_scopes[]" value="vault.write"> vault.write</label><br>
|
||||
<label><input type="checkbox" name="key_scopes[]" value="generate"> generate</label><br>
|
||||
<label><input type="checkbox" name="key_scopes[]" value="admin"> admin</label><br>
|
||||
<label><input type="checkbox" name="key_scopes[]" value="webhook.manage"> webhook.manage</label></td></tr>
|
||||
</table>
|
||||
<?php submit_button('Generate Key', 'primary', 'create_key'); ?>
|
||||
</form>
|
||||
<?php if (is_array($keys) && !empty($keys)): ?>
|
||||
<table class="wp-list-table widefat fixed striped">
|
||||
<thead><tr><th>Label</th><th>Key</th><th>Scopes</th><th>Created</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($keys as $id => $k): ?>
|
||||
<tr><td><?php echo esc_html($k['label'] ?? ''); ?></td><td><code><?php echo esc_html(substr($k['api_key'] ?? $id, 0, 16) . '...'); ?></code></td><td><?php echo esc_html(implode(', ', $k['scopes'] ?? [])); ?></td><td><?php echo esc_html($k['created_at'] ?? ''); ?></td><td><a href="<?php echo wp_nonce_url(admin_url('admin.php?page=walletpress-keys&action=revoke&id=' . $id), 'walletpress_key'); ?>" class="button button-small">Revoke</a></td></tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// WEBHOOKS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
public function page_webhooks(): void {
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_webhook'])) {
|
||||
check_admin_referer('walletpress_webhook');
|
||||
$result = $this->api()->create_webhook(esc_url_raw($_POST['webhook_url']), array_map('sanitize_text_field', $_POST['webhook_events'] ?? []), sanitize_text_field($_POST['webhook_secret']));
|
||||
if ($result) $this->notice('Webhook created.');
|
||||
}
|
||||
$webhooks = $this->api()->list_webhooks();
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>Webhooks</h1>
|
||||
<form method="post" class="walletpress-card">
|
||||
<?php wp_nonce_field('walletpress_webhook'); ?>
|
||||
<h2>New Webhook</h2>
|
||||
<table class="form-table">
|
||||
<tr><th>URL</th><td><input type="url" name="webhook_url" class="regular-text" placeholder="https://hooks.example.com/events" required></td></tr>
|
||||
<tr><th>Secret</th><td><input type="text" name="webhook_secret" class="regular-text" placeholder="Shared secret for HMAC verification"></td></tr>
|
||||
<tr><th>Events</th><td><label><input type="checkbox" name="webhook_events[]" value="wallet.generated" checked> wallet.generated</label><br>
|
||||
<label><input type="checkbox" name="webhook_events[]" value="payment.received"> payment.received</label><br>
|
||||
<label><input type="checkbox" name="webhook_events[]" value="gate.accessed"> gate.accessed</label><br>
|
||||
<label><input type="checkbox" name="webhook_events[]" value="alert.triggered"> alert.triggered</label></td></tr>
|
||||
</table>
|
||||
<?php submit_button('Create Webhook', 'primary', 'create_webhook'); ?>
|
||||
</form>
|
||||
<?php if (is_array($webhooks) && !empty($webhooks)): ?>
|
||||
<table class="wp-list-table widefat fixed striped">
|
||||
<thead><tr><th>URL</th><th>Events</th><th>Created</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($webhooks as $id => $w): ?>
|
||||
<tr><td><code><?php echo esc_html($w['url'] ?? ''); ?></code></td><td><?php echo esc_html(implode(', ', $w['events'] ?? [])); ?></td><td><?php echo esc_html($w['created_at'] ?? ''); ?></td><td><a href="<?php echo wp_nonce_url(admin_url('admin.php?page=walletpress-webhooks&action=delete&id=' . $id), 'walletpress_webhook'); ?>" class="button button-small">Delete</a></td></tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// ALERTS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
public function page_alerts(): void {
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>Balance Alerts</h1>
|
||||
<div class="walletpress-card">
|
||||
<p>Get notified when wallet balances change or when specific on-chain events occur.</p>
|
||||
<form method="post">
|
||||
<table class="form-table">
|
||||
<tr><th>Wallet Address</th><td><input type="text" name="alert_address" class="regular-text code" placeholder="Address to monitor" required></td></tr>
|
||||
<tr><th>Chain</th><td><select name="alert_chain"><?php foreach ($this->plugin->supported_chains() as $k => $c): ?><option value="<?php echo esc_attr($k); ?>"><?php echo esc_html($c['label']); ?></option><?php endforeach; ?></select></td></tr>
|
||||
<tr><th>Trigger</th><td><select name="alert_type"><option value="balance_change">Balance Change</option><option value="incoming_tx">Incoming TX</option><option value="outgoing_tx">Outgoing TX</option><option value="token_movement">Token Movement</option></select></td></tr>
|
||||
<tr><th>Notification</th><td><select name="alert_channel"><option value="email">Email</option><option value="webhook">Webhook</option><option value="telegram">Telegram</option></select></td></tr>
|
||||
</table>
|
||||
<?php submit_button('Create Alert', 'primary'); ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// AUDIT LOG
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
public function page_audit(): void {
|
||||
$audit = $this->api()->audit_trail(100);
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>Audit Trail</h1>
|
||||
<?php if (!is_array($audit) || empty($audit)): $this->notice('No audit entries yet.', 'info'); return; endif; ?>
|
||||
<table class="wp-list-table widefat fixed striped">
|
||||
<thead><tr><th>Timestamp</th><th>Action</th><th>Wallet</th><th>Details</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($audit as $entry): ?>
|
||||
<tr><td><?php echo esc_html($entry['timestamp'] ?? $entry['time'] ?? ''); ?></td>
|
||||
<td><code><?php echo esc_html($entry['action'] ?? $entry['event'] ?? ''); ?></code></td>
|
||||
<td><code><?php echo esc_html(isset($entry['wallet_id']) ? substr($entry['wallet_id'], 0, 12) : '-'); ?></code></td>
|
||||
<td><?php echo esc_html(is_string($entry['details'] ?? '') ? $entry['details'] : wp_json_encode($entry['details'] ?? [])); ?></td></tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// PLUGINS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
public function page_plugins(): void {
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>Plugin System</h1>
|
||||
<p>WalletPress supports event-driven plugins that hook into wallet lifecycle events.</p>
|
||||
<div class="walletpress-cols">
|
||||
<div class="walletpress-col">
|
||||
<div class="walletpress-card">
|
||||
<h3>Available Events</h3>
|
||||
<ul>
|
||||
<li><code>before_generate</code> — Before wallet generation</li>
|
||||
<li><code>after_generate</code> — After wallet generated</li>
|
||||
<li><code>before_import</code> — Before wallet import</li>
|
||||
<li><code>after_import</code> — After wallet imported</li>
|
||||
<li><code>before_rotate</code> — Before key rotation</li>
|
||||
<li><code>after_rotate</code> — After key rotated</li>
|
||||
<li><code>before_sweep</code> — Before fund sweep</li>
|
||||
<li><code>after_sweep</code> — After fund sweep</li>
|
||||
<li><code>vault_change</code> — Any vault modification</li>
|
||||
</ul>
|
||||
<p class="description">Plugins are Python files dropped in the backend's <code>/app/plugins/</code> directory. They receive event data and can transform, log, or reject operations.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="walletpress-col">
|
||||
<div class="walletpress-card">
|
||||
<h3>Create a Plugin</h3>
|
||||
<pre># /app/plugins/my_plugin.py
|
||||
from app.wallet_plugins import register_hook
|
||||
|
||||
def on_generate(context):
|
||||
log = context.get("logger")
|
||||
wallet = context.get("wallet", {})
|
||||
log.info(f"Generated {wallet.get('chain')} wallet")
|
||||
return context # or modify it
|
||||
|
||||
register_hook("after_generate", on_generate)</pre>
|
||||
<p>See <code>wallet_plugins.py</code> in the backend for the full API.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// SETTINGS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
public function page_settings(): void {
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>WalletPress Settings</h1>
|
||||
<form method="post" action="options.php">
|
||||
<?php settings_fields('walletpress_settings'); ?>
|
||||
<div class="walletpress-card">
|
||||
<h2>API Connection</h2>
|
||||
<table class="form-table">
|
||||
<tr><th><label for="wau">Backend API URL</label></th><td><input type="url" id="wau" name="walletpress_api_url" value="<?php echo esc_attr(get_option('walletpress_api_url', '')); ?>" class="regular-text" placeholder="https://api.yourdomain.com"></td></tr>
|
||||
<tr><th><label for="wak">API Key</label></th><td><input type="password" id="wak" name="walletpress_api_key" value="<?php echo esc_attr(get_option('walletpress_api_key', '') ? '********' : ''); ?>" class="regular-text"></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="walletpress-card">
|
||||
<h2>Merchant Wallet</h2>
|
||||
<table class="form-table">
|
||||
<tr><th><label for="mw2">Wallet Address</label></th><td><input type="text" id="mw2" name="walletpress_merchant_wallet" value="<?php echo esc_attr(get_option('walletpress_merchant_wallet', '')); ?>" class="regular-text code"></td></tr>
|
||||
<tr><th><label for="mc2">Chain</label></th><td><select id="mc2" name="walletpress_merchant_chain"><?php foreach ($this->plugin->supported_chains() as $k => $c): ?><option value="<?php echo esc_attr($k); ?>" <?php selected(get_option('walletpress_merchant_chain', 'solana'), $k); ?>><?php echo esc_html($c['label']); ?></option><?php endforeach; ?></select></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="walletpress-card">
|
||||
<h2>Features</h2>
|
||||
<table class="form-table">
|
||||
<tr><th>Wallet Login</th><td><label><input type="checkbox" name="walletpress_login_enabled" value="1" <?php checked('1', get_option('walletpress_login_enabled', '1')); ?>> Enable wallet-based login</label></td></tr>
|
||||
<tr><th>Crypto Payments</th><td><label><input type="checkbox" name="walletpress_payments_enabled" value="1" <?php checked('1', get_option('walletpress_payments_enabled', '1')); ?>> Enable crypto payments</label></td></tr>
|
||||
<tr><th><label for="dc">Default Chain</label></th><td><select id="dc" name="walletpress_default_chain"><?php foreach ($this->plugin->supported_chains() as $k => $c): ?><option value="<?php echo esc_attr($k); ?>" <?php selected(get_option('walletpress_default_chain', 'solana'), $k); ?>><?php echo esc_html($c['label']); ?></option><?php endforeach; ?></select></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="walletpress-card">
|
||||
<h2>User Settings</h2>
|
||||
<table class="form-table">
|
||||
<tr><th>Auto-Create Users</th><td><label><input type="checkbox" name="walletpress_auto_create_users" value="1" <?php checked('1', get_option('walletpress_auto_create_users', '1')); ?>> Create WP user on first wallet login</label></td></tr>
|
||||
<tr><th><label for="dr">Default Role</label></th><td><select id="dr" name="walletpress_default_role"><?php foreach (wp_roles()->get_names() as $k => $v): ?><option value="<?php echo esc_attr($k); ?>" <?php selected(get_option('walletpress_default_role', 'subscriber'), $k); ?>><?php echo esc_html($v); ?></option><?php endforeach; ?></select></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<?php submit_button('Save Settings'); ?>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
292
wp-plugin/includes/class-walletpress-api.php
Normal file
292
wp-plugin/includes/class-walletpress-api.php
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class WalletPressAPI {
|
||||
private string $base_url;
|
||||
private string $api_key;
|
||||
private int $timeout;
|
||||
|
||||
public function __construct(string $base_url = '', string $api_key = '', int $timeout = 30) {
|
||||
$this->base_url = untrailingslashit($base_url);
|
||||
$this->api_key = $api_key;
|
||||
$this->timeout = $timeout;
|
||||
}
|
||||
|
||||
public function is_configured(): bool {
|
||||
return !empty($this->base_url) && !empty($this->api_key);
|
||||
}
|
||||
|
||||
private function url(string $path): string {
|
||||
return $this->base_url . $path;
|
||||
}
|
||||
|
||||
private function headers(): array {
|
||||
$h = ['Content-Type' => 'application/json'];
|
||||
if (!empty($this->api_key)) {
|
||||
$h['X-API-Key'] = $this->api_key;
|
||||
$h['Authorization'] = 'Bearer ' . $this->api_key;
|
||||
}
|
||||
return $h;
|
||||
}
|
||||
|
||||
private function call(string $method, string $path, ?array $body = null): ?array {
|
||||
$args = ['timeout' => $this->timeout, 'headers' => $this->headers()];
|
||||
if ($body !== null) {
|
||||
$args['body'] = wp_json_encode($body);
|
||||
}
|
||||
$response = $method === 'GET' ? wp_remote_get($this->url($path), $args) : wp_remote_post($this->url($path), $args);
|
||||
if (is_wp_error($response)) {
|
||||
$this->log('API error: ' . $response->get_error_message());
|
||||
return null;
|
||||
}
|
||||
$code = wp_remote_retrieve_response_code($response);
|
||||
$data = json_decode(wp_remote_retrieve_body($response), true);
|
||||
if ($code < 200 || $code >= 300) {
|
||||
$this->log("API {$code}: " . wp_remote_retrieve_body($response));
|
||||
return null;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function log(string $msg): void {
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log("[WalletPress] {$msg}");
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Health ────────────────────────────────────────────
|
||||
public function health(): ?array { return $this->call('GET', '/health'); }
|
||||
|
||||
// ─── Chain Vault (wallet factory) ──────────────────────
|
||||
private const CV = '/api/v1/chain-vault';
|
||||
|
||||
public function list_chains(): ?array { return $this->call('GET', self::CV . '/chains'); }
|
||||
public function stats(): ?array { return $this->call('GET', self::CV . '/stats'); }
|
||||
public function health_score(string $wallet_id): ?array { return $this->call('GET', self::CV . "/health-score/{$wallet_id}"); }
|
||||
public function healthz(): ?array { return $this->call('GET', self::CV . '/healthz'); }
|
||||
|
||||
// --- Generation ---
|
||||
public function generate_wallet(string $chain, string $label = '', array $tags = []): ?array {
|
||||
return $this->call('POST', self::CV . '/generate', ['chain' => $chain, 'label' => $label, 'tags' => $tags]);
|
||||
}
|
||||
public function generate_batch(array $chains, string $label_prefix = ''): ?array {
|
||||
return $this->call('POST', self::CV . '/generate/batch', ['chains' => $chains, 'label_prefix' => $label_prefix]);
|
||||
}
|
||||
public function generate_all(): ?array { return $this->call('POST', self::CV . '/generate/batch', ['chains' => []]); }
|
||||
public function from_mnemonic(string $mnemonic, string $passphrase = ''): ?array {
|
||||
return $this->call('POST', self::CV . '/from-mnemonic', ['mnemonic' => $mnemonic, 'passphrase' => $passphrase]);
|
||||
}
|
||||
|
||||
// --- Vault ---
|
||||
public function list_vault(): ?array { return $this->call('GET', self::CV . '/vault'); }
|
||||
public function get_wallet(string $wallet_id): ?array { return $this->call('GET', self::CV . "/vault/{$wallet_id}"); }
|
||||
public function rotate_wallet(string $wallet_id): ?array {
|
||||
return $this->call('POST', self::CV . '/rotate', ['wallet_id' => $wallet_id]);
|
||||
}
|
||||
public function export_wallets(string $format = 'json', array $ids = []): ?array {
|
||||
return $this->call('POST', self::CV . '/export', ['format' => $format, 'ids' => $ids]);
|
||||
}
|
||||
public function rotate_sweep(string $wallet_id, string $to_address): ?array {
|
||||
return $this->call('POST', self::CV . '/rotate-sweep', ['wallet_id' => $wallet_id, 'to_address' => $to_address]);
|
||||
}
|
||||
public function distribute_wallets(array $distributions): ?array {
|
||||
return $this->call('POST', self::CV . '/distribute', ['distributions' => $distributions]);
|
||||
}
|
||||
|
||||
// --- Wallet Tree ---
|
||||
public function wallet_tree(): ?array { return $this->call('GET', self::CV . '/tree'); }
|
||||
public function cluster_generate(string $chain, int $count = 10): ?array {
|
||||
return $this->call('POST', self::CV . '/cluster', ['chain' => $chain, 'count' => $count]);
|
||||
}
|
||||
|
||||
// --- Escrow ---
|
||||
public function create_escrow(string $platform, string $deposit_address, array $conditions, float $amount): ?array {
|
||||
return $this->call('POST', self::CV . '/escrow', [
|
||||
'platform' => $platform, 'deposit_address' => $deposit_address,
|
||||
'conditions' => $conditions, 'amount' => $amount,
|
||||
]);
|
||||
}
|
||||
public function release_escrow(string $escrow_id): ?array {
|
||||
return $this->call('POST', self::CV . '/escrow/release', ['escrow_id' => $escrow_id]);
|
||||
}
|
||||
|
||||
// --- Mnemonic convert (full /derive endpoint) ---
|
||||
public function derive_address(string $mnemonic, string $chain, string $path = ''): ?array {
|
||||
return $this->call('POST', self::CV . '/derive-address', ['mnemonic' => $mnemonic, 'chain' => $chain, 'path' => $path]);
|
||||
}
|
||||
|
||||
// --- Import ---
|
||||
public function import_wallet(string $chain, string $private_key, string $label = ''): ?array {
|
||||
return $this->call('POST', self::CV . '/import', ['chain' => $chain, 'private_key' => $private_key, 'label' => $label]);
|
||||
}
|
||||
|
||||
// --- HD Wallet ---
|
||||
public function create_hd_wallet(string $mnemonic, string $password = ''): ?array {
|
||||
return $this->call('POST', self::CV . '/hd-wallet', ['mnemonic' => $mnemonic, 'password' => $password]);
|
||||
}
|
||||
|
||||
// --- Paper Wallet ---
|
||||
public function paper_wallet(string $wallet_id): ?array {
|
||||
return $this->call('GET', self::CV . "/paper-wallet/{$wallet_id}");
|
||||
}
|
||||
|
||||
// --- Audit ---
|
||||
public function audit_trail(int $limit = 50): ?array {
|
||||
return $this->call('GET', self::CV . '/audit-trail', ['limit' => $limit]);
|
||||
}
|
||||
|
||||
// --- Bulk Operations ---
|
||||
public function bulk_filter(array $filters): ?array {
|
||||
return $this->call('POST', self::CV . '/bulk/filter', ['filters' => $filters]);
|
||||
}
|
||||
public function bulk_delete(array $ids): ?array {
|
||||
return $this->call('POST', self::CV . '/bulk/delete', ['ids' => $ids]);
|
||||
}
|
||||
public function bulk_export(string $format = 'csv', array $ids = []): ?array {
|
||||
return $this->call('POST', self::CV . '/bulk/export', ['format' => $format, 'ids' => $ids]);
|
||||
}
|
||||
|
||||
// --- Validation ---
|
||||
public function validate_address(string $chain, string $address): ?array {
|
||||
return $this->call('GET', self::CV . "/validate/{$chain}/{$address}");
|
||||
}
|
||||
public function validate_all(): ?array { return $this->call('GET', self::CV . '/validate/all'); }
|
||||
|
||||
// --- Balances ---
|
||||
public function list_balances(): ?array { return $this->call('GET', self::CV . '/balances'); }
|
||||
public function balance_snapshot(): ?array { return $this->call('POST', self::CV . '/balances/snapshot'); }
|
||||
public function balance_history(int $days = 30): ?array {
|
||||
return $this->call('GET', self::CV . '/balances/history', ['days' => $days]);
|
||||
}
|
||||
|
||||
// --- RPC Chains ---
|
||||
public function rpc_chains(): ?array { return $this->call('GET', self::CV . '/rpc-chains'); }
|
||||
|
||||
// --- Cross-Chain Linking ---
|
||||
public function link_proof(string $source_address, string $source_chain, string $target_chain): ?array {
|
||||
return $this->call('POST', self::CV . '/link/proof', [
|
||||
'source_address' => $source_address, 'source_chain' => $source_chain, 'target_chain' => $target_chain,
|
||||
]);
|
||||
}
|
||||
public function link_verify(string $address, string $chain, string $proof): ?array {
|
||||
return $this->call('POST', self::CV . '/link/verify', ['address' => $address, 'chain' => $chain, 'proof' => $proof]);
|
||||
}
|
||||
|
||||
// --- Temporal Wallets ---
|
||||
public function temporal_create(string $chain, int $ttl_seconds, string $label = ''): ?array {
|
||||
return $this->call('POST', self::CV . '/temporal/create', ['chain' => $chain, 'ttl_seconds' => $ttl_seconds, 'label' => $label]);
|
||||
}
|
||||
public function temporal_release(string $temporal_id): ?array {
|
||||
return $this->call('POST', self::CV . '/temporal/release', ['temporal_id' => $temporal_id]);
|
||||
}
|
||||
public function temporal_list(): ?array { return $this->call('GET', self::CV . '/temporal/list'); }
|
||||
|
||||
// --- Sweep ---
|
||||
public function sweep_wallet(string $from_wallet_id, string $to_address): ?array {
|
||||
return $this->call('POST', self::CV . '/sweep', ['from_wallet_id' => $from_wallet_id, 'to_address' => $to_address]);
|
||||
}
|
||||
|
||||
// --- Funding Bundle ---
|
||||
public function funding_bundle(string $chain, string $wallet_id, float $amount): ?array {
|
||||
return $this->call('POST', self::CV . '/funding-bundle', ['chain' => $chain, 'wallet_id' => $wallet_id, 'amount' => $amount]);
|
||||
}
|
||||
|
||||
// --- Deployer Setup ---
|
||||
public function deployer_setup(string $chain, string $wallet_id): ?array {
|
||||
return $this->call('POST', self::CV . '/deployer-setup', ['chain' => $chain, 'wallet_id' => $wallet_id]);
|
||||
}
|
||||
|
||||
// --- Transaction Builder ---
|
||||
public function tx_build(string $chain, string $from, string $to, float $amount, array $opts = []): ?array {
|
||||
return $this->call('POST', self::CV . '/tx/build', array_merge(['chain' => $chain, 'from' => $from, 'to' => $to, 'amount' => $amount], $opts));
|
||||
}
|
||||
public function tx_broadcast(string $chain, string $signed_tx): ?array {
|
||||
return $this->call('POST', self::CV . '/tx/broadcast', ['chain' => $chain, 'signed_tx' => $signed_tx]);
|
||||
}
|
||||
|
||||
// --- API Keys ---
|
||||
public function create_api_key(string $label, array $scopes = ['vault.read']): ?array {
|
||||
return $this->call('POST', self::CV . '/api-keys', ['label' => $label, 'scopes' => $scopes]);
|
||||
}
|
||||
public function list_api_keys(): ?array { return $this->call('GET', self::CV . '/api-keys'); }
|
||||
public function revoke_api_key(string $key_id): ?array {
|
||||
return $this->call('POST', self::CV . '/api-keys/revoke', ['key_id' => $key_id]);
|
||||
}
|
||||
|
||||
// --- Alerts ---
|
||||
public function create_alert(array $config): ?array {
|
||||
return $this->call('POST', self::CV . '/alerts', $config);
|
||||
}
|
||||
public function list_alerts(): ?array { return $this->call('GET', self::CV . '/alerts'); }
|
||||
public function delete_alert(string $alert_id): ?array {
|
||||
return $this->call('POST', self::CV . '/alerts/delete', ['alert_id' => $alert_id]);
|
||||
}
|
||||
|
||||
// --- Webhooks ---
|
||||
public function create_webhook(string $url, array $events = [], string $secret = ''): ?array {
|
||||
return $this->call('POST', self::CV . '/webhooks', ['url' => $url, 'events' => $events, 'secret' => $secret]);
|
||||
}
|
||||
public function list_webhooks(): ?array { return $this->call('GET', self::CV . '/webhooks'); }
|
||||
public function delete_webhook(string $webhook_id): ?array {
|
||||
return $this->call('DELETE', self::CV . "/webhooks/{$webhook_id}");
|
||||
}
|
||||
|
||||
// ─── Wallet Analysis (domain/wallet) ──────────────────
|
||||
private const WA = '/api/v1/wallet';
|
||||
|
||||
public function get_balance(string $address, string $chain = 'solana'): ?array {
|
||||
return $this->call('GET', self::WA . "/{$address}/balance", ['chain' => $chain]);
|
||||
}
|
||||
public function analyze_wallet(string $address, string $chain = 'solana'): ?array {
|
||||
return $this->call('GET', self::WA . "/{$address}/analyze", ['chain' => $chain]);
|
||||
}
|
||||
public function scan_wallet(string $address, string $chain = 'solana', string $tier = 'free'): ?array {
|
||||
return $this->call('POST', self::WA . '/scan', ['address' => $address, 'chain' => $chain, 'tier' => $tier]);
|
||||
}
|
||||
public function verify_signature(string $address, string $message, string $signature, string $chain = 'solana'): ?array {
|
||||
return $this->call('POST', self::WA . '/verify', ['address' => $address, 'message' => $message, 'signature' => $signature, 'chain' => $chain]);
|
||||
}
|
||||
public function get_transactions(string $address, string $chain = 'solana', int $limit = 50): ?array {
|
||||
return $this->call('GET', self::WA . "/{$address}/transactions", ['chain' => $chain, 'limit' => $limit]);
|
||||
}
|
||||
|
||||
// ─── Payments ──────────────────────────────────────────
|
||||
public function create_payment(string $from, string $to, float $amount, string $token = 'SOL', string $chain = 'solana'): ?array {
|
||||
return $this->call('POST', '/api/v1/chain-vault/payments/create', [
|
||||
'wallet_id' => 'payment_' . sanitize_key($from),
|
||||
'amount_usd' => $amount,
|
||||
'chain' => $chain,
|
||||
'token' => $token,
|
||||
'description' => "Payment from {$from} to {$to}",
|
||||
]);
|
||||
}
|
||||
|
||||
public function verify_payment(string $tx_hash, string $chain = 'solana'): ?array {
|
||||
return $this->call('POST', '/api/v1/chain-vault/payments/verify', [
|
||||
'payment_id' => 'pay_verification',
|
||||
'tx_hash' => $tx_hash,
|
||||
'chain' => $chain,
|
||||
]);
|
||||
}
|
||||
|
||||
// ─── Token Ownership ───────────────────────────────────
|
||||
public function check_ownership(string $address, string $contract, string $chain = 'solana'): ?array {
|
||||
return $this->call('GET', "/api/v1/wallet-memory/labels/{$address}", ['chain' => $chain]);
|
||||
}
|
||||
|
||||
// ─── Wallet Memory (clustering) ────────────────────────
|
||||
private const WM = '/api/v1/wallet-memory';
|
||||
|
||||
public function cluster_wallets(array $addresses, string $method = 'behavioral'): ?array {
|
||||
return $this->call('POST', self::WM . '/cluster', ['addresses' => $addresses, 'method' => $method]);
|
||||
}
|
||||
public function resolve_entity(string $address): ?array {
|
||||
return $this->call('GET', self::WM . "/resolve/{$address}");
|
||||
}
|
||||
public function wallet_labels(string $address): ?array {
|
||||
return $this->call('GET', self::WM . "/labels/{$address}");
|
||||
}
|
||||
public function risk_score(string $address, string $chain = 'solana'): ?array {
|
||||
return $this->call('GET', self::WM . "/risk/{$address}", ['chain' => $chain]);
|
||||
}
|
||||
}
|
||||
211
wp-plugin/includes/class-walletpress-auth.php
Normal file
211
wp-plugin/includes/class-walletpress-auth.php
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class WalletPressAuth {
|
||||
private static ?WalletPress $plugin = null;
|
||||
|
||||
public function __construct(WalletPress $plugin) {
|
||||
self::$plugin = $plugin;
|
||||
|
||||
add_action('wp_ajax_walletpress_disconnect', [__CLASS__, 'ajax_disconnect']);
|
||||
add_action('wp_ajax_nopriv_walletpress_disconnect', [__CLASS__, 'ajax_disconnect']);
|
||||
add_filter('show_admin_bar', [__CLASS__, 'maybe_show_admin_bar'], 10, 2);
|
||||
}
|
||||
|
||||
public static function render_connect_button(): string {
|
||||
if (is_user_logged_in()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="walletpress-connect">
|
||||
<button class="walletpress-btn walletpress-btn-metamask" data-wallet="metamask">
|
||||
<span class="walletpress-wallet-icon">🦊</span>
|
||||
<?php esc_html_e('Sign in with MetaMask', 'walletpress'); ?>
|
||||
</button>
|
||||
<button class="walletpress-btn walletpress-btn-phantom" data-wallet="phantom">
|
||||
<span class="walletpress-wallet-icon">👻</span>
|
||||
<?php esc_html_e('Sign in with Phantom', 'walletpress'); ?>
|
||||
</button>
|
||||
<button class="walletpress-btn walletpress-btn-walletconnect" data-wallet="walletconnect">
|
||||
<span class="walletpress-wallet-icon">🔗</span>
|
||||
<?php esc_html_e('WalletConnect', 'walletpress'); ?>
|
||||
</button>
|
||||
<p class="walletpress-error" style="display:none;color:#dc3232;"></p>
|
||||
<p class="walletpress-status" style="display:none;"></p>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
public static function rest_nonce(): WP_REST_Response {
|
||||
$nonce = wp_generate_password(24, false);
|
||||
$hash = wp_hash($nonce, 'walletpress_auth');
|
||||
|
||||
set_transient('walletpress_nonce_' . $hash, $nonce, 300);
|
||||
|
||||
return new WP_REST_Response([
|
||||
'nonce' => $nonce,
|
||||
'hash' => $hash,
|
||||
'expires_in' => 300,
|
||||
], 200);
|
||||
}
|
||||
|
||||
public static function rest_verify(WP_REST_Request $request): WP_REST_Response {
|
||||
$address = sanitize_text_field($request->get_param('address'));
|
||||
$signature = sanitize_text_field($request->get_param('signature'));
|
||||
$message = sanitize_text_field($request->get_param('message'));
|
||||
$chain = sanitize_text_field($request->get_param('chain')) ?: 'solana';
|
||||
$nonce_hash = sanitize_text_field($request->get_param('nonce_hash'));
|
||||
|
||||
if (empty($address) || empty($signature) || empty($message) || empty($nonce_hash)) {
|
||||
return new WP_REST_Response(['error' => 'Missing required fields'], 400);
|
||||
}
|
||||
|
||||
$cached = get_transient('walletpress_nonce_' . $nonce_hash);
|
||||
if (!$cached) {
|
||||
return new WP_REST_Response(['error' => 'Nonce expired or invalid'], 401);
|
||||
}
|
||||
|
||||
$expected = sprintf(
|
||||
__('WalletPress Login\n\nAddress: %s\nNonce: %s\nChain: %s\n\nThis signature does not cost gas.', 'walletpress'),
|
||||
$address, $cached, $chain
|
||||
);
|
||||
if ($message !== $expected) {
|
||||
return new WP_REST_Response(['error' => 'Message mismatch'], 400);
|
||||
}
|
||||
|
||||
$api = self::$plugin ? self::$plugin->api() : null;
|
||||
if ($api && $api->is_configured()) {
|
||||
$verified = $api->verify_signature($address, $message, $signature, $chain);
|
||||
if ($verified === null) {
|
||||
return new WP_REST_Response(['error' => 'Signature verification failed'], 401);
|
||||
}
|
||||
}
|
||||
|
||||
delete_transient('walletpress_nonce_' . $nonce_hash);
|
||||
|
||||
$user = self::find_user_by_wallet($address);
|
||||
if (!$user && get_option('walletpress_auto_create_users', '1') === '1') {
|
||||
$user_id = self::create_user($address, $chain);
|
||||
$user = get_user_by('ID', $user_id);
|
||||
}
|
||||
|
||||
if ($user) {
|
||||
wp_set_current_user($user->ID);
|
||||
wp_set_auth_cookie($user->ID);
|
||||
|
||||
update_user_meta($user->ID, 'walletpress_address', $address);
|
||||
update_user_meta($user->ID, 'walletpress_chain', $chain);
|
||||
update_user_meta($user->ID, 'walletpress_connected_at', current_time('mysql'));
|
||||
|
||||
return new WP_REST_Response([
|
||||
'success' => true,
|
||||
'user_id' => $user->ID,
|
||||
'display_name' => $user->display_name,
|
||||
'wallet' => $address,
|
||||
], 200);
|
||||
}
|
||||
|
||||
return new WP_REST_Response(['error' => 'User creation disabled and no existing user found'], 403);
|
||||
}
|
||||
|
||||
public static function rest_me(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!is_user_logged_in()) {
|
||||
return new WP_REST_Response(['error' => 'Not authenticated'], 401);
|
||||
}
|
||||
$user = wp_get_current_user();
|
||||
return new WP_REST_Response([
|
||||
'id' => $user->ID,
|
||||
'display_name' => $user->display_name,
|
||||
'email' => $user->user_email,
|
||||
'wallet' => get_user_meta($user->ID, 'walletpress_address', true),
|
||||
'chain' => get_user_meta($user->ID, 'walletpress_chain', true),
|
||||
], 200);
|
||||
}
|
||||
|
||||
public static function rest_balance(WP_REST_Request $request): WP_REST_Response {
|
||||
$address = sanitize_text_field($request->get_param('address'));
|
||||
$chain = sanitize_text_field($request->get_param('chain')) ?: 'solana';
|
||||
|
||||
if (empty($address)) {
|
||||
return new WP_REST_Response(['error' => 'Address required'], 400);
|
||||
}
|
||||
|
||||
$api = self::$plugin ? self::$plugin->api() : null;
|
||||
if (!$api || !$api->is_configured()) {
|
||||
return new WP_REST_Response(['error' => 'API not configured'], 503);
|
||||
}
|
||||
|
||||
$balance = $api->get_balance($address, $chain);
|
||||
if ($balance === null) {
|
||||
return new WP_REST_Response(['error' => 'Failed to fetch balance'], 502);
|
||||
}
|
||||
|
||||
return new WP_REST_Response($balance, 200);
|
||||
}
|
||||
|
||||
public static function ajax_disconnect(): void {
|
||||
check_ajax_referer('walletpress_nonce', 'nonce');
|
||||
|
||||
if (!is_user_logged_in()) {
|
||||
wp_send_json_error(['error' => 'Not logged in']);
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
delete_user_meta($user_id, 'walletpress_address');
|
||||
delete_user_meta($user_id, 'walletpress_chain');
|
||||
|
||||
wp_logout();
|
||||
wp_send_json_success(['disconnected' => true]);
|
||||
}
|
||||
|
||||
public static function maybe_show_admin_bar(bool $show, int $user_id): bool {
|
||||
if (!is_admin() && get_user_meta($user_id, 'walletpress_address', true)) {
|
||||
return false;
|
||||
}
|
||||
return $show;
|
||||
}
|
||||
|
||||
private static function find_user_by_wallet(string $address): ?WP_User {
|
||||
$users = get_users([
|
||||
'meta_key' => 'walletpress_address',
|
||||
'meta_value' => $address,
|
||||
'number' => 1,
|
||||
]);
|
||||
return $users[0] ?? null;
|
||||
}
|
||||
|
||||
private static function create_user(string $address, string $chain): int {
|
||||
$username = 'wallet_' . substr($address, 0, 12);
|
||||
$email = $username . '@walletpress.local';
|
||||
$role = get_option('walletpress_default_role', 'subscriber');
|
||||
|
||||
$user_id = wp_insert_user([
|
||||
'user_login' => $username,
|
||||
'user_email' => $email,
|
||||
'user_pass' => wp_generate_password(32),
|
||||
'display_name' => substr($address, 0, 6) . '...' . substr($address, -4),
|
||||
'role' => $role,
|
||||
]);
|
||||
|
||||
if (is_wp_error($user_id)) {
|
||||
$suffix = 1;
|
||||
while (is_wp_error($user_id)) {
|
||||
$username = 'wallet_' . substr($address, 0, 10) . $suffix;
|
||||
$email = $username . '@walletpress.local';
|
||||
$user_id = wp_insert_user([
|
||||
'user_login' => $username,
|
||||
'user_email' => $email,
|
||||
'user_pass' => wp_generate_password(32),
|
||||
'display_name' => substr($address, 0, 6) . '...' . substr($address, -4),
|
||||
'role' => $role,
|
||||
]);
|
||||
$suffix++;
|
||||
}
|
||||
}
|
||||
|
||||
return $user_id;
|
||||
}
|
||||
}
|
||||
107
wp-plugin/includes/class-walletpress-gate.php
Normal file
107
wp-plugin/includes/class-walletpress-gate.php
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class WalletPressGate {
|
||||
private static ?WalletPress $plugin = null;
|
||||
|
||||
public function __construct(WalletPress $plugin) {
|
||||
self::$plugin = $plugin;
|
||||
}
|
||||
|
||||
public static function render_gate(array $atts, string $content = ''): string {
|
||||
$atts = shortcode_atts([
|
||||
'id' => '',
|
||||
'token' => '',
|
||||
'chain' => '',
|
||||
'min_amount' => '1',
|
||||
'fallback' => '',
|
||||
], $atts, 'wallet_gate');
|
||||
|
||||
if (!is_user_logged_in()) {
|
||||
return self::fallback($atts['fallback']) ?: do_shortcode('[wallet_connect]');
|
||||
}
|
||||
|
||||
$user = wp_get_current_user();
|
||||
$wallet = get_user_meta($user->ID, 'walletpress_address', true);
|
||||
$chain = $atts['chain'] ?: get_user_meta($user->ID, 'walletpress_chain', true) ?: 'solana';
|
||||
|
||||
if (!$wallet) {
|
||||
return self::fallback($atts['fallback']) ?: do_shortcode('[wallet_connect]');
|
||||
}
|
||||
|
||||
if (!empty($atts['id'])) {
|
||||
$gates = get_option('walletpress_gates', []);
|
||||
$gate = $gates[intval($atts['id'])] ?? null;
|
||||
if ($gate) {
|
||||
$atts['token'] = $gate['token'];
|
||||
$atts['chain'] = $gate['chain'];
|
||||
$atts['min_amount'] = $gate['min_amount'];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($atts['token'])) {
|
||||
return $content ?: esc_html__('No token specified for gate.', 'walletpress');
|
||||
}
|
||||
|
||||
$owns = self::check_ownership($wallet, $atts['token'], $chain, floatval($atts['min_amount']));
|
||||
|
||||
if ($owns) {
|
||||
return do_shortcode($content);
|
||||
}
|
||||
|
||||
return self::fallback($atts['fallback']);
|
||||
}
|
||||
|
||||
public static function rest_verify_ownership(WP_REST_Request $request): WP_REST_Response {
|
||||
$address = sanitize_text_field($request->get_param('address'));
|
||||
$contract = sanitize_text_field($request->get_param('contract'));
|
||||
$chain = sanitize_text_field($request->get_param('chain')) ?: 'solana';
|
||||
$min_amount = floatval($request->get_param('min_amount') ?: 1);
|
||||
|
||||
if (empty($address) || empty($contract)) {
|
||||
return new WP_REST_Response(['error' => 'address and contract required'], 400);
|
||||
}
|
||||
|
||||
$owns = self::check_ownership($address, $contract, $chain, $min_amount);
|
||||
|
||||
return new WP_REST_Response([
|
||||
'owns' => $owns,
|
||||
'address' => $address,
|
||||
'contract' => $contract,
|
||||
'chain' => $chain,
|
||||
'min_amount' => $min_amount,
|
||||
], 200);
|
||||
}
|
||||
|
||||
private static function check_ownership(string $address, string $contract, string $chain, float $min_amount = 1): bool {
|
||||
$api = self::$plugin ? self::$plugin->api() : null;
|
||||
if ($api && $api->is_configured()) {
|
||||
$result = $api->check_ownership($address, $contract, $chain);
|
||||
if ($result !== null) {
|
||||
$balance = floatval($result['balance'] ?? $result['amount'] ?? 0);
|
||||
return $balance >= $min_amount;
|
||||
}
|
||||
$balance = $api->get_balance($address, $chain);
|
||||
if ($balance !== null) {
|
||||
$tokens = $balance['tokens'] ?? $balance['data']['tokens'] ?? [];
|
||||
foreach ($tokens as $token) {
|
||||
$tok_addr = $token['address'] ?? $token['mint'] ?? '';
|
||||
if (strtolower($tok_addr) === strtolower($contract)) {
|
||||
$amount = floatval($token['amount'] ?? $token['balance'] ?? 0);
|
||||
if ($amount >= $min_amount) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static function fallback(string $fallback): string {
|
||||
if (!empty($fallback)) {
|
||||
return '<div class="walletpress-gate-fallback">' . wp_kses_post($fallback) . '</div>';
|
||||
}
|
||||
return '<div class="walletpress-gate-fallback">' . esc_html__('You do not hold the required tokens to view this content.', 'walletpress') . '</div>';
|
||||
}
|
||||
}
|
||||
106
wp-plugin/includes/class-walletpress-payments.php
Normal file
106
wp-plugin/includes/class-walletpress-payments.php
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class WalletPressPayments {
|
||||
private static ?WalletPress $plugin = null;
|
||||
|
||||
public function __construct(WalletPress $plugin) {
|
||||
self::$plugin = $plugin;
|
||||
}
|
||||
|
||||
public static function render_pay_button(array $atts): string {
|
||||
$atts = shortcode_atts([
|
||||
'amount' => '10',
|
||||
'token' => 'SOL',
|
||||
'chain' => 'solana',
|
||||
'to' => '',
|
||||
'label' => '',
|
||||
'product' => '',
|
||||
'success' => '',
|
||||
], $atts, 'wallet_pay');
|
||||
|
||||
$to_address = $atts['to'] ?: get_option('walletpress_merchant_wallet', '');
|
||||
|
||||
if (empty($to_address)) {
|
||||
return '<p class="walletpress-error">' . esc_html__('Payment not configured. Set a merchant wallet in settings.', 'walletpress') . '</p>';
|
||||
}
|
||||
|
||||
$label = $atts['label'] ?: sprintf(__('Pay %s %s', 'walletpress'), $atts['amount'], $atts['token']);
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="walletpress-payment" data-amount="<?php echo esc_attr($atts['amount']); ?>" data-token="<?php echo esc_attr($atts['token']); ?>" data-chain="<?php echo esc_attr($atts['chain']); ?>" data-to="<?php echo esc_attr($to_address); ?>" data-product="<?php echo esc_attr($atts['product']); ?>" data-success="<?php echo esc_attr($atts['success']); ?>">
|
||||
<button class="walletpress-btn walletpress-btn-pay">
|
||||
<?php echo esc_html($label); ?>
|
||||
</button>
|
||||
<div class="walletpress-payment-status" style="display:none;"></div>
|
||||
<div class="walletpress-payment-qr" style="display:none;"></div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
public static function rest_create_payment(WP_REST_Request $request): WP_REST_Response {
|
||||
$from = sanitize_text_field($request->get_param('from'));
|
||||
$amount = floatval($request->get_param('amount'));
|
||||
$token = sanitize_text_field($request->get_param('token')) ?: 'SOL';
|
||||
$chain = sanitize_text_field($request->get_param('chain')) ?: 'solana';
|
||||
$to = sanitize_text_field($request->get_param('to'));
|
||||
|
||||
if (empty($from) || empty($to)) {
|
||||
return new WP_REST_Response(['error' => 'from and to required'], 400);
|
||||
}
|
||||
|
||||
$api = self::$plugin ? self::$plugin->api() : null;
|
||||
if ($api && $api->is_configured()) {
|
||||
$result = $api->create_payment($from, $to, $amount, $token, $chain);
|
||||
if ($result) {
|
||||
return new WP_REST_Response($result, 200);
|
||||
}
|
||||
}
|
||||
|
||||
return new WP_REST_Response([
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
'amount' => $amount,
|
||||
'token' => $token,
|
||||
'chain' => $chain,
|
||||
'status' => 'pending',
|
||||
], 200);
|
||||
}
|
||||
|
||||
public static function rest_verify_payment(WP_REST_Request $request): WP_REST_Response {
|
||||
$tx_hash = sanitize_text_field($request->get_param('tx_hash'));
|
||||
$chain = sanitize_text_field($request->get_param('chain')) ?: 'solana';
|
||||
$product = sanitize_text_field($request->get_param('product'));
|
||||
|
||||
if (empty($tx_hash)) {
|
||||
return new WP_REST_Response(['error' => 'tx_hash required'], 400);
|
||||
}
|
||||
|
||||
$api = self::$plugin ? self::$plugin->api() : null;
|
||||
if ($api && $api->is_configured()) {
|
||||
$result = $api->verify_payment($tx_hash, $chain);
|
||||
if ($result !== null && !empty($result['confirmed'])) {
|
||||
if (!empty($product)) {
|
||||
self::unlock_product($tx_hash, $product);
|
||||
}
|
||||
return new WP_REST_Response(array_merge($result, ['unlocked' => true]), 200);
|
||||
}
|
||||
}
|
||||
|
||||
return new WP_REST_Response(['tx_hash' => $tx_hash, 'confirmed' => false, 'status' => 'pending'], 200);
|
||||
}
|
||||
|
||||
private static function unlock_product(string $tx_hash, string $product): void {
|
||||
if (!is_user_logged_in()) {
|
||||
return;
|
||||
}
|
||||
$user_id = get_current_user_id();
|
||||
$purchases = get_user_meta($user_id, 'walletpress_purchases', true) ?: [];
|
||||
if (!in_array($product, $purchases, true)) {
|
||||
$purchases[] = $product;
|
||||
update_user_meta($user_id, 'walletpress_purchases', $purchases);
|
||||
}
|
||||
}
|
||||
}
|
||||
252
wp-plugin/includes/class-walletpress.php
Normal file
252
wp-plugin/includes/class-walletpress.php
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class WalletPress {
|
||||
private array $modules = [];
|
||||
private ?WalletPressAPI $api = null;
|
||||
|
||||
/**
|
||||
* Encrypt a value using WordPress salt as the key.
|
||||
*
|
||||
* P2-18 fix: Was AES-256-CBC without authentication (malleable, vulnerable
|
||||
* to padding-oracle / bit-flipping attacks). Now AES-256-GCM with the
|
||||
* 16-byte auth tag appended to the IV field. Format:
|
||||
* base64( iv (12B) || ciphertext || tag (16B) )
|
||||
* On legacy decrypt() (CBC), auto-detects and decodes the old format
|
||||
* so existing encrypted values still work after an update.
|
||||
*/
|
||||
public static function encrypt(string $plaintext): string {
|
||||
if ($plaintext === '') return '';
|
||||
$key = defined('LOGGED_IN_KEY') ? LOGGED_IN_KEY : wp_salt('logged_in');
|
||||
// Derive a 32-byte key from the salt (it may be shorter than 32 chars)
|
||||
$key = hash('sha256', $key, true);
|
||||
$iv = openssl_random_pseudo_bytes(12);
|
||||
$tag = '';
|
||||
$cipher = openssl_encrypt(
|
||||
$plaintext,
|
||||
'aes-256-gcm',
|
||||
$key,
|
||||
OPENSSL_RAW_DATA,
|
||||
$iv,
|
||||
$tag,
|
||||
'',
|
||||
16
|
||||
);
|
||||
// Format: base64(iv (12) || ciphertext || tag (16))
|
||||
return base64_encode($iv . $cipher . $tag);
|
||||
}
|
||||
|
||||
/** Decrypt a value encrypted by encrypt(). Supports legacy CBC format. */
|
||||
public static function decrypt(string $ciphertext): string {
|
||||
if ($ciphertext === '') return '';
|
||||
$key = defined('LOGGED_IN_KEY') ? LOGGED_IN_KEY : wp_salt('logged_in');
|
||||
$data = base64_decode($ciphertext, true);
|
||||
if ($data === false || strlen($data) < 16) return '';
|
||||
|
||||
// Auto-detect format. GCM: 12B IV + N ciphertext + 16B tag = at least 28B.
|
||||
// CBC legacy: 16B IV + N ciphertext. Heuristic: try GCM first if
|
||||
// (len - 12 - 16) >= 0; otherwise legacy CBC.
|
||||
$key = hash('sha256', $key, true);
|
||||
if (strlen($data) >= 28) {
|
||||
// Try GCM first
|
||||
$iv = substr($data, 0, 12);
|
||||
$tag = substr($data, -16);
|
||||
$cipher = substr($data, 12, -16);
|
||||
$plain = openssl_decrypt($cipher, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag, '');
|
||||
if ($plain !== false) return $plain;
|
||||
// Fall through to CBC attempt
|
||||
}
|
||||
// Legacy CBC: 16B IV + ciphertext
|
||||
$iv = substr($data, 0, 16);
|
||||
$cipher = substr($data, 16);
|
||||
return openssl_decrypt($cipher, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv) ?: '';
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_action('init', [$this, 'load_textdomain']);
|
||||
add_action('init', [$this, 'register_shortcodes']);
|
||||
add_filter('pre_update_option_walletpress_api_key', [__CLASS__, 'encrypt'], 10, 1);
|
||||
add_filter('option_walletpress_api_key', [__CLASS__, 'decrypt'], 10, 1);
|
||||
add_action('wp_enqueue_scripts', [$this, 'enqueue_frontend']);
|
||||
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin']);
|
||||
add_action('rest_api_init', [$this, 'register_routes']);
|
||||
add_filter('script_loader_tag', [$this, 'add_module_scripts'], 10, 3);
|
||||
|
||||
$this->api = new WalletPressAPI(
|
||||
get_option('walletpress_api_url', ''),
|
||||
get_option('walletpress_api_key', '')
|
||||
);
|
||||
|
||||
$this->modules = [
|
||||
'admin' => new WalletPressAdmin($this),
|
||||
'auth' => new WalletPressAuth($this),
|
||||
'gate' => new WalletPressGate($this),
|
||||
'payments' => new WalletPressPayments($this),
|
||||
];
|
||||
|
||||
do_action('walletpress_init', $this);
|
||||
}
|
||||
|
||||
public function api(): WalletPressAPI { return $this->api; }
|
||||
public function module(string $name): ?object { return $this->modules[$name] ?? null; }
|
||||
|
||||
public function load_textdomain(): void {
|
||||
load_plugin_textdomain('walletpress', false, dirname(plugin_basename(WALLETPRESS_FILE)) . '/languages');
|
||||
}
|
||||
|
||||
public function register_shortcodes(): void {
|
||||
add_shortcode('wallet_connect', [WalletPressAuth::class, 'render_connect_button']);
|
||||
add_shortcode('wallet_gate', [WalletPressGate::class, 'render_gate']);
|
||||
add_shortcode('wallet_pay', [WalletPressPayments::class, 'render_pay_button']);
|
||||
add_shortcode('wallet_profile', [$this, 'render_profile']);
|
||||
add_shortcode('wallet_generate', [$this, 'render_generate_shortcode']);
|
||||
}
|
||||
|
||||
public function render_generate_shortcode(): string {
|
||||
if (!current_user_can('manage_options')) return '';
|
||||
ob_start(); ?>
|
||||
<div class="walletpress-generate-shortcode">
|
||||
<form class="wp-generate-form">
|
||||
<select name="chain"><?php foreach ($this->supported_chains() as $k => $c): ?>
|
||||
<option value="<?php echo esc_attr($k); ?>"><?php echo esc_html($c['label']); ?></option>
|
||||
<?php endforeach; ?></select>
|
||||
<input type="text" name="label" placeholder="Label (optional)">
|
||||
<button type="submit" class="walletpress-btn walletpress-btn-pay">Generate</button>
|
||||
</form>
|
||||
<pre class="wp-gen-result" style="display:none;"></pre>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
public function register_routes(): void {
|
||||
$c = 'WalletPressAuth';
|
||||
$g = 'WalletPressGate';
|
||||
$p = 'WalletPressPayments';
|
||||
|
||||
register_rest_route('walletpress/v1', '/auth/nonce', ['methods' => 'GET', 'callback' => [$c, 'rest_nonce'], 'permission_callback' => '__return_true']);
|
||||
register_rest_route('walletpress/v1', '/auth/verify', ['methods' => 'POST', 'callback' => [$c, 'rest_verify'], 'permission_callback' => '__return_true']);
|
||||
register_rest_route('walletpress/v1', '/auth/me', ['methods' => 'GET', 'callback' => [$c, 'rest_me'], 'permission_callback' => '__return_true']);
|
||||
register_rest_route('walletpress/v1', '/auth/balance', ['methods' => 'GET', 'callback' => [$c, 'rest_balance'], 'permission_callback' => '__return_true']);
|
||||
|
||||
register_rest_route('walletpress/v1', '/generate', ['methods' => 'POST', 'callback' => [$this, 'rest_generate'], 'permission_callback' => function() { return current_user_can('manage_options'); }]);
|
||||
register_rest_route('walletpress/v1', '/vault', ['methods' => 'GET', 'callback' => [$this, 'rest_vault'], 'permission_callback' => function() { return current_user_can('manage_options'); }]);
|
||||
register_rest_route('walletpress/v1', '/verify-ownership', ['methods' => 'POST', 'callback' => [$g, 'rest_verify_ownership'], 'permission_callback' => '__return_true']);
|
||||
|
||||
register_rest_route('walletpress/v1', '/payments/create', ['methods' => 'POST', 'callback' => [$p, 'rest_create_payment'], 'permission_callback' => '__return_true']);
|
||||
register_rest_route('walletpress/v1', '/payments/verify', ['methods' => 'POST', 'callback' => [$p, 'rest_verify_payment'], 'permission_callback' => '__return_true']);
|
||||
}
|
||||
|
||||
public function rest_generate(WP_REST_Request $req): WP_REST_Response {
|
||||
$result = $this->api->generate_wallet(sanitize_text_field($req->get_param('chain')), sanitize_text_field($req->get_param('label')));
|
||||
return new WP_REST_Response($result ?: ['error' => 'Generation failed'], $result ? 200 : 502);
|
||||
}
|
||||
|
||||
public function rest_vault(): WP_REST_Response {
|
||||
$vault = $this->api->list_vault();
|
||||
return new WP_REST_Response($vault ?: ['error' => 'Failed to fetch vault'], $vault ? 200 : 502);
|
||||
}
|
||||
|
||||
public function enqueue_frontend(): void {
|
||||
wp_enqueue_script('walletpress', WALLETPRESS_URL . 'assets/js/walletpress.js', [], WALLETPRESS_VERSION, true);
|
||||
wp_enqueue_style('walletpress', WALLETPRESS_URL . 'assets/css/walletpress.css', [], WALLETPRESS_VERSION);
|
||||
$user = wp_get_current_user();
|
||||
wp_localize_script('walletpress', 'walletpress', [
|
||||
'rest' => esc_url_raw(rest_url('walletpress/v1')),
|
||||
'nonce' => wp_create_nonce('wp_rest'),
|
||||
'api_url' => get_option('walletpress_api_url', ''),
|
||||
'user' => $user->ID ? ['id' => $user->ID, 'display_name' => $user->display_name, 'wallet' => get_user_meta($user->ID, 'walletpress_address', true) ?: null] : null,
|
||||
'chains' => $this->supported_chains(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function enqueue_admin(string $hook): void {
|
||||
if (str_contains($hook, 'walletpress')) {
|
||||
wp_enqueue_style('walletpress-admin', WALLETPRESS_URL . 'assets/css/walletpress.css', [], WALLETPRESS_VERSION);
|
||||
wp_enqueue_script('walletpress-admin', WALLETPRESS_URL . 'assets/js/walletpress-admin.js', [], WALLETPRESS_VERSION, true);
|
||||
wp_localize_script('walletpress-admin', 'walletpress_admin', [
|
||||
'nonce' => wp_create_nonce('walletpress_admin'),
|
||||
'ajax' => admin_url('admin-ajax.php'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function add_module_scripts(string $tag, string $handle, string $src): string {
|
||||
return ($handle === 'walletpress') ? str_replace('<script', '<script type="module"', $tag) : $tag;
|
||||
}
|
||||
|
||||
public function supported_chains(): array {
|
||||
// Maps plugin-friendly names to backend chain keys.
|
||||
// Only chains the backend actually supports (verified in tests/test_address_vectors.py).
|
||||
// See ADDRESS_GENERATION.md for the full chain truth table.
|
||||
return [
|
||||
'btc' => ['label' => 'Bitcoin', 'icon' => 'btc', 'wallet' => '', 'backend' => 'btc'],
|
||||
'btc-segwit'=> ['label' => 'Bitcoin SegWit', 'icon' => 'btc', 'wallet' => '', 'backend' => 'btc-segwit'],
|
||||
'eth' => ['label' => 'Ethereum', 'icon' => 'eth', 'wallet' => 'metamask','backend' => 'eth'],
|
||||
'base' => ['label' => 'Base', 'icon' => 'base','wallet' => 'metamask','backend' => 'base'],
|
||||
'polygon' => ['label' => 'Polygon', 'icon' => 'matic','wallet'=> 'metamask','backend' => 'polygon'],
|
||||
'arbitrum' => ['label' => 'Arbitrum', 'icon' => 'arb', 'wallet' => 'metamask','backend' => 'arbitrum'],
|
||||
'optimism' => ['label' => 'Optimism', 'icon' => 'op', 'wallet' => 'metamask','backend' => 'optimism'],
|
||||
'avalanche' => ['label' => 'Avalanche', 'icon' => 'avax','wallet' => 'metamask','backend' => 'avalanche'],
|
||||
'bsc' => ['label' => 'BNB Chain', 'icon' => 'bnb', 'wallet' => 'metamask','backend' => 'bsc'],
|
||||
'fantom' => ['label' => 'Fantom', 'icon' => 'ftm', 'wallet' => 'metamask','backend' => 'fantom'],
|
||||
'gnosis' => ['label' => 'Gnosis', 'icon' => 'xdai','wallet' => 'metamask','backend' => 'gnosis'],
|
||||
'celo' => ['label' => 'Celo', 'icon' => 'celo','wallet' => 'metamask','backend' => 'celo'],
|
||||
'scroll' => ['label' => 'Scroll', 'icon' => 'scr', 'wallet' => 'metamask','backend' => 'scroll'],
|
||||
'zksync' => ['label' => 'zkSync Era', 'icon' => 'zk', 'wallet' => 'metamask','backend' => 'zksync'],
|
||||
'blast' => ['label' => 'Blast', 'icon' => 'blast','wallet'=> 'metamask','backend' => 'blast'],
|
||||
'mantle' => ['label' => 'Mantle', 'icon' => 'mnt', 'wallet' => 'metamask','backend' => 'mantle'],
|
||||
'linea' => ['label' => 'Linea', 'icon' => 'linea','wallet'=> 'metamask','backend' => 'linea'],
|
||||
'metis' => ['label' => 'Metis', 'icon' => 'metis','wallet'=> 'metamask','backend' => 'metis'],
|
||||
'opbnb' => ['label' => 'opBNB', 'icon' => 'bnb', 'wallet' => 'metamask','backend' => 'opbnb'],
|
||||
'core' => ['label' => 'Core Chain', 'icon' => 'core','wallet' => 'metamask','backend' => 'core'],
|
||||
'frax' => ['label' => 'Fraxchain', 'icon' => 'frax','wallet' => 'metamask','backend' => 'frax'],
|
||||
'kava' => ['label' => 'Kava', 'icon' => 'kava','wallet' => 'metamask','backend' => 'kava'],
|
||||
'moonbeam' => ['label' => 'Moonbeam', 'icon' => 'glmr','wallet' => 'metamask','backend' => 'moonbeam'],
|
||||
'cronos' => ['label' => 'Cronos', 'icon' => 'cro', 'wallet' => 'metamask','backend' => 'cronos'],
|
||||
'aurora' => ['label' => 'Aurora', 'icon' => 'aurora','wallet'=> 'metamask','backend' => 'aurora'],
|
||||
'harmony' => ['label' => 'Harmony', 'icon' => 'one', 'wallet' => 'metamask','backend' => 'harmony'],
|
||||
'boba' => ['label' => 'Boba Network', 'icon' => 'boba','wallet' => 'metamask','backend' => 'boba'],
|
||||
'evmos' => ['label' => 'Evmos', 'icon' => 'evmos','wallet'=> 'metamask','backend' => 'evmos'],
|
||||
'sol' => ['label' => 'Solana', 'icon' => 'sol', 'wallet' => 'phantom', 'backend' => 'sol'],
|
||||
'trx' => ['label' => 'TRON', 'icon' => 'trx', 'wallet' => '', 'backend' => 'trx'],
|
||||
'doge' => ['label' => 'Dogecoin', 'icon' => 'doge','wallet' => '', 'backend' => 'doge'],
|
||||
'ltc' => ['label' => 'Litecoin', 'icon' => 'ltc', 'wallet' => '', 'backend' => 'ltc'],
|
||||
'bch' => ['label' => 'Bitcoin Cash', 'icon' => 'bch', 'wallet' => '', 'backend' => 'bch'],
|
||||
'dash' => ['label' => 'Dash', 'icon' => 'dash','wallet' => '', 'backend' => 'dash'],
|
||||
'zec' => ['label' => 'Zcash', 'icon' => 'zec', 'wallet' => '', 'backend' => 'zec'],
|
||||
'atom' => ['label' => 'Cosmos Hub', 'icon' => 'atom','wallet' => '', 'backend' => 'atom'],
|
||||
'osmo' => ['label' => 'Osmosis', 'icon' => 'osmo','wallet' => '', 'backend' => 'osmo'],
|
||||
'inj' => ['label' => 'Injective', 'icon' => 'inj', 'wallet' => '', 'backend' => 'inj'],
|
||||
'algo' => ['label' => 'Algorand', 'icon' => 'algo','wallet' => '', 'backend' => 'algo'],
|
||||
'xlm' => ['label' => 'Stellar', 'icon' => 'xlm', 'wallet' => '', 'backend' => 'xlm'],
|
||||
'xrp' => ['label' => 'XRP', 'icon' => 'xrp', 'wallet' => '', 'backend' => 'xrp'],
|
||||
'dot' => ['label' => 'Polkadot', 'icon' => 'dot', 'wallet' => '', 'backend' => 'dot'],
|
||||
'ksm' => ['label' => 'Kusama', 'icon' => 'ksm', 'wallet' => '', 'backend' => 'ksm'],
|
||||
'ada' => ['label' => 'Cardano', 'icon' => 'ada', 'wallet' => '', 'backend' => 'ada'],
|
||||
'xmr' => ['label' => 'Monero', 'icon' => 'xmr', 'wallet' => '', 'backend' => 'xmr'],
|
||||
'xtz' => ['label' => 'Tezos', 'icon' => 'xtz', 'wallet' => '', 'backend' => 'xtz'],
|
||||
'fil' => ['label' => 'Filecoin', 'icon' => 'fil', 'wallet' => '', 'backend' => 'fil'],
|
||||
'near' => ['label' => 'NEAR', 'icon' => 'near','wallet' => '', 'backend' => 'near'],
|
||||
'sui' => ['label' => 'Sui', 'icon' => 'sui', 'wallet' => '', 'backend' => 'sui'],
|
||||
'apt' => ['label' => 'Aptos', 'icon' => 'apt', 'wallet' => '', 'backend' => 'apt'],
|
||||
'ton' => ['label' => 'TON', 'icon' => 'ton', 'wallet' => '', 'backend' => 'ton'],
|
||||
];
|
||||
}
|
||||
|
||||
public function render_profile(): string {
|
||||
if (!is_user_logged_in()) return do_shortcode('[wallet_connect]');
|
||||
$user = wp_get_current_user();
|
||||
$wallet = get_user_meta($user->ID, 'walletpress_address', true);
|
||||
ob_start(); ?>
|
||||
<div class="walletpress-profile">
|
||||
<h3><?php esc_html_e('Wallet Profile', 'walletpress'); ?></h3>
|
||||
<p><strong><?php esc_html_e('User:', 'walletpress'); ?></strong> <?php echo esc_html($user->display_name); ?></p>
|
||||
<?php if ($wallet): ?>
|
||||
<p><strong><?php esc_html_e('Wallet:', 'walletpress'); ?></strong> <code><?php echo esc_html(substr($wallet, 0, 6) . '...' . substr($wallet, -4)); ?></code></p>
|
||||
<button class="walletpress-disconnect button"><?php esc_html_e('Disconnect Wallet', 'walletpress'); ?></button>
|
||||
<?php else: echo do_shortcode('[wallet_connect]'); endif; ?>
|
||||
</div>
|
||||
<?php return ob_get_clean();
|
||||
}
|
||||
}
|
||||
107
wp-plugin/readme.txt
Normal file
107
wp-plugin/readme.txt
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
=== WalletPress ===
|
||||
Contributors: rugmunch
|
||||
Tags: crypto, wallet, blockchain, token-gate, web3, metamask, phantom, solana, ethereum, nft, defi
|
||||
Requires at least: 6.0
|
||||
Tested up to: 6.7
|
||||
Stable tag: 1.0.0-beta
|
||||
Requires PHP: 8.1
|
||||
License: GPLv2 or later
|
||||
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
|
||||
Self-hosted wallet management platform. Generate, vault, analyze, and gate content with crypto wallets across 55 blockchains.
|
||||
|
||||
== Description ==
|
||||
|
||||
WalletPress is a self-hosted wallet management platform that brings the full power of your multi-chain crypto backend into WordPress. It is a **developer product first** — you deploy the backend, configure the plugin, and unlock enterprise-grade wallet infrastructure with a clean admin UI.
|
||||
|
||||
= Features =
|
||||
|
||||
* **Wallet Vault** — Browse, export, and manage HD wallets across 55 chains (Solana, Ethereum, Base, Polygon, BSC, Arbitrum, Avalanche, Optimism, zkSync, Scroll, Blast, Mantle, Linea, opBNB, Manta, Polygon zkEVM, StarkNet, Sui, Aptos, Bitcoin, Litecoin, Dogecoin, Cosmos, Polkadot, Kusama, TRON, and more)
|
||||
* **Wallet Generation** — Generate single wallets, batch wallets, or full-chain sweeps with BIP39, BIP32, BIP44, BIP49, BIP84, BIP86 derivation
|
||||
* **Wallet Analysis** — Check balances, analyze transaction history, scan for risk, and verify signatures
|
||||
* **Mnemonic Converter** — Convert a BIP39 mnemonic phrase to addresses on all supported chains instantly
|
||||
* **Token Gating** — Lock any content behind token/NFT ownership with simple shortcodes `[wallet_gate id="1"]`
|
||||
* **Crypto Payments** — Accept SOL, ETH, MATIC, BNB, and ERC-20/BEP-20/SPL tokens. Users connect their wallet and pay with one click
|
||||
* **Wallet Login** — MetaMask EIP-1193 and Phantom wallet Connect button with nonce-based signature verification. Auto-creates WP users
|
||||
* **RBAC** — Role-based API key permissions with granular scopes
|
||||
* **API Keys** — Generate, list, rotate, and revoke API keys with scope-limited access
|
||||
* **Webhooks** — Create webhooks for wallet.generated, wallet.swept, payment.received, gate.verified events
|
||||
* **Audit Trail** — Full audit log of all wallet operations
|
||||
* **Balance Snapshots** — Periodic balance tracking with history viewer
|
||||
* **Sweep & Rotate** — Sweep funds to a destination address, rotate wallet keys, or create temporal (TTL-expiring) wallets
|
||||
* **Paper Wallets** — Generate paper wallet PDFs with QR codes
|
||||
* **Admin Dashboard** — 15 admin pages covering every feature
|
||||
|
||||
= Supported Chains =
|
||||
|
||||
Solana, Ethereum, Base, Polygon, BSC, Arbitrum, Avalanche, Optimism, zkSync, Scroll, Blast, Mantle, Linea, opBNB, Manta, Polygon zkEVM, StarkNet, Sui, Aptos, Neon, Near, Aurora, Celo, Moonbeam, Moonriver, Fantom, Cronos, Gnosis, Telos
|
||||
|
||||
== Installation ==
|
||||
|
||||
1. Upload the `walletpress` folder to `/wp-content/plugins/`
|
||||
2. Activate the plugin through the Plugins screen
|
||||
3. Go to **WalletPress → Setup** in your WP admin
|
||||
4. Enter your backend API URL and API key
|
||||
5. Configure your merchant wallet address and default chain
|
||||
6. Start generating wallets, creating token gates, and accepting payments
|
||||
|
||||
= Backend Requirements =
|
||||
|
||||
WalletPress requires a running instance of the `rmi-backend` API. See the Deployment Guide at `docs/DEPLOYMENT.md` for setup instructions. The backend provides:
|
||||
|
||||
* REST API at `/api/v1/chain-vault` and `/api/v1/wallet`
|
||||
* 86+ endpoints covering all wallet operations
|
||||
* Postgres + Redis + ClickHouse storage
|
||||
* Optional: Reth (archive node), Neo4j (entity graphs), Qdrant (vector memory)
|
||||
|
||||
== Frequently Asked Questions ==
|
||||
|
||||
= Do I need a third-party service? =
|
||||
|
||||
No. WalletPress is self-hosted. You deploy the backend on your own infrastructure. No monthly SaaS fees, no API usage limits.
|
||||
|
||||
= How do users connect their wallets? =
|
||||
|
||||
Frontend includes MetaMask (EIP-1193) and Phantom connectors. The Connect button generates a nonce, the user signs it with their wallet, and the server verifies the signature. This creates/authenticates a WP user account.
|
||||
|
||||
= What happens to existing users? =
|
||||
|
||||
Existing WP users can link a wallet address in their profile. Wallet-authenticated users get auto-registered as WP users with the role you configure.
|
||||
|
||||
= Can I sell access to content? =
|
||||
|
||||
Yes. The Token Gating shortcode locks content behind token/NFT ownership. Combined with Crypto Payments, users can purchase access and automatically unlock gated content.
|
||||
|
||||
= Is the plugin compatible with other page builders? =
|
||||
|
||||
Yes. The `[wallet_gate]` shortcode works in Classic Editor, Gutenberg, Elementor, WPBakery, and any builder that supports shortcodes.
|
||||
|
||||
== Screenshots ==
|
||||
|
||||
1. WalletPress Admin Dashboard — overview of vault, gates, payments, and system health
|
||||
2. Wallet Vault — browse all wallets across chains with search and filter
|
||||
3. Wallet Generation — generate single or batch wallets with label/chain options
|
||||
4. Token Gates — create and manage token/NFT gating rules
|
||||
5. Crypto Payments — configure payment form and view transaction history
|
||||
6. Settings — API connection, merchant wallet, chain defaults
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 1.0.0-beta =
|
||||
* Initial beta release
|
||||
* Wallet vault with 55 chains
|
||||
* Wallet generation (single, batch, full sweep)
|
||||
* Mnemonic to addresses converter
|
||||
* Token gating with shortcode
|
||||
* Crypto payments with wallet connection
|
||||
* MetaMask/Phantom wallet login
|
||||
* API key management with scopes
|
||||
* Webhook creation and management
|
||||
* Audit trail viewer
|
||||
* Balance snapshots and history
|
||||
* 15 admin pages
|
||||
|
||||
== Upgrade Notice ==
|
||||
|
||||
= 1.0.0-beta =
|
||||
First beta release. Test thoroughly before production use.
|
||||
38
wp-plugin/walletpress.php
Normal file
38
wp-plugin/walletpress.php
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
/**
|
||||
* Plugin Name: WalletPress
|
||||
* Plugin URI: https://walletpress.cc
|
||||
* Description: Self-hosted wallet management platform. Generate, vault, analyze, distribute, and gate with crypto wallets across 55 chains. REST API + WP Admin + CLI.
|
||||
* Version: 1.0.0-beta
|
||||
* Author: Rug Munch Media LLC
|
||||
* Author URI: https://rugmunch.io
|
||||
* License: GPL v2 or later
|
||||
* Text Domain: walletpress
|
||||
* Domain Path: /languages
|
||||
* Requires PHP: 8.1
|
||||
* Requires WP: 6.0
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
define('WALLETPRESS_VERSION', '1.0.0-beta');
|
||||
define('WALLETPRESS_FILE', __FILE__);
|
||||
define('WALLETPRESS_DIR', plugin_dir_path(__FILE__));
|
||||
define('WALLETPRESS_URL', plugin_dir_url(__FILE__));
|
||||
|
||||
require_once WALLETPRESS_DIR . 'includes/class-walletpress.php';
|
||||
require_once WALLETPRESS_DIR . 'includes/class-walletpress-admin.php';
|
||||
require_once WALLETPRESS_DIR . 'includes/class-walletpress-api.php';
|
||||
require_once WALLETPRESS_DIR . 'includes/class-walletpress-auth.php';
|
||||
require_once WALLETPRESS_DIR . 'includes/class-walletpress-gate.php';
|
||||
require_once WALLETPRESS_DIR . 'includes/class-walletpress-payments.php';
|
||||
|
||||
function walletpress(): WalletPress {
|
||||
static $instance = null;
|
||||
if ($instance === null) {
|
||||
$instance = new WalletPress();
|
||||
}
|
||||
return $instance;
|
||||
}
|
||||
|
||||
walletpress()->init();
|
||||
Loading…
Add table
Add a link
Reference in a new issue