import { useState } from "react";
import { ethers } from "ethers";
import { Relayer } from "@abstraxn/relayer";
/* ================= CONFIG ================= */
const RPC_URL = "";
const SAFE_ADDRESS = "";
const TARGET = "";
const CHAIN_ID = 137n;
const OWNER1_PK = "";
const OWNER2_PK = "";
/* ================= SAFE ABI ================= */
const SAFE_ABI = [
"function nonce() view returns (uint256)",
"function execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes) payable returns (bool)",
];
/* ================= TYPES ================= */
type SafeTx = {
to: string;
value: bigint;
data: string;
operation: number;
safeTxGas: bigint;
baseGas: bigint;
gasPrice: bigint;
gasToken: string;
refundReceiver: string;
nonce: bigint;
};
type SignatureItem = {
signer: string;
signature: string;
};
/* ================= EIP-712 TYPE ================= */
type TypedDataField = {
name: string;
type: string;
};
const SAFE_TX_TYPE: {
SafeTx: TypedDataField[];
} = {
SafeTx: [
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "data", type: "bytes" },
{ name: "operation", type: "uint8" },
{ name: "safeTxGas", type: "uint256" },
{ name: "baseGas", type: "uint256" },
{ name: "gasPrice", type: "uint256" },
{ name: "gasToken", type: "address" },
{ name: "refundReceiver", type: "address" },
{ name: "nonce", type: "uint256" },
],
} as const;
/* ================= HELPERS ================= */
function sortSignatures(sigs: SignatureItem[]): string[] {
return sigs
.sort((a, b) =>
a.signer.toLowerCase().localeCompare(b.signer.toLowerCase()),
)
.map((s) => s.signature);
}
function packSignatures(signatures: string[]): string {
return "0x" + signatures.map((sig) => sig.slice(2)).join("");
}
/* ================= APP ================= */
export default function SafeTx() {
const [txHash, setTxHash] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const executeMetaTx = async (): Promise<void> => {
try {
setLoading(true);
/* ---------- Provider & wallets ---------- */
const provider = new ethers.JsonRpcProvider(RPC_URL);
const owner1 = new ethers.Wallet(OWNER1_PK);
const owner2 = new ethers.Wallet(OWNER2_PK);
const safe = new ethers.Contract(SAFE_ADDRESS, SAFE_ABI, provider);
console.log("safe", safe);
/* ---------- Build SafeTx ---------- */
const nonce: bigint = await safe.nonce();
console.log("nonce", nonce);
const safeTx: SafeTx = {
to: TARGET,
value: 0n,
data: "0x",
operation: 0, // CALL
safeTxGas: 0n,
baseGas: 0n,
gasPrice: 0n,
gasToken: ethers.ZeroAddress,
refundReceiver: ethers.ZeroAddress,
nonce,
};
const domain = {
chainId: CHAIN_ID,
verifyingContract: SAFE_ADDRESS,
};
/* ---------- Owners sign ---------- */
const sig1: string = await owner1.signTypedData(
domain,
SAFE_TX_TYPE,
safeTx,
);
const sig2: string = await owner2.signTypedData(
domain,
SAFE_TX_TYPE,
safeTx,
);
/* ---------- Sort & pack ---------- */
const ordered = sortSignatures([
{ signer: await owner1.getAddress(), signature: sig1 },
{ signer: await owner2.getAddress(), signature: sig2 },
]);
const signatureBytes: string = packSignatures(ordered);
console.log("signatureBytes", signatureBytes);
/* ---------- Execute via relayer ---------- */
const relayerConfig = {
relayerUrl: "",
isSafeTx: true,
webSocket: {
enabled: true,
autoConnect: true,
reconnection: true,
},
};
const relayer = new Relayer(relayerConfig);
await relayer.sendSafeRelayerTxWithRealTimeUpdates({
safeAddress: SAFE_ADDRESS,
safeExecTxPayload: {
safeTx: safeTx,
signatureBytes: signatureBytes,
},
chainId: Number(CHAIN_ID),
enableRealTimeUpdates: true,
webSocketEvents: {
onTransactionUpdate: (update) => {
console.log("update", update);
console.log(`Transaction ${update.txId} status: ${update.status}`);
// Set transaction hash when available
if (update.hash) {
setTxHash(update.hash);
}
if (update.status === "confirmed") {
console.log("🎉 Transaction confirmed!", update.blockNumber);
} else if (
update.status === "failed" ||
update.status === "rejected"
) {
console.error("Transaction failed:", update.reason);
alert(`Transaction failed: ${update.reason || "Unknown error"}`);
}
},
onError: (error) => {
console.error("WebSocket error:", error);
},
},
});
} catch (err) {
console.error(err);
alert("Meta-transaction failed — see console");
} finally {
setLoading(false);
}
};
return (
<div style={{ padding: 20 }}>
<h2>Safe Manual Meta-Tx (2 Owners, TS)</h2>
<button onClick={executeMetaTx} disabled={loading}>
{loading ? "Executing..." : "Execute Meta-Tx"}
</button>
{txHash && (
<p>
Tx Hash:
<br />
<a
href={`https://polygonscan.com/tx/${txHash}`}
target="_blank"
rel="noreferrer"
>
{txHash}
</a>
</p>
)}
</div>
);
}