Skip to content

Commit

Permalink
Merge branch 'development' into prepare-release
Browse files Browse the repository at this point in the history
  • Loading branch information
dasanra committed Feb 10, 2025
2 parents dc5071d + c389c5f commit 9c4a8a4
Show file tree
Hide file tree
Showing 109 changed files with 2,848 additions and 1,915 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
coverage
*.lcov

#esbuild report
meta-*.json

# nyc test coverage
.nyc_output

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@types/node": "^22.10.9",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"esbuild": "^0.24.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.3",
Expand All @@ -35,7 +36,7 @@
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
"tsc-alias": "^1.8.8",
"typescript": "^5.3.3"
"typescript": "^5.6.3"
},
"lint-staged": {
"./packages/**/*.{js,jsx,ts,tsx}": [
Expand Down
15 changes: 12 additions & 3 deletions packages/api-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@
"name": "@safe-global/api-kit",
"version": "2.5.9",
"description": "SDK that facilitates the interaction with the Safe Transaction Service API",
"main": "dist/src/index.js",
"typings": "dist/src/index.d.ts",
"types": "dist/src/index.d.ts",
"main": "dist/cjs/index.cjs",
"module": "dist/esm/index.mjs",
"exports": {
"types": "./dist/src/index.d.ts",
"require": "./dist/cjs/index.cjs",
"import": "./dist/esm/index.mjs"
},
"keywords": [
"Ethereum",
"Wallet",
Expand All @@ -23,7 +29,10 @@
"format:check": "prettier --check \"*/**/*.{js,json,md,ts}\"",
"format": "prettier --write \"*/**/*.{js,json,md,ts}\"",
"unbuild": "rimraf dist .nyc_output cache",
"build": "yarn unbuild && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json"
"build": "yarn unbuild && yarn build:esm && yarn build:cjs && yarn build:types",
"build:esm": "esbuild ./src/index --format=esm --bundle --packages=external --outdir=dist/esm --out-extension:.js=.mjs",
"build:cjs": "esbuild ./src/index --format=cjs --bundle --packages=external --outdir=dist/cjs --out-extension:.js=.cjs",
"build:types": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json"
},
"repository": {
"type": "git",
Expand Down
40 changes: 34 additions & 6 deletions packages/api-kit/src/SafeApiKit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,16 @@ import { signDelegate } from '@safe-global/api-kit/utils/signDelegate'
import { validateEip3770Address, validateEthereumAddress } from '@safe-global/protocol-kit'
import {
Eip3770Address,
isSafeOperation,
SafeMultisigConfirmationListResponse,
SafeMultisigTransactionResponse,
SafeOperation,
SafeOperationConfirmationListResponse,
SafeOperationResponse
SafeOperationResponse,
UserOperationV06
} from '@safe-global/types-kit'
import { TRANSACTION_SERVICE_URLS } from './utils/config'
import { isEmptyData } from './utils'
import { getAddSafeOperationProps } from './utils/safeOperation'
import { getAddSafeOperationProps, isSafeOperation } from './utils/safeOperation'

export interface SafeApiKitConfig {
/** chainId - The chainId */
Expand Down Expand Up @@ -817,6 +817,8 @@ class SafeApiKit {
*/
async getSafeOperationsByAddress({
safeAddress,
executed,
hasConfirmations,
ordering,
limit,
offset
Expand All @@ -841,12 +843,36 @@ class SafeApiKit {
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 Expand Up @@ -918,21 +944,23 @@ class SafeApiKit {
const getISOString = (date: number | undefined) =>
!date ? null : new Date(date * 1000).toISOString()

const userOperationV06 = userOperation as UserOperationV06

return sendRequest({
url: `${this.#txServiceBaseUrl}/v1/safes/${safeAddress}/safe-operations/`,
method: HttpMethod.Post,
body: {
initCode: isEmptyData(userOperationV06.initCode) ? null : userOperationV06.initCode,
nonce: userOperation.nonce,
initCode: isEmptyData(userOperation.initCode) ? null : userOperation.initCode,
callData: userOperation.callData,
callGasLimit: userOperation.callGasLimit.toString(),
verificationGasLimit: userOperation.verificationGasLimit.toString(),
preVerificationGas: userOperation.preVerificationGas.toString(),
maxFeePerGas: userOperation.maxFeePerGas.toString(),
maxPriorityFeePerGas: userOperation.maxPriorityFeePerGas.toString(),
paymasterAndData: isEmptyData(userOperation.paymasterAndData)
paymasterAndData: isEmptyData(userOperationV06.paymasterAndData)
? null
: userOperation.paymasterAndData,
: userOperationV06.paymasterAndData,
entryPoint,
validAfter: getISOString(options?.validAfter),
validUntil: getISOString(options?.validUntil),
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
19 changes: 13 additions & 6 deletions packages/api-kit/src/utils/safeOperation.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import { SafeOperation } from '@safe-global/types-kit'
import { AddSafeOperationProps } from '../types/safeTransactionServiceTypes'

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

return {
entryPoint: safeOperation.data.entryPoint,
moduleAddress: safeOperation.moduleAddress,
safeAddress: safeOperation.data.safe,
entryPoint: safeOperation.options.entryPoint,
moduleAddress: safeOperation.options.moduleAddress,
safeAddress: userOperation.sender,
userOperation,
options: {
validAfter: safeOperation.data.validAfter,
validUntil: safeOperation.data.validUntil
validAfter: safeOperation.options.validAfter,
validUntil: safeOperation.options.validUntil
}
}
}

export const isSafeOperation = (
obj: AddSafeOperationProps | SafeOperation
): obj is SafeOperation => {
return 'signatures' in obj && 'getUserOperation' in obj && 'getHash' in obj
}
3 changes: 1 addition & 2 deletions packages/api-kit/tests/e2e/addMessageSignature.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import Safe, {
EthSafeSignature,
buildSignatureBytes,
hashSafeMessage,
SigningMethod,
buildContractSignature
} from '@safe-global/protocol-kit'
import { SafeMessage } from '@safe-global/types-kit'
import { SafeMessage, SigningMethod } from '@safe-global/types-kit'
import SafeApiKit from '@safe-global/api-kit/index'
import chai from 'chai'
import chaiAsPromised from 'chai-as-promised'
Expand Down
54 changes: 4 additions & 50 deletions packages/api-kit/tests/e2e/addSafeOperation.test.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,18 @@
import chai from 'chai'
import chaiAsPromised from 'chai-as-promised'
import sinon from 'sinon'
import Safe from '@safe-global/protocol-kit'
import SafeApiKit from '@safe-global/api-kit/index'
import { getAddSafeOperationProps } from '@safe-global/api-kit/utils/safeOperation'
import { BundlerClient, Safe4337Pack } from '@safe-global/relay-kit'
import { generateTransferCallData } from '@safe-global/relay-kit/packs/safe-4337/testing-utils/helpers'
import {
ENTRYPOINT_ABI,
ENTRYPOINT_ADDRESS_V06,
RPC_4337_CALLS
} from '@safe-global/relay-kit/packs/safe-4337/constants'
// Needs to be imported from dist folder in order to mock the getEip4337BundlerProvider function
import * as safe4337Utils from '@safe-global/relay-kit/dist/src/packs/safe-4337/utils'
import { Safe4337Pack } from '@safe-global/relay-kit'
import { generateTransferCallData } from '@safe-global/relay-kit/test-utils'
import { getKits } from '../utils/setupKits'

chai.use(chaiAsPromised)

const SIGNER_PK = '0x83a415ca62e11f5fa5567e98450d0f82ae19ff36ef876c10a8d448c788a53676'
const SAFE_ADDRESS = '0x60C4Ab82D06Fd7dFE9517e17736C2Dcc77443EF0' // 1/2 Safe (v1.4.1) with signer above being an owner + 4337 module enabled
const PAYMASTER_TOKEN_ADDRESS = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238'
const PAYMASTER_ADDRESS = '0x0000000000325602a77416A16136FDafd04b299f'
const BUNDLER_URL = 'https://bundler.url'
const BUNDLER_URL = 'https://api.pimlico.io/v2/sepolia/rpc?apikey=pim_Vjs7ohRqWdvsjUegngf9Bg'
const TX_SERVICE_URL = 'https://safe-transaction-sepolia.staging.5afe.dev/api'

let safeApiKit: SafeApiKit
Expand All @@ -36,58 +27,22 @@ describe('addSafeOperation', () => {
operation: 0
}

const requestStub = sinon.stub()
// Setup mocks for the bundler client
before(async () => {
sinon.stub(safe4337Utils, 'getEip4337BundlerProvider').returns({
request: requestStub,
readContract: sinon
.stub()
.withArgs({
address: ENTRYPOINT_ADDRESS_V06,
abi: ENTRYPOINT_ABI,
functionName: 'getNonce',
args: [SAFE_ADDRESS, BigInt(0)]
})
.resolves(123n)
} as unknown as BundlerClient)
;({ safeApiKit, protocolKit } = await getKits({
safeAddress: SAFE_ADDRESS,
signer: SIGNER_PK,
txServiceUrl: TX_SERVICE_URL
}))

requestStub.withArgs({ method: RPC_4337_CALLS.CHAIN_ID }).resolves('0xaa36a7')
requestStub
.withArgs({ method: RPC_4337_CALLS.SUPPORTED_ENTRY_POINTS })
.resolves([ENTRYPOINT_ADDRESS_V06])
requestStub
.withArgs({ method: 'pimlico_getUserOperationGasPrice' })
.resolves({ fast: { maxFeePerGas: '0x3b9aca00', maxPriorityFeePerGas: '0x3b9aca00' } })
requestStub
.withArgs({ method: RPC_4337_CALLS.ESTIMATE_USER_OPERATION_GAS, params: sinon.match.any })
.resolves({
preVerificationGas: BigInt(Date.now()),
callGasLimit: BigInt(Date.now()),
verificationGasLimit: BigInt(Date.now())
})

safe4337Pack = await Safe4337Pack.init({
provider: protocolKit.getSafeProvider().provider,
signer: protocolKit.getSafeProvider().signer,
options: { safeAddress: SAFE_ADDRESS },
bundlerUrl: BUNDLER_URL,
paymasterOptions: {
paymasterTokenAddress: PAYMASTER_TOKEN_ADDRESS,
paymasterAddress: PAYMASTER_ADDRESS
}
safeModulesVersion: '0.2.0'
})
})

after(() => {
sinon.restore()
})

describe('should fail', () => {
it('if safeAddress is empty', async () => {
const safeOperation = await safe4337Pack.createTransaction({ transactions: [transferUSDC] })
Expand Down Expand Up @@ -183,7 +138,6 @@ describe('addSafeOperation', () => {
transactions: [transferUSDC, 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
Expand Down
43 changes: 9 additions & 34 deletions packages/api-kit/tests/e2e/confirmSafeOperation.test.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,18 @@
import chai from 'chai'
import chaiAsPromised from 'chai-as-promised'
import sinon from 'sinon'
import { BundlerClient, Safe4337InitOptions, Safe4337Pack } from '@safe-global/relay-kit'
import { generateTransferCallData } from '@safe-global/relay-kit/packs/safe-4337/testing-utils/helpers'
import { Safe4337InitOptions, Safe4337Pack, SafeOperation } from '@safe-global/relay-kit'
import SafeApiKit from '@safe-global/api-kit/index'
import { getAddSafeOperationProps } from '@safe-global/api-kit/utils/safeOperation'
import { SafeOperation } from '@safe-global/types-kit'
// Needs to be imported from dist folder in order to mock the getEip4337BundlerProvider function
import * as safe4337Utils from '@safe-global/relay-kit/dist/src/packs/safe-4337/utils'
import { generateTransferCallData } from '@safe-global/relay-kit/test-utils'
import { getApiKit, getEip1193Provider } from '../utils/setupKits'
import {
ENTRYPOINT_ADDRESS_V06,
RPC_4337_CALLS
} from '@safe-global/relay-kit/packs/safe-4337/constants'

chai.use(chaiAsPromised)

const PRIVATE_KEY_1 = '0x83a415ca62e11f5fa5567e98450d0f82ae19ff36ef876c10a8d448c788a53676'
const PRIVATE_KEY_2 = '0xb88ad5789871315d0dab6fc5961d6714f24f35a6393f13a6f426dfecfc00ab44'
const PRIVATE_KEY_1 = '0x83a415ca62e11f5fa5567e98450d0f82ae19ff36ef876c10a8d448c788a53676' // 0x56e2C102c664De6DfD7315d12c0178b61D16F171
const PRIVATE_KEY_2 = '0xb88ad5789871315d0dab6fc5961d6714f24f35a6393f13a6f426dfecfc00ab44' // 0x9cCBDE03eDd71074ea9c49e413FA9CDfF16D263B
const SAFE_ADDRESS = '0x60C4Ab82D06Fd7dFE9517e17736C2Dcc77443EF0' // 4337 enabled 1/2 Safe (v1.4.1) owned by PRIVATE_KEY_1 + PRIVATE_KEY_2
const TX_SERVICE_URL = 'https://safe-transaction-sepolia.staging.5afe.dev/api'
const BUNDLER_URL = `https://bundler.url`
const BUNDLER_URL = 'https://api.pimlico.io/v2/sepolia/rpc?apikey=pim_Vjs7ohRqWdvsjUegngf9Bg'
const PAYMASTER_TOKEN_ADDRESS = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238'

let safeApiKit: SafeApiKit
Expand All @@ -31,8 +23,8 @@ let safeOpHash: string
describe('confirmSafeOperation', () => {
const transferUSDC = {
to: PAYMASTER_TOKEN_ADDRESS,
data: generateTransferCallData(SAFE_ADDRESS, 100_000n),
value: Date.now().toString(), // Make sure that the transaction hash is unique
data: generateTransferCallData(SAFE_ADDRESS, 100_000n) + Date.now().toString(), // Make sure that the transaction hash is unique
value: '0',
operation: 0
}

Expand All @@ -41,7 +33,8 @@ describe('confirmSafeOperation', () => {
provider: options.provider || getEip1193Provider(),
signer: options.signer || PRIVATE_KEY_1,
options: { safeAddress: SAFE_ADDRESS },
bundlerUrl: BUNDLER_URL
bundlerUrl: BUNDLER_URL,
safeModulesVersion: '0.2.0'
})

const createSignature = async (safeOperation: SafeOperation, signer: string) => {
Expand All @@ -68,21 +61,7 @@ describe('confirmSafeOperation', () => {
return signedSafeOperation
}

const requestStub = sinon.stub()

before(async () => {
sinon.stub(safe4337Utils, 'getEip4337BundlerProvider').returns({
request: requestStub
} as unknown as BundlerClient)

requestStub.withArgs({ method: RPC_4337_CALLS.CHAIN_ID }).resolves('0xaa36a7')
requestStub
.withArgs({ method: RPC_4337_CALLS.SUPPORTED_ENTRY_POINTS })
.resolves([ENTRYPOINT_ADDRESS_V06])
requestStub
.withArgs({ method: 'pimlico_getUserOperationGasPrice' })
.resolves({ fast: { maxFeePerGas: '0x3b9aca00', maxPriorityFeePerGas: '0x3b9aca00' } })

safe4337Pack = await getSafe4337Pack({ signer: PRIVATE_KEY_1 })
safeApiKit = getApiKit(TX_SERVICE_URL)

Expand All @@ -91,10 +70,6 @@ describe('confirmSafeOperation', () => {
safeOpHash = safeOperation.getHash()
})

after(() => {
sinon.restore()
})

describe('should fail', () => {
it('if SafeOperation hash is empty', async () => {
const signature = await createSignature(safeOperation, PRIVATE_KEY_2)
Expand Down
Loading

0 comments on commit 9c4a8a4

Please sign in to comment.