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
|
||||
- --ccqEnabled=true
|
||||
- --ccqAllowedRequesters=beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe,25021A4FCAf61F2EADC8202D3833Df48B2Fa0D54
|
||||
- --ccqAllowedPeers=12D3KooWSnju8zhywCYVi2JwTqky1sySPnmtYLsHHzc4WerMnDQH
|
||||
- --ccqAllowedPeers=12D3KooWSnju8zhywCYVi2JwTqky1sySPnmtYLsHHzc4WerMnDQH,12D3KooWM6WqedfR6ehtTd1y6rJu3ZUrEkTjcJJnJZYesjd89zj8
|
||||
# - --logLevel=debug
|
||||
securityContext:
|
||||
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