diff --git a/package-lock.json b/package-lock.json index e025ee86c9..288e41267f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3599,6 +3599,11 @@ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", @@ -9579,6 +9584,17 @@ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, + "node_modules/chart.js": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.3.0.tgz", + "integrity": "sha512-ynG0E79xGfMaV2xAHdbhwiPLczxnNNnasrmPEXriXsPJGjmhOBYzFVEsB65w2qMDz+CaBJJuJD0inE/ab/h36g==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=7" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -23083,6 +23099,15 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", + "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -28409,6 +28434,7 @@ "bech32": "2.0.0", "bignumber.js": "9.0.2", "byte-size": "8.1.0", + "chart.js": "^4.3.0", "chokidar": "3.5.3", "core-js": "3.20.3", "crypto-browserify": "3.12.0", @@ -28427,6 +28453,7 @@ "moment": "2.29.4", "normalize-url": "7.0.3", "react": "17.0.2", + "react-chartjs-2": "5.2.0", "react-dom": "17.0.2", "react-dropzone": "11.5.1", "react-hook-form": "7.41.5", diff --git a/packages/api-react/src/hooks/useGetTotalHarvestersSummaryQuery.ts b/packages/api-react/src/hooks/useGetTotalHarvestersSummaryQuery.ts index 5c729555ff..377c3e66c5 100644 --- a/packages/api-react/src/hooks/useGetTotalHarvestersSummaryQuery.ts +++ b/packages/api-react/src/hooks/useGetTotalHarvestersSummaryQuery.ts @@ -13,6 +13,7 @@ export default function useGetTotalHarvestersSummaryQuery() { let plots = new BigNumber(0); let plotsProcessed = new BigNumber(0); let totalPlotSize = new BigNumber(0); + let totalEffectivePlotSize = new BigNumber(0); let plotFilesTotal = new BigNumber(0); let initialized = !!data?.length; let initializedHarvesters = 0; @@ -22,6 +23,7 @@ export default function useGetTotalHarvestersSummaryQuery() { failedToOpenFilenames = failedToOpenFilenames.plus(harvester.failedToOpenFilenames); noKeyFilenames = noKeyFilenames.plus(harvester.noKeyFilenames); totalPlotSize = totalPlotSize.plus(harvester.totalPlotSize); + totalEffectivePlotSize = totalEffectivePlotSize.plus(harvester.totalEffectivePlotSize); plots = plots.plus(harvester.plots); if (harvester.syncing) { @@ -45,6 +47,7 @@ export default function useGetTotalHarvestersSummaryQuery() { plots, plotsProcessed, totalPlotSize, + totalEffectivePlotSize, plotFilesTotal, initialized, initializedHarvesters, @@ -63,6 +66,7 @@ export default function useGetTotalHarvestersSummaryQuery() { harvesters: data?.length ?? 0, plotsProcessed: memoized.plotsProcessed, totalPlotSize: memoized.totalPlotSize, + totalEffectivePlotSize: memoized.totalEffectivePlotSize, plotFilesTotal: memoized.plotFilesTotal, initializedHarvesters: memoized.initializedHarvesters, }; diff --git a/packages/api-react/src/hooks/useService.ts b/packages/api-react/src/hooks/useService.ts index d0b9106f19..329a4403b2 100644 --- a/packages/api-react/src/hooks/useService.ts +++ b/packages/api-react/src/hooks/useService.ts @@ -82,6 +82,8 @@ export default function useService( }).unwrap(); refetch(); + } catch (e) { + console.error(e); } finally { setIsStarting(false); } @@ -99,6 +101,8 @@ export default function useService( }).unwrap(); refetch(); + } catch (e) { + console.error(e); } finally { setIsStopping(false); } diff --git a/packages/api-react/src/services/client.ts b/packages/api-react/src/services/client.ts index 5ef59db8f2..6a5fc99cbd 100644 --- a/packages/api-react/src/services/client.ts +++ b/packages/api-react/src/services/client.ts @@ -44,7 +44,10 @@ export const clientApi = apiWithTag.injectEndpoints({ }), clientStartService: mutation(build, Client, 'startService'), + + clientStopService: mutation(build, Client, 'stopService'), }), }); -export const { useCloseMutation, useGetStateQuery, useClientStartServiceMutation } = clientApi; +export const { useCloseMutation, useGetStateQuery, useClientStartServiceMutation, useClientStopServiceMutation } = + clientApi; diff --git a/packages/api-react/src/services/daemon.ts b/packages/api-react/src/services/daemon.ts index 0b30261827..d07a255b66 100644 --- a/packages/api-react/src/services/daemon.ts +++ b/packages/api-react/src/services/daemon.ts @@ -123,6 +123,7 @@ export const daemonApi = apiWithTag.injectEndpoints({ installed, canInstall, bladebitMemoryWarning, + cudaSupport, } = plotters[plotterName] as PlotterApi; if (!plotterName.startsWith('bladebit')) { @@ -142,24 +143,11 @@ export const daemonApi = apiWithTag.injectEndpoints({ // if (plotterName.startsWith('bladebit')) const majorVersion = typeof version === 'string' ? +version.split('.')[0] : 0; - if (majorVersion > 1) { - const bbDisk = PlotterName.BLADEBIT_DISK; - availablePlotters[bbDisk] = { - displayName, - version: `${version} (Disk plot)`, - options: optionsForPlotter(bbDisk), - defaults: defaultsForPlotter(bbDisk), - installInfo: { - installed, - canInstall, - bladebitMemoryWarning, - }, - }; - + if (majorVersion <= 1) { const bbRam = PlotterName.BLADEBIT_RAM; availablePlotters[bbRam] = { displayName, - version: `${version} (RAM plot)`, + version: typeof version === 'string' ? `${version} (RAM plot)` : version, options: optionsForPlotter(bbRam), defaults: defaultsForPlotter(bbRam), installInfo: { @@ -168,17 +156,45 @@ export const daemonApi = apiWithTag.injectEndpoints({ bladebitMemoryWarning, }, }; - } else { - const bbRam = PlotterName.BLADEBIT_RAM; - availablePlotters[bbRam] = { + return; + } + const bbDisk = PlotterName.BLADEBIT_DISK; + availablePlotters[bbDisk] = { + displayName, + version: `${version} (Disk plot)`, + options: optionsForPlotter(bbDisk), + defaults: defaultsForPlotter(bbDisk), + installInfo: { + installed, + canInstall, + bladebitMemoryWarning, + }, + }; + + const bbRam = PlotterName.BLADEBIT_RAM; + availablePlotters[bbRam] = { + displayName, + version: `${version} (RAM plot)`, + options: optionsForPlotter(bbRam), + defaults: defaultsForPlotter(bbRam), + installInfo: { + installed, + canInstall, + bladebitMemoryWarning, + }, + }; + if (cudaSupport) { + const bbCuda = PlotterName.BLADEBIT_CUDA; + availablePlotters[bbCuda] = { displayName, - version: `${version} (RAM plot)`, - options: optionsForPlotter(bbRam), - defaults: defaultsForPlotter(bbRam), + version: `${version} (CUDA plot)`, + options: optionsForPlotter(bbCuda), + defaults: defaultsForPlotter(bbCuda), installInfo: { - installed: false, - canInstall: false, + installed, + canInstall, bladebitMemoryWarning, + cudaSupport, }, }; } @@ -196,6 +212,8 @@ export const daemonApi = apiWithTag.injectEndpoints({ transformResponse: (response) => response.version, providesTags: [{ type: 'RunningServices', id: 'LIST' }], }), + + getKeysForPlotting: query(build, Daemon, 'getKeysForPlotting'), }), }); @@ -211,6 +229,7 @@ export const { useMigrateKeyringMutation, useUnlockKeyringMutation, useGetVersionQuery, + useGetKeysForPlottingQuery, useGetPlottersQuery, useStopPlottingMutation, diff --git a/packages/api-react/src/services/farmer.ts b/packages/api-react/src/services/farmer.ts index 5b73abf4de..d0cb60ade2 100644 --- a/packages/api-react/src/services/farmer.ts +++ b/packages/api-react/src/services/farmer.ts @@ -20,6 +20,9 @@ export const apiWithTag = api.enhanceEndpoints({ 'HarvestersSummary', 'HarvesterPlotsKeysMissing', 'HarvesterPlotsDuplicates', + 'MissingSignagePoints', + 'FilterChallengeStat', + 'partialStats', ], }); @@ -32,7 +35,12 @@ export const farmerApi = apiWithTag.injectEndpoints({ providesTags: [{ type: 'Harvesters', id: 'LIST' }], onCacheEntryAdded: onCacheEntryAddedInvalidate(baseQuery, api, [ { - command: 'onHarvesterChanged', + command: 'onHarvesterUpdated', + service: Farmer, + endpoint: 'getHarvesters', + }, + { + command: 'onHarvesterRemoved', service: Farmer, endpoint: 'getHarvesters', }, @@ -231,25 +239,75 @@ export const farmerApi = apiWithTag.injectEndpoints({ { type: 'Pools', id: 'LIST' }, ] : [{ type: 'Pools', id: 'LIST' }], + onCacheEntryAdded: onCacheEntryAddedInvalidate(baseQuery, api, [ + { + command: 'onSubmittedPartial', + service: Farmer, + endpoint: 'getPoolState', + }, + { + command: 'onFailedPartial', + service: Farmer, + endpoint: 'getPoolState', + }, + ]), + }), + + getPartialStatsOffset: query(build, Farmer, 'getPartialStatsOffset', { + providesTags: ['partialStats'], + }), + + resetPartialStats: mutation(build, Farmer, 'resetPartialStats', { + invalidatesTags: ['partialStats'], }), setPayoutInstructions: mutation(build, Farmer, 'setPayoutInstructions', { invalidatesTags: (_result, _error, { launcherId }) => [{ type: 'PayoutInstructions', id: launcherId }], }), - getFarmingInfo: query(build, Farmer, 'getFarmingInfo', { + getNewFarmingInfo: query(build, Farmer, 'getNewFarmingInfo', { + onCacheEntryAdded: onCacheEntryAddedInvalidate(baseQuery, api, [ + { + command: 'onFarmingInfoChanged', + service: Farmer, + endpoint: 'getNewFarmingInfo', + }, + ]), + }), + + getMissingSignagePoints: query(build, Farmer, 'getMissingSignagePoints', { + providesTags: ['MissingSignagePoints'], + onCacheEntryAdded: onCacheEntryAddedInvalidate(baseQuery, api, [ + { + command: 'onNewSignagePoint', + service: Farmer, + endpoint: 'getMissingSignagePoints', + }, + ]), + }), + + resetMissingSignagePoints: mutation(build, Farmer, 'resetMissingSignagePoints', { + invalidatesTags: ['MissingSignagePoints'], + }), + + getFilterChallengeStat: query(build, Farmer, 'getFilterChallengeStat', { + providesTags: ['FilterChallengeStat'], onCacheEntryAdded: onCacheEntryAddedInvalidate(baseQuery, api, [ { command: 'onFarmingInfoChanged', service: Farmer, - endpoint: 'getFarmingInfo', + endpoint: 'getFilterChallengeStat', }, ]), }), + + resetFilterChallengeStat: mutation(build, Farmer, 'resetFilterChallengeStat', { + invalidatesTags: ['FilterChallengeStat'], + }), }), }); -// TODO add new farming info query and event for last_attepmtp_proofs +// TODO add new farming info query and event for last_attempt_proofs export const { useFarmerPingQuery, @@ -268,5 +326,11 @@ export const { useGetSignagePointsQuery, useGetPoolStateQuery, useSetPayoutInstructionsMutation, - useGetFarmingInfoQuery, + useGetNewFarmingInfoQuery, + useGetMissingSignagePointsQuery, + useResetMissingSignagePointsMutation, + useGetFilterChallengeStatQuery, + useResetFilterChallengeStatMutation, + useGetPartialStatsOffsetQuery, + useResetPartialStatsMutation, } = farmerApi; diff --git a/packages/api-react/src/services/harvester.ts b/packages/api-react/src/services/harvester.ts index 8d758d5fbd..a99531508e 100644 --- a/packages/api-react/src/services/harvester.ts +++ b/packages/api-react/src/services/harvester.ts @@ -1,10 +1,12 @@ import { Harvester } from '@chia-network/api'; +import api, { baseQuery } from '../api'; +import onCacheEntryAddedInvalidate from '../utils/onCacheEntryAddedInvalidate'; import { query, mutation } from '../utils/reduxToolkitEndpointAbstractions'; import { apiWithTag } from './farmer'; const apiWithTag2 = apiWithTag.enhanceEndpoints({ - addTagTypes: ['Plots', 'PlotDirectories'], + addTagTypes: ['Plots', 'PlotDirectories', 'harvesterConfig'], }); export const harvesterApi = apiWithTag2.injectEndpoints({ @@ -71,6 +73,24 @@ export const harvesterApi = apiWithTag2.injectEndpoints({ { type: 'PlotDirectories', id: dirname }, ], }), + + getHarvesterConfig: query(build, Harvester, 'getHarvesterConfig', { + providesTags: ['harvesterConfig'], + }), + + updateHarvesterConfig: mutation(build, Harvester, 'updateHarvesterConfig', { + invalidatesTags: ['harvesterConfig'], + }), + + getFarmingInfo: query(build, Harvester, 'getFarmingInfo', { + onCacheEntryAdded: onCacheEntryAddedInvalidate(baseQuery, api, [ + { + command: 'onFarmingInfoChanged', + service: Harvester, + endpoint: 'getFarmingInfo', + }, + ]), + }), }), }); @@ -81,4 +101,7 @@ export const { useGetPlotDirectoriesQuery, useAddPlotDirectoryMutation, useRemovePlotDirectoryMutation, + useGetFarmingInfoQuery, + useGetHarvesterConfigQuery, + useUpdateHarvesterConfigMutation, } = harvesterApi; diff --git a/packages/api-react/src/services/index.ts b/packages/api-react/src/services/index.ts index 13435f58dd..630cce2cb9 100644 --- a/packages/api-react/src/services/index.ts +++ b/packages/api-react/src/services/index.ts @@ -12,6 +12,7 @@ export const { useCloseMutation, useGetStateQuery, useClientStartServiceMutation, + useClientStopServiceMutation, } = client; // daemon hooks @@ -29,6 +30,7 @@ export const { useMigrateKeyringMutation, useUnlockKeyringMutation, useGetVersionQuery, + useGetKeysForPlottingQuery, useGetPlottersQuery, useStopPlottingMutation, @@ -62,7 +64,13 @@ export const { useGetSignagePointsQuery, useGetPoolStateQuery, useSetPayoutInstructionsMutation, - useGetFarmingInfoQuery, + useGetNewFarmingInfoQuery, + useGetMissingSignagePointsQuery, + useResetMissingSignagePointsMutation, + useGetFilterChallengeStatQuery, + useResetFilterChallengeStatMutation, + useGetPartialStatsOffsetQuery, + useResetPartialStatsMutation, } = farmer; // full node hooks @@ -214,6 +222,9 @@ export const { useGetPlotDirectoriesQuery, useAddPlotDirectoryMutation, useRemovePlotDirectoryMutation, + useGetFarmingInfoQuery, + useGetHarvesterConfigQuery, + useUpdateHarvesterConfigMutation, } = harvester; // plotter hooks diff --git a/packages/api/src/@types/BlockchainState.ts b/packages/api/src/@types/BlockchainState.ts index fcb703a637..f8a6986c11 100644 --- a/packages/api/src/@types/BlockchainState.ts +++ b/packages/api/src/@types/BlockchainState.ts @@ -55,6 +55,7 @@ type BlockchainState = { nodeId: string; peak: Peak; space: number; + averageBlockTime: number; subSlotIters: number; sync: Sync; }; diff --git a/packages/api/src/@types/FarmedAmount.ts b/packages/api/src/@types/FarmedAmount.ts index 2f612bd87b..e3373c5214 100644 --- a/packages/api/src/@types/FarmedAmount.ts +++ b/packages/api/src/@types/FarmedAmount.ts @@ -4,6 +4,8 @@ type FarmedAmount = { feeAmount: number; lastHeightFarmed: number; poolRewardAmount: number; + lastTimeFarmed: number; + blocksWon: number; }; export default FarmedAmount; diff --git a/packages/api/src/@types/FarmingInfo.ts b/packages/api/src/@types/FarmingInfo.ts index 20d69fd49e..02b68340e6 100644 --- a/packages/api/src/@types/FarmingInfo.ts +++ b/packages/api/src/@types/FarmingInfo.ts @@ -1,10 +1,11 @@ +import BigNumber from 'bignumber.js'; + type FarmingInfo = { challengeHash: string; - signagePoint: string; - timestamp: number; - passedFilter: number; - proofs: number; totalPlots: number; + foundProofs: number; + eligiblePlots: number; + time: BigNumber; }; export default FarmingInfo; diff --git a/packages/api/src/@types/Harvester.ts b/packages/api/src/@types/Harvester.ts index 08df35a970..fb32e7fe35 100644 --- a/packages/api/src/@types/Harvester.ts +++ b/packages/api/src/@types/Harvester.ts @@ -15,7 +15,11 @@ type HarvesterSyncingStatus = { plotFilesTotal: number; }; -type Harvester = { +export type CPUHarvesting = 1; +export type GPUHarvesting = 2; +export type HarvestingMode = CPUHarvesting | GPUHarvesting; + +type HarvesterInfo = { connection: Connection; duplicates: string[]; failedToOpenFilenames: string[]; @@ -23,11 +27,13 @@ type Harvester = { lastSyncTime?: BigNumber; syncing?: HarvesterSyncingStatus; totalPlotSize: number; + totalEffectivePlotSize: number; plots: Plot[]; + harvestingMode?: HarvestingMode; }; export type HarvesterSummary = Modify< - Harvester, + HarvesterInfo, { duplicates: number; failedToOpenFilenames: number; @@ -36,4 +42,4 @@ export type HarvesterSummary = Modify< } >; -export default Harvester; +export default HarvesterInfo; diff --git a/packages/api/src/@types/NewFarmingInfo.ts b/packages/api/src/@types/NewFarmingInfo.ts new file mode 100644 index 0000000000..40798c69af --- /dev/null +++ b/packages/api/src/@types/NewFarmingInfo.ts @@ -0,0 +1,12 @@ +type NewFarmingInfo = { + nodeId: string; + challengeHash: string; + signagePoint: string; + timestamp: number; + passedFilter: number; + proofs: number; + totalPlots: number; + lookupTime: number; +}; + +export default NewFarmingInfo; diff --git a/packages/api/src/@types/Plot.ts b/packages/api/src/@types/Plot.ts index 338b404d09..e8186f39b0 100644 --- a/packages/api/src/@types/Plot.ts +++ b/packages/api/src/@types/Plot.ts @@ -7,6 +7,7 @@ type Plot = { poolPublicKey: string; size: number; timeModified: number; + compressionLevel?: number; }; export default Plot; diff --git a/packages/api/src/@types/Plotter.ts b/packages/api/src/@types/Plotter.ts index eb3cf3804e..206def566b 100644 --- a/packages/api/src/@types/Plotter.ts +++ b/packages/api/src/@types/Plotter.ts @@ -7,13 +7,14 @@ interface CommonOptions { canPlotInParallel: boolean; canDelayParallelPlots: boolean; canSetBufferSize: boolean; + haveTempDir: boolean; } interface BladeBitRamOptions extends CommonOptions { haveBladebitWarmStart: boolean; haveBladebitDisableNUMA: boolean; haveBladebitNoCpuAffinity: boolean; - haveBladebitOutputDir: boolean; + haveBladebitCompressionLevel: boolean; } interface BladeBitDiskOptions extends BladeBitRamOptions { @@ -28,13 +29,22 @@ interface BladeBitDiskOptions extends BladeBitRamOptions { haveBladebitDiskNoT2Direct: boolean; } +interface BladeBitCudaOptions extends BladeBitRamOptions { + haveBladebitDeviceIndex: boolean; + haveBladebitDisableDirectDownloads: boolean; +} + interface MadMaxOptions extends CommonOptions { haveMadmaxNumBucketsPhase3: boolean; haveMadmaxThreadMultiplier: boolean; haveMadmaxTempToggle: boolean; } -export type PlotterOptions = CommonOptions & BladeBitRamOptions & BladeBitDiskOptions & MadMaxOptions; +export type PlotterOptions = CommonOptions & + BladeBitRamOptions & + BladeBitDiskOptions & + BladeBitCudaOptions & + MadMaxOptions; interface CommonDefaults { plotterName: string; @@ -47,10 +57,11 @@ interface CommonDefaults { } interface BladeBitRamDefaults extends CommonDefaults { - plotType?: 'ramplot' | 'diskplot'; + plotType?: 'ramplot' | 'diskplot' | 'cudaplot'; bladebitWarmStart?: boolean; bladebitDisableNUMA?: boolean; bladebitNoCpuAffinity?: boolean; + bladebitCompressionLevel?: number; } interface BladeBitDiskDefaults extends BladeBitRamDefaults { @@ -65,6 +76,11 @@ interface BladeBitDiskDefaults extends BladeBitRamDefaults { bladebitDiskNoT2Direct?: boolean; } +interface BladeBitCudaDefaults extends BladeBitRamDefaults { + bladebitDeviceIndex?: number; + bladebitDisableDirectDownloads?: boolean; +} + interface MadMaxDefaults extends CommonDefaults { madmaxNumBucketsPhase3?: number; madmaxThreadMultiplier?: number; @@ -72,13 +88,18 @@ interface MadMaxDefaults extends CommonDefaults { madmaxTempToggle?: boolean; } -export type PlotterDefaults = CommonDefaults & BladeBitRamDefaults & BladeBitDiskDefaults & MadMaxDefaults; +export type PlotterDefaults = CommonDefaults & + BladeBitRamDefaults & + BladeBitDiskDefaults & + BladeBitCudaDefaults & + MadMaxDefaults; type PlotterInstallInfo = { version?: string; installed: boolean; canInstall?: boolean; bladebitMemoryWarning?: string; + cudaSupport?: boolean; }; type Plotter = { @@ -96,6 +117,7 @@ export type PlotterApi = { canInstall?: boolean; // not sure if bladebitMemoryWarning should be here bladebitMemoryWarning?: string; + cudaSupport?: boolean; }; export type PlottersApi = { [key in PlotterName]?: PlotterApi }; diff --git a/packages/api/src/@types/PoolState.ts b/packages/api/src/@types/PoolState.ts index 41237293fc..6ffec84284 100644 --- a/packages/api/src/@types/PoolState.ts +++ b/packages/api/src/@types/PoolState.ts @@ -1,8 +1,8 @@ import type BigNumber from 'bignumber.js'; type PoolState = { - authenticationTokenTimeout: number; - currentDifficulty: number; + authenticationTokenTimeout: number | null; + currentDifficulty: number | null; currentPoints: number; nextFarmerUpdate: number | BigNumber; nextPoolInfoUpdate: number | BigNumber; @@ -20,10 +20,23 @@ type PoolState = { poolUrl: string; targetPuzzleHash: string; }; - poolErrors24H: { - errorCode: number; - errorMessage: string; - }[]; + poolErrors24H: Array< + [ + number, + { + errorCode: number; + errorMessage: string; + } + ] + >; + validPartialsSinceStart: number; + validPartials24h: Array<[number, number]>; + invalidPartialsSinceStart: number; + invalidPartials24h: Array<[number, number]>; + stalePartialsSinceStart: number; + stalePartials24h: Array<[number, number]>; + missingPartialsSinceStart: number; + missingPartials24h: Array<[number, number]>; }; export default PoolState; diff --git a/packages/api/src/@types/WalletCreate.ts b/packages/api/src/@types/WalletCreate.ts index 5760b7c565..efa487741d 100644 --- a/packages/api/src/@types/WalletCreate.ts +++ b/packages/api/src/@types/WalletCreate.ts @@ -25,7 +25,7 @@ type WalletCreateRecoveryDID = { numVerificationsRequired: number; }; -type WalletCreatePool = { +export type WalletCreatePool = { totalFee: number; transaction: Transaction; launcherId: string; diff --git a/packages/api/src/@types/index.ts b/packages/api/src/@types/index.ts index f328d66d66..26975c2a6a 100644 --- a/packages/api/src/@types/index.ts +++ b/packages/api/src/@types/index.ts @@ -13,11 +13,12 @@ export type { default as Coin2 } from './Coin2'; export type { default as CoinSolution } from './CoinSolution'; export type { default as Connection } from './Connection'; export type { default as FarmingInfo } from './FarmingInfo'; +export type { default as NewFarmingInfo } from './NewFarmingInfo'; export type { default as Fingerprint } from './Fingerprint'; export type { default as Foliage } from './Foliage'; export type { default as FoliageTransactionBlock } from './FoliageTransactionBlock'; export type { default as G2Element } from './G2Element'; -export type { default as Harveste, HarvesterSummary } from './Harvester'; +export type { default as HarvesterInfo, HarvesterSummary } from './Harvester'; export type { default as HarvesterPlotsPaginated } from './HarvesterPlotsPaginated'; export type { default as Header } from './Header'; export type { default as InitialTargetState } from './InitialTargetState'; @@ -67,3 +68,4 @@ export type { default as Transaction } from './Transaction'; export type { default as UnconfirmedPlotNFT } from './UnconfirmedPlotNFT'; export type { default as Wallet } from './Wallet'; export type { default as WalletBalance } from './WalletBalance'; +export type { default as WalletCreate, WalletCreatePool } from './WalletCreate'; diff --git a/packages/api/src/Client.ts b/packages/api/src/Client.ts index 495a4c49d0..a3eca5b960 100644 --- a/packages/api/src/Client.ts +++ b/packages/api/src/Client.ts @@ -53,6 +53,8 @@ export default class Client extends EventEmitter { private connectServicePromise: Map> = new Map(); + private stopServicePromise: Map> = new Map(); + private daemon: Daemon; private closed = false; @@ -205,7 +207,16 @@ export default class Client extends EventEmitter { async startService(args: { service: ServiceNameValue; disableWait?: boolean }) { const { service, disableWait } = args; + if (this.connectServicePromise.has(service)) { + await this.connectServicePromise.get(service); + return; + } + const startServiceAction = async () => { + if (this.stopServicePromise.has(service)) { + await this.stopServicePromise.get(service); + } + if (this.started.has(service)) { return; } @@ -262,44 +273,56 @@ export default class Client extends EventEmitter { await Promise.all(services.map(async (service) => this.startService({ service }))); } - async stopService(args: { service: ServiceNameValue }) { - const { service } = args; - if (!this.started.has(service)) { - return; - } + async stopService(args: { service: ServiceNameValue; disableWait?: boolean }) { + const { service, disableWait } = args; - const response = await this.daemon.isRunning({ service }); - if (response.isRunning) { - log(`Closing down service: ${service}`); - await this.daemon.stopService({ service }); - } + const stopServiceAction = async () => { + if (!this.started.has(service)) { + log(`Service: ${service} is already stopped`); + return; + } + + const response = await this.daemon.isRunning({ service }); + if (response.isRunning) { + log(`Closing down service: ${service}`); + await this.daemon.stopService({ service }); + } + + // wait for service initialisation + log(`Waiting for service: ${service}`); + if (!disableWait) { + while (true) { + try { + const { data } = await this.send( + new Message({ + command: 'ping', + origin: this.origin, + destination: service, + }), + 1000 + ); - // wait for service initialisation - log(`Waiting for service: ${service}`); - while (true) { - try { - const { data } = await this.send( - new Message({ - command: 'ping', - origin: this.origin, - destination: service, - }), - 1000 - ); - - if (data.success) { - await sleep(1000); + if (data.success) { + await sleep(1000); + } + } catch (error) { + break; + } } - } catch (error) { - break; } - } - log(`Service: ${service} stopped`); + log(`Service: ${service} stopped`); - this.started.delete(service); - this.connectServicePromise.delete(service); - this.emit('state', this.getState()); + this.started.delete(service); + this.connectServicePromise.delete(service); + this.emit('state', this.getState()); + }; + + const stopServiceTask = stopServiceAction(); + this.stopServicePromise.set(service, stopServiceTask); + await stopServiceTask.finally(() => { + this.stopServicePromise.delete(service); + }); } private handleOpen = async () => { diff --git a/packages/api/src/constants/PlotterName.ts b/packages/api/src/constants/PlotterName.ts index 9a1a650274..618278ee79 100644 --- a/packages/api/src/constants/PlotterName.ts +++ b/packages/api/src/constants/PlotterName.ts @@ -1,6 +1,7 @@ enum PlotterName { BLADEBIT_RAM = 'bladebit_ram', BLADEBIT_DISK = 'bladebit_disk', + BLADEBIT_CUDA = 'bladebit_cuda', CHIAPOS = 'chiapos', MADMAX = 'madmax', } diff --git a/packages/api/src/constants/Plotters.ts b/packages/api/src/constants/Plotters.ts index e0da34926c..e72b5a8d43 100644 --- a/packages/api/src/constants/Plotters.ts +++ b/packages/api/src/constants/Plotters.ts @@ -4,13 +4,14 @@ import PlotterName from './PlotterName'; export const bladebitRamOptions: PlotterOptions = { kSizes: [32], haveNumBuckets: false, + haveTempDir: false, haveMadmaxNumBucketsPhase3: false, haveMadmaxThreadMultiplier: false, haveMadmaxTempToggle: false, haveBladebitWarmStart: true, haveBladebitDisableNUMA: true, haveBladebitNoCpuAffinity: true, - haveBladebitOutputDir: true, + haveBladebitCompressionLevel: true, haveBladebitDiskCache: false, haveBladebitDiskF1Threads: false, haveBladebitDiskFpThreads: false, @@ -20,6 +21,8 @@ export const bladebitRamOptions: PlotterOptions = { haveBladebitDiskAlternate: false, haveBladebitDiskNoT1Direct: false, haveBladebitDiskNoT2Direct: false, + haveBladebitDeviceIndex: false, + haveBladebitDisableDirectDownloads: false, canDisableBitfieldPlotting: false, canPlotInParallel: false, canDelayParallelPlots: false, @@ -39,6 +42,7 @@ export const bladebitRamDefaults: PlotterDefaults = { bladebitWarmStart: false, bladebitDisableNUMA: false, bladebitNoCpuAffinity: false, + bladebitCompressionLevel: 0, bladebitDiskCache: undefined, bladebitDiskF1Threads: undefined, bladebitDiskFpThreads: undefined, @@ -48,6 +52,8 @@ export const bladebitRamDefaults: PlotterDefaults = { bladebitDiskAlternate: undefined, bladebitDiskNoT1Direct: undefined, bladebitDiskNoT2Direct: undefined, + bladebitDeviceIndex: undefined, + bladebitDisableDirectDownloads: undefined, disableBitfieldPlotting: undefined, parallel: false, delay: 0, @@ -62,7 +68,8 @@ export const bladebitDiskOptions: PlotterOptions = { haveBladebitWarmStart: true, haveBladebitNoCpuAffinity: true, haveBladebitDisableNUMA: true, - haveBladebitOutputDir: false, + haveTempDir: true, + haveBladebitCompressionLevel: true, haveBladebitDiskCache: true, haveBladebitDiskF1Threads: true, haveBladebitDiskFpThreads: true, @@ -72,6 +79,8 @@ export const bladebitDiskOptions: PlotterOptions = { haveBladebitDiskAlternate: true, haveBladebitDiskNoT1Direct: true, haveBladebitDiskNoT2Direct: true, + haveBladebitDeviceIndex: false, + haveBladebitDisableDirectDownloads: false, canDisableBitfieldPlotting: false, canPlotInParallel: false, canDelayParallelPlots: false, @@ -91,6 +100,7 @@ export const bladebitDiskDefaults: PlotterDefaults = { bladebitWarmStart: false, bladebitDisableNUMA: false, bladebitNoCpuAffinity: false, + bladebitCompressionLevel: 0, bladebitDiskCache: undefined, bladebitDiskF1Threads: undefined, bladebitDiskFpThreads: undefined, @@ -100,6 +110,66 @@ export const bladebitDiskDefaults: PlotterDefaults = { bladebitDiskAlternate: undefined, bladebitDiskNoT1Direct: undefined, bladebitDiskNoT2Direct: undefined, + bladebitDeviceIndex: undefined, + bladebitDisableDirectDownloads: undefined, + disableBitfieldPlotting: undefined, + parallel: false, + delay: 0, +}; + +export const bladebitCudaOptions: PlotterOptions = { + kSizes: [32], + haveNumBuckets: false, + haveMadmaxNumBucketsPhase3: false, + haveMadmaxThreadMultiplier: false, + haveMadmaxTempToggle: false, + haveBladebitWarmStart: true, + haveBladebitDisableNUMA: true, + haveBladebitNoCpuAffinity: true, + haveTempDir: true, + haveBladebitCompressionLevel: true, + haveBladebitDiskCache: false, + haveBladebitDiskF1Threads: false, + haveBladebitDiskFpThreads: false, + haveBladebitDiskCThreads: false, + haveBladebitDiskP2Threads: false, + haveBladebitDiskP3Threads: false, + haveBladebitDiskAlternate: false, + haveBladebitDiskNoT1Direct: false, + haveBladebitDiskNoT2Direct: false, + haveBladebitDeviceIndex: true, + haveBladebitDisableDirectDownloads: true, + canDisableBitfieldPlotting: false, + canPlotInParallel: false, + canDelayParallelPlots: false, + canSetBufferSize: false, +}; + +export const bladebitCudaDefaults: PlotterDefaults = { + plotterName: PlotterName.BLADEBIT_CUDA, + plotType: 'cudaplot', + plotSize: 32, + numThreads: 0, + numBuckets: undefined, + madmaxNumBucketsPhase3: undefined, + madmaxThreadMultiplier: undefined, + madmaxWaitForCopy: undefined, + madmaxTempToggle: undefined, + bladebitWarmStart: false, + bladebitDisableNUMA: false, + bladebitNoCpuAffinity: false, + bladebitCompressionLevel: 0, + bladebitDiskCache: undefined, + bladebitDiskF1Threads: undefined, + bladebitDiskFpThreads: undefined, + bladebitDiskCThreads: undefined, + bladebitDiskP2Threads: undefined, + bladebitDiskP3Threads: undefined, + bladebitDiskAlternate: undefined, + bladebitDiskNoT1Direct: undefined, + bladebitDiskNoT2Direct: undefined, + bladebitDeviceIndex: 0, + bladebitDisableDirectDownloads: false, disableBitfieldPlotting: undefined, parallel: false, delay: 0, @@ -114,7 +184,8 @@ export const chiaposOptions: PlotterOptions = { haveBladebitWarmStart: false, haveBladebitDisableNUMA: false, haveBladebitNoCpuAffinity: false, - haveBladebitOutputDir: false, + haveTempDir: true, + haveBladebitDeviceIndex: false, haveBladebitDiskCache: false, haveBladebitDiskF1Threads: false, haveBladebitDiskFpThreads: false, @@ -124,6 +195,8 @@ export const chiaposOptions: PlotterOptions = { haveBladebitDiskAlternate: false, haveBladebitDiskNoT1Direct: false, haveBladebitDiskNoT2Direct: false, + haveBladebitCompressionLevel: false, + haveBladebitDisableDirectDownloads: false, canDisableBitfieldPlotting: true, canPlotInParallel: true, canDelayParallelPlots: true, @@ -142,6 +215,7 @@ export const chiaposDefaults: PlotterDefaults = { bladebitWarmStart: undefined, bladebitDisableNUMA: undefined, bladebitNoCpuAffinity: undefined, + bladebitCompressionLevel: undefined, bladebitDiskCache: undefined, bladebitDiskF1Threads: undefined, bladebitDiskFpThreads: undefined, @@ -151,6 +225,8 @@ export const chiaposDefaults: PlotterDefaults = { bladebitDiskAlternate: undefined, bladebitDiskNoT1Direct: undefined, bladebitDiskNoT2Direct: undefined, + bladebitDeviceIndex: undefined, + bladebitDisableDirectDownloads: undefined, disableBitfieldPlotting: false, parallel: false, delay: 0, @@ -165,7 +241,8 @@ export const madmaxOptions: PlotterOptions = { haveBladebitWarmStart: false, haveBladebitDisableNUMA: false, haveBladebitNoCpuAffinity: false, - haveBladebitOutputDir: false, + haveTempDir: true, + haveBladebitCompressionLevel: false, haveBladebitDiskCache: false, haveBladebitDiskF1Threads: false, haveBladebitDiskFpThreads: false, @@ -175,6 +252,8 @@ export const madmaxOptions: PlotterOptions = { haveBladebitDiskAlternate: false, haveBladebitDiskNoT1Direct: false, haveBladebitDiskNoT2Direct: false, + haveBladebitDeviceIndex: false, + haveBladebitDisableDirectDownloads: false, canDisableBitfieldPlotting: false, canPlotInParallel: false, canDelayParallelPlots: false, @@ -193,6 +272,7 @@ export const madmaxDefaults: PlotterDefaults = { bladebitWarmStart: undefined, bladebitDisableNUMA: undefined, bladebitNoCpuAffinity: undefined, + bladebitCompressionLevel: undefined, bladebitDiskCache: undefined, bladebitDiskF1Threads: undefined, bladebitDiskFpThreads: undefined, @@ -202,6 +282,8 @@ export const madmaxDefaults: PlotterDefaults = { bladebitDiskAlternate: undefined, bladebitDiskNoT1Direct: undefined, bladebitDiskNoT2Direct: undefined, + bladebitDeviceIndex: undefined, + bladebitDisableDirectDownloads: undefined, disableBitfieldPlotting: undefined, parallel: false, delay: 0, diff --git a/packages/api/src/services/Daemon.ts b/packages/api/src/services/Daemon.ts index 3e60068204..a98d2c9701 100644 --- a/packages/api/src/services/Daemon.ts +++ b/packages/api/src/services/Daemon.ts @@ -128,9 +128,10 @@ export default class Daemon extends Service { } startPlotting(inputArgs: { - bladebitDisableNUMA: boolean; - bladebitWarmStart: boolean; + bladebitDisableNUMA?: boolean; + bladebitWarmStart?: boolean; bladebitNoCpuAffinity?: boolean; + bladebitCompressionLevel?: number; bladebitDiskCache?: number; bladebitDiskF1Threads?: number; bladebitDiskFpThreads?: number; @@ -140,6 +141,8 @@ export default class Daemon extends Service { bladebitDiskAlternate?: boolean; bladebitDiskNoT1Direct?: boolean; bladebitDiskNoT2Direct?: boolean; + bladebitDeviceIndex?: number; + bladebitDisableDirectDownloads?: boolean; c?: string; delay: number; disableBitfieldPlotting?: boolean; @@ -168,6 +171,7 @@ export default class Daemon extends Service { bladebitDisableNUMA: 'm', bladebitWarmStart: 'w', bladebitNoCpuAffinity: 'no_cpu_affinity', + bladebitCompressionLevel: 'compress', bladebitDiskCache: 'cache', bladebitDiskF1Threads: 'f1_threads', bladebitDiskFpThreads: 'fp_threads', @@ -177,6 +181,8 @@ export default class Daemon extends Service { bladebitDiskAlternate: 'alternate', bladebitDiskNoT1Direct: 'no_t1_direct', bladebitDiskNoT2Direct: 'no_t2_direct', + bladebitDeviceIndex: 'device', + bladebitDisableDirectDownloads: 'no_direct_downloads', disableBitfieldPlotting: 'e', excludeFinalDir: 'x', farmerPublicKey: 'f', @@ -229,4 +235,11 @@ export default class Daemon extends Service { getVersion() { return this.command<{ version: string }>('get_version'); } + + getKeysForPlotting(args?: { fingerprints?: number[] }) { + return this.command<{ keys: { [fingerprint: number]: { farmerPublicKey: string; poolPublicKey: string } } }>( + 'get_keys_for_plotting', + args + ); + } } diff --git a/packages/api/src/services/Farmer.ts b/packages/api/src/services/Farmer.ts index d00b061b94..2bea371825 100644 --- a/packages/api/src/services/Farmer.ts +++ b/packages/api/src/services/Farmer.ts @@ -1,7 +1,7 @@ import type Connection from '../@types/Connection'; -import type FarmingInfo from '../@types/FarmingInfo'; import Harvester, { type HarvesterSummary } from '../@types/Harvester'; import type HarvesterPlotsPaginated from '../@types/HarvesterPlotsPaginated'; +import type NewFarmingInfo from '../@types/NewFarmingInfo'; import type PoolState from '../@types/PoolState'; import type ProofOfSpace from '../@types/ProofOfSpace'; import type RewardTargets from '../@types/RewardTargets'; @@ -13,27 +13,200 @@ import Service from './Service'; import type { Options } from './Service'; const FARMING_INFO_MAX_ITEMS = 1000; + +// To reduce unnecessary data, utilize array for storing latency data +export type LatencyRecord = [number, number]; // [timestamp, latency] +export type LatencyInfo = { + latency: LatencyRecord[]; + avg: number; + min: number; + max: number; + latest: number; + totalPlots: number; +}; +export type LatencyData = { + [nodeId: string]: LatencyInfo; +}; +export type MissingSignagePointsRecord = [number, number]; // [timestamp, count of missing sps] + export default class Farmer extends Service { // last FARMING_INFO_MAX_ITEMS farming info - private farmingInfo: FarmingInfo[] = []; + private newFarmingInfo: NewFarmingInfo[] = []; + + private latencyData: LatencyData = {}; + + private missingSps: MissingSignagePointsRecord[] = []; + + private totalMissingSps: number = 0; + + private totalPlotFilterChallenge: number = 0; + + private totalPlotsPassingFilter: number = 0; + + private latestPartialStats = { + time: new Date(), + valid: 0, + stale: 0, + invalid: 0, + missing: 0, + }; + + // This is used to reset partial stats. + private partialStatsOffset = { + resetTime: new Date(), + valid: 0, + stale: 0, + invalid: 0, + missing: 0, + }; constructor(client: Client, options?: Options) { super(ServiceName.FARMER, client, options, async () => { - this.onNewFarmingInfo((data) => { + this.onNewFarmingInfo((data: { farmingInfo: NewFarmingInfo }) => { const { farmingInfo } = data; if (farmingInfo) { - this.farmingInfo = [farmingInfo, ...this.farmingInfo].slice(0, FARMING_INFO_MAX_ITEMS); + this.totalPlotFilterChallenge += farmingInfo.totalPlots; + this.totalPlotsPassingFilter += farmingInfo.passedFilter; + + this.newFarmingInfo = [farmingInfo, ...this.newFarmingInfo].slice(0, FARMING_INFO_MAX_ITEMS); + + // The Unit of Python's timestamp is seconds so converting it to milliseconds + // to make it easy to compare with js timestamps + const jsTimestamp = farmingInfo.timestamp * 1000; + const latencyRecords: LatencyRecord[] = [ + ...(this.latencyData[farmingInfo.nodeId] ? this.latencyData[farmingInfo.nodeId].latency : []), + [jsTimestamp, farmingInfo.lookupTime], + ]; - this.emit('farming_info_changed', this.farmingInfo, null); + const now = Date.now() / 1000; // Convert to seconds from milliseconds + let deleteStartIndex = -1; + let deleteCount = -1; + let latencySum = 0; + let latencyMax = 0; + let latencyMin = 0; + for (let i = 0; i < latencyRecords.length; i++) { + const d = latencyRecords[i]; + const [timestamp, latency] = d; + + // Retire records older than or equal to 24 hours + if (now - timestamp >= 86_400) { + if (deleteStartIndex === -1) { + deleteStartIndex = i; + deleteCount = 1; + } else { + deleteCount++; + } + } else { + latencySum += latency; + latencyMax = Math.max(latencyMax, latency); + latencyMin = Math.min(latencyMin, latency); + } + } + if (deleteStartIndex > -1 && deleteCount > -1) { + latencyRecords.splice(deleteStartIndex, deleteCount); + } + + const latencyInfo: LatencyInfo = { + latency: latencyRecords, + avg: latencySum / latencyRecords.length, + max: latencyMax, + min: latencyMin, + latest: farmingInfo.lookupTime, + totalPlots: farmingInfo.totalPlots, + }; + + this.latencyData = { + ...this.latencyData, + [farmingInfo.nodeId]: latencyInfo, + }; + + this.emit( + 'farming_info_changed', + { + newFarmingInfo: this.newFarmingInfo, + latencyData: this.latencyData, + }, + null + ); } }); + + this.onNewSignagePoint((data: { missingSignagePoints: MissingSignagePointsRecord }) => { + if (!data.missingSignagePoints) { + return; + } + const missingSps = [data.missingSignagePoints, ...this.missingSps]; + const now = Date.now() / 1000; // Convert to seconds from milliseconds + + let deletingIndex = -1; + for (let i = missingSps.length - 1; i >= 0; i--) { + const [timestamp] = missingSps[i]; + if (now - timestamp <= 86_400) { + break; + } else { + deletingIndex = i; + } + } + + this.totalMissingSps += +data.missingSignagePoints[1]; + + if (deletingIndex > -1) { + // Remove array items expired. + missingSps.splice(deletingIndex, this.missingSps.length - deletingIndex); + } + + this.missingSps = missingSps; + }); }); } - async getFarmingInfo() { + async getNewFarmingInfo() { + await this.whenReady(); + return { + newFarmingInfo: this.newFarmingInfo, + latencyData: this.latencyData, + }; + } + + async getMissingSignagePoints() { await this.whenReady(); - return this.farmingInfo; + return { + missingSignagePoints: this.missingSps, + totalMissingSps: this.totalMissingSps, + }; + } + + resetMissingSignagePoints() { + this.missingSps = []; + this.totalMissingSps = 0; + } + + getFilterChallengeStat(height: number) { + const n = this.totalPlotFilterChallenge; + const x = this.totalPlotsPassingFilter; + let fb = 9; // Filter bits + if (height < 5_496_000) { + fb = 9; + } else if (height < 10_542_000) { + fb = 8; + } else if (height < 15_592_000) { + fb = 7; + } else if (height < 20_643_000) { + fb = 6; + } else { + fb = 5; + } + return { + n, + x, + fb, + }; + } + + resetFilterChallengeStat() { + this.totalPlotFilterChallenge = 0; + this.totalPlotsPassingFilter = 0; } async getRewardTargets(args: { searchForPrivateKey: boolean }) { @@ -61,7 +234,29 @@ export default class Farmer extends Service { } async getPoolState() { - return this.command<{ poolState: PoolState[] }>('get_pool_state'); + const res = await this.command<{ poolState: PoolState[] }>('get_pool_state'); + this.latestPartialStats = { + time: new Date(), + valid: res.poolState.reduce((acc, val) => acc + val.validPartialsSinceStart, 0), + stale: res.poolState.reduce((acc, val) => acc + val.stalePartialsSinceStart, 0), + invalid: res.poolState.reduce((acc, val) => acc + val.invalidPartialsSinceStart, 0), + missing: res.poolState.reduce((acc, val) => acc + val.missingPartialsSinceStart, 0), + }; + return res; + } + + getPartialStatsOffset() { + return this.partialStatsOffset; + } + + resetPartialStats() { + this.partialStatsOffset = { + resetTime: new Date(), + valid: this.latestPartialStats.valid, + stale: this.latestPartialStats.stale, + invalid: this.latestPartialStats.invalid, + missing: this.latestPartialStats.missing, + }; } async setPayoutInstructions(args: { launcherId: string; payoutInstructions: string }) { @@ -135,4 +330,12 @@ export default class Farmer extends Service { onFarmingInfoChanged(callback: (data: any, message?: Message) => void, processData?: (data: any) => any) { return this.onCommand('farming_info_changed', callback, processData); } + + onSubmittedPartial(callback: (data: any, message?: Message) => void, processData?: (data: any) => any) { + return this.onCommand('submitted_partial', callback, processData); + } + + onFailedPartial(callback: (data: any, message?: Message) => void, processData?: (data: any) => any) { + return this.onCommand('failed_partial', callback, processData); + } } diff --git a/packages/api/src/services/Harvester.ts b/packages/api/src/services/Harvester.ts index b30aec13ab..cfd0075f45 100644 --- a/packages/api/src/services/Harvester.ts +++ b/packages/api/src/services/Harvester.ts @@ -1,12 +1,44 @@ +import type FarmingInfo from '../@types/FarmingInfo'; import Client from '../Client'; import type Message from '../Message'; import ServiceName from '../constants/ServiceName'; import Service from './Service'; import type { Options } from './Service'; +const FARMING_INFO_MAX_ITEMS = 1000; +export type FarmingInfoWithIndex = FarmingInfo & { index: number }; +export type HarvesterConfig = { + useGpuHarvesting: boolean | null; + gpuIndex: number | null; + enforceGpuIndex: boolean | null; + disableCpuAffinity: boolean | null; + parallelDecompressorCount: number | null; + decompressorThreadCount: number | null; + recursivePlotScan: boolean | null; + refreshParameterIntervalSeconds: number | null; +}; + export default class Harvester extends Service { + private farmingInfo: FarmingInfoWithIndex[] = []; + + private farmingInfoIndex = 0; + constructor(client: Client, options?: Options) { - super(ServiceName.HARVESTER, client, options); + super(ServiceName.HARVESTER, client, options, async () => { + this.onFarmingInfo((data) => { + const dataWithIndex: FarmingInfoWithIndex = { + ...data, + index: this.farmingInfoIndex++, + }; + this.farmingInfo = [dataWithIndex, ...this.farmingInfo].slice(0, FARMING_INFO_MAX_ITEMS); + this.emit('farming_info_changed', this.farmingInfo, null); + }); + }); + } + + async getFarmingInfo() { + await this.whenReady(); + return this.farmingInfo; } async refreshPlots() { @@ -29,7 +61,32 @@ export default class Harvester extends Service { return this.command('remove_plot_directory', args); } + async getHarvesterConfig() { + return this.command('get_harvester_config'); + } + + async updateHarvesterConfig(args: { + useGpuHarvesting?: boolean; + gpuIndex?: number; + enforceGpuIndex?: boolean; + disableCpuAffinity?: boolean; + parallelDecompressorCount?: number; + decompressorThreadCount?: number; + recursivePlotScan?: boolean; + refreshParameterIntervalSeconds?: number; + }) { + return this.command('update_harvester_config', args); + } + onRefreshPlots(callback: (data: any, message: Message) => void, processData?: (data: any) => any) { return this.onCommand('refresh_plots', callback, processData); } + + onFarmingInfo(callback: (data: any, message: Message) => void, processData?: (data: any) => any) { + return this.onCommand('farming_info', callback, processData); + } + + onFarmingInfoChanged(callback: (data: any, message: Message) => void, processData?: (data: any) => any) { + return this.onCommand('farming_info_changed', callback, processData); + } } diff --git a/packages/api/src/services/index.ts b/packages/api/src/services/index.ts index d34f0f0c37..b90549fa89 100644 --- a/packages/api/src/services/index.ts +++ b/packages/api/src/services/index.ts @@ -1,8 +1,10 @@ export { default as Daemon } from './Daemon'; export { default as Events } from './Events'; export { default as Farmer } from './Farmer'; +export type { LatencyData, LatencyInfo, LatencyRecord } from './Farmer'; export { default as FullNode } from './FullNode'; export { default as Harvester } from './Harvester'; +export type { FarmingInfoWithIndex, HarvesterConfig } from './Harvester'; export { default as PlotterService } from './PlotterService'; export { default as Service } from './Service'; export { default as WalletService } from './WalletService'; diff --git a/packages/api/src/utils/defaultsForPlotter.ts b/packages/api/src/utils/defaultsForPlotter.ts index 0ff8ee241d..0cfa8e0d8a 100644 --- a/packages/api/src/utils/defaultsForPlotter.ts +++ b/packages/api/src/utils/defaultsForPlotter.ts @@ -1,6 +1,12 @@ import { PlotterDefaults } from '../@types/Plotter'; import PlotterName from '../constants/PlotterName'; -import { bladebitRamDefaults, bladebitDiskDefaults, madmaxDefaults, chiaposDefaults } from '../constants/Plotters'; +import { + bladebitRamDefaults, + bladebitDiskDefaults, + bladebitCudaDefaults, + madmaxDefaults, + chiaposDefaults, +} from '../constants/Plotters'; export default function defaultsForPlotter(plotterName: PlotterName): PlotterDefaults { switch (plotterName) { @@ -8,6 +14,8 @@ export default function defaultsForPlotter(plotterName: PlotterName): PlotterDef return bladebitRamDefaults; case PlotterName.BLADEBIT_DISK: return bladebitDiskDefaults; + case PlotterName.BLADEBIT_CUDA: + return bladebitCudaDefaults; case PlotterName.MADMAX: return madmaxDefaults; case PlotterName.CHIAPOS: // fallthrough diff --git a/packages/api/src/utils/optionsForPlotter.ts b/packages/api/src/utils/optionsForPlotter.ts index 0bd2a7766b..53579f3cd5 100644 --- a/packages/api/src/utils/optionsForPlotter.ts +++ b/packages/api/src/utils/optionsForPlotter.ts @@ -1,6 +1,12 @@ import { PlotterOptions } from '../@types/Plotter'; import PlotterName from '../constants/PlotterName'; -import { bladebitRamOptions, bladebitDiskOptions, madmaxOptions, chiaposOptions } from '../constants/Plotters'; +import { + bladebitRamOptions, + bladebitDiskOptions, + bladebitCudaOptions, + madmaxOptions, + chiaposOptions, +} from '../constants/Plotters'; export default function optionsForPlotter(plotterName: PlotterName): PlotterOptions { switch (plotterName) { @@ -8,6 +14,8 @@ export default function optionsForPlotter(plotterName: PlotterName): PlotterOpti return bladebitRamOptions; case PlotterName.BLADEBIT_DISK: return bladebitDiskOptions; + case PlotterName.BLADEBIT_CUDA: + return bladebitCudaOptions; case PlotterName.MADMAX: return madmaxOptions; case PlotterName.CHIAPOS: // fallthrough diff --git a/packages/core/src/components/CardStep/CardStep.tsx b/packages/core/src/components/CardStep/CardStep.tsx index 0aae96ece3..2c6793382d 100644 --- a/packages/core/src/components/CardStep/CardStep.tsx +++ b/packages/core/src/components/CardStep/CardStep.tsx @@ -6,24 +6,26 @@ import Flex from '../Flex'; type Props = { children: ReactNode; title: ReactNode; - step: ReactNode; + step?: ReactNode; action?: ReactNode; }; export default function CardStep(props: Props) { const { children, step, title, action } = props; + const avatar = React.useMemo(() => { + if (step === undefined) { + return undefined; + } + return ( + + {step} + + ); + }, [step]); return ( - - {step} - - } - title={{title}} - action={action} - /> + {title}} action={action} /> diff --git a/packages/core/src/components/FormatBytes/FormatBytes.tsx b/packages/core/src/components/FormatBytes/FormatBytes.tsx index cd74f53858..20f94f5929 100644 --- a/packages/core/src/components/FormatBytes/FormatBytes.tsx +++ b/packages/core/src/components/FormatBytes/FormatBytes.tsx @@ -22,10 +22,11 @@ type Props = { precision?: number; removeUnit?: boolean; fixedDecimals?: boolean; + effectiveSize?: boolean; }; export default function FormatBytes(props: Props) { - const { value, precision, unit, removeUnit, unitSeparator = ' ', fixedDecimals } = props; + const { value, precision, unit, removeUnit, unitSeparator = ' ', fixedDecimals, effectiveSize } = props; if (value === null || value === undefined) { return null; @@ -60,7 +61,7 @@ export default function FormatBytes(props: Props) { humanValue = humanValue.decimalPlaces(precision ?? 2); } - if (precision || fixedDecimals) { + if (typeof precision === 'number' || fixedDecimals) { humanValue = humanValue.toFixed(precision ?? 2); } else { humanValue = humanValue.toString(); @@ -70,6 +71,10 @@ export default function FormatBytes(props: Props) { return humanValue; } + if (effectiveSize && humanUnit) { + humanUnit += 'e'; + } + return ( <> {humanValue} diff --git a/packages/gui/package.json b/packages/gui/package.json index 0eb81103fa..9194d66aee 100644 --- a/packages/gui/package.json +++ b/packages/gui/package.json @@ -165,6 +165,7 @@ "bech32": "2.0.0", "bignumber.js": "9.0.2", "byte-size": "8.1.0", + "chart.js": "^4.3.0", "chokidar": "3.5.3", "core-js": "3.20.3", "crypto-browserify": "3.12.0", @@ -183,6 +184,7 @@ "moment": "2.29.4", "normalize-url": "7.0.3", "react": "17.0.2", + "react-chartjs-2": "5.2.0", "react-dom": "17.0.2", "react-dropzone": "11.5.1", "react-hook-form": "7.41.5", diff --git a/packages/gui/src/components/app/AppRouter.tsx b/packages/gui/src/components/app/AppRouter.tsx index 273935d285..dca973298c 100644 --- a/packages/gui/src/components/app/AppRouter.tsx +++ b/packages/gui/src/components/app/AppRouter.tsx @@ -7,6 +7,7 @@ import Block from '../block/Block'; import DashboardSideBar from '../dashboard/DashboardSideBar'; import Farm from '../farm/Farm'; import FullNode from '../fullNode/FullNode'; +import Harvest from '../harvest/Harvest'; import NFTs from '../nfts/NFTs'; import { CreateOffer } from '../offers/OfferManager'; import Plot from '../plot/Plot'; @@ -52,6 +53,7 @@ export default function AppRouter() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/packages/gui/src/components/dashboard/DashboardSideBar.tsx b/packages/gui/src/components/dashboard/DashboardSideBar.tsx index 7369925e55..c269c678ba 100644 --- a/packages/gui/src/components/dashboard/DashboardSideBar.tsx +++ b/packages/gui/src/components/dashboard/DashboardSideBar.tsx @@ -1,8 +1,9 @@ import { useLocalStorage } from '@chia-network/api-react'; import { Flex, SideBarItem } from '@chia-network/core'; import { - Farming as FarmingIcon, + Farm as FarmIcon, FullNode as FullNodeIcon, + Harvest as HarvestIcon, Plots as PlotsIcon, Pooling as PoolingIcon, NFTs as NFTsIcon, @@ -88,12 +89,24 @@ export default function DashboardSideBar(props: DashboardSideBarProps) { data-testid="DashboardSideBar-fullnode" end /> + Farm} + data-testid="DashboardSideBar-farming" + /> Plots} data-testid="DashboardSideBar-plots" /> + Harvest} + data-testid="DashboardSideBar-harvest" + /> {/* } Wallets} /> */} - - Farming} - data-testid="DashboardSideBar-farming" - /> - - + + + + + + - diff --git a/packages/gui/src/components/farm/FarmHeader.tsx b/packages/gui/src/components/farm/FarmHeader.tsx index 00e8286b03..c56075ffcd 100644 --- a/packages/gui/src/components/farm/FarmHeader.tsx +++ b/packages/gui/src/components/farm/FarmHeader.tsx @@ -18,7 +18,7 @@ export default function FarmHeader() { - Your Farm Overview + Farm Summary diff --git a/packages/gui/src/components/farm/FarmHealth.tsx b/packages/gui/src/components/farm/FarmHealth.tsx new file mode 100644 index 0000000000..cea8bfd2a0 --- /dev/null +++ b/packages/gui/src/components/farm/FarmHealth.tsx @@ -0,0 +1,405 @@ +import { + useGetFilterChallengeStatQuery, + useGetMissingSignagePointsQuery, + useGetPoolStateQuery, + useResetMissingSignagePointsMutation, + useResetFilterChallengeStatMutation, + useGetPartialStatsOffsetQuery, +} from '@chia-network/api-react'; +import { Flex, StateIndicator, State, Tooltip } from '@chia-network/core'; +import { Trans } from '@lingui/macro'; +import { Box, Button, Paper, Typography, CircularProgress } from '@mui/material'; +import React from 'react'; +import styled from 'styled-components'; + +import FarmerStatus from '../../constants/FarmerStatus'; +import useFarmerStatus from '../../hooks/useFarmerStatus'; +import { binomialProb } from '../../util/math'; + +const StyledTable = styled.table` + border-collapse: collapse; + tr:not(:last-child) td:first-child { + padding-right: 8px; + } + td { + vertical-align: top; + } + tr:not(:first-child) td { + border-top: 1px solid #ccc; + } + tr:not(:last-child) td { + padding: 4px 8px; + } + tr:last-child td { + padding-top: 4px; + & > div { + display: flex; + justify-content: flex-end; + } + } +`; + +const StyledInput = styled.input` + font-size: 0.6875rem; + color: #fff; + width: 38px; + background: transparent; + border: none; + padding: 0; + display: inline-block; +`; + +const indicatorStyle = { + marginTop: 1, + '> div > div': { + display: 'inline-flex', + }, + '.cancel-icon': { + g: { + circle: { + stroke: '#D32F2F', + fill: '#D32F2F', + }, + }, + }, + '.checkmark-icon': { + g: { + circle: { + stroke: '#3AAC59', + fill: '#3AAC59', + }, + path: { + stroke: '#3AAC59', + fill: '#3AAC59', + }, + }, + }, + '.reload-icon': { + g: { + circle: { + stroke: '#FF9800', + fill: '#FF9800', + }, + path: { + fill: '#FF9800', + }, + }, + }, +}; + +export default React.memo(FarmHealth); +function FarmHealth() { + const { farmerStatus, blockchainState } = useFarmerStatus(); + const { data: missingSpsData, isLoading: isLoadingMissingSps } = useGetMissingSignagePointsQuery(); + const [resetMissingSps] = useResetMissingSignagePointsMutation(); + const { data: poolStateData, isLoading: isLoadingPoolStateData } = useGetPoolStateQuery(); + const { data: filterChallengeStat, isLoading: isLoadingFilterChallengeStat } = useGetFilterChallengeStatQuery( + blockchainState?.peak.height || 0 + ); + const [resetFilterChallengeStat] = useResetFilterChallengeStatMutation(); + const { data: partialStatsOffset, isLoading: isLoadingPartialStatsOffset } = useGetPartialStatsOffsetQuery(); + const [significantLevel, setSignificantLevel] = React.useState(1); // 1% + + const famSyncStatus = React.useMemo(() => { + if (farmerStatus === FarmerStatus.SYNCHING) { + return ( + + Syncing + + ); + } + + if (farmerStatus === FarmerStatus.NOT_AVAILABLE) { + return ( + + Not available + + ); + } + + if (farmerStatus === FarmerStatus.NOT_CONNECTED) { + return ( + + Not connected + + ); + } + + if (farmerStatus === FarmerStatus.NOT_RUNNING) { + return ( + + Not running + + ); + } + + return ( + + Synced + + ); + }, [farmerStatus]); + + const cumulativeBinomialProbability = React.useMemo(() => { + if (!filterChallengeStat) { + return null; + } + return binomialProb(filterChallengeStat.n, 1 / 2 ** filterChallengeStat.fb, filterChallengeStat.x); + }, [filterChallengeStat]); + + const plotsPassingFilter = React.useMemo(() => { + if (farmerStatus === FarmerStatus.SYNCHING) { + return ( + + Syncing + + ); + } + + if (farmerStatus === FarmerStatus.NOT_AVAILABLE || isLoadingFilterChallengeStat || !blockchainState) { + return ( + + Not available + + ); + } + + if (farmerStatus === FarmerStatus.NOT_CONNECTED) { + return ( + + Not connected + + ); + } + + if (farmerStatus === FarmerStatus.NOT_RUNNING) { + return ( + + Not running + + ); + } + + if (isLoadingFilterChallengeStat || cumulativeBinomialProbability === null) { + return ( + + Not available + + ); + } + + if (cumulativeBinomialProbability < significantLevel / 100.0) { + return ( + + Warning + + ); + } + + return ( + + OK + + ); + }, [farmerStatus, isLoadingFilterChallengeStat, blockchainState, cumulativeBinomialProbability, significantLevel]); + + const plotPassingFilterWithTooltip = React.useMemo(() => { + if (isLoadingFilterChallengeStat || !filterChallengeStat) { + return ( + + + Plots passing filter + + {plotsPassingFilter} + + ); + } + const displayPercentage = + cumulativeBinomialProbability !== null ? (cumulativeBinomialProbability * 100).toFixed(3) : '-'; + const expectedPlotsPassingFilter = + Math.round(filterChallengeStat.n * (1 / 2 ** filterChallengeStat.fb) * 1000) / 1000; + const tooltipTitle = ( + + + + + Total plot filter challenges + + {filterChallengeStat.n} + + + + Total plots passing filter + + {filterChallengeStat.x} + + + + Plot pass ratio + + 1 / {2 ** filterChallengeStat.fb} + + + + Expected Total plots passing filter + + {expectedPlotsPassingFilter} + + + + Probability where {filterChallengeStat.x} or fewer plots +
+ passing filter in {filterChallengeStat.n} challenges + + {displayPercentage} % + + + + Warning Threshold + + + { + setSignificantLevel(+e.target.value); + }} + /> + % + + + + +
+ +
+ + + +
+ ); + + return ( + + + + Plots passing filter + + {plotsPassingFilter} + + + ); + }, [ + plotsPassingFilter, + isLoadingFilterChallengeStat, + filterChallengeStat, + cumulativeBinomialProbability, + resetFilterChallengeStat, + significantLevel, + ]); + + const missingSpsWithTooltip = React.useMemo(() => { + if (isLoadingMissingSps) { + return ; + } + if (!missingSpsData?.totalMissingSps) { + return ( + + + Missing signage point + + + None + + + ); + } + + const tooltipTitle = ( + + ); + + return ( + + + + Missing signage point + + + {missingSpsData.totalMissingSps} + + + + ); + }, [missingSpsData, isLoadingMissingSps, resetMissingSps]); + + const stalePartials = React.useMemo(() => { + if (isLoadingPoolStateData) { + return ; + } + if (!poolStateData || poolStateData.length === 0) { + return ( + + None + + ); + } + + let stalePartialsSinceStart = 0; + for (let i = 0; i < poolStateData.length; i++) { + const d = poolStateData[i]; + stalePartialsSinceStart += d.stalePartialsSinceStart; + } + + if (!isLoadingPartialStatsOffset && partialStatsOffset) { + stalePartialsSinceStart -= partialStatsOffset.stale; + } + + if (stalePartialsSinceStart === 0) { + return ( + + None + + ); + } + + return ( + + {stalePartialsSinceStart} + + ); + }, [poolStateData, isLoadingPoolStateData, isLoadingPartialStatsOffset, partialStatsOffset]); + + return ( + + + + Farm Health + + + + + + Sync status + + {famSyncStatus} + + {plotPassingFilterWithTooltip} + {missingSpsWithTooltip} + + + Stale partials + + {stalePartials} + + + + ); +} diff --git a/packages/gui/src/components/farm/FarmLastAttemptedProof.tsx b/packages/gui/src/components/farm/FarmLastAttemptedProof.tsx index e3537d19ea..f397d7ed17 100644 --- a/packages/gui/src/components/farm/FarmLastAttemptedProof.tsx +++ b/packages/gui/src/components/farm/FarmLastAttemptedProof.tsx @@ -1,4 +1,4 @@ -import { useGetFarmingInfoQuery } from '@chia-network/api-react'; +import { useGetNewFarmingInfoQuery } from '@chia-network/api-react'; import { Link, Table, Card } from '@chia-network/core'; import { Trans } from '@lingui/macro'; import moment from 'moment'; @@ -35,9 +35,14 @@ const cols = [ export default function FarmLastAttemptedProof() { // const { size } = usePlots(); - const { data: lastAttemptedProof, isLoading } = useGetFarmingInfoQuery(); + const { data, isLoading } = useGetNewFarmingInfoQuery(); - const reducedLastAttemptedProof = lastAttemptedProof?.slice(0, 5); + const reducedLastAttemptedProof = React.useMemo(() => { + if (!data) { + return data; + } + return data.newFarmingInfo.slice(0, 5); + }, [data]); const isEmpty = !reducedLastAttemptedProof?.length; return ( diff --git a/packages/gui/src/components/farm/PoolingHealth.tsx b/packages/gui/src/components/farm/PoolingHealth.tsx new file mode 100644 index 0000000000..ace241e2fa --- /dev/null +++ b/packages/gui/src/components/farm/PoolingHealth.tsx @@ -0,0 +1,284 @@ +import { + useGetPoolStateQuery, + useGetPartialStatsOffsetQuery, + useResetPartialStatsMutation, +} from '@chia-network/api-react'; +import { Flex, StateIndicator, State, Tooltip } from '@chia-network/core'; +import { Trans } from '@lingui/macro'; +import { Box, Paper, Typography, CircularProgress, Button } from '@mui/material'; +import React from 'react'; + +const indicatorStyle = { + marginTop: 1, + '> div > div': { + display: 'inline-flex', + }, + '.cancel-icon': { + g: { + circle: { + stroke: '#D32F2F', + fill: '#D32F2F', + }, + }, + }, + '.checkmark-icon': { + g: { + circle: { + stroke: '#3AAC59', + fill: '#3AAC59', + }, + path: { + stroke: '#3AAC59', + fill: '#3AAC59', + }, + }, + }, + '.reload-icon': { + g: { + circle: { + stroke: '#FF9800', + fill: '#FF9800', + }, + path: { + fill: '#FF9800', + }, + }, + }, +}; + +export default React.memo(PoolingHealth); +function PoolingHealth() { + const { data, isLoading } = useGetPoolStateQuery(); + const { data: partialStatsOffset, isLoading: isLoadingPartialStatsOffset } = useGetPartialStatsOffsetQuery(); + const [resetPartialStatsOffset] = useResetPartialStatsMutation(); + + const totalPartials = React.useMemo(() => { + if (!data) { + return 0; + } + let value = 0; + for (let i = 0; i < data.length; i++) { + const d = data[i]; + value += + d.validPartialsSinceStart + + d.invalidPartialsSinceStart + + d.missingPartialsSinceStart + + d.stalePartialsSinceStart; + } + if (!isLoadingPartialStatsOffset && partialStatsOffset) { + value -= + partialStatsOffset.valid + partialStatsOffset.stale + partialStatsOffset.invalid + partialStatsOffset.missing; + } + return value; + }, [data, isLoadingPartialStatsOffset, partialStatsOffset]); + + const validPartials = React.useMemo(() => { + if (isLoading || !data) { + return ; + } + if (!data || data.length === 0) { + return ( + + None + + ); + } + + let value = 0; + for (let i = 0; i < data.length; i++) { + const d = data[i]; + value += d.validPartialsSinceStart; + } + + if (!isLoadingPartialStatsOffset && partialStatsOffset) { + value -= partialStatsOffset.valid; + } + + const rate = Math.floor((value / totalPartials) * 1000) / 10; + if (value === totalPartials) { + return ( + + 100% {value} / {totalPartials} + + ); + } + + return ( + + {rate}% {value} / {totalPartials} + + ); + }, [isLoading, data, totalPartials, isLoadingPartialStatsOffset, partialStatsOffset]); + + const invalidPartials = React.useMemo(() => { + if (isLoading || !data) { + return ; + } + if (!data || data.length === 0) { + return ( + + None + + ); + } + + let value = 0; + for (let i = 0; i < data.length; i++) { + const d = data[i]; + value += d.invalidPartialsSinceStart; + } + if (!isLoadingPartialStatsOffset && partialStatsOffset) { + value -= partialStatsOffset.invalid; + } + + const rate = Math.floor((value / totalPartials) * 1000) / 10; + if (value === 0) { + return ( + + None {value} / {totalPartials} + + ); + } + + return ( + + {rate}% {value} / {totalPartials} + + ); + }, [isLoading, data, totalPartials, isLoadingPartialStatsOffset, partialStatsOffset]); + + const missingPartials = React.useMemo(() => { + if (isLoading || !data) { + return ; + } + if (!data || data.length === 0) { + return ( + + None + + ); + } + + let value = 0; + for (let i = 0; i < data.length; i++) { + const d = data[i]; + value += d.missingPartialsSinceStart; + } + if (!isLoadingPartialStatsOffset && partialStatsOffset) { + value -= partialStatsOffset.missing; + } + + const rate = Math.floor((value / totalPartials) * 1000) / 10; + if (value === 0) { + return ( + + None {value} / {totalPartials} + + ); + } + + return ( + + {rate}% {value} / {totalPartials} + + ); + }, [isLoading, data, totalPartials, isLoadingPartialStatsOffset, partialStatsOffset]); + + const stalePartials = React.useMemo(() => { + if (isLoading || !data) { + return ; + } + if (!data || data.length === 0) { + return ( + + None + + ); + } + + let value = 0; + for (let i = 0; i < data.length; i++) { + const d = data[i]; + value += d.stalePartialsSinceStart; + } + if (!isLoadingPartialStatsOffset && partialStatsOffset) { + value -= partialStatsOffset.stale; + } + + const rate = Math.floor((value / totalPartials) * 1000) / 10; + if (value === 0) { + return ( + + None {value} / {totalPartials} + + ); + } + + return ( + + {rate}% {value} / {totalPartials} + + ); + }, [isLoading, data, totalPartials, isLoadingPartialStatsOffset, partialStatsOffset]); + + const onClickResetButton = React.useCallback(() => { + resetPartialStatsOffset(); + }, [resetPartialStatsOffset]); + + return ( + + + Pooling + + + + + Pooling Health + + + + + Partials successfully sent and acknowledged by pools}> + + + Valid Partials + + {validPartials} + + + Partials sent to pools but too late}> + + + Stale Partials + + {stalePartials} + + + Partials not good enough or rejected by pools}> + + + Invalid partials + + {invalidPartials} + + + + Partials found but not sent to pools. This usually happens when a partial is found before connections to + pools are established + + } + > + + + Missing partials + + {missingPartials} + + + + + + ); +} diff --git a/packages/gui/src/components/farm/card/EstimatedEarning.tsx b/packages/gui/src/components/farm/card/EstimatedEarning.tsx new file mode 100644 index 0000000000..33f3954036 --- /dev/null +++ b/packages/gui/src/components/farm/card/EstimatedEarning.tsx @@ -0,0 +1,76 @@ +import { useGetBlockchainStateQuery, useGetTotalHarvestersSummaryQuery } from '@chia-network/api-react'; +import { State, CardSimple } from '@chia-network/core'; +import { Trans } from '@lingui/macro'; +import BigNumber from 'bignumber.js'; +import moment from 'moment'; +import React, { useMemo } from 'react'; + +import FullNodeState from '../../../constants/FullNodeState'; +import useFullNodeState from '../../../hooks/useFullNodeState'; +import FarmCardNotAvailable from './FarmCardNotAvailable'; + +type EstimatedEarningProps = { + period: 'daily' | 'monthly'; +}; + +export default React.memo(EstimatedEarning); +function EstimatedEarning(props: EstimatedEarningProps) { + const { period } = props; + const { state: fullNodeState } = useFullNodeState(); + + const { data, isLoading: isLoadingBlockchainState, error: errorBlockchainState } = useGetBlockchainStateQuery(); + const { + totalEffectivePlotSize, + isLoading: isLoadingTotalHarvesterSummary, + error: errorLoadingPlots, + } = useGetTotalHarvestersSummaryQuery(); + + const isLoading = isLoadingBlockchainState || isLoadingTotalHarvesterSummary; + const error = errorBlockchainState || errorLoadingPlots; + + const totalNetworkSpace = useMemo(() => new BigNumber(data?.space ?? 0), [data]); + + const proportion = useMemo(() => { + if (isLoading || totalNetworkSpace.isZero()) { + return new BigNumber(0); + } + + return totalEffectivePlotSize.div(totalNetworkSpace); + }, [isLoading, totalEffectivePlotSize, totalNetworkSpace]); + + const expectedTimeToWin = React.useMemo(() => { + if (fullNodeState !== FullNodeState.SYNCED || !data) { + return null; + } + + const averageBlockMinutes = data.averageBlockTime / 60; + const minutes = !proportion.isZero() ? new BigNumber(averageBlockMinutes).div(proportion) : new BigNumber(0); + + return moment + .duration({ + minutes: minutes.toNumber(), + }) + .humanize(); + }, [proportion, data, fullNodeState]); + + if (fullNodeState !== FullNodeState.SYNCED) { + const state = fullNodeState === FullNodeState.SYNCHING ? State.WARNING : undefined; + + return Estimated Time to Win} state={state} />; + } + + return ( + Estimated daily XCH : Estimated monthly XCH} + value={`${expectedTimeToWin}`} + tooltip={ + + You have {(proportion * 100).toFixed(4)}% of the space on the network, so farming a block will take{' '} + {expectedTimeToWin} in expectation. Actual results may take 3 to 4 times longer than this estimate. + + } + loading={isLoading} + error={error} + /> + ); +} diff --git a/packages/gui/src/components/farm/card/FarmCardExpectedTimeToWin.tsx b/packages/gui/src/components/farm/card/ExpectedTimeToWin.tsx similarity index 73% rename from packages/gui/src/components/farm/card/FarmCardExpectedTimeToWin.tsx rename to packages/gui/src/components/farm/card/ExpectedTimeToWin.tsx index a9f5562b87..8344da71c5 100644 --- a/packages/gui/src/components/farm/card/FarmCardExpectedTimeToWin.tsx +++ b/packages/gui/src/components/farm/card/ExpectedTimeToWin.tsx @@ -9,14 +9,13 @@ import FullNodeState from '../../../constants/FullNodeState'; import useFullNodeState from '../../../hooks/useFullNodeState'; import FarmCardNotAvailable from './FarmCardNotAvailable'; -const MINUTES_PER_BLOCK = (24 * 60) / 4608; // 0.3125 - -export default function FarmCardExpectedTimeToWin() { +export default React.memo(ExpectedTimeToWin); +function ExpectedTimeToWin() { const { state: fullNodeState } = useFullNodeState(); const { data, isLoading: isLoadingBlockchainState, error: errorBlockchainState } = useGetBlockchainStateQuery(); const { - totalPlotSize, + totalEffectivePlotSize, isLoading: isLoadingTotalHarvesterSummary, error: errorLoadingPlots, } = useGetTotalHarvestersSummaryQuery(); @@ -31,16 +30,23 @@ export default function FarmCardExpectedTimeToWin() { return new BigNumber(0); } - return totalPlotSize.div(totalNetworkSpace); - }, [isLoading, totalPlotSize, totalNetworkSpace]); + return totalEffectivePlotSize.div(totalNetworkSpace); + }, [isLoading, totalEffectivePlotSize, totalNetworkSpace]); + + const expectedTimeToWin = React.useMemo(() => { + if (fullNodeState !== FullNodeState.SYNCED || !data) { + return null; + } - const minutes = !proportion.isZero() ? new BigNumber(MINUTES_PER_BLOCK).div(proportion) : new BigNumber(0); + const averageBlockMinutes = data.averageBlockTime / 60; + const minutes = !proportion.isZero() ? new BigNumber(averageBlockMinutes).div(proportion) : new BigNumber(0); - const expectedTimeToWin = moment - .duration({ - minutes: minutes.toNumber(), - }) - .humanize(); + return moment + .duration({ + minutes: minutes.toNumber(), + }) + .humanize(); + }, [proportion, data, fullNodeState]); if (fullNodeState !== FullNodeState.SYNCED) { const state = fullNodeState === FullNodeState.SYNCHING ? State.WARNING : undefined; diff --git a/packages/gui/src/components/farm/card/FarmCardBlockRewards.tsx b/packages/gui/src/components/farm/card/FarmCardBlockRewards.tsx deleted file mode 100644 index bc85251876..0000000000 --- a/packages/gui/src/components/farm/card/FarmCardBlockRewards.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { useGetFarmedAmountQuery } from '@chia-network/api-react'; -import { useCurrencyCode, mojoToChiaLocaleString, CardSimple, useLocale } from '@chia-network/core'; -import { Trans } from '@lingui/macro'; -import BigNumber from 'bignumber.js'; -import React, { useMemo } from 'react'; - -export default function FarmCardBlockRewards() { - const currencyCode = useCurrencyCode(); - const [locale] = useLocale(); - const { data, isLoading, error } = useGetFarmedAmountQuery(); - - const farmerRewardAmount = data?.farmerRewardAmount; - const poolRewardAmount = data?.poolRewardAmount; - - const blockRewards = useMemo(() => { - if (farmerRewardAmount !== undefined && poolRewardAmount !== undefined) { - const val = new BigNumber(farmerRewardAmount).plus(new BigNumber(poolRewardAmount)); - - return ( - <> - {mojoToChiaLocaleString(val, locale)} -   - {currencyCode} - - ); - } - return undefined; - }, [farmerRewardAmount, poolRewardAmount, locale, currencyCode]); - - return ( - Block Rewards} - description={Without fees} - value={blockRewards} - loading={isLoading} - error={error} - /> - ); -} diff --git a/packages/gui/src/components/farm/card/FarmCardLastHeightFarmed.tsx b/packages/gui/src/components/farm/card/FarmCardLastHeightFarmed.tsx deleted file mode 100644 index 6f0064579b..0000000000 --- a/packages/gui/src/components/farm/card/FarmCardLastHeightFarmed.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useGetFarmedAmountQuery } from '@chia-network/api-react'; -import { FormatLargeNumber, CardSimple } from '@chia-network/core'; -import { Trans } from '@lingui/macro'; -import React from 'react'; - -export default function FarmCardLastHeightFarmed() { - const { data, isLoading, error } = useGetFarmedAmountQuery(); - - const lastHeightFarmed = data?.lastHeightFarmed; - - return ( - Last Height Farmed} - value={} - description={!lastHeightFarmed && No blocks farmed yet} - loading={isLoading} - error={error} - /> - ); -} diff --git a/packages/gui/src/components/farm/card/FarmCardPlotCount.tsx b/packages/gui/src/components/farm/card/FarmCardPlotCount.tsx deleted file mode 100644 index 89c50aeb22..0000000000 --- a/packages/gui/src/components/farm/card/FarmCardPlotCount.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { useGetTotalHarvestersSummaryQuery } from '@chia-network/api-react'; -import { FormatLargeNumber, CardSimple } from '@chia-network/core'; -import { Trans } from '@lingui/macro'; -import React from 'react'; - -export default function FarmCardPlotCount() { - const { plots, isLoading } = useGetTotalHarvestersSummaryQuery(); - - return ( - Plot Count} value={} loading={isLoading} /> - ); -} diff --git a/packages/gui/src/components/farm/card/FarmCardStatus.tsx b/packages/gui/src/components/farm/card/FarmCardStatus.tsx deleted file mode 100644 index 43627f8157..0000000000 --- a/packages/gui/src/components/farm/card/FarmCardStatus.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { StateIndicator, State, CardSimple } from '@chia-network/core'; -import { Trans } from '@lingui/macro'; -import React from 'react'; - -import FarmerStatus from '../../../constants/FarmerStatus'; -import useFarmerStatus from '../../../hooks/useFarmerStatus'; -import FarmCardNotAvailable from './FarmCardNotAvailable'; - -export default function FarmCardStatus() { - const farmerStatus = useFarmerStatus(); - - if (farmerStatus === FarmerStatus.SYNCHING) { - return ( - Farming Status} - value={ - - Syncing - - } - /> - ); - } - - if (farmerStatus === FarmerStatus.NOT_AVAILABLE) { - return Farming Status} />; - } - - if (farmerStatus === FarmerStatus.NOT_CONNECTED) { - return ( - Farming Status} - value={ - - Error - - } - description={Farmer is not connected} - /> - ); - } - - if (farmerStatus === FarmerStatus.NOT_RUNNING) { - return ( - Farming Status} - value={ - - Error - - } - description={Farmer is not running} - /> - ); - } - - return ( - Farming Status} - value={ - - Farming - - } - /> - ); -} diff --git a/packages/gui/src/components/farm/card/FarmCardTotalChiaFarmed.tsx b/packages/gui/src/components/farm/card/FarmCardTotalChiaFarmed.tsx deleted file mode 100644 index a562a5dbcc..0000000000 --- a/packages/gui/src/components/farm/card/FarmCardTotalChiaFarmed.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useGetFarmedAmountQuery } from '@chia-network/api-react'; -import { useCurrencyCode, mojoToChiaLocaleString, CardSimple, useLocale } from '@chia-network/core'; -import { Trans } from '@lingui/macro'; -import React, { useMemo } from 'react'; - -export default function FarmCardTotalChiaFarmed() { - const currencyCode = useCurrencyCode(); - const [locale] = useLocale(); - const { data, isLoading, error } = useGetFarmedAmountQuery(); - - const farmedAmount = data?.farmedAmount; - - const totalChiaFarmed = useMemo(() => { - if (farmedAmount !== undefined) { - return ( - <> - {mojoToChiaLocaleString(farmedAmount, locale)} -   - {currencyCode} - - ); - } - return undefined; - }, [farmedAmount, locale, currencyCode]); - - return ( - Total Chia Farmed} value={totalChiaFarmed} loading={isLoading} error={error} /> - ); -} diff --git a/packages/gui/src/components/farm/card/FarmCardTotalSizeOfPlots.tsx b/packages/gui/src/components/farm/card/FarmCardTotalSizeOfPlots.tsx deleted file mode 100644 index 435688b762..0000000000 --- a/packages/gui/src/components/farm/card/FarmCardTotalSizeOfPlots.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { useGetTotalHarvestersSummaryQuery } from '@chia-network/api-react'; -import { FormatBytes, CardSimple } from '@chia-network/core'; -import { Trans } from '@lingui/macro'; -import React from 'react'; - -export default function FarmCardTotalSizeOfPlots() { - const { totalPlotSize, isLoading } = useGetTotalHarvestersSummaryQuery(); - - return ( - Total Size of Plots} - value={} - loading={isLoading} - /> - ); -} diff --git a/packages/gui/src/components/farm/card/FarmCardUserFees.tsx b/packages/gui/src/components/farm/card/FarmCardUserFees.tsx deleted file mode 100644 index 449aa8bf7d..0000000000 --- a/packages/gui/src/components/farm/card/FarmCardUserFees.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { useGetFarmedAmountQuery } from '@chia-network/api-react'; -import { useCurrencyCode, mojoToChiaLocaleString, CardSimple, useLocale } from '@chia-network/core'; -import { Trans } from '@lingui/macro'; -import React, { useMemo } from 'react'; - -export default function FarmCardUserFees() { - const currencyCode = useCurrencyCode(); - const [locale] = useLocale(); - const { data, isLoading, error } = useGetFarmedAmountQuery(); - - const feeAmount = data?.feeAmount; - - const userTransactionFees = useMemo(() => { - if (feeAmount !== undefined) { - return ( - <> - {mojoToChiaLocaleString(feeAmount, locale)} -   - {currencyCode} - - ); - } - return undefined; - }, [feeAmount, locale, currencyCode]); - - return ( - User Transaction Fees} - value={userTransactionFees} - loading={isLoading} - error={error} - /> - ); -} diff --git a/packages/gui/src/components/farm/card/FarmCards.tsx b/packages/gui/src/components/farm/card/FarmCards.tsx deleted file mode 100644 index 787acfd559..0000000000 --- a/packages/gui/src/components/farm/card/FarmCards.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Grid } from '@mui/material'; -import React from 'react'; - -import FarmCardBlockRewards from './FarmCardBlockRewards'; -import FarmCardExpectedTimeToWin from './FarmCardExpectedTimeToWin'; -import FarmCardLastHeightFarmed from './FarmCardLastHeightFarmed'; -import FarmCardPlotCount from './FarmCardPlotCount'; -import FarmCardStatus from './FarmCardStatus'; -import FarmCardTotalChiaFarmed from './FarmCardTotalChiaFarmed'; -import FarmCardTotalNetworkSpace from './FarmCardTotalNetworkSpace'; -import FarmCardTotalSizeOfPlots from './FarmCardTotalSizeOfPlots'; -import FarmCardUserFees from './FarmCardUserFees'; - -export default function FarmCards() { - return ( -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- ); -} diff --git a/packages/gui/src/components/farm/card/FarmingRewardsCards.tsx b/packages/gui/src/components/farm/card/FarmingRewardsCards.tsx new file mode 100644 index 0000000000..619c1d618c --- /dev/null +++ b/packages/gui/src/components/farm/card/FarmingRewardsCards.tsx @@ -0,0 +1,174 @@ +import { useGetBlockchainStateQuery, useGetTotalHarvestersSummaryQuery } from '@chia-network/api-react'; +import { State, CardSimple, useCurrencyCode, mojoToChiaLocaleString, useLocale } from '@chia-network/core'; +import { Trans } from '@lingui/macro'; +import { Grid, Typography, Box } from '@mui/material'; +import BigNumber from 'bignumber.js'; +import moment from 'moment'; +import React, { useMemo } from 'react'; + +import FullNodeState from '../../../constants/FullNodeState'; +import useFullNodeState from '../../../hooks/useFullNodeState'; +import FarmCardNotAvailable from './FarmCardNotAvailable'; + +const MOJO_PER_CHIA = 1_000_000_000_000; +const BLOCKS_PER_YEAR = 1_681_920; // 32 * 6 * 24 * 365 +function getBlockRewardByHeight(height: number) { + if (height === 0) { + return 21_000_000 * MOJO_PER_CHIA; + } + if (height < 3 * BLOCKS_PER_YEAR) { + return 2 * MOJO_PER_CHIA; + } + if (height < 6 * BLOCKS_PER_YEAR) { + return 1 * MOJO_PER_CHIA; + } + if (height < 9 * BLOCKS_PER_YEAR) { + return 0.5 * MOJO_PER_CHIA; + } + if (height < 12 * BLOCKS_PER_YEAR) { + return 0.25 * MOJO_PER_CHIA; + } + + return 0.125 * MOJO_PER_CHIA; +} + +export default React.memo(FarmingRewardsCards); +function FarmingRewardsCards() { + const { state: fullNodeState } = useFullNodeState(); + + const { data, isLoading: isLoadingBlockchainState, error: errorBlockchainState } = useGetBlockchainStateQuery(); + const { + totalEffectivePlotSize, + isLoading: isLoadingTotalHarvesterSummary, + error: errorLoadingPlots, + } = useGetTotalHarvestersSummaryQuery(); + const currencyCode = useCurrencyCode(); + const [locale] = useLocale(); + + const isLoading = isLoadingBlockchainState || isLoadingTotalHarvesterSummary; + const error = errorBlockchainState || errorLoadingPlots; + + const totalNetworkSpace = useMemo(() => new BigNumber(data?.space ?? 0), [data]); + + const proportion = useMemo(() => { + if (isLoading || totalNetworkSpace.isZero()) { + return new BigNumber(0); + } + + return totalEffectivePlotSize.div(totalNetworkSpace); + }, [isLoading, totalEffectivePlotSize, totalNetworkSpace]); + + const expectedTimeToWinSeconds = React.useMemo(() => { + if (fullNodeState !== FullNodeState.SYNCED || !data) { + return null; + } + + return !proportion.isZero() ? new BigNumber(data.averageBlockTime).div(proportion) : new BigNumber(0); + }, [proportion, data, fullNodeState]); + + const expectedTimeToWinCard = React.useMemo(() => { + if (fullNodeState !== FullNodeState.SYNCED || !expectedTimeToWinSeconds) { + const state = fullNodeState === FullNodeState.SYNCHING ? State.WARNING : undefined; + + return Estimated Time to Win} state={state} />; + } + + const minutes = expectedTimeToWinSeconds.div(60); + const expectedTimeToWinHumanized = moment + .duration({ + minutes: minutes.toNumber(), + }) + .humanize(); + + return ( + Estimated Time to Win} + value={`${expectedTimeToWinHumanized}`} + tooltip={ + + You have {proportion.multipliedBy(100).toNumber().toFixed(4)}% of the space on the network, so farming a + block will take {expectedTimeToWinHumanized} in expectation. Actual results may take 3 to 4 times longer + than this estimate. + + } + loading={isLoading} + error={error} + /> + ); + }, [proportion, expectedTimeToWinSeconds, fullNodeState, isLoading, error]); + + const estimatedDailyXCHCard = React.useMemo(() => { + if (fullNodeState !== FullNodeState.SYNCED || !expectedTimeToWinSeconds || !data) { + const state = fullNodeState === FullNodeState.SYNCHING ? State.WARNING : undefined; + + return Estimated daily XCH} state={state} />; + } + + const estimatedDailyXCH = new BigNumber(86_400) + .div(expectedTimeToWinSeconds) + .multipliedBy(getBlockRewardByHeight(data.peak.height)) + .dp(0); + + return ( + Estimated daily XCH} + value={ + <> + {mojoToChiaLocaleString(estimatedDailyXCH, locale)} +   + {currencyCode} + + } + loading={isLoading} + error={error} + /> + ); + }, [data, expectedTimeToWinSeconds, fullNodeState, isLoading, error, currencyCode, locale]); + + const estimatedMonthlyXCHCard = React.useMemo(() => { + if (fullNodeState !== FullNodeState.SYNCED || !expectedTimeToWinSeconds || !data) { + const state = fullNodeState === FullNodeState.SYNCHING ? State.WARNING : undefined; + + return Estimated monthly XCH} state={state} />; + } + + const estimatedMonthlyXCH = new BigNumber(86_400 * 31) + .div(expectedTimeToWinSeconds) + .multipliedBy(getBlockRewardByHeight(data.peak.height)) + .dp(0); + + return ( + Estimated monthly XCH} + value={ + <> + {mojoToChiaLocaleString(estimatedMonthlyXCH, locale)} +   + {currencyCode} + + } + loading={isLoading} + error={error} + /> + ); + }, [data, expectedTimeToWinSeconds, fullNodeState, isLoading, error, currencyCode, locale]); + + return ( + + + Farming Rewards + + + + {expectedTimeToWinCard} + + + {estimatedDailyXCHCard} + + + {estimatedMonthlyXCHCard} + + + + ); +} diff --git a/packages/gui/src/components/farm/card/FarmingRewardsHistoryCards.tsx b/packages/gui/src/components/farm/card/FarmingRewardsHistoryCards.tsx new file mode 100644 index 0000000000..90d4f6362b --- /dev/null +++ b/packages/gui/src/components/farm/card/FarmingRewardsHistoryCards.tsx @@ -0,0 +1,125 @@ +import { useGetFarmedAmountQuery } from '@chia-network/api-react'; +import { + useCurrencyCode, + mojoToChiaLocaleString, + useLocale, + CardSimple, + FormatLargeNumber, + Tooltip, +} from '@chia-network/core'; +import { Trans } from '@lingui/macro'; +import { Grid, Typography, Box } from '@mui/material'; +import moment from 'moment'; +import React, { useMemo } from 'react'; +import styled from 'styled-components'; + +const StyledTable = styled.table` + border-collapse: collapse; + td:first-child { + padding-right: 8px; + } +`; + +export default React.memo(FarmingRewardsHistoryCards); +function FarmingRewardsHistoryCards() { + const currencyCode = useCurrencyCode(); + const [locale] = useLocale(); + const { data, isLoading, error } = useGetFarmedAmountQuery(); + + const totalChiaFarmedCard = useMemo(() => { + if (!data || isLoading) { + return Total XCH Farmed} value="-" loading={isLoading} error={error} />; + } + + let cardValue: React.ReactElement | string = '-'; + if (data && data.farmedAmount) { + const title = ( + + + + + Pool Reward + + + {mojoToChiaLocaleString(data.poolRewardAmount, locale)} +   + {currencyCode} + + + + + Farmer Reward + + + {mojoToChiaLocaleString(data.farmerRewardAmount, locale)} +   + {currencyCode} + + + + + Block Fee + + + {mojoToChiaLocaleString(data.feeAmount, locale)} +   + {currencyCode} + + + + + ); + + cardValue = ( + + {mojoToChiaLocaleString(data.farmedAmount, locale)} +   + {currencyCode} + + ); + } + + return Total XCH Farmed} value={cardValue} loading={isLoading} error={error} />; + }, [data, locale, currencyCode, isLoading, error]); + + const blocksWonCard = useMemo(() => { + if (!data || isLoading) { + return Blocks won} value="-" loading={isLoading} error={error} />; + } + + return Blocks won} value={data.blocksWon} loading={isLoading} error={error} />; + }, [data, isLoading, error]); + + const lastBlockWonCard = useMemo(() => { + if (!data || isLoading) { + return Last block won} value="-" loading={isLoading} error={error} />; + } + + const time = moment(data.lastTimeFarmed * 1000); + const cardValue = ( + + {time.format('LL')} - Block + + ); + return Last block won} value={cardValue} loading={isLoading} error={error} />; + }, [data, isLoading, error]); + + return ( + + + Farming Rewards History + + + + {totalChiaFarmedCard} + + + {blocksWonCard} + + + {lastBlockWonCard} + + + + ); +} diff --git a/packages/gui/src/components/farm/card/NetspaceCards.tsx b/packages/gui/src/components/farm/card/NetspaceCards.tsx new file mode 100644 index 0000000000..92c12c4da4 --- /dev/null +++ b/packages/gui/src/components/farm/card/NetspaceCards.tsx @@ -0,0 +1,25 @@ +import { Trans } from '@lingui/macro'; +import { Grid, Typography } from '@mui/material'; +import React from 'react'; + +import FarmCardTotalNetworkSpace from './TotalNetworkSpace'; +import FarmCardTotalSizeOfPlots from './TotalSizeOfPlots'; + +export default React.memo(NetSpaceCards); +function NetSpaceCards() { + return ( +
+ + Netspace + + + + + + + + + +
+ ); +} diff --git a/packages/gui/src/components/farm/card/FarmCardTotalNetworkSpace.tsx b/packages/gui/src/components/farm/card/TotalNetworkSpace.tsx similarity index 71% rename from packages/gui/src/components/farm/card/FarmCardTotalNetworkSpace.tsx rename to packages/gui/src/components/farm/card/TotalNetworkSpace.tsx index 887291714f..16ce118f45 100644 --- a/packages/gui/src/components/farm/card/FarmCardTotalNetworkSpace.tsx +++ b/packages/gui/src/components/farm/card/TotalNetworkSpace.tsx @@ -3,16 +3,17 @@ import { FormatBytes, CardSimple } from '@chia-network/core'; import { Trans } from '@lingui/macro'; import React from 'react'; -export default function FarmCardTotalNetworkSpace() { +export default React.memo(TotalNetworkSpace); +function TotalNetworkSpace() { const { data, isLoading, error } = useGetBlockchainStateQuery(); const totalNetworkSpace = data?.space ?? 0; return ( Total Network Space} + title={Total Netspace} value={} - description={Best estimate over last 24 hours} loading={isLoading} + tooltip={Best estimate over last 24 hours} error={error} /> ); diff --git a/packages/gui/src/components/farm/card/TotalSizeOfPlots.tsx b/packages/gui/src/components/farm/card/TotalSizeOfPlots.tsx new file mode 100644 index 0000000000..485dc43f44 --- /dev/null +++ b/packages/gui/src/components/farm/card/TotalSizeOfPlots.tsx @@ -0,0 +1,17 @@ +import { useGetTotalHarvestersSummaryQuery } from '@chia-network/api-react'; +import { FormatBytes, CardSimple } from '@chia-network/core'; +import { Trans } from '@lingui/macro'; +import React from 'react'; + +export default React.memo(TotalSizeOfPlots); +function TotalSizeOfPlots() { + const { totalEffectivePlotSize, isLoading } = useGetTotalHarvestersSummaryQuery(); + + return ( + Farming Space} + value={} + loading={isLoading} + /> + ); +} diff --git a/packages/gui/src/components/harvest/Harvest.tsx b/packages/gui/src/components/harvest/Harvest.tsx new file mode 100644 index 0000000000..a88f5a13e7 --- /dev/null +++ b/packages/gui/src/components/harvest/Harvest.tsx @@ -0,0 +1,19 @@ +import { Flex, LayoutDashboardSub } from '@chia-network/core'; +import React from 'react'; +import { Route, Routes } from 'react-router-dom'; + +import PlotAdd from '../plot/add/PlotAdd'; +import HarvesterOverview from './HarvesterOverview'; + +export default function Harvester() { + return ( + + + + } /> + } /> + + + + ); +} diff --git a/packages/gui/src/components/harvest/HarvesterDetail.tsx b/packages/gui/src/components/harvest/HarvesterDetail.tsx new file mode 100644 index 0000000000..9d26a77603 --- /dev/null +++ b/packages/gui/src/components/harvest/HarvesterDetail.tsx @@ -0,0 +1,209 @@ +import { HarvesterInfo, LatencyData } from '@chia-network/api'; +import { Flex, FormatBytes, Tooltip } from '@chia-network/core'; +import { Trans } from '@lingui/macro'; +import { Box, Paper, Typography, LinearProgress, Chip } from '@mui/material'; +import BigNumber from 'bignumber.js'; +import * as React from 'react'; + +import isLocalhost from '../../util/isLocalhost'; +import HarvesterLatency from './HarvesterLatency'; +import HarvesterPlotDetails from './HarvesterPlotDetails'; + +export type HarvesterLatencyGraphProps = { + harvester?: HarvesterInfo; + latencyData?: LatencyData; + totalFarmSizeRaw?: BigNumber; + totalFarmSizeEffective?: BigNumber; +}; + +export default React.memo(HarvesterLatencyGraph); + +function HarvesterLatencyGraph(props: HarvesterLatencyGraphProps) { + const { harvester, latencyData, totalFarmSizeRaw, totalFarmSizeEffective } = props; + // const { isDarkMode } = useDarkMode(); + const nodeId = harvester?.connection.nodeId; + const host = harvester?.connection.host; + // const latencyRecords = latencyData && nodeId ? latencyData[nodeId] : undefined; + const isLocal = host ? isLocalhost(host) : undefined; + const simpleNodeId = nodeId ? `${nodeId.substring(0, 6)}...${nodeId.substring(nodeId.length - 6)}` : undefined; + const harvestingMode = harvester?.harvestingMode; + const noPlots = harvester && harvester.plots.length === 0; + + const cardTitle = React.useMemo(() => { + let chip; + if (harvestingMode === 2) { + chip = ; + } else if (typeof harvestingMode !== 'number') { + chip = ; + } + + return ( + + + + + + {isLocal ? Local Harvester : Remote Harvester} + +   + + + {simpleNodeId} + + + + {chip} + + + + {host} + + + + {noPlots && ( + + + This harvester does not have any plots to harvest + + + )} + + ); + }, [isLocal, nodeId, simpleNodeId, host, harvestingMode, noPlots]); + + const space = React.useMemo(() => { + if (noPlots) { + return null; + } + + const effectiveSpace = harvester ? new BigNumber(harvester.totalEffectivePlotSize) : undefined; + const totalSpaceOccupation = + harvester && totalFarmSizeRaw + ? new BigNumber(harvester.totalPlotSize).div(totalFarmSizeRaw).multipliedBy(100) + : undefined; + const effectiveSpaceOccupation = + effectiveSpace && totalFarmSizeEffective + ? effectiveSpace.div(totalFarmSizeEffective).multipliedBy(100) + : undefined; + + let earnedSpacePercentage: React.ReactElement | string = '-'; + if (harvester && totalFarmSizeRaw && effectiveSpace) { + const harvesterTotalPlotSize = new BigNumber(harvester.totalPlotSize); + earnedSpacePercentage = ( + }> + {Math.round( + effectiveSpace.minus(harvesterTotalPlotSize).div(harvesterTotalPlotSize).multipliedBy(1000).toNumber() + ) / 10}{' '} + % + + ); + } + + return ( + + + + + Space + + + + + + + + + + + + + + + + + + + + + + +
+ + Total Space + + + + + + + Earned + + + {earnedSpacePercentage} + + + more space + + + + +
+ + + + span': { backgroundColor: '#1a8284' } }} + /> + +
+ +
+ + Effective Space + +
+ + + + span': { backgroundColor: '#5ece71' } }} + /> + +
+
+
+
+ ); + }, [harvester, totalFarmSizeRaw, totalFarmSizeEffective, noPlots]); + + const harvesterLatency = React.useMemo(() => { + if (noPlots) { + return null; + } + + return ; + }, [latencyData, nodeId, noPlots]); + + const plotDetails = React.useMemo(() => { + if (noPlots) { + return null; + } + + return ; + }, [harvester, noPlots]); + + return ( + + {cardTitle} + + {space} + {harvesterLatency} + {plotDetails} + + + ); +} diff --git a/packages/gui/src/components/harvest/HarvesterLatency.tsx b/packages/gui/src/components/harvest/HarvesterLatency.tsx new file mode 100644 index 0000000000..c582dbb605 --- /dev/null +++ b/packages/gui/src/components/harvest/HarvesterLatency.tsx @@ -0,0 +1,124 @@ +import { LatencyInfo } from '@chia-network/api'; +import { Flex } from '@chia-network/core'; +import { Trans } from '@lingui/macro'; +import { Box, Paper, Typography, Select, MenuItem, FormControl, SelectChangeEvent } from '@mui/material'; +import * as React from 'react'; + +import { PureLatencyBarChart, getSliceOfLatency, formatTime } from './LatencyCharts'; + +export type HarvesterLatencyProps = { + latencyInfo?: LatencyInfo; +}; + +export default React.memo(HarvesterLatency); +function HarvesterLatency(props: HarvesterLatencyProps) { + const { latencyInfo } = props; + const [period, setPeriod] = React.useState<'1h' | '12h' | '24h' | '64sp'>('64sp'); // in Hour + + const slice = React.useMemo(() => { + if (!latencyInfo || latencyInfo.latency.length === 0) { + return []; + } + return getSliceOfLatency(latencyInfo.latency, period); + }, [latencyInfo, period]); + + const stat = React.useMemo(() => { + if (!latencyInfo || latencyInfo.latency.length === 0 || Number.isNaN(latencyInfo.max)) { + return { avg: 0, max: 0, avgEl: N/A, maxEl: N/A }; + } + if (period === '24h') { + const avg = Math.round((latencyInfo.avg / 1000) * 100) / 100; + const max = Math.round((latencyInfo.max / 1000) * 100) / 100; + return { + avg, + max, + avgEl: formatTime(avg), + maxEl: formatTime(max), + }; + } + let sum = 0; + let max = 0; + for (let i = 0; i < slice.length; i++) { + const latency = slice[i][1]; + sum += latency; + max = max < latency ? latency : max; + } + const avg = Math.round((sum / 1000 / slice.length) * 100) / 100; + max = Math.round((max / 1000) * 100) / 100; + return { + avg, + max, + avgEl: formatTime(avg), + maxEl: formatTime(max), + }; + }, [slice, latencyInfo, period]); + + const unit = stat.avg > 2000 ? 's' : 'ms'; + + const onChangePeriod = React.useCallback((e: SelectChangeEvent) => { + const selectEl = e.target as HTMLSelectElement; + if (!selectEl) { + return; + } + setPeriod(selectEl.value as '1h' | '12h' | '24h' | '64sp'); + }, []); + + return ( + + + + + + + + + + + + + +
+ + Harvester latency + + + + + +
+ + Avg {stat.avgEl} + + + Max {stat.maxEl} + +
+ + + +
+
+
+ ); +} diff --git a/packages/gui/src/components/harvest/HarvesterOverview.tsx b/packages/gui/src/components/harvest/HarvesterOverview.tsx new file mode 100644 index 0000000000..8361107772 --- /dev/null +++ b/packages/gui/src/components/harvest/HarvesterOverview.tsx @@ -0,0 +1,209 @@ +import { useGetHarvestersQuery, useGetNewFarmingInfoQuery } from '@chia-network/api-react'; +import { Flex, FormatBytes, FormatLargeNumber, CardSimple } from '@chia-network/core'; +import { Trans } from '@lingui/macro'; +import { Grid, Typography } from '@mui/material'; +import BigNumber from 'bignumber.js'; +import React from 'react'; + +import { PLOT_FILTER } from '../../util/plot'; +import HarvesterDetail from './HarvesterDetail'; + +export default function HarvesterOverview() { + const { isLoading: isLoadingHarvesters, data: harvesters } = useGetHarvestersQuery(); + const { isLoading: isLoadingFarmingInfo, data } = useGetNewFarmingInfoQuery(); + + const newFarmingInfo = data?.newFarmingInfo; + const latencyData = data?.latencyData; + + const totalFarmSizeRaw = React.useMemo(() => { + if (!harvesters) { + return new BigNumber(0); + } + let size = new BigNumber(0); + for (let i = 0; i < harvesters.length; i++) { + const h = harvesters[i]; + const totalPlotSize = new BigNumber(h.totalPlotSize); + size = size.plus(totalPlotSize); + } + return size; + }, [harvesters]); + + const totalFarmSizeEffective = React.useMemo(() => { + if (!harvesters) { + return new BigNumber(0); + } + let size = new BigNumber(0); + for (let i = 0; i < harvesters.length; i++) { + const h = harvesters[i]; + const totalEffectivePlotSize = new BigNumber(h.totalEffectivePlotSize); + size = size.plus(totalEffectivePlotSize); + } + return size; + }, [harvesters]); + + const numberOfPlots = React.useMemo(() => { + if (!harvesters) { + return 'N/A'; + } + let plots = 0; + for (let i = 0; i < harvesters.length; i++) { + const h = harvesters[i]; + plots += h.plots.length; + } + return ; + }, [harvesters]); + + const eligiblePlots = React.useMemo(() => { + if (!newFarmingInfo || newFarmingInfo.length === 0) { + return { + tooltip: The average number of plots which passed filter over last 64 signage points, + value: '0', + }; + } + + const eligiblePlotsPerSp: Record = {}; + let latestTotalPlots = 0; + for (let i = 0; i < newFarmingInfo.length; i++) { + const nfi = newFarmingInfo[i]; + eligiblePlotsPerSp[nfi.signagePoint] = eligiblePlotsPerSp[nfi.signagePoint] || { + totalPlots: 0, + passedFilter: 0, + }; + eligiblePlotsPerSp[nfi.signagePoint].totalPlots += nfi.totalPlots; + eligiblePlotsPerSp[nfi.signagePoint].passedFilter += nfi.passedFilter; + if (i === 0) { + latestTotalPlots = nfi.totalPlots; + } + if (Object.keys(eligiblePlotsPerSp).length > 64) { + // Only cares last 64 sps + break; + } + } + + let sumPassedFilter = 0; + const sps = Object.keys(eligiblePlotsPerSp); + for (let i = 0; i < sps.length; i++) { + const sp = sps[i]; + const { passedFilter } = eligiblePlotsPerSp[sp]; + sumPassedFilter += passedFilter; + } + + const expectedAvgPassedFilter = Math.round((latestTotalPlots / PLOT_FILTER) * 1000) / 1000; + const avgPassedFilter = sps.length > 0 ? Math.round((sumPassedFilter / sps.length) * 1000) / 1000 : 0; + return { + tooltip: ( + + The average number of plots which passed filter over the last 64 signage points. It is expected to be{' '} + {expectedAvgPassedFilter} for total {latestTotalPlots} plots + + ), + value: avgPassedFilter, + }; + }, [newFarmingInfo]); + + const duplicatePlots = React.useMemo(() => { + if (!harvesters) { + return 'N/A'; + } + let plots = 0; + for (let i = 0; i < harvesters.length; i++) { + const h = harvesters[i]; + plots += h.duplicates.length; + } + return ; + }, [harvesters]); + + const harvesterSummaries = React.useMemo(() => { + if (!harvesters) { + return undefined; + } + const elements: React.ReactElement[] = []; + for (let i = 0; i < harvesters.length; i++) { + const h = harvesters[i]; + // When there're only 1 harvester, occupy the entire width of the page. + const md = harvesters.length === 1 ? 12 : 6; + elements.push( + + + + ); + } + return elements; + }, [harvesters, latencyData, totalFarmSizeRaw, totalFarmSizeEffective]); + + return ( + + + Harvester Summary + + + + Space + + + + Total farm size raw} + value={} + /> + + + Total farm size effective} + value={} + /> + + + Number of plots} + value={numberOfPlots} + /> + + + + + + Harvesting Effectiveness + + + + Eligible plots per signage point} + tooltip={eligiblePlots.tooltip} + value={eligiblePlots.value} + /> + + + Duplicate plots} + value={duplicatePlots} + /> + + + + + + Harvesters + + + {harvesterSummaries} + + + + ); +} diff --git a/packages/gui/src/components/harvest/HarvesterPlotDetails.tsx b/packages/gui/src/components/harvest/HarvesterPlotDetails.tsx new file mode 100644 index 0000000000..65bca9686c --- /dev/null +++ b/packages/gui/src/components/harvest/HarvesterPlotDetails.tsx @@ -0,0 +1,187 @@ +import { HarvesterInfo } from '@chia-network/api'; +import { Flex } from '@chia-network/core'; +import { Trans } from '@lingui/macro'; +import { Box, Paper, Typography } from '@mui/material'; +import * as React from 'react'; + +import { + ColorCodesForCompressions, + ColorCodesForKSizes, + DoughnutChartData, + PurePlotDetailsChart, +} from './PlotDetailsChart'; + +export type HarvesterPlotDetailsProps = { + harvester?: HarvesterInfo; +}; + +export default React.memo(HarvesterPlotDetails); +function HarvesterPlotDetails(props: HarvesterPlotDetailsProps) { + const { harvester } = props; + + const plotSummary = React.useMemo(() => { + if (!harvester) { + return { totalPlots: 0, totalOg: 0, totalPlotNft: 0 }; + } + + const totalPlots = harvester.plots.length; + let totalOg = 0; + let totalPlotNft = 0; + + for (let i = 0; i < harvester.plots.length; i++) { + const p = harvester.plots[i]; + if (p.poolContractPuzzleHash) { + totalPlotNft++; + } else { + totalOg++; + } + } + + return { totalPlots, totalOg, totalPlotNft }; + }, [harvester]); + + const plotStats = React.useMemo(() => { + if (!harvester) { + return { compressionRecords: undefined, compressionData: undefined, sizeRecords: undefined, sizeData: undefined }; + } + const { plots } = harvester; + const totalPlots = plots.length; + const plotsByCompression: Record = {}; + const plotsBySize: Record = {}; + const plotsBySizeAndCompression: Record> = {}; + for (let i = 0; i < plots.length; i++) { + const p = plots[i]; + const cl = p.compressionLevel || 0; + const s = +p.size; + + plotsByCompression[cl] = (plotsByCompression[cl] || 0) + 1; + plotsBySize[s] = (plotsBySize[s] || 0) + 1; + plotsBySizeAndCompression[s] = plotsBySizeAndCompression[s] || {}; + plotsBySizeAndCompression[s][cl] = (plotsBySizeAndCompression[s][cl] || 0) + 1; + } + + const kSizes = Object.keys(plotsBySize).sort((a, b) => +a - +b); + const breakDown: React.ReactElement[] = []; + const kSizeData: DoughnutChartData = { data: [], colors: [], labels: [] }; + const kSizeAndCompressionData: DoughnutChartData = { data: [], colors: [], labels: [] }; + for (let i = 0; i < kSizes.length; i++) { + const kSize = +kSizes[i]; + const count = plotsBySize[kSize]; + const percentage = (count / totalPlots) * 100; + const bgColor = ColorCodesForKSizes[kSize] || ColorCodesForKSizes[35]; + kSizeData.labels.push(`K${kSize}`); + kSizeData.data.push(count); + kSizeData.colors.push(bgColor); + + const kSizeAndCompressionBreakDown: React.ReactElement[] = []; + const compressions = plotsBySizeAndCompression[kSize] ? Object.keys(plotsBySizeAndCompression[kSize]) : []; + for (let k = 0; k < compressions.length; k++) { + const cl = +compressions[k]; + const countCompression = plotsBySizeAndCompression[kSize][cl]; + const percentageCompression = Math.round((countCompression / count) * 100); + const bgColorSize = ColorCodesForCompressions[cl] || ColorCodesForCompressions[9]; + kSizeAndCompressionData.labels.push(`C${cl}`); + kSizeAndCompressionData.data.push(countCompression); + kSizeAndCompressionData.colors.push(bgColorSize); + + kSizeAndCompressionBreakDown.push( + + + C{cl} {countCompression} {percentageCompression}% + + ); + } + + breakDown.push( + + + + K{kSize} {count} {Math.round(percentage)}% + + +
+ + Compression + + {kSizeAndCompressionBreakDown} + + + ); + } + + return { breakDown, kSizeData, kSizeAndCompressionData }; + }, [harvester]); + + const plotDetailsChart = React.useMemo(() => { + if (!plotStats.kSizeData || !plotStats.kSizeAndCompressionData) { + return undefined; + } + if (plotStats.kSizeData.data.length <= 1 && plotStats.kSizeAndCompressionData.data.length <= 1) { + return undefined; + } + + return ; + }, [plotStats]); + + return ( + + + + + Plot details + + + + Total plots: {plotSummary.totalPlots} + + + Total OG: {plotSummary.totalOg} + + + Total plotNFT: {plotSummary.totalPlotNft} + + + + + + Plot Sizes + + + {plotStats.breakDown} + + + {plotDetailsChart} + + + + + ); +} diff --git a/packages/gui/src/components/harvest/LatencyCharts.tsx b/packages/gui/src/components/harvest/LatencyCharts.tsx new file mode 100644 index 0000000000..897892b833 --- /dev/null +++ b/packages/gui/src/components/harvest/LatencyCharts.tsx @@ -0,0 +1,155 @@ +import { LatencyRecord } from '@chia-network/api'; +import { Chart as ChartJS, BarElement, CategoryScale, LinearScale, BarController } from 'chart.js'; +import * as React from 'react'; +import { Bar } from 'react-chartjs-2'; + +ChartJS.register(BarElement, CategoryScale, LinearScale, BarController); + +export function getSliceOfLatency(data: LatencyRecord[], period: '1h' | '12h' | '24h' | '64sp') { + if (period.endsWith('sp')) { + const n = +period.split('sp')[0]; + return data.slice(data.length - n); + } + + const periodInHours = +period.split('h')[0]; + const now = Date.now(); + const periodInMs = 3_600_000 * periodInHours; + + // @TODO Replace below with better algorithm for performance + for (let i = 0; i < data.length; i++) { + const d = data[i][0]; + if (now - d < periodInMs) { + return data.slice(i); + } + } + return [] as LatencyRecord[]; +} + +export function formatTime(timeInMs: number) { + if (timeInMs < 1000) { + return `${timeInMs} ms`; + } + + const [integerPart, decimalPart] = `${timeInMs / 1000}`.split('.'); + let intStr = ''; + + for (let i = 0; i < integerPart.length; i++) { + const tailIndex = integerPart.length - 1 - i; + if (i % 3 === 0 && i !== 0) { + intStr = `${integerPart.charAt(tailIndex)},${intStr}`; + } else { + intStr = integerPart.charAt(tailIndex) + intStr; + } + } + + if (typeof decimalPart === 'string') { + const decimalStr = `${Math.round(+decimalPart.slice(0, 4) / 10) / 1000}`.replace(/^0[.]/, ''); + return `${intStr}.${decimalStr} s`; + } + + return `${intStr} s`; +} + +export type BarChartProps = { + period: '1h' | '12h' | '24h' | '64sp'; + latency: LatencyRecord[]; + unit: 's' | 'ms'; +}; +export const PureLatencyBarChart = React.memo(LatencyBarChart); +function LatencyBarChart(props: BarChartProps) { + const { latency, period, unit } = props; + + const options = React.useMemo( + () => ({ + responsive: true, + animation: false, + maintainAspectRatio: false, + scales: { + x: { + display: false, + }, + y: { + ticks: { + callback(value: number) { + const formattedValue = unit === 'ms' ? value : value / 1000; + return `${formattedValue} ${unit}`; + }, + }, + }, + }, + }), + [unit] + ); + + const data = React.useMemo(() => { + const labels: string[] = []; + const records: number[] = []; + const backgroundColors: string[] = []; + + if (period.endsWith('sp')) { + const nData = Math.min(latency.length, +period.split('sp')[0]); + + for (let i = latency.length - nData; i < latency.length; i++) { + const [t, val] = latency[i]; + labels.push(`${t}`); + const valInMs = val / 1000; + records.push(valInMs); + if (valInMs < 8000) { + // Normal color + backgroundColors.push('#ccdde1'); + } else if (valInMs < 20_000) { + // Warning color + backgroundColors.push('#ffd388'); + } else { + // Fatal color + backgroundColors.push('#faa7b0'); + } + } + + return { + labels, + datasets: [ + { + data: records, + backgroundColor: backgroundColors, + skipNull: true, + barThickness: 5, + }, + ], + }; + } + + if (period.endsWith('h')) { + for (let i = 0; i < latency.length; i++) { + const [t, val] = latency[i]; + labels.push(`${t}`); + const valInMs = val / 1000; + records.push(valInMs); + if (valInMs < 8000) { + // Normal color + backgroundColors.push('#ccdde1'); + } else if (valInMs < 20_000) { + // Warning color + backgroundColors.push('#ffd388'); + } else { + // Fatal color + backgroundColors.push('#faa7b0'); + } + } + } + + return { + labels, + datasets: [ + { + data: records, + backgroundColor: backgroundColors, + skipNull: true, + barThickness: 1, + }, + ], + }; + }, [latency, period]); + + return ; +} diff --git a/packages/gui/src/components/harvest/PlotDetailsChart.tsx b/packages/gui/src/components/harvest/PlotDetailsChart.tsx new file mode 100644 index 0000000000..8c495224c8 --- /dev/null +++ b/packages/gui/src/components/harvest/PlotDetailsChart.tsx @@ -0,0 +1,76 @@ +import { Chart as ChartJS, ArcElement, Tooltip, ChartOptions } from 'chart.js'; +import * as React from 'react'; +import { Doughnut } from 'react-chartjs-2'; + +ChartJS.register(ArcElement, Tooltip); + +export const ColorCodesForCompressions: Record = { + 0: '#5ECE71', + 1: '#1EBF89', + 2: '#1A8284', + 3: '#094D4C', + 4: '#FFFAE3', + 5: '#E8FBBA', + 6: '#D4FF72', + 7: '#95B0B7', + 8: '#CCDDE1', + 9: '#E2EDF0', +}; + +export const ColorCodesForKSizes: Record = { + 25: '#E2EDF0', + 31: '#95B0B7', + 32: '#7676A9', + 33: '#C3C3EE', + 34: '#BCEFF2', + 35: '#474765', +}; + +export type DoughnutChartData = { data: number[]; colors: string[]; labels: string[] }; + +const donutOptions: ChartOptions<'doughnut'> = { + plugins: { + tooltip: { + callbacks: { + label: (ctx) => { + const plots = ctx.dataset.data[ctx.dataIndex]; + return `${(ctx.dataset as DoughnutChartData).labels[ctx.dataIndex]} - ${plots} plot${plots > 1 ? 's' : ''}`; + }, + }, + }, + }, +}; + +export type PlotDetailsProps = { + kSizeData: DoughnutChartData; + compressionData: DoughnutChartData; +}; + +export const PurePlotDetailsChart = React.memo(PlotDetailsChart); +function PlotDetailsChart(props: PlotDetailsProps) { + const { kSizeData, compressionData } = props; + const data = React.useMemo( + () => ({ + datasets: [ + { + data: compressionData.data, + backgroundColor: compressionData.colors, + labels: compressionData.labels, + cutout: '66%', + radius: '100%', + borderWidth: 0, + }, + { + data: kSizeData.data, + backgroundColor: kSizeData.colors, + labels: kSizeData.labels, + cutout: '66%', + radius: '100%', + borderWidth: 0, + }, + ], + }), + [kSizeData, compressionData] + ); + return ; +} diff --git a/packages/gui/src/components/plot/PlotStatus.tsx b/packages/gui/src/components/plot/PlotStatus.tsx index 0cc329ee89..60dce7a5be 100644 --- a/packages/gui/src/components/plot/PlotStatus.tsx +++ b/packages/gui/src/components/plot/PlotStatus.tsx @@ -36,7 +36,7 @@ type Props = { export default function PlotStatus(props: Props) { const { plot } = props; - const farmerStatus = useFarmerStatus(); + const { farmerStatus } = useFarmerStatus(); const color = Color[farmerStatus]; const title = Title[farmerStatus]; const description = Description[farmerStatus]; diff --git a/packages/gui/src/components/plot/add/PlotAddChooseKeys.tsx b/packages/gui/src/components/plot/add/PlotAddChooseKeys.tsx new file mode 100644 index 0000000000..573f8f22f6 --- /dev/null +++ b/packages/gui/src/components/plot/add/PlotAddChooseKeys.tsx @@ -0,0 +1,192 @@ +import { toBech32m } from '@chia-network/api'; +import { useGetKeysForPlottingQuery } from '@chia-network/api-react'; +import { CardStep, TextField, Button } from '@chia-network/core'; +import { Trans, t } from '@lingui/macro'; +import { Grid, FormControl, Typography, Switch, FormControlLabel, ButtonGroup } from '@mui/material'; +import React from 'react'; +import { useFormContext } from 'react-hook-form'; + +import { getUniqueName } from '../../../hooks/usePlotNFTName'; + +type Props = { + step: number; + currencyCode: string; + fingerprint: number; +}; + +export default function PlotAddChooseKeys(props: Props) { + const { step, currencyCode, fingerprint } = props; + const { watch, setValue } = useFormContext(); + const { isLoading, data } = useGetKeysForPlottingQuery({ fingerprints: [fingerprint] }); + const [manualSetup, setManualSetup] = React.useState(false); + const [poolKeyType, setPoolKeyType] = React.useState<'p2SingletonPuzzleHash' | 'poolPublicKey'>( + 'p2SingletonPuzzleHash' + ); + const p2SingletonPuzzleHash = watch('p2SingletonPuzzleHash'); + const farmerPKInput = watch('farmerPublicKey'); + const prevP2SingletonPuzzleHash = React.useRef(p2SingletonPuzzleHash); + + let p2SingletonPuzzleHashChanged = false; + if (prevP2SingletonPuzzleHash.current !== p2SingletonPuzzleHash) { + p2SingletonPuzzleHashChanged = true; + prevP2SingletonPuzzleHash.current = p2SingletonPuzzleHash; + } + + const plotNFTContractAddressHelperText = React.useMemo(() => { + if (!p2SingletonPuzzleHash) { + return Used to create a pool plot.; + } + const plotNFTName = getUniqueName(p2SingletonPuzzleHash); + return ( + + This is the plot nft contract address of "{plotNFTName}". If you want to change this, select "None" at Plot to a + Plot NFT form. + + ); + }, [p2SingletonPuzzleHash]); + + const poolPublicKeyHelperText = React.useMemo( + () => (Not recommended) Used to create an old style plot for solo farming., + [] + ); + + const onChangeSetupKeys = React.useCallback( + (e: React.ChangeEvent) => { + setManualSetup(e.target.checked); + }, + [setManualSetup] + ); + + const handleSetPoolPublicKey = React.useCallback(() => { + setValue('p2SingletonPuzzleHash', ''); + setPoolKeyType('poolPublicKey'); + }, [setPoolKeyType, setValue]); + + const handleSetP2SingletonPuzzleHash = React.useCallback(() => { + setValue('poolPublicKey', ''); + setPoolKeyType('p2SingletonPuzzleHash'); + }, [setPoolKeyType, setValue]); + + const isFarmerPKForCurrentWallet = React.useMemo(() => { + const farmerPublicKeyForFingerprint = + isLoading || !data || !data.keys[fingerprint] ? undefined : data.keys[fingerprint].farmerPublicKey; + return farmerPublicKeyForFingerprint && farmerPKInput === farmerPublicKeyForFingerprint; + }, [isLoading, data, fingerprint, farmerPKInput]); + + React.useEffect(() => { + if (!p2SingletonPuzzleHashChanged) { + return; + } + if (poolKeyType === 'p2SingletonPuzzleHash') { + if (!p2SingletonPuzzleHash) { + setValue('plotNFTContractAddr', ''); + } else { + setValue('plotNFTContractAddr', toBech32m(p2SingletonPuzzleHash, currencyCode.toLowerCase())); + } + } else { + setValue('poolPublicKey', ''); + setPoolKeyType('p2SingletonPuzzleHash'); + } + }, [p2SingletonPuzzleHash, setValue, currencyCode, poolKeyType, p2SingletonPuzzleHashChanged]); + + React.useEffect(() => { + if (isLoading || !data || !data.keys[fingerprint]) { + return; + } + setValue('farmerPublicKey', data.keys[fingerprint].farmerPublicKey); + }, [isLoading, data, fingerprint, setValue]); + + React.useEffect(() => { + if (isLoading || !data || !data.keys[fingerprint]) { + return; + } + if (poolKeyType === 'poolPublicKey') { + setValue('poolPublicKey', data.keys[fingerprint].poolPublicKey); + } else if (p2SingletonPuzzleHash) { + setValue('plotNFTContractAddr', toBech32m(p2SingletonPuzzleHash, currencyCode.toLowerCase())); + } + }, [isLoading, data, fingerprint, setValue, poolKeyType, p2SingletonPuzzleHash, currencyCode]); + + return ( + Keys}> + + + You can customize farmer public key, pool public key / pool contract address here manually. +
+ Usually you don't need manual set up so please consider carefully whether you really need to edit these keys + for a plot. One possible situation is to create a plot for someone who asks you to plot with your great + hardware. +
+
+ + } + label={Set up keys manually} + /> + + + + + Farmer Public Key} + helperText={ + isFarmerPKForCurrentWallet ? ( + This is the farmer public key corresponding to the current logged-in wallet + ) : undefined + } + disabled={!manualSetup} + /> + + + {!p2SingletonPuzzleHash && ( + + + + + + + )} + + + Plot NFT Pool Contract Address + ) : ( + Pool Public Key + ) + } + helperText={ + poolKeyType === 'p2SingletonPuzzleHash' ? plotNFTContractAddressHelperText : poolPublicKeyHelperText + } + disabled={(poolKeyType === 'p2SingletonPuzzleHash' && Boolean(p2SingletonPuzzleHash)) || !manualSetup} + /> + + + +
+ ); +} diff --git a/packages/gui/src/components/plot/add/PlotAddChooseSize.tsx b/packages/gui/src/components/plot/add/PlotAddChooseSize.tsx index 28b15326f2..5d359f3245 100644 --- a/packages/gui/src/components/plot/add/PlotAddChooseSize.tsx +++ b/packages/gui/src/components/plot/add/PlotAddChooseSize.tsx @@ -24,11 +24,17 @@ export default function PlotAddChooseSize(props: Props) { const { watch, setValue } = useFormContext(); const openDialog = useOpenDialog(); + const op = plotter.options; + const isBladebit3OrNewer = + plotter.defaults.plotterName.startsWith('bladebit') && plotter.version && +plotter.version.split('.')[0] >= 3; + const plotterName = watch('plotterName'); const plotSize = watch('plotSize'); const overrideK = watch('overrideK'); const isKLow = plotSize < MIN_MAINNET_K_SIZE; + const compressionAvailable = op.haveBladebitCompressionLevel && isBladebit3OrNewer; + const [allowedPlotSizes, setAllowedPlotSizes] = useState( getPlotSizeOptions(plotterName).filter((option) => plotter.options.kSizes.includes(option.value)) ); @@ -68,7 +74,10 @@ export default function PlotAddChooseSize(props: Props) { }, [plotSize, overrideK, setValue, openDialog]); return ( - Choose Plot Size}> + Choose K value and compression level : Choose K value} + > { @@ -80,11 +89,11 @@ export default function PlotAddChooseSize(props: Props) { - + - Plot Size + K value + 0 - No compression + 1 + 2 + 3 + 4 + 5 + 6 + 7 + + + + )} ); diff --git a/packages/gui/src/components/plot/add/PlotAddForm.tsx b/packages/gui/src/components/plot/add/PlotAddForm.tsx index 7d6b9d6273..74087cbe57 100644 --- a/packages/gui/src/components/plot/add/PlotAddForm.tsx +++ b/packages/gui/src/components/plot/add/PlotAddForm.tsx @@ -1,4 +1,4 @@ -import { defaultPlotter, toBech32m, fromBech32m } from '@chia-network/api'; +import { defaultPlotter, toBech32m, fromBech32m, WalletCreatePool } from '@chia-network/api'; import { useStartPlottingMutation, useCreateNewPoolWalletMutation } from '@chia-network/api-react'; import { Back, useShowError, ButtonLoading, Flex, Form } from '@chia-network/core'; import { t, Trans } from '@lingui/macro'; @@ -12,17 +12,18 @@ import { plottingInfo } from '../../../constants/plotSizes'; import useUnconfirmedPlotNFTs from '../../../hooks/useUnconfirmedPlotNFTs'; import PlotAddConfig from '../../../types/PlotAdd'; import { PlotterDefaults, PlotterOptions } from '../../../types/Plotter'; +import PlotAddChooseKeys from './PlotAddChooseKeys'; import PlotAddChoosePlotter from './PlotAddChoosePlotter'; import PlotAddChooseSize from './PlotAddChooseSize'; import PlotAddNFT from './PlotAddNFT'; import PlotAddNumberOfPlots from './PlotAddNumberOfPlots'; import PlotAddSelectFinalDirectory from './PlotAddSelectFinalDirectory'; -import PlotAddSelectTemporaryDirectory from './PlotAddSelectTemporaryDirectory'; type FormData = PlotAddConfig & { p2SingletonPuzzleHash?: string; createNFT?: boolean; plotNFTContractAddr?: string; + useManualKeySetup: boolean; }; type Props = { @@ -85,7 +86,7 @@ export default function PlotAddForm(props: Props) { }; const methods = useForm({ - defaultValues: defaultsForPlotter(PlotterName.CHIAPOS), + defaultValues: defaultsForPlotter(PlotterName.BLADEBIT_DISK), }); const { watch, setValue, reset } = methods; @@ -101,7 +102,6 @@ export default function PlotAddForm(props: Props) { const plotter = plotters[plotterName] ?? defaultPlotter; let step = 1; - const allowTempDirectorySelection: boolean = plotter.options.haveBladebitOutputDir === false; const handlePlotterChanged = (newPlotterName: PlotterName) => { const defaults = defaultsForPlotter(newPlotterName); @@ -118,9 +118,12 @@ export default function PlotAddForm(props: Props) { plotterName: formPlotterName, workspaceLocation, workspaceLocation2, + useManualKeySetup, + farmerPublicKey, + poolPublicKey, + plotNFTContractAddr, ...rest } = data; - const { farmerPublicKey, poolPublicKey, plotNFTContractAddr } = rest; let selectedP2SingletonPuzzleHash = p2SingletonPuzzleHash; @@ -137,10 +140,10 @@ export default function PlotAddForm(props: Props) { initialTargetState, initialTargetState: { state: stateLocal }, } = nftData; - const { transaction, p2SingletonPuzzleHash: p2SingletonPuzzleHashLocal } = await createNewPoolWallet({ + const { transaction, p2SingletonPuzzleHash: p2SingletonPuzzleHashLocal } = (await createNewPoolWallet({ initialTargetState, fee, - }).unwrap(); + }).unwrap()) as WalletCreatePool; if (!p2SingletonPuzzleHashLocal) { throw new Error(t`p2SingletonPuzzleHash is not defined`); @@ -161,6 +164,8 @@ export default function PlotAddForm(props: Props) { plotterName: formPlotterName, workspaceLocation, workspaceLocation2: formPlotterName === 'madmax' ? workspaceLocation2 || workspaceLocation : workspaceLocation2, + farmerPublicKey: undefined as string | undefined, + poolPublicKey: undefined as string | undefined, }; if (!selectedP2SingletonPuzzleHash && plotNFTContractAddr) { @@ -175,6 +180,15 @@ export default function PlotAddForm(props: Props) { plotAddConfig.fingerprint = fingerprint; } + if (useManualKeySetup) { + if (farmerPublicKey) { + plotAddConfig.farmerPublicKey = farmerPublicKey; + } + if (poolPublicKey && !selectedP2SingletonPuzzleHash) { + plotAddConfig.poolPublicKey = poolPublicKey; + } + } + await startPlotting(plotAddConfig).unwrap(); navigate('/dashboard/plot'); @@ -191,12 +205,12 @@ export default function PlotAddForm(props: Props) { Add a Plot + + - - {allowTempDirectorySelection && } - + Create diff --git a/packages/gui/src/components/plot/add/PlotAddNFT.tsx b/packages/gui/src/components/plot/add/PlotAddNFT.tsx index d3b289c6f9..bfb5d460af 100644 --- a/packages/gui/src/components/plot/add/PlotAddNFT.tsx +++ b/packages/gui/src/components/plot/add/PlotAddNFT.tsx @@ -76,7 +76,6 @@ const PlotAddNFT = forwardRef((props: Props, ref) => { Learn more - diff --git a/packages/gui/src/components/plot/add/PlotAddNumberOfPlots.tsx b/packages/gui/src/components/plot/add/PlotAddNumberOfPlots.tsx index cd570c3835..ded87a75a7 100644 --- a/packages/gui/src/components/plot/add/PlotAddNumberOfPlots.tsx +++ b/packages/gui/src/components/plot/add/PlotAddNumberOfPlots.tsx @@ -8,7 +8,7 @@ import { TooltipIcon, Select, } from '@chia-network/core'; -import { Trans, t } from '@lingui/macro'; +import { Trans } from '@lingui/macro'; import { Grid, FormControl, @@ -38,7 +38,7 @@ export default function PlotAddNumberOfPlots(props: Props) { const op = plotter.options; return ( - Choose Number of Plots}> + Options}> @@ -443,39 +443,40 @@ export default function PlotAddNumberOfPlots(props: Props) { /> - - - - Farmer Public Key} - /> - - - - - Pool Public Key} - /> - - - - - Plot NFT Pool Contract Address} - /> - + {op.haveBladebitDisableDirectDownloads && ( + + + } + label={ + <> + Disable direct download{' '} + + + Don't allocate host tables using pinned buffers, instead download to intermediate pinned + buffers then copy to the final host buffer. + + + + } + /> + + + )} + {op.haveBladebitDeviceIndex && ( + + + GPU Device Index} + helperText={Which CUDA device to use when plotting} + /> + + + )} diff --git a/packages/gui/src/components/plot/add/PlotAddSelectFinalDirectory.tsx b/packages/gui/src/components/plot/add/PlotAddSelectFinalDirectory.tsx index 9695084e9e..ecdc45d766 100644 --- a/packages/gui/src/components/plot/add/PlotAddSelectFinalDirectory.tsx +++ b/packages/gui/src/components/plot/add/PlotAddSelectFinalDirectory.tsx @@ -7,13 +7,17 @@ import { useFormContext } from 'react-hook-form'; import PlotLocalStorageKeys from '../../../constants/plotLocalStorage'; import useSelectDirectory from '../../../hooks/useSelectDirectory'; +import Plotter from '../../../types/Plotter'; +import PlotAddSelectTemporaryDirectory from './PlotAddSelectTemporaryDirectory'; type Props = { step: number; + plotter: Plotter; }; export default function PlotAddSelectFinalDirectory(props: Props) { - const { step } = props; + const { step, plotter } = props; + const allowTempDirSelection = plotter.options.haveTempDir === true; const selectDirectory = useSelectDirectory(); const { setValue, watch } = useFormContext(); @@ -30,7 +34,11 @@ export default function PlotAddSelectFinalDirectory(props: Props) { } return ( - Select Final Directory}> + Select Temp/Final Directory : Select Final Directory} + > + {allowTempDirSelection && } Select the final destination for the folder where you would like the plot to be stored. We recommend you use a diff --git a/packages/gui/src/components/plot/add/PlotAddSelectTemporaryDirectory.tsx b/packages/gui/src/components/plot/add/PlotAddSelectTemporaryDirectory.tsx index ee43e7de99..0f757de9ab 100644 --- a/packages/gui/src/components/plot/add/PlotAddSelectTemporaryDirectory.tsx +++ b/packages/gui/src/components/plot/add/PlotAddSelectTemporaryDirectory.tsx @@ -1,5 +1,5 @@ import { usePrefs } from '@chia-network/api-react'; -import { AdvancedOptions, ButtonSelected, CardStep, Flex, TextField, Checkbox, TooltipIcon } from '@chia-network/core'; +import { AdvancedOptions, ButtonSelected, Flex, TextField, Checkbox, TooltipIcon } from '@chia-network/core'; import { Trans } from '@lingui/macro'; import { FormControl, FormControlLabel, Typography } from '@mui/material'; import React from 'react'; @@ -10,12 +10,11 @@ import useSelectDirectory from '../../../hooks/useSelectDirectory'; import Plotter from '../../../types/Plotter'; type Props = { - step: number; plotter: Plotter; }; export default function PlotAddSelectTemporaryDirectory(props: Props) { - const { step, plotter } = props; + const { plotter } = props; const selectDirectory = useSelectDirectory(); const { setValue, watch } = useFormContext(); const op = plotter.options; @@ -45,7 +44,7 @@ export default function PlotAddSelectTemporaryDirectory(props: Props) { } return ( - Select Temporary Directory}> + <> Select the temporary destination for the folder where you would like the plot to be stored. We recommend you @@ -146,6 +145,6 @@ export default function PlotAddSelectTemporaryDirectory(props: Props) { )} - + ); } diff --git a/packages/gui/src/components/plot/queue/PlotQueueSize.tsx b/packages/gui/src/components/plot/queue/PlotQueueSize.tsx index ce9900349d..abb308a73e 100644 --- a/packages/gui/src/components/plot/queue/PlotQueueSize.tsx +++ b/packages/gui/src/components/plot/queue/PlotQueueSize.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { getPlotSize } from '../../../constants/plotSizes'; +import { getEffectivePlotSize } from '../../../constants/plotSizes'; import type PlotQueueItem from '../../../types/PlotQueueItem'; type Props = { @@ -11,7 +11,7 @@ export default function PlotQueueSize(props: Props) { const { queueItem: { size }, } = props; - const item = getPlotSize(size as 25 | 32 | 33 | 34 | 35); + const item = getEffectivePlotSize(size as 25 | 32 | 33 | 34 | 35); if (!item) { return null; } diff --git a/packages/gui/src/components/plotNFT/PlotNFTAbsorbRewards.tsx b/packages/gui/src/components/plotNFT/PlotNFTAbsorbRewards.tsx index 7c02da7430..c4114dd48a 100644 --- a/packages/gui/src/components/plotNFT/PlotNFTAbsorbRewards.tsx +++ b/packages/gui/src/components/plotNFT/PlotNFTAbsorbRewards.tsx @@ -131,7 +131,7 @@ export default function PlotNFTAbsorbRewards(props: Props) { diff --git a/packages/gui/src/components/plotNFT/PlotNFTAdd.tsx b/packages/gui/src/components/plotNFT/PlotNFTAdd.tsx index 5c8e9d612c..e4b59907ae 100644 --- a/packages/gui/src/components/plotNFT/PlotNFTAdd.tsx +++ b/packages/gui/src/components/plotNFT/PlotNFTAdd.tsx @@ -55,6 +55,7 @@ export default function PlotNFTAdd(props: Props) { )} Want to Join a Pool? Create a Plot NFT} description={ diff --git a/packages/gui/src/components/plotNFT/PlotNFTChangePool.tsx b/packages/gui/src/components/plotNFT/PlotNFTChangePool.tsx index 77e973f213..c832ec9789 100644 --- a/packages/gui/src/components/plotNFT/PlotNFTChangePool.tsx +++ b/packages/gui/src/components/plotNFT/PlotNFTChangePool.tsx @@ -103,6 +103,7 @@ export default function PlotNFTChangePool(props: Props) { )} Change Pool} submitTitle={Change} diff --git a/packages/gui/src/components/plotNFT/select/PlotNFTSelectBase.tsx b/packages/gui/src/components/plotNFT/select/PlotNFTSelectBase.tsx index 13cd432d3f..36efa8beac 100644 --- a/packages/gui/src/components/plotNFT/select/PlotNFTSelectBase.tsx +++ b/packages/gui/src/components/plotNFT/select/PlotNFTSelectBase.tsx @@ -24,7 +24,7 @@ type Props = { }; export default function PlotNFTSelectBase(props: Props) { - const { step = 1, onCancel, title, description, hideFee = false, feeDescription } = props; + const { step, onCancel, title, description, hideFee = false, feeDescription } = props; // const { nfts } = usePlotNFTs(); const { setValue } = useFormContext(); const self = useWatch({ @@ -123,7 +123,7 @@ export default function PlotNFTSelectBase(props: Props) { - Verify Pool Details}> + Verify Pool Details}> {poolInfo.error && {poolInfo.error.message}} {poolInfo.loading && } diff --git a/packages/gui/src/components/plotNFT/select/PlotNFTSelectFaucet.tsx b/packages/gui/src/components/plotNFT/select/PlotNFTSelectFaucet.tsx index f567a41198..2b9e53057f 100644 --- a/packages/gui/src/components/plotNFT/select/PlotNFTSelectFaucet.tsx +++ b/packages/gui/src/components/plotNFT/select/PlotNFTSelectFaucet.tsx @@ -11,7 +11,7 @@ type Props = { }; export default function PlotNFTSelectFaucet(props: Props) { - const { step = 1, onCancel } = props; + const { step, onCancel } = props; const currencyCode = useCurrencyCode(); const openExternal = useOpenExternal(); diff --git a/packages/gui/src/components/settings/Settings.tsx b/packages/gui/src/components/settings/Settings.tsx index db9de787ba..eb65e92c5f 100644 --- a/packages/gui/src/components/settings/Settings.tsx +++ b/packages/gui/src/components/settings/Settings.tsx @@ -10,6 +10,7 @@ import SettingsAdvanced from './SettingsAdvanced'; import SettingsCustody from './SettingsCustody'; import SettingsDataLayer from './SettingsDataLayer'; import SettingsGeneral from './SettingsGeneral'; +import SettingsHarvester from './SettingsHarvester'; import SettingsIntegration from './SettingsIntegration'; import SettingsNFT from './SettingsNFT'; import SettingsNotifications from './SettingsNotifications'; @@ -35,6 +36,7 @@ export default function Settings() { { id: 'profiles', label: 'Profiles (DIDs)', Component: SettingsProfiles, path: 'profiles/*' }, { id: 'nft', label: 'NFT', Component: SettingsNFT, path: 'nft' }, { id: 'datalayer', label: 'DataLayer', Component: SettingsDataLayer, path: 'datalayer' }, + { id: 'harvester', label: 'Harvester', Component: SettingsHarvester, path: 'harvester' }, { id: 'integration', label: 'Integration', Component: SettingsIntegration, path: 'integration' }, { id: 'notifications', label: 'Notifications', Component: SettingsNotifications, path: 'notifications' }, { id: 'advanced', label: 'Advanced', Component: SettingsAdvanced, path: 'advanced' }, diff --git a/packages/gui/src/components/settings/SettingsHarvester.tsx b/packages/gui/src/components/settings/SettingsHarvester.tsx new file mode 100644 index 0000000000..090a6fe6dd --- /dev/null +++ b/packages/gui/src/components/settings/SettingsHarvester.tsx @@ -0,0 +1,493 @@ +import { ServiceName, HarvesterConfig } from '@chia-network/api'; +import { + useGetHarvesterConfigQuery, + useUpdateHarvesterConfigMutation, + useClientStartServiceMutation, + useClientStopServiceMutation, +} from '@chia-network/api-react'; +import { ButtonLoading, Flex, SettingsHR, SettingsSection, SettingsTitle, SettingsText } from '@chia-network/core'; +import { Trans } from '@lingui/macro'; +import { Warning as WarningIcon } from '@mui/icons-material'; +import { FormControlLabel, FormHelperText, Grid, Switch, TextField, Snackbar } from '@mui/material'; +import React from 'react'; + +const messageAnchorOrigin = { vertical: 'bottom' as const, horizontal: 'center' as const }; + +export default function SettingsHarvester() { + const { data, isLoading } = useGetHarvesterConfigQuery(); + const [updateHarvesterConfig, { isLoading: isUpdating }] = useUpdateHarvesterConfigMutation(); + const [startService, { isLoading: isStarting }] = useClientStartServiceMutation(); + const [stopService, { isLoading: isStopping }] = useClientStopServiceMutation(); + const [message, setMessage] = React.useState(false); + const [configUpdateRequests, setConfigUpdateRequests] = React.useState({ + useGpuHarvesting: null, + gpuIndex: null, + enforceGpuIndex: null, + disableCpuAffinity: null, + parallelDecompressorCount: null, + decompressorThreadCount: null, + recursivePlotScan: null, + refreshParameterIntervalSeconds: null, + }); + + const isProcessing = isStarting || isStopping || isUpdating || isLoading; + + const onChangeHarvestingMode = React.useCallback( + (e: React.ChangeEvent) => { + if (isProcessing || !data) { + return; + } + setConfigUpdateRequests((prev) => ({ + ...prev, + useGpuHarvesting: e.target.checked, + })); + }, + [data, setConfigUpdateRequests, isProcessing] + ); + + const onChangeGPUIndex = React.useCallback( + (e: React.ChangeEvent) => { + const value = +e.target.value; + if (isProcessing || !data || Number.isNaN(value)) { + return; + } + setConfigUpdateRequests((prev) => ({ + ...prev, + gpuIndex: value, + })); + }, + [data, setConfigUpdateRequests, isProcessing] + ); + + const onChangeEnforceGPUIndex = React.useCallback( + (e: React.ChangeEvent) => { + if (isProcessing || !data) { + return; + } + setConfigUpdateRequests((prev) => ({ + ...prev, + enforceGpuIndex: e.target.checked, + })); + }, + [data, setConfigUpdateRequests, isProcessing] + ); + + const onChangeDisableCpuAffinity = React.useCallback( + (e: React.ChangeEvent) => { + if (isProcessing || !data) { + return; + } + setConfigUpdateRequests((prev) => ({ + ...prev, + disableCpuAffinity: e.target.checked, + })); + }, + [data, setConfigUpdateRequests, isProcessing] + ); + + const onChangeParallelDecompressorCount = React.useCallback( + (e: React.ChangeEvent) => { + const value = +e.target.value; + if (isProcessing || !data || Number.isNaN(value)) { + return; + } + setConfigUpdateRequests((prev) => ({ + ...prev, + parallelDecompressorCount: value, + })); + }, + [data, setConfigUpdateRequests, isProcessing] + ); + + const onChangeDecompressorThreadCount = React.useCallback( + (e: React.ChangeEvent) => { + const value = +e.target.value; + if (isProcessing || !data || Number.isNaN(value)) { + return; + } + setConfigUpdateRequests((prev) => ({ + ...prev, + decompressorThreadCount: value, + })); + }, + [data, setConfigUpdateRequests, isProcessing] + ); + + const onChangeRecursivePlotScan = React.useCallback( + (e: React.ChangeEvent) => { + if (isProcessing || !data) { + return; + } + setConfigUpdateRequests((prev) => ({ + ...prev, + recursivePlotScan: e.target.checked, + })); + }, + [data, setConfigUpdateRequests, isProcessing] + ); + + const onChangeRefreshParameterIntervalSeconds = React.useCallback( + (e: React.ChangeEvent) => { + const value = +e.target.value; + if (isProcessing || !data || Number.isNaN(value)) { + return; + } + setConfigUpdateRequests((prev) => ({ + ...prev, + refreshParameterIntervalSeconds: value, + })); + }, + [data, setConfigUpdateRequests, isProcessing] + ); + + const onClickRestartHarvester = React.useCallback(async () => { + if (isProcessing) { + return; + } + + let error: unknown; + const onError = (e: unknown) => { + console.error(e); + error = e; + setMessage(Failed to update Harvester config); + }; + + await updateHarvesterConfig({ + useGpuHarvesting: configUpdateRequests.useGpuHarvesting ?? undefined, + gpuIndex: configUpdateRequests.gpuIndex ?? undefined, + enforceGpuIndex: configUpdateRequests.enforceGpuIndex ?? undefined, + disableCpuAffinity: configUpdateRequests.disableCpuAffinity ?? undefined, + parallelDecompressorCount: configUpdateRequests.parallelDecompressorCount ?? undefined, + decompressorThreadCount: configUpdateRequests.decompressorThreadCount ?? undefined, + recursivePlotScan: configUpdateRequests.recursivePlotScan ?? undefined, + refreshParameterIntervalSeconds: configUpdateRequests.refreshParameterIntervalSeconds ?? undefined, + }) + .unwrap() + .catch(onError); + if (error) { + return; + } + + await stopService({ service: ServiceName.HARVESTER, disableWait: true }).catch(onError); + if (error) { + return; + } + await startService({ service: ServiceName.HARVESTER, disableWait: true }).catch(onError); + if (error) { + return; + } + setMessage(Successfully restarted Harvester); + }, [stopService, isProcessing, startService, updateHarvesterConfig, configUpdateRequests]); + + const onCloseMessage = React.useCallback(() => { + setMessage(false); + }, []); + + const harvestingModeSwitch = React.useMemo(() => { + if (isLoading || !data) { + return ; + } + const checked = (configUpdateRequests.useGpuHarvesting ?? data.useGpuHarvesting) || false; + return ; + }, [data, isLoading, onChangeHarvestingMode, isProcessing, configUpdateRequests]); + + const gpuIndexInput = React.useMemo(() => { + if (isLoading || !data) { + return ; + } + const value = configUpdateRequests.gpuIndex ?? data.gpuIndex; + return ; + }, [data, isLoading, onChangeGPUIndex, isProcessing, configUpdateRequests]); + + const enforceGPUIndexSwitch = React.useMemo(() => { + if (isLoading || !data) { + return ; + } + const checked = (configUpdateRequests.enforceGpuIndex ?? data.enforceGpuIndex) || false; + return ; + }, [data, isLoading, onChangeEnforceGPUIndex, isProcessing, configUpdateRequests]); + + const disableCpuAffinitySwitch = React.useMemo(() => { + if (isLoading || !data) { + return ; + } + const checked = (configUpdateRequests.disableCpuAffinity ?? data.disableCpuAffinity) || false; + return ; + }, [data, isLoading, onChangeDisableCpuAffinity, isProcessing, configUpdateRequests]); + + const parallelDecompressorCountInput = React.useMemo(() => { + if (isLoading || !data) { + return ; + } + const value = configUpdateRequests.parallelDecompressorCount ?? data.parallelDecompressorCount; + return ( + + ); + }, [data, isLoading, onChangeParallelDecompressorCount, isProcessing, configUpdateRequests]); + + const decompressorThreadCountInput = React.useMemo(() => { + if (isLoading || !data) { + return ; + } + const value = configUpdateRequests.decompressorThreadCount ?? data.decompressorThreadCount; + return ( + + ); + }, [data, isLoading, onChangeDecompressorThreadCount, isProcessing, configUpdateRequests]); + + const recursivePlotScanSwitch = React.useMemo(() => { + if (isLoading || !data) { + return ; + } + const checked = (configUpdateRequests.recursivePlotScan ?? data.recursivePlotScan) || false; + return ; + }, [data, isLoading, onChangeRecursivePlotScan, isProcessing, configUpdateRequests]); + + const refreshParameterIntervalSecondsInput = React.useMemo(() => { + if (isLoading || !data) { + return ; + } + const value = configUpdateRequests.refreshParameterIntervalSeconds ?? data.refreshParameterIntervalSeconds; + return ( + + ); + }, [data, isLoading, onChangeRefreshParameterIntervalSeconds, isProcessing, configUpdateRequests]); + + const restartButton = React.useMemo(() => { + if (!data || isLoading) { + return null; + } + + let isRestartRequired = false; + + const updateRequestsKeys = Object.keys(configUpdateRequests) as Array; + for (let i = 0; i < updateRequestsKeys.length; i++) { + const key = updateRequestsKeys[i]; + if (configUpdateRequests[key] !== null && data[key] !== configUpdateRequests[key]) { + isRestartRequired = true; + break; + } + } + + return ( + } + disabled={!isRestartRequired} + > + Restart Local Harvester to apply changes + + ); + }, [data, isLoading, configUpdateRequests, onClickRestartHarvester, isProcessing]); + + return ( + + + + + Harvester Services + + + + Harvester manages plots and fetches proofs of space corresponding to challenges sent by a farmer. + + + + + + + + + + + + All changes below will take effect the next time Harvester restarts. + + + + + + + + Recursive Plot Scan + + + + + + + + Whether to scan plots directory recursively + + + + + + + + Plots Refresh Interval (seconds) + + + + + + + + Interval seconds to refresh plots. + + + + + + + + Enable GPU Harvesting + + + + + + + + Enable/Disable GPU harvesting + + + + + + + + GPU Device Index + + + + + + + + Specify GPU device to harvest + + + + + + + + Enforce GPU Index + + + + + + + + * Enabled - use device at specified index if available; else error +
+ + * Disabled - use device at specified index if available; if not, attempt to use device at other indices; + if no GPUs available, use CPU + +
+
+
+ + + + + Disable CPU Affinity + + + + + + + + + Disable assigning automatic thread affinity. This is useful when you want to manually assign thread + affinity. + + + + + + + + + Parallel Decompressor Count + + + + + + + + Number of proofs decompressed in parallel during harvesting + + + + + + + + Decompressor Thread Count + + + + + + + + + Number of threads for a decompressor context. +
+ The product of "Parallel Decompressors Count" and this value must be less than or equal to the total + thread count on system. +
+
+
+
+ + + + {restartButton} + + + + + *Usually it takes seconds to complete restarting. + + + +
+
+ ); +} diff --git a/packages/gui/src/components/vcs/VCList.tsx b/packages/gui/src/components/vcs/VCList.tsx index 4c37753389..e06cc4ada8 100644 --- a/packages/gui/src/components/vcs/VCList.tsx +++ b/packages/gui/src/components/vcs/VCList.tsx @@ -5,7 +5,7 @@ import { useLazyGetProofsForRootQuery, useVCCoinAdded, } from '@chia-network/api-react'; -import { Flex, More, MenuItem, AlertDialog, useOpenDialog, useDarkMode } from '@chia-network/core'; +import { Flex, More, MenuItem, AlertDialog, Loading, useOpenDialog, useDarkMode } from '@chia-network/core'; import { VCZeroStateBackground as VCZeroStateBackgroundIcon, VCZeroStateBackgroundDark as VCZeroStateBackgroundDarkIcon, @@ -149,7 +149,9 @@ export default function VCList() { return []; }, [VCsLocalStorage, blockchainVCs?.vcRecords, fingerprint, proofs]); - if (isLoading) return null; + if (isLoading) { + return ; + } const allVCsSortLatest = sortByTimestamp ? allVCs.sort((a: any, b: any) => { @@ -300,7 +302,7 @@ export default function VCList() { ); } - if (allVCsSortLatest.length === 0) { + if (!allVCsSortLatest?.length) { return renderZeroState(); } @@ -313,7 +315,7 @@ export default function VCList() { - Verifiable Credentials: {allVCs.length} + Verifiable Credentials: {allVCs?.length ?? 0} {renderActionsDropdown()} diff --git a/packages/gui/src/constants/PlotterName.ts b/packages/gui/src/constants/PlotterName.ts index 9a1a650274..618278ee79 100644 --- a/packages/gui/src/constants/PlotterName.ts +++ b/packages/gui/src/constants/PlotterName.ts @@ -1,6 +1,7 @@ enum PlotterName { BLADEBIT_RAM = 'bladebit_ram', BLADEBIT_DISK = 'bladebit_disk', + BLADEBIT_CUDA = 'bladebit_cuda', CHIAPOS = 'chiapos', MADMAX = 'madmax', } diff --git a/packages/gui/src/constants/plotSizes.ts b/packages/gui/src/constants/plotSizes.ts index 578c3354ea..013640f9be 100644 --- a/packages/gui/src/constants/plotSizes.ts +++ b/packages/gui/src/constants/plotSizes.ts @@ -1,48 +1,51 @@ import PlotterName from './PlotterName'; type PlotSize = { - label: string; + effectivePlotSize: string; value: number; workspace: string; defaultRam: number; }; -export function getPlotSize(kSize: 25 | 32 | 33 | 34 | 35) { - return ( - { - 25: '600MiB', - 32: '101.4GiB', - 33: '208.8GiB', - 34: '429.8GiB', - 35: '884.1GiB', - }[kSize] || 'Size Unknown' - ); +export function getEffectivePlotSize(kSize: 25 | 32 | 33 | 34 | 35) { + const sizeInBytes = (2 * kSize + 1) * 2 ** (kSize - 1); + if (kSize < 32) { + return `${sizeInBytes / 1024 / 1024}MiBe`; + } + return `${sizeInBytes / 1024 / 1024 / 1024}GiBe`; } export const plottingInfo: Record = { [PlotterName.CHIAPOS]: [ - { value: 25, label: getPlotSize(25), workspace: '1.8GiB', defaultRam: 512 }, - { value: 32, label: getPlotSize(32), workspace: '239GiB', defaultRam: 3390 }, - { value: 33, label: getPlotSize(33), workspace: '521GiB', defaultRam: 7400 }, + { value: 25, effectivePlotSize: getEffectivePlotSize(25), workspace: '1.8GiB', defaultRam: 512 }, + { value: 32, effectivePlotSize: getEffectivePlotSize(32), workspace: '239GiB', defaultRam: 3390 }, + { value: 33, effectivePlotSize: getEffectivePlotSize(33), workspace: '521GiB', defaultRam: 7400 }, // workspace are guesses using 55.35% - rounded up - past here - { value: 34, label: getPlotSize(34), workspace: '1041GiB', defaultRam: 14_800 }, - { value: 35, label: getPlotSize(35), workspace: '2175GiB', defaultRam: 29_600 }, + { value: 34, effectivePlotSize: getEffectivePlotSize(34), workspace: '1041GiB', defaultRam: 14_800 }, + { value: 35, effectivePlotSize: getEffectivePlotSize(35), workspace: '2175GiB', defaultRam: 29_600 }, ], [PlotterName.MADMAX]: [ - { value: 25, label: getPlotSize(25), workspace: '1.8GiB', defaultRam: 512 }, - { value: 32, label: getPlotSize(32), workspace: '239GiB', defaultRam: 3390 }, - { value: 33, label: getPlotSize(33), workspace: '521GiB', defaultRam: 7400 }, + { value: 25, effectivePlotSize: getEffectivePlotSize(25), workspace: '1.8GiB', defaultRam: 512 }, + { value: 32, effectivePlotSize: getEffectivePlotSize(32), workspace: '239GiB', defaultRam: 3390 }, + { value: 33, effectivePlotSize: getEffectivePlotSize(33), workspace: '521GiB', defaultRam: 7400 }, // workspace are guesses using 55.35% - rounded up - past here - { value: 34, label: getPlotSize(34), workspace: '1041GiB', defaultRam: 14_800 }, - { value: 35, label: getPlotSize(35), workspace: '2175GiB', defaultRam: 29_600 }, + { value: 34, effectivePlotSize: getEffectivePlotSize(34), workspace: '1041GiB', defaultRam: 14_800 }, + { value: 35, effectivePlotSize: getEffectivePlotSize(35), workspace: '2175GiB', defaultRam: 29_600 }, + ], + [PlotterName.BLADEBIT_RAM]: [ + { value: 32, effectivePlotSize: getEffectivePlotSize(32), workspace: '416GiB', defaultRam: 3390 }, + ], + [PlotterName.BLADEBIT_DISK]: [ + { value: 32, effectivePlotSize: getEffectivePlotSize(32), workspace: '480GiB', defaultRam: 3390 }, + ], + [PlotterName.BLADEBIT_CUDA]: [ + { value: 32, effectivePlotSize: getEffectivePlotSize(32), workspace: '128GiB', defaultRam: 128_000 }, ], - [PlotterName.BLADEBIT_RAM]: [{ value: 32, label: getPlotSize(32), workspace: '416GiB', defaultRam: 3390 }], - [PlotterName.BLADEBIT_DISK]: [{ value: 32, label: getPlotSize(32), workspace: '480GiB', defaultRam: 3390 }], }; export function getPlotSizeOptions(plotterName: PlotterName) { return plottingInfo[plotterName].map((item) => ({ value: item.value, - label: `${item.label} (k=${item.value}, temporary space: ${item.workspace})`, + label: `k=${item.value} (Effective plot size: ${item.effectivePlotSize}, Temporary space: ${item.workspace})`, })); } diff --git a/packages/gui/src/hooks/useFarmerStatus.ts b/packages/gui/src/hooks/useFarmerStatus.ts index 415ca5c7c7..46ebf9ba66 100644 --- a/packages/gui/src/hooks/useFarmerStatus.ts +++ b/packages/gui/src/hooks/useFarmerStatus.ts @@ -1,32 +1,50 @@ -import { ServiceName } from '@chia-network/api'; +import { BlockchainState, ServiceName } from '@chia-network/api'; import { useService } from '@chia-network/api-react'; import FarmerStatus from '../constants/FarmerStatus'; import FullNodeState from '../constants/FullNodeState'; import useFullNodeState from './useFullNodeState'; -export default function useFarmerStatus(): FarmerStatus { - const { state: fullNodeState, isLoading: isLoadingFullNodeState } = useFullNodeState(); +export default function useFarmerStatus(): { + farmerStatus: FarmerStatus; + blockchainState?: BlockchainState; +} { + const { state: fullNodeState, isLoading: isLoadingFullNodeState, data: blockchainState } = useFullNodeState(); const { isRunning, isLoading: isLoadingIsRunning } = useService(ServiceName.FARMER); const isLoading = isLoadingIsRunning || isLoadingFullNodeState; if (fullNodeState === FullNodeState.SYNCHING) { - return FarmerStatus.SYNCHING; + return { + farmerStatus: FarmerStatus.SYNCHING, + blockchainState, + }; } if (fullNodeState === FullNodeState.ERROR) { - return FarmerStatus.NOT_AVAILABLE; + return { + farmerStatus: FarmerStatus.NOT_AVAILABLE, + blockchainState, + }; } if (isLoading /* || !farmerConnected */) { - return FarmerStatus.NOT_CONNECTED; + return { + farmerStatus: FarmerStatus.NOT_CONNECTED, + blockchainState, + }; } if (!isRunning) { - return FarmerStatus.NOT_RUNNING; + return { + farmerStatus: FarmerStatus.NOT_RUNNING, + blockchainState, + }; } - return FarmerStatus.FARMING; + return { + farmerStatus: FarmerStatus.FARMING, + blockchainState, + }; } diff --git a/packages/gui/src/hooks/useFullNodeState.ts b/packages/gui/src/hooks/useFullNodeState.ts index bc9b825702..085a7356da 100644 --- a/packages/gui/src/hooks/useFullNodeState.ts +++ b/packages/gui/src/hooks/useFullNodeState.ts @@ -1,3 +1,4 @@ +import { BlockchainState } from '@chia-network/api'; import { useGetBlockchainStateQuery } from '@chia-network/api-react'; import FullNodeState from '../constants/FullNodeState'; @@ -5,6 +6,7 @@ import FullNodeState from '../constants/FullNodeState'; export default function useFullNodeState(): { isLoading: boolean; state?: FullNodeState; + data?: BlockchainState; error?: Error; } { const { @@ -32,6 +34,7 @@ export default function useFullNodeState(): { return { isLoading, state, - error, + data: blockchainState, + error: error as Error, }; } diff --git a/packages/gui/src/hooks/usePlotNFTName.ts b/packages/gui/src/hooks/usePlotNFTName.ts index 74bda3d3d8..081dce13b8 100644 --- a/packages/gui/src/hooks/usePlotNFTName.ts +++ b/packages/gui/src/hooks/usePlotNFTName.ts @@ -7,7 +7,7 @@ const uniqueNames: { [key: string]: string; } = {}; -function getUniqueName(seed: string, iteration = 0): string { +export function getUniqueName(seed: string, iteration = 0): string { const computedName = Object.keys(uniqueNames).find((key) => uniqueNames[key] === seed); if (computedName) { return computedName; diff --git a/packages/gui/src/hooks/useUnconfirmedPlotNFTs.ts b/packages/gui/src/hooks/useUnconfirmedPlotNFTs.ts index 5965e21d98..649c6e14e9 100644 --- a/packages/gui/src/hooks/useUnconfirmedPlotNFTs.ts +++ b/packages/gui/src/hooks/useUnconfirmedPlotNFTs.ts @@ -9,7 +9,7 @@ const LOCAL_STORAGE_KEY = 'unconfirmedPlotNFTsV2'; export default function useUnconfirmedPlotNFTs(): { isLoading: boolean; unconfirmed: UnconfirmedPlotNFT[]; - add: (item: UnconfirmedPlotNFT) => void; + add: (item: Omit) => void; remove: (transactionId: string) => void; } { const { data: fingerprint, isLoading } = useGetLoggedInFingerprintQuery(); diff --git a/packages/gui/src/types/PlotAdd.ts b/packages/gui/src/types/PlotAdd.ts index 960f6af8bc..9a42a79490 100644 --- a/packages/gui/src/types/PlotAdd.ts +++ b/packages/gui/src/types/PlotAdd.ts @@ -1,10 +1,11 @@ import Fingerprint from './Fingerprint'; type PlotAdd = { - plotType?: 'ramplot' | 'diskplot'; + plotType?: 'ramplot' | 'diskplot' | 'cudaplot'; bladebitDisableNUMA?: boolean; bladebitWarmStart?: boolean; bladebitNoCpuAffinity?: boolean; + bladebitCompressionLevel?: number; bladebit2Cache?: number; bladebit2F1Threads?: number; bladebit2FpThreads?: number; @@ -14,11 +15,12 @@ type PlotAdd = { bladebit2Alternate?: boolean; bladebit2NoT1Direct?: boolean; bladebit2NoT2Direct?: boolean; + bladebitDeviceIndex?: number; + bladebitDisableDirectDownloads?: boolean; c: string; delay: number; disableBitfieldPlotting?: boolean; excludeFinalDir?: boolean; - farmerPublicKey?: string; finalLocation: string; fingerprint?: Fingerprint; madmaxNumBucketsPhase3?: number; @@ -33,10 +35,11 @@ type PlotAdd = { plotCount: number; plotSize: number; plotterName: string; - poolPublicKey?: string; queue: string; workspaceLocation: string; workspaceLocation2: string; + farmerPublicKey?: string; + poolPublicKey?: string; }; export default PlotAdd; diff --git a/packages/gui/src/types/Plotter.ts b/packages/gui/src/types/Plotter.ts index 22b752fedf..5a34fab01e 100644 --- a/packages/gui/src/types/Plotter.ts +++ b/packages/gui/src/types/Plotter.ts @@ -5,13 +5,14 @@ interface CommonOptions { canPlotInParallel: boolean; canDelayParallelPlots: boolean; canSetBufferSize: boolean; + haveTempDir: boolean; } interface BladeBitRamOptions extends CommonOptions { haveBladebitWarmStart: boolean; haveBladebitDisableNUMA: boolean; haveBladebitNoCpuAffinity: boolean; - haveBladebitOutputDir: boolean; + haveBladebitCompressionLevel: boolean; } interface BladeBitDiskOptions extends BladeBitRamOptions { @@ -26,13 +27,22 @@ interface BladeBitDiskOptions extends BladeBitRamOptions { haveBladebitDiskNoT2Direct: boolean; } +interface BladeBitCudaOptions extends BladeBitRamOptions { + haveBladebitDeviceIndex: boolean; + haveBladebitDisableDirectDownloads: boolean; +} + interface MadMaxOptions extends CommonOptions { haveMadmaxNumBucketsPhase3: boolean; haveMadmaxThreadMultiplier: boolean; haveMadmaxTempToggle: boolean; } -export type PlotterOptions = CommonOptions & BladeBitRamOptions & BladeBitDiskOptions & MadMaxOptions; +export type PlotterOptions = CommonOptions & + BladeBitRamOptions & + BladeBitDiskOptions & + BladeBitCudaOptions & + MadMaxOptions; interface CommonDefaults { plotterName: string; @@ -45,10 +55,11 @@ interface CommonDefaults { } interface BladeBitRamDefaults extends CommonDefaults { - plotType?: 'ramplot' | 'diskplot'; + plotType?: 'ramplot' | 'diskplot' | 'cudaplot'; bladebitWarmStart?: boolean; bladebitDisableNUMA?: boolean; bladebitNoCpuAffinity?: boolean; + bladebitCompressionLevel?: number; } interface BladeBitDiskDefaults extends BladeBitRamDefaults { @@ -63,6 +74,11 @@ interface BladeBitDiskDefaults extends BladeBitRamDefaults { bladebitDiskNoT2Direct?: boolean; } +interface BladeBitCudaDefaults extends BladeBitRamDefaults { + bladebitDeviceIndex?: number; + bladebitDisableDirectDownloads?: boolean; +} + interface MadMaxDefaults extends CommonDefaults { madmaxNumBucketsPhase3?: number; madmaxThreadMultiplier?: number; @@ -70,7 +86,11 @@ interface MadMaxDefaults extends CommonDefaults { madmaxTempToggle?: boolean; } -export type PlotterDefaults = CommonDefaults & BladeBitRamDefaults & BladeBitDiskDefaults & MadMaxDefaults; +export type PlotterDefaults = CommonDefaults & + BladeBitRamDefaults & + BladeBitDiskDefaults & + BladeBitCudaDefaults & + MadMaxDefaults; type PlotterInstallInfo = { version?: string; diff --git a/packages/gui/src/util/math.ts b/packages/gui/src/util/math.ts new file mode 100644 index 0000000000..65f9066646 --- /dev/null +++ b/packages/gui/src/util/math.ts @@ -0,0 +1,48 @@ +const ln2pi = Math.log(2 * Math.PI); +// Compute ln(n!) - natural logarithm of the factorial of n +function lnFact(m: number) { + let k = m; + if (m === 0 || m === 1) { + return 0; + } + if (m < 10) { + // Compute factorial directly for small n + let f = 2; + for (let i = 3; i <= m; i++) { + f *= i; + } + return Math.log(f); + } + // Log-Gamma function approximation + k++; + const lnN = Math.log(k); + const one810 = 0.001_234_567_901_234_567_9; + let ret = ln2pi - lnN; + let k6 = k * k * k; + k6 *= k6; + ret += k * (2 * lnN + Math.log(k * Math.sinh(1 / k) + one810 / k6) - 2); + ret /= 2; + return ret; +} + +// Compute ln(C(n, k)) - natural logarithm of the binomial coefficient C(n, k) +function lnComb(m: number, k: number, lnFactM: number) { + return lnFactM - lnFact(k) - lnFact(m - k); +} + +// Compute probability P(X <= t) where X has binomial distribution with n +// trials and success probability p. +export function binomialProb(n: number, p: number, t: number) { + let s = 0; + const lnP = Math.log(p); + const lnPInv = Math.log(1 - p); + const lnFactN = lnFact(n); + + for (let i = 0; i <= t; i++) { + const c = lnComb(n, i, lnFactN); + const lnProb = c + i * lnP + (n - i) * lnPInv; + s += Math.exp(lnProb); + } + + return s; +} diff --git a/packages/gui/src/util/plot.ts b/packages/gui/src/util/plot.ts new file mode 100644 index 0000000000..1e84e7b784 --- /dev/null +++ b/packages/gui/src/util/plot.ts @@ -0,0 +1 @@ +export const PLOT_FILTER = 512; diff --git a/packages/icons/src/Farm.tsx b/packages/icons/src/Farm.tsx index 5e6245b7b0..c1c0bdb63b 100644 --- a/packages/icons/src/Farm.tsx +++ b/packages/icons/src/Farm.tsx @@ -1,8 +1,8 @@ import { SvgIcon, SvgIconProps } from '@mui/material'; import React from 'react'; -import FarmIcon from './images/farm.svg'; +import FarmIcon from './images/Farm.svg'; export default function Farm(props: SvgIconProps) { - return ; + return ; } diff --git a/packages/icons/src/Harvest.tsx b/packages/icons/src/Harvest.tsx new file mode 100644 index 0000000000..1c85258ed3 --- /dev/null +++ b/packages/icons/src/Harvest.tsx @@ -0,0 +1,8 @@ +import { SvgIcon, SvgIconProps } from '@mui/material'; +import React from 'react'; + +import HarvestIcon from './images/Harvest.svg'; + +export default function Harvest(props: SvgIconProps) { + return ; +} diff --git a/packages/icons/src/images/Farm.svg b/packages/icons/src/images/Farm.svg new file mode 100644 index 0000000000..e847717154 --- /dev/null +++ b/packages/icons/src/images/Farm.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/icons/src/images/Harvest.svg b/packages/icons/src/images/Harvest.svg new file mode 100644 index 0000000000..fa0fc3ae4b --- /dev/null +++ b/packages/icons/src/images/Harvest.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/icons/src/images/farm.svg b/packages/icons/src/images/farm.svg deleted file mode 100644 index fa5eb3ba7b..0000000000 --- a/packages/icons/src/images/farm.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/icons/src/index.ts b/packages/icons/src/index.ts index 9707e4154d..6793d8e16a 100644 --- a/packages/icons/src/index.ts +++ b/packages/icons/src/index.ts @@ -4,6 +4,7 @@ export { default as Farm } from './Farm'; export { default as Farming } from './Farming'; export { default as Fees } from './Fees'; export { default as FullNode } from './FullNode'; +export { default as Harvest } from './Harvest'; export { default as Home } from './Home'; export { default as Keys } from './Keys'; export { default as LinkSmall } from './Link'; diff --git a/packages/wallets/src/components/WalletHistoryClawbackChip.tsx b/packages/wallets/src/components/WalletHistoryClawbackChip.tsx index 403d97674a..c4553bc0f0 100644 --- a/packages/wallets/src/components/WalletHistoryClawbackChip.tsx +++ b/packages/wallets/src/components/WalletHistoryClawbackChip.tsx @@ -1,6 +1,6 @@ import { TransactionType } from '@chia-network/api'; import type { Transaction } from '@chia-network/api'; -import { useGetAutoClaimQuery } from '@chia-network/api-react'; +import { useGetAutoClaimQuery, useGetTimestampForHeightQuery, useGetHeightInfoQuery } from '@chia-network/api-react'; import { useTrans, Button } from '@chia-network/core'; import { defineMessage } from '@lingui/macro'; import { AccessTime as AccessTimeIcon } from '@mui/icons-material'; @@ -16,11 +16,23 @@ type Props = { export default function WalletHistoryClawbackChip(props: Props) { const { transactionRow, setClawbackClaimTransactionDialogProps } = props; - const { data: autoClaimData, isLoading } = useGetAutoClaimQuery(); - const isAutoClaimEnabled = !isLoading && autoClaimData?.enabled; + const { data: autoClaimData, isLoading: isGetAutoClaimLoading } = useGetAutoClaimQuery(); + const isAutoClaimEnabled = !isGetAutoClaimLoading && autoClaimData?.enabled; + + const { data: height, isLoading: isGetHeightInfoLoading } = useGetHeightInfoQuery(undefined, { + pollingInterval: 3000, + }); + + const { data: lastBlockTimeStampData, isLoading: isGetTimestampForHeightLoading } = useGetTimestampForHeightQuery({ + height: height || 0, + }); + + const lastBlockTimeStamp = lastBlockTimeStampData?.timestamp || 0; const t = useTrans(); + if (isGetHeightInfoLoading || isGetTimestampForHeightLoading || !lastBlockTimeStamp) return null; + let text = ''; let Icon; let onClick; @@ -28,7 +40,10 @@ export default function WalletHistoryClawbackChip(props: Props) { if (transactionRow.metadata?.timeLock) { canBeClaimedAt.add(transactionRow.metadata.timeLock, 'seconds'); } - const currentTime = moment(); + const currentTime = moment.unix(lastBlockTimeStamp - 20); // extra 20 seconds so if the auto claim is enabled, it will not show to button to claim it + // console.log('currentTime___: ', currentTime.format()); + // console.log('canBeClaimedAt: ', canBeClaimedAt.format()); + const timeLeft = canBeClaimedAt.diff(currentTime, 'seconds'); // when you are a recipient of a new clawback transaction @@ -47,7 +62,7 @@ export default function WalletHistoryClawbackChip(props: Props) { message: 'Can be claimed in ', }) ); - text += canBeClaimedAt.fromNow(true); // ... 3 days + text += canBeClaimedAt.from(currentTime, true); // ... 3 days } else if (transactionRow.sent === 0) { text = t( defineMessage({ diff --git a/packages/wallets/src/components/cat/WalletCAT.tsx b/packages/wallets/src/components/cat/WalletCAT.tsx index 3f54a3d371..68d33cb657 100644 --- a/packages/wallets/src/components/cat/WalletCAT.tsx +++ b/packages/wallets/src/components/cat/WalletCAT.tsx @@ -4,7 +4,7 @@ import { Flex, Loading, MenuItem, useOpenDialog } from '@chia-network/core'; import { Offers as OffersIcon } from '@chia-network/icons'; import { Trans } from '@lingui/macro'; import { Edit as RenameIcon, Fingerprint as FingerprintIcon } from '@mui/icons-material'; -import { Box, ListItemIcon, Alert, Typography } from '@mui/material'; +import { ListItemIcon, Alert, Typography } from '@mui/material'; import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -107,18 +107,22 @@ export default function WalletCAT(props: Props) { ]} /> - - - - - - - - - - - - + + + + {(() => { + switch (selectedTab) { + case 'summary': + return ; + case 'send': + return ; + case 'receive': + return ; + default: + return null; + } + })()} + ); }