Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b54e567
[trivial] Updated description for `tasks.site`. (#22343)
mhrheaume Dec 8, 2025
cd16434
[umaaas] add send and receive failure reason filters for operations (…
bsiaotickchong Dec 9, 2025
25f1d8d
[Bridge] Update phone number regex to accept 7-15 digits (#22382)
AaryamanBhute Dec 9, 2025
bc3600a
Added `failHtlcs` to `LightsparkClient`. (#22393)
mhrheaume Dec 10, 2025
8568a2b
[ops] add hardcore mode easter egg (#22425)
bsiaotickchong Dec 10, 2025
06eb143
[striga] fix verify-identity modal not being scrollable (#22469)
bsiaotickchong Dec 12, 2025
88fd808
Add new currencies, mwk, xof, xaf, and rwf (#22501)
matthappens Dec 15, 2025
0b6cef3
[uma-bridge] fix overflowing email verification button text (#22532)
bsiaotickchong Dec 15, 2025
87f0ed9
[striga] update kycsumsubmodal scroll button (#22534)
bsiaotickchong Dec 16, 2025
dcc9c37
[striga] link bank UI fixes (#22577)
bsiaotickchong Dec 17, 2025
2a00685
[uma-bridge] fix mobile drawer scroll bleed (#22593)
bsiaotickchong Dec 18, 2025
7f5ef95
Striga design updates / kyc fixes (#22603)
matthappens Dec 18, 2025
78df179
Add dedicated name match failed page for Nigeria (#22653)
matthappens Dec 18, 2025
3e356c9
[AI tools] Several small tweaks to make cursor work better. (#22683)
jklein24 Dec 19, 2025
fee46bb
[uma-bridge] add segment events for all flows, update event name form…
bsiaotickchong Dec 22, 2025
08c3f7f
fix double display of currency for currencies without a narrow symbol…
matthappens Dec 23, 2025
6d23ba4
[uma-bridge] add new 2fa code input design (#22794)
bsiaotickchong Dec 24, 2025
1cc7197
feat: adding idempotency to JS withdrawal request (#22827)
pengying Dec 28, 2025
a3d32a4
adding changesets
pengying Jan 3, 2026
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/curvy-experts-fall.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lightsparkdev/core": patch
---

Adding currency support for XOF, XAF, MWK, RWF
5 changes: 5 additions & 0 deletions .changeset/little-hands-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lightsparkdev/ui": patch
---

UI updates and fixes for bank linking, kyc, 2fa, filters
5 changes: 5 additions & 0 deletions .changeset/nice-pumas-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lightsparkdev/lightspark-sdk": patch
---

Adding ability to fail HTLCs and add an idempotency key to requestWithdrawal
2 changes: 1 addition & 1 deletion .mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ description = "Start ops frontend connected to minikube"
run = "VITE_PROXY_TARGET=http://app.minikube.local yarn start ops"

[tasks.site]
description = "Start CN2 frontend connected to minikube"
description = "Start CN2 frontend development server"
run = "yarn start site"

[tasks.site-minikube]
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
87 changes: 87 additions & 0 deletions packages/core/src/utils/currency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ export const CurrencyUnit = {
TZS: "TZS",
UGX: "UGX",
BWP: "BWP",
XOF: "XOF",
XAF: "XAF",
MWK: "MWK",
RWF: "RWF",
USDT: "USDT",
USDC: "USDC",

Expand Down Expand Up @@ -102,6 +106,10 @@ const standardUnitConversionObj = {
[CurrencyUnit.TZS]: (v: number) => v,
[CurrencyUnit.UGX]: (v: number) => v,
[CurrencyUnit.BWP]: (v: number) => v,
[CurrencyUnit.XOF]: (v: number) => v,
[CurrencyUnit.XAF]: (v: number) => v,
[CurrencyUnit.MWK]: (v: number) => v,
[CurrencyUnit.RWF]: (v: number) => v,
[CurrencyUnit.USDT]: (v: number) => v,
[CurrencyUnit.USDC]: (v: number) => v,
};
Expand Down Expand Up @@ -149,6 +157,10 @@ const CONVERSION_MAP = {
[CurrencyUnit.TZS]: toBitcoinConversion,
[CurrencyUnit.UGX]: toBitcoinConversion,
[CurrencyUnit.BWP]: toBitcoinConversion,
[CurrencyUnit.XOF]: toBitcoinConversion,
[CurrencyUnit.XAF]: toBitcoinConversion,
[CurrencyUnit.MWK]: toBitcoinConversion,
[CurrencyUnit.RWF]: toBitcoinConversion,
[CurrencyUnit.USDT]: toBitcoinConversion,
[CurrencyUnit.USDC]: toBitcoinConversion,
},
Expand Down Expand Up @@ -180,6 +192,10 @@ const CONVERSION_MAP = {
[CurrencyUnit.TZS]: toMicrobitcoinConversion,
[CurrencyUnit.UGX]: toMicrobitcoinConversion,
[CurrencyUnit.BWP]: toMicrobitcoinConversion,
[CurrencyUnit.XOF]: toMicrobitcoinConversion,
[CurrencyUnit.XAF]: toMicrobitcoinConversion,
[CurrencyUnit.MWK]: toMicrobitcoinConversion,
[CurrencyUnit.RWF]: toMicrobitcoinConversion,
[CurrencyUnit.USDT]: toMicrobitcoinConversion,
[CurrencyUnit.USDC]: toMicrobitcoinConversion,
},
Expand Down Expand Up @@ -211,6 +227,10 @@ const CONVERSION_MAP = {
[CurrencyUnit.TZS]: toMillibitcoinConversion,
[CurrencyUnit.UGX]: toMillibitcoinConversion,
[CurrencyUnit.BWP]: toMillibitcoinConversion,
[CurrencyUnit.XOF]: toMillibitcoinConversion,
[CurrencyUnit.XAF]: toMillibitcoinConversion,
[CurrencyUnit.MWK]: toMillibitcoinConversion,
[CurrencyUnit.RWF]: toMillibitcoinConversion,
[CurrencyUnit.USDT]: toMillibitcoinConversion,
[CurrencyUnit.USDC]: toMillibitcoinConversion,
},
Expand Down Expand Up @@ -242,6 +262,10 @@ const CONVERSION_MAP = {
[CurrencyUnit.TZS]: toMillisatoshiConversion,
[CurrencyUnit.UGX]: toMillisatoshiConversion,
[CurrencyUnit.BWP]: toMillisatoshiConversion,
[CurrencyUnit.XOF]: toMillisatoshiConversion,
[CurrencyUnit.XAF]: toMillisatoshiConversion,
[CurrencyUnit.MWK]: toMillisatoshiConversion,
[CurrencyUnit.RWF]: toMillisatoshiConversion,
[CurrencyUnit.USDT]: toMillisatoshiConversion,
[CurrencyUnit.USDC]: toMillisatoshiConversion,
},
Expand Down Expand Up @@ -273,6 +297,10 @@ const CONVERSION_MAP = {
[CurrencyUnit.TZS]: toNanobitcoinConversion,
[CurrencyUnit.UGX]: toNanobitcoinConversion,
[CurrencyUnit.BWP]: toNanobitcoinConversion,
[CurrencyUnit.XOF]: toNanobitcoinConversion,
[CurrencyUnit.XAF]: toNanobitcoinConversion,
[CurrencyUnit.MWK]: toNanobitcoinConversion,
[CurrencyUnit.RWF]: toNanobitcoinConversion,
[CurrencyUnit.USDT]: toNanobitcoinConversion,
[CurrencyUnit.USDC]: toNanobitcoinConversion,
},
Expand Down Expand Up @@ -304,6 +332,10 @@ const CONVERSION_MAP = {
[CurrencyUnit.TZS]: toSatoshiConversion,
[CurrencyUnit.UGX]: toSatoshiConversion,
[CurrencyUnit.BWP]: toSatoshiConversion,
[CurrencyUnit.XOF]: toSatoshiConversion,
[CurrencyUnit.XAF]: toSatoshiConversion,
[CurrencyUnit.MWK]: toSatoshiConversion,
[CurrencyUnit.RWF]: toSatoshiConversion,
[CurrencyUnit.USDT]: toSatoshiConversion,
[CurrencyUnit.USDC]: toSatoshiConversion,
},
Expand All @@ -328,6 +360,10 @@ const CONVERSION_MAP = {
[CurrencyUnit.TZS]: standardUnitConversionObj,
[CurrencyUnit.UGX]: standardUnitConversionObj,
[CurrencyUnit.BWP]: standardUnitConversionObj,
[CurrencyUnit.XOF]: standardUnitConversionObj,
[CurrencyUnit.XAF]: standardUnitConversionObj,
[CurrencyUnit.MWK]: standardUnitConversionObj,
[CurrencyUnit.RWF]: standardUnitConversionObj,
[CurrencyUnit.USDT]: standardUnitConversionObj,
[CurrencyUnit.USDC]: standardUnitConversionObj,
};
Expand Down Expand Up @@ -412,6 +448,10 @@ export type CurrencyMap = {
[CurrencyUnit.TZS]: number;
[CurrencyUnit.UGX]: number;
[CurrencyUnit.BWP]: number;
[CurrencyUnit.XOF]: number;
[CurrencyUnit.XAF]: number;
[CurrencyUnit.MWK]: number;
[CurrencyUnit.RWF]: number;
[CurrencyUnit.USDT]: number;
[CurrencyUnit.USDC]: number;
[CurrencyUnit.FUTURE_VALUE]: number;
Expand Down Expand Up @@ -446,6 +486,10 @@ export type CurrencyMap = {
[CurrencyUnit.TZS]: string;
[CurrencyUnit.UGX]: string;
[CurrencyUnit.BWP]: string;
[CurrencyUnit.XOF]: string;
[CurrencyUnit.XAF]: string;
[CurrencyUnit.MWK]: string;
[CurrencyUnit.RWF]: string;
[CurrencyUnit.USDT]: string;
[CurrencyUnit.USDC]: string;
[CurrencyUnit.FUTURE_VALUE]: string;
Expand Down Expand Up @@ -661,6 +705,10 @@ function convertCurrencyAmountValues(
tzs: CurrencyUnit.TZS,
ugx: CurrencyUnit.UGX,
bwp: CurrencyUnit.BWP,
xof: CurrencyUnit.XOF,
xaf: CurrencyUnit.XAF,
mwk: CurrencyUnit.MWK,
rwf: CurrencyUnit.RWF,
mibtc: CurrencyUnit.MICROBITCOIN,
mlbtc: CurrencyUnit.MILLIBITCOIN,
nbtc: CurrencyUnit.NANOBITCOIN,
Expand Down Expand Up @@ -740,6 +788,10 @@ export function mapCurrencyAmount(
tzs,
ugx,
bwp,
xof,
xaf,
mwk,
rwf,
usdt,
usdc,
} = convertCurrencyAmountValues(unit, value, unitsPerBtc, conversionOverride);
Expand Down Expand Up @@ -769,6 +821,10 @@ export function mapCurrencyAmount(
[CurrencyUnit.TZS]: tzs,
[CurrencyUnit.UGX]: ugx,
[CurrencyUnit.BWP]: bwp,
[CurrencyUnit.XOF]: xof,
[CurrencyUnit.XAF]: xaf,
[CurrencyUnit.MWK]: mwk,
[CurrencyUnit.RWF]: rwf,
[CurrencyUnit.MICROBITCOIN]: mibtc,
[CurrencyUnit.MILLIBITCOIN]: mlbtc,
[CurrencyUnit.NANOBITCOIN]: nbtc,
Expand Down Expand Up @@ -884,6 +940,22 @@ export function mapCurrencyAmount(
value: bwp,
unit: CurrencyUnit.BWP,
}),
[CurrencyUnit.XOF]: formatCurrencyStr({
value: xof,
unit: CurrencyUnit.XOF,
}),
[CurrencyUnit.XAF]: formatCurrencyStr({
value: xaf,
unit: CurrencyUnit.XAF,
}),
[CurrencyUnit.MWK]: formatCurrencyStr({
value: mwk,
unit: CurrencyUnit.MWK,
}),
[CurrencyUnit.RWF]: formatCurrencyStr({
value: rwf,
unit: CurrencyUnit.RWF,
}),
[CurrencyUnit.USDT]: formatCurrencyStr({
value: usdt,
unit: CurrencyUnit.USDT,
Expand Down Expand Up @@ -1006,6 +1078,14 @@ export const abbrCurrencyUnit = (unit: CurrencyUnitType) => {
return "UGX";
case CurrencyUnit.BWP:
return "BWP";
case CurrencyUnit.XOF:
return "XOF";
case CurrencyUnit.XAF:
return "XAF";
case CurrencyUnit.MWK:
return "MWK";
case CurrencyUnit.RWF:
return "RWF";
}
return "Unsupported CurrencyUnit";
};
Expand Down Expand Up @@ -1175,6 +1255,13 @@ export function formatCurrencyStr(
const unitStr = isUmaCurrencyAmount(amount)
? amount.currency.code
: abbrCurrencyUnit(unit as CurrencyUnitType);

// Skip appending if the formatted string already contains the currency code
// This happens for currencies like XOF where Intl.NumberFormat uses the code as the symbol
if (formattedStr.includes(unitStr)) {
return formattedStr;
}

const unitSuffix = options?.appendUnits?.plural && num > 1 ? "s" : "";
const unitStrWithSuffix = `${unitStr}${unitSuffix}`;
formattedStr += ` ${
Expand Down
19 changes: 19 additions & 0 deletions packages/core/src/utils/tests/currency.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,3 +436,22 @@ describe("formatCurrencyStr", () => {
).toBe("$1,000.12 MXN");
});
});

it("should not append XOF if it's already in the formatted string", () => {
/* This test verifies the fix for PQA-394 where XOF was appearing twice.
* If Intl.NumberFormat formats with currencyDisplay: 'code', it will include
* XOF in the output, and we shouldn't append it again. */
const formatted = formatCurrencyStr(
{ value: 221900, unit: CurrencyUnit.XOF },
{ appendUnits: { plural: false, lowercase: false } },
);

/* Count occurrences of 'XOF' or 'CFA' in the result */
const xofCount = (formatted.match(/XOF/g) || []).length;
const cfaCount = (formatted.match(/CFA/g) || []).length;

/* Should have either XOF or CFA, but not both, and only once */
expect(xofCount + cfaCount).toBeGreaterThan(0);
expect(xofCount).toBeLessThanOrEqual(1);
expect(cfaCount).toBeLessThanOrEqual(1);
});
42 changes: 36 additions & 6 deletions packages/lightspark-sdk/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { CreateUmaInvitationWithPayment } from "./graphql/CreateUmaInvitationWit
import { CreateUmaInvoice } from "./graphql/CreateUmaInvoice.js";
import { DecodeInvoice } from "./graphql/DecodeInvoice.js";
import { DeleteApiToken } from "./graphql/DeleteApiToken.js";
import { FailHtlcs } from "./graphql/FailHtlcs.js";
import { FetchUmaInvitation } from "./graphql/FetchUmaInvitation.js";
import { FundNode } from "./graphql/FundNode.js";
import { IncomingPaymentsForInvoice } from "./graphql/IncomingPaymentsForInvoice.js";
Expand Down Expand Up @@ -76,6 +77,8 @@ import type ComplianceProvider from "./objects/ComplianceProvider.js";
import type CreateApiTokenOutput from "./objects/CreateApiTokenOutput.js";
import type CurrencyAmount from "./objects/CurrencyAmount.js";
import { CurrencyAmountFromJson } from "./objects/CurrencyAmount.js";
import type FailHtlcsOutput from "./objects/FailHtlcsOutput.js";
import { FailHtlcsOutputFromJson } from "./objects/FailHtlcsOutput.js";
import type FeeEstimate from "./objects/FeeEstimate.js";
import { FeeEstimateFromJson } from "./objects/FeeEstimate.js";
import type IncomingPayment from "./objects/IncomingPayment.js";
Expand Down Expand Up @@ -736,6 +739,28 @@ class LightsparkClient {
return InvoiceFromJson(invoiceJson);
}

/**
* Fails all pending HTLCs (Hash Time Locked Contracts) for a given invoice.
*
* @param invoiceId The ID of the invoice for which to fail HTLCs.
* @param cancelInvoice Whether to also cancel the invoice after failing the HTLCs.
* @returns The output containing the invoice ID, or undefined if the operation failed.
*/
public async failHtlcs(
invoiceId: string,
cancelInvoice: boolean,
): Promise<FailHtlcsOutput | undefined> {
const response = await this.requester.makeRawRequest(FailHtlcs, {
invoice_id: invoiceId,
cancel_invoice: cancelInvoice,
});
const output = response.fail_htlcs;
if (!output) {
return undefined;
}
return FailHtlcsOutputFromJson(output);
}

/**
* Decodes an encoded lightning invoice string.
*
Expand Down Expand Up @@ -1206,15 +1231,20 @@ class LightsparkClient {
amountSats: number,
bitcoinAddress: string,
mode: WithdrawalMode,
idempotencyKey: string | undefined = undefined,
): Promise<WithdrawalRequest> {
const variables: Record<string, string | number> = {
node_id: nodeId,
amount_sats: amountSats,
bitcoin_address: bitcoinAddress,
withdrawal_mode: mode,
};
if (idempotencyKey !== undefined) {
variables.idempotency_key = idempotencyKey;
}
const response = await this.requester.makeRawRequest(
RequestWithdrawal,
{
node_id: nodeId,
amount_sats: amountSats,
bitcoin_address: bitcoinAddress,
withdrawal_mode: mode,
},
variables,
nodeId,
);
return WithdrawalRequestFromJson(response.request_withdrawal.request);
Expand Down
19 changes: 19 additions & 0 deletions packages/lightspark-sdk/src/graphql/FailHtlcs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright ©, 2023-present, Lightspark Group, Inc. - All Rights Reserved

import { FRAGMENT as FailHtlcsOutputFragment } from "../objects/FailHtlcsOutput.js";

export const FailHtlcs = `
mutation FailHtlcs(
$invoice_id: ID!,
$cancel_invoice: Boolean!
) {
fail_htlcs(input: {
invoice_id: $invoice_id,
cancel_invoice: $cancel_invoice
}) {
...FailHtlcsOutputFragment
}
}

${FailHtlcsOutputFragment}
`;
2 changes: 2 additions & 0 deletions packages/lightspark-sdk/src/graphql/RequestWithdrawal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ export const RequestWithdrawal = `
$bitcoin_address: String!
$amount_sats: Long!
$withdrawal_mode: WithdrawalMode!
$idempotency_key: String
) {
request_withdrawal(input: {
node_id: $node_id
bitcoin_address: $bitcoin_address
amount_sats: $amount_sats
withdrawal_mode: $withdrawal_mode
idempotency_key: $idempotency_key
}) {
request {
...WithdrawalRequestFragment
Expand Down
Loading