Skip to content

Commit bc28b51

Browse files
committed
fix: enforce safe invoice params, guard bigint-to-number
1 parent 323ae93 commit bc28b51

4 files changed

Lines changed: 90 additions & 18 deletions

File tree

src/factories/payments-processors/alby-nwc-payments-processor-factory.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ const getAlbyNwcConfig = (settings: Settings): { nwcUrl: string; replyTimeoutMs:
3636
throw error
3737
}
3838

39+
const invoiceExpirySeconds = settings.paymentsProcessors?.alby?.invoiceExpirySeconds
40+
if (typeof invoiceExpirySeconds !== 'number' || !Number.isInteger(invoiceExpirySeconds) || invoiceExpirySeconds <= 0) {
41+
const error = new Error('Setting paymentsProcessors.alby.invoiceExpirySeconds must be a positive integer.')
42+
debug('Unable to create Alby NWC payments processor. %o', error)
43+
throw error
44+
}
45+
3946
return { nwcUrl, replyTimeoutMs }
4047
}
4148

src/payments-processors/alby-nwc-payments-processor.ts

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,23 @@ const timestampToDate = (unixSeconds?: number): Date | null => {
4141
return null
4242
}
4343

44+
const toSafeNumber = (value: bigint, fieldName: string): number => {
45+
if (value < 0n) {
46+
throw new Error(`${fieldName} must be a non-negative bigint.`)
47+
}
48+
49+
if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
50+
throw new Error(`${fieldName} exceeds Number.MAX_SAFE_INTEGER.`)
51+
}
52+
53+
const asNumber = Number(value)
54+
if (!Number.isSafeInteger(asNumber)) {
55+
throw new Error(`${fieldName} is not a safe integer.`)
56+
}
57+
58+
return asNumber
59+
}
60+
4461
export class AlbyNwcInvoice implements Invoice {
4562
id: string
4663
pubkey: string
@@ -117,24 +134,42 @@ export class AlbyNwcPaymentsProcessor implements IPaymentsProcessor {
117134
)) as NwcTransaction
118135
const status = mapNwcStateToInvoiceStatus(transaction.state)
119136

120-
const invoice = new AlbyNwcInvoice()
121-
invoice.id = transaction.payment_hash || invoiceId
122-
invoice.pubkey = typeof invoiceOrId === 'string' ? '' : invoiceOrId.pubkey
123-
invoice.bolt11 = transaction.invoice || (typeof invoiceOrId === 'string' ? '' : invoiceOrId.bolt11)
124-
invoice.amountRequested =
125-
typeof transaction.amount === 'number' && Number.isFinite(transaction.amount)
126-
? BigInt(Math.trunc(transaction.amount))
127-
: typeof invoiceOrId === 'string'
128-
? 0n
137+
const invoice: GetInvoiceResponse = {
138+
id: transaction.payment_hash || invoiceId,
139+
status,
140+
confirmedAt: status === InvoiceStatus.COMPLETED ? (timestampToDate(transaction.settled_at) ?? new Date()) : null,
141+
expiresAt: timestampToDate(transaction.expires_at),
142+
updatedAt: new Date(),
143+
}
144+
145+
if (typeof invoiceOrId !== 'string') {
146+
invoice.pubkey = invoiceOrId.pubkey
147+
invoice.bolt11 = transaction.invoice || invoiceOrId.bolt11
148+
invoice.amountRequested =
149+
typeof transaction.amount === 'number' && Number.isFinite(transaction.amount)
150+
? BigInt(Math.trunc(transaction.amount))
129151
: invoiceOrId.amountRequested
130-
invoice.amountPaid = status === InvoiceStatus.COMPLETED ? invoice.amountRequested : undefined
131-
invoice.unit = InvoiceUnit.MSATS
132-
invoice.status = status
133-
invoice.description = transaction.description || (typeof invoiceOrId === 'string' ? '' : invoiceOrId.description)
134-
invoice.confirmedAt = status === InvoiceStatus.COMPLETED ? (timestampToDate(transaction.settled_at) ?? new Date()) : null
135-
invoice.expiresAt = timestampToDate(transaction.expires_at)
136-
invoice.createdAt = timestampToDate(transaction.created_at) ?? new Date()
137-
invoice.updatedAt = new Date()
152+
invoice.amountPaid = status === InvoiceStatus.COMPLETED ? invoice.amountRequested : undefined
153+
invoice.unit = InvoiceUnit.MSATS
154+
invoice.description = transaction.description || invoiceOrId.description
155+
invoice.createdAt = timestampToDate(transaction.created_at) ?? invoiceOrId.createdAt
156+
} else {
157+
if (transaction.invoice) {
158+
invoice.bolt11 = transaction.invoice
159+
}
160+
if (typeof transaction.amount === 'number' && Number.isFinite(transaction.amount)) {
161+
invoice.amountRequested = BigInt(Math.trunc(transaction.amount))
162+
invoice.amountPaid = status === InvoiceStatus.COMPLETED ? invoice.amountRequested : undefined
163+
invoice.unit = InvoiceUnit.MSATS
164+
}
165+
if (transaction.description) {
166+
invoice.description = transaction.description
167+
}
168+
const createdAt = timestampToDate(transaction.created_at)
169+
if (createdAt) {
170+
invoice.createdAt = createdAt
171+
}
172+
}
138173

