Skip to content
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

[api kit] Update delegates endpoint to v2 #804

Merged
merged 20 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@
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 @@ -109,7 +110,7 @@
* @throws "Not Found"
* @throws "Ensure this field has at least 1 hexadecimal chars (not counting 0x)."
*/
async decodeData(data: string): Promise<any> {

Check warning on line 113 in packages/api-kit/src/SafeApiKit.ts

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type
if (data === '') {
throw new Error('Invalid data')
}
Expand Down Expand Up @@ -256,7 +257,7 @@
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 @@
if (offset) {
url.searchParams.set('offset', offset)
}

return sendRequest({
url: url.toString(),
method: HttpMethod.Get
Expand Down Expand Up @@ -316,10 +318,9 @@
}
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 = {

Check warning on line 323 in packages/api-kit/src/SafeApiKit.ts

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is SafeDelegateResponse right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const body: any = {
const body: SafeDelegateResponse = {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is the request body which is marked as any on the type.

safe: safeAddress ? this.#getEip3770Address(safeAddress).address : null,
delegate,
delegator,
Expand All @@ -327,7 +328,7 @@
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 @@
}
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'
yagopv marked this conversation as resolved.
Show resolved Hide resolved
)
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