Feature: Offchain signatures (#668)
* offchain signatures wip * offchain signing wip * offchain signatures wip * offchain signatures wip * save signatures to the history service * fix eth signer & useEfefct hook * offchain signatures wip * signature check, mainnet testing wip * dep update * disable offchain signing for smart contract wallets * Refactor EIP712 signer * bring back .env.example * Check if save version is >1.1.1 * use canTryoffchainSigning boolean variable, add comment about 4001 error * move semver selector for safe version/offchain signatures to a constant, make use of empty_data for isContractWallet * remove TYPE when sending txs to history service * add eth_signTypedData_v4 signer, dep bump, add missing await * add comments about version check for canTryOffchainSigning variable * hide "please sign notification" * dep bump * dep bump * Check if connected is ledger before trying offchain signatures * minor fixes, temp deployment to test trezor * add hardwareWallet boolean property to wallet model, disable offchain signatures for hw wallets
This commit is contained in:
parent
c73dafe3ce
commit
a6b70a1663
|
@ -17,3 +17,6 @@ REACT_APP_FORTMATIC_KEY=
|
|||
REACT_APP_LATEST_SAFE_VERSION=
|
||||
# Leave it untouched, version will set using dotenv-expand
|
||||
REACT_APP_APP_VERSION=$npm_package_version
|
||||
|
||||
# all environments
|
||||
REACT_APP_INFURA_TOKEN=
|
66
package.json
66
package.json
|
@ -43,19 +43,19 @@
|
|||
"dependencies": {
|
||||
"@gnosis.pm/safe-contracts": "1.1.1-dev.1",
|
||||
"@gnosis.pm/util-contracts": "2.0.6",
|
||||
"@material-ui/core": "4.9.5",
|
||||
"@material-ui/core": "4.9.8",
|
||||
"@material-ui/icons": "4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.39",
|
||||
"@openzeppelin/contracts": "^2.5.0",
|
||||
"@testing-library/jest-dom": "5.1.1",
|
||||
"@testing-library/jest-dom": "5.3.0",
|
||||
"@welldone-software/why-did-you-render": "4.0.5",
|
||||
"async-sema": "^3.1.0",
|
||||
"axios": "0.19.2",
|
||||
"bignumber.js": "9.0.0",
|
||||
"bnc-onboard": "1.5.0",
|
||||
"connected-react-router": "6.7.0",
|
||||
"connected-react-router": "6.8.0",
|
||||
"currency-flags": "^2.1.1",
|
||||
"date-fns": "2.10.0",
|
||||
"date-fns": "2.11.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"ethereum-ens": "0.8.0",
|
||||
"final-form": "4.18.7",
|
||||
|
@ -63,20 +63,20 @@
|
|||
"immortal-db": "^1.0.2",
|
||||
"immutable": "^4.0.0-rc.9",
|
||||
"js-cookie": "^2.2.1",
|
||||
"lint-staged": "^10.0.7",
|
||||
"lint-staged": "10.0.10",
|
||||
"material-ui-search-bar": "^1.0.0-beta.13",
|
||||
"notistack": "https://github.com/gnosis/notistack.git#v0.9.4",
|
||||
"optimize-css-assets-webpack-plugin": "5.0.3",
|
||||
"polished": "^3.4.2",
|
||||
"polished": "3.5.1",
|
||||
"qrcode.react": "1.0.0",
|
||||
"query-string": "6.11.1",
|
||||
"react": "16.13.0",
|
||||
"react-dev-utils": "^10.0.0",
|
||||
"react-dom": "16.13.0",
|
||||
"react": "16.13.1",
|
||||
"react-dev-utils": "10.2.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-final-form": "6.3.5",
|
||||
"react-final-form-listeners": "^1.0.2",
|
||||
"react-ga": "^2.7.0",
|
||||
"react-hot-loader": "4.12.19",
|
||||
"react-hot-loader": "4.12.20",
|
||||
"react-qr-reader": "^2.2.1",
|
||||
"react-redux": "7.2.0",
|
||||
"react-router-dom": "5.1.2",
|
||||
|
@ -92,7 +92,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.8.4",
|
||||
"@babel/core": "7.8.7",
|
||||
"@babel/core": "7.9.0",
|
||||
"@babel/plugin-proposal-class-properties": "7.8.3",
|
||||
"@babel/plugin-proposal-decorators": "7.8.3",
|
||||
"@babel/plugin-proposal-do-expressions": "7.8.3",
|
||||
|
@ -104,7 +104,7 @@
|
|||
"@babel/plugin-proposal-logical-assignment-operators": "7.8.3",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.8.3",
|
||||
"@babel/plugin-proposal-numeric-separator": "7.8.3",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.8.3",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.9.0",
|
||||
"@babel/plugin-proposal-pipeline-operator": "7.8.3",
|
||||
"@babel/plugin-proposal-throw-expressions": "7.8.3",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
|
@ -112,15 +112,15 @@
|
|||
"@babel/plugin-transform-member-expression-literals": "7.8.3",
|
||||
"@babel/plugin-transform-property-literals": "7.8.3",
|
||||
"@babel/polyfill": "7.8.7",
|
||||
"@babel/preset-env": "7.8.7",
|
||||
"@babel/preset-flow": "7.8.3",
|
||||
"@babel/preset-react": "7.8.3",
|
||||
"@testing-library/react": "9.5.0",
|
||||
"autoprefixer": "9.7.4",
|
||||
"@babel/preset-env": "7.9.0",
|
||||
"@babel/preset-flow": "7.9.0",
|
||||
"@babel/preset-react": "7.9.4",
|
||||
"@testing-library/react": "10.0.1",
|
||||
"autoprefixer": "9.7.5",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-eslint": "10.1.0",
|
||||
"babel-jest": "25.1.0",
|
||||
"babel-loader": "8.0.6",
|
||||
"babel-jest": "25.2.4",
|
||||
"babel-loader": "8.1.0",
|
||||
"babel-plugin-dynamic-import-node": "^2.3.0",
|
||||
"babel-plugin-transform-es3-member-expression-literals": "^6.22.0",
|
||||
"babel-plugin-transform-es3-property-literals": "^6.22.0",
|
||||
|
@ -130,38 +130,38 @@
|
|||
"detect-port": "^1.3.0",
|
||||
"dotenv-expand": "^5.1.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-prettier": "^6.10.0",
|
||||
"eslint-plugin-flowtype": "^4.6.0",
|
||||
"eslint-plugin-import": "^2.20.1",
|
||||
"eslint-config-prettier": "6.10.1",
|
||||
"eslint-plugin-flowtype": "4.7.0",
|
||||
"eslint-plugin-import": "2.20.2",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||
"eslint-plugin-prettier": "^3.1.2",
|
||||
"eslint-plugin-react": "^7.18.3",
|
||||
"eslint-plugin-sort-destructure-keys": "^1.3.3",
|
||||
"ethereumjs-abi": "0.6.8",
|
||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
||||
"file-loader": "5.1.0",
|
||||
"flow-bin": "0.120.1",
|
||||
"fs-extra": "8.1.0",
|
||||
"html-loader": "^0.5.5",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"file-loader": "6.0.0",
|
||||
"flow-bin": "0.121.0",
|
||||
"fs-extra": "9.0.0",
|
||||
"html-loader": "1.0.0",
|
||||
"html-webpack-plugin": "4.0.3",
|
||||
"husky": "^4.2.2",
|
||||
"jest": "25.1.0",
|
||||
"jest": "25.2.4",
|
||||
"jest-dom": "4.0.0",
|
||||
"json-loader": "^0.5.7",
|
||||
"mini-css-extract-plugin": "0.9.0",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"postcss-mixins": "6.2.3",
|
||||
"postcss-simple-vars": "^5.0.2",
|
||||
"prettier": "^1.19.1",
|
||||
"prettier": "2.0.2",
|
||||
"run-with-testrpc": "0.3.1",
|
||||
"style-loader": "1.1.3",
|
||||
"terser-webpack-plugin": "2.3.5",
|
||||
"truffle": "5.1.16",
|
||||
"truffle": "5.1.19",
|
||||
"truffle-contract": "4.0.31",
|
||||
"truffle-solidity-loader": "0.1.32",
|
||||
"url-loader": "3.0.0",
|
||||
"webpack": "4.42.0",
|
||||
"webpack-bundle-analyzer": "3.6.0",
|
||||
"url-loader": "4.0.0",
|
||||
"webpack": "4.42.1",
|
||||
"webpack-bundle-analyzer": "3.6.1",
|
||||
"webpack-cli": "3.3.11",
|
||||
"webpack-dev-server": "3.10.3",
|
||||
"webpack-manifest-plugin": "2.2.0"
|
||||
|
|
|
@ -23,7 +23,7 @@ export const onboard = new Onboard({
|
|||
dappId: BLOCKNATIVE_API_KEY,
|
||||
networkId: getNetworkId(),
|
||||
subscriptions: {
|
||||
wallet: wallet => {
|
||||
wallet: (wallet) => {
|
||||
if (wallet.provider) {
|
||||
// this function will intialize web3 and store it somewhere available throughout the dapp and
|
||||
// can also instantiate your contracts with the web3 instance
|
||||
|
@ -31,7 +31,7 @@ export const onboard = new Onboard({
|
|||
providerName = wallet.name
|
||||
}
|
||||
},
|
||||
address: address => {
|
||||
address: (address) => {
|
||||
if (!lastUsedAddress && address) {
|
||||
lastUsedAddress = address
|
||||
store.dispatch(fetchProvider(providerName))
|
||||
|
|
|
@ -84,7 +84,7 @@ export const ADDRESS_REPEATED_ERROR = 'Address already introduced'
|
|||
|
||||
export const uniqueAddress = (addresses: string[] | List<string>) =>
|
||||
simpleMemoize((value: string) => {
|
||||
const addressAlreadyExists = addresses.some(address => sameAddress(value, address))
|
||||
const addressAlreadyExists = addresses.some((address) => sameAddress(value, address))
|
||||
return addressAlreadyExists ? ADDRESS_REPEATED_ERROR : undefined
|
||||
})
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ export const nftTokensSelector = (state: GlobalState) => state[NFT_TOKENS_REDUCE
|
|||
export const nftAssetsListSelector: Selector<GlobalState, NFTAssets, List<NFTAssets>> = createSelector(
|
||||
nftAssetsSelector,
|
||||
(assets: NFTAssets) => {
|
||||
return assets ? List(Object.entries(assets).map(item => item[1])) : List([])
|
||||
return assets ? List(Object.entries(assets).map((item) => item[1])) : List([])
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -22,7 +22,7 @@ export const activeNftAssetsListSelector: Selector<GlobalState, NFTAssets, List<
|
|||
nftAssetsListSelector,
|
||||
safeActiveAssetsSelector,
|
||||
(assets: List<NFTAssets>, activeAssetsList: Set<string>) => {
|
||||
return assets.filter(asset => activeAssetsList.has(asset.address))
|
||||
return assets.filter((asset) => activeAssetsList.has(asset.address))
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -30,7 +30,7 @@ export const safeActiveSelectorMap: Selector<GlobalState, NFTAssets, List<NFTAss
|
|||
activeNftAssetsListSelector,
|
||||
(activeAssets: List<NFTAssets>) => {
|
||||
let assetsMap = {}
|
||||
activeAssets.forEach(asset => {
|
||||
activeAssets.forEach((asset) => {
|
||||
assetsMap[asset.address] = asset
|
||||
})
|
||||
return assetsMap
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
export * from './gas'
|
||||
export * from './send'
|
||||
export * from './safeTxSignerEIP712'
|
||||
export * from './offchainSigner'
|
||||
export * from './txHistory'
|
||||
export * from './notifiedTransactions'
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
// @flow
|
||||
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
|
||||
const EIP712_NOT_SUPPORTED_ERROR_MSG = "EIP712 is not supported by user's wallet"
|
||||
|
||||
const generateTypedDataFrom = async ({
|
||||
baseGas,
|
||||
data,
|
||||
gasPrice,
|
||||
gasToken,
|
||||
nonce,
|
||||
operation,
|
||||
refundReceiver,
|
||||
safeAddress,
|
||||
safeTxGas,
|
||||
to,
|
||||
valueInWei,
|
||||
}) => {
|
||||
const typedData = {
|
||||
types: {
|
||||
EIP712Domain: [
|
||||
{
|
||||
type: 'address',
|
||||
name: 'verifyingContract',
|
||||
},
|
||||
],
|
||||
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' },
|
||||
],
|
||||
},
|
||||
domain: {
|
||||
verifyingContract: safeAddress,
|
||||
},
|
||||
primaryType: 'SafeTx',
|
||||
message: {
|
||||
to,
|
||||
value: valueInWei,
|
||||
data,
|
||||
operation,
|
||||
safeTxGas,
|
||||
baseGas,
|
||||
gasPrice,
|
||||
gasToken,
|
||||
refundReceiver,
|
||||
nonce: Number(nonce),
|
||||
},
|
||||
}
|
||||
|
||||
return typedData
|
||||
}
|
||||
|
||||
type EIP712RpcCallVersion = 'v3' | 'v4'
|
||||
|
||||
export const getEIP712Signer = (version: ?EIP712RpcCallVersion) => async (txArgs) => {
|
||||
const web3 = getWeb3()
|
||||
const typedData = await generateTypedDataFrom(txArgs)
|
||||
|
||||
let method = 'eth_signTypedData_v3'
|
||||
if (version === 'v4') {
|
||||
method = 'eth_signTypedData_v4'
|
||||
}
|
||||
if (!version) {
|
||||
method = 'eth_signTypedData'
|
||||
}
|
||||
|
||||
const jsonTypedData = JSON.stringify(typedData)
|
||||
const signedTypedData = {
|
||||
jsonrpc: '2.0',
|
||||
method,
|
||||
params: version === 'v3' || version === 'v4' ? [txArgs.sender, jsonTypedData] : [jsonTypedData, txArgs.sender],
|
||||
from: txArgs.sender,
|
||||
id: new Date().getTime(),
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
web3.currentProvider.sendAsync(signedTypedData, (err, signature) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
|
||||
if (signature.result == null) {
|
||||
reject(new Error(EIP712_NOT_SUPPORTED_ERROR_MSG))
|
||||
return
|
||||
}
|
||||
|
||||
resolve(signature.result.replace(EMPTY_DATA, ''))
|
||||
})
|
||||
})
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
// @flow
|
||||
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
|
||||
const ETH_SIGN_NOT_SUPPORTED_ERROR_MSG = 'ETH_SIGN_NOT_SUPPORTED_ERROR_MSG'
|
||||
|
||||
export const generateEthSignature = async ({
|
||||
baseGas,
|
||||
data,
|
||||
gasPrice,
|
||||
gasToken,
|
||||
nonce,
|
||||
operation,
|
||||
refundReceiver,
|
||||
safeInstance,
|
||||
safeTxGas,
|
||||
sender,
|
||||
to,
|
||||
valueInWei,
|
||||
}) => {
|
||||
const web3 = await getWeb3()
|
||||
const txHash = await safeInstance.getTransactionHash(
|
||||
to,
|
||||
valueInWei,
|
||||
data,
|
||||
operation,
|
||||
safeTxGas,
|
||||
baseGas,
|
||||
gasPrice,
|
||||
gasToken,
|
||||
refundReceiver,
|
||||
nonce,
|
||||
{
|
||||
from: sender,
|
||||
},
|
||||
)
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
web3.currentProvider.sendAsync(
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
method: 'eth_sign',
|
||||
params: [sender, txHash],
|
||||
id: new Date().getTime(),
|
||||
},
|
||||
async function (err, signature) {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
|
||||
if (signature.result == null) {
|
||||
reject(new Error(ETH_SIGN_NOT_SUPPORTED_ERROR_MSG))
|
||||
return
|
||||
}
|
||||
|
||||
resolve(signature.result.replace(EMPTY_DATA, ''))
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
// @flow
|
||||
|
||||
// import { getEIP712Signer } from './EIP712Signer'
|
||||
import { generateEthSignature } from './ethSigner'
|
||||
|
||||
// 1. we try to sign via EIP-712 if user's wallet supports it
|
||||
// 2. If not, try to use eth_sign (Safe version has to be >1.1.1)
|
||||
// If eth_sign, doesn't work continue with the regular flow (on-chain signatures, more in createTransaction.js)
|
||||
|
||||
const signingFuncs = [generateEthSignature]
|
||||
|
||||
export const SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES = '>=1.1.1'
|
||||
|
||||
export const tryOffchainSigning = async (txArgs) => {
|
||||
let signature
|
||||
for (let signingFunc of signingFuncs) {
|
||||
try {
|
||||
signature = await signingFunc(txArgs)
|
||||
|
||||
break
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
// Metamask sign request error code
|
||||
if (err.code === 4001) {
|
||||
throw new Error('User denied sign request')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return signature
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
// @flow
|
||||
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
|
||||
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 refundReceiver = 0
|
||||
const typedData = {
|
||||
types: {
|
||||
EIP712Domain: [
|
||||
{
|
||||
type: 'address',
|
||||
name: 'verifyingContract',
|
||||
},
|
||||
],
|
||||
SafeTx: [
|
||||
{ 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: 'address', name: 'refundReceiver' },
|
||||
{ type: 'uint256', name: 'nonce' },
|
||||
],
|
||||
},
|
||||
domain: {
|
||||
verifyingContract: safeAddress,
|
||||
},
|
||||
primaryType: 'SafeTx',
|
||||
message: {
|
||||
to,
|
||||
value: Number(valueInWei),
|
||||
data,
|
||||
operation,
|
||||
safeTxGas: txGasEstimate,
|
||||
dataGas: dataGasEstimate,
|
||||
gasPrice,
|
||||
gasToken: txGasToken,
|
||||
refundReceiver,
|
||||
nonce: Number(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 jsonTypedData = JSON.stringify(typedData)
|
||||
const signedTypedData = {
|
||||
method: 'eth_signTypedData_v3',
|
||||
// To change once Metamask fixes their status
|
||||
// https://github.com/MetaMask/metamask-extension/pull/5368
|
||||
// https://github.com/MetaMask/metamask-extension/issues/5366
|
||||
params: [sender, jsonTypedData],
|
||||
from: sender,
|
||||
}
|
||||
const txSignedResponse = await web3.currentProvider.sendAsync(signedTypedData)
|
||||
|
||||
return txSignedResponse.result.replace(EMPTY_DATA, '')
|
||||
}
|
|
@ -54,6 +54,7 @@ export const getApprovalTransaction = async ({
|
|||
|
||||
try {
|
||||
const web3 = getWeb3()
|
||||
|
||||
const contract = new web3.eth.Contract(GnosisSafeSol.abi, safeInstance.address)
|
||||
|
||||
return contract.methods.approveHash(txHash)
|
||||
|
|
|
@ -19,10 +19,10 @@ const calculateBodyFrom = async (
|
|||
gasPrice: string | number,
|
||||
gasToken: string,
|
||||
refundReceiver: string,
|
||||
transactionHash: string,
|
||||
transactionHash: string | null,
|
||||
sender: string,
|
||||
confirmationType: TxServiceType,
|
||||
origin: string | null,
|
||||
signature: ?string,
|
||||
) => {
|
||||
const contractTransactionHash = await safeInstance.getTransactionHash(
|
||||
to,
|
||||
|
@ -51,8 +51,8 @@ const calculateBodyFrom = async (
|
|||
contractTransactionHash,
|
||||
transactionHash,
|
||||
sender: getWeb3().utils.toChecksumAddress(sender),
|
||||
confirmationType,
|
||||
origin,
|
||||
signature,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -75,9 +75,9 @@ export const saveTxToHistory = async ({
|
|||
safeInstance,
|
||||
safeTxGas,
|
||||
sender,
|
||||
signature,
|
||||
to,
|
||||
txHash,
|
||||
type,
|
||||
valueInWei,
|
||||
}: {
|
||||
safeInstance: any,
|
||||
|
@ -91,10 +91,10 @@ export const saveTxToHistory = async ({
|
|||
gasPrice: string | number,
|
||||
gasToken: string,
|
||||
refundReceiver: string,
|
||||
txHash: string,
|
||||
txHash: string | null,
|
||||
sender: string,
|
||||
type: TxServiceType,
|
||||
origin: string | null,
|
||||
signature: ?string,
|
||||
}) => {
|
||||
const url = buildTxServiceUrl(safeInstance.address)
|
||||
const body = await calculateBodyFrom(
|
||||
|
@ -109,10 +109,10 @@ export const saveTxToHistory = async ({
|
|||
gasPrice,
|
||||
gasToken,
|
||||
refundReceiver,
|
||||
txHash,
|
||||
txHash || null,
|
||||
sender,
|
||||
type,
|
||||
origin || null,
|
||||
signature,
|
||||
)
|
||||
const response = await axios.post(url, body)
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ export const getCurrentMasterContractLastVersion = async () => {
|
|||
return safeMasterVersion
|
||||
}
|
||||
|
||||
export const getSafeVersion = async (safeAddress: string) => {
|
||||
export const getSafeVersionInfo = async (safeAddress: string) => {
|
||||
try {
|
||||
const safeMaster = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const lastSafeVersion = await getCurrentMasterContractLastVersion()
|
||||
|
|
|
@ -26,11 +26,11 @@ const activateAssetsByBalance = (safeAddress: string) => async (
|
|||
|
||||
// active tokens by balance, excluding those already blacklisted and the `null` address
|
||||
const activeByBalance = Object.entries(availableAssets)
|
||||
.filter(asset => {
|
||||
.filter((asset) => {
|
||||
const { address, numberOfTokens } = asset[1]
|
||||
return address !== null && !blacklistedAssets.has(address) && numberOfTokens > 0
|
||||
})
|
||||
.map(asset => {
|
||||
.map((asset) => {
|
||||
return asset[0]
|
||||
})
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ export const getEthAsToken = (balance: string) => {
|
|||
}
|
||||
|
||||
export const calculateActiveErc20TokensFrom = (tokens: List<Token>) => {
|
||||
const activeTokens = List().withMutations(list =>
|
||||
const activeTokens = List().withMutations((list) =>
|
||||
tokens.forEach((token: Token) => {
|
||||
const isDeactivated = isEther(token.symbol) || !token.status
|
||||
if (isDeactivated) {
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
import ENS from 'ethereum-ens'
|
||||
import Web3 from 'web3'
|
||||
|
||||
import { sameAddress } from './ethAddresses'
|
||||
import { EMPTY_DATA } from './ethTransactions'
|
||||
|
||||
import { getNetwork } from '~/config/index'
|
||||
import type { ProviderProps } from '~/logic/wallets/store/model/provider'
|
||||
|
||||
|
@ -27,6 +30,8 @@ export const WALLET_PROVIDER = {
|
|||
OPERA: 'OPERA',
|
||||
DAPPER: 'DAPPER',
|
||||
AUTHEREUM: 'AUTHEREUM',
|
||||
LEDGER: 'LEDGER',
|
||||
TREZOR: 'TREZOR',
|
||||
}
|
||||
|
||||
export const INJECTED_PROVIDERS = [
|
||||
|
@ -88,7 +93,16 @@ export const getAccountFrom: Function = async (web3Provider): Promise<string | n
|
|||
return accounts && accounts.length > 0 ? accounts[0] : null
|
||||
}
|
||||
|
||||
export const getNetworkIdFrom = web3Provider => web3Provider.eth.net.getId()
|
||||
export const getNetworkIdFrom = (web3Provider) => web3Provider.eth.net.getId()
|
||||
|
||||
const isHardwareWallet = (walletName: $Values<typeof WALLET_PROVIDER>) =>
|
||||
sameAddress(WALLET_PROVIDER.LEDGER, walletName) || sameAddress(WALLET_PROVIDER.TREZOR, walletName)
|
||||
|
||||
const isSmartContractWallet = async (web3Provider, account) => {
|
||||
const contractCode: string = await web3Provider.eth.getCode(account)
|
||||
|
||||
return contractCode.replace(EMPTY_DATA, '').replace(/0/g, '') !== ''
|
||||
}
|
||||
|
||||
export const getProviderInfo: Function = async (
|
||||
web3Provider,
|
||||
|
@ -96,18 +110,21 @@ export const getProviderInfo: Function = async (
|
|||
): Promise<ProviderProps> => {
|
||||
web3 = new Web3(web3Provider)
|
||||
|
||||
const name = providerName
|
||||
const account = await getAccountFrom(web3)
|
||||
const network = await getNetworkIdFrom(web3)
|
||||
const smartContractWallet = await isSmartContractWallet(web3, account)
|
||||
const hardwareWallet = isHardwareWallet(providerName)
|
||||
|
||||
const available = account !== null
|
||||
|
||||
return {
|
||||
name,
|
||||
name: providerName,
|
||||
available,
|
||||
loaded: true,
|
||||
account,
|
||||
network,
|
||||
smartContractWallet,
|
||||
hardwareWallet,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,15 +12,7 @@ import type { ProviderProps } from '~/logic/wallets/store/model/provider'
|
|||
import { makeProvider } from '~/logic/wallets/store/model/provider'
|
||||
|
||||
export const processProviderResponse = (dispatch: ReduxDispatch<*>, provider: ProviderProps) => {
|
||||
const { account, available, loaded, name, network } = provider
|
||||
|
||||
const walletRecord = makeProvider({
|
||||
name,
|
||||
available,
|
||||
loaded,
|
||||
account,
|
||||
network,
|
||||
})
|
||||
const walletRecord = makeProvider(provider)
|
||||
|
||||
dispatch(addProvider(walletRecord))
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ export type ProviderProps = {
|
|||
available: boolean,
|
||||
account: string,
|
||||
network: number,
|
||||
smartContractWallet: boolean,
|
||||
hardwareWallet: boolean,
|
||||
}
|
||||
|
||||
export const makeProvider: RecordFactory<ProviderProps> = Record({
|
||||
|
@ -16,6 +18,8 @@ export const makeProvider: RecordFactory<ProviderProps> = Record({
|
|||
available: false,
|
||||
account: '',
|
||||
network: 0,
|
||||
smartContractWallet: false,
|
||||
hardwareWallet: false,
|
||||
})
|
||||
|
||||
export type Provider = RecordOf<ProviderProps>
|
||||
|
|
|
@ -5,7 +5,7 @@ import { ETHEREUM_NETWORK, ETHEREUM_NETWORK_IDS } from '~/logic/wallets/getWeb3'
|
|||
import type { Provider } from '~/logic/wallets/store/model/provider'
|
||||
import { PROVIDER_REDUCER_ID } from '~/logic/wallets/store/reducer/provider'
|
||||
|
||||
const providerSelector = (state: any): Provider => state[PROVIDER_REDUCER_ID]
|
||||
export const providerSelector = (state: any): Provider => state[PROVIDER_REDUCER_ID]
|
||||
|
||||
export const userAccountSelector = createSelector(providerSelector, (provider: Provider) => {
|
||||
const account = provider.get('account')
|
||||
|
|
|
@ -57,7 +57,7 @@ export const getSupportedWallets = () => {
|
|||
const { isDesktop } = window
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
if (isDesktop) return wallets.filter(wallet => wallet.desktop).map(({ desktop, ...rest }) => rest)
|
||||
if (isDesktop) return wallets.filter((wallet) => wallet.desktop).map(({ desktop, ...rest }) => rest)
|
||||
|
||||
return wallets.map(({ desktop, ...rest }) => rest)
|
||||
}
|
||||
|
|
|
@ -65,6 +65,6 @@ const Routes = ({ defaultSafe, location }: RoutesProps) => {
|
|||
|
||||
// $FlowFixMe
|
||||
export default connect<Object, Object, ?Function, ?Object>(
|
||||
state => ({ defaultSafe: defaultSafeSelector(state) }),
|
||||
(state) => ({ defaultSafe: defaultSafeSelector(state) }),
|
||||
null,
|
||||
)(withRouter(Routes))
|
||||
|
|
|
@ -50,7 +50,7 @@ class Load extends React.Component<Props> {
|
|||
|
||||
await loadSafe(safeName, safeAddress, owners, addSafe)
|
||||
|
||||
const url = `${SAFELIST_ADDRESS}/${safeAddress}/balances/`
|
||||
const url = `${SAFELIST_ADDRESS}/${safeAddress}/balances`
|
||||
history.push(url)
|
||||
} catch (error) {
|
||||
console.error('Error while loading the Safe', error)
|
||||
|
|
|
@ -86,10 +86,10 @@ export const createSafe = (values: Object, userAccount: string): Promise<OpenSta
|
|||
const promiEvent = deploymentTxMethod.send({ from: userAccount, value: 0 })
|
||||
|
||||
promiEvent
|
||||
.once('transactionHash', txHash => {
|
||||
.once('transactionHash', (txHash) => {
|
||||
saveToStorage(SAFE_PENDING_CREATION_STORAGE_KEY, { txHash, ...values })
|
||||
})
|
||||
.then(async receipt => {
|
||||
.then(async (receipt) => {
|
||||
await checkReceiptStatus(receipt.transactionHash)
|
||||
|
||||
const safeAddress = receipt.events.ProxyCreation.returnValues.proxy
|
||||
|
@ -138,7 +138,7 @@ const Open = ({ addSafe, network, provider, userAccount }: Props) => {
|
|||
load()
|
||||
}, [])
|
||||
|
||||
const createSafeProxy = async formValues => {
|
||||
const createSafeProxy = async (formValues) => {
|
||||
let values = formValues
|
||||
|
||||
// save form values, used when the user rejects the TX and wants to retry
|
||||
|
@ -154,7 +154,7 @@ const Open = ({ addSafe, network, provider, userAccount }: Props) => {
|
|||
setShowProgress(true)
|
||||
}
|
||||
|
||||
const onSafeCreated = async safeAddress => {
|
||||
const onSafeCreated = async (safeAddress) => {
|
||||
const pendingCreation = await loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY)
|
||||
|
||||
const name = getSafeNameFrom(pendingCreation)
|
||||
|
|
|
@ -180,7 +180,7 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider
|
|||
},
|
||||
]
|
||||
|
||||
const onError = error => {
|
||||
const onError = (error) => {
|
||||
setIntervalStarted(false)
|
||||
setWaitingSafeDeployed(false)
|
||||
setContinueButtonDisabled(false)
|
||||
|
@ -226,7 +226,7 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider
|
|||
|
||||
setStepIndex(0)
|
||||
submittedPromise
|
||||
.once('transactionHash', txHash => {
|
||||
.once('transactionHash', (txHash) => {
|
||||
setSafeCreationTxHash(txHash)
|
||||
setStepIndex(1)
|
||||
setIntervalStarted(true)
|
||||
|
@ -249,7 +249,7 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider
|
|||
return
|
||||
}
|
||||
|
||||
const isTxMined = async txHash => {
|
||||
const isTxMined = async (txHash) => {
|
||||
const web3 = getWeb3()
|
||||
|
||||
const receipt = await web3.eth.getTransactionReceipt(txHash)
|
||||
|
|
|
@ -40,7 +40,7 @@ const Coins = (props: Props) => {
|
|||
const { showReceiveFunds, showSendFunds } = props
|
||||
const classes = useStyles()
|
||||
const columns = generateColumns()
|
||||
const autoColumns = columns.filter(c => !c.custom)
|
||||
const autoColumns = columns.filter((c) => !c.custom)
|
||||
const currencySelected = useSelector(currentCurrencySelector)
|
||||
const activeTokens = useSelector(extendedSafeTokensSelector)
|
||||
const currencyValues = useSelector(currencyValuesListSelector)
|
||||
|
|
|
@ -84,7 +84,7 @@ const Collectibles = () => {
|
|||
const nftTokens: NFTTokensState = useSelector(nftTokensSelector)
|
||||
const activeAssetsList = useSelector(activeNftAssetsListSelector)
|
||||
|
||||
const handleItemSend = nftToken => {
|
||||
const handleItemSend = (nftToken) => {
|
||||
setSelectedToken(nftToken)
|
||||
setSendNFTsModalOpen(true)
|
||||
}
|
||||
|
@ -93,7 +93,7 @@ const Collectibles = () => {
|
|||
<Card className={classes.cardOuter}>
|
||||
<div className={classes.cardInner}>
|
||||
{activeAssetsList.size ? (
|
||||
activeAssetsList.map(nftAsset => {
|
||||
activeAssetsList.map((nftAsset) => {
|
||||
return (
|
||||
<React.Fragment key={nftAsset.slug}>
|
||||
<div className={classes.title}>
|
||||
|
@ -104,7 +104,7 @@ const Collectibles = () => {
|
|||
<div className={classes.gridRow}>
|
||||
{nftTokens
|
||||
.filter(({ assetAddress }) => nftAsset.address === assetAddress)
|
||||
.map(nftToken => (
|
||||
.map((nftToken) => (
|
||||
<Item
|
||||
data={nftToken}
|
||||
key={`${nftAsset.slug}_${nftToken.tokenId}`}
|
||||
|
|
|
@ -36,7 +36,7 @@ import { sm } from '~/theme/variables'
|
|||
type Props = {
|
||||
initialValues: Object,
|
||||
onClose: () => void,
|
||||
onNext: any => void,
|
||||
onNext: (any) => void,
|
||||
recipientAddress?: string,
|
||||
selectedToken?: NFTToken | {},
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
|
|||
}
|
||||
}, [selectedEntry, pristine])
|
||||
|
||||
const handleSubmit = values => {
|
||||
const handleSubmit = (values) => {
|
||||
// If the input wasn't modified, there was no mutation of the recipientAddress
|
||||
if (!values.recipientAddress) {
|
||||
values.recipientAddress = selectedEntry.address
|
||||
|
@ -110,9 +110,9 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
|
|||
const formState = args[2]
|
||||
const mutators = args[3]
|
||||
const { assetAddress } = formState.values
|
||||
const selectedNFTTokens = nftTokens.filter(nftToken => nftToken.assetAddress === assetAddress)
|
||||
const selectedNFTTokens = nftTokens.filter((nftToken) => nftToken.assetAddress === assetAddress)
|
||||
|
||||
const handleScan = value => {
|
||||
const handleScan = (value) => {
|
||||
let scannedAddress = value
|
||||
|
||||
if (scannedAddress.startsWith('ethereum:')) {
|
||||
|
@ -144,7 +144,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
|
|||
</Row>
|
||||
{selectedEntry && selectedEntry.address ? (
|
||||
<div
|
||||
onKeyDown={e => {
|
||||
onKeyDown={(e) => {
|
||||
if (e.keyCode !== 9) {
|
||||
setSelectedEntry(null)
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@ const AddCustomAsset = (props: Props) => {
|
|||
}
|
||||
}
|
||||
|
||||
const formSpyOnChangeHandler = async state => {
|
||||
const formSpyOnChangeHandler = async (state) => {
|
||||
const { dirty, errors, submitSucceeded, validating, values } = state
|
||||
// for some reason this is called after submitting, we don't need to update the values
|
||||
// after submit
|
||||
|
|
|
@ -63,7 +63,7 @@ const AddCustomToken = (props: Props) => {
|
|||
} = props
|
||||
const [formValues, setFormValues] = useState(INITIAL_FORM_STATE)
|
||||
|
||||
const handleSubmit = values => {
|
||||
const handleSubmit = (values) => {
|
||||
const token = {
|
||||
address: values.address,
|
||||
decimals: values.decimals,
|
||||
|
@ -97,7 +97,7 @@ const AddCustomToken = (props: Props) => {
|
|||
}
|
||||
}
|
||||
|
||||
const formSpyOnChangeHandler = async state => {
|
||||
const formSpyOnChangeHandler = async (state) => {
|
||||
const { dirty, errors, submitSucceeded, validating, values } = state
|
||||
// for some reason this is called after submitting, we don't need to update the values
|
||||
// after submit
|
||||
|
|
|
@ -75,7 +75,7 @@ const AssetsList = (props: Props) => {
|
|||
setFilterValue(value)
|
||||
}
|
||||
|
||||
const getItemKey = index => {
|
||||
const getItemKey = (index) => {
|
||||
return index
|
||||
}
|
||||
|
||||
|
@ -95,7 +95,7 @@ const AssetsList = (props: Props) => {
|
|||
}
|
||||
}
|
||||
|
||||
const createItemData = assetsList => {
|
||||
const createItemData = (assetsList) => {
|
||||
return {
|
||||
assets: assetsList,
|
||||
activeAssetsAddresses,
|
||||
|
|
|
@ -121,7 +121,7 @@ const Layout = (props: Props) => {
|
|||
const etherScanLink = getEtherScanLink('address', address)
|
||||
const web3Instance = getWeb3()
|
||||
|
||||
const openGenericModal = modalConfig => {
|
||||
const openGenericModal = (modalConfig) => {
|
||||
setModal({ ...modalConfig, isOpen: true })
|
||||
}
|
||||
|
||||
|
|
|
@ -100,7 +100,7 @@ export const getTxTableData = (
|
|||
): List<TransactionRow> => {
|
||||
const cancelTxsByNonce = cancelTxs.reduce((acc, tx) => acc.set(tx.nonce, tx), Map())
|
||||
|
||||
return transactions.map(tx => {
|
||||
return transactions.map((tx) => {
|
||||
if (INCOMING_TX_TYPES.includes(tx.type)) {
|
||||
return getIncomingTxTableData(tx)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// @flow
|
||||
import { push } from 'connected-react-router'
|
||||
import type { GetState, Dispatch as ReduxDispatch } from 'redux'
|
||||
import semverSatisfies from 'semver/functions/satisfies'
|
||||
|
||||
import { onboardUser } from '~/components/ConnectButton'
|
||||
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
||||
|
@ -8,22 +9,22 @@ import { type NotificationsQueue, getNotificationsFromTxType, showSnackbar } fro
|
|||
import {
|
||||
CALL,
|
||||
type NotifiedTransaction,
|
||||
TX_TYPE_CONFIRMATION,
|
||||
TX_TYPE_EXECUTION,
|
||||
getApprovalTransaction,
|
||||
getExecutionTransaction,
|
||||
saveTxToHistory,
|
||||
} from '~/logic/safe/transactions'
|
||||
import { SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES, tryOffchainSigning } from '~/logic/safe/transactions/offchainSigner'
|
||||
import { getCurrentSafeVersion } from '~/logic/safe/utils/safeVersion'
|
||||
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
|
||||
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||
import { userAccountSelector } from '~/logic/wallets/store/selectors'
|
||||
import { providerSelector } from '~/logic/wallets/store/selectors'
|
||||
import { SAFELIST_ADDRESS } from '~/routes/routes'
|
||||
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||
import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from '~/routes/safe/store/actions/utils'
|
||||
import { type GlobalState } from '~/store'
|
||||
import { getErrorMessage } from '~/test/utils/ethereumErrors'
|
||||
|
||||
type CreateTransactionArgs = {
|
||||
export type CreateTransactionArgs = {
|
||||
safeAddress: string,
|
||||
to: string,
|
||||
valueInWei: string,
|
||||
|
@ -59,11 +60,12 @@ const createTransaction = ({
|
|||
const ready = await onboardUser()
|
||||
if (!ready) return
|
||||
|
||||
const from = userAccountSelector(state)
|
||||
const { account: from, hardwareWallet, smartContractWallet } = providerSelector(state)
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const lastTx = await getLastTx(safeAddress)
|
||||
const nonce = await getNewTxNonce(txNonce, lastTx, safeInstance)
|
||||
const isExecution = await shouldExecuteTransaction(safeInstance, nonce, lastTx)
|
||||
const safeVersion = await getCurrentSafeVersion(safeInstance)
|
||||
|
||||
// https://docs.gnosis.io/safe/docs/docs5/#pre-validated-signatures
|
||||
const sigs = `0x000000000000000000000000${from.replace(
|
||||
|
@ -94,6 +96,33 @@ const createTransaction = ({
|
|||
}
|
||||
|
||||
try {
|
||||
// Here we're checking that safe contract version is greater or equal 1.1.1, but
|
||||
// theoretically EIP712 should also work for 1.0.0 contracts
|
||||
// Also, offchain signatures are not working for ledger wallet because of a bug in their library:
|
||||
// https://github.com/LedgerHQ/ledgerjs/issues/378
|
||||
const canTryOffchainSigning =
|
||||
!isExecution &&
|
||||
!smartContractWallet &&
|
||||
semverSatisfies(safeVersion, SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES) &&
|
||||
!hardwareWallet
|
||||
if (canTryOffchainSigning) {
|
||||
const signature = await tryOffchainSigning({ ...txArgs, safeAddress })
|
||||
|
||||
if (signature) {
|
||||
closeSnackbar(beforeExecutionKey)
|
||||
|
||||
await saveTxToHistory({
|
||||
...txArgs,
|
||||
signature,
|
||||
origin,
|
||||
})
|
||||
showSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded, enqueueSnackbar, closeSnackbar)
|
||||
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tx = isExecution ? await getExecutionTransaction(txArgs) : await getApprovalTransaction(txArgs)
|
||||
|
||||
const sendParams = { from, value: 0 }
|
||||
|
@ -110,7 +139,7 @@ const createTransaction = ({
|
|||
|
||||
await tx
|
||||
.send(sendParams)
|
||||
.once('transactionHash', async hash => {
|
||||
.once('transactionHash', async (hash) => {
|
||||
txHash = hash
|
||||
closeSnackbar(beforeExecutionKey)
|
||||
|
||||
|
@ -120,7 +149,6 @@ const createTransaction = ({
|
|||
await saveTxToHistory({
|
||||
...txArgs,
|
||||
txHash,
|
||||
type: isExecution ? TX_TYPE_EXECUTION : TX_TYPE_CONFIRMATION,
|
||||
origin,
|
||||
})
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
|
@ -128,10 +156,10 @@ const createTransaction = ({
|
|||
console.error(err)
|
||||
}
|
||||
})
|
||||
.on('error', error => {
|
||||
.on('error', (error) => {
|
||||
console.error('Tx error: ', error)
|
||||
})
|
||||
.then(receipt => {
|
||||
.then((receipt) => {
|
||||
closeSnackbar(pendingExecutionKey)
|
||||
showSnackbar(
|
||||
isExecution
|
||||
|
|
|
@ -69,7 +69,7 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: ReduxDis
|
|||
|
||||
const remoteOwners = await gnosisSafe.getOwners()
|
||||
// Converts from [ { address, ownerName} ] to address array
|
||||
const localOwners = localSafe.owners.map(localOwner => localOwner.address)
|
||||
const localOwners = localSafe.owners.map((localOwner) => localOwner.address)
|
||||
const localThreshold = localSafe.threshold
|
||||
|
||||
// Updates threshold values
|
||||
|
@ -81,16 +81,16 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: ReduxDis
|
|||
}
|
||||
|
||||
// If the remote owners does not contain a local address, we remove that local owner
|
||||
localOwners.forEach(localAddress => {
|
||||
const remoteOwnerIndex = remoteOwners.findIndex(remoteAddress => sameAddress(remoteAddress, localAddress))
|
||||
localOwners.forEach((localAddress) => {
|
||||
const remoteOwnerIndex = remoteOwners.findIndex((remoteAddress) => sameAddress(remoteAddress, localAddress))
|
||||
if (remoteOwnerIndex === -1) {
|
||||
dispatch(removeSafeOwner({ safeAddress, ownerAddress: localAddress }))
|
||||
}
|
||||
})
|
||||
|
||||
// If the remote has an owner that we don't have locally, we add it
|
||||
remoteOwners.forEach(remoteAddress => {
|
||||
const localOwnerIndex = localOwners.findIndex(localAddress => sameAddress(remoteAddress, localAddress))
|
||||
remoteOwners.forEach((remoteAddress) => {
|
||||
const localOwnerIndex = localOwners.findIndex((localAddress) => sameAddress(remoteAddress, localAddress))
|
||||
if (localOwnerIndex === -1) {
|
||||
dispatch(
|
||||
addSafeOwner({
|
||||
|
|
|
@ -23,7 +23,7 @@ const nonStandardERC20 = [
|
|||
// https://rinkeby.etherscan.io/address/0x0cf0ee63788a0849fe5297f3407f701e122cc023#readContract
|
||||
// It doesn't have a `balanceOf` method implemented.
|
||||
const isStandardERC20 = (address: string): boolean => {
|
||||
return !nonStandardERC20.find(token => sameAddress(address, token.address) && sameAddress(NETWORK, token.network))
|
||||
return !nonStandardERC20.find((token) => sameAddress(address, token.address) && sameAddress(NETWORK, token.network))
|
||||
}
|
||||
|
||||
const getTokenBalances = (tokens: List<Token>, safeAddress: string) => {
|
||||
|
@ -38,7 +38,7 @@ const getTokenBalances = (tokens: List<Token>, safeAddress: string) => {
|
|||
// As a fallback, we're using `balances`
|
||||
const method = isStandardERC20(address) ? 'balanceOf' : 'balances'
|
||||
|
||||
return new Promise(resolve => {
|
||||
return new Promise((resolve) => {
|
||||
const request = onlyBalanceToken.methods[method](safeAddress).call.request((error, balance) => {
|
||||
if (error) {
|
||||
// if there's no balance, we log the error, but `resolve` with a default '0'
|
||||
|
@ -87,7 +87,7 @@ const fetchTokenBalances = (safeAddress: string, tokens: List<Token>) => async (
|
|||
try {
|
||||
const withBalances = await getTokenBalances(tokens, safeAddress)
|
||||
|
||||
const balances = Map().withMutations(map => {
|
||||
const balances = Map().withMutations((map) => {
|
||||
withBalances.forEach(({ address, balance }) => {
|
||||
map.set(address, balance)
|
||||
})
|
||||
|
|
|
@ -80,7 +80,7 @@ export const buildTransactionFrom = async (safeAddress: string, tx: TxServiceMod
|
|||
let ownerName = 'UNKNOWN'
|
||||
|
||||
if (owners) {
|
||||
const storedOwner = owners.find(owner => sameAddress(conf.owner, owner.address))
|
||||
const storedOwner = owners.find((owner) => sameAddress(conf.owner, owner.address))
|
||||
|
||||
if (storedOwner) {
|
||||
ownerName = storedOwner.name
|
||||
|
@ -222,11 +222,9 @@ export const buildIncomingTransactionFrom = async (tx: IncomingTxServiceModel) =
|
|||
let symbol = 'ETH'
|
||||
let decimals = 18
|
||||
|
||||
const fee = await web3.eth.getTransaction(tx.transactionHash).then(({ gas, gasPrice }) =>
|
||||
bn(gas)
|
||||
.div(gasPrice)
|
||||
.toFixed(),
|
||||
)
|
||||
const fee = await web3.eth
|
||||
.getTransaction(tx.transactionHash)
|
||||
.then(({ gas, gasPrice }) => bn(gas).div(gasPrice).toFixed())
|
||||
|
||||
if (tx.tokenAddress) {
|
||||
try {
|
||||
|
@ -236,7 +234,9 @@ export const buildIncomingTransactionFrom = async (tx: IncomingTxServiceModel) =
|
|||
} catch (err) {
|
||||
try {
|
||||
const { methods } = new web3.eth.Contract(ALTERNATIVE_TOKEN_ABI, tx.tokenAddress)
|
||||
const [tokenSymbol, tokenDecimals] = await Promise.all([methods.symbol, methods.decimals].map(m => m().call()))
|
||||
const [tokenSymbol, tokenDecimals] = await Promise.all(
|
||||
[methods.symbol, methods.decimals].map((m) => m().call()),
|
||||
)
|
||||
symbol = web3.utils.hexToString(tokenSymbol)
|
||||
decimals = tokenDecimals
|
||||
} catch (e) {
|
||||
|
@ -306,7 +306,7 @@ export const loadSafeTransactions = async (safeAddress: string): Promise<SafeTra
|
|||
transactions.map((tx: TxServiceModel) => buildTransactionFrom(safeAddress, tx)),
|
||||
)
|
||||
|
||||
const groupedTxs = List(txsRecord).groupBy(tx => (tx.get('cancellationTx') ? 'cancel' : 'outgoing'))
|
||||
const groupedTxs = List(txsRecord).groupBy((tx) => (tx.get('cancellationTx') ? 'cancel' : 'outgoing'))
|
||||
|
||||
return {
|
||||
outgoing: Map().set(safeAddress, groupedTxs.get('outgoing')),
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
// @flow
|
||||
import type { Dispatch as ReduxDispatch } from 'redux'
|
||||
import semverSatisfies from 'semver/functions/satisfies'
|
||||
|
||||
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
||||
import { type NotificationsQueue, getNotificationsFromTxType, showSnackbar } from '~/logic/notifications'
|
||||
import { generateSignaturesFromTxConfirmations } from '~/logic/safe/safeTxSigner'
|
||||
import {
|
||||
type NotifiedTransaction,
|
||||
TX_TYPE_CONFIRMATION,
|
||||
TX_TYPE_EXECUTION,
|
||||
getApprovalTransaction,
|
||||
getExecutionTransaction,
|
||||
saveTxToHistory,
|
||||
} from '~/logic/safe/transactions'
|
||||
import { userAccountSelector } from '~/logic/wallets/store/selectors'
|
||||
import { SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES, tryOffchainSigning } from '~/logic/safe/transactions/offchainSigner'
|
||||
import { getCurrentSafeVersion } from '~/logic/safe/utils/safeVersion'
|
||||
import { providerSelector } from '~/logic/wallets/store/selectors'
|
||||
import fetchSafe from '~/routes/safe/store/actions/fetchSafe'
|
||||
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||
import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from '~/routes/safe/store/actions/utils'
|
||||
|
@ -41,11 +42,12 @@ const processTransaction = ({
|
|||
}: ProcessTransactionArgs) => async (dispatch: ReduxDispatch<GlobalState>, getState: Function) => {
|
||||
const state: GlobalState = getState()
|
||||
|
||||
const from = userAccountSelector(state)
|
||||
const { account: from, hardwareWallet, smartContractWallet } = providerSelector(state)
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const lastTx = await getLastTx(safeAddress)
|
||||
const nonce = await getNewTxNonce(null, lastTx, safeInstance)
|
||||
const isExecution = approveAndExecute || (await shouldExecuteTransaction(safeInstance, nonce, lastTx))
|
||||
const safeVersion = await getCurrentSafeVersion(safeInstance)
|
||||
|
||||
let sigs = generateSignaturesFromTxConfirmations(tx.confirmations, approveAndExecute && userAddress)
|
||||
// https://docs.gnosis.io/safe/docs/docs5/#pre-validated-signatures
|
||||
|
@ -79,6 +81,33 @@ const processTransaction = ({
|
|||
}
|
||||
|
||||
try {
|
||||
// Here we're checking that safe contract version is greater or equal 1.1.1, but
|
||||
// theoretically EIP712 should also work for 1.0.0 contracts
|
||||
// Also, offchain signatures are not working for ledger wallet because of a bug in their library:
|
||||
// https://github.com/LedgerHQ/ledgerjs/issues/378
|
||||
const canTryOffchainSigning =
|
||||
!isExecution &&
|
||||
!smartContractWallet &&
|
||||
semverSatisfies(safeVersion, SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES) &&
|
||||
!hardwareWallet
|
||||
if (canTryOffchainSigning) {
|
||||
const signature = await tryOffchainSigning({ ...txArgs, safeAddress })
|
||||
|
||||
if (signature) {
|
||||
closeSnackbar(beforeExecutionKey)
|
||||
|
||||
await saveTxToHistory({
|
||||
...txArgs,
|
||||
signature,
|
||||
origin,
|
||||
})
|
||||
showSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded, enqueueSnackbar, closeSnackbar)
|
||||
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
transaction = isExecution ? await getExecutionTransaction(txArgs) : await getApprovalTransaction(txArgs)
|
||||
|
||||
const sendParams = { from, value: 0 }
|
||||
|
@ -95,7 +124,7 @@ const processTransaction = ({
|
|||
|
||||
await transaction
|
||||
.send(sendParams)
|
||||
.once('transactionHash', async hash => {
|
||||
.once('transactionHash', async (hash) => {
|
||||
txHash = hash
|
||||
closeSnackbar(beforeExecutionKey)
|
||||
|
||||
|
@ -105,17 +134,16 @@ const processTransaction = ({
|
|||
await saveTxToHistory({
|
||||
...txArgs,
|
||||
txHash,
|
||||
type: isExecution ? TX_TYPE_EXECUTION : TX_TYPE_CONFIRMATION,
|
||||
})
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
.on('error', error => {
|
||||
.on('error', (error) => {
|
||||
console.error('Processing transaction error: ', error)
|
||||
})
|
||||
.then(receipt => {
|
||||
.then((receipt) => {
|
||||
closeSnackbar(pendingExecutionKey)
|
||||
|
||||
showSnackbar(
|
||||
|
|
|
@ -7,7 +7,7 @@ import { NOTIFICATIONS, enhanceSnackbarForAction } from '~/logic/notifications'
|
|||
import closeSnackbarAction from '~/logic/notifications/store/actions/closeSnackbar'
|
||||
import enqueueSnackbar from '~/logic/notifications/store/actions/enqueueSnackbar'
|
||||
import { getAwaitingTransactions } from '~/logic/safe/transactions/awaitingTransactions'
|
||||
import { getSafeVersion } from '~/logic/safe/utils/safeVersion'
|
||||
import { getSafeVersionInfo } from '~/logic/safe/utils/safeVersion'
|
||||
import { isUserOwner } from '~/logic/wallets/ethAddresses'
|
||||
import { userAccountSelector } from '~/logic/wallets/store/selectors'
|
||||
import { getIncomingTxAmount } from '~/routes/safe/components/Transactions/TxsTable/columns'
|
||||
|
@ -43,7 +43,7 @@ const sendAwaitingTransactionNotification = async (
|
|||
? lastTimeUserLoggedInForSafes[safeAddress]
|
||||
: null
|
||||
|
||||
const filteredDuplicatedAwaitingTxList = awaitingTxsSubmissionDateList.filter(submissionDate => {
|
||||
const filteredDuplicatedAwaitingTxList = awaitingTxsSubmissionDateList.filter((submissionDate) => {
|
||||
return lastTimeUserLoggedIn ? new Date(submissionDate) > new Date(lastTimeUserLoggedIn) : true
|
||||
})
|
||||
|
||||
|
@ -83,7 +83,7 @@ const notificationsMiddleware = (store: Store<GlobalState>) => (next: Function)
|
|||
)
|
||||
const awaitingTxsSubmissionDateList = awaitingTransactions
|
||||
.get(safeAddress, List([]))
|
||||
.map(tx => tx.submissionDate)
|
||||
.map((tx) => tx.submissionDate)
|
||||
|
||||
const safes = safesMapSelector(state)
|
||||
const currentSafe = safes.get(safeAddress)
|
||||
|
@ -113,7 +113,7 @@ const notificationsMiddleware = (store: Store<GlobalState>) => (next: Function)
|
|||
const viewedSafes = state.currentSession ? state.currentSession.get('viewedSafes') : []
|
||||
const recurringUser = viewedSafes.includes(safeAddress)
|
||||
|
||||
const newIncomingTransactions = incomingTransactions.filter(tx => tx.blockNumber > latestIncomingTxBlock)
|
||||
const newIncomingTransactions = incomingTransactions.filter((tx) => tx.blockNumber > latestIncomingTxBlock)
|
||||
|
||||
const { message, ...TX_INCOMING_MSG } = NOTIFICATIONS.TX_INCOMING_MSG
|
||||
|
||||
|
@ -128,7 +128,7 @@ const notificationsMiddleware = (store: Store<GlobalState>) => (next: Function)
|
|||
),
|
||||
)
|
||||
} else {
|
||||
newIncomingTransactions.forEach(tx => {
|
||||
newIncomingTransactions.forEach((tx) => {
|
||||
dispatch(
|
||||
enqueueSnackbar(
|
||||
enhanceSnackbarForAction({
|
||||
|
@ -156,7 +156,7 @@ const notificationsMiddleware = (store: Store<GlobalState>) => (next: Function)
|
|||
const state: GlobalState = store.getState()
|
||||
const currentSafeAddress = safeParamAddressFromStateSelector(state)
|
||||
const isUserOwner = grantedSelector(state)
|
||||
const { needUpdate } = await getSafeVersion(currentSafeAddress)
|
||||
const { needUpdate } = await getSafeVersionInfo(currentSafeAddress)
|
||||
|
||||
const notificationKey = `${currentSafeAddress}`
|
||||
const onNotificationClicked = () => {
|
||||
|
|
|
@ -22,8 +22,8 @@ export const SAFE_REDUCER_ID = 'safes'
|
|||
export type SafeReducerState = Map<string, *>
|
||||
|
||||
export const buildSafe = (storedSafe: SafeProps) => {
|
||||
const names = storedSafe.owners.map(owner => owner.name)
|
||||
const addresses = storedSafe.owners.map(owner => getWeb3().utils.toChecksumAddress(owner.address))
|
||||
const names = storedSafe.owners.map((owner) => owner.name)
|
||||
const addresses = storedSafe.owners.map((owner) => getWeb3().utils.toChecksumAddress(owner.address))
|
||||
const owners = buildOwnersFrom(Array.from(names), Array.from(addresses))
|
||||
const activeTokens = Set(storedSafe.activeTokens)
|
||||
const activeAssets = Set(storedSafe.activeAssets)
|
||||
|
@ -50,20 +50,20 @@ export default handleActions<SafeReducerState, *>(
|
|||
const safe = action.payload
|
||||
const safeAddress = safe.address
|
||||
|
||||
return state.updateIn(['safes', safeAddress], prevSafe => prevSafe.merge(safe))
|
||||
return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.merge(safe))
|
||||
},
|
||||
[ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state: SafeReducerState, action: ActionType<Function>): SafeReducerState => {
|
||||
const tokenAddress = action.payload
|
||||
|
||||
return state.withMutations(map => {
|
||||
return state.withMutations((map) => {
|
||||
map
|
||||
.get('safes')
|
||||
.keySeq()
|
||||
.forEach(safeAddress => {
|
||||
.forEach((safeAddress) => {
|
||||
const safeActiveTokens = map.getIn(['safes', safeAddress, 'activeTokens'])
|
||||
const activeTokens = safeActiveTokens.add(tokenAddress)
|
||||
|
||||
map.updateIn(['safes', safeAddress], prevSafe => prevSafe.merge({ activeTokens }))
|
||||
map.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.merge({ activeTokens }))
|
||||
})
|
||||
})
|
||||
},
|
||||
|
@ -75,7 +75,7 @@ export default handleActions<SafeReducerState, *>(
|
|||
// with initial props and it would overwrite existing ones
|
||||
|
||||
if (state.hasIn(['safes', safe.address])) {
|
||||
return state.updateIn(['safes', safe.address], prevSafe => prevSafe.merge(safe))
|
||||
return state.updateIn(['safes', safe.address], (prevSafe) => prevSafe.merge(safe))
|
||||
}
|
||||
|
||||
return state.setIn(['safes', safe.address], SafeRecord(safe))
|
||||
|
@ -88,7 +88,7 @@ export default handleActions<SafeReducerState, *>(
|
|||
[ADD_SAFE_OWNER]: (state: SafeReducerState, action: ActionType<Function>): SafeReducerState => {
|
||||
const { ownerAddress, ownerName, safeAddress } = action.payload
|
||||
|
||||
return state.updateIn(['safes', safeAddress], prevSafe =>
|
||||
return state.updateIn(['safes', safeAddress], (prevSafe) =>
|
||||
prevSafe.merge({
|
||||
owners: prevSafe.owners.push(makeOwner({ address: ownerAddress, name: ownerName })),
|
||||
}),
|
||||
|
@ -97,19 +97,19 @@ export default handleActions<SafeReducerState, *>(
|
|||
[REMOVE_SAFE_OWNER]: (state: SafeReducerState, action: ActionType<Function>): SafeReducerState => {
|
||||
const { ownerAddress, safeAddress } = action.payload
|
||||
|
||||
return state.updateIn(['safes', safeAddress], prevSafe =>
|
||||
return state.updateIn(['safes', safeAddress], (prevSafe) =>
|
||||
prevSafe.merge({
|
||||
owners: prevSafe.owners.filter(o => o.address.toLowerCase() !== ownerAddress.toLowerCase()),
|
||||
owners: prevSafe.owners.filter((o) => o.address.toLowerCase() !== ownerAddress.toLowerCase()),
|
||||
}),
|
||||
)
|
||||
},
|
||||
[REPLACE_SAFE_OWNER]: (state: SafeReducerState, action: ActionType<Function>): SafeReducerState => {
|
||||
const { oldOwnerAddress, ownerAddress, ownerName, safeAddress } = action.payload
|
||||
|
||||
return state.updateIn(['safes', safeAddress], prevSafe =>
|
||||
return state.updateIn(['safes', safeAddress], (prevSafe) =>
|
||||
prevSafe.merge({
|
||||
owners: prevSafe.owners
|
||||
.filter(o => o.address.toLowerCase() !== oldOwnerAddress.toLowerCase())
|
||||
.filter((o) => o.address.toLowerCase() !== oldOwnerAddress.toLowerCase())
|
||||
.push(makeOwner({ address: ownerAddress, name: ownerName })),
|
||||
}),
|
||||
)
|
||||
|
@ -117,18 +117,18 @@ export default handleActions<SafeReducerState, *>(
|
|||
[EDIT_SAFE_OWNER]: (state: SafeReducerState, action: ActionType<Function>): SafeReducerState => {
|
||||
const { ownerAddress, ownerName, safeAddress } = action.payload
|
||||
|
||||
return state.updateIn(['safes', safeAddress], prevSafe => {
|
||||
return state.updateIn(['safes', safeAddress], (prevSafe) => {
|
||||
const ownerToUpdateIndex = prevSafe.owners.findIndex(
|
||||
o => o.address.toLowerCase() === ownerAddress.toLowerCase(),
|
||||
(o) => o.address.toLowerCase() === ownerAddress.toLowerCase(),
|
||||
)
|
||||
const updatedOwners = prevSafe.owners.update(ownerToUpdateIndex, owner => owner.set('name', ownerName))
|
||||
const updatedOwners = prevSafe.owners.update(ownerToUpdateIndex, (owner) => owner.set('name', ownerName))
|
||||
return prevSafe.merge({ owners: updatedOwners })
|
||||
})
|
||||
},
|
||||
[UPDATE_SAFE_THRESHOLD]: (state: SafeReducerState, action: ActionType<Function>): SafeReducerState => {
|
||||
const { safeAddress, threshold } = action.payload
|
||||
|
||||
return state.updateIn(['safes', safeAddress], prevSafe => prevSafe.set('threshold', threshold))
|
||||
return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.set('threshold', threshold))
|
||||
},
|
||||
[SET_DEFAULT_SAFE]: (state: SafeReducerState, action: ActionType<Function>): SafeReducerState =>
|
||||
state.set('defaultSafe', action.payload),
|
||||
|
|
|
@ -259,9 +259,9 @@ export const safeBalancesSelector: OutputSelector<GlobalState, RouterProps, Map<
|
|||
export const getActiveTokensAddressesForAllSafes: OutputSelector<GlobalState, any, Set<string>> = createSelector(
|
||||
safesListSelector,
|
||||
(safes: List<Safe>) => {
|
||||
const addresses = Set().withMutations(set => {
|
||||
const addresses = Set().withMutations((set) => {
|
||||
safes.forEach((safe: Safe) => {
|
||||
safe.activeTokens.forEach(tokenAddress => {
|
||||
safe.activeTokens.forEach((tokenAddress) => {
|
||||
set.add(tokenAddress)
|
||||
})
|
||||
})
|
||||
|
@ -274,9 +274,9 @@ export const getActiveTokensAddressesForAllSafes: OutputSelector<GlobalState, an
|
|||
export const getBlacklistedTokensAddressesForAllSafes: OutputSelector<GlobalState, any, Set<string>> = createSelector(
|
||||
safesListSelector,
|
||||
(safes: List<Safe>) => {
|
||||
const addresses = Set().withMutations(set => {
|
||||
const addresses = Set().withMutations((set) => {
|
||||
safes.forEach((safe: Safe) => {
|
||||
safe.blacklistedTokens.forEach(tokenAddress => {
|
||||
safe.blacklistedTokens.forEach((tokenAddress) => {
|
||||
set.add(tokenAddress)
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue