-
Notifications
You must be signed in to change notification settings - Fork 215
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #7935 from Agoric/dc-cosmjs-interpose-net
feat(casting): makeHttpClient for explicit net access with cosmjs
- Loading branch information
Showing
5 changed files
with
353 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,196 @@ | ||
const { stringify: jq } = JSON; | ||
|
||
/** | ||
* @file to regenerate | ||
* 1. set RECORDING=true in test-interpose-net-access.js | ||
* 2. run: yarn test test/test-test-interpose-net-access.js --update-snapshots | ||
* 3. for each map in test-test-interpose-net-access.js.md, copy it and | ||
* 4. replace all occurences of => with : and paste as args to Object.fromEntries() | ||
* 5. change RECORDING back to false | ||
*/ | ||
export const web1 = new Map([ | ||
[ | ||
jq([ | ||
'https://emerynet.rpc.agoric.net/', | ||
{ | ||
method: 'POST', | ||
body: jq({ | ||
id: 1208387614, | ||
method: 'no-such-method', | ||
params: [], | ||
jsonrpc: '2.0', | ||
}), | ||
headers: { 'Content-Type': 'application/json' }, | ||
}, | ||
]), | ||
{ | ||
error: { | ||
code: -32601, | ||
message: 'Method not found', | ||
}, | ||
id: 1208387614, | ||
jsonrpc: '2.0', | ||
}, | ||
], | ||
[ | ||
jq([ | ||
'https://emerynet.rpc.agoric.net/', | ||
{ | ||
method: 'POST', | ||
body: jq({ | ||
jsonrpc: '2.0', | ||
id: 797030719, | ||
method: 'abci_query', | ||
params: { | ||
path: '/cosmos.bank.v1beta1.Query/Balance', | ||
data: '0a2d61676f726963313430646d6b727a326534326572676a6a37677976656a687a6d6a7a7572767165713832616e67120475697374', | ||
prove: false, | ||
}, | ||
}), | ||
headers: { 'Content-Type': 'application/json' }, | ||
}, | ||
]), | ||
{ | ||
id: 797030719, | ||
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: 1757612624, | ||
method: 'abci_query', | ||
params: { | ||
path: '/agoric.vstorage.Query/Children', | ||
data: '', | ||
prove: false, | ||
}, | ||
}), | ||
headers: { 'Content-Type': 'application/json' }, | ||
}, | ||
]), | ||
{ | ||
id: 1757612624, | ||
jsonrpc: '2.0', | ||
result: { | ||
response: { | ||
code: 0, | ||
codespace: '', | ||
height: '123985', | ||
index: '0', | ||
info: '', | ||
key: null, | ||
log: '', | ||
proofOps: null, | ||
value: | ||
'CgxhY3Rpdml0eWhhc2gKCmJlYW5zT3dpbmcKBmVncmVzcwoTaGlnaFByaW9yaXR5U2VuZGVycwoJcHVibGlzaGVkCgpzd2luZ1N0b3Jl', | ||
}, | ||
}, | ||
}, | ||
], | ||
]); | ||
|
||
/** | ||
* @param {string} str | ||
* ack: https://stackoverflow.com/a/7616484 | ||
*/ | ||
const hashCode = str => { | ||
let hash = 0; | ||
let i; | ||
let chr; | ||
if (str.length === 0) return hash; | ||
for (i = 0; i < str.length; i += 1) { | ||
chr = str.charCodeAt(i); | ||
// eslint-disable-next-line no-bitwise | ||
hash = (hash << 5) - hash + chr; | ||
// eslint-disable-next-line no-bitwise | ||
hash |= 0; // Convert to 32bit integer | ||
} | ||
return hash; | ||
}; | ||
|
||
/** | ||
* Normalize JSON RPC request ID | ||
* | ||
* tendermint-rpc generates ids using ambient access to Math.random() | ||
* So we normalize them to a hash of the rest of the JSON. | ||
* | ||
* Earlier, we tried a sequence number, but it was non-deterministic | ||
* with multiple interleaved requests. | ||
* | ||
* @param {string} argsKey | ||
*/ | ||
const normalizeID = argsKey => { | ||
// arbitrary string unlikely to occur in a request. from `pwgen 16 -1` | ||
const placeholder = 'Ajaz1chei7ohnguv'; | ||
|
||
const noid = argsKey.replace(/\\"id\\":\d+/, `\\"id\\":${placeholder}`); | ||
const id = Math.abs(hashCode(noid)); | ||
return noid.replace(placeholder, `${id}`); | ||
}; | ||
|
||
/** | ||
* Wrap `fetch` to capture JSON RPC IO traffic. | ||
* | ||
* @param {typeof window.fetch} fetch | ||
* returns wraped fetch along with a .web map for use with {@link replayIO} | ||
*/ | ||
export const captureIO = fetch => { | ||
const web = new Map(); | ||
/** @type {typeof window.fetch} */ | ||
// @ts-expect-error mock | ||
const f = async (...args) => { | ||
const key = normalizeID(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 }; | ||
}; | ||
|
||
/** | ||
* Replay captured JSON RPC IO. | ||
* | ||
* @param {Map<string, any>} web map from | ||
* JSON-stringified fetch args to fetched JSON data. | ||
*/ | ||
export const replayIO = web => { | ||
/** @type {typeof window.fetch} */ | ||
// @ts-expect-error mock | ||
const f = async (...args) => { | ||
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; | ||
}; |
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,97 @@ | ||
// @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, replayIO, web1, web2 } from './net-access-fixture.js'; | ||
|
||
/** @type {import('ava').TestFn<Awaited<ReturnType<typeof makeTestContext>>>} */ | ||
const test = /** @type {any} */ (anyTest); | ||
|
||
const RECORDING = false; | ||
|
||
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 (RECORDING: ${RECORDING})`, async t => { | ||
const { context: io } = t; | ||
|
||
const { fetch: fetchMock, web } = io.recording | ||
? captureIO(io.fetch) | ||
: { fetch: replayIO(web2), web: new Map() }; | ||
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: '' }); | ||
if (io.recording) { | ||
t.snapshot(web); | ||
} | ||
t.deepEqual(children, { | ||
children: scenario2.children, | ||
pagination: undefined, | ||
}); | ||
}); |
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,2 @@ | ||
/** @file for backwards compatibility */ | ||
export * from '../dist/agoric/vstorage/query.js'; |