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(api-kit): 4337 API functions #777

Merged
merged 25 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
10e6907
Implement functions to call 4337 api endpoints
tmjssz Apr 19, 2024
aed236b
e2e tests for addSafeOperation function
tmjssz Apr 19, 2024
f4ad53d
e2e tests for getSafeOperation function
tmjssz Apr 19, 2024
9c49f60
e2e tests for getSafeOperationsByAddress function
tmjssz Apr 19, 2024
5153baa
Endpoint tests for 4337 api functions
tmjssz Apr 19, 2024
ba4937e
set staging backend for SafeOperations tests
dasanra Apr 26, 2024
a9e0a66
remove unnecessary property from test setup
dasanra Apr 26, 2024
4640747
Use string to avoid precision errors in gas parameters
dasanra Apr 26, 2024
f0846b4
fix endpoint test
dasanra Apr 26, 2024
295b68b
Move shared types to `safe-core-sdk-types`
tmjssz Apr 29, 2024
2097ef0
Mock bundler client calls for addSafeOperation endpoint tests
tmjssz May 3, 2024
236d0fc
Use mock data in realistic format for unit test
tmjssz May 8, 2024
483d90b
Small fix to remove `eslint-disable-next-line` line
tmjssz May 8, 2024
83f494e
Assert length of SafeOperations in the getSafeOperation e2e test
tmjssz May 8, 2024
b02b85e
Rename `isEmptyHexData` util function to `isEmptyData`
tmjssz May 8, 2024
46fd0c4
Extend getSafeOperationsByAddress parameters
tmjssz May 8, 2024
56315ef
Refactor `addSafeOperation` function params to remove internal coupli…
tmjssz May 14, 2024
05054d3
Update packages/api-kit/.env.example
tmjssz May 14, 2024
cd124d8
Adapt SafeOperation response object types to latest API changes
tmjssz May 15, 2024
18d0c84
Merge branch 'development' of https://github.com/safe-global/safe-cor…
yagopv May 16, 2024
9a4ad4f
Update new tests to use the new getKits utils
yagopv May 16, 2024
29b6cd1
Fix import
yagopv May 16, 2024
8099ed4
Rename master-copies
yagopv May 16, 2024
6ac7ea9
Rename `SafeOperation` class to `EthSafeOperation`
tmjssz May 16, 2024
26142c1
chore: add deprecations re-exporting deprecated types
dasanra May 21, 2024
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
129 changes: 128 additions & 1 deletion packages/api-kit/src/SafeApiKit.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import {
AddMessageProps,
AddSafeDelegateProps,
AddSafeOperationProps,
AllTransactionsListResponse,
AllTransactionsOptions,
DeleteSafeDelegateProps,
GetSafeDelegateProps,
GetSafeOperationListProps,
GetSafeOperationListResponse,
SafeSingletonResponse,
GetSafeMessageListProps,
ModulesResponse,
Expand All @@ -20,6 +23,7 @@
SafeMultisigTransactionEstimate,
SafeMultisigTransactionEstimateResponse,
SafeMultisigTransactionListResponse,
SafeOperationResponse,
SafeServiceInfoResponse,
SignatureResponse,
TokenInfoListResponse,
Expand All @@ -35,6 +39,7 @@
SafeMultisigTransactionResponse
} from '@safe-global/safe-core-sdk-types'
import { TRANSACTION_SERVICE_URLS } from './utils/config'
import { isEmptyData } from './utils'

export interface SafeApiKitConfig {
/** chainId - The chainId */
Expand Down Expand Up @@ -96,7 +101,7 @@
*/
async getServiceSingletonsInfo(): Promise<SafeSingletonResponse[]> {
return sendRequest({
url: `${this.#txServiceBaseUrl}/v1/about/master-copies`,
url: `${this.#txServiceBaseUrl}/v1/about/singletons`,
method: HttpMethod.Get
})
}
Expand All @@ -110,7 +115,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 118 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 @@ -320,7 +325,7 @@
const { address: delegator } = this.#getEip3770Address(delegatorAddress)
const signature = await signDelegate(signer, delegate, this.#chainId)

const body: any = {

Check warning on line 328 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 @@ -714,6 +719,128 @@
}
})
}

/**
* Get the SafeOperations that were sent from a particular address.
* @param getSafeOperationsProps - The parameters to filter the list of SafeOperations
* @throws "Safe address must not be empty"
* @throws "Invalid Ethereum address {safeAddress}"
* @returns The SafeOperations sent from the given Safe's address
*/
async getSafeOperationsByAddress({
safeAddress,
ordering,
limit,
offset
}: GetSafeOperationListProps): Promise<GetSafeOperationListResponse> {
if (!safeAddress) {
throw new Error('Safe address must not be empty')
}

const { address } = this.#getEip3770Address(safeAddress)

const url = new URL(`${this.#txServiceBaseUrl}/v1/safes/${address}/safe-operations/`)

if (ordering) {
url.searchParams.set('ordering', ordering)
}

if (limit) {
url.searchParams.set('limit', limit)
}

if (offset) {
url.searchParams.set('offset', offset)
}

return sendRequest({
url: url.toString(),
method: HttpMethod.Get
})
}

