Skip to content

Commit

Permalink
Merge pull request #983 from golemfactory/feature/JST-993/deposit-in-…
Browse files Browse the repository at this point in the history
…allocation-in-3.0

Deposit in allocation [in golem-js 3.0]
  • Loading branch information
SewerynKras authored Jun 19, 2024
2 parents 03559c2 + d7c1a4a commit ecab235
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 23 deletions.
75 changes: 75 additions & 0 deletions examples/advanced/reuse-allocation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* This advanced example demonstrates create an allocation manually and then reuse
* it across multiple market orders.
*/
import { MarketOrderSpec, GolemNetwork } from "@golem-sdk/golem-js";
import { pinoPrettyLogger } from "@golem-sdk/pino-logger";
(async () => {
const glm = new GolemNetwork({
logger: pinoPrettyLogger({
level: "info",
}),
});

try {
await glm.connect();

const allocation = await glm.payment.createAllocation({
budget: 1,
expirationSec: 3600,
});

const firstOrder: MarketOrderSpec = {
demand: {
workload: { imageTag: "golem/alpine:latest" },
},
market: {
rentHours: 0.5,
pricing: {
model: "burn-rate",
avgGlmPerHour: 0.5,
},
},
payment: {
// You can either pass the allocation object ...
allocation,
},
};
const secondOrder: MarketOrderSpec = {
demand: {
workload: { imageTag: "golem/alpine:latest" },
},
market: {
rentHours: 0.5,
pricing: {
model: "burn-rate",
avgGlmPerHour: 0.5,
},
},
payment: {
// ... or just the allocation ID
allocation: allocation.id,
},
};

const lease1 = await glm.oneOf(firstOrder);
const lease2 = await glm.oneOf(secondOrder);

await lease1
.getExeUnit()
.then((exe) => exe.run("echo Running on first lease"))
.then((res) => console.log(res.stdout));
await lease2
.getExeUnit()
.then((exe) => exe.run("echo Running on second lease"))
.then((res) => console.log(res.stdout));

await lease1.finalize();
await lease2.finalize();
await glm.payment.releaseAllocation(allocation);
} catch (err) {
console.error("Failed to run the example", err);
} finally {
await glm.disconnect();
}
})().catch(console.error);
57 changes: 57 additions & 0 deletions src/golem-network/golem-network.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,35 @@ describe("Golem Network", () => {
verify(mockLeaseProcess.finalize()).once();
verify(mockPayment.releaseAllocation(allocation)).once();
});
it("should not release the allocation if it was provided by the user", async () => {
const allocation = instance(mock(Allocation));

const mockLeaseProcess = mock(LeaseProcess);
const testProcess = instance(mockLeaseProcess);
when(mockLeaseProcess.finalize()).thenResolve();
when(mockLease.createLease(_, _, _)).thenReturn(testProcess);

when(mockMarket.collectDraftOfferProposals(_)).thenReturn(new Subject<OfferProposal>());
jest.spyOn(DraftOfferProposalPool.prototype, "acquire").mockResolvedValue({} as OfferProposal);

const glm = getGolemNetwork();
await glm.connect();

const lease = await glm.oneOf({
...order,
payment: {
allocation,
},
});

expect(lease).toBe(testProcess);

await glm.disconnect();

verify(mockLeaseProcess.finalize()).once();
verify(mockPayment.createAllocation(_)).never();
verify(mockPayment.releaseAllocation(allocation)).never();
});
});

