mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-07-22 13:45:47 +00:00
Add multisig pending-operations UI, fix broken multisig address calc, new signing portal
- calculateMultisigAddress was completely broken (hex-decoded an SS58 string and never hashed the preimage) - fixed via @pezkuwi/util-crypto's real encodeMultiAddress/createKeyMulti, verified against the actual known multisig address on-chain. - ReservesDashboardPage had stale Noter/Berdevk addresses that don't match the real signers - centralized as BRIDGE_MULTISIG_SPECIFIC_ADDRESSES. - USDTBridge withdrawal called assets.burn directly as a single-signer extrinsic (always fails - only the multisig is Admin) while only checking status.isFinalized (a failed dispatch is still finalized, so it silently did nothing) - replaced with the correct transfer-to-custody flow the relayer actually watches for. - New MultisigOperationsPage (/multisig/pending) lists pending calls from real Multisig.Multisigs storage and lets any of the 5 signers approve/reject with their own wallet extension. - New standalone sign/ app (deployed separately at pezbridge-sign.pex.mom) - a dedicated, gated signing portal for the same operations, so signing isn't dependent on this app alone.
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<title>PezBridge Sign</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+4251
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "pezbridge-sign",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "Dedicated, minimal-surface signing portal for PezkuwiChain multisig operations - gated to known signatory addresses only.",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"@pezkuwi/api": "^16.5.36",
|
||||
"@pezkuwi/extension-dapp": "^0.62.20",
|
||||
"@pezkuwi/extension-inject": "^0.62.20",
|
||||
"@pezkuwi/keyring": "^14.0.25",
|
||||
"@pezkuwi/util": "^14.0.25",
|
||||
"@pezkuwi/util-crypto": "^14.0.25"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.10",
|
||||
"vite-plugin-node-polyfills": "^0.22.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { web3Accounts } from '@pezkuwi/extension-dapp';
|
||||
import type { InjectedAccountWithMeta } from '@pezkuwi/extension-inject/types';
|
||||
import type { ApiPromise } from '@pezkuwi/api';
|
||||
import {
|
||||
getMultisigMembers,
|
||||
calculateMultisigAddress,
|
||||
approveMultisigTx,
|
||||
cancelMultisigTx,
|
||||
isMultisigMember,
|
||||
USDT_MULTISIG_CONFIG,
|
||||
} from '@pezkuwi/lib/multisig';
|
||||
import {
|
||||
listPendingOperations,
|
||||
decodeAndVerifyCallData,
|
||||
type PendingOperation,
|
||||
} from '@pezkuwi/lib/multisig-operations';
|
||||
import { connectApi } from './lib/chain';
|
||||
import { getSigner } from './lib/get-signer';
|
||||
import { SIGNER_SETS, type SignerSet } from './signer-sets';
|
||||
|
||||
const PEX_MOM_URL = 'https://pex.mom';
|
||||
|
||||
interface OperationsPanelProps {
|
||||
api: ApiPromise;
|
||||
account: InjectedAccountWithMeta;
|
||||
signerSet: SignerSet;
|
||||
}
|
||||
|
||||
function OperationsPanel({ api, account, signerSet }: OperationsPanelProps) {
|
||||
const [operations, setOperations] = useState<PendingOperation[]>([]);
|
||||
const [otherSignatories, setOtherSignatories] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [processingHash, setProcessingHash] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
const [manualCallData, setManualCallData] = useState<Record<string, string>>({});
|
||||
const [decodeErrors, setDecodeErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const allMembers = await getMultisigMembers(api, signerSet.specificAddresses);
|
||||
const multisigAddr = calculateMultisigAddress(allMembers);
|
||||
setOtherSignatories(allMembers.filter((addr) => addr !== account.address));
|
||||
const ops = await listPendingOperations(api, multisigAddr);
|
||||
setOperations(ops);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load pending operations');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [api, account.address, signerSet]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
const handleDecode = (callHash: string) => {
|
||||
const hex = manualCallData[callHash];
|
||||
if (!hex) return;
|
||||
try {
|
||||
const { call, description } = decodeAndVerifyCallData(api, hex, callHash);
|
||||
setOperations((prev) =>
|
||||
prev.map((op) => (op.callHash === callHash ? { ...op, resolvedCall: call, description, isAutoMatched: false } : op))
|
||||
);
|
||||
setDecodeErrors((prev) => ({ ...prev, [callHash]: '' }));
|
||||
} catch (err) {
|
||||
setDecodeErrors((prev) => ({ ...prev, [callHash]: err instanceof Error ? err.message : 'Decode failed' }));
|
||||
}
|
||||
};
|
||||
|
||||
const runExtrinsic = async (
|
||||
callHash: string,
|
||||
tx: ReturnType<typeof approveMultisigTx> | ReturnType<typeof cancelMultisigTx>,
|
||||
successMessage: string
|
||||
) => {
|
||||
setProcessingHash(callHash);
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
try {
|
||||
const injector = await getSigner(account.address);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
tx.signAndSend(account.address, { signer: injector.signer }, ({ status, dispatchError }) => {
|
||||
if (status.isInBlock || status.isFinalized) {
|
||||
if (dispatchError) {
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
||||
reject(new Error(`${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`));
|
||||
} else {
|
||||
reject(new Error(dispatchError.toString()));
|
||||
}
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
}).catch(reject);
|
||||
});
|
||||
setSuccess(successMessage);
|
||||
await load();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Transaction failed');
|
||||
} finally {
|
||||
setProcessingHash(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleApprove = (op: PendingOperation) => {
|
||||
if (!op.resolvedCall) return;
|
||||
const tx = approveMultisigTx(api, op.resolvedCall, otherSignatories, op.when, op.threshold);
|
||||
runExtrinsic(op.callHash, tx, 'Approval submitted successfully.');
|
||||
};
|
||||
|
||||
const handleReject = (op: PendingOperation) => {
|
||||
const tx = cancelMultisigTx(api, op.callHash, otherSignatories, op.when, op.threshold);
|
||||
runExtrinsic(op.callHash, tx, 'Operation cancelled successfully.');
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="panel">
|
||||
<div className="panel-header">
|
||||
<h2>{signerSet.name}</h2>
|
||||
<button className="btn-outline" onClick={load} disabled={loading}>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && <div className="alert alert-error">{error}</div>}
|
||||
{success && <div className="alert alert-success">{success}</div>}
|
||||
|
||||
{loading ? (
|
||||
<div className="loading">Loading pending operations…</div>
|
||||
) : operations.length === 0 ? (
|
||||
<div className="empty">No pending operations right now.</div>
|
||||
) : (
|
||||
<div className="ops-list">
|
||||
{operations.map((op) => {
|
||||
const alreadyApproved = op.approvals.includes(account.address);
|
||||
const canReject = op.depositor === account.address;
|
||||
const isProcessing = processingHash === op.callHash;
|
||||
|
||||
return (
|
||||
<div key={op.callHash} className="op-card">
|
||||
<div className="op-header">
|
||||
<div>
|
||||
<p className="op-desc">{op.description}</p>
|
||||
<code className="op-hash">
|
||||
{op.callHash.slice(0, 10)}…{op.callHash.slice(-8)}
|
||||
</code>
|
||||
</div>
|
||||
<span className={`badge ${op.approvalsCount >= op.threshold ? 'badge-ready' : ''}`}>
|
||||
{op.approvalsCount}/{op.threshold} approvals
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="op-meta">
|
||||
Proposed by <code>{op.depositor.slice(0, 8)}…{op.depositor.slice(-6)}</code>
|
||||
</p>
|
||||
{alreadyApproved && <p className="op-approved">You already approved this operation</p>}
|
||||
|
||||
{!op.resolvedCall && (
|
||||
<div className="decode-box">
|
||||
<p className="hint">This call couldn't be auto-identified from its hash alone - paste the known call data to decode and verify it.</p>
|
||||
<textarea
|
||||
placeholder="Paste call data (0x...)"
|
||||
value={manualCallData[op.callHash] ?? ''}
|
||||
onChange={(e) => setManualCallData((prev) => ({ ...prev, [op.callHash]: e.target.value }))}
|
||||
rows={2}
|
||||
/>
|
||||
{decodeErrors[op.callHash] && <p className="error-text">{decodeErrors[op.callHash]}</p>}
|
||||
<button className="btn-outline" onClick={() => handleDecode(op.callHash)} disabled={!manualCallData[op.callHash]}>
|
||||
Decode
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="op-actions">
|
||||
<button
|
||||
className="btn-primary"
|
||||
onClick={() => handleApprove(op)}
|
||||
disabled={!op.resolvedCall || alreadyApproved || isProcessing}
|
||||
>
|
||||
{isProcessing ? 'Processing…' : 'Approve'}
|
||||
</button>
|
||||
{canReject && (
|
||||
<button className="btn-danger" onClick={() => handleReject(op)} disabled={isProcessing}>
|
||||
Reject
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="threshold-note">
|
||||
{USDT_MULTISIG_CONFIG.threshold} of {USDT_MULTISIG_CONFIG.members.length} signatures are required to execute any
|
||||
operation - no single signer, including this site, can move funds alone.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [api, setApi] = useState<ApiPromise | null>(null);
|
||||
const [apiReady, setApiReady] = useState(false);
|
||||
const [account, setAccount] = useState<InjectedAccountWithMeta | null>(null);
|
||||
const [accounts, setAccounts] = useState<InjectedAccountWithMeta[]>([]);
|
||||
const [authorizedSets, setAuthorizedSets] = useState<SignerSet[] | null>(null); // null = not checked yet
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
const [connectError, setConnectError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
connectApi()
|
||||
.then((instance) => {
|
||||
setApi(instance);
|
||||
setApiReady(true);
|
||||
})
|
||||
.catch((err) => setConnectError(err instanceof Error ? err.message : 'Failed to connect to chain'));
|
||||
}, []);
|
||||
|
||||
const handleConnect = useCallback(async () => {
|
||||
setConnecting(true);
|
||||
setConnectError(null);
|
||||
try {
|
||||
const accs = await web3Accounts();
|
||||
if (accs.length === 0) {
|
||||
setConnectError(
|
||||
'No accounts found. Install a Pezkuwi/Substrate wallet extension, unlock it, and authorize this site.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
setAccounts(accs);
|
||||
setAccount(accs[0]);
|
||||
} catch (err) {
|
||||
setConnectError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setConnecting(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Gate: once a wallet is connected, check membership across every known signer set. Anyone
|
||||
// not authorized for at least one is bounced to pex.mom - this page has nothing to show them.
|
||||
useEffect(() => {
|
||||
if (!api || !apiReady || !account) return;
|
||||
let cancelled = false;
|
||||
|
||||
Promise.all(SIGNER_SETS.map((set) => isMultisigMember(api, account.address, set.specificAddresses))).then(
|
||||
(results) => {
|
||||
if (cancelled) return;
|
||||
const matched = SIGNER_SETS.filter((_, i) => results[i]);
|
||||
if (matched.length === 0) {
|
||||
window.location.href = PEX_MOM_URL;
|
||||
return;
|
||||
}
|
||||
setAuthorizedSets(matched);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [api, apiReady, account]);
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<header className="app-header">
|
||||
<h1>PezBridge Sign</h1>
|
||||
<p className="subtitle">Signing portal for PezkuwiChain multisig operations</p>
|
||||
</header>
|
||||
|
||||
{connectError && <div className="alert alert-error">{connectError}</div>}
|
||||
|
||||
{!account ? (
|
||||
<div className="connect-box">
|
||||
<p>Connect the wallet extension holding one of your authorized signer accounts.</p>
|
||||
<button className="btn-primary" onClick={handleConnect} disabled={connecting || !apiReady}>
|
||||
{connecting ? 'Connecting…' : !apiReady ? 'Connecting to chain…' : 'Connect Wallet'}
|
||||
</button>
|
||||
</div>
|
||||
) : authorizedSets === null ? (
|
||||
<div className="loading">Verifying signer status on-chain…</div>
|
||||
) : (
|
||||
<>
|
||||
{accounts.length > 1 && (
|
||||
<div className="account-switcher">
|
||||
<label htmlFor="account-select">Account:</label>
|
||||
<select
|
||||
id="account-select"
|
||||
value={account.address}
|
||||
onChange={(e) => {
|
||||
const next = accounts.find((a) => a.address === e.target.value) ?? null;
|
||||
setAccount(next);
|
||||
setAuthorizedSets(null);
|
||||
}}
|
||||
>
|
||||
{accounts.map((a) => (
|
||||
<option key={a.address} value={a.address}>
|
||||
{a.meta.name ?? a.address}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{api &&
|
||||
authorizedSets.map((set) => <OperationsPanel key={set.name} api={api} account={account} signerSet={set} />)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: #0a0e14;
|
||||
color: #e6e9ef;
|
||||
}
|
||||
|
||||
.app {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 20px;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.app-header h1 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #8b93a3;
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.connect-box {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
background: #131a24;
|
||||
border: 1px solid #232c3a;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.connect-box p {
|
||||
color: #8b93a3;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.account-switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 0.85rem;
|
||||
color: #8b93a3;
|
||||
}
|
||||
|
||||
.account-switcher select {
|
||||
background: #131a24;
|
||||
color: #e6e9ef;
|
||||
border: 1px solid #232c3a;
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #131a24;
|
||||
border: 1px solid #232c3a;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.panel-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #8b93a3;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.ops-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.op-card {
|
||||
background: #0f1520;
|
||||
border: 1px solid #232c3a;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.op-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.op-desc {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.op-hash {
|
||||
font-size: 0.75rem;
|
||||
color: #6b7385;
|
||||
}
|
||||
|
||||
.op-meta {
|
||||
font-size: 0.8rem;
|
||||
color: #8b93a3;
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
|
||||
.op-approved {
|
||||
font-size: 0.8rem;
|
||||
color: #4ade80;
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 0.75rem;
|
||||
border: 1px solid #3a4557;
|
||||
border-radius: 999px;
|
||||
padding: 2px 10px;
|
||||
white-space: nowrap;
|
||||
color: #8b93a3;
|
||||
}
|
||||
|
||||
.badge-ready {
|
||||
border-color: #4ade80;
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.decode-box {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.decode-box textarea {
|
||||
width: 100%;
|
||||
background: #0a0e14;
|
||||
color: #e6e9ef;
|
||||
border: 1px solid #232c3a;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 0.75rem;
|
||||
color: #e0b04a;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 0.75rem;
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.op-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.threshold-note {
|
||||
margin-top: 16px;
|
||||
font-size: 0.75rem;
|
||||
color: #6b7385;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-outline,
|
||||
.btn-danger {
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #4ade80;
|
||||
color: #0a0e14;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-primary:disabled,
|
||||
.btn-outline:disabled,
|
||||
.btn-danger:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
color: #e6e9ef;
|
||||
border-color: #3a4557;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #f87171;
|
||||
color: #1a0a0a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: rgba(248, 113, 113, 0.1);
|
||||
border: 1px solid rgba(248, 113, 113, 0.4);
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: rgba(74, 222, 128, 0.1);
|
||||
border: 1px solid rgba(74, 222, 128, 0.4);
|
||||
color: #4ade80;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ApiPromise, WsProvider } from '@pezkuwi/api';
|
||||
|
||||
// Same endpoint the rest of this ecosystem's tooling (pwap-web, PezbridgeBot) uses for Pezkuwi
|
||||
// Asset Hub - the chain this multisig lives on.
|
||||
const RPC_ENDPOINT = 'wss://asset-hub-rpc.pezkuwichain.io';
|
||||
|
||||
export function connectApi(): Promise<ApiPromise> {
|
||||
const provider = new WsProvider(RPC_ENDPOINT);
|
||||
return ApiPromise.create({ provider });
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Extension-only signer helper - deliberately simpler than pwap-web's get-signer.ts (which also
|
||||
* supports WalletConnect for mobile). The real 5 multisig signatories operate from desktop/
|
||||
* server contexts, and this site's whole purpose is to be a minimal, easy-to-audit surface -
|
||||
* not pulling in the WalletConnect session-management stack keeps that true. Add it back here
|
||||
* only if a signer genuinely needs to sign from a phone.
|
||||
*/
|
||||
import { web3Enable, web3FromAddress } from '@pezkuwi/extension-dapp';
|
||||
|
||||
export async function getSigner(address: string) {
|
||||
await web3Enable('PezBridge Sign');
|
||||
return web3FromAddress(address);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Production Rollup alias for vite-plugin-node-polyfills/shims/process (mirrors web/'s copy -
|
||||
// see its comment for why: Rollup can't resolve the plugin's virtual module during a real
|
||||
// build, only in dev). IMPORTANT: must not reference the `process` identifier at runtime -
|
||||
// vite-plugin-node-polyfills rewrites it to `__process_polyfill`, creating a circular TDZ. Use
|
||||
// bracket notation so the plugin leaves this file alone.
|
||||
const g: Record<string, unknown> =
|
||||
typeof globalThis !== 'undefined' ? (globalThis as Record<string, unknown>)
|
||||
: typeof window !== 'undefined' ? (window as unknown as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export default (g['process'] ?? { env: {} }) as any;
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
import { BRIDGE_MULTISIG_SPECIFIC_ADDRESSES } from '@pezkuwi/lib/multisig';
|
||||
|
||||
export interface SignerSet {
|
||||
name: string;
|
||||
specificAddresses: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every known multisig this site gates access for. The connected wallet is authorized if it's
|
||||
* a member of ANY set below - add new entries here as new signing needs come up (this is the
|
||||
* single registry the login gate checks against, per the design decision to keep one dedicated
|
||||
* portal for all future signing needs rather than a bespoke UI per multisig).
|
||||
*/
|
||||
export const SIGNER_SETS: SignerSet[] = [
|
||||
{ name: 'USDT Bridge Treasury', specificAddresses: BRIDGE_MULTISIG_SPECIFIC_ADDRESSES },
|
||||
];
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
"strict": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noImplicitAny": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@pezkuwi/lib": ["../shared/lib"],
|
||||
"@pezkuwi/lib/*": ["../shared/lib/*"]
|
||||
},
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react-swc';
|
||||
import path from 'path';
|
||||
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
||||
|
||||
// Deliberately minimal compared to web/vite.config.ts: this app has exactly one job (gate +
|
||||
// sign multisig operations), so it carries none of web/'s SPA routing, i18n, or UI-kit
|
||||
// machinery - fewer dependencies is itself a security property for a signing-critical site.
|
||||
export default defineConfig(({ command }) => ({
|
||||
server: {
|
||||
host: '::',
|
||||
port: 8090,
|
||||
},
|
||||
plugins: [
|
||||
react(),
|
||||
nodePolyfills({
|
||||
globals: { Buffer: true, global: true, process: true },
|
||||
protocolImports: true,
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
mainFields: ['browser', 'module', 'main', 'exports'],
|
||||
alias: {
|
||||
// Rollup cannot resolve the plugin's virtual shim module in production - alias to a real
|
||||
// file (mirrors web/vite.config.ts's identical workaround). Dev mode leaves the plugin's
|
||||
// own virtual module handling it.
|
||||
...(command === 'build'
|
||||
? { 'vite-plugin-node-polyfills/shims/process': path.resolve(__dirname, './src/lib/process-shim.ts') }
|
||||
: {}),
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@pezkuwi/lib': path.resolve(__dirname, '../shared/lib'),
|
||||
},
|
||||
dedupe: ['react', '@pezkuwi/util-crypto', '@pezkuwi/util', '@pezkuwi/api', '@pezkuwi/extension-dapp', '@pezkuwi/keyring'],
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['@pezkuwi/util-crypto', '@pezkuwi/util', '@pezkuwi/api', '@pezkuwi/extension-dapp', '@pezkuwi/keyring', 'buffer'],
|
||||
},
|
||||
build: {
|
||||
chunkSizeWarningLimit: 600,
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user