diff --git a/packages/api-kit/src/SafeApiKit.ts b/packages/api-kit/src/SafeApiKit.ts index f18a370df..b79188e68 100644 --- a/packages/api-kit/src/SafeApiKit.ts +++ b/packages/api-kit/src/SafeApiKit.ts @@ -27,6 +27,7 @@ import { TransferListResponse } from '@safe-global/api-kit/types/safeTransactionServiceTypes' import { HttpMethod, sendRequest } from '@safe-global/api-kit/utils/httpRequests' +import { signDelegate } from '@safe-global/api-kit/utils/signDelegate' import { validateEip3770Address, validateEthereumAddress } from '@safe-global/protocol-kit' import { Eip3770Address, @@ -256,7 +257,7 @@ class SafeApiKit { limit, offset }: GetSafeDelegateProps): Promise { - const url = new URL(`${this.#txServiceBaseUrl}/v1/delegates`) + const url = new URL(`${this.#txServiceBaseUrl}/v2/delegates`) if (safeAddress) { const { address: safe } = this.#getEip3770Address(safeAddress) @@ -279,6 +280,7 @@ class SafeApiKit { if (offset) { url.searchParams.set('offset', offset) } + return sendRequest({ url: url.toString(), method: HttpMethod.Get @@ -316,9 +318,8 @@ class SafeApiKit { } const { address: delegate } = this.#getEip3770Address(delegateAddress) const { address: delegator } = this.#getEip3770Address(delegatorAddress) - const totp = Math.floor(Date.now() / 1000 / 3600) - const data = delegate + totp - const signature = await signer.signMessage(data) + const signature = await signDelegate(signer, delegate, this.#chainId) + const body: any = { safe: safeAddress ? this.#getEip3770Address(safeAddress).address : null, delegate, @@ -327,7 +328,7 @@ class SafeApiKit { signature } return sendRequest({ - url: `${this.#txServiceBaseUrl}/v1/delegates/`, + url: `${this.#txServiceBaseUrl}/v2/delegates/`, method: HttpMethod.Post, body }) @@ -357,14 +358,12 @@ class SafeApiKit { } const { address: delegate } = this.#getEip3770Address(delegateAddress) const { address: delegator } = this.#getEip3770Address(delegatorAddress) - const totp = Math.floor(Date.now() / 1000 / 3600) - const data = delegate + totp - const signature = await signer.signMessage(data) + const signature = await signDelegate(signer, delegate, this.#chainId) + return sendRequest({ - url: `${this.#txServiceBaseUrl}/v1/delegates/${delegate}`, + url: `${this.#txServiceBaseUrl}/v2/delegates/${delegate}`, method: HttpMethod.Delete, body: { - delegate, delegator, signature } diff --git a/packages/api-kit/src/utils/signDelegate.ts b/packages/api-kit/src/utils/signDelegate.ts new file mode 100644 index 000000000..85f2b2ee9 --- /dev/null +++ b/packages/api-kit/src/utils/signDelegate.ts @@ -0,0 +1,33 @@ +import { Signer } from 'ethers' + +// TODO: remove this function in favor of viem#pad +function padHex( + hex: string, + { dir = 'left', size = 32 }: { dir?: string; size?: number } = {} +): string { + if (size === null) return hex + const result = hex.replace('0x', '') + if (result.length > size * 2) throw new Error(`Size (${result.length}) exceeds padding size.`) + + return `0x${result[dir === 'right' ? 'padEnd' : 'padStart'](size * 2, '0')}` +} + +export async function signDelegate(signer: Signer, delegateAddress: string, chainId: bigint) { + const domain = { + name: 'Safe Transaction Service', + version: '1.0', + chainId: chainId + } + + const types = { + Delegate: [ + { name: 'delegateAddress', type: 'bytes32' }, + { name: 'totp', type: 'uint256' } + ] + } + + const totp = Math.floor(Date.now() / 1000 / 3600) + const paddedAddress = padHex(delegateAddress, { size: 32, dir: 'right' }) + + return await signer.signTypedData(domain, types, { delegateAddress: paddedAddress, totp }) +} diff --git a/packages/api-kit/tests/e2e/addSafeDelegate.test.ts b/packages/api-kit/tests/e2e/addSafeDelegate.test.ts index e3b00bf04..d91efe0ce 100644 --- a/packages/api-kit/tests/e2e/addSafeDelegate.test.ts +++ b/packages/api-kit/tests/e2e/addSafeDelegate.test.ts @@ -13,12 +13,13 @@ let signer: Signer describe('addSafeDelegate', () => { before(async () => { ;({ safeApiKit, signer } = await getServiceClient( - '0x83a415ca62e11f5fa5567e98450d0f82ae19ff36ef876c10a8d448c788a53676' + '0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d', + 'https://safe-transaction-sepolia.staging.5afe.dev/api' )) }) it('should fail if Label is empty', async () => { - const delegateAddress = '0x9cCBDE03eDd71074ea9c49e413FA9CDfF16D263B' + const delegateAddress = '0xe4bb611E4e4164D54Ad7361B9d58b0A1eBD462B8' const delegatorAddress = await signer.getAddress() const delegateConfig: AddSafeDelegateProps = { delegateAddress, @@ -46,7 +47,7 @@ describe('addSafeDelegate', () => { }) it('should fail if Safe delegator address is empty', async () => { - const delegateAddress = '0x9cCBDE03eDd71074ea9c49e413FA9CDfF16D263B' + const delegateAddress = '0xe4bb611E4e4164D54Ad7361B9d58b0A1eBD462B8' const delegatorAddress = '' const delegateConfig: AddSafeDelegateProps = { delegateAddress, @@ -60,8 +61,8 @@ describe('addSafeDelegate', () => { }) it('should fail if Safe address is not checksummed', async () => { - const safeAddress = '0xF8ef84392f7542576F6b9d1b140334144930Ac78'.toLowerCase() - const delegateAddress = '0x9cCBDE03eDd71074ea9c49e413FA9CDfF16D263B' + const safeAddress = '0xe4bb611E4e4164D54Ad7361B9d58b0A1eBD462B8'.toLowerCase() + const delegateAddress = '0xe4bb611E4e4164D54Ad7361B9d58b0A1eBD462B8' const delegatorAddress = await signer.getAddress() const delegateConfig: AddSafeDelegateProps = { safeAddress, @@ -76,8 +77,8 @@ describe('addSafeDelegate', () => { }) it('should fail if Safe delegate address is not checksummed', async () => { - const safeAddress = '0xF8ef84392f7542576F6b9d1b140334144930Ac78' - const delegateAddress = '0x9cCBDE03eDd71074ea9c49e413FA9CDfF16D263B'.toLowerCase() + const safeAddress = '0xe4bb611E4e4164D54Ad7361B9d58b0A1eBD462B8' + const delegateAddress = '0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0'.toLowerCase() const delegatorAddress = await signer.getAddress() const delegateConfig: AddSafeDelegateProps = { safeAddress, @@ -92,8 +93,8 @@ describe('addSafeDelegate', () => { }) it('should fail if Safe delegator address is not checksummed', async () => { - const safeAddress = '0xF8ef84392f7542576F6b9d1b140334144930Ac78' - const delegateAddress = '0x9cCBDE03eDd71074ea9c49e413FA9CDfF16D263B' + const safeAddress = '0xe4bb611E4e4164D54Ad7361B9d58b0A1eBD462B8' + const delegateAddress = '0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0' const delegatorAddress = (await signer.getAddress()).toLowerCase() const delegateConfig: AddSafeDelegateProps = { safeAddress, @@ -125,10 +126,11 @@ describe('addSafeDelegate', () => { it('should fail if the signer is not an owner of the Safe', async () => { const { safeApiKit, signer } = await getServiceClient( - '0xb0057716d5917badaf911b193b12b910811c1497b5bada8d7711f758981c3773' + '0xb0057716d5917badaf911b193b12b910811c1497b5bada8d7711f758981c3773', + 'https://safe-transaction-sepolia.staging.5afe.dev/api' ) - const safeAddress = '0xF8ef84392f7542576F6b9d1b140334144930Ac78' - const delegateAddress = '0x9cCBDE03eDd71074ea9c49e413FA9CDfF16D263B' + const safeAddress = '0xe4bb611E4e4164D54Ad7361B9d58b0A1eBD462B8' + const delegateAddress = '0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0' const delegatorAddress = await signer.getAddress() const delegateConfig: AddSafeDelegateProps = { safeAddress, @@ -145,8 +147,8 @@ describe('addSafeDelegate', () => { }) it('should add a new delegate', async () => { - const safeAddress = '0xF8ef84392f7542576F6b9d1b140334144930Ac78' - const delegateAddress = '0x9cCBDE03eDd71074ea9c49e413FA9CDfF16D263B' + const safeAddress = '0xe4bb611E4e4164D54Ad7361B9d58b0A1eBD462B8' + const delegateAddress = '0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0' const delegatorAddress = await signer.getAddress() const delegateConfig: AddSafeDelegateProps = { safeAddress, @@ -196,7 +198,7 @@ describe('addSafeDelegate', () => { }) it('should add a new delegate EIP-3770', async () => { - const safeAddress = '0xF8ef84392f7542576F6b9d1b140334144930Ac78' + const safeAddress = '0xe4bb611E4e4164D54Ad7361B9d58b0A1eBD462B8' const eip3770SafeAddress = `${config.EIP_3770_PREFIX}:${safeAddress}` const delegateAddress = '0x9cCBDE03eDd71074ea9c49e413FA9CDfF16D263B' const eip3770DelegateAddress = `${config.EIP_3770_PREFIX}:${delegateAddress}` diff --git a/packages/api-kit/tests/e2e/getSafeDelegates.test.ts b/packages/api-kit/tests/e2e/getSafeDelegates.test.ts index b88b2c863..fae2bf610 100644 --- a/packages/api-kit/tests/e2e/getSafeDelegates.test.ts +++ b/packages/api-kit/tests/e2e/getSafeDelegates.test.ts @@ -13,7 +13,8 @@ let signer: Signer describe('getSafeDelegates', () => { before(async () => { ;({ safeApiKit, signer } = await getServiceClient( - '0x83a415ca62e11f5fa5567e98450d0f82ae19ff36ef876c10a8d448c788a53676' + '0x83a415ca62e11f5fa5567e98450d0f82ae19ff36ef876c10a8d448c788a53676', + 'https://safe-transaction-sepolia.staging.5afe.dev/api' )) }) diff --git a/packages/api-kit/tests/e2e/removeSafeDelegate.test.ts b/packages/api-kit/tests/e2e/removeSafeDelegate.test.ts index 4e642f21f..b25073046 100644 --- a/packages/api-kit/tests/e2e/removeSafeDelegate.test.ts +++ b/packages/api-kit/tests/e2e/removeSafeDelegate.test.ts @@ -13,7 +13,8 @@ let signer: Signer describe('removeSafeDelegate', () => { before(async () => { ;({ safeApiKit, signer } = await getServiceClient( - '0x83a415ca62e11f5fa5567e98450d0f82ae19ff36ef876c10a8d448c788a53676' + '0x83a415ca62e11f5fa5567e98450d0f82ae19ff36ef876c10a8d448c788a53676', + 'https://safe-transaction-sepolia.staging.5afe.dev/api' )) }) diff --git a/packages/api-kit/tests/endpoint/index.test.ts b/packages/api-kit/tests/endpoint/index.test.ts index 002fb3420..ce34ba1d6 100644 --- a/packages/api-kit/tests/endpoint/index.test.ts +++ b/packages/api-kit/tests/endpoint/index.test.ts @@ -14,10 +14,12 @@ import sinon from 'sinon' import sinonChai from 'sinon-chai' import config from '../utils/config' import { getServiceClient } from '../utils/setupServiceClient' +import { signDelegate } from '@safe-global/api-kit/utils/signDelegate' chai.use(chaiAsPromised) chai.use(sinonChai) +const chainId = '0xaa36a7' const safeAddress = '0xF8ef84392f7542576F6b9d1b140334144930Ac78' const eip3770SafeAddress = `${config.EIP_3770_PREFIX}:${safeAddress}` const randomAddress = '0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0' @@ -180,7 +182,7 @@ describe('Endpoint tests', () => { .expect(safeApiKit.getSafeDelegates({ safeAddress })) .to.be.eventually.deep.equals({ data: { success: true } }) chai.expect(fetchData).to.have.been.calledWith({ - url: `${txServiceBaseUrl}/v1/delegates?safe=${safeAddress}`, + url: `${txServiceBaseUrl}/v2/delegates?safe=${safeAddress}`, method: 'get' }) }) @@ -190,7 +192,7 @@ describe('Endpoint tests', () => { .expect(safeApiKit.getSafeDelegates({ safeAddress: eip3770SafeAddress })) .to.be.eventually.deep.equals({ data: { success: true } }) chai.expect(fetchData).to.have.been.calledWith({ - url: `${txServiceBaseUrl}/v1/delegates?safe=${safeAddress}`, + url: `${txServiceBaseUrl}/v2/delegates?safe=${safeAddress}`, method: 'get' }) }) @@ -202,14 +204,13 @@ describe('Endpoint tests', () => { signer, label: 'label' } - const totp = Math.floor(Date.now() / 1000 / 3600) - const data = delegateAddress + totp - const signature = await signer.signMessage(data) + + const signature = await signDelegate(signer, delegateAddress, chainId) await chai .expect(safeApiKit.addSafeDelegate(delegateConfig)) .to.be.eventually.deep.equals({ data: { success: true } }) chai.expect(fetchData).to.have.been.calledWith({ - url: `${txServiceBaseUrl}/v1/delegates/`, + url: `${txServiceBaseUrl}/v2/delegates/`, method: 'post', body: { safe: null, @@ -228,14 +229,13 @@ describe('Endpoint tests', () => { signer, label: 'label' } - const totp = Math.floor(Date.now() / 1000 / 3600) - const data = delegateAddress + totp - const signature = await signer.signMessage(data) + + const signature = await signDelegate(signer, delegateAddress, chainId) await chai .expect(safeApiKit.addSafeDelegate(delegateConfig)) .to.be.eventually.deep.equals({ data: { success: true } }) chai.expect(fetchData).to.have.been.calledWith({ - url: `${txServiceBaseUrl}/v1/delegates/`, + url: `${txServiceBaseUrl}/v2/delegates/`, method: 'post', body: { safe: null, @@ -253,17 +253,15 @@ describe('Endpoint tests', () => { delegatorAddress, signer } - const totp = Math.floor(Date.now() / 1000 / 3600) - const data = delegateAddress + totp - const signature = await signer.signMessage(data) + + const signature = await signDelegate(signer, delegateAddress, chainId) await chai .expect(safeApiKit.removeSafeDelegate(delegateConfig)) .to.be.eventually.deep.equals({ data: { success: true } }) chai.expect(fetchData).to.have.been.calledWith({ - url: `${txServiceBaseUrl}/v1/delegates/${delegateConfig.delegateAddress}`, + url: `${txServiceBaseUrl}/v2/delegates/${delegateConfig.delegateAddress}`, method: 'delete', body: { - delegate: delegateAddress, delegator: delegatorAddress, signature } @@ -276,17 +274,15 @@ describe('Endpoint tests', () => { delegatorAddress, signer } - const totp = Math.floor(Date.now() / 1000 / 3600) - const data = delegateAddress + totp - const signature = await signer.signMessage(data) + + const signature = await signDelegate(signer, delegateAddress, chainId) await chai .expect(safeApiKit.removeSafeDelegate(delegateConfig)) .to.be.eventually.deep.equals({ data: { success: true } }) chai.expect(fetchData).to.have.been.calledWith({ - url: `${txServiceBaseUrl}/v1/delegates/${delegateAddress}`, + url: `${txServiceBaseUrl}/v2/delegates/${delegateAddress}`, method: 'delete', body: { - delegate: delegateAddress, delegator: delegatorAddress, signature }