Skip to content

Commit

Permalink
Feat: track tx origin
Browse files Browse the repository at this point in the history
  • Loading branch information
katspaugh committed Oct 17, 2023
1 parent 8189cd7 commit 25fd2c5
Show file tree
Hide file tree
Showing 12 changed files with 83 additions and 85 deletions.
6 changes: 1 addition & 5 deletions src/components/tx/SignOrExecuteForm/ExecuteForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ import commonCss from '@/components/tx-flow/common/styles.module.css'
import { TxSecurityContext } from '../security/shared/TxSecurityContext'
import useIsSafeOwner from '@/hooks/useIsSafeOwner'
import NonOwnerError from '@/components/tx/SignOrExecuteForm/NonOwnerError'
import { useAppSelector } from '@/store'
import { selectQueuedTransactionById } from '@/store/txQueueSlice'

const ExecuteForm = ({
safeTx,
Expand All @@ -52,8 +50,6 @@ const ExecuteForm = ({
const { setTxFlow } = useContext(TxModalContext)
const { needsRiskConfirmation, isRiskConfirmed, setIsRiskIgnored } = useContext(TxSecurityContext)

const tx = useAppSelector((state) => selectQueuedTransactionById(state, txId))

// Check that the transaction is executable
const isExecutionLoop = useIsExecutionLoop()

Expand Down Expand Up @@ -89,7 +85,7 @@ const ExecuteForm = ({
const txOptions = getTxOptions(advancedParams, currentChain)

try {
const executedTxId = await executeTx(txOptions, safeTx, txId, origin, willRelay, tx)
const executedTxId = await executeTx(txOptions, safeTx, txId, origin, willRelay)
setTxFlow(<SuccessScreen txId={executedTxId} />, undefined, false)
} catch (_err) {
const err = asError(_err)
Expand Down
6 changes: 1 addition & 5 deletions src/components/tx/SignOrExecuteForm/SignForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ import commonCss from '@/components/tx-flow/common/styles.module.css'
import { TxSecurityContext } from '../security/shared/TxSecurityContext'
import NonOwnerError from '@/components/tx/SignOrExecuteForm/NonOwnerError'
import BatchButton from './BatchButton'
import { useAppSelector } from '@/store'
import { selectQueuedTransactionById } from '@/store/txQueueSlice'

const SignForm = ({
safeTx,
Expand All @@ -40,8 +38,6 @@ const SignForm = ({
const { needsRiskConfirmation, isRiskConfirmed, setIsRiskIgnored } = useContext(TxSecurityContext)
const hasSigned = useAlreadySigned(safeTx)

const tx = useAppSelector((state) => selectQueuedTransactionById(state, txId))

// On modal submit
const handleSubmit = async (e: SyntheticEvent, isAddingToBatch = false) => {
e.preventDefault()
Expand All @@ -57,7 +53,7 @@ const SignForm = ({
setSubmitError(undefined)

try {
await (isAddingToBatch ? addToBatch(safeTx, origin) : signTx(safeTx, txId, origin, tx))
await (isAddingToBatch ? addToBatch(safeTx, origin) : signTx(safeTx, txId, origin))
} catch (_err) {
const err = asError(_err)
trackError(Errors._805, err)
Expand Down
21 changes: 8 additions & 13 deletions src/components/tx/SignOrExecuteForm/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,17 @@ import type { OnboardAPI } from '@web3-onboard/core'
import { getSafeTxGas, getRecommendedNonce } from '@/services/tx/tx-sender/recommendedNonce'
import useAsync from '@/hooks/useAsync'
import { useUpdateBatch } from '@/hooks/useDraftBatch'
import { type Transaction, type TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk'
import { type TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk'

type TxActions = {
addToBatch: (safeTx?: SafeTransaction, origin?: string) => Promise<string>
signTx: (safeTx?: SafeTransaction, txId?: string, origin?: string, transaction?: Transaction) => Promise<string>
signTx: (safeTx?: SafeTransaction, txId?: string, origin?: string) => Promise<string>
executeTx: (
txOptions: TransactionOptions,
safeTx?: SafeTransaction,
txId?: string,
origin?: string,
isRelayed?: boolean,
transaction?: Transaction,
) => Promise<string>
}

Expand Down Expand Up @@ -85,30 +84,28 @@ export const useTxActions = (): TxActions => {
return await dispatchTxSigning(safeTx, version, onboard, chainId, txId)
}

const signTx: TxActions['signTx'] = async (safeTx, txId, origin, transaction) => {
const signTx: TxActions['signTx'] = async (safeTx, txId, origin) => {
assertTx(safeTx)
assertWallet(wallet)
assertOnboard(onboard)

const humanDescription = transaction?.transaction?.txInfo?.humanDescription

// Smart contract wallets must sign via an on-chain tx
if (await isSmartContractWallet(wallet)) {
// If the first signature is a smart contract wallet, we have to propose w/o signatures
// Otherwise the backend won't pick up the tx
// The signature will be added once the on-chain signature is indexed
const id = txId || (await proposeTx(wallet.address, safeTx, txId, origin)).txId
await dispatchOnChainSigning(safeTx, id, onboard, chainId, humanDescription)
await dispatchOnChainSigning(safeTx, id, onboard, chainId)
return id
}

// Otherwise, sign off-chain
const signedTx = await dispatchTxSigning(safeTx, version, onboard, chainId, txId, humanDescription)
const signedTx = await dispatchTxSigning(safeTx, version, onboard, chainId, txId)
const tx = await proposeTx(wallet.address, signedTx, txId, origin)
return tx.txId
}

const executeTx: TxActions['executeTx'] = async (txOptions, safeTx, txId, origin, isRelayed, transaction) => {
const executeTx: TxActions['executeTx'] = async (txOptions, safeTx, txId, origin, isRelayed) => {
assertTx(safeTx)
assertWallet(wallet)
assertOnboard(onboard)
Expand All @@ -127,13 +124,11 @@ export const useTxActions = (): TxActions => {
txId = tx.txId
}

const humanDescription = tx?.txInfo?.humanDescription || transaction?.transaction?.txInfo?.humanDescription

// Relay or execute the tx via connected wallet
if (isRelayed) {
await dispatchTxRelay(safeTx, safe, txId, txOptions.gasLimit, humanDescription)
await dispatchTxRelay(safeTx, safe, txId, txOptions.gasLimit)
} else {
await dispatchTxExecution(safeTx, txOptions, txId, onboard, chainId, safeAddress, humanDescription)
await dispatchTxExecution(safeTx, txOptions, txId, onboard, chainId, safeAddress)
}

return txId
Expand Down
14 changes: 11 additions & 3 deletions src/hooks/useTxNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import useIsSafeOwner from '@/hooks/useIsSafeOwner'
import useWallet from './wallets/useWallet'
import useSafeAddress from './useSafeAddress'
import { getExplorerLink } from '@/utils/gateway'
import { getTxDetails } from '@/services/tx/txDetails'

const TxNotifications = {
[TxEvent.SIGN_FAILED]: 'Failed to sign. Please try again.',
Expand Down Expand Up @@ -69,15 +70,22 @@ const useTxNotifications = (): void => {
const entries = Object.entries(TxNotifications) as [keyof typeof TxNotifications, string][]

const unsubFns = entries.map(([event, baseMessage]) =>
txSubscribe(event, (detail) => {
txSubscribe(event, async (detail) => {
const isError = 'error' in detail
const isSuccess = successEvents.includes(event)
const message = isError ? `${baseMessage} ${formatError(detail.error)}` : baseMessage
const txId = 'txId' in detail ? detail.txId : undefined
const txHash = 'txHash' in detail ? detail.txHash : undefined
const groupKey = 'groupKey' in detail && detail.groupKey ? detail.groupKey : txId || ''
const humanDescription =
'humanDescription' in detail && detail.humanDescription ? detail.humanDescription : 'Transaction'

let humanDescription = 'Transaction'
const id = txId || txHash
if (id) {
try {
const txDetails = await getTxDetails(chain.chainId, id)
humanDescription = txDetails.txInfo.humanDescription || humanDescription
} catch {}
}

dispatch(
showNotification({
Expand Down
32 changes: 26 additions & 6 deletions src/hooks/useTxTracking.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,43 @@
import { trackEvent, WALLET_EVENTS } from '@/services/analytics'
import { getTxDetails } from '@/services/tx/txDetails'
import { TxEvent, txSubscribe } from '@/services/tx/txEvents'
import { useEffect } from 'react'
import useChainId from './useChainId'

const events = {
[TxEvent.SIGNED]: WALLET_EVENTS.OFF_CHAIN_SIGNATURE,
[TxEvent.PROCESSING]: WALLET_EVENTS.ON_CHAIN_INTERACTION,
[TxEvent.RELAYING]: WALLET_EVENTS.RELAYED_EXECUTION,
[TxEvent.SIGNED]: WALLET_EVENTS.OFFCHAIN_SIGNATURE,
[TxEvent.PROCESSING]: WALLET_EVENTS.ONCHAIN_INTERACTION,
[TxEvent.PROCESSING_MODULE]: WALLET_EVENTS.ONCHAIN_INTERACTION,
[TxEvent.RELAYING]: WALLET_EVENTS.ONCHAIN_INTERACTION,
}

export const useTxTracking = (): void => {
const chainId = useChainId()

useEffect(() => {
const unsubFns = Object.entries(events).map(([txEvent, analyticsEvent]) =>
txSubscribe(txEvent as TxEvent, () => {
trackEvent(analyticsEvent)
txSubscribe(txEvent as TxEvent, async (detail) => {
const txId = 'txId' in detail ? detail.txId : undefined
const txHash = 'txHash' in detail ? detail.txHash : undefined
const id = txId || txHash

let origin = ''
if (id) {
try {
const txDetails = await getTxDetails(chainId, id)
origin = txDetails.safeAppInfo?.url || ''
} catch {}
}

trackEvent({
...analyticsEvent,
label: origin,
})
}),
)

return () => {
unsubFns.forEach((unsub) => unsub())
}
}, [])
}, [chainId])
}
6 changes: 2 additions & 4 deletions src/services/analytics/events/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,12 @@ export const WALLET_EVENTS = {
action: 'WalletConnect peer',
category: WALLET_CATEGORY,
},
OFF_CHAIN_SIGNATURE: {
OFFCHAIN_SIGNATURE: {
event: EventType.META,
action: 'Off-chain signature',
category: WALLET_CATEGORY,
},
// Please note that this event isn't triggered for any on-chain interaction.
// It's only triggered on safe tx execution and batch execution.
ON_CHAIN_INTERACTION: {
ONCHAIN_INTERACTION: {
event: EventType.META,
action: 'On-chain interaction',
category: WALLET_CATEGORY,
Expand Down
14 changes: 4 additions & 10 deletions src/services/tx/tx-sender/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ export const dispatchTxProposal = async ({
txDispatch(txId ? TxEvent.SIGNATURE_PROPOSED : TxEvent.PROPOSED, {
txId: proposedTx.txId,
signerAddress: txId ? sender : undefined,
humanDescription: proposedTx?.txInfo?.humanDescription,
})
}

Expand All @@ -79,7 +78,6 @@ export const dispatchTxSigning = async (
onboard: OnboardAPI,
chainId: SafeInfo['chainId'],
txId?: string,
humanDescription?: string,
): Promise<SafeTransaction> => {
const sdk = await getSafeSDKWithSigner(onboard, chainId)

Expand All @@ -90,7 +88,6 @@ export const dispatchTxSigning = async (
txDispatch(TxEvent.SIGN_FAILED, {
txId,
error: asError(error),
humanDescription,
})
throw error
}
Expand All @@ -108,11 +105,10 @@ export const dispatchOnChainSigning = async (
txId: string,
onboard: OnboardAPI,
chainId: SafeInfo['chainId'],
humanDescription?: string,
) => {
const sdkUnchecked = await getUncheckedSafeSDK(onboard, chainId)
const safeTxHash = await sdkUnchecked.getTransactionHash(safeTx)
const eventParams = { txId, humanDescription }
const eventParams = { txId }

try {
// With the unchecked signer, the contract call resolves once the tx
Expand Down Expand Up @@ -140,10 +136,9 @@ export const dispatchTxExecution = async (
onboard: OnboardAPI,
chainId: SafeInfo['chainId'],
safeAddress: string,
humanDescription?: string,
): Promise<string> => {
const sdkUnchecked = await getUncheckedSafeSDK(onboard, chainId)
const eventParams = { txId, humanDescription }
const eventParams = { txId }

// Execute the tx
let result: TransactionResult | undefined
Expand Down Expand Up @@ -327,7 +322,6 @@ export const dispatchTxRelay = async (
safe: SafeInfo,
txId: string,
gasLimit?: string | number,
humanDescription?: string,
) => {
const readOnlySafeContract = getReadOnlyCurrentGnosisSafeContract(safe)

Expand Down Expand Up @@ -356,9 +350,9 @@ export const dispatchTxRelay = async (
txDispatch(TxEvent.RELAYING, { taskId, txId })

// Monitor relay tx
waitForRelayedTx(taskId, [txId], safe.address.value, humanDescription)
waitForRelayedTx(taskId, [txId], safe.address.value)
} catch (error) {
txDispatch(TxEvent.FAILED, { txId, error: asError(error), humanDescription })
txDispatch(TxEvent.FAILED, { txId, error: asError(error) })
throw error
}
}
Expand Down
15 changes: 15 additions & 0 deletions src/services/tx/txDetails.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import memoize from 'lodash/memoize'
import { getTransactionDetails } from '@safe-global/safe-gateway-typescript-sdk'

/**
* Get and memoize transaction details from Safe Gatewa
* @param id Transaction id or hash
* @param chainId Chain id
* @returns Transaction details
*/
export const getTxDetails = memoize(
(id: string, chainId: string) => {
return getTransactionDetails(id, chainId)
},
(id: string, chainId: string) => `${id}-${chainId}`,
)
29 changes: 14 additions & 15 deletions src/services/tx/txEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,25 @@ export enum TxEvent {
}

type Id = { txId: string; groupKey?: string } | { txId?: string; groupKey: string }
type HumanDescription = { humanDescription?: string }

interface TxEvents {
[TxEvent.SIGNED]: { txId?: string }
[TxEvent.SIGN_FAILED]: HumanDescription & { txId?: string; error: Error }
[TxEvent.PROPOSE_FAILED]: HumanDescription & { error: Error }
[TxEvent.PROPOSED]: HumanDescription & { txId: string }
[TxEvent.SIGNATURE_PROPOSE_FAILED]: HumanDescription & { txId: string; error: Error }
[TxEvent.SIGNATURE_PROPOSED]: HumanDescription & { txId: string; signerAddress: string }
[TxEvent.SIGN_FAILED]: { txId?: string; error: Error }
[TxEvent.PROPOSE_FAILED]: { error: Error }
[TxEvent.PROPOSED]: { txId: string }
[TxEvent.SIGNATURE_PROPOSE_FAILED]: { txId: string; error: Error }
[TxEvent.SIGNATURE_PROPOSED]: { txId: string; signerAddress: string }
[TxEvent.SIGNATURE_INDEXED]: { txId: string }
[TxEvent.ONCHAIN_SIGNATURE_REQUESTED]: Id & HumanDescription
[TxEvent.ONCHAIN_SIGNATURE_SUCCESS]: Id & HumanDescription
[TxEvent.EXECUTING]: Id & HumanDescription
[TxEvent.PROCESSING]: Id & HumanDescription & { txHash: string }
[TxEvent.PROCESSING_MODULE]: Id & HumanDescription & { txHash: string }
[TxEvent.PROCESSED]: Id & HumanDescription & { safeAddress: string }
[TxEvent.REVERTED]: Id & HumanDescription & { error: Error }
[TxEvent.ONCHAIN_SIGNATURE_REQUESTED]: Id
[TxEvent.ONCHAIN_SIGNATURE_SUCCESS]: Id
[TxEvent.EXECUTING]: Id
[TxEvent.PROCESSING]: Id & { txHash: string }
[TxEvent.PROCESSING_MODULE]: Id & { txHash: string }
[TxEvent.PROCESSED]: Id & { safeAddress: string }
[TxEvent.REVERTED]: Id & { error: Error }
[TxEvent.RELAYING]: Id & { taskId: string }
[TxEvent.FAILED]: Id & HumanDescription & { error: Error }
[TxEvent.SUCCESS]: Id & HumanDescription
[TxEvent.FAILED]: Id & { error: Error }
[TxEvent.SUCCESS]: Id
[TxEvent.SAFE_APPS_REQUEST]: { safeAppRequestId: RequestId; safeTxHash: string }
[TxEvent.BATCH_ADD]: Id
}
Expand Down
Loading

0 comments on commit 25fd2c5

Please sign in to comment.