From b3bd6dad809c3e59ced8af9c1ba619ce4deca311 Mon Sep 17 00:00:00 2001 From: josemarinas <36479864+josemarinas@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:37:23 +0200 Subject: [PATCH] feat(sdk): add `getProtocolVersion` function (#76) * add: getProtocolVersion function * fix: prettier * fix: linting * Update sdk/src/introspection.ts Co-authored-by: Carles <75954325+banasa44@users.noreply.github.com> * Update sdk/src/introspection.ts Co-authored-by: Michael Heuer <20623991+Michael-A-Heuer@users.noreply.github.com> * Update sdk/test/unit/introspection.test.ts Co-authored-by: Michael Heuer <20623991+Michael-A-Heuer@users.noreply.github.com> * remove: constant ADDRESS_ONE * fix: prettier * update: changelog and package version * fix: export of addres one * fix: update getProtocolVersion return comment * chore: add ADDRESS_ONE to constants --------- Co-authored-by: Carles <75954325+banasa44@users.noreply.github.com> Co-authored-by: Michael Heuer <20623991+Michael-A-Heuer@users.noreply.github.com> --- sdk/CHANGELOG.md | 1 + sdk/package.json | 3 ++- sdk/src/errors.ts | 6 +++++ sdk/src/introspection.ts | 38 ++++++++++++++++++++++++++++ sdk/test/constants.ts | 2 ++ sdk/test/mocks.ts | 37 +++++++++++++++++++++++++++ sdk/test/unit/ens.test.ts | 5 ++-- sdk/test/unit/introspection.test.ts | 39 ++++++++++++++++++++++++++++- 8 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 sdk/test/mocks.ts diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md index cc972eee..5995346a 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add `resolveEnsName` functions +- Add `getProtocolVersion` function ### Removed diff --git a/sdk/package.json b/sdk/package.json index c5805f59..df2480a7 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,7 +1,7 @@ { "name": "@aragon/osx-commons-sdk", "author": "Aragon Association", - "version": "0.0.1-alpha.8", + "version": "0.0.1-alpha.9", "license": "MIT", "main": "dist/index.js", "module": "dist/osx-commons-sdk.esm.js", @@ -58,6 +58,7 @@ "@ethersproject/contracts": "^5.7.0", "@ethersproject/hash": "^5.7.0", "@ethersproject/providers": "^5.7.0", + "@ethersproject/address": "^5.7.0", "ipfs-http-client": "^51.0.0" } } diff --git a/sdk/src/errors.ts b/sdk/src/errors.ts index be10de72..4b011dd5 100644 --- a/sdk/src/errors.ts +++ b/sdk/src/errors.ts @@ -77,3 +77,9 @@ export class InvalidBitPositionError extends SdkError { ); } } + +export class InvalidAddressError extends SdkError { + constructor(address: string, cause?: any) { + super(`Invalid address: ${address}`, cause); + } +} diff --git a/sdk/src/introspection.ts b/sdk/src/introspection.ts index aaa1664f..3b39d481 100644 --- a/sdk/src/introspection.ts +++ b/sdk/src/introspection.ts @@ -1,5 +1,9 @@ +import {InvalidAddressError} from './errors'; import {Interface} from '@ethersproject/abi'; +import {isAddress} from '@ethersproject/address'; import {BigNumber} from '@ethersproject/bignumber'; +import {Contract} from '@ethersproject/contracts'; +import {JsonRpcProvider} from '@ethersproject/providers'; /** * Gets the interfaceId of a given interface @@ -16,3 +20,37 @@ export function getInterfaceId(iface: Interface): string { } return interfaceId.toHexString(); } + +/** + * Gets the protocol version of a contract, if the contract does not have a + * protocolVersion function, it will return [1, 0, 0] + * + * @export + * @param {string} rpc + * @param {string} contractAddress + * @return {*} {Promise<[number, number, number]>} + */ +export async function getProtocolVersion( + rpc: string, + contractAddress: string +): Promise<[number, number, number]> { + if (!isAddress(contractAddress)) { + throw new InvalidAddressError(contractAddress); + } + const provider = new JsonRpcProvider(rpc); + const iface = new Interface([ + 'function protocolVersion() public pure returns (uint8[3] memory)', + ]); + const contract = new Contract(contractAddress, iface, provider); + let version: [number, number, number]; + try { + version = await contract.protocolVersion(); + } catch (e) { + // version 1.0.0 of the contract does not have a protocolVersion function + // so if we receive an error we cannot differentiate between a call exception + // and a contract that does not have the function. So we assume that is + // a version 1.0.0 contract that does not have the function and return [1, 0, 0] + version = [1, 0, 0]; + } + return version; +} diff --git a/sdk/test/constants.ts b/sdk/test/constants.ts index 22cf5b97..b54ac612 100644 --- a/sdk/test/constants.ts +++ b/sdk/test/constants.ts @@ -93,6 +93,8 @@ export const TEST_ABI: MetadataAbiInput[] = [ }, ]; +export const ADDRESS_ONE = `0x${'0'.repeat(39)}1`; + export const TEST_ENS_NAME = 'subdomain.test.eth'; export const TEST_INVALID_ENS_NAME = 'test.invalid'; diff --git a/sdk/test/mocks.ts b/sdk/test/mocks.ts new file mode 100644 index 00000000..e4f0443a --- /dev/null +++ b/sdk/test/mocks.ts @@ -0,0 +1,37 @@ +export function mockContractProtocolVersion( + version: [number, number, number] = [1, 0, 0], + throwException: boolean = false +) { + jest + .spyOn(jest.requireActual('@ethersproject/contracts'), 'Contract') + .mockImplementation(() => { + return { + protocolVersion: () => { + if (throwException) { + throw new Error('Error'); + } + return Promise.resolve(version); + }, + }; + }); +} + +export function mockJSONRPCProvider( + chainId: number = 1, + blockNumber: number = 1 +) { + return jest + .spyOn(jest.requireActual('@ethersproject/providers'), 'JsonRpcProvider') + .mockImplementation(() => { + return { + getNetwork: () => { + return { + chainId, + }; + }, + getBlockNumber: () => { + return Promise.resolve(blockNumber); + }, + }; + }); +} diff --git a/sdk/test/unit/ens.test.ts b/sdk/test/unit/ens.test.ts index a5b364d9..034eda4a 100644 --- a/sdk/test/unit/ens.test.ts +++ b/sdk/test/unit/ens.test.ts @@ -5,6 +5,7 @@ import { resolveEnsName, } from '../../src'; import { + ADDRESS_ONE, TEST_ENS_NAME, TEST_HTTP_URI, TEST_INVALID_ENS_NAME, @@ -20,7 +21,7 @@ describe('ens', () => { { input: TEST_ENS_NAME, network: 'mainnet', - output: `0x${'0'.repeat(39)}1`, + output: ADDRESS_ONE, }, { input: TEST_HTTP_URI, @@ -48,7 +49,7 @@ describe('ens', () => { { input: TEST_ENS_NAME, network: 'mainnet', - output: `0x${'0'.repeat(39)}1`, + output: ADDRESS_ONE, }, { input: TEST_HTTP_URI, diff --git a/sdk/test/unit/introspection.test.ts b/sdk/test/unit/introspection.test.ts index d2834210..7950794b 100644 --- a/sdk/test/unit/introspection.test.ts +++ b/sdk/test/unit/introspection.test.ts @@ -1,4 +1,10 @@ -import {getInterfaceId} from '../../src'; +import { + InvalidAddressError, + getInterfaceId, + getProtocolVersion, +} from '../../src'; +import {ADDRESS_ONE, TEST_HTTP_URI} from '../constants'; +import {mockContractProtocolVersion, mockJSONRPCProvider} from '../mocks'; import {Interface} from '@ethersproject/abi'; describe('introspection', () => { @@ -13,4 +19,35 @@ describe('introspection', () => { expect(interfaceId).toEqual('0x9bb235aa'); }); }); + + describe('getProtocolVersion', () => { + it('should return the correct protocol version', async () => { + const expectedVersion: [number, number, number] = [1, 3, 0]; + // mock call to the contract + mockJSONRPCProvider(); + // mock the call to the contract + mockContractProtocolVersion(expectedVersion); + const version = await getProtocolVersion(TEST_HTTP_URI, ADDRESS_ONE); + expect(version).toEqual(expectedVersion); + }); + it('should fail when an invalid address is passed', async () => { + const expectedVersion: [number, number, number] = [1, 3, 0]; + // mock call to the contract + mockJSONRPCProvider(); + // mock the call to the contract + mockContractProtocolVersion(expectedVersion); + await expect(() => + getProtocolVersion(TEST_HTTP_URI, '0x') + ).rejects.toThrow(new InvalidAddressError('0x')); + }); + it('should return [1,0,0] when the call throws an error', async () => { + const expectedVersion: [number, number, number] = [1, 0, 0]; + // mock call to the contract + mockJSONRPCProvider(); + // mock the call to the contract + mockContractProtocolVersion(expectedVersion, true); + const version = await getProtocolVersion(TEST_HTTP_URI, ADDRESS_ONE); + expect(version).toEqual(expectedVersion); + }); + }); });