Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Manage on-chain EVM and Solana wallets: balances, transfers, message signing, and transaction history via Privy Server Wallets.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
utils/propose.mjs
1import { createPublicClient, http, encodeFunctionData, hashTypedData, getAddress } from 'viem';2import { privateKeyToAccount } from 'viem/accounts';3import qrcode from 'qrcode-terminal';45// ----- Generic Safe transaction proposer -----6//7// Required env vars:8// CHAIN_ID — 143 (mainnet) or 10143 (testnet)9// SAFE_ADDRESS — checksummed Safe multisig address10// PRIVATE_KEY — agent wallet private key11//12// Transaction mode (pick ONE):13//14// A) Contract deployment (delegatecall into CreateCall):15// DEPLOYMENT_BYTECODE — raw creation bytecode16//17// B) Arbitrary transaction (direct call to any contract):18// TX_TO — target contract address19// TX_DATA — encoded calldata (hex)20// TX_VALUE — value in wei (optional, defaults to "0")2122const NETWORKS = {23143: { rpcUrl: 'https://rpc.monad.xyz', txService: 'https://api.safe.global/tx-service/monad/api/v1', safePrefix: 'monad' },2410143: { rpcUrl: 'https://testnet-rpc.monad.xyz', txService: 'https://api.safe.global/tx-service/monad-testnet/api/v1', safePrefix: 'monad-testnet' },25};2627const CREATE_CALL_ADDRESS = '0x9b35Af71d77eaf8d7e40252370304687390A1A52';2829const CHAIN_ID = Number(process.env.CHAIN_ID);30const network = NETWORKS[CHAIN_ID];31if (!network) {32console.error(`❌ Unsupported CHAIN_ID: ${CHAIN_ID}. Use 143 (mainnet) or 10143 (testnet).`);33process.exit(1);34}3536const SAFE_ADDRESS = getAddress(process.env.SAFE_ADDRESS);3738function buildTransaction() {39// Mode A: contract deployment via CreateCall40if (process.env.DEPLOYMENT_BYTECODE) {41const createCallData = encodeFunctionData({42abi: [{43name: 'performCreate',44type: 'function',45inputs: [{ name: 'value', type: 'uint256' }, { name: 'deploymentData', type: 'bytes' }],46outputs: [{ name: '', type: 'address' }],47}],48functionName: 'performCreate',49args: [0n, process.env.DEPLOYMENT_BYTECODE],50});51return { to: CREATE_CALL_ADDRESS, value: '0', data: createCallData, operation: 1 }; // DELEGATECALL52}5354// Mode B: arbitrary contract call55if (process.env.TX_TO && process.env.TX_DATA) {56return {57to: getAddress(process.env.TX_TO),58value: process.env.TX_VALUE || '0',59data: process.env.TX_DATA,60operation: 0, // CALL61};62}6364console.error('❌ Provide either DEPLOYMENT_BYTECODE (deploy) or TX_TO + TX_DATA (contract call).');65process.exit(1);66}6768async function main() {69const account = privateKeyToAccount(process.env.PRIVATE_KEY);70console.log(`✅ Agent address: ${account.address}`);71console.log(`✅ Network: ${network.safePrefix} (chain ${CHAIN_ID})`);7273const publicClient = createPublicClient({74transport: http(network.rpcUrl),75});7677const nonce = await publicClient.readContract({78address: SAFE_ADDRESS,79abi: [{ name: 'nonce', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint256' }] }],80functionName: 'nonce',81});8283console.log(`✅ Safe nonce: ${nonce}`);8485const { to, value, data, operation } = buildTransaction();8687const txData = {88to,89value,90data,91operation,92safeTxGas: '0',93baseGas: '0',94gasPrice: '0',95gasToken: '0x0000000000000000000000000000000000000000',96refundReceiver: '0x0000000000000000000000000000000000000000',97nonce: nonce.toString(),98};99100const domain = {101chainId: CHAIN_ID,102verifyingContract: SAFE_ADDRESS,103};104105const types = {106SafeTx: [107{ name: 'to', type: 'address' },108{ name: 'value', type: 'uint256' },109{ name: 'data', type: 'bytes' },110{ name: 'operation', type: 'uint8' },111{ name: 'safeTxGas', type: 'uint256' },112{ name: 'baseGas', type: 'uint256' },113{ name: 'gasPrice', type: 'uint256' },114{ name: 'gasToken', type: 'address' },115{ name: 'refundReceiver', type: 'address' },116{ name: 'nonce', type: 'uint256' },117],118};119120// Sign with EIP-712121console.log('✍️ Signing with EIP-712...');122const signature = await account.signTypedData({123domain,124types,125primaryType: 'SafeTx',126message: txData,127});128129const txHash = hashTypedData({130domain,131types,132primaryType: 'SafeTx',133message: txData,134});135136console.log(`✅ Transaction hash: ${txHash}`);137console.log(`✅ Agent signed (1/2)`);138139// POST to Transaction Service API140console.log('📤 Posting to Transaction Service API...');141const response = await fetch(`${network.txService}/safes/${SAFE_ADDRESS}/multisig-transactions/`, {142method: 'POST',143headers: { 'Content-Type': 'application/json' },144body: JSON.stringify({145...txData,146contractTransactionHash: txHash,147sender: account.address,148signature,149}),150});151152if (response.ok) {153const safeUrl = `https://app.safe.global/transactions/queue?safe=${network.safePrefix}:${SAFE_ADDRESS}`;154console.log('✅ Transaction proposed successfully!');155console.log('');156console.log('🎉 Transaction appears in Safe UI queue!');157console.log('');158console.log('Scan QR code to approve on mobile:');159console.log('');160qrcode.generate(safeUrl, { small: true });161console.log('');162console.log('User can now:');163console.log(`1. Open: ${safeUrl}`);164console.log('2. See pending transaction (Agent already signed 1/2)');165console.log('3. Sign with their wallet (2/2)');166console.log('4. Execute');167} else {168const error = await response.text();169console.error(`❌ API Error: ${response.status}`);170console.error(error);171process.exit(1);172}173}174175main();