describe("manyOf()", () => {
Expand Down Expand Up @@ -127,5 +156,33 @@ describe("Golem Network", () => {
verify(mockLeasePool.drainAndClear()).once();
verify(mockPayment.releaseAllocation(allocation)).once();
});
it("should not release the allocation if it was provided by the user", async () => {
const allocation = instance(mock(Allocation));

when(mockMarket.collectDraftOfferProposals(_)).thenReturn(new Subject<OfferProposal>());
const mockLeasePool = mock(LeaseProcessPool);
when(mockLeasePool.drainAndClear()).thenResolve();
const leasePool = instance(mockLeasePool);
when(mockLease.createLeaseProcessPool(_, _, _)).thenReturn(leasePool);

const glm = getGolemNetwork();
await glm.connect();

const pool = await glm.manyOf({
concurrency: 3,
order: {
...order,
payment: {
allocation,
},
},
});

expect(pool).toBe(leasePool);
await glm.disconnect();
verify(mockLeasePool.drainAndClear()).once();
verify(mockPayment.createAllocation(_)).never();
verify(mockPayment.releaseAllocation(allocation)).never();
});
});
});
54 changes: 40 additions & 14 deletions src/golem-network/golem-network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
MarketOptions,
OfferProposal,
} from "../market";
import { IPaymentApi, PaymentModule, PaymentModuleImpl, PaymentModuleOptions } from "../payment";
import { Allocation, IPaymentApi, PaymentModule, PaymentModuleImpl, PaymentModuleOptions } from "../payment";
import { ActivityModule, ActivityModuleImpl, IActivityApi, IFileServer } from "../activity";
import { INetworkApi, Network, NetworkModule, NetworkModuleImpl, NetworkOptions } from "../network";
import { EventEmitter } from "eventemitter3";
Expand Down Expand Up @@ -111,14 +111,22 @@ export interface GolemNetworkOptions {
>;
}

type AllocationOptions = {
/**
* Optionally pass an existing allocation to use or an ID of an allocation that already exists in yagna.
* If this is not provided, a new allocation will be created based on an estimated budget.
*/
allocation?: Allocation | string;
};

/**
* Represents the order specifications which will result in access to LeaseProcess.
*/
export interface MarketOrderSpec {
demand: BuildDemandOptions;
market: MarketOptions;
activity?: LeaseProcessOptions["activity"];
payment?: LeaseProcessOptions["payment"];
payment?: LeaseProcessOptions["payment"] & AllocationOptions;
network?: Network;
}

Expand Down Expand Up @@ -314,6 +322,26 @@ export class GolemNetwork {
this.hasConnection = false;
}

private async getAllocationFromOrder({
order,
concurrency,
}: {
order: MarketOrderSpec;
concurrency: Concurrency;
}): Promise<Allocation> {
if (!order.payment?.allocation) {
const budget = this.market.estimateBudget({ order, concurrency });
return this.payment.createAllocation({
budget,
expirationSec: order.market.rentHours * 60 * 60,
});
}
if (typeof order.payment.allocation === "string") {
return this.payment.getAllocation(order.payment.allocation);
}
return order.payment.allocation;
}

