-
Notifications
You must be signed in to change notification settings - Fork 19
/
step-by-step.ts
176 lines (159 loc) · 7.48 KB
/
step-by-step.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
/**
* This advanced example demonstrates how to perform all market interactions "manually".
* It should give you a basic understanding of how this SDK works under the hood.
* If you're just getting started with golem-js, take a look at the basic examples first.
* Keep in mind that this is not the recommended way to interact with the Golem Network, as
* it doesn't cover all edge cases and error handling. This example should be used for educational purposes only.
*/
import {
Allocation,
GolemNetwork,
MarketOrderSpec,
OfferProposal,
OfferProposalReceivedEvent,
} from "@golem-sdk/golem-js";
import { pinoPrettyLogger } from "@golem-sdk/pino-logger";
import { filter, map, switchMap, take } from "rxjs";
(async () => {
const logger = pinoPrettyLogger({
level: "info",
});
const glm = new GolemNetwork({
logger,
});
const RENTAL_DURATION_HOURS = 10 / 60;
const ALLOCATION_DURATION_HOURS = RENTAL_DURATION_HOURS + 0.25;
console.assert(
ALLOCATION_DURATION_HOURS > RENTAL_DURATION_HOURS,
"Always create allocations that will live longer than the planned rental duration",
);
let allocation: Allocation | undefined;
try {
await glm.connect();
// Define the order that we're going to place on the market
const order: MarketOrderSpec = {
demand: {
workload: {
imageTag: "golem/alpine:latest",
minCpuCores: 1,
minMemGib: 2,
},
},
market: {
// We're only going to rent the provider for 5 minutes max
rentHours: RENTAL_DURATION_HOURS,
pricing: {
model: "linear",
maxStartPrice: 1,
maxCpuPerHourPrice: 1,
maxEnvPerHourPrice: 1,
},
},
};
// Allocate funds to cover the order, we will only pay for the actual usage
// so any unused funds will be returned to us at the end
allocation = await glm.payment.createAllocation({
budget: glm.market.estimateBudget({ order, maxAgreements: 1 }),
expirationSec: ALLOCATION_DURATION_HOURS * 60 * 60,
});
// Convert the human-readable order to a protocol-level format that we will publish on the network
const demandSpecification = await glm.market.buildDemandDetails(order.demand, order.market, allocation);
// Publish the order on the market
// This methods creates and observable that publishes the order and refreshes it every 30 minutes.
// Unsubscribing from the observable will remove the order from the market
const demand$ = glm.market.publishAndRefreshDemand(demandSpecification);
// Now, for each created demand, let's listen to proposals from providers
const offerProposal$ = demand$.pipe(
switchMap((demand) => glm.market.collectMarketProposalEvents(demand)),
// to keep things simple we don't care about any other events
// related to this demand, only proposals from providers
filter((event): event is OfferProposalReceivedEvent => event.type === "ProposalReceived"),
map((event) => event.proposal),
);
// Each received proposal can be in one of two states: initial or draft
// Initial proposals are the first ones received from providers and require us to respond with a counter-offer
// Draft proposals are the ones that we have already negotiated and are ready to be accepted
// Both types come in the same stream, so let's write a handler that will respond to initial proposals
// and save draft proposals for later
const draftProposals: OfferProposal[] = [];
const offerProposalsSubscription = offerProposal$.subscribe((offerProposal) => {
if (offerProposal.isInitial()) {
// here we can define our own counter-offer
// to keep this example simple, we will respond with the same
// specification as the one we used to publish the demand
// feel free to modify this to your needs
glm.market.negotiateProposal(offerProposal, demandSpecification).catch(console.error);
} else if (offerProposal.isDraft()) {
draftProposals.push(offerProposal);
}
});
// Let's wait for a couple seconds to receive some proposals
while (draftProposals.length < 1) {
console.log("Waiting for proposals...");
await new Promise((resolve) => setTimeout(resolve, 1000));
}
// We have received at least one draft proposal, we can now stop listening for more
offerProposalsSubscription.unsubscribe();
// Remember that signing the proposal can fail, so in a production environment
// you should handle the error and retry with a different proposal.
// To keep this example simple, we will not retry and just crash if the signing fails
const draftProposal = draftProposals[0]!;
const agreement = await glm.market.proposeAgreement(draftProposal);
console.log("Agreement signed with provider", agreement.provider.name);
// Provider is ready to start the computation
// Let's setup payment first
// As the computation happens, we will receive debit notes to inform us about the cost
// and an invoice at the end to settle the payment
const invoiceSubscription = glm.payment
.observeInvoices()
.pipe(
// make sure we only process invoices related to our agreement
filter((invoice) => invoice.agreementId === agreement.id),
// end the stream after we receive an invoice
take(1),
)
.subscribe((invoice) => {
console.log("Received invoice for ", invoice.getPreciseAmount().toFixed(4), "GLM");
glm.payment.acceptInvoice(invoice, allocation!, invoice.amount).catch(console.error);
});
const debitNoteSubscription = glm.payment
.observeDebitNotes()
.pipe(
// make sure we only process invoices related to our agreement
filter((debitNote) => debitNote.agreementId === agreement.id),
)
.subscribe((debitNote) => {
console.log("Received debit note for ", debitNote.getPreciseAmount().toFixed(4), "GLM");
glm.payment.acceptDebitNote(debitNote, allocation!, debitNote.totalAmountDue).catch(console.error);
});
// Start the computation
// First lets start the activity - this will deploy our image on the provider's machine
const activity = await glm.activity.createActivity(agreement);
// Then let's create a ExeUnit, which is a set of utilities to interact with the
// providers machine, like running commands, uploading files, etc.
const exe = await glm.activity.createExeUnit(activity);
// Now we can run a simple command on the provider's machine
const result = await exe.run("echo Hello, Golem 👋!");
console.log("Result of the command ran on the provider's machine:", result.stdout);
// We're done, let's clean up
// First we need to destroy the activity
await glm.activity.destroyActivity(activity);
// Then let's terminate the agreement
await glm.market.terminateAgreement(agreement);
// Before we finish, let's wait for the invoice to be settled
while (!invoiceSubscription.closed) {
console.log("Waiting for the invoice to be settled...");
await new Promise((resolve) => setTimeout(resolve, 1000));
}
// We're done! Let's cleanup the subscriptions, release the remaining funds and disconnect from the network
invoiceSubscription.unsubscribe();
debitNoteSubscription.unsubscribe();
} catch (err) {
console.error("Failed to run example on Golem", err);
} finally {
if (allocation) {
await glm.payment.releaseAllocation(allocation);
}
await glm.disconnect();
}
})().catch(console.error);