evernode-js-client 0.5.10 → 0.5.11
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 +25 -3
- package/clean-pkg.sh +4 -0
- package/npm-readme.md +4 -0
- package/package.json +10 -1
- package/remove-versions.sh +10 -0
- package/src/clients/base-evernode-client.js +609 -0
- package/src/clients/host-client.js +560 -0
- package/src/clients/registry-client.js +54 -0
- package/src/clients/tenant-client.js +276 -0
- package/src/defaults.js +21 -0
- package/src/eccrypto.js +258 -0
- package/src/encryption-helper.js +96 -0
- package/src/event-emitter.js +45 -0
- package/src/evernode-common.js +113 -0
- package/src/evernode-helpers.js +45 -0
- package/src/firestore/firestore-handler.js +309 -0
- package/src/index.js +37 -0
- package/src/state-helpers.js +396 -0
- package/src/transaction-helper.js +62 -0
- package/src/util-helpers.js +50 -0
- package/src/xfl-helpers.js +130 -0
- package/src/xrpl-account.js +515 -0
- package/src/xrpl-api.js +301 -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 +409 -0
- package/index.js +0 -15876
@@ -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
|
+
}
|