if (!props.apiBaseUrl || !props.safeAddress) return "[New Transaction] One of the following props is missing: apiBaseUrl, safeAddress"; State.init({ recipient: "", value: Number(0), //initialized to 0 to avoid ethers complaints and enable valueless tx contract: "", tokenDecimals: 0, data: "0x", // transaction calldata operation: 0, gasToken: "0x0000000000000000000000000000000000000000", safeTxGas: 0, baseGas: 0, gasPrice: 0, refundReceiver: "0x0000000000000000000000000000000000000000", nonce: 0, txHash: "0x", sender: null, signature: "", origin: "NEAR Blockchain Operating System", }); if (state.sender === null) { const accounts = Ethers.send("eth_requestAccounts", []); if (accounts.length) { const checksummedAddr = ethers.utils.getAddress(accounts[0]); State.update({ sender: checksummedAddr }); } else { return <Web3Connect />; } } const getAbi = () => { // fetch abi const url = "https://gist.githubusercontent.com/veox/8800debbf56e24718f9f483e1e40c35c/raw/f853187315486225002ba56e5283c1dba0556e6f/erc20.abi.json"; const erc20Abi = fetch(url); let iface; if (erc20Abi.ok) iface = new ethers.utils.Interface(erc20Abi.body); // get token decimals, parse units via decimals const encodedData = iface.encodeFunctionData("decimals", []); Ethers.provider() .call({ to: state.contract, data: encodedData, }) .then((tokenDecimals) => { State.update({ tokenDecimals: parseInt(Number(tokenDecimals)) }); }); const amount = ethers.utils.parseUnits( state.value.toString(), state.tokenDecimals ); State.update({ data: iface.encodeFunctionData("transfer", [ state.recipient.toString(), amount, ]), }); }; const getNonce = (_contract, _addr, _to, _value) => { // support ERC20 tokens const contract = _contract; if (contract) getAbi(); const addr = ethers.utils.getAddress(_addr); // convert input addrs to checksum const to = ethers.utils.getAddress(_to); const value = Number(_value); State.update({ safeAddress: addr }); State.update({ recipient: to }); State.update({ value: value }); const url = props.apiBaseUrl + `api/v1/safes/${addr}/`; // http options const options = { headers: { accept: "application/json", }, mode: "no-cors", }; // get nonce const res = fetch(url, options); State.update({ nonce: res.body.nonce }); }; const getAndSignTxHash = () => { // get txhash from contract // The Gnosis Safe contract ABI const safeAbi = fetch( "https://raw.githubusercontent.com/safe-global/safe-deployments/main/src/assets/v1.3.0/gnosis_safe_l2.json" ).body; const abiJson = JSON.parse(safeAbi)["abi"]; const signer = Ethers.provider().getSigner(); const safe = new ethers.Contract(props.safeAddress, abiJson, signer); // obtain txHash const txHash = safe .getTransactionHash( state.recipient, state.value, state.data, state.operation, state.safeTxGas, state.baseGas, state.gasPrice, state.gasToken, state.refundReceiver, state.nonce ) .then((res) => { State.update({ txHash: res }); // sign contractTransactionHash using private key of Gnosis Safe owner (or deployer) const signature = signer .signMessage(ethers.utils.arrayify(res)) .then((sig) => { const alterV = ethers.utils.hexDataSlice(sig, 0, 64) + "1f"; State.update({ signature: ethers.utils.hexlify(alterV) }); }); }); }; const postToSafeApi = () => { // craft transaction from state vars const transaction = { safe: props.safeAddress, to: state.recipient, value: state.value, data: state.data, operation: state.operation, gasToken: state.gasToken, safeTxGas: state.safeTxGas, baseGas: state.baseGas, gasPrice: state.gasPrice, refundReceiver: state.refundReceiver, nonce: state.nonce, // Nonce of the Safe, transaction cannot be executed until Safe's nonce is accurate contractTransactionHash: state.txHash, // Contract transaction hash calculated from all the fields sender: state.sender, // must be checksummed Owner of the Safe signature: state.signature, // One or more ECDSA signatures of the `contractTransactionHash` as an hex string origin: state.origin, }; const transactionsUrl = props.apiBaseUrl + `/api/v1/safes/${props.safeAddress}/multisig-transactions/`; const params = JSON.stringify(transaction); const proposalOptions = { method: "POST", headers: { "Content-Type": "application/json", }, mode: "no-cors", body: params, }; // post to gnosis API backend asyncFetch(transactionsUrl, proposalOptions).then((res) => console.log(res)); }; const TWStyles = state.styles; const css = fetch( "https://gist.githubusercontent.com/Pikqi/658b6ee444d26dd69f0d5150797077dd/raw/d8f929729176bb30d86e2839443fddb83a87a685/tw-all-classes.css" ); if (!css.ok) { return ( <Widget props={{ color1: "#ef4444", color2: "#7f1d1d", }} src="nui.sking.near/widget/Feedback.Spinner" /> ); } if (!state.styles) { const colors = { primaryGreen: "#00EC97", primaryBlack: "#151718", accentYellow: "#F2FF9F", accentGreen: "#17D9D4", accentBlue: "#3D7FFF", darkGray: "#3E3E3E", lightGray: "#B6B6B6", }; State.update({ styles: styled.div` ${css.body} .bg-primary-black { background-color: ${colors.primaryBlack} } .bg-primary-green { background-color: ${colors.primaryGreen} } .bg-dark-gray { background-color: ${colors.darkGray} } .text-gray { color: ${colors.lightGray} } .text-green { color: ${colors.primaryGreen} } .dot { height: 40px; width: 40px; background-color: #bbb; border-radius: 50%; display: inline-block; } ul { list-style-type: none; } .border-b { border-bottom: 1px solid ${colors.darkGray}; } .border { border: 1px solid ${colors.darkGray}; } .input-border { border: 1px solid ${colors.darkGray}; border-radius: 25px; } .cta { border: 1px solid ${colors.primaryGreen}; border-radius: 25px; background-color: ${colors.primaryBlack} text-align: center; color: ${colors.primaryGreen} } input:active { border: 1px solid ${colors.primaryGreen} background-color: ${colors.primaryGreen} } `, }); } function signButton() { return ( <button onClick={() => getNonce( state.contract, props.safeAddress, state.recipient, state.value ).then(getAndSignTxHash()) } label="SignButton" className="cta px-10 py-2 w-full bg-primary-black" > <span>Sign Transaction</span> </button> ); } function proposeButton() { return ( <button onClick={() => postToSafeApi()} label="ProposeButton" className="cta px-10 py-2 w-full bg-primary-black" > <span>Propose Transaction</span> </button> ); } return ( <TWStyles> <div className="bg-primary-black text-white border"> <h1 className="text-xl font-bold border-b py-3 px-8 text-green"> New Transaction </h1> <div className="flex flex-col"> <div className="border-b px-8 py-4"> <p className="text-gray">Sending from</p> <p>{props.safeAddress}</p> <input value={state.recipient} onChange={(e) => State.update({ recipient: e.target.value })} placeholder="Recipient address" label="RecipientAddressInput" className="w-full px-4 py-2 bg-dark-gray input-border mb-3 text-gray" /> <input value={state.contract} onChange={(e) => State.update({ contract: e.target.value })} placeholder="ERC20 address - leave empty if performing native currency transfer (ETH, MATIC, xDAI)" label="TokenAddressInput" className="w-full px-4 py-2 bg-dark-gray input-border mb-3 text-gray" /> </div> <div className="border-b py-4 px-8"> <input value={state.value} onChange={(e) => State.update({ value: e.target.value })} placeholder="ETH Amount" label="ETHValueInput" className="w-full px-4 py-2 bg-dark-gray input-border mb-3 text-gray" /> {state.signature === "" ? signButton() : proposeButton()} </div> </div> </div> </TWStyles> );