Merge pull request #66 from gnosis/development
PoC: Use EIP-712 signedTypedData in Web3's HttpProvideras signer with PersonalEdition contracts
This commit is contained in:
commit
f355ae1b26
|
@ -1,10 +1,16 @@
|
|||
// @flow
|
||||
import { TX_SERVICE_HOST, ENABLED_TX_SERVICE_MODULES, ENABLED_TX_SERVICE_REMOVAL_SENDER } from '~/config/names'
|
||||
import {
|
||||
TX_SERVICE_HOST,
|
||||
ENABLED_TX_SERVICE_MODULES,
|
||||
ENABLED_TX_SERVICE_REMOVAL_SENDER,
|
||||
SIGNATURES_VIA_METAMASK,
|
||||
} from '~/config/names'
|
||||
|
||||
const devConfig = {
|
||||
[TX_SERVICE_HOST]: 'http://localhost:8000/api/v1/',
|
||||
[ENABLED_TX_SERVICE_MODULES]: false,
|
||||
[ENABLED_TX_SERVICE_REMOVAL_SENDER]: false,
|
||||
[SIGNATURES_VIA_METAMASK]: false,
|
||||
}
|
||||
|
||||
export default devConfig
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
// @flow
|
||||
import { ensureOnce } from '~/utils/singleton'
|
||||
import { TX_SERVICE_HOST, ENABLED_TX_SERVICE_MODULES, ENABLED_TX_SERVICE_REMOVAL_SENDER } from '~/config/names'
|
||||
import {
|
||||
TX_SERVICE_HOST,
|
||||
ENABLED_TX_SERVICE_MODULES,
|
||||
ENABLED_TX_SERVICE_REMOVAL_SENDER,
|
||||
SIGNATURES_VIA_METAMASK,
|
||||
} from '~/config/names'
|
||||
import devConfig from './development'
|
||||
import testConfig from './testing'
|
||||
import prodConfig from './production'
|
||||
|
@ -38,3 +43,9 @@ export const allowedRemoveSenderInTxHistoryService = () => {
|
|||
|
||||
return config[ENABLED_TX_SERVICE_REMOVAL_SENDER]
|
||||
}
|
||||
|
||||
export const signaturesViaMetamask = () => {
|
||||
const config = getConfig()
|
||||
|
||||
return config[SIGNATURES_VIA_METAMASK]
|
||||
}
|
||||
|
|
|
@ -3,3 +3,4 @@
|
|||
export const TX_SERVICE_HOST = 'tsh'
|
||||
export const ENABLED_TX_SERVICE_MODULES = 'tsm'
|
||||
export const ENABLED_TX_SERVICE_REMOVAL_SENDER = 'trs'
|
||||
export const SIGNATURES_VIA_METAMASK = 'svm'
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
// @flow
|
||||
import { TX_SERVICE_HOST, ENABLED_TX_SERVICE_MODULES, ENABLED_TX_SERVICE_REMOVAL_SENDER } from '~/config/names'
|
||||
import {
|
||||
TX_SERVICE_HOST,
|
||||
ENABLED_TX_SERVICE_MODULES,
|
||||
ENABLED_TX_SERVICE_REMOVAL_SENDER,
|
||||
SIGNATURES_VIA_METAMASK,
|
||||
} from '~/config/names'
|
||||
|
||||
const prodConfig = {
|
||||
[TX_SERVICE_HOST]: 'https://safe-transaction-history.dev.gnosisdev.com/api/v1/',
|
||||
[ENABLED_TX_SERVICE_MODULES]: false,
|
||||
[ENABLED_TX_SERVICE_REMOVAL_SENDER]: false,
|
||||
[SIGNATURES_VIA_METAMASK]: false,
|
||||
}
|
||||
|
||||
export default prodConfig
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
// @flow
|
||||
import { TX_SERVICE_HOST, ENABLED_TX_SERVICE_MODULES, ENABLED_TX_SERVICE_REMOVAL_SENDER } from '~/config/names'
|
||||
import {
|
||||
TX_SERVICE_HOST,
|
||||
ENABLED_TX_SERVICE_MODULES,
|
||||
ENABLED_TX_SERVICE_REMOVAL_SENDER,
|
||||
SIGNATURES_VIA_METAMASK,
|
||||
} from '~/config/names'
|
||||
|
||||
const testConfig = {
|
||||
[TX_SERVICE_HOST]: 'http://localhost:8000/api/v1/',
|
||||
[ENABLED_TX_SERVICE_MODULES]: false,
|
||||
[ENABLED_TX_SERVICE_REMOVAL_SENDER]: false,
|
||||
[SIGNATURES_VIA_METAMASK]: false,
|
||||
}
|
||||
|
||||
export default testConfig
|
||||
|
|
|
@ -4,10 +4,12 @@ import { ensureOnce } from '~/utils/singleton'
|
|||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
import { promisify } from '~/utils/promisify'
|
||||
import GnosisSafeSol from '#/GnosisSafeTeamEdition.json'
|
||||
import GnosisPersonalSafeSol from '#/GnosisSafePersonalEdition.json'
|
||||
import ProxyFactorySol from '#/ProxyFactory.json'
|
||||
import CreateAndAddModules from '#/CreateAndAddModules.json'
|
||||
import DailyLimitModule from '#/DailyLimitModule.json'
|
||||
import { calculateGasOf, calculateGasPrice, EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||
import { signaturesViaMetamask } from '~/config'
|
||||
|
||||
let proxyFactoryMaster
|
||||
let createAndAddModuleMaster
|
||||
|
@ -30,8 +32,14 @@ function createAndAddModulesData(dataArray) {
|
|||
return dataArray.reduce((acc, data) => acc + mw.setup.getData(data).substr(74), EMPTY_DATA)
|
||||
}
|
||||
|
||||
|
||||
const createGnosisSafeContract = (web3: any) => {
|
||||
if (signaturesViaMetamask()) {
|
||||
const gnosisSafe = contract(GnosisPersonalSafeSol)
|
||||
gnosisSafe.setProvider(web3.currentProvider)
|
||||
|
||||
return gnosisSafe
|
||||
}
|
||||
|
||||
const gnosisSafe = contract(GnosisSafeSol)
|
||||
gnosisSafe.setProvider(web3.currentProvider)
|
||||
|
||||
|
|
|
@ -3,6 +3,9 @@ import { calculateGasOf, checkReceiptStatus, calculateGasPrice } from '~/logic/w
|
|||
import { type Operation, submitOperation } from '~/logic/safe/safeTxHistory'
|
||||
import { getDailyLimitModuleFrom } from '~/logic/contracts/dailyLimitContracts'
|
||||
import { getSafeEthereumInstance } from '~/logic/safe/safeFrontendOperations'
|
||||
import { generateMetamaskSignature, generateTxGasEstimateFrom, estimateDataGas } from '~/logic/safe/safeTxSigner'
|
||||
import { storeSignature, getSignaturesFrom } from '~/utils/localStorage/signatures'
|
||||
import { signaturesViaMetamask } from '~/config'
|
||||
|
||||
export const approveTransaction = async (
|
||||
safeAddress: string,
|
||||
|
@ -15,6 +18,14 @@ export const approveTransaction = async (
|
|||
) => {
|
||||
const gasPrice = await calculateGasPrice()
|
||||
|
||||
if (signaturesViaMetamask()) {
|
||||
const safe = await getSafeEthereumInstance(safeAddress)
|
||||
const txGasEstimate = await generateTxGasEstimateFrom(safe, safeAddress, data, to, valueInWei, operation)
|
||||
const signature =
|
||||
await generateMetamaskSignature(safe, safeAddress, sender, to, valueInWei, nonce, data, operation, txGasEstimate)
|
||||
storeSignature(safeAddress, nonce, signature)
|
||||
}
|
||||
|
||||
const gnosisSafe = await getSafeEthereumInstance(safeAddress)
|
||||
const txData = gnosisSafe.contract.approveTransactionWithParameters.getData(to, valueInWei, data, operation, nonce)
|
||||
const gas = await calculateGasOf(txData, sender, safeAddress)
|
||||
|
@ -39,6 +50,40 @@ export const executeTransaction = async (
|
|||
) => {
|
||||
const gasPrice = await calculateGasPrice()
|
||||
|
||||
if (signaturesViaMetamask()) {
|
||||
const safe = await getSafeEthereumInstance(safeAddress)
|
||||
const txGasEstimate = await generateTxGasEstimateFrom(safe, safeAddress, data, to, valueInWei, operation)
|
||||
const signature =
|
||||
await generateMetamaskSignature(safe, safeAddress, sender, to, valueInWei, nonce, data, operation, txGasEstimate)
|
||||
storeSignature(safeAddress, nonce, signature)
|
||||
|
||||
const sigs = getSignaturesFrom(safeAddress, nonce)
|
||||
|
||||
const threshold = await safe.getThreshold()
|
||||
const gas = await estimateDataGas(safe, to, valueInWei, data, operation, txGasEstimate, 0, nonce, Number(threshold))
|
||||
const numOwners = await safe.getOwners()
|
||||
const gasIncludingRemovingStoreUpfront = gas + txGasEstimate + (numOwners.length * 15000)
|
||||
|
||||
const txReceipt = await safe.execTransactionAndPaySubmitter(
|
||||
to,
|
||||
valueInWei,
|
||||
data,
|
||||
operation,
|
||||
txGasEstimate,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
sigs,
|
||||
{ from: sender, gas: gasIncludingRemovingStoreUpfront, gasPrice },
|
||||
)
|
||||
|
||||
const txHash = txReceipt.tx
|
||||
await checkReceiptStatus(txHash)
|
||||
// await submitOperation(safeAddress, to, valueInWei, data, operation, nonce, txHash, sender, 'execution')
|
||||
|
||||
return txHash
|
||||
}
|
||||
|
||||
const gnosisSafe = await getSafeEthereumInstance(safeAddress)
|
||||
const txConfirmationData =
|
||||
gnosisSafe.contract.execTransactionIfApproved.getData(to, valueInWei, data, operation, nonce)
|
||||
|
|
|
@ -0,0 +1,161 @@
|
|||
// @flow
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
import { promisify } from '~/utils/promisify'
|
||||
import { BigNumber } from 'bignumber.js'
|
||||
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||
import { getSignaturesFrom } from '~/utils/localStorage/signatures'
|
||||
|
||||
const estimateDataGasCosts = (data) => {
|
||||
const reducer = (accumulator, currentValue) => {
|
||||
if (currentValue === EMPTY_DATA) {
|
||||
return accumulator + 0
|
||||
}
|
||||
|
||||
if (currentValue === '00') {
|
||||
return accumulator + 4
|
||||
}
|
||||
|
||||
return accumulator + 68
|
||||
}
|
||||
|
||||
return data.match(/.{2}/g).reduce(reducer, 0)
|
||||
}
|
||||
|
||||
|
||||
export const estimateDataGas = (
|
||||
safe: any,
|
||||
to: string,
|
||||
valueInWei: number,
|
||||
data: string,
|
||||
operation: number,
|
||||
txGasEstimate: number,
|
||||
gasToken: number,
|
||||
nonce: number,
|
||||
signatureCount: number,
|
||||
) => {
|
||||
// numbers < 256 are 192 -> 31 * 4 + 68
|
||||
// numbers < 65k are 256 -> 30 * 4 + 2 * 68
|
||||
// For signature array length and dataGasEstimate we already calculated
|
||||
// the 0 bytes so we just add 64 for each non-zero byte
|
||||
const gasPrice = 0 // no need to get refund when we submit txs to metamask
|
||||
const signatureCost = signatureCount * (68 + 2176 + 2176) // array count (3 -> r, s, v) * signature count
|
||||
|
||||
const sigs = getSignaturesFrom(safe.address, nonce)
|
||||
const payload = safe.contract.execTransactionAndPaySubmitter
|
||||
.getData(to, valueInWei, data, operation, txGasEstimate, 0, gasPrice, gasToken, sigs)
|
||||
|
||||
let dataGasEstimate = estimateDataGasCosts(payload) + signatureCost
|
||||
if (dataGasEstimate > 65536) {
|
||||
dataGasEstimate += 64
|
||||
} else {
|
||||
dataGasEstimate += 128
|
||||
}
|
||||
return dataGasEstimate + 34000 // Add aditional gas costs (e.g. base tx costs, transfer costs)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
export const generateTxGasEstimateFrom = async (
|
||||
safe: any,
|
||||
safeAddress: string,
|
||||
data: string,
|
||||
to: string,
|
||||
valueInWei: number,
|
||||
operation: number,
|
||||
) => {
|
||||
try {
|
||||
const estimateData = safe.contract.requiredTxGas.getData(to, valueInWei, data, operation)
|
||||
const estimateResponse = await promisify(cb => getWeb3().eth.call({
|
||||
to: safeAddress,
|
||||
from: safeAddress,
|
||||
data: estimateData,
|
||||
}, cb))
|
||||
const txGasEstimate = new BigNumber(estimateResponse.substring(138), 16)
|
||||
|
||||
// Add 10k else we will fail in case of nested calls
|
||||
return Promise.resolve(txGasEstimate.toNumber() + 10000)
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line
|
||||
console.log("Error calculating tx gas estimation " + error)
|
||||
return Promise.resolve(0)
|
||||
}
|
||||
}
|
||||
|
||||
const generateTypedDataFrom = async (
|
||||
safe: any,
|
||||
safeAddress: string,
|
||||
to: string,
|
||||
valueInWei: number,
|
||||
nonce: number,
|
||||
data: string,
|
||||
operation: number,
|
||||
txGasEstimate: number,
|
||||
) => {
|
||||
const txGasToken = 0
|
||||
// const threshold = await safe.getThreshold()
|
||||
// estimateDataGas(safe, to, valueInWei, data, operation, txGasEstimate, txGasToken, nonce, threshold)
|
||||
const dataGasEstimate = 0
|
||||
const gasPrice = 0
|
||||
const typedData = {
|
||||
types: {
|
||||
EIP712Domain: [
|
||||
{
|
||||
type: 'address',
|
||||
name: 'verifyingContract',
|
||||
},
|
||||
],
|
||||
PersonalSafeTx: [
|
||||
{ type: 'address', name: 'to' },
|
||||
{ type: 'uint256', name: 'value' },
|
||||
{ type: 'bytes', name: 'data' },
|
||||
{ type: 'uint8', name: 'operation' },
|
||||
{ type: 'uint256', name: 'safeTxGas' },
|
||||
{ type: 'uint256', name: 'dataGas' },
|
||||
{ type: 'uint256', name: 'gasPrice' },
|
||||
{ type: 'address', name: 'gasToken' },
|
||||
{ type: 'uint256', name: 'nonce' },
|
||||
],
|
||||
},
|
||||
domain: {
|
||||
verifyingContract: safeAddress,
|
||||
},
|
||||
primaryType: 'PersonalSafeTx',
|
||||
message: {
|
||||
to,
|
||||
value: valueInWei,
|
||||
data,
|
||||
operation,
|
||||
safeTxGas: txGasEstimate,
|
||||
dataGas: dataGasEstimate,
|
||||
gasPrice,
|
||||
gasToken: txGasToken,
|
||||
nonce,
|
||||
},
|
||||
}
|
||||
|
||||
return typedData
|
||||
}
|
||||
|
||||
export const generateMetamaskSignature = async (
|
||||
safe: any,
|
||||
safeAddress: string,
|
||||
sender: string,
|
||||
to: string,
|
||||
valueInWei: number,
|
||||
nonce: number,
|
||||
data: string,
|
||||
operation: number,
|
||||
txGasEstimate: number,
|
||||
) => {
|
||||
const web3 = getWeb3()
|
||||
const typedData =
|
||||
await generateTypedDataFrom(safe, safeAddress, to, valueInWei, nonce, data, operation, txGasEstimate)
|
||||
const signedTypedData = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'eth_signTypedData',
|
||||
params: [sender, typedData],
|
||||
id: Date.now(),
|
||||
}
|
||||
const txSignedResponse = await promisify(cb => web3.currentProvider.sendAsync(signedTypedData, cb))
|
||||
|
||||
return txSignedResponse.result.replace(EMPTY_DATA, '')
|
||||
}
|
|
@ -12,6 +12,7 @@ import { promisify } from '~/utils/promisify'
|
|||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
import { safeTransactionsSelector } from '~/routes/safe/store/selectors'
|
||||
import fetchSafe from '~/routes/safe/store/actions/fetchSafe'
|
||||
import { signaturesViaMetamask } from '~/config'
|
||||
import { testTransactionFrom, testSizeOfTransactions } from './utils/historyServiceHelper'
|
||||
|
||||
|
||||
|
@ -34,7 +35,7 @@ describe('Transactions Suite', () => {
|
|||
const gnosisSafe = await getSafeEthereumInstance(safeAddress)
|
||||
const firstTxData = gnosisSafe.contract.addOwnerWithThreshold.getData(accounts[1], 2)
|
||||
const executor = accounts[0]
|
||||
const nonce = Date.now()
|
||||
const nonce = signaturesViaMetamask() ? await gnosisSafe.nonce() : Date.now()
|
||||
const firstTxHash = await createTransaction(safe, 'Add Owner Second account', safeAddress, 0, nonce, executor, firstTxData)
|
||||
await store.dispatch(fetchSafe(safe))
|
||||
safe = getSafeFrom(store.getState(), safeAddress)
|
||||
|
|
|
@ -4,6 +4,9 @@ import abi from 'ethereumjs-abi'
|
|||
import { promisify } from '~/utils/promisify'
|
||||
|
||||
/*
|
||||
console.log(`to[${to}] \n\n valieInWei[${valueInWei}] \n\n
|
||||
data[${data}] \n\n operation[${operation}] \n\n sigs[${sigs}]`)
|
||||
|
||||
const gnosisSafe = await getSafeEthereumInstance(address)
|
||||
await printOutApprove("Remove owner 3", address, await gnosisSafe.getOwners(), tx.get('data'), tx.get('nonce'))
|
||||
const txData =
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
// @flow
|
||||
import { Map } from 'immutable'
|
||||
import { load } from '~/utils/localStorage'
|
||||
|
||||
const getSignaturesKeyFrom = (safeAddress: string) => `TXS-SIGNATURES-${safeAddress}`
|
||||
|
||||
export const storeSignature = (safeAddress: string, nonce: number, signature: string) => {
|
||||
const signaturesKey = getSignaturesKeyFrom(safeAddress)
|
||||
const subjects = Map(load(signaturesKey)) || Map()
|
||||
|
||||
try {
|
||||
const key = `${nonce}`
|
||||
const existingSignatures = subjects.get(key)
|
||||
const signatures = existingSignatures ? existingSignatures + signature : signature
|
||||
const updatedSubjects = subjects.set(key, signatures)
|
||||
const serializedState = JSON.stringify(updatedSubjects)
|
||||
localStorage.setItem(signaturesKey, serializedState)
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
console.log('Error storing signatures in localstorage')
|
||||
}
|
||||
}
|
||||
|
||||
export const getSignaturesFrom = (safeAddress: string, nonce: number) => {
|
||||
const key = getSignaturesKeyFrom(safeAddress)
|
||||
const data: any = load(key)
|
||||
|
||||
const signatures = data ? Map(data) : Map()
|
||||
const txSigs = signatures.get(String(nonce)) || ''
|
||||
|
||||
return `0x${txSigs}`
|
||||
}
|
Loading…
Reference in New Issue