UNPKG

bitgo

Version:
1,029 lines (1,028 loc) • 160 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const assert = require("assert"); const sdk_test_1 = require("@bitgo/sdk-test"); const nock = require("nock"); const sdk_core_1 = require("@bitgo/sdk-core"); const abstract_lightning_1 = require("@bitgo/abstract-lightning"); const src_1 = require("../../../../src"); describe('Lightning wallets', function () { const coinName = 'tlnbtc'; const bitgo = sdk_test_1.TestBitGo.decorate(src_1.BitGo, { env: 'mock' }); let basecoin; let wallets; let bgUrl; const userAuthKey = { id: 'def', pub: 'xpub661MyMwAqRbcGYjYsnsDj1SHdiXynWEXNnfNgMSpokN54FKyMqbu7rWEfVNDs6uAJmz86UVFtq4sefhQpXZhSAzQcL9zrEPtiLNNZoeSxCG', encryptedPrv: '{"iv":"zYhhaNdW0wPfJEoBjZ4pvg==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tgAMua9jjhw=","ct":"HcrbxQvNlWG5tLMndYzdNCYa1l+1h7o+vSsweA0+q1le3tWt6jLUJSEjZN+JI8lTZ2KPFQgLulQQhsUa+ytUCBi0vSgjF7x7CprT7l2Cfjkew00XsEd7wnmtJUsrQk8m69Co7tIRA3oEgzrnYwy4qOM81lbNNyQ="}', source: 'user', coinSpecific: { tlnbtc: { purpose: 'userAuth', }, }, }; const nodeAuthKey = { id: 'ghi', pub: 'xpub661MyMwAqRbcG9xnTnAnRbJPo3MAHyRtH4zeehN8exYk4VFz5buepUzebhix33BKhS5Eb4V3LEfW5pYiSR8qmaEnyrpeghhKY8JfzAsUDpq', encryptedPrv: '{"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="}', source: 'user', coinSpecific: { tlnbtc: { purpose: 'nodeAuth', }, }, }; before(function () { bitgo.initializeTestVars(); basecoin = bitgo.coin(coinName); wallets = basecoin.wallets(); bgUrl = src_1.common.Environments[bitgo.getEnv()].uri; }); after(function () { nock.cleanAll(); nock.pendingMocks().length.should.equal(0); }); describe('Generate lightning wallet', function () { it('should validate parameters', async function () { await wallets .generateWallet({ passphrase: 'pass123', enterprise: 'ent123', passcodeEncryptionCode: 'code123', subType: 'lightningCustody', }) .should.be.rejectedWith("error(s) parsing generate lightning wallet request params: Invalid value 'undefined' supplied to GenerateLightningWalletOptions.label, expected string."); await wallets .generateWallet({ label: 'my ln wallet', enterprise: 'ent123', passcodeEncryptionCode: 'code123', subType: 'lightningCustody', }) .should.be.rejectedWith("error(s) parsing generate lightning wallet request params: Invalid value 'undefined' supplied to GenerateLightningWalletOptions.passphrase, expected string."); await wallets .generateWallet({ label: 'my ln wallet', passphrase: 'pass123', passcodeEncryptionCode: 'code123', subType: 'lightningCustody', }) .should.be.rejectedWith("error(s) parsing generate lightning wallet request params: Invalid value 'undefined' supplied to GenerateLightningWalletOptions.enterprise, expected string."); await wallets .generateWallet({ label: 'my ln wallet', passphrase: 'pass123', enterprise: 'ent123', subType: 'lightningCustody', }) .should.be.rejectedWith("error(s) parsing generate lightning wallet request params: Invalid value 'undefined' supplied to GenerateLightningWalletOptions.passcodeEncryptionCode, expected string."); await wallets .generateWallet({ label: 123, passphrase: 'pass123', enterprise: 'ent123', passcodeEncryptionCode: 'code123', subType: 'lightningCustody', }) .should.be.rejectedWith("error(s) parsing generate lightning wallet request params: Invalid value '123' supplied to GenerateLightningWalletOptions.label, expected string."); await wallets .generateWallet({ label: 'my ln wallet', passphrase: 123, enterprise: 'ent123', passcodeEncryptionCode: 'code123', subType: 'lightningCustody', }) .should.be.rejectedWith("error(s) parsing generate lightning wallet request params: Invalid value '123' supplied to GenerateLightningWalletOptions.passphrase, expected string."); await wallets .generateWallet({ label: 'my ln wallet', passphrase: 'pass123', enterprise: 123, passcodeEncryptionCode: 'code123', subType: 'lightningCustody', }) .should.be.rejectedWith("error(s) parsing generate lightning wallet request params: Invalid value '123' supplied to GenerateLightningWalletOptions.enterprise, expected string."); await wallets .generateWallet({ label: 'my ln wallet', passphrase: 'pass123', enterprise: 'ent123', passcodeEncryptionCode: 123, subType: 'lightningCustody', }) .should.be.rejectedWith("error(s) parsing generate lightning wallet request params: Invalid value '123' supplied to GenerateLightningWalletOptions.passcodeEncryptionCode, expected string."); }); for (const subType of ['lightningCustody', 'lightningSelfCustody']) { it(`should generate ${subType} lightning wallet`, async function () { const params = { label: 'my ln wallet', passphrase: 'pass123', enterprise: 'ent123', passcodeEncryptionCode: 'code123', subType: subType, }; const validateKeyRequest = (body) => { const baseChecks = body.pub.startsWith('xpub') && !!body.encryptedPrv && body.keyType === 'independent' && body.source === 'user'; if (body.originalPasscodeEncryptionCode !== undefined) { return baseChecks && body.originalPasscodeEncryptionCode === 'code123' && body.coinSpecific === undefined; } else { const coinSpecific = body.coinSpecific && body.coinSpecific.tlnbtc; return baseChecks && !!coinSpecific && ['userAuth', 'nodeAuth'].includes(coinSpecific.purpose); } }; const validateWalletRequest = (body) => { return (body.label === 'my ln wallet' && body.m === 1 && body.n === 1 && body.type === 'hot' && body.subType === subType && body.enterprise === 'ent123' && Array.isArray(body.keys) && body.keys.length === 1 && body.keys[0] === 'keyId1' && body.coinSpecific && body.coinSpecific.tlnbtc && Array.isArray(body.coinSpecific.tlnbtc.keys) && body.coinSpecific.tlnbtc.keys.length === 2 && body.coinSpecific.tlnbtc.keys.includes('keyId2') && body.coinSpecific.tlnbtc.keys.includes('keyId3')); }; nock(bgUrl) .post('/api/v2/' + coinName + '/key', (body) => validateKeyRequest(body)) .reply(200, { id: 'keyId1' }); nock(bgUrl) .post('/api/v2/' + coinName + '/key', (body) => validateKeyRequest(body)) .reply(200, { id: 'keyId2' }); nock(bgUrl) .post('/api/v2/' + coinName + '/key', (body) => validateKeyRequest(body)) .reply(200, { id: 'keyId3' }); nock(bgUrl) .post('/api/v2/' + coinName + '/wallet/add', (body) => validateWalletRequest(body)) .reply(200, { id: 'walletId' }); const response = await wallets.generateWallet(params); assert.ok(response.wallet); assert.ok(response.encryptedWalletPassphrase); assert.equal(bitgo.decrypt({ input: response.encryptedWalletPassphrase, password: params.passcodeEncryptionCode }), params.passphrase); }); } }); describe('invoices', function () { let wallet; beforeEach(function () { wallet = (0, abstract_lightning_1.getLightningWallet)(new src_1.Wallet(bitgo, basecoin, { id: 'walletId', coin: 'tlnbtc', subType: 'lightningCustody', coinSpecific: { keys: ['def', 'ghi'] }, })); }); it('should list invoices', async function () { const invoice = { valueMsat: 1000n, paymentHash: 'foo', invoice: 'tlnfoobar', walletId: wallet.wallet.id(), status: 'open', expiresAt: new Date(), createdAt: new Date(), updatedAt: new Date(), }; const query = { status: 'open', startDate: new Date(), limit: 100n, }; const listInvoicesNock = nock(bgUrl) .get(`/api/v2/wallet/${wallet.wallet.id()}/lightning/invoice`) .query(abstract_lightning_1.InvoiceQuery.encode(query)) .reply(200, { invoices: [abstract_lightning_1.Invoice.encode(invoice)] }); const invoiceResponse = await wallet.listInvoices(query); assert.strictEqual(invoiceResponse.invoices.length, 1); assert.deepStrictEqual(invoiceResponse.invoices[0], invoice); listInvoicesNock.done(); }); it('should work properly with pagination while listing invoices', async function () { const invoice1 = { valueMsat: 1000n, paymentHash: 'foo1', invoice: 'tlnfoobar1', walletId: wallet.wallet.id(), status: 'open', expiresAt: new Date(), createdAt: new Date(), updatedAt: new Date(), }; const invoice2 = { ...invoice1, paymentHash: 'foo2', invoice: 'tlnfoobar2', }; const invoice3 = { ...invoice1, paymentHash: 'foo3', invoice: 'tlnfoobar3', }; const allInvoices = [abstract_lightning_1.Invoice.encode(invoice1), abstract_lightning_1.Invoice.encode(invoice2), abstract_lightning_1.Invoice.encode(invoice3)]; const query = { status: 'open', startDate: new Date(), limit: 2n, }; const listInvoicesNock = nock(bgUrl) .get(`/api/v2/wallet/${wallet.wallet.id()}/lightning/invoice`) .query(abstract_lightning_1.InvoiceQuery.encode(query)) .reply(200, { invoices: allInvoices.slice(0, 2), nextBatchPrevId: invoice2.paymentHash }); const invoiceResponse = await wallet.listInvoices(query); assert.strictEqual(invoiceResponse.invoices.length, 2); assert.deepStrictEqual(invoiceResponse.invoices[0], invoice1); assert.deepStrictEqual(invoiceResponse.invoices[1], invoice2); assert.strictEqual(invoiceResponse.nextBatchPrevId, invoice2.paymentHash); listInvoicesNock.done(); }); it('listInvoices should throw error if wp response is invalid', async function () { const listInvoicesNock = nock(bgUrl) .get(`/api/v2/wallet/${wallet.wallet.id()}/lightning/invoice`) .reply(200, { invoices: [{ valueMsat: '1000' }] }); await assert.rejects(async () => await wallet.listInvoices({}), /Invalid list invoices response/); listInvoicesNock.done(); }); it('should create invoice', async function () { const createInvoice = { valueMsat: 1000n, memo: 'test invoice', expiry: 100, }; const invoice = { invoice: 'tlnabc', paymentHash: '123', expiresAt: new Date(), status: 'open', walletId: wallet.wallet.id(), valueMsat: 1000n, createdAt: new Date(), updatedAt: new Date(), memo: 'test invoice', }; const createInvoiceNock = nock(bgUrl) .post(`/api/v2/wallet/${wallet.wallet.id()}/lightning/invoice`, abstract_lightning_1.CreateInvoiceBody.encode(createInvoice)) .reply(200, abstract_lightning_1.Invoice.encode(invoice)); const createInvoiceResponse = await wallet.createInvoice(createInvoice); assert.deepStrictEqual(createInvoiceResponse, invoice); createInvoiceNock.done(); }); it('createInvoice should throw error if wp response is invalid', async function () { const createInvoice = { valueMsat: 1000n, memo: 'test invoice', expiry: 100, }; const createInvoiceNock = nock(bgUrl) .post(`/api/v2/wallet/${wallet.wallet.id()}/lightning/invoice`) .reply(200, { valueMsat: '1000' }); await assert.rejects(async () => await wallet.createInvoice(createInvoice), /Invalid create invoice response/); createInvoiceNock.done(); }); it('should pay invoice', async function () { const params = { invoice: 'lnbc1...', amountMsat: 1000n, feeLimitMsat: 100n, feeLimitRatio: 0.1, sequenceId: '123', comment: 'test payment', passphrase: 'password123', }; const txRequestResponse = { txRequestId: 'txReq123', state: 'delivered', }; const lndResponse = { status: 'settled', paymentHash: 'paymentHash123', amountMsat: params.amountMsat !== undefined ? params.amountMsat.toString() : undefined, feeMsat: params.feeLimitMsat !== undefined ? params.feeLimitMsat.toString() : undefined, paymentPreimage: 'preimage123', }; const finalPaymentResponse = { txRequestId: 'txReq123', state: 'delivered', transactions: [ { unsignedTx: { coinSpecific: { ...lndResponse, }, }, }, ], }; const createTransferData = { id: 'fake_id', coin: 'tlnbtc', state: 'initialized', }; const createTxRequestNock = nock(bgUrl) .post(`/api/v2/wallet/${wallet.wallet.id()}/txrequests`) .reply(200, txRequestResponse); const createTransferNock = nock(bgUrl) .post(`/api/v2/wallet/${wallet.wallet.id()}/txrequests/${txRequestResponse.txRequestId}/transfers`) .reply(200, createTransferData); const sendTxRequestNock = nock(bgUrl) .post(`/api/v2/wallet/${wallet.wallet.id()}/txrequests/${txRequestResponse.txRequestId}/transactions/0/send`) .reply(200, finalPaymentResponse); const userAuthKeyNock = nock(bgUrl) .get('/api/v2/' + coinName + '/key/def') .reply(200, userAuthKey); const nodeAuthKeyNock = nock(bgUrl) .get('/api/v2/' + coinName + '/key/ghi') .reply(200, nodeAuthKey); const response = await wallet.payInvoice(params); assert.strictEqual(response.txRequestId, 'txReq123'); assert.strictEqual(response.txRequestState, 'delivered'); assert.ok(response.paymentStatus); assert.strictEqual(response.paymentStatus.status, finalPaymentResponse.transactions[0].unsignedTx.coinSpecific.status); assert.strictEqual(response.paymentStatus.paymentHash, finalPaymentResponse.transactions[0].unsignedTx.coinSpecific.paymentHash); assert.strictEqual(response.paymentStatus.amountMsat, finalPaymentResponse.transactions[0].unsignedTx.coinSpecific.amountMsat); assert.strictEqual(response.paymentStatus.feeMsat, finalPaymentResponse.transactions[0].unsignedTx.coinSpecific.feeMsat); assert.strictEqual(response.paymentStatus.paymentPreimage, finalPaymentResponse.transactions[0].unsignedTx.coinSpecific.paymentPreimage); createTxRequestNock.done(); sendTxRequestNock.done(); createTransferNock.done(); userAuthKeyNock.done(); nodeAuthKeyNock.done(); }); it('should handle pending approval when paying invoice', async function () { const params = { invoice: 'lnbc1...', amountMsat: 1000n, feeLimitMsat: 100n, feeLimitRatio: 0.1, sequenceId: '123', comment: 'test payment', passphrase: 'password123', }; const txRequestResponse = { txRequestId: 'txReq123', state: 'pendingApproval', pendingApprovalId: 'approval123', }; const pendingApprovalData = { id: 'approval123', state: sdk_core_1.State.PENDING, creator: 'user123', info: { type: sdk_core_1.Type.TRANSACTION_REQUEST, }, }; const createTxRequestNock = nock(bgUrl) .post(`/api/v2/wallet/${wallet.wallet.id()}/txrequests`) .reply(200, txRequestResponse); const getPendingApprovalNock = nock(bgUrl) .get(`/api/v2/${coinName}/pendingapprovals/${txRequestResponse.pendingApprovalId}`) .reply(200, pendingApprovalData); const userAuthKeyNock = nock(bgUrl) .get('/api/v2/' + coinName + '/key/def') .reply(200, userAuthKey); const nodeAuthKeyNock = nock(bgUrl) .get('/api/v2/' + coinName + '/key/ghi') .reply(200, nodeAuthKey); const response = await wallet.payInvoice(params); assert.strictEqual(response.txRequestId, 'txReq123'); assert.strictEqual(response.txRequestState, 'pendingApproval'); assert.ok(response.pendingApproval); assert.strictEqual(response.paymentStatus, undefined); createTxRequestNock.done(); getPendingApprovalNock.done(); userAuthKeyNock.done(); nodeAuthKeyNock.done(); }); }); describe('payments', function () { let wallet; beforeEach(function () { wallet = (0, abstract_lightning_1.getLightningWallet)(new src_1.Wallet(bitgo, basecoin, { id: 'walletId', coin: 'tlnbtc', subType: 'lightningCustody', coinSpecific: { keys: ['def', 'ghi'] }, })); }); it('should get payments', async function () { const payment = { id: '8308fddb-2356-49aa-8548-10c23099854c', paymentHash: 'foo', walletId: wallet.wallet.id(), txRequestId: 'txReqId', status: 'settled', invoice: 'tlnfoobar', destination: 'destination', feeLimitMsat: 100n, amountMsat: 1000n, createdAt: new Date(), updatedAt: new Date(), }; const query = { status: 'settled', startDate: new Date(), limit: 100n, }; const listPaymentsNock = nock(bgUrl) .get(`/api/v2/wallet/${wallet.wallet.id()}/lightning/payment`) .query(abstract_lightning_1.PaymentQuery.encode(query)) .reply(200, { payments: [abstract_lightning_1.PaymentInfo.encode(payment)] }); const listPaymentsResponse = await wallet.listPayments(query); assert.strictEqual(listPaymentsResponse.payments.length, 1); assert.deepStrictEqual(listPaymentsResponse.payments[0], payment); listPaymentsNock.done(); }); it('should work properly with pagination while listing payments', async function () { const payment1 = { id: '8308fddb-2356-49aa-8548-10c23099854c', paymentHash: 'foo1', walletId: wallet.wallet.id(), txRequestId: 'txReqId1', status: 'settled', invoice: 'tlnfoobar1', destination: 'destination', feeLimitMsat: 100n, amountMsat: 1000n, createdAt: new Date(), updatedAt: new Date(), }; const payment2 = { ...payment1, id: '8308fddb-2356-49aa-8548-10c23099854d', paymentHash: 'foo2', txRequestId: 'txReqId2', invoice: 'tlnfoobar2', }; const payment3 = { ...payment1, id: '8308fddb-2356-49aa-8548-10c23099854e', paymentHash: 'foo3', txRequestId: 'txReqId3', invoice: 'tlnfoobar3', }; const allPayments = [abstract_lightning_1.PaymentInfo.encode(payment1), abstract_lightning_1.PaymentInfo.encode(payment2), abstract_lightning_1.PaymentInfo.encode(payment3)]; const query = { status: 'settled', startDate: new Date(), limit: 2n, }; const listPaymentsNock = nock(bgUrl) .get(`/api/v2/wallet/${wallet.wallet.id()}/lightning/payment`) .query(abstract_lightning_1.PaymentQuery.encode(query)) .reply(200, { payments: allPayments.slice(0, 2), nextBatchPrevId: payment2.paymentHash }); const listPaymentsResponse = await wallet.listPayments(query); assert.strictEqual(listPaymentsResponse.payments.length, 2); assert.deepStrictEqual(listPaymentsResponse.payments[0], payment1); assert.deepStrictEqual(listPaymentsResponse.payments[1], payment2); assert.strictEqual(listPaymentsResponse.nextBatchPrevId, payment2.paymentHash); listPaymentsNock.done(); }); it('listPayments should throw error if wp response is invalid', async function () { const listPaymentsNock = nock(bgUrl) .get(`/api/v2/wallet/${wallet.wallet.id()}/lightning/payment`) .reply(200, { payments: [{ amountMsat: '1000' }] }); await assert.rejects(async () => await wallet.listPayments({}), /Invalid payment list response/); listPaymentsNock.done(); }); }); describe('Get lightning key(s)', function () { const walletData = { id: 'fakeid', coin: coinName, keys: ['abc'], coinSpecific: { keys: ['def', 'ghi'] }, subType: 'lightningCustody', }; const userKeyData = { id: 'abc', pub: 'xpub1', encryptedPrv: 'encryptedPrv1', source: 'user', }; const userAuthKeyData = { id: 'def', pub: 'xpub2', encryptedPrv: 'encryptedPrv2', source: 'user', coinSpecific: { tlnbtc: { purpose: 'userAuth', }, }, }; const nodeAuthKeyData = { id: 'ghi', pub: 'xpub3', encryptedPrv: 'encryptedPrv3', source: 'user', coinSpecific: { tlnbtc: { purpose: 'nodeAuth', }, }, }; it('should get lightning key', async function () { const wallet = new src_1.Wallet(bitgo, basecoin, walletData); const keyNock = nock(bgUrl) .get('/api/v2/' + coinName + '/key/abc') .reply(200, userKeyData); const key = await (0, abstract_lightning_1.getLightningKeychain)(wallet); assert.deepStrictEqual(key, userKeyData); keyNock.done(); }); it('should get lightning auth keys', async function () { const wallet = new src_1.Wallet(bitgo, basecoin, walletData); const userAuthKeyNock = nock(bgUrl) .get('/api/v2/' + coinName + '/key/def') .reply(200, userAuthKeyData); const nodeAuthKeyNock = nock(bgUrl) .get('/api/v2/' + coinName + '/key/ghi') .reply(200, nodeAuthKeyData); const { userAuthKey, nodeAuthKey } = await (0, abstract_lightning_1.getLightningAuthKeychains)(wallet); assert.deepStrictEqual(userAuthKey, userAuthKeyData); assert.deepStrictEqual(nodeAuthKey, nodeAuthKeyData); userAuthKeyNock.done(); nodeAuthKeyNock.done(); }); it('should fail to get lightning key for invalid number of keys', async function () { const wallet = new src_1.Wallet(bitgo, basecoin, { ...walletData, keys: [] }); await assert.rejects(async () => await (0, abstract_lightning_1.getLightningKeychain)(wallet), /Error: Invalid number of key in lightning wallet: 0/); }); it('should fail to get lightning auth keys for invalid number of keys', async function () { const wallet = new src_1.Wallet(bitgo, basecoin, { ...walletData, coinSpecific: { keys: ['def'] } }); await assert.rejects(async () => await (0, abstract_lightning_1.getLightningAuthKeychains)(wallet), /Error: Invalid number of auth keys in lightning wallet: 1/); }); it('should fail to get lightning key for invalid response', async function () { const wallet = new src_1.Wallet(bitgo, basecoin, walletData); nock(bgUrl) .get('/api/v2/' + coinName + '/key/abc') .reply(200, { ...userKeyData, source: 'backup' }); await assert.rejects(async () => await (0, abstract_lightning_1.getLightningKeychain)(wallet), /Error: Invalid user key/); }); it('should fail to get lightning auth keys for invalid response', async function () { const wallet = new src_1.Wallet(bitgo, basecoin, walletData); nock(bgUrl) .get('/api/v2/' + coinName + '/key/def') .reply(200, { ...userAuthKeyData, source: 'backup' }); nock(bgUrl) .get('/api/v2/' + coinName + '/key/ghi') .reply(200, nodeAuthKeyData); await assert.rejects(async () => await (0, abstract_lightning_1.getLightningAuthKeychains)(wallet), /Error: Invalid lightning auth key: def/); }); }); describe('Update lightning wallet coin specific', function () { const walletData = { id: 'fakeid', coin: coinName, keys: ['abc'], coinSpecific: { keys: ['def', 'ghi'] }, subType: 'lightningSelfCustody', }; const watchOnlyAccounts = { master_key_birthday_timestamp: 'dummy', master_key_fingerprint: 'dummy', accounts: [ { xpub: 'upub5Eep7H5q39PzQZLVEYLBytDyBNeV74E8mQsyeL6UozFq9Y3MsZ52G7YGuqrJPgoyAqF7TBeJdnkrHrVrB5pkWkPJ9cJGAePMU6F1Gyw6aFH', purpose: 49, coin_type: 0, account: 0, }, { xpub: 'vpub5ZU1PHGpQoDSHckYico4nsvwsD3mTh6UjqL5zyGWXZXzBjTYMNKot7t9eRPQY71hJcnNN9r1ss25g3xA9rmoJ5nWPg8jEWavrttnsVa1qw1', purpose: 84, coin_type: 0, account: 0, }, ], }; it('should update wallet', async function () { const wallet = new src_1.Wallet(bitgo, basecoin, walletData); const userAuthKeyNock = nock(bgUrl) .get('/api/v2/' + coinName + '/key/def') .reply(200, userAuthKey); const nodeAuthKeyNock = nock(bgUrl) .get('/api/v2/' + coinName + '/key/ghi') .reply(200, nodeAuthKey); let capturedBody; const wpWalletUpdateNock = nock(bgUrl) .put(`/api/v2/tlnbtc/wallet/${walletData.id}`) .reply(function (uri, requestBody) { capturedBody = requestBody; return [200]; }); const params = { signerMacaroon: 'signerMacaroon', signerAdminMacaroon: 'signerAdminMacaroon', signerTlsKey: 'signerTlsKey', signerTlsCert: 'signerTlsCert', watchOnlyAccounts, passphrase: 'password123', }; await assert.doesNotReject(async () => await (0, abstract_lightning_1.updateWalletCoinSpecific)(wallet, params)); assert.ok(userAuthKeyNock.isDone()); assert.ok(nodeAuthKeyNock.isDone()); assert.ok(wpWalletUpdateNock.isDone()); // Verify structure and required fields assert.ok(capturedBody.coinSpecific?.tlnbtc?.signedRequest, 'signedRequest should exist'); const signedRequest = capturedBody.coinSpecific.tlnbtc.signedRequest; assert.ok(signedRequest.signerTlsCert, 'signerTlsCert should exist'); assert.ok(signedRequest.watchOnlyAccounts, 'watchOnlyAccounts should exist'); assert.ok(signedRequest.encryptedSignerTlsKey, 'encryptedSignerTlsKey should exist'); assert.ok(signedRequest.encryptedSignerAdminMacaroon, 'encryptedSignerAdminMacaroon should exist'); assert.ok(signedRequest.encryptedSignerMacaroon, 'encryptedSignerMacaroon should exist'); // Verify signature exists assert.ok(capturedBody.coinSpecific.tlnbtc.signature, 'signature should exist'); // we should not pass passphrase to the backend assert.strictEqual(signedRequest.passphrase, undefined, 'passphrase should not exist in request'); }); }); describe('On chain withdrawal', function () { let wallet; beforeEach(function () { const watchOnlyAccounts = { master_key_birthday_timestamp: 'dummy', master_key_fingerprint: 'dummy', accounts: [ { xpub: 'tpubDCmiWMkTJrZ24t1Z6ECR3HyynCyZ9zGsWqhcLh6H4yFK2CDozSszD1pP2Li4Nx1YYtRcvmNbdb3nD1SzFejYtPFfTocTv2EaAgJCg4zpJpA', purpose: 49, coin_type: 0, account: 0, }, { xpub: 'tpubDCFN7bsxR9UTKggdH2pmv5HeHGQNiDrJwa1EZFtP9sH5PF28i37FHpoYSYARQkKZ6Mi98pkp7oypDcxFmE4dQGq8jV8Gv3L6gmWBeRwPxkP', purpose: 84, coin_type: 0, account: 0, }, ], }; wallet = (0, abstract_lightning_1.getLightningWallet)(new src_1.Wallet(bitgo, basecoin, { id: 'walletId', coin: 'tlnbtc', subType: 'lightningCustody', coinSpecific: { keys: ['def', 'ghi'], watchOnlyAccounts }, })); }); it('should withdraw on chain', async function () { const params = { recipients: [ { amountSat: 100000n, address: 'tb1px7du3rxpt5mqtmvauxmpl7xk2qsmv5xmz9g7w5drpzzuelx869dqwape7k', }, ], satsPerVbyte: 15n, passphrase: 'password123', }; const unsignedPsbtHex = '70736274ff01007d02000000015e50b8d96cebdc3273d9a100eb68392d980d5b934b8170c80a23488b595268ca0100000000ffffffff02a086010000000000225120379bc88cc15d3605ed9de1b61ff8d65021b650db1151e751a30885ccfcc7d15affa80a000000000016001480a06f2e6b77e817fd5de6e41ea512c563c26cb800000000000100ea02000000000101a158d806735bb7c54e4c701d4f5821cd5342d48d5e1fcbed1169e6e45aa444be0100000000ffffffff02a086010000000000225120379bc88cc15d3605ed9de1b61ff8d65021b650db1151e751a30885ccfcc7d15a6a310c000000000016001478a5d98c7160484b9b00f1782803c58edfc49b9a024730440220407d9162f52371df246dcfa2943d40fbdcb0d4b6768f7682c65193378b2845a60220101c7bc460c93d2976961ac23400f0f10c145efb989a3addb7f03ebaaa2200950121037e17444c85c8b7da07f12fd53cb2ca142c2b4932d0f898649c4b5be0021da0980000000001030401000000220602e57146e5b4762a7ff374adf4072047b67ef115ad46a34189bdeb6a4f88db9b0818000000005400008000000080000000800100000006000000000022020379abbe44004ff7e527bdee3dd8d95e5cd250053f35ee92258b97aa83dfa93c621800000000540000800000008000000080010000005000000000'; const txRequestResponse = { txRequestId: 'txReq123', state: 'pendingDelivery', transactions: [ { unsignedTx: { serializedTxHex: unsignedPsbtHex, }, }, ], }; const txRequestWithSignatureResponse = { txRequestId: 'txReq123', state: 'pendingDelivery', transactions: [ { unsignedTx: { serializedTxHex: unsignedPsbtHex, coinSpecific: { signature: 'someSignature', }, }, }, ], }; const finalWithdrawResponse = { txRequestId: 'txReq123', state: 'delivered', transactions: [ { unsignedTx: { serializedTxHex: unsignedPsbtHex, coinSpecific: { signature: 'someSignature', status: 'delivered', txid: 'tx123', }, }, }, ], }; const transferResponse = { entries: [ { address: 'tb1qr54zqkf0sjwdycygulkcuw0audm8n6r77xj3q6', wallet: '6846918381f118cc42c335b037929665', value: -1000000, valueString: '-1000000', }, { address: 'tb1qmddle8nhnjcv93t0qnr2tmhvvxfem4ppxq4rzc', value: 498590, valueString: '498590', isChange: true, isPayGo: false, }, { address: 'tb1qjq48cqk2u80hewdcndf539m8nnnvt8453kr23h', value: 500000, valueString: '500000', isChange: false, isPayGo: false, }, ], id: '6846918481f118cc42c33611410906e5', coin: 'tlnbtc', wallet: 'walletId', walletType: 'hot', enterprise: '6846918381f118cc42c3354ff05d72d8', organization: '6846918381f118cc42c3355f6807d11e', txidType: 'transactionHash', txRequestId: 'txReq123', height: 999999999, heightId: '999999999-6846918481f118cc42c33611410906e5', date: '2025-06-09T07:47:16.090Z', type: 'send', value: -1000000, valueString: '-1000000', intendedValueString: '-1000000', baseValue: -998590, baseValueString: '-998590', baseValueWithoutFees: -998590, baseValueWithoutFeesString: '-998590', feeString: '1410', payGoFee: 0, payGoFeeString: '0', usd: -1.23456, usdRate: 123456, state: 'initialized', instant: false, isReward: false, isUnlock: false, isFee: false, senderInformationVerified: false, tags: ['6846918381f118cc42c335b037929665', '6846918381f118cc42c3354ff05d72d8'], history: [ { date: '2025-06-09T07:47:16.102Z', user: '6846918281f118cc42c33352779df88f', action: 'created', }, ], coinSpecific: { isOffchain: false, }, metadata: [], createdTime: '2025-06-09T07:47:16.102Z', }; const createTxRequestNock = nock(bgUrl) .post(`/api/v2/wallet/${wallet.wallet.id()}/txrequests`) .reply(200, txRequestResponse); const storeSignatureNock = nock(bgUrl) .put(`/api/v2/wallet/${wallet.wallet.id()}/txrequests/${txRequestResponse.txRequestId}/coinSpecific`) .reply(200, txRequestWithSignatureResponse); const createTransferNock = nock(bgUrl) .post(`/api/v2/wallet/${wallet.wallet.id()}/txrequests/${txRequestResponse.txRequestId}/transfers`) .reply(200, transferResponse); const sendTxRequestNock = nock(bgUrl) .post(`/api/v2/wallet/${wallet.wallet.id()}/txrequests/${txRequestResponse.txRequestId}/transactions/0/send`) .reply(200, finalWithdrawResponse); const userAuthKeyNock = nock(bgUrl) .get('/api/v2/' + coinName + '/key/def') .reply(200, userAuthKey); const nodeAuthKeyNock = nock(bgUrl) .get('/api/v2/' + coinName + '/key/ghi') .reply(200, nodeAuthKey); const response = await wallet.withdrawOnchain(params); assert.strictEqual(response.txRequestId, 'txReq123'); assert.strictEqual(response.txRequestState, 'delivered'); assert.strictEqual(response.withdrawStatus?.status, 'delivered'); assert.strictEqual(response.withdrawStatus?.txid, 'tx123'); assert.strictEqual(response.withdrawStatus.signature, undefined); userAuthKeyNock.done(); nodeAuthKeyNock.done(); createTxRequestNock.done(); storeSignatureNock.done(); createTransferNock.done(); sendTxRequestNock.done(); }); it('should handle pending approval when withdrawing onchain', async function () { const params = { recipients: [ { amountSat: 100000n, address: 'tb1px7du3rxpt5mqtmvauxmpl7xk2qsmv5xmz9g7w5drpzzuelx869dqwape7k', }, ], satsPerVbyte: 15n, passphrase: 'password123', }; const unsignedPsbtHex = '70736274ff01007d02000000015e50b8d96cebdc3273d9a100eb68392d980d5b934b8170c80a23488b595268ca0100000000ffffffff02a086010000000000225120379bc88cc15d3605ed9de1b61ff8d65021b650db1151e751a30885ccfcc7d15affa80a000000000016001480a06f2e6b77e817fd5de6e41ea512c563c26cb800000000000100ea02000000000101a158d806735bb7c54e4c701d4f5821cd5342d48d5e1fcbed1169e6e45aa444be0100000000ffffffff02a086010000000000225120379bc88cc15d3605ed9de1b61ff8d65021b650db1151e751a30885ccfcc7d15a6a310c000000000016001478a5d98c7160484b9b00f1782803c58edfc49b9a024730440220407d9162f52371df246dcfa2943d40fbdcb0d4b6768f7682c65193378b2845a60220101c7bc460c93d2976961ac23400f0f10c145efb989a3addb7f03ebaaa2200950121037e17444c85c8b7da07f12fd53cb2ca142c2b4932d0f898649c4b5be0021da0980000000001030401000000220602e57146e5b4762a7ff374adf4072047b67ef115ad46a34189bdeb6a4f88db9b0818000000005400008000000080000000800100000006000000000022020379abbe44004ff7e527bdee3dd8d95e5cd250053f35ee92258b97aa83dfa93c621800000000540000800000008000000080010000005000000000'; const txRequestResponse = { txRequestId: 'txReq123', state: 'pendingApproval', pendingApprovalId: 'approval123', transactions: [ { unsignedTx: { serializedTxHex: unsignedPsbtHex, }, }, ], }; const txRequestWithSignatureResponse = { txRequestId: 'txReq123', state: 'pendingApproval', pendingApprovalId: 'approval123', transactions: [ { unsignedTx: { serializedTxHex: unsignedPsbtHex, coinSpecific: { signature: 'someSignature', }, }, }, ], }; const pendingApprovalData = { id: 'approval123', state: sdk_core_1.State.PENDING, creator: 'user123', info: { type: sdk_core_1.Type.TRANSACTION_REQUEST, }, }; const createTxRequestNock = nock(bgUrl) .post(`/api/v2/wallet/${wallet.wallet.id()}/txrequests`) .reply(200, txRequestResponse); const getPendingApprovalNock = nock(bgUrl) .get(`/api/v2/${coinName}/pendingapprovals/${txRequestResponse.pendingApprovalId}`) .reply(200, pendingApprovalData); const userAuthKeyNock = nock(bgUrl) .get('/api/v2/' + coinName + '/key/def') .reply(200, userAuthKey); const nodeAuthKeyNock = nock(bgUrl) .get('/api/v2/' + coinName + '/key/ghi') .reply(200, nodeAuthKey); const storeSignatureNock = nock(bgUrl) .put(`/api/v2/wallet/${wallet.wallet.id()}/txrequests/${txRequestResponse.txRequestId}/coinSpecific`) .reply(200, txRequestWithSignatureResponse); const response = await wallet.withdrawOnchain(params); assert.strictEqual(response.txRequestId, 'txReq123'); assert.strictEqual(response.txRequestState, 'pendingApproval'); assert.ok(response.pendingApproval); userAuthKeyNock.done(); nodeAuthKeyNock.done(); storeSignatureNock.done(); createTxRequestNock.done(); getPendingApprovalNock.done(); }); }); describe('transactions', function () { let wallet; beforeEach(function () { wallet = (0, abstract_lightning_1.getLightningWallet)(new src_1.Wallet(bitgo, basecoin, { id: 'walletId', coin: 'tlnbtc', subType: 'lightningCustody', coinSpecific: { keys: ['def', 'ghi'] }, })); }); it('should list transactions', async function () { const transaction = { id: 'tx123', normalizedTxHash: 'normalizedHash123', blockHeight: 100000, inputIds: ['input1', 'input2'], entries: [ { inputs: 1, outputs: 2, value: 50000, valueString: '50000', address: 'testAddress', wallet: wallet.wallet.id(), }, ], inputs: [ { id: 'input1', value: 50000, valueString: '50000', address: 'inputAddress', wallet: wallet.wallet.id(), }, ], outputs: [ { id: 'output1', value: 49500, valueString: '49500', address: 'outputAddress', wallet: wallet.wallet.id(), }, ], size: 250, date: new Date('2023-01-01T00:00:00Z'), fee: 500, feeString: '500', hex: 'deadbeef', confirmations: 6, }; const query = { limit: 100n, startDate: new Date(), }; const listTransactionsNock = nock(bgUrl) .get(`/api/v2/wallet/${wallet.wallet.id()}/lightning/transaction`) .query(abstract_lightning_1.TransactionQuery.encode(query)) .reply(200, { transactions: [transaction] }); const listTransactionsResponse = await wallet.listTransactions(query); assert.strictEqual(listTransactionsResponse.transactions.length, 1); assert.deepStrictEqual(listTransactionsResponse.transactions[0], transaction); assert.strictEqual(listTransactionsResponse.nextBatchPrevId, undefined); listTransactionsNock.done(); }); it('should work properly with pagination while listing transactions', async function () { const transaction1 = { id: 'tx123', normalizedTxHash: 'normalizedHash123', blockHeight: 100000, inputIds: ['input1', 'input2'], entries: [ { inputs: 1, outputs: 2, value: 50000, valueString: '50000', address: 'testAddress', wallet: wallet.wallet.id(), }, ], inputs: [ { id: 'input1', value: 50000, valueString: '50000', address: 'inputAddress', wallet: wallet.wallet.id(), }, ], outputs: [ { id: 'output1', value: 49500, valueString: '49500', address: 'outputAddress', wallet: wallet.wallet.id(), }, ], size: 250, date: new Date('2023-01-01T00:00:00Z'), fee: 500, feeString: '500', hex: 'deadbeef', confirmations: 6, }; const transaction2 = { ...transaction1, id: 'tx456', normalizedTxHash: 'normalizedHash456', blockHeight: 100001, date: new Date('2023-01-02T00:00:00Z'), }; const query = { limit: 2n, startDate: new Date('2023-01-01'), }; const listTransactionsNock = nock(bgUrl) .get(`/api/v2/wallet/${wallet.wallet.id()}/lightning/transaction`) .query(abstract_lightning_1.TransactionQuery.encode(query)) .reply(200, { transactions: [transaction1, transaction2], nextBatchPrevId: transaction2.id }); const listTransactionsResponse = await wallet.listTransactions(query); assert.strictEqual(listTransactionsResponse.transactions.length, 2); assert.deepStrictEqual(listTransactionsResponse.transactions[0], transaction1); assert.deepStrictEqual(listTransactionsResponse.transactions[1], transaction2); assert.strictEqual(listTransactionsResponse.nextBatchPrevId, transaction2.id); listTransactionsNock.done();