canvas-website/src/components/safe/TransactionComposer.tsx

160 lines
4.7 KiB
TypeScript

import { useState } from "react"
import { useProposeTransaction, useSafeBalances } from "../../hooks/useSafeTransaction"
import { useWalletConnection } from "../../hooks/useWallet"
export function TransactionComposer() {
const { address, isConnected } = useWalletConnection()
const { data: balances } = useSafeBalances()
const { propose, isLoading, error } = useProposeTransaction()
const [recipient, setRecipient] = useState("")
const [amount, setAmount] = useState("")
const [tokenAddress, setTokenAddress] = useState("")
const [title, setTitle] = useState("")
const [signerKey, setSignerKey] = useState("")
const [result, setResult] = useState<string | null>(null)
const handleSubmit = async () => {
if (!recipient || !amount || !signerKey) return
const res = await propose({
recipientAddress: recipient,
amount,
tokenAddress: tokenAddress || undefined,
title: title || undefined,
signerPrivateKey: signerKey,
})
if (res) {
setResult(`Proposed: ${res.safeTxHash.slice(0, 16)}...`)
setRecipient("")
setAmount("")
setTitle("")
}
}
const inputStyle: React.CSSProperties = {
width: "100%",
padding: "8px 10px",
border: "1px solid #334155",
borderRadius: 6,
background: "#1e293b",
color: "#e2e8f0",
fontSize: 12,
fontFamily: "monospace",
outline: "none",
boxSizing: "border-box",
}
const labelStyle: React.CSSProperties = {
fontSize: 11,
fontWeight: 600,
color: "#94a3b8",
marginBottom: 4,
display: "block",
}
return (
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
{/* Title */}
<div>
<label style={labelStyle}>Description (optional)</label>
<input
style={inputStyle}
placeholder="Payment for..."
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</div>
{/* Recipient */}
<div>
<label style={labelStyle}>Recipient Address *</label>
<input
style={inputStyle}
placeholder="0x..."
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
/>
</div>
{/* Token (optional) */}
<div>
<label style={labelStyle}>Token Address (empty = native ETH)</label>
<select
style={{ ...inputStyle, fontFamily: "Inter, system-ui, sans-serif" }}
value={tokenAddress}
onChange={(e) => setTokenAddress(e.target.value)}
>
<option value="">ETH (native)</option>
{balances?.balances
?.filter((b) => b.tokenAddress && b.token)
.map((b) => (
<option key={b.tokenAddress} value={b.tokenAddress!}>
{b.token!.symbol} {(Number(b.balance) / 10 ** b.token!.decimals).toFixed(4)}
</option>
))}
</select>
</div>
{/* Amount */}
<div>
<label style={labelStyle}>Amount * {tokenAddress ? "(token units)" : "(wei)"}</label>
<input
style={inputStyle}
placeholder={tokenAddress ? "100" : "1000000000000000"}
value={amount}
onChange={(e) => setAmount(e.target.value)}
type="text"
/>
</div>
{/* Signer Key */}
<div>
<label style={labelStyle}>Signer Private Key *</label>
<input
style={inputStyle}
type="password"
placeholder="0x..."
value={signerKey}
onChange={(e) => setSignerKey(e.target.value)}
/>
<span style={{ fontSize: 10, color: "#64748b", marginTop: 2, display: "block" }}>
Must be a Safe owner. Key is sent to treasury-service for signing.
</span>
</div>
{/* Submit */}
<button
onClick={handleSubmit}
disabled={isLoading || !recipient || !amount || !signerKey}
style={{
padding: "10px 16px",
border: "none",
borderRadius: 6,
background: isLoading ? "#334155" : "#12ff80",
color: isLoading ? "#94a3b8" : "#0f172a",
fontWeight: 700,
fontSize: 13,
cursor: isLoading ? "not-allowed" : "pointer",
transition: "all 0.15s",
}}
>
{isLoading ? "Proposing..." : "Propose Transaction"}
</button>
{/* Result / Error */}
{result && (
<div style={{ padding: 8, borderRadius: 6, background: "#064e3b", color: "#34d399", fontSize: 11 }}>
{result}
</div>
)}
{error && (
<div style={{ padding: 8, borderRadius: 6, background: "#450a0a", color: "#fca5a5", fontSize: 11 }}>
{error}
</div>
)}
</div>
)
}