Skip to content

Commit

Permalink
feat: Safe kit testing (#911)
Browse files Browse the repository at this point in the history
  • Loading branch information
yagopv authored Jul 20, 2024
1 parent 0f22ff1 commit fbc4957
Show file tree
Hide file tree
Showing 21 changed files with 1,046 additions and 182 deletions.
6 changes: 5 additions & 1 deletion packages/protocol-kit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import {
validateEthereumAddress,
validateEip3770Address
} from './utils'
import EthSafeTransaction from './utils/transactions/SafeTransaction'
import EthSafeMessage from './utils/messages/SafeMessage'
import { SafeTransactionOptionalProps } from './utils/transactions/types'
import { encodeMultiSendData, standardizeSafeTransactionData } from './utils/transactions/utils'
import {
Expand Down Expand Up @@ -105,7 +107,9 @@ export {
getEip712MessageTypes,
hashSafeMessage,
generateTypedData,
SafeProvider
SafeProvider,
EthSafeTransaction,
EthSafeMessage
}

export * from './types'
Expand Down
2 changes: 2 additions & 0 deletions packages/relay-kit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export * from './packs/gelato/GelatoRelayPack'
export * from './packs/gelato/types'

export * from './packs/safe-4337/Safe4337Pack'
export { default as EthSafeOperation } from './packs/safe-4337/SafeOperation'

export * from './packs/safe-4337/estimators'
export * from './packs/safe-4337/types'

Expand Down
256 changes: 245 additions & 11 deletions packages/safe-kit/src/SafeClient.test.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,263 @@
import { SafeClient } from './SafeClient' // Adjust the import path based on your directory structure
import Safe from '@safe-global/protocol-kit'
import Safe, * as protocolKitModule from '@safe-global/protocol-kit'
import SafeApiKit from '@safe-global/api-kit'

// Mock dependencies
import * as utils from './utils'
import { SafeClient } from './SafeClient'
import { MESSAGES, SafeClientTxStatus } from './constants'

jest.mock('@safe-global/protocol-kit')
jest.mock('@safe-global/api-kit')
jest.mock('@safe-global/safe-kit/utils', () => ({
createSafeClientResult: jest.fn(),
sendTransaction: jest.fn(),
proposeTransaction: jest.fn(),
waitSafeTxReceipt: jest.fn()
}))
jest.mock('./utils', () => {
return {
...jest.requireActual('./utils'),
sendTransaction: jest.fn().mockResolvedValue('0xSafeDeploymentEthereumHash'),
proposeTransaction: jest.fn().mockResolvedValue('0xSafeTxHash'),
waitSafeTxReceipt: jest.fn()
}
})

const TRANSACTION = { to: '0xEthereumAddres', value: '0', data: '0x' }
const DEPLOYMENT_TRANSACTION = { to: '0xMultisig', value: '0', data: '0x' }
const TRANSACTION_BATCH = [TRANSACTION]
const SAFE_ADDRESS = '0xSafeAddress'
const SAFE_TX_HASH = '0xSafeTxHash'
const DEPLOYMENT_ETHEREUM_TX_HASH = '0xSafeDeploymentEthereumHash'
const ETHEREUM_TX_HASH = '0xEthereumTxHash'
const SAFE_TRANSACTION = new protocolKitModule.EthSafeTransaction({
...TRANSACTION,
operation: 0,
safeTxGas: '0',
baseGas: '0',
gasPrice: '0',
gasToken: '0x',
refundReceiver: '0x',
nonce: 0
})
const SAFE_PROVIDER = {
provider: 'http://ethereum.provider',
signer: '0xSignerAddress'
}
const PENDING_TRANSACTIONS = [{ safeTxHash: '0xPendingSafeTxHash' }]

describe('SafeClient', () => {
let safeClient: SafeClient
let protocolKit: jest.Mocked<Safe>
let protocolKit: Safe
let apiKit: jest.Mocked<SafeApiKit>

beforeEach(() => {
protocolKit = new Safe() as jest.Mocked<Safe>
protocolKit = new Safe()
apiKit = new SafeApiKit({ chainId: 1n }) as jest.Mocked<SafeApiKit>

safeClient = new SafeClient(protocolKit, apiKit)

protocolKit.getAddress = jest.fn().mockResolvedValue(SAFE_ADDRESS)
protocolKit.createTransaction = jest.fn().mockResolvedValue(SAFE_TRANSACTION)
protocolKit.signTransaction = jest.fn().mockResolvedValue(SAFE_TRANSACTION)
protocolKit.executeTransaction = jest.fn().mockResolvedValue({ hash: ETHEREUM_TX_HASH })
protocolKit.connect = jest.fn().mockResolvedValue(protocolKit)
protocolKit.getSafeProvider = jest.fn().mockResolvedValue(SAFE_PROVIDER)
protocolKit.createSafeDeploymentTransaction = jest
.fn()
.mockResolvedValue(DEPLOYMENT_TRANSACTION)
protocolKit.wrapSafeTransactionIntoDeploymentBatch = jest
.fn()
.mockResolvedValue(DEPLOYMENT_TRANSACTION)
})

afterEach(() => {
jest.clearAllMocks()
})

it('should allow to instantiate a SafeClient', () => {
expect(safeClient).toBeInstanceOf(SafeClient)
expect(safeClient.protocolKit).toBe(protocolKit)
expect(safeClient.apiKit).toBe(apiKit)
})

describe('send', () => {
it('should propose the transaction if Safe account exists and has threshold > 1', async () => {
protocolKit.isSafeDeployed = jest.fn().mockResolvedValue(true)
protocolKit.getThreshold = jest.fn().mockResolvedValue(2)

const result = await safeClient.send({ transactions: TRANSACTION_BATCH })

expect(protocolKit.createTransaction).toHaveBeenCalledWith({
transactions: TRANSACTION_BATCH
})

expect(utils.proposeTransaction).toHaveBeenCalledWith({
safeTransaction: SAFE_TRANSACTION,
protocolKit,
apiKit
})
expect(result).toMatchObject({
description: MESSAGES[SafeClientTxStatus.PENDING_SIGNATURES],
safeAddress: SAFE_ADDRESS,
status: SafeClientTxStatus.PENDING_SIGNATURES,
transactions: {
ethereumTxHash: undefined,
safeTxHash: SAFE_TX_HASH
}
})
})

it('should execute the transaction if Safe account exists and has threshold === 1', async () => {
protocolKit.isSafeDeployed = jest.fn().mockResolvedValue(true)
protocolKit.getThreshold = jest.fn().mockResolvedValue(1)

const result = await safeClient.send({ transactions: TRANSACTION_BATCH })

expect(protocolKit.createTransaction).toHaveBeenCalledWith({
transactions: TRANSACTION_BATCH
})

expect(protocolKit.signTransaction).toHaveBeenCalledWith(SAFE_TRANSACTION)
expect(protocolKit.executeTransaction).toHaveBeenCalledWith(SAFE_TRANSACTION, {})

expect(result).toMatchObject({
description: MESSAGES[SafeClientTxStatus.EXECUTED],
safeAddress: SAFE_ADDRESS,
status: SafeClientTxStatus.EXECUTED,
transactions: {
ethereumTxHash: ETHEREUM_TX_HASH,
safeTxHash: undefined
}
})
})

it('should deploy and propose the transaction if Safe account does not exist and has threshold > 1', async () => {
protocolKit.isSafeDeployed = jest.fn().mockResolvedValue(false)
protocolKit.getThreshold = jest.fn().mockResolvedValue(2)

const result = await safeClient.send({ transactions: TRANSACTION_BATCH })

expect(protocolKit.createSafeDeploymentTransaction).toHaveBeenCalledWith(undefined, {})
expect(utils.sendTransaction).toHaveBeenCalledWith({
transaction: DEPLOYMENT_TRANSACTION,
protocolKit
})
expect(protocolKit.connect).toHaveBeenCalled()
expect(protocolKit.signTransaction).toHaveBeenCalledWith(SAFE_TRANSACTION)
expect(utils.proposeTransaction).toHaveBeenCalledWith({
safeTransaction: SAFE_TRANSACTION,
protocolKit,
apiKit
})
expect(result).toMatchObject({
description: MESSAGES[SafeClientTxStatus.DEPLOYED_AND_PENDING_SIGNATURES],
safeAddress: SAFE_ADDRESS,
status: SafeClientTxStatus.DEPLOYED_AND_PENDING_SIGNATURES,
transactions: {
ethereumTxHash: undefined,
safeTxHash: SAFE_TX_HASH
},
safeAccountDeployment: {
ethereumTxHash: DEPLOYMENT_ETHEREUM_TX_HASH
}
})
})

it('should deploy and execute the transaction if Safe account does not exist and has threshold === 1', async () => {
protocolKit.isSafeDeployed = jest.fn().mockResolvedValue(false)
protocolKit.getThreshold = jest.fn().mockResolvedValue(1)

const result = await safeClient.send({ transactions: TRANSACTION_BATCH })

expect(protocolKit.signTransaction).toHaveBeenCalledWith(SAFE_TRANSACTION)
expect(protocolKit.wrapSafeTransactionIntoDeploymentBatch).toHaveBeenCalledWith(
SAFE_TRANSACTION,
{}
)
expect(protocolKit.connect).toHaveBeenCalled()
expect(result).toMatchObject({
description: MESSAGES[SafeClientTxStatus.DEPLOYED_AND_EXECUTED],
safeAddress: SAFE_ADDRESS,
status: SafeClientTxStatus.DEPLOYED_AND_EXECUTED,
transactions: {
ethereumTxHash: DEPLOYMENT_ETHEREUM_TX_HASH,
safeTxHash: undefined
},
safeAccountDeployment: {
ethereumTxHash: DEPLOYMENT_ETHEREUM_TX_HASH
}
})
})
})

describe('confirm', () => {
it('should confirm the transaction when enough signatures', async () => {
const TRANSACTION_RESPONSE = {
confirmations: [{ signature: '0x1' }, { signature: '0x2' }],
confirmationsRequired: 2
}

apiKit.getTransaction = jest.fn().mockResolvedValue(TRANSACTION_RESPONSE)

const result = await safeClient.confirm({ safeTxHash: SAFE_TX_HASH })

expect(apiKit.getTransaction).toHaveBeenCalledWith(SAFE_TX_HASH)
expect(protocolKit.signTransaction).toHaveBeenCalledWith(TRANSACTION_RESPONSE)
expect(apiKit.confirmTransaction).toHaveBeenCalledWith(SAFE_TX_HASH, undefined)
expect(protocolKit.executeTransaction).toHaveBeenCalledWith(TRANSACTION_RESPONSE)
expect(result).toMatchObject({
description: MESSAGES[SafeClientTxStatus.EXECUTED],
safeAddress: SAFE_ADDRESS,
status: SafeClientTxStatus.EXECUTED,
transactions: {
ethereumTxHash: ETHEREUM_TX_HASH,
safeTxHash: SAFE_TX_HASH
}
})
})

it('should indicate more signatures are required when threshold is not matched', async () => {
const TRANSACTION_RESPONSE = {
confirmations: [{ signature: '0x1' }],
confirmationsRequired: 2
}

apiKit.getTransaction = jest.fn().mockResolvedValue(TRANSACTION_RESPONSE)

const result = await safeClient.confirm({ safeTxHash: SAFE_TX_HASH })

expect(apiKit.getTransaction).toHaveBeenCalledWith(SAFE_TX_HASH)
expect(protocolKit.signTransaction).toHaveBeenCalledWith(TRANSACTION_RESPONSE)
expect(apiKit.confirmTransaction).toHaveBeenCalledWith(SAFE_TX_HASH, undefined)

expect(result).toMatchObject({
description: MESSAGES[SafeClientTxStatus.PENDING_SIGNATURES],
safeAddress: SAFE_ADDRESS,
status: SafeClientTxStatus.PENDING_SIGNATURES,
transactions: {
ethereumTxHash: undefined,
safeTxHash: SAFE_TX_HASH
}
})
})
})

describe('getPendingTransactions', () => {
it('should return the pending transactions for the Safe address', async () => {
apiKit.getPendingTransactions = jest.fn().mockResolvedValue(PENDING_TRANSACTIONS)

const result = await safeClient.getPendingTransactions()

expect(protocolKit.getAddress).toHaveBeenCalled()
expect(apiKit.getPendingTransactions).toHaveBeenCalledWith(SAFE_ADDRESS)
expect(result).toBe(PENDING_TRANSACTIONS)
})
})

describe('extend', () => {
it('should enable the extension of the SafeClient with additional functionality', async () => {
const extendedClient = safeClient.extend(() => ({
extendedFunction: () => 'extendedFunction',
extendedProp: 'extendedProp'
}))

expect(extendedClient).toBeInstanceOf(SafeClient)
expect(extendedClient.extendedFunction()).toEqual('extendedFunction')
expect(extendedClient.extendedProp).toEqual('extendedProp')
})
})
})
Loading

0 comments on commit fbc4957

Please sign in to comment.