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 760d1a0d4..d805fbac4 100644 --- a/packages/cli/package/src/lib/configs/project/provider/provider4.ts +++ b/packages/cli/package/src/lib/configs/project/provider/provider4.ts @@ -337,13 +337,13 @@ type OfferResource = Record; export type ResourcePrices = Record; -const CPU_PRICE_UNITS = "USDC/PhysicalCore"; -const RAM_PRICE_UNITS = "USDC/MiB"; -const STORAGE_PRICE_UNITS = "USDC/MiB"; -const BANDWIDTH_PRICE_UNITS = "USDC/Mb"; -const IP_PRICE_UNITS = "USDC/IP"; +export const CPU_PRICE_UNITS = "USDC/PhysicalCore"; +export const RAM_PRICE_UNITS = "USDC/MiB"; +export const STORAGE_PRICE_UNITS = "USDC/MiB"; +export const BANDWIDTH_PRICE_UNITS = "USDC/Mb"; +export const IP_PRICE_UNITS = "USDC/IP"; -const offerResourcesSchema = { +const offerResourcePricesSchema = { type: "object", description: "Resource prices for the offer", additionalProperties: false, @@ -420,7 +420,7 @@ const offerSchema = { items: { type: "string" }, uniqueItems: true, }, - resourcePrices: offerResourcesSchema, + resourcePrices: offerResourcePricesSchema, minProtocolVersion: { type: "integer", description: `Min protocol version. Must be less then or equal to maxProtocolVersion. Default: ${numToStr( @@ -550,8 +550,9 @@ export default { }, async refineSchema(schema) { const dataCentersFromChain = await getDataCentersFromChain(); + const resourcesFromChain = await getResourcesFromChain(); - const dataCenters = { + const offer = { properties: { dataCenterName: { type: "string", @@ -564,72 +565,43 @@ export default { }, ), }, + resourcePrices: { + properties: { + cpu: { properties: {} }, + ram: { properties: {} }, + storage: { properties: {} }, + bandwidth: { properties: {} }, + ip: { properties: {} }, + }, + }, }, }; - const resourcesFromChain = await getResourcesFromChain(); + function oneOfResources(resourceType: ResourceType) { + return { + properties: { + name: { + oneOf: Object.entries(resourcesFromChain[resourceType]).map( + ([name, { id }]) => { + return { const: name, description: `id: ${id}` }; + }, + ), + }, + }, + }; + } const resourceNames = { properties: { resources: { properties: { - cpu: { - properties: { - name: { - oneOf: Object.entries(resourcesFromChain.cpu).map( - ([name, { id }]) => { - return { const: name, description: `id: ${id}` }; - }, - ), - }, - }, - }, - ram: { - properties: { - name: { - oneOf: Object.entries(resourcesFromChain.ram).map( - ([name, { id }]) => { - return { const: name, description: `id: ${id}` }; - }, - ), - }, - }, - }, + cpu: oneOfResources("cpu"), + ram: oneOfResources("ram"), storage: { - items: { - properties: { - name: { - oneOf: Object.entries(resourcesFromChain.storage).map( - ([name, { id }]) => { - return { const: name, description: `id: ${id}` }; - }, - ), - }, - }, - }, - }, - bandwidth: { - properties: { - name: { - oneOf: Object.entries(resourcesFromChain.bandwidth).map( - ([name, { id }]) => { - return { const: name, description: `id: ${id}` }; - }, - ), - }, - }, - }, - ip: { - properties: { - name: { - oneOf: Object.entries(resourcesFromChain.ip).map( - ([name, { id }]) => { - return { const: name, description: `id: ${id}` }; - }, - ), - }, - }, + items: oneOfResources("storage"), }, + bandwidth: oneOfResources("bandwidth"), + ip: oneOfResources("ip"), }, }, }, @@ -645,8 +617,8 @@ export default { }, }, offers: { - additionalProperties: dataCenters, - properties: { [OFFER_NAME_EXAMPLE]: dataCenters }, + additionalProperties: offer, + properties: { [OFFER_NAME_EXAMPLE]: offer }, }, computePeers: { additionalProperties: resourceNames, @@ -655,44 +627,67 @@ export default { }, }); - mergedSchema.properties.resources.properties.cpu.properties = - Object.fromEntries( - Object.entries(resourcesFromChain.cpu).map(([name, { id }]) => { - return [ - name, - { - ...peerCPUDetailsSchema, - description: `id: ${id}. ${peerCPUDetailsSchema.description}`, + function refineResources( + resourceType: "cpu" | "ram" | "storage", + originalSchema: { description: string }, + ) { + mergedSchema.properties.resources.properties[resourceType] = { + type: "object", + additionalProperties: false, + required: [], + description: + resourcesPerResourceTypeSchema.properties[resourceType].description, + properties: Object.fromEntries( + Object.entries(resourcesFromChain[resourceType]).map( + ([name, { id }]) => { + return [ + name, + { + ...originalSchema, + description: `id: ${id}. ${originalSchema.description}`, + }, + ]; }, - ]; - }), - ); + ), + ), + }; + } - mergedSchema.properties.resources.properties.ram.properties = - Object.fromEntries( - Object.entries(resourcesFromChain.ram).map(([name, { id }]) => { - return [ - name, - { - ...peerRamDetailsSchema, - description: `id: ${id}. ${peerRamDetailsSchema.description}`, + refineResources("cpu", peerCPUDetailsSchema); + refineResources("ram", peerRamDetailsSchema); + refineResources("storage", peerStorageDetailsSchema); + + function refineResourcePrices(resourceType: ResourceType) { + mergedSchema.properties.offers.additionalProperties.properties.resourcePrices.properties[ + resourceType + ] = { + type: "object", + additionalProperties: false, + required: [], + description: + offerResourcePricesSchema.properties[resourceType].description, + properties: Object.fromEntries( + Object.entries(resourcesFromChain[resourceType]).map( + ([name, { id }]) => { + return [ + name, + { + ...offerResourcePricesSchema.properties[resourceType] + .additionalProperties, + description: `${offerResourcePricesSchema.properties[resourceType].additionalProperties.description} for resource with id: ${id}`, + }, + ]; }, - ]; - }), - ); + ), + ), + }; + } - mergedSchema.properties.resources.properties.storage.properties = - Object.fromEntries( - Object.entries(resourcesFromChain.storage).map(([name, { id }]) => { - return [ - name, - { - ...peerStorageDetailsSchema, - description: `id: ${id}. ${peerStorageDetailsSchema.description}`, - }, - ]; - }), - ); + refineResourcePrices("cpu"); + refineResourcePrices("ram"); + refineResourcePrices("storage"); + refineResourcePrices("bandwidth"); + refineResourcePrices("ip"); return mergedSchema; }, @@ -1635,7 +1630,7 @@ function removeOptionalStorageDetails( return res; } -type CPUMetadata = { +export type CPUMetadata = { manufacturer: string; brand: string; architecture: string; @@ -1654,7 +1649,7 @@ const cpuMetadataSchema = { required: ["manufacturer", "brand", "architecture", "generation"], } as const satisfies JSONSchemaType; -type RAMMetadata = { +export type RAMMetadata = { type: string; generation: string; }; @@ -1669,7 +1664,7 @@ const ramMetadataSchema = { required: ["type", "generation"], } as const satisfies JSONSchemaType; -type StorageMetadata = { +export type StorageMetadata = { type: string; }; @@ -1682,7 +1677,7 @@ const storageMetadataSchema = { required: ["type"], } as const satisfies JSONSchemaType; -type BandwidthMetadata = { +export type BandwidthMetadata = { type: string; }; @@ -1695,7 +1690,7 @@ const bandwidthMetadataSchema = { required: ["type"], } as const satisfies JSONSchemaType; -type IPMetadata = { +export type IPMetadata = { version: string; }; diff --git a/packages/cli/package/test/tests/provider.test.ts b/packages/cli/package/test/tests/provider.test.ts index 71bb4fabe..9c85b1886 100644 --- a/packages/cli/package/test/tests/provider.test.ts +++ b/packages/cli/package/test/tests/provider.test.ts @@ -29,7 +29,25 @@ import { initProviderConfig, options as providerConfigOptions, } from "../../src/lib/configs/project/provider/provider.js"; -import { dataCenterToHumanReadableString } from "../../src/lib/configs/project/provider/provider4.js"; +import { + dataCenterToHumanReadableString, + OnChainResourceType, + type CPUMetadata, + type RAMMetadata, + type StorageMetadata, + type BandwidthMetadata, + type IPMetadata, + cpuResourceToHumanReadableString, + ramResourceToHumanReadableString, + storageResourceToHumanReadableString, + bandwidthResourceToHumanReadableString, + ipResourceToHumanReadableString, + CPU_PRICE_UNITS, + RAM_PRICE_UNITS, + STORAGE_PRICE_UNITS, + BANDWIDTH_PRICE_UNITS, + IP_PRICE_UNITS, +} from "../../src/lib/configs/project/provider/provider4.js"; import { OFFER_FLAG_NAME, PRIV_KEY_FLAG_NAME, @@ -38,6 +56,7 @@ import { import { getContractsByPrivKey, getEventValue, + getEventValues, sign, } from "../../src/lib/dealClient.js"; import { stringifyUnknown } from "../../src/lib/helpers/stringifyUnknown.js"; @@ -54,6 +73,7 @@ const PRIV_KEY_1 = { async function initProviderConfigWithPath( path: string, ): Promise>>> { + // @ts-expect-error Don't know how to solve this error but it's valid const providerConfig = await getConfigInitFunction({ ...providerConfigOptions, reset: true, @@ -67,6 +87,7 @@ async function initProviderConfigWithPath( "Provider config must already exists in a quickstart template", ); + // @ts-expect-error Don't know how to solve this error but it's valid return providerConfig; } @@ -208,7 +229,7 @@ describe("provider tests", () => { providerOrWallet, }); - const createdDatacenterId = await getEventValue({ + const createdDatacenterId = getEventValue({ txReceipt: createDatacenterTxReceipt, contract: contracts.diamond, eventName: "DatacenterCreated", @@ -238,6 +259,8 @@ describe("provider tests", () => { await providerConfig.$commit(); + await fluence({ args: ["provider", "register"], flags: PRIV_KEY_1, cwd }); + await fluence({ args: ["provider", "offer-create"], flags: { @@ -300,6 +323,174 @@ describe("provider tests", () => { ); }, ); + + wrappedTest("create offer with newly added resources", async () => { + const cwd = join("test", "tmp", "addResources"); + await initializeTemplate(cwd); + + const { contracts, providerOrWallet } = await getContractsByPrivKey( + LOCAL_NET_DEFAULT_WALLET_KEY, + ); + + const CPU_RESOURCE_METADATA: CPUMetadata = { + manufacturer: "Fluence", + brand: "F1", + architecture: "RISC-V", + generation: "1", + }; + + const CPU_RESOURCE_NAME = cpuResourceToHumanReadableString( + CPU_RESOURCE_METADATA, + ); + + const RAM_RESOURCE_METADATA: RAMMetadata = { + type: "DDR", + generation: "6", + }; + + const RAM_RESOURCE_NAME = ramResourceToHumanReadableString( + RAM_RESOURCE_METADATA, + ); + + const STORAGE_RESOURCE_METADATA: StorageMetadata = { + type: "HDD", + }; + + const STORAGE_RESOURCE_NAME = storageResourceToHumanReadableString( + STORAGE_RESOURCE_METADATA, + ); + + const BANDWIDTH_RESOURCE_METADATA: BandwidthMetadata = { + type: "semi-dedicated", + }; + + const BANDWIDTH_RESOURCE_NAME = bandwidthResourceToHumanReadableString( + BANDWIDTH_RESOURCE_METADATA, + ); + + const IP_RESOURCE_METADATA: IPMetadata = { + version: "8", + }; + + const IP_RESOURCE_NAME = + ipResourceToHumanReadableString(IP_RESOURCE_METADATA); + + const createResourcesTxReceipt = await sign({ + title: "Create resources", + method: contracts.diamond.registerResources, + args: [ + [ + { + ty: OnChainResourceType.VCPU, + metadata: JSON.stringify(CPU_RESOURCE_METADATA), + }, + { + ty: OnChainResourceType.RAM, + metadata: JSON.stringify(RAM_RESOURCE_METADATA), + }, + { + ty: OnChainResourceType.STORAGE, + metadata: JSON.stringify(STORAGE_RESOURCE_METADATA), + }, + { + ty: OnChainResourceType.NETWORK_BANDWIDTH, + metadata: JSON.stringify(BANDWIDTH_RESOURCE_METADATA), + }, + { + ty: OnChainResourceType.PUBLIC_IP, + metadata: JSON.stringify(IP_RESOURCE_METADATA), + }, + ], + ], + providerOrWallet, + }); + + const resources = getEventValues({ + txReceipt: createResourcesTxReceipt, + contract: contracts.diamond, + eventName: "ResourceCreated", + value: "resources", + }); + + assert( + resources.every((resource) => { + return ( + typeof resource === "object" && + resource !== null && + "resourceId" in resource + ); + }), + "All ResourceCreated events must have resourceId and ty", + ); + + const providerConfig = await initProviderConfigWithPath(cwd); + + Object.values(providerConfig.computePeers).forEach((computePeer) => { + computePeer.resources.cpu.name = CPU_RESOURCE_NAME; + computePeer.resources.ram.name = RAM_RESOURCE_NAME; + + computePeer.resources.storage.forEach((s) => { + s.name = STORAGE_RESOURCE_NAME; + }); + + computePeer.resources.bandwidth.name = BANDWIDTH_RESOURCE_NAME; + computePeer.resources.ip.name = IP_RESOURCE_NAME; + }); + + const [defaultOfferName] = Object.keys(providerConfig.offers); + + assert( + defaultOfferName !== undefined && + providerConfig.offers[defaultOfferName] !== undefined, + "Default offer must exist in the provider config", + ); + + const resourcePrices = + providerConfig.offers[defaultOfferName].resourcePrices; + + resourcePrices.cpu = { [CPU_RESOURCE_NAME]: `1 ${CPU_PRICE_UNITS}` }; + resourcePrices.ram = { [RAM_RESOURCE_NAME]: `1 ${RAM_PRICE_UNITS}` }; + + resourcePrices.storage = { + [STORAGE_RESOURCE_NAME]: `1 ${STORAGE_PRICE_UNITS}`, + }; + + resourcePrices.bandwidth = { + [BANDWIDTH_RESOURCE_NAME]: `1 ${BANDWIDTH_PRICE_UNITS}`, + }; + + resourcePrices.ip = { [IP_RESOURCE_NAME]: `1 ${IP_PRICE_UNITS}` }; + + await providerConfig.$commit(); + + await fluence({ args: ["provider", "register"], flags: PRIV_KEY_1, cwd }); + + await fluence({ + args: ["provider", "offer-create"], + flags: { + ...PRIV_KEY_1, + [OFFER_FLAG_NAME]: defaultOfferName, + }, + cwd, + }); + + const offerInfoWithNewResources = await fluence({ + args: ["provider", "offer-info"], + flags: { + ...PRIV_KEY_1, + [OFFER_FLAG_NAME]: defaultOfferName, + }, + cwd, + }); + + resources.forEach(({ resourceId }) => { + assert(typeof resourceId === "string", "Resource id must be a string"); + + expect(offerInfoWithNewResources).toEqual( + expect.stringContaining(resourceId), + ); + }); + }); }); async function checkProviderNameIsCorrect(cwd: string, providerName: string) {