-
-
Save rmeissner/0fa5719dc6b306ba84ee34bebddc860b to your computer and use it in GitHub Desktop.
import EIP712Domain from "eth-typed-data"; | |
import BigNumber from "bignumber.js"; | |
import * as ethUtil from 'ethereumjs-util'; | |
import { ethers } from "ethers"; | |
import axios from "axios"; | |
/* | |
* Safe relay service example | |
* * * * * * * * * * * * * * * * * * * */ | |
const gnosisEstimateTransaction = async (safe: string, tx: any): Promise<any> => { | |
console.log(JSON.stringify(tx)); | |
try { | |
const resp = await axios.post(`https://safe-relay.rinkeby.gnosis.pm/api/v2/safes/${safe}/transactions/estimate/`, tx) | |
console.log(resp.data) | |
return resp.data | |
} catch (e) { | |
console.log(JSON.stringify(e.response.data)) | |
throw e | |
} | |
} | |
const gnosisSubmitTx = async (safe: string, tx: any): Promise<any> => { | |
try { | |
const resp = await axios.post(`https://safe-relay.rinkeby.gnosis.pm/api/v1/safes/${safe}/transactions/`, tx) | |
console.log(resp.data) | |
return resp.data | |
} catch (e) { | |
console.log(JSON.stringify(e.response.data)) | |
throw e | |
} | |
} | |
const { utils } = ethers; | |
const execute = async (safe, privateKey) => { | |
const safeDomain = new EIP712Domain({ | |
verifyingContract: safe, | |
}); | |
const SafeTx = safeDomain.createType('SafeTx', [ | |
{ type: "address", name: "to" }, | |
{ type: "uint256", name: "value" }, | |
{ type: "bytes", name: "data" }, | |
{ type: "uint8", name: "operation" }, | |
{ type: "uint256", name: "safeTxGas" }, | |
{ type: "uint256", name: "baseGas" }, | |
{ type: "uint256", name: "gasPrice" }, | |
{ type: "address", name: "gasToken" }, | |
{ type: "address", name: "refundReceiver" }, | |
{ type: "uint256", name: "nonce" }, | |
]); | |
const to = utils.getAddress("0x0ebd146ffd9e20bf74e374e5f3a5a567a798177e"); | |
const baseTxn = { | |
to, | |
value: "1000", | |
data: "0x", | |
operation: "0", | |
}; | |
console.log(JSON.stringify({ baseTxn })); | |
const { safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, lastUsedNonce } = await gnosisEstimateTransaction( | |
safe, | |
baseTxn, | |
); | |
const txn = { | |
...baseTxn, | |
safeTxGas, | |
baseGas, | |
gasPrice, | |
gasToken, | |
nonce: lastUsedNonce === undefined ? 0 : lastUsedNonce + 1, | |
refundReceiver: refundReceiver || "0x0000000000000000000000000000000000000000", | |
}; | |
console.log({txn}) | |
const safeTx = new SafeTx({ | |
...txn, | |
data: utils.arrayify(txn.data) | |
}); | |
const signer = async data => { | |
let { r, s, v } = ethUtil.ecsign(data, ethUtil.toBuffer(privateKey)); | |
return { | |
r: new BigNumber(r.toString('hex'), 16).toString(10), | |
s: new BigNumber(s.toString('hex'), 16).toString(10), | |
v | |
} | |
} | |
const signature = await safeTx.sign(signer); | |
console.log({ signature }); | |
const toSend = { | |
...txn, | |
dataGas: baseGas, | |
signatures: [signature], | |
}; | |
console.log(JSON.stringify({ toSend })); | |
const { data } = await gnosisSubmitTx(safe, toSend); | |
console.log({data}) | |
console.log("Done?"); | |
} | |
// This example uses the relay service to execute a transaction for a Safe | |
execute("<safe>", "<0x_signer_private_key>") | |
/* | |
* Safe transaction service example | |
* * * * * * * * * * * * * * * * * * * */ | |
const gnosisProposeTx = async (safe: string, tx: any): Promise<any> => { | |
try { | |
const resp = await axios.post(`https://safe-transaction.rinkeby.gnosis.io/api/v1/safes/${safe}/transactions/`, tx) | |
console.log(resp.data) | |
return resp.data | |
} catch (e) { | |
if (e.response) console.log(JSON.stringify(e.response.data)) | |
throw e | |
} | |
} | |
const submit = async (safe, sender, privateKey) => { | |
const safeDomain = new EIP712Domain({ | |
verifyingContract: safe, | |
}); | |
const SafeTx = safeDomain.createType('SafeTx', [ | |
{ type: "address", name: "to" }, | |
{ type: "uint256", name: "value" }, | |
{ type: "bytes", name: "data" }, | |
{ type: "uint8", name: "operation" }, | |
{ type: "uint256", name: "safeTxGas" }, | |
{ type: "uint256", name: "baseGas" }, | |
{ type: "uint256", name: "gasPrice" }, | |
{ type: "address", name: "gasToken" }, | |
{ type: "address", name: "refundReceiver" }, | |
{ type: "uint256", name: "nonce" }, | |
]); | |
const to = utils.getAddress("0x0ebd146ffd9e20bf74e374e5f3a5a567a798177e"); | |
const baseTxn = { | |
to, | |
value: "1000", | |
data: "0x", | |
operation: "0", | |
}; | |
console.log(JSON.stringify({ baseTxn })); | |
// Let the Safe service estimate the tx and retrieve the nonce | |
const { safeTxGas, lastUsedNonce } = await gnosisEstimateTransaction( | |
safe, | |
baseTxn, | |
); | |
const txn = { | |
...baseTxn, | |
safeTxGas, | |
// Here we can also set any custom nonce | |
nonce: lastUsedNonce === undefined ? 0 : lastUsedNonce + 1, | |
// We don't want to use the refund logic of the safe to lets use the default values | |
baseGas: 0, | |
gasPrice: 0, | |
gasToken: "0x0000000000000000000000000000000000000000", | |
refundReceiver: "0x0000000000000000000000000000000000000000", | |
}; | |
console.log({txn}) | |
const safeTx = new SafeTx({ | |
...txn, | |
data: utils.arrayify(txn.data) | |
}); | |
const signer = async data => { | |
let { r, s, v } = ethUtil.ecsign(data, ethUtil.toBuffer(privateKey)); | |
return ethUtil.toRpcSig(v, r, s) | |
} | |
const signature = await safeTx.sign(signer); | |
console.log({ signature }); | |
const toSend = { | |
...txn, | |
sender, | |
contractTransactionHash: "0x" + safeTx.signHash().toString('hex'), | |
signature: signature, | |
}; | |
console.log(JSON.stringify({ toSend })); | |
const { data } = await gnosisProposeTx(safe, toSend); | |
console.log({data}) | |
console.log("Done?"); | |
} | |
// This example uses the transaction service to propose a transaction to the Safe Multisig interface | |
submit("<safe>", "<0x_signer_address>", "<0x_signer_private_key>") |
I am running into an issue where I get an error message that says Signer=... is not an owner or delegate.
, it appears that something is off with the signature generation but I am not sure what. I also had to use a hardcoded contractTransactionHash
since safeTx.signHash()
returns the wrong signature for some reason.
I had to add the chainId to the EIP712Domain when using rinkeby.
const safeDomain = new EIP712Domain({
verifyingContract: safe,
chainId: 4
});
I got inspired by this gist and got it to work with npx, now you can propose a tx to a safe by running:
npx @defi-wonderland/gnosis-safe-proposor --safe YOUR_SAFE_ADDRESS --to YOUR_TARGET_ADDRESS --data YOUR_TX_DATA
The open-sourced code can be found at: https://github.com/defi-wonderland/gnosis-safe-proposor
Please let me know if you have any input, let's improve it together!
@rmeissner Hi, I'm running into this error when POSTing to that transactions endpoint. I generated the signature using your code and I'm not sure where the owner "0x29Cb99AF1F6d86CE56E84eF3e5fDdb142d04D0fD" came from