Skip to content

Commit 3bdc8af

Browse files
committed
common: fix queryContractSmart parsing
1 parent 697f0ef commit 3bdc8af

File tree

1 file changed

+204
-5
lines changed

1 file changed

+204
-5
lines changed

common/src/queryContractSmart.ts

+204-5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,195 @@ export function uint32ToString(val: number): string {
3232
return Buffer.from(buf).toString('hex');
3333
}
3434

35+
// https://github.com/confio/cosmjs-types/blob/66e52711914fccd2a9d1a03e392d3628fdf499e2/src/binary.ts#L60-L68
36+
enum WireType {
37+
Varint = 0,
38+
39+
Fixed64 = 1,
40+
41+
Bytes = 2,
42+
43+
Fixed32 = 5,
44+
}
45+
46+
// https://github.com/confio/cosmjs-types/blob/66e52711914fccd2a9d1a03e392d3628fdf499e2/src/binary.ts#L496-L498
47+
function indexOutOfRange(reader: BinaryReader, writeLength?: number) {
48+
return RangeError(
49+
'index out of range: ' + reader.pos + ' + ' + (writeLength || 1) + ' > ' + reader.len
50+
);
51+
}
52+
53+
// adapted from https://github.com/confio/cosmjs-types/blob/66e52711914fccd2a9d1a03e392d3628fdf499e2/src/binary.ts#L96
54+
export class BinaryReader {
55+
buf: Uint8Array;
56+
pos: number;
57+
type: number;
58+
len: number;
59+
60+
assertBounds(): void {
61+
if (this.pos > this.len) throw new RangeError('premature EOF');
62+
}
63+
64+
constructor(buf: Uint8Array) {
65+
this.buf = buf;
66+
this.pos = 0;
67+
this.type = 0;
68+
this.len = this.buf.length;
69+
}
70+
71+
/**
72+
* Read an unsigned 32 bit varint.
73+
*
74+
* See https://github.com/protocolbuffers/protobuf/blob/8a71927d74a4ce34efe2d8769fda198f52d20d12/js/experimental/runtime/kernel/buffer_decoder.js#L220
75+
*/
76+
varint32read(): number {
77+
let b = this.buf[this.pos++];
78+
let result = b & 0x7f;
79+
if ((b & 0x80) == 0) {
80+
this.assertBounds();
81+
return result;
82+
}
83+
84+
b = this.buf[this.pos++];
85+
result |= (b & 0x7f) << 7;
86+
if ((b & 0x80) == 0) {
87+
this.assertBounds();
88+
return result;
89+
}
90+
91+
b = this.buf[this.pos++];
92+
result |= (b & 0x7f) << 14;
93+
if ((b & 0x80) == 0) {
94+
this.assertBounds();
95+
return result;
96+
}
97+
98+
b = this.buf[this.pos++];
99+
result |= (b & 0x7f) << 21;
100+
if ((b & 0x80) == 0) {
101+
this.assertBounds();
102+
return result;
103+
}
104+
105+
// Extract only last 4 bits
106+
b = this.buf[this.pos++];
107+
result |= (b & 0x0f) << 28;
108+
109+
for (let readBytes = 5; (b & 0x80) !== 0 && readBytes < 10; readBytes++)
110+
b = this.buf[this.pos++];
111+
112+
if ((b & 0x80) != 0) throw new Error('invalid varint');
113+
114+
this.assertBounds();
115+
116+
// Result can have 32 bits, convert it to unsigned
117+
return result >>> 0;
118+
}
119+
120+
skip(length?: number) {
121+
if (typeof length === 'number') {
122+
if (this.pos + length > this.len) throw indexOutOfRange(this, length);
123+
this.pos += length;
124+
} else {
125+
do {
126+
if (this.pos >= this.len) throw indexOutOfRange(this);
127+
} while (this.buf[this.pos++] & 128);
128+
}
129+
return this;
130+
}
131+
132+
skipType(wireType: number) {
133+
switch (wireType) {
134+
case WireType.Varint:
135+
this.skip();
136+
break;
137+
case WireType.Fixed64:
138+
this.skip(8);
139+
break;
140+
case WireType.Bytes:
141+
this.skip(this.uint32());
142+
break;
143+
case 3:
144+
while ((wireType = this.uint32() & 7) !== 4) {
145+
this.skipType(wireType);
146+
}
147+
break;
148+
case WireType.Fixed32:
149+
this.skip(4);
150+
break;
151+
152+
/* istanbul ignore next */
153+
default:
154+
throw Error('invalid wire type ' + wireType + ' at offset ' + this.pos);
155+
}
156+
return this;
157+
}
158+
159+
uint32(): number {
160+
return this.varint32read();
161+
}
162+
163+
bytes(): Uint8Array {
164+
const len = this.uint32(),
165+
start = this.pos;
166+
this.pos += len;
167+
this.assertBounds();
168+
return this.buf.subarray(start, start + len);
169+
}
170+
}
171+
172+
// https://github.com/confio/cosmjs-types/blob/66e52711914fccd2a9d1a03e392d3628fdf499e2/src/cosmwasm/wasm/v1/query.ts#L111-L118
173+
/**
174+
* QuerySmartContractStateResponse is the response type for the
175+
* Query/SmartContractState RPC method
176+
*/
177+
export interface QuerySmartContractStateResponse {
178+
/** Data contains the json data returned from the smart contract */
179+
data: Uint8Array;
180+
}
181+
182+
// https://github.com/confio/cosmjs-types/blob/66e52711914fccd2a9d1a03e392d3628fdf499e2/src/cosmwasm/wasm/v1/query.ts#L855-L859
183+
function createBaseQuerySmartContractStateResponse(): QuerySmartContractStateResponse {
184+
return {
185+
data: new Uint8Array(),
186+
};
187+
}
188+
189+
// https://github.com/confio/cosmjs-types/blob/66e52711914fccd2a9d1a03e392d3628fdf499e2/src/cosmwasm/wasm/v1/query.ts#L871-L887
190+
const QuerySmartContractStateResponse = {
191+
decode(input: BinaryReader | Uint8Array, length?: number): QuerySmartContractStateResponse {
192+
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
193+
let end = length === undefined ? reader.len : reader.pos + length;
194+
const message = createBaseQuerySmartContractStateResponse();
195+
while (reader.pos < end) {
196+
const tag = reader.uint32();
197+
switch (tag >>> 3) {
198+
case 1:
199+
message.data = reader.bytes();
200+
break;
201+
default:
202+
reader.skipType(tag & 7);
203+
break;
204+
}
205+
}
206+
return message;
207+
},
208+
};
209+
210+
// https://github.com/cosmos/cosmjs/blob/e819a1fc0e99a3e5320d8d6667a08d3b92e5e836/packages/encoding/src/utf8.ts#L15-L25
211+
/**
212+
* Takes UTF-8 data and decodes it to a string.
213+
*
214+
* In lossy mode, the [REPLACEMENT CHARACTER](https://en.wikipedia.org/wiki/Specials_(Unicode_block))
215+
* is used to substitude invalid encodings.
216+
* By default lossy mode is off and invalid data will lead to exceptions.
217+
*/
218+
export function fromUtf8(data: Uint8Array, lossy = false): string {
219+
const fatal = !lossy;
220+
return new TextDecoder('utf-8', { fatal }).decode(data);
221+
}
222+
223+
// akin to https://github.com/cosmos/cosmjs/blob/e819a1fc0e99a3e5320d8d6667a08d3b92e5e836/packages/cosmwasm-stargate/src/modules/wasm/queries.ts#L135-L150
35224
export async function queryContractSmart(
36225
rpc: string,
37226
address: string,
@@ -61,10 +250,20 @@ export async function queryContractSmart(
61250
throw new Error(`Query failed with unknown error`);
62251
}
63252
}
64-
const asciiResponse = Buffer.from(response.data.result.response.value, 'base64').toString(
65-
'ascii'
66-
);
67-
return JSON.parse(
68-
asciiResponse.substring(asciiResponse.indexOf('{') || asciiResponse.indexOf('['))
253+
// decode like https://github.com/confio/cosmjs-types/blob/66e52711914fccd2a9d1a03e392d3628fdf499e2/src/cosmwasm/wasm/v1/query.ts#L1621
254+
const { data: responseData } = QuerySmartContractStateResponse.decode(
255+
Buffer.from(response.data.result.response.value, 'base64')
69256
);
257+
// By convention, smart queries must return a valid JSON document (see https://github.com/CosmWasm/cosmwasm/issues/144)
258+
let responseText: string;
259+
try {
260+
responseText = fromUtf8(responseData);
261+
} catch (error) {
262+
throw new Error(`Could not UTF-8 decode smart query response from contract: ${error}`);
263+
}
264+
try {
265+
return JSON.parse(responseText);
266+
} catch (error) {
267+
throw new Error(`Could not JSON parse smart query response from contract: ${error}`);
268+
}
70269
}

0 commit comments

Comments
 (0)