From 524d9142ba748a09ff1e9bf858b2d161cde88a1f Mon Sep 17 00:00:00 2001 From: Seweryn Kras Date: Wed, 22 May 2024 18:03:35 +0200 Subject: [PATCH 1/4] feat: implement many-of in golem-network Add unit tests to golem-network Add example for many-of --- examples/basic/many-of.ts | 64 ++++ src/activity/activity.module.ts | 2 +- src/agreement/lease-process-pool.ts | 21 ++ src/agreement/lease-process.ts | 7 +- src/deployment/builder.ts | 2 +- src/experimental/job/job.ts | 2 +- src/experimental/job/job_manager.ts | 2 +- src/golem-network.ts | 331 --------------------- src/golem-network/golem-network.test.ts | 124 ++++++++ src/golem-network/golem-network.ts | 379 ++++++++++++++++++++++++ src/golem-network/index.ts | 1 + src/market/market.module.ts | 2 +- src/payment/payment.module.ts | 2 +- tests/examples/examples.json | 1 + 14 files changed, 602 insertions(+), 338 deletions(-) create mode 100644 examples/basic/many-of.ts delete mode 100644 src/golem-network.ts create mode 100644 src/golem-network/golem-network.test.ts create mode 100644 src/golem-network/golem-network.ts create mode 100644 src/golem-network/index.ts diff --git a/examples/basic/many-of.ts b/examples/basic/many-of.ts new file mode 100644 index 000000000..4005f3d6b --- /dev/null +++ b/examples/basic/many-of.ts @@ -0,0 +1,64 @@ +/** + * This example demonstrates how easily lease multiple machines at once. + */ + +import { DemandSpec, GolemNetwork } from "@golem-sdk/golem-js"; +import { pinoPrettyLogger } from "@golem-sdk/pino-logger"; + +const demandOptions: DemandSpec = { + demand: { + activity: { imageTag: "golem/alpine:latest" }, + }, + market: { + maxAgreements: 1, + rentHours: 0.5, + pricing: { + model: "linear", + maxStartPrice: 0.5, + maxCpuPerHourPrice: 1.0, + maxEnvPerHourPrice: 0.5, + }, + }, + payment: { + driver: "erc20", + network: "holesky", + }, +}; + +(async () => { + const glm = new GolemNetwork({ + logger: pinoPrettyLogger({ + level: "info", + }), + }); + + try { + await glm.connect(); + // create a pool that can grow up to 3 leases at the same time + const pool = await glm.manyOf(3, demandOptions); + await Promise.allSettled([ + pool.withLease(async (lease) => + lease + .getExeUnit() + .then((exe) => exe.run("echo Hello, Golem from the first machine! 👋")) + .then((res) => console.log(res.stdout)), + ), + pool.withLease(async (lease) => + lease + .getExeUnit() + .then((exe) => exe.run("echo Hello, Golem from the second machine! 👋")) + .then((res) => console.log(res.stdout)), + ), + pool.withLease(async (lease) => + lease + .getExeUnit() + .then((exe) => exe.run("echo Hello, Golem from the third machine! 👋")) + .then((res) => console.log(res.stdout)), + ), + ]); + } catch (err) { + console.error("Failed to run the example", err); + } finally { + await glm.disconnect(); + } +})().catch(console.error); diff --git a/src/activity/activity.module.ts b/src/activity/activity.module.ts index 64fe200a5..108b5e21f 100644 --- a/src/activity/activity.module.ts +++ b/src/activity/activity.module.ts @@ -3,7 +3,7 @@ import { EventEmitter } from "eventemitter3"; import { Agreement } from "../agreement"; import { Activity, IActivityApi } from "./index"; import { defaultLogger } from "../shared/utils"; -import { GolemServices } from "../golem-network"; +import { GolemServices } from "../golem-network/golem-network"; import { WorkContext, WorkOptions } from "./work"; export interface ActivityEvents {} diff --git a/src/agreement/lease-process-pool.ts b/src/agreement/lease-process-pool.ts index e8ecc987c..71a5ed878 100644 --- a/src/agreement/lease-process-pool.ts +++ b/src/agreement/lease-process-pool.ts @@ -314,4 +314,25 @@ export class LeaseProcessPool { } this.events.emit("ready"); } + + /** + * Acquire a lease process from the pool and release it after the callback is done + * @example + * ```typescript + * const result = await pool.withLease(async (lease) => { + * // Do something with the lease + * return result; + * // pool.release(lease) is called automatically + * // even if an error is thrown in the callback + * }); + * ``` + */ + public async withLease(callback: (lease: LeaseProcess) => Promise): Promise { + const lease = await this.acquire(); + try { + return await callback(lease); + } finally { + await this.release(lease); + } + } } diff --git a/src/agreement/lease-process.ts b/src/agreement/lease-process.ts index 9fa777911..2a6733977 100644 --- a/src/agreement/lease-process.ts +++ b/src/agreement/lease-process.ts @@ -72,9 +72,14 @@ export class LeaseProcess { } /** - * @return Resolves when the lease will be fully terminated and all pending business operations finalized + * Resolves when the lease will be fully terminated and all pending business operations finalized. + * If the lease is already finalized, it will resolve immediately. */ async finalize() { + if (this.paymentProcess.isFinished()) { + return; + } + try { this.logger.debug("Waiting for payment process of agreement to finish", { agreementId: this.agreement.id }); if (this.currentActivity) { diff --git a/src/deployment/builder.ts b/src/deployment/builder.ts index 107d0b603..0e2f02f20 100644 --- a/src/deployment/builder.ts +++ b/src/deployment/builder.ts @@ -1,7 +1,7 @@ import { GolemConfigError } from "../shared/error/golem-error"; import { NetworkOptions } from "../network"; import { Deployment, DeploymentComponents } from "./deployment"; -import { GolemNetwork } from "../golem-network"; +import { GolemNetwork } from "../golem-network/golem-network"; import { validateDeployment } from "./validate-deployment"; import { MarketOptions } from "../market"; import { PaymentModuleOptions } from "../payment"; diff --git a/src/experimental/job/job.ts b/src/experimental/job/job.ts index bffc3b9a8..65a843855 100644 --- a/src/experimental/job/job.ts +++ b/src/experimental/job/job.ts @@ -5,7 +5,7 @@ import { NetworkOptions } from "../../network"; import { PaymentModuleOptions } from "../../payment"; import { EventEmitter } from "eventemitter3"; import { GolemAbortError, GolemUserError } from "../../shared/error/golem-error"; -import { GolemNetwork } from "../../golem-network"; +import { GolemNetwork } from "../../golem-network/golem-network"; import { Logger } from "../../shared/utils"; import { ActivityDemandDirectorConfigOptions } from "../../market/demand/options"; diff --git a/src/experimental/job/job_manager.ts b/src/experimental/job/job_manager.ts index 910b1ddde..9f3ec5db1 100644 --- a/src/experimental/job/job_manager.ts +++ b/src/experimental/job/job_manager.ts @@ -2,7 +2,7 @@ import { v4 } from "uuid"; import { Job, RunJobOptions } from "./job"; import { defaultLogger, Logger, runtimeContextChecker, YagnaOptions } from "../../shared/utils"; import { GolemUserError } from "../../shared/error/golem-error"; -import { GolemNetwork } from "../../golem-network"; +import { GolemNetwork } from "../../golem-network/golem-network"; import { GftpStorageProvider, NullStorageProvider, diff --git a/src/golem-network.ts b/src/golem-network.ts deleted file mode 100644 index d1783d09a..000000000 --- a/src/golem-network.ts +++ /dev/null @@ -1,331 +0,0 @@ -import { DataTransferProtocol, DeploymentOptions, GolemDeploymentBuilder } from "./deployment"; -import { defaultLogger, Logger, YagnaApi } from "./shared/utils"; -import { - Demand, - DemandSpec, - DraftOfferProposalPool, - MarketApi, - MarketModule, - MarketModuleImpl, - MarketOptions, - OfferProposal, -} from "./market"; -import { IPaymentApi, PaymentModule, PaymentModuleImpl, PaymentModuleOptions } from "./payment"; -import { ActivityModule, ActivityModuleImpl, IActivityApi, IFileServer } from "./activity"; -import { NetworkModule, NetworkModuleImpl } from "./network/network.module"; -import { EventEmitter } from "eventemitter3"; -import { LeaseProcess } from "./agreement"; -import { DebitNoteRepository, InvoiceRepository, MarketApiAdapter, PaymentApiAdapter } from "./shared/yagna"; -import { ActivityApiAdapter } from "./shared/yagna/adapters/activity-api-adapter"; -import { ActivityRepository } from "./shared/yagna/repository/activity-repository"; -import { AgreementRepository } from "./shared/yagna/repository/agreement-repository"; -import { IAgreementApi } from "./agreement/agreement"; -import { AgreementApiAdapter } from "./shared/yagna/adapters/agreement-api-adapter"; -import { ProposalRepository } from "./shared/yagna/repository/proposal-repository"; -import { CacheService } from "./shared/cache/CacheService"; -import { IProposalRepository } from "./market/offer-proposal"; -import { DemandRepository } from "./shared/yagna/repository/demand-repository"; -import { IDemandRepository } from "./market/demand"; -import { GftpServerAdapter } from "./shared/storage/GftpServerAdapter"; -import { - GftpStorageProvider, - NullStorageProvider, - StorageProvider, - WebSocketBrowserStorageProvider, -} from "./shared/storage"; - -export interface GolemNetworkOptions { - logger?: Logger; - api?: { - key?: string; - url?: string; - }; - market?: Partial; - payment?: PaymentModuleOptions; - deployment?: Partial; - dataTransferProtocol: DataTransferProtocol; -} - -export interface GolemNetworkEvents { - /** Fires when all startup operations related to GN are completed */ - connected: () => void; - - /** Fires when an error will be encountered */ - error: (err: Error) => void; - - /** Fires when all shutdown operations related to GN are completed */ - disconnected: () => void; -} - -/** - * Dependency Container - */ -export type GolemServices = { - yagna: YagnaApi; - logger: Logger; - paymentApi: IPaymentApi; - activityApi: IActivityApi; - agreementApi: IAgreementApi; - marketApi: MarketApi; - proposalCache: CacheService; - proposalRepository: IProposalRepository; - demandRepository: IDemandRepository; - fileServer: IFileServer; - storageProvider: StorageProvider; -}; - -/** - * General purpose and high-level API for the Golem Network - * - * This class is the main entry-point for developers that would like to build on Golem Network - * using `@golem-sdk/golem-js`. It is supposed to provide an easy access API for use 80% of use cases. - */ -export class GolemNetwork { - public readonly events = new EventEmitter(); - - public readonly options: GolemNetworkOptions; - - private readonly logger: Logger; - - private readonly yagna: YagnaApi; - - public readonly market: MarketModule; - public readonly payment: PaymentModule; - public readonly activity: ActivityModule; - public readonly network: NetworkModule; - - /** - * Dependency Container - */ - public readonly services: GolemServices; - - private hasConnection = false; - - private readonly storageProvider: StorageProvider; - - constructor(options: Partial = {}) { - const optDefaults: GolemNetworkOptions = { - dataTransferProtocol: "gftp", - }; - - this.options = { - ...optDefaults, - ...options, - }; - - this.logger = options.logger ?? defaultLogger("golem-network"); - - this.logger.debug("Creating Golem Network instance with options", { options: this.options }); - - try { - this.yagna = new YagnaApi({ - logger: this.logger, - apiKey: this.options.api?.key, - basePath: this.options.api?.url, - }); - - this.storageProvider = this.createStorageProvider(); - - const demandCache = new CacheService(); - const proposalCache = new CacheService(); - - const demandRepository = new DemandRepository(this.yagna.market, demandCache); - const proposalRepository = new ProposalRepository(this.yagna.market, proposalCache); - const agreementRepository = new AgreementRepository(this.yagna.market, demandRepository); - - this.services = { - logger: this.logger, - yagna: this.yagna, - storageProvider: this.storageProvider, - demandRepository, - proposalCache, - proposalRepository, - paymentApi: new PaymentApiAdapter( - this.yagna, - new InvoiceRepository(this.yagna.payment, this.yagna.market), - new DebitNoteRepository(this.yagna.payment, this.yagna.market), - this.logger, - ), - activityApi: new ActivityApiAdapter( - this.yagna.activity.state, - this.yagna.activity.control, - new ActivityRepository(this.yagna.activity.state, agreementRepository), - ), - agreementApi: new AgreementApiAdapter( - this.yagna.appSessionId, - this.yagna.market, - agreementRepository, - this.logger, - ), - marketApi: new MarketApiAdapter(this.yagna, this.logger), - fileServer: new GftpServerAdapter(this.storageProvider), - }; - - this.market = new MarketModuleImpl(this.services); - this.payment = new PaymentModuleImpl(this.services, this.options.payment); - this.activity = new ActivityModuleImpl(this.services); - - this.network = new NetworkModuleImpl(); - } catch (err) { - this.events.emit("error", err); - throw err; - } - } - - /** - * "Connects" to the network by initializing the underlying components required to perform operations on Golem Network - * - * @return Resolves when all initialization steps are completed - */ - async connect() { - try { - await this.yagna.connect(); - await this.services.paymentApi.connect(); - await this.storageProvider.init(); - this.events.emit("connected"); - this.hasConnection = true; - } catch (err) { - this.events.emit("error", err); - throw err; - } - } - - /** - * "Disconnects" from the Golem Network - * - * @return Resolves when all shutdown steps are completed - */ - async disconnect() { - await this.storageProvider.close(); - await this.services.paymentApi.disconnect(); - await this.yagna.disconnect(); - - this.services.proposalCache.flushAll(); - - this.events.emit("disconnected"); - this.hasConnection = false; - } - - /** - * Creates a new instance of deployment builder that will be bound to this GolemNetwork instance - * - * Use Case: Building a complex deployment topology and requesting resources for the whole construct - * - * @return The new instance of the builder - */ - creteDeploymentBuilder(): GolemDeploymentBuilder { - return new GolemDeploymentBuilder(this); - } - - /** - * Define your computational resource demand and access a single instance - * - * Use Case: Get a single instance of a resource from the market to execute operations on - * - * @example - * ```ts - * const result = await glm - * .oneOf(spec) - * .then((lease) => lease.getExeUnit()) - * .then((exe) => exe.run("echo 'Hello World'")) - * .then((res) => res.stdout); - * ``` - * - * @param demand - */ - async oneOf(demand: DemandSpec): Promise { - const proposalPool = new DraftOfferProposalPool({ - logger: this.logger, - }); - - const budget = this.market.estimateBudget(demand); - const allocation = await this.payment.createAllocation({ - budget, - expirationSec: demand.market.rentHours * 60 * 60, - }); - const demandSpecification = await this.market.buildDemandDetails(demand.demand, allocation); - - const proposalSubscription = this.market - .startCollectingProposals({ - demandSpecification, - }) - .subscribe((proposalsBatch) => proposalsBatch.forEach((proposal) => proposalPool.add(proposal))); - - const draftProposal = await proposalPool.acquire(); - - const agreement = await this.market.proposeAgreement(draftProposal); - - const lease = this.market.createLease(agreement, allocation); - - // Attach handlers for cleanup after all work's done - lease.events.once("finalized", async () => { - await this.payment.releaseAllocation(allocation); - }); - - // We managed to create the activity, no need to look for more agreement candidates - proposalSubscription.unsubscribe(); - - return lease; - - // TODO: Maintain a in-memory repository (pool) of Leases, so that when glm.disconnect() will be called, we can call this.leaseRepository.clear(), which will cleanly shut all of them down - } - - /** - * Define your computational resource demand for many instances, and access any of the instances from within a resource group - * - * Use Case: Get resources from the market for the same purpose, grouped together and schedule operations towards the group instead of individual resources - * - * @example - * ```ts - * const result = await glm - * .manyOf(spec) - * .then(group => group.exec((exe) => exe.run("echo 'HelloWorld'"))) - * .then(res => res.stdout) - * ``` - * - * @param demand - */ - // public manyOf(demand: DemandBuildParams): ResourceGroup { - // throw new Error("Not implemented"); - // } - - /** - * Use Case: Get resources for different purposes from the market, grouped per purpose and schedule operations toward particular groups - * - * @example - * ```ts - * const order = await glm - * .compose() - * .addNetwork("default", netConfig) - * .addResourceGroup("one", specOne) - * .addResourceGroup("two", specTwo) - * .addResourcesToNetwork("one", "default") - * .addResourcesToNetwork("two", "default) - * .validate() <--- check if we have cash before we start? :) - * .request(); - * - * await order.getResourceGroup("one").exec((exe) => exe.run("echo 'Hello One!'")); - * await order.getResourceGroup("two").exec((exe) => exe.run("echo 'Hello Two!'")); - * ``` - */ - // public compose(): void {} - isConnected() { - return this.hasConnection; - } - - private createStorageProvider(): StorageProvider { - if (typeof this.options.dataTransferProtocol === "string") { - switch (this.options.dataTransferProtocol) { - case "ws": - return new WebSocketBrowserStorageProvider(this.yagna, {}); - case "gftp": - default: - return new GftpStorageProvider(); - } - } else if (this.options.dataTransferProtocol !== undefined) { - return this.options.dataTransferProtocol; - } else { - return new NullStorageProvider(); - } - } -} diff --git a/src/golem-network/golem-network.test.ts b/src/golem-network/golem-network.test.ts new file mode 100644 index 000000000..6c299b4b5 --- /dev/null +++ b/src/golem-network/golem-network.test.ts @@ -0,0 +1,124 @@ +import { Observable, Subscription } from "rxjs"; +import { ActivityModuleImpl } from "../activity"; +import { LeaseProcess, LeaseProcessPool } from "../agreement"; +import { DemandSpec, DraftOfferProposalPool, MarketModuleImpl, OfferProposal } from "../market"; +import { NetworkModuleImpl } from "../network/network.module"; +import { Allocation, PaymentModuleImpl } from "../payment"; +import { YagnaApi } from "../shared/utils"; +import { MarketApiAdapter, PaymentApiAdapter } from "../shared/yagna"; +import { ActivityApiAdapter } from "../shared/yagna/adapters/activity-api-adapter"; +import { AgreementApiAdapter } from "../shared/yagna/adapters/agreement-api-adapter"; +import { GolemNetwork } from "./golem-network"; +import { _, instance, mock, reset, when, verify } from "@johanblumenberg/ts-mockito"; + +const demandOptions: DemandSpec = Object.freeze({ + demand: { + activity: { imageTag: "golem/alpine:latest" }, + }, + market: { + maxAgreements: 1, + rentHours: 0.5, + pricing: { + model: "linear", + maxStartPrice: 0.5, + maxCpuPerHourPrice: 1.0, + maxEnvPerHourPrice: 0.5, + }, + }, + payment: { + driver: "erc20", + network: "holesky", + }, +} as const); +const mockMarket = mock(MarketModuleImpl); +const mockPayment = mock(PaymentModuleImpl); +const mockActivity = mock(ActivityModuleImpl); +const mockNetwork = mock(NetworkModuleImpl); +const mockYagna = mock(YagnaApi); +const mockPaymentApi = mock(PaymentApiAdapter); +const mockActivityApi = mock(ActivityApiAdapter); +const mockAgreementApi = mock(AgreementApiAdapter); +const mockMarketApi = mock(MarketApiAdapter); + +afterEach(() => { + reset(mockYagna); + reset(mockActivity); + reset(mockMarket); + reset(mockPayment); + reset(mockNetwork); + reset(mockPaymentApi); + reset(mockActivityApi); + reset(mockAgreementApi); + reset(mockMarketApi); + jest.clearAllMocks(); +}); +function getGolemNetwork() { + return new GolemNetwork({ + override: { + yagna: instance(mockYagna), + activity: instance(mockActivity), + market: instance(mockMarket), + payment: instance(mockPayment), + network: instance(mockNetwork), + paymentApi: instance(mockPaymentApi), + activityApi: instance(mockActivityApi), + agreementApi: instance(mockAgreementApi), + marketApi: instance(mockMarketApi), + }, + }); +} + +function mockStartCollectingProposals() { + const mockSubscription = { unsubscribe: jest.fn() as Subscription["unsubscribe"] } as Subscription; + const mockObservable = { + subscribe: jest.fn().mockReturnValue(mockSubscription) as Observable["subscribe"], + } as Observable; + when(mockMarket.startCollectingProposals(_)).thenReturn(mockObservable); + return mockSubscription; +} +function mockPaymentCreateAllocation() { + const mockAllocation = {} as Allocation; + when(mockPayment.createAllocation(_)).thenReturn(Promise.resolve(mockAllocation)); + return mockAllocation; +} + +describe("Golem Network", () => { + describe("oneOf()", () => { + it("should create a lease and clean it up when disconnected", async () => { + const mockLease = { + finalize: jest.fn().mockImplementation(() => Promise.resolve()) as LeaseProcess["finalize"], + } as LeaseProcess; + when(mockMarket.createLease(_, _)).thenReturn(mockLease); + const mockSubscription = mockStartCollectingProposals(); + const mockAllocation = mockPaymentCreateAllocation(); + jest.spyOn(DraftOfferProposalPool.prototype, "acquire").mockResolvedValue({} as OfferProposal); + + const glm = getGolemNetwork(); + await glm.connect(); + const lease = await glm.oneOf(demandOptions); + expect(lease === mockLease).toBe(true); + await glm.disconnect(); + expect(mockLease.finalize).toHaveBeenCalled(); + expect(mockSubscription.unsubscribe).toHaveBeenCalled(); + verify(mockPayment.releaseAllocation(mockAllocation)).once(); + }); + }); + describe("manyOf()", () => { + it("should create a pool and clean it up when disconnected", async () => { + const mockSubscription = mockStartCollectingProposals(); + const mockAllocation = mockPaymentCreateAllocation(); + const mockLeasePool = mock(LeaseProcessPool); + when(mockLeasePool.drainAndClear()).thenResolve(); + when(mockMarket.createLeaseProcessPool(_, _, _)).thenReturn(instance(mockLeasePool)); + + const glm = getGolemNetwork(); + await glm.connect(); + const leasePool = await glm.manyOf(3, demandOptions); + expect(leasePool === instance(mockLeasePool)).toBe(true); + await glm.disconnect(); + verify(mockLeasePool.drainAndClear()).once(); + expect(mockSubscription.unsubscribe).toHaveBeenCalled(); + verify(mockPayment.releaseAllocation(mockAllocation)).once(); + }); + }); +}); diff --git a/src/golem-network/golem-network.ts b/src/golem-network/golem-network.ts new file mode 100644 index 000000000..696b63f5a --- /dev/null +++ b/src/golem-network/golem-network.ts @@ -0,0 +1,379 @@ +import { DataTransferProtocol, DeploymentOptions, GolemDeploymentBuilder } from "../deployment"; +import { defaultLogger, Logger, YagnaApi } from "../shared/utils"; +import { + Demand, + DemandSpec, + DraftOfferProposalPool, + MarketApi, + MarketModule, + MarketModuleImpl, + MarketOptions, + OfferProposal, +} from "../market"; +import { IPaymentApi, PaymentModule, PaymentModuleImpl, PaymentModuleOptions } from "../payment"; +import { ActivityModule, ActivityModuleImpl, IActivityApi, IFileServer } from "../activity"; +import { NetworkModule, NetworkModuleImpl } from "../network/network.module"; +import { EventEmitter } from "eventemitter3"; +import { LeaseProcess, LeaseProcessPool } from "../agreement"; +import { DebitNoteRepository, InvoiceRepository, MarketApiAdapter, PaymentApiAdapter } from "../shared/yagna"; +import { ActivityApiAdapter } from "../shared/yagna/adapters/activity-api-adapter"; +import { ActivityRepository } from "../shared/yagna/repository/activity-repository"; +import { AgreementRepository } from "../shared/yagna/repository/agreement-repository"; +import { IAgreementApi } from "../agreement/agreement"; +import { AgreementApiAdapter } from "../shared/yagna/adapters/agreement-api-adapter"; +import { ProposalRepository } from "../shared/yagna/repository/proposal-repository"; +import { CacheService } from "../shared/cache/CacheService"; +import { IProposalRepository } from "../market/offer-proposal"; +import { DemandRepository } from "../shared/yagna/repository/demand-repository"; +import { IDemandRepository } from "../market/demand"; +import { GftpServerAdapter } from "../shared/storage/GftpServerAdapter"; +import { + GftpStorageProvider, + NullStorageProvider, + StorageProvider, + WebSocketBrowserStorageProvider, +} from "../shared/storage"; + +export interface GolemNetworkOptions { + logger?: Logger; + api?: { + key?: string; + url?: string; + }; + market?: Partial; + payment?: PaymentModuleOptions; + deployment?: Partial; + dataTransferProtocol?: DataTransferProtocol; + override?: Partial< + GolemServices & { + market: MarketModule; + payment: PaymentModule; + activity: ActivityModule; + network: NetworkModule; + } + >; +} + +export interface GolemNetworkEvents { + /** Fires when all startup operations related to GN are completed */ + connected: () => void; + + /** Fires when an error will be encountered */ + error: (err: Error) => void; + + /** Fires when all shutdown operations related to GN are completed */ + disconnected: () => void; +} + +/** + * Dependency Container + */ +export type GolemServices = { + yagna: YagnaApi; + logger: Logger; + paymentApi: IPaymentApi; + activityApi: IActivityApi; + agreementApi: IAgreementApi; + marketApi: MarketApi; + proposalCache: CacheService; + proposalRepository: IProposalRepository; + demandRepository: IDemandRepository; + fileServer: IFileServer; + storageProvider: StorageProvider; +}; + +/** + * General purpose and high-level API for the Golem Network + * + * This class is the main entry-point for developers that would like to build on Golem Network + * using `@golem-sdk/golem-js`. It is supposed to provide an easy access API for use 80% of use cases. + */ +export class GolemNetwork { + public readonly events = new EventEmitter(); + + public readonly options: GolemNetworkOptions; + + private readonly logger: Logger; + + private readonly yagna: YagnaApi; + + public readonly market: MarketModule; + public readonly payment: PaymentModule; + public readonly activity: ActivityModule; + public readonly network: NetworkModule; + + /** + * Dependency Container + */ + public readonly services: GolemServices; + + private hasConnection = false; + + private readonly storageProvider: StorageProvider; + + /** + * List af additional tasks that should be executed when the network is being shut down + * (for example finalizing lease processes created with `oneOf`) + */ + private readonly cleanupTasks: (() => Promise | void)[] = []; + + constructor(options: Partial = {}) { + const optDefaults: GolemNetworkOptions = { + dataTransferProtocol: "gftp", + }; + + this.options = { + ...optDefaults, + ...options, + }; + + this.logger = options.logger ?? defaultLogger("golem-network"); + + this.logger.debug("Creating Golem Network instance with options", { options: this.options }); + + try { + this.yagna = + options.override?.yagna || + new YagnaApi({ + logger: this.logger, + apiKey: this.options.api?.key, + basePath: this.options.api?.url, + }); + + this.storageProvider = options.override?.storageProvider || this.createStorageProvider(); + + const demandCache = new CacheService(); + const proposalCache = new CacheService(); + + const demandRepository = new DemandRepository(this.yagna.market, demandCache); + const proposalRepository = new ProposalRepository(this.yagna.market, proposalCache); + const agreementRepository = new AgreementRepository(this.yagna.market, demandRepository); + + this.services = { + logger: this.logger, + yagna: this.yagna, + storageProvider: this.storageProvider, + demandRepository, + proposalCache, + proposalRepository, + paymentApi: + this.options.override?.paymentApi || + new PaymentApiAdapter( + this.yagna, + new InvoiceRepository(this.yagna.payment, this.yagna.market), + new DebitNoteRepository(this.yagna.payment, this.yagna.market), + this.logger, + ), + activityApi: + this.options.override?.activityApi || + new ActivityApiAdapter( + this.yagna.activity.state, + this.yagna.activity.control, + new ActivityRepository(this.yagna.activity.state, agreementRepository), + ), + agreementApi: + this.options.override?.agreementApi || + new AgreementApiAdapter(this.yagna.appSessionId, this.yagna.market, agreementRepository, this.logger), + marketApi: this.options.override?.marketApi || new MarketApiAdapter(this.yagna, this.logger), + fileServer: this.options.override?.fileServer || new GftpServerAdapter(this.storageProvider), + }; + + this.market = this.options.override?.market || new MarketModuleImpl(this.services); + this.payment = this.options.override?.payment || new PaymentModuleImpl(this.services, this.options.payment); + this.activity = this.options.override?.activity || new ActivityModuleImpl(this.services); + this.network = this.options.override?.network || new NetworkModuleImpl(); + } catch (err) { + this.events.emit("error", err); + throw err; + } + } + + /** + * "Connects" to the network by initializing the underlying components required to perform operations on Golem Network + * + * @return Resolves when all initialization steps are completed + */ + async connect() { + try { + await this.yagna.connect(); + await this.services.paymentApi.connect(); + await this.storageProvider.init(); + this.events.emit("connected"); + this.hasConnection = true; + } catch (err) { + this.events.emit("error", err); + throw err; + } + } + + /** + * "Disconnects" from the Golem Network + * + * @return Resolves when all shutdown steps are completed + */ + async disconnect() { + await Promise.allSettled(this.cleanupTasks.map((task) => task())); + await this.storageProvider.close(); + await this.services.paymentApi.disconnect(); + await this.yagna.disconnect(); + + this.services.proposalCache.flushAll(); + + this.events.emit("disconnected"); + this.hasConnection = false; + } + + /** + * Creates a new instance of deployment builder that will be bound to this GolemNetwork instance + * + * Use Case: Building a complex deployment topology and requesting resources for the whole construct + * + * @return The new instance of the builder + */ + creteDeploymentBuilder(): GolemDeploymentBuilder { + return new GolemDeploymentBuilder(this); + } + + /** + * Define your computational resource demand and access a single instance + * + * Use Case: Get a single instance of a resource from the market to execute operations on + * + * @example + * ```ts + * const lease = await glm.oneOf(demand); + * await lease + * .getExeUnit() + * .then((exe) => exe.run("echo Hello, Golem! 👋")) + * .then((res) => console.log(res.stdout)); + * await lease.finalize(); + * ``` + * + * @param demand + */ + async oneOf(demand: DemandSpec): Promise { + const proposalPool = new DraftOfferProposalPool({ + logger: this.logger, + }); + + const budget = this.market.estimateBudget(demand); + const allocation = await this.payment.createAllocation({ + budget, + expirationSec: demand.market.rentHours * 60 * 60, + }); + const demandSpecification = await this.market.buildDemandDetails(demand.demand, allocation); + + const proposal$ = this.market.startCollectingProposals({ + demandSpecification, + }); + const proposalSubscription = proposalPool.readFrom(proposal$); + + const draftProposal = await proposalPool.acquire(); + + const agreement = await this.market.proposeAgreement(draftProposal); + + const lease = this.market.createLease(agreement, allocation); + + // We managed to create the activity, no need to look for more agreement candidates + proposalSubscription.unsubscribe(); + + this.cleanupTasks.push(async () => { + // First finalize the lease (which will wait for all payments to be processed) + // and only then release the allocation + await lease.finalize().catch((err) => this.logger.error("Error while finalizing lease", err)); + await this.payment + .releaseAllocation(allocation) + .catch((err) => this.logger.error("Error while releasing allocation", err)); + }); + + return lease; + } + + /** + * Define your computational resource demand and access a pool of instances. + * The pool will grow up to the specified concurrency level. + * + * @example + * ```ts + * // create a pool that can grow up to 3 leases at the same time + * const pool = await glm.manyOf(3, demand); + * await Promise.allSettled([ + * pool.withLease(async (lease) => + * lease + * .getExeUnit() + * .then((exe) => exe.run("echo Hello, Golem from the first machine! 👋")) + * .then((res) => console.log(res.stdout)), + * ), + * pool.withLease(async (lease) => + * lease + * .getExeUnit() + * .then((exe) => exe.run("echo Hello, Golem from the second machine! 👋")) + * .then((res) => console.log(res.stdout)), + * ), + * pool.withLease(async (lease) => + * lease + * .getExeUnit() + * .then((exe) => exe.run("echo Hello, Golem from the third machine! 👋")) + * .then((res) => console.log(res.stdout)), + * ), + * ]); + * ``` + * + * @param concurrency Maximum number of leases that can be active at the same time + * @param demand Demand specification + */ + public async manyOf(concurrency: number, demand: DemandSpec): Promise { + const proposalPool = new DraftOfferProposalPool({ + logger: this.logger, + }); + + const budget = this.market.estimateBudget(demand); + const allocation = await this.payment.createAllocation({ + budget, + expirationSec: demand.market.rentHours * 60 * 60, + }); + const demandSpecification = await this.market.buildDemandDetails(demand.demand, allocation); + + const proposal$ = this.market.startCollectingProposals({ + demandSpecification, + }); + const subscription = proposalPool.readFrom(proposal$); + + const leaseProcessPool = this.market.createLeaseProcessPool(proposalPool, allocation, { + replicas: concurrency, + }); + this.cleanupTasks.push(() => subscription.unsubscribe()); + this.cleanupTasks.push(async () => { + // First drain the pool (which will wait for all leases to be paid for) + // and only then release the allocation + await leaseProcessPool + .drainAndClear() + .catch((err) => this.logger.error("Error while draining lease process pool", err)); + await this.payment + .releaseAllocation(allocation) + .catch((err) => this.logger.error("Error while releasing allocation", err)); + }); + + return leaseProcessPool; + } + + isConnected() { + return this.hasConnection; + } + + private createStorageProvider(): StorageProvider { + if (typeof this.options.dataTransferProtocol === "string") { + switch (this.options.dataTransferProtocol) { + case "ws": + return new WebSocketBrowserStorageProvider(this.yagna, {}); + case "gftp": + default: + return new GftpStorageProvider(); + } + } else if (this.options.dataTransferProtocol !== undefined) { + return this.options.dataTransferProtocol; + } else { + return new NullStorageProvider(); + } + } +} diff --git a/src/golem-network/index.ts b/src/golem-network/index.ts new file mode 100644 index 000000000..2a17939a9 --- /dev/null +++ b/src/golem-network/index.ts @@ -0,0 +1 @@ +export * from "./golem-network"; diff --git a/src/market/market.module.ts b/src/market/market.module.ts index b89555acf..5b7d4b70c 100644 --- a/src/market/market.module.ts +++ b/src/market/market.module.ts @@ -159,7 +159,7 @@ export interface MarketModule { createLease(agreement: Agreement, allocation: Allocation): LeaseProcess; /** - * Factory that creates new agreement pool that's fully configured + * Factory that creates new lease process pool that's fully configured */ createLeaseProcessPool( draftPool: DraftOfferProposalPool, diff --git a/src/payment/payment.module.ts b/src/payment/payment.module.ts index 6846dc3d6..6dac44769 100644 --- a/src/payment/payment.module.ts +++ b/src/payment/payment.module.ts @@ -5,7 +5,7 @@ import { Allocation, DebitNote, Invoice, InvoiceProcessor, IPaymentApi } from ". import { defaultLogger, YagnaApi } from "../shared/utils"; import { DebitNoteFilter, InvoiceFilter } from "./service"; import { Observable } from "rxjs"; -import { GolemServices } from "../golem-network"; +import { GolemServices } from "../golem-network/golem-network"; import { PaymentSpec } from "../market"; import { PayerDetails } from "./PayerDetails"; import { CreateAllocationParams } from "./types"; diff --git a/tests/examples/examples.json b/tests/examples/examples.json index 4d1242995..8836fff58 100644 --- a/tests/examples/examples.json +++ b/tests/examples/examples.json @@ -1,5 +1,6 @@ [ { "cmd": "tsx", "path": "examples/basic/hello-world.ts" }, { "cmd": "tsx", "path": "examples/pool/hello-world.ts" }, + { "cmd": "tsx", "path": "examples/basic/many-of.ts" }, { "cmd": "tsx", "path": "examples/deployment/new-api.ts" } ] From 8f0db1a4b6e7a536ef25e4977b490131e5b3bf96 Mon Sep 17 00:00:00 2001 From: Seweryn Kras Date: Thu, 23 May 2024 11:32:39 +0200 Subject: [PATCH 2/4] feat: remove unused market and deployment options and flatten payment options --- examples/basic/hello-world-v2.ts | 4 --- examples/basic/hello-world.ts | 4 --- examples/basic/many-of.ts | 4 --- examples/deployment/new-api.ts | 11 ------ examples/experimental/express/server.ts | 4 --- examples/experimental/job/cancel.ts | 4 --- examples/experimental/job/getJobById.ts | 4 --- examples/experimental/job/waitForResults.ts | 4 --- examples/pool/hello-world.ts | 6 ---- src/experimental/job/job.test.ts | 4 --- src/golem-network/golem-network.test.ts | 8 ++--- src/golem-network/golem-network.ts | 29 ++++++++++++--- src/market/market.module.ts | 7 ---- src/payment/payment.module.ts | 39 ++++++++++++++++----- tests/e2e/express.spec.ts | 4 --- 15 files changed, 59 insertions(+), 77 deletions(-) diff --git a/examples/basic/hello-world-v2.ts b/examples/basic/hello-world-v2.ts index b00f79c68..28971d643 100644 --- a/examples/basic/hello-world-v2.ts +++ b/examples/basic/hello-world-v2.ts @@ -15,10 +15,6 @@ const demandOptions: DemandSpec = { maxEnvPerHourPrice: 0.5, }, }, - payment: { - driver: "erc20", - network: "holesky", - }, }; (async () => { diff --git a/examples/basic/hello-world.ts b/examples/basic/hello-world.ts index 6a751a533..eedf5ca79 100644 --- a/examples/basic/hello-world.ts +++ b/examples/basic/hello-world.ts @@ -30,10 +30,6 @@ import { pinoPrettyLogger } from "@golem-sdk/pino-logger"; maxEnvPerHourPrice: 1, }, }, - payment: { - network: "holesky", - driver: "erc20", - }, }; const proposalPool = new DraftOfferProposalPool({ diff --git a/examples/basic/many-of.ts b/examples/basic/many-of.ts index 4005f3d6b..8c9ac36af 100644 --- a/examples/basic/many-of.ts +++ b/examples/basic/many-of.ts @@ -19,10 +19,6 @@ const demandOptions: DemandSpec = { maxEnvPerHourPrice: 0.5, }, }, - payment: { - driver: "erc20", - network: "holesky", - }, }; (async () => { diff --git a/examples/deployment/new-api.ts b/examples/deployment/new-api.ts index 80c6705b3..d47886c7e 100644 --- a/examples/deployment/new-api.ts +++ b/examples/deployment/new-api.ts @@ -6,17 +6,6 @@ async function main() { logger: pinoPrettyLogger({ level: "info", }), - market: { - maxAgreements: 1, - rentHours: 0.5, - pricing: { - model: "linear", - maxStartPrice: 1, - maxCpuPerHourPrice: 1, - maxEnvPerHourPrice: 1, - }, - }, - dataTransferProtocol: "gftp", }); try { diff --git a/examples/experimental/express/server.ts b/examples/experimental/express/server.ts index e0e96f308..d71271bcb 100644 --- a/examples/experimental/express/server.ts +++ b/examples/experimental/express/server.ts @@ -39,10 +39,6 @@ app.post("/tts", async (req, res) => { maxEnvPerHourPrice: 1, }, }, - payment: { - driver: "erc20", - network: "holesky", - }, }); job.events.on("created", () => { diff --git a/examples/experimental/job/cancel.ts b/examples/experimental/job/cancel.ts index 3d23dacfa..8913833c6 100644 --- a/examples/experimental/job/cancel.ts +++ b/examples/experimental/job/cancel.ts @@ -23,10 +23,6 @@ async function main() { maxEnvPerHourPrice: 1, }, }, - payment: { - driver: "erc20", - network: "holesky", - }, }); console.log("Job object created, initial status is", job.state); diff --git a/examples/experimental/job/getJobById.ts b/examples/experimental/job/getJobById.ts index 4091d86aa..f63b1683e 100644 --- a/examples/experimental/job/getJobById.ts +++ b/examples/experimental/job/getJobById.ts @@ -21,10 +21,6 @@ function startJob() { maxEnvPerHourPrice: 1, }, }, - payment: { - driver: "erc20", - network: "holesky", - }, }); console.log("Job object created, initial status is", job.state); diff --git a/examples/experimental/job/waitForResults.ts b/examples/experimental/job/waitForResults.ts index ddd73ff6b..ff860252b 100644 --- a/examples/experimental/job/waitForResults.ts +++ b/examples/experimental/job/waitForResults.ts @@ -25,10 +25,6 @@ async function main() { maxEnvPerHourPrice: 1, }, }, - payment: { - driver: "erc20", - network: "holesky", - }, }); console.log("Job object created, initial status is", job.state); diff --git a/examples/pool/hello-world.ts b/examples/pool/hello-world.ts index 4b04a7168..c1d600922 100644 --- a/examples/pool/hello-world.ts +++ b/examples/pool/hello-world.ts @@ -34,12 +34,6 @@ const demandOptions = { const glm = new GolemNetwork({ logger, - payment: { - payment: { - driver: "erc20", - network: "holesky", - }, - }, }); let allocation: Allocation | undefined; diff --git a/src/experimental/job/job.test.ts b/src/experimental/job/job.test.ts index 1dc309a78..ebaf12952 100644 --- a/src/experimental/job/job.test.ts +++ b/src/experimental/job/job.test.ts @@ -58,10 +58,6 @@ describe.skip("Job", () => { maxCpuPerHourPrice: 1, }, }, - payment: { - network: "holesky", - driver: "erc20", - }, }, instance(imock()), ); diff --git a/src/golem-network/golem-network.test.ts b/src/golem-network/golem-network.test.ts index 6c299b4b5..233d0dc32 100644 --- a/src/golem-network/golem-network.test.ts +++ b/src/golem-network/golem-network.test.ts @@ -10,6 +10,7 @@ import { ActivityApiAdapter } from "../shared/yagna/adapters/activity-api-adapte import { AgreementApiAdapter } from "../shared/yagna/adapters/agreement-api-adapter"; import { GolemNetwork } from "./golem-network"; import { _, instance, mock, reset, when, verify } from "@johanblumenberg/ts-mockito"; +import { GftpStorageProvider } from "../shared/storage"; const demandOptions: DemandSpec = Object.freeze({ demand: { @@ -25,10 +26,6 @@ const demandOptions: DemandSpec = Object.freeze({ maxEnvPerHourPrice: 0.5, }, }, - payment: { - driver: "erc20", - network: "holesky", - }, } as const); const mockMarket = mock(MarketModuleImpl); const mockPayment = mock(PaymentModuleImpl); @@ -39,6 +36,7 @@ const mockPaymentApi = mock(PaymentApiAdapter); const mockActivityApi = mock(ActivityApiAdapter); const mockAgreementApi = mock(AgreementApiAdapter); const mockMarketApi = mock(MarketApiAdapter); +const mockStorageProvider = mock(GftpStorageProvider); afterEach(() => { reset(mockYagna); @@ -50,6 +48,7 @@ afterEach(() => { reset(mockActivityApi); reset(mockAgreementApi); reset(mockMarketApi); + reset(mockStorageProvider); jest.clearAllMocks(); }); function getGolemNetwork() { @@ -64,6 +63,7 @@ function getGolemNetwork() { activityApi: instance(mockActivityApi), agreementApi: instance(mockAgreementApi), marketApi: instance(mockMarketApi), + storageProvider: instance(mockStorageProvider), }, }); } diff --git a/src/golem-network/golem-network.ts b/src/golem-network/golem-network.ts index 696b63f5a..358c8123f 100644 --- a/src/golem-network/golem-network.ts +++ b/src/golem-network/golem-network.ts @@ -1,4 +1,4 @@ -import { DataTransferProtocol, DeploymentOptions, GolemDeploymentBuilder } from "../deployment"; +import { DataTransferProtocol, GolemDeploymentBuilder } from "../deployment"; import { defaultLogger, Logger, YagnaApi } from "../shared/utils"; import { Demand, @@ -7,7 +7,6 @@ import { MarketApi, MarketModule, MarketModuleImpl, - MarketOptions, OfferProposal, } from "../market"; import { IPaymentApi, PaymentModule, PaymentModuleImpl, PaymentModuleOptions } from "../payment"; @@ -35,15 +34,35 @@ import { } from "../shared/storage"; export interface GolemNetworkOptions { + /** + * Logger instance to use for logging. + * If no logger is provided you can view debug logs by setting the + * `DEBUG` environment variable to `golem-js:*`. + */ logger?: Logger; + /** + * Set the API key and URL for the Yagna API. + */ api?: { key?: string; url?: string; }; - market?: Partial; - payment?: PaymentModuleOptions; - deployment?: Partial; + /** + * Set payment-related options. + * This is where you can specify the network, payment driver and more. + * By default, the network is set to the `holesky` test network. + */ + payment?: Partial; + /** + * Set the data transfer protocol to use for file transfers. + * Default is `gftp`. + */ dataTransferProtocol?: DataTransferProtocol; + /** + * Override some of the services used by the GolemNetwork instance. + * This is useful for testing or when you want to provide your own implementation of some services. + * Only set this if you know what you are doing. + */ override?: Partial< GolemServices & { market: MarketModule; diff --git a/src/market/market.module.ts b/src/market/market.module.ts index 5b7d4b70c..05afde254 100644 --- a/src/market/market.module.ts +++ b/src/market/market.module.ts @@ -40,19 +40,12 @@ export interface DemandBuildParams { export type DemandEngine = "vm" | "vm-nvidia" | "wasmtime"; -export type PaymentSpec = { - network: string; - driver: "erc20"; - token?: "glm" | "tglm"; -}; - /** * Represents the new demand specification which is accepted by GolemNetwork and MarketModule */ export interface DemandSpec { demand: BuildDemandOptions; market: MarketOptions; - payment: PaymentSpec; } export interface MarketOptions { diff --git a/src/payment/payment.module.ts b/src/payment/payment.module.ts index 6dac44769..e27cc8c9f 100644 --- a/src/payment/payment.module.ts +++ b/src/payment/payment.module.ts @@ -6,14 +6,30 @@ import { defaultLogger, YagnaApi } from "../shared/utils"; import { DebitNoteFilter, InvoiceFilter } from "./service"; import { Observable } from "rxjs"; import { GolemServices } from "../golem-network/golem-network"; -import { PaymentSpec } from "../market"; import { PayerDetails } from "./PayerDetails"; import { CreateAllocationParams } from "./types"; export interface PaymentModuleOptions { debitNoteFilter?: DebitNoteFilter; invoiceFilter?: InvoiceFilter; - payment: PaymentSpec; + /** + * Network used to facilitate the payment. + * (for example: "mainnet", "holesky") + * @default holesky + */ + network?: string; + /** + * Payment driver used to facilitate the payment. + * (for example: "erc20") + * @default erc20 + */ + driver?: "erc20"; + /** + * Token used to facilitate the payment. + * If unset, it will be inferred from the network. + * (for example: "glm", "tglm") + */ + token?: "glm" | "tglm"; } export interface PaymentModuleEvents {} @@ -56,15 +72,22 @@ export class PaymentModuleImpl implements PaymentModule { private readonly logger = defaultLogger("payment"); - private readonly options: PaymentModuleOptions = { + private readonly options: Required = { debitNoteFilter: () => true, invoiceFilter: () => true, - payment: { driver: "erc20", network: "holesky" }, + driver: "erc20", + network: "holesky", + token: "tglm", }; constructor(deps: GolemServices, options?: PaymentModuleOptions) { if (options) { - this.options = options; + const network = options.network || this.options.network; + const driver = options.driver || this.options.driver; + const debitNoteFilter = options.debitNoteFilter || this.options.debitNoteFilter; + const invoiceFilter = options.invoiceFilter || this.options.invoiceFilter; + const token = options.token || (network === "mainnet" ? "glm" : "tglm"); + this.options = { network, driver, token, debitNoteFilter, invoiceFilter }; } this.logger = deps.logger; @@ -74,14 +97,14 @@ export class PaymentModuleImpl implements PaymentModule { private getPaymentPlatform(): string { const mainnets = ["mainnet", "polygon"]; - const token = mainnets.includes(this.options.payment.network) ? "glm" : "tglm"; - return `${this.options.payment.driver}-${this.options.payment.network}-${token}`; + const token = mainnets.includes(this.options.network) ? "glm" : "tglm"; + return `${this.options.driver}-${this.options.network}-${token}`; } async getPayerDetails(): Promise { const { identity: address } = await this.yagnaApi.identity.getIdentity(); - return new PayerDetails(this.options.payment.network, this.options.payment.driver, address); + return new PayerDetails(this.options.network, this.options.driver, address); } observeDebitNotes(): Observable { diff --git a/tests/e2e/express.spec.ts b/tests/e2e/express.spec.ts index 1f39c6d44..2c531446d 100644 --- a/tests/e2e/express.spec.ts +++ b/tests/e2e/express.spec.ts @@ -44,10 +44,6 @@ describe("Express", function () { maxCpuPerHourPrice: 1, }, }, - payment: { - driver: "erc20", - network: "holesky", - }, }); job.events.on("created", () => { From 8f41c44a24dea060bd20faddfb80b3639f3b874a Mon Sep 17 00:00:00 2001 From: Seweryn Kras Date: Thu, 23 May 2024 14:14:17 +0200 Subject: [PATCH 3/4] fix: assume glm token when network is polygon --- src/payment/PayerDetails.ts | 9 ++------- src/payment/payment.module.ts | 10 +++++----- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/payment/PayerDetails.ts b/src/payment/PayerDetails.ts index 4acbf29d7..ee7195bfd 100644 --- a/src/payment/PayerDetails.ts +++ b/src/payment/PayerDetails.ts @@ -1,15 +1,10 @@ export class PayerDetails { - public readonly token: "glm" | "tglm"; - constructor( public readonly network: string, public readonly driver: string, public readonly address: string, - ) { - const mainnets = ["polygon", "mainnet"]; - - this.token = mainnets.includes(this.network) ? "glm" : "tglm"; - } + public readonly token: "glm" | "tglm", + ) {} getPaymentPlatform() { return `${this.driver}-${this.network}-${this.token}`; diff --git a/src/payment/payment.module.ts b/src/payment/payment.module.ts index e27cc8c9f..070ffb412 100644 --- a/src/payment/payment.module.ts +++ b/src/payment/payment.module.ts @@ -63,6 +63,8 @@ export interface PaymentModule { getPayerDetails(): Promise; } +const MAINNETS = Object.freeze(["mainnet", "polygon"]); + export class PaymentModuleImpl implements PaymentModule { events: EventEmitter = new EventEmitter(); @@ -86,7 +88,7 @@ export class PaymentModuleImpl implements PaymentModule { const driver = options.driver || this.options.driver; const debitNoteFilter = options.debitNoteFilter || this.options.debitNoteFilter; const invoiceFilter = options.invoiceFilter || this.options.invoiceFilter; - const token = options.token || (network === "mainnet" ? "glm" : "tglm"); + const token = options.token || MAINNETS.includes(network) ? "glm" : "tglm"; this.options = { network, driver, token, debitNoteFilter, invoiceFilter }; } @@ -96,15 +98,13 @@ export class PaymentModuleImpl implements PaymentModule { } private getPaymentPlatform(): string { - const mainnets = ["mainnet", "polygon"]; - const token = mainnets.includes(this.options.network) ? "glm" : "tglm"; - return `${this.options.driver}-${this.options.network}-${token}`; + return `${this.options.driver}-${this.options.network}-${this.options.token}`; } async getPayerDetails(): Promise { const { identity: address } = await this.yagnaApi.identity.getIdentity(); - return new PayerDetails(this.options.network, this.options.driver, address); + return new PayerDetails(this.options.network, this.options.driver, address, this.options.token); } observeDebitNotes(): Observable { From d87d90b60756d7ec760620c4a5775ae11669acc8 Mon Sep 17 00:00:00 2001 From: Seweryn Kras Date: Thu, 23 May 2024 15:22:53 +0200 Subject: [PATCH 4/4] feat(many-of): combine demand and concurrency into one argument --- examples/basic/many-of.ts | 7 +++++-- src/golem-network/golem-network.test.ts | 5 ++++- src/golem-network/golem-network.ts | 18 +++++++++++++----- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/examples/basic/many-of.ts b/examples/basic/many-of.ts index 8c9ac36af..468700644 100644 --- a/examples/basic/many-of.ts +++ b/examples/basic/many-of.ts @@ -5,7 +5,7 @@ import { DemandSpec, GolemNetwork } from "@golem-sdk/golem-js"; import { pinoPrettyLogger } from "@golem-sdk/pino-logger"; -const demandOptions: DemandSpec = { +const demand: DemandSpec = { demand: { activity: { imageTag: "golem/alpine:latest" }, }, @@ -31,7 +31,10 @@ const demandOptions: DemandSpec = { try { await glm.connect(); // create a pool that can grow up to 3 leases at the same time - const pool = await glm.manyOf(3, demandOptions); + const pool = await glm.manyOf({ + concurrency: 3, + demand, + }); await Promise.allSettled([ pool.withLease(async (lease) => lease diff --git a/src/golem-network/golem-network.test.ts b/src/golem-network/golem-network.test.ts index 233d0dc32..86944f2be 100644 --- a/src/golem-network/golem-network.test.ts +++ b/src/golem-network/golem-network.test.ts @@ -113,7 +113,10 @@ describe("Golem Network", () => { const glm = getGolemNetwork(); await glm.connect(); - const leasePool = await glm.manyOf(3, demandOptions); + const leasePool = await glm.manyOf({ + concurrency: 3, + demand: demandOptions, + }); expect(leasePool === instance(mockLeasePool)).toBe(true); await glm.disconnect(); verify(mockLeasePool.drainAndClear()).once(); diff --git a/src/golem-network/golem-network.ts b/src/golem-network/golem-network.ts index 358c8123f..85b8dd574 100644 --- a/src/golem-network/golem-network.ts +++ b/src/golem-network/golem-network.ts @@ -13,7 +13,7 @@ import { IPaymentApi, PaymentModule, PaymentModuleImpl, PaymentModuleOptions } f import { ActivityModule, ActivityModuleImpl, IActivityApi, IFileServer } from "../activity"; import { NetworkModule, NetworkModuleImpl } from "../network/network.module"; import { EventEmitter } from "eventemitter3"; -import { LeaseProcess, LeaseProcessPool } from "../agreement"; +import { LeaseProcess, LeaseProcessPool, LeaseProcessPoolOptions } from "../agreement"; import { DebitNoteRepository, InvoiceRepository, MarketApiAdapter, PaymentApiAdapter } from "../shared/yagna"; import { ActivityApiAdapter } from "../shared/yagna/adapters/activity-api-adapter"; import { ActivityRepository } from "../shared/yagna/repository/activity-repository"; @@ -84,6 +84,12 @@ export interface GolemNetworkEvents { disconnected: () => void; } +interface ManyOfOptions { + concurrency: LeaseProcessPoolOptions["replicas"]; + // TODO: rename to `order` or something similar when DemandSpec is renamed to MarketOrder + demand: DemandSpec; +} + /** * Dependency Container */ @@ -315,7 +321,10 @@ export class GolemNetwork { * @example * ```ts * // create a pool that can grow up to 3 leases at the same time - * const pool = await glm.manyOf(3, demand); + * const pool = await glm.manyOf({ + * concurrency: 3, + * demand + * }); * await Promise.allSettled([ * pool.withLease(async (lease) => * lease @@ -338,10 +347,9 @@ export class GolemNetwork { * ]); * ``` * - * @param concurrency Maximum number of leases that can be active at the same time - * @param demand Demand specification + * @param options Demand specification and concurrency level */ - public async manyOf(concurrency: number, demand: DemandSpec): Promise { + public async manyOf({ concurrency, demand }: ManyOfOptions): Promise { const proposalPool = new DraftOfferProposalPool({ logger: this.logger, });