Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add polling receipt by hash #10

Merged
merged 1 commit into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/serious-pants-carry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nilfoundation/niljs": patch
---

Add simple polling implementation, allow to run publish workflow only manually, extend walletClient from publicClient
4 changes: 0 additions & 4 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
name: Release

on:
push:
branches:
- main
- master
workflow_dispatch:

concurrency:
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"node": ">=18.0.0"
},
"type": "module",
"main": "dist/niljs.cjs.cjs",
"main": "dist/niljs.cjs",
"module": "dist/niljs.esm.js",
"files": [
"dist"
Expand All @@ -34,7 +34,8 @@
"lint": "biome check .",
"biome:fix": "biome check --apply .",
"changeset": "changeset",
"changeset:publish": "npm run build && changeset publish"
"changeset:publish": "changeset publish",
"git-hooks:update": "git config core.hooksPath .git/hooks/ && rm -rf .git/hooks && npx --no-install simple-git-hooks"
},
"license": "MIT",
"peerDependencies": {
Expand Down
47 changes: 42 additions & 5 deletions src/clients/PublicClient.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { IReceipt } from "../index.js";
import { BaseClient } from "./BaseClient.js";
import type { IPublicClientConfig } from "./types/ClientConfigs.js";

Expand Down Expand Up @@ -205,11 +206,47 @@ class PublicClient extends BaseClient {

return res.result;
}

/**
* getMessageReceiptByHash returns the message receipt by the hash.
* @param shardId - The shard id.
* @param hash - The hash.
* @returns The message receipt.
*/
public async getMessageReceiptByHash(
shardId: number,
hash: Uint8Array,
): Promise<IReceipt> {
const res = await this.rpcClient.request({
method: "eth_getMessageReceipt",
params: [shardId, hash],
});

return res.result;
}

/**
* sendRawMessage sends a raw message to the network.
* @param message - The message to send.
* @returns The hash of the message.
* @example
* import { PublicClient } from '@nilfoundation/niljs';
*
* const client = new PublicClient({
* endpoint: 'http://127.0.0.1:8529'
* })
*
* const message = Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
* const hash = await client.sendRawMessage(message);
*/
public async sendRawMessage(message: Uint8Array): Promise<Uint8Array> {
const res = await this.rpcClient.request({
method: "eth_sendRawMessage",
params: [message],
});

return res.hash;
}
}

export { PublicClient };

