From a9c20519b31c2f258a37a845487e850f9a8c255a Mon Sep 17 00:00:00 2001 From: Aaron Cook Date: Mon, 20 Dec 2021 14:44:17 +0100 Subject: [PATCH] Add data decoder/master copy endpoint calls (#42) * build: Add get master copies + decoded data Co-authored-by: Aaron Cook * chore: Clean-up types * fix: Remove unnecessary test * fix: Broken tests + bump version * fix: Remove frontend types from response * build: Add e2e tests * fix: Remove lint Co-authored-by: Diogo Soares Co-authored-by: Aaron Cook --- e2e/get-decoded-data.test.ts | 28 +++ e2e/get-master-copy.test.ts | 15 ++ package.json | 2 +- src/endpoint.ts | 25 +-- src/index.ts | 101 +++++----- src/types/api.ts | 349 ----------------------------------- src/types/decoded-data.ts | 24 +++ src/types/master-copies.ts | 9 + src/utils.ts | 12 -- tests/endpoint.test.js | 41 +--- tests/utils.test.js | 14 +- 11 files changed, 149 insertions(+), 471 deletions(-) create mode 100644 e2e/get-decoded-data.test.ts create mode 100644 e2e/get-master-copy.test.ts delete mode 100644 src/types/api.ts create mode 100644 src/types/decoded-data.ts create mode 100644 src/types/master-copies.ts diff --git a/e2e/get-decoded-data.test.ts b/e2e/get-decoded-data.test.ts new file mode 100644 index 00000000..71a48fb4 --- /dev/null +++ b/e2e/get-decoded-data.test.ts @@ -0,0 +1,28 @@ +import { getDecodedData } from '../src' +import config from './config' + +describe('getDecodedData tests', () => { + it('should post getDecodedData', async () => { + const result = await getDecodedData( + config.baseUrl, + '4', + '0x095ea7b3000000000000000000000000ae9844f89d98c150f5e61bfc676d68b4921559900000000000000000000000000000000000000000000000000001c6bf52634000', + ) + + expect(result).toStrictEqual({ + method: 'approve', + parameters: [ + { + name: 'spender', + type: 'address', + value: '0xae9844F89D98c150F5e61bfC676D68b492155990', + }, + { + name: 'value', + type: 'uint256', + value: '500000000000000', + }, + ], + }) + }) +}) diff --git a/e2e/get-master-copy.test.ts b/e2e/get-master-copy.test.ts new file mode 100644 index 00000000..2df80fe8 --- /dev/null +++ b/e2e/get-master-copy.test.ts @@ -0,0 +1,15 @@ +import { getMasterCopies } from '../src' +import config from './config' + +describe('get mastercopy tests', () => { + it('should get master copy response', async () => { + const masterCopies = await getMasterCopies(config.baseUrl, '4') + + expect(Array.isArray(masterCopies)).toBe(true) + + // version 1.1.1 should be present + const lastVersionRinkeby = masterCopies.find((mastercopy) => mastercopy.version === '1.1.1') + expect(lastVersionRinkeby).toBeDefined() + expect(lastVersionRinkeby.address).toBe('0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F') + }) +}) diff --git a/package.json b/package.json index 5c363f54..7d5a489b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gnosis.pm/safe-react-gateway-sdk", - "version": "2.6.0", + "version": "2.7.0", "main": "dist/index.min.js", "types": "dist/index.d.ts", "files": [ diff --git a/src/endpoint.ts b/src/endpoint.ts index bde752ac..e9e246d5 100644 --- a/src/endpoint.ts +++ b/src/endpoint.ts @@ -1,29 +1,18 @@ -import { fetchData, insertParams, stringifyQuery } from './utils' -import { paths } from './types/api' +import { fetchData, stringifyQuery } from './utils' type Primitive = string | number | boolean | null interface Params { - path?: { [key: string]: Primitive } query?: { [key: string]: Primitive } body?: unknown } -export function callEndpoint( - baseUrl: string, - path: T, - parameters?: paths[T]['get']['parameters'], - rawUrl?: string, -): Promise { - let url = rawUrl - let body - if (!url) { - const params = parameters as Params - const pathname = insertParams(path, params?.path) - const search = stringifyQuery(params?.query) - url = `${baseUrl}${pathname}${search}` - body = params?.body +export function callEndpoint(url: string, params?: Params, rawUrl?: string): Promise { + if (rawUrl) { + return fetchData(rawUrl) } - return fetchData(url, body) + const search = stringifyQuery(params?.query) + const endpoint = `${url}${search}` + return fetchData(endpoint, params?.body) } diff --git a/src/index.ts b/src/index.ts index b7cdf1ac..7b6ee0bc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,16 @@ import { callEndpoint } from './endpoint' -import { operations } from './types/api' -import { SafeTransactionEstimation, TransactionDetails, TransactionListPage } from './types/transactions' +import { + MultisigTransactionRequest, + SafeTransactionEstimation, + SafeTransactionEstimationRequest, + TransactionDetails, + TransactionListPage, +} from './types/transactions' import { FiatCurrencies, OwnedSafes, SafeBalanceResponse, SafeCollectibleResponse, SafeInfo } from './types/common' +import { MasterCopyReponse } from './types/master-copies' import { ChainListResponse, ChainInfo } from './types/chains' import { SafeAppsResponse } from './types/safe-apps' +import { DecodedDataResponse } from './types/decoded-data' export * from './types/safe-apps' export * from './types/transactions' export * from './types/chains' @@ -15,7 +22,7 @@ export * from './types/common' * Get basic information about a Safe. E.g. owners, modules, version etc */ export function getSafeInfo(baseUrl: string, chainId: string, address: string): Promise { - return callEndpoint(baseUrl, '/chains/{chainId}/safes/{address}/', { path: { chainId, address } }) + return callEndpoint(`${baseUrl}/chains/${chainId}/safes/${address}/`) } /** @@ -26,10 +33,12 @@ export function getBalances( chainId: string, address: string, currency = 'usd', - query: operations['safes_balances_list']['parameters']['query'] = {}, + query: { + trusted?: boolean // Return trusted tokens + exclude_spam?: boolean // Return spam tokens + } = {}, ): Promise { - return callEndpoint(baseUrl, '/chains/{chainId}/safes/{address}/balances/{currency}/', { - path: { chainId, address, currency }, + return callEndpoint(`${baseUrl}/chains/${chainId}/safes/${address}/balances/${currency}/`, { query, }) } @@ -38,14 +47,14 @@ export function getBalances( * Get a list of supported fiat currencies (e.g. USD, EUR etc) */ export function getFiatCurrencies(baseUrl: string): Promise { - return callEndpoint(baseUrl, '/balances/supported-fiat-codes') + return callEndpoint(`${baseUrl}/balances/supported-fiat-codes`) } /** * Get the addresses of all Safes belonging to an owner */ export function getOwnedSafes(baseUrl: string, chainId: string, address: string): Promise { - return callEndpoint(baseUrl, '/chains/{chainId}/owners/{address}/safes', { path: { chainId, address } }) + return callEndpoint(`${baseUrl}/chains/${chainId}/owners/${address}/safes`) } /** @@ -55,9 +64,12 @@ export function getCollectibles( baseUrl: string, chainId: string, address: string, - query: operations['safes_collectibles_list']['parameters']['query'] = {}, + query: { + trusted?: boolean // Return trusted tokens + exclude_spam?: boolean // Return spam tokens + } = {}, ): Promise { - return callEndpoint(baseUrl, '/chains/{chainId}/safes/{address}/collectibles/', { path: { chainId, address }, query }) + return callEndpoint(`${baseUrl}/chains/${chainId}/safes/${address}/collectibles/`, { query }) } /** @@ -66,15 +78,10 @@ export function getCollectibles( export function getTransactionHistory( baseUrl: string, chainId: string, - address: string, + safeAddress: string, pageUrl?: string, ): Promise { - return callEndpoint( - baseUrl, - '/chains/{chainId}/safes/{safe_address}/transactions/history', - { path: { chainId, safe_address: address }, query: {} }, - pageUrl, - ) + return callEndpoint(`${baseUrl}/chains/${chainId}/safes/${safeAddress}/transactions/history`, undefined, pageUrl) } /** @@ -83,15 +90,10 @@ export function getTransactionHistory( export function getTransactionQueue( baseUrl: string, chainId: string, - address: string, + safeAddress: string, pageUrl?: string, ): Promise { - return callEndpoint( - baseUrl, - '/chains/{chainId}/safes/{safe_address}/transactions/queued', - { path: { chainId, safe_address: address }, query: {} }, - pageUrl, - ) + return callEndpoint(`${baseUrl}/chains/${chainId}/safes/${safeAddress}/transactions/queued`, undefined, pageUrl) } /** @@ -102,9 +104,7 @@ export function getTransactionDetails( chainId: string, transactionId: string, ): Promise { - return callEndpoint(baseUrl, '/chains/{chainId}/transactions/{transactionId}', { - path: { chainId, transactionId }, - }) + return callEndpoint(`${baseUrl}/chains/${chainId}/transactions/${transactionId}`) } /** @@ -113,11 +113,10 @@ export function getTransactionDetails( export function postSafeGasEstimation( baseUrl: string, chainId: string, - address: string, - body: operations['post_safe_gas_estimation']['parameters']['body'], + safeAddress: string, + body: SafeTransactionEstimationRequest, ): Promise { - return callEndpoint(baseUrl, '/chains/{chainId}/safes/{safe_address}/multisig-transactions/estimations', { - path: { chainId, safe_address: address }, + return callEndpoint(`${baseUrl}/chains/${chainId}/safes/${safeAddress}/multisig-transactions/estimations`, { body, }) } @@ -128,11 +127,10 @@ export function postSafeGasEstimation( export function proposeTransaction( baseUrl: string, chainId: string, - address: string, - body: operations['propose_transaction']['parameters']['body'], + safeAddress: string, + body: MultisigTransactionRequest, ): Promise { - return callEndpoint(baseUrl, '/chains/{chainId}/transactions/{safe_address}/propose', { - path: { chainId, safe_address: address }, + return callEndpoint(`${baseUrl}/chains/${chainId}/transactions/${safeAddress}/propose`, { body, }) } @@ -142,9 +140,13 @@ export function proposeTransaction( */ export function getChainsConfig( baseUrl: string, - query?: operations['chains_list']['parameters']['query'], + query?: { + ordering?: string + limit?: number + offset?: number + }, ): Promise { - return callEndpoint(baseUrl, '/chains/', { + return callEndpoint(`${baseUrl}/chains/`, { query, }) } @@ -153,18 +155,31 @@ export function getChainsConfig( * Returns a chain config */ export function getChainConfig(baseUrl: string, chainId: string): Promise { - return callEndpoint(baseUrl, '/chains/{chainId}/', { - path: { chainId: chainId }, - }) + return callEndpoint(`${baseUrl}/chains/${chainId}/`) } /** * Returns Safe Apps List */ export function getSafeApps(baseUrl: string, chainId: string): Promise { - return callEndpoint(baseUrl, '/chains/{chainId}/safe-apps', { - path: { chainId: chainId }, - }) + return callEndpoint(`${baseUrl}/chains/${chainId}/safe-apps`) } +/** + * Returns List of Master Copies + */ +export function getMasterCopies(baseUrl: string, chainId: string): Promise { + return callEndpoint(`${baseUrl}/chains/${chainId}/about/master-copies`) +} + +/** + * Returns decoded data + */ +export function getDecodedData(baseUrl: string, chainId: string, encodedData: string): Promise { + return callEndpoint(`${baseUrl}/chains/${chainId}/data-decoder`, { + body: { + data: encodedData, + }, + }) +} /* eslint-enable @typescript-eslint/explicit-module-boundary-types */ diff --git a/src/types/api.ts b/src/types/api.ts deleted file mode 100644 index 752f5524..00000000 --- a/src/types/api.ts +++ /dev/null @@ -1,349 +0,0 @@ -import { FiatCurrencies, OwnedSafes, SafeBalanceResponse, SafeCollectibleResponse, SafeInfo } from './common' -import { - MultisigTransactionRequest, - TransactionDetails, - SafeTransactionEstimation, - SafeTransactionEstimationRequest, - TransactionListPage, -} from './transactions' -import { ChainListResponse, ChainInfo } from './chains' -import { SafeAppsResponse } from './safe-apps' - -export interface paths { - '/chains/{chainId}/safes/{address}/': { - /** Get status of the safe */ - get: operations['safes_read'] - parameters: { - path: { - chainId: string - address: string - } - } - } - '/chains/{chainId}/safes/{address}/balances/{currency}/': { - get: operations['safes_balances_list'] - parameters: { - path: { - chainId: string - address: string - currency: string - } - } - } - '/balances/supported-fiat-codes': { - get: operations['get_supported_fiat'] - parameters: null - } - '/chains/{chainId}/safes/{address}/collectibles/': { - /** Get collectibles (ERC721 tokens) and information about them */ - get: operations['safes_collectibles_list'] - parameters: { - path: { - chainId: string - address: string - } - } - } - '/chains/{chainId}/safes/{safe_address}/transactions/history': { - get: operations['history_transactions'] - parameters: { - path: { - chainId: string - safe_address: string - } - } - } - '/chains/{chainId}/safes/{safe_address}/transactions/queued': { - get: operations['queued_transactions'] - parameters: { - path: { - chainId: string - safe_address: string - } - } - } - '/chains/{chainId}/transactions/{transactionId}': { - get: operations['get_transactions'] - parameters: { - path: { - chainId: string - transactionId: string - } - } - } - '/chains/{chainId}/safes/{safe_address}/multisig-transactions/estimations': { - /** This is actually supposed to be POST but it breaks our type paradise */ - get: operations['post_safe_gas_estimation'] - parameters: { - path: { - chainId: string - safe_address: string - } - } - } - '/chains/{chainId}/transactions/{safe_address}/propose': { - /** This is actually supposed to be POST but it breaks our type paradise */ - get: operations['propose_transaction'] - parameters: { - path: { - chainId: string - safe_address: string - } - } - } - '/chains/{chainId}/owners/{address}/safes': { - get: operations['get_owned_safes'] - parameters: { - path: { - chainId: string - address: string - } - } - } - '/chains/': { - get: operations['chains_list'] - parameters: { - query: { - ordering?: string - limit?: number - offset?: number - } - } - } - '/chains/{chainId}/': { - get: operations['chains_read'] - parameters: { - path: { - chainId: string - } - } - } - '/chains/{chainId}/safe-apps': { - get: operations['safe_apps_read'] - parameters: { - path: { - chainId: string - } - } - } -} - -export interface operations { - /** Get status of the safe */ - safes_read: { - parameters: { - path: { - chainId: string - address: string - } - } - responses: { - 200: { - schema: SafeInfo - } - /** Safe not found */ - 404: unknown - /** - * code = 1: Checksum address validation failed - * code = 50: Cannot get Safe info - */ - 422: unknown - } - } - /** Get balance for Ether and ERC20 tokens with USD fiat conversion */ - safes_balances_list: { - parameters: { - path: { - chainId: string - address: string - currency: string - } - query: { - /** If `True` just trusted tokens will be returned */ - trusted?: boolean - /** If `True` spam tokens will not be returned */ - exclude_spam?: boolean - } - } - responses: { - 200: { - schema: SafeBalanceResponse - } - /** Safe not found */ - 404: unknown - /** Safe address checksum not valid */ - 422: unknown - } - } - get_supported_fiat: { - parameters: null - responses: { - 200: { - schema: FiatCurrencies - } - } - } - /** Get collectibles (ERC721 tokens) and information about them */ - safes_collectibles_list: { - parameters: { - path: { - chainId: string - address: string - } - query: { - /** If `True` just trusted tokens will be returned */ - trusted?: boolean - /** If `True` spam tokens will not be returned */ - exclude_spam?: boolean - } - } - responses: { - 200: { - schema: SafeCollectibleResponse[] - } - /** Safe not found */ - 404: unknown - /** Safe address checksum not valid */ - 422: unknown - } - } - history_transactions: { - parameters: { - path: { - chainId: string - safe_address: string - } - query: { - /** Taken from the Page['next'] or Page['previous'] */ - page_url?: string - } - } - responses: { - 200: { - schema: TransactionListPage - } - } - } - queued_transactions: { - parameters: { - path: { - chainId: string - safe_address: string - } - query: { - /** Taken from the Page['next'] or Page['previous'] */ - page_url?: string - } - } - responses: { - 200: { - schema: TransactionListPage - } - } - } - get_transactions: { - parameters: { - path: { - chainId: string - transactionId: string - } - } - responses: { - 200: { - schema: TransactionDetails - } - } - } - post_safe_gas_estimation: { - parameters: { - path: { - chainId: string - safe_address: string - } - body: SafeTransactionEstimationRequest - } - responses: { - 200: { - schema: SafeTransactionEstimation - } - /** Safe not found */ - 404: unknown - /** Safe address checksum not valid */ - 422: unknown - } - } - propose_transaction: { - parameters: { - path: { - chainId: string - safe_address: string - } - body: MultisigTransactionRequest - } - responses: { - 200: { - schema: TransactionDetails - } - /** Safe not found */ - 404: unknown - /** Safe address checksum not valid */ - 422: unknown - } - } - get_owned_safes: { - parameters: { - path: { - chainId: string - address: string - } - } - responses: { - 200: { - schema: OwnedSafes - } - } - } - chains_list: { - parameters: { - query?: { - /** Which field to use when ordering the results. */ - ordering?: string - /** Number of results to return per page. */ - limit?: number - /** The initial index from which to return the results. */ - offset?: number - } - } - responses: { - 200: { - schema: ChainListResponse - } - } - } - chains_read: { - parameters: { - path: { - /** A unique value identifying this chain. */ - chainId: string - } - } - responses: { - 200: { - schema: ChainInfo - } - } - } - safe_apps_read: { - parameters: { - path: { - /** A unique value identifying this chain. */ - chainId: string - } - } - responses: { - 200: { - schema: SafeAppsResponse - } - } - } -} diff --git a/src/types/decoded-data.ts b/src/types/decoded-data.ts new file mode 100644 index 00000000..047c58e1 --- /dev/null +++ b/src/types/decoded-data.ts @@ -0,0 +1,24 @@ +export type DecodedDataBasicParameter = { + name: string + type: string + value: string +} +export type DecodedDataParameterValue = { + operation: 0 | 1 + to: string + value: string + data: string + dataDecoded: { + method: string + parameters: DecodedDataBasicParameter[] + } | null +} + +export type DecodedDataParameter = { + valueDecoded?: DecodedDataParameterValue[] +} & DecodedDataBasicParameter + +export type DecodedDataResponse = { + method: string + parameters: DecodedDataParameter[] +} diff --git a/src/types/master-copies.ts b/src/types/master-copies.ts new file mode 100644 index 00000000..f06abf90 --- /dev/null +++ b/src/types/master-copies.ts @@ -0,0 +1,9 @@ +export enum MasterCopyDeployer { + GNOSIS = 'Gnosis', + CIRCLES = 'Circles', +} + +export type MasterCopyReponse = { + address: string + version: string +}[] diff --git a/src/utils.ts b/src/utils.ts index ded76c3d..c2b9fcc5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -7,18 +7,6 @@ export type ErrorResponse = { message: string } -function replaceParam(str: string, key: string, value: string): string { - return str.replace(new RegExp(`\\{${key}\\}`, 'g'), value) -} - -export function insertParams(template: string, params?: Params): string { - return params - ? Object.keys(params).reduce((result: string, key) => { - return replaceParam(result, key, String(params[key])) - }, template) - : template -} - export function stringifyQuery(query?: Params): string { if (!query) { return '' diff --git a/tests/endpoint.test.js b/tests/endpoint.test.js index 9c2b69c7..2fb52619 100644 --- a/tests/endpoint.test.js +++ b/tests/endpoint.test.js @@ -14,7 +14,7 @@ jest.mock('../src/utils', () => { describe('callEndpoint', () => { it('should accept just a path', async () => { await expect( - callEndpoint('https://safe-client.staging.gnosisdev.com/v1', '/balances/supported-fiat-codes'), + callEndpoint('https://safe-client.staging.gnosisdev.com/v1/balances/supported-fiat-codes'), ).resolves.toEqual({ success: true }) expect(fetchData).toHaveBeenCalledWith( @@ -23,36 +23,9 @@ describe('callEndpoint', () => { ) }) - it('should accept a path param', async () => { - await expect( - callEndpoint('https://safe-client.staging.gnosisdev.com/v1', '/chains/4/safe/{address}', { - path: { address: '0x123' }, - }), - ).resolves.toEqual({ success: true }) - - expect(fetchData).toHaveBeenCalledWith( - 'https://safe-client.staging.gnosisdev.com/v1/chains/4/safe/0x123', - undefined, - ) - }) - - it('should accept several path params', async () => { - await expect( - callEndpoint('https://safe-client.staging.gnosisdev.com/v1', '/chains/4/balances/{address}/{currency}', { - path: { address: '0x123', currency: 'usd' }, - }), - ).resolves.toEqual({ success: true }) - - expect(fetchData).toHaveBeenCalledWith( - 'https://safe-client.staging.gnosisdev.com/v1/chains/4/balances/0x123/usd', - undefined, - ) - }) - it('should accept query params', async () => { await expect( - callEndpoint('https://safe-client.staging.gnosisdev.com/v1', '/chains/4/balances/{address}/{currency}', { - path: { address: '0x123', currency: 'usd' }, + callEndpoint('https://safe-client.staging.gnosisdev.com/v1/chains/4/balances/0x123/usd', { query: { exclude_spam: true }, }), ).resolves.toEqual({ success: true }) @@ -65,8 +38,7 @@ describe('callEndpoint', () => { it('should accept body', async () => { await expect( - callEndpoint('https://safe-client.staging.gnosisdev.com/v1', '/chains/4/transactions/{safe_address}/propose', { - path: { safe_address: '0x123' }, + callEndpoint('https://safe-client.staging.gnosisdev.com/v1/chains/4/transactions/0x123/propose', { body: { test: 'test' }, }), ).resolves.toEqual({ success: true }) @@ -80,13 +52,12 @@ describe('callEndpoint', () => { it('should accept a raw URL', async () => { await expect( callEndpoint( - 'https://safe-client.staging.gnosisdev.com/v1', - '/chains/4/balances/{address}/{currency}', - { path: { address: '0x123', currency: 'usd' }, query: { exclude_spam: true } }, + 'https://safe-client.staging.gnosisdev.com/v1/chains/4/balances/0x123/usd', + { query: { exclude_spam: true } }, '/test-url?raw=true', ), ).resolves.toEqual({ success: true }) - expect(fetchData).toHaveBeenCalledWith('/test-url?raw=true', undefined) + expect(fetchData).toHaveBeenCalledWith('/test-url?raw=true') }) }) diff --git a/tests/utils.test.js b/tests/utils.test.js index 7fdc5ca4..f4d76cce 100644 --- a/tests/utils.test.js +++ b/tests/utils.test.js @@ -1,21 +1,9 @@ import fetch from 'isomorphic-unfetch' -import { fetchData, insertParams, stringifyQuery } from '../src/utils' +import { fetchData, stringifyQuery } from '../src/utils' jest.mock('isomorphic-unfetch') describe('utils', () => { - describe('insertParams', () => { - it('should insert a param into a string', () => { - expect(insertParams('/{network}/safe/{address}', { address: '0x0' })).toBe('/{network}/safe/0x0') - }) - - it('should insert several params into a string', () => { - expect(insertParams('/{network}/safe/{address}', { address: '0x0', network: 'rinkeby' })).toBe( - '/rinkeby/safe/0x0', - ) - }) - }) - describe('stringifyQuery', () => { it('should stringify query params', () => { expect(stringifyQuery({ spam: true, page: 11, name: 'token', exclude: null })).toBe(