From a1d3c557e19b68e13587a0d520b82813179fa1e3 Mon Sep 17 00:00:00 2001 From: ppsimatikas Date: Mon, 27 May 2024 14:43:31 +0300 Subject: [PATCH] Add Code Coverage (#52) Co-authored-by: kosmotrade --- .github/workflows/pull-request.yaml | 5 +- .nvmrc | 1 + README.md | 3 + jest.config.json | 12 +++- package.json | 2 + src/network.ts | 2 + src/utils/kdf.ts | 2 +- tests/unit/utils.kdf.test.ts | 10 +++ tests/unit/utils.signature.test.ts | 30 ++++++++- tests/unit/utils.transaction.test.ts | 26 +++++++- tests/unit/wc.handlers.test.ts | 91 +++++++++++++++++++++++++++- 11 files changed, 175 insertions(+), 9 deletions(-) create mode 100644 .nvmrc diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index 3743a9a..e38beac 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -20,7 +20,6 @@ jobs: - name: Install & Build run: yarn && yarn build - - name: Lint & Unit Test + - name: Lint & Unit Test & Coverage run: | - yarn lint - yarn test unit + yarn verify diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..9bcccb9 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v20.13.1 diff --git a/README.md b/README.md index 09156fb..27c3940 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,9 @@ Intended to be used on server-side applications only. ## Local Testing +This project requires the Node.js version 20.0.0+. +If you are using nvm, you can run `nvm use` and use the node version in `.nvmrc`. + ```sh # Install yarn diff --git a/jest.config.json b/jest.config.json index 41a336a..1ce45b5 100644 --- a/jest.config.json +++ b/jest.config.json @@ -1,4 +1,12 @@ { "preset": "ts-jest", - "testEnvironment": "node" -} \ No newline at end of file + "testEnvironment": "node", + "coverageThreshold": { + "global": { + "branches": 60, + "functions": 60, + "lines": 60, + "statements": 60 + } + } +} diff --git a/package.json b/package.json index e881021..d07a75a 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "build": "rm -rf ./dist && tsc", "lint": "eslint . --ignore-pattern dist/", "test": "jest --testTimeout 30000", + "coverage": "yarn test --coverage", + "verify": "yarn lint && yarn coverage unit", "fmt": "prettier --write '{src,examples,tests}/**/*.{js,jsx,ts,tsx}'" }, "devDependencies": { diff --git a/src/network.ts b/src/network.ts index 392c3cb..b83b1cb 100644 --- a/src/network.ts +++ b/src/network.ts @@ -7,6 +7,7 @@ import { arbitrum, optimism, optimismSepolia, + localhost, } from "viem/chains"; // All supported networks @@ -18,6 +19,7 @@ const SUPPORTED_NETWORKS = createNetworkMap([ arbitrum, optimism, optimismSepolia, + localhost, ]); interface NetworkFields { diff --git a/src/utils/kdf.ts b/src/utils/kdf.ts index da9fc8c..7284783 100644 --- a/src/utils/kdf.ts +++ b/src/utils/kdf.ts @@ -12,7 +12,7 @@ export function najPublicKeyStrToUncompressedHexPoint( export async function deriveChildPublicKey( parentUncompressedPublicKeyHex: string, signerId: string, - path = "" + path: string = "" ): Promise { const ec = new EC("secp256k1"); const scalar = await sha256Hash( diff --git a/tests/unit/utils.kdf.test.ts b/tests/unit/utils.kdf.test.ts index 44add24..8c06f2c 100644 --- a/tests/unit/utils.kdf.test.ts +++ b/tests/unit/utils.kdf.test.ts @@ -10,6 +10,8 @@ const DECOMPRESSED_HEX = "04a410e78ef8a4f81ffc7e1f2e60c6fd6ccd5ed1689ea83b980215a58ded51871d16b2c99f3772e6b017b35a2367883552ea6b545f82552e3b05bae56975d40241"; const CHILD_PK = "04445b302250c5ba69e6a45d39b73a4cefc99a7e6e75ac164080c8bc68aa8c16fc332cf9b485f0f8ed0d815affdf3f9ad7e450c2658351fb09de2ad54e0f60795d"; +const CHILD_PK_NO_PATH = + "043e6054b729c98e7b5fd46d4c182e7e243c1b035290cf3e5db1cb0e032382dd5065675c05934a1f7cfdf524aa8af2ede5251f03ba7ea5f154cdbadd65d0e9c36a"; describe("Crypto Functions", () => { it("converts NEAR public key string to uncompressed hex point", () => { @@ -27,6 +29,14 @@ describe("Crypto Functions", () => { expect(result).toEqual(CHILD_PK); }); + it("derives child public key without path", async () => { + const parentHex = DECOMPRESSED_HEX; + const signerId = "ethdenver2024.testnet"; + const result = await deriveChildPublicKey(parentHex, signerId); + expect(result).toMatch(/^04[0-9a-f]+$/); + expect(result).toEqual(CHILD_PK_NO_PATH); + }); + it("converts uncompressed hex point to EVM address", () => { const uncompressedHex = CHILD_PK; const result = uncompressedHexPointToEvmAddress(uncompressedHex); diff --git a/tests/unit/utils.signature.test.ts b/tests/unit/utils.signature.test.ts index 49af33e..0a342ce 100644 --- a/tests/unit/utils.signature.test.ts +++ b/tests/unit/utils.signature.test.ts @@ -1,4 +1,7 @@ -import { signatureFromTxHash } from "../../src/utils/signature"; +import { + signatureFromTxHash, + pickValidSignature, +} from "../../src/utils/signature"; describe("utility: get Signature", () => { const url: string = "https://archival-rpc.testnet.near.org"; @@ -33,3 +36,28 @@ describe("utility: get Signature", () => { ); }); }); + +describe("utility: pickValidSignature", () => { + const sig0 = "0x88LS5pkj99pd6B6noZU6sagQ1QDwHHoSy3qpHr5xLNsR"; + const sig1 = "0xHaG9L4HnP69v6wSnAmKfzsCUhDaVMRZWNGhGqnepsMTD"; + + it("No signature is valid, should throw error", async () => { + expect(() => pickValidSignature([false, false], [sig0, sig1])) + .toThrow("Invalid signature"); + }); + + it("both sig0 and sig1 are valid, should return sig0", async () => { + const sig = pickValidSignature([true, true], [sig0, sig1]); + expect(sig).toEqual(sig0); + }); + + it("sig0 is valid, should return sig0", async () => { + const sig = pickValidSignature([true, false], [sig0, sig1]); + expect(sig).toEqual(sig0); + }); + + it("sig1 is valid, should return sig1", async () => { + const sig = pickValidSignature([false, true], [sig0, sig1]); + expect(sig).toEqual(sig1); + }); +}); diff --git a/tests/unit/utils.transaction.test.ts b/tests/unit/utils.transaction.test.ts index 76766c7..549a5fc 100644 --- a/tests/unit/utils.transaction.test.ts +++ b/tests/unit/utils.transaction.test.ts @@ -1,5 +1,5 @@ import { TransactionWithSignature } from "../../src"; -import { buildTxPayload, addSignature } from "../../src/utils/transaction"; +import { buildTxPayload, addSignature, toPayload } from "../../src/utils/transaction"; describe("Transaction Builder Functions", () => { it("buildTxPayload", async () => { @@ -11,6 +11,15 @@ describe("Transaction Builder Functions", () => { 36, 9, 101, 199, 230, 132, 140, 98, 211, 7, 68, 130, 233, 88, 145, 179, ]); }); + + it("fails: toPayload", async () => { + const txHash = + "0x02e783aa36a7808309e8bb84773f7cbb8094deadbeef0000000000000000000000000b00b1e50180c00"; + expect(() => toPayload(txHash)).toThrow( + `Payload Hex must have 32 bytes: ${txHash}` + ); + }); + it("addSignature", async () => { const testTx: TransactionWithSignature = { transaction: @@ -27,4 +36,19 @@ describe("Transaction Builder Functions", () => { "0x02f86b83aa36a780845974e6f084d0aa7af08094deadbeef0000000000000000000000000b00b1e50180c001a0ef532579e267c932b959a1adb9e455ac3c5397d0473471c4c3dd5d62fd4d7edea07c195e658c713d601d245311a259115bb91ec87c86acb07c03bd9c1936a6a9e8" ); }); + + it("fails: addSignature", async () => { + const testTx: TransactionWithSignature = { + transaction: + "0x02e883aa36a780845974e6f084d0aa7af08094deadbeef0000000000000000000000000b00b1e50180c0", + signature: { + big_r: "02EF532579E267C932B959A1ADB9E455AC3C5397D0473471C4C3DD5D62FD4D7EDE", + big_s: "7C195E658C713D601D245311A259115BB91EC87C86ACB07C03BD9C1936A6A9E8", + }, + }; + const sender = "0xInvalidSenderAddress"; + expect(() => addSignature(testTx, sender)).toThrow( + "Signature is not valid" + ); + }); }); diff --git a/tests/unit/wc.handlers.test.ts b/tests/unit/wc.handlers.test.ts index e905b8c..ede0d1a 100644 --- a/tests/unit/wc.handlers.test.ts +++ b/tests/unit/wc.handlers.test.ts @@ -5,7 +5,7 @@ import { offChainRecovery, wcRouter, } from "../../src/wallet-connect/handlers"; -import { MessageData } from "../../src/types/types"; +import { MessageData, TypedMessageData } from "../../src/types/types"; describe("Wallet Connect", () => { const chainId = "eip155:11155111"; @@ -33,6 +33,20 @@ describe("Wallet Connect", () => { ]); }); + it("fail with wrong method", async () => { + const messageString = "Hello!"; + const request = { + method: "eth_fail", + params: [from, toHex(messageString)], + }; + + await expect(wcRouter( + request.method, + chainId, + request.params as PersonalSignParams + )).rejects.toThrow("Unhandled session_request method: eth_fail"); + }); + it("opensea login", async () => { const request = { method: "personal_sign", @@ -277,5 +291,80 @@ Challenge: 4113fc3ab2cc60f5d595b2e55349f1eec56fd0c70d4287081fe7156848263626` "0x491e245db3914b85807f3807f2125b9ed9722d0e9f3fa0fe325b31893fa5e693387178ae4a51f304556c1b2e9dd24f1120d073f93017af006ad801a639214ea61b" ); }); + + it("recovering eth_signTypedData", async () => { + // TODO: Find a real examples of Ethereum apps and simulate them on the test + const recoveryData = { + type: "eth_signTypedData", + data: { + address: "0xf11c22d61ecd7b1adcb6b43542fe8a96b9328dc7", + message: { + from: { + name: "Cow", + wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + }, + to: { + name: "Bob", + wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", + }, + contents: "Hello, Bob!", + }, + types: { + Person: [ + { name: "name", type: "string" }, + { name: "wallet", type: "address" }, + ], + Mail: [ + { name: "from", type: "Person" }, + { name: "to", type: "Person" }, + { name: "contents", type: "string" }, + ], + }, + primaryType: "Mail", + domain: { + name: "Ether Mail", + version: "1", + chainId: 1, + verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", + } + } as TypedMessageData, + }; + const r = + "0x491E245DB3914B85807F3807F2125B9ED9722D0E9F3FA0FE325B31893FA5E693"; + const s = + "0x387178AE4A51F304556C1B2E9DD24F1120D073F93017AF006AD801A639214EA6"; + const sigs: [Hex, Hex] = [ + serializeSignature({ r, s, yParity: 0 }), + serializeSignature({ r, s, yParity: 1 }), + ]; + + await expect(offChainRecovery(recoveryData, sigs)).rejects.toThrow( + "Invalid signature" + ); + }); + + it("fail with wrong type", async () => { + const recoveryData = { + type: "wrong_type", + data: { + address: "0xf11c22d61ecd7b1adcb6b43542fe8a96b9328dc7", + message: { + raw: "0x57656c636f6d6520746f204f70656e536561210a0a436c69636b20746f207369676e20696e20616e642061636365707420746865204f70656e536561205465726d73206f662053657276696365202868747470733a2f2f6f70656e7365612e696f2f746f732920616e64205072697661637920506f6c696379202868747470733a2f2f6f70656e7365612e696f2f70726976616379292e0a0a5468697320726571756573742077696c6c206e6f742074726967676572206120626c6f636b636861696e207472616e73616374696f6e206f7220636f737420616e792067617320666565732e0a0a57616c6c657420616464726573733a0a3078663131633232643631656364376231616463623662343335343266653861393662393332386463370a0a4e6f6e63653a0a63336432623238622d623964652d346239662d383935362d316336663739373133613431", + }, + } as MessageData, + }; + const r = + "0x491E245DB3914B85807F3807F2125B9ED9722D0E9F3FA0FE325B31893FA5E693"; + const s = + "0x387178AE4A51F304556C1B2E9DD24F1120D073F93017AF006AD801A639214EA6"; + const sigs: [Hex, Hex] = [ + serializeSignature({ r, s, yParity: 0 }), + serializeSignature({ r, s, yParity: 1 }), + ]; + + await expect(offChainRecovery(recoveryData, sigs)).rejects.toThrow( + "Invalid Path" + ); + }); }); });