evernode-js-client 0.4.51 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,357 @@
1
+ const { XrplConstants } = require('../xrpl-common');
2
+ const { BaseEvernodeClient } = require('./base-evernode-client');
3
+ const { EvernodeEvents, EvernodeConstants, MemoFormats, MemoTypes, ErrorCodes } = require('../evernode-common');
4
+ const { XrplAccount } = require('../xrpl-account');
5
+ const { EncryptionHelper } = require('../encryption-helper');
6
+ const { Buffer } = require('buffer');
7
+ const codec = require('ripple-address-codec');
8
+ const { XflHelpers } = require('../xfl-helpers');
9
+ const { EvernodeHelpers } = require('../evernode-helpers');
10
+
11
+ const OFFER_WAIT_TIMEOUT = 60;
12
+
13
+ const HostEvents = {
14
+ AcquireLease: EvernodeEvents.AcquireLease,
15
+ ExtendLease: EvernodeEvents.ExtendLease
16
+ }
17
+
18
+ class HostClient extends BaseEvernodeClient {
19
+
20
+ constructor(xrpAddress, xrpSecret, options = {}) {
21
+ super(xrpAddress, xrpSecret, Object.values(HostEvents), true, options);
22
+ }
23
+
24
+ async getRegistrationNft() {
25
+ // Find an owned NFT with matching Evernode host NFT prefix.
26
+ const nft = (await this.xrplAcc.getNfts()).find(n => n.URI.startsWith(EvernodeConstants.NFT_PREFIX_HEX) && n.Issuer === this.registryAddress);
27
+ if (nft) {
28
+ // Check whether the token was actually issued from Evernode registry contract.
29
+ const issuerHex = nft.NFTokenID.substr(8, 40);
30
+ const issuerAddr = codec.encodeAccountID(Buffer.from(issuerHex, 'hex'));
31
+ if (issuerAddr == this.registryAddress) {
32
+ return nft;
33
+ }
34
+ }
35
+
36
+ return null;
37
+ }
38
+
39
+ async getRegistration() {
40
+ // Check whether we own an evernode host token.
41
+ const nft = await this.getRegistrationNft();
42
+ if (nft) {
43
+ const host = await this.getHostInfo();
44
+ return (host?.nfTokenId == nft.NFTokenID) ? host : null;
45
+ }
46
+
47
+ return null;
48
+ }
49
+
50
+ async getLeaseOffers() {
51
+ return await EvernodeHelpers.getLeaseOffers(this.xrplAcc);
52
+ }
53
+
54
+ async cancelOffer(offerIndex) {
55
+ return this.xrplAcc.cancelOffer(offerIndex);
56
+ }
57
+
58
+ async isRegistered() {
59
+ return (await this.getRegistration()) !== null;
60
+ }
61
+
62
+ async prepareAccount(domain) {
63
+ const [flags, trustLines, msgKey, curDomain] = await Promise.all([
64
+ this.xrplAcc.getFlags(),
65
+ this.xrplAcc.getTrustLines(EvernodeConstants.EVR, this.config.evrIssuerAddress),
66
+ this.xrplAcc.getMessageKey(),
67
+ this.xrplAcc.getDomain()]);
68
+
69
+ let accountSetFields = {};
70
+ accountSetFields = (!flags.lsfDefaultRipple) ? { ...accountSetFields, Flags: { asfDefaultRipple: true } } : accountSetFields;
71
+ accountSetFields = (!msgKey) ? { ...accountSetFields, MessageKey: this.accKeyPair.publicKey } : accountSetFields;
72
+
73
+ domain = domain.toLowerCase();
74
+ accountSetFields = (!curDomain || curDomain !== domain) ?
75
+ { ...accountSetFields, Domain: domain } : accountSetFields;
76
+
77
+ if (Object.keys(accountSetFields).length !== 0)
78
+ await this.xrplAcc.setAccountFields(accountSetFields);
79
+
80
+ if (trustLines.length === 0)
81
+ await this.xrplAcc.setTrustLine(EvernodeConstants.EVR, this.config.evrIssuerAddress, "99999999999999");
82
+ }
83
+
84
+ async offerLease(leaseIndex, leaseAmount, tosHash) {
85
+ // <prefix><lease index 16)><half of tos hash><lease amount (uint32)>
86
+ const prefixLen = EvernodeConstants.LEASE_NFT_PREFIX_HEX.length / 2;
87
+ const halfToSLen = tosHash.length / 4;
88
+ const uriBuf = Buffer.allocUnsafe(prefixLen + halfToSLen + 10);
89
+ Buffer.from(EvernodeConstants.LEASE_NFT_PREFIX_HEX, 'hex').copy(uriBuf);
90
+ uriBuf.writeUInt16BE(leaseIndex, prefixLen);
91
+ Buffer.from(tosHash, 'hex').copy(uriBuf, prefixLen + 2, 0, halfToSLen);
92
+ uriBuf.writeBigInt64BE(XflHelpers.getXfl(leaseAmount.toString()), prefixLen + 2 + halfToSLen);
93
+ const uri = uriBuf.toString('hex').toUpperCase();
94
+
95
+ await this.xrplAcc.mintNft(uri, 0, 0, { isBurnable: true, isHexUri: true });
96
+
97
+ const nft = await this.xrplAcc.getNftByUri(uri, true);
98
+ if (!nft)
99
+ throw "Offer lease NFT creation error.";
100
+
101
+ await this.xrplAcc.offerSellNft(nft.NFTokenID,
102
+ leaseAmount.toString(),
103
+ EvernodeConstants.EVR,
104
+ this.config.evrIssuerAddress);
105
+ }
106
+
107
+ async expireLease(nfTokenId, tenantAddress = null) {
108
+ await this.xrplAcc.burnNft(nfTokenId, tenantAddress);
109
+ }
110
+
111
+ async register(countryCode, cpuMicroSec, ramMb, diskMb, totalInstanceCount, cpuModel, cpuCount, cpuSpeed, description, options = {}) {
112
+ if (!/^([A-Z]{2})$/.test(countryCode))
113
+ throw "countryCode should consist of 2 uppercase alphabetical characters";
114
+ else if (!cpuMicroSec || isNaN(cpuMicroSec) || cpuMicroSec % 1 != 0 || cpuMicroSec < 0)
115
+ throw "cpuMicroSec should be a positive integer";
116
+ else if (!ramMb || isNaN(ramMb) || ramMb % 1 != 0 || ramMb < 0)
117
+ throw "ramMb should be a positive integer";
118
+ else if (!diskMb || isNaN(diskMb) || diskMb % 1 != 0 || diskMb < 0)
119
+ throw "diskMb should be a positive integer";
120
+ else if (!totalInstanceCount || isNaN(totalInstanceCount) || totalInstanceCount % 1 != 0 || totalInstanceCount < 0)
121
+ throw "totalInstanceCount should be a positive intiger";
122
+ else if (!cpuCount || isNaN(cpuCount) || cpuCount % 1 != 0 || cpuCount < 0)
123
+ throw "CPU count should be a positive integer";
124
+ else if (!cpuSpeed || isNaN(cpuSpeed) || cpuSpeed % 1 != 0 || cpuSpeed < 0)
125
+ throw "CPU speed should be a positive integer";
126
+ else if (!cpuModel)
127
+ throw "cpu model cannot be empty";
128
+
129
+ // Need to use control characters inside this regex to match ascii characters.
130
+ // Here we allow all the characters in ascii range except ";" for the description.
131
+ // no-control-regex is enabled default by eslint:recommended, So we disable it only for next line.
132
+ // eslint-disable-next-line no-control-regex
133
+ else if (!/^((?![;])[\x00-\x7F]){0,26}$/.test(description))
134
+ throw "description should consist of 0-26 ascii characters except ';'";
135
+
136
+ if (await this.isRegistered())
137
+ throw "Host already registered.";
138
+
139
+ // Check whether is there any missed NFT sell offer that needs to be accepted
140
+ // from the client-side in order to complete the registration.
141
+ const regNft = await this.getRegistrationNft();
142
+ if (!regNft) {
143
+ const regInfo = await this.getHostInfo(this.xrplAcc.address);
144
+ if (regInfo) {
145
+ const registryAcc = new XrplAccount(this.registryAddress, null, { xrplApi: this.xrplApi });
146
+ const sellOffer = (await registryAcc.getNftOffers()).find(o => o.NFTokenID == regInfo.nfTokenId);
147
+ if (sellOffer) {
148
+ await this.xrplAcc.buyNft(sellOffer.index);
149
+ console.log("Registration was successfully completed after acquiring the NFT.");
150
+ return await this.isRegistered();
151
+ }
152
+ }
153
+ }
154
+
155
+ const memoData = `${countryCode};${cpuMicroSec};${ramMb};${diskMb};${totalInstanceCount};${cpuModel};${cpuCount};${cpuSpeed};${description}`
156
+ const tx = await this.xrplAcc.makePayment(this.registryAddress,
157
+ this.config.hostRegFee.toString(),
158
+ EvernodeConstants.EVR,
159
+ this.config.evrIssuerAddress,
160
+ [{ type: MemoTypes.HOST_REG, format: MemoFormats.TEXT, data: memoData }],
161
+ options.transactionOptions);
162
+
163
+ console.log('Waiting for the sell offer')
164
+ const regAcc = new XrplAccount(this.registryAddress, null, { xrplApi: this.xrplApi });
165
+ let offer = null;
166
+ let attempts = 0;
167
+ let offerLedgerIndex = 0;
168
+ while (attempts < OFFER_WAIT_TIMEOUT) {
169
+ const nft = (await regAcc.getNfts()).find(n => n.URI === `${EvernodeConstants.NFT_PREFIX_HEX}${tx.id}`);
170
+ if (nft) {
171
+ offer = (await regAcc.getNftOffers()).find(o => o.Destination === this.xrplAcc.address && o.NFTokenID === nft.NFTokenID && o.Flags === 1);
172
+ offerLedgerIndex = this.xrplApi.ledgerIndex;
173
+ if (offer)
174
+ break;
175
+ await new Promise(resolve => setTimeout(resolve, 1000));
176
+ attempts++;
177
+ }
178
+
179
+ }
180
+ if (!offer)
181
+ throw 'No sell offer found within timeout.';
182
+
183
+ console.log('Accepting the sell offer..');
184
+
185
+ // Wait until the next ledger after the offer is created.
186
+ // Otherwise if the offer accepted in the same legder which it's been created,
187
+ // We cannot fetch the offer from registry contract event handler since it's getting deleted immediately.
188
+ await new Promise(async resolve => {
189
+ while (this.xrplApi.ledgerIndex <= offerLedgerIndex)
190
+ await new Promise(resolve2 => setTimeout(resolve2, 1000));
191
+ resolve();
192
+ });
193
+
194
+ await this.xrplAcc.buyNft(offer.index);
195
+
196
+ return await this.isRegistered();
197
+ }
198
+
199
+ async deregister(options = {}) {
200
+
201
+ if (!(await this.isRegistered()))
202
+ throw "Host not registered."
203
+
204
+ const regNFT = await this.getRegistrationNft();
205
+ await this.xrplAcc.makePayment(this.registryAddress,
206
+ XrplConstants.MIN_XRP_AMOUNT,
207
+ XrplConstants.XRP,
208
+ null,
209
+ [{ type: MemoTypes.HOST_DEREG, format: MemoFormats.HEX, data: regNFT.NFTokenID }],
210
+ options.transactionOptions);
211
+
212
+ console.log('Waiting for the buy offer')
213
+ const regAcc = new XrplAccount(this.registryAddress, null, { xrplApi: this.xrplApi });
214
+ let offer = null;
215
+ let attempts = 0;
216
+ let offerLedgerIndex = 0;
217
+ while (attempts < OFFER_WAIT_TIMEOUT) {
218
+ offer = (await regAcc.getNftOffers()).find(o => (o.NFTokenID == regNFT.NFTokenID) && (o.Flags === 0));
219
+ offerLedgerIndex = this.xrplApi.ledgerIndex;
220
+ if (offer)
221
+ break;
222
+ await new Promise(resolve => setTimeout(resolve, 1000));
223
+ attempts++;
224
+ }
225
+ if (!offer)
226
+ throw 'No buy offer found within timeout.';
227
+
228
+ console.log('Accepting the buy offer..');
229
+
230
+ // Wait until the next ledger after the offer is created.
231
+ // Otherwise if the offer accepted in the same legder which it's been created,
232
+ // We cannot fetch the offer from registry contract event handler since it's getting deleted immediately.
233
+ await new Promise(async resolve => {
234
+ while (this.xrplApi.ledgerIndex <= offerLedgerIndex)
235
+ await new Promise(resolve2 => setTimeout(resolve2, 1000));
236
+ resolve();
237
+ });
238
+
239
+ await this.xrplAcc.sellNft(
240
+ offer.index,
241
+ [{ type: MemoTypes.HOST_POST_DEREG, format: MemoFormats.HEX, data: regNFT.NFTokenID }]
242
+ );
243
+
244
+ return await this.isRegistered();
245
+ }
246
+
247
+ async updateRegInfo(activeInstanceCount = null, version = null, totalInstanceCount = null, tokenID = null, countryCode = null, cpuMicroSec = null, ramMb = null, diskMb = null, description = null, options = {}) {
248
+ const dataStr = `${tokenID ? tokenID : ''};${countryCode ? countryCode : ''};${cpuMicroSec ? cpuMicroSec : ''};${ramMb ? ramMb : ''};${diskMb ? diskMb : ''};${totalInstanceCount ? totalInstanceCount : ''};${activeInstanceCount !== undefined ? activeInstanceCount : ''};${description ? description : ''};${version ? version : ''}`;
249
+ return await this.xrplAcc.makePayment(this.registryAddress,
250
+ XrplConstants.MIN_XRP_AMOUNT,
251
+ XrplConstants.XRP,
252
+ null,
253
+ [{ type: MemoTypes.HOST_UPDATE_INFO, format: MemoFormats.TEXT, data: dataStr }],
254
+ options.transactionOptions);
255
+ }
256
+
257
+ async heartbeat(options = {}) {
258
+ return this.xrplAcc.makePayment(this.registryAddress,
259
+ XrplConstants.MIN_XRP_AMOUNT,
260
+ XrplConstants.XRP,
261
+ null,
262
+ [{ type: MemoTypes.HEARTBEAT, format: "", data: "" }],
263
+ options.transactionOptions);
264
+ }
265
+
266
+ async acquireSuccess(txHash, tenantAddress, instanceInfo, options = {}) {
267
+
268
+ // Encrypt the instance info with the tenant's encryption key (Specified in MessageKey field of the tenant account).
269
+ const tenantAcc = new XrplAccount(tenantAddress, null, { xrplApi: this.xrplApi });
270
+ const encKey = await tenantAcc.getMessageKey();
271
+ if (!encKey)
272
+ throw "Tenant encryption key not set.";
273
+
274
+ const encrypted = await EncryptionHelper.encrypt(encKey, instanceInfo);
275
+ const memos = [
276
+ { type: MemoTypes.ACQUIRE_SUCCESS, format: MemoFormats.BASE64, data: encrypted },
277
+ { type: MemoTypes.ACQUIRE_REF, format: MemoFormats.HEX, data: txHash }];
278
+
279
+ return this.xrplAcc.makePayment(tenantAddress,
280
+ XrplConstants.MIN_XRP_AMOUNT,
281
+ XrplConstants.XRP,
282
+ null,
283
+ memos,
284
+ options.transactionOptions);
285
+ }
286
+
287
+ async acquireError(txHash, tenantAddress, leaseAmount, reason, options = {}) {
288
+
289
+ const memos = [
290
+ { type: MemoTypes.ACQUIRE_ERROR, format: MemoFormats.JSON, data: { type: ErrorCodes.ACQUIRE_ERR, reason: reason } },
291
+ { type: MemoTypes.ACQUIRE_REF, format: MemoFormats.HEX, data: txHash }];
292
+
293
+ return this.xrplAcc.makePayment(tenantAddress,
294
+ leaseAmount.toString(),
295
+ EvernodeConstants.EVR,
296
+ this.config.evrIssuerAddress,
297
+ memos,
298
+ options.transactionOptions);
299
+ }
300
+
301
+ async extendSuccess(txHash, tenantAddress, expiryMoment, options = {}) {
302
+ let buf = Buffer.allocUnsafe(4);
303
+ buf.writeUInt32BE(expiryMoment);
304
+
305
+ const memos = [
306
+ { type: MemoTypes.EXTEND_SUCCESS, format: MemoFormats.HEX, data: buf.toString('hex') },
307
+ { type: MemoTypes.EXTEND_REF, format: MemoFormats.HEX, data: txHash }];
308
+
309
+ return this.xrplAcc.makePayment(tenantAddress,
310
+ XrplConstants.MIN_XRP_AMOUNT,
311
+ XrplConstants.XRP,
312
+ null,
313
+ memos,
314
+ options.transactionOptions);
315
+ }
316
+
317
+ async extendError(txHash, tenantAddress, reason, refund, options = {}) {
318
+
319
+ const memos = [
320
+ { type: MemoTypes.EXTEND_ERROR, format: MemoFormats.JSON, data: { type: ErrorCodes.EXTEND_ERR, reason: reason } },
321
+ { type: MemoTypes.EXTEND_REF, format: MemoFormats.HEX, data: txHash }];
322
+
323
+ // Required to refund the paid EVR amount as the offer extention is not successfull.
324
+ return this.xrplAcc.makePayment(tenantAddress,
325
+ refund.toString(),
326
+ EvernodeConstants.EVR,
327
+ this.config.evrIssuerAddress,
328
+ memos,
329
+ options.transactionOptions);
330
+ }
331
+
332
+ async refundTenant(txHash, tenantAddress, refundAmount, options = {}) {
333
+ const memos = [
334
+ { type: MemoTypes.REFUND, format: '', data: '' },
335
+ { type: MemoTypes.REFUND_REF, format: MemoFormats.HEX, data: txHash }];
336
+
337
+ return this.xrplAcc.makePayment(tenantAddress,
338
+ refundAmount.toString(),
339
+ EvernodeConstants.EVR,
340
+ this.config.evrIssuerAddress,
341
+ memos,
342
+ options.transactionOptions);
343
+ }
344
+
345
+ getLeaseNFTokenIdPrefix() {
346
+ let buf = Buffer.allocUnsafe(24);
347
+ buf.writeUInt16BE(1);
348
+ buf.writeUInt16BE(0, 2);
349
+ codec.decodeAccountID(this.xrplAcc.address).copy(buf, 4);
350
+ return buf.toString('hex');
351
+ }
352
+ }
353
+
354
+ module.exports = {
355
+ HostEvents,
356
+ HostClient
357
+ }
@@ -0,0 +1,52 @@
1
+ const { EvernodeEvents } = require('../evernode-common');
2
+ const { BaseEvernodeClient } = require('./base-evernode-client');
3
+ const { DefaultValues } = require('../defaults');
4
+
5
+ const RegistryEvents = {
6
+ HostRegistered: EvernodeEvents.HostRegistered,
7
+ HostDeregistered: EvernodeEvents.HostDeregistered,
8
+ HostRegUpdated: EvernodeEvents.HostRegUpdated,
9
+ RegistryInitialized: EvernodeEvents.RegistryInitialized,
10
+ Heartbeat: EvernodeEvents.Heartbeat,
11
+ HostPostDeregistered: EvernodeEvents.HostPostDeregistered,
12
+ DeadHostPrune: EvernodeEvents.DeadHostPrune
13
+ }
14
+
15
+ class RegistryClient extends BaseEvernodeClient {
16
+
17
+ /**
18
+ * Constructs a registry client instance.
19
+ * @param {object} options [Optional] An object with 'rippledServer' URL and 'registryAddress'.
20
+ */
21
+ constructor(options = {}) {
22
+ super((options.registryAddress || DefaultValues.registryAddress), null, Object.values(RegistryEvents), false, options);
23
+ }
24
+
25
+ /**
26
+ * Gets all the active hosts registered in Evernode without paginating.
27
+ * @returns The list of active hosts.
28
+ */
29
+ async getActiveHosts() {
30
+ let fullHostList = [];
31
+ const hosts = await this.getHosts();
32
+ if (hosts.nextPageToken) {
33
+ let currentPageToken = hosts.nextPageToken;
34
+ let nextHosts = null;
35
+ fullHostList = fullHostList.concat(hosts.data);
36
+ while (currentPageToken) {
37
+ nextHosts = await this.getHosts(null, null, currentPageToken);
38
+ fullHostList = fullHostList.concat(nextHosts.nextPageToken ? nextHosts.data : nextHosts);
39
+ currentPageToken = nextHosts.nextPageToken;
40
+ }
41
+ } else {
42
+ fullHostList = fullHostList.concat(hosts);
43
+ }
44
+ // Filter only active hosts.
45
+ return fullHostList.filter(h => h.active);
46
+ }
47
+ }
48
+
49
+ module.exports = {
50
+ RegistryEvents,
51
+ RegistryClient
52
+ }
@@ -0,0 +1,264 @@
1
+ const { BaseEvernodeClient } = require('./base-evernode-client');
2
+ const { EvernodeEvents, MemoFormats, MemoTypes, ErrorCodes, ErrorReasons, EvernodeConstants } = require('../evernode-common');
3
+ const { EncryptionHelper } = require('../encryption-helper');
4
+ const { XrplAccount } = require('../xrpl-account');
5
+ const { UtilHelpers } = require('../util-helpers');
6
+ const { Buffer } = require('buffer');
7
+ const codec = require('ripple-address-codec');
8
+ const { EvernodeHelpers } = require('../evernode-helpers');
9
+ const { TransactionHelper } = require('../transaction-helper');
10
+
11
+ const DEFAULT_WAIT_TIMEOUT = 60000;
12
+
13
+ const TenantEvents = {
14
+ AcquireSuccess: EvernodeEvents.AcquireSuccess,
15
+ AcquireError: EvernodeEvents.AcquireError,
16
+ ExtendSuccess: EvernodeEvents.ExtendSuccess,
17
+ ExtendError: EvernodeEvents.ExtendError,
18
+ }
19
+
20
+ class TenantClient extends BaseEvernodeClient {
21
+
22
+ /**
23
+ * Constructs a tenant client instance.
24
+ * @param {string} xrpAddress XRPL address of the tenant.
25
+ * @param {string} XRPL secret of the tenant.
26
+ * @param {object} options [Optional] An object with 'rippledServer' URL and 'registryAddress'.
27
+ */
28
+ constructor(xrpAddress, xrpSecret, options = {}) {
29
+ super(xrpAddress, xrpSecret, Object.values(TenantEvents), false, options);
30
+ }
31
+
32
+ async prepareAccount() {
33
+ try {
34
+ if (!await this.xrplAcc.getMessageKey())
35
+ await this.xrplAcc.setAccountFields({ MessageKey: this.accKeyPair.publicKey });
36
+ }
37
+ catch (err) {
38
+ console.log("Error in preparing user xrpl account for Evernode.", err);
39
+ }
40
+ }
41
+
42
+ async getLeaseHost(hostAddress) {
43
+ const host = new XrplAccount(hostAddress, null, { xrplApi: this.xrplApi });
44
+ // Find an owned NFT with matching Evernode host NFT prefix.
45
+ const nft = (await host.getNfts()).find(n => n.URI.startsWith(EvernodeConstants.NFT_PREFIX_HEX));
46
+ if (!nft)
47
+ throw { reason: ErrorReasons.HOST_INVALID, error: "Host is not registered." };
48
+
49
+ // Check whether the token was actually issued from Evernode registry contract.
50
+ const issuerHex = nft.NFTokenID.substr(8, 40);
51
+ const issuerAddr = codec.encodeAccountID(Buffer.from(issuerHex, 'hex'));
52
+ if (issuerAddr != this.registryAddress)
53
+ throw { reason: ErrorReasons.HOST_INVALID, error: "Host is not registered." };
54
+
55
+ // Check whether active.
56
+ const hostInfo = await this.getHostInfo(host.address);
57
+ if (!hostInfo)
58
+ throw { reason: ErrorReasons.HOST_INVALID, error: "Host is not registered." };
59
+ else if (!hostInfo.active)
60
+ throw { reason: ErrorReasons.HOST_INACTIVE, error: "Host is not active." };
61
+
62
+ return host;
63
+ }
64
+
65
+ /**
66
+ *
67
+ * @param {string} hostAddress XRPL address of the host to acquire the lease.
68
+ * @param {object} requirement The instance requirements and configuration.
69
+ * @param {object} options [Optional] Options for the XRPL transaction.
70
+ * @returns The transaction result.
71
+ */
72
+ async acquireLeaseSubmit(hostAddress, requirement, options = {}) {
73
+
74
+ const hostAcc = await this.getLeaseHost(hostAddress);
75
+ let selectedOfferIndex = options.leaseOfferIndex;
76
+
77
+ // Attempt to get first available offer, if offer is not specified in options.
78
+ if (!selectedOfferIndex) {
79
+ const nftOffers = await EvernodeHelpers.getLeaseOffers(hostAcc);
80
+ selectedOfferIndex = nftOffers && nftOffers[0] && nftOffers[0].index;
81
+
82
+ if (!selectedOfferIndex)
83
+ throw { reason: ErrorReasons.NO_OFFER, error: "No offers available." };
84
+ }
85
+
86
+ // Encrypt the requirements with the host's encryption key (Specified in MessageKey field of the host account).
87
+ const encKey = await hostAcc.getMessageKey();
88
+ if (!encKey)
89
+ throw { reason: ErrorReasons.INTERNAL_ERR, error: "Host encryption key not set." };
90
+
91
+ const ecrypted = await EncryptionHelper.encrypt(encKey, requirement, {
92
+ iv: options.iv, // Must be null or 16 bytes.
93
+ ephemPrivateKey: options.ephemPrivateKey // Must be null or 32 bytes.
94
+ });
95
+
96
+ return this.xrplAcc.buyNft(selectedOfferIndex, [{ type: MemoTypes.ACQUIRE_LEASE, format: MemoFormats.BASE64, data: ecrypted }], options.transactionOptions);
97
+ }
98
+
99
+ /**
100
+ * Watch for the acquire-success response after the acquire request is made.
101
+ * @param {object} tx The transaction returned by the acquireLeaseSubmit function.
102
+ * @param {object} options [Optional] Options for the XRPL transaction.
103
+ * @returns An object including transaction details,instance info, and acquireReference Id.
104
+ */
105
+ async watchAcquireResponse(tx, options = {}) {
106
+ console.log(`Waiting for acquire response... (txHash: ${tx.id})`);
107
+
108
+ const failTimeout = setTimeout(() => {
109
+ throw({ error: ErrorCodes.ACQUIRE_ERR, reason: ErrorReasons.TIMEOUT });
110
+ }, options.timeout || DEFAULT_WAIT_TIMEOUT);
111
+
112
+ let relevantTx = null;
113
+ while (!relevantTx) {
114
+ const txList = await this.xrplAcc.getAccountTrx(tx.details.ledger_index);
115
+ for (let t of txList) {
116
+ t.tx.Memos = TransactionHelper.deserializeMemos(t.tx?.Memos);
117
+ const res = await this.extractEvernodeEvent(t.tx);
118
+ if ((res?.name === EvernodeEvents.AcquireSuccess || res?.name === EvernodeEvents.AcquireError) && res?.data?.acquireRefId === tx.id) {
119
+ clearTimeout(failTimeout);
120
+ relevantTx = res;
121
+ break;
122
+ }
123
+ }
124
+ await new Promise(resolve => setTimeout(resolve, 2000));
125
+ }
126
+
127
+ if (relevantTx?.name === TenantEvents.AcquireSuccess) {
128
+ return({
129
+ transaction: relevantTx?.data.transaction,
130
+ instance: relevantTx?.data.payload.content,
131
+ acquireRefId: relevantTx?.data.acquireRefId
132
+ });
133
+ } else if (relevantTx?.name === TenantEvents.AcquireError) {
134
+ throw({
135
+ error: ErrorCodes.ACQUIRE_ERR,
136
+ transaction: relevantTx?.data.transaction,
137
+ reason: relevantTx?.data.reason,
138
+ acquireRefId: relevantTx?.data.acquireRefId
139
+ })
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Acquire an instance from a host
145
+ * @param {string} hostAddress XRPL address of the host to acquire the lease.
146
+ * @param {object} requirement The instance requirements and configuration.
147
+ * @param {object} options [Optional] Options for the XRPL transaction.
148
+ * @returns An object including transaction details,instance info, and acquireReference Id.
149
+ */
150
+ acquireLease(hostAddress, requirement, options = {}) {
151
+ return new Promise(async (resolve, reject) => {
152
+ const tx = await this.acquireLeaseSubmit(hostAddress, requirement, options).catch(error => {
153
+ reject({ error: ErrorCodes.ACQUIRE_ERR, reason: error.reason || ErrorReasons.TRANSACTION_FAILURE, content: error.error || error });
154
+ });
155
+ if (tx) {
156
+ try {
157
+ const response = await this.watchAcquireResponse(tx, options);
158
+ resolve(response);
159
+ } catch (error) {
160
+ reject(error);
161
+ }
162
+ }
163
+ });
164
+ }
165
+
166
+ /**
167
+ * This function is called by a tenant client to submit the extend lease transaction in certain host. This function will be called inside extendLease function. This function can take four parameters as follows.
168
+ * @param {string} hostAddress XRPL account address of the host.
169
+ * @param {number} amount Cost for the extended moments , in EVRs.
170
+ * @param {string} tokenID Tenant received instance name. this name can be retrieve by performing acquire Lease.
171
+ * @param {object} options This is an optional field and contains necessary details for the transactions.
172
+ * @returns The transaction result.
173
+ */
174
+ async extendLeaseSubmit(hostAddress, amount, tokenID, options = {}) {
175
+ const host = await this.getLeaseHost(hostAddress);
176
+ return this.xrplAcc.makePayment(host.address, amount.toString(), EvernodeConstants.EVR, this.config.evrIssuerAddress,
177
+ [{ type: MemoTypes.EXTEND_LEASE, format: MemoFormats.HEX, data: tokenID }], options.transactionOptions);
178
+ }
179
+
180
+ /**
181
+ * This function watches for an extendlease-success response(transaction) and returns the response or throws the error response on extendlease-error response from the host XRPL account. This function is called within the extendLease function.
182
+ * @param {object} tx Response of extendLeaseSubmit.
183
+ * @param {object} options This is an optional field and contains necessary details for the transactions.
184
+ * @returns An object including transaction details.
185
+ */
186
+ async watchExtendResponse(tx, options = {}) {
187
+ console.log(`Waiting for extend lease response... (txHash: ${tx.id})`);
188
+
189
+ const failTimeout = setTimeout(() => {
190
+ throw({ error: ErrorCodes.EXTEND_ERR, reason: ErrorReasons.TIMEOUT });
191
+ }, options.timeout || DEFAULT_WAIT_TIMEOUT);
192
+
193
+ let relevantTx = null;
194
+ while (!relevantTx) {
195
+ const txList = await this.xrplAcc.getAccountTrx(tx.details.ledger_index);
196
+ for (let t of txList) {
197
+ t.tx.Memos = TransactionHelper.deserializeMemos(t.tx.Memos);
198
+ const res = await this.extractEvernodeEvent(t.tx);
199
+ if ((res?.name === TenantEvents.ExtendSuccess || res?.name === TenantEvents.ExtendError) && res?.data?.extendRefId === tx.id) {
200
+ clearTimeout(failTimeout);
201
+ relevantTx = res;
202
+ break;
203
+ }
204
+ }
205
+ await new Promise(resolve => setTimeout(resolve, 1000));
206
+ }
207
+
208
+ if (relevantTx?.name === TenantEvents.ExtendSuccess) {
209
+ return({
210
+ transaction: relevantTx?.data.transaction,
211
+ expiryMoment: relevantTx?.data.expiryMoment,
212
+ extendeRefId: relevantTx?.data.extendRefId
213
+ });
214
+ } else if (relevantTx?.name === TenantEvents.ExtendError) {
215
+ throw({
216
+ error: ErrorCodes.EXTEND_ERR,
217
+ transaction: relevantTx?.data.transaction,
218
+ reason: relevantTx?.data.reason
219
+ })
220
+ }
221
+ }
222
+
223
+ /**
224
+ * This function is called by a tenant client to extend an available instance in certain host. This function can take four parameters as follows.
225
+ * @param {string} hostAddress XRPL account address of the host.
226
+ * @param {number} moments 1190 ledgers (est. 1 hour).
227
+ * @param {string} instanceName Tenant received instance name. this name can be retrieve by performing acquire Lease.
228
+ * @param {object} options This is an optional field and contains necessary details for the transactions.
229
+ * @returns An object including transaction details.
230
+ */
231
+ extendLease(hostAddress, moments, instanceName, options = {}) {
232
+ return new Promise(async (resolve, reject) => {
233
+ const tokenID = instanceName;
234
+ const nft = (await this.xrplAcc.getNfts())?.find(n => n.NFTokenID == tokenID);
235
+
236
+ if (!nft) {
237
+ reject({ error: ErrorCodes.EXTEND_ERR, reason: ErrorReasons.NO_NFT, content: 'Could not find the nft for lease extend request.' });
238
+ return;
239
+ }
240
+
241
+ let minLedgerIndex = this.xrplApi.ledgerIndex;
242
+
243
+ // Get the agreement lease amount from the nft and calculate EVR amount to be sent.
244
+ const uriInfo = UtilHelpers.decodeLeaseNftUri(nft.URI);
245
+ const tx = await this.extendLeaseSubmit(hostAddress, moments * uriInfo.leaseAmount, tokenID, options).catch(error => {
246
+ reject({ error: ErrorCodes.EXTEND_ERR, reason: error.reason || ErrorReasons.TRANSACTION_FAILURE, content: error.error || error });
247
+ });
248
+
249
+ if (tx) {
250
+ try {
251
+ const response = await this.watchExtendResponse(tx, minLedgerIndex, options)
252
+ resolve(response);
253
+ } catch (error) {
254
+ reject(error);
255
+ }
256
+ }
257
+ });
258
+ }
259
+ }
260
+
261
+ module.exports = {
262
+ TenantEvents,
263
+ TenantClient
264
+ }