evernode-js-client 0.5.12 → 0.5.13

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ }