Skip to content

Commit 7c77b81

Browse files
authored
Add the owned safes endpoint; add e2e tests; parse error message (#15)
* Add the owned safes endpoint; add e2e tests; parse error message * Run prettier * Replace reviewdog * Update the error handling and tests * Bump version
1 parent ab53b5b commit 7c77b81

18 files changed

+197
-61
lines changed

.github/workflows/lint.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ jobs:
66
runs-on: ubuntu-latest
77
steps:
88
- uses: actions/checkout@v2
9-
- uses: reviewdog/action-eslint@master
9+
- uses: gnosis/safe-react-eslint-plus-action@main

.github/workflows/test.yml

+5-4
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ jobs:
77
steps:
88
- uses: actions/checkout@v2
99
- run: npm install
10-
- run: npm test
11-
- uses: mattallty/jest-github-action@master
10+
11+
- name: Run tests
12+
uses: mattallty/jest-github-action@v1
1213
env:
1314
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
14-
CI: true
1515
with:
16-
test-command: 'echo done'
16+
test-command: 'npm run test:ci -- --outputFile=/home/runner/work/_actions/mattallty/jest-github-action/v1/dist/jest.results.json'
17+
coverage-comment: false

.prettierrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
"trailingComma": "all",
55
"singleQuote": true,
66
"semi": false
7-
}
7+
}

README.md

+7-7
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ This will create a folder called `openapi` with an OpenAPI JSON and the correspo
1414

1515
## Adding an endpoint
1616

