fix treasury signer withdrawal workflow
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
DATABASE_URL=postgresql://solace_user:[email protected]:5432/solace_treasury
|
||||
|
||||
# Chain 138 Configuration
|
||||
RPC_URL=http://192.168.11.235:8545
|
||||
RPC_URL=http://192.168.11.211:8545
|
||||
CHAIN_ID=138
|
||||
CONTRACT_ADDRESS=0x96bF8cd30C4f0e22fa33469f8e2C23AA3faF525C
|
||||
|
||||
|
||||
@@ -12,18 +12,18 @@
|
||||
"indexer:start": "tsx src/indexer/indexer.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@trpc/server": "^10.45.0",
|
||||
"@trpc/client": "^10.45.0",
|
||||
"@trpc/server": "^10.45.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"drizzle-orm": "^0.29.0",
|
||||
"postgres": "^3.4.0",
|
||||
"viem": "^2.0.0",
|
||||
"zod": "^3.22.4",
|
||||
"dotenv": "^16.3.1"
|
||||
"viem": "^2.56.5",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"drizzle-kit": "^0.20.0",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "5.3.3"
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
+52
-16
@@ -7,6 +7,7 @@ import { treasuryRouter } from "./api/treasury";
|
||||
import { exportRouter } from "./api/exports";
|
||||
import { fetchTreasuryLedger } from "./api/ledger";
|
||||
import { ensureTreasury } from "./db/bootstrap";
|
||||
import { createPublicClient, http as viemHttp, defineChain, isAddress, verifyMessage } from "viem";
|
||||
|
||||
const port = Number(process.env.PORT || 3001);
|
||||
const nodeEnv = process.env.NODE_ENV || "development";
|
||||
@@ -17,6 +18,12 @@ const clientRpcUrl =
|
||||
"https://rpc-http-pub.d-bis.org";
|
||||
const chainId = process.env.CHAIN_ID || "138";
|
||||
const contractAddress = process.env.CONTRACT_ADDRESS || "";
|
||||
const allowedOrigin = process.env.CORS_ORIGIN || "https://treasury.d-bis.org";
|
||||
const chain138 = defineChain({ id: 138, name: "Solace Chain 138", nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, rpcUrls: { default: { http: [rpcUrl] } } });
|
||||
const publicClient = createPublicClient({ chain: chain138, transport: viemHttp(rpcUrl) });
|
||||
const ownerAbi = [{ type: "function", name: "isOwner", stateMutability: "view", inputs: [{ name: "owner", type: "address" }], outputs: [{ type: "bool" }] }] as const;
|
||||
const requestWindows = new Map<string, { started: number; count: number }>();
|
||||
const API_RATE_LIMIT = 120;
|
||||
|
||||
for (const envVar of ["DATABASE_URL", "RPC_URL", "CHAIN_ID"] as const) {
|
||||
if (!process.env[envVar]) {
|
||||
@@ -55,11 +62,11 @@ async function checkRpc(): Promise<boolean> {
|
||||
|
||||
function sendJson(res: http.ServerResponse, status: number, body: unknown) {
|
||||
const payload = JSON.stringify(body);
|
||||
res.writeHead(status, {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
res.writeHead(status, {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": allowedOrigin,
|
||||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type",
|
||||
"Access-Control-Allow-Headers": "Content-Type, X-Wallet-Address, X-Wallet-Message, X-Wallet-Signature",
|
||||
});
|
||||
res.end(payload);
|
||||
}
|
||||
@@ -79,11 +86,11 @@ function sendText(
|
||||
body: string,
|
||||
contentType: string
|
||||
) {
|
||||
res.writeHead(status, {
|
||||
"Content-Type": contentType,
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
res.writeHead(status, {
|
||||
"Content-Type": contentType,
|
||||
"Access-Control-Allow-Origin": allowedOrigin,
|
||||
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type",
|
||||
"Access-Control-Allow-Headers": "Content-Type, X-Wallet-Address, X-Wallet-Message, X-Wallet-Signature",
|
||||
});
|
||||
res.end(body);
|
||||
}
|
||||
@@ -91,9 +98,9 @@ function sendText(
|
||||
const server = http.createServer(async (req, res) => {
|
||||
if (req.method === "OPTIONS") {
|
||||
res.writeHead(204, {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Origin": allowedOrigin,
|
||||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type",
|
||||
"Access-Control-Allow-Headers": "Content-Type, X-Wallet-Address, X-Wallet-Message, X-Wallet-Signature",
|
||||
});
|
||||
res.end();
|
||||
return;
|
||||
@@ -101,6 +108,14 @@ const server = http.createServer(async (req, res) => {
|
||||
|
||||
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
||||
|
||||
if (url.pathname.startsWith("/api/")) {
|
||||
const key = req.socket.remoteAddress || "unknown";
|
||||
const now = Date.now();
|
||||
const window = requestWindows.get(key);
|
||||
if (!window || now - window.started >= 60_000) requestWindows.set(key, { started: now, count: 1 });
|
||||
else if (++window.count > API_RATE_LIMIT) { res.writeHead(429, { "Retry-After": "60" }); res.end(JSON.stringify({ error: "Rate limit exceeded" })); return; }
|
||||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
req.method === "GET" &&
|
||||
@@ -111,8 +126,6 @@ const server = http.createServer(async (req, res) => {
|
||||
status: dbOk && rpcOk ? "ok" : "degraded",
|
||||
environment: nodeEnv,
|
||||
chainId: Number(chainId),
|
||||
rpcUrl,
|
||||
clientRpcUrl,
|
||||
contractAddress: contractAddress || null,
|
||||
database: dbOk,
|
||||
rpc: rpcOk,
|
||||
@@ -129,12 +142,31 @@ const server = http.createServer(async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
async function authorizedTreasury(treasuryId: string) {
|
||||
const wallet = req.headers["x-wallet-address"];
|
||||
const signature = req.headers["x-wallet-signature"];
|
||||
const message = req.headers["x-wallet-message"];
|
||||
const rows = await treasuryRouter.getById(treasuryId);
|
||||
const treasury = rows[0];
|
||||
if (!treasury || typeof wallet !== "string" || typeof signature !== "string" || typeof message !== "string" || !isAddress(wallet) || !contractAddress || treasury.mainWallet !== contractAddress.toLowerCase()) { console.warn(JSON.stringify({ event: "treasury_authorization_denied", treasuryId, ip: req.socket.remoteAddress })); return null; }
|
||||
const timestamp = Number(message.match(/\nTimestamp: (\d+)$/)?.[1]);
|
||||
if (!Number.isFinite(timestamp) || Math.abs(Date.now() - timestamp) > 300000) return null;
|
||||
if (!await verifyMessage({ address: wallet, message, signature: signature as `0x${string}` })) return null;
|
||||
const owner = await publicClient.readContract({ address: contractAddress as `0x${string}`, abi: ownerAbi, functionName: "isOwner", args: [wallet] });
|
||||
return owner ? treasury : null;
|
||||
}
|
||||
|
||||
if (req.method === "GET" && url.pathname === "/api/ledger") {
|
||||
const wallet = url.searchParams.get("wallet");
|
||||
if (!wallet) {
|
||||
sendJson(res, 400, { error: "wallet query parameter required" });
|
||||
return;
|
||||
}
|
||||
if (!isAddress(wallet)) { sendJson(res, 400, { error: "valid wallet address required" }); return; }
|
||||
if (typeof req.headers["x-wallet-address"] !== "string" || req.headers["x-wallet-address"]!.toLowerCase() !== wallet.toLowerCase() || typeof req.headers["x-wallet-signature"] !== "string" || typeof req.headers["x-wallet-message"] !== "string") {
|
||||
sendJson(res, 403, { error: "Wallet authorization required" });
|
||||
return;
|
||||
}
|
||||
const ledger = await fetchTreasuryLedger(wallet);
|
||||
sendJson(res, 200, { ledger });
|
||||
return;
|
||||
@@ -146,6 +178,7 @@ const server = http.createServer(async (req, res) => {
|
||||
sendJson(res, 400, { error: "wallet query parameter required" });
|
||||
return;
|
||||
}
|
||||
if (!isAddress(wallet)) { sendJson(res, 400, { error: "valid wallet address required" }); return; }
|
||||
let treasury = await treasuryRouter.getByWallet(wallet);
|
||||
if (!treasury && contractAddress.toLowerCase() === wallet.toLowerCase()) {
|
||||
const ensured = await ensureTreasury();
|
||||
@@ -158,10 +191,9 @@ const server = http.createServer(async (req, res) => {
|
||||
const ledgerMatch = url.pathname.match(/^\/api\/treasury\/([^/]+)\/ledger$/);
|
||||
if (req.method === "GET" && ledgerMatch) {
|
||||
const treasuryId = ledgerMatch[1];
|
||||
const treasuryRows = await treasuryRouter.getById(treasuryId);
|
||||
const treasury = treasuryRows[0];
|
||||
const treasury = await authorizedTreasury(treasuryId);
|
||||
if (!treasury) {
|
||||
sendJson(res, 404, { error: "Treasury not found" });
|
||||
sendJson(res, 403, { error: "Treasury authorization required" });
|
||||
return;
|
||||
}
|
||||
const ledger = await fetchTreasuryLedger(treasury.mainWallet);
|
||||
@@ -172,6 +204,7 @@ const server = http.createServer(async (req, res) => {
|
||||
const subAccountsMatch = url.pathname.match(/^\/api\/treasury\/([^/]+)\/sub-accounts$/);
|
||||
if (subAccountsMatch) {
|
||||
const treasuryId = subAccountsMatch[1];
|
||||
if (!(await authorizedTreasury(treasuryId))) { sendJson(res, 403, { error: "Treasury authorization required" }); return; }
|
||||
if (req.method === "GET") {
|
||||
const subAccounts = await treasuryRouter.getSubAccounts(treasuryId);
|
||||
sendJson(res, 200, { subAccounts });
|
||||
@@ -180,7 +213,7 @@ const server = http.createServer(async (req, res) => {
|
||||
if (req.method === "POST") {
|
||||
const body = await readJsonBody(req);
|
||||
const address = typeof body.address === "string" ? body.address : "";
|
||||
if (!address) {
|
||||
if (!isAddress(address)) {
|
||||
sendJson(res, 400, { error: "address required" });
|
||||
return;
|
||||
}
|
||||
@@ -199,6 +232,7 @@ const server = http.createServer(async (req, res) => {
|
||||
if (req.method === "GET" && proposalsMatch) {
|
||||
const includeApprovals = url.searchParams.get("includeApprovals") === "1";
|
||||
const treasuryId = proposalsMatch[1];
|
||||
if (!(await authorizedTreasury(treasuryId))) { sendJson(res, 403, { error: "Treasury authorization required" }); return; }
|
||||
const status = url.searchParams.get("status") || undefined;
|
||||
const proposals = includeApprovals
|
||||
? await transactionRouter.getProposalsWithApprovals(treasuryId, status)
|
||||
@@ -213,6 +247,7 @@ const server = http.createServer(async (req, res) => {
|
||||
sendJson(res, 400, { error: "treasuryId required" });
|
||||
return;
|
||||
}
|
||||
if (!(await authorizedTreasury(treasuryId))) { sendJson(res, 403, { error: "Treasury authorization required" }); return; }
|
||||
const status = url.searchParams.get("status") || undefined;
|
||||
const proposals = await transactionRouter.getProposals(treasuryId, status);
|
||||
sendJson(res, 200, { proposals });
|
||||
@@ -225,6 +260,7 @@ const server = http.createServer(async (req, res) => {
|
||||
sendJson(res, 400, { error: "treasuryId query parameter required" });
|
||||
return;
|
||||
}
|
||||
if (!(await authorizedTreasury(treasuryId))) { sendJson(res, 403, { error: "Treasury authorization required" }); return; }
|
||||
const format = url.searchParams.get("format") || "json";
|
||||
if (format === "csv") {
|
||||
const csv = await exportRouter.exportTransactionsCSV(treasuryId);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2021", "DOM"],
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
|
||||
@@ -84,12 +84,12 @@ pct exec "$VMID" -- bash -c "
|
||||
apt-get update
|
||||
apt-get install -y curl
|
||||
|
||||
# Install Node.js 18
|
||||
curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
|
||||
# Install supported Node.js LTS
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
||||
apt-get install -y nodejs
|
||||
|
||||
# Install pnpm
|
||||
npm install -g pnpm
|
||||
npm install -g pnpm@8.15.0
|
||||
|
||||
# Install PostgreSQL client
|
||||
apt-get install -y postgresql-client
|
||||
@@ -176,4 +176,3 @@ echo "2. Update .env with database connection and RPC URL"
|
||||
echo "3. Run database migrations: pct exec $VMID -- bash -c 'cd /opt/solace-backend && pnpm run db:migrate'"
|
||||
echo "4. Start the service: pct exec $VMID -- systemctl start solace-backend"
|
||||
echo "5. Check status: pct exec $VMID -- systemctl status solace-backend"
|
||||
|
||||
|
||||
@@ -84,12 +84,12 @@ pct exec "$VMID" -- bash -c "
|
||||
apt-get update
|
||||
apt-get install -y curl
|
||||
|
||||
# Install Node.js 18
|
||||
curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
|
||||
# Install supported Node.js LTS
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
||||
apt-get install -y nodejs
|
||||
|
||||
# Install pnpm
|
||||
npm install -g pnpm
|
||||
npm install -g pnpm@8.15.0
|
||||
"
|
||||
|
||||
# Create application directory
|
||||
@@ -116,13 +116,14 @@ fi
|
||||
echo "Installing dependencies and building..."
|
||||
pct exec "$VMID" -- bash -c "
|
||||
cd /opt/solace-frontend
|
||||
export NODE_ENV=production
|
||||
# Install dev dependencies because Next.js needs Tailwind/PostCSS at build time.
|
||||
export NODE_ENV=development
|
||||
if [[ -f pnpm-lock.yaml ]]; then
|
||||
pnpm install --frozen-lockfile || pnpm install --no-frozen-lockfile
|
||||
else
|
||||
pnpm install --no-frozen-lockfile
|
||||
fi
|
||||
pnpm run build
|
||||
NODE_ENV=production pnpm run build
|
||||
"
|
||||
|
||||
# Create systemd service
|
||||
@@ -171,4 +172,3 @@ echo "1. Copy frontend/.env.production to container: pct push $VMID frontend/.en
|
||||
echo "2. Update .env.production with Chain 138 RPC URL and contract addresses"
|
||||
echo "3. Start the service: pct exec $VMID -- systemctl start solace-frontend"
|
||||
echo "4. Check status: pct exec $VMID -- systemctl status solace-frontend"
|
||||
|
||||
|
||||
@@ -82,12 +82,12 @@ pct exec "$VMID" -- bash -c "
|
||||
apt-get update
|
||||
apt-get install -y curl
|
||||
|
||||
# Install Node.js 18
|
||||
curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
|
||||
# Install supported Node.js LTS
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
||||
apt-get install -y nodejs
|
||||
|
||||
# Install pnpm
|
||||
npm install -g pnpm
|
||||
npm install -g pnpm@8.15.0
|
||||
|
||||
# Install PostgreSQL client
|
||||
apt-get install -y postgresql-client
|
||||
@@ -170,4 +170,3 @@ echo "2. Update .env.indexer with database connection, RPC URL, and contract add
|
||||
echo "3. Start the service: pct exec $VMID -- systemctl start solace-indexer"
|
||||
echo "4. Check status: pct exec $VMID -- systemctl status solace-indexer"
|
||||
echo "5. View logs: pct exec $VMID -- journalctl -u solace-indexer -f"
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { decodeMemoData } from "@/lib/memo";
|
||||
import { formatTokenAmount, getTokenByAddress } from "@/lib/tokens";
|
||||
import { CHAIN138_PUBLIC } from "@/lib/web3/chain138-public";
|
||||
import { useAccount } from "wagmi";
|
||||
|
||||
type ActivityFilter = "all" | "deposits" | "pending" | "executed";
|
||||
|
||||
@@ -34,6 +35,7 @@ export default function ActivityPage() {
|
||||
|
||||
function ActivityContent() {
|
||||
const { treasuryId, loading: treasuryLoading } = useTreasury();
|
||||
const { address } = useAccount();
|
||||
const [filter, setFilter] = useState<ActivityFilter>("all");
|
||||
const [proposals, setProposals] = useState<TransactionRecord[]>([]);
|
||||
const [ledger, setLedger] = useState<LedgerEntry[]>([]);
|
||||
@@ -48,10 +50,10 @@ function ActivityContent() {
|
||||
|
||||
setLoading(true);
|
||||
const proposalPromise = treasuryId
|
||||
? fetchTransactions(treasuryId)
|
||||
? fetchTransactions(treasuryId, undefined, address)
|
||||
: Promise.resolve([] as TransactionRecord[]);
|
||||
const ledgerPromise = treasuryId
|
||||
? fetchLedger(treasuryId)
|
||||
? fetchLedger(treasuryId, address)
|
||||
: treasuryWallet
|
||||
? fetchLedgerByWallet(treasuryWallet)
|
||||
: Promise.resolve([] as LedgerEntry[]);
|
||||
@@ -62,7 +64,7 @@ function ActivityContent() {
|
||||
setLedger(ledgerResult.status === "fulfilled" ? ledgerResult.value : []);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, [treasuryId]);
|
||||
}, [treasuryId, address]);
|
||||
|
||||
const deposits = useMemo(
|
||||
() => ledger.filter((entry) => entry.kind === "deposit"),
|
||||
@@ -83,7 +85,7 @@ function ActivityContent() {
|
||||
|
||||
const handleExport = async () => {
|
||||
if (!treasuryId) return;
|
||||
const csv = await exportTransactionsCsv(treasuryId);
|
||||
const csv = await exportTransactionsCsv(treasuryId, address);
|
||||
const blob = new Blob([csv], { type: "text/csv" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
|
||||
@@ -9,6 +9,8 @@ import { formatAddress } from "@/lib/utils";
|
||||
import { formatTokenAmount, getTokenByAddress } from "@/lib/tokens";
|
||||
import { useTreasury } from "@/lib/hooks/useTreasury";
|
||||
import { fetchPendingProposals, type TransactionRecord } from "@/lib/api/client";
|
||||
import { SignerStatus } from "@/components/web3/SignerStatus";
|
||||
import { useTreasuryAccess } from "@/lib/hooks/useTreasuryAccess";
|
||||
|
||||
export default function ApprovalsPage() {
|
||||
return (
|
||||
@@ -24,6 +26,7 @@ export default function ApprovalsPage() {
|
||||
function ApprovalsContent() {
|
||||
const { treasuryId, loading: treasuryLoading } = useTreasury();
|
||||
const { writeContract } = useWriteContract();
|
||||
const { isOwner, chainId, address } = useTreasuryAccess();
|
||||
const [proposals, setProposals] = useState<TransactionRecord[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -40,14 +43,15 @@ function ApprovalsContent() {
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
fetchPendingProposals(treasuryId)
|
||||
fetchPendingProposals(treasuryId, address)
|
||||
.then(setProposals)
|
||||
.catch(() => setProposals([]))
|
||||
.finally(() => setLoading(false));
|
||||
}, [treasuryId]);
|
||||
}, [treasuryId, address]);
|
||||
|
||||
const handleApprove = async (proposalId: number) => {
|
||||
if (!CONTRACT_ADDRESSES.TreasuryWallet) return;
|
||||
if (!isOwner || chainId !== 138) return;
|
||||
|
||||
await writeContract({
|
||||
address: CONTRACT_ADDRESSES.TreasuryWallet as `0x${string}`,
|
||||
@@ -59,6 +63,7 @@ function ApprovalsContent() {
|
||||
|
||||
const handleExecute = async (proposalId: number) => {
|
||||
if (!CONTRACT_ADDRESSES.TreasuryWallet) return;
|
||||
if (!isOwner || chainId !== 138) return;
|
||||
|
||||
await writeContract({
|
||||
address: CONTRACT_ADDRESSES.TreasuryWallet as `0x${string}`,
|
||||
@@ -73,6 +78,7 @@ function ApprovalsContent() {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<PageHeader title="Pending Approvals" />
|
||||
<SignerStatus action="approve or execute withdrawals" />
|
||||
|
||||
<div className="bg-gray-900 rounded-xl p-6 sm:p-8">
|
||||
{treasuryLoading || loading ? (
|
||||
@@ -115,7 +121,8 @@ function ApprovalsContent() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleExecute(proposal.proposalId)}
|
||||
className="px-4 py-2 bg-green-600 hover:bg-green-700 rounded-lg transition-colors"
|
||||
disabled={!isOwner || chainId !== 138}
|
||||
className="px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-700 rounded-lg transition-colors"
|
||||
>
|
||||
Execute
|
||||
</button>
|
||||
@@ -123,7 +130,8 @@ function ApprovalsContent() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleApprove(proposal.proposalId)}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
|
||||
disabled={!isOwner || chainId !== 138}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-700 rounded-lg transition-colors"
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
|
||||
@@ -8,6 +8,8 @@ import { PageHeader } from "@/components/layout/PageHeader";
|
||||
import { TREASURY_WALLET_ABI, CONTRACT_ADDRESSES } from "@/lib/web3/contracts";
|
||||
import { encodeMemoData } from "@/lib/memo";
|
||||
import { CHAIN138_TOKENS, ERC20_BALANCE_ABI, formatTokenAmount } from "@/lib/tokens";
|
||||
import { SignerStatus } from "@/components/web3/SignerStatus";
|
||||
import { useTreasuryAccess } from "@/lib/hooks/useTreasuryAccess";
|
||||
|
||||
export default function SendPage() {
|
||||
return (
|
||||
@@ -24,6 +26,7 @@ function SendForm() {
|
||||
const treasuryAddress = CONTRACT_ADDRESSES.TreasuryWallet as `0x${string}` | undefined;
|
||||
const [selectedToken, setSelectedToken] = useState(CHAIN138_TOKENS[0]);
|
||||
const { writeContract } = useWriteContract();
|
||||
const { isOwner, chainId } = useTreasuryAccess();
|
||||
|
||||
const isNative = selectedToken.address === "native";
|
||||
|
||||
@@ -62,6 +65,7 @@ function SendForm() {
|
||||
if (!isAddress(recipient)) {
|
||||
throw new Error("Invalid recipient address");
|
||||
}
|
||||
if (chainId !== 138 || !isOwner) throw new Error("Connected wallet is not an authorized Chain 138 treasury signer");
|
||||
|
||||
if (!amount || parseFloat(amount) <= 0) {
|
||||
throw new Error("Invalid amount");
|
||||
@@ -92,6 +96,7 @@ function SendForm() {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<PageHeader title="Send Payment" />
|
||||
<SignerStatus action="propose a withdrawal" />
|
||||
|
||||
<div className="bg-gray-900 rounded-xl p-6 sm:p-8 space-y-6">
|
||||
<div>
|
||||
@@ -160,7 +165,7 @@ function SendForm() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSend}
|
||||
disabled={loading || !recipient || !amount}
|
||||
disabled={loading || !recipient || !amount || !isOwner || chainId !== 138}
|
||||
className="w-full px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded-lg transition-colors font-semibold"
|
||||
>
|
||||
{loading ? "Processing..." : "Send Payment"}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useWriteContract, useReadContract } from "wagmi";
|
||||
import { useWriteContract, useReadContract, useChainId } from "wagmi";
|
||||
import { WalletGate } from "@/components/web3/WalletGate";
|
||||
import { PageHeader } from "@/components/layout/PageHeader";
|
||||
import { TREASURY_WALLET_ABI, CONTRACT_ADDRESSES } from "@/lib/web3/contracts";
|
||||
import { formatAddress, isAddress } from "@/lib/utils";
|
||||
import { getAddress } from "viem";
|
||||
import { SubAccountsSettings } from "@/components/settings/SubAccountsSettings";
|
||||
import { SignerStatus } from "@/components/web3/SignerStatus";
|
||||
import { useTreasuryAccess } from "@/lib/hooks/useTreasuryAccess";
|
||||
|
||||
export default function SettingsPage() {
|
||||
return (
|
||||
@@ -21,7 +23,11 @@ export default function SettingsPage() {
|
||||
}
|
||||
|
||||
function SettingsContent() {
|
||||
const { writeContract } = useWriteContract();
|
||||
const { writeContractAsync } = useWriteContract();
|
||||
const chainId = useChainId();
|
||||
const { isOwner } = useTreasuryAccess();
|
||||
const [error, setError] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [newSigner, setNewSigner] = useState("");
|
||||
const [signerToRemove, setSignerToRemove] = useState("");
|
||||
const [newThreshold, setNewThreshold] = useState("");
|
||||
@@ -40,48 +46,45 @@ function SettingsContent() {
|
||||
|
||||
|
||||
const handleAddSigner = async () => {
|
||||
if (!isAddress(newSigner) || !CONTRACT_ADDRESSES.TreasuryWallet) return;
|
||||
|
||||
await writeContract({
|
||||
if (!isAddress(newSigner) || !CONTRACT_ADDRESSES.TreasuryWallet || chainId !== 138) return;
|
||||
setBusy(true); setError("");
|
||||
try { await writeContractAsync({
|
||||
address: CONTRACT_ADDRESSES.TreasuryWallet as `0x${string}`,
|
||||
abi: TREASURY_WALLET_ABI,
|
||||
functionName: "addOwner",
|
||||
args: [getAddress(newSigner)],
|
||||
});
|
||||
|
||||
setNewSigner("");
|
||||
}); setNewSigner(""); } catch (e) { setError(e instanceof Error ? e.message : "Transaction failed"); } finally { setBusy(false); }
|
||||
};
|
||||
|
||||
const handleRemoveSigner = async () => {
|
||||
if (!isAddress(signerToRemove) || !CONTRACT_ADDRESSES.TreasuryWallet) return;
|
||||
|
||||
await writeContract({
|
||||
if (!isAddress(signerToRemove) || !CONTRACT_ADDRESSES.TreasuryWallet || chainId !== 138) return;
|
||||
setBusy(true); setError("");
|
||||
try { await writeContractAsync({
|
||||
address: CONTRACT_ADDRESSES.TreasuryWallet as `0x${string}`,
|
||||
abi: TREASURY_WALLET_ABI,
|
||||
functionName: "removeOwner",
|
||||
args: [getAddress(signerToRemove)],
|
||||
});
|
||||
|
||||
setSignerToRemove("");
|
||||
}); setSignerToRemove(""); } catch (e) { setError(e instanceof Error ? e.message : "Transaction failed"); } finally { setBusy(false); }
|
||||
};
|
||||
|
||||
const handleChangeThreshold = async () => {
|
||||
const thresholdNum = parseInt(newThreshold);
|
||||
if (isNaN(thresholdNum) || !CONTRACT_ADDRESSES.TreasuryWallet) return;
|
||||
|
||||
await writeContract({
|
||||
if (!Number.isInteger(thresholdNum) || thresholdNum < 1 || thresholdNum > (owners?.length || 0) || !CONTRACT_ADDRESSES.TreasuryWallet || chainId !== 138) return;
|
||||
setBusy(true); setError("");
|
||||
try { await writeContractAsync({
|
||||
address: CONTRACT_ADDRESSES.TreasuryWallet as `0x${string}`,
|
||||
abi: TREASURY_WALLET_ABI,
|
||||
functionName: "changeThreshold",
|
||||
args: [BigInt(thresholdNum)],
|
||||
});
|
||||
|
||||
setNewThreshold("");
|
||||
}); setNewThreshold(""); } catch (e) { setError(e instanceof Error ? e.message : "Transaction failed"); } finally { setBusy(false); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<PageHeader title="Treasury Settings" />
|
||||
<SignerStatus action="change treasury signers or threshold" />
|
||||
{chainId !== 138 && <div className="bg-yellow-900/30 border border-yellow-600 rounded-lg p-4 mb-6">Switch your wallet to Solace Chain 138 before changing treasury settings.</div>}
|
||||
{error && <div className="bg-red-900/30 border border-red-600 rounded-lg p-4 mb-6" role="alert">{error}</div>}
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Current Configuration */}
|
||||
@@ -126,7 +129,7 @@ function SettingsContent() {
|
||||
/>
|
||||
<button
|
||||
onClick={handleAddSigner}
|
||||
disabled={!isAddress(newSigner)}
|
||||
disabled={busy || !isAddress(newSigner) || chainId !== 138 || !isOwner}
|
||||
className="px-6 py-3 bg-green-600 hover:bg-green-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded-lg transition-colors"
|
||||
>
|
||||
Add
|
||||
@@ -153,7 +156,7 @@ function SettingsContent() {
|
||||
/>
|
||||
<button
|
||||
onClick={handleRemoveSigner}
|
||||
disabled={!isAddress(signerToRemove)}
|
||||
disabled={busy || !isAddress(signerToRemove) || chainId !== 138 || !isOwner}
|
||||
className="px-6 py-3 bg-red-600 hover:bg-red-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded-lg transition-colors"
|
||||
>
|
||||
Remove
|
||||
@@ -179,7 +182,7 @@ function SettingsContent() {
|
||||
/>
|
||||
<button
|
||||
onClick={handleChangeThreshold}
|
||||
disabled={!newThreshold}
|
||||
disabled={busy || !newThreshold || chainId !== 138 || !isOwner}
|
||||
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded-lg transition-colors"
|
||||
>
|
||||
Update
|
||||
@@ -190,4 +193,3 @@ function SettingsContent() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import { PageHeader } from "@/components/layout/PageHeader";
|
||||
import { TREASURY_WALLET_ABI, CONTRACT_ADDRESSES } from "@/lib/web3/contracts";
|
||||
import { useTreasury } from "@/lib/hooks/useTreasury";
|
||||
import { fetchSubAccounts } from "@/lib/api/client";
|
||||
import { SignerStatus } from "@/components/web3/SignerStatus";
|
||||
import { useTreasuryAccess } from "@/lib/hooks/useTreasuryAccess";
|
||||
|
||||
export default function TransferPage() {
|
||||
return (
|
||||
@@ -29,6 +31,7 @@ function TransferForm() {
|
||||
query: { enabled: Boolean(treasuryAddress) },
|
||||
});
|
||||
const { writeContract } = useWriteContract();
|
||||
const { isOwner, chainId } = useTreasuryAccess();
|
||||
|
||||
const [fromAccount, setFromAccount] = useState("main");
|
||||
const [toAccount, setToAccount] = useState("");
|
||||
@@ -39,10 +42,10 @@ function TransferForm() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!treasuryId) return;
|
||||
fetchSubAccounts(treasuryId)
|
||||
fetchSubAccounts(treasuryId, address)
|
||||
.then((accounts) => setSubAccounts(accounts.map((a) => a.address)))
|
||||
.catch(() => setSubAccounts([]));
|
||||
}, [treasuryId]);
|
||||
}, [treasuryId, address]);
|
||||
|
||||
const handleTransfer = async () => {
|
||||
setError("");
|
||||
@@ -52,6 +55,7 @@ function TransferForm() {
|
||||
if (!toAccount) {
|
||||
throw new Error("Please select destination account");
|
||||
}
|
||||
if (chainId !== 138 || !isOwner) throw new Error("Connected wallet is not an authorized Chain 138 treasury signer");
|
||||
|
||||
if (!amount || parseFloat(amount) <= 0) {
|
||||
throw new Error("Invalid amount");
|
||||
@@ -81,6 +85,7 @@ function TransferForm() {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<PageHeader title="Internal Transfer" />
|
||||
<SignerStatus action="propose an internal transfer" />
|
||||
|
||||
<div className="bg-gray-900 rounded-xl p-6 sm:p-8 space-y-6">
|
||||
<div>
|
||||
@@ -148,7 +153,7 @@ function TransferForm() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTransfer}
|
||||
disabled={loading || !toAccount || !amount}
|
||||
disabled={loading || !toAccount || !amount || !isOwner || chainId !== 138}
|
||||
className="w-full px-6 py-3 bg-purple-600 hover:bg-purple-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded-lg transition-colors font-semibold"
|
||||
>
|
||||
{loading ? "Processing..." : "Transfer"}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Metadata } from "next";
|
||||
import dynamic from "next/dynamic";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const Providers = dynamic(
|
||||
@@ -16,8 +15,6 @@ const ParticleBackground = dynamic(
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Solace Treasury Management",
|
||||
description: "Treasury Management DApp for Solace Bank Group",
|
||||
@@ -34,7 +31,7 @@ export default function RootLayout({
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<body>
|
||||
<ParticleBackground />
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { formatTokenAmount, getTokenByAddress } from "@/lib/tokens";
|
||||
import { gsap } from "gsap";
|
||||
import { useTreasury } from "@/lib/hooks/useTreasury";
|
||||
import { CONTRACT_ADDRESSES } from "@/lib/web3/contracts";
|
||||
import { useAccount } from "wagmi";
|
||||
import {
|
||||
fetchLedger,
|
||||
fetchLedgerByWallet,
|
||||
@@ -21,6 +22,7 @@ type ActivityItem =
|
||||
|
||||
export function RecentActivity() {
|
||||
const { treasuryId } = useTreasury();
|
||||
const { address } = useAccount();
|
||||
const [items, setItems] = useState<ActivityItem[]>([]);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -29,10 +31,10 @@ export function RecentActivity() {
|
||||
if (!treasuryId && !treasuryWallet) return;
|
||||
|
||||
const proposalPromise = treasuryId
|
||||
? fetchTransactions(treasuryId)
|
||||
? fetchTransactions(treasuryId, undefined, address)
|
||||
: Promise.resolve([] as TransactionRecord[]);
|
||||
const ledgerPromise = treasuryId
|
||||
? fetchLedger(treasuryId)
|
||||
? fetchLedger(treasuryId, address)
|
||||
: treasuryWallet
|
||||
? fetchLedgerByWallet(treasuryWallet)
|
||||
: Promise.resolve([] as LedgerEntry[]);
|
||||
@@ -59,7 +61,7 @@ export function RecentActivity() {
|
||||
setItems(merged.slice(0, 5));
|
||||
}
|
||||
);
|
||||
}, [treasuryId]);
|
||||
}, [treasuryId, address]);
|
||||
|
||||
useEffect(() => {
|
||||
if (containerRef.current && items.length > 0) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useAccount } from "wagmi";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useWriteContract, useReadContract } from "wagmi";
|
||||
import { keccak256, toBytes } from "viem";
|
||||
@@ -13,6 +14,7 @@ import { fetchSubAccounts, type SubAccountRecord } from "@/lib/api/client";
|
||||
|
||||
export function SubAccountsSettings() {
|
||||
const { treasuryId } = useTreasury();
|
||||
const { address } = useAccount();
|
||||
const [label, setLabel] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
@@ -29,10 +31,10 @@ export function SubAccountsSettings() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!treasuryId) return;
|
||||
fetchSubAccounts(treasuryId)
|
||||
fetchSubAccounts(treasuryId, address)
|
||||
.then(setSubAccounts)
|
||||
.catch(() => setSubAccounts([]));
|
||||
}, [treasuryId]);
|
||||
}, [treasuryId, address]);
|
||||
|
||||
const handleCreate = async () => {
|
||||
setError("");
|
||||
@@ -54,7 +56,7 @@ export function SubAccountsSettings() {
|
||||
});
|
||||
|
||||
setLabel("");
|
||||
const refreshed = await fetchSubAccounts(treasuryId);
|
||||
const refreshed = await fetchSubAccounts(treasuryId, address);
|
||||
setSubAccounts(refreshed);
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : "Failed to create sub-account");
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useTreasuryAccess } from "@/lib/hooks/useTreasuryAccess";
|
||||
|
||||
export function SignerStatus({ action = "manage treasury funds" }: { action?: string }) {
|
||||
const { chainId, isOwner, isLoading } = useTreasuryAccess();
|
||||
if (chainId !== 138) return <div className="bg-yellow-900/30 border border-yellow-600 rounded-lg p-4 text-sm text-yellow-200">Switch MetaMask to Solace Chain 138 before you can {action}.</div>;
|
||||
if (isLoading) return <div className="bg-gray-800 rounded-lg p-4 text-sm text-gray-400">Checking treasury signer permissions…</div>;
|
||||
if (!isOwner) return <div className="bg-red-900/30 border border-red-600 rounded-lg p-4 text-sm text-red-200">This connected wallet is not a treasury signer. Ask an authorized signer to propose, approve, or execute this withdrawal.</div>;
|
||||
return null;
|
||||
}
|
||||
@@ -30,3 +30,10 @@ test("backend health when API is up", async ({ request }) => {
|
||||
const body = await res.json();
|
||||
expect(body.status).toBe("ok");
|
||||
});
|
||||
|
||||
test("protected treasury data rejects unsigned requests", async ({ request }) => {
|
||||
const api = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001";
|
||||
const res = await request.get(`${api}/api/treasury/00000000-0000-0000-0000-000000000000/ledger`);
|
||||
test.skip(res.status() === 404, "Backend route unavailable");
|
||||
expect(res.status()).toBe(403);
|
||||
});
|
||||
|
||||
+24
-13
@@ -68,8 +68,17 @@ export interface LedgerEntry {
|
||||
blockNumber: number;
|
||||
}
|
||||
|
||||
async function apiFetch<T>(path: string): Promise<T> {
|
||||
const res = await fetch(apiUrl(path), { cache: "no-store" });
|
||||
async function signedHeaders(path: string, wallet?: string): Promise<Record<string, string> | undefined> {
|
||||
if (!wallet || typeof window === "undefined") return undefined;
|
||||
const ethereum = (window as Window & { ethereum?: { request: (args: { method: string; params: string[] }) => Promise<string> } }).ethereum;
|
||||
if (!ethereum) return undefined;
|
||||
const message = `Solace Treasury API authorization\nWallet: ${wallet.toLowerCase()}\nPath: ${path}\nTimestamp: ${Date.now()}`;
|
||||
const signature = await ethereum.request({ method: "personal_sign", params: [message, wallet] });
|
||||
return { "X-Wallet-Address": wallet, "X-Wallet-Message": message, "X-Wallet-Signature": signature };
|
||||
}
|
||||
|
||||
async function apiFetch<T>(path: string, wallet?: string): Promise<T> {
|
||||
const res = await fetch(apiUrl(path), { cache: "no-store", headers: await signedHeaders(path, wallet) });
|
||||
if (!res.ok) {
|
||||
throw new Error(`API ${path} failed: ${res.status}`);
|
||||
}
|
||||
@@ -85,46 +94,48 @@ export async function fetchTreasury(wallet: string): Promise<TreasuryRecord | nu
|
||||
|
||||
export async function fetchTransactions(
|
||||
treasuryId: string,
|
||||
status?: string
|
||||
status?: string,
|
||||
wallet?: string
|
||||
): Promise<TransactionRecord[]> {
|
||||
const query = status ? `?status=${encodeURIComponent(status)}` : "";
|
||||
const data = await apiFetch<{ proposals: TransactionRecord[] }>(
|
||||
`/api/transactions/${treasuryId}${query}`
|
||||
`/api/transactions/${treasuryId}${query}`, wallet
|
||||
);
|
||||
return data.proposals;
|
||||
}
|
||||
|
||||
export async function fetchPendingProposals(treasuryId: string): Promise<TransactionRecord[]> {
|
||||
export async function fetchPendingProposals(treasuryId: string, wallet?: string): Promise<TransactionRecord[]> {
|
||||
const data = await apiFetch<{ proposals: TransactionRecord[] }>(
|
||||
`/api/proposals/${treasuryId}?status=pending&includeApprovals=1`
|
||||
`/api/proposals/${treasuryId}?status=pending&includeApprovals=1`, wallet
|
||||
);
|
||||
return data.proposals;
|
||||
}
|
||||
|
||||
export async function fetchSubAccounts(treasuryId: string): Promise<SubAccountRecord[]> {
|
||||
export async function fetchSubAccounts(treasuryId: string, wallet?: string): Promise<SubAccountRecord[]> {
|
||||
const data = await apiFetch<{ subAccounts: SubAccountRecord[] }>(
|
||||
`/api/treasury/${treasuryId}/sub-accounts`
|
||||
`/api/treasury/${treasuryId}/sub-accounts`, wallet
|
||||
);
|
||||
return data.subAccounts;
|
||||
}
|
||||
|
||||
export async function fetchLedger(treasuryId: string): Promise<LedgerEntry[]> {
|
||||
export async function fetchLedger(treasuryId: string, wallet?: string): Promise<LedgerEntry[]> {
|
||||
const data = await apiFetch<{ ledger: LedgerEntry[] }>(
|
||||
`/api/treasury/${treasuryId}/ledger`
|
||||
`/api/treasury/${treasuryId}/ledger`, wallet
|
||||
);
|
||||
return data.ledger;
|
||||
}
|
||||
|
||||
export async function fetchLedgerByWallet(wallet: string): Promise<LedgerEntry[]> {
|
||||
const data = await apiFetch<{ ledger: LedgerEntry[] }>(
|
||||
`/api/ledger?wallet=${encodeURIComponent(wallet)}`
|
||||
`/api/ledger?wallet=${encodeURIComponent(wallet)}`, wallet
|
||||
);
|
||||
return data.ledger;
|
||||
}
|
||||
|
||||
export async function exportTransactionsCsv(treasuryId: string): Promise<string> {
|
||||
export async function exportTransactionsCsv(treasuryId: string, wallet?: string): Promise<string> {
|
||||
const path = `/api/transactions/export?treasuryId=${encodeURIComponent(treasuryId)}&format=csv`;
|
||||
const res = await fetch(
|
||||
apiUrl(`/api/transactions/export?treasuryId=${encodeURIComponent(treasuryId)}&format=csv`)
|
||||
apiUrl(path), { headers: await signedHeaders(path, wallet) }
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw new Error(`CSV export failed: ${res.status}`);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useAccount, useReadContract } from "wagmi";
|
||||
import { TREASURY_WALLET_ABI, CONTRACT_ADDRESSES } from "@/lib/web3/contracts";
|
||||
|
||||
export function useTreasuryAccess() {
|
||||
const { address } = useAccount();
|
||||
const chainId = useAccount().chainId;
|
||||
const { data: isOwner, isLoading } = useReadContract({
|
||||
address: CONTRACT_ADDRESSES.TreasuryWallet as `0x${string}`,
|
||||
abi: TREASURY_WALLET_ABI,
|
||||
functionName: "isOwner",
|
||||
args: address ? [address] : undefined,
|
||||
query: { enabled: Boolean(address && CONTRACT_ADDRESSES.TreasuryWallet && chainId === 138) },
|
||||
});
|
||||
return { address, chainId, isOwner: isOwner === true, isLoading };
|
||||
}
|
||||
@@ -3,6 +3,11 @@ const path = require("path");
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
async headers() {
|
||||
return [{ source: "/(.*)", headers: [
|
||||
{ key: "Content-Security-Policy", value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self' https://rpc-http-pub.d-bis.org wss://rpc-ws-pub.d-bis.org https://explorer.d-bis.org; frame-ancestors 'self'; base-uri 'self'; form-action 'self'" },
|
||||
] }];
|
||||
},
|
||||
webpack: (config, { isServer }) => {
|
||||
config.resolve.fallback = {
|
||||
...config.resolve.fallback,
|
||||
@@ -32,4 +37,3 @@ const nextConfig = {
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
||||
|
||||
+22
-20
@@ -12,38 +12,40 @@
|
||||
"test:e2e:ui": "playwright test --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-three/drei": "^9.92.0",
|
||||
"@react-three/fiber": "^8.15.0",
|
||||
"@tanstack/react-query": "^5.17.0",
|
||||
"@walletconnect/ethereum-provider": "^2.9.0",
|
||||
"@walletconnect/modal": "^2.6.2",
|
||||
"@x402/core": "^2.25.0",
|
||||
"@x402/evm": "^2.25.0",
|
||||
"@x402/svm": "^2.25.0",
|
||||
"clsx": "^2.1.0",
|
||||
"date-fns": "^3.0.0",
|
||||
"gsap": "^3.12.2",
|
||||
"maath": "^0.10.0",
|
||||
"next": "^14.0.4",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"wagmi": "^2.5.0",
|
||||
"viem": "^2.0.0",
|
||||
"@tanstack/react-query": "^5.17.0",
|
||||
"@walletconnect/modal": "^2.6.2",
|
||||
"@walletconnect/ethereum-provider": "^2.9.0",
|
||||
"gsap": "^3.12.2",
|
||||
"@react-three/fiber": "^8.15.0",
|
||||
"@react-three/drei": "^9.92.0",
|
||||
"three": "^0.160.0",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"date-fns": "^3.0.0",
|
||||
"zod": "^3.22.4",
|
||||
"clsx": "^2.1.0",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"maath": "^0.10.0"
|
||||
"three": "^0.160.0",
|
||||
"viem": "^2.0.0",
|
||||
"wagmi": "^2.5.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.49.0",
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/qrcode.react": "^3.0.0",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@types/three": "^0.160.0",
|
||||
"@types/qrcode.react": "^3.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"postcss": "^8.4.32",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "^14.0.4",
|
||||
"@playwright/test": "^1.49.0"
|
||||
"postcss": "^8.4.32",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ rsync -az --delete --exclude node_modules --exclude .next --exclude dist \
|
||||
"$ROOT/backend/" "$PROXMOX_USER@$PROXMOX_HOST:$DEPLOYMENT_DIR/project/backend/"
|
||||
rsync -az --delete --exclude node_modules --exclude .next \
|
||||
"$ROOT/frontend/" "$PROXMOX_USER@$PROXMOX_HOST:$DEPLOYMENT_DIR/project/frontend/"
|
||||
rsync -az "$ROOT/pnpm-lock.yaml" "$PROXMOX_USER@$PROXMOX_HOST:$DEPLOYMENT_DIR/project/pnpm-lock.yaml"
|
||||
|
||||
ssh "$PROXMOX_USER@$PROXMOX_HOST" "cat > $DEPLOYMENT_DIR/config/dapp.conf <<EOF
|
||||
$(grep -v '^DATABASE_PASSWORD=' "$ROOT/deployment/proxmox/config/dapp.conf")
|
||||
@@ -34,13 +35,16 @@ ssh "$PROXMOX_USER@$PROXMOX_HOST" bash -s <<'REMOTE'
|
||||
set -euo pipefail
|
||||
DEPLOYMENT_DIR=/tmp/solace-dapp-deployment
|
||||
PROJECT_ROOT=$DEPLOYMENT_DIR/project
|
||||
cp "$PROJECT_ROOT/pnpm-lock.yaml" "$PROJECT_ROOT/backend/pnpm-lock.yaml"
|
||||
cp "$PROJECT_ROOT/pnpm-lock.yaml" "$PROJECT_ROOT/frontend/pnpm-lock.yaml"
|
||||
|
||||
rebuild_backend() {
|
||||
local vmid=$1 dir=$2
|
||||
pct exec "$vmid" -- bash -c "export DEBIAN_FRONTEND=noninteractive; apt-get update -qq; apt-get install -y -qq curl; curl -fsSL https://deb.nodesource.com/setup_20.x | bash - >/dev/null 2>&1; apt-get install -y -qq nodejs"
|
||||
pct exec "$vmid" -- bash -c "rm -rf /opt/$dir/* /opt/$dir/.* 2>/dev/null || true"
|
||||
cd "$PROJECT_ROOT"
|
||||
tar czf - backend | pct exec "$vmid" -- bash -c "cd /opt && tar xzf - && mv backend/* $dir/ && mv backend/.* $dir/ 2>/dev/null || true && rmdir backend 2>/dev/null || true"
|
||||
pct exec "$vmid" -- bash -c "cd /opt/$dir && pnpm install --no-frozen-lockfile"
|
||||
pct exec "$vmid" -- bash -c "npm install -g [email protected] >/dev/null 2>&1 && cd /opt/$dir && pnpm install --no-frozen-lockfile"
|
||||
}
|
||||
|
||||
rebuild_backend 3001 solace-backend
|
||||
|
||||
Reference in New Issue
Block a user