diff --git a/src/components/common/RenameNodeModal.spec.tsx b/src/components/common/RenameNodeModal.spec.tsx index c77a128005..5a8f14739b 100644 --- a/src/components/common/RenameNodeModal.spec.tsx +++ b/src/components/common/RenameNodeModal.spec.tsx @@ -6,9 +6,9 @@ import { initChartFromNetwork } from 'utils/chart'; import { defaultRepoState } from 'utils/constants'; import { createNetwork } from 'utils/network'; import { + bitcoinServiceMock, injections, lightningServiceMock, - bitcoinServiceMock, litdServiceMock, renderWithProviders, tapServiceMock, @@ -41,6 +41,7 @@ describe('RenameNodeModal', () => { repoState: defaultRepoState, managedImages: testManagedImages, customImages: [], + manualMineCount: 6, }); const initialState = { network: { diff --git a/src/components/designer/bitcoin/actions/MineBlocksInput.tsx b/src/components/designer/bitcoin/actions/MineBlocksInput.tsx index c8bb21e7f8..5577a99f5f 100644 --- a/src/components/designer/bitcoin/actions/MineBlocksInput.tsx +++ b/src/components/designer/bitcoin/actions/MineBlocksInput.tsx @@ -1,11 +1,11 @@ -import React, { useState } from 'react'; +import React from 'react'; import { useAsyncCallback } from 'react-async-hook'; import { ToolOutlined } from '@ant-design/icons'; import styled from '@emotion/styled'; import { Button, Form, Input, InputNumber } from 'antd'; import { usePrefixedTranslation } from 'hooks'; import { BitcoinNode } from 'shared/types'; -import { useStoreActions } from 'store'; +import { useStoreActions, useStoreState } from 'store'; const InputGroup = Input.Group; @@ -20,24 +20,32 @@ const Styled = { const MineBlocksInput: React.FC<{ node: BitcoinNode }> = ({ node }) => { const { l } = usePrefixedTranslation('cmps.designer.bitcoind.MineBlocksInput'); - const [value, setValue] = useState(6); + const network = useStoreState(s => s.network.networkById(node.networkId)); const { notify } = useStoreActions(s => s.app); const { mine } = useStoreActions(s => s.bitcoin); + const updateManualMineCount = useStoreActions( + actions => actions.network.updateManualMineCount, + ); + const mineAsync = useAsyncCallback(async () => { try { - await mine({ blocks: value, node }); + await mine({ blocks: network.manualMineCount, node }); } catch (error: any) { notify({ message: l('error'), error }); } }); + const handleManualMineCountChange = (count: number) => { + updateManualMineCount({ id: node.networkId, count }); + }; + return ( setValue(parseInt(v as any))} + onChange={v => handleManualMineCountChange(parseInt(v as any))} disabled={mineAsync.loading} /> { repoState: defaultRepoState, managedImages: testManagedImages, customImages: [], + manualMineCount: 6, }); const initialState = { diff --git a/src/components/designer/lightning/actions/CreateInvoiceModal.spec.tsx b/src/components/designer/lightning/actions/CreateInvoiceModal.spec.tsx index c092bedda4..7a1939ee6b 100644 --- a/src/components/designer/lightning/actions/CreateInvoiceModal.spec.tsx +++ b/src/components/designer/lightning/actions/CreateInvoiceModal.spec.tsx @@ -150,6 +150,7 @@ describe('CreateInvoiceModal', () => { repoState: defaultRepoState, managedImages: testManagedImages, customImages: [], + manualMineCount: 6, }); const asset: LightningNodeChannelAsset = { id: 'abcd', diff --git a/src/components/designer/lightning/actions/OpenChannelModal.spec.tsx b/src/components/designer/lightning/actions/OpenChannelModal.spec.tsx index 8e1a4ac755..d76da8723d 100644 --- a/src/components/designer/lightning/actions/OpenChannelModal.spec.tsx +++ b/src/components/designer/lightning/actions/OpenChannelModal.spec.tsx @@ -288,6 +288,7 @@ describe('OpenChannelModal', () => { repoState: defaultRepoState, managedImages: testManagedImages, customImages: [], + manualMineCount: 6, }); const asset: LightningNodeChannelAsset = { id: 'abcd', diff --git a/src/components/designer/lightning/actions/PayInvoiceModal.spec.tsx b/src/components/designer/lightning/actions/PayInvoiceModal.spec.tsx index b9bf825f55..7d3c97c79f 100644 --- a/src/components/designer/lightning/actions/PayInvoiceModal.spec.tsx +++ b/src/components/designer/lightning/actions/PayInvoiceModal.spec.tsx @@ -149,6 +149,7 @@ describe('PayInvoiceModal', () => { repoState: defaultRepoState, managedImages: testManagedImages, customImages: [], + manualMineCount: 6, }); const asset: LightningNodeChannelAsset = { id: 'abcd', diff --git a/src/components/network/ImportNetwork.spec.tsx b/src/components/network/ImportNetwork.spec.tsx index ded5bbb11d..9a7bf054fa 100644 --- a/src/components/network/ImportNetwork.spec.tsx +++ b/src/components/network/ImportNetwork.spec.tsx @@ -72,6 +72,7 @@ describe('ImportNetwork component', () => { repoState: defaultRepoState, managedImages: testManagedImages, customImages: [], + manualMineCount: 6, }); chart = initChartFromNetwork(network); filesMock.read.mockResolvedValue(JSON.stringify({ network, chart })); diff --git a/src/lib/docker/composeFile.spec.ts b/src/lib/docker/composeFile.spec.ts index d9bfb59f46..c9605ae0e9 100644 --- a/src/lib/docker/composeFile.spec.ts +++ b/src/lib/docker/composeFile.spec.ts @@ -19,6 +19,7 @@ describe('ComposeFile', () => { repoState: defaultRepoState, managedImages: testManagedImages, customImages: [], + manualMineCount: 6, }); const btcNode = network.nodes.bitcoin[0]; const lndNode = network.nodes.lightning[0] as LndNode; diff --git a/src/lib/docker/dockerService.spec.ts b/src/lib/docker/dockerService.spec.ts index ace1c4520d..44b7e49ff7 100644 --- a/src/lib/docker/dockerService.spec.ts +++ b/src/lib/docker/dockerService.spec.ts @@ -192,6 +192,7 @@ describe('DockerService', () => { repoState: defaultRepoState, managedImages: testManagedImages, customImages: [], + manualMineCount: 6, }); net.nodes.lightning[0].backendName = 'invalid'; dockerService.saveComposeFile(net); @@ -217,6 +218,7 @@ describe('DockerService', () => { repoState: defaultRepoState, managedImages: testManagedImages, customImages: [], + manualMineCount: 6, }); net.nodes.lightning[0].backendName = 'invalid'; dockerService.saveComposeFile(net); @@ -242,6 +244,7 @@ describe('DockerService', () => { repoState: defaultRepoState, managedImages: testManagedImages, customImages: [], + manualMineCount: 6, }); net.nodes.lightning[0].backendName = 'invalid'; dockerService.saveComposeFile(net); @@ -308,6 +311,7 @@ describe('DockerService', () => { repoState: defaultRepoState, managedImages: testManagedImages, customImages: [], + manualMineCount: 6, }); dockerService.saveComposeFile(net); const { backendName } = net.nodes.lightning[0] as LitdNode; @@ -331,6 +335,7 @@ describe('DockerService', () => { repoState: defaultRepoState, managedImages: testManagedImages, customImages: [], + manualMineCount: 6, }); const litdNode = net.nodes.lightning[0] as LitdNode; litdNode.backendName = 'invalid'; @@ -357,6 +362,7 @@ describe('DockerService', () => { repoState: defaultRepoState, managedImages: testManagedImages, customImages: [], + manualMineCount: 6, }); net.nodes.lightning[0].implementation = 'unknown' as any; dockerService.saveComposeFile(net); @@ -392,6 +398,7 @@ describe('DockerService', () => { repoState: defaultRepoState, managedImages: testManagedImages, customImages: [], + manualMineCount: 6, }); const chart = initChartFromNetwork(net); // return 'any' to suppress "The operand of a 'delete' operator must be optional.ts(2790)" error @@ -522,6 +529,7 @@ describe('DockerService', () => { repoState: defaultRepoState, managedImages: testManagedImages, customImages: [], + manualMineCount: 6, }); const chart = initChartFromNetwork(net); const fileData: NetworksFile = { @@ -716,6 +724,7 @@ describe('DockerService', () => { repoState: defaultRepoState, managedImages: testManagedImages, customImages: [], + manualMineCount: 6, }); composeMock.upAll.mockResolvedValue(mockResult); await dockerService.start(net); diff --git a/src/lib/litd/litdProxyClient.spec.ts b/src/lib/litd/litdProxyClient.spec.ts index 608b2188c4..ef8d619572 100644 --- a/src/lib/litd/litdProxyClient.spec.ts +++ b/src/lib/litd/litdProxyClient.spec.ts @@ -20,6 +20,7 @@ describe('LitdProxyClient', () => { repoState: defaultRepoState, managedImages: testManagedImages, customImages: [], + manualMineCount: 6, }); const node = network.nodes.lightning[0] as LitdNode; let actualIpc: IpcSender; diff --git a/src/lib/litd/litdService.spec.ts b/src/lib/litd/litdService.spec.ts index 3760134547..883a886bb7 100644 --- a/src/lib/litd/litdService.spec.ts +++ b/src/lib/litd/litdService.spec.ts @@ -24,6 +24,7 @@ describe('LitdService', () => { repoState: defaultRepoState, managedImages: testManagedImages, customImages: [], + manualMineCount: 6, }); const node = network.nodes.lightning[0] as LitdNode; diff --git a/src/store/models/designer.spec.ts b/src/store/models/designer.spec.ts index 7d42244824..7139b18805 100644 --- a/src/store/models/designer.spec.ts +++ b/src/store/models/designer.spec.ts @@ -133,6 +133,7 @@ describe('Designer model', () => { tapdNodes: 0, litdNodes: 0, customNodes: {}, + manualMineCount: 6, }); store.getActions().designer.setActiveId(firstNetwork().id); const { removeChart } = store.getActions().designer; @@ -581,6 +582,7 @@ describe('Designer model', () => { tapdNodes: 0, litdNodes: 0, customNodes: {}, + manualMineCount: 6, }); const newId = store.getState().network.networks[1].id; setActiveId(newId); @@ -610,6 +612,7 @@ describe('Designer model', () => { tapdNodes: 0, litdNodes: 0, customNodes: {}, + manualMineCount: 6, }); const newId = store.getState().network.networks[1].id; setActiveId(newId); @@ -647,6 +650,7 @@ describe('Designer model', () => { tapdNodes: 0, litdNodes: 0, customNodes: {}, + manualMineCount: 0, }); const newId = store.getState().network.networks[1].id; setActiveId(newId); diff --git a/src/store/models/lit.spec.ts b/src/store/models/lit.spec.ts index d808ba928c..10f7822de3 100644 --- a/src/store/models/lit.spec.ts +++ b/src/store/models/lit.spec.ts @@ -44,6 +44,7 @@ describe('LIT Model', () => { repoState: defaultRepoState, managedImages: testManagedImages, customImages: [], + manualMineCount: 6, }); const initialState = { network: { diff --git a/src/store/models/network.spec.ts b/src/store/models/network.spec.ts index 745d9a2276..22df734aae 100644 --- a/src/store/models/network.spec.ts +++ b/src/store/models/network.spec.ts @@ -65,6 +65,7 @@ describe('Network model', () => { tapdNodes: 0, litdNodes: 1, customNodes: {}, + manualMineCount: 6, }; beforeEach(() => { @@ -1023,6 +1024,55 @@ describe('Network model', () => { }); }); + describe('ManualMineCount', () => { + beforeEach(async () => { + await store.getActions().network.addNetwork(addNetworkArgs); + }); + + it('should set manual mine count for a network', () => { + const { setManualMineCount } = store.getActions().network; + const networkId = firstNetwork().id; + + expect(firstNetwork().manualMineCount).toBe(6); + setManualMineCount({ id: networkId, count: 10 }); + expect(firstNetwork().manualMineCount).toBe(10); + }); + + it('should fail to set manual mine count with invalid network id', () => { + const { setManualMineCount } = store.getActions().network; + + expect(() => setManualMineCount({ id: 999, count: 10 })).toThrow( + "Network with the id '999' was not found.", + ); + }); + + it('should update manual mine count and persist changes', async () => { + const { updateManualMineCount } = store.getActions().network; + const networkId = firstNetwork().id; + + (injections.dockerService.saveComposeFile as jest.Mock).mockClear(); + (injections.dockerService.saveNetworks as jest.Mock).mockClear(); + + await updateManualMineCount({ id: networkId, count: 15 }); + + expect(firstNetwork().manualMineCount).toBe(15); + + expect(injections.dockerService.saveComposeFile).toHaveBeenCalledTimes(1); + expect(injections.dockerService.saveNetworks).toHaveBeenCalledTimes(1); + }); + + it('should fail to update manual mine count with invalid network id', () => { + const { updateManualMineCount } = store.getActions().network; + + (injections.dockerService.saveComposeFile as jest.Mock).mockClear(); + (injections.dockerService.saveNetworks as jest.Mock).mockClear(); + + expect(() => updateManualMineCount({ id: 999, count: 10 })).rejects.toThrow( + "Network with the id '999' was not found.", + ); + }); + }); + describe('Other actions', () => { it('should remove a network', async () => { expect(store.getState().network.networks).toHaveLength(0); diff --git a/src/store/models/network.ts b/src/store/models/network.ts index 46093e7a1a..c52633c57b 100644 --- a/src/store/models/network.ts +++ b/src/store/models/network.ts @@ -51,6 +51,7 @@ interface AddNetworkArgs { tapdNodes: number; litdNodes: number; customNodes: Record; + manualMineCount: number; } export interface AutoMinerModel { @@ -192,6 +193,14 @@ export interface NetworkModel { setAutoMineMode: Action; setMiningState: Action; mineBlock: Thunk; + setManualMineCount: Action; + updateManualMineCount: Thunk< + NetworkModel, + { id: number; count: number }, + StoreInjections, + RootModel, + Promise + >; } const networkModel: NetworkModel = { @@ -286,6 +295,7 @@ const networkModel: NetworkModel = { managedImages: computedManagedImages, customImages, basePorts: settings.basePorts, + manualMineCount: 6, }); actions.add(network); const { networks } = getState(); @@ -1074,6 +1084,25 @@ const networkModel: NetworkModel = { } }, ), + setManualMineCount: action((state, { id, count }) => { + const network = state.networks.find(n => n.id === id); + if (!network) throw new Error(l('networkByIdErr', { networkId: id })); + network.manualMineCount = count; + }), + updateManualMineCount: thunk( + async (actions, { id, count }, { getState, injections }) => { + const networks = getState().networks; + const network = networks.find(n => n.id === id); + if (!network) throw new Error(l('networkByIdErr', { networkId: id })); + + // Update the manual mine count + actions.setManualMineCount({ id, count }); + + // Save the changes to disk + await actions.save(); + await injections.dockerService.saveComposeFile(network); + }, + ), }; export default networkModel; diff --git a/src/types/index.ts b/src/types/index.ts index a70c89830b..ee01a5d322 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -31,6 +31,7 @@ export interface Network { lightning: LightningNode[]; tap: TapNode[]; }; + manualMineCount: number; } /** diff --git a/src/utils/network.spec.ts b/src/utils/network.spec.ts index 05efdf07dd..c7024963a1 100644 --- a/src/utils/network.spec.ts +++ b/src/utils/network.spec.ts @@ -163,6 +163,7 @@ describe('Network Utils', () => { repoState: defaultRepoState, managedImages: testManagedImages, customImages: [], + manualMineCount: 6, }); }); @@ -507,6 +508,7 @@ describe('Network Utils', () => { repoState: defaultRepoState, managedImages: testManagedImages, customImages: [], + manualMineCount: 6, }); }); diff --git a/src/utils/network.ts b/src/utils/network.ts index d8106052f1..5f826c7b5f 100644 --- a/src/utils/network.ts +++ b/src/utils/network.ts @@ -456,6 +456,7 @@ export const createNetwork = (config: { customImages: { image: CustomImage; count: number }[]; status?: Status; basePorts?: NodeBasePorts; + manualMineCount: number; }): Network => { const { id, @@ -471,6 +472,7 @@ export const createNetwork = (config: { managedImages, customImages, basePorts, + manualMineCount, } = config; // need explicit undefined check because Status.Starting is 0 const status = config.status !== undefined ? config.status : Status.Stopped; @@ -487,6 +489,7 @@ export const createNetwork = (config: { tap: [], }, autoMineMode: AutoMineMode.AutoOff, + manualMineCount, }; const { bitcoin, lightning } = network.nodes; diff --git a/src/utils/tests/helpers.ts b/src/utils/tests/helpers.ts index 456214d71c..537e1cbc1f 100644 --- a/src/utils/tests/helpers.ts +++ b/src/utils/tests/helpers.ts @@ -240,6 +240,7 @@ export const getNetwork = ( repoState: defaultRepoState, managedImages: testManagedImages, customImages: [], + manualMineCount: 6, }; if (tapNodeCount > 0) { config.lndNodes = 0;