Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3501dbc
[UmaProber] Add Tether as a currency unit (#20124)
exs6350 Aug 28, 2025
fc09146
[nage] add feature to create api tokens (#20167)
bsiaotickchong Aug 29, 2025
d13e00c
[nage] add delete api token feature (#20186)
bsiaotickchong Aug 29, 2025
f6838ad
[Nage] Fix command center typography and add auto scroll (#19868)
JasonCWang Aug 29, 2025
3d739e4
[Nage] Add update name / start of invitations into nage settings (#20…
JasonCWang Sep 3, 2025
d123ce5
CI update lock file for PR
Sep 3, 2025
0999056
[nage] add filter query params for DataManagerTable (#20270)
bsiaotickchong Sep 4, 2025
3cebc1a
[site] route to umaaas routes depending on user role (#20317)
bsiaotickchong Sep 8, 2025
3bc3694
[Nage] Add sandbox platform creation flow (#20257)
JasonCWang Sep 8, 2025
12ff69b
[nage] show nav items depending on user role (#20318)
bsiaotickchong Sep 8, 2025
35338f4
[nage] fix nav button and command center search button styling (#20319)
bsiaotickchong Sep 8, 2025
bdfd74a
[ui] default button justifyContent to center (#20339)
bsiaotickchong Sep 8, 2025
591cb03
[Nage] Update settings styling, update nage select, and implement use…
JasonCWang Sep 9, 2025
32682c0
[nage] refresh table data on platform change (#20376)
bsiaotickchong Sep 9, 2025
9f6470f
[Nage] Add sandbox deletion functionality and separate prod/sandbox c…
JasonCWang Sep 10, 2025
685fedc
[nage] add progress bar loading style to data manager tables (#20398)
bsiaotickchong Sep 10, 2025
e89a841
add blocked ip screen to add credentials component (#20034)
matthappens Sep 11, 2025
6fc3567
Make bobby tables textarea resizable (#20476)
k15z Sep 15, 2025
d858f58
[lightspark-sdk] Allow RemoteSigningWebhookHandler validator to be as…
coreymartin Sep 17, 2025
747cd8a
[Nage] Add basic uuid frontend to command center (#20475)
JasonCWang Sep 18, 2025
9e35794
[nage] add filter pill redesign to DataManagerTable
bsiaotickchong Sep 17, 2025
9939bf6
CI update lock file for PR
Sep 18, 2025
f9a38c7
[TazaPay] Add UPI bank linking interface for INR (#20145)
AaryamanBhute Sep 19, 2025
b255b33
add page to update iban and standing order for striga users (#20491)
matthappens Sep 19, 2025
1d081a2
[nage] add Apply button to date filter and fix change handling (#20602)
bsiaotickchong Sep 19, 2025
797dcdb
Update from public js-sdk main branch (#20119)
lightspark-ci-js-sdk[bot] Sep 19, 2025
3c4ce69
CI update lock file for PR
Sep 19, 2025
f429ce0
chore: adding changesets
pengying Sep 19, 2025
a1b33fc
[nage] fix string and id filters, wrap pill filters in header (#20604)
bsiaotickchong Sep 20, 2025
3ef2208
fix changesets
coreymartin Sep 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/better-turkeys-feel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lightsparkdev/core": patch
---

- Add USDT to supported currencies
5 changes: 5 additions & 0 deletions .changeset/petite-pans-post.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lightsparkdev/lightspark-sdk": patch
---

- Adding async validator for remote signing
5 changes: 5 additions & 0 deletions .changeset/poor-rules-invent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lightsparkdev/ui": patch
---

- Component and theme updates
6 changes: 5 additions & 1 deletion apps/examples/remote-signing-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,11 @@ async function handleRemoteSigningWebhook(
signatureHeader: string,
) {
const validator = {
should_sign: (webhook: WebhookEvent) => true,
should_sign: async (webhook: WebhookEvent) => {
// Simulate async policy, e.g., fetch account config or check DB
await new Promise((r) => setTimeout(r, 1));
return webhook.event_type === WebhookEventType.REMOTE_SIGNING;
},
};

const remoteSigningHandler = new RemoteSigningWebhookHandler(
Expand Down
37 changes: 35 additions & 2 deletions packages/core/src/utils/currency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const CurrencyUnit = {
EUR: "EUR",
GBP: "GBP",
INR: "INR",
USDT: "USDT",

Bitcoin: "BITCOIN",
Microbitcoin: "MICROBITCOIN",
Expand All @@ -36,6 +37,7 @@ export const CurrencyUnit = {
Php: "PHP",
Gbp: "GBP",
Inr: "INR",
Usdt: "USDT",
} as const;

export type CurrencyUnitType = (typeof CurrencyUnit)[keyof typeof CurrencyUnit];
Expand Down Expand Up @@ -67,6 +69,7 @@ const standardUnitConversionObj = {
[CurrencyUnit.EUR]: (v: number) => v,
[CurrencyUnit.GBP]: (v: number) => v,
[CurrencyUnit.INR]: (v: number) => v,
[CurrencyUnit.USDT]: (v: number) => v,
};

/* Round without decimals since we're returning cents: */
Expand Down Expand Up @@ -97,6 +100,7 @@ const CONVERSION_MAP = {
[CurrencyUnit.EUR]: toBitcoinConversion,
[CurrencyUnit.GBP]: toBitcoinConversion,
[CurrencyUnit.INR]: toBitcoinConversion,
[CurrencyUnit.USDT]: toBitcoinConversion,
},
[CurrencyUnit.MICROBITCOIN]: {
[CurrencyUnit.BITCOIN]: (v: number) => v / 1_000_000,
Expand All @@ -111,6 +115,7 @@ const CONVERSION_MAP = {
[CurrencyUnit.EUR]: toMicrobitcoinConversion,
[CurrencyUnit.GBP]: toMicrobitcoinConversion,
[CurrencyUnit.INR]: toMicrobitcoinConversion,
[CurrencyUnit.USDT]: toMicrobitcoinConversion,
},
[CurrencyUnit.MILLIBITCOIN]: {
[CurrencyUnit.BITCOIN]: (v: number) => v / 1_000,
Expand All @@ -125,6 +130,7 @@ const CONVERSION_MAP = {
[CurrencyUnit.EUR]: toMillibitcoinConversion,
[CurrencyUnit.GBP]: toMillibitcoinConversion,
[CurrencyUnit.INR]: toMillibitcoinConversion,
[CurrencyUnit.USDT]: toMillibitcoinConversion,
},
[CurrencyUnit.MILLISATOSHI]: {
[CurrencyUnit.BITCOIN]: (v: number) => v / 100_000_000_000,
Expand All @@ -139,6 +145,7 @@ const CONVERSION_MAP = {
[CurrencyUnit.EUR]: toMillisatoshiConversion,
[CurrencyUnit.GBP]: toMillisatoshiConversion,
[CurrencyUnit.INR]: toMillisatoshiConversion,
[CurrencyUnit.USDT]: toMillisatoshiConversion,
},
[CurrencyUnit.NANOBITCOIN]: {
[CurrencyUnit.BITCOIN]: (v: number) => v / 1_000_000_000,
Expand All @@ -153,6 +160,7 @@ const CONVERSION_MAP = {
[CurrencyUnit.EUR]: toNanobitcoinConversion,
[CurrencyUnit.GBP]: toNanobitcoinConversion,
[CurrencyUnit.INR]: toNanobitcoinConversion,
[CurrencyUnit.USDT]: toNanobitcoinConversion,
},
[CurrencyUnit.SATOSHI]: {
[CurrencyUnit.BITCOIN]: (v: number) => v / 100_000_000,
Expand All @@ -167,13 +175,15 @@ const CONVERSION_MAP = {
[CurrencyUnit.EUR]: toSatoshiConversion,
[CurrencyUnit.GBP]: toSatoshiConversion,
[CurrencyUnit.INR]: toSatoshiConversion,
[CurrencyUnit.USDT]: toSatoshiConversion,
},
[CurrencyUnit.USD]: standardUnitConversionObj,
[CurrencyUnit.MXN]: standardUnitConversionObj,
[CurrencyUnit.PHP]: standardUnitConversionObj,
[CurrencyUnit.EUR]: standardUnitConversionObj,
[CurrencyUnit.GBP]: standardUnitConversionObj,
[CurrencyUnit.INR]: standardUnitConversionObj,
[CurrencyUnit.USDT]: standardUnitConversionObj,
};

export function convertCurrencyAmountValue(
Expand Down Expand Up @@ -241,6 +251,7 @@ export type CurrencyMap = {
[CurrencyUnit.EUR]: number;
[CurrencyUnit.GBP]: number;
[CurrencyUnit.INR]: number;
[CurrencyUnit.USDT]: number;
[CurrencyUnit.FUTURE_VALUE]: number;
formatted: {
sats: string;
Expand All @@ -258,6 +269,7 @@ export type CurrencyMap = {
[CurrencyUnit.EUR]: string;
[CurrencyUnit.GBP]: string;
[CurrencyUnit.INR]: string;
[CurrencyUnit.USDT]: string;
[CurrencyUnit.FUTURE_VALUE]: string;
};
isZero: boolean;
Expand Down Expand Up @@ -459,6 +471,7 @@ function convertCurrencyAmountValues(
mibtc: CurrencyUnit.MICROBITCOIN,
mlbtc: CurrencyUnit.MILLIBITCOIN,
nbtc: CurrencyUnit.NANOBITCOIN,
usdt: CurrencyUnit.USDT,
};
return Object.entries(namesToUnits).reduce(
(acc, [name, unit]) => {
Expand Down Expand Up @@ -505,8 +518,21 @@ export function mapCurrencyAmount(
* preferred_currency_unit on CurrencyAmount types: */
const conversionOverride = getPreferredConversionOverride(currencyAmountArg);

const { sats, msats, btc, usd, mxn, php, mibtc, mlbtc, nbtc, eur, gbp, inr } =
convertCurrencyAmountValues(unit, value, unitsPerBtc, conversionOverride);
const {
sats,
msats,
btc,
usd,
mxn,
php,
mibtc,
mlbtc,
nbtc,
eur,
gbp,
inr,
usdt,
} = convertCurrencyAmountValues(unit, value, unitsPerBtc, conversionOverride);

const mapWithCurrencyUnits = {
[CurrencyUnit.BITCOIN]: btc,
Expand All @@ -521,6 +547,7 @@ export function mapCurrencyAmount(
[CurrencyUnit.MICROBITCOIN]: mibtc,
[CurrencyUnit.MILLIBITCOIN]: mlbtc,
[CurrencyUnit.NANOBITCOIN]: nbtc,
[CurrencyUnit.USDT]: usdt,
[CurrencyUnit.FUTURE_VALUE]: NaN,
formatted: {
[CurrencyUnit.BITCOIN]: formatCurrencyStr({
Expand Down Expand Up @@ -571,6 +598,10 @@ export function mapCurrencyAmount(
value: inr,
unit: CurrencyUnit.INR,
}),
[CurrencyUnit.USDT]: formatCurrencyStr({
value: usdt,
unit: CurrencyUnit.USDT,
}),
[CurrencyUnit.FUTURE_VALUE]: "-",
},
};
Expand Down Expand Up @@ -651,6 +682,8 @@ export const abbrCurrencyUnit = (unit: CurrencyUnitType) => {
return "GBP";
case CurrencyUnit.INR:
return "INR";
case CurrencyUnit.USDT:
return "USDT";
}
return "Unsupported CurrencyUnit";
};
Expand Down
151 changes: 151 additions & 0 deletions packages/lightspark-sdk/src/tests/remote-signing-handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { hexToBytes } from "@lightsparkdev/core";
import { createHmac } from "crypto";
import type LightsparkClient from "../client.js";
import { RemoteSigningWebhookHandler } from "../webhooks.js";

describe("RemoteSigningWebhookHandler (integration with wasm)", () => {
const seedHex =
"1a6deac8f74fb2e332677e3f4833b5e962f80d153fb368b8ee322a9caca4113d56cccd88f1c6a74e152669d8cd373fee2f27e3645d80de27640177a8c71395f8";
const seedBytes = hexToBytes(seedHex);

test("revoke/ack path returns undefined and does not call client", async () => {
const dataHex =
"7b226576656e745f74797065223a202252454d4f54455f5349474e494e47222c20226576656e745f6964223a20223031386665366130346663323036613830303030393538343032643337663465222c20226170695f76657273696f6e223a2022323032332d30392d3133222c202274696d657374616d70223a2022323032342d30362d30355430343a32303a31362e3936323134312b30303a3030222c2022656e746974795f6964223a20224368616e6e656c3a30313866653661302d346661372d363965332d303030302d653139633836386564366564222c202264617461223a207b227375625f6576656e745f74797065223a202252455645414c5f434f554e54455250415254595f5045525f434f4d4d49544d454e545f534543524554222c2022626974636f696e5f6e6574776f726b223a202252454754455354222c20227065725f636f6d6d69746d656e745f7365637265745f696478223a2032373336333139312c20227065725f636f6d6d69746d656e745f736563726574223a202236363837613837386538393733353535353131343039653363633762643934663535336336643265616230656231353831343761383337386266386335333261222c20226e6f64655f6964223a20226e6f64655f776974685f7365727665725f7369676e696e673a30313861393633352d333637332d383864662d303030302d383237663233303531623139227d7d";
const dataBytes = hexToBytes(dataHex);
const sig =
"864f7f575ec2425eb6ecfe457cbf17b41da0e4cbb70cda126c00ea507fbf699c";
const webhookSecret = "39kyJO140v7fYkwHnR7jz8Y3UphqVeNYQk44Xx049ws";

const calls: Array<[Parameters<LightsparkClient["executeRawQuery"]>[0]]> =
[];
const executeRawQuery: LightsparkClient["executeRawQuery"] = async (
query,
) => {
calls.push([query]);
return null;
};
const client = { executeRawQuery } as unknown as LightsparkClient;

const handler = new RemoteSigningWebhookHandler(client, seedBytes, {
should_sign: async () => true,
});

const res = await handler.handleWebhookRequest(
dataBytes,
sig,
webhookSecret,
);
expect(res).toBeUndefined();
expect(calls.length).toBe(0);
});

test("async validator true → executes UpdateChannelPerCommitmentPoint", async () => {
const dataHex =
"7b226576656e745f74797065223a202252454d4f54455f5349474e494e47222c20226576656e745f6964223a20223031386665366138313066333036613830303030363835353239663539316563222c20226170695f76657273696f6e223a2022323032332d30392d3133222c202274696d657374616d70223a2022323032342d30362d30355430343a32383a34352e3137313839392b30303a3030222c2022656e746974795f6964223a20224368616e6e656c3a30313866653661382d313039362d363965332d303030302d623630373635343330663635222c202264617461223a207b227375625f6576656e745f74797065223a20224745545f5045525f434f4d4d49544d454e545f504f494e54222c2022626974636f696e5f6e6574776f726b223a202252454754455354222c202264657269766174696f6e5f70617468223a20226d2f332f3139333238222c20227065725f636f6d6d69746d656e745f706f696e745f696478223a2032373336333139312c20226e6f64655f6964223a20226e6f64655f776974685f7365727665725f7369676e696e673a30313861393633352d333637332d383864662d303030302d383237663233303531623139227d7d";
const dataBytes = hexToBytes(dataHex);
const sig =
"5809279c1fd8088a6c62be82d3e858c11ce187ee97a0fadae1ae498e2f69442c";
const webhookSecret = "39kyJO140v7fYkwHnR7jz8Y3UphqVeNYQk44Xx049ws";

const expectedVarsJson =
'{"channel_id":"Channel:018fe6a8-1096-69e3-0000-b60765430f65","per_commitment_point":"027ba8a666d57947ba8337d0e211cae9125dbaf7c9883cb34f49393bc1a4907dd8","per_commitment_point_index":27363191}';

const calls: Array<[Parameters<LightsparkClient["executeRawQuery"]>[0]]> =
[];
const executeRawQuery: LightsparkClient["executeRawQuery"] = async (
query,
) => {
calls.push([query]);
return null;
};
const client = { executeRawQuery } as unknown as LightsparkClient;

const handler = new RemoteSigningWebhookHandler(client, seedBytes, {
should_sign: async () => true,
});

await handler.handleWebhookRequest(dataBytes, sig, webhookSecret);
expect(calls.length).toBe(1);
const [[firstArg]] = calls;
expect(firstArg.queryPayload).toMatch(
/^mutation UpdateChannelPerCommitmentPoint/,
);
expect(firstArg.variables).toEqual(JSON.parse(expectedVarsJson));
});

test("async validator false → does not execute query and throws", async () => {
const dataHex =
"7b226576656e745f74797065223a202252454d4f54455f5349474e494e47222c20226576656e745f6964223a20223031386665366138313066333036613830303030363835353239663539316563222c20226170695f76657273696f6e223a2022323032332d30392d3133222c202274696d657374616d70223a2022323032342d30362d30355430343a32383a34352e3137313839392b30303a3030222c2022656e746974795f6964223a20224368616e6e656c3a30313866653661382d313039362d363965332d303030302d623630373635343330663635222c202264617461223a207b227375625f6576656e745f74797065223a20224745545f5045525f434f4d4d49544d454e545f504f494e54222c2022626974636f696e5f6e6574776f726b223a202252454754455354222c202264657269766174696f6e5f70617468223a20226d2f332f3139333238222c20227065725f636f6d6d69746d656e745f706f696e745f696478223a2032373336333139312c20226e6f64655f6964223a20226e6f64655f776974685f7365727665725f7369676e696e673a30313861393633352d333637332d383864662d303030302d383237663233303531623139227d7d";
const dataBytes = hexToBytes(dataHex);
const sig =
"5809279c1fd8088a6c62be82d3e858c11ce187ee97a0fadae1ae498e2f69442c";
const webhookSecret = "39kyJO140v7fYkwHnR7jz8Y3UphqVeNYQk44Xx049ws";

const calls: Array<[Parameters<LightsparkClient["executeRawQuery"]>[0]]> =
[];
const executeRawQuery: LightsparkClient["executeRawQuery"] = async (
query,
) => {
calls.push([query]);
return null;
};
const client = { executeRawQuery } as unknown as LightsparkClient;

const handler = new RemoteSigningWebhookHandler(client, seedBytes, {
should_sign: async () => false,
});

await expect(
handler.handleWebhookRequest(dataBytes, sig, webhookSecret),
).rejects.toBeTruthy();
expect(calls.length).toBe(0);
});

test("async validator false (DERIVE_KEY_AND_SIGN) → decline_to_sign_messages", async () => {
const event = {
event_type: "REMOTE_SIGNING",
event_id: "abc-derive",
timestamp: "2024-06-05T04:28:45.171899+00:00",
entity_id: "Node:018fe6a8-1096-69e3-0000-b60765430f65",
data: {
sub_event_type: "DERIVE_KEY_AND_SIGN",
bitcoin_network: "REGTEST",
signing_jobs: [
{
id: "payload-1",
derivation_path: "m/3/2106220917/0",
message:
"476bdd1db5d91897d00d75300eef50c0da7e0b2dada06dde93cbb5903b7e16b2",
is_raw: true,
},
],
},
};
const json = JSON.stringify(event);
const dataBytes = new TextEncoder().encode(json);
const webhookSecret = "39kyJO140v7fYkwHnR7jz8Y3UphqVeNYQk44Xx049ws";
const sig = createHmac("sha256", webhookSecret)
.update(dataBytes)
.digest("hex");

const calls: Array<[Parameters<LightsparkClient["executeRawQuery"]>[0]]> =
[];
const executeRawQuery: LightsparkClient["executeRawQuery"] = async (
query,
) => {
calls.push([query]);
return null;
};
const client = { executeRawQuery } as unknown as LightsparkClient;

const handler = new RemoteSigningWebhookHandler(client, seedBytes, {
should_sign: async () => false,
});

await handler.handleWebhookRequest(dataBytes, sig, webhookSecret);
expect(calls.length).toBe(1);
const [[firstArg]] = calls;
expect(firstArg.queryPayload).toMatch(/decline_to_sign_messages\s*\(/);
expect(firstArg.variables).toEqual({ payload_ids: ["payload-1"] });
});
});
14 changes: 12 additions & 2 deletions packages/lightspark-sdk/src/webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const parseWebhook = (data: Uint8Array): WebhookEvent => {
};

type Validator = {
should_sign: (event: WebhookEvent) => boolean;
should_sign: (event: WebhookEvent) => boolean | Promise<boolean>;
};

export class RemoteSigningWebhookHandler {
Expand Down Expand Up @@ -71,15 +71,25 @@ export class RemoteSigningWebhookHandler {
);
}

// Pre-parse to expose a typed event to the validator and allow async decisions.
const event = await verifyAndParseWebhook(
data,
webhookSignature,
webhookSecret,
);

const decision = await this.validator.should_sign(event);

const { wasm_handle_remote_signing_webhook_event } = await import(
"@lightsparkdev/crypto-wasm"
);

const response = wasm_handle_remote_signing_webhook_event(
data,
webhookSignature,
webhookSecret,
this.#masterSeed,
this.validator,
{ should_sign: () => decision },
);
if (!response) {
return;
Expand Down
1 change: 1 addition & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
"@wojtekmaj/react-datetimerange-picker": "^5.5.0",
"@zxing/browser": "^0.1.1",
"@zxing/library": "^0.19.2",
"dayjs": "^1.11.7",
"deep-object-diff": "^1.1.9",
"deepmerge": "^4.3.1",
"libphonenumber-js": "^1.11.1",
Expand Down
Loading