From 33685bfc619ba83460aac29077f6d1754e8b78e0 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Mon, 5 Jan 2026 09:45:56 -0800 Subject: [PATCH 01/15] Restrict first and last name input fields to be more than 1 characters and to not contain digits (#22563) ## Reason closes PX-1302 Yellowcard does not allow names containing numbers, and I can't really think of a reason we should either? GitOrigin-RevId: e6fc9162d884d82256e3ee71c8330edeebb53983 --- packages/ui/src/hooks/useFields.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/ui/src/hooks/useFields.tsx b/packages/ui/src/hooks/useFields.tsx index 567009b22..efc367988 100644 --- a/packages/ui/src/hooks/useFields.tsx +++ b/packages/ui/src/hooks/useFields.tsx @@ -22,6 +22,7 @@ const defaultMsgs = { postalCode: "Please enter a valid zip code.", state: "Please enter a valid two-letter state abbreviation.", name: "Name must be at least three characters.", + humanName: "Name must be at least 1 character and cannot contain digits", code: "Code must be eight characters long.", password: "Password must be at least 12 characters, must contain at least two types of characters: lowercase, uppercase, numbers, special", @@ -39,6 +40,7 @@ const regexp = { phone: /^[0-9]{7,15}$/, postalCode: /(^\d{5}$)|(^\d{5}-\d{4}$)/, email: /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, + humanName: /^[^0-9]+$/, state: /^(A[LKSZRAEP]|C[AOT]|D[EC]|F[LM]|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEHINOPST]|N[CDEHJMVY]|O[HKR]|P[ARW]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$/, clabe: /^[0-9]{18}$/, @@ -82,6 +84,10 @@ export const v: Validators = { (msg = defaultMsgs.name) => (value) => value.trim().length < 3 ? msg : false, + humanName: + (msg = defaultMsgs.humanName) => + (value) => + value.trim().length < 1 || !regexp.humanName.test(value) ? msg : false, code: (msg = defaultMsgs.code) => (value) => @@ -161,6 +167,7 @@ const defaultValidators: Record = { ], city: [v.required("City is required.")], name: [v.required("Name is required."), v.name()], + humanName: [v.required("Name is required."), v.humanName()], code: [v.required("Please enter a code."), v.code()], email: [v.required("Email is required."), v.email()], phoneNumber: [v.required("Phone number is required."), v.phone()], From 521c3bb44268e36d1f244885106d8d1d0ed2795a Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Mon, 5 Jan 2026 12:00:34 -0800 Subject: [PATCH 02/15] =?UTF-8?q?Revert=20"Restrict=20first=20and=20last?= =?UTF-8?q?=20name=20input=20fields=20to=20be=20more=20than=201=20c?= =?UTF-8?q?=E2=80=A6=20(#22867)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …haracters and to not contain digits (#22563)" This reverts commit e6fc9162d884d82256e3ee71c8330edeebb53983. ## Reason there's an issue where sign up in mx is blocked, reverting this to be safe GitOrigin-RevId: 6ead3d065fd639bd7f7f1e309450801d0f794a90 --- packages/ui/src/hooks/useFields.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/ui/src/hooks/useFields.tsx b/packages/ui/src/hooks/useFields.tsx index efc367988..567009b22 100644 --- a/packages/ui/src/hooks/useFields.tsx +++ b/packages/ui/src/hooks/useFields.tsx @@ -22,7 +22,6 @@ const defaultMsgs = { postalCode: "Please enter a valid zip code.", state: "Please enter a valid two-letter state abbreviation.", name: "Name must be at least three characters.", - humanName: "Name must be at least 1 character and cannot contain digits", code: "Code must be eight characters long.", password: "Password must be at least 12 characters, must contain at least two types of characters: lowercase, uppercase, numbers, special", @@ -40,7 +39,6 @@ const regexp = { phone: /^[0-9]{7,15}$/, postalCode: /(^\d{5}$)|(^\d{5}-\d{4}$)/, email: /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, - humanName: /^[^0-9]+$/, state: /^(A[LKSZRAEP]|C[AOT]|D[EC]|F[LM]|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEHINOPST]|N[CDEHJMVY]|O[HKR]|P[ARW]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$/, clabe: /^[0-9]{18}$/, @@ -84,10 +82,6 @@ export const v: Validators = { (msg = defaultMsgs.name) => (value) => value.trim().length < 3 ? msg : false, - humanName: - (msg = defaultMsgs.humanName) => - (value) => - value.trim().length < 1 || !regexp.humanName.test(value) ? msg : false, code: (msg = defaultMsgs.code) => (value) => @@ -167,7 +161,6 @@ const defaultValidators: Record = { ], city: [v.required("City is required.")], name: [v.required("Name is required."), v.name()], - humanName: [v.required("Name is required."), v.humanName()], code: [v.required("Please enter a code."), v.code()], email: [v.required("Email is required."), v.email()], phoneNumber: [v.required("Phone number is required."), v.phone()], From daf7521bb18fe2da49fd3d7e7a1783032e31696c Mon Sep 17 00:00:00 2001 From: Brian Siao Tick Chong Date: Mon, 5 Jan 2026 16:51:48 -0800 Subject: [PATCH 03/15] [ops] unify sparkcore and paycore searches into one (#22822) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Reason The sparkcore vs paycore ent searches is kind of frustrating because it doesn't indicate if it exists in one or the other. So I wanted to combine both into a single search so you don't have to toggle the schema manually (and it'll show results for both schemas) ## Overview - deletes old searches for sparkcore and paycore - queries both schemas for results in new unified search input - search types are combined (defaults to auto if it doesn't exist for one of the schemas) - shows the schema on the actual ent! ## Test Plan works locally ![Screenshot 2025-12-24 at 6.08.51 PM.png](https://app.graphite.com/user-attachments/assets/58a9fb5e-b937-49eb-9f1e-ce2fb1d68e09.png) ![Screenshot 2025-12-24 at 6.08.56 PM.png](https://app.graphite.com/user-attachments/assets/8e456b16-b5a2-4910-8459-6cda78857ef2.png) ## Summary by CodeRabbit * **New Features** * Unified search input consolidates results across schemas and shows schema on results. * CardPage header supports custom right-side content (used to show schema badges). * **Refactor** * Replaced two specialized search inputs with a single unified one for consistent behavior. * Inspector now uses a single link component for entity navigation for consistent styling. * **Chores** * Removed legacy per-schema search components and related query wiring. ✏️ Tip: You can customize this high-level summary in your review settings. GitOrigin-RevId: 7ae439a34e2c04d40bf2163190a78179dfbf000a --- packages/ui/src/components/CardPage.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) 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, From d335bc2ab142fe91393a35a930fd3bb15d1aebf2 Mon Sep 17 00:00:00 2001 From: Peng Ying Date: Mon, 5 Jan 2026 18:32:59 -0800 Subject: [PATCH 04/15] Update OTP entry to move cursor to first unpopulated input (#22860) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Reason This change fixes a bug in the `CodeInput` component (unified variant) where clicking in the middle of the input would place the cursor at that arbitrary position, preventing users from easily completing the code. ## Overview An `onMouseDown` handler has been added to the `UnifiedCodeInputContainer`. This handler intercepts clicks and, if the click occurs on an input position *after* the first empty one, it prevents the default focus behavior and redirects focus to the first available empty input field. If all inputs are filled, it focuses the last input. ## Test Plan - All existing tests pass. - Two new test cases have been added to `js/apps/examples/ui-test-app/src/tests/CodeInput.test.tsx` to specifically cover: - Redirecting focus to the first empty input when clicking on a later input. - Allowing normal focus when clicking on or before the first empty input. - Lint checks passed. --- [Slack Thread](https://lightsparkgroup.slack.com/archives/C0447HDL3LY/p1767211308856809?thread_ts=1767211308.856809&cid=C0447HDL3LY) Open in
Cursor Open in Web ## Summary by CodeRabbit * **New Features** * Added a configurable autoFocus option for modals (defaults to enabled); specific modals can opt out. * **Bug Fixes** * Improved unified code-input focus behavior: clicking empty areas redirects to the first empty input; clicks on filled inputs preserve focus; when all inputs are filled, focus stays on the last. * **Tests** * Added tests for unified code-input focus behavior covering empty, partially filled, and all-empty scenarios. ✏️ Tip: You can customize this high-level summary in your review settings. --------- Co-authored-by: Cursor Agent Co-authored-by: Brian Siao Tick Chong Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> GitOrigin-RevId: ac96b2361390be252d2bfb994cce33852571c499 --- .../ui-test-app/src/tests/CodeInput.test.tsx | 65 ++++++++++++++++++- .../ui/src/components/CodeInput/CodeInput.tsx | 27 +++++++- 2 files changed, 89 insertions(+), 3 deletions(-) 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/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 ( From 0c7c8514d9d94ab5039f2ed4a05b3d6221bba75c Mon Sep 17 00:00:00 2001 From: Brian Siao Tick Chong Date: Tue, 6 Jan 2026 15:14:26 -0800 Subject: [PATCH 05/15] [tazapay] fixes for link bank screen (#22900) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Reason small fixes for tazapay link bank screen ## Overview - adds description to tazapay link bank screen - removes unused info icon from accountNumber input - fixes bank icon ## Test Plan ![Screenshot 2026-01-06 at 2.47.00 PM.png](https://app.graphite.com/user-attachments/assets/60b6b4c1-9b49-4c9b-8076-eb7f0cdf60de.png) ## Summary by CodeRabbit * **New Features** * Added multilingual descriptions for bank linking flow with support for English, Spanish, Portuguese, German, French, Italian, and other languages. * **Bug Fixes** * Removed unnecessary icon from account number input field. * **Style** * Updated bank icon to solid design throughout the bank linking interface. ✏️ Tip: You can customize this high-level summary in your review settings. GitOrigin-RevId: 1e5f3efad1862c652b919583644e25b30439667f --- packages/ui/src/icons/central/BankSolid.tsx | 18 ++++++++++++++++++ packages/ui/src/icons/central/index.tsx | 1 + 2 files changed, 19 insertions(+) create mode 100644 packages/ui/src/icons/central/BankSolid.tsx 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"; From 96f2cf8b6f59d93f9175efea6f16fba96a2fe3a8 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Wed, 7 Jan 2026 09:46:09 -0800 Subject: [PATCH 06/15] Add YC region mapping for Zambia & the Zambian Kwacha to currencies (#22913) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Reason closes PX-1310 Add Zambia as a YC integration. Zambia uses the Kwacha (ZMW) so add that as well 90% of this PR is files updated by adding ZMW currency ## Test go through onboarding flow for Zambian users ## Summary by CodeRabbit * **New Features** * Zambian Kwacha (ZMW) added across the platform and APIs. * Yellow Card onboarding now supports Zambia (ZMW) as an account option, including Zambia-specific mobile network choices and phone validation/masked display. * Feature gate introduced to control staged rollout of Yellow Card Zambia onboarding. ✏️ Tip: You can customize this high-level summary in your review settings. GitOrigin-RevId: ffc4b510f63c9e5bbaed4ae9bcbba24be0a9a8f3 --- packages/core/src/utils/currency.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/core/src/utils/currency.ts b/packages/core/src/utils/currency.ts index 56b40a2a0..cc02068b9 100644 --- a/packages/core/src/utils/currency.ts +++ b/packages/core/src/utils/currency.ts @@ -43,6 +43,7 @@ export const CurrencyUnit = { XAF: "XAF", MWK: "MWK", RWF: "RWF", + ZMW: "ZMW", USDT: "USDT", USDC: "USDC", @@ -110,6 +111,7 @@ const standardUnitConversionObj = { [CurrencyUnit.XAF]: (v: number) => v, [CurrencyUnit.MWK]: (v: number) => v, [CurrencyUnit.RWF]: (v: number) => v, + [CurrencyUnit.ZMW]: (v: number) => v, [CurrencyUnit.USDT]: (v: number) => v, [CurrencyUnit.USDC]: (v: number) => v, }; @@ -161,6 +163,7 @@ const CONVERSION_MAP = { [CurrencyUnit.XAF]: toBitcoinConversion, [CurrencyUnit.MWK]: toBitcoinConversion, [CurrencyUnit.RWF]: toBitcoinConversion, + [CurrencyUnit.ZMW]: toBitcoinConversion, [CurrencyUnit.USDT]: toBitcoinConversion, [CurrencyUnit.USDC]: toBitcoinConversion, }, @@ -196,6 +199,7 @@ const CONVERSION_MAP = { [CurrencyUnit.XAF]: toMicrobitcoinConversion, [CurrencyUnit.MWK]: toMicrobitcoinConversion, [CurrencyUnit.RWF]: toMicrobitcoinConversion, + [CurrencyUnit.ZMW]: toMicrobitcoinConversion, [CurrencyUnit.USDT]: toMicrobitcoinConversion, [CurrencyUnit.USDC]: toMicrobitcoinConversion, }, @@ -231,6 +235,7 @@ const CONVERSION_MAP = { [CurrencyUnit.XAF]: toMillibitcoinConversion, [CurrencyUnit.MWK]: toMillibitcoinConversion, [CurrencyUnit.RWF]: toMillibitcoinConversion, + [CurrencyUnit.ZMW]: toMillibitcoinConversion, [CurrencyUnit.USDT]: toMillibitcoinConversion, [CurrencyUnit.USDC]: toMillibitcoinConversion, }, @@ -266,6 +271,7 @@ const CONVERSION_MAP = { [CurrencyUnit.XAF]: toMillisatoshiConversion, [CurrencyUnit.MWK]: toMillisatoshiConversion, [CurrencyUnit.RWF]: toMillisatoshiConversion, + [CurrencyUnit.ZMW]: toMillisatoshiConversion, [CurrencyUnit.USDT]: toMillisatoshiConversion, [CurrencyUnit.USDC]: toMillisatoshiConversion, }, @@ -301,6 +307,7 @@ const CONVERSION_MAP = { [CurrencyUnit.XAF]: toNanobitcoinConversion, [CurrencyUnit.MWK]: toNanobitcoinConversion, [CurrencyUnit.RWF]: toNanobitcoinConversion, + [CurrencyUnit.ZMW]: toNanobitcoinConversion, [CurrencyUnit.USDT]: toNanobitcoinConversion, [CurrencyUnit.USDC]: toNanobitcoinConversion, }, @@ -336,6 +343,7 @@ const CONVERSION_MAP = { [CurrencyUnit.XAF]: toSatoshiConversion, [CurrencyUnit.MWK]: toSatoshiConversion, [CurrencyUnit.RWF]: toSatoshiConversion, + [CurrencyUnit.ZMW]: toSatoshiConversion, [CurrencyUnit.USDT]: toSatoshiConversion, [CurrencyUnit.USDC]: toSatoshiConversion, }, @@ -364,6 +372,7 @@ const CONVERSION_MAP = { [CurrencyUnit.XAF]: standardUnitConversionObj, [CurrencyUnit.MWK]: standardUnitConversionObj, [CurrencyUnit.RWF]: standardUnitConversionObj, + [CurrencyUnit.ZMW]: standardUnitConversionObj, [CurrencyUnit.USDT]: standardUnitConversionObj, [CurrencyUnit.USDC]: standardUnitConversionObj, }; @@ -452,6 +461,7 @@ export type CurrencyMap = { [CurrencyUnit.XAF]: number; [CurrencyUnit.MWK]: number; [CurrencyUnit.RWF]: number; + [CurrencyUnit.ZMW]: number; [CurrencyUnit.USDT]: number; [CurrencyUnit.USDC]: number; [CurrencyUnit.FUTURE_VALUE]: number; @@ -490,6 +500,7 @@ export type CurrencyMap = { [CurrencyUnit.XAF]: string; [CurrencyUnit.MWK]: string; [CurrencyUnit.RWF]: string; + [CurrencyUnit.ZMW]: string; [CurrencyUnit.USDT]: string; [CurrencyUnit.USDC]: string; [CurrencyUnit.FUTURE_VALUE]: string; @@ -709,6 +720,7 @@ function convertCurrencyAmountValues( xaf: CurrencyUnit.XAF, mwk: CurrencyUnit.MWK, rwf: CurrencyUnit.RWF, + zmw: CurrencyUnit.ZMW, mibtc: CurrencyUnit.MICROBITCOIN, mlbtc: CurrencyUnit.MILLIBITCOIN, nbtc: CurrencyUnit.NANOBITCOIN, @@ -792,6 +804,7 @@ export function mapCurrencyAmount( xaf, mwk, rwf, + zmw, usdt, usdc, } = convertCurrencyAmountValues(unit, value, unitsPerBtc, conversionOverride); @@ -825,6 +838,7 @@ export function mapCurrencyAmount( [CurrencyUnit.XAF]: xaf, [CurrencyUnit.MWK]: mwk, [CurrencyUnit.RWF]: rwf, + [CurrencyUnit.ZMW]: zmw, [CurrencyUnit.MICROBITCOIN]: mibtc, [CurrencyUnit.MILLIBITCOIN]: mlbtc, [CurrencyUnit.NANOBITCOIN]: nbtc, @@ -956,6 +970,10 @@ export function mapCurrencyAmount( value: rwf, unit: CurrencyUnit.RWF, }), + [CurrencyUnit.ZMW]: formatCurrencyStr({ + value: zmw, + unit: CurrencyUnit.ZMW, + }), [CurrencyUnit.USDT]: formatCurrencyStr({ value: usdt, unit: CurrencyUnit.USDT, @@ -1086,6 +1104,8 @@ export const abbrCurrencyUnit = (unit: CurrencyUnitType) => { return "MWK"; case CurrencyUnit.RWF: return "RWF"; + case CurrencyUnit.ZMW: + return "ZMW"; } return "Unsupported CurrencyUnit"; }; From 8792becb18fcee389e06e4843142b9bc2153efe2 Mon Sep 17 00:00:00 2001 From: Brian Siao Tick Chong Date: Wed, 7 Jan 2026 11:48:10 -0800 Subject: [PATCH 07/15] [uma-bridge] fix most mobile cta buttons to bottom of screen, center login and create account content (#22814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Reason Fixes mobile cta buttons to bottom of bridge card form, and centers login and create account content. This will help prevent adding fixed heights such that the button is either too low or too high on the screen ## Overview - adds parameters to BridgeCardForm to handle centering content and fixing cta content to the bottom of the screen ## Test Plan Tested all onboarding flows manually ![Screenshot 2025-12-23 at 4.11.41 PM.png](https://app.graphite.com/user-attachments/assets/e4253f2c-97e7-46ab-bd5e-994cac13df34.png) ![Screenshot 2025-12-24 at 1.32.22 PM.png](https://app.graphite.com/user-attachments/assets/bd9f6430-4714-4972-a5ac-ab68ac0ea880.png) GitOrigin-RevId: 83b42641cfe9dc65f2521a3b6b4ef7776f79d30d --- packages/ui/src/components/CardForm/CardForm.tsx | 1 + 1 file changed, 1 insertion(+) 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; `; From bed9431f91f327d6af235dc6c6c86a334b155c15 Mon Sep 17 00:00:00 2001 From: Corey Martin Date: Mon, 12 Jan 2026 17:23:59 -0800 Subject: [PATCH 08/15] [site] Support filtering by payment hash in transactions table (#23014) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding filtering by payment hash per request from Chivo: Screenshot 2026-01-12 at 1 59
30 PM ## Summary by CodeRabbit * **New Features** * Added a "Payment Hash" column to Transactions and a "Payment Hash" string filter (64-character hex) to search and filter transactions by payment hash. * **Style** * Consolidated filter action button layout into a shared component for consistent spacing, wrapping, and max-width across table filters. ✏️ Tip: You can customize this high-level summary in your review settings. GitOrigin-RevId: 5c51f07e6a4bc347492fafd60e38aacb0a6cc107 --- .../DataManagerTable/AppliedButtonsContainer.tsx | 14 ++++++++++++++ .../src/components/DataManagerTable/EnumFilter.tsx | 10 +--------- .../src/components/DataManagerTable/IdFilter.tsx | 10 +--------- .../components/DataManagerTable/StringFilter.tsx | 10 +--------- 4 files changed, 17 insertions(+), 27 deletions(-) create mode 100644 packages/ui/src/components/DataManagerTable/AppliedButtonsContainer.tsx 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; -`; From 04978c46d7d58584bfe4a4678a8d0d1205cb0eb6 Mon Sep 17 00:00:00 2001 From: Aaryaman Bhute <35084309+AaryamanBhute@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:41:55 -0800 Subject: [PATCH 09/15] [Tazapay] Add AED support (#23189) ## Reason Added support for the United Arab Emirates Dirham (AED) as a new currency option in our payment system to expand our service offerings in the Middle East region. ## Overview - Added AED to the `CurrencyUnit` and `FiatCurrencyUnit` enums across all GraphQL schemas - Created a new `TazapayAedAccountIntegration` class to handle AED-specific banking operations - Implemented IBAN validation and BIC code extraction for AED bank accounts - Added support for creating AED bank destinations in the Tazapay client ## Test Plan - Verified AED IBAN validation logic works correctly with test IBANs from UAE banks - Confirmed the new currency appears correctly in all relevant GraphQL schemas - Tested the integration with Tazapay's API for AED transactions - Ensured proper BIC code extraction from UAE IBANs GitOrigin-RevId: 0ec9cc6b9a2101ac9dcbaabb02aa126e3ec00344 --- packages/core/src/utils/currency.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/core/src/utils/currency.ts b/packages/core/src/utils/currency.ts index cc02068b9..e64fc4507 100644 --- a/packages/core/src/utils/currency.ts +++ b/packages/core/src/utils/currency.ts @@ -44,6 +44,7 @@ export const CurrencyUnit = { MWK: "MWK", RWF: "RWF", ZMW: "ZMW", + AED: "AED", USDT: "USDT", USDC: "USDC", @@ -59,6 +60,7 @@ export const CurrencyUnit = { Gbp: "GBP", Inr: "INR", Brl: "BRL", + Aed: "AED", Usdt: "USDT", Usdc: "USDC", } as const; @@ -112,6 +114,7 @@ const standardUnitConversionObj = { [CurrencyUnit.MWK]: (v: number) => v, [CurrencyUnit.RWF]: (v: number) => v, [CurrencyUnit.ZMW]: (v: number) => v, + [CurrencyUnit.AED]: (v: number) => v, [CurrencyUnit.USDT]: (v: number) => v, [CurrencyUnit.USDC]: (v: number) => v, }; @@ -164,6 +167,7 @@ const CONVERSION_MAP = { [CurrencyUnit.MWK]: toBitcoinConversion, [CurrencyUnit.RWF]: toBitcoinConversion, [CurrencyUnit.ZMW]: toBitcoinConversion, + [CurrencyUnit.AED]: toBitcoinConversion, [CurrencyUnit.USDT]: toBitcoinConversion, [CurrencyUnit.USDC]: toBitcoinConversion, }, @@ -200,6 +204,7 @@ const CONVERSION_MAP = { [CurrencyUnit.MWK]: toMicrobitcoinConversion, [CurrencyUnit.RWF]: toMicrobitcoinConversion, [CurrencyUnit.ZMW]: toMicrobitcoinConversion, + [CurrencyUnit.AED]: toMicrobitcoinConversion, [CurrencyUnit.USDT]: toMicrobitcoinConversion, [CurrencyUnit.USDC]: toMicrobitcoinConversion, }, @@ -236,6 +241,7 @@ const CONVERSION_MAP = { [CurrencyUnit.MWK]: toMillibitcoinConversion, [CurrencyUnit.RWF]: toMillibitcoinConversion, [CurrencyUnit.ZMW]: toMillibitcoinConversion, + [CurrencyUnit.AED]: toMillibitcoinConversion, [CurrencyUnit.USDT]: toMillibitcoinConversion, [CurrencyUnit.USDC]: toMillibitcoinConversion, }, @@ -272,6 +278,7 @@ const CONVERSION_MAP = { [CurrencyUnit.MWK]: toMillisatoshiConversion, [CurrencyUnit.RWF]: toMillisatoshiConversion, [CurrencyUnit.ZMW]: toMillisatoshiConversion, + [CurrencyUnit.AED]: toMillisatoshiConversion, [CurrencyUnit.USDT]: toMillisatoshiConversion, [CurrencyUnit.USDC]: toMillisatoshiConversion, }, @@ -308,6 +315,7 @@ const CONVERSION_MAP = { [CurrencyUnit.MWK]: toNanobitcoinConversion, [CurrencyUnit.RWF]: toNanobitcoinConversion, [CurrencyUnit.ZMW]: toNanobitcoinConversion, + [CurrencyUnit.AED]: toNanobitcoinConversion, [CurrencyUnit.USDT]: toNanobitcoinConversion, [CurrencyUnit.USDC]: toNanobitcoinConversion, }, @@ -344,6 +352,7 @@ const CONVERSION_MAP = { [CurrencyUnit.MWK]: toSatoshiConversion, [CurrencyUnit.RWF]: toSatoshiConversion, [CurrencyUnit.ZMW]: toSatoshiConversion, + [CurrencyUnit.AED]: toSatoshiConversion, [CurrencyUnit.USDT]: toSatoshiConversion, [CurrencyUnit.USDC]: toSatoshiConversion, }, @@ -373,6 +382,7 @@ const CONVERSION_MAP = { [CurrencyUnit.MWK]: standardUnitConversionObj, [CurrencyUnit.RWF]: standardUnitConversionObj, [CurrencyUnit.ZMW]: standardUnitConversionObj, + [CurrencyUnit.AED]: standardUnitConversionObj, [CurrencyUnit.USDT]: standardUnitConversionObj, [CurrencyUnit.USDC]: standardUnitConversionObj, }; @@ -462,6 +472,7 @@ export type CurrencyMap = { [CurrencyUnit.MWK]: number; [CurrencyUnit.RWF]: number; [CurrencyUnit.ZMW]: number; + [CurrencyUnit.AED]: number; [CurrencyUnit.USDT]: number; [CurrencyUnit.USDC]: number; [CurrencyUnit.FUTURE_VALUE]: number; @@ -501,6 +512,7 @@ export type CurrencyMap = { [CurrencyUnit.MWK]: string; [CurrencyUnit.RWF]: string; [CurrencyUnit.ZMW]: string; + [CurrencyUnit.AED]: string; [CurrencyUnit.USDT]: string; [CurrencyUnit.USDC]: string; [CurrencyUnit.FUTURE_VALUE]: string; @@ -721,6 +733,7 @@ function convertCurrencyAmountValues( mwk: CurrencyUnit.MWK, rwf: CurrencyUnit.RWF, zmw: CurrencyUnit.ZMW, + aed: CurrencyUnit.AED, mibtc: CurrencyUnit.MICROBITCOIN, mlbtc: CurrencyUnit.MILLIBITCOIN, nbtc: CurrencyUnit.NANOBITCOIN, @@ -805,6 +818,7 @@ export function mapCurrencyAmount( mwk, rwf, zmw, + aed, usdt, usdc, } = convertCurrencyAmountValues(unit, value, unitsPerBtc, conversionOverride); @@ -839,6 +853,7 @@ export function mapCurrencyAmount( [CurrencyUnit.MWK]: mwk, [CurrencyUnit.RWF]: rwf, [CurrencyUnit.ZMW]: zmw, + [CurrencyUnit.AED]: aed, [CurrencyUnit.MICROBITCOIN]: mibtc, [CurrencyUnit.MILLIBITCOIN]: mlbtc, [CurrencyUnit.NANOBITCOIN]: nbtc, @@ -974,6 +989,10 @@ export function mapCurrencyAmount( value: zmw, unit: CurrencyUnit.ZMW, }), + [CurrencyUnit.AED]: formatCurrencyStr({ + value: aed, + unit: CurrencyUnit.AED, + }), [CurrencyUnit.USDT]: formatCurrencyStr({ value: usdt, unit: CurrencyUnit.USDT, @@ -1106,6 +1125,8 @@ export const abbrCurrencyUnit = (unit: CurrencyUnitType) => { return "RWF"; case CurrencyUnit.ZMW: return "ZMW"; + case CurrencyUnit.AED: + return "AED"; } return "Unsupported CurrencyUnit"; }; From 36218946e07ad53ba77f448c2c85c007b8d0a9ff Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Mon, 26 Jan 2026 14:52:59 -0800 Subject: [PATCH 10/15] Add GTQ - Guatamalan Quetzal currency (#23249) ## Reason Thunes onboarding commences! The first 2 countries we'll iterate on for the implementation are El Salvador and Guatemala, as they're both reasonably supported in the dev environment. El Salvador is USD, which is convenient. For Guatemala we'll need the Quetzal . Fun fact about the Quetzal, the currency notes display their value in Mayan numerals in the top right corner GitOrigin-RevId: e4e32220f64b0226e65d37da445784a5d00c47c0 --- packages/core/src/utils/currency.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/core/src/utils/currency.ts b/packages/core/src/utils/currency.ts index e64fc4507..35bfe585c 100644 --- a/packages/core/src/utils/currency.ts +++ b/packages/core/src/utils/currency.ts @@ -45,6 +45,7 @@ export const CurrencyUnit = { RWF: "RWF", ZMW: "ZMW", AED: "AED", + GTQ: "GTQ", USDT: "USDT", USDC: "USDC", @@ -115,6 +116,7 @@ const standardUnitConversionObj = { [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, }; @@ -168,6 +170,7 @@ const CONVERSION_MAP = { [CurrencyUnit.RWF]: toBitcoinConversion, [CurrencyUnit.ZMW]: toBitcoinConversion, [CurrencyUnit.AED]: toBitcoinConversion, + [CurrencyUnit.GTQ]: toBitcoinConversion, [CurrencyUnit.USDT]: toBitcoinConversion, [CurrencyUnit.USDC]: toBitcoinConversion, }, @@ -205,6 +208,7 @@ const CONVERSION_MAP = { [CurrencyUnit.RWF]: toMicrobitcoinConversion, [CurrencyUnit.ZMW]: toMicrobitcoinConversion, [CurrencyUnit.AED]: toMicrobitcoinConversion, + [CurrencyUnit.GTQ]: toMicrobitcoinConversion, [CurrencyUnit.USDT]: toMicrobitcoinConversion, [CurrencyUnit.USDC]: toMicrobitcoinConversion, }, @@ -242,6 +246,7 @@ const CONVERSION_MAP = { [CurrencyUnit.RWF]: toMillibitcoinConversion, [CurrencyUnit.ZMW]: toMillibitcoinConversion, [CurrencyUnit.AED]: toMillibitcoinConversion, + [CurrencyUnit.GTQ]: toMillibitcoinConversion, [CurrencyUnit.USDT]: toMillibitcoinConversion, [CurrencyUnit.USDC]: toMillibitcoinConversion, }, @@ -279,6 +284,7 @@ const CONVERSION_MAP = { [CurrencyUnit.RWF]: toMillisatoshiConversion, [CurrencyUnit.ZMW]: toMillisatoshiConversion, [CurrencyUnit.AED]: toMillisatoshiConversion, + [CurrencyUnit.GTQ]: toMillisatoshiConversion, [CurrencyUnit.USDT]: toMillisatoshiConversion, [CurrencyUnit.USDC]: toMillisatoshiConversion, }, @@ -316,6 +322,7 @@ const CONVERSION_MAP = { [CurrencyUnit.RWF]: toNanobitcoinConversion, [CurrencyUnit.ZMW]: toNanobitcoinConversion, [CurrencyUnit.AED]: toNanobitcoinConversion, + [CurrencyUnit.GTQ]: toNanobitcoinConversion, [CurrencyUnit.USDT]: toNanobitcoinConversion, [CurrencyUnit.USDC]: toNanobitcoinConversion, }, @@ -353,6 +360,7 @@ const CONVERSION_MAP = { [CurrencyUnit.RWF]: toSatoshiConversion, [CurrencyUnit.ZMW]: toSatoshiConversion, [CurrencyUnit.AED]: toSatoshiConversion, + [CurrencyUnit.GTQ]: toSatoshiConversion, [CurrencyUnit.USDT]: toSatoshiConversion, [CurrencyUnit.USDC]: toSatoshiConversion, }, @@ -383,6 +391,7 @@ const CONVERSION_MAP = { [CurrencyUnit.RWF]: standardUnitConversionObj, [CurrencyUnit.ZMW]: standardUnitConversionObj, [CurrencyUnit.AED]: standardUnitConversionObj, + [CurrencyUnit.GTQ]: standardUnitConversionObj, [CurrencyUnit.USDT]: standardUnitConversionObj, [CurrencyUnit.USDC]: standardUnitConversionObj, }; @@ -473,6 +482,7 @@ export type CurrencyMap = { [CurrencyUnit.RWF]: number; [CurrencyUnit.ZMW]: number; [CurrencyUnit.AED]: number; + [CurrencyUnit.GTQ]: number; [CurrencyUnit.USDT]: number; [CurrencyUnit.USDC]: number; [CurrencyUnit.FUTURE_VALUE]: number; @@ -513,6 +523,7 @@ export type CurrencyMap = { [CurrencyUnit.RWF]: string; [CurrencyUnit.ZMW]: string; [CurrencyUnit.AED]: string; + [CurrencyUnit.GTQ]: string; [CurrencyUnit.USDT]: string; [CurrencyUnit.USDC]: string; [CurrencyUnit.FUTURE_VALUE]: string; @@ -734,6 +745,7 @@ function convertCurrencyAmountValues( rwf: CurrencyUnit.RWF, zmw: CurrencyUnit.ZMW, aed: CurrencyUnit.AED, + gtq: CurrencyUnit.GTQ, mibtc: CurrencyUnit.MICROBITCOIN, mlbtc: CurrencyUnit.MILLIBITCOIN, nbtc: CurrencyUnit.NANOBITCOIN, @@ -819,6 +831,7 @@ export function mapCurrencyAmount( rwf, zmw, aed, + gtq, usdt, usdc, } = convertCurrencyAmountValues(unit, value, unitsPerBtc, conversionOverride); @@ -854,6 +867,7 @@ export function mapCurrencyAmount( [CurrencyUnit.RWF]: rwf, [CurrencyUnit.ZMW]: zmw, [CurrencyUnit.AED]: aed, + [CurrencyUnit.GTQ]: gtq, [CurrencyUnit.MICROBITCOIN]: mibtc, [CurrencyUnit.MILLIBITCOIN]: mlbtc, [CurrencyUnit.NANOBITCOIN]: nbtc, @@ -993,6 +1007,10 @@ export function mapCurrencyAmount( value: aed, unit: CurrencyUnit.AED, }), + [CurrencyUnit.GTQ]: formatCurrencyStr({ + value: gtq, + unit: CurrencyUnit.GTQ, + }), [CurrencyUnit.USDT]: formatCurrencyStr({ value: usdt, unit: CurrencyUnit.USDT, @@ -1127,6 +1145,8 @@ export const abbrCurrencyUnit = (unit: CurrencyUnitType) => { return "ZMW"; case CurrencyUnit.AED: return "AED"; + case CurrencyUnit.GTQ: + return "GTQ"; } return "Unsupported CurrencyUnit"; }; From b8f309f8c7bc5ee87a8c600ee7769fa960c7619e Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Thu, 29 Jan 2026 16:52:28 -0800 Subject: [PATCH 11/15] use agent-md-refactor skill to reduce the size and increase the effectiveness of our claude code md files (#23278) ## Reason Use the agents-md-refactor skill to tighten up our CLAUDE.md files in sparkcore more on the agents refactor md here : [link](https://skills.sh/softaworks/agent-toolkit/agent-md-refactor ) ## What Changed 1. Root CLAUDE.md - Cut from 73 to 37 lines. Removed setup prerequisites (not AI instructions), kept only essential commands and links. 2. sparkcore/CLAUDE.md - Cut from 160 to 68 lines. Removed redundant debugging tips, setup docs, and verbose explanations. Kept actionable patterns. 3. js/CLAUDE.md - Cut from 163 to 69 lines. Removed release process docs, React stack details, and configuration file explanations. Kept commands and patterns. 4. New linked files - Extracted cross-cutting concerns: - .claude/docs/graphql-workflow.md - Single source for GraphQL regeneration - .claude/docs/testing.md - Consolidated testing philosophy from your CLAUDE.local.md 5. Kept untouched: - sparkcore/sparkcore/spark/CLAUDE.md - Domain-specific, well-organized, essential for Spark protocol work - CLAUDE.local.md - Your personal overrides (git workflow with Graphite, Python commands) GitOrigin-RevId: 5db5c5e8a25f9dc3b48b83fcfd73e3374a81dadb --- CLAUDE.md | 177 +++++++++++++----------------------------------------- 1 file changed, 41 insertions(+), 136 deletions(-) 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` | From c0a5b466f1cd2e79c569246dee73cd4273cfda24 Mon Sep 17 00:00:00 2001 From: Corey Martin Date: Mon, 9 Feb 2026 12:57:29 -0800 Subject: [PATCH 12/15] Tests for Secp256k1SigningKey (#23583) https://lightsparkgroup.slack.com/archives/C08T5ATQBBK/p1770617598579799?thread_ts=1770562711.801429&cid=C08T5ATQBBK --------- Co-authored-by: Claude Opus 4.6 GitOrigin-RevId: 8d490d46961abd9a99c66cf40fbac67a0f48c0b0 --- .../crypto/tests/Secp256k1SigningKey.test.ts | 297 ++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 packages/core/src/crypto/tests/Secp256k1SigningKey.test.ts 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); + } + }); + }); +}); From 8317254ec5553db4f79eca3e7f0f7679a542a15e Mon Sep 17 00:00:00 2001 From: Corey Martin Date: Mon, 9 Feb 2026 13:06:41 -0800 Subject: [PATCH 13/15] [core, lightspark-cli] Replace secp256k1 with @noble/curves (#23584) https://lightsparkgroup.slack.com/archives/C08T5ATQBBK/p1770617598579799?thread_ts=1770562711.801429&cid=C08T5ATQBBK --------- Co-authored-by: Claude Opus 4.6 GitOrigin-RevId: 006a2b9a49e1acd2baa34d588108b1d07e508c6e --- packages/core/package.json | 3 +-- packages/core/src/crypto/SigningKey.ts | 6 +++--- packages/lightspark-cli/package.json | 5 ++--- packages/lightspark-cli/src/index.ts | 17 +++++++++++++---- 4 files changed, 19 insertions(+), 12 deletions(-) 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/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); From ec50142f1750cd4d29764cd817d4a3556b61b4bd Mon Sep 17 00:00:00 2001 From: Lightspark Eng Date: Mon, 9 Feb 2026 21:16:09 +0000 Subject: [PATCH 14/15] CI update lock file for PR --- yarn.lock | 58 +++++++++++++++++-------------------------------------- 1 file changed, 18 insertions(+), 40 deletions(-) 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" From 00c2d5140d25cff02b294d0a8bed3e5ec02ad8f5 Mon Sep 17 00:00:00 2001 From: Corey Martin Date: Mon, 9 Feb 2026 13:20:46 -0800 Subject: [PATCH 15/15] add changesets --- .changeset/light-bars-visit.md | 6 ++++++ .changeset/spotty-yaks-lie.md | 8 ++++++++ .changeset/tender-jobs-call.md | 9 +++++++++ 3 files changed, 23 insertions(+) create mode 100644 .changeset/light-bars-visit.md create mode 100644 .changeset/spotty-yaks-lie.md create mode 100644 .changeset/tender-jobs-call.md 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).