Skip to content

Commit

Permalink
sdk/js-query: deserialization support
Browse files Browse the repository at this point in the history
  • Loading branch information
evan-gray committed Nov 14, 2023
1 parent 11bc1a5 commit 65701f9
Show file tree
Hide file tree
Showing 13 changed files with 696 additions and 72 deletions.
4 changes: 4 additions & 0 deletions sdk/js-query/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.0.6

Deserialization support

## 0.0.5

Mock support
Expand Down
2 changes: 1 addition & 1 deletion sdk/js-query/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@wormhole-foundation/wormhole-query-sdk",
"version": "0.0.5",
"version": "0.0.6",
"description": "Wormhole cross-chain query SDK",
"homepage": "https://wormhole.com",
"main": "./lib/cjs/index.js",
Expand Down
100 changes: 35 additions & 65 deletions sdk/js-query/src/mock/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,16 @@ import { Buffer } from "buffer";
import {
ChainQueryType,
EthCallQueryRequest,
EthCallQueryResponse,
EthCallWithFinalityQueryRequest,
EthCallWithFinalityQueryResponse,
PerChainQueryResponse,
QueryProxyQueryResponse,
QueryRequest,
hexToUint8Array,
QueryResponse,
sign,
} from "../query";
import { BinaryWriter } from "../query/BinaryWriter";
import { BytesLike } from "@ethersproject/bytes";
import { keccak256 } from "@ethersproject/keccak256";

export type QueryProxyQueryResponse = {
signatures: string[];
bytes: string;
};

const QUERY_RESPONSE_PREFIX = "query_response_0000000000000000000|";

