diff --git a/packages/cli/package/src/lib/chain/conversions.ts b/packages/cli/package/src/lib/chain/conversions.ts index 8f043f130..46247b8c7 100644 --- a/packages/cli/package/src/lib/chain/conversions.ts +++ b/packages/cli/package/src/lib/chain/conversions.ts @@ -15,7 +15,9 @@ * along with this program. If not, see . */ -import { bufferToBase64 } from "../helpers/typesafeStringify.js"; +import type { ResourceType } from "../configs/project/provider/provider4.js"; +import { stringifyUnknown } from "../helpers/stringifyUnknown.js"; +import { bufferToBase64, numToStr } from "../helpers/typesafeStringify.js"; const PREFIX = new Uint8Array([0, 36, 8, 1, 18, 32]); const BASE_58_PREFIX = "z"; @@ -77,3 +79,40 @@ export function hexStringToUTF8ToBase64String(hexString: string): string { return bufferToBase64(Buffer.from(cleanHexString, "utf-8")); } + +export async function resourceSupply( + resourceType: ResourceType, + supply: number, +) { + const xbytes = (await import("xbytes")).default; + + switch (resourceType) { + case "cpu": + return { supply, supplyString: numToStr(supply) }; + case "ram": + return { + supply: Math.round(supply / (await getRamToBytesFromChain())), + supplyString: xbytes(supply), + }; + case "storage": + return { + supply: Math.round(supply / STORAGE_TO_BYTES), + supplyString: xbytes(supply), + }; + case "bandwidth": + return { supply, supplyString: numToStr(supply) }; + case "ip": + return { supply, supplyString: numToStr(supply) }; + default: + const unknownResourceType: never = resourceType; + throw new Error( + `Unknown resource type ${stringifyUnknown(unknownResourceType)}`, + ); + } +} + +const STORAGE_TO_BYTES = 1_000_000_000; + +async function getRamToBytesFromChain() { + return Promise.resolve(1_000_000_000); +} diff --git a/packages/cli/package/src/lib/chain/offer/offer.ts b/packages/cli/package/src/lib/chain/offer/offer.ts index 099f3dd8a..78171d45e 100644 --- a/packages/cli/package/src/lib/chain/offer/offer.ts +++ b/packages/cli/package/src/lib/chain/offer/offer.ts @@ -158,7 +158,7 @@ export async function createOffers(flags: OffersArgs) { return [ contracts.deployment.usdc, resourcePricesArray, - computePeersToRegister, + setCPUSupplyForCP(computePeersToRegister), ]; }, getTitle() { @@ -265,7 +265,7 @@ export async function createOffers(flags: OffersArgs) { let offersInfo: GetOffersInfoReturnType[1] = []; const getOffersInfoRes = await setTryTimeout( - "Getting offers info from indexer", + "get offers info from indexer", async () => { [offerInfoErrors, offersInfo] = await getOffersInfo(createdOffers); @@ -299,6 +299,53 @@ ${await offersInfoToString([offerInfoErrors, offersInfo])} `); } +function setCPUSupplyForCP( + computePeersToRegister: { + peerId: Uint8Array; + unitIds: Uint8Array[]; + resourcesByType: { + readonly cpu: { + readonly resourceId: `0x${string}`; + readonly details: string; + readonly name: string; + readonly supply: number; + }; + }; + owner: string; + }[], +) { + return computePeersToRegister.map( + ({ + peerId, + unitIds, + resourcesByType: { cpu, ...restResources }, + owner, + }) => { + return { + peerId, + unitIds, + resources: [ + setCPUSupply(cpu, unitIds), + ...resourcesByTypeToArr(restResources), + ], + owner, + }; + }, + ); +} + +function setCPUSupply( + cpu: { + readonly resourceId: `0x${string}`; + readonly details: string; + readonly name: string; + readonly supply: number; + }, + unitIds: Uint8Array[], +): OnChainResource { + return { ...cpu, supply: unitIds.length * 2 }; +} + async function confirmOffer(offer: EnsureOfferConfig) { return confirm({ message: `The following offer will be created: ${color.yellow(offer.offerName)}\n${yamlDiffPatch( @@ -361,7 +408,7 @@ function formatOfferResource({ details, }: { resourceId: `0x${string}`; - supply: number; + supply: number | string; details: string; }) { return { @@ -400,7 +447,7 @@ export async function addRemainingCPs({ sliceValuesToRegister: sliceCPsByNumberOfCUs(remainingCPs), sliceIndex: CUsToRegisterCount, getArgs(CPsToRegister) { - return [offerId, CPsToRegister]; + return [offerId, setCPUSupplyForCP(CPsToRegister)]; }, getTitle({ valuesToRegister: CPsToRegister }) { return `Add compute peers:\n${CPsToRegister.map( @@ -462,7 +509,11 @@ async function addRemainingCUs({ }, sliceIndex: CUs.length, getArgs(CUsToRegister) { - return [peerId, CUsToRegister, resourcesByType.cpu]; + return [ + peerId, + CUsToRegister, + setCPUSupply(resourcesByType.cpu, CUsToRegister), + ]; }, getTitle({ sliceCount: numberOfCUsForAddCU }) { return `Add ${numToStr(numberOfCUsForAddCU)} compute units\nto compute peer ${cpName} (${peerIdBase58})\nfor offer ${offerName} (${offerId})`; @@ -723,11 +774,6 @@ export async function resolveOffersFromProviderConfig( return new Uint8Array(Buffer.from(id.slice(2), "hex")); }), owner: offerIndexerInfo.providerId, - resources: Object.values(resourcesByTypeWithName).flatMap( - (resource) => { - return Array.isArray(resource) ? resource : [resource]; - }, - ), resourcesByType: resourcesByTypeWithName, }; }, @@ -882,6 +928,8 @@ async function ensureOfferConfigs() { "Unreachable. Config is validated so it must return result", ); + const xbytes = (await import("xbytes")).default; + const resources = { cpu: { ...cpu, @@ -890,6 +938,7 @@ async function ensureOfferConfigs() { }, ram: { ...ram, + supply: xbytes.parseSize(ram.supply), resourceId: `0x${ramId}`, details: JSON.stringify(ram.details), }, @@ -897,6 +946,7 @@ async function ensureOfferConfigs() { ({ id: storageId, ...storage }) => { return { ...storage, + supply: xbytes.parseSize(storage.supply), resourceId: `0x${storageId}`, details: JSON.stringify(storage.details), }; @@ -925,9 +975,6 @@ async function ensureOfferConfigs() { unitIds: times(resourcesWithIds.cpu.supply).map(() => { return randomBytes(32); }), - resources: Object.values(resources).flatMap((resource) => { - return Array.isArray(resource) ? resource : [resource]; - }), resourcesByType: resources, owner: walletAddress, }; @@ -964,6 +1011,16 @@ async function ensureOfferConfigs() { ); } +function resourcesByTypeToArr( + resourcesByType: Partial< + Record + >, +) { + return Object.values(resourcesByType).flatMap((resource) => { + return Array.isArray(resource) ? resource : [resource]; + }); +} + type CPFromProviderConfig = Awaited< ReturnType >[number]["computePeersFromProviderConfig"][number]; diff --git a/packages/cli/package/src/lib/chain/offer/updateOffers.ts b/packages/cli/package/src/lib/chain/offer/updateOffers.ts index 7a70d4366..054493981 100644 --- a/packages/cli/package/src/lib/chain/offer/updateOffers.ts +++ b/packages/cli/package/src/lib/chain/offer/updateOffers.ts @@ -19,6 +19,7 @@ import { color } from "@oclif/color"; import omit from "lodash-es/omit.js"; import { commandObj } from "../../commandObj.js"; +import type { ResourceType } from "../../configs/project/provider/provider4.js"; import { initNewProviderArtifactsConfig } from "../../configs/project/providerArtifacts/providerArtifacts.js"; import { CLI_NAME, @@ -29,7 +30,10 @@ import { numToStr } from "../../helpers/typesafeStringify.js"; import { splitErrorsAndResults } from "../../helpers/utils.js"; import { confirm } from "../../prompt.js"; import { ensureFluenceEnv } from "../../resolveFluenceEnv.js"; -import { peerIdHexStringToBase58String } from "../conversions.js"; +import { + peerIdHexStringToBase58String, + resourceSupply, +} from "../conversions.js"; import { ptFormat, ptFormatWithSymbol } from "../currencies.js"; import { assertProviderIsRegistered } from "../providerInfo.js"; @@ -449,30 +453,40 @@ type ResourceInfo = { }; type ResourceSupplyUpdate = { - name: string; + resourceType: ResourceType; onChainResource: ResourceInfo; configuredResource: ResourceInfo; }; async function createResourceSupplyUpdateTx( peerId: string, - { name, onChainResource, configuredResource }: ResourceSupplyUpdate, + { resourceType, onChainResource, configuredResource }: ResourceSupplyUpdate, ) { - if (onChainResource.supply === configuredResource.supply) { + if (onChainResource.supply === configuredResource.supply * 2) { return null; } const { contracts } = await getContracts(); + const { supplyString: onChainSupplyString } = await resourceSupply( + resourceType, + configuredResource.supply, + ); + + const { supply, supplyString } = await resourceSupply( + resourceType, + configuredResource.supply, + ); + return { - description: `\nChanging ${name} supply from ${numToStr( - onChainResource.supply, - )} to ${numToStr(configuredResource.supply)}`, + description: `\nChanging ${resourceType} supply from ${ + onChainSupplyString + } to ${supplyString}`, tx: populateTx( contracts.diamond.changeResourceMaxSupplyV2, peerId, onChainResource.resourceId, - configuredResource.supply, + supply * 2, ), }; } @@ -496,12 +510,12 @@ async function populateChangeResourceSupplyTx({ const resourcePriceUpdates: ResourceSupplyUpdate[] = [ { - name: "CPU", + resourceType: "cpu", onChainResource: peer.resourcesByType.cpu, configuredResource: configuredPeer.resourcesByType.cpu, }, { - name: "RAM", + resourceType: "ram", onChainResource: peer.resourcesByType.ram, configuredResource: configuredPeer.resourcesByType.ram, }, @@ -516,19 +530,19 @@ async function populateChangeResourceSupplyTx({ } return { - name: "storage", + resourceType: "storage", onChainResource: onChainStorage, configuredResource: configuredStorage, - }; + } as const; }) .filter(Boolean), { - name: "IP", + resourceType: "ip", onChainResource: peer.resourcesByType.ip, configuredResource: configuredPeer.resourcesByType.ip, }, { - name: "bandwidth", + resourceType: "bandwidth", onChainResource: peer.resourcesByType.bandwidth, configuredResource: configuredPeer.resourcesByType.bandwidth, }, diff --git a/packages/cli/package/src/lib/configs/project/provider/provider4.ts b/packages/cli/package/src/lib/configs/project/provider/provider4.ts index df13b1b3a..8b5d0899a 100644 --- a/packages/cli/package/src/lib/configs/project/provider/provider4.ts +++ b/packages/cli/package/src/lib/configs/project/provider/provider4.ts @@ -321,7 +321,8 @@ const peerCPUSchema = { required: peerResourceSchema.required, } as const satisfies JSONSchemaType; -type PeerRAM = PeerResource & { +type PeerRAM = Omit & { + supply: string; details?: PeerRamDetails; }; @@ -335,15 +336,15 @@ const peerRAMSchema = { description: `${OVERRIDE_OR_EXTEND_DEDSCRIPTION}${peerRamDetailsSchema.description}`, }, supply: { - type: "integer", - minimum: 1, - description: "Amount of RAM in GB", + type: "string", + description: "Amount of RAM", }, }, required: peerResourceSchema.required, } as const satisfies JSONSchemaType; -type PeerStorage = PeerResource & { +type PeerStorage = Omit & { + supply: string; details?: PeerStorageDetails; }; @@ -357,8 +358,7 @@ const peerStorageSchema = { description: `${OVERRIDE_OR_EXTEND_DEDSCRIPTION}${peerStorageDetailsSchema.description}`, }, supply: { - type: "integer", - minimum: 1, + type: "string", description: "Amount of storage in GB", }, }, @@ -602,7 +602,7 @@ export default { }, validate(config) { return validateBatchAsync( - // TODO: validate 4 GB RAM per 1 CPU core + validateEnoughRAMPerCPUCore(config), validateCC(config), validateNoDuplicatePeerNamesInOffers(config), validateProtocolVersions(config), @@ -765,13 +765,13 @@ export function defaultComputePeerConfig({ }, ram: { name: RAM_RESOURCE_NAME, - supply: 1, + supply: "11 GB", details: DEFAULT_RAM_DETAILS, }, storage: [ { name: STORAGE_RESOURCE_NAME, - supply: 1, + supply: "11 GB", details: DEFAULT_STORAGE_DETAILS, }, ], @@ -1088,6 +1088,35 @@ export const onChainResourceTypeToResourceType: Record< [OnChainResourceType.PUBLIC_IP]: "ip", }; +const BYTES_PER_CORE = 4_000_000_000; + +async function validateEnoughRAMPerCPUCore({ + computePeers, +}: { + computePeers: ComputePeers; +}) { + const xbytes = (await import("xbytes")).default; + + const errors = Object.entries(computePeers).reduce( + (acc, [peerName, { resources }]) => { + const cpu = resources.cpu.supply; + const ram = xbytes.parseSize(resources.ram.supply); + const expectedRam = cpu * BYTES_PER_CORE; + + if (expectedRam > ram) { + acc.push( + `Compute peer ${color.yellow(peerName)} has not enough RAM per CPU core. Expected: ${xbytes(expectedRam)}. Got: ${xbytes(ram)}`, + ); + } + + return acc; + }, + [], + ); + + return errors.length === 0 ? true : errors.join("\n"); +} + function validateComputePeerIPs({ computePeers, }: { @@ -1529,68 +1558,6 @@ type ChainResources = { ip: Record; }; -// const resourcesMock = [ -// { -// ty: OnChainResourceType.VCPU, -// metadata: -// '{"manufacturer":"Intel","brand":"Xeon","architecture":"x86_64","generation":"Skylake"}', -// id: "0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCC1", -// }, -// { -// ty: OnChainResourceType.VCPU, -// metadata: -// '{"manufacturer":"AMD","brand":"EPYC","architecture":"x86_64","generation":"Rome"}', -// id: "0x211122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCC1", -// }, -// { -// ty: OnChainResourceType.RAM, -// metadata: '{"type":"DDR4","generation":"4"}', -// id: "0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCC2", -// }, -// { -// ty: OnChainResourceType.RAM, -// metadata: '{"type":"DDR4","generation":"5"}', -// id: "0x211122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCC2", -// }, -// { -// ty: OnChainResourceType.STORAGE, -// metadata: '{"type":"SSD"}', -// id: "0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCC3", -// }, -// { -// ty: OnChainResourceType.STORAGE, -// metadata: '{"type":"HDD"}', -// id: "0x211122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCC3", -// }, -// { -// ty: OnChainResourceType.NETWORK_BANDWIDTH, -// metadata: '{"type":"shared"}', -// id: "0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCC4", -// }, -// { -// ty: OnChainResourceType.PUBLIC_IP, -// metadata: '{"version":"IPv4"}', -// id: "0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCC5", -// }, -// { -// ty: OnChainResourceType.GPU, -// metadata: '{"manufacturer":"Nvidia","model":"RTX 3090"}', -// id: "0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCC6", -// }, -// ] as const satisfies { -// ty: OnChainResourceType; -// metadata: string; -// id: string; -// }[]; - -// async function getResources() { -// return new Promise((res) => { -// setTimeout(() => { -// res(resourcesMock); -// }, 1000); -// }); -// } - let resourcesPromise: undefined | Promise = undefined; async function getResourcesFromChain(): Promise { @@ -1602,10 +1569,8 @@ async function getResourcesFromChain(): Promise { } async function getResourcesFromChainImpl(): Promise { - // TODO: get resources from chain const { readonlyContracts } = await getReadonlyContracts(); const resources = await readonlyContracts.diamond.getResources(); - // const resources = await getResources(); const chainResources: ChainResources = { cpu: {}, diff --git a/packages/cli/package/src/lib/dealClient.ts b/packages/cli/package/src/lib/dealClient.ts index 3081186cd..30c28950a 100644 --- a/packages/cli/package/src/lib/dealClient.ts +++ b/packages/cli/package/src/lib/dealClient.ts @@ -706,6 +706,13 @@ export async function guessTxSizeAndSign< valuesToRegister = sliceValuesToRegister(sliceIndex); try { + dbg( + `\nESTIMATING GAS:\n\n${methodCallToString([ + signArgs.method, + ...(await getArgs(valuesToRegister)), + ])}\n\nESTIMATING GAS END\n`, + ); + const populatedTx = await populateTx( signArgs.method, ...(await getArgs(valuesToRegister)), diff --git a/packages/cli/package/yarn.lock b/packages/cli/package/yarn.lock index d15bd4888..13fde0c2c 100644 --- a/packages/cli/package/yarn.lock +++ b/packages/cli/package/yarn.lock @@ -1964,7 +1964,7 @@ __metadata: dependencies: "@actions/core": "npm:1.11.1" "@aws-sdk/lib-storage": "npm:^3.501.0" - "@fluencelabs/deal-ts-clients": "npm:0.23.2-feat-marketplace-v2-resources-6fb9f61-7385-1.0" + "@fluencelabs/deal-ts-clients": "npm:0.23.2-feat-marketplace-v2-resources-106f89e-7374-1.0" "@graphql-codegen/cli": "npm:^5.0.3" "@graphql-codegen/typescript": "npm:^4.1.1" "@graphql-codegen/typescript-graphql-request": "npm:^6.2.0" @@ -2038,9 +2038,9 @@ __metadata: languageName: unknown linkType: soft -"@fluencelabs/deal-ts-clients@npm:0.23.2-feat-marketplace-v2-resources-6fb9f61-7385-1.0": - version: 0.23.2-feat-marketplace-v2-resources-6fb9f61-7385-1.0 - resolution: "@fluencelabs/deal-ts-clients@npm:0.23.2-feat-marketplace-v2-resources-6fb9f61-7385-1.0" +"@fluencelabs/deal-ts-clients@npm:0.23.2-feat-marketplace-v2-resources-106f89e-7374-1.0": + version: 0.23.2-feat-marketplace-v2-resources-106f89e-7374-1.0 + resolution: "@fluencelabs/deal-ts-clients@npm:0.23.2-feat-marketplace-v2-resources-106f89e-7374-1.0" dependencies: "@graphql-typed-document-node/core": "npm:^3.2.0" debug: "npm:^4.3.4" @@ -2052,7 +2052,7 @@ __metadata: graphql-tag: "npm:^2.12.6" ipfs-http-client: "npm:^60.0.1" multiformats: "npm:^13.0.1" - checksum: 10c0/779a403c6dde8833246cc55b42d6979a6f30e7fc46c3fcbe2e8e8558a1a766efd0d4211ce1b06db6db8c22374320752dc0338ea5a067373e3061e598f6e65ce4 + checksum: 10c0/8adbb8f6521efc8a81cc8d18d9ac58cc4b6b39d9024e5b816cd79b95571a8a51f157f97db3401187f9925378f44031858ed359d2183e08803b1284671fbad2a5 languageName: node linkType: hard