diff --git a/.changeset/clever-nails-run.md b/.changeset/clever-nails-run.md new file mode 100644 index 00000000..e0257904 --- /dev/null +++ b/.changeset/clever-nails-run.md @@ -0,0 +1,5 @@ +--- +"@nilfoundation/niljs": patch +--- + +Enable ssz serialization diff --git a/.changeset/thirty-buses-rest.md b/.changeset/thirty-buses-rest.md new file mode 100644 index 00000000..850cc012 --- /dev/null +++ b/.changeset/thirty-buses-rest.md @@ -0,0 +1,5 @@ +--- +"@nilfoundation/niljs": patch +--- + +Add WalletClient unit tests diff --git a/package-lock.json b/package-lock.json index 155bcfda..5d617f4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,9 +20,7 @@ "@commitlint/cli": "^19.3.0", "@commitlint/config-conventional": "^19.2.2", "@rollup/plugin-node-resolve": "^15.2.3", - "@types/elliptic": "^6.4.18", "@vitest/coverage-v8": "^1.6.0", - "elliptic": "^6.5.5", "rimraf": "^5.0.7", "rollup": "^4.17.2", "rollup-plugin-dts": "^6.1.0", @@ -2464,15 +2462,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/@types/bn.js": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.5.tgz", - "integrity": "sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/conventional-commits-parser": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", @@ -2482,15 +2471,6 @@ "@types/node": "*" } }, - "node_modules/@types/elliptic": { - "version": "6.4.18", - "resolved": "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.18.tgz", - "integrity": "sha512-UseG6H5vjRiNpQvrhy4VF/JXdA3V/Fp5amvveaL+fs28BZ6xIKJBPnUPRlEaZpysD9MbpfaLi8lbl7PGUAkpWw==", - "dev": true, - "dependencies": { - "@types/bn.js": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -2935,12 +2915,6 @@ "node": ">=4" } }, - "node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, "node_modules/boxen": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", @@ -3063,12 +3037,6 @@ "wcwidth": "^1.0.1" } }, - "node_modules/brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", - "dev": true - }, "node_modules/brotli-size": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/brotli-size/-/brotli-size-4.0.0.tgz", @@ -3777,21 +3745,6 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, - "node_modules/elliptic": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.5.tgz", - "integrity": "sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw==", - "dev": true, - "dependencies": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -4658,16 +4611,6 @@ "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", "dev": true }, - "node_modules/hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -4680,17 +4623,6 @@ "node": ">= 0.4" } }, - "node_modules/hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "dev": true, - "dependencies": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, "node_modules/hosted-git-info": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz", @@ -5970,18 +5902,6 @@ "node": ">=4" } }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true - }, - "node_modules/minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", - "dev": true - }, "node_modules/minimatch": { "version": "9.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", diff --git a/package.json b/package.json index a295681b..36091188 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "require": "./dist/niljs.cjs.js" } }, + "sideEffects": false, "description": "Typescript library to interact with the Nil blockchain. Can be used in the browser or in Node.js.", "scripts": { "test": "vitest -c ./test/vitest.config.ts", @@ -50,9 +51,7 @@ "@commitlint/cli": "^19.3.0", "@commitlint/config-conventional": "^19.2.2", "@rollup/plugin-node-resolve": "^15.2.3", - "@types/elliptic": "^6.4.18", "@vitest/coverage-v8": "^1.6.0", - "elliptic": "^6.5.5", "rimraf": "^5.0.7", "rollup": "^4.17.2", "rollup-plugin-dts": "^6.1.0", diff --git a/src/clients/PublicClient.test.ts b/src/clients/PublicClient.test.ts index 0aa215f4..033b6364 100644 --- a/src/clients/PublicClient.test.ts +++ b/src/clients/PublicClient.test.ts @@ -1,12 +1,12 @@ import { defaultAddress } from "../../test/mocks/address.js"; -import { endpoint } from "../../test/mocks/endpoint.js"; import { rawMsg } from "../../test/mocks/message.js"; import { masterShardId } from "../../test/mocks/shard.js"; +import { testEnv } from "../../test/testEnv.js"; import { addHexPrefix } from "../index.js"; import { PublicClient } from "./PublicClient.js"; const client = new PublicClient({ - endpoint, + endpoint: testEnv.endpoint, }); test("getBlockByHash", async ({ expect }) => { diff --git a/src/clients/PublicClient.ts b/src/clients/PublicClient.ts index b4ea6f45..d5a7e301 100644 --- a/src/clients/PublicClient.ts +++ b/src/clients/PublicClient.ts @@ -290,6 +290,7 @@ class PublicClient extends BaseClient { /** * getGasPrice returns the gas price in wei. + * @param shardId - The shard id. * @returns The gas price. */ public async getGasPrice(shardId: number): Promise { @@ -297,6 +298,17 @@ class PublicClient extends BaseClient { return stubGasPrice; } + + /** + * estimateGasLimit returns the gas limit. + * @param shardId - The shard id. + * @returns The gas limit. + */ + public async estimateGasLimit(shardId: number): Promise { + const stubGasLimit = BigInt(1000000); + + return stubGasLimit; + } } export { PublicClient }; diff --git a/src/clients/WalletClient.test.ts b/src/clients/WalletClient.test.ts index c0006336..2bed6084 100644 --- a/src/clients/WalletClient.test.ts +++ b/src/clients/WalletClient.test.ts @@ -1,75 +1,47 @@ -import { endpoint } from "../../test/mocks/endpoint.js"; - -import abi from "../../test/mocks/contracts/simpleStorage/bin/SimpleStorage.abi"; -import { type IMessage, LocalKeySigner, generatePrivateKey } from "../index.js"; +import { defaultAddress } from "../../test/mocks/address.js"; +import { bytecode as precompiledContractBytecode } from "../../test/mocks/contracts/simpleStorage/bytecode.js"; +import { testEnv } from "../../test/testEnv.js"; +import { type IMessage, LocalKeySigner, addHexPrefix } from "../index.js"; import { WalletClient } from "./WalletClient.js"; const client = new WalletClient({ - endpoint, + endpoint: testEnv.endpoint, signer: new LocalKeySigner({ - privateKey: generatePrivateKey(), + privateKey: testEnv.localPrivKey, }), }); -test("sendMessage", async ({ expect }) => { - const newMessage = { - to: "0x1234", - data: 100, - } as unknown as IMessage; - - const hash = await client.sendMessage(newMessage); - - expect(hash).toBeDefined(); -}); - -test("sendMessage with from field", async ({ expect }) => { - const newMessage = { - from: "0x1234", - to: "0x1234", - data: 100, - } as unknown as IMessage; +test("prepareMessage", async () => { + const message = { + to: addHexPrefix(defaultAddress), + }; - const hash = await client.sendMessage(newMessage); + const preparedMessage = await client.prepareMessage(message as IMessage); - expect(hash).toBeDefined(); + expect(preparedMessage.from).toBeDefined(); + expect(preparedMessage.gasPrice).toBeDefined(); }); -test("sendMessage with from field and shouldValidate false", async ({ - expect, -}) => { - const newMessage = { - from: "0x1234", - to: "0x1234", - data: 100, - } as unknown as IMessage; +test("sendMessage", async () => { + const message = { + to: addHexPrefix(defaultAddress), + value: 0n, + }; - const hash = await client.sendMessage(newMessage, { shouldValidate: false }); + const result = await client.sendMessage(message); - expect(hash).toBeDefined(); + expect(result).toBeDefined(); }); -test("sendRawMessage", async ({ expect }) => { - const newMessage = { - to: "0x1234", - data: 100, - } as unknown as IMessage; - const signedMessage = client.signMessage(newMessage); - - const hash = await client.sendRawMessage(signedMessage); - - expect(hash).toBeDefined(); -}); - -test("Deploy contract", async ({ expect }) => { - const newMessage = { - data: 100, - } as unknown as IMessage; - - const hash = await client.deployContract({ - bytecode: new Uint8Array(), - args: new Uint8Array(), - abi: abi, +test("deployContract", async () => { + const result = await client.deployContract({ + deployData: { + bytecode: precompiledContractBytecode, + }, }); - expect(hash).toBeDefined(); + expect(result).toBeDefined(); }); + +// TODO: implement this test and this feature +// test("deployContract with constructor", async () => { diff --git a/src/clients/WalletClient.ts b/src/clients/WalletClient.ts index 3a631d50..1937dd39 100644 --- a/src/clients/WalletClient.ts +++ b/src/clients/WalletClient.ts @@ -1,13 +1,22 @@ import invariant from "tiny-invariant"; +import { prepareDeployData } from "../encoding/deployData.js"; import { messageToSsz, signedMessageToSsz } from "../encoding/toSsz.js"; -import { type IReceipt, getShardIdFromAddress, toHex } from "../index.js"; +import { + type IReceipt, + addHexPrefix, + getShardIdFromAddress, + toHex, +} from "../index.js"; import type { ISigner } from "../signers/index.js"; import type { IMessage } from "../types/IMessage.js"; import { assertIsValidMessage } from "../utils/assert.js"; import { startPollingUntilCondition } from "../utils/polling.js"; import { PublicClient } from "./PublicClient.js"; +import { emptyAddress } from "./constants.js"; import type { IWalletClientConfig } from "./types/ClientConfigs.js"; -import type { IDeployContractOption } from "./types/IDeployContractOption.js"; +import type { IDeployContractData } from "./types/IDeployContractData.js"; +import type { IDeployContractOptions } from "./types/IDeployContractOptions.js"; +import type { ISendMessage } from "./types/ISendMessage.js"; import type { ISendMessageOptions } from "./types/ISendMessageOptions.js"; import type { ISignMessageOptions } from "./types/ISignMessageOptions.js"; @@ -33,7 +42,8 @@ class WalletClient extends PublicClient { this.signer = config.signer; const address = this.signer.getAddress(); - this.shardId = getShardIdFromAddress(address); + // TODO - get shardId from address and remove default value + this.shardId = 0 ?? getShardIdFromAddress(address); } /** @@ -41,19 +51,28 @@ class WalletClient extends PublicClient { * @param message - The message to send. * @returns The prepared message. */ - private async prepareMessage(message: IMessage): Promise { - const { gasPrice } = message; + public async prepareMessage(message: ISendMessage): Promise { const finalMsg = { ...message, from: message.from ? message.from : this.signer.getAddress(), + data: message.data ?? Uint8Array.from([]), }; - if (!gasPrice) { - const gasPrice = await this.getGasPrice(this.shardId); - finalMsg.gasPrice = gasPrice; - } + const promises = [ + message.seqno ?? + this.getMessageCount(this.shardId, finalMsg.from, "latest"), + message.gasPrice ?? this.getGasPrice(this.shardId), + message.gasLimit ?? this.estimateGasLimit(this.shardId), + ] as const; - return finalMsg; + const [seqno, gasPrice, gasLimit] = await Promise.all(promises); + + return { + ...finalMsg, + seqno, + gasPrice, + gasLimit, + }; } /** @@ -74,7 +93,7 @@ class WalletClient extends PublicClient { * const hash = await client.sendMessage(message); */ public async sendMessage( - message: IMessage, + message: ISendMessage, { shouldValidate = true } = {} as ISendMessageOptions, ): Promise { const preparedMsg = await this.prepareMessage(message); @@ -84,7 +103,7 @@ class WalletClient extends PublicClient { shouldValidate: false, }); - return await this.sendRawMessage(signedMessage); + return await this.sendRawMessage(addHexPrefix(toHex(signedMessage))); } /** @@ -111,11 +130,9 @@ class WalletClient extends PublicClient { "Serialized message is required to sign a message.", ); - const signature = this.signer.sign(serializedMessage); - return signedMessageToSsz({ ...message, - ...signature, + ...this.signer.sign(serializedMessage), }); } @@ -133,26 +150,33 @@ class WalletClient extends PublicClient { * const contract = Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); * const hash = await client.deployContract(contract); */ - public async deployContract({ - bytecode, - ...rest - }: IDeployContractOption): Promise { - const hash = await this.sendMessage({ - data: toHex(bytecode), - ...rest, - } as IMessage); + public async deployContract( + { deployData, ...restData }: IDeployContractData, + { shouldValidate = true }: IDeployContractOptions = {}, + ): Promise { + const hash = await this.sendMessage( + { + data: prepareDeployData(deployData), + value: 0n, + to: emptyAddress, + ...restData, + }, + { shouldValidate }, + ); // in the future we want to use subscribe method to get the receipt // for now it is simple short polling const receipt = await startPollingUntilCondition( - async () => await this.getMessageReceiptByHash(this.shardId, hash), - (receipt) => receipt !== undefined, + () => this.getMessageReceiptByHash(this.shardId, hash), + (receipt) => Boolean(receipt), 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."); + // here it is now always false but we need a fix from the node (add money) + invariant( + receipt?.success, + `Contract deployment failed. Receipt: ${JSON.stringify(receipt)}`, + ); return hash; } diff --git a/src/clients/constants.ts b/src/clients/constants.ts new file mode 100644 index 00000000..dc45587e --- /dev/null +++ b/src/clients/constants.ts @@ -0,0 +1,3 @@ +const emptyAddress = "0x0000000000000000000000000000000000000000"; + +export { emptyAddress }; diff --git a/src/clients/types/IDeployContractData.ts b/src/clients/types/IDeployContractData.ts new file mode 100644 index 00000000..bf0e14d9 --- /dev/null +++ b/src/clients/types/IDeployContractData.ts @@ -0,0 +1,11 @@ +import type { IDeployData } from "./IDeployData.js"; +import type { ISendMessage } from "./ISendMessage.js"; + +/** + * Data to send a deploy contract message. + */ +type IDeployContractData = { + deployData: IDeployData; +} & Omit; + +export type { IDeployContractData }; diff --git a/src/clients/types/IDeployContractOptions.ts b/src/clients/types/IDeployContractOptions.ts new file mode 100644 index 00000000..034607b7 --- /dev/null +++ b/src/clients/types/IDeployContractOptions.ts @@ -0,0 +1,8 @@ +import type { ISendMessageOptions } from "./ISendMessageOptions.js"; + +/** + * The options for deploying a contract. + */ +type IDeployContractOptions = ISendMessageOptions; + +export type { IDeployContractOptions }; diff --git a/src/clients/types/IDeployData.ts b/src/clients/types/IDeployData.ts new file mode 100644 index 00000000..fd29ae42 --- /dev/null +++ b/src/clients/types/IDeployData.ts @@ -0,0 +1,22 @@ +import type { Hex } from "@noble/curves/abstract/utils"; +import type { Abi } from "abitype"; + +/** + * Deploy data is a data structure that contains information to deploy a contract. + */ +type IDeployData = { + /** + * Compiled contract bytecode. + */ + bytecode: Hex; + /** + * The contract's constructor arguments. + */ + args?: Uint8Array; + /** + * The contract's Application Binary Interface (ABI). + */ + abi?: Abi | readonly unknown[]; +}; + +export type { IDeployData }; diff --git a/src/clients/types/ISendMessage.ts b/src/clients/types/ISendMessage.ts new file mode 100644 index 00000000..5c054078 --- /dev/null +++ b/src/clients/types/ISendMessage.ts @@ -0,0 +1,20 @@ +import type { Hex } from "@noble/curves/abstract/utils"; +import type { IMessage } from "../../index.js"; + +/** + * Data structure for the send message request + */ +type ISendMessage = { + to: string; + value: bigint; + from?: string; + seqno?: number; + gasPrice?: bigint; + gasLimit?: bigint; + data?: Hex; +} & Omit< + IMessage, + "to" | "value" | "from" | "seqno" | "gasPrice" | "gasLimit" | "data" +>; + +export type { ISendMessage }; diff --git a/src/clients/types/ISignMessageOptions.ts b/src/clients/types/ISignMessageOptions.ts index 5fe651ef..94fb4612 100644 --- a/src/clients/types/ISignMessageOptions.ts +++ b/src/clients/types/ISignMessageOptions.ts @@ -1,14 +1,8 @@ +import type { ISendMessageOptions } from "./ISendMessageOptions.js"; + /** * Options for signing a message. */ -type ISignMessageOptions = { - /** - * If true, the message will be validated before signing. - * If the message is invalid, an error will be thrown. - * If false, the message will not be validated before sending. - * @default true - */ - shouldValidate?: boolean; -}; +type ISignMessageOptions = ISendMessageOptions; export type { ISignMessageOptions }; diff --git a/src/encoding/deployData.test.ts b/src/encoding/deployData.test.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/encoding/deployData.ts b/src/encoding/deployData.ts new file mode 100644 index 00000000..82b33271 --- /dev/null +++ b/src/encoding/deployData.ts @@ -0,0 +1,13 @@ +import type { IDeployData } from "../clients/types/IDeployData.js"; +import { removeHexPrefix } from "../index.js"; +import { hexToBytes } from "./fromHex.js"; + +/** + * Prepare deploy data. + * @param d - Deploy data + * @returns Deploy data as bytes + */ +const prepareDeployData = (d: IDeployData) => + hexToBytes(removeHexPrefix(d.bytecode)); + +export { prepareDeployData }; diff --git a/src/encoding/fromSsz.ts b/src/encoding/fromSsz.ts index 4e28ad4e..8c1db26a 100644 --- a/src/encoding/fromSsz.ts +++ b/src/encoding/fromSsz.ts @@ -1,15 +1,6 @@ -import { - type IMessage, - type ISignature, - bytesToString, - toHex, -} from "../index.js"; +import { type IMessage, addHexPrefix, toHex } from "../index.js"; import type { ISignedMessage } from "../types/ISignedMessage.js"; -import { - SszMessageSchema, - SszSignatureSchema, - SszSignedMessageSchema, -} from "./ssz.js"; +import { SszMessageSchema, SszSignedMessageSchema } from "./ssz.js"; /** * Convert SSZ encoded message to a message object. @@ -21,26 +12,9 @@ const sszToMessage = (ssz: Uint8Array): IMessage => { return { ...rest, - from: bytesToString(from), - to: bytesToString(to), - data: bytesToString(data), - signature: null, - }; -}; - -/** - * Convert SSZ encoded signature to a signature object. - * @param ssz - SSZ encoded signature - * @returns ISignature - Signature object - */ -const sszToSignature = (ssz: Uint8Array): ISignature => { - const { r, s, v, yParity } = SszSignatureSchema.deserialize(ssz); - - return { - r: toHex(r), - s: toHex(s), - v: v ? v : undefined, - yParity, + from: addHexPrefix(toHex(from)), + to: addHexPrefix(toHex(to)), + data: new Uint8Array(data), }; }; @@ -50,19 +24,14 @@ const sszToSignature = (ssz: Uint8Array): ISignature => { * @returns ISignedMessage - Signed message object */ const sszToSignedMessage = (ssz: Uint8Array): ISignedMessage => { - const { from, to, data, v, r, s, ...rest } = - SszSignedMessageSchema.deserialize(ssz); + const { from, to, data, ...rest } = SszSignedMessageSchema.deserialize(ssz); return { ...rest, - from: bytesToString(from), - to: bytesToString(to), - data: bytesToString(data), - signature: null, - v: v ? v : undefined, - r: toHex(r), - s: toHex(s), + from: addHexPrefix(toHex(from)), + to: addHexPrefix(toHex(to)), + data: new Uint8Array(data), }; }; -export { sszToMessage, sszToSignature, sszToSignedMessage }; +export { sszToMessage, sszToSignedMessage }; diff --git a/src/encoding/ssz.ts b/src/encoding/ssz.ts index 2bae9acc..c4d2f350 100644 --- a/src/encoding/ssz.ts +++ b/src/encoding/ssz.ts @@ -1,42 +1,39 @@ import { ByteVectorType, ContainerType, - OptionalType, + ListBasicType, UintBigintType, UintNumberType, } from "@chainsafe/ssz"; -const Bytes32 = new ByteVectorType(32); -const Bytes96 = new ByteVectorType(96); -const Uint32 = new UintNumberType(4); -const UintBn64 = new UintBigintType(8); +const basicTypes = { + Uint8: new UintNumberType(1), + Uint64: new UintNumberType(8), + UintBn256: new UintBigintType(32), +}; + +const Bytes20 = new ByteVectorType(20); /** * SSZ schema for a message object. It includes all the fields of a message object. */ const SszMessageSchema = new ContainerType({ - index: Uint32, - shardId: Uint32, - from: Bytes32, - to: Bytes32, - value: UintBn64, - data: Bytes96, - seqno: Uint32, - signature: new OptionalType(Bytes96), - maxPriorityFeePerGas: UintBn64, - gasPrice: UintBn64, - maxFeePerGas: UintBn64, - chainId: Uint32, + seqno: basicTypes.Uint64, + gasPrice: basicTypes.UintBn256, + gasLimit: basicTypes.UintBn256, + from: Bytes20, + to: Bytes20, + value: basicTypes.UintBn256, + data: new ListBasicType(basicTypes.Uint8, 24576), }); /** * SSZ schema for a signature object. It includes all the fields of a signature object. */ const SszSignatureSchema = new ContainerType({ - r: Bytes32, - s: Bytes32, - v: new OptionalType(UintBn64), - yParity: Uint32, + r: basicTypes.UintBn256, + s: basicTypes.UintBn256, + v: basicTypes.Uint8, }); /** @@ -47,4 +44,4 @@ const SszSignedMessageSchema = new ContainerType({ ...SszSignatureSchema.fields, }); -export { SszMessageSchema, SszSignedMessageSchema, SszSignatureSchema }; +export { SszMessageSchema, SszSignedMessageSchema }; diff --git a/src/encoding/toSsz.ts b/src/encoding/toSsz.ts index d3b07950..7718c15e 100644 --- a/src/encoding/toSsz.ts +++ b/src/encoding/toSsz.ts @@ -1,53 +1,24 @@ import {} from "@chainsafe/ssz"; -import type { ISignature } from "../index.js"; +import { hexToBytes, removeHexPrefix } from "../index.js"; import type { IMessage } from "../types/IMessage.js"; import type { ISignedMessage } from "../types/ISignedMessage.js"; -import { - SszMessageSchema, - SszSignatureSchema, - SszSignedMessageSchema, -} from "./ssz.js"; -import { toBytes } from "./toBytes.js"; +import { SszMessageSchema, SszSignedMessageSchema } from "./ssz.js"; /** - * Process a message object to convert all string fields to bytes and BigInt fields to BigInt. + * Process a message before serializing it to SSZ. * @param message - Message object * @returns IMessage - Processed message object ready to be SSZ encoded. */ -const prepareMessage = ({ from, to, data, signature, ...rest }: IMessage) => ({ - ...rest, - from: toBytes(from), - to: toBytes(to), - data: toBytes(data), - signature: signature ? toBytes(signature) : null, -}); - -/** - * Process a signature object to convert all string fields to bytes and BigInt fields to BigInt. - * @param signature - Signature object - * @returns ISignature - Processed signature object ready to be SSZ encoded. - */ -const prepareSignature = ({ r, s, v, yParity }: ISignature) => ({ - r: toBytes(r), - s: toBytes(s), - v: v ? v : null, - yParity, -}); - -/** - * Process a signed message object to convert all string fields to bytes and BigInt fields to BigInt. - * @param signedMessage - Signed message object - * @returns ISignedMessage - Processed signed message object ready to be SSZ encoded. - */ -const processSignedMessage = ({ - v, - r, - s, - yParity, +const prepareMessage = ({ + from, + to, + data, ...rest -}: ISignedMessage) => ({ - ...prepareMessage(rest), - ...prepareSignature({ r, s, v, yParity }), +}: T) => ({ + ...rest, + from: hexToBytes(removeHexPrefix(from)), + to: hexToBytes(removeHexPrefix(to)), + data: Array.from(hexToBytes(data)), }); /** @@ -61,17 +32,6 @@ const messageToSsz = (message: IMessage): Uint8Array => { return serialized; }; -/** - * Convert a signature object to SSZ encoded Uint8Array. - * @param signature - Signature object - * @returns Uint8Array - SSZ encoded signature - */ -const signatureToSsz = (signature: ISignature): Uint8Array => { - const serialized = SszSignatureSchema.serialize(prepareSignature(signature)); - - return serialized; -}; - /** * Convert a signed message object to SSZ encoded Uint8Array. * @param message - Message object with signature @@ -80,10 +40,9 @@ const signatureToSsz = (signature: ISignature): Uint8Array => { * const serializedTx = signedMessageToSsz(signedMessage); */ const signedMessageToSsz = (message: ISignedMessage): Uint8Array => { - const serialized = SszSignedMessageSchema.serialize( - processSignedMessage(message), - ); + const serialized = SszSignedMessageSchema.serialize(prepareMessage(message)); + return serialized; }; -export { messageToSsz, signedMessageToSsz, signatureToSsz }; +export { messageToSsz, signedMessageToSsz }; diff --git a/src/signers/LocalKeySigner.test.ts b/src/signers/LocalKeySigner.test.ts index f2395f7d..40ec1db1 100644 --- a/src/signers/LocalKeySigner.test.ts +++ b/src/signers/LocalKeySigner.test.ts @@ -1,10 +1,9 @@ +import { secp256k1 } from "@noble/curves/secp256k1"; import { accounts } from "../../test/mocks/accounts.js"; +import { toHex } from "../index.js"; import { LocalKeySigner } from "./LocalKeySigner.js"; -import elliptic = require("elliptic"); -const ec = new elliptic.ec("secp256k1"); - -test("LocalKeySigner should return public key", async () => { +test("getPublicKey", async () => { const privateKey = accounts[0].privateKey; const signer = new LocalKeySigner({ privateKey }); @@ -13,6 +12,15 @@ test("LocalKeySigner should return public key", async () => { expect(publicKey).toBe(accounts[0].publicKey); }); +test("getAddress", async () => { + const privateKey = accounts[0].privateKey; + const signer = new LocalKeySigner({ privateKey }); + + const address = signer.getAddress(); + + expect(address).toBeDefined(); +}); + test("LocalKeySigner should throw error if invalid private key is provided", async () => { /** * The private key is invalid. @@ -21,7 +29,7 @@ test("LocalKeySigner should throw error if invalid private key is provided", asy expect(() => new LocalKeySigner({ privateKey })).toThrowError(); }); -test("Signature should be valid", async () => { +test("sign", async () => { const privateKey = accounts[0].privateKey; const signer = new LocalKeySigner({ privateKey }); const message = Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); @@ -30,8 +38,12 @@ test("Signature should be valid", async () => { * Sign the message with the private key and verify the signature with the public key. */ const signature = signer.sign(message); - const publicKey = ec.keyFromPublic(accounts[0].publicKey, "hex"); - const verified = publicKey.verify(message, signature); + + const verified = secp256k1.verify( + toHex(message), + signature, + signer.getPublicKey(), + ); expect(verified).toBe(true); }); diff --git a/src/signers/LocalKeySigner.ts b/src/signers/LocalKeySigner.ts index 8cd0e52e..bbe751b6 100644 --- a/src/signers/LocalKeySigner.ts +++ b/src/signers/LocalKeySigner.ts @@ -1,6 +1,6 @@ import type { Hex } from "@noble/curves/abstract/utils"; import { secp256k1 } from "@noble/curves/secp256k1"; -import { toHex } from "../encoding/toHex.js"; +import { addHexPrefix, removeHexPrefix } from "../index.js"; import { assertIsAddress, assertIsHexString, @@ -28,21 +28,23 @@ class LocalKeySigner implements ISigner { private address?: IAddress = undefined; constructor(config: ILocalKeySignerConfig) { - const { privateKey } = config; + const privateKey = addHexPrefix(config.privateKey); assertIsValidPrivateKey(privateKey); this.privateKey = privateKey; } public sign(data: Uint8Array): ISignature { - const signature = secp256k1.sign(data, this.privateKey); + const signature = secp256k1.sign( + removeHexPrefix(data), + removeHexPrefix(this.privateKey), + ); const { r, s, recovery } = signature; return { - r: toHex(r), - s: toHex(s), - v: recovery ? 28n : 27n, - yParity: recovery, + r, + s, + v: recovery, }; } diff --git a/src/signers/index.ts b/src/signers/index.ts index 1c7774bd..04409f2d 100644 --- a/src/signers/index.ts +++ b/src/signers/index.ts @@ -5,3 +5,4 @@ export * from "./types/ILocalKeySignerConfig.js"; export * from "./types/IPrivateKey.js"; export * from "./types/ISigner.js"; export * from "./types/ISignature.js"; +export * from "./privateKey.js"; diff --git a/src/signers/privateKey.test.ts b/src/signers/privateKey.test.ts new file mode 100644 index 00000000..ce34b19d --- /dev/null +++ b/src/signers/privateKey.test.ts @@ -0,0 +1,7 @@ +import { generateRandomPrivateKey } from "./privateKey.js"; + +test("generatePrivateKey", async ({ expect }) => { + const result = generateRandomPrivateKey(); + + expect(result).toBeDefined(); +}); diff --git a/src/signers/privateKey.ts b/src/signers/privateKey.ts new file mode 100644 index 00000000..68599bbc --- /dev/null +++ b/src/signers/privateKey.ts @@ -0,0 +1,13 @@ +import { secp256k1 } from "@noble/curves/secp256k1"; +import { type IPrivateKey, addHexPrefix, toHex } from "../index.js"; + +/** + * Generate a new private key. + * @returns Hex - Private key + * @example + * const privateKey = generatePrivateKey(); + */ +const generateRandomPrivateKey = (): IPrivateKey => + addHexPrefix(toHex(secp256k1.utils.randomPrivateKey())) as IPrivateKey; + +export { generateRandomPrivateKey }; diff --git a/src/signers/publicKey.test.ts b/src/signers/publicKey.test.ts index 915853bd..34fcd954 100644 --- a/src/signers/publicKey.test.ts +++ b/src/signers/publicKey.test.ts @@ -1,8 +1,9 @@ import { accounts } from "../../test/mocks/accounts.js"; -import { generatePrivateKey, getPublicKey } from "./publicKey.js"; +import { generateRandomPrivateKey } from "./privateKey.js"; +import { getPublicKey } from "./publicKey.js"; test("generatePrivateKey", async ({ expect }) => { - const result = generatePrivateKey(); + const result = generateRandomPrivateKey(); expect(result).toBeDefined(); }); diff --git a/src/signers/publicKey.ts b/src/signers/publicKey.ts index 5bcdada3..6797b206 100644 --- a/src/signers/publicKey.ts +++ b/src/signers/publicKey.ts @@ -1,12 +1,6 @@ import { type Hex, bytesToHex } from "@noble/curves/abstract/utils"; import { secp256k1 } from "@noble/curves/secp256k1"; -import { - type ISignature, - addHexPrefix, - removeHexPrefix, - toBytes, - toHex, -} from "../index.js"; +import { type ISignature, addHexPrefix, removeHexPrefix } from "../index.js"; import { keccak_256 } from "../utils/keccak256.js"; import type { IAddress } from "./types/IAddress.js"; import type { IPrivateKey } from "./types/IPrivateKey.js"; @@ -14,20 +8,14 @@ import type { IPrivateKey } from "./types/IPrivateKey.js"; /** * Returns the public key from the private key using the secp256k1 curve. */ -const getPublicKey = (privateKey: IPrivateKey): Hex => { - const publicKey = secp256k1.getPublicKey(removeHexPrefix(privateKey), false); +const getPublicKey = (privateKey: IPrivateKey, isCompressed = false): Hex => { + const publicKey = secp256k1.getPublicKey( + removeHexPrefix(privateKey), + isCompressed, + ); return addHexPrefix(bytesToHex(publicKey)); }; -/** - * Generate a new private key. - * @returns Hex - Private key - * @example - * const privateKey = generatePrivateKey(); - */ -const generatePrivateKey = (): IPrivateKey => - addHexPrefix(toHex(secp256k1.utils.randomPrivateKey())) as IPrivateKey; - const recoverPublicKey = ( messageHash: Hex | Uint8Array, signature: ISignature, @@ -41,13 +29,8 @@ const recoverPublicKey = ( * @returns Address in hex format */ const getAddressFromPublicKey = (publicKey: Hex): IAddress => { - const bytes = keccak_256(toBytes(removeHexPrefix(publicKey))); - return toHex(bytes) as IAddress; + const bytes = keccak_256(publicKey).slice(-20); + return addHexPrefix(bytesToHex(bytes)); }; -export { - getPublicKey, - generatePrivateKey, - recoverPublicKey, - getAddressFromPublicKey, -}; +export { getPublicKey, recoverPublicKey, getAddressFromPublicKey }; diff --git a/src/signers/types/ILocalKeySignerConfig.ts b/src/signers/types/ILocalKeySignerConfig.ts index 6f4e7e29..cf3d60c2 100644 --- a/src/signers/types/ILocalKeySignerConfig.ts +++ b/src/signers/types/ILocalKeySignerConfig.ts @@ -8,7 +8,7 @@ type ILocalKeySignerConfig = { * Private key to sign the data. * @example '0x4b3b4c4d4e4f505152535455565758595a6162636465666768696a6b6c6d6e6f' */ - privateKey: IPrivateKey; + privateKey: IPrivateKey | string; }; export type { ILocalKeySignerConfig }; diff --git a/src/signers/types/ISignature.ts b/src/signers/types/ISignature.ts index 7b62c436..cfde5a6e 100644 --- a/src/signers/types/ISignature.ts +++ b/src/signers/types/ISignature.ts @@ -1,13 +1,10 @@ -import type { Hex } from "@noble/curves/abstract/utils"; - /** * Interface for the Signature. It contains the r, s, and yParity values. */ type ISignature = { - r: Hex; - s: Hex; - v?: bigint; - yParity: number; + r: bigint; + s: bigint; + v: number; }; export type { ISignature }; diff --git a/src/types/IBlock.ts b/src/types/IBlock.ts index 6a418263..11648407 100644 --- a/src/types/IBlock.ts +++ b/src/types/IBlock.ts @@ -1,39 +1,21 @@ +import type { Hex } from "@noble/curves/abstract/utils"; + /** * The block type. */ type IBlock = { - /** - * The block id. - */ - id: number; - /** - * The previous block hash. - */ - prevBlock: string; - /** - * The smart contracts root hash. - */ - smartContractsRoot: string; - /** - * The messages root hash. - */ - messagesRoot: string; - /** - * The receipts root hash. - */ - receiptsRoot: string; - /** - * The child blocks root hash. - */ + id: string; + prevBlock: Hex; + smartContractsRoot: Hex; + inMessagesRoot: Hex; + outMessagesRoot: Hex; + outMessagesNum: number; + receiptsRoot: Hex; childBlocksRootHash: string; - /** - * The master chain hash. - */ - masterChainHash: string; - /** - * The logs bloom. - */ - logsBloom: string; + masterChainHash: Hex; + // biome-ignore lint/suspicious/noExplicitAny: need to investigate + logsBloom: any; + timestamp: number; }; export type { IBlock }; diff --git a/src/types/IMessage.ts b/src/types/IMessage.ts index f18b1342..c2d1a60b 100644 --- a/src/types/IMessage.ts +++ b/src/types/IMessage.ts @@ -4,18 +4,13 @@ import type { Hex } from "@noble/curves/abstract/utils"; * The interface for the message object. This object is used to represent a message in the network. */ interface IMessage { - index: number; - shardId: number; from: string; to: string; value: bigint; data: Hex; seqno: number; - signature?: string | null; - maxPriorityFeePerGas: bigint; gasPrice: bigint; - maxFeePerGas: bigint; - chainId: number; + gasLimit: bigint; } export type { IMessage }; diff --git a/src/utils/message.ts b/src/utils/address.ts similarity index 100% rename from src/utils/message.ts rename to src/utils/address.ts diff --git a/src/utils/assert.ts b/src/utils/assert.ts index a58ce5f6..895b159b 100644 --- a/src/utils/assert.ts +++ b/src/utils/assert.ts @@ -3,8 +3,8 @@ import invariant from "tiny-invariant"; import { type IBlock, isValidBlock } from "../index.js"; import type { IPrivateKey } from "../signers/index.js"; import type { IMessage } from "../types/IMessage.js"; +import { isAddress } from "./address.js"; import { isHexString } from "./hex.js"; -import { isAddress } from "./message.js"; /** * Checks if the value is a string. @@ -54,27 +54,16 @@ const assertIsValidPrivateKey = ( * @param message - The message to validate. */ const assertIsValidMessage = (message: IMessage) => { - const { chainId, maxPriorityFeePerGas, gasPrice, maxFeePerGas, to } = message; + const { gasPrice, to } = message; invariant( typeof to === "string" && isAddress(to), `Expected a valid 'to' address but got ${to}`, ); invariant( - typeof chainId === "number" && chainId > 0, - `Expected a valid 'chainId' but got ${chainId}`, - ); - invariant( - typeof gasPrice === "number" && gasPrice > 0, + (typeof gasPrice === "number" || typeof gasPrice === "bigint") && + gasPrice > 0, `Expected a valid 'gasPrice' but got ${gasPrice}`, ); - invariant( - typeof maxFeePerGas === "number" && maxFeePerGas > 0, - `Expected a valid 'maxFeePerGas' but got ${maxFeePerGas}`, - ); - invariant( - typeof maxPriorityFeePerGas === "number" && maxPriorityFeePerGas > 0, - `Expected a valid 'maxPriorityFeePerGas' but got ${maxPriorityFeePerGas}`, - ); }; /** diff --git a/src/utils/block.ts b/src/utils/block.ts index df425628..d43837a8 100644 --- a/src/utils/block.ts +++ b/src/utils/block.ts @@ -5,7 +5,7 @@ import type { IBlock } from "../index.js"; * @param block - The block to check. * @returns True if the block is valid, false otherwise. */ -const isValidBlock = (block: IBlock): boolean => { +const isValidBlock = (block: IBlock): block is IBlock => { return true; }; diff --git a/src/utils/hex.ts b/src/utils/hex.ts index d228d5b0..559d72a9 100644 --- a/src/utils/hex.ts +++ b/src/utils/hex.ts @@ -11,7 +11,7 @@ const isHexString = (value: Hex): value is Hex => { return ( typeof value === "string" && value.startsWith("0x") && - HEX_REGEX.test(removeHexPrefix(value)) + HEX_REGEX.test(removeHexPrefix(value) as string) ); }; @@ -20,9 +20,9 @@ const isHexString = (value: Hex): value is Hex => { * @param hex hex-string * @returns format: base16-string */ -const removeHexPrefix = (hex: Hex): string => { +const removeHexPrefix = (hex: Hex) => { if (typeof hex !== "string") { - throw new Error(`Expected a hex string but got ${hex}`); + return hex; } return hex.startsWith("0x") ? hex.slice(2) : hex; @@ -33,7 +33,7 @@ const removeHexPrefix = (hex: Hex): string => { * @param hex base16-string * @returns format: hex-string */ -const addHexPrefix = (hex: string): string => { +const addHexPrefix = (hex: string): `0x${string}` => { return `0x${removeHexPrefix(hex)}`; }; diff --git a/src/utils/index.ts b/src/utils/index.ts index 6f221441..c0505bdb 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,5 @@ export * from "./assert.js"; export * from "./hex.js"; -export * from "./message.js"; +export * from "./address.js"; export * from "./keccak256.js"; export * from "./block.js"; diff --git a/src/utils/keccak256.ts b/src/utils/keccak256.ts index 6d52ee93..71c8ed2f 100644 --- a/src/utils/keccak256.ts +++ b/src/utils/keccak256.ts @@ -1,5 +1,5 @@ import type { Hex } from "@noble/curves/abstract/utils"; -import { keccak_256 as keccak_256Module } from "@noble/hashes/sha3"; +import { keccak_256 as keccak_256Noble } from "@noble/hashes/sha3"; /** * Returns the keccak-256 hash of the data. It is used in the Nil blockchain. @@ -7,7 +7,7 @@ import { keccak_256 as keccak_256Module } from "@noble/hashes/sha3"; * @returns The keccak-256 hash. */ const keccak_256 = (data: Hex) => { - return keccak_256Module(data); + return keccak_256Noble(data); }; export { keccak_256 }; diff --git a/test/mocks/contracts/simpleStorage/abi.json b/test/mocks/contracts/simpleStorage/abi.json new file mode 100644 index 00000000..a7ca7087 --- /dev/null +++ b/test/mocks/contracts/simpleStorage/abi.json @@ -0,0 +1,22 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "newValue", + "type": "uint256" + } + ], + "name": "ValueChanged", + "type": "event" + }, + { + "inputs": [], + "name": "increment", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/test/mocks/contracts/simpleStorage/bytecode.ts b/test/mocks/contracts/simpleStorage/bytecode.ts new file mode 100644 index 00000000..30b7e4ed --- /dev/null +++ b/test/mocks/contracts/simpleStorage/bytecode.ts @@ -0,0 +1,4 @@ +const bytecode = + "0x6080604052348015600e575f80fd5b506101508061001c5f395ff3fe608060405234801561000f575f80fd5b5060043610610029575f3560e01c8063d09de08a1461002d575b5f80fd5b610035610037565b005b60015f8082825461004891906100bf565b925050819055507f93fe6d397c74fdf1402a8b72e47b68512f0510d7b98a4bc4cbdf6ac7108b3c595f5460405161007f9190610101565b60405180910390a1565b5f819050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f6100c982610089565b91506100d483610089565b92508282019050808211156100ec576100eb610092565b5b92915050565b6100fb81610089565b82525050565b5f6020820190506101145f8301846100f2565b9291505056fea26469706673582212200151a7fd2785bb9d825712b669f5faa0d4b843a26a04e3e64f4ca35fdeb830c464736f6c63430008190033"; + +export { bytecode }; diff --git a/test/mocks/endpoint.ts b/test/mocks/endpoint.ts deleted file mode 100644 index cc15d13d..00000000 --- a/test/mocks/endpoint.ts +++ /dev/null @@ -1,5 +0,0 @@ -const defaultRpcEndpoint = "http://127.0.0.1:8529"; - -const endpoint = process.env.RPC_ENDPOINT ?? defaultRpcEndpoint; - -export { endpoint }; diff --git a/test/testEnv.ts b/test/testEnv.ts new file mode 100644 index 00000000..9ef7e355 --- /dev/null +++ b/test/testEnv.ts @@ -0,0 +1,10 @@ +const defaultRpcEndpoint = "http://127.0.0.1:8529"; +const defaultPrivateKey = + "41285f03e8692676bf80a98e4052a008026427a7302ca97cb06edcd60689850b"; + +const testEnv = { + localPrivKey: process.env.LOCAL_PRIV_KEY ?? defaultPrivateKey, + endpoint: process.env.RPC_ENDPOINT ?? defaultRpcEndpoint, +} as const; + +export { testEnv }; diff --git a/tsconfig.json b/tsconfig.json index dfa856fe..2847a987 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,7 @@ "lib": ["ES2021"], "skipLibCheck": true, "noEmitOnError": true, - "types": ["vitest/globals"] + "types": ["vitest/globals"], + "resolveJsonModule": true } }