Skip to content

Commit

Permalink
feat(api kit) Update delegates endpoint to v2 (safe-global#804)
Browse files Browse the repository at this point in the history
* Update api endpoint version
  • Loading branch information
leonardotc authored May 10, 2024
1 parent 9caadb3 commit 8cf4bf8
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 47 deletions.
19 changes: 9 additions & 10 deletions packages/api-kit/src/SafeApiKit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -256,7 +257,7 @@ class SafeApiKit {
limit,
offset
}: GetSafeDelegateProps): Promise<SafeDelegateListResponse> {
const url = new URL(`${this.#txServiceBaseUrl}/v1/delegates`)
const url = new URL(`${this.#txServiceBaseUrl}/v2/delegates`)

if (safeAddress) {
const { address: safe } = this.#getEip3770Address(safeAddress)
Expand All @@ -279,6 +280,7 @@ class SafeApiKit {
if (offset) {
url.searchParams.set('offset', offset)
}

return sendRequest({
url: url.toString(),
method: HttpMethod.Get
Expand Down Expand Up @@ -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,
Expand All @@ -327,7 +328,7 @@ class SafeApiKit {
signature
}
return sendRequest({
url: `${this.#txServiceBaseUrl}/v1/delegates/`,
url: `${this.#txServiceBaseUrl}/v2/delegates/`,
method: HttpMethod.Post,
body
})
Expand Down Expand Up @@ -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
}
Expand Down
33 changes: 33 additions & 0 deletions packages/api-kit/src/utils/signDelegate.ts
Original file line number Diff line number Diff line change
@@ -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 })
}
32 changes: 17 additions & 15 deletions packages/api-kit/tests/e2e/addSafeDelegate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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}`
Expand Down
3 changes: 2 additions & 1 deletion packages/api-kit/tests/e2e/getSafeDelegates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
))
})

Expand Down
3 changes: 2 additions & 1 deletion packages/api-kit/tests/e2e/removeSafeDelegate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
))
})

Expand Down
36 changes: 16 additions & 20 deletions packages/api-kit/tests/endpoint/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'
})
})
Expand All @@ -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'
})
})
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down

0 comments on commit 8cf4bf8

Please sign in to comment.