/**
* Define your computational resource demand and access a single instance
*
Expand All @@ -338,12 +366,7 @@ export class GolemNetwork {
selectProposal: order.market.proposalSelector,
});

const budget = this.market.estimateBudget({ order, concurrency: 1 });
const allocation = await this.payment.createAllocation({
budget,
expirationSec: order.market.rentHours * 60 * 60,
});

const allocation = await this.getAllocationFromOrder({ order, concurrency: 1 });
const demandSpecification = await this.market.buildDemandDetails(order.demand, allocation);

const draftProposal$ = this.market.collectDraftOfferProposals({
Expand Down Expand Up @@ -380,6 +403,10 @@ export class GolemNetwork {
.removeNetworkNode(order.network, networkNode)
.catch((err) => this.logger.error("Error while removing network node", err));
}
// Don't release the allocation if it was provided by the user
if (order.payment?.allocation) {
return;
}
await this.payment
.releaseAllocation(allocation)
.catch((err) => this.logger.error("Error while releasing allocation", err));
Expand Down Expand Up @@ -430,11 +457,7 @@ export class GolemNetwork {
selectProposal: order.market.proposalSelector,
});

const budget = this.market.estimateBudget({ concurrency, order });
const allocation = await this.payment.createAllocation({
budget,
expirationSec: order.market.rentHours * 60 * 60,
});
const allocation = await this.getAllocationFromOrder({ order, concurrency });
const demandSpecification = await this.market.buildDemandDetails(order.demand, allocation);

const draftProposal$ = this.market.collectDraftOfferProposals({
Expand Down Expand Up @@ -464,7 +487,10 @@ export class GolemNetwork {
await leaseProcessPool
.drainAndClear()
.catch((err) => this.logger.error("Error while draining lease process pool", err));

// Don't release the allocation if it was provided by the user
if (order.payment?.allocation) {
return;
}
await this.payment
.releaseAllocation(allocation)
.catch((err) => this.logger.error("Error while releasing allocation", err));
Expand Down
26 changes: 25 additions & 1 deletion src/payment/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,31 @@ export interface IPaymentApi {
}

export type CreateAllocationParams = {
/**
* How much to allocate
*/
budget: number;
paymentPlatform: string;
/**
* How long the allocation should be valid
*/
expirationSec: number;
/**
* Optionally override the payment platform to use for this allocation
*/
paymentPlatform?: string;
/**
* Optionally provide a deposit to be used for the allocation, instead of using funds from the yagna wallet.
* Deposit is a way to pay for the computation using someone else's funds. The other party has to
* call the `createDeposit` method on the `LockPayment` smart contract and provide the deposit ID.
*/
deposit?: {
/**
* Address of the smart contract that holds the deposit.
*/
contract: string;
/**
* ID of the deposit, obtained by calling the `createDeposit` method on the smart contract.
*/
id: string;
};
};
11 changes: 4 additions & 7 deletions src/payment/payment.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export interface PaymentModule {

observeInvoices(): Observable<Invoice>;

createAllocation(params: { budget: number; expirationSec: number }): Promise<Allocation>;
createAllocation(params: CreateAllocationParams): Promise<Allocation>;

releaseAllocation(allocation: Allocation): Promise<void>;

Expand Down Expand Up @@ -136,16 +136,13 @@ export class PaymentModuleImpl implements PaymentModule {
return this.paymentApi.receivedInvoices$;
}

async createAllocation(params: { budget: number; expirationSec: number }): Promise<Allocation> {
const payer = await this.getPayerDetails();

this.logger.info("Creating allocation", { params: params, payer });
async createAllocation(params: CreateAllocationParams): Promise<Allocation> {
this.logger.info("Creating allocation", { params: params });

try {
const allocation = await this.paymentApi.createAllocation({
budget: params.budget,
paymentPlatform: this.getPaymentPlatform(),
expirationSec: params.expirationSec,
...params,
});
this.events.emit("allocationCreated", allocation);
return allocation;
Expand Down
3 changes: 2 additions & 1 deletion src/shared/yagna/adapters/payment-api-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,14 @@ export class PaymentApiAdapter implements IPaymentApi {
const model = await this.yagna.payment.createAllocation({
totalAmount: params.budget.toString(),
paymentPlatform: params.paymentPlatform,
address: address,
address,
timestamp: now.toISOString(),
timeout: new Date(+now + params.expirationSec * 1000).toISOString(),
makeDeposit: false,
remainingAmount: "",
spentAmount: "",
allocationId: "",
deposit: params.deposit,
});

this.logger.debug(
Expand Down
1 change: 1 addition & 0 deletions tests/examples/examples.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
{ "cmd": "tsx", "path": "examples/advanced/proposal-filter.ts" },
{ "cmd": "tsx", "path": "examples/advanced/proposal-predefined-filter.ts" },
{ "cmd": "tsx", "path": "examples/advanced/override-module.ts" },
{ "cmd": "tsx", "path": "examples/advanced/reuse-allocation.ts" },
{ "cmd": "tsx", "path": "examples/experimental/deployment/new-api.ts" },
{ "cmd": "tsx", "path": "examples/experimental/job/getJobById.ts" },
{ "cmd": "tsx", "path": "examples/experimental/job/waitForResults.ts" },
Expand Down

0 comments on commit ecab235

Please sign in to comment.