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: Get pending SafeOperations support #1123

Merged
merged 9 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
26 changes: 26 additions & 0 deletions packages/api-kit/src/SafeApiKit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@
* @throws "Not Found"
* @throws "Ensure this field has at least 1 hexadecimal chars (not counting 0x)."
*/
async decodeData(data: string, to?: string): Promise<any> {

Check warning on line 125 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 @@ -258,7 +258,7 @@
return sendRequest({
url: `${this.#txServiceBaseUrl}/v1/safes/${address}/`,
method: HttpMethod.Get
}).then((response: any) => {

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

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type
// FIXME remove when the transaction service returns the singleton property instead of masterCopy
if (!response?.singleton) {
const { masterCopy, ...rest } = response
Expand Down Expand Up @@ -347,7 +347,7 @@
const { address: delegator } = this.#getEip3770Address(delegatorAddress)
const signature = await signDelegate(signer, delegate, this.#chainId)

const body: any = {

Check warning on line 350 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 @@ -415,7 +415,7 @@
return sendRequest({
url: `${this.#txServiceBaseUrl}/v1/safes/${address}/creation/`,
method: HttpMethod.Get
}).then((response: any) => {

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

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type
// FIXME remove when the transaction service returns the singleton property instead of masterCopy
if (!response?.singleton) {
const { masterCopy, ...rest } = response
Expand Down Expand Up @@ -817,6 +817,8 @@
*/
async getSafeOperationsByAddress({
safeAddress,
executed,
hasConfirmations,
ordering,
limit,
offset
Expand All @@ -841,12 +843,36 @@
url.searchParams.set('offset', offset.toString())
}

if (hasConfirmations != null) {
url.searchParams.set('has_confirmations', hasConfirmations.toString())
}

if (executed != null) {
url.searchParams.set('executed', executed.toString())
}

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

/**
* Get the SafeOperations that are pending to send to the bundler
* @param getSafeOperationsProps - The parameters to filter the list of SafeOperations
* @throws "Safe address must not be empty"
* @throws "Invalid Ethereum address {safeAddress}"
* @returns The pending SafeOperations
*/
async getPendingSafeOperations(
props: Omit<GetSafeOperationListProps, 'executed'>
): Promise<GetSafeOperationListResponse> {
return this.getSafeOperationsByAddress({
...props,
executed: false
})
}

/**
* Get a SafeOperation by its hash.
* @param safeOperationHash The SafeOperation hash
Expand Down
2 changes: 2 additions & 0 deletions packages/api-kit/src/types/safeTransactionServiceTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ export type GetSafeOperationListProps = {
safeAddress: string
/** Which field to use when ordering the results. It can be: `user_operation__nonce`, `created` (default: `-user_operation__nonce`) */
ordering?: string
executed?: boolean
hasConfirmations?: boolean
} & ListOptions

export type GetSafeOperationListResponse = ListResponse<SafeOperationResponse>
Expand Down
50 changes: 50 additions & 0 deletions packages/api-kit/tests/e2e/getPendingSafeOperations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import SafeApiKit from '@safe-global/api-kit/index'
import chai from 'chai'
import chaiAsPromised from 'chai-as-promised'
import { getApiKit } from '../utils/setupKits'

chai.use(chaiAsPromised)

const SAFE_ADDRESS = '0x60C4Ab82D06Fd7dFE9517e17736C2Dcc77443EF0' // v1.4.1
const TX_SERVICE_URL = 'https://safe-transaction-sepolia.staging.5afe.dev/api'

let safeApiKit: SafeApiKit

describe('getPendingSafeOperations', () => {
before(async () => {
safeApiKit = getApiKit(TX_SERVICE_URL)
})

describe('should fail', () => {
it('should fail if safeAddress is empty', async () => {
await chai
.expect(safeApiKit.getPendingSafeOperations({ safeAddress: '' }))
.to.be.rejectedWith('Safe address must not be empty')
})

it('should fail if safeAddress is invalid', async () => {
await chai
.expect(safeApiKit.getPendingSafeOperations({ safeAddress: '0x123' }))
.to.be.rejectedWith('Invalid Ethereum address 0x123')
})
})

it('should get pending safe operations', async () => {
const allSafeOperations = await safeApiKit.getSafeOperationsByAddress({
safeAddress: SAFE_ADDRESS
})

// Prepared 2 executed SafeOperations in the E2E Safe account
const pendingSafeOperations = await safeApiKit.getPendingSafeOperations({
safeAddress: SAFE_ADDRESS
})

const executedSafeOperations = await safeApiKit.getSafeOperationsByAddress({
safeAddress: SAFE_ADDRESS,
executed: true
})

chai.expect(executedSafeOperations.count).equals(2)
chai.expect(allSafeOperations.count - pendingSafeOperations.count).equals(2)
})
})
31 changes: 31 additions & 0 deletions packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,35 @@ describe('getSafeOperationsByAddress', () => {
chai.expect(response).to.have.property('results').to.be.an('array')
chai.expect(response.results[0]).to.be.deep.equal(safeOperations[1])
})

it('should get pending safe operations', async () => {
const allSafeOperations = await safeApiKit.getSafeOperationsByAddress({
safeAddress: SAFE_ADDRESS
})

// Prepared 2 executed SafeOperations in the E2E Safe account
const pendingSafeOperations = await safeApiKit.getSafeOperationsByAddress({
safeAddress: SAFE_ADDRESS,
executed: false
})

const executedSafeOperations = await safeApiKit.getSafeOperationsByAddress({
safeAddress: SAFE_ADDRESS,
executed: true
})

chai.expect(executedSafeOperations.count).equals(2)
chai.expect(allSafeOperations.count - pendingSafeOperations.count).equals(2)
})

it('should get all safe operations without confirmations', async () => {
const response = await safeApiKit.getSafeOperationsByAddress({
safeAddress: SAFE_ADDRESS,
offset: 1,
hasConfirmations: false
})

chai.expect(response).to.have.property('count').equals(0)
chai.expect(response).to.have.property('results').to.be.an('array')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -193,12 +193,14 @@ describe('SafeOperationClient', () => {

describe('getPendingSafeOperations', () => {
it('should return the pending Safe operations for the Safe address', async () => {
apiKit.getSafeOperationsByAddress = jest.fn().mockResolvedValue(PENDING_SAFE_OPERATIONS)
apiKit.getPendingSafeOperations = jest.fn().mockResolvedValue(PENDING_SAFE_OPERATIONS)

const result = await safeOperationClient.getPendingSafeOperations()

expect(protocolKit.getAddress).toHaveBeenCalled()
expect(apiKit.getSafeOperationsByAddress).toHaveBeenCalledWith({ safeAddress: SAFE_ADDRESS })
expect(apiKit.getPendingSafeOperations).toHaveBeenCalledWith({
safeAddress: SAFE_ADDRESS
})
expect(result).toBe(PENDING_SAFE_OPERATIONS)
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export class SafeOperationClient {
async getPendingSafeOperations(options?: ListOptions): Promise<GetSafeOperationListResponse> {
const safeAddress = await this.protocolKit.getAddress()

return this.apiKit.getSafeOperationsByAddress({ safeAddress, ...options })
return this.apiKit.getPendingSafeOperations({ safeAddress, ...options })
}

/**
Expand Down
Loading