Skip to content

Commit a5b1d69

Browse files
authored
Merge pull request #5598 from BitGo/BTC--1776
feat: add support to create lightning payments
2 parents 1a9ffdf + ffa5e71 commit a5b1d69

File tree

6 files changed

+275
-32
lines changed

6 files changed

+275
-32
lines changed

modules/abstract-lightning/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
]
4040
},
4141
"dependencies": {
42+
"@bitgo/public-types": "4.17.0",
4243
"@bitgo/sdk-core": "^29.0.0",
4344
"@bitgo/statics": "^51.0.1",
4445
"@bitgo/utxo-lib": "^11.2.2",

modules/abstract-lightning/src/codecs/api/payment.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as t from 'io-ts';
22
import { BigIntFromString } from 'io-ts-types/BigIntFromString';
33
import { DateFromISOString } from 'io-ts-types/DateFromISOString';
4+
import { LightningPaymentRequest, optionalString } from '@bitgo/public-types';
45

56
// codecs for lightning wallet payment related apis
67

@@ -84,3 +85,31 @@ export const PaymentQuery = t.partial(
8485
'PaymentQuery'
8586
);
8687
export type PaymentQuery = t.TypeOf<typeof PaymentQuery>;
88+
89+
export const SubmitPaymentParams = t.intersection([
90+
LightningPaymentRequest,
91+
t.type({
92+
sequenceId: optionalString,
93+
comment: optionalString,
94+
}),
95+
]);
96+
97+
export type SubmitPaymentParams = t.TypeOf<typeof SubmitPaymentParams>;
98+
99+
export const LndCreatePaymentResponse = t.intersection(
100+
[
101+
t.type({
102+
status: PaymentStatus,
103+
paymentHash: t.string,
104+
}),
105+
t.partial({
106+
paymentPreimage: t.string,
107+
amountMsat: t.string,
108+
feeMsat: t.string,
109+
failureReason: PaymentFailureReason,
110+
}),
111+
],
112+
'LndCreatePaymentResponse'
113+
);
114+
115+
export type LndCreatePaymentResponse = t.TypeOf<typeof LndCreatePaymentResponse>;

modules/abstract-lightning/src/wallet/lightning.ts

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
import * as sdkcore from '@bitgo/sdk-core';
2+
import {
3+
PendingApprovalData,
4+
PendingApprovals,
5+
RequestTracer,
6+
RequestType,
7+
TxRequest,
8+
commonTssMethods,
9+
TxRequestState,
10+
} from '@bitgo/sdk-core';
211
import * as t from 'io-ts';
312
import { createMessageSignature, unwrapLightningCoinSpecific } from '../lightning';
413
import {
@@ -8,8 +17,19 @@ import {
817
InvoiceQuery,
918
LightningAuthKeychain,
1019
LightningKeychain,
20+
LndCreatePaymentResponse,
21+
SubmitPaymentParams,
1122
UpdateLightningWalletSignedRequest,
1223
} from '../codecs';
24+
import { LightningPaymentIntent, LightningPaymentRequest } from '@bitgo/public-types';
25+
26+
export type PayInvoiceResponse = {
27+
txRequestId: string;
28+
txRequestState: TxRequestState;
29+
pendingApproval?: PendingApprovalData;
30+
// Absent if there's a pending approval
31+
paymentStatus?: LndCreatePaymentResponse;
32+
};
1333

1434
export interface ILightningWallet {
1535
/**
@@ -38,8 +58,9 @@ export interface ILightningWallet {
3858
/**
3959
* Pay a lightning invoice
4060
* @param params Payment parameters (to be defined)
61+
* @param passphrase wallet passphrase to decrypt the user auth key
4162
*/
42-
payInvoice(params: unknown): Promise<unknown>;
63+
payInvoice(params: unknown, passphrase: string): Promise<PayInvoiceResponse>;
4364

4465
/**
4566
* Get the lightning keychain for the given wallet.
@@ -91,8 +112,61 @@ export class SelfCustodialLightningWallet implements ILightningWallet {
91112
});
92113
}
93114

94-
async payInvoice(params: unknown): Promise<unknown> {
95-
throw new Error('Method not implemented.');
115+
async payInvoice(params: SubmitPaymentParams, passphrase: string): Promise<PayInvoiceResponse> {
116+
const reqId = new RequestTracer();
117+
this.wallet.bitgo.setRequestTracer(reqId);
118+
119+
const { userAuthKey } = await this.getLightningAuthKeychains();
120+
const signature = createMessageSignature(
121+
t.exact(LightningPaymentRequest).encode(params),
122+
this.wallet.bitgo.decrypt({ password: passphrase, input: userAuthKey.encryptedPrv })
123+
);
124+
125+
const paymentIntent: LightningPaymentIntent = {
126+
comment: params.comment,
127+
sequenceId: params.sequenceId,
128+
intentType: 'payment',
129+
signedRequest: {
130+
invoice: params.invoice,
131+
amountMsat: params.amountMsat,
132+
feeLimitMsat: params.feeLimitMsat,
133+
feeLimitRatio: params.feeLimitRatio,
134+
},
135+
signature,
136+
};
137+
138+
const transactionRequestCreate = (await this.wallet.bitgo
139+
.post(this.wallet.bitgo.url('/wallet/' + this.wallet.id() + '/txrequests', 2))
140+
.send(LightningPaymentIntent.encode(paymentIntent))
141+
.result()) as TxRequest;
142+
143+
if (transactionRequestCreate.state === 'pendingApproval') {
144+
const pendingApprovals = new PendingApprovals(this.wallet.bitgo, this.wallet.baseCoin);
145+
const pendingApproval = await pendingApprovals.get({ id: transactionRequestCreate.pendingApprovalId });
146+
return {
147+
pendingApproval: pendingApproval.toJSON(),
148+
txRequestId: transactionRequestCreate.txRequestId,
149+
txRequestState: transactionRequestCreate.state,
150+
};
151+
}
152+
153+
const transactionRequestSend = await commonTssMethods.sendTxRequest(
154+
this.wallet.bitgo,
155+
this.wallet.id(),
156+
transactionRequestCreate.txRequestId,
157+
RequestType.tx,
158+
reqId
159+
);
160+
161+
const coinSpecific = transactionRequestSend.transactions?.[0]?.unsignedTx?.coinSpecific;
162+
163+
return {
164+
txRequestId: transactionRequestCreate.txRequestId,
165+
txRequestState: transactionRequestSend.state,
166+
paymentStatus: coinSpecific
167+
? t.exact(LndCreatePaymentResponse).encode(coinSpecific as LndCreatePaymentResponse)
168+
: undefined,
169+
};
96170
}
97171

98172
async listInvoices(params: InvoiceQuery): Promise<InvoiceInfo[]> {

modules/bitgo/test/v2/unit/lightning/lightningWallets.ts

Lines changed: 167 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
2+
// @ts-nocheck
3+
14
import * as assert from 'assert';
25
import { TestBitGo } from '@bitgo/sdk-test';
36
import * as nock from 'nock';
4-
import { BaseCoin } from '@bitgo/sdk-core';
7+
import { BaseCoin, PendingApprovalData, State, Type } from '@bitgo/sdk-core';
58
import {
69
CreateInvoiceBody,
710
getLightningWallet,
811
Invoice,
912
InvoiceInfo,
1013
InvoiceQuery,
14+
LndCreatePaymentResponse,
1115
SelfCustodialLightningWallet,
16+
SubmitPaymentParams,
1217
} from '@bitgo/abstract-lightning';
1318

1419
import { BitGo, common, GenerateLightningWalletOptions, Wallet, Wallets } from '../../../../src';
@@ -20,6 +25,32 @@ describe('Lightning wallets', function () {
2025
let wallets: Wallets;
2126
let bgUrl: string;
2227

28+
const userAuthKey = {
29+
id: 'def',
30+
pub: 'xpub661MyMwAqRbcGYjYsnsDj1SHdiXynWEXNnfNgMSpokN54FKyMqbu7rWEfVNDs6uAJmz86UVFtq4sefhQpXZhSAzQcL9zrEPtiLNNZoeSxCG',
31+
encryptedPrv:
32+
'{"iv":"zYhhaNdW0wPfJEoBjZ4pvg==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tgAMua9jjhw=","ct":"HcrbxQvNlWG5tLMndYzdNCYa1l+1h7o+vSsweA0+q1le3tWt6jLUJSEjZN+JI8lTZ2KPFQgLulQQhsUa+ytUCBi0vSgjF7x7CprT7l2Cfjkew00XsEd7wnmtJUsrQk8m69Co7tIRA3oEgzrnYwy4qOM81lbNNyQ="}',
33+
source: 'user',
34+
coinSpecific: {
35+
tlnbtc: {
36+
purpose: 'userAuth',
37+
},
38+
},
39+
};
40+
41+
const nodeAuthKey = {
42+
id: 'ghi',
43+
pub: 'xpub661MyMwAqRbcG9xnTnAnRbJPo3MAHyRtH4zeehN8exYk4VFz5buepUzebhix33BKhS5Eb4V3LEfW5pYiSR8qmaEnyrpeghhKY8JfzAsUDpq',
44+
encryptedPrv:
45+
'{"iv":"bH6eGbnl9x8PZECPrgvcng==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"o8yknV6nTI8=","ct":"nGyzAToIzYkQeIdcVafoWHtMx7+Fgj0YldCme3WA1yxJAA0QulZVhblMZN/7efCRIumA0NNmpH7dxH6n8cVlz/Z+RUgC2q9lgvZKUoJcYNTjWUfkmkJutXX2tr8yVxm+eC/hnRiyfVLZ2qPxctvDlBVBfgLuPyc="}',
46+
source: 'user',
47+
coinSpecific: {
48+
tlnbtc: {
49+
purpose: 'nodeAuth',
50+
},
51+
},
52+
};
53+
2354
before(function () {
2455
bitgo.initializeTestVars();
2556

@@ -191,7 +222,7 @@ describe('Lightning wallets', function () {
191222
let wallet: SelfCustodialLightningWallet;
192223
beforeEach(function () {
193224
wallet = getLightningWallet(
194-
new Wallet(bitgo, basecoin, { id: 'walletId', coin: 'tlnbtc' })
225+
new Wallet(bitgo, basecoin, { id: 'walletId', coin: 'tlnbtc', coinSpecific: { keys: ['def', 'ghi'] } })
195226
) as SelfCustodialLightningWallet;
196227
});
197228

@@ -262,6 +293,140 @@ describe('Lightning wallets', function () {
262293
await assert.rejects(async () => await wallet.createInvoice(createInvoice), /Invalid create invoice response/);
263294
createInvoiceNock.done();
264295
});
296+
297+
it('should pay invoice', async function () {
298+
const params: SubmitPaymentParams = {
299+
invoice: 'lnbc1...',
300+
amountMsat: 1000n,
301+
feeLimitMsat: 100n,
302+
feeLimitRatio: 0.1,
303+
sequenceId: '123',
304+
comment: 'test payment',
305+
};
306+
307+
const txRequestResponse = {
308+
txRequestId: 'txReq123',
309+
state: 'delivered',
310+
};
311+
312+
const lndResponse: LndCreatePaymentResponse = {
313+
status: 'settled',
314+
paymentHash: 'paymentHash123',
315+
amountMsat: params.amountMsat.toString(),
316+
feeMsat: params.feeLimitMsat.toString(),
317+
paymentPreimage: 'preimage123',
318+
};
319+
320+
const finalPaymentResponse = {
321+
txRequestId: 'txReq123',
322+
state: 'delivered',
323+
transactions: [
324+
{
325+
unsignedTx: {
326+
coinSpecific: {
327+
...lndResponse,
328+
},
329+
},
330+
},
331+
],
332+
};
333+
334+
const createTxRequestNock = nock(bgUrl)
335+
.post(`/api/v2/wallet/${wallet.wallet.id()}/txrequests`)
336+
.reply(200, txRequestResponse);
337+
338+
const sendTxRequestNock = nock(bgUrl)
339+
.post(`/api/v2/wallet/${wallet.wallet.id()}/txrequests/${txRequestResponse.txRequestId}/transactions/0/send`)
340+
.reply(200, finalPaymentResponse);
341+
342+
const userAuthKeyNock = nock(bgUrl)
343+
.get('/api/v2/' + coinName + '/key/def')
344+
.reply(200, userAuthKey);
345+
const nodeAuthKeyNock = nock(bgUrl)
346+
.get('/api/v2/' + coinName + '/key/ghi')
347+
.reply(200, nodeAuthKey);
348+
349+
const response = await wallet.payInvoice(params, 'password123');
350+
assert.strictEqual(response.txRequestId, 'txReq123');
351+
assert.strictEqual(response.txRequestState, 'delivered');
352+
assert.strictEqual(
353+
response.paymentStatus.status,
354+
finalPaymentResponse.transactions[0].unsignedTx.coinSpecific.status
355+
);
356+
assert.strictEqual(
357+
response.paymentStatus.paymentHash,
358+
finalPaymentResponse.transactions[0].unsignedTx.coinSpecific.paymentHash
359+
);
360+
assert.strictEqual(
361+
response.paymentStatus.amountMsat,
362+
finalPaymentResponse.transactions[0].unsignedTx.coinSpecific.amountMsat
363+
);
364+
assert.strictEqual(
365+
response.paymentStatus.feeMsat,
366+
finalPaymentResponse.transactions[0].unsignedTx.coinSpecific.feeMsat
367+
);
368+
assert.strictEqual(
369+
response.paymentStatus.paymentPreimage,
370+
finalPaymentResponse.transactions[0].unsignedTx.coinSpecific.paymentPreimage
371+
);
372+
373+
createTxRequestNock.done();
374+
sendTxRequestNock.done();
375+
userAuthKeyNock.done();
376+
nodeAuthKeyNock.done();
377+
});
378+
379+
it('should handle pending approval when paying invoice', async function () {
380+
const params: SubmitPaymentParams = {
381+
invoice: 'lnbc1...',
382+
amountMsat: 1000n,
383+
feeLimitMsat: 100n,
384+
feeLimitRatio: 0.1,
385+
sequenceId: '123',
386+
comment: 'test payment',
387+
};
388+
389+
const txRequestResponse = {
390+
txRequestId: 'txReq123',
391+
state: 'pendingApproval',
392+
pendingApprovalId: 'approval123',
393+
};
394+
395+
const pendingApprovalData: PendingApprovalData = {
396+
id: 'approval123',
397+
state: State.PENDING,
398+
creator: 'user123',
399+
info: {
400+
type: Type.TRANSACTION_REQUEST,
401+
},
402+
};
403+
404+
const createTxRequestNock = nock(bgUrl)
405+
.post(`/api/v2/wallet/${wallet.wallet.id()}/txrequests`)
406+
.reply(200, txRequestResponse);
407+
408+
const getPendingApprovalNock = nock(bgUrl)
409+
.get(`/api/v2/${coinName}/pendingapprovals/${txRequestResponse.pendingApprovalId}`)
410+
.reply(200, pendingApprovalData);
411+
412+
const userAuthKeyNock = nock(bgUrl)
413+
.get('/api/v2/' + coinName + '/key/def')
414+
.reply(200, userAuthKey);
415+
const nodeAuthKeyNock = nock(bgUrl)
416+
.get('/api/v2/' + coinName + '/key/ghi')
417+
.reply(200, nodeAuthKey);
418+
419+
const response = await wallet.payInvoice(params, 'password123');
420+
assert.strictEqual(response.txRequestId, 'txReq123');
421+
assert.strictEqual(response.txRequestState, 'pendingApproval');
422+
assert(response.pendingApproval);
423+
assert.strictEqual(response.status, undefined);
424+
425+
createTxRequestNock.done();
426+
getPendingApprovalNock.done();
427+
userAuthKeyNock.done();
428+
nodeAuthKeyNock.done();
429+
});
265430
});
266431

267432
describe('Get lightning key(s)', function () {
@@ -386,32 +551,6 @@ describe('Lightning wallets', function () {
386551
coinSpecific: { keys: ['def', 'ghi'] },
387552
};
388553

389-
const userAuthKey = {
390-
id: 'def',
391-
pub: 'xpub661MyMwAqRbcGYjYsnsDj1SHdiXynWEXNnfNgMSpokN54FKyMqbu7rWEfVNDs6uAJmz86UVFtq4sefhQpXZhSAzQcL9zrEPtiLNNZoeSxCG',
392-
encryptedPrv:
393-
'{"iv":"zYhhaNdW0wPfJEoBjZ4pvg==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tgAMua9jjhw=","ct":"HcrbxQvNlWG5tLMndYzdNCYa1l+1h7o+vSsweA0+q1le3tWt6jLUJSEjZN+JI8lTZ2KPFQgLulQQhsUa+ytUCBi0vSgjF7x7CprT7l2Cfjkew00XsEd7wnmtJUsrQk8m69Co7tIRA3oEgzrnYwy4qOM81lbNNyQ="}',
394-
source: 'user',
395-
coinSpecific: {
396-
tlnbtc: {
397-
purpose: 'userAuth',
398-
},
399-
},
400-
};
401-
402-
const nodeAuthKey = {
403-
id: 'ghi',
404-
pub: 'xpub661MyMwAqRbcG9xnTnAnRbJPo3MAHyRtH4zeehN8exYk4VFz5buepUzebhix33BKhS5Eb4V3LEfW5pYiSR8qmaEnyrpeghhKY8JfzAsUDpq',
405-
encryptedPrv:
406-
'{"iv":"bH6eGbnl9x8PZECPrgvcng==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"o8yknV6nTI8=","ct":"nGyzAToIzYkQeIdcVafoWHtMx7+Fgj0YldCme3WA1yxJAA0QulZVhblMZN/7efCRIumA0NNmpH7dxH6n8cVlz/Z+RUgC2q9lgvZKUoJcYNTjWUfkmkJutXX2tr8yVxm+eC/hnRiyfVLZ2qPxctvDlBVBfgLuPyc="}',
407-
source: 'user',
408-
coinSpecific: {
409-
tlnbtc: {
410-
purpose: 'nodeAuth',
411-
},
412-
},
413-
};
414-
415554
const watchOnlyAccounts = {
416555
master_key_birthday_timestamp: 'dummy',
417556
master_key_fingerprint: 'dummy',

0 commit comments

Comments
 (0)