Skip to content

Commit 8f9736d

Browse files
committed
feat(express): migrated lightningPayment to type route
Ticket: WP-5438
1 parent ee849a3 commit 8f9736d

File tree

6 files changed

+1319
-10
lines changed

6 files changed

+1319
-10
lines changed

modules/express/src/clientRoutes.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1694,12 +1694,10 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
16941694
);
16951695

16961696
// lightning - pay invoice
1697-
app.post(
1698-
'/api/v2/:coin/wallet/:id/lightning/payment',
1699-
parseBody,
1697+
router.post('express.v2.wallet.lightningPayment', [
17001698
prepareBitGo(config),
1701-
promiseWrapper(handlePayLightningInvoice)
1702-
);
1699+
typedPromiseWrapper(handlePayLightningInvoice),
1700+
]);
17031701

17041702
// lightning - onchain withdrawal
17051703
app.post(

modules/express/src/lightning/lightningInvoiceRoutes.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as express from 'express';
22
import { ApiResponseError } from '../errors';
33
import { CreateInvoiceBody, getLightningWallet, Invoice, SubmitPaymentParams } from '@bitgo/abstract-lightning';
44
import { decodeOrElse } from '@bitgo/sdk-core';
5+
import { ExpressApiRouteRequest } from '../typedRoutes/api';
56

67
export async function handleCreateLightningInvoice(req: express.Request): Promise<any> {
78
const bitgo = req.bitgo;
@@ -17,14 +18,16 @@ export async function handleCreateLightningInvoice(req: express.Request): Promis
1718
return Invoice.encode(await lightningWallet.createInvoice(params));
1819
}
1920

20-
export async function handlePayLightningInvoice(req: express.Request): Promise<any> {
21+
export async function handlePayLightningInvoice(
22+
req: ExpressApiRouteRequest<'express.v2.wallet.lightningPayment', 'post'>
23+
): Promise<any> {
2124
const bitgo = req.bitgo;
2225
const params = decodeOrElse(SubmitPaymentParams.name, SubmitPaymentParams, req.body, (error) => {
2326
throw new ApiResponseError(`Invalid request body to pay lightning invoice`, 400);
2427
});
2528

26-
const coin = bitgo.coin(req.params.coin);
27-
const wallet = await coin.wallets().get({ id: req.params.id });
29+
const coin = bitgo.coin(req.decoded.coin);
30+
const wallet = await coin.wallets().get({ id: req.decoded.id });
2831
const lightningWallet = getLightningWallet(wallet);
2932

3033
return await lightningWallet.payInvoice(params);

modules/express/src/typedRoutes/api/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { PostCoinSign } from './v2/coinSign';
4141
import { PostSendCoins } from './v2/sendCoins';
4242
import { PostGenerateShareTSS } from './v2/generateShareTSS';
4343
import { PostOfcExtSignPayload } from './v2/ofcExtSignPayload';
44+
import { PostLightningWalletPayment } from './v2/lightningPayment';
4445

4546
// Too large types can cause the following error
4647
//
@@ -189,6 +190,12 @@ export const ExpressKeychainChangePasswordApiSpec = apiSpec({
189190
},
190191
});
191192

193+
export const ExpressLightningWalletPaymentApiSpec = apiSpec({
194+
'express.v2.wallet.lightningPayment': {
195+
post: PostLightningWalletPayment,
196+
},
197+
});
198+
192199
export const ExpressLightningGetStateApiSpec = apiSpec({
193200
'express.lightning.getState': {
194201
get: GetLightningState,
@@ -278,6 +285,7 @@ export type ExpressApi = typeof ExpressPingApiSpec &
278285
typeof ExpressV2WalletCreateAddressApiSpec &
279286
typeof ExpressKeychainLocalApiSpec &
280287
typeof ExpressKeychainChangePasswordApiSpec &
288+
typeof ExpressLightningWalletPaymentApiSpec &
281289
typeof ExpressLightningGetStateApiSpec &
282290
typeof ExpressLightningInitWalletApiSpec &
283291
typeof ExpressLightningUnlockWalletApiSpec &
@@ -311,6 +319,7 @@ export const ExpressApi: ExpressApi = {
311319
...ExpressV2WalletCreateAddressApiSpec,
312320
...ExpressKeychainLocalApiSpec,
313321
...ExpressKeychainChangePasswordApiSpec,
322+
...ExpressLightningWalletPaymentApiSpec,
314323
...ExpressLightningGetStateApiSpec,
315324
...ExpressLightningInitWalletApiSpec,
316325
...ExpressLightningUnlockWalletApiSpec,
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
import * as t from 'io-ts';
2+
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
3+
import { BigIntFromString } from 'io-ts-types/BigIntFromString';
4+
import { BitgoExpressError } from '../../schemas/error';
5+
6+
/**
7+
* Path parameters for lightning payment API
8+
*/
9+
export const LightningPaymentParams = {
10+
/** The coin identifier (e.g., 'tlnbtc', 'lnbtc') */
11+
coin: t.string,
12+
/** The wallet ID */
13+
id: t.string,
14+
} as const;
15+
16+
/**
17+
* Request body for paying a lightning invoice
18+
*/
19+
export const LightningPaymentRequestBody = {
20+
/** The BOLT #11 encoded lightning invoice to pay */
21+
invoice: t.string,
22+
/** The wallet passphrase to decrypt signing keys */
23+
passphrase: t.string,
24+
/** Amount to pay in millisatoshis (required for zero-amount invoices) */
25+
amountMsat: optional(BigIntFromString),
26+
/** Maximum fee limit in millisatoshis */
27+
feeLimitMsat: optional(BigIntFromString),
28+
/** Fee limit as a ratio of payment amount (e.g., 0.01 for 1%) */
29+
feeLimitRatio: optional(t.number),
30+
/** Custom sequence ID for tracking this payment */
31+
sequenceId: optional(t.string),
32+
/** Comment or memo for this payment (not sent to recipient) */
33+
comment: optional(t.string),
34+
} as const;
35+
36+
/**
37+
* Payment status on the Lightning Network
38+
*/
39+
const PaymentStatus = t.union([
40+
/** Payment is in progress on the Lightning Network */
41+
t.literal('in_flight'),
42+
/** Payment completed successfully (preimage received) */
43+
t.literal('settled'),
44+
/** Payment failed (see failureReason for details) */
45+
t.literal('failed'),
46+
]);
47+
48+
/**
49+
* Payment failure reasons
50+
*/
51+
const PaymentFailureReason = t.union([
52+
/** Payment timed out */
53+
t.literal('TIMEOUT'),
54+
/** No route found to destination */
55+
t.literal('NO_ROUTE'),
56+
/** Non-recoverable error occurred */
57+
t.literal('ERROR'),
58+
/** Invoice has incorrect payment details */
59+
t.literal('INCORRECT_PAYMENT_DETAILS'),
60+
/** Insufficient channel outbound capacity */
61+
t.literal('INSUFFICIENT_BALANCE'),
62+
/** Insufficient custodial lightning balance */
63+
t.literal('INSUFFICIENT_WALLET_BALANCE'),
64+
/** Excess custodial lightning balance */
65+
t.literal('EXCESS_WALLET_BALANCE'),
66+
/** Invoice has expired */
67+
t.literal('INVOICE_EXPIRED'),
68+
/** Payment was already settled */
69+
t.literal('PAYMENT_ALREADY_SETTLED'),
70+
/** Payment is already in flight */
71+
t.literal('PAYMENT_ALREADY_IN_FLIGHT'),
72+
/** Temporary error, retry later */
73+
t.literal('TRANSIENT_ERROR_RETRY_LATER'),
74+
/** Payment was canceled */
75+
t.literal('CANCELED'),
76+
/** Payment was force failed */
77+
t.literal('FORCE_FAILED'),
78+
]);
79+
80+
/**
81+
* Lightning Network payment status details
82+
*/
83+
const LndCreatePaymentResponse = t.intersection([
84+
t.type({
85+
/** Current payment status */
86+
status: PaymentStatus,
87+
/** Payment hash identifying this payment */
88+
paymentHash: t.string,
89+
}),
90+
t.partial({
91+
/** Internal BitGo payment ID */
92+
paymentId: t.string,
93+
/** Payment preimage (present when settled) */
94+
paymentPreimage: t.string,
95+
/** Actual amount paid in millisatoshis */
96+
amountMsat: t.string,
97+
/** Actual fee paid in millisatoshis */
98+
feeMsat: t.string,
99+
/** Failure reason (present when failed) */
100+
failureReason: PaymentFailureReason,
101+
}),
102+
]);
103+
104+
/**
105+
* Transaction request state
106+
*/
107+
const TxRequestState = t.union([
108+
/** Waiting for commitment shares */
109+
t.literal('pendingCommitment'),
110+
/** Waiting for additional approvals */
111+
t.literal('pendingApproval'),
112+
/** Transaction request was canceled */
113+
t.literal('canceled'),
114+
/** Transaction request was rejected */
115+
t.literal('rejected'),
116+
/** Transaction request initialized */
117+
t.literal('initialized'),
118+
/** Waiting for delivery to network */
119+
t.literal('pendingDelivery'),
120+
/** Successfully delivered to network */
121+
t.literal('delivered'),
122+
/** Waiting for user signature */
123+
t.literal('pendingUserSignature'),
124+
/** Transaction has been signed */
125+
t.literal('signed'),
126+
]);
127+
128+
/**
129+
* Pending approval state
130+
*/
131+
const PendingApprovalState = t.union([
132+
/** Waiting for approval */
133+
t.literal('pending'),
134+
/** Waiting for signature */
135+
t.literal('awaitingSignature'),
136+
/** Waiting for BitGo admin approval */
137+
t.literal('pendingBitGoAdminApproval'),
138+
/** Waiting for ID verification */
139+
t.literal('pendingIdVerification'),
140+
/** Waiting for custodian approval */
141+
t.literal('pendingCustodianApproval'),
142+
/** Waiting for final approval */
143+
t.literal('pendingFinalApproval'),
144+
/** Approval granted */
145+
t.literal('approved'),
146+
/** Processing the approval */
147+
t.literal('processing'),
148+
/** Approval rejected */
149+
t.literal('rejected'),
150+
]);
151+
152+
/**
153+
* Pending approval type
154+
*/
155+
const PendingApprovalType = t.union([
156+
/** Request to change user permissions */
157+
t.literal('userChangeRequest'),
158+
/** Request to approve a transaction */
159+
t.literal('transactionRequest'),
160+
/** Request to change policy rules */
161+
t.literal('policyRuleRequest'),
162+
/** Request to update required approvals count */
163+
t.literal('updateApprovalsRequiredRequest'),
164+
/** Full transaction request with complete details */
165+
t.literal('transactionRequestFull'),
166+
]);
167+
168+
/**
169+
* Transaction request details within pending approval info
170+
*/
171+
const TransactionRequestDetails = t.intersection([
172+
t.type({
173+
/** Coin-specific transaction details */
174+
coinSpecific: t.record(t.string, t.unknown),
175+
/** Recipients of the transaction */
176+
recipients: t.unknown,
177+
/** Build parameters for the transaction */
178+
buildParams: t.intersection([
179+
t.partial({
180+
/** Type of transaction */
181+
type: t.union([
182+
/** Split UTXOs into multiple outputs */
183+
t.literal('fanout'),
184+
/** Combine UTXOs into fewer outputs */
185+
t.literal('consolidate'),
186+
]),
187+
}),
188+
t.record(t.string, t.unknown),
189+
]),
190+
}),
191+
t.partial({
192+
/** Source wallet for the transaction */
193+
sourceWallet: t.string,
194+
}),
195+
]);
196+
197+
/**
198+
* Pending approval information
199+
*/
200+
const PendingApprovalInfo = t.intersection([
201+
t.type({
202+
/** Type of pending approval */
203+
type: PendingApprovalType,
204+
}),
205+
t.partial({
206+
/** Transaction request details (for transaction-related approvals) */
207+
transactionRequest: TransactionRequestDetails,
208+
}),
209+
]);
210+
211+
/**
212+
* Pending approval details
213+
*/
214+
const PendingApproval = t.intersection([
215+
t.type({
216+
/** Pending approval ID */
217+
id: t.string,
218+
/** Approval state */
219+
state: PendingApprovalState,
220+
/** User ID of the approval creator */
221+
creator: t.string,
222+
/** Pending approval information */
223+
info: PendingApprovalInfo,
224+
}),
225+
t.partial({
226+
/** Wallet ID (for wallet-level approvals) */
227+
wallet: t.string,
228+
/** Enterprise ID (for enterprise-level approvals) */
229+
enterprise: t.string,
230+
/** Number of approvals required */
231+
approvalsRequired: t.number,
232+
/** Associated transaction request ID */
233+
txRequestId: t.string,
234+
}),
235+
]);
236+
237+
/**
238+
* Response for paying a lightning invoice
239+
*/
240+
export const LightningPaymentResponse = t.intersection([
241+
t.type({
242+
/** Payment request ID for tracking */
243+
txRequestId: t.string,
244+
/** Status of the payment request ('delivered', 'pendingApproval', etc.) */
245+
txRequestState: TxRequestState,
246+
}),
247+
t.partial({
248+
/** Pending approval details (present when approval is required) */
249+
pendingApproval: PendingApproval,
250+
/** Payment status on the Lightning Network (absent when pending approval) */
251+
paymentStatus: LndCreatePaymentResponse,
252+
}),
253+
]);
254+
255+
/**
256+
* Response status codes
257+
*/
258+
export const LightningPaymentResponseObj = {
259+
/** Successfully submitted payment */
260+
200: LightningPaymentResponse,
261+
/** Invalid request */
262+
400: BitgoExpressError,
263+
} as const;
264+
265+
/**
266+
* Pay a Lightning Invoice
267+
*
268+
* Submits a payment for a BOLT #11 lightning invoice. The payment is signed with the user's
269+
* authentication key and submitted to BitGo. If the payment requires additional approvals
270+
* (based on wallet policy), returns pending approval details. Otherwise, the payment is
271+
* immediately submitted to the Lightning Network.
272+
*
273+
* Fee limits can be controlled using either `feeLimitMsat` (absolute limit) or `feeLimitRatio`
274+
* (as a ratio of payment amount). If both are provided, the more restrictive limit applies.
275+
*
276+
* For zero-amount invoices (invoices without a specified amount), the `amountMsat` field is required.
277+
*
278+
* @operationId express.v2.wallet.lightningPayment
279+
* @tag express
280+
*/
281+
export const PostLightningWalletPayment = httpRoute({
282+
path: '/api/v2/{coin}/wallet/{id}/lightning/payment',
283+
method: 'POST',
284+
request: httpRequest({
285+
params: LightningPaymentParams,
286+
body: LightningPaymentRequestBody,
287+
}),
288+
response: LightningPaymentResponseObj,
289+
});

0 commit comments

Comments
 (0)