/**
* Usage:
Expand Down Expand Up @@ -51,15 +46,8 @@ export class QueryProxyMock {
"cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0",
]
) {}
sign(serializedResponse: BytesLike) {
const digest = hexToUint8Array(
keccak256(
Buffer.concat([
Buffer.from(QUERY_RESPONSE_PREFIX),
hexToUint8Array(keccak256(serializedResponse)),
])
)
);
sign(serializedResponse: Uint8Array) {
const digest = QueryResponse.digest(serializedResponse);
return this.mockPrivateKeys.map(
(key, idx) => `${sign(key, digest)}${idx.toString(16).padStart(2, "0")}`
);
Expand All @@ -82,14 +70,11 @@ export class QueryProxyMock {
* @returns a promise result matching the query proxy's query response
*/
async mock(queryRequest: QueryRequest): Promise<QueryProxyQueryResponse> {
const serializedRequest = queryRequest.serialize();
const writer = new BinaryWriter()
.writeUint8(1) // version
.writeUint16(0) // source = off-chain
.writeUint8Array(new Uint8Array(new Array(65))) // empty signature for mock
.writeUint32(serializedRequest.length)
.writeUint8Array(serializedRequest)
.writeUint8(queryRequest.requests.length);
const queryResponse = new QueryResponse(
0, // source = off-chain
Buffer.from(new Array(65)).toString("hex"), // empty signature for mock
queryRequest
);
for (const perChainRequest of queryRequest.requests) {
const rpc = this.rpcMap[perChainRequest.chainId];
if (!rpc) {
Expand All @@ -98,7 +83,6 @@ export class QueryProxyMock {
);
}
const type = perChainRequest.query.type();
writer.writeUint16(perChainRequest.chainId).writeUint8(type);
if (type === ChainQueryType.EthCall) {
const query = perChainRequest.query as EthCallQueryRequest;
const response = await axios.post(rpc, [
Expand Down Expand Up @@ -132,25 +116,18 @@ export class QueryProxyMock {
`Invalid block result for chain ${perChainRequest.chainId} block ${query.blockTag}`
);
}
const results = callResults.map(
(callResult: any) =>
new Uint8Array(Buffer.from(callResult.result.substring(2), "hex"))
queryResponse.responses.push(
new PerChainQueryResponse(
perChainRequest.chainId,
new EthCallQueryResponse(
BigInt(parseInt(blockResult.number.substring(2), 16)), // block number
blockResult.hash, // hash
BigInt(parseInt(blockResult.timestamp.substring(2), 16)) *
BigInt("1000000"), // time in seconds -> microseconds
callResults.map((callResult: any) => callResult.result)
)
)
);
const perChainWriter = new BinaryWriter()
.writeUint64(BigInt(parseInt(blockResult.number.substring(2), 16))) // block number
.writeUint8Array(
new Uint8Array(Buffer.from(blockResult.hash.substring(2), "hex"))
) // hash
.writeUint64(
BigInt(parseInt(blockResult.timestamp.substring(2), 16)) *
BigInt("1000000")
) // time in seconds -> microseconds
.writeUint8(results.length);
for (const result of results) {
perChainWriter.writeUint32(result.length).writeUint8Array(result);
}
const serialized = perChainWriter.data();
writer.writeUint32(serialized.length).writeUint8Array(serialized);
} else if (type === ChainQueryType.EthCallWithFinality) {
const query = perChainRequest.query as EthCallWithFinalityQueryRequest;
const response = await axios.post(rpc, [
Expand Down Expand Up @@ -213,30 +190,23 @@ export class QueryProxyMock {
`Requested block for eth_call_with_finality has not yet reached the requested finality. Block: ${blockNumber}, ${query.finality}: ${latestBlockNumberMatchingFinality}`
);
}
const results = callResults.map(
(callResult: any) =>
new Uint8Array(Buffer.from(callResult.result.substring(2), "hex"))
queryResponse.responses.push(
new PerChainQueryResponse(
perChainRequest.chainId,
new EthCallWithFinalityQueryResponse(
BigInt(parseInt(blockResult.number.substring(2), 16)), // block number
blockResult.hash, // hash
BigInt(parseInt(blockResult.timestamp.substring(2), 16)) *
BigInt("1000000"), // time in seconds -> microseconds
callResults.map((callResult: any) => callResult.result)
)
)
);
const perChainWriter = new BinaryWriter()
.writeUint64(blockNumber) // block number
.writeUint8Array(
new Uint8Array(Buffer.from(blockResult.hash.substring(2), "hex"))
) // hash
.writeUint64(
BigInt(parseInt(blockResult.timestamp.substring(2), 16)) *
BigInt("1000000")
) // time in seconds -> microseconds
.writeUint8(results.length);
for (const result of results) {
perChainWriter.writeUint32(result.length).writeUint8Array(result);
}
const serialized = perChainWriter.data();
writer.writeUint32(serialized.length).writeUint8Array(serialized);
} else {
throw new Error(`Unsupported query type for mock: ${type}`);
}
}
const serializedResponse = writer.data();
const serializedResponse = queryResponse.serialize();
return {
signatures: this.sign(serializedResponse),
bytes: Buffer.from(serializedResponse).toString("hex"),
Expand Down
58 changes: 58 additions & 0 deletions sdk/js-query/src/query/BinaryReader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Buffer } from "buffer";
import { uint8ArrayToHex } from "./utils";

// BinaryReader provides the inverse of BinaryWriter
// Numbers are encoded as big endian
export class BinaryReader {
private _buffer: Buffer;
private _offset: number;

constructor(
arrayBuffer: WithImplicitCoercion<ArrayBuffer | SharedArrayBuffer>
) {
this._buffer = Buffer.from(arrayBuffer);
this._offset = 0;
}

readUint8(): number {
const tmp = this._buffer.readUint8(this._offset);
this._offset += 1;
return tmp;
}

readUint16(): number {
const tmp = this._buffer.readUint16BE(this._offset);
this._offset += 2;
return tmp;
}

readUint32(): number {
const tmp = this._buffer.readUint32BE(this._offset);
this._offset += 4;
return tmp;
}

readUint64(): bigint {
const tmp = this._buffer.readBigUInt64BE(this._offset);
this._offset += 8;
return tmp;
}

readUint8Array(length: number): Uint8Array {
const tmp = this._buffer.subarray(this._offset, this._offset + length);
this._offset += length;
return new Uint8Array(tmp);
}

readHex(length: number): string {
return uint8ArrayToHex(this.readUint8Array(length));
}

readString(length: number): string {
const tmp = this._buffer
.subarray(this._offset, this._offset + length)
.toString();
this._offset += length;
return tmp;
}
}
68 changes: 67 additions & 1 deletion sdk/js-query/src/query/ethCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { Buffer } from "buffer";
import { BinaryWriter } from "./BinaryWriter";
import { HexString } from "./consts";
import { ChainQueryType, ChainSpecificQuery } from "./request";
import { hexToUint8Array, isValidHexString } from "./utils";
import { coalesceUint8Array, hexToUint8Array, isValidHexString } from "./utils";
import { BinaryReader } from "./BinaryReader";
import { ChainSpecificResponse } from "./response";

export interface EthCallData {
to: string;
Expand Down Expand Up @@ -37,6 +39,25 @@ export class EthCallQueryRequest implements ChainSpecificQuery {
});
return writer.data();
}

static from(bytes: string | Uint8Array): EthCallQueryRequest {
const reader = new BinaryReader(coalesceUint8Array(bytes));
return this.fromReader(reader);
}

static fromReader(reader: BinaryReader): EthCallQueryRequest {
const blockTagLength = reader.readUint32();
const blockTag = reader.readString(blockTagLength);
const callDataLength = reader.readUint8();
const callData: EthCallData[] = [];
for (let idx = 0; idx < callDataLength; idx++) {
const to = reader.readHex(20);
const dataLength = reader.readUint32();
const data = reader.readHex(dataLength);
callData.push({ to, data });
}
return new EthCallQueryRequest(blockTag, callData);
}
}

export function parseBlockId(blockId: BlockTag): string {
Expand All @@ -59,3 +80,48 @@ export function parseBlockId(blockId: BlockTag): string {

return blockId;
}

export class EthCallQueryResponse implements ChainSpecificResponse {
constructor(
public blockNumber: bigint,
public blockHash: string,
public blockTime: bigint,
public results: string[] = []
) {}

type(): ChainQueryType {
return ChainQueryType.EthCall;
}

serialize(): Uint8Array {
const writer = new BinaryWriter()
.writeUint64(this.blockNumber)
.writeUint8Array(hexToUint8Array(this.blockHash))
.writeUint64(this.blockTime)
.writeUint8(this.results.length);
for (const result of this.results) {
const arr = hexToUint8Array(result);
writer.writeUint32(arr.length).writeUint8Array(arr);
}
return writer.data();
}

static from(bytes: string | Uint8Array): EthCallQueryResponse {
const reader = new BinaryReader(coalesceUint8Array(bytes));
return this.fromReader(reader);
}

static fromReader(reader: BinaryReader): EthCallQueryResponse {
const blockNumber = reader.readUint64();
const blockHash = reader.readHex(32);
const blockTime = reader.readUint64();
const resultsLength = reader.readUint8();
const results: string[] = [];
for (let idx = 0; idx < resultsLength; idx++) {
const resultLength = reader.readUint32();
const result = reader.readHex(resultLength);
results.push(result);
}
return new EthCallQueryResponse(blockNumber, blockHash, blockTime, results);
}
}
Loading

0 comments on commit 65701f9

Please sign in to comment.