139174
return invoice
140175
})
@@ -155,9 +190,10 @@ export class AlbyNwcPaymentsProcessor implements IPaymentsProcessor {
155190
try {
156191
return await this.withClient(async (client) => {
157192
const expirySeconds = this.settings().paymentsProcessors?.alby?.invoiceExpirySeconds
193+
const amount = toSafeNumber(amountMsats, 'CreateInvoiceRequest.amount')
158194
const transaction = (await this.withReplyTimeout(
159195
client.makeInvoice({
160-
amount: Number(amountMsats),
196+
amount,
161197
description,
162198
expiry: expirySeconds,
163199
}),

test/unit/factories/payments-processors/alby-nwc-payments-processor-factory.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ describe('createAlbyNwcPaymentsProcessor', () => {
1515
paymentsProcessors: {
1616
alby: {
1717
replyTimeoutMs: 10_000,
18+
invoiceExpirySeconds: 900,
1819
},
1920
},
2021
} as any
@@ -52,12 +53,28 @@ describe('createAlbyNwcPaymentsProcessor', () => {
5253
paymentsProcessors: {
5354
alby: {
5455
replyTimeoutMs: 0,
56+
invoiceExpirySeconds: 900,
5557
},
5658
},
5759
} as any)
5860
).to.throw('Setting paymentsProcessors.alby.replyTimeoutMs must be a positive number.')
5961
})
6062

63+
it('throws when settings.paymentsProcessors.alby.invoiceExpirySeconds is invalid', () => {
64+
process.env.ALBY_NWC_URL = 'nostr+walletconnect://wallet?relay=wss://relay&secret=abc'
65+
66+
expect(() =>
67+
createAlbyNwcPaymentsProcessor({
68+
paymentsProcessors: {
69+
alby: {
70+
replyTimeoutMs: 10_000,
71+
invoiceExpirySeconds: 0,
72+
},
73+
},
74+
} as any)
75+
).to.throw('Setting paymentsProcessors.alby.invoiceExpirySeconds must be a positive integer.')
76+
})
77+
6178
it('creates the processor when config is valid', () => {
6279
process.env.ALBY_NWC_URL = 'nostr+walletconnect://wallet?relay=wss://relay&secret=abc'
6380

test/unit/payments-processors/alby-nwc-payments-processor.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,4 +206,16 @@ describe('AlbyNwcPaymentsProcessor', () => {
206206

207207
expect(clearTimeoutSpy.called).to.equal(true)
208208
})
209+
210+
it('throws when createInvoice amount exceeds Number.MAX_SAFE_INTEGER', async () => {
211+
const processor = new AlbyNwcPaymentsProcessor('nostr+walletconnect://wallet?relay=wss://relay&secret=abc', 10_000, settings)
212+
213+
await expect(
214+
processor.createInvoice({
215+
amount: BigInt(Number.MAX_SAFE_INTEGER) + 1n,
216+
description: 'Unsafe amount',
217+
requestId: 'pubkey-unsafe',
218+
})
219+
).to.be.rejectedWith('CreateInvoiceRequest.amount exceeds Number.MAX_SAFE_INTEGER.')
220+
})
209221
})

0 commit comments

Comments
 (0)