Feature #1440: Handle transactions with set safeTxGas from Safe Apps (#1442)

* sdk version update

* point sdk to a newer commit

* Update iframe message handler

* ConfirmTransactionModal tweaks to support params

* handle sendTransactionsWithParams, display safeTxGas in app tx modal

* new sdk version

* yarn lock update

* install libudev in travis

* update sdk version

* Estimating safeTxGas for Safe Apps Txs WIP

* safetxgas estimation warning wip

* gas estimation in confirm transaction modal

* yarn lock update

* review fixes

* Change estimation loading msg, use imported vars to index payload type

Co-authored-by: Daniel Sanchez <daniel.sanchez@gnosis.pm>
This commit is contained in:
Mikhail Mikheev 2020-10-22 17:43:59 +04:00 committed by GitHub
parent 2e5df72296
commit 56430b5966
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1274 additions and 1473 deletions

View File

@ -30,7 +30,7 @@ before_script:
before_install:
# Needed to deploy pull request and releases
- sudo apt-get update
- sudo apt-get -y install python-pip python-dev libusb-1.0-0-dev
- sudo apt-get -y install python-pip python-dev libusb-1.0-0-dev libudev-dev
- pip install awscli --upgrade --user
script:
- yarn lint:check

View File

@ -163,7 +163,7 @@
]
},
"dependencies": {
"@gnosis.pm/safe-apps-sdk": "0.4.2",
"@gnosis.pm/safe-apps-sdk": "https://github.com/gnosis/safe-apps-sdk#a6c168b",
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#70e57bdd1e0fd5dfdf5768076577c1e000b5fe28",
"@gnosis.pm/util-contracts": "2.0.6",

View File

Before

Width:  |  Height:  |  Size: 337 B

After

Width:  |  Height:  |  Size: 337 B

View File

Before

Width:  |  Height:  |  Size: 345 B

After

Width:  |  Height:  |  Size: 345 B

View File

Before

Width:  |  Height:  |  Size: 324 B

After

Width:  |  Height:  |  Size: 324 B

View File

@ -1,52 +1,7 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<g>
<path d="M256,0C114.497,0,0,114.507,0,256c0,141.503,114.507,256,256,256c141.503,0,256-114.507,256-256
C512,114.497,397.492,0,256,0z M256,472c-119.393,0-216-96.615-216-216c0-119.393,96.615-216,216-216
c119.393,0,216,96.615,216,216C472,375.393,375.384,472,256,472z" fill="#ff0000"/>
</g>
</g>
<g>
<g>
<path d="M256,214.33c-11.046,0-20,8.954-20,20v128.793c0,11.046,8.954,20,20,20s20-8.955,20-20.001V234.33
C276,223.284,267.046,214.33,256,214.33z" fill="#ff0000" />
</g>
</g>
<g>
<g>
<circle cx="256" cy="162.84" r="27" fill="#ff0000"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<g fill="none" fill-rule="evenodd">
<rect width="2" height="8" x="9" y="8" fill="#B2B5B2" rx="1"/>
<rect width="2" height="2" x="9" y="4" fill="#B2B5B2" stroke="#B2B5B2" stroke-width=".5" rx="1"/>
<circle cx="10" cy="10" r="9" stroke="#B2B5B2" stroke-width="2"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 391 B

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<g>
<path d="M256,0C114.497,0,0,114.507,0,256c0,141.503,114.507,256,256,256c141.503,0,256-114.507,256-256
C512,114.497,397.492,0,256,0z M256,472c-119.393,0-216-96.615-216-216c0-119.393,96.615-216,216-216
c119.393,0,216,96.615,216,216C472,375.393,375.384,472,256,472z" fill="#ff0000"/>
</g>
</g>
<g>
<g>
<path d="M256,214.33c-11.046,0-20,8.954-20,20v128.793c0,11.046,8.954,20,20,20s20-8.955,20-20.001V234.33
C276,223.284,267.046,214.33,256,214.33z" fill="#ff0000" />
</g>
</g>
<g>
<g>
<circle cx="256" cy="162.84" r="27" fill="#ff0000"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 859 B

View File

@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<g fill="none" fill-rule="evenodd">
<rect width="2" height="8" x="9" y="8" fill="#B2B5B2" rx="1"/>
<rect width="2" height="2" x="9" y="4" fill="#B2B5B2" stroke="#B2B5B2" stroke-width=".5" rx="1"/>
<circle cx="10" cy="10" r="9" stroke="#B2B5B2" stroke-width="2"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 391 B

View File

@ -5,10 +5,10 @@ import { useSelector } from 'react-redux'
import { useRouteMatch, useHistory } from 'react-router-dom'
import styled from 'styled-components'
import AlertIcon from './assets/alert.svg'
import CheckIcon from './assets/check.svg'
import ErrorIcon from './assets/error.svg'
import InfoIcon from './assets/info.svg'
import AlertIcon from 'src/assets/icons/alert.svg'
import CheckIcon from 'src/assets/icons/check.svg'
import ErrorIcon from 'src/assets/icons/error.svg'
import InfoIcon from 'src/assets/icons/info.svg'
import AppLayout from 'src/components/AppLayout'
import SafeListSidebarProvider, { SafeListSidebarContext } from 'src/components/SafeListSidebar'

View File

@ -107,6 +107,7 @@ interface CreateTransactionArgs {
txData?: string
txNonce?: number | string
valueInWei: string
safeTxGas?: number
}
type CreateTransactionAction = ThunkAction<Promise<void>, AppReduxState, undefined, AnyAction>
@ -124,6 +125,7 @@ const createTransaction = (
operation = CALL,
navigateToTransactionsTab = true,
origin = null,
safeTxGas: safeTxGasArg,
}: CreateTransactionArgs,
onUserConfirm?: ConfirmEventHandler,
onError?: ErrorEventHandler,
@ -143,7 +145,8 @@ const createTransaction = (
const nonce = await getNewTxNonce(txNonce?.toString(), lastTx, safeInstance)
const isExecution = await shouldExecuteTransaction(safeInstance, nonce, lastTx)
const safeVersion = await getCurrentSafeVersion(safeInstance)
const safeTxGas = await estimateSafeTxGas(safeInstance, safeAddress, txData, to, valueInWei, operation)
const safeTxGas =
safeTxGasArg || (await estimateSafeTxGas(safeInstance, safeAddress, txData, to, valueInWei, operation))
// https://docs.gnosis.io/safe/docs/docs5/#pre-validated-signatures
const sigs = `0x000000000000000000000000${from.replace(

View File

@ -1,7 +1,8 @@
import React from 'react'
import React, { useEffect, useState } from 'react'
import { Icon, Text, Title, GenericModal, ModalFooterConfirmation } from '@gnosis.pm/safe-react-components'
import { Transaction } from '@gnosis.pm/safe-apps-sdk'
import { Transaction, SendTransactionParams } from '@gnosis.pm/safe-apps-sdk'
import styled from 'styled-components'
import { useDispatch } from 'react-redux'
import AddressInfo from 'src/components/AddressInfo'
import DividerLine from 'src/components/DividerLine'
@ -15,11 +16,13 @@ import Img from 'src/components/layout/Img'
import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'
import { SafeApp } from 'src/routes/safe/components/Apps/types.d'
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
import { useDispatch } from 'react-redux'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { MULTI_SEND_ADDRESS } from 'src/logic/contracts/safeContracts'
import { DELEGATE_CALL, TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import { encodeMultiSendCall } from 'src/logic/safe/transactions/multisend'
import { estimateSafeTxGas } from 'src/logic/safe/transactions/gasNew'
import GasEstimationInfo from './GasEstimationInfo'
import { getNetworkInfo } from 'src/config'
const isTxValid = (t: Transaction): boolean => {
@ -67,6 +70,7 @@ type OwnProps = {
isOpen: boolean
app: SafeApp
txs: Transaction[]
params?: SendTransactionParams
safeAddress: string
safeName: string
ethBalance: string
@ -84,10 +88,39 @@ const ConfirmTransactionModal = ({
safeAddress,
ethBalance,
safeName,
params,
onUserConfirm,
onClose,
onTxReject,
}: OwnProps): React.ReactElement | null => {
const [estimatedSafeTxGas, setEstimatedSafeTxGas] = useState(0)
const [estimatingGas, setEstimatingGas] = useState(false)
useEffect(() => {
const estimateGas = async () => {
try {
setEstimatingGas(true)
const safeTxGas = await estimateSafeTxGas(
undefined,
safeAddress,
encodeMultiSendCall(txs),
MULTI_SEND_ADDRESS,
'0',
DELEGATE_CALL,
)
setEstimatedSafeTxGas(safeTxGas)
} catch (err) {
console.error(err)
} finally {
setEstimatingGas(false)
}
}
if (params?.safeTxGas) {
estimateGas()
}
}, [params, safeAddress, txs])
const dispatch = useDispatch()
if (!isOpen) {
return null
@ -117,6 +150,7 @@ const ConfirmTransactionModal = ({
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
origin: app.id,
navigateToTransactionsTab: false,
safeTxGas: Math.max(params?.safeTxGas || 0, estimatedSafeTxGas),
},
handleUserConfirmation,
handleTxRejection,
@ -162,6 +196,18 @@ const ConfirmTransactionModal = ({
</Collapse>
</Wrapper>
))}
<DividerLine withArrow={false} />
{params?.safeTxGas && (
<div className="section">
<Heading tag="h3">SafeTxGas</Heading>
<StyledTextBox>{params?.safeTxGas}</StyledTextBox>
<GasEstimationInfo
appEstimation={params.safeTxGas}
internalEstimation={estimatedSafeTxGas}
loading={estimatingGas}
/>
</div>
)}
</>
)

View File

@ -0,0 +1,47 @@
import React from 'react'
import styled from 'styled-components'
import Img from 'src/components/layout/Img'
import CheckIcon from 'src/assets/icons/check.svg'
import AlertIcon from 'src/assets/icons/alert.svg'
type OwnProps = {
appEstimation: number
internalEstimation: number
loading: boolean
}
const Container = styled.div`
display: flex;
align-items: center;
`
const imgStyles = {
marginRight: '5px',
}
const GasEstimationInfo = ({ appEstimation, internalEstimation, loading }: OwnProps): React.ReactElement => {
if (loading) {
return <p>Checking transaction parameters...</p>
}
let content: React.ReactElement | null = null
if (appEstimation >= internalEstimation) {
content = (
<>
<Img alt="Success" src={CheckIcon} style={imgStyles} /> Gas estimation is OK
</>
)
}
if (internalEstimation === 0) {
content = (
<>
<Img alt="Warning" src={AlertIcon} style={imgStyles} /> Error while estimating gas. The transaction may fail.
</>
)
}
return <Container>{content}</Container>
}
export default GasEstimationInfo

View File

@ -9,6 +9,7 @@ import {
RequestId,
Transaction,
LowercaseNetworks,
SendTransactionParams,
} from '@gnosis.pm/safe-apps-sdk'
import { useDispatch, useSelector } from 'react-redux'
import { useEffect, useCallback, MutableRefObject } from 'react'
@ -45,7 +46,7 @@ const NETWORK_NAME = getNetworkName()
const useIframeMessageHandler = (
selectedApp: SafeApp | undefined,
openConfirmationModal: (txs: Transaction[], requestId: RequestId) => void,
openConfirmationModal: (txs: Transaction[], params: SendTransactionParams | undefined, requestId: RequestId) => void,
closeModal: () => void,
iframeRef: MutableRefObject<HTMLIFrameElement | null>,
): ReturnType => {
@ -70,17 +71,35 @@ const useIframeMessageHandler = (
)
useEffect(() => {
const handleIframeMessage = (msg: CustomMessageEvent) => {
if (!msg?.data.messageId) {
const handleIframeMessage = (
messageId: SDKMessageIds,
messagePayload: SDKMessageToPayload[typeof messageId],
requestId: RequestId,
) => {
if (!messageId) {
console.error('ThirdPartyApp: A message was received without message id.')
return
}
const { requestId } = msg.data
switch (msg.data.messageId) {
switch (messageId) {
// typescript doesn't narrow type in switch/case statements
// issue: https://github.com/microsoft/TypeScript/issues/20375
// possible solution: https://stackoverflow.com/a/43879897/7820085
case SDK_MESSAGES.SEND_TRANSACTIONS: {
if (msg.data.data) {
openConfirmationModal(msg.data.data, requestId)
if (messagePayload) {
openConfirmationModal(
messagePayload as SDKMessageToPayload[typeof SDK_MESSAGES.SEND_TRANSACTIONS],
undefined,
requestId,
)
}
break
}
case SDK_MESSAGES.SEND_TRANSACTIONS_V2: {
const payload = messagePayload as SDKMessageToPayload[typeof SDK_MESSAGES.SEND_TRANSACTIONS_V2]
if (payload) {
openConfirmationModal(payload.txs, payload.params, requestId)
}
break
}
@ -106,7 +125,7 @@ const useIframeMessageHandler = (
break
}
default: {
console.error(`ThirdPartyApp: A message was received with an unknown message id ${msg.data.messageId}.`)
console.error(`ThirdPartyApp: A message was received with an unknown message id ${messageId}.`)
break
}
}
@ -119,7 +138,7 @@ const useIframeMessageHandler = (
console.error(`ThirdPartyApp: A message was received from an unknown origin ${message.origin}`)
return
}
handleIframeMessage(message)
handleIframeMessage(message.data.messageId, message.data.data, message.data.requestId)
}
window.addEventListener('message', onIframeMessage)

View File

@ -1,5 +1,11 @@
import React, { useCallback, useEffect, useRef, useState, useMemo } from 'react'
import { INTERFACE_MESSAGES, Transaction, RequestId, LowercaseNetworks } from '@gnosis.pm/safe-apps-sdk'
import {
INTERFACE_MESSAGES,
Transaction,
RequestId,
LowercaseNetworks,
SendTransactionParams,
} from '@gnosis.pm/safe-apps-sdk'
import { Card, IconText, Loader, Menu, Title } from '@gnosis.pm/safe-react-components'
import { useSelector } from 'react-redux'
import styled, { css } from 'styled-components'
@ -47,13 +53,15 @@ const CenteredMT = styled.div`
type ConfirmTransactionModalState = {
isOpen: boolean
txs: Transaction[]
requestId: RequestId | undefined
requestId?: RequestId
params?: SendTransactionParams
}
const INITIAL_CONFIRM_TX_MODAL_STATE: ConfirmTransactionModalState = {
isOpen: false,
txs: [],
requestId: undefined,
params: undefined,
}
const NETWORK_NAME = getNetworkName()
@ -75,11 +83,12 @@ const Apps = (): React.ReactElement => {
const ethBalance = useSelector(safeEthBalanceSelector)
const openConfirmationModal = useCallback(
(txs: Transaction[], requestId: RequestId) =>
(txs: Transaction[], params: SendTransactionParams | undefined, requestId: RequestId) =>
setConfirmTransactionModal({
isOpen: true,
txs,
requestId,
params,
}),
[setConfirmTransactionModal],
)
@ -215,6 +224,7 @@ const Apps = (): React.ReactElement => {
txs={confirmTransactionModal.txs}
onClose={closeConfirmationModal}
onUserConfirm={onUserTxConfirm}
params={confirmTransactionModal.params}
onTxReject={onTxReject}
/>
</>

View File

@ -7,7 +7,7 @@ import TableContainer from '@material-ui/core/TableContainer'
import TableRow from '@material-ui/core/TableRow'
import { Skeleton } from '@material-ui/lab'
import InfoIcon from 'src/assets/icons/info.svg'
import InfoIcon from 'src/assets/icons/info_red.svg'
import Img from 'src/components/layout/Img'
import Table from 'src/components/Table'

2488
yarn.lock

File diff suppressed because it is too large Load Diff