-
Notifications
You must be signed in to change notification settings - Fork 219
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(casting): makeHttpClient for explicit net access with cosmjs
- test: query using generated vstorage stubs - captureIO test utility - net access fixture
- Loading branch information
Showing
4 changed files
with
313 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
// @ts-check | ||
|
||
const { freeze } = Object; | ||
|
||
const filterBadStatus = res => { | ||
if (res.status >= 400) { | ||
throw new Error(`Bad status on response: ${res.status}`); | ||
} | ||
return res; | ||
}; | ||
|
||
/** | ||
* Make an RpcClient using explicit access to the network. | ||
* | ||
* The RpcClient implementations included in cosmjs | ||
* such as {@link https://cosmos.github.io/cosmjs/latest/tendermint-rpc/classes/HttpClient.html HttpClient} | ||
* use ambient authority (fetch or axios) for network access. | ||
* | ||
* To facilitate cooperation without vulnerability, | ||
* as well as unit testing, etc. this RpcClient maker takes | ||
* network access as a parameter, following | ||
* {@link https://github.com/Agoric/agoric-sdk/wiki/OCap-Discipline|OCap Discipline}. | ||
* | ||
* @param {string} url | ||
* @param {typeof window.fetch} fetch | ||
* @returns {import('@cosmjs/tendermint-rpc').RpcClient} | ||
*/ | ||
export const makeHttpClient = (url, fetch) => { | ||
const headers = {}; // XXX needed? | ||
|
||
// based on cosmjs 0.30.1: | ||
// https://github.com/cosmos/cosmjs/blob/33271bc51cdc865cadb647a1b7ab55d873637f39/packages/tendermint-rpc/src/rpcclients/http.ts#L37 | ||
// https://github.com/cosmos/cosmjs/blob/33271bc51cdc865cadb647a1b7ab55d873637f39/packages/tendermint-rpc/src/rpcclients/httpclient.ts#L25 | ||
return freeze({ | ||
disconnect: () => { | ||
// nothing to be done | ||
}, | ||
|
||
/** | ||
* @param {import('@cosmjs/json-rpc').JsonRpcRequest} request | ||
*/ | ||
execute: async request => { | ||
const settings = { | ||
method: 'POST', | ||
body: request ? JSON.stringify(request) : undefined, | ||
headers: { | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
'Content-Type': 'application/json', | ||
...headers, | ||
}, | ||
}; | ||
return fetch(url, settings) | ||
.then(filterBadStatus) | ||
.then(res => res.json()); | ||
}, | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
const { stringify: jq } = JSON; | ||
|
||
export const web1 = new Map([ | ||
[ | ||
jq([ | ||
'https://emerynet.rpc.agoric.net/', | ||
{ | ||
method: 'POST', | ||
body: jq({ | ||
id: 1, | ||
method: 'no-such-method', | ||
params: [], | ||
jsonrpc: '2.0', | ||
}), | ||
headers: { 'Content-Type': 'application/json' }, | ||
}, | ||
]), | ||
{ | ||
error: { | ||
code: -32601, | ||
message: 'Method not found', | ||
}, | ||
id: 1, | ||
jsonrpc: '2.0', | ||
}, | ||
], | ||
[ | ||
jq([ | ||
'https://emerynet.rpc.agoric.net/', | ||
{ | ||
method: 'POST', | ||
body: jq({ | ||
jsonrpc: '2.0', | ||
id: 2, | ||
method: 'abci_query', | ||
params: { | ||
path: '/cosmos.bank.v1beta1.Query/Balance', | ||
data: '0a2d61676f726963313430646d6b727a326534326572676a6a37677976656a687a6d6a7a7572767165713832616e67120475697374', | ||
prove: false, | ||
}, | ||
}), | ||
headers: { 'Content-Type': 'application/json' }, | ||
}, | ||
]), | ||
{ | ||
id: 2, | ||
jsonrpc: '2.0', | ||
result: { | ||
response: { | ||
code: 0, | ||
codespace: '', | ||
height: '123985', | ||
index: '0', | ||
info: '', | ||
key: null, | ||
log: '', | ||
proofOps: null, | ||
value: 'ChAKBHVpc3QSCDI1MDUwMDAw', | ||
}, | ||
}, | ||
}, | ||
], | ||
]); | ||
|
||
export const web2 = new Map([ | ||
[ | ||
jq([ | ||
'https://emerynet.rpc.agoric.net/', | ||
{ | ||
method: 'POST', | ||
body: jq({ | ||
jsonrpc: '2.0', | ||
id: 1, | ||
method: 'abci_query', | ||
params: { | ||
path: '/agoric.vstorage.Query/Children', | ||
data: '', | ||
prove: false, | ||
}, | ||
}), | ||
headers: { 'Content-Type': 'application/json' }, | ||
}, | ||
]), | ||
{ | ||
id: 753972443441, | ||
jsonrpc: '2.0', | ||
result: { | ||
response: { | ||
code: 0, | ||
codespace: '', | ||
height: '123985', | ||
index: '0', | ||
info: '', | ||
key: null, | ||
log: '', | ||
proofOps: null, | ||
value: | ||
'CgxhY3Rpdml0eWhhc2gKCmJlYW5zT3dpbmcKBmVncmVzcwoTaGlnaFByaW9yaXR5U2VuZGVycwoJcHVibGlzaGVkCgpzd2luZ1N0b3Jl', | ||
}, | ||
}, | ||
}, | ||
], | ||
]); | ||
|
||
/** | ||
* capture JSON RPC IO traffic | ||
* | ||
* This was used to generate the fixtures above. | ||
* | ||
* @param {typeof window.fetch} fetch | ||
*/ | ||
export const captureIO = fetch => { | ||
const web = new Map(); | ||
/** @type {typeof window.fetch} */ | ||
// @ts-expect-error mock | ||
const f = async (...args) => { | ||
const key = JSON.stringify(args); | ||
const resp = await fetch(...args); | ||
return { | ||
json: async () => { | ||
const data = await resp.json(); | ||
web.set(key, data); | ||
return data; | ||
}, | ||
}; | ||
}; | ||
return { fetch: f, web }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
// @ts-check | ||
/* global globalThis */ | ||
import anyTest from 'ava'; | ||
import { | ||
createProtobufRpcClient, | ||
QueryClient, | ||
setupBankExtension, | ||
} from '@cosmjs/stargate'; | ||
import { Tendermint34Client } from '@cosmjs/tendermint-rpc'; | ||
import { QueryClientImpl } from '@agoric/cosmic-proto/vstorage/query.js'; | ||
|
||
import { makeHttpClient } from '../src/makeHttpClient.js'; | ||
import { captureIO, web1, web2 } from './net-access-fixture.js'; | ||
|
||
/** @type {import('ava').TestFn<Awaited<ReturnType<typeof makeTestContext>>>} */ | ||
const test = /** @type {any} */ (anyTest); | ||
|
||
/** @param {Map<string, any>} web */ | ||
const replayIO = web => { | ||
// tendermint-rpc generates ids using ambient access to Math.random() | ||
// So we normalize them to sequence numbers. | ||
let nextID = 0; | ||
const normalizeID = data => | ||
data.replace(/\\"id\\":\d+/, `\\"id\\":${nextID}`); | ||
|
||
/** @type {typeof window.fetch} */ | ||
// @ts-expect-error mock | ||
const f = async (...args) => { | ||
nextID += 1; | ||
const key = normalizeID(JSON.stringify(args)); | ||
const data = web.get(key); | ||
if (!data) throw Error(`no data for ${key}`); | ||
return { | ||
json: async () => data, | ||
}; | ||
}; | ||
return f; | ||
}; | ||
|
||
const makeTestContext = async () => { | ||
return { fetch: globalThis.fetch }; | ||
}; | ||
|
||
test.before(async t => { | ||
t.context = await makeTestContext(); | ||
}); | ||
|
||
const scenario1 = { | ||
endpoint: 'https://emerynet.rpc.agoric.net/', | ||
request: { | ||
id: 1, | ||
method: 'no-such-method', | ||
params: [], | ||
}, | ||
gov2: { | ||
addr: 'agoric140dmkrz2e42ergjj7gyvejhzmjzurvqeq82ang', | ||
balance: { amount: '25050000', denom: 'uist' }, | ||
}, | ||
}; | ||
|
||
test('interpose net access', async t => { | ||
const fetchMock = replayIO(web1); | ||
const rpcClient = makeHttpClient(scenario1.endpoint, fetchMock); | ||
|
||
t.log('raw JSON RPC'); | ||
const res = await rpcClient.execute({ | ||
...scenario1.request, | ||
jsonrpc: '2.0', | ||
}); | ||
t.like(res, { error: { message: 'Method not found' } }); | ||
|
||
t.log('Cosmos SDK RPC: balance query'); | ||
const tmClient = await Tendermint34Client.create(rpcClient); | ||
const qClient = new QueryClient(tmClient); | ||
const ext = setupBankExtension(qClient); | ||
const actual = await ext.bank.balance( | ||
scenario1.gov2.addr, | ||
scenario1.gov2.balance.denom, | ||
); | ||
|
||
t.deepEqual(actual, scenario1.gov2.balance); | ||
}); | ||
|
||
const scenario2 = { | ||
endpoint: 'https://emerynet.rpc.agoric.net/', | ||
children: [ | ||
'activityhash', | ||
'beansOwing', | ||
'egress', | ||
'highPrioritySenders', | ||
'published', | ||
'swingStore', | ||
], | ||
}; | ||
|
||
test('vstorage query: Children', async t => { | ||
const fetchMock = replayIO(web2); | ||
const rpcClient = makeHttpClient(scenario2.endpoint, fetchMock); | ||
|
||
const tmClient = await Tendermint34Client.create(rpcClient); | ||
const qClient = new QueryClient(tmClient); | ||
const rpc = createProtobufRpcClient(qClient); | ||
const queryService = new QueryClientImpl(rpc); | ||
|
||
const children = await queryService.Children({ path: '' }); | ||
t.deepEqual(children, { | ||
children: scenario2.children, | ||
pagination: undefined, | ||
}); | ||
}); | ||
|
||
// Fixtures for the tests above were captured via integration testing like... | ||
test.skip('vstorage query: Data (capture IO)', async t => { | ||
const { context: io } = t; | ||
const { fetch: fetchMock, web } = captureIO(io.fetch); | ||
const rpcClient = makeHttpClient(scenario2.endpoint, fetchMock); | ||
|
||
const tmClient = await Tendermint34Client.create(rpcClient); | ||
const qClient = new QueryClient(tmClient); | ||
const rpc = createProtobufRpcClient(qClient); | ||
const queryService = new QueryClientImpl(rpc); | ||
|
||
const data = await queryService.Data({ path: '' }); | ||
t.deepEqual(data, { value: '' }); | ||
|
||
t.snapshot(web); | ||
}); |