evernode-js-client 0.5.10 → 0.5.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,560 @@
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
+ const { StateHelpers } = require('../state-helpers');
11
+
12
+ const OFFER_WAIT_TIMEOUT = 60;
13
+
14
+ const HostEvents = {
15
+ AcquireLease: EvernodeEvents.AcquireLease,
16
+ ExtendLease: EvernodeEvents.ExtendLease
17
+ }
18
+
19
+ const HOST_COUNTRY_CODE_MEMO_OFFSET = 0;
20
+ const HOST_CPU_MICROSEC_MEMO_OFFSET = 2;
21
+ const HOST_RAM_MB_MEMO_OFFSET = 6;
22
+ const HOST_DISK_MB_MEMO_OFFSET = 10;
23
+ const HOST_TOT_INS_COUNT_MEMO_OFFSET = 14;
24
+ const HOST_CPU_MODEL_NAME_MEMO_OFFSET = 18;
25
+ const HOST_CPU_COUNT_MEMO_OFFSET = 58;
26
+ const HOST_CPU_SPEED_MEMO_OFFSET = 60;
27
+ const HOST_DESCRIPTION_MEMO_OFFSET = 62;
28
+ const HOST_EMAIL_ADDRESS_MEMO_OFFSET = 88;
29
+ const HOST_REG_MEMO_SIZE = 128;
30
+
31
+ const HOST_UPDATE_TOKEN_ID_MEMO_OFFSET = 0;
32
+ const HOST_UPDATE_COUNTRY_CODE_MEMO_OFFSET = 32;
33
+ const HOST_UPDATE_CPU_MICROSEC_MEMO_OFFSET = 34;
34
+ const HOST_UPDATE_RAM_MB_MEMO_OFFSET = 38;
35
+ const HOST_UPDATE_DISK_MB_MEMO_OFFSET = 42;
36
+ const HOST_UPDATE_TOT_INS_COUNT_MEMO_OFFSET = 46;
37
+ const HOST_UPDATE_ACT_INS_COUNT_MEMO_OFFSET = 50;
38
+ const HOST_UPDATE_DESCRIPTION_MEMO_OFFSET = 54;
39
+ const HOST_UPDATE_VERSION_MEMO_OFFSET = 80;
40
+ const HOST_UPDATE_MEMO_SIZE = 83;
41
+
42
+ class HostClient extends BaseEvernodeClient {
43
+
44
+ constructor(xrpAddress, xrpSecret, options = {}) {
45
+ super(xrpAddress, xrpSecret, Object.values(HostEvents), true, options);
46
+ }
47
+
48
+ async getRegistrationNft() {
49
+ // Find an owned NFT with matching Evernode host NFT prefix.
50
+ const nft = (await this.xrplAcc.getNfts()).find(n => n.URI.startsWith(EvernodeConstants.NFT_PREFIX_HEX) && n.Issuer === this.registryAddress);
51
+ if (nft) {
52
+ // Check whether the token was actually issued from Evernode registry contract.
53
+ const issuerHex = nft.NFTokenID.substr(8, 40);
54
+ const issuerAddr = codec.encodeAccountID(Buffer.from(issuerHex, 'hex'));
55
+ if (issuerAddr == this.registryAddress) {
56
+ return nft;
57
+ }
58
+ }
59
+
60
+ return null;
61
+ }
62
+
63
+ async getRegistration() {
64
+ // Check whether we own an evernode host token.
65
+ const nft = await this.getRegistrationNft();
66
+ if (nft) {
67
+ const host = await this.getHostInfo();
68
+ return (host?.nfTokenId == nft.NFTokenID) ? host : null;
69
+ }
70
+
71
+ return null;
72
+ }
73
+
74
+ async getLeaseOffers() {
75
+ return await EvernodeHelpers.getLeaseOffers(this.xrplAcc);
76
+ }
77
+
78
+ async cancelOffer(offerIndex) {
79
+ return this.xrplAcc.cancelOffer(offerIndex);
80
+ }
81
+
82
+ async isRegistered() {
83
+ return (await this.getRegistration()) !== null;
84
+ }
85
+
86
+ async prepareAccount(domain) {
87
+ const [flags, trustLines, msgKey, curDomain] = await Promise.all([
88
+ this.xrplAcc.getFlags(),
89
+ this.xrplAcc.getTrustLines(EvernodeConstants.EVR, this.config.evrIssuerAddress),
90
+ this.xrplAcc.getMessageKey(),
91
+ this.xrplAcc.getDomain()]);
92
+
93
+ let accountSetFields = {};
94
+ accountSetFields = (!flags.lsfDefaultRipple) ? { ...accountSetFields, Flags: { asfDefaultRipple: true } } : accountSetFields;
95
+ accountSetFields = (!msgKey) ? { ...accountSetFields, MessageKey: this.accKeyPair.publicKey } : accountSetFields;
96
+
97
+ domain = domain.toLowerCase();
98
+ accountSetFields = (!curDomain || curDomain !== domain) ?
99
+ { ...accountSetFields, Domain: domain } : accountSetFields;
100
+
101
+ if (Object.keys(accountSetFields).length !== 0)
102
+ await this.xrplAcc.setAccountFields(accountSetFields);
103
+
104
+ if (trustLines.length === 0)
105
+ await this.xrplAcc.setTrustLine(EvernodeConstants.EVR, this.config.evrIssuerAddress, "99999999999999");
106
+ }
107
+
108
+ async offerLease(leaseIndex, leaseAmount, tosHash) {
109
+ // <prefix><lease index 16)><half of tos hash><lease amount (uint32)>
110
+ const prefixLen = EvernodeConstants.LEASE_NFT_PREFIX_HEX.length / 2;
111
+ const halfToSLen = tosHash.length / 4;
112
+ const uriBuf = Buffer.allocUnsafe(prefixLen + halfToSLen + 10);
113
+ Buffer.from(EvernodeConstants.LEASE_NFT_PREFIX_HEX, 'hex').copy(uriBuf);
114
+ uriBuf.writeUInt16BE(leaseIndex, prefixLen);
115
+ Buffer.from(tosHash, 'hex').copy(uriBuf, prefixLen + 2, 0, halfToSLen);
116
+ uriBuf.writeBigInt64BE(XflHelpers.getXfl(leaseAmount.toString()), prefixLen + 2 + halfToSLen);
117
+ const uri = uriBuf.toString('hex').toUpperCase();
118
+
119
+ await this.xrplAcc.mintNft(uri, 0, 0, { isBurnable: true, isHexUri: true });
120
+
121
+ const nft = await this.xrplAcc.getNftByUri(uri, true);
122
+ if (!nft)
123
+ throw "Offer lease NFT creation error.";
124
+
125
+ await this.xrplAcc.offerSellNft(nft.NFTokenID,
126
+ leaseAmount.toString(),
127
+ EvernodeConstants.EVR,
128
+ this.config.evrIssuerAddress);
129
+ }
130
+
131
+ async expireLease(nfTokenId, tenantAddress = null) {
132
+ await this.xrplAcc.burnNft(nfTokenId, tenantAddress);
133
+ }
134
+
135
+ async register(countryCode, cpuMicroSec, ramMb, diskMb, totalInstanceCount, cpuModel, cpuCount, cpuSpeed, description, emailAddress, options = {}) {
136
+ if (!/^([A-Z]{2})$/.test(countryCode))
137
+ throw "countryCode should consist of 2 uppercase alphabetical characters";
138
+ else if (!cpuMicroSec || isNaN(cpuMicroSec) || cpuMicroSec % 1 != 0 || cpuMicroSec < 0)
139
+ throw "cpuMicroSec should be a positive integer";
140
+ else if (!ramMb || isNaN(ramMb) || ramMb % 1 != 0 || ramMb < 0)
141
+ throw "ramMb should be a positive integer";
142
+ else if (!diskMb || isNaN(diskMb) || diskMb % 1 != 0 || diskMb < 0)
143
+ throw "diskMb should be a positive integer";
144
+ else if (!totalInstanceCount || isNaN(totalInstanceCount) || totalInstanceCount % 1 != 0 || totalInstanceCount < 0)
145
+ throw "totalInstanceCount should be a positive intiger";
146
+ else if (!cpuCount || isNaN(cpuCount) || cpuCount % 1 != 0 || cpuCount < 0)
147
+ throw "CPU count should be a positive integer";
148
+ else if (!cpuSpeed || isNaN(cpuSpeed) || cpuSpeed % 1 != 0 || cpuSpeed < 0)
149
+ throw "CPU speed should be a positive integer";
150
+ else if (!cpuModel)
151
+ throw "cpu model cannot be empty";
152
+
153
+ // Need to use control characters inside this regex to match ascii characters.
154
+ // Here we allow all the characters in ascii range except ";" for the description.
155
+ // no-control-regex is enabled default by eslint:recommended, So we disable it only for next line.
156
+ // eslint-disable-next-line no-control-regex
157
+ else if (!/^((?![;])[\x00-\x7F]){0,26}$/.test(description))
158
+ throw "description should consist of 0-26 ascii characters except ';'";
159
+
160
+ else if (!emailAddress || !(/[a-z0-9]+@[a-z]+.[a-z]{2,3}/.test(emailAddress)) || (emailAddress.length > 40))
161
+ throw "Email address should be valid and can not have more than 40 characters.";
162
+
163
+ if (await this.isRegistered())
164
+ throw "Host already registered.";
165
+
166
+ // Check whether is there any missed NFT sell offer that needs to be accepted
167
+ // from the client-side in order to complete the registration.
168
+ const regNft = await this.getRegistrationNft();
169
+ if (!regNft) {
170
+ const regInfo = await this.getHostInfo(this.xrplAcc.address);
171
+ if (regInfo) {
172
+ const registryAcc = new XrplAccount(this.registryAddress, null, { xrplApi: this.xrplApi });
173
+ const sellOffer = (await registryAcc.getNftOffers()).find(o => o.NFTokenID == regInfo.nfTokenId);
174
+ if (sellOffer) {
175
+ await this.xrplAcc.buyNft(sellOffer.index);
176
+ console.log("Registration was successfully completed after acquiring the NFT.");
177
+ return await this.isRegistered();
178
+ }
179
+ }
180
+ }
181
+
182
+ // Check the availability of an initiated transfer.
183
+ // Need to modify the amount accordingly.
184
+ const stateTransfereeAddrKey = StateHelpers.generateTransfereeAddrStateKey(this.xrplAcc.address);
185
+ const stateTransfereeAddrIndex = StateHelpers.getHookStateIndex(this.registryAddress, stateTransfereeAddrKey);
186
+ let transfereeAddrLedgerEntry = {};
187
+ let transfereeAddrStateData = {};
188
+ let transferredNFTokenId = null;
189
+
190
+ try {
191
+ const res = await this.xrplApi.getLedgerEntry(stateTransfereeAddrIndex);
192
+ transfereeAddrLedgerEntry = { ...transfereeAddrLedgerEntry, ...res };
193
+ transfereeAddrStateData = transfereeAddrLedgerEntry?.HookStateData;
194
+ const transfereeAddrStateDecoded = StateHelpers.decodeTransfereeAddrState(Buffer.from(stateTransfereeAddrKey, 'hex'), Buffer.from(transfereeAddrStateData, 'hex'));
195
+ transferredNFTokenId = transfereeAddrStateDecoded?.transferredNfTokenId;
196
+
197
+ }
198
+ catch (e) {
199
+ console.log("No initiated transfers were found.");
200
+ }
201
+
202
+ // <country_code(2)><cpu_microsec(4)><ram_mb(4)><disk_mb(4)><no_of_total_instances(4)><cpu_model(40)><cpu_count(2)><cpu_speed(2)><description(26)><email_address(40)>
203
+ const memoBuf = Buffer.alloc(HOST_REG_MEMO_SIZE, 0);
204
+ Buffer.from(countryCode.substr(0, 2), "utf-8").copy(memoBuf, HOST_COUNTRY_CODE_MEMO_OFFSET);
205
+ memoBuf.writeUInt32BE(cpuMicroSec, HOST_CPU_MICROSEC_MEMO_OFFSET);
206
+ memoBuf.writeUInt32BE(ramMb, HOST_RAM_MB_MEMO_OFFSET);
207
+ memoBuf.writeUInt32BE(diskMb, HOST_DISK_MB_MEMO_OFFSET);
208
+ memoBuf.writeUInt32BE(totalInstanceCount, HOST_TOT_INS_COUNT_MEMO_OFFSET);
209
+ Buffer.from(cpuModel.substr(0, 40), "utf-8").copy(memoBuf, HOST_CPU_MODEL_NAME_MEMO_OFFSET);
210
+ memoBuf.writeUInt16BE(cpuCount, HOST_CPU_COUNT_MEMO_OFFSET);
211
+ memoBuf.writeUInt16BE(cpuSpeed, HOST_CPU_SPEED_MEMO_OFFSET);
212
+ Buffer.from(description.substr(0, 26), "utf-8").copy(memoBuf, HOST_DESCRIPTION_MEMO_OFFSET);
213
+ Buffer.from(emailAddress.substr(0, 40), "utf-8").copy(memoBuf, HOST_EMAIL_ADDRESS_MEMO_OFFSET);
214
+
215
+ const tx = await this.xrplAcc.makePayment(this.registryAddress,
216
+ (transferredNFTokenId) ? EvernodeConstants.NOW_IN_EVRS : this.config.hostRegFee.toString(),
217
+ EvernodeConstants.EVR,
218
+ this.config.evrIssuerAddress,
219
+ [{ type: MemoTypes.HOST_REG, format: MemoFormats.HEX, data: memoBuf.toString('hex') }],
220
+ options.transactionOptions);
221
+
222
+ console.log('Waiting for the sell offer')
223
+ const regAcc = new XrplAccount(this.registryAddress, null, { xrplApi: this.xrplApi });
224
+ let offer = null;
225
+ let attempts = 0;
226
+ let offerLedgerIndex = 0;
227
+ while (attempts < OFFER_WAIT_TIMEOUT) {
228
+ const nft = (await regAcc.getNfts()).find(n => (n.URI === `${EvernodeConstants.NFT_PREFIX_HEX}${tx.id}`) || (n.NFTokenID === transferredNFTokenId));
229
+ if (nft) {
230
+ offer = (await regAcc.getNftOffers()).find(o => o.Destination === this.xrplAcc.address && o.NFTokenID === nft.NFTokenID && o.Flags === 1);
231
+ offerLedgerIndex = this.xrplApi.ledgerIndex;
232
+ if (offer)
233
+ break;
234
+ await new Promise(resolve => setTimeout(resolve, 1000));
235
+ attempts++;
236
+ }
237
+
238
+ }
239
+ if (!offer)
240
+ throw 'No sell offer found within timeout.';
241
+
242
+ console.log('Accepting the sell offer..');
243
+
244
+ // Wait until the next ledger after the offer is created.
245
+ // Otherwise if the offer accepted in the same legder which it's been created,
246
+ // We cannot fetch the offer from registry contract event handler since it's getting deleted immediately.
247
+ await new Promise(async resolve => {
248
+ while (this.xrplApi.ledgerIndex <= offerLedgerIndex)
249
+ await new Promise(resolve2 => setTimeout(resolve2, 1000));
250
+ resolve();
251
+ });
252
+
253
+ await this.xrplAcc.buyNft(offer.index);
254
+
255
+ return await this.isRegistered();
256
+ }
257
+
258
+ async deregister(options = {}) {
259
+
260
+ if (!(await this.isRegistered()))
261
+ throw "Host not registered."
262
+
263
+ const regNFT = await this.getRegistrationNft();
264
+
265
+ // To obtain registration NFT Page Keylet and index.
266
+ const nftPageDataBuf = await EvernodeHelpers.getNFTPageAndLocation(regNFT.NFTokenID, this.xrplAcc, this.xrplApi);
267
+
268
+ await this.xrplAcc.makePayment(this.registryAddress,
269
+ XrplConstants.MIN_XRP_AMOUNT,
270
+ XrplConstants.XRP,
271
+ null,
272
+ [
273
+ { type: MemoTypes.HOST_DEREG, format: MemoFormats.HEX, data: regNFT.NFTokenID },
274
+ { type: MemoTypes.HOST_REGISTRY_REF, format: MemoFormats.HEX, data: nftPageDataBuf.toString('hex') }
275
+ ],
276
+ options.transactionOptions);
277
+
278
+ console.log('Waiting for the buy offer')
279
+ const regAcc = new XrplAccount(this.registryAddress, null, { xrplApi: this.xrplApi });
280
+ let offer = null;
281
+ let attempts = 0;
282
+ let offerLedgerIndex = 0;
283
+ while (attempts < OFFER_WAIT_TIMEOUT) {
284
+ offer = (await regAcc.getNftOffers()).find(o => (o.NFTokenID == regNFT.NFTokenID) && (o.Flags === 0));
285
+ offerLedgerIndex = this.xrplApi.ledgerIndex;
286
+ if (offer)
287
+ break;
288
+ await new Promise(resolve => setTimeout(resolve, 1000));
289
+ attempts++;
290
+ }
291
+ if (!offer)
292
+ throw 'No buy offer found within timeout.';
293
+
294
+ console.log('Accepting the buy offer..');
295
+
296
+ // Wait until the next ledger after the offer is created.
297
+ // Otherwise if the offer accepted in the same legder which it's been created,
298
+ // We cannot fetch the offer from registry contract event handler since it's getting deleted immediately.
299
+ await new Promise(async resolve => {
300
+ while (this.xrplApi.ledgerIndex <= offerLedgerIndex)
301
+ await new Promise(resolve2 => setTimeout(resolve2, 1000));
302
+ resolve();
303
+ });
304
+
305
+ await this.xrplAcc.sellNft(
306
+ offer.index,
307
+ [{ type: MemoTypes.HOST_POST_DEREG, format: MemoFormats.HEX, data: regNFT.NFTokenID }]
308
+ );
309
+
310
+ return await this.isRegistered();
311
+ }
312
+
313
+ async updateRegInfo(activeInstanceCount = null, version = null, totalInstanceCount = null, tokenID = null, countryCode = null, cpuMicroSec = null, ramMb = null, diskMb = null, description = null, options = {}) {
314
+ // <token_id(32)><country_code(2)><cpu_microsec(4)><ram_mb(4)><disk_mb(4)><total_instance_count(4)><active_instances(4)><description(26)><version(3)>
315
+ const memoBuf = Buffer.alloc(HOST_UPDATE_MEMO_SIZE, 0);
316
+ if (tokenID)
317
+ Buffer.from(tokenID.substr(0, 32), "hex").copy(memoBuf, HOST_UPDATE_TOKEN_ID_MEMO_OFFSET);
318
+ if (countryCode)
319
+ Buffer.from(countryCode.substr(0, 2), "utf-8").copy(memoBuf, HOST_UPDATE_COUNTRY_CODE_MEMO_OFFSET);
320
+ if (cpuMicroSec)
321
+ memoBuf.writeUInt32BE(cpuMicroSec, HOST_UPDATE_CPU_MICROSEC_MEMO_OFFSET);
322
+ if (ramMb)
323
+ memoBuf.writeUInt32BE(ramMb, HOST_UPDATE_RAM_MB_MEMO_OFFSET);
324
+ if (diskMb)
325
+ memoBuf.writeUInt32BE(diskMb, HOST_UPDATE_DISK_MB_MEMO_OFFSET);
326
+ if (totalInstanceCount)
327
+ memoBuf.writeUInt32BE(totalInstanceCount, HOST_UPDATE_TOT_INS_COUNT_MEMO_OFFSET);
328
+ if (activeInstanceCount)
329
+ memoBuf.writeUInt32BE(activeInstanceCount, HOST_UPDATE_ACT_INS_COUNT_MEMO_OFFSET);
330
+ if (description)
331
+ Buffer.from(description.substr(0, 26), "utf-8").copy(memoBuf, HOST_UPDATE_DESCRIPTION_MEMO_OFFSET);
332
+ if (version) {
333
+ const components = version.split('.').map(v => parseInt(v));
334
+ if (components.length != 3)
335
+ throw 'Invalid version format.';
336
+ memoBuf.writeUInt8(components[0], HOST_UPDATE_VERSION_MEMO_OFFSET);
337
+ memoBuf.writeUInt8(components[1], HOST_UPDATE_VERSION_MEMO_OFFSET + 1);
338
+ memoBuf.writeUInt8(components[2], HOST_UPDATE_VERSION_MEMO_OFFSET + 2);
339
+ }
340
+
341
+ // To obtain registration NFT Page Keylet and index.
342
+ if (!tokenID)
343
+ tokenID = (await this.getRegistrationNft()).NFTokenID;
344
+ const nftPageDataBuf = await EvernodeHelpers.getNFTPageAndLocation(tokenID, this.xrplAcc, this.xrplApi);
345
+
346
+ return await this.xrplAcc.makePayment(this.registryAddress,
347
+ XrplConstants.MIN_XRP_AMOUNT,
348
+ XrplConstants.XRP,
349
+ null,
350
+ [
351
+ { type: MemoTypes.HOST_UPDATE_INFO, format: MemoFormats.HEX, data: memoBuf.toString('hex') },
352
+ { type: MemoTypes.HOST_REGISTRY_REF, format: MemoFormats.HEX, data: nftPageDataBuf.toString('hex') }
353
+ ],
354
+ options.transactionOptions);
355
+ }
356
+
357
+ async heartbeat(options = {}) {
358
+ // To obtain registration NFT Page Keylet and index.
359
+ const regNFT = await this.getRegistrationNft();
360
+ const nftPageDataBuf = await EvernodeHelpers.getNFTPageAndLocation(regNFT.NFTokenID, this.xrplAcc, this.xrplApi);
361
+
362
+ return this.xrplAcc.makePayment(this.registryAddress,
363
+ XrplConstants.MIN_XRP_AMOUNT,
364
+ XrplConstants.XRP,
365
+ null,
366
+ [
367
+ { type: MemoTypes.HEARTBEAT, format: "", data: "" },
368
+ { type: MemoTypes.HOST_REGISTRY_REF, format: MemoFormats.HEX, data: nftPageDataBuf.toString('hex') }
369
+ ],
370
+ options.transactionOptions);
371
+ }
372
+
373
+ async acquireSuccess(txHash, tenantAddress, instanceInfo, options = {}) {
374
+
375
+ // Encrypt the instance info with the tenant's encryption key (Specified in MessageKey field of the tenant account).
376
+ const tenantAcc = new XrplAccount(tenantAddress, null, { xrplApi: this.xrplApi });
377
+ const encKey = await tenantAcc.getMessageKey();
378
+ if (!encKey)
379
+ throw "Tenant encryption key not set.";
380
+
381
+ const encrypted = await EncryptionHelper.encrypt(encKey, instanceInfo);
382
+ const memos = [
383
+ { type: MemoTypes.ACQUIRE_SUCCESS, format: MemoFormats.BASE64, data: encrypted },
384
+ { type: MemoTypes.ACQUIRE_REF, format: MemoFormats.HEX, data: txHash }];
385
+
386
+ return this.xrplAcc.makePayment(tenantAddress,
387
+ XrplConstants.MIN_XRP_AMOUNT,
388
+ XrplConstants.XRP,
389
+ null,
390
+ memos,
391
+ options.transactionOptions);
392
+ }
393
+
394
+ async acquireError(txHash, tenantAddress, leaseAmount, reason, options = {}) {
395
+
396
+ const memos = [
397
+ { type: MemoTypes.ACQUIRE_ERROR, format: MemoFormats.JSON, data: { type: ErrorCodes.ACQUIRE_ERR, reason: reason } },
398
+ { type: MemoTypes.ACQUIRE_REF, format: MemoFormats.HEX, data: txHash }];
399
+
400
+ return this.xrplAcc.makePayment(tenantAddress,
401
+ leaseAmount.toString(),
402
+ EvernodeConstants.EVR,
403
+ this.config.evrIssuerAddress,
404
+ memos,
405
+ options.transactionOptions);
406
+ }
407
+
408
+ async extendSuccess(txHash, tenantAddress, expiryMoment, options = {}) {
409
+ let buf = Buffer.allocUnsafe(4);
410
+ buf.writeUInt32BE(expiryMoment);
411
+
412
+ const memos = [
413
+ { type: MemoTypes.EXTEND_SUCCESS, format: MemoFormats.HEX, data: buf.toString('hex') },
414
+ { type: MemoTypes.EXTEND_REF, format: MemoFormats.HEX, data: txHash }];
415
+
416
+ return this.xrplAcc.makePayment(tenantAddress,
417
+ XrplConstants.MIN_XRP_AMOUNT,
418
+ XrplConstants.XRP,
419
+ null,
420
+ memos,
421
+ options.transactionOptions);
422
+ }
423
+
424
+ async extendError(txHash, tenantAddress, reason, refund, options = {}) {
425
+
426
+ const memos = [
427
+ { type: MemoTypes.EXTEND_ERROR, format: MemoFormats.JSON, data: { type: ErrorCodes.EXTEND_ERR, reason: reason } },
428
+ { type: MemoTypes.EXTEND_REF, format: MemoFormats.HEX, data: txHash }];
429
+
430
+ // Required to refund the paid EVR amount as the offer extention is not successfull.
431
+ return this.xrplAcc.makePayment(tenantAddress,
432
+ refund.toString(),
433
+ EvernodeConstants.EVR,
434
+ this.config.evrIssuerAddress,
435
+ memos,
436
+ options.transactionOptions);
437
+ }
438
+
439
+ async refundTenant(txHash, tenantAddress, refundAmount, options = {}) {
440
+ const memos = [
441
+ { type: MemoTypes.REFUND, format: '', data: '' },
442
+ { type: MemoTypes.REFUND_REF, format: MemoFormats.HEX, data: txHash }];
443
+
444
+ return this.xrplAcc.makePayment(tenantAddress,
445
+ refundAmount.toString(),
446
+ EvernodeConstants.EVR,
447
+ this.config.evrIssuerAddress,
448
+ memos,
449
+ options.transactionOptions);
450
+ }
451
+
452
+ async requestRebate(options = {}) {
453
+
454
+ // To obtain registration NFT Page Keylet and index.
455
+ const regNFT = await this.getRegistrationNft();
456
+ const nftPageDataBuf = await EvernodeHelpers.getNFTPageAndLocation(regNFT.NFTokenID, this.xrplAcc, this.xrplApi);
457
+
458
+ return this.xrplAcc.makePayment(this.registryAddress,
459
+ XrplConstants.MIN_XRP_AMOUNT,
460
+ XrplConstants.XRP,
461
+ null,
462
+ [
463
+ { type: MemoTypes.HOST_REBATE, format: "", data: "" },
464
+ { type: MemoTypes.HOST_REGISTRY_REF, format: MemoFormats.HEX, data: nftPageDataBuf.toString('hex') }
465
+ ],
466
+ options.transactionOptions);
467
+ }
468
+
469
+ getLeaseNFTokenIdPrefix() {
470
+ let buf = Buffer.allocUnsafe(24);
471
+ buf.writeUInt16BE(1);
472
+ buf.writeUInt16BE(0, 2);
473
+ codec.decodeAccountID(this.xrplAcc.address).copy(buf, 4);
474
+ return buf.toString('hex');
475
+ }
476
+
477
+ async transfer(transfereeAddress = this.xrplAcc.address, options = {}) {
478
+ if (!(await this.isRegistered()))
479
+ throw "Host is not registered.";
480
+
481
+ const transfereeAcc = new XrplAccount(transfereeAddress, null, { xrplApi: this.xrplApi });
482
+
483
+ if (this.xrplAcc.address !== transfereeAddress) {
484
+ // Find the new transferee also owns an Evernode Host Registration NFT.
485
+ const nft = (await transfereeAcc.getNfts()).find(n => n.URI.startsWith(EvernodeConstants.NFT_PREFIX_HEX) && n.Issuer === this.registryAddress);
486
+ if (nft) {
487
+ // Check whether the token was actually issued from Evernode registry contract.
488
+ const issuerHex = nft.NFTokenID.substr(8, 40);
489
+ const issuerAddr = codec.encodeAccountID(Buffer.from(issuerHex, 'hex'));
490
+ if (issuerAddr == this.registryAddress) {
491
+ throw "The transferee is already registered in Evernode.";
492
+ }
493
+ }
494
+ }
495
+
496
+ let memoData = Buffer.allocUnsafe(20);
497
+ codec.decodeAccountID(transfereeAddress).copy(memoData);
498
+
499
+ // To obtain registration NFT Page Keylet and index.
500
+ const regNFT = await this.getRegistrationNft();
501
+ const nftPageDataBuf = await EvernodeHelpers.getNFTPageAndLocation(regNFT.NFTokenID, this.xrplAcc, this.xrplApi);
502
+
503
+ await this.xrplAcc.makePayment(this.registryAddress,
504
+ XrplConstants.MIN_XRP_AMOUNT,
505
+ XrplConstants.XRP,
506
+ null,
507
+ [
508
+ { type: MemoTypes.HOST_TRANSFER, format: MemoFormats.HEX, data: memoData.toString('hex') },
509
+ { type: MemoTypes.HOST_REGISTRY_REF, format: MemoFormats.HEX, data: nftPageDataBuf.toString('hex') }
510
+ ],
511
+ options.transactionOptions);
512
+
513
+ let offer = null;
514
+ let attempts = 0;
515
+ let offerLedgerIndex = 0;
516
+ const regAcc = new XrplAccount(this.registryAddress, null, { xrplApi: this.xrplApi });
517
+
518
+ while (attempts < OFFER_WAIT_TIMEOUT) {
519
+ offer = (await regAcc.getNftOffers()).find(o => (o.NFTokenID == regNFT.NFTokenID) && (o.Flags === 0));
520
+ offerLedgerIndex = this.xrplApi.ledgerIndex;
521
+ if (offer)
522
+ break;
523
+ await new Promise(resolve => setTimeout(resolve, 1000));
524
+ attempts++;
525
+ }
526
+ if (!offer)
527
+ throw 'No buy offer found within timeout.';
528
+
529
+ console.log('Accepting the buy offer..');
530
+
531
+ // Wait until the next ledger after the offer is created.
532
+ // Otherwise if the offer accepted in the same legder which it's been created,
533
+ // We cannot fetch the offer from registry contract event handler since it's getting deleted immediately.
534
+ await new Promise(async resolve => {
535
+ while (this.xrplApi.ledgerIndex <= offerLedgerIndex)
536
+ await new Promise(resolve2 => setTimeout(resolve2, 1000));
537
+ resolve();
538
+ });
539
+
540
+ await this.xrplAcc.sellNft(offer.index);
541
+ }
542
+
543
+ async isTransferee() {
544
+
545
+ // Check the availability of TRANSFEREE state for this host address.
546
+ const stateTransfereeAddrKey = StateHelpers.generateTransfereeAddrStateKey(this.xrplAcc.address);
547
+ const stateTransfereeAddrIndex = StateHelpers.getHookStateIndex(this.registryAddress, stateTransfereeAddrKey);
548
+ const res = await this.xrplApi.getLedgerEntry(stateTransfereeAddrIndex);
549
+
550
+ if (res && res?.HookStateData)
551
+ return true;
552
+
553
+ return false;
554
+ }
555
+ }
556
+
557
+ module.exports = {
558
+ HostEvents,
559
+ HostClient
560
+ }
@@ -0,0 +1,54 @@
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
+ HostTransfer: EvernodeEvents.HostTransfer,
14
+ HostRebate: EvernodeEvents.HostRebate
15
+ }
16
+
17
+ class RegistryClient extends BaseEvernodeClient {
18
+
19
+ /**
20
+ * Constructs a registry client instance.
21
+ * @param {object} options [Optional] An object with 'rippledServer' URL and 'registryAddress'.
22
+ */
23
+ constructor(options = {}) {
24
+ super((options.registryAddress || DefaultValues.registryAddress), null, Object.values(RegistryEvents), false, options);
25
+ }
26
+
27
+ /**
28
+ * Gets all the active hosts registered in Evernode without paginating.
29
+ * @returns The list of active hosts.
30
+ */
31
+ async getActiveHosts() {
32
+ let fullHostList = [];
33
+ const hosts = await this.getHosts();
34
+ if (hosts.nextPageToken) {
35
+ let currentPageToken = hosts.nextPageToken;
36
+ let nextHosts = null;
37
+ fullHostList = fullHostList.concat(hosts.data);
38
+ while (currentPageToken) {
39
+ nextHosts = await this.getHosts(null, null, currentPageToken);
40
+ fullHostList = fullHostList.concat(nextHosts.nextPageToken ? nextHosts.data : nextHosts);
41
+ currentPageToken = nextHosts.nextPageToken;
42
+ }
43
+ } else {
44
+ fullHostList = fullHostList.concat(hosts);
45
+ }
46
+ // Filter only active hosts.
47
+ return fullHostList.filter(h => h.active);
48
+ }
49
+ }
50
+
51
+ module.exports = {
52
+ RegistryEvents,
53
+ RegistryClient
54
+ }