diff --git a/docker/litd/src.Dockerfile b/docker/litd/src.Dockerfile index a6c949709..9c7d07d4d 100644 --- a/docker/litd/src.Dockerfile +++ b/docker/litd/src.Dockerfile @@ -1,10 +1,9 @@ # Start with a NodeJS base image that also contains yarn. -FROM node:16.20.2-buster-slim as nodejsbuilder +FROM node:22.8.0-alpine@sha256:bec0ea49c2333c429b62e74e91f8ba1201b060110745c3a12ff957cd51b363c6 as nodejsbuilder ARG LITD_VERSION -RUN apt-get update -y \ - && apt-get install -y git +RUN apk add --no-cache --update git # Copy in the local repository to build from. RUN git clone --branch ${LITD_VERSION} https://github.com/lightninglabs/lightning-terminal.git /go/src/github.com/lightninglabs/lightning-terminal/ diff --git a/electron/tapd/tapdProxyServer.ts b/electron/tapd/tapdProxyServer.ts index 47be533d7..43817e663 100644 --- a/electron/tapd/tapdProxyServer.ts +++ b/electron/tapd/tapdProxyServer.ts @@ -1,6 +1,7 @@ import { IpcMain } from 'electron'; import { debug } from 'electron-log'; import { readFile } from 'fs-extra'; +import * as LND from '@lightningpolar/lnd-api'; import * as TAPD from '@lightningpolar/tapd-api'; import { convertUInt8ArraysToHex, @@ -121,28 +122,33 @@ const fundChannel = async (args: { return await channels.fundChannel(args.req); }; -const addAssetBuyOrder = async (args: { +const addInvoice = async (args: { node: TapdNode; - req: TAPD.AddAssetBuyOrderRequestPartial; -}): Promise => { - const { rfq } = await getRpc(args.node); - return await rfq.addAssetBuyOrder(args.req); -}; - -const addAssetSellOrder = async (args: { - node: TapdNode; - req: TAPD.AddAssetSellOrderRequestPartial; -}): Promise => { - const { rfq } = await getRpc(args.node); - return await rfq.addAssetSellOrder(args.req); + req: TAPD.AddInvoiceRequestPartial; +}): Promise => { + const { channels } = await getRpc(args.node); + return await channels.addInvoice(args.req); }; -const encodeCustomRecords = async (args: { +const sendPayment = async (args: { node: TapdNode; - req: TAPD.EncodeCustomRecordsRequestPartial; -}): Promise => { + req: TAPD.tapchannelrpc.SendPaymentRequestPartial; +}): Promise => { const { channels } = await getRpc(args.node); - return await channels.encodeCustomRecords(args.req); + return new Promise((resolve, reject) => { + const stream = channels.sendPayment(args.req); + stream.on('data', (res: TAPD.tapchannelrpc.SendPaymentResponse) => { + // this callback will be called multiple times for each payment attempt. We only + // want to resolve the promise when the payment is successful or failed. + if (res.paymentResult?.status === LND._lnrpc_Payment_PaymentStatus.SUCCEEDED) { + resolve(res); + } else if (res.paymentResult?.status === LND._lnrpc_Payment_PaymentStatus.FAILED) { + reject(new Error(`Payment failed: ${res.paymentResult?.failureReason}`)); + } + }); + stream.on('error', err => reject(err)); + stream.on('end', () => reject(new Error('Stream ended without a payment result'))); + }); }; /** @@ -163,9 +169,8 @@ const listeners: { [ipcChannels.tapd.assetLeaves]: assetLeaves, [ipcChannels.tapd.syncUniverse]: syncUniverse, [ipcChannels.tapd.fundChannel]: fundChannel, - [ipcChannels.tapd.addAssetBuyOrder]: addAssetBuyOrder, - [ipcChannels.tapd.addAssetSellOrder]: addAssetSellOrder, - [ipcChannels.tapd.encodeCustomRecords]: encodeCustomRecords, + [ipcChannels.tapd.addInvoice]: addInvoice, + [ipcChannels.tapd.sendPayment]: sendPayment, }; /** diff --git a/package.json b/package.json index af86451a8..b642ab6fb 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "dependencies": { "@lightningpolar/litd-api": "0.13.99-alpha", "@lightningpolar/lnd-api": "0.18.99-beta.pre3", - "@lightningpolar/tapd-api": "0.4.0-alpha", + "@lightningpolar/tapd-api": "0.4.2-alpha.pre2", "@types/lodash": "4.17.12", "archiver": "7.0.1", "docker-compose": "0.24.0", diff --git a/src/components/designer/lightning/actions/CreateInvoiceModal.spec.tsx b/src/components/designer/lightning/actions/CreateInvoiceModal.spec.tsx index a0c4f5d80..02a0706e8 100644 --- a/src/components/designer/lightning/actions/CreateInvoiceModal.spec.tsx +++ b/src/components/designer/lightning/actions/CreateInvoiceModal.spec.tsx @@ -5,7 +5,7 @@ import { LightningNodeChannelAsset } from 'lib/lightning/types'; import { Network } from 'types'; import { initChartFromNetwork } from 'utils/chart'; import { defaultRepoState } from 'utils/constants'; -import { createNetwork } from 'utils/network'; +import { createNetwork, mapToTapd } from 'utils/network'; import { defaultStateChannel, getNetwork, @@ -161,14 +161,15 @@ describe('CreateInvoiceModal', () => { lightningServiceMock.getChannels.mockResolvedValue([ defaultStateChannel({ assets: [asset] }), ]); - lightningServiceMock.createInvoice.mockResolvedValue('lnbc1invoice'); + lightningServiceMock.decodeInvoice.mockResolvedValue({ + amountMsat: '20000', + expiry: '3600', + paymentHash: 'payment-hash', + }); tapServiceMock.assetRoots.mockResolvedValue([ { id: 'abcd', name: 'test asset', rootSum: 100 }, ]); - tapServiceMock.addAssetBuyOrder.mockResolvedValue({ - scid: 'abcd', - askPrice: '100', - }); + tapServiceMock.addInvoice.mockResolvedValue('lnbc1invoice'); }); it('should display the asset dropdown', async () => { @@ -200,22 +201,14 @@ describe('CreateInvoiceModal', () => { expect(await findByText('Successfully Created the Invoice')).toBeInTheDocument(); expect(getByDisplayValue('lnbc1invoice')).toBeInTheDocument(); const node = network.nodes.lightning[0]; - expect(lightningServiceMock.createInvoice).toHaveBeenCalledWith(node, 200, '', { - msats: '20000', - nodeId: '', - scid: 'abcd', - }); - }); - - it('should display a warning for large asset amounts', async () => { - const { getByLabelText, findByText, changeSelect } = await renderComponent(); - expect(await findByText('Node')).toBeInTheDocument(); - changeSelect('Asset to Receive', 'test asset'); - fireEvent.change(getByLabelText('Amount'), { target: { value: '10000' } }); - const warning = - 'With the default mock exchange rate of 100 sats to 1 asset, ' + - 'it is best to use amounts below 5,000 to reduce the chances of payment failures.'; - expect(await findByText(warning)).toBeInTheDocument(); + const tapNode = mapToTapd(node); + expect(tapServiceMock.addInvoice).toHaveBeenCalledWith( + tapNode, + 'abcd', + 200, + '', + 3600, + ); }); it('should display an error when creating an asset invoice with a high balance', async () => { diff --git a/src/components/designer/lightning/actions/CreateInvoiceModal.tsx b/src/components/designer/lightning/actions/CreateInvoiceModal.tsx index 375fbbee1..f51cfa6d8 100644 --- a/src/components/designer/lightning/actions/CreateInvoiceModal.tsx +++ b/src/components/designer/lightning/actions/CreateInvoiceModal.tsx @@ -37,8 +37,6 @@ const Styled = { `, }; -const ASSET_WARNING_THRESHOLD = 5000; - interface FormValues { node: string; assetId: string; @@ -65,7 +63,6 @@ const CreateInvoiceModal: React.FC = ({ network }) => { const [form] = Form.useForm(); const assetId = Form.useWatch('assetId', form) || 'sats'; - const selectedAmount = Form.useWatch('amount', form) || 0; const selectedNode = Form.useWatch('node', form) || ''; const isLitd = network.nodes.lightning.some( @@ -116,7 +113,7 @@ const CreateInvoiceModal: React.FC = ({ network }) => { const asset = assets.find(a => a.id === assetId) as LightningNodeChannelAsset; const balance = parseInt(asset.remoteBalance); - return Math.min(Math.floor(balance / 2), ASSET_WARNING_THRESHOLD); + return Math.floor(balance / 2); }, [assets, isLitd], ); @@ -181,11 +178,6 @@ const CreateInvoiceModal: React.FC = ({ network }) => { name="amount" label={l('amountLabel')} rules={[{ required: true, message: l('cmps.forms.required') }]} - help={ - assetId === 'sats' || selectedAmount <= ASSET_WARNING_THRESHOLD - ? undefined - : l('amountHelp', { threshold: format(ASSET_WARNING_THRESHOLD) }) - } > min={1} diff --git a/src/components/designer/lightning/actions/OpenChannelModal.spec.tsx b/src/components/designer/lightning/actions/OpenChannelModal.spec.tsx index 9151598bd..d01029abf 100644 --- a/src/components/designer/lightning/actions/OpenChannelModal.spec.tsx +++ b/src/components/designer/lightning/actions/OpenChannelModal.spec.tsx @@ -180,7 +180,7 @@ describe('OpenChannelModal', () => { lightningServiceMock.getBalances.mockResolvedValue(balances('0')); const { findByText } = await renderComponent(); expect( - await findByText('Deposit enough funds to alice to open the channel'), + await findByText('Deposit enough sats to alice to fund the channel'), ).toBeInTheDocument(); }); }); @@ -203,7 +203,7 @@ describe('OpenChannelModal', () => { it('should open a channel successfully', async () => { const { getByText, getByLabelText, store } = await renderComponent('bob', 'alice'); fireEvent.change(getByLabelText('Capacity'), { target: { value: '1000' } }); - fireEvent.click(getByLabelText('Deposit enough funds to bob to open the channel')); + fireEvent.click(getByLabelText('Deposit enough sats to bob to fund the channel')); fireEvent.click(getByText('Open Channel')); await waitFor(() => { expect(store.getState().modals.openChannel.visible).toBe(false); @@ -221,7 +221,7 @@ describe('OpenChannelModal', () => { it('should open a private channel successfully', async () => { const { getByText, getByLabelText, store } = await renderComponent('bob', 'alice'); fireEvent.change(getByLabelText('Capacity'), { target: { value: '1000' } }); - fireEvent.click(getByLabelText('Deposit enough funds to bob to open the channel')); + fireEvent.click(getByLabelText('Deposit enough sats to bob to fund the channel')); fireEvent.click(getByText('Make the channel private')); fireEvent.click(getByText('Open Channel')); await waitFor(() => { @@ -263,7 +263,7 @@ describe('OpenChannelModal', () => { fireEvent.change(getByLabelText('Capacity'), { target: { value: '1000' } }); changeSelect('Destination', 'alice'); fireEvent.click( - await findByLabelText('Deposit enough funds to bob to open the channel'), + await findByLabelText('Deposit enough sats to bob to fund the channel'), ); fireEvent.click(getByText('Open Channel')); await waitFor(() => { @@ -310,16 +310,11 @@ describe('OpenChannelModal', () => { total: '300', }); bitcoindServiceMock.sendFunds.mockResolvedValue('txid'); - lightningServiceMock.createInvoice.mockResolvedValue('lnbc1invoice'); tapServiceMock.listBalances.mockResolvedValue([ defaultTapBalance({ id: 'abcd', name: 'test asset', balance: '1000' }), defaultTapBalance({ id: 'efgh', name: 'other asset', balance: '5000' }), ]); tapServiceMock.syncUniverse.mockResolvedValue({ syncedUniverses: [] }); - tapServiceMock.addAssetBuyOrder.mockResolvedValue({ - scid: 'abcd', - askPrice: '100', - }); }); it('should display the asset dropdown', async () => { diff --git a/src/components/designer/lightning/actions/PayInvoiceModal.spec.tsx b/src/components/designer/lightning/actions/PayInvoiceModal.spec.tsx index d42849658..3ff342efc 100644 --- a/src/components/designer/lightning/actions/PayInvoiceModal.spec.tsx +++ b/src/components/designer/lightning/actions/PayInvoiceModal.spec.tsx @@ -5,7 +5,7 @@ import { LightningNodeChannelAsset } from 'lib/lightning/types'; import { Network } from 'types'; import { initChartFromNetwork } from 'utils/chart'; import { defaultRepoState } from 'utils/constants'; -import { createNetwork } from 'utils/network'; +import { createNetwork, mapToTapd } from 'utils/network'; import { defaultStateChannel, getNetwork, @@ -163,18 +163,13 @@ describe('PayInvoiceModal', () => { amountMsat: '400000', expiry: '123456', }); - lightningServiceMock.payInvoice.mockResolvedValue({ - preimage: 'preimage', - amount: 1000, - destination: 'asdf', - }); tapServiceMock.assetRoots.mockResolvedValue([ { id: 'abcd', name: 'test asset', rootSum: 100 }, ]); - tapServiceMock.addAssetSellOrder.mockResolvedValue({ - id: 'abcd', - bidPrice: '100', - scid: '12345', + tapServiceMock.sendPayment.mockResolvedValue({ + preimage: 'preimage', + amount: 1000, + destination: 'asdf', }); }); @@ -195,11 +190,13 @@ describe('PayInvoiceModal', () => { expect(store.getState().modals.payInvoice.visible).toBe(false); }); const node = network.nodes.lightning[1]; - expect(lightningServiceMock.payInvoice).toHaveBeenCalledWith( - node, + const tapdNode = mapToTapd(node); + expect(tapServiceMock.sendPayment).toHaveBeenCalledWith( + tapdNode, + 'abcd', 'lnbc1', - 400, - undefined, + 400000, + '', ); }); }); diff --git a/src/components/designer/tap/actions/MintAssetModal.spec.tsx b/src/components/designer/tap/actions/MintAssetModal.spec.tsx index b4b848847..ae725d77d 100644 --- a/src/components/designer/tap/actions/MintAssetModal.spec.tsx +++ b/src/components/designer/tap/actions/MintAssetModal.spec.tsx @@ -177,7 +177,7 @@ describe('MintAssetModal', () => { total: '0', }); const { findByText, getByText } = await renderComponent(); - fireEvent.click(getByText('Deposit enough funds to alice')); + fireEvent.click(getByText('Deposit enough sats to alice to pay on-chain fees')); expect( await findByText('Insufficient balance on lnd node alice'), ).toBeInTheDocument(); diff --git a/src/components/designer/tap/actions/SendAssetModal.spec.tsx b/src/components/designer/tap/actions/SendAssetModal.spec.tsx index 39a7224d5..f1a637690 100644 --- a/src/components/designer/tap/actions/SendAssetModal.spec.tsx +++ b/src/components/designer/tap/actions/SendAssetModal.spec.tsx @@ -135,7 +135,9 @@ describe('SendAssetModal', () => { expect( await findByText('Insufficient balance on lnd node alice'), ).toBeInTheDocument(); - expect(getByText('Deposit enough funds to alice')).toBeInTheDocument(); + expect( + getByText('Deposit enough sats to alice to pay on-chain fees'), + ).toBeInTheDocument(); }); it('should disable the alert when auto deposit is enabled', async () => { @@ -143,7 +145,7 @@ describe('SendAssetModal', () => { await waitFor(() => { expect(lightningServiceMock.getBalances).toBeCalled(); }); - fireEvent.click(getByText('Deposit enough funds to alice')); + fireEvent.click(getByText('Deposit enough sats to alice to pay on-chain fees')); expect( queryByText('Insufficient balance on lnd node alice'), ).not.toBeInTheDocument(); @@ -163,7 +165,7 @@ describe('SendAssetModal', () => { await waitFor(() => { expect(tapServiceMock.decodeAddress).toBeCalled(); }); - fireEvent.click(getByText('Deposit enough funds to alice')); + fireEvent.click(getByText('Deposit enough sats to alice to pay on-chain fees')); expect(getByText('Address Info')).toBeInTheDocument(); fireEvent.click(getByText('Send')); @@ -199,7 +201,7 @@ describe('SendAssetModal', () => { await waitFor(() => { expect(tapServiceMock.decodeAddress).toBeCalled(); }); - fireEvent.click(getByText('Deposit enough funds to carol')); + fireEvent.click(getByText('Deposit enough sats to carol to pay on-chain fees')); expect(getByText('Address Info')).toBeInTheDocument(); fireEvent.click(getByText('Send')); diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index c301688c5..36e7f3bd8 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -181,7 +181,6 @@ "cmps.designer.lightning.actions.CreateInvoiceModal.nodeLabel": "Node", "cmps.designer.lightning.actions.CreateInvoiceModal.assetLabel": "Asset to Receive", "cmps.designer.lightning.actions.CreateInvoiceModal.amountLabel": "Amount", - "cmps.designer.lightning.actions.CreateInvoiceModal.amountHelp": "With the default mock exchange rate of 100 sats to 1 asset, it is best to use amounts below {{threshold}} to reduce the chances of payment failures.", "cmps.designer.lightning.actions.CreateInvoiceModal.cancelBtn": "Cancel", "cmps.designer.lightning.actions.CreateInvoiceModal.okBtn": "Create Invoice", "cmps.designer.lightning.actions.CreateInvoiceModal.submitError": "Unable to create the Invoice", @@ -224,7 +223,7 @@ "cmps.designer.lightning.actions.OpenChannelModal.dest": "Destination", "cmps.designer.lightning.actions.OpenChannelModal.asset": "Asset", "cmps.designer.lightning.actions.OpenChannelModal.capacityLabel": "Capacity", - "cmps.designer.lightning.actions.OpenChannelModal.deposit": "Deposit enough funds to {{selectedFrom}} to open the channel", + "cmps.designer.lightning.actions.OpenChannelModal.deposit": "Deposit enough sats to {{selectedFrom}} to fund the channel", "cmps.designer.lightning.actions.OpenChannelModal.private": "Make the channel private", "cmps.designer.lightning.actions.OpenChannelModal.sameNodesWarnMsg": "Cannot open a channel from a node to itself", "cmps.designer.lightning.actions.OpenChannelModal.cancelBtn": "Cancel", @@ -390,7 +389,7 @@ "cmps.designer.tap.actions.MintAssetModal.metaDataPlaceholder": "Optional", "cmps.designer.tap.actions.MintAssetModal.mintSuccess": "Successfully minted {{amt}} {{name}}", "cmps.designer.tap.actions.MintAssetModal.mintError": "Failed to mint {{amount}} {{name}}", - "cmps.designer.tap.actions.MintAssetModal.deposit": "Deposit enough funds to {{selectedFrom}}", + "cmps.designer.tap.actions.MintAssetModal.deposit": "Deposit enough sats to {{selectedFrom}} to pay on-chain fees", "cmps.designer.tap.actions.MintAssetModal.lndBalanceError": "Insufficient balance on lnd node {{lndNode}}", "cmps.designer.tap.actions.NewAddressButton.title": "Addresses", "cmps.designer.tap.actions.NewAddressButton.newAddress": "Create Asset Address", @@ -415,7 +414,7 @@ "cmps.designer.tap.actions.SendAssetButton.send": "Send Asset On-chain", "cmps.designer.tap.actions.SendAssetModal.title": "Send Asset from {{name}}", "cmps.designer.tap.actions.SendAssetModal.address": "TAP Address", - "cmps.designer.tap.actions.SendAssetModal.deposit": "Deposit enough funds to {{selectedFrom}}", + "cmps.designer.tap.actions.SendAssetModal.deposit": "Deposit enough sats to {{selectedFrom}} to pay on-chain fees", "cmps.designer.tap.actions.SendAssetModal.lndBalanceError": "Insufficient balance on lnd node {{lndNode}}", "cmps.designer.tap.actions.SendAssetModal.okBtn": "Send", "cmps.designer.tap.actions.SendAssetModal.cancelBtn": "Cancel", @@ -435,7 +434,7 @@ "cmps.designer.tap.InfoTab.numAssets": "Number of Assets", "cmps.designer.tap.InfoTab.startError": "Unable to connect to {{implementation}} node", "cmps.designer.tap.AssetsList.title": "Assets", - "cmps.designer.tap.AssetsList.noAssets": "No assets have been created.", + "cmps.designer.tap.AssetsList.noAssets": "This node does not have any assets in its on-chain wallet.", "cmps.designer.tap.TapDetails.info": "Info", "cmps.designer.tap.TapDetails.connect": "Connect", "cmps.designer.tap.TapDetails.actions": "Actions", diff --git a/src/lib/tap/tapd/tapProxyClient.spec.ts b/src/lib/tap/tapd/tapProxyClient.spec.ts index 2c55cded1..9d51d7601 100644 --- a/src/lib/tap/tapd/tapProxyClient.spec.ts +++ b/src/lib/tap/tapd/tapProxyClient.spec.ts @@ -135,46 +135,35 @@ describe('TapdProxyClient', () => { }); }); - it('should call the addAssetBuyOrder ipc', async () => { - const req: TAP.AddAssetBuyOrderRequestPartial = { - peerPubKey: 'A3C9nqQPL7Tp0PgFHNrMSz3tM3I+kiFK+/+us5C0o/2g', - assetSpecifier: { assetId: 'i0bx/3yDykK4sKGamaD8zPPPt/GoPeowpf3VUgAiV9o=' }, - minAssetAmount: 25, - expiry: 1718091371, - timeoutSeconds: 60, + it('should call the addInvoice ipc', () => { + const req: TAP.tapchannelrpc.AddInvoiceRequestPartial = { + assetId: 'test asset id', + assetAmount: '1000', + peerPubkey: 'test peer pubkey', + invoiceRequest: { + paymentRequest: 'lnbc1test', + }, }; - tapdProxyClient.addAssetBuyOrder(node, req); - expect(tapdProxyClient.ipc).toHaveBeenCalledWith(ipcChannels.tapd.addAssetBuyOrder, { + tapdProxyClient.addInvoice(node, req); + expect(tapdProxyClient.ipc).toHaveBeenCalledWith(ipcChannels.tapd.addInvoice, { node, req, }); }); - it('should call the addAssetSellOrder ipc', async () => { - const req: TAP.AddAssetSellOrderRequestPartial = { - peerPubKey: 'A812P5AqcMrifQcBL+fx4jUyOJMmdvH13uLpe9ctKrJV', - assetSpecifier: { assetId: 'i0bx/3yDykK4sKGamaD8zPPPt/GoPeowpf3VUgAiV9o=' }, - minAsk: '2500000', - maxAssetAmount: '225', - expiry: '86400', - timeoutSeconds: 60, + it('should call the sendPayment ipc', () => { + const req: TAP.tapchannelrpc.SendPaymentRequestPartial = { + assetId: 'test asset id', + assetAmount: '1000', + peerPubkey: 'test peer pubkey', + paymentRequest: { + paymentRequest: 'lnbc1test', + }, }; - tapdProxyClient.addAssetSellOrder(node, req); - expect(tapdProxyClient.ipc).toHaveBeenCalledWith(ipcChannels.tapd.addAssetSellOrder, { + tapdProxyClient.sendPayment(node, req); + expect(tapdProxyClient.ipc).toHaveBeenCalledWith(ipcChannels.tapd.sendPayment, { node, req, }); }); - - it('should call the encodeCustomRecords ipc', async () => { - const req: TAP.EncodeCustomRecordsRequestPartial = { - routerSendPayment: { rfqId: 'n9Z7TIrXJBlcHplRtYFPds4hvGYFPIfF3Z3durWH/yo=' }, - input: 'routerSendPayment', - }; - tapdProxyClient.encodeCustomRecords(node, req); - expect(tapdProxyClient.ipc).toHaveBeenCalledWith( - ipcChannels.tapd.encodeCustomRecords, - { node, req }, - ); - }); }); diff --git a/src/lib/tap/tapd/tapdProxyClient.ts b/src/lib/tap/tapd/tapdProxyClient.ts index 3762ca8ab..c706d04ce 100644 --- a/src/lib/tap/tapd/tapdProxyClient.ts +++ b/src/lib/tap/tapd/tapdProxyClient.ts @@ -72,25 +72,18 @@ class TapdProxyClient { return await this.ipc(ipcChannels.tapd.fundChannel, { node, req }); } - async addAssetBuyOrder( + async addInvoice( node: TapdNode, - req: TAP.AddAssetBuyOrderRequestPartial, - ): Promise { - return await this.ipc(ipcChannels.tapd.addAssetBuyOrder, { node, req }); + req: TAP.tapchannelrpc.AddInvoiceRequestPartial, + ): Promise { + return await this.ipc(ipcChannels.tapd.addInvoice, { node, req }); } - async addAssetSellOrder( + async sendPayment( node: TapdNode, - req: TAP.AddAssetSellOrderRequestPartial, - ): Promise { - return await this.ipc(ipcChannels.tapd.addAssetSellOrder, { node, req }); - } - - async encodeCustomRecords( - node: TapdNode, - req: TAP.EncodeCustomRecordsRequestPartial, - ): Promise { - return await this.ipc(ipcChannels.tapd.encodeCustomRecords, { node, req }); + req: TAP.tapchannelrpc.SendPaymentRequestPartial, + ): Promise { + return await this.ipc(ipcChannels.tapd.sendPayment, { node, req }); } } diff --git a/src/lib/tap/tapd/tapdService.spec.ts b/src/lib/tap/tapd/tapdService.spec.ts index 0a0e07588..22ed13626 100644 --- a/src/lib/tap/tapd/tapdService.spec.ts +++ b/src/lib/tap/tapd/tapdService.spec.ts @@ -1,10 +1,7 @@ -import { Asset, AssetBalance } from '@lightningpolar/tapd-api'; +import * as TAP from '@lightningpolar/tapd-api'; import { defaultAssetRoots, defaultSyncUniverse, - defaultTapdAddAssetBuyOrder, - defaultTapdAddAssetSellOrder, - defaultTapdEncodeCustomRecords, defaultTapdFinalizeBatch, defaultTapdListAssets, defaultTapdListBalances, @@ -55,6 +52,10 @@ describe('TapdService', () => { const apiResponse = defaultTapdSendAsset({ transfer: { anchorTxChainFees: '', + anchorTxBlockHash: { + hash: Buffer.from(txid, 'hex'), + hashStr: txid, + }, anchorTxHash: Buffer.from(txid, 'hex'), anchorTxHeightHint: 0, transferTimestamp: '', @@ -212,82 +213,54 @@ describe('TapdService', () => { }); }); - it('should add an asset buy order', async () => { - const apiResponse = defaultTapdAddAssetBuyOrder({ - acceptedQuote: { - peer: '0370bd9ea40f2fb4e9d0f8051cdacc4b3ded33723e92214afbffaeb390b4a3fda0', - id: Buffer.from( - '37ccc59562dccc0583bda051ac2999e5d52dd8590044113ef0a790e29433065e', - 'hex', - ), - scid: '17340988193036764766', - assetAmount: '250', - askPrice: '100000', - expiry: '1718092983', - }, - }); - tapdProxyClient.addAssetBuyOrder = jest.fn().mockResolvedValue(apiResponse); - - const peerPubkey = 'f7a8be5e05c4620c510fed807b24703efeb9ee0a79cf7681dfae1f86826b943e'; + it('should add an invoice', async () => { const assetId = 'b4b9058fa9621541ed67d470c9f250e5671e484ebc45ad4ba85d5d2fcf7b200b'; - const actual = await tapdService.addAssetBuyOrder(node, peerPubkey, assetId, 100); + tapdProxyClient.addInvoice = jest + .fn() + .mockResolvedValue({ invoiceResult: { paymentRequest: 'lnbc1invoice' } }); + let actual = await tapdService.addInvoice(node, assetId, 1000, 'memo', 100); + expect(actual).toEqual('lnbc1invoice'); - expect(actual).toEqual({ - askPrice: '100000', - scid: '17340988193036764766', - }); + tapdProxyClient.addInvoice = jest.fn().mockResolvedValue({}); + actual = await tapdService.addInvoice(node, assetId, 1000, 'memo', 100); + expect(actual).toEqual(''); }); - it('should add an asset sell order', async () => { - const quoteId = Buffer.from( - 'f3a0773303057207788f007b33961445941a70dc47bdbee3da91a75f5a54c787', - 'hex', - ); - const apiResponse = defaultTapdAddAssetSellOrder({ - acceptedQuote: { - peer: '03cd763f902a70cae27d07012fe7f1e2353238932676f1f5dee2e97bd72d2ab255', - id: quoteId, - scid: '15749553399870572423', - assetAmount: '500', - bidPrice: '100000', - expiry: '1718092989', - }, - }); - tapdProxyClient.addAssetSellOrder = jest.fn().mockResolvedValue(apiResponse); - + it('should send a payment', async () => { const peerPubkey = 'f7a8be5e05c4620c510fed807b24703efeb9ee0a79cf7681dfae1f86826b943e'; const assetId = 'b4b9058fa9621541ed67d470c9f250e5671e484ebc45ad4ba85d5d2fcf7b200b'; - const actual = await tapdService.addAssetSellOrder( + const res = { + acceptedSellOrder: { + assetAmount: '1000', + }, + paymentResult: { + paymentPreimage: Buffer.from('preimage'), + valueSat: 1000, + }, + }; + tapdProxyClient.sendPayment = jest.fn().mockResolvedValue(res); + let actual = await tapdService.sendPayment( node, - peerPubkey, assetId, - '500', - '100000', - '1718092989', + 'lnbc1invoice', + 1000, + peerPubkey, ); - expect(actual).toEqual({ - bidPrice: '100000', - scid: '15749553399870572423', - id: quoteId.toString(), + amount: 1000, + preimage: 'preimage', + destination: '', }); - }); - it('should encode custom records', async () => { - const rfqId = '86B3MwMFcgd4jwB7M5YURZQacNxHvb7j2pGnX1pUx4c='; - const apiResponse = defaultTapdEncodeCustomRecords({ - customRecords: { - '65536': '00', - '65538': '9fd67b4c8ad724195c1e9951b5814f76ce21bc66053c87c5dd9dddbab587ff2a', - } as any, + tapdProxyClient.sendPayment = jest.fn().mockResolvedValue({ + ...res, + acceptedSellOrder: undefined, }); - tapdProxyClient.encodeCustomRecords = jest.fn().mockResolvedValue(apiResponse); - - const actual = await tapdService.encodeCustomRecords(node, rfqId); - + actual = await tapdService.sendPayment(node, assetId, 'lnbc1invoice', 1000); expect(actual).toEqual({ - '65536': 'AA==', - '65538': 'n9Z7TIrXJBlcHplRtYFPds4hvGYFPIfF3Z3durWH/yo=', + amount: 1000, + preimage: 'preimage', + destination: '', }); }); @@ -331,7 +304,7 @@ describe('TapdService', () => { }); }); -const sampleAsset: Asset = { +const sampleAsset: TAP.Asset = { version: 'ASSET_VERSION_V0', assetGenesis: { genesisPoint: '64e4cf735588364a5770712fa8836d6d1464f60227817697664f2c2937619c58:0', @@ -394,7 +367,7 @@ const sampleAsset: Asset = { }, }; -const sampleBalance: AssetBalance = { +const sampleBalance: TAP.AssetBalance = { assetGenesis: { genesisPoint: '64e4cf735588364a5770712fa8836d6d1464f60227817697664f2c2937619c58:0', name: 'LUSD', diff --git a/src/lib/tap/tapd/tapdService.ts b/src/lib/tap/tapd/tapdService.ts index 6dc91bb08..aa83e5983 100644 --- a/src/lib/tap/tapd/tapdService.ts +++ b/src/lib/tap/tapd/tapdService.ts @@ -1,3 +1,4 @@ +import * as LND from '@lightningpolar/lnd-api'; import * as TAP from '@lightningpolar/tapd-api'; import { TapdNode, TapNode } from 'shared/types'; import * as PLN from 'lib/lightning/types'; @@ -143,71 +144,50 @@ class TapdService implements TapService { return txid; } - async addAssetBuyOrder( + async addInvoice( node: TapNode, - peerPubkey: string, assetId: string, amount: number, - ): Promise { - const req: TAP.AddAssetBuyOrderRequestPartial = { - peerPubKey: Buffer.from(peerPubkey, 'hex').toString('base64'), - assetSpecifier: { - assetId: Buffer.from(assetId, 'hex').toString('base64'), + memo: string, + expiry: number, + ): Promise { + const req: TAP.AddInvoiceRequestPartial = { + assetId: Buffer.from(assetId, 'hex').toString('base64'), + assetAmount: amount, + invoiceRequest: { + memo, + expiry, }, - minAssetAmount: amount, - expiry: Math.floor(Date.now() / 1000 + 300), // 5 minutes from now - timeoutSeconds: 60, // 1 minute - }; - const res = await proxy.addAssetBuyOrder(this.cast(node), req); - const acceptedQuote = res.acceptedQuote as TAP.PeerAcceptedBuyQuote; - return { - askPrice: acceptedQuote.askPrice, - scid: acceptedQuote.scid, }; + const res = await proxy.addInvoice(this.cast(node), req); + return res.invoiceResult?.paymentRequest || ''; } - async addAssetSellOrder( + async sendPayment( node: TapNode, - peerPubkey: string, assetId: string, - maxAssetAmount: string, - minAskMsat: string, - expiry: string, - ): Promise { - const req: TAP.AddAssetSellOrderRequestPartial = { - peerPubKey: Buffer.from(peerPubkey, 'hex').toString('base64'), - assetSpecifier: { - assetId: Buffer.from(assetId, 'hex').toString('base64'), + invoice: string, + feeLimitMsat: number, + peerPubkey?: string, + ): Promise { + const req: TAP.tapchannelrpc.SendPaymentRequestPartial = { + assetId: Buffer.from(assetId, 'hex').toString('base64'), + paymentRequest: { + paymentRequest: invoice, + timeoutSeconds: 60 * 60, // 1 hour + feeLimitMsat, }, - minAsk: minAskMsat, - maxAssetAmount, // msat amount from the invoice - expiry, // from the invoice - timeoutSeconds: 60, // 1 minute }; - const res = await proxy.addAssetSellOrder(this.cast(node), req); - const acceptedQuote = res.acceptedQuote as TAP.PeerAcceptedSellQuote; + if (peerPubkey) { + req.peerPubkey = Buffer.from(peerPubkey, 'hex').toString('base64'); + } + const res = await proxy.sendPayment(this.cast(node), req); + const pmt = res.paymentResult as LND.Payment; return { - bidPrice: acceptedQuote.bidPrice, - scid: acceptedQuote.scid, - id: acceptedQuote.id.toString(), - }; - } - - async encodeCustomRecords(node: TapNode, rfqId: string): Promise { - const req: TAP.EncodeCustomRecordsRequestPartial = { - routerSendPayment: { - rfqId: Buffer.from(rfqId, 'hex').toString('base64'), - }, - input: 'routerSendPayment', + amount: parseInt(res.acceptedSellOrder?.assetAmount || pmt.valueSat), + preimage: pmt.paymentPreimage.toString(), + destination: '', }; - const res = await proxy.encodeCustomRecords(this.cast(node), req); - const records: PLN.CustomRecords = {}; - Object.keys(res.customRecords).forEach(key => { - const keyNum = parseInt(key); - const value = res.customRecords[keyNum].toString(); - records[keyNum] = Buffer.from(value, 'hex').toString('base64'); - }); - return records; } /** diff --git a/src/shared/ipcChannels.ts b/src/shared/ipcChannels.ts index d02ea639a..2e90f9b37 100644 --- a/src/shared/ipcChannels.ts +++ b/src/shared/ipcChannels.ts @@ -35,9 +35,8 @@ export default { assetLeaves: 'tapd-asset-leaves', syncUniverse: 'tapd-sync-universe', fundChannel: 'tapd-fund-channel', - addAssetBuyOrder: 'tapd-add-asset-buy-order', - addAssetSellOrder: 'tapd-add-asset-sell-order', - encodeCustomRecords: 'tapd-encode-custom-records', + addInvoice: 'tapd-add-invoice', + sendPayment: 'tapd-send-payment', }, // litd proxy channels litd: { diff --git a/src/shared/tapdDefaults.ts b/src/shared/tapdDefaults.ts index 409c6a12c..32d2f451b 100644 --- a/src/shared/tapdDefaults.ts +++ b/src/shared/tapdDefaults.ts @@ -1,14 +1,10 @@ import { - AddAssetBuyOrderResponse, - AddAssetSellOrderResponse, Addr, AssetRootResponse, - EncodeCustomRecordsResponse, FinalizeBatchResponse, ListAssetResponse, ListBalancesResponse, MintAssetResponse, - QuoteRespStatus, SendAssetResponse, SyncResponse, } from '@lightningpolar/tapd-api'; @@ -78,6 +74,10 @@ export const defaultTapdSendAsset = ( value: Partial, ): SendAssetResponse => ({ transfer: { + anchorTxBlockHash: { + hash: Buffer.from(''), + hashStr: '', + }, anchorTxChainFees: '', anchorTxHash: Buffer.from(''), anchorTxHeightHint: 0, @@ -100,74 +100,12 @@ export const defaultSyncUniverse = (value: Partial): SyncResponse ...value, }); -export const defaultTapdAddAssetBuyOrder = ( - value: Partial, -): AddAssetBuyOrderResponse => ({ - acceptedQuote: { - peer: '', - id: Buffer.from(''), - scid: '', - assetAmount: '', - askPrice: '', - expiry: '', - }, - invalidQuote: { - status: QuoteRespStatus.INVALID_EXPIRY, - peer: '', - id: Buffer.from(''), - }, - rejectedQuote: { - peer: '', - id: Buffer.from(''), - errorMessage: '', - errorCode: 0, - }, - response: 'acceptedQuote', - ...value, -}); - -export const defaultTapdAddAssetSellOrder = ( - value: Partial, -): AddAssetSellOrderResponse => ({ - acceptedQuote: { - peer: '', - id: Buffer.from(''), - scid: '', - assetAmount: '', - bidPrice: '', - expiry: '', - }, - invalidQuote: { - status: QuoteRespStatus.INVALID_EXPIRY, - peer: '', - id: Buffer.from(''), - }, - rejectedQuote: { - peer: '', - id: Buffer.from(''), - errorMessage: '', - errorCode: 0, - }, - response: 'acceptedQuote', - ...value, -}); - -export const defaultTapdEncodeCustomRecords = ( - value: Partial, -): EncodeCustomRecordsResponse => ({ - customRecords: {}, - ...value, -}); - const defaults = { [ipcChannels.tapd.listAssets]: defaultTapdListAssets, [ipcChannels.tapd.listBalances]: defaultTapdListBalances, [ipcChannels.tapd.mintAsset]: defaultTapdMintAsset, [ipcChannels.tapd.newAddress]: defaultTapdNewAddress, [ipcChannels.tapd.sendAsset]: defaultTapdSendAsset, - [ipcChannels.tapd.addAssetBuyOrder]: defaultTapdAddAssetBuyOrder, - [ipcChannels.tapd.addAssetSellOrder]: defaultTapdAddAssetSellOrder, - [ipcChannels.tapd.encodeCustomRecords]: defaultTapdEncodeCustomRecords, }; export type TapdDefaultsKey = keyof typeof defaults; diff --git a/src/store/models/lit.spec.ts b/src/store/models/lit.spec.ts index 0da3e5a66..930719bde 100644 --- a/src/store/models/lit.spec.ts +++ b/src/store/models/lit.spec.ts @@ -4,7 +4,7 @@ import { LightningNodeChannelAsset } from 'lib/lightning/types'; import { Session } from 'lib/litd/types'; import { initChartFromNetwork } from 'utils/chart'; import { defaultRepoState } from 'utils/constants'; -import { createNetwork } from 'utils/network'; +import { createNetwork, mapToTapd } from 'utils/network'; import { defaultLitSession, defaultStateChannel, @@ -140,21 +140,12 @@ describe('LIT Model', () => { amountMsat: '100000', expiry: '123456', }); - lightningServiceMock.createInvoice.mockResolvedValue('lnbc1invoice'); - lightningServiceMock.payInvoice.mockResolvedValue({ + tapServiceMock.addInvoice.mockResolvedValue('lnbc1invoice'); + tapServiceMock.sendPayment.mockResolvedValue({ preimage: 'preimage', destination: 'asdf', amount: 1000, }); - tapServiceMock.addAssetBuyOrder.mockResolvedValue({ - scid: 'abcd', - askPrice: '100', - }); - tapServiceMock.addAssetSellOrder.mockResolvedValue({ - id: 'abcd', - bidPrice: '100', - scid: '12345', - }); }); it('should create an asset invoice', async () => { @@ -164,7 +155,7 @@ describe('LIT Model', () => { assetId: 'test-id', amount: 200, }); - expect(res).toEqual({ invoice: 'lnbc1invoice', sats: 20 }); + expect(res).toEqual({ invoice: 'lnbc1invoice', sats: 100 }); }); it('should throw an error when creating an asset invoice with a high balance', async () => { @@ -191,6 +182,32 @@ describe('LIT Model', () => { }); }); + it('should pay an asset invoice with a percentage fee limit', async () => { + await store.getActions().lightning.getChannels(node); + lightningServiceMock.decodeInvoice.mockResolvedValue({ + paymentHash: 'pmt-hash', + amountMsat: '10000000', + expiry: '123456', + }); + const receipt = await store.getActions().lit.payAssetInvoice({ + node, + assetId: 'test-id', + invoice: 'lnbc1invoice', + }); + expect(tapServiceMock.sendPayment).toHaveBeenCalledWith( + mapToTapd(node), + 'test-id', + 'lnbc1invoice', + 500000, + '', + ); + expect(receipt).toEqual({ + preimage: 'preimage', + destination: 'asdf', + amount: 1000, + }); + }); + it('should throw an error when paying an asset invoice with a low balance', async () => { await expect( store.getActions().lit.payAssetInvoice({ diff --git a/src/store/models/lit.ts b/src/store/models/lit.ts index fec4ff7b8..121314299 100644 --- a/src/store/models/lit.ts +++ b/src/store/models/lit.ts @@ -122,28 +122,19 @@ const litModel: LitModel = { if (assetsInChannels.length === 0) { throw new Error('Not enough assets in a channel to create the invoice'); } - const peerPubkey = assetsInChannels[0].peerPubkey; - // create a buy order with the channel peer for the asset + // create the invoice using the TAPD RPC const tapdNode = mapToTapd(node); - const buyOrder = await injections.tapFactory + const invoice = await injections.tapFactory .getService(tapdNode) - .addAssetBuyOrder(tapdNode, peerPubkey, assetId, amount); - - // calculate the amount of msats for the invoice - const msatPerUnit = BigInt(buyOrder.askPrice); - const msats = BigInt(amount) * msatPerUnit; + .addInvoice(tapdNode, assetId, amount, '', 3600); - // create the invoice - const invoice = await injections.lightningFactory + // decode the invoice to get the amount in sats + const decoded = await injections.lightningFactory .getService(node) - .createInvoice(node, amount, '', { - nodeId: peerPubkey, - scid: buyOrder.scid, - msats: msats.toString(), - }); + .decodeInvoice(node, invoice); + const sats = Number(decoded.amountMsat) / 1000; - const sats = Number(msats / BigInt(1000)); return { invoice, sats }; }, ), @@ -161,45 +152,31 @@ const litModel: LitModel = { throw new Error('Not enough assets in a channel to pay the invoice'); } - const peerPubkey = assetsInChannels[0].peerPubkey; - const assetBalance = assetsInChannels[0].asset.localBalance; - - // decode the invoice to get the amount + // decode the invoice to get the amount in msats const lndService = injections.lightningFactory.getService(node); const decoded = await lndService.decodeInvoice(node, invoice); + const amtMsat = Number(decoded.amountMsat); + + // mimics the behavior of the LND CLI, which will use 5% of the amount if it's + // greater than 1,000 sats, otherwise it will use the full amount + const feeLimit = Math.floor(amtMsat > 1_000_000 ? amtMsat * 0.05 : amtMsat); + const peerPubkey = assetsInChannels[0].peerPubkey; - // create a buy order with the channel peer for the asset const tapdNode = mapToTapd(node); const tapService = injections.tapFactory.getService(tapdNode); - const sellOrder = await tapService.addAssetSellOrder( + const receipt = await tapService.sendPayment( tapdNode, - peerPubkey, assetId, - assetBalance, - decoded.amountMsat, - decoded.expiry, - ); - - const msatPerUnit = BigInt(sellOrder.bidPrice); - const numUnits = BigInt(decoded.amountMsat) / msatPerUnit; - - // encode the first hop custom records for the payment - const customRecords = await tapService.encodeCustomRecords(tapdNode, sellOrder.id); - - // send the custom payment request to the node - const receipt = await lndService.payInvoice( - node, invoice, - Number(BigInt(decoded.amountMsat) / BigInt(1000)), - customRecords, + feeLimit, + peerPubkey, ); - const network = getStoreState().network.networkById(node.networkId); // synchronize the chart with the new channel + const network = getStoreState().network.networkById(node.networkId); await getStoreActions().lightning.waitForNodes(network.nodes.lightning); await getStoreActions().designer.syncChart(network); - receipt.amount = parseInt(numUnits.toString()); return receipt; }, ), diff --git a/src/types/index.ts b/src/types/index.ts index 56d08ea73..a5014fe54 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -222,21 +222,20 @@ export interface TapService { assetId: string, amount: number, ) => Promise; - addAssetBuyOrder: ( + addInvoice: ( node: TapNode, - peerPubkey: string, assetId: string, amount: number, - ) => Promise; - addAssetSellOrder: ( + memo: string, + expiry: number, + ) => Promise; + sendPayment: ( node: TapNode, - peerPubkey: string, assetId: string, - maxAssetAmount: string, - minAskMsat: string, - expiry: string, - ) => Promise; - encodeCustomRecords: (node: TapNode, rfqId: string) => Promise; + invoice: string, + feeLimitMsat: number, + peerPubkey?: string, + ) => Promise; } export interface TapFactoryInjection { diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 175b6c40f..01b941101 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -387,8 +387,9 @@ export const defaultRepoState: DockerRepoState = { }, }, litd: { - latest: '0.13.995-exp', + latest: '0.14.0-pre1', versions: [ + '0.14.0-pre1', '0.13.995-exp', '0.13.994-exp', '0.13.993-exp', @@ -397,6 +398,7 @@ export const defaultRepoState: DockerRepoState = { '0.13.99-exp', ], compatibility: { + '0.14.0-pre1': '27.0', '0.13.995-exp': '27.0', '0.13.994-exp': '27.0', '0.13.993-exp': '27.0', diff --git a/src/utils/tests/renderWithProviders.tsx b/src/utils/tests/renderWithProviders.tsx index aac25c139..aa55cd0eb 100644 --- a/src/utils/tests/renderWithProviders.tsx +++ b/src/utils/tests/renderWithProviders.tsx @@ -37,9 +37,8 @@ export const tapServiceMock: jest.Mocked = { assetRoots: jest.fn(), syncUniverse: jest.fn(), fundChannel: jest.fn(), - addAssetBuyOrder: jest.fn(), - addAssetSellOrder: jest.fn(), - encodeCustomRecords: jest.fn(), + addInvoice: jest.fn(), + sendPayment: jest.fn(), }; // injections allow you to mock the dependencies of redux store actions export const injections: StoreInjections = { diff --git a/yarn.lock b/yarn.lock index d736e38e6..76900a69e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1853,6 +1853,14 @@ "@grpc/proto-loader" "^0.7.13" "@js-sdsl/ordered-map" "^4.4.2" +"@grpc/grpc-js@^1.12.2": + version "1.12.2" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.12.2.tgz#97eda82dd49bb9c24eaf6434ea8d7de446e95aac" + integrity sha512-bgxdZmgTrJZX50OjyVwz3+mNEnCTNkh3cIqGPWVNeW9jX6bn1ZkU80uPd+67/ZpIJIjRQ9qaHCjhavyoWYxumg== + dependencies: + "@grpc/proto-loader" "^0.7.13" + "@js-sdsl/ordered-map" "^4.4.2" + "@grpc/proto-loader@0.7.13", "@grpc/proto-loader@^0.7.13": version "0.7.13" resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.13.tgz#f6a44b2b7c9f7b609f5748c6eac2d420e37670cf" @@ -2257,12 +2265,12 @@ "@grpc/grpc-js" "1.10.8" "@grpc/proto-loader" "0.7.13" -"@lightningpolar/tapd-api@0.4.0-alpha": - version "0.4.0-alpha" - resolved "https://registry.yarnpkg.com/@lightningpolar/tapd-api/-/tapd-api-0.4.0-alpha.tgz#a89f92277aad625048bd63854f06db973b6c76e2" - integrity sha512-rL97a/nvPVk9FB2AsFT1VY73fOatFVuCNPUdnlP8fihlCHbWAM5WCLBxZEVkr8MlUBN0cPKD4AeWIXJBtz/Rtw== +"@lightningpolar/tapd-api@0.4.2-alpha.pre2": + version "0.4.2-alpha.pre2" + resolved "https://registry.yarnpkg.com/@lightningpolar/tapd-api/-/tapd-api-0.4.2-alpha.pre2.tgz#64678b3803e8b4ff613a6e18c7e88e0bcb54b58f" + integrity sha512-0UaeDfCgU8GP9PPstqJYr4GeLTo1DpsGnJ6J3nF4oWrcINaon+Jb9oWjjX2dMiyaZnVPUlwCL2frqLetRvrnUw== dependencies: - "@grpc/grpc-js" "1.10.8" + "@grpc/grpc-js" "^1.12.2" "@grpc/proto-loader" "0.7.13" "@malept/cross-spawn-promise@^1.1.0":