evernode-js-client 0.4.53 → 0.5.0
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.
- package/.eslintrc.json +14 -0
- package/LICENSE +21 -0
- package/README.md +26 -0
- package/clean-pkg.sh +4 -0
- package/npm-readme.md +4 -0
- package/package.json +16 -1
- package/remove-versions.sh +10 -0
- package/src/clients/base-evernode-client.js +567 -0
- package/src/clients/host-client.js +357 -0
- package/src/clients/registry-client.js +52 -0
- package/src/clients/tenant-client.js +264 -0
- package/src/defaults.js +21 -0
- package/src/eccrypto.js +258 -0
- package/src/encryption-helper.js +41 -0
- package/src/event-emitter.js +45 -0
- package/src/evernode-common.js +103 -0
- package/src/evernode-helpers.js +14 -0
- package/src/firestore/firestore-handler.js +309 -0
- package/src/index.js +37 -0
- package/src/state-helpers.js +283 -0
- package/src/transaction-helper.js +62 -0
- package/src/util-helpers.js +48 -0
- package/src/xfl-helpers.js +130 -0
- package/src/xrpl-account.js +473 -0
- package/src/xrpl-api.js +275 -0
- package/src/xrpl-common.js +17 -0
- package/test/package-lock.json +884 -0
- package/test/package.json +9 -0
- package/test/test.js +379 -0
- package/index.js +0 -15166
@@ -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
|
+
}
|