-
Notifications
You must be signed in to change notification settings - Fork 477
/
Copy pathhooks.ts
245 lines (205 loc) · 8.56 KB
/
hooks.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
import { assertTx, assertWallet, assertOnboard, assertChainInfo } from '@/utils/helpers'
import { useMemo } from 'react'
import { type TransactionOptions, type SafeTransaction } from '@safe-global/safe-core-sdk-types'
import { sameString } from '@safe-global/protocol-kit/dist/src/utils'
import useSafeInfo from '@/hooks/useSafeInfo'
import useWallet from '@/hooks/wallets/useWallet'
import useOnboard from '@/hooks/wallets/useOnboard'
import { isSmartContractWallet } from '@/utils/wallets'
import {
dispatchDelegateTxSigning,
dispatchOnChainSigning,
dispatchTxExecution,
dispatchTxProposal,
dispatchTxRelay,
dispatchTxSigning,
} from '@/services/tx/tx-sender'
import { useHasPendingTxs } from '@/hooks/usePendingTxs'
import { getSafeTxGas, getNonces } from '@/services/tx/tx-sender/recommendedNonce'
import type { AsyncResult } from '@/hooks/useAsync'
import useAsync from '@/hooks/useAsync'
import { useUpdateBatch } from '@/hooks/useDraftBatch'
import { getTransactionDetails, type TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk'
import { useCurrentChain } from '@/hooks/useChains'
type TxActions = {
addToBatch: (safeTx?: SafeTransaction, origin?: string) => Promise<string>
signTx: (safeTx?: SafeTransaction, txId?: string, origin?: string) => Promise<string>
executeTx: (
txOptions: TransactionOptions,
safeTx?: SafeTransaction,
txId?: string,
origin?: string,
isRelayed?: boolean,
) => Promise<string>
signDelegateTx: (safeTx?: SafeTransaction) => Promise<string>
proposeTx: (safeTx: SafeTransaction, txId?: string, origin?: string) => Promise<TransactionDetails>
}
type txDetails = AsyncResult<TransactionDetails>
export const useProposeTx = (safeTx?: SafeTransaction, txId?: string, origin?: string): txDetails => {
const { proposeTx } = useTxActions()
const { safe } = useSafeInfo()
return useAsync(
() => {
if (!safeTx) return
if (txId) return getTransactionDetails(safe.chainId, txId)
return proposeTx(safeTx, txId, origin)
},
[safeTx, txId, origin, proposeTx],
false,
)
}
export const useTxActions = (): TxActions => {
const { safe } = useSafeInfo()
const onboard = useOnboard()
const wallet = useWallet()
const [addTxToBatch] = useUpdateBatch()
const chain = useCurrentChain()
return useMemo<TxActions>(() => {
const safeAddress = safe.address.value
const { chainId, version } = safe
const _propose = async (sender: string, safeTx: SafeTransaction, txId?: string, origin?: string) => {
return dispatchTxProposal({
chainId,
safeAddress,
sender,
safeTx,
txId,
origin,
})
}
const proposeTx: TxActions['proposeTx'] = async (safeTx, txId, origin) => {
assertTx(safeTx)
// assertWallet(wallet)
return _propose(wallet?.address || safe.owners[0].value, safeTx, txId, origin)
}
const addToBatch: TxActions['addToBatch'] = async (safeTx, origin) => {
assertTx(safeTx)
assertWallet(wallet)
const tx = await _propose(wallet.address, safeTx, undefined, origin)
await addTxToBatch(tx)
return tx.txId
}
const signRelayedTx = async (safeTx: SafeTransaction, txId?: string): Promise<SafeTransaction> => {
assertTx(safeTx)
assertWallet(wallet)
// Smart contracts cannot sign transactions off-chain
if (await isSmartContractWallet(wallet.chainId, wallet.address)) {
throw new Error('Cannot relay an unsigned transaction from a smart contract wallet')
}
return await dispatchTxSigning(safeTx, version, wallet.provider, txId)
}
const signTx: TxActions['signTx'] = async (safeTx, txId, origin) => {
assertTx(safeTx)
assertWallet(wallet)
assertOnboard(onboard)
// Smart contract wallets must sign via an on-chain tx
if (await isSmartContractWallet(wallet.chainId, wallet.address)) {
// 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 _propose(wallet.address, safeTx, txId, origin)).txId
await dispatchOnChainSigning(safeTx, id, wallet.provider, chainId, wallet.address, safeAddress)
return id
}
// Otherwise, sign off-chain
const signedTx = await dispatchTxSigning(safeTx, version, wallet.provider, txId)
const tx = await _propose(wallet.address, signedTx, txId, origin)
return tx.txId
}
const signDelegateTx: TxActions['signDelegateTx'] = async (safeTx) => {
assertTx(safeTx)
assertWallet(wallet)
assertOnboard(onboard)
const signedTx = await dispatchDelegateTxSigning(safeTx, wallet)
const tx = await _propose(wallet.address, signedTx)
return tx.txId
}
const executeTx: TxActions['executeTx'] = async (txOptions, safeTx, txId, origin, isRelayed) => {
assertTx(safeTx)
assertWallet(wallet)
assertOnboard(onboard)
assertChainInfo(chain)
let tx: TransactionDetails | undefined
// Relayed transactions must be fully signed, so request a final signature if needed
if (isRelayed && safeTx.signatures.size < safe.threshold) {
if (txId) {
safeTx = await signRelayedTx(safeTx)
tx = await _propose(wallet.address, safeTx, txId, origin)
} else {
tx = await _propose(wallet.address, safeTx, txId, origin)
safeTx = await signRelayedTx(safeTx)
}
txId = tx.txId
}
// Propose the tx if there's no id yet ("immediate execution")
if (!txId) {
tx = await _propose(wallet.address, safeTx, txId, origin)
txId = tx.txId
}
// Relay or execute the tx via connected wallet
if (isRelayed) {
await dispatchTxRelay(safeTx, safe, txId, chain, txOptions.gasLimit)
} else {
const isSmartAccount = await isSmartContractWallet(wallet.chainId, wallet.address)
await dispatchTxExecution(safeTx, txOptions, txId, wallet.provider, wallet.address, safeAddress, isSmartAccount)
}
return txId
}
return { addToBatch, signTx, executeTx, signDelegateTx, proposeTx }
}, [safe, wallet, addTxToBatch, onboard, chain])
}
export const useValidateNonce = (safeTx: SafeTransaction | undefined): boolean => {
const { safe } = useSafeInfo()
return !!safeTx && safeTx?.data.nonce === safe.nonce
}
export const useImmediatelyExecutable = (): boolean => {
const { safe } = useSafeInfo()
const hasPending = useHasPendingTxs()
return safe.threshold === 1 && !hasPending
}
// Check if the executor is the safe itself (it won't work)
export const useIsExecutionLoop = (): boolean => {
const wallet = useWallet()
const { safeAddress } = useSafeInfo()
return wallet ? sameString(wallet.address, safeAddress) : false
}
export const useRecommendedNonce = (): number | undefined => {
const { safeAddress, safe } = useSafeInfo()
const [recommendedNonce] = useAsync(
async () => {
if (!safe.chainId || !safeAddress) return
if (!safe.deployed) return 0
const nonces = await getNonces(safe.chainId, safeAddress)
return nonces?.recommendedNonce
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[safeAddress, safe.chainId, safe.txQueuedTag, safe.txHistoryTag], // update when tx queue or history changes
false, // keep old recommended nonce while refreshing to avoid skeleton
)
return recommendedNonce
}
export const useSafeTxGas = (safeTx: SafeTransaction | undefined): string | undefined => {
const { safeAddress, safe } = useSafeInfo()
// Memoize only the necessary params so that the useAsync hook is not called every time safeTx changes
const safeTxParams = useMemo(() => {
return !safeTx?.data?.to
? undefined
: {
to: safeTx?.data.to,
value: safeTx?.data?.value,
data: safeTx?.data?.data,
operation: safeTx?.data?.operation,
}
}, [safeTx?.data.to, safeTx?.data.value, safeTx?.data.data, safeTx?.data.operation])
const [safeTxGas] = useAsync(() => {
if (!safe.chainId || !safeAddress || !safeTxParams || !safe.version) return
return getSafeTxGas(safe.chainId, safeAddress, safe.version, safeTxParams)
}, [safeAddress, safe.chainId, safe.version, safeTxParams])
return safeTxGas
}
export const useAlreadySigned = (safeTx: SafeTransaction | undefined): boolean => {
const wallet = useWallet()
const hasSigned =
safeTx && wallet && (safeTx.signatures.has(wallet.address.toLowerCase()) || safeTx.signatures.has(wallet.address))
return Boolean(hasSigned)
}