CCQ: Query SDK (#3421)
* CCQ: Query SDK * CCQ: TS SDK fixes --------- Co-authored-by: Kevin Peters <kevin@w7.xyz>
This commit is contained in:
parent
9aa4d0329d
commit
e0606497e0
|
@ -166,7 +166,7 @@ spec:
|
||||||
# - --chainGovernorEnabled=true
|
# - --chainGovernorEnabled=true
|
||||||
- --ccqEnabled=true
|
- --ccqEnabled=true
|
||||||
- --ccqAllowedRequesters=beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe,25021A4FCAf61F2EADC8202D3833Df48B2Fa0D54
|
- --ccqAllowedRequesters=beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe,25021A4FCAf61F2EADC8202D3833Df48B2Fa0D54
|
||||||
- --ccqAllowedPeers=12D3KooWSnju8zhywCYVi2JwTqky1sySPnmtYLsHHzc4WerMnDQH
|
- --ccqAllowedPeers=12D3KooWSnju8zhywCYVi2JwTqky1sySPnmtYLsHHzc4WerMnDQH,12D3KooWM6WqedfR6ehtTd1y6rJu3ZUrEkTjcJJnJZYesjd89zj8
|
||||||
# - --logLevel=debug
|
# - --logLevel=debug
|
||||||
securityContext:
|
securityContext:
|
||||||
capabilities:
|
capabilities:
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env*
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# build
|
||||||
|
/lib
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"printWidth": 80,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "always"
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
## 0.0.1
|
||||||
|
|
||||||
|
Initial release
|
|
@ -0,0 +1,13 @@
|
||||||
|
Copyright 2023 Wormhole Project Contributors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
|
@ -0,0 +1 @@
|
||||||
|
Wormhole cross-chain queries SDK
|
|
@ -0,0 +1,7 @@
|
||||||
|
import type { Config } from "@jest/types";
|
||||||
|
|
||||||
|
const config: Config.InitialOptions = {
|
||||||
|
preset: "ts-jest",
|
||||||
|
testEnvironment: "node",
|
||||||
|
};
|
||||||
|
export default config;
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"name": "@wormhole-foundation/wormhole-query-sdk",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Wormhole cross-chain query SDK",
|
||||||
|
"homepage": "https://wormhole.com",
|
||||||
|
"main": "./lib/cjs/index.js",
|
||||||
|
"module": "./lib/esm/index.js",
|
||||||
|
"files": [
|
||||||
|
"lib/"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"test": "jest --verbose",
|
||||||
|
"build": "tsc -p tsconfig.json && tsc -p tsconfig-cjs.json"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"wormhole",
|
||||||
|
"sdk",
|
||||||
|
"cross-chain",
|
||||||
|
"query"
|
||||||
|
],
|
||||||
|
"author": "Wormhole Foundation",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"axios": "^1.4.0",
|
||||||
|
"jest": "^29.5.0",
|
||||||
|
"prettier": "^2.3.2",
|
||||||
|
"ts-jest": "^29.1.0",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"typescript": "^5.1.3"
|
||||||
|
},
|
||||||
|
"sideEffects": false,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/elliptic": "^6.4.14",
|
||||||
|
"elliptic": "^6.5.4",
|
||||||
|
"web3": "^4.1.2"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./query";
|
|
@ -0,0 +1,58 @@
|
||||||
|
// BinaryWriter appends data to the end of a buffer, resizing the buffer as needed
|
||||||
|
// Numbers are encoded as big endian
|
||||||
|
export class BinaryWriter {
|
||||||
|
private _buffer: Buffer;
|
||||||
|
private _offset: number;
|
||||||
|
|
||||||
|
constructor(initialSize: number = 1024) {
|
||||||
|
if (initialSize < 0) throw new Error("Initial size must be non-negative");
|
||||||
|
this._buffer = Buffer.alloc(initialSize);
|
||||||
|
this._offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the buffer has the capacity to write `size` bytes, otherwise allocate more memory
|
||||||
|
_ensure(size: number) {
|
||||||
|
const remaining = this._buffer.length - this._offset;
|
||||||
|
if (remaining < size) {
|
||||||
|
const oldBuffer = this._buffer;
|
||||||
|
const newSize = this._buffer.length * 2 + size;
|
||||||
|
this._buffer = Buffer.alloc(newSize);
|
||||||
|
oldBuffer.copy(this._buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeUint8(value: number) {
|
||||||
|
if (value < 0 || value > 255) throw new Error("Invalid value");
|
||||||
|
this._ensure(1);
|
||||||
|
this._buffer.writeUint8(value, this._offset);
|
||||||
|
this._offset += 1;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeUint16(value: number) {
|
||||||
|
if (value < 0 || value > 65535) throw new Error("Invalid value");
|
||||||
|
this._ensure(2);
|
||||||
|
this._offset = this._buffer.writeUint16BE(value, this._offset);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeUint32(value: number) {
|
||||||
|
if (value < 0 || value > 4294967295) throw new Error("Invalid value");
|
||||||
|
this._ensure(4);
|
||||||
|
this._offset = this._buffer.writeUint32BE(value, this._offset);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeUint8Array(value: Uint8Array) {
|
||||||
|
this._ensure(value.length);
|
||||||
|
this._buffer.set(value, this._offset);
|
||||||
|
this._offset += value.length;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
data(): Uint8Array {
|
||||||
|
const copy = new Uint8Array(this._offset);
|
||||||
|
copy.set(this._buffer.subarray(0, this._offset));
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export type Network = "MAINNET" | "TESTNET" | "DEVNET";
|
||||||
|
|
||||||
|
export type HexString = string;
|
|
@ -0,0 +1,280 @@
|
||||||
|
import {
|
||||||
|
afterAll,
|
||||||
|
beforeAll,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
jest,
|
||||||
|
test,
|
||||||
|
} from "@jest/globals";
|
||||||
|
import Web3, { ETH_DATA_FORMAT } from "web3";
|
||||||
|
import axios from "axios";
|
||||||
|
import {
|
||||||
|
EthCallData,
|
||||||
|
EthCallQueryRequest,
|
||||||
|
PerChainQueryRequest,
|
||||||
|
QueryRequest,
|
||||||
|
sign,
|
||||||
|
} from "..";
|
||||||
|
|
||||||
|
jest.setTimeout(125000);
|
||||||
|
|
||||||
|
const CI = false;
|
||||||
|
const ENV = "DEVNET";
|
||||||
|
const ETH_NODE_URL = CI ? "ws://eth-devnet:8545" : "ws://localhost:8545";
|
||||||
|
|
||||||
|
const CCQ_SERVER_URL = "http://localhost:6069/v1";
|
||||||
|
const QUERY_URL = CCQ_SERVER_URL + "/query";
|
||||||
|
const HEALTH_URL = CCQ_SERVER_URL + "/health";
|
||||||
|
const PRIVATE_KEY =
|
||||||
|
"cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0";
|
||||||
|
const WETH_ADDRESS = "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E";
|
||||||
|
|
||||||
|
let web3: Web3;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
web3 = new Web3(ETH_NODE_URL);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
web3.provider?.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
function createTestEthCallData(
|
||||||
|
to: string,
|
||||||
|
name: string,
|
||||||
|
outputType: string
|
||||||
|
): EthCallData {
|
||||||
|
return {
|
||||||
|
to,
|
||||||
|
data: web3.eth.abi.encodeFunctionCall(
|
||||||
|
{
|
||||||
|
constant: true,
|
||||||
|
inputs: [],
|
||||||
|
name,
|
||||||
|
outputs: [{ name, type: outputType }],
|
||||||
|
payable: false,
|
||||||
|
stateMutability: "view",
|
||||||
|
type: "function",
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("eth call", () => {
|
||||||
|
test("serialize request", () => {
|
||||||
|
const toAddress = "0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270";
|
||||||
|
const nameCallData = createTestEthCallData(toAddress, "name", "string");
|
||||||
|
const totalSupplyCallData = createTestEthCallData(
|
||||||
|
toAddress,
|
||||||
|
"totalSupply",
|
||||||
|
"uint256"
|
||||||
|
);
|
||||||
|
const ethCall = new EthCallQueryRequest("0x28d9630", [
|
||||||
|
nameCallData,
|
||||||
|
totalSupplyCallData,
|
||||||
|
]);
|
||||||
|
const chainId = 5;
|
||||||
|
const ethQuery = new PerChainQueryRequest(chainId, ethCall);
|
||||||
|
const nonce = 1;
|
||||||
|
const request = new QueryRequest(nonce, [ethQuery]);
|
||||||
|
const serialized = request.serialize();
|
||||||
|
expect(Buffer.from(serialized).toString("hex")).toEqual(
|
||||||
|
"0100000001010005010000004600000009307832386439363330020d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000406fdde030d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000418160ddd"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
test("successful query", async () => {
|
||||||
|
const nameCallData = createTestEthCallData(WETH_ADDRESS, "name", "string");
|
||||||
|
const totalSupplyCallData = createTestEthCallData(
|
||||||
|
WETH_ADDRESS,
|
||||||
|
"totalSupply",
|
||||||
|
"uint256"
|
||||||
|
);
|
||||||
|
const blockNumber = await web3.eth.getBlockNumber(ETH_DATA_FORMAT);
|
||||||
|
const ethCall = new EthCallQueryRequest(blockNumber, [
|
||||||
|
nameCallData,
|
||||||
|
totalSupplyCallData,
|
||||||
|
]);
|
||||||
|
const chainId = 2;
|
||||||
|
const ethQuery = new PerChainQueryRequest(chainId, ethCall);
|
||||||
|
const nonce = 1;
|
||||||
|
const request = new QueryRequest(nonce, [ethQuery]);
|
||||||
|
const serialized = request.serialize();
|
||||||
|
const digest = QueryRequest.digest(ENV, serialized);
|
||||||
|
const signature = sign(PRIVATE_KEY, digest);
|
||||||
|
const response = await axios.put(
|
||||||
|
QUERY_URL,
|
||||||
|
{
|
||||||
|
signature,
|
||||||
|
bytes: Buffer.from(serialized).toString("hex"),
|
||||||
|
},
|
||||||
|
{ headers: { "X-API-Key": "my_secret_key" } }
|
||||||
|
);
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
});
|
||||||
|
test("missing api-key should fail", async () => {
|
||||||
|
const nameCallData = createTestEthCallData(WETH_ADDRESS, "name", "string");
|
||||||
|
const totalSupplyCallData = createTestEthCallData(
|
||||||
|
WETH_ADDRESS,
|
||||||
|
"totalSupply",
|
||||||
|
"uint256"
|
||||||
|
);
|
||||||
|
const blockNumber = await web3.eth.getBlockNumber(ETH_DATA_FORMAT);
|
||||||
|
const ethCall = new EthCallQueryRequest(blockNumber, [
|
||||||
|
nameCallData,
|
||||||
|
totalSupplyCallData,
|
||||||
|
]);
|
||||||
|
const chainId = 2;
|
||||||
|
const ethQuery = new PerChainQueryRequest(chainId, ethCall);
|
||||||
|
const nonce = 1;
|
||||||
|
const request = new QueryRequest(nonce, [ethQuery]);
|
||||||
|
const serialized = request.serialize();
|
||||||
|
const digest = QueryRequest.digest(ENV, serialized);
|
||||||
|
const signature = sign(PRIVATE_KEY, digest);
|
||||||
|
let err = false;
|
||||||
|
await axios
|
||||||
|
.put(QUERY_URL, {
|
||||||
|
signature,
|
||||||
|
bytes: Buffer.from(serialized).toString("hex"),
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
err = true;
|
||||||
|
expect(error.response.status).toBe(401);
|
||||||
|
expect(error.response.data).toBe("api key is missing\n");
|
||||||
|
});
|
||||||
|
expect(err).toBe(true);
|
||||||
|
});
|
||||||
|
test("invalid api-key should fail", async () => {
|
||||||
|
const nameCallData = createTestEthCallData(WETH_ADDRESS, "name", "string");
|
||||||
|
const totalSupplyCallData = createTestEthCallData(
|
||||||
|
WETH_ADDRESS,
|
||||||
|
"totalSupply",
|
||||||
|
"uint256"
|
||||||
|
);
|
||||||
|
const blockNumber = await web3.eth.getBlockNumber(ETH_DATA_FORMAT);
|
||||||
|
const ethCall = new EthCallQueryRequest(blockNumber, [
|
||||||
|
nameCallData,
|
||||||
|
totalSupplyCallData,
|
||||||
|
]);
|
||||||
|
const chainId = 2;
|
||||||
|
const ethQuery = new PerChainQueryRequest(chainId, ethCall);
|
||||||
|
const nonce = 1;
|
||||||
|
const request = new QueryRequest(nonce, [ethQuery]);
|
||||||
|
const serialized = request.serialize();
|
||||||
|
const digest = QueryRequest.digest(ENV, serialized);
|
||||||
|
const signature = sign(PRIVATE_KEY, digest);
|
||||||
|
let err = false;
|
||||||
|
await axios
|
||||||
|
.put(
|
||||||
|
QUERY_URL,
|
||||||
|
{
|
||||||
|
signature,
|
||||||
|
bytes: Buffer.from(serialized).toString("hex"),
|
||||||
|
},
|
||||||
|
{ headers: { "X-API-Key": "some_junk" } }
|
||||||
|
)
|
||||||
|
.catch(function (error) {
|
||||||
|
err = true;
|
||||||
|
expect(error.response.status).toBe(403);
|
||||||
|
expect(error.response.data).toBe("invalid api key\n");
|
||||||
|
});
|
||||||
|
expect(err).toBe(true);
|
||||||
|
});
|
||||||
|
test("unauthorized call should fail", async () => {
|
||||||
|
const nameCallData = createTestEthCallData(WETH_ADDRESS, "name", "string");
|
||||||
|
const totalSupplyCallData = createTestEthCallData(
|
||||||
|
WETH_ADDRESS,
|
||||||
|
"totalSupply",
|
||||||
|
"uint256"
|
||||||
|
);
|
||||||
|
const blockNumber = await web3.eth.getBlockNumber(ETH_DATA_FORMAT);
|
||||||
|
const ethCall = new EthCallQueryRequest(blockNumber, [
|
||||||
|
nameCallData,
|
||||||
|
totalSupplyCallData, // API key "my_secret_key_2" is not authorized to do total supply.
|
||||||
|
]);
|
||||||
|
const chainId = 2;
|
||||||
|
const ethQuery = new PerChainQueryRequest(chainId, ethCall);
|
||||||
|
const nonce = 1;
|
||||||
|
const request = new QueryRequest(nonce, [ethQuery]);
|
||||||
|
const serialized = request.serialize();
|
||||||
|
const digest = QueryRequest.digest(ENV, serialized);
|
||||||
|
const signature = sign(PRIVATE_KEY, digest);
|
||||||
|
let err = false;
|
||||||
|
await axios
|
||||||
|
.put(
|
||||||
|
QUERY_URL,
|
||||||
|
{
|
||||||
|
signature,
|
||||||
|
bytes: Buffer.from(serialized).toString("hex"),
|
||||||
|
},
|
||||||
|
{ headers: { "X-API-Key": "my_secret_key_2" } }
|
||||||
|
)
|
||||||
|
.catch(function (error) {
|
||||||
|
err = true;
|
||||||
|
expect(error.response.status).toBe(400);
|
||||||
|
expect(error.response.data).toBe(
|
||||||
|
`call "ethCall:2:000000000000000000000000ddb64fe46a91d46ee29420539fc25fd07c5fea3e:18160ddd" not authorized\n`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(err).toBe(true);
|
||||||
|
});
|
||||||
|
test("unsigned query should fail if not allowed", async () => {
|
||||||
|
const nameCallData = createTestEthCallData(WETH_ADDRESS, "name", "string");
|
||||||
|
const totalSupplyCallData = createTestEthCallData(
|
||||||
|
WETH_ADDRESS,
|
||||||
|
"totalSupply",
|
||||||
|
"uint256"
|
||||||
|
);
|
||||||
|
const blockNumber = await web3.eth.getBlockNumber(ETH_DATA_FORMAT);
|
||||||
|
const ethCall = new EthCallQueryRequest(blockNumber, [
|
||||||
|
nameCallData,
|
||||||
|
totalSupplyCallData,
|
||||||
|
]);
|
||||||
|
const chainId = 2;
|
||||||
|
const ethQuery = new PerChainQueryRequest(chainId, ethCall);
|
||||||
|
const nonce = 1;
|
||||||
|
const request = new QueryRequest(nonce, [ethQuery]);
|
||||||
|
const serialized = request.serialize();
|
||||||
|
const signature = "";
|
||||||
|
let err = false;
|
||||||
|
await axios
|
||||||
|
.put(
|
||||||
|
QUERY_URL,
|
||||||
|
{
|
||||||
|
signature,
|
||||||
|
bytes: Buffer.from(serialized).toString("hex"),
|
||||||
|
},
|
||||||
|
{ headers: { "X-API-Key": "my_secret_key" } }
|
||||||
|
)
|
||||||
|
.catch(function (error) {
|
||||||
|
err = true;
|
||||||
|
expect(error.response.status).toBe(400);
|
||||||
|
expect(error.response.data).toBe(`request not signed\n`);
|
||||||
|
});
|
||||||
|
expect(err).toBe(true);
|
||||||
|
});
|
||||||
|
test("unsigned query should succeed if allowed", async () => {
|
||||||
|
const nameCallData = createTestEthCallData(WETH_ADDRESS, "name", "string");
|
||||||
|
const blockNumber = await web3.eth.getBlockNumber(ETH_DATA_FORMAT);
|
||||||
|
const ethCall = new EthCallQueryRequest(blockNumber, [nameCallData]);
|
||||||
|
const chainId = 2;
|
||||||
|
const ethQuery = new PerChainQueryRequest(chainId, ethCall);
|
||||||
|
const nonce = 1;
|
||||||
|
const request = new QueryRequest(nonce, [ethQuery]);
|
||||||
|
const serialized = request.serialize();
|
||||||
|
const signature = "";
|
||||||
|
const response = await axios.put(
|
||||||
|
QUERY_URL,
|
||||||
|
{
|
||||||
|
signature,
|
||||||
|
bytes: Buffer.from(serialized).toString("hex"),
|
||||||
|
},
|
||||||
|
{ headers: { "X-API-Key": "my_secret_key_2" } } // This API key allows unsigned queries.
|
||||||
|
);
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
});
|
||||||
|
test("health check", async () => {
|
||||||
|
const response = await axios.get(HEALTH_URL);
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { BinaryWriter } from "./BinaryWriter";
|
||||||
|
import { HexString } from "./consts";
|
||||||
|
import { ChainQueryType, ChainSpecificQuery } from "./request";
|
||||||
|
import { hexToUint8Array, isValidHexString } from "./utils";
|
||||||
|
|
||||||
|
export interface EthCallData {
|
||||||
|
to: string;
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can be a block number or a block hash
|
||||||
|
export type BlockTag = number | HexString;
|
||||||
|
|
||||||
|
export class EthCallQueryRequest implements ChainSpecificQuery {
|
||||||
|
blockTag: string;
|
||||||
|
|
||||||
|
constructor(blockTag: BlockTag, public callData: EthCallData[]) {
|
||||||
|
if (typeof blockTag === "number") {
|
||||||
|
this.blockTag = `0x${blockTag.toString(16)}`;
|
||||||
|
} else if (isValidHexString(blockTag)) {
|
||||||
|
if (!blockTag.startsWith("0x")) {
|
||||||
|
blockTag = `0x${blockTag}`;
|
||||||
|
}
|
||||||
|
this.blockTag = blockTag;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Invalid block tag: ${blockTag}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type(): ChainQueryType {
|
||||||
|
return ChainQueryType.EthCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize(): Uint8Array {
|
||||||
|
const writer = new BinaryWriter()
|
||||||
|
.writeUint32(this.blockTag.length)
|
||||||
|
.writeUint8Array(Buffer.from(this.blockTag))
|
||||||
|
.writeUint8(this.callData.length);
|
||||||
|
this.callData.forEach(({ to, data }) => {
|
||||||
|
const dataArray = hexToUint8Array(data);
|
||||||
|
writer
|
||||||
|
.writeUint8Array(hexToUint8Array(to))
|
||||||
|
.writeUint32(dataArray.length)
|
||||||
|
.writeUint8Array(dataArray);
|
||||||
|
});
|
||||||
|
return writer.data();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export * from "./request";
|
||||||
|
export * from "./utils";
|
||||||
|
export * from "./ethCall";
|
||||||
|
export * from "./consts";
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { BinaryWriter } from "./BinaryWriter";
|
||||||
|
import { Network } from "./consts";
|
||||||
|
import { utils } from "web3";
|
||||||
|
import { hexToUint8Array } from "./utils";
|
||||||
|
|
||||||
|
export const MAINNET_QUERY_REQUEST_PREFIX =
|
||||||
|
"mainnet_query_request_000000000000|";
|
||||||
|
|
||||||
|
export const TESTNET_QUERY_REQUEST_PREFIX =
|
||||||
|
"testnet_query_request_000000000000|";
|
||||||
|
|
||||||
|
export const DEVNET_QUERY_REQUEST_PREFIX =
|
||||||
|
"devnet_query_request_0000000000000|";
|
||||||
|
|
||||||
|
export function getPrefix(network: Network) {
|
||||||
|
return network === "MAINNET"
|
||||||
|
? MAINNET_QUERY_REQUEST_PREFIX
|
||||||
|
: network === "TESTNET"
|
||||||
|
? TESTNET_QUERY_REQUEST_PREFIX
|
||||||
|
: DEVNET_QUERY_REQUEST_PREFIX;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class QueryRequest {
|
||||||
|
constructor(
|
||||||
|
public nonce: number,
|
||||||
|
public requests: PerChainQueryRequest[] = [],
|
||||||
|
public version: number = 1
|
||||||
|
) {}
|
||||||
|
|
||||||
|
serialize(): Uint8Array {
|
||||||
|
const writer = new BinaryWriter()
|
||||||
|
.writeUint8(this.version)
|
||||||
|
.writeUint32(this.nonce)
|
||||||
|
.writeUint8(this.requests.length);
|
||||||
|
this.requests.forEach((request) =>
|
||||||
|
writer.writeUint8Array(request.serialize())
|
||||||
|
);
|
||||||
|
return writer.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
static digest(network: Network, bytes: Uint8Array): Uint8Array {
|
||||||
|
const prefix = getPrefix(network);
|
||||||
|
const data = Buffer.concat([Buffer.from(prefix), bytes]);
|
||||||
|
return hexToUint8Array(utils.keccak256(data).slice(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PerChainQueryRequest {
|
||||||
|
constructor(public chainId: number, public query: ChainSpecificQuery) {}
|
||||||
|
|
||||||
|
serialize(): Uint8Array {
|
||||||
|
const writer = new BinaryWriter()
|
||||||
|
.writeUint16(this.chainId)
|
||||||
|
.writeUint8(this.query.type());
|
||||||
|
const queryData = this.query.serialize();
|
||||||
|
return writer
|
||||||
|
.writeUint32(queryData.length)
|
||||||
|
.writeUint8Array(queryData)
|
||||||
|
.data();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChainSpecificQuery {
|
||||||
|
type(): ChainQueryType;
|
||||||
|
serialize(): Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ChainQueryType {
|
||||||
|
EthCall = 1,
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import * as elliptic from "elliptic";
|
||||||
|
|
||||||
|
export function isValidHexString(s: string): boolean {
|
||||||
|
return /^(0x)?[0-9a-fA-F]+$/.test(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hexToUint8Array(s: string): Uint8Array {
|
||||||
|
if (!isValidHexString(s)) {
|
||||||
|
throw new Error(`${s} is not hex`);
|
||||||
|
}
|
||||||
|
if (s.startsWith("0x")) {
|
||||||
|
s = s.slice(2);
|
||||||
|
}
|
||||||
|
s.padStart(s.length + (s.length % 2), "0");
|
||||||
|
return new Uint8Array(Buffer.from(s, "hex"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param key Private key used to sign `data`
|
||||||
|
* @param data Data for signing
|
||||||
|
* @returns ECDSA secp256k1 signature
|
||||||
|
*/
|
||||||
|
export function sign(key: string, data: Uint8Array): string {
|
||||||
|
const ec = new elliptic.ec("secp256k1");
|
||||||
|
const keyPair = ec.keyFromPrivate(key);
|
||||||
|
const signature = keyPair.sign(data, { canonical: true });
|
||||||
|
const packed =
|
||||||
|
signature.r.toString("hex").padStart(64, "0") +
|
||||||
|
signature.s.toString("hex").padStart(64, "0") +
|
||||||
|
Buffer.from([signature.recoveryParam ?? 0]).toString("hex");
|
||||||
|
return packed;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "CommonJS",
|
||||||
|
"outDir": "./lib/cjs"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es6",
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "./lib/esm",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"downlevelIteration": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"lib": ["dom", "es5", "scripthost", "es2020.bigint"]
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "**/*.test.*", "**/__tests__/*"]
|
||||||
|
}
|
Loading…
Reference in New Issue