-
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 12 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 |
---|---|---|
|
@@ -3,7 +3,8 @@ import { | |
SafeMultisigTransactionResponse, | ||
SafeTransactionData, | ||
UserOperation, | ||
SafeOperationResponse | ||
SafeOperationResponse, | ||
ListResponse | ||
} from '@safe-global/safe-core-sdk-types' | ||
|
||
export type SafeServiceInfoResponse = { | ||
|
@@ -89,17 +90,12 @@ export type SafeDelegateResponse = { | |
readonly signature: string | ||
} | ||
|
||
export type SafeDelegateListResponse = { | ||
readonly count: number | ||
readonly next?: string | ||
readonly previous?: string | ||
readonly results: { | ||
readonly safe: string | ||
readonly delegate: string | ||
readonly delegator: string | ||
readonly label: string | ||
}[] | ||
} | ||
export type SafeDelegateListResponse = ListResponse<{ | ||
readonly safe: string | ||
readonly delegate: string | ||
readonly delegator: string | ||
readonly label: string | ||
}> | ||
|
||
export type SafeMultisigTransactionEstimate = { | ||
readonly to: string | ||
|
@@ -125,12 +121,7 @@ export type ProposeTransactionProps = { | |
origin?: string | ||
} | ||
|
||
export type SafeMultisigTransactionListResponse = { | ||
readonly count: number | ||
readonly next?: string | ||
readonly previous?: string | ||
readonly results: SafeMultisigTransactionResponse[] | ||
} | ||
export type SafeMultisigTransactionListResponse = ListResponse<SafeMultisigTransactionResponse> | ||
|
||
export type TransferResponse = { | ||
readonly type?: string | ||
|
@@ -144,12 +135,7 @@ export type TransferResponse = { | |
readonly from: string | ||
} | ||
|
||
export type TransferListResponse = { | ||
readonly count: number | ||
readonly next?: string | ||
readonly previous?: string | ||
readonly results: TransferResponse[] | ||
} | ||
export type TransferListResponse = ListResponse<TransferResponse> | ||
|
||
export type SafeModuleTransaction = { | ||
readonly created?: string | ||
|
@@ -166,12 +152,7 @@ export type SafeModuleTransaction = { | |
readonly dataDecoded?: string | ||
} | ||
|
||
export type SafeModuleTransactionListResponse = { | ||
readonly count: number | ||
readonly next?: string | ||
readonly previous?: string | ||
readonly results: SafeModuleTransaction[] | ||
} | ||
export type SafeModuleTransactionListResponse = ListResponse<SafeModuleTransaction> | ||
|
||
export type Erc20Info = { | ||
readonly name: string | ||
|
@@ -189,12 +170,7 @@ export type TokenInfoResponse = { | |
readonly logoUri?: string | ||
} | ||
|
||
export type TokenInfoListResponse = { | ||
readonly count: number | ||
readonly next?: string | ||
readonly previous?: string | ||
readonly results: TokenInfoResponse[] | ||
} | ||
export type TokenInfoListResponse = ListResponse<TokenInfoResponse> | ||
|
||
export type TransferWithTokenInfoResponse = TransferResponse & { | ||
readonly tokenInfo: TokenInfoResponse | ||
|
@@ -230,16 +206,11 @@ export type AllTransactionsOptions = { | |
trusted?: boolean | ||
} | ||
|
||
export type AllTransactionsListResponse = { | ||
readonly count: number | ||
readonly next?: string | ||
readonly previous?: string | ||
readonly results: Array< | ||
| SafeModuleTransactionWithTransfersResponse | ||
| SafeMultisigTransactionWithTransfersResponse | ||
| EthereumTxWithTransfersResponse | ||
> | ||
} | ||
export type AllTransactionsListResponse = ListResponse< | ||
| SafeModuleTransactionWithTransfersResponse | ||
| SafeMultisigTransactionWithTransfersResponse | ||
| EthereumTxWithTransfersResponse | ||
> | ||
|
||
export type ModulesResponse = { | ||
safes: string[] | ||
|
@@ -265,12 +236,7 @@ export type SafeMessage = { | |
readonly preparedSignature: string | ||
} | ||
|
||
export type SafeMessageListResponse = { | ||
readonly count: number | ||
readonly next?: string | ||
readonly previous?: string | ||
readonly results: SafeMessage[] | ||
} | ||
export type SafeMessageListResponse = ListResponse<SafeMessage> | ||
|
||
export type AddMessageProps = { | ||
message: string | EIP712TypedData | ||
|
@@ -301,12 +267,7 @@ export type GetSafeOperationListProps = { | |
offset?: string | ||
} | ||
|
||
export type GetSafeOperationListResponse = { | ||
readonly count: number | ||
readonly next?: string | ||
readonly previous?: string | ||
readonly results: Array<SafeOperationResponse> | ||
} | ||
export type GetSafeOperationListResponse = ListResponse<SafeOperationResponse> | ||
|
||
export type AddSafeOperationProps = { | ||
/** Address of the EntryPoint contract */ | ||
|
@@ -325,3 +286,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 safeOperation.getHash() | ||
}) | ||
|
||
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) | ||
}) | ||
}) |
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 👍