diff --git a/.changeset/light-bars-visit.md b/.changeset/light-bars-visit.md new file mode 100644 index 000000000..e20062613 --- /dev/null +++ b/.changeset/light-bars-visit.md @@ -0,0 +1,6 @@ +--- +"@lightsparkdev/lightspark-cli": patch +--- + +- Replace secp256k1 with @noble/curves: Same dependency swap — secp256k1 replaced with @noble/curves for key generation and validation. Removes native build + requirement. diff --git a/.changeset/spotty-yaks-lie.md b/.changeset/spotty-yaks-lie.md new file mode 100644 index 000000000..414f026d4 --- /dev/null +++ b/.changeset/spotty-yaks-lie.md @@ -0,0 +1,8 @@ +--- +"@lightsparkdev/core": patch +--- + +- Replace secp256k1 with @noble/curves: The native secp256k1 npm package (which requires native compilation/bindings) has been replaced with @noble/curves/secp256k1, a + pure JS implementation. This removes native build dependencies, improving portability and install reliability. Secp256k1SigningKey.sign() now uses @noble/curves API + internally (DER-encoded output is preserved). +- Add 3 new currencies: ZMW (Zambian Kwacha), AED (UAE Dirham), GTQ (Guatemalan Quetzal) — added to CurrencyUnit, conversion maps, formatting, and CurrencyMap type. diff --git a/.changeset/tender-jobs-call.md b/.changeset/tender-jobs-call.md new file mode 100644 index 000000000..9fc2e5749 --- /dev/null +++ b/.changeset/tender-jobs-call.md @@ -0,0 +1,9 @@ +--- +"@lightsparkdev/ui": patch +--- + +- CodeInput: Clicking the unified code input now moves the cursor to the first empty input field instead of wherever the user clicked, improving OTP entry UX. +- CardForm: CardFormContentFull now sets width: 100% for proper full-width layout. +- CardPage: New headerRightContent prop to render content on the right side of the card page header. +- DataManagerTable filters: AppliedButtonsContainer extracted to a shared component with max-width: 100% on buttons (prevents overflow for long filter values). +- New icon: BankSolid icon added (exported as CentralBankSolid). diff --git a/CLAUDE.md b/CLAUDE.md index 48f85e0a4..190b21a94 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,163 +1,68 @@ -# CLAUDE.md +# JS Monorepo -This file provides guidance to Claude Code when working in the js/ directory. +TypeScript monorepo for Lightning Network SDKs and internal applications. Yarn workspaces + Turbo. -## Overview +## Quick Reference -JavaScript/TypeScript monorepo for Lightspark's Lightning Network and UMA services. Public SDKs, internal applications, and shared packages using Yarn workspaces + Turbo. +| Action | Command | +|--------|---------| +| Install | `yarn` | +| Start apps | `yarn start site uma-bridge ops` | +| Build | `yarn build` | +| Test | `yarn test` | +| Lint + format | `yarn lint && yarn format` | +| Full checks | `yarn checks` | +| GraphQL regen | `yarn gql-codegen` | +| Clean all | `yarn clean-all` | ## Structure -- **packages/** - Shared libraries - - **core/** - Auth, utilities - - **lightspark-sdk/** - Public Lightning SDK - - **ui/** - React components, design system - - **private/** - Internal utilities, GraphQL clients -- **apps/examples/** - Public examples and CLI tools -- **apps/private/** - Internal apps (site, ops, uma-bridge) - -## Essential Commands - -### Setup -```bash -nvm use || nvm install # Match Node version -corepack enable && corepack prepare --activate # Enable Yarn -yarn # Install all workspace dependencies ``` - -### Development Workflow -```bash -# Start applications -yarn start site uma-bridge ops # Specific apps (@lightsparkdev/ prefix implied) -yarn start private # All private apps -yarn start examples # All examples - -# Code quality -yarn lint && yarn format # Lint + Prettier -yarn checks # Full validation: deps, lint, format, test, circular-deps - -# Building -yarn build # Build all workspaces with Turbo caching -yarn build --force # Rebuild without cache - -# Testing -yarn test # Run all tests -yarn workspace @lightsparkdev/ui test # Test specific workspace - -# GraphQL codegen -yarn gql-codegen # Regenerate TypeScript types from schemas +packages/ + core/ # Auth, utilities + lightspark-sdk/ # Public Lightning SDK + ui/ # React components + private/ # Internal utilities +apps/ + examples/ # Public examples + private/ # Internal apps (site, ops, uma-bridge) ``` -### Workspace Targeting +## Workspace Commands + ```bash -# From repo root +# Target specific workspace yarn workspace @lightsparkdev/ -# From workspace directory -cd apps/private/uma-bridge && yarn start +# Examples +yarn workspace @lightsparkdev/uma-bridge start +yarn workspace @lightsparkdev/ui test ``` -## Architecture & Patterns +## Code Patterns -### Monorepo Management -- **Yarn workspaces** with workspace protocol (`"@lightsparkdev/ui": "*"`) -- **Turbo** orchestrates builds with caching and parallelization +### Dependencies +- Use workspace protocol for internal deps: `"@lightsparkdev/ui": "*"` - Shared configs: `@lightsparkdev/{tsconfig,eslint-config}` -- Build artifacts in `dist/`, ignored by git ### GraphQL -- TypeScript types auto-generated via GraphQL Code Generator -- Schema variants per API surface (internal, third-party) -- Fragments/operations defined per app -- Real-time subscriptions for transaction updates -- **After Python schema changes**: run `yarn gql-codegen` from root - -### React Stack -- **Vite** - Dev server and bundler -- **Emotion** - CSS-in-JS styling -- **React Router** - Navigation -- **React Query** - Server state -- **Zustand** - Client state - -### Testing -- **Jest** - Unit/integration tests -- **React Testing Library** - Component tests -- **Cypress** - E2E tests -- Tests colocated with source (`.test.ts`, `.spec.ts`) - -## Configuration Files - -- **turbo.json** - Build pipeline, task dependencies, caching -- **package.json** (root) - Workspace definitions, scripts -- **packages/eslint-config/** - Shared linting rules -- **packages/tsconfig/** - TypeScript presets - -## Code Standards - -- **TypeScript strict mode** enabled -- **ESLint** extends shared configs -- **Prettier** with import organization -- **Circular dependency detection** via madge -- **Prefer Edit tool** over inline rewrites for existing code - -## Common Task Workflows - -### Adding New Package -1. Create directory in `packages/` or `apps/` -2. Add workspace reference in root `package.json` -3. Create `package.json` with dependencies (use workspace protocol for internal deps) -4. Add build config to `turbo.json` if needed -5. Run `yarn` to link workspace - -### GraphQL Type Updates -After Python backend schema changes: -```bash -yarn gql-codegen # All workspaces -yarn workspace @lightsparkdev/uma-bridge gql-codegen # Specific app -``` - -### Debugging Build Issues -```bash -yarn clean-all # Remove dist/ and caches -yarn build --force # Bypass Turbo cache -yarn clean-resolve # Nuclear option: reset lockfile -``` - -### UMA Bridge Development +After Python schema changes: ```bash -# Lint bridge + UI package -yarn run lint --filter=@lightsparkdev/ui --filter=@lightsparkdev/uma-bridge - -# Start with Vite dev server (proxy configured for backend) -yarn workspace @lightsparkdev/uma-bridge start +yarn gql-codegen # All workspaces +yarn workspace @lightsparkdev/uma-bridge gql-codegen # Specific ``` -Integrates with: Plaid, Tazapay, Striga, other payment providers - ### Adding Dependencies ```bash -# Workspace-specific -yarn workspace @lightsparkdev/ add - -# Root-level (affects all workspaces) -yarn add -W +yarn workspace @lightsparkdev/ add # To workspace +yarn add -W # Root level ``` -## Release Process - -**Public packages** (SDK, core utilities): -- Changesets for version management -- Copybara syncs to public repo on main merge -- Release PR auto-created -- Merge Release PR → npm publish - -**Private packages** (`packages/private/`, `apps/private/`): -- Not published to npm -- Versioned internally only - ## Troubleshooting -**Import errors**: Check workspace dependencies use `"*"` not version numbers -**Type errors after GraphQL changes**: Run `yarn gql-codegen` -**Stale build artifacts**: `yarn clean-all && yarn build` -**Turbo cache issues**: Add `--force` flag to bypass cache \ No newline at end of file +| Issue | Fix | +|-------|-----| +| Import errors | Check deps use `"*"` not versions | +| Type errors after GraphQL | `yarn gql-codegen` | +| Stale builds | `yarn clean-all && yarn build` | +| Cache issues | `yarn build --force` | diff --git a/apps/examples/ui-test-app/src/tests/CodeInput.test.tsx b/apps/examples/ui-test-app/src/tests/CodeInput.test.tsx index 249173fdc..e256b83ee 100644 --- a/apps/examples/ui-test-app/src/tests/CodeInput.test.tsx +++ b/apps/examples/ui-test-app/src/tests/CodeInput.test.tsx @@ -1,6 +1,7 @@ import { jest } from "@jest/globals"; import { CodeInput } from "@lightsparkdev/ui/components/CodeInput/CodeInput"; import { fireEvent, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { render } from "./render"; describe("CodeInput", () => { @@ -8,10 +9,13 @@ describe("CodeInput", () => { const mockClipboardReadWithoutNumbers = jest.fn(() => Promise.resolve("sdkjfnsd"), ); - Object.assign(navigator, { - clipboard: { + // `userEvent.setup()` may install `navigator.clipboard` as a getter-only prop. + // Use `defineProperty` so this mock is resilient regardless of that setup. + Object.defineProperty(navigator, "clipboard", { + value: { readText: mockClipboardReadWithoutNumbers, }, + configurable: true, }); }); @@ -134,4 +138,61 @@ describe("CodeInput", () => { expect(inputFields[3]).toHaveValue(null); expect(inputFields[2]).toHaveFocus(); }); + + it("redirects focus to first empty input when clicking on empty input in unified variant", async () => { + const user = userEvent.setup(); + render(); + const inputFields = screen.getAllByRole("textbox"); + expect(inputFields).toHaveLength(6); + + // Enter some digits in the first two positions + fireEvent.keyDown(inputFields[0], { key: "1" }); + fireEvent.keyDown(inputFields[1], { key: "2" }); + + // Now focus should be on the third input (index 2) + expect(inputFields[2]).toHaveFocus(); + + // Simulate clicking on the 5th input (index 4) - an empty position + // onMouseDown should redirect focus to the first empty input (index 2) + await user.click(inputFields[4]); + expect(inputFields[2]).toHaveFocus(); + }); + + it("allows clicking on any filled input in unified variant", async () => { + const user = userEvent.setup(); + render(); + const inputFields = screen.getAllByRole("textbox"); + + // Enter some digits + fireEvent.keyDown(inputFields[0], { key: "1" }); + fireEvent.keyDown(inputFields[1], { key: "2" }); + fireEvent.keyDown(inputFields[2], { key: "3" }); + + // Focus should be on the 4th input (index 3) + expect(inputFields[3]).toHaveFocus(); + + // Clicking on a filled input (index 1) should work normally - no redirect + await user.click(inputFields[1]); + expect(inputFields[1]).toHaveFocus(); + + // Can also click on index 0 + await user.click(inputFields[0]); + expect(inputFields[0]).toHaveFocus(); + + // Can also click on index 2 + await user.click(inputFields[2]); + expect(inputFields[2]).toHaveFocus(); + }); + + it("focuses first input when all inputs are empty in unified variant", () => { + render(); + const inputFields = screen.getAllByRole("textbox"); + + // Blur the auto-focused first input + fireEvent.blur(inputFields[0]); + + // Click on a middle input when all are empty + fireEvent.mouseDown(inputFields[3]); + expect(inputFields[0]).toHaveFocus(); + }); }); diff --git a/packages/core/package.json b/packages/core/package.json index 3551aea7b..49dbabe21 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -63,10 +63,10 @@ }, "license": "Apache-2.0", "dependencies": { + "@noble/curves": "^1.9.7", "dayjs": "^1.11.7", "graphql": "^16.6.0", "graphql-ws": "^5.11.3", - "secp256k1": "^5.0.1", "ws": "^8.12.1", "zen-observable-ts": "^1.1.0" }, @@ -77,7 +77,6 @@ "@types/crypto-js": "^4.1.1", "@types/jest": "^29.5.3", "@types/lodash-es": "^4.17.6", - "@types/secp256k1": "^4.0.3", "@types/ws": "^8.5.4", "auto-bind": "^5.0.1", "eslint": "^8.3.0", diff --git a/packages/core/src/crypto/SigningKey.ts b/packages/core/src/crypto/SigningKey.ts index 8626e2e98..2045a9095 100644 --- a/packages/core/src/crypto/SigningKey.ts +++ b/packages/core/src/crypto/SigningKey.ts @@ -1,4 +1,4 @@ -import secp256k1 from "secp256k1"; +import { secp256k1 } from "@noble/curves/secp256k1"; import { createSha256Hash } from "../utils/createHash.js"; import { hexToBytes } from "../utils/hex.js"; import type { CryptoInterface } from "./crypto.js"; @@ -46,7 +46,7 @@ export class Secp256k1SigningKey extends SigningKey { async sign(data: Uint8Array) { const keyBytes = new Uint8Array(hexToBytes(this.privateKey)); const hash = await createSha256Hash(data); - const signResult = secp256k1.ecdsaSign(hash, keyBytes); - return secp256k1.signatureExport(signResult.signature); + const sig = secp256k1.sign(hash, keyBytes); + return sig.toBytes("der"); } } diff --git a/packages/core/src/crypto/tests/Secp256k1SigningKey.test.ts b/packages/core/src/crypto/tests/Secp256k1SigningKey.test.ts new file mode 100644 index 000000000..fb4759d62 --- /dev/null +++ b/packages/core/src/crypto/tests/Secp256k1SigningKey.test.ts @@ -0,0 +1,297 @@ +import { describe, expect, test } from "@jest/globals"; +import { bytesToHex, hexToBytes } from "../../utils/hex.js"; +import { Secp256k1SigningKey } from "../SigningKey.js"; + +/** Parse a DER-encoded ECDSA signature and return its structure. */ +function parseDER(bytes: Uint8Array) { + let offset = 0; + const tag = bytes[offset++]; + // outer SEQUENCE tag + expect(tag).toBe(0x30); + + let totalLen = bytes[offset++]; + // handle lengths > 127 (multi-byte length encoding) + if (totalLen & 0x80) { + const numBytes = totalLen & 0x7f; + totalLen = 0; + for (let i = 0; i < numBytes; i++) { + totalLen = (totalLen << 8) | bytes[offset++]; + } + } + + // Parse r INTEGER + expect(bytes[offset++]).toBe(0x02); // INTEGER tag + const rLen = bytes[offset++]; + const r = bytes.slice(offset, offset + rLen); + offset += rLen; + + // Parse s INTEGER + expect(bytes[offset++]).toBe(0x02); // INTEGER tag + const sLen = bytes[offset++]; + const s = bytes.slice(offset, offset + sLen); + offset += sLen; + + // Should have consumed everything + expect(offset).toBe(bytes.length); + + return { r, rLen, s, sLen, totalLen }; +} + +/** Verify DER structure is valid for an ECDSA secp256k1 signature. */ +function assertValidDER(sigBytes: Uint8Array) { + const { r, s, rLen, sLen } = parseDER(sigBytes); + + // r and s should be 32 or 33 bytes (33 if leading zero for sign bit) + expect(rLen).toBeGreaterThanOrEqual(1); + expect(rLen).toBeLessThanOrEqual(33); + expect(sLen).toBeGreaterThanOrEqual(1); + expect(sLen).toBeLessThanOrEqual(33); + + // If 33 bytes, the first byte must be 0x00 (padding for positive sign) + if (rLen === 33) expect(r[0]).toBe(0x00); + if (sLen === 33) expect(s[0]).toBe(0x00); + + // s must be in low-S form (both libraries default to lowS: true) + // The secp256k1 curve order n/2 high byte is 0x7F... + // A low-S value's first non-zero byte (after optional 0x00 pad) must be <= 0x7F + const sValue = sLen === 33 ? s.slice(1) : s; + expect(sValue[0]).toBeLessThanOrEqual(0x7f); +} + +/* ---------- test keys ------------------------------------------------ */ + +// Well-known test keys (from BIP-32 test vectors, not used for real funds) +const TEST_KEYS = { + key1: "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35", + key2: "0000000000000000000000000000000000000000000000000000000000000001", + key3: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140", + // Random but fixed key + key4: "a3fd2b4f5e6c7d8a9b0c1d2e3f405162738495a6b7c8d9e0f1a2b3c4d5e6f708", +}; + +/* ---------- test cases ----------------------------------------------- */ + +const TEST_VECTORS: Array<{ + name: string; + key: string; + messageHex: string; + expectedSigHex: string; +}> = [ + { + name: "key1 + empty message", + key: TEST_KEYS.key1, + messageHex: "", + expectedSigHex: + "3045022100abf6d5fc099a738944afa491783ea0abf3981959808850d86d167853fdeafd0202202dfb6f08fdb7c7d583022761d2ac3734ae81b6185c28783ff114ad0390a6d163", + }, + { + name: "key1 + 'test message'", + key: TEST_KEYS.key1, + messageHex: bytesToHex(new TextEncoder().encode("test message")), + expectedSigHex: + "3044022036e786a9664f71abcd44f0ff32b64a6e5212a18efa5ca57ba9dc9179991cd108022049ffc43f7840653d6470d5b1981ad22b7c5bcda15b26c6e9464451e47249bef8", + }, + { + name: "key1 + 'hello world'", + key: TEST_KEYS.key1, + messageHex: bytesToHex(new TextEncoder().encode("hello world")), + expectedSigHex: + "30440220767b1a1842e64c18f6e3983f173b83174c0776de5f207265ad417162a924f66e0220068bfb0814aea346eefb2bc85225a74f4762d7c376e98d807d4a70c3ce266480", + }, + { + name: "key1 + single zero byte", + key: TEST_KEYS.key1, + messageHex: "00", + expectedSigHex: + "3045022100f6a24b6b335dce767af4c5c0f6f08d2871f75e3227128baf256a3a74f409dd9602205c271c276b8913d98ba05fb513ed1cbf0284927aefbbb8999a91cd081ce8114d", + }, + { + name: "key1 + single 0xff byte", + key: TEST_KEYS.key1, + messageHex: "ff", + expectedSigHex: + "3044022039cf537339901a19a6f50f280760b18bc0e6cb2cfbc3ffcb212a672dc31a8d070220391d90f0d6775bcdfaf73c585558381838382eca108bf79d4d1ee4b4437c603f", + }, + { + name: "key1 + 32 zero bytes (looks like a hash)", + key: TEST_KEYS.key1, + messageHex: "00".repeat(32), + expectedSigHex: + "3045022100a6c97d383fa27dcfaef3664831d63db7a6b12e415e00fa2e134de1ec519cae8e02205570dcd6a94beed0715444e4bad86495f402dbdb625de0efa20ccf347ea7a43a", + }, + { + name: "key1 + 256 bytes of sequential data", + key: TEST_KEYS.key1, + messageHex: Array.from({ length: 256 }, (_, i) => + i.toString(16).padStart(2, "0"), + ).join(""), + expectedSigHex: + "3044022052ee02a14c1f64907f41e470c3b00c73b7a010105df0d0d66b99707ab6352b4c022064d99357bd9c31e4e6fb37844ea3c442b4eaf5a5d16a31d955a9c9a3a17dad29", + }, + { + name: "key2 (smallest valid key) + 'test message'", + key: TEST_KEYS.key2, + messageHex: bytesToHex(new TextEncoder().encode("test message")), + expectedSigHex: + "3045022100fab80f0a195c3481c924f3a84729dbb4317c1e98b2b856be21712b2006c14f7a02200b9d3f1dbab6b9f4f37f7aee76e259069c9e2438ad0fd95a77d9fe4dcbb6cdbe", + }, + { + name: "key3 (near curve order) + 'test message'", + key: TEST_KEYS.key3, + messageHex: bytesToHex(new TextEncoder().encode("test message")), + expectedSigHex: + "3045022100c32d992fb5a08fcd1e54f8978fece3d3d787ab12457996f35819acc024de4f5702204ff52c357699851f6d362850d57cd215733bb6420aad0eda73e519a9062d3eae", + }, + { + name: "key4 + 'test message'", + key: TEST_KEYS.key4, + messageHex: bytesToHex(new TextEncoder().encode("test message")), + expectedSigHex: + "3045022100e3034b1ce07b08ed8558866e95591d1e60c6ef7edd092583ef3903cc09626e1702204e4be959ed8d5905bc3903cd1958307d9300fa36d754c932ed756b580b8cab56", + }, + { + name: "key4 + long repeated pattern (1KB)", + key: TEST_KEYS.key4, + messageHex: "deadbeef".repeat(256), + expectedSigHex: + "3045022100f342cbad7fbccf56094dc27bbef448cdff8cc4784d8597cbfbc064e72c14343102207b0a09c74fa8c7298279a7bf8ca98b45f1e8bf8201b468f5548156c5e678d5dd", + }, + { + name: "key2 + empty message", + key: TEST_KEYS.key2, + messageHex: "", + expectedSigHex: + "3044022077c8d336572f6f466055b5f70f433851f8f535f6c4fc71133a6cfd71079d03b702200ed9f5eb8aa5b266abac35d416c3207e7a538bf5f37649727d7a9823b1069577", + }, + { + name: "key3 + empty message", + key: TEST_KEYS.key3, + messageHex: "", + expectedSigHex: + "3045022100ea045bf0962ecc4d5aa84c8e716c87c9d5f49fba8e1ff0300ab2631de3d83b43022051270ec8105346fddf35da5958d99ff55a0c0f720d6ae7f3e3eadd40a9ccfe0e", + }, +]; + +/* ---------- tests ---------------------------------------------------- */ + +describe("Secp256k1SigningKey", () => { + describe("deterministic signature vectors", () => { + for (const vector of TEST_VECTORS) { + test(vector.name, async () => { + const signingKey = new Secp256k1SigningKey(vector.key); + const message = hexToBytes(vector.messageHex); + + const signature = await signingKey.sign(message); + const sigBytes = new Uint8Array(signature); + const sigHex = bytesToHex(sigBytes); + + assertValidDER(sigBytes); + expect(sigHex).toBe(vector.expectedSigHex); + }); + } + }); + + describe("determinism (same key+message → same signature)", () => { + test("10 sequential signs produce identical output", async () => { + const signingKey = new Secp256k1SigningKey(TEST_KEYS.key1); + const message = new TextEncoder().encode("determinism check"); + + const signatures: string[] = []; + for (let i = 0; i < 10; i++) { + const sig = await signingKey.sign(message); + signatures.push(bytesToHex(new Uint8Array(sig))); + } + + const first = signatures[0]; + for (const sig of signatures) { + expect(sig).toBe(first); + } + }); + }); + + describe("DER encoding structure", () => { + test("signature starts with 0x30 SEQUENCE tag", async () => { + const signingKey = new Secp256k1SigningKey(TEST_KEYS.key1); + const sig = new Uint8Array( + await signingKey.sign(new TextEncoder().encode("DER check")), + ); + expect(sig[0]).toBe(0x30); + }); + + test("signature contains exactly two INTEGERs", async () => { + const signingKey = new Secp256k1SigningKey(TEST_KEYS.key1); + const sig = new Uint8Array( + await signingKey.sign(new TextEncoder().encode("DER structure")), + ); + const { r, s } = parseDER(sig); + // Both r and s should be non-empty + expect(r.length).toBeGreaterThan(0); + expect(s.length).toBeGreaterThan(0); + }); + + test("total DER length matches actual byte length", async () => { + const signingKey = new Secp256k1SigningKey(TEST_KEYS.key1); + for (const msg of ["a", "bb", "ccc", "dddd"]) { + const sig = new Uint8Array( + await signingKey.sign(new TextEncoder().encode(msg)), + ); + // DER: 0x30 + // Total bytes = 2 (tag+len) + contents length + const contentLen = sig[1]; + expect(sig.length).toBe(2 + contentLen); + } + }); + }); + + describe("different keys produce different signatures for same message", () => { + test("all test keys produce unique signatures", async () => { + const message = new TextEncoder().encode("unique check"); + const sigs = new Set(); + + for (const key of Object.values(TEST_KEYS)) { + const signingKey = new Secp256k1SigningKey(key); + const sig = await signingKey.sign(message); + sigs.add(bytesToHex(new Uint8Array(sig))); + } + + expect(sigs.size).toBe(Object.keys(TEST_KEYS).length); + }); + }); + + describe("different messages produce different signatures for same key", () => { + test("varied messages produce unique signatures", async () => { + const signingKey = new Secp256k1SigningKey(TEST_KEYS.key1); + const messages = [ + "message 1", + "message 2", + "message 3", + "", + "a", + "ab", + "abc", + ]; + const sigs = new Set(); + + for (const msg of messages) { + const sig = await signingKey.sign(new TextEncoder().encode(msg)); + sigs.add(bytesToHex(new Uint8Array(sig))); + } + + expect(sigs.size).toBe(messages.length); + }); + }); + + describe("signature byte length is reasonable", () => { + test("DER signatures are between 68 and 72 bytes", async () => { + // DER-encoded secp256k1 signatures are typically 70-72 bytes + // but can be as short as 68 if both r and s have no leading zero + const signingKey = new Secp256k1SigningKey(TEST_KEYS.key1); + for (let i = 0; i < 20; i++) { + const msg = new TextEncoder().encode(`length check ${i}`); + const sig = new Uint8Array(await signingKey.sign(msg)); + expect(sig.length).toBeGreaterThanOrEqual(68); + expect(sig.length).toBeLessThanOrEqual(72); + } + }); + }); +}); diff --git a/packages/core/src/utils/currency.ts b/packages/core/src/utils/currency.ts index 56b40a2a0..35bfe585c 100644 --- a/packages/core/src/utils/currency.ts +++ b/packages/core/src/utils/currency.ts @@ -43,6 +43,9 @@ export const CurrencyUnit = { XAF: "XAF", MWK: "MWK", RWF: "RWF", + ZMW: "ZMW", + AED: "AED", + GTQ: "GTQ", USDT: "USDT", USDC: "USDC", @@ -58,6 +61,7 @@ export const CurrencyUnit = { Gbp: "GBP", Inr: "INR", Brl: "BRL", + Aed: "AED", Usdt: "USDT", Usdc: "USDC", } as const; @@ -110,6 +114,9 @@ const standardUnitConversionObj = { [CurrencyUnit.XAF]: (v: number) => v, [CurrencyUnit.MWK]: (v: number) => v, [CurrencyUnit.RWF]: (v: number) => v, + [CurrencyUnit.ZMW]: (v: number) => v, + [CurrencyUnit.AED]: (v: number) => v, + [CurrencyUnit.GTQ]: (v: number) => v, [CurrencyUnit.USDT]: (v: number) => v, [CurrencyUnit.USDC]: (v: number) => v, }; @@ -161,6 +168,9 @@ const CONVERSION_MAP = { [CurrencyUnit.XAF]: toBitcoinConversion, [CurrencyUnit.MWK]: toBitcoinConversion, [CurrencyUnit.RWF]: toBitcoinConversion, + [CurrencyUnit.ZMW]: toBitcoinConversion, + [CurrencyUnit.AED]: toBitcoinConversion, + [CurrencyUnit.GTQ]: toBitcoinConversion, [CurrencyUnit.USDT]: toBitcoinConversion, [CurrencyUnit.USDC]: toBitcoinConversion, }, @@ -196,6 +206,9 @@ const CONVERSION_MAP = { [CurrencyUnit.XAF]: toMicrobitcoinConversion, [CurrencyUnit.MWK]: toMicrobitcoinConversion, [CurrencyUnit.RWF]: toMicrobitcoinConversion, + [CurrencyUnit.ZMW]: toMicrobitcoinConversion, + [CurrencyUnit.AED]: toMicrobitcoinConversion, + [CurrencyUnit.GTQ]: toMicrobitcoinConversion, [CurrencyUnit.USDT]: toMicrobitcoinConversion, [CurrencyUnit.USDC]: toMicrobitcoinConversion, }, @@ -231,6 +244,9 @@ const CONVERSION_MAP = { [CurrencyUnit.XAF]: toMillibitcoinConversion, [CurrencyUnit.MWK]: toMillibitcoinConversion, [CurrencyUnit.RWF]: toMillibitcoinConversion, + [CurrencyUnit.ZMW]: toMillibitcoinConversion, + [CurrencyUnit.AED]: toMillibitcoinConversion, + [CurrencyUnit.GTQ]: toMillibitcoinConversion, [CurrencyUnit.USDT]: toMillibitcoinConversion, [CurrencyUnit.USDC]: toMillibitcoinConversion, }, @@ -266,6 +282,9 @@ const CONVERSION_MAP = { [CurrencyUnit.XAF]: toMillisatoshiConversion, [CurrencyUnit.MWK]: toMillisatoshiConversion, [CurrencyUnit.RWF]: toMillisatoshiConversion, + [CurrencyUnit.ZMW]: toMillisatoshiConversion, + [CurrencyUnit.AED]: toMillisatoshiConversion, + [CurrencyUnit.GTQ]: toMillisatoshiConversion, [CurrencyUnit.USDT]: toMillisatoshiConversion, [CurrencyUnit.USDC]: toMillisatoshiConversion, }, @@ -301,6 +320,9 @@ const CONVERSION_MAP = { [CurrencyUnit.XAF]: toNanobitcoinConversion, [CurrencyUnit.MWK]: toNanobitcoinConversion, [CurrencyUnit.RWF]: toNanobitcoinConversion, + [CurrencyUnit.ZMW]: toNanobitcoinConversion, + [CurrencyUnit.AED]: toNanobitcoinConversion, + [CurrencyUnit.GTQ]: toNanobitcoinConversion, [CurrencyUnit.USDT]: toNanobitcoinConversion, [CurrencyUnit.USDC]: toNanobitcoinConversion, }, @@ -336,6 +358,9 @@ const CONVERSION_MAP = { [CurrencyUnit.XAF]: toSatoshiConversion, [CurrencyUnit.MWK]: toSatoshiConversion, [CurrencyUnit.RWF]: toSatoshiConversion, + [CurrencyUnit.ZMW]: toSatoshiConversion, + [CurrencyUnit.AED]: toSatoshiConversion, + [CurrencyUnit.GTQ]: toSatoshiConversion, [CurrencyUnit.USDT]: toSatoshiConversion, [CurrencyUnit.USDC]: toSatoshiConversion, }, @@ -364,6 +389,9 @@ const CONVERSION_MAP = { [CurrencyUnit.XAF]: standardUnitConversionObj, [CurrencyUnit.MWK]: standardUnitConversionObj, [CurrencyUnit.RWF]: standardUnitConversionObj, + [CurrencyUnit.ZMW]: standardUnitConversionObj, + [CurrencyUnit.AED]: standardUnitConversionObj, + [CurrencyUnit.GTQ]: standardUnitConversionObj, [CurrencyUnit.USDT]: standardUnitConversionObj, [CurrencyUnit.USDC]: standardUnitConversionObj, }; @@ -452,6 +480,9 @@ export type CurrencyMap = { [CurrencyUnit.XAF]: number; [CurrencyUnit.MWK]: number; [CurrencyUnit.RWF]: number; + [CurrencyUnit.ZMW]: number; + [CurrencyUnit.AED]: number; + [CurrencyUnit.GTQ]: number; [CurrencyUnit.USDT]: number; [CurrencyUnit.USDC]: number; [CurrencyUnit.FUTURE_VALUE]: number; @@ -490,6 +521,9 @@ export type CurrencyMap = { [CurrencyUnit.XAF]: string; [CurrencyUnit.MWK]: string; [CurrencyUnit.RWF]: string; + [CurrencyUnit.ZMW]: string; + [CurrencyUnit.AED]: string; + [CurrencyUnit.GTQ]: string; [CurrencyUnit.USDT]: string; [CurrencyUnit.USDC]: string; [CurrencyUnit.FUTURE_VALUE]: string; @@ -709,6 +743,9 @@ function convertCurrencyAmountValues( xaf: CurrencyUnit.XAF, mwk: CurrencyUnit.MWK, rwf: CurrencyUnit.RWF, + zmw: CurrencyUnit.ZMW, + aed: CurrencyUnit.AED, + gtq: CurrencyUnit.GTQ, mibtc: CurrencyUnit.MICROBITCOIN, mlbtc: CurrencyUnit.MILLIBITCOIN, nbtc: CurrencyUnit.NANOBITCOIN, @@ -792,6 +829,9 @@ export function mapCurrencyAmount( xaf, mwk, rwf, + zmw, + aed, + gtq, usdt, usdc, } = convertCurrencyAmountValues(unit, value, unitsPerBtc, conversionOverride); @@ -825,6 +865,9 @@ export function mapCurrencyAmount( [CurrencyUnit.XAF]: xaf, [CurrencyUnit.MWK]: mwk, [CurrencyUnit.RWF]: rwf, + [CurrencyUnit.ZMW]: zmw, + [CurrencyUnit.AED]: aed, + [CurrencyUnit.GTQ]: gtq, [CurrencyUnit.MICROBITCOIN]: mibtc, [CurrencyUnit.MILLIBITCOIN]: mlbtc, [CurrencyUnit.NANOBITCOIN]: nbtc, @@ -956,6 +999,18 @@ export function mapCurrencyAmount( value: rwf, unit: CurrencyUnit.RWF, }), + [CurrencyUnit.ZMW]: formatCurrencyStr({ + value: zmw, + unit: CurrencyUnit.ZMW, + }), + [CurrencyUnit.AED]: formatCurrencyStr({ + value: aed, + unit: CurrencyUnit.AED, + }), + [CurrencyUnit.GTQ]: formatCurrencyStr({ + value: gtq, + unit: CurrencyUnit.GTQ, + }), [CurrencyUnit.USDT]: formatCurrencyStr({ value: usdt, unit: CurrencyUnit.USDT, @@ -1086,6 +1141,12 @@ export const abbrCurrencyUnit = (unit: CurrencyUnitType) => { return "MWK"; case CurrencyUnit.RWF: return "RWF"; + case CurrencyUnit.ZMW: + return "ZMW"; + case CurrencyUnit.AED: + return "AED"; + case CurrencyUnit.GTQ: + return "GTQ"; } return "Unsupported CurrencyUnit"; }; diff --git a/packages/lightspark-cli/package.json b/packages/lightspark-cli/package.json index 9375bf26a..66dbabeb8 100644 --- a/packages/lightspark-cli/package.json +++ b/packages/lightspark-cli/package.json @@ -32,7 +32,6 @@ "@types/jsonwebtoken": "^9.0.2", "@types/node": "^20.2.5", "@types/qrcode-terminal": "^0.12.0", - "@types/secp256k1": "^4.0.3", "eslint": "^8.3.0", "eslint-watch": "^8.0.0", "nodemon": "^2.0.22", @@ -47,13 +46,13 @@ "@lightsparkdev/core": "1.4.8", "@lightsparkdev/crypto-wasm": "0.1.22", "@lightsparkdev/lightspark-sdk": "1.9.15", + "@noble/curves": "^1.9.7", "commander": "^11.0.0", "dayjs": "^1.11.7", "dotenv": "^16.3.1", "jose": "^4.15.5", "jsonwebtoken": "^9.0.1", - "qrcode-terminal": "^0.12.0", - "secp256k1": "^5.0.1" + "qrcode-terminal": "^0.12.0" }, "engines": { "node": ">=18" diff --git a/packages/lightspark-cli/src/index.ts b/packages/lightspark-cli/src/index.ts index 3aa4f81dc..1c2031507 100644 --- a/packages/lightspark-cli/src/index.ts +++ b/packages/lightspark-cli/src/index.ts @@ -16,12 +16,12 @@ import { getCredentialsFromEnvOrThrow, type EnvCredentials, } from "@lightsparkdev/lightspark-sdk/env"; +import { secp256k1 } from "@noble/curves/secp256k1"; import type { OptionValues } from "commander"; import { Command, InvalidArgumentError } from "commander"; import { randomBytes } from "crypto"; import * as fs from "fs/promises"; import qrcode from "qrcode-terminal"; -import secp256k1 from "secp256k1"; import { bytesToHex, getCryptoLibNetwork, @@ -621,13 +621,22 @@ const generateNodeKeys = async (options: OptionValues) => { console.log(`Extended public key:\n${extendedPublicKey}`); }; +const isValidPrivateKey = (key: Uint8Array): boolean => { + try { + secp256k1.getPublicKey(key); + return true; + } catch { + return false; + } +}; + const generateSecp256k1Keypair = () => { - let privateKey; + let privateKey: Uint8Array; do { privateKey = new Uint8Array(randomBytes(32)); - } while (!secp256k1.privateKeyVerify(privateKey)); + } while (!isValidPrivateKey(privateKey)); - const publicKey = secp256k1.publicKeyCreate(privateKey); + const publicKey = secp256k1.getPublicKey(privateKey); const publicKeyAsHex = bytesToHex(publicKey); const privateKeyAsHex = bytesToHex(privateKey); diff --git a/packages/ui/src/components/CardForm/CardForm.tsx b/packages/ui/src/components/CardForm/CardForm.tsx index d9e512c39..1a3779a04 100644 --- a/packages/ui/src/components/CardForm/CardForm.tsx +++ b/packages/ui/src/components/CardForm/CardForm.tsx @@ -460,6 +460,7 @@ const CardFormContentFull = styled.div<{ paddingBottom?: number | undefined }>` flex-direction: column; align-self: center; height: 100%; + width: 100%; padding-bottom: ${({ paddingBottom }) => paddingBottom ?? 0}px; `; diff --git a/packages/ui/src/components/CardPage.tsx b/packages/ui/src/components/CardPage.tsx index 8846e4fdc..1957b5880 100644 --- a/packages/ui/src/components/CardPage.tsx +++ b/packages/ui/src/components/CardPage.tsx @@ -28,6 +28,7 @@ type Props = { maxContentWidth?: number; rightContent?: React.ReactNode; preHeaderContent?: React.ReactNode; + headerRightContent?: React.ReactNode; expandRight?: boolean; id?: string; }; @@ -43,6 +44,9 @@ export function CardPage(props: Props) { {props.title} + {props.headerRightContent && ( + {props.headerRightContent} + )} ) : null; @@ -350,6 +354,11 @@ const CardPageHeader = styled.div<{ headerMarginBottom?: number }>` } `; +const CardPageHeaderRight = styled.div` + display: flex; + align-items: center; +`; + export const CardPageContent = styled.div` ${({ maxContentWidth, diff --git a/packages/ui/src/components/CodeInput/CodeInput.tsx b/packages/ui/src/components/CodeInput/CodeInput.tsx index e5e38d02a..b6af4a32d 100644 --- a/packages/ui/src/components/CodeInput/CodeInput.tsx +++ b/packages/ui/src/components/CodeInput/CodeInput.tsx @@ -331,6 +331,27 @@ export function CodeInput({ const inputsPerGroup = Math.ceil(codeLength / 2); + /** + * When clicking on the unified code input container, handle focus appropriately + * Uses onMouseDown instead of onClick because mousedown fires before focus, + * allowing us to prevent the default focus behavior and redirect to the correct input. + */ + const onContainerMouseDown = useCallback( + (event: React.MouseEvent) => { + const target = event.target as HTMLInputElement; + const isClickingFilledInput = + target.tagName === "INPUT" && inputState[target.id]?.value !== ""; + if (!isClickingFilledInput) { + event.preventDefault(); + const firstEmptyIndex = codeFromInputState(inputState).length; + const targetIndex = + firstEmptyIndex < codeLength ? firstEmptyIndex : codeLength - 1; + getRef(getInputId(targetIndex), inputRefs)?.focus(); + } + }, + [codeLength, getInputId, inputState, inputRefs], + ); + const inputs = []; for (let i = 0; i < codeLength; i += 1) { const inputId = getInputId(i); @@ -413,7 +434,11 @@ export function CodeInput({ } if (variant === "unified") { - return {inputs}; + return ( + + {inputs} + + ); } return ( diff --git a/packages/ui/src/components/DataManagerTable/AppliedButtonsContainer.tsx b/packages/ui/src/components/DataManagerTable/AppliedButtonsContainer.tsx new file mode 100644 index 000000000..780d7b8db --- /dev/null +++ b/packages/ui/src/components/DataManagerTable/AppliedButtonsContainer.tsx @@ -0,0 +1,14 @@ +import styled from "@emotion/styled"; +import { Spacing } from "../../styles/tokens/spacing.js"; +import { ButtonSelector } from "../Button.js"; + +export const AppliedButtonsContainer = styled.div` + margin-top: ${Spacing.px.sm}; + display: flex; + gap: ${Spacing.px.xs}; + flex-wrap: wrap; + + ${ButtonSelector()} { + max-width: 100%; + } +`; diff --git a/packages/ui/src/components/DataManagerTable/EnumFilter.tsx b/packages/ui/src/components/DataManagerTable/EnumFilter.tsx index 31e149ab6..6b435c91d 100644 --- a/packages/ui/src/components/DataManagerTable/EnumFilter.tsx +++ b/packages/ui/src/components/DataManagerTable/EnumFilter.tsx @@ -1,9 +1,8 @@ -import styled from "@emotion/styled"; import { ensureArray } from "@lightsparkdev/core"; -import { Spacing } from "../../styles/tokens/spacing.js"; import { z } from "../../styles/z-index.js"; import { Button } from "../Button.js"; import Select from "../Select.js"; +import { AppliedButtonsContainer } from "./AppliedButtonsContainer.js"; import { Filter, type FilterState } from "./Filter.js"; import { FilterType, type EnumFilterValue } from "./filters.js"; @@ -105,10 +104,3 @@ export const EnumFilter = ({ ); }; - -const AppliedButtonsContainer = styled.div` - margin-top: ${Spacing.px.sm}; - display: flex; - gap: ${Spacing.px.xs}; - flex-wrap: wrap; -`; diff --git a/packages/ui/src/components/DataManagerTable/IdFilter.tsx b/packages/ui/src/components/DataManagerTable/IdFilter.tsx index ffd45f31d..d9a8d2819 100644 --- a/packages/ui/src/components/DataManagerTable/IdFilter.tsx +++ b/packages/ui/src/components/DataManagerTable/IdFilter.tsx @@ -1,7 +1,6 @@ -import styled from "@emotion/styled"; -import { Spacing } from "../../styles/tokens/spacing.js"; import { Button } from "../Button.js"; import { TextInput } from "../TextInput.js"; +import { AppliedButtonsContainer } from "./AppliedButtonsContainer.js"; import { Filter, type FilterState } from "./Filter.js"; import { FilterType } from "./filters.js"; @@ -148,10 +147,3 @@ export const IdFilter = ({ ); }; - -const AppliedButtonsContainer = styled.div` - margin-top: ${Spacing.px.sm}; - display: flex; - gap: ${Spacing.px.xs}; - flex-wrap: wrap; -`; diff --git a/packages/ui/src/components/DataManagerTable/StringFilter.tsx b/packages/ui/src/components/DataManagerTable/StringFilter.tsx index 79a9d842c..d6eb6666a 100644 --- a/packages/ui/src/components/DataManagerTable/StringFilter.tsx +++ b/packages/ui/src/components/DataManagerTable/StringFilter.tsx @@ -1,7 +1,6 @@ -import styled from "@emotion/styled"; -import { Spacing } from "../../styles/tokens/spacing.js"; import { Button } from "../Button.js"; import { TextInput } from "../TextInput.js"; +import { AppliedButtonsContainer } from "./AppliedButtonsContainer.js"; import { Filter, type FilterState } from "./Filter.js"; import { FilterType } from "./filters.js"; @@ -82,10 +81,3 @@ export const StringFilter = ({ ); }; - -const AppliedButtonsContainer = styled.div` - margin-top: ${Spacing.px.sm}; - display: flex; - gap: ${Spacing.px.xs}; - flex-wrap: wrap; -`; diff --git a/packages/ui/src/icons/central/BankSolid.tsx b/packages/ui/src/icons/central/BankSolid.tsx new file mode 100644 index 000000000..716f15e8b --- /dev/null +++ b/packages/ui/src/icons/central/BankSolid.tsx @@ -0,0 +1,18 @@ +export function BankSolid() { + return ( + + + + ); +} diff --git a/packages/ui/src/icons/central/index.tsx b/packages/ui/src/icons/central/index.tsx index 44f5028dd..fe01e739c 100644 --- a/packages/ui/src/icons/central/index.tsx +++ b/packages/ui/src/icons/central/index.tsx @@ -16,6 +16,7 @@ export { ArrowUpRight as CentralArrowUpRight } from "./ArrowUpRight.js"; export { At as CentralAt } from "./At.js"; export { Bank as CentralBank } from "./Bank.js"; export { BankBold as CentralBankBold } from "./BankBold.js"; +export { BankSolid as CentralBankSolid } from "./BankSolid.js"; export { BarsThree as CentralBarsThree } from "./BarsThree.js"; export { Bell as CentralBell } from "./Bell.js"; export { Bell2 as CentralBell2 } from "./Bell2.js"; diff --git a/yarn.lock b/yarn.lock index 3aef89ee1..2e66106a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2132,10 +2132,10 @@ __metadata: "@arethetypeswrong/cli": "npm:^0.17.4" "@lightsparkdev/eslint-config": "npm:*" "@lightsparkdev/tsconfig": "npm:0.0.1" + "@noble/curves": "npm:^1.9.7" "@types/crypto-js": "npm:^4.1.1" "@types/jest": "npm:^29.5.3" "@types/lodash-es": "npm:^4.17.6" - "@types/secp256k1": "npm:^4.0.3" "@types/ws": "npm:^8.5.4" auto-bind: "npm:^5.0.1" dayjs: "npm:^1.11.7" @@ -2149,7 +2149,6 @@ __metadata: prettier: "npm:3.0.3" prettier-plugin-organize-imports: "npm:^3.2.4" publint: "npm:^0.3.9" - secp256k1: "npm:^5.0.1" ts-jest: "npm:^29.1.1" tsc-absolute: "npm:^1.0.1" tsup: "npm:^8.2.4" @@ -2205,10 +2204,10 @@ __metadata: "@lightsparkdev/eslint-config": "npm:*" "@lightsparkdev/lightspark-sdk": "npm:1.9.15" "@lightsparkdev/tsconfig": "npm:0.0.1" + "@noble/curves": "npm:^1.9.7" "@types/jsonwebtoken": "npm:^9.0.2" "@types/node": "npm:^20.2.5" "@types/qrcode-terminal": "npm:^0.12.0" - "@types/secp256k1": "npm:^4.0.3" commander: "npm:^11.0.0" dayjs: "npm:^1.11.7" dotenv: "npm:^16.3.1" @@ -2220,7 +2219,6 @@ __metadata: prettier: "npm:3.0.3" prettier-plugin-organize-imports: "npm:^3.2.4" qrcode-terminal: "npm:^0.12.0" - secp256k1: "npm:^5.0.1" ts-node: "npm:^10.9.1" tsc-absolute: "npm:^1.0.1" typescript: "npm:^5.6.2" @@ -2783,6 +2781,15 @@ __metadata: languageName: node linkType: hard +"@noble/curves@npm:^1.9.7": + version: 1.9.7 + resolution: "@noble/curves@npm:1.9.7" + dependencies: + "@noble/hashes": "npm:1.8.0" + checksum: 10/3cfe2735ea94972988ca9e217e0ebb2044372a7160b2079bf885da789492a6291fc8bf76ca3d8bf8dee477847ee2d6fac267d1e6c4f555054059f5e8c4865d44 + languageName: node + linkType: hard + "@noble/hashes@npm:1.3.2, @noble/hashes@npm:^1.3.2": version: 1.3.2 resolution: "@noble/hashes@npm:1.3.2" @@ -2790,6 +2797,13 @@ __metadata: languageName: node linkType: hard +"@noble/hashes@npm:1.8.0": + version: 1.8.0 + resolution: "@noble/hashes@npm:1.8.0" + checksum: 10/474b7f56bc6fb2d5b3a42132561e221b0ea4f91e590f4655312ca13667840896b34195e2b53b7f097ec080a1fdd3b58d902c2a8d0fbdf51d2e238b53808a177e + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -4227,15 +4241,6 @@ __metadata: languageName: node linkType: hard -"@types/secp256k1@npm:^4.0.3": - version: 4.0.6 - resolution: "@types/secp256k1@npm:4.0.6" - dependencies: - "@types/node": "npm:*" - checksum: 10/211f823be990b55612e604d620acf0dc3bc942d3836bdd8da604269effabc86d98161e5947487b4e4e128f9180fc1682daae2f89ea7a4d9648fdfe52fba365fc - languageName: node - linkType: hard - "@types/semver@npm:^7.3.12, @types/semver@npm:^7.5.0": version: 7.5.6 resolution: "@types/semver@npm:7.5.6" @@ -6952,21 +6957,6 @@ __metadata: languageName: node linkType: hard -"elliptic@npm:^6.5.7": - version: 6.5.7 - resolution: "elliptic@npm:6.5.7" - dependencies: - bn.js: "npm:^4.11.9" - brorand: "npm:^1.1.0" - hash.js: "npm:^1.0.0" - hmac-drbg: "npm:^1.0.1" - inherits: "npm:^2.0.4" - minimalistic-assert: "npm:^1.0.1" - minimalistic-crypto-utils: "npm:^1.0.1" - checksum: 10/fbad1fad0a5cc07df83f80cc1f7a784247ef59075194d3e340eaeb2f4dd594825ee24c7e9b0cf279c9f1982efe610503bb3139737926428c4821d4fca1bcf348 - languageName: node - linkType: hard - "emittery@npm:^0.13.1": version: 0.13.1 resolution: "emittery@npm:0.13.1" @@ -13219,18 +13209,6 @@ __metadata: languageName: node linkType: hard -"secp256k1@npm:^5.0.1": - version: 5.0.1 - resolution: "secp256k1@npm:5.0.1" - dependencies: - elliptic: "npm:^6.5.7" - node-addon-api: "npm:^5.0.0" - node-gyp: "npm:latest" - node-gyp-build: "npm:^4.2.0" - checksum: 10/63fbd35624be4fd9cf3d39e5f79c5471b4a8aea6944453b2bea7b100bb1c77a25c55e6e08e2210cdabdf478c4c62d34c408b34214f2afd9367e19a52a3a4236c - languageName: node - linkType: hard - "sembear@npm:^0.7.0": version: 0.7.0 resolution: "sembear@npm:0.7.0"