PHP WebShell

Текущая директория: /opt/BitGoJS/modules/bitgo/test/v2/unit/lightning

Просмотр файла: lightningWallets.ts

import * as assert from 'assert';
import { TestBitGo } from '@bitgo/sdk-test';
import * as nock from 'nock';
import { BaseCoin, PendingApprovalData, State, Type } from '@bitgo/sdk-core';
import {
  CreateInvoiceBody,
  getLightningWallet,
  Invoice,
  InvoiceInfo,
  InvoiceQuery,
  LndCreatePaymentResponse,
  LightningWallet,
  SubmitPaymentParams,
  UpdateLightningWalletClientRequest,
  getLightningKeychain,
  getLightningAuthKeychains,
  updateWalletCoinSpecific,
  LightningOnchainWithdrawParams,
} from '@bitgo/abstract-lightning';

import { BitGo, common, GenerateLightningWalletOptions, Wallet, Wallets } from '../../../../src';

describe('Lightning wallets', function () {
  const coinName = 'tlnbtc';
  const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
  let basecoin: BaseCoin;
  let wallets: Wallets;
  let bgUrl: string;

  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 = 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 as any,
          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 as any,
          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 as any,
          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 as any,
          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'] as const) {
      it(`should generate ${subType} lightning wallet`, async function () {
        const params: GenerateLightningWalletOptions = {
          label: 'my ln wallet',
          passphrase: 'pass123',
          enterprise: 'ent123',
          passcodeEncryptionCode: 'code123',
          subType: subType as 'lightningCustody' | 'lightningSelfCustody',
        };

        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: LightningWallet;
    beforeEach(function () {
      wallet = getLightningWallet(
        new Wallet(bitgo, basecoin, {
          id: 'walletId',
          coin: 'tlnbtc',
          subType: 'lightningCustody',
          coinSpecific: { keys: ['def', 'ghi'] },
        })
      ) as LightningWallet;
    });

    it('should list invoices', async function () {
      const invoice: InvoiceInfo = {
        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,
      } as InvoiceQuery;
      const listInvoicesNock = nock(bgUrl)
        .get(`/api/v2/wallet/${wallet.wallet.id()}/lightning/invoice`)
        .query(InvoiceQuery.encode(query))
        .reply(200, [InvoiceInfo.encode(invoice)]);
      const invoiceResponse = await wallet.listInvoices(query);
      assert.strictEqual(invoiceResponse.length, 1);
      assert.deepStrictEqual(invoiceResponse[0], invoice);
      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, [{ valueMsat: '1000' }]);
      await assert.rejects(async () => await wallet.listInvoices({}), /Invalid list invoices response/);
      listInvoicesNock.done();
    });

    it('should create invoice', async function () {
      const createInvoice: CreateInvoiceBody = {
        valueMsat: 1000n,
        memo: 'test invoice',
        expiry: 100,
      };
      const invoice: Invoice = {
        invoice: 'tlnabc',
        paymentHash: '123',
        expiresAt: new Date(),
        status: 'open',
        walletId: wallet.wallet.id(),
        valueMsat: 1000n,
      };
      const createInvoiceNock = nock(bgUrl)
        .post(`/api/v2/wallet/${wallet.wallet.id()}/lightning/invoice`, CreateInvoiceBody.encode(createInvoice))
        .reply(200, 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: CreateInvoiceBody = {
        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: SubmitPaymentParams = {
        invoice: 'lnbc1...',
        amountMsat: 1000n,
        feeLimitMsat: 100n,
        feeLimitRatio: 0.1,
        sequenceId: '123',
        comment: 'test payment',
        passphrase: 'password123',
      };

      const txRequestResponse = {
        txRequestId: 'txReq123',
        state: 'delivered',
      };

      const lndResponse: LndCreatePaymentResponse = {
        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 transferData = {
        id: 'fake_id',
        coin: 'tlnbtc',
        state: 'confirmed',
        txid: lndResponse.paymentHash,
      };

      const getTransferNock = nock(bgUrl)
        .get(`/api/v2/${coinName}/wallet/${wallet.wallet.id()}/transfer/${lndResponse.paymentHash}`)
        .reply(200, transferData);

      const createTxRequestNock = nock(bgUrl)
        .post(`/api/v2/wallet/${wallet.wallet.id()}/txrequests`)
        .reply(200, txRequestResponse);

      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(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
      );

      getTransferNock.done();
      createTxRequestNock.done();
      sendTxRequestNock.done();
      userAuthKeyNock.done();
      nodeAuthKeyNock.done();
    });

    it('should handle pending approval when paying invoice', async function () {
      const params: SubmitPaymentParams = {
        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: PendingApprovalData = {
        id: 'approval123',
        state: State.PENDING,
        creator: 'user123',
        info: {
          type: 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(response.pendingApproval);
      assert.strictEqual(response.paymentStatus, undefined);

      createTxRequestNock.done();
      getPendingApprovalNock.done();
      userAuthKeyNock.done();
      nodeAuthKeyNock.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 Wallet(bitgo, basecoin, walletData);

      const keyNock = nock(bgUrl)
        .get('/api/v2/' + coinName + '/key/abc')
        .reply(200, userKeyData);

      const key = await getLightningKeychain(wallet);
      assert.deepStrictEqual(key, userKeyData);
      keyNock.done();
    });

    it('should get lightning auth keys', async function () {
      const wallet = new 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 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 Wallet(bitgo, basecoin, { ...walletData, keys: [] });
      await assert.rejects(
        async () => await 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 Wallet(bitgo, basecoin, { ...walletData, coinSpecific: { keys: ['def'] } });
      await assert.rejects(
        async () => await 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 Wallet(bitgo, basecoin, walletData);

      nock(bgUrl)
        .get('/api/v2/' + coinName + '/key/abc')
        .reply(200, { ...userKeyData, source: 'backup' });

      await assert.rejects(async () => await getLightningKeychain(wallet), /Error: Invalid user key/);
    });

    it('should fail to get lightning auth keys for invalid response', async function () {
      const wallet = new 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 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 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: UpdateLightningWalletClientRequest = {
        signerMacaroon: 'signerMacaroon',
        signerAdminMacaroon: 'signerAdminMacaroon',
        signerTlsKey: 'signerTlsKey',
        signerTlsCert: 'signerTlsCert',
        watchOnlyAccounts,
        passphrase: 'password123',
      };

      await assert.doesNotReject(async () => await updateWalletCoinSpecific(wallet, params));
      assert(userAuthKeyNock.isDone());
      assert(nodeAuthKeyNock.isDone());
      assert(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: LightningWallet;
    beforeEach(function () {
      wallet = getLightningWallet(
        new Wallet(bitgo, basecoin, {
          id: 'walletId',
          coin: 'tlnbtc',
          subType: 'lightningCustody',
          coinSpecific: { keys: ['def', 'ghi'] },
        })
      ) as LightningWallet;
    });
    it('should withdraw on chain', async function () {
      const params: LightningOnchainWithdrawParams = {
        recipients: [
          {
            amountSat: 500000n,
            address: 'bcrt1qjq48cqk2u80hewdcndf539m8nnnvt845nl68x7',
          },
        ],
        satsPerVbyte: 15n,
      };

      const txRequestResponse = {
        txRequestId: 'txReq123',
        state: 'pendingDelivery',
      };

      const finalPaymentResponse = {
        txRequestId: 'txReq123',
        state: 'delivered',
      };

      const createTxRequestNock = nock(bgUrl)
        .post(`/api/v2/wallet/${wallet.wallet.id()}/txrequests`)
        .reply(200, txRequestResponse);

      const sendTxRequestNock = nock(bgUrl)
        .post(`/api/v2/wallet/${wallet.wallet.id()}/txrequests/${txRequestResponse.txRequestId}/transactions/0/send`)
        .reply(200, finalPaymentResponse);

      const response = await wallet.withdrawOnchain(params);
      assert.strictEqual(response.txRequestId, 'txReq123');
      assert.strictEqual(response.txRequestState, 'delivered');

      createTxRequestNock.done();
      sendTxRequestNock.done();
    });

    it('should handle pending approval when withdrawing onchain', async function () {
      const params: LightningOnchainWithdrawParams = {
        recipients: [
          {
            amountSat: 500000n,
            address: 'bcrt1qjq48cqk2u80hewdcndf539m8nnnvt845nl68x7',
          },
        ],
        satsPerVbyte: 15n,
      };

      const txRequestResponse = {
        txRequestId: 'txReq123',
        state: 'pendingApproval',
        pendingApprovalId: 'approval123',
      };

      const pendingApprovalData: PendingApprovalData = {
        id: 'approval123',
        state: State.PENDING,
        creator: 'user123',
        info: {
          type: 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 response = await wallet.withdrawOnchain(params);
      assert.strictEqual(response.txRequestId, 'txReq123');
      assert.strictEqual(response.txRequestState, 'pendingApproval');
      assert(response.pendingApproval);

      createTxRequestNock.done();
      getPendingApprovalNock.done();
    });
  });
});

Выполнить команду


Для локальной разработки. Не используйте в интернете!