-
Notifications
You must be signed in to change notification settings - Fork 182
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): Support for SafeOperation confirmations endpoints #876
Changes from 8 commits
d9a7f16
64f3387
9ec95a1
6a513b4
c3cf9d5
19d4a46
1c8c2aa
35bc7d9
a209bd8
057f920
2b08384
3cec83d
b218e00
1fab95d
55b5214
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -325,3 +325,12 @@ export type AddSafeOperationProps = { | |
validAfter?: number | ||
} | ||
} | ||
|
||
export type GetSafeOperationConfirmationListProps = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is a little bit weird to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes good point. Changed the type to number |
||
/** The hash of the SafeOperation to get confirmations for */ | ||
safeOperationHash: string | ||
/** Maximum number of results to return per page */ | ||
limit?: string | ||
/** Initial index from which to return the results */ | ||
offset?: string | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import chai from 'chai' | ||
import chaiAsPromised from 'chai-as-promised' | ||
import { Safe4337InitOptions, Safe4337Pack } from '@safe-global/relay-kit' | ||
import { generateTransferCallData } from '@safe-global/relay-kit/packs/safe-4337/testing-utils/helpers' | ||
import SafeApiKit from '@safe-global/api-kit/index' | ||
import { getAddSafeOperationProps } from '@safe-global/api-kit/utils/safeOperation' | ||
import { SafeOperation } from '@safe-global/safe-core-sdk-types' | ||
import { getApiKit, getEip1193Provider } from '../utils/setupKits' | ||
|
||
chai.use(chaiAsPromised) | ||
|
||
const PRIVATE_KEY_1 = '0x83a415ca62e11f5fa5567e98450d0f82ae19ff36ef876c10a8d448c788a53676' | ||
const PRIVATE_KEY_2 = '0xb88ad5789871315d0dab6fc5961d6714f24f35a6393f13a6f426dfecfc00ab44' | ||
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 PAYMASTER_TOKEN_ADDRESS = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238' | ||
|
||
let safeApiKit: SafeApiKit | ||
let safe4337Pack: Safe4337Pack | ||
let safeOperation: SafeOperation | ||
let safeOpHash: string | ||
|
||
describe('confirmSafeOperation', () => { | ||
const transferUSDC = { | ||
to: PAYMASTER_TOKEN_ADDRESS, | ||
data: generateTransferCallData(SAFE_ADDRESS, 100_000n), | ||
value: '0', | ||
operation: 0 | ||
} | ||
|
||
const getSafe4337Pack = async (options: Partial<Safe4337InitOptions>) => | ||
Safe4337Pack.init({ | ||
provider: options.provider || getEip1193Provider(), | ||
signer: options.signer || PRIVATE_KEY_1, | ||
options: { safeAddress: SAFE_ADDRESS }, | ||
bundlerUrl: BUNDLER_URL | ||
}) | ||
|
||
const createSignature = async (safeOperation: SafeOperation, signer: string) => { | ||
const safe4337Pack = await getSafe4337Pack({ signer }) | ||
const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) | ||
const signerAddress = await safe4337Pack.protocolKit.getSafeProvider().getSignerAddress() | ||
return signedSafeOperation.getSignature(signerAddress!) | ||
} | ||
|
||
/** | ||
* Add a new Safe operation to the transaction service. | ||
* @returns Resolves with the signed Safe operation | ||
*/ | ||
const addSafeOperation = async (): Promise<SafeOperation> => { | ||
const safeOperation = await safe4337Pack.createTransaction({ | ||
transactions: [transferUSDC] | ||
}) | ||
|
||
const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) | ||
const addSafeOperationProps = await getAddSafeOperationProps(signedSafeOperation) | ||
|
||
await safeApiKit.addSafeOperation(addSafeOperationProps) | ||
|
||
return signedSafeOperation | ||
} | ||
|
||
before(async () => { | ||
safe4337Pack = await getSafe4337Pack({ signer: PRIVATE_KEY_1 }) | ||
safeApiKit = getApiKit(TX_SERVICE_URL) | ||
|
||
// Submit a new Safe operation to the transaction service | ||
safeOperation = await addSafeOperation() | ||
safeOpHash = await safe4337Pack.getSafeOperationHash(safeOperation) | ||
}) | ||
|
||
describe('should fail', () => { | ||
it('if SafeOperation hash is empty', async () => { | ||
const signature = await createSignature(safeOperation, PRIVATE_KEY_2) | ||
await chai | ||
.expect(safeApiKit.confirmSafeOperation('', signature!.data)) | ||
.to.be.rejectedWith('Invalid SafeOperation hash') | ||
}) | ||
|
||
it('if signature is empty', async () => { | ||
await chai | ||
.expect(safeApiKit.confirmSafeOperation(safeOpHash, '')) | ||
.to.be.rejectedWith('Invalid signature') | ||
}) | ||
|
||
it('if signature is invalid', async () => { | ||
await chai | ||
.expect(safeApiKit.confirmSafeOperation(safeOpHash, '0xInvalidSignature')) | ||
.to.be.rejectedWith('Bad Request') | ||
}) | ||
}) | ||
|
||
it('should allow to create and confirm a SafeOperation signature using a Safe signer', async () => { | ||
const signerAddress1 = await safe4337Pack.protocolKit.getSafeProvider().getSignerAddress() | ||
|
||
// Create a signature for the Safe operation using owner 2 | ||
const signatureSigner2 = await createSignature(safeOperation, PRIVATE_KEY_2) | ||
|
||
// Add the second signature to the Safe operation | ||
await chai.expect(safeApiKit.confirmSafeOperation(safeOpHash, signatureSigner2!.data)).to.be | ||
.fulfilled | ||
|
||
// Check that the Safe operation is now confirmed by both owners | ||
const safeOperationResponse = await safeApiKit.getSafeOperation(safeOpHash) | ||
chai.expect(safeOperationResponse.confirmations).to.have.lengthOf(2) | ||
|
||
chai | ||
.expect(safeOperationResponse.confirmations![0].signature) | ||
.to.eq(safeOperation.getSignature(signerAddress1!)!.data) | ||
|
||
chai.expect(safeOperationResponse.confirmations![1].signature).to.eq(signatureSigner2!.data) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import chai from 'chai' | ||
import chaiAsPromised from 'chai-as-promised' | ||
import SafeApiKit from '@safe-global/api-kit/index' | ||
import { getApiKit } from '../utils/setupKits' | ||
|
||
chai.use(chaiAsPromised) | ||
|
||
let safeApiKit: SafeApiKit | ||
|
||
const TX_SERVICE_URL = 'https://safe-transaction-sepolia.staging.5afe.dev/api' | ||
|
||
describe('getSafeOperationConfirmations', () => { | ||
before(async () => { | ||
safeApiKit = getApiKit(TX_SERVICE_URL) | ||
}) | ||
|
||
it('should fail if safeOperationHash is empty', async () => { | ||
await chai | ||
.expect(safeApiKit.getSafeOperationConfirmations({ safeOperationHash: '' })) | ||
.to.be.rejectedWith('Invalid SafeOperation hash') | ||
}) | ||
|
||
it('should return an empty array if the safeOperationHash is not found', async () => { | ||
const safeOperationHash = '0x0000000000000000000000000000000000000000000000000000000000000000' | ||
const safeOpConfirmations = await safeApiKit.getSafeOperationConfirmations({ | ||
safeOperationHash | ||
}) | ||
chai.expect(safeOpConfirmations.count).to.be.equal(0) | ||
chai.expect(safeOpConfirmations.results.length).to.be.equal(0) | ||
}) | ||
|
||
it('should return the SafeOperation with the given safeOperationHash', async () => { | ||
const safeOperationHash = '0x375d3bd580600ce04d7d2c1d8d88d85f27b9c7d14d7b544f2ee585d672f2b449' | ||
const safeOpConfirmations = await safeApiKit.getSafeOperationConfirmations({ | ||
safeOperationHash | ||
}) | ||
chai.expect(safeOpConfirmations.count).to.be.equal(2) | ||
chai.expect(safeOpConfirmations.results.length).to.be.equal(2) | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please double check how we do this. This kind of looks like a getSafeOperationConfirmationsByHash with a different name as the hash itself is a part of a "perma-link". In this case, it would be common to separate the interface as
getSafeOperationConfirmations(hash, { limit, offset })
since that would be somehow associated with a domain model.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the function signature accordingly 👍