diff --git a/src/ajv.ts b/src/ajv.ts index 693b83e..934a8bf 100644 --- a/src/ajv.ts +++ b/src/ajv.ts @@ -1,10 +1,33 @@ import Ajv, { type ErrorObject } from 'ajv' +import { ETHER_TO_GWEI } from './constants' + +function validDpositAmounts (data: boolean, deposits: string[]): boolean { + let sum = 0 + for (let i = 0; i < deposits.length; i++) { + const amount = parseInt(deposits[i]) + if (amount % ETHER_TO_GWEI !== 0 || amount > 32 * ETHER_TO_GWEI) { + return false + } + sum += amount + } + if (sum !== 32 * ETHER_TO_GWEI) { + return false + } else { + return true + } +} + export function validatePayload ( data: any, schema: any, ): ErrorObject[] | undefined | null | boolean { const ajv = new Ajv() + ajv.addKeyword({ + keyword: 'validDpositAmounts', + validate: validDpositAmounts, + errors: true, + }) const validate = ajv.compile(schema) const isValid = validate(data) if (!isValid) { diff --git a/src/constants.ts b/src/constants.ts index 0a8d0f8..274d3e8 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -98,7 +98,7 @@ export const signEnrPayload = ( export const DKG_ALGORITHM = 'default' -export const CONFIG_VERSION = 'v1.7.0' +export const CONFIG_VERSION = 'v1.8.0' export const SDK_VERSION = pjson.version diff --git a/src/index.ts b/src/index.ts index f3ca275..70f92db 100644 --- a/src/index.ts +++ b/src/index.ts @@ -39,7 +39,7 @@ export class Client extends Base { * An example of how to instantiate obol-sdk Client: * [obolClient](https://github.com/ObolNetwork/obol-sdk-examples/blob/main/TS-Example/index.ts#L29) */ - constructor ( + constructor( config: { baseUrl?: string, chainId?: number }, signer?: Signer, ) { @@ -56,7 +56,7 @@ export class Client extends Base { * An example of how to use createClusterDefinition: * [createObolCluster](https://github.com/ObolNetwork/obol-sdk-examples/blob/main/TS-Example/index.ts) */ - async createClusterDefinition (newCluster: ClusterPayload): Promise { + async createClusterDefinition(newCluster: ClusterPayload): Promise { if (!this.signer) { throw new Error('Signer is required in createClusterDefinition') } validatePayload(newCluster, definitionSchema) @@ -70,8 +70,8 @@ export class Client extends Base { timestamp: new Date().toISOString(), threshold: Math.ceil((2 * newCluster.operators.length) / 3), num_validators: newCluster.validators.length, + deposit_amounts: newCluster.deposit_amounts ? newCluster.deposit_amounts : ['32000000000'] } - try { const address = await this.signer.getAddress() @@ -114,7 +114,7 @@ export class Client extends Base { * An example of how to use acceptClusterDefinition: * [acceptClusterDefinition](https://github.com/ObolNetwork/obol-sdk-examples/blob/main/TS-Example/index.ts) */ - async acceptClusterDefinition ( + async acceptClusterDefinition( operatorPayload: OperatorPayload, configHash: string, ): Promise { @@ -166,7 +166,7 @@ export class Client extends Base { * An example of how to use getClusterDefinition: * [getObolClusterDefinition](https://github.com/ObolNetwork/obol-sdk-examples/blob/main/TS-Example/index.ts) */ - async getClusterDefinition (configHash: string): Promise { + async getClusterDefinition(configHash: string): Promise { const clusterDefinition: ClusterDefintion = await this.request( `/dv/${configHash}`, { @@ -185,7 +185,7 @@ export class Client extends Base { * An example of how to use getClusterLock: * [getObolClusterLock](https://github.com/ObolNetwork/obol-sdk-examples/blob/main/TS-Example/index.ts) */ - async getClusterLock (configHash: string): Promise { + async getClusterLock(configHash: string): Promise { const lock: ClusterLock = await this.request( `/lock/configHash/${configHash}`, { diff --git a/src/schema.ts b/src/schema.ts index d431816..474a3b3 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -53,6 +53,14 @@ export const definitionSchema = { required: ['fee_recipient_address', 'withdrawal_address'], }, }, + deposit_amounts: { + type: 'array', + items: { + type: 'string', + pattern: '^[0-9]+$', + }, + validDpositAmounts: true + }, }, required: ['name', 'operators', 'validators'], } diff --git a/test/methods.test.ts b/test/methods.test.ts index 2e6443f..9cce35b 100644 --- a/test/methods.test.ts +++ b/test/methods.test.ts @@ -35,23 +35,23 @@ describe('Cluster Client', () => { .mockReturnValue(Promise.resolve({ config_hash: mockConfigHash })) const configHash = - await clientInstance.createClusterDefinition(clusterConfigV1X7) + await clientInstance.createClusterDefinition(clusterConfigV1X8) expect(configHash).toEqual(mockConfigHash) }) test('acceptClusterDefinition should return cluster definition', async () => { clientInstance['request'] = jest .fn() - .mockReturnValue(Promise.resolve(clusterLockV1X7.cluster_definition)) + .mockReturnValue(Promise.resolve(clusterLockV1X8.cluster_definition)) const clusterDefinition = await clientInstance.acceptClusterDefinition( { - enr: clusterLockV1X7.cluster_definition.operators[0].enr, - version: clusterLockV1X7.cluster_definition.version, + enr: clusterLockV1X8.cluster_definition.operators[0].enr, + version: clusterLockV1X8.cluster_definition.version, }, - clusterLockV1X7.cluster_definition.config_hash, + clusterLockV1X8.cluster_definition.config_hash, ) - expect(clusterDefinition).toEqual(clusterLockV1X7.cluster_definition) + expect(clusterDefinition).toEqual(clusterLockV1X8.cluster_definition) }) test('createClusterDefinition should throw an error on invalid operators', async () => { @@ -60,7 +60,7 @@ describe('Cluster Client', () => { .mockReturnValue(Promise.resolve({ config_hash: mockConfigHash })) try { await clientInstance.createClusterDefinition({ - ...clusterConfigV1X7, + ...clusterConfigV1X8, operators: [], }) } catch (error: any) { @@ -70,9 +70,38 @@ describe('Cluster Client', () => { } }) + //cause we default to 32000000000 + test('createClusterDefinition should accept a configuration without deposit_amounts', async () => { + clientInstance['request'] = jest + .fn() + .mockReturnValue(Promise.resolve({ config_hash: mockConfigHash })) + + const configHash = await clientInstance.createClusterDefinition({ + ...clusterConfigV1X7, + }) + + expect(configHash).toEqual(mockConfigHash) + }) + + test('createClusterDefinition should throw on not valid deposit_amounts ', async () => { + clientInstance['request'] = jest + .fn() + .mockReturnValue(Promise.resolve({ config_hash: mockConfigHash })) + try { + await clientInstance.createClusterDefinition({ + ...clusterConfigV1X7, + deposit_amounts: ['34000000'] + }) + } catch (error: any) { + expect(error.message).toEqual( + "Schema compilation errors', must pass \"validDpositAmounts\" keyword validation", + ) + } + }) + test('validatePayload should throw an error on empty schema', async () => { try { - validatePayload({ ...clusterConfigV1X7, operators: [] }, '') + validatePayload({ ...clusterConfigV1X8, operators: [] }, '') } catch (error: any) { expect(error.message).toEqual('schema must be object or boolean') } @@ -81,26 +110,30 @@ describe('Cluster Client', () => { test('getClusterdefinition should return cluster definition if config hash exist', async () => { clientInstance['request'] = jest .fn() - .mockReturnValue(Promise.resolve(clusterLockV1X7.cluster_definition)) + .mockReturnValue(Promise.resolve(clusterLockV1X8.cluster_definition)) const clusterDefinition = await clientInstance.getClusterDefinition( - clusterLockV1X7.cluster_definition.config_hash, + clusterLockV1X8.cluster_definition.config_hash, + ) + + expect(clusterDefinition.deposit_amounts?.length).toEqual( + clusterLockV1X8.cluster_definition.deposit_amounts.length, ) expect(clusterDefinition.config_hash).toEqual( - clusterLockV1X7.cluster_definition.config_hash, + clusterLockV1X8.cluster_definition.config_hash, ) }) test('getClusterLock should return lockFile if exist', async () => { clientInstance['request'] = jest .fn() - .mockReturnValue(Promise.resolve(clusterLockV1X7)) + .mockReturnValue(Promise.resolve(clusterLockV1X8)) const clusterLock = await clientInstance.getClusterLock( - clusterLockV1X7.cluster_definition.config_hash, + clusterLockV1X8.cluster_definition.config_hash, ) - expect(clusterLock.lock_hash).toEqual(clusterLockV1X7.lock_hash) + expect(clusterLock.lock_hash).toEqual(clusterLockV1X8.lock_hash) }) test('request method should set user agent header', async () => { @@ -140,7 +173,7 @@ describe('Cluster Client without a signer', () => { test('createClusterDefinition should throw an error without signer', async () => { try { - await clientInstance.createClusterDefinition(clusterConfigV1X7) + await clientInstance.createClusterDefinition(clusterConfigV1X8) } catch (err: any) { expect(err.message).toEqual('Signer is required in createClusterDefinition') } @@ -150,10 +183,10 @@ describe('Cluster Client without a signer', () => { try { await clientInstance.acceptClusterDefinition( { - enr: clusterLockV1X7.cluster_definition.operators[0].enr, - version: clusterLockV1X7.cluster_definition.version, + enr: clusterLockV1X8.cluster_definition.operators[0].enr, + version: clusterLockV1X8.cluster_definition.version, }, - clusterLockV1X7.cluster_definition.config_hash, + clusterLockV1X8.cluster_definition.config_hash, ) } catch (err: any) { expect(err.message).toEqual('Signer is required in acceptClusterDefinition') @@ -163,25 +196,25 @@ describe('Cluster Client without a signer', () => { test('getClusterdefinition should return cluster definition if config hash exist', async () => { clientInstance['request'] = jest .fn() - .mockReturnValue(Promise.resolve(clusterLockV1X7.cluster_definition)) + .mockReturnValue(Promise.resolve(clusterLockV1X8.cluster_definition)) const clusterDefinition = await clientInstance.getClusterDefinition( - clusterLockV1X7.cluster_definition.config_hash, + clusterLockV1X8.cluster_definition.config_hash, ) expect(clusterDefinition.config_hash).toEqual( - clusterLockV1X7.cluster_definition.config_hash, + clusterLockV1X8.cluster_definition.config_hash, ) }) test('getClusterLock should return lockFile if exist', async () => { clientInstance['request'] = jest .fn() - .mockReturnValue(Promise.resolve(clusterLockV1X7)) + .mockReturnValue(Promise.resolve(clusterLockV1X8)) const clusterLock = await clientInstance.getClusterLock( - clusterLockV1X7.cluster_definition.config_hash, + clusterLockV1X8.cluster_definition.config_hash, ) - expect(clusterLock.lock_hash).toEqual(clusterLockV1X7.lock_hash) + expect(clusterLock.lock_hash).toEqual(clusterLockV1X8.lock_hash) }) test.each([{ version: 'v1.7.0', clusterLock: clusterLockV1X7 }, { version: 'v1.8.0', clusterLock: clusterLockV1X8 }])( diff --git a/test/sdk-package-test/cluster.test.ts b/test/sdk-package-test/cluster.test.ts index 6ec4a8c..de85f67 100755 --- a/test/sdk-package-test/cluster.test.ts +++ b/test/sdk-package-test/cluster.test.ts @@ -1,6 +1,6 @@ import request from 'supertest' import dotenv from 'dotenv' -import { clusterConfigV1X7, clusterLockV1X7, clusterLockV1X8, enr } from './fixtures' +import { clusterConfigV1X8, clusterLockV1X7, clusterLockV1X8, enr } from './fixtures' import { client, updateClusterDef, @@ -27,12 +27,12 @@ describe('Cluster Definition', () => { let clusterDefinition: ClusterDefintion let secondConfigHash: string const clientWithoutAsigner = new Client({ - baseUrl: 'https://obol-api-nonprod-dev.dev.obol.tech', + baseUrl: 'https://02d0-2a01-9700-155f-0-31cb-c12e-9908-fe82.ngrok-free.app', chainId: 17000, }) beforeAll(async () => { - configHash = await client.createClusterDefinition(clusterConfigV1X7) + configHash = await client.createClusterDefinition(clusterConfigV1X8) }) it('should post a cluster definition and return confighash', async () => { @@ -41,7 +41,7 @@ describe('Cluster Definition', () => { it('should throw on post a cluster without a signer', async () => { try { - await clientWithoutAsigner.createClusterDefinition(clusterConfigV1X7) + await clientWithoutAsigner.createClusterDefinition(clusterConfigV1X8) } catch (err: any) { expect(err.message).toEqual('Signer is required in createClusterDefinition') } @@ -71,9 +71,9 @@ describe('Cluster Definition', () => { it('should update the cluster which the operator belongs to', async () => { const signerAddress = await signer.getAddress() - clusterConfigV1X7.operators.push({ address: signerAddress }) + clusterConfigV1X8.operators.push({ address: signerAddress }) - secondConfigHash = await client.createClusterDefinition(clusterConfigV1X7) + secondConfigHash = await client.createClusterDefinition(clusterConfigV1X8) const definitionData: ClusterDefintion = await client.acceptClusterDefinition( @@ -108,10 +108,10 @@ describe('Cluster Definition', () => { describe('Poll Cluster Lock', () => { // Test polling getClusterLock through mimicing the whole flow using obol-api endpoints - const { definition_hash: _, ...rest } = clusterLockV1X7.cluster_definition + const { definition_hash: _, ...rest } = clusterLockV1X8.cluster_definition const clusterWithoutDefHash = rest const clientWithoutAsigner = new Client({ - baseUrl: 'https://obol-api-nonprod-dev.dev.obol.tech', + baseUrl: 'https://02d0-2a01-9700-155f-0-31cb-c12e-9908-fe82.ngrok-free.app', chainId: 17000, }) @@ -126,7 +126,7 @@ describe('Poll Cluster Lock', () => { const pollReqIntervalId = setInterval(async function () { try { const lockFile = await client.getClusterLock( - clusterLockV1X7.cluster_definition.config_hash, + clusterLockV1X8.cluster_definition.config_hash, ) if (lockFile?.lock_hash) { clearInterval(pollReqIntervalId) @@ -144,8 +144,8 @@ describe('Poll Cluster Lock', () => { }, 5000) }), (async () => { - await updateClusterDef(clusterLockV1X7.cluster_definition) - await publishLockFile(clusterLockV1X7) + await updateClusterDef(clusterLockV1X8.cluster_definition) + await publishLockFile(clusterLockV1X8) })(), ]) expect(lockObject).toHaveProperty('lock_hash') @@ -158,7 +158,7 @@ describe('Poll Cluster Lock', () => { const pollReqIntervalId = setInterval(async function () { try { const lockFile = await clientWithoutAsigner.getClusterLock( - clusterLockV1X7.cluster_definition.config_hash, + clusterLockV1X8.cluster_definition.config_hash, ) if (lockFile?.lock_hash) { clearInterval(pollReqIntervalId) @@ -175,8 +175,8 @@ describe('Poll Cluster Lock', () => { }, 5000) }), (async () => { - await updateClusterDef(clusterLockV1X7.cluster_definition) - await publishLockFile(clusterLockV1X7) + await updateClusterDef(clusterLockV1X8.cluster_definition) + await publishLockFile(clusterLockV1X8) })(), ]) expect(lockObject).toHaveProperty('lock_hash') @@ -185,10 +185,13 @@ describe('Poll Cluster Lock', () => { it('should fetch the cluster definition for the configHash', async () => { const clusterDefinition: ClusterDefintion = await client.getClusterDefinition( - clusterLockV1X7.cluster_definition.config_hash, + clusterLockV1X8.cluster_definition.config_hash, ) + expect(clusterDefinition.deposit_amounts?.length).toEqual( + clusterLockV1X8.cluster_definition.deposit_amounts.length, + ) expect(clusterDefinition.config_hash).toEqual( - clusterLockV1X7.cluster_definition.config_hash, + clusterLockV1X8.cluster_definition.config_hash, ) }) @@ -200,8 +203,8 @@ describe('Poll Cluster Lock', () => { }) afterAll(async () => { - const configHash = clusterLockV1X7.cluster_definition.config_hash - const lockHash = clusterLockV1X7.lock_hash + const configHash = clusterLockV1X8.cluster_definition.config_hash + const lockHash = clusterLockV1X8.lock_hash await request(app) .delete(`/lock/${lockHash}`) diff --git a/test/sdk-package-test/utils.ts b/test/sdk-package-test/utils.ts index d6ecb3e..cc468fb 100644 --- a/test/sdk-package-test/utils.ts +++ b/test/sdk-package-test/utils.ts @@ -15,7 +15,7 @@ const wallet = new ethers.Wallet(privateKey) export const signer = wallet.connect(null) export const client: Client = new Client( - { baseUrl: 'https://obol-api-nonprod-dev.dev.obol.tech', chainId: 17000 }, + { baseUrl: 'https://02d0-2a01-9700-155f-0-31cb-c12e-9908-fe82.ngrok-free.app', chainId: 17000 }, signer, )