147 lines
4.8 KiB
TypeScript
147 lines
4.8 KiB
TypeScript
/** Same-origin in production (nginx /api/ → backend). Local dev uses localhost:3001. */
|
|
function apiUrl(path: string): string {
|
|
const normalized = path.startsWith("/") ? path : `/${path}`;
|
|
const configured = (process.env.NEXT_PUBLIC_API_URL ?? "").trim();
|
|
|
|
if (typeof window !== "undefined") {
|
|
// Ignore localhost baked in at build time — browser must hit same-origin /api/.
|
|
if (configured && !/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?/i.test(configured)) {
|
|
return `${configured.replace(/\/$/, "")}${normalized}`;
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
if (configured) {
|
|
return `${configured.replace(/\/$/, "")}${normalized}`;
|
|
}
|
|
if (process.env.NODE_ENV === "production") {
|
|
return normalized;
|
|
}
|
|
return `http://localhost:3001${normalized}`;
|
|
}
|
|
|
|
export interface TreasuryRecord {
|
|
id: string;
|
|
organizationId: string;
|
|
chainId: number;
|
|
mainWallet: string;
|
|
label: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface TransactionRecord {
|
|
id: string;
|
|
treasuryId: string;
|
|
proposalId: number;
|
|
walletAddress: string;
|
|
to: string;
|
|
value: string;
|
|
token: string | null;
|
|
data: string | null;
|
|
status: string;
|
|
proposer: string;
|
|
createdAt: string;
|
|
executedAt: string | null;
|
|
approvalCount?: number;
|
|
}
|
|
|
|
export interface SubAccountRecord {
|
|
id: string;
|
|
treasuryId: string;
|
|
address: string;
|
|
label: string | null;
|
|
metadataHash: string | null;
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface LedgerEntry {
|
|
id: string;
|
|
kind: "deposit" | "transfer_out";
|
|
txHash: string;
|
|
from: string;
|
|
to: string;
|
|
value: string;
|
|
token: string | null;
|
|
tokenSymbol: string | null;
|
|
timestamp: string;
|
|
blockNumber: number;
|
|
}
|
|
|
|
async function signedHeaders(path: string, wallet?: string, method = "GET"): 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 domain = window.location.host;
|
|
const nonce = crypto.randomUUID();
|
|
const message = `Solace Treasury API authorization\nDomain: ${domain}\nChain ID: 138\nWallet: ${wallet.toLowerCase()}\nMethod: ${method}\nPath: ${path}\nTimestamp: ${Date.now()}\nNonce: ${nonce}`;
|
|
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}`);
|
|
}
|
|
return res.json() as Promise<T>;
|
|
}
|
|
|
|
export async function fetchTreasury(wallet: string): Promise<TreasuryRecord | null> {
|
|
const data = await apiFetch<{ treasury: TreasuryRecord | null }>(
|
|
`/api/treasury?wallet=${encodeURIComponent(wallet)}`
|
|
);
|
|
return data.treasury;
|
|
}
|
|
|
|
export async function fetchTransactions(
|
|
treasuryId: string,
|
|
status?: string,
|
|
wallet?: string
|
|
): Promise<TransactionRecord[]> {
|
|
const query = status ? `?status=${encodeURIComponent(status)}` : "";
|
|
const data = await apiFetch<{ proposals: TransactionRecord[] }>(
|
|
`/api/transactions/${treasuryId}${query}`, wallet
|
|
);
|
|
return data.proposals;
|
|
}
|
|
|
|
export async function fetchPendingProposals(treasuryId: string, wallet?: string): Promise<TransactionRecord[]> {
|
|
const data = await apiFetch<{ proposals: TransactionRecord[] }>(
|
|
`/api/proposals/${treasuryId}?status=pending&includeApprovals=1`, wallet
|
|
);
|
|
return data.proposals;
|
|
}
|
|
|
|
export async function fetchSubAccounts(treasuryId: string, wallet?: string): Promise<SubAccountRecord[]> {
|
|
const data = await apiFetch<{ subAccounts: SubAccountRecord[] }>(
|
|
`/api/treasury/${treasuryId}/sub-accounts`, wallet
|
|
);
|
|
return data.subAccounts;
|
|
}
|
|
|
|
export async function fetchLedger(treasuryId: string, wallet?: string): Promise<LedgerEntry[]> {
|
|
const data = await apiFetch<{ ledger: LedgerEntry[] }>(
|
|
`/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)}`, wallet
|
|
);
|
|
return data.ledger;
|
|
}
|
|
|
|
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(path), { headers: await signedHeaders(path, wallet) }
|
|
);
|
|
if (!res.ok) {
|
|
throw new Error(`CSV export failed: ${res.status}`);
|
|
}
|
|
return res.text();
|
|
}
|