Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(relay-kit): Add tx service compatibility to Safe4337Pack #836

Merged
merged 19 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions packages/api-kit/src/SafeApiKit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
SafeMultisigTransactionEstimate,
SafeMultisigTransactionEstimateResponse,
SafeMultisigTransactionListResponse,
SafeOperationResponse,
SafeServiceInfoResponse,
SignatureResponse,
TokenInfoListResponse,
Expand All @@ -36,10 +35,14 @@
import {
Eip3770Address,
SafeMultisigConfirmationListResponse,
SafeMultisigTransactionResponse
SafeMultisigTransactionResponse,
SafeOperationResponse,
SafeOperation,
isSafeOperation
} from '@safe-global/safe-core-sdk-types'
import { TRANSACTION_SERVICE_URLS } from './utils/config'
import { isEmptyData } from './utils'
import { getAddSafeOperationProps } from './utils/safeOperation'

export interface SafeApiKitConfig {
/** chainId - The chainId */
Expand Down Expand Up @@ -115,7 +118,7 @@
* @throws "Not Found"
* @throws "Ensure this field has at least 1 hexadecimal chars (not counting 0x)."
*/
async decodeData(data: string): Promise<any> {

Check warning on line 121 in packages/api-kit/src/SafeApiKit.ts

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type
if (data === '') {
throw new Error('Invalid data')
}
Expand Down Expand Up @@ -325,7 +328,7 @@
const { address: delegator } = this.#getEip3770Address(delegatorAddress)
const signature = await signDelegate(signer, delegate, this.#chainId)

const body: any = {

Check warning on line 331 in packages/api-kit/src/SafeApiKit.ts

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type
safe: safeAddress ? this.#getEip3770Address(safeAddress).address : null,
delegate,
delegator,
Expand Down Expand Up @@ -786,15 +789,23 @@
* @throws "Invalid module address {moduleAddress}"
* @throws "Signature must not be empty"
*/
async addSafeOperation({
entryPoint,
moduleAddress: moduleAddressProp,
options,
safeAddress: safeAddressProp,
userOperation
}: AddSafeOperationProps): Promise<void> {
async addSafeOperation(safeOperation: AddSafeOperationProps | SafeOperation): Promise<void> {
let safeAddress: string, moduleAddress: string
let addSafeOperationProps: AddSafeOperationProps

if (isSafeOperation(safeOperation)) {
addSafeOperationProps = await getAddSafeOperationProps(safeOperation)
} else {
addSafeOperationProps = safeOperation
}

const {
entryPoint,
moduleAddress: moduleAddressProp,
options,
safeAddress: safeAddressProp,
userOperation
} = addSafeOperationProps
if (!safeAddressProp) {
throw new Error('Safe address must not be empty')
}
Expand Down
41 changes: 2 additions & 39 deletions packages/api-kit/src/types/safeTransactionServiceTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { Signer, TypedDataDomain, TypedDataField } from 'ethers'
import {
SafeMultisigTransactionResponse,
SafeTransactionData,
UserOperation
UserOperation,
SafeOperationResponse
} from '@safe-global/safe-core-sdk-types'

export type SafeServiceInfoResponse = {
Expand Down Expand Up @@ -289,44 +290,6 @@ export type EIP712TypedData = {
message: Record<string, unknown>
}

export type SafeOperationConfirmation = {
readonly created: string
readonly modified: string
readonly owner: string
readonly signature: string
readonly signatureType: string
}

export type UserOperationResponse = {
readonly ethereumTxHash: string
readonly sender: string
readonly userOperationHash: string
readonly nonce: number
readonly initCode: null | string
readonly callData: null | string
readonly callGasLimit: number
readonly verificationGasLimit: number
readonly preVerificationGas: number
readonly maxFeePerGas: number
readonly maxPriorityFeePerGas: number
readonly paymaster: null | string
readonly paymasterData: null | string
readonly signature: string
readonly entryPoint: string
}

export type SafeOperationResponse = {
readonly created: string
readonly modified: string
readonly safeOperationHash: string
readonly validAfter: string
readonly validUntil: string
readonly moduleAddress: string
readonly confirmations?: Array<SafeOperationConfirmation>
readonly preparedSignature?: string
readonly userOperation?: UserOperationResponse
}

export type GetSafeOperationListProps = {
/** Address of the Safe to get SafeOperations for */
safeAddress: string
Expand Down
17 changes: 17 additions & 0 deletions packages/api-kit/src/utils/safeOperation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { SafeOperation } from '@safe-global/safe-core-sdk-types'

export const getAddSafeOperationProps = async (safeOperation: SafeOperation) => {
const userOperation = safeOperation.toUserOperation()
userOperation.signature = safeOperation.encodedSignatures() // Without validity dates

return {
entryPoint: safeOperation.data.entryPoint,
moduleAddress: safeOperation.moduleAddress,
safeAddress: safeOperation.data.safe,
userOperation,
options: {
validAfter: safeOperation.data.validAfter,
validUntil: safeOperation.data.validUntil
}
}
}
47 changes: 20 additions & 27 deletions packages/api-kit/tests/e2e/addSafeOperation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ import chaiAsPromised from 'chai-as-promised'
import { ethers } from 'ethers'
import sinon from 'sinon'
import sinonChai from 'sinon-chai'
import { SafeOperation } from '@safe-global/safe-core-sdk-types'
import Safe from '@safe-global/protocol-kit'
import SafeApiKit from '@safe-global/api-kit'
import { Safe4337Pack } from '@safe-global/relay-kit'
import { generateTransferCallData } from '@safe-global/relay-kit/src/packs/safe-4337/testing-utils/helpers'
import { RPC_4337_CALLS } from '@safe-global/relay-kit/packs/safe-4337/constants'
import { getSafe4337ModuleDeployment } from '@safe-global/safe-modules-deployments'
import config from '../utils/config'
import { getKits } from '../utils/setupKits'
import { getAddSafeOperationProps } from '@safe-global/api-kit/utils/safeOperation'

chai.use(chaiAsPromised)
chai.use(sinonChai)
Expand All @@ -26,7 +25,6 @@ const TX_SERVICE_URL = 'https://safe-transaction-sepolia.staging.5afe.dev/api'
let safeApiKit: SafeApiKit
let protocolKit: Safe
let safe4337Pack: Safe4337Pack
let moduleAddress: string

describe('addSafeOperation', () => {
const transferUSDC = {
Expand Down Expand Up @@ -76,31 +74,8 @@ describe('addSafeOperation', () => {
paymasterAddress: PAYMASTER_ADDRESS
}
})

const chainId = (await protocolKit.getSafeProvider().getChainId()).toString()

moduleAddress = getSafe4337ModuleDeployment({
released: true,
version: '0.2.0',
network: chainId
})?.networkAddresses[chainId] as string
})

const getAddSafeOperationProps = async (safeOperation: SafeOperation) => {
const userOperation = safeOperation.toUserOperation()
userOperation.signature = safeOperation.encodedSignatures()
return {
entryPoint: safeOperation.data.entryPoint,
moduleAddress,
safeAddress: SAFE_ADDRESS,
userOperation,
options: {
validAfter: safeOperation.data.validAfter,
validUntil: safeOperation.data.validUntil
}
}
}

describe('should fail', () => {
it('if safeAddress is empty', async () => {
const safeOperation = await safe4337Pack.createTransaction({ transactions: [transferUSDC] })
Expand Down Expand Up @@ -172,7 +147,7 @@ describe('addSafeOperation', () => {
})
})

it('should add a new SafeOperation', async () => {
it('should add a new SafeOperation using an standard UserOperation and props', async () => {
const safeOperation = await safe4337Pack.createTransaction({ transactions: [transferUSDC] })
const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation)
const addSafeOperationProps = await getAddSafeOperationProps(signedSafeOperation)
Expand All @@ -190,4 +165,22 @@ describe('addSafeOperation', () => {
})
chai.expect(safeOperationsAfter.count).to.equal(initialNumSafeOperations + 1)
})

it('should add a new SafeOperation using a SafeOperation object from the relay-kit', async () => {
const safeOperation = await safe4337Pack.createTransaction({ transactions: [transferUSDC] })
const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation)

// Get the number of SafeOperations before adding a new one
const safeOperationsBefore = await safeApiKit.getSafeOperationsByAddress({
safeAddress: SAFE_ADDRESS
})
const initialNumSafeOperations = safeOperationsBefore.count

await chai.expect(safeApiKit.addSafeOperation(signedSafeOperation)).to.be.fulfilled

const safeOperationsAfter = await safeApiKit.getSafeOperationsByAddress({
safeAddress: SAFE_ADDRESS
})
chai.expect(safeOperationsAfter.count).to.equal(initialNumSafeOperations + 1)
})
})
59 changes: 58 additions & 1 deletion packages/relay-kit/src/packs/safe-4337/Safe4337Pack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ describe('Safe4337Pack', () => {
})
})

it('should all to sign a SafeOperation', async () => {
it('should allow to sign a SafeOperation', async () => {
const transferUSDC = {
to: fixtures.PAYMASTER_TOKEN_ADDRESS,
data: generateTransferCallData(fixtures.SAFE_ADDRESS_v1_4_1, 100_000n),
Expand Down Expand Up @@ -527,6 +527,34 @@ describe('Safe4337Pack', () => {
})
})

it('should allow to sign a SafeOperation using a SafeOperationResponse object from the api to add a signature', async () => {
const safe4337Pack = await createSafe4337Pack({
options: {
safeAddress: fixtures.SAFE_ADDRESS_v1_4_1
}
})

expect(await safe4337Pack.signSafeOperation(fixtures.SAFE_OPERATION_RESPONSE)).toMatchObject({
signatures: new Map()
.set(
fixtures.OWNER_1.toLowerCase(),
new protocolKit.EthSafeSignature(
fixtures.OWNER_1,
'0xcb28e74375889e400a4d8aca46b8c59e1cf8825e373c26fa99c2fd7c078080e64fe30eaf1125257bdfe0b358b5caef68aa0420478145f52decc8e74c979d43ab1c',
false
)
)
.set(
fixtures.OWNER_2.toLowerCase(),
new protocolKit.EthSafeSignature(
fixtures.OWNER_2,
'0xcb28e74375889e400a4d8aca46b8c59e1cf8825e373c26fa99c2fd7c078080e64fe30eaf1125257bdfe0b358b5caef68aa0420478145f52decc8e74c979d43ab1d',
false
)
)
})
})

it('should allow to send an UserOperation to a bundler', async () => {
const transferUSDC = {
to: fixtures.PAYMASTER_TOKEN_ADDRESS,
Expand Down Expand Up @@ -554,6 +582,35 @@ describe('Safe4337Pack', () => {
])
})

it('should allow to send a UserOperation to the bundler using a SafeOperationResponse object from the api', async () => {
const safe4337Pack = await createSafe4337Pack({
options: {
safeAddress: fixtures.SAFE_ADDRESS_v1_4_1
}
})

await safe4337Pack.executeTransaction({ executable: fixtures.SAFE_OPERATION_RESPONSE })

expect(sendMock).toHaveBeenCalledWith(constants.RPC_4337_CALLS.SEND_USER_OPERATION, [
utils.userOperationToHexValues({
sender: '0xE322e721bCe76cE7FCf3A475f139A9314571ad3D',
nonce: '3',
initCode: '0x',
callData:
'0x7bb37428000000000000000000000000e322e721bce76ce7fcf3a475f139a9314571ad3d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
callGasLimit: 122497n,
verificationGasLimit: 123498n,
preVerificationGas: 50705n,
maxFeePerGas: 105183831060n,
maxPriorityFeePerGas: 1380000000n,
paymasterAndData: '0x',
signature:
'0x000000000000000000000000cb28e74375889e400a4d8aca46b8c59e1cf8825e373c26fa99c2fd7c078080e64fe30eaf1125257bdfe0b358b5caef68aa0420478145f52decc8e74c979d43ab1d'
}),
fixtures.ENTRYPOINTS[0]
])
})

it('should return a UserOperation based on a userOpHash', async () => {
const safe4337Pack = await createSafe4337Pack({
options: {
Expand Down
Loading
Loading