17-
Endpoints are defined in `src/types/gateway.ts` and `src/index.ts. Each endpoint consists of:
17+
Endpoints are defined in `src/types/gateway.ts` and `src/index.ts`. Each endpoint consists of:
1818

19-
* a path definition
20-
* operation definition (params and response types)
21-
* response definition
22-
* a function that fetches the endpoint
19+
- a path definition
20+
- operation definition (params and response types)
21+
- response definition
22+
- a function that fetches the endpoint
2323

2424
To add a new endpoint, follow the pattern set by the existing endpoints.
2525

@@ -33,8 +33,8 @@ yarn eslint:fix
3333

3434
## Tests
3535

36-
To run the tests:
36+
To run the tests locally:
3737

3838
```
39-
jest test --watch
39+
yarn test
4040
```

babel.config.js

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
11
module.exports = {
2-
presets: [
3-
['@babel/preset-env', { targets: { node: 'current' } }],
4-
'@babel/preset-typescript',
5-
],
6-
};
2+
presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'],
3+
}

e2e/config.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const config = {
2+
baseUrl: 'https://safe-client.staging.gnosisdev.com/v1',
3+
}
4+
5+
export default config

e2e/get-owned-safes.test.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { getOwnedSafes } from '../src'
2+
import config from './config'
3+
4+
describe('getOwnedSages tests', () => {
5+
it('should get owned safes on rinkeby', async () => {
6+
const data = await getOwnedSafes(config.baseUrl, '4', '0x661E1CF4aAAf6a95C89EA8c81D120E6c62adDFf9')
7+
8+
expect(data).toEqual({
9+
safes: ['0x9B5dc27B104356516B05b02F6166a54F6D74e40B', '0xb3b83bf204C458B461de9B0CD2739DB152b4fa5A'],
10+
})
11+
})
12+
13+
it('should return an empty array if no owned safes', async () => {
14+
const data = await getOwnedSafes(config.baseUrl, '1', '0x661E1CF4aAAf6a95C89EA8c81D120E6c62adDFf9')
15+
expect(data).toEqual({ safes: [] })
16+
})
17+
18+
it('should throw for bad addresses', async () => {
19+
const req = getOwnedSafes(config.baseUrl, '4', '0x661E1CF4aAAf6a95C89EA8c81D120E6c62adDfF9')
20+
await expect(req).rejects.toThrow('1: Checksum address validation failed')
21+
})
22+
})

e2e/propose-transaction.test.ts

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { proposeTransaction } from '../src'
2+
import config from './config'
3+
4+
describe('proposeTransaction tests', () => {
5+
it('should propose a transaction and fail', async () => {
6+
const req = proposeTransaction(config.baseUrl, '4', '0x4f9BD57BCC68Bf7770429F137922B3afD23d83E7', {
7+
to: '0x49d4450977E2c95362C13D3a31a09311E0Ea26A6',
8+
value: '0',
9+
data: '0xe8dde2320000000000000000000000000000000000000000000000000000000000000000',
10+
operation: 0,
11+
nonce: '1',
12+
safeTxGas: '39557',
13+
baseGas: '0',
14+
gasPrice: '0',
15+
gasToken: '0x0000000000000000000000000000000000000000',
16+
refundReceiver: '0x0000000000000000000000000000000000000000',
17+
safeTxHash: '0x98798b6d9400b25397e85eb79c444a06f93d153555c1d7fd026176f02055a824',
18+
sender: '0x474e5Ded6b5D078163BFB8F6dBa355C3aA5478C8',
19+
origin: null,
20+
})
21+
await expect(req).rejects.toThrow(
22+
'1337: {"nonFieldErrors":["Tx with safe-tx-hash=0x98798b6d9400b25397e85eb79c444a06f93d153555c1d7fd026176f02055a824 for safe=0x4f9BD57BCC68Bf7770429F137922B3afD23d83E7 was already executed in tx-hash=0x1baa941b8696ff3b0a8831f11da243a02f028122b6298ee61e3a1c0d5eeb171a"]}',
23+
)
24+
})
25+
})

e2e/safes-read.test.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { getSafeInfo } from '../src'
2+
import config from './config'
3+
4+
describe('getSafeInfo tests', () => {
5+
it('should get safe info on rinkeby', async () => {
6+
const address = '0x9B5dc27B104356516B05b02F6166a54F6D74e40B'
7+
const data = await getSafeInfo(config.baseUrl, '4', address)
8+
9+
expect(data.address.value).toBe(address)
10+
expect(data.guard).toBe(null)
11+
expect(data.nonce).toBe(3)
12+
expect(data.owners).toEqual([
13+
{
14+
value: '0x21D62C6894741DE97944D7844ED44D7782C66ABC',
15+
},
16+
{
17+
value: '0x661E1CF4aAAf6a95C89EA8c81D120E6c62adDFf9',
18+
},
19+
{
20+
value: '0x8814db983b821D65647C565fBf7c1092fC32437D',
21+
},
22+
{
23+
value: '0x474e5Ded6b5D078163BFB8F6dBa355C3aA5478C8',
24+
},
25+
])
26+
expect(data.threshold).toBe(1)
27+
expect(data.version).toBe('1.1.1')
28+
})
29+
})

jest.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -191,4 +191,4 @@ module.exports = {
191191

192192
// Whether to use watchman for file crawling
193193
// watchman: true,
194-
};
194+
}

package.json

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@gnosis.pm/safe-react-gateway-sdk",
3-
"version": "2.1.0",
3+
"version": "2.2.0",
44
"main": "dist/index.min.js",
55
"types": "dist/index.d.ts",
66
"files": [
@@ -10,12 +10,13 @@
1010
"author": "katspaugh",
1111
"license": "MIT",
1212
"dependencies": {
13-
"unfetch": "^4.2.0"
13+
"isomorphic-unfetch": "^3.1.0"
1414
},
1515
"devDependencies": {
1616
"@babel/core": "^7.15.0",
1717
"@babel/preset-env": "^7.15.0",
1818
"@babel/preset-typescript": "^7.15.0",
19+
"@types/jest": "^27.0.1",
1920
"@typescript-eslint/eslint-plugin": "^4.29.0",
2021
"@typescript-eslint/parser": "^4.29.0",
2122
"babel-jest": "^27.0.6",
@@ -39,8 +40,8 @@
3940
"build": "rm -rf dist && webpack --mode production",
4041
"prepare": "yarn build",
4142
"prettier": "prettier -w",
42-
"test:watch": "jest tests/ --watch",
43-
"test": "jest --ci --coverage --json --outputFile='jest.results.json' ./tests"
43+
"test": "jest --watch",
44+
"test:ci": "jest --ci --coverage --json --watchAll=false --testLocationInResults --runInBand --testPathPattern=tests"
4445
},
4546
"husky": {
4647
"hooks": {

src/index.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ export function getFiatCurrencies(baseUrl: string) {
2727
return callEndpoint(baseUrl, '/balances/supported-fiat-codes')
2828
}
2929

30+
export function getOwnedSafes(baseUrl: string, chainId: string, address: string) {
31+
return callEndpoint(baseUrl, '/chains/{chainId}/owners/{address}/safes', { path: { chainId, address } })
32+
}
33+
3034
export function getCollectibles(
3135
baseUrl: string,
3236
chainId: string,
@@ -60,11 +64,11 @@ export function getTransactionDetails(baseUrl: string, chainId: string, transact
6064
})
6165
}
6266

63-
export function postTransaction(
67+
export function proposeTransaction(
6468
baseUrl: string,
6569
chainId: string,
6670
address: string,
67-
body: operations['post_transaction']['parameters']['body'],
71+
body: operations['propose_transaction']['parameters']['body'],
6872
) {
6973
return callEndpoint(baseUrl, '/chains/{chainId}/transactions/{safe_address}/propose', {
7074
path: { chainId, safe_address: address },

src/types/gateway.ts

+24-2
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,23 @@ export interface paths {
6464
}
6565
'/chains/{chainId}/transactions/{safe_address}/propose': {
6666
/** This is actually supposed to be POST but it breaks our type paradise */
67-
get: operations['post_transaction']
67+
get: operations['propose_transaction']
6868
parameters: {
6969
path: {
7070
chainId: string
7171
safe_address: string
7272
}
7373
}
7474
}
75+
'/chains/{chainId}/owners/{address}/safes': {
76+
get: operations['get_owned_safes']
77+
parameters: {
78+
path: {
79+
chainId: string
80+
address: string
81+
}
82+
}
83+
}
7584
}
7685

7786
type StringValue = {
@@ -264,7 +273,7 @@ export interface operations {
264273
}
265274
}
266275
}
267-
post_transaction: {
276+
propose_transaction: {
268277
parameters: {
269278
path: {
270279
chainId: string
@@ -282,4 +291,17 @@ export interface operations {
282291
422: unknown
283292
}
284293
}
294+
get_owned_safes: {
295+
parameters: {
296+
path: {
297+
chainId: string
298+
address: string
299+
}
300+
}
301+
responses: {
302+
200: {
303+
schema: string[]
304+
}
305+
}
306+
}
285307
}

src/utils.ts

+16-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
import fetch from 'unfetch'
1+
import fetch from 'isomorphic-unfetch'
22

33
export type Params = Record<string, string | number | boolean | null>
44

5+
export type ErrorResponse = {
6+
code: number
7+
message: string
8+
}
9+
510
function replaceParam(str: string, key: string, value: string): string {
611
return str.replace(new RegExp(`\\{${key}\\}`, 'g'), value)
712
}
@@ -46,16 +51,18 @@ export async function fetchData<T>(url: string, body?: unknown): Promise<T> {
4651
}
4752

4853
const resp = await fetch(url, options)
54+
const json = await resp.json()
4955

5056
if (!resp.ok) {
51-
throw Error(resp.statusText)
52-
}
53-
54-
// If the reponse is empty, don't try to parse it
55-
const text = await resp.text()
56-
if (!text) {
57-
return text as unknown as T
57+
let errTxt = ''
58+
try {
59+
const err = json as ErrorResponse
60+
errTxt = `${err.code}: ${err.message}`
61+
} catch (e) {
62+
errTxt = resp.statusText
63+
}
64+
throw new Error(errTxt)
5865
}
5966

60-
return resp.json()
67+
return json
6168
}

tests/utils.test.js

+10-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import fetch from 'unfetch'
1+
import fetch from 'isomorphic-unfetch'
22
import { fetchData, insertParams, stringifyQuery } from '../src/utils'
33

4-
jest.mock('unfetch')
4+
jest.mock('isomorphic-unfetch')
55

66
describe('utils', () => {
77
describe('insertParams', () => {
@@ -69,23 +69,25 @@ describe('utils', () => {
6969
return Promise.resolve({
7070
ok: false,
7171
statusText: 'Failed',
72+
json: () => ({ code: 1337, message: 'something went wrong' }),
7273
})
7374
})
7475

75-
await expect(fetchData('/test/safe?q=123')).rejects.toThrow('Failed')
76+
await expect(fetchData('/test/safe?q=123')).rejects.toThrow('1337: something went wrong')
7677
expect(fetch).toHaveBeenCalledWith('/test/safe?q=123', undefined)
7778
})
7879

79-
it('should fallback to raw text if no JSON in response', async () => {
80+
it('should throw the response text for 50x errors', async () => {
8081
fetch.mockImplementation(() => {
8182
return Promise.resolve({
82-
ok: true,
83-
text: () => Promise.resolve(''),
84-
json: () => Promise.reject('Unexpected end of JSON input'),
83+
ok: false,
84+
statusText: 'Failed',
85+
json: () => null,
8586
})
8687
})
8788

88-
await expect(fetchData('/propose', 123)).resolves.toEqual('')
89+
await expect(fetchData('/test/safe?q=123')).rejects.toThrow('Failed')
90+
expect(fetch).toHaveBeenCalledWith('/test/safe?q=123', undefined)
8991
})
9092
})
9193
})

tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"forceConsistentCasingInFileNames": true,
1515
"moduleResolution": "node",
1616
"resolveJsonModule": true,
17-
"isolatedModules": true,
17+
"isolatedModules": true
1818
},
1919
"include": ["src"]
2020
}

webpack.config.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/* eslint-disable @typescript-eslint/no-var-requires */
2-
const path = require('path');
2+
const path = require('path')
33
const webpack = require('webpack')
4-
const CopyPlugin = require('copy-webpack-plugin');
4+
const CopyPlugin = require('copy-webpack-plugin')
55

6-
const dist = path.join(__dirname, '/dist');
6+
const dist = path.join(__dirname, '/dist')
77

88
module.exports = {
99
entry: './src/index.ts',
@@ -29,6 +29,6 @@ module.exports = {
2929
},
3030
plugins: [
3131
new webpack.EnvironmentPlugin({ REACT_APP_ENV: 'dev' }),
32-
new CopyPlugin({ patterns: [{ from: 'src/types', to: path.join(dist, 'types') }] })
32+
new CopyPlugin({ patterns: [{ from: 'src/types', to: path.join(dist, 'types') }] }),
3333
],
34-
};
34+
}

0 commit comments

Comments
 (0)