// this client is subject to change a lot
// we need to add more methods to interact with the network
// and we need to know shard id before executing the request
// we won't have a single source of data for all shards so we need to know shard id.
55 changes: 18 additions & 37 deletions src/clients/WalletClient.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import invariant from "tiny-invariant";
import { messageToSsz, signedMessageToSsz } from "../encoding/toSsz.js";
import { type IReceipt, getShardIdFromAddress } from "../index.js";
import type { ISigner } from "../signers/index.js";
import type { IMessage } from "../types/IMessage.js";
import type { IReceipt } from "../types/IReceipt.js";
import { assertIsValidMessage } from "../utils/assert.js";
import { BaseClient } from "./BaseClient.js";
import { startPollingUntilCondition } from "../utils/polling.js";
import { PublicClient } from "./PublicClient.js";
import type { IWalletClientConfig } from "./types/ClientConfigs.js";
import type { ISendMessageOptions } from "./types/ISendMessageOptions.js";
import type { ISignMessageOptions } from "./types/ISignMessageOptions.js";
Expand All @@ -15,13 +16,15 @@ import type { ISignMessageOptions } from "./types/ISignMessageOptions.js";
* Wallet client alllows to use api that require signing data and private key usage.
* @example
* import { WalletClient } from '@nilfoundation/niljs';
* import { LocalKeySigner } from '@nilfoundation/niljs';
*
* const client = new WalletClient({
* endpoint: 'http://127.0.0.1:8529'
* signer: new LocalKeySigner({ privateKey: "xxx" })
* })
*/
class WalletClient extends BaseClient {
private signer?: ISigner;
class WalletClient extends PublicClient {
private signer: ISigner;

constructor(config: IWalletClientConfig) {
super(config);
Expand Down Expand Up @@ -88,29 +91,6 @@ class WalletClient extends BaseClient {
});
}

/**
* sendRawMessage sends a raw message to the network.
* @param message - The message to send.
* @returns The hash of the message.
* @example
* import { WalletClient } from '@nilfoundation/niljs';
*
* const client = new WalletClient({
* endpoint: 'http://127.0.0.1:8529'
* })
*
* const message = Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
* const hash = await client.sendRawMessage(message);
*/
public async sendRawMessage(message: Uint8Array): Promise<Uint8Array> {
const res = await this.rpcClient.request({
method: "eth_sendRawMessage",
params: [message],
});

return res.hash;
}

/**
* deployContract deploys a contract to the network.
* @param contract - The contract to deploy.
Expand All @@ -127,19 +107,20 @@ class WalletClient extends BaseClient {
*/
public async deployContract(contract: Uint8Array): Promise<Uint8Array> {
const hash = await this.sendRawMessage(contract);
// there will be a method to get receipt by hash, but it is not implemented yet
// it will use kinda polling to get receipt asap
// mocking it for now:
const receipt: Partial<IReceipt> = {
success: true,
};
const address = this.signer.getAddress();
const shardId = getShardIdFromAddress(address);

// in the future we want to use subscribe method to get the receipt
// for now it is simple short polling
const receipt = await startPollingUntilCondition<IReceipt>(
async () => await this.getMessageReceiptByHash(shardId, hash),
(receipt) => receipt !== undefined,
1000,
);

// ! compiling smart contract to the bytecode shall not be included in this library
// it can be done by hardhat
invariant(
receipt.success,
"Contract deployment failed. Please check the contract bytecode.",
);
invariant(receipt?.success, "Contract deployment failed.");

return hash;
}
Expand Down
2 changes: 1 addition & 1 deletion src/clients/types/ClientConfigs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type IWalletClientConfig = IClientBaseConfig & {
* signer: signer
* })
*/
signer?: ISigner;
signer: ISigner;
};

export type { IClientBaseConfig, IPublicClientConfig, IWalletClientConfig };
36 changes: 29 additions & 7 deletions src/signers/LocalKeySigner.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import type { Hex } from "@noble/curves/abstract/utils";
import { secp256k1 } from "@noble/curves/secp256k1";
import { toHex } from "../encoding/toHex.js";
import { assertIsHexString, assertIsValidPrivateKey } from "../utils/assert.js";
import { getPublicKey } from "./publicKey.js";
import {
assertIsAddress,
assertIsHexString,
assertIsValidPrivateKey,
} from "../utils/assert.js";
import { getAddressFromPublicKey, getPublicKey } from "./publicKey.js";
import type { IAddress } from "./types/IAddress.js";
import type { ILocalKeySignerConfig } from "./types/ILocalKeySignerConfig.js";
import type { ISignature } from "./types/ISignature.js";
import type { ISigner } from "./types/ISigner.js";
Expand All @@ -17,17 +23,14 @@ import type { ISigner } from "./types/ISigner.js";
* const signer = new LocalKeySigner({ privateKey });
*/
class LocalKeySigner implements ISigner {
private publicKey;
private privateKey;
private publicKey?: Hex = undefined;
private address?: IAddress = undefined;

constructor(config: ILocalKeySignerConfig) {
const { privateKey } = config;
assertIsValidPrivateKey(privateKey);

const publicKey = getPublicKey(privateKey);
assertIsHexString(publicKey);

this.publicKey = publicKey;
this.privateKey = privateKey;
}

Expand All @@ -44,8 +47,27 @@ class LocalKeySigner implements ISigner {
}

public getPublicKey() {
if (this.publicKey) {
return this.publicKey;
}

const publicKey = getPublicKey(this.privateKey);
assertIsHexString(publicKey);

this.publicKey = publicKey;
return this.publicKey;
}

public getAddress() {
if (this.address) {
return this.address;
}

this.address = getAddressFromPublicKey(this.getPublicKey());
assertIsAddress(this.address);

return this.address;
}
}

