Skip to content

Instantly share code, notes, and snippets.

@rmeissner
Last active March 12, 2023 19:54
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save rmeissner/0fa5719dc6b306ba84ee34bebddc860b to your computer and use it in GitHub Desktop.
Save rmeissner/0fa5719dc6b306ba84ee34bebddc860b to your computer and use it in GitHub Desktop.
Example Safe signature generation with uport eip712 lib
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>")
@ryan-lith
Copy link

@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

{"nonFieldErrors":["Signature=0xf4a407d06a23c4e30c31e2e029cb99af1f6d86ce56e84ef3e5fddb142d04d0fd4225808f1af100e0cf96a7cac2b051e86e7679d8b1883d2b33f280fedda55e6c00 for owner=0x29Cb99AF1F6d86CE56E84eF3e5fDdb142d04D0fD is not valid"]}

@eranrund
Copy link

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.

@lukeledet
Copy link

I had to add the chainId to the EIP712Domain when using rinkeby.

const safeDomain = new EIP712Domain({
  verifyingContract: safe,
  chainId: 4
});

@0xGorilla
Copy link

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment