Skip to content

Commit

Permalink
Merge pull request #166 from Skwst/feat-bridge-aware-translations
Browse files Browse the repository at this point in the history
feature: Added KPK-Translations
  • Loading branch information
jfschwarz authored Nov 25, 2024
2 parents 17a08f9 + 58c0faf commit 782fed9
Show file tree
Hide file tree
Showing 9 changed files with 1,017 additions and 58 deletions.
4 changes: 1 addition & 3 deletions extension/src/browser/Drawer/RolePermissionCheck.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,7 @@ const RolePermissionCheck: React.FC<{
const { route } = useRoute()
const provider = useProvider()

const translationAvailable = !!useApplicableTranslation(
transactionState.transaction
)
const translationAvailable = !!useApplicableTranslation(index)

useEffect(() => {
let canceled = false
Expand Down
38 changes: 5 additions & 33 deletions extension/src/browser/Drawer/Translate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { IconButton } from '../../components'
import { ForkProvider } from '../../providers'
import { useApplicableTranslation } from '../../transactionTranslations'
import { useProvider } from '../ProvideProvider'
import { TransactionState, useDispatch, useTransactions } from '../../state'
import { TransactionState } from '../../state'

import classes from './style.module.css'

Expand All @@ -15,15 +15,9 @@ type Props = {
labeled?: true
}

export const Translate: React.FC<Props> = ({
transactionState,
index,
labeled,
}) => {
export const Translate: React.FC<Props> = ({ index, labeled }) => {
const provider = useProvider()
const dispatch = useDispatch()
const transactions = useTransactions()
const translation = useApplicableTranslation(transactionState.transaction)
const translation = useApplicableTranslation(index)

if (!(provider instanceof ForkProvider)) {
// Transaction translation is only supported when using ForkProvider
Expand All @@ -34,38 +28,16 @@ export const Translate: React.FC<Props> = ({
return null
}

const handleTranslate = async () => {
const laterTransactions = transactions
.slice(index + 1)
.map((txState) => txState.transaction)

// remove the transaction and all later ones from the store
dispatch({
type: 'REMOVE_TRANSACTION',
payload: { id: transactionState.id },
})

// revert to checkpoint before the transaction to remove
const checkpoint = transactionState.snapshotId // the ForkProvider uses checkpoints as IDs for the recorded transactions
await provider.request({ method: 'evm_revert', params: [checkpoint] })

// re-simulate all transactions starting with the translated ones
const replayTransaction = [...translation.result, ...laterTransactions]
for (const tx of replayTransaction) {
provider.sendMetaTransaction(tx)
}
}

if (labeled) {
return (
<button onClick={handleTranslate} className={classes.link}>
<button onClick={translation.apply} className={classes.link}>
{translation.title}
<BiWrench />
</button>
)
} else {
return (
<IconButton onClick={handleTranslate} title={translation.title}>
<IconButton onClick={translation.apply} title={translation.title}>
<BiWrench />
</IconButton>
)
Expand Down
4 changes: 4 additions & 0 deletions extension/src/browser/Drawer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useDispatch, useTransactions } from '../../state'
import Submit from './Submit'
import { Transaction, TransactionBadge } from './Transaction'
import classes from './style.module.css'
import { useGloballyApplicableTranslation } from '../../transactionTranslations'

const TransactionsDrawer: React.FC = () => {
const [expanded, setExpanded] = useState(true)
Expand All @@ -19,6 +20,9 @@ const TransactionsDrawer: React.FC = () => {
const provider = useProvider()
const { route } = useRoute()

// for now we assume global translations are generally auto-applied, so we don't need to show a button for them
useGloballyApplicableTranslation()

const scrollContainerRef = useRef<HTMLDivElement | null>(null)

const [scrollItemIntoView, setScrollItemIntoView] = useState<
Expand Down
237 changes: 222 additions & 15 deletions extension/src/transactionTranslations/index.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,248 @@
import { useEffect, useState } from 'react'
import { ChainId } from 'ser-kit'
import { useCallback, useEffect, useState } from 'react'
import { ChainId, parsePrefixedAddress } from 'ser-kit'

import { useRoute } from '../routes'

import cowswapSetPreSignature from './cowswapSetPreSignature'
import { TransactionTranslation } from './types'
import uniswapMulticall from './uniswapMulticall'
import { MetaTransactionData } from '@safe-global/safe-core-sdk-types'
import kpkBridgeAware from './karpatkeyInstitutional/kpkBridgeAware'
import { TransactionState, useDispatch, useTransactions } from '../state'
import { useProvider } from '../browser/ProvideProvider'
import { ForkProvider } from '../providers'

// ADD ANY NEW TRANSLATIONS TO THIS ARRAY
const translations: TransactionTranslation[] = [
uniswapMulticall,
cowswapSetPreSignature,
kpkBridgeAware,
]

interface ApplicableTranslation {
/** Title of the applied translation (TransactionTranslation.title) */
title: string
/** If true, the translation will be applied automatically. By default translations will be suggested to the user who then applies them by clicking a button **/
autoApply?: boolean
/** The translation result (return value of TransactionTranslation.translate) */
result: MetaTransactionData[]
}

export const useApplicableTranslation = (
encodedTransaction: MetaTransactionData
) => {
export const useGloballyApplicableTranslation = () => {
const provider = useProvider()
const transactions = useTransactions()

const dispatch = useDispatch()
const { chainId, route } = useRoute()
const [_, avatarAddress] = parsePrefixedAddress(route.avatar)

const apply = useCallback(
async (translation: ApplicableTranslation) => {
if (!(provider instanceof ForkProvider)) {
throw new Error(
'Transaction translation is only supported when using ForkProvider'
)
}

const newTransactions = translation.result
const firstDifferenceIndex = transactions.findIndex(
(tx, index) =>
!transactionsEqual(tx.transaction, newTransactions[index])
)

if (
firstDifferenceIndex === -1 &&
newTransactions.length === transactions.length
) {
console.warn(
'Global translations returned the original set of transactions. It should return undefined in that case.'
)
return
}

if (firstDifferenceIndex !== -1) {
// remove all transactions from the store starting at the first difference
dispatch({
type: 'REMOVE_TRANSACTION',
payload: { id: transactions[firstDifferenceIndex].id },
})

// revert to checkpoint before first difference
const checkpoint = transactions[firstDifferenceIndex].snapshotId // the ForkProvider uses checkpoints as IDs for the recorded transactions
await provider.request({ method: 'evm_revert', params: [checkpoint] })
}

// re-simulate all transactions starting at the first difference
const replayTransaction =
firstDifferenceIndex === -1
? newTransactions.slice(transactions.length)
: newTransactions.slice(firstDifferenceIndex)
for (const tx of replayTransaction) {
provider.sendMetaTransaction(tx)
}
},
[provider, dispatch, transactions]
)

useEffect(() => {
let canceled = false
const run = async () => {
const translation = await findGloballyApplicableTranslation(
transactions,
chainId,
avatarAddress
)
if (canceled) return

if (translation?.autoApply) {
apply(translation)
} else {
throw new Error('Not implemented')
}
}
run()
return () => {
canceled = true
}
}, [transactions, chainId, avatarAddress, apply])
}

const findGloballyApplicableTranslation = async (
transactions: TransactionState[],
chainId: ChainId,
avatarAddress: `0x${string}`
): Promise<ApplicableTranslation | undefined> => {
if (transactions.length === 0) return undefined

// we cache the result of the translation to avoid test-running translation functions over and over again
const key = cacheKeyGlobal(transactions, chainId, avatarAddress)
if (applicableTranslationsCache.has(key)) {
return await applicableTranslationsCache.get(key)
}

const tryApplyingTranslations = async () => {
for (const translation of translations) {
if (!('translateGlobal' in translation)) continue
const result = await translation.translateGlobal(
transactions.map((txState) => txState.transaction),
chainId,
avatarAddress
)
if (result) {
return {
title: translation.title,
autoApply: translation.autoApply,
result,
}
break
}
}
}
const resultPromise = tryApplyingTranslations()
applicableTranslationsCache.set(key, resultPromise)

return await resultPromise
}

export const useApplicableTranslation = (transactionIndex: number) => {
const provider = useProvider()
const transactions = useTransactions()
const metaTransaction = transactions[transactionIndex].transaction

const dispatch = useDispatch()
const { chainId, route } = useRoute()
const [_, avatarAddress] = parsePrefixedAddress(route.avatar)

const [translation, setTranslation] = useState<
ApplicableTranslation | undefined
>(undefined)

const { chainId } = useRoute()
const apply = useCallback(
async (translation: ApplicableTranslation) => {
const transactionState = transactions[transactionIndex]
const laterTransactions = transactions
.slice(transactionIndex + 1)
.map((txState) => txState.transaction)

if (!(provider instanceof ForkProvider)) {
throw new Error(
'Transaction translation is only supported when using ForkProvider'
)
}

// remove the transaction and all later ones from the store
dispatch({
type: 'REMOVE_TRANSACTION',
payload: { id: transactionState.id },
})

// revert to checkpoint before the transaction to remove
const checkpoint = transactionState.snapshotId // the ForkProvider uses checkpoints as IDs for the recorded transactions
await provider.request({ method: 'evm_revert', params: [checkpoint] })

// re-simulate all transactions starting with the translated ones
const replayTransaction = [...translation.result, ...laterTransactions]
for (const tx of replayTransaction) {
provider.sendMetaTransaction(tx)
}
},
[provider, dispatch, transactions, transactionIndex]
)

useEffect(() => {
findApplicableTranslation(encodedTransaction, chainId).then(setTranslation)
}, [encodedTransaction, chainId])
let canceled = false
const run = async () => {
const translation = await findApplicableTranslation(
metaTransaction,
chainId,
avatarAddress
)
if (canceled) return

return translation
if (translation?.autoApply) {
apply(translation)
} else {
setTranslation(translation)
}
}
run()
return () => {
canceled = true
}
}, [metaTransaction, chainId, avatarAddress, apply])

return translation && !translation.autoApply
? {
title: translation.title,
result: translation.result,
apply: () => apply(translation),
}
: undefined
}

const findApplicableTranslation = async (
transaction: MetaTransactionData,
chainId: ChainId
metaTransaction: MetaTransactionData,
chainId: ChainId,
avatarAddress: `0x${string}`
): Promise<ApplicableTranslation | undefined> => {
// we cache the result of the translation to avoid test-running translation functions over and over again
const key = cacheKey(transaction, chainId)
const key = cacheKey(metaTransaction, chainId, avatarAddress)
if (applicableTranslationsCache.has(key)) {
return await applicableTranslationsCache.get(key)
}

const tryApplyingTranslations = async () => {
for (const translation of translations) {
const result = await translation.translate(transaction, chainId)
if (!('translate' in translation)) continue
const result = await translation.translate(
metaTransaction,
chainId,
avatarAddress
)
if (result) {
return {
title: translation.title,
autoApply: translation.autoApply,
result,
}
break
Expand All @@ -69,7 +259,24 @@ const applicableTranslationsCache = new Map<
string,
Promise<ApplicableTranslation | undefined>
>()
const cacheKey = (transaction: MetaTransactionData, chainId: ChainId) =>
`${chainId}:${transaction.to}:${transaction.value}:${transaction.data}:${

const cacheKey = (
transaction: MetaTransactionData,
chainId: ChainId,
avatarAddress: `0x${string}`
) =>
`${chainId}:${avatarAddress}:${transaction.to}:${transaction.value}:${transaction.data}:${
transaction.operation || 0
}`

const cacheKeyGlobal = (
transactions: TransactionState[],
chainId: ChainId,
avatarAddress: `0x${string}`
) => `${chainId}:${avatarAddress}:${transactions.map((tx) => tx.id).join(',')}`

const transactionsEqual = (a: MetaTransactionData, b: MetaTransactionData) =>
a.to.toLowerCase() === b.to.toLowerCase() &&
(a.value || '0' === b.value || '0') &&
a.data === b.data &&
(a.operation || 0 === b.operation || 0)
Loading

0 comments on commit 782fed9

Please sign in to comment.