export { LocalKeySigner };
20 changes: 19 additions & 1 deletion src/signers/publicKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import {
type ISignature,
addHexPrefix,
removeHexPrefix,
toBytes,
toHex,
} from "../index.js";
import { keccak_256 } from "../utils/keccak256.js";
import type { IAddress } from "./types/IAddress.js";
import type { IPrivateKey } from "./types/IPrivateKey.js";

/**
Expand All @@ -31,4 +34,19 @@ const recoverPublicKey = (
//
};

export { getPublicKey, generatePrivateKey, recoverPublicKey };
/**
* Returns the address from the public key.
* @param publicKey - Public key in hex format
* @returns Address in hex format
*/
const getAddressFromPublicKey = (publicKey: Hex): IAddress => {
const bytes = keccak_256(toBytes(removeHexPrefix(publicKey)));
return toHex(bytes) as IAddress;
};

export {
getPublicKey,
generatePrivateKey,
recoverPublicKey,
getAddressFromPublicKey,
};
6 changes: 6 additions & 0 deletions src/signers/types/IAddress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Address type represents an address in hexadecimal format.
*/
type IAddress = `0x${string}`;

export type { IAddress };
8 changes: 8 additions & 0 deletions src/signers/types/ISigner.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Hex } from "@noble/curves/abstract/utils";
import type { IAddress } from "./IAddress.js";
import type { ISignature } from "./ISignature.js";

/**
Expand All @@ -21,6 +22,13 @@ abstract class ISigner {
* const publicKey = signer.getPublicKey();
*/
abstract getPublicKey(): Hex;
/**
* Returns the address.
* @returns The address.
* @example
* const address = signer.getAddress();
*/
abstract getAddress(): IAddress;
}

export { ISigner };
2 changes: 1 addition & 1 deletion src/utils/assert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const assertIsValidPrivateKey = (
message?: string,
): void => {
invariant(
isHexString(privateKey) && privateKey.length === 64,
isHexString(privateKey) && privateKey.length === 32 * 2 + 2,
message ?? `Expected a valid private key, but got ${privateKey}`,
);
};
Expand Down
16 changes: 12 additions & 4 deletions src/utils/hex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,25 @@ const HEX_REGEX = /^[0-9a-fA-F]+$/;
* Otherwise, it returns false.
* @param value - The value to check.
*/
const isHexString = (value: Hex): boolean => {
return typeof value === "string" && HEX_REGEX.test(value);
const isHexString = (value: Hex): value is Hex => {
return (
typeof value === "string" &&
value.startsWith("0x") &&
HEX_REGEX.test(removeHexPrefix(value))
);
};

/**
* Remove a hex prefix from a hex string.
* @param hex hex-string
* @returns format: base16-string
*/
const removeHexPrefix = (hex: string): string => {
return hex.replace(/^0x/i, "");
const removeHexPrefix = (hex: Hex): string => {
if (typeof hex !== "string") {
throw new Error(`Expected a hex string but got ${hex}`);
}

return hex.startsWith("0x") ? hex.slice(2) : hex;
};

/**
Expand Down
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./assert.js";
export * from "./hex.js";
export * from "./message.js";
export * from "./keccak256.js";
13 changes: 13 additions & 0 deletions src/utils/keccak256.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Hex } from "@noble/curves/abstract/utils";
import { keccak_256 as keccak_256Module } from "@noble/hashes/sha3";

/**
* Returns the keccak-256 hash of the data. It is used in the Nil blockchain.
* @param data - The data to hash.
* @returns The keccak-256 hash.
*/
const keccak_256 = (data: Hex) => {
return keccak_256Module(data);
};

export { keccak_256 };
Loading
Loading