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.
- 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 -15129
@@ -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
|
+
}
|