Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 34 additions & 3 deletions __mocks__/@breeztech/breez-sdk-spark-react-native.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
/* eslint-disable camelcase */
// The *_Tags identifiers mirror the SDK's generated enum names verbatim.
// Full member set verified against the 0.22.3 generated d.ts
// (breez_sdk_spark.d.ts, `export declare enum SdkEvent_Tags`). Keep this
// complete: a missing member reads as `undefined`, every tag comparison goes
// silently false, and a test passes green while asserting the wrong branch.
const SdkEvent_Tags = {
PaymentPending: "PaymentPending",
Synced: "Synced",
UnclaimedDeposits: "UnclaimedDeposits",
ClaimedDeposits: "ClaimedDeposits",
PaymentSucceeded: "PaymentSucceeded",
PaymentPending: "PaymentPending",
PaymentFailed: "PaymentFailed",
Synced: "Synced",
Optimization: "Optimization",
// 0.17.0 renamed the variant (OptimizationEvent -> AutoOptimizationEvent);
// verified against the 0.22.3 generated d.ts.
AutoOptimization: "AutoOptimization",
LightningAddressChanged: "LightningAddressChanged",
NewDeposits: "NewDeposits",
}

// Every enum below must mirror the real SDK's generated numbering
Expand All @@ -18,6 +30,23 @@ const SdkEvent_Tags = {
// values under a "Complete" key); PaymentType did too. Copy the generated
// values when adding a new enum here — do not invent readable strings.
//
// 0.22.x: prepareSendPayment takes a tagged-union PaymentRequest; app code
// constructs `new PaymentRequest.Input({ input })`. Mirror of the generated
// class shape — tests only ever read `.tag` and `.inner.input`.
const PaymentRequest_Tags = {
Input: "Input",
CrossChain: "CrossChain",
}
const PaymentRequest = {
Input: class {
constructor(inner) {
this.tag = PaymentRequest_Tags.Input
this.inner = Object.freeze({ ...inner })
}
},
instanceOf: (obj) => Boolean(obj && obj.tag && obj.inner),
}

// generated: PaymentType — Send = 0, Receive = 1
const PaymentType = {
Send: 0,
Expand All @@ -44,6 +73,8 @@ class BitcoinAddress {
}

module.exports = {
PaymentRequest,
PaymentRequest_Tags,
__esModule: true,
BitcoinAddress,
Bolt11Invoice,
Expand Down
61 changes: 61 additions & 0 deletions __tests__/mocks/breez-sdk-spark-mock-parity.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import * as fs from "fs"
import * as path from "path"

// The manual mock at __mocks__/@breeztech/breez-sdk-spark-react-native.js
// claims its *_Tags objects mirror the SDK's generated enums verbatim. That
// claim has silently drifted before: a member missing from the mock reads as
// `undefined`, every tag comparison against it goes false, and a test passes
// green while asserting the wrong branch. This spec pins the claim to the
// installed package's generated d.ts so drift fails loudly on SDK bumps.
// eslint-disable-next-line @typescript-eslint/no-var-requires
const mock = require("../../__mocks__/@breeztech/breez-sdk-spark-react-native.js")

const generatedDts = fs.readFileSync(
path.join(
__dirname,
"../../node_modules/@breeztech/breez-sdk-spark-react-native/lib/typescript/module/src/generated/breez_sdk_spark.d.ts",
),
"utf8",
)

const realEnumMembers = (enumName: string): Record<string, string | number> => {
const enumBlock = generatedDts.match(
new RegExp(`export declare enum ${enumName} \\{([\\s\\S]*?)\\}`),
)
if (!enumBlock) {
throw new Error(`enum ${enumName} not found in generated d.ts`)
}
const members: Record<string, string | number> = {}
// Matches both string enums (`Synced = "Synced"`) and numeric enums
// (`Completed = 0`) — the numeric ones (PaymentStatus, PaymentType) are the
// ones that drifted destructively before (string values under a "Complete"
// key), so both shapes must be pinned.
for (const member of enumBlock[1].matchAll(/(\w+) = ("[^"]+"|\d+)/g)) {
const rawValue = member[2]
members[member[1]] = rawValue.startsWith('"')
? rawValue.slice(1, -1)
: Number(rawValue)
}
if (Object.keys(members).length === 0) {
throw new Error(`enum ${enumName} matched no members in generated d.ts`)
}
return members
}

describe("breez-sdk-spark manual mock parity with the generated SDK enums", () => {
it("SdkEvent_Tags mirrors the generated enum exactly — members and values", () => {
expect(mock.SdkEvent_Tags).toEqual(realEnumMembers("SdkEvent_Tags"))
})

it("PaymentRequest_Tags mirrors the generated enum exactly — members and values", () => {
expect(mock.PaymentRequest_Tags).toEqual(realEnumMembers("PaymentRequest_Tags"))
})

it("PaymentStatus mirrors the generated numeric enum exactly — members and values", () => {
expect(mock.PaymentStatus).toEqual(realEnumMembers("PaymentStatus"))
})

it("PaymentType mirrors the generated numeric enum exactly — members and values", () => {
expect(mock.PaymentType).toEqual(realEnumMembers("PaymentType"))
})
})
14 changes: 11 additions & 3 deletions __tests__/utils/breez-fee-extraction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,18 @@ describe("extractFeeFromPaymentMethod", () => {
).toEqual(BigInt(4))
})

it("returns 0 for unknown methods", () => {
expect(extractFeeFromPaymentMethod({ tag: "SomethingNew" }, "fast")).toEqual(
BigInt(0),
it("throws on unrecognized payment method tags — never a silent 0-sat fee", () => {
// 0.22.x added CrossChainAddress to the SendPaymentMethod union; any tag
// this module doesn't understand must not render as a free send.
expect(() =>
extractFeeFromPaymentMethod({ tag: "CrossChainAddress", inner: {} }, "fast"),
).toThrow(/unrecognized payment method.*CrossChainAddress/i)
expect(() => extractFeeFromPaymentMethod({ tag: "SomethingNew" }, "fast")).toThrow(
/unrecognized payment method.*SomethingNew/i,
)
})

it("returns 0 when no payment method is passed at all", () => {
expect(extractFeeFromPaymentMethod(undefined, "fast")).toEqual(BigInt(0))
})
})
3 changes: 2 additions & 1 deletion app/components/home-screen/Transactions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ const Transactions: React.FC<Props> = ({

const addBreezEventListener = async () => {
const listenerId = await addEventListener((e: SdkEvent) => {
if (e.tag !== SdkEvent_Tags.Synced && e.tag !== SdkEvent_Tags.Optimization) {
// eslint-disable-next-line camelcase -- mirrors the SDK's generated enum name
if (e.tag !== SdkEvent_Tags.Synced && e.tag !== SdkEvent_Tags.AutoOptimization) {
fetchPaymentsBreez()
}
})
Expand Down
44 changes: 31 additions & 13 deletions app/utils/breez-sdk/fee-extraction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,35 @@ type SpeedFee = {
}

// Structural view of the SDK's SendPaymentMethod union — only the fields fee
// extraction reads. Optional everywhere so any variant assigns to it.
// extraction reads. `inner` is opaque at the type level because 0.22.x added
// variants (CrossChainAddress) whose inner shares NO properties with the fee
// fields, which breaks structural assignability of the union as a whole; each
// tag branch below narrows to exactly the fields that variant carries.
export type SendPaymentMethodLike = {
tag: string
inner?: {
lightningFeeSats?: bigint
fee?: bigint
feeQuote?: {
speedFast: SpeedFee
speedMedium: SpeedFee
speedSlow: SpeedFee
}
inner?: object
}

type FeeFields = {
lightningFeeSats?: bigint
fee?: bigint
feeQuote?: {
speedFast: SpeedFee
speedMedium: SpeedFee
speedSlow: SpeedFee
}
}

export const extractFeeFromPaymentMethod = (
paymentMethod: SendPaymentMethodLike | undefined,
selectedFeeType?: OnchainFeeSpeed,
): bigint => {
const inner = paymentMethod?.inner as FeeFields | undefined
if (paymentMethod?.tag === "Bolt11Invoice") {
return paymentMethod.inner?.lightningFeeSats ?? BigInt(0)
return inner?.lightningFeeSats ?? BigInt(0)
}
if (paymentMethod?.tag === "BitcoinAddress") {
const feeQuote = paymentMethod.inner?.feeQuote
const feeQuote = inner?.feeQuote
if (!feeQuote) {
// No quote means no knowable fee. Throwing (classified upstream as an
// "sdk" fee error) beats the old fall-through to 0, which displayed a
Expand All @@ -54,7 +60,19 @@ export const extractFeeFromPaymentMethod = (
return feeQuote.speedMedium.userFeeSat + feeQuote.speedMedium.l1BroadcastFeeSat
}
if (paymentMethod?.tag === "SparkAddress" || paymentMethod?.tag === "SparkInvoice") {
return paymentMethod.inner?.fee ?? BigInt(0)
return inner?.fee ?? BigInt(0)
}
if (paymentMethod === undefined) {
// Defensive only: the SDK types paymentMethod as required on the prepare
// response, so callers never actually hit this. Kept as 0 for direct
// callers passing nothing.
return BigInt(0)
}
return BigInt(0)
// Any other tag is a variant this module doesn't understand — 0.22.x added
// SendPaymentMethod.CrossChainAddress to the union, and future SDK versions
// may add more. Rendering an unknown variant as a 0-sat fee would display a
// free send and skip the amount+fee balance check (the same failure mode the
// BitcoinAddress no-quote branch above throws on). Throwing is classified
// upstream as an "sdk" fee error via classifyBreezSdkError.
throw new Error(`Unrecognized payment method in prepare response: ${paymentMethod.tag}`)
}
17 changes: 13 additions & 4 deletions app/utils/breez-sdk/spark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import RNFS from "react-native-fs"
import { PaymentType } from "@galoymoney/client"
import * as Keychain from "react-native-keychain"
import {
PaymentRequest,
defaultConfig,
Network,
ReceivePaymentMethod,
Expand Down Expand Up @@ -245,7 +246,9 @@ export const fetchBreezFee = async ({

if (paymentType === "lightning") {
const prepareResponse = await sdk.prepareSendPayment({
paymentRequest,
// 0.22.x: paymentRequest is a tagged union; Input wraps the raw string
// (bolt11 / address / BIP-21) exactly as the SDK parsed it before.
paymentRequest: new PaymentRequest.Input({ input: paymentRequest }),
amount: BigInt(amountSats),
tokenIdentifier: undefined,
conversionOptions: undefined,
Expand All @@ -257,7 +260,9 @@ export const fetchBreezFee = async ({

if (paymentType === "onchain") {
const prepareResponse = await sdk.prepareSendPayment({
paymentRequest,
// 0.22.x: paymentRequest is a tagged union; Input wraps the raw string
// (bolt11 / address / BIP-21) exactly as the SDK parsed it before.
paymentRequest: new PaymentRequest.Input({ input: paymentRequest }),
amount: BigInt(amountSats),
tokenIdentifier: undefined,
conversionOptions: undefined,
Expand Down Expand Up @@ -389,7 +394,9 @@ export const payLightningBreez = async (
const sdk = getSDKInstance()

const prepareResponse = await sdk.prepareSendPayment({
paymentRequest,
// 0.22.x: paymentRequest is a tagged union; Input wraps the raw string
// (bolt11 / address / BIP-21) exactly as the SDK parsed it before.
paymentRequest: new PaymentRequest.Input({ input: paymentRequest }),
amount: amountSats !== undefined ? BigInt(amountSats) : undefined,
tokenIdentifier: undefined,
conversionOptions: undefined,
Expand Down Expand Up @@ -424,7 +431,9 @@ export const payOnchainBreez = async (
const sdk = getSDKInstance()

const prepareResponse = await sdk.prepareSendPayment({
paymentRequest,
// 0.22.x: paymentRequest is a tagged union; Input wraps the raw string
// (bolt11 / address / BIP-21) exactly as the SDK parsed it before.
paymentRequest: new PaymentRequest.Input({ input: paymentRequest }),
amount: BigInt(amountSats),
tokenIdentifier: undefined,
conversionOptions: undefined,
Expand Down
4 changes: 2 additions & 2 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ PODS:
- breez_sdk_liquidFFI (0.11.13)
- BreezSDKLiquid (0.11.13):
- breez_sdk_liquidFFI (= 0.11.13)
- BreezSdkSparkReactNative (0.13.6):
- BreezSdkSparkReactNative (0.22.3):
- DoubleConversion
- glog
- hermes-engine
Expand Down Expand Up @@ -3088,7 +3088,7 @@ SPEC CHECKSUMS:
breez_sdk_liquid: e05cd39b19b9e029ecf50195e4e7df0aefb745cd
breez_sdk_liquidFFI: f05fadc0611126ade76d1fe6761ed8b020aabefb
BreezSDKLiquid: ee6bf5a57f1b2533dc3c14c24c9773496f17b756
BreezSdkSparkReactNative: 7f843c40572498538808eb6f6b399fe5e0bc7839
BreezSdkSparkReactNative: fe0a5d91aeca17fed9a9972847b60ca3cd9d82c7
BVLinearGradient: cb006ba232a1f3e4f341bb62c42d1098c284da70
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"dependencies": {
"@apollo/client": "3.9.0-alpha.5",
"@bitcoinerlab/secp256k1": "^1.0.5",
"@breeztech/breez-sdk-spark-react-native": "^0.13.4",
"@breeztech/breez-sdk-spark-react-native": "0.22.3",
"@breeztech/react-native-breez-sdk-liquid": "^0.11.13",
"@flash/client": "git+https://github.com/lnflash/flash-client.git",
"@formatjs/intl-getcanonicallocales": "^2.3.0",
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1347,10 +1347,10 @@
dependencies:
"@noble/curves" "^1.7.0"

"@breeztech/breez-sdk-spark-react-native@^0.13.4":
version "0.13.6"
resolved "https://registry.yarnpkg.com/@breeztech/breez-sdk-spark-react-native/-/breez-sdk-spark-react-native-0.13.6.tgz#5f4d9ab71826a277c406250a2aac3ad1304b4dd4"
integrity sha512-HFGN+AFuLnt02JdW+7/6LfhWZPeUB2lnEQHqm0xJCSGh/kqsUxFX+7O8OdQtFOGx9bbTAX8WK1JsUQLoxnEgww==
"@breeztech/breez-sdk-spark-react-native@0.22.3":
version "0.22.3"
resolved "https://registry.yarnpkg.com/@breeztech/breez-sdk-spark-react-native/-/breez-sdk-spark-react-native-0.22.3.tgz#37a8efe6296501355361029ce856d1bb52fdae35"
integrity sha512-QqOfzs5BeI+JbP/vp6+B51LnSwT465unXwrlT+xB+DzU6/gkZTK+KWOMKKJ58TjTDPB92GDVSOm3ts+BKZfJhA==
dependencies:
uniffi-bindgen-react-native "^0.29.3-1"

Expand Down
Loading