/**
* Get a SafeOperation by its hash.
* @param safeOperationHash The SafeOperation hash
* @throws "SafeOperation hash must not be empty"
* @throws "Not found."
* @returns The SafeOperation
*/
async getSafeOperation(safeOperationHash: string): Promise<SafeOperationResponse> {
if (!safeOperationHash) {
throw new Error('SafeOperation hash must not be empty')
}

return sendRequest({
url: `${this.#txServiceBaseUrl}/v1/safe-operations/${safeOperationHash}/`,
method: HttpMethod.Get
})
}

/**
* Create a new 4337 SafeOperation for a Safe.
* @param addSafeOperationProps - The configuration of the SafeOperation
* @throws "Safe address must not be empty"
* @throws "Invalid Safe address {safeAddress}"
* @throws "Module address must not be empty"
* @throws "Invalid module address {moduleAddress}"
* @throws "Signature must not be empty"
*/
async addSafeOperation({
entryPoint,
moduleAddress: moduleAddressProp,
options,
safeAddress: safeAddressProp,
userOperation
}: AddSafeOperationProps): Promise<void> {
let safeAddress: string, moduleAddress: string

if (!safeAddressProp) {
throw new Error('Safe address must not be empty')
}
try {
safeAddress = this.#getEip3770Address(safeAddressProp).address
} catch (err) {
throw new Error(`Invalid Safe address ${safeAddressProp}`)
}

if (!moduleAddressProp) {
throw new Error('Module address must not be empty')
}

try {
moduleAddress = this.#getEip3770Address(moduleAddressProp).address
} catch (err) {
throw new Error(`Invalid module address ${moduleAddressProp}`)
}

if (isEmptyData(userOperation.signature)) {
throw new Error('Signature must not be empty')
}

return sendRequest({
url: `${this.#txServiceBaseUrl}/v1/safes/${safeAddress}/safe-operations/`,
method: HttpMethod.Post,
body: {
nonce: Number(userOperation.nonce),
initCode: isEmptyData(userOperation.initCode) ? null : userOperation.initCode,
callData: userOperation.callData,
callDataGasLimit: userOperation.callGasLimit.toString(),
verificationGasLimit: userOperation.verificationGasLimit.toString(),
preVerificationGas: userOperation.preVerificationGas.toString(),
maxFeePerGas: userOperation.maxFeePerGas.toString(),
maxPriorityFeePerGas: userOperation.maxPriorityFeePerGas.toString(),
paymasterAndData: isEmptyData(userOperation.paymasterAndData)
? null
: userOperation.paymasterAndData,
entryPoint,
validAfter: !options?.validAfter ? null : options?.validAfter,
validUntil: !options?.validUntil ? null : options?.validUntil,
signature: userOperation.signature,
moduleAddress
}
})
}
}

export default SafeApiKit
77 changes: 76 additions & 1 deletion packages/api-kit/src/types/safeTransactionServiceTypes.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Signer, TypedDataDomain, TypedDataField } from 'ethers'
import {
SafeMultisigTransactionResponse,
SafeTransactionData
SafeTransactionData,
UserOperation
} from '@safe-global/safe-core-sdk-types'

export type SafeServiceInfoResponse = {
Expand Down Expand Up @@ -287,3 +288,77 @@ export type EIP712TypedData = {
types: TypedDataField
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 callDataGasLimit: 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
/** Which field to use when ordering the results */
ordering?: string
/** Maximum number of results to return per page */
limit?: string
/** Initial index from which to return the results */
offset?: string
}

export type GetSafeOperationListResponse = {
readonly count: number
readonly next?: string
readonly previous?: string
readonly results: Array<SafeOperationResponse>
}

export type AddSafeOperationProps = {
/** Address of the EntryPoint contract */
entryPoint: string
/** Address of the Safe4337Module contract */
moduleAddress: string
/** Address of the Safe to add a SafeOperation for */
safeAddress: string
/** UserOperation object to add */
userOperation: UserOperation
/** Options object */
options?: {
/** The UserOperation will remain valid until this block's timestamp */
validUntil?: number
/** The UserOperation will be valid after this block's timestamp */
validAfter?: number
}
}
1 change: 1 addition & 0 deletions packages/api-kit/src/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const EMPTY_DATA = '0x'
3 changes: 3 additions & 0 deletions packages/api-kit/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { EMPTY_DATA } from './constants'

export const isEmptyData = (input: string) => !input || input === EMPTY_DATA
Loading
Loading