@eluvio/elv-client-js 3.2.4 → 3.2.5
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/dist/ElvClient-min.js +15 -11
- package/dist/ElvClient-node-min.js +17 -13
- package/dist/ElvFrameClient-min.js +12 -8
- package/dist/ElvPermissionsClient-min.js +13 -9
- package/dist/ElvWalletClient-min.js +58 -0
- package/dist/ElvWalletClient-node-min.js +78 -0
- package/dist/src/AuthorizationClient.js +2248 -1990
- package/dist/src/ContentObjectVerification.js +164 -173
- package/dist/src/Crypto.js +376 -324
- package/dist/src/ElvClient.js +1185 -1019
- package/dist/src/ElvWallet.js +119 -95
- package/dist/src/EthClient.js +1040 -896
- package/dist/src/FrameClient.js +331 -300
- package/dist/src/HttpClient.js +153 -147
- package/dist/src/Id.js +1 -3
- package/dist/src/PermissionsClient.js +1294 -1168
- package/dist/src/RemoteSigner.js +263 -211
- package/dist/src/UserProfileClient.js +1164 -1023
- package/dist/src/Utils.js +229 -184
- package/dist/src/client/ABRPublishing.js +895 -858
- package/dist/src/client/AccessGroups.js +1102 -959
- package/dist/src/client/ContentAccess.js +3724 -3431
- package/dist/src/client/ContentManagement.js +2252 -2068
- package/dist/src/client/Contracts.js +647 -563
- package/dist/src/client/Files.js +1886 -1757
- package/dist/src/client/NFT.js +126 -112
- package/dist/src/client/NTP.js +478 -422
- package/dist/src/index.js +11 -0
- package/dist/src/marketplaceClient/ClientMethods.js +1918 -0
- package/dist/src/marketplaceClient/Configuration.js +29 -0
- package/dist/src/marketplaceClient/Utils.js +304 -0
- package/dist/src/marketplaceClient/index.js +1553 -0
- package/dist/src/walletClient/ClientMethods.js +1978 -0
- package/dist/src/walletClient/Configuration.js +29 -0
- package/dist/src/walletClient/Utils.js +304 -0
- package/dist/src/walletClient/index.js +1571 -0
- package/package.json +8 -4
- package/src/ElvClient.js +4 -1
- package/src/Utils.js +22 -4
- package/src/index.js +7 -0
- package/src/walletClient/ClientMethods.js +1048 -0
- package/src/walletClient/Configuration.js +40 -0
- package/src/walletClient/README.md +185 -0
- package/src/walletClient/Utils.js +234 -0
- package/src/walletClient/index.js +884 -0
- package/testScripts/TestMarketplaceClient.js +25 -0
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
const {ElvClient} = require("../ElvClient");
|
|
2
|
+
const Configuration = require("./Configuration");
|
|
3
|
+
const {LinkTargetHash, FormatNFT, ActionPopup} = require("./Utils");
|
|
4
|
+
const UrlJoin = require("url-join");
|
|
5
|
+
const Utils = require("../Utils");
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Use the <a href="#.Initialize">Initialize</a> method to initialize a new client.
|
|
10
|
+
*
|
|
11
|
+
*
|
|
12
|
+
* See the Modules section on the sidebar for all client methods unrelated to login and authorization
|
|
13
|
+
*/
|
|
14
|
+
class ElvWalletClient {
|
|
15
|
+
constructor({client, network, mode, marketplaceInfo, storeAuthToken}) {
|
|
16
|
+
this.client = client;
|
|
17
|
+
this.loggedIn = false;
|
|
18
|
+
|
|
19
|
+
this.network = network;
|
|
20
|
+
this.mode = mode;
|
|
21
|
+
this.purchaseMode = Configuration[network][mode].purchaseMode;
|
|
22
|
+
this.mainSiteId = Configuration[network][mode].siteId;
|
|
23
|
+
this.appUrl = Configuration[network][mode].appUrl;
|
|
24
|
+
this.publicStaticToken = client.staticToken;
|
|
25
|
+
this.storeAuthToken = storeAuthToken;
|
|
26
|
+
|
|
27
|
+
this.selectedMarketplaceInfo = marketplaceInfo;
|
|
28
|
+
|
|
29
|
+
this.availableMarketplaces = {};
|
|
30
|
+
this.availableMarketplacesById = {};
|
|
31
|
+
this.marketplaceHashes = {};
|
|
32
|
+
|
|
33
|
+
// Caches
|
|
34
|
+
this.cachedMarketplaces = {};
|
|
35
|
+
this.cachedCSS = {};
|
|
36
|
+
|
|
37
|
+
this.utils = client.utils;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
Log(message, error=false) {
|
|
41
|
+
if(error) {
|
|
42
|
+
// eslint-disable-next-line no-console
|
|
43
|
+
console.error("Eluvio Wallet Client:", message);
|
|
44
|
+
} else {
|
|
45
|
+
// eslint-disable-next-line no-console
|
|
46
|
+
console.log("Eluvio Wallet Client:", message);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Initialize the wallet client.
|
|
52
|
+
*
|
|
53
|
+
* Specify tenantSlug and marketplaceSlug to automatically associate this tenant with a particular marketplace.
|
|
54
|
+
*
|
|
55
|
+
* @methodGroup Initialization
|
|
56
|
+
* @namedParams
|
|
57
|
+
* @param {string} network=main - Name of the Fabric network to use (`main`, `demo`)
|
|
58
|
+
* @param {string} mode=production - Environment to use (`production`, `staging`)
|
|
59
|
+
* @param {Object=} marketplaceParams - Marketplace parameters
|
|
60
|
+
* @param {boolean=} storeAuthToken=true - If specified, auth tokens will be stored in localstorage (if available)
|
|
61
|
+
*
|
|
62
|
+
* @returns {Promise<ElvWalletClient>}
|
|
63
|
+
*/
|
|
64
|
+
static async Initialize({
|
|
65
|
+
network="main",
|
|
66
|
+
mode="production",
|
|
67
|
+
marketplaceParams,
|
|
68
|
+
storeAuthToken=true
|
|
69
|
+
}) {
|
|
70
|
+
let { tenantSlug, marketplaceSlug, marketplaceId, marketplaceHash } = (marketplaceParams || {});
|
|
71
|
+
|
|
72
|
+
if(!Configuration[network]) {
|
|
73
|
+
throw Error(`ElvWalletClient: Invalid network ${network}`);
|
|
74
|
+
} else if(!Configuration[network][mode]) {
|
|
75
|
+
throw Error(`ElvWalletClient: Invalid mode ${mode}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const client = await ElvClient.FromNetworkName({networkName: network, assumeV3: true});
|
|
79
|
+
|
|
80
|
+
const walletClient = new ElvWalletClient({
|
|
81
|
+
client,
|
|
82
|
+
network,
|
|
83
|
+
mode,
|
|
84
|
+
marketplaceInfo: {
|
|
85
|
+
tenantSlug,
|
|
86
|
+
marketplaceSlug,
|
|
87
|
+
marketplaceId: marketplaceHash ? client.utils.DecodeVersionHash(marketplaceHash).objectId : marketplaceId,
|
|
88
|
+
marketplaceHash
|
|
89
|
+
},
|
|
90
|
+
storeAuthToken
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if(window && window.location && window.location.href) {
|
|
94
|
+
let url = new URL(window.location.href);
|
|
95
|
+
if(url.searchParams.get("elvToken")) {
|
|
96
|
+
await walletClient.Authenticate({token: url.searchParams.get("elvToken")});
|
|
97
|
+
|
|
98
|
+
url.searchParams.delete("elvToken");
|
|
99
|
+
|
|
100
|
+
window.history.replaceState("", "", url);
|
|
101
|
+
} else if(storeAuthToken && typeof localStorage !== "undefined") {
|
|
102
|
+
try {
|
|
103
|
+
// Load saved auth token
|
|
104
|
+
let savedToken = localStorage.getItem(`__elv-token-${network}`);
|
|
105
|
+
if(savedToken) {
|
|
106
|
+
await walletClient.Authenticate({token: savedToken});
|
|
107
|
+
}
|
|
108
|
+
// eslint-disable-next-line no-empty
|
|
109
|
+
} catch(error) {}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await walletClient.LoadAvailableMarketplaces();
|
|
114
|
+
|
|
115
|
+
return walletClient;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Login and authorization */
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Direct the user to the Eluvio Media Wallet login page.
|
|
122
|
+
*
|
|
123
|
+
* <b>NOTE:</b> The domain of the opening window (popup flow) or domain of the `callbackUrl` (redirect flow) MUST be allowed in the metadata of the specified marketplace.
|
|
124
|
+
*
|
|
125
|
+
* @methodGroup Login
|
|
126
|
+
* @namedParams
|
|
127
|
+
* @param {string=} method=redirect - How to present the login page.
|
|
128
|
+
* - `redirect` - Redirect to the wallet login page. Upon login, the page will be redirected back to the specified `redirectUrl` with the authorization token.
|
|
129
|
+
* - `popup` - Open the wallet login page in a new tab. Upon login, authorization information will be sent back to the client via message and the tab will be closed.
|
|
130
|
+
* @param {string=} provider - If logging in via a specific method, specify the provider and mode. Options: `oauth`, `metamask`
|
|
131
|
+
* @param {string=} mode - If logging in via a specific method, specify the mode. Options `login` (Log In), `create` (Sign Up)
|
|
132
|
+
* @param {string=} callbackUrl - If using the redirect flow, the URL to redirect back to after login.
|
|
133
|
+
* @param {Object=} marketplaceParams - Parameters of a marketplace to associate the login with. If not specified, the marketplace parameters used upon client initialization will be used. A marketplace is required when using the redirect flow.
|
|
134
|
+
* @param {boolean=} clearLogin=false - If specified, the user will be prompted to log in anew even if they are already logged in on the Eluvio Media Wallet app
|
|
135
|
+
*
|
|
136
|
+
* @throws - If using the popup flow and the user closes the popup, this method will throw an error.
|
|
137
|
+
*/
|
|
138
|
+
async LogIn({
|
|
139
|
+
method="redirect",
|
|
140
|
+
provider,
|
|
141
|
+
mode="login",
|
|
142
|
+
callbackUrl,
|
|
143
|
+
marketplaceParams,
|
|
144
|
+
clearLogin=false,
|
|
145
|
+
callback
|
|
146
|
+
}) {
|
|
147
|
+
let loginUrl = new URL(this.appUrl);
|
|
148
|
+
loginUrl.hash = "/login";
|
|
149
|
+
|
|
150
|
+
loginUrl.searchParams.set("origin", window.location.origin);
|
|
151
|
+
loginUrl.searchParams.set("action", "login");
|
|
152
|
+
|
|
153
|
+
if(provider) {
|
|
154
|
+
loginUrl.searchParams.set("provider", provider);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if(mode) {
|
|
158
|
+
loginUrl.searchParams.set("mode", mode);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if(marketplaceParams) {
|
|
162
|
+
loginUrl.searchParams.set("mid", (await this.MarketplaceInfo({marketplaceParams})).marketplaceHash);
|
|
163
|
+
} else if((this.selectedMarketplaceInfo || {}).marketplaceHash) {
|
|
164
|
+
loginUrl.searchParams.set("mid", this.selectedMarketplaceInfo.marketplaceHash);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if(clearLogin) {
|
|
168
|
+
loginUrl.searchParams.set("clear", "");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if(method === "redirect") {
|
|
172
|
+
loginUrl.searchParams.set("response", "redirect");
|
|
173
|
+
loginUrl.searchParams.set("source", "origin");
|
|
174
|
+
loginUrl.searchParams.set("redirect", callbackUrl);
|
|
175
|
+
|
|
176
|
+
window.location = loginUrl;
|
|
177
|
+
} else {
|
|
178
|
+
loginUrl.searchParams.set("response", "message");
|
|
179
|
+
loginUrl.searchParams.set("source", "parent");
|
|
180
|
+
|
|
181
|
+
await new Promise(async (resolve, reject) => {
|
|
182
|
+
await ActionPopup({
|
|
183
|
+
mode: "tab",
|
|
184
|
+
url: loginUrl.toString(),
|
|
185
|
+
onCancel: () => reject("User cancelled login"),
|
|
186
|
+
onMessage: async (event, Close) => {
|
|
187
|
+
if(!event || !event.data || event.data.type !== "LoginResponse") {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
if(callback) {
|
|
193
|
+
await callback(event.data.params);
|
|
194
|
+
} else {
|
|
195
|
+
await this.Authenticate({token: event.data.params.clientSigningToken || event.data.params.clientAuthToken});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
resolve();
|
|
199
|
+
} catch(error) {
|
|
200
|
+
reject(error);
|
|
201
|
+
} finally {
|
|
202
|
+
Close();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Remove authorization for the current user.
|
|
212
|
+
*
|
|
213
|
+
* @methodGroup Login
|
|
214
|
+
*/
|
|
215
|
+
LogOut() {
|
|
216
|
+
this.__authorization = {};
|
|
217
|
+
this.loggedIn = false;
|
|
218
|
+
|
|
219
|
+
this.cachedMarketplaces = {};
|
|
220
|
+
|
|
221
|
+
// Delete saved auth token
|
|
222
|
+
if(typeof localStorage !== "undefined") {
|
|
223
|
+
try {
|
|
224
|
+
localStorage.removeItem(`__elv-token-${this.network}`);
|
|
225
|
+
// eslint-disable-next-line no-empty
|
|
226
|
+
} catch(error) {}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Authenticate with an ElvWalletClient authorization token
|
|
232
|
+
*
|
|
233
|
+
* @methodGroup Authorization
|
|
234
|
+
* @namedParams
|
|
235
|
+
* @param {string} token - A previously generated ElvWalletClient authorization token;
|
|
236
|
+
*/
|
|
237
|
+
async Authenticate({token}) {
|
|
238
|
+
let decodedToken;
|
|
239
|
+
try {
|
|
240
|
+
decodedToken = JSON.parse(this.utils.FromB58ToStr(token)) || {};
|
|
241
|
+
} catch(error) {
|
|
242
|
+
throw new Error("Invalid authorization token " + token);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if(!decodedToken.expiresAt || Date.now() > decodedToken.expiresAt) {
|
|
246
|
+
throw Error("ElvWalletClient: Provided authorization token has expired");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
this.client.SetStaticToken({token: decodedToken.fabricToken});
|
|
250
|
+
|
|
251
|
+
if(decodedToken.clusterToken) {
|
|
252
|
+
this.client.SetRemoteSigner({authToken: decodedToken.clusterToken});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return this.SetAuthorization(decodedToken);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Authenticate with an OAuth ID token
|
|
260
|
+
*
|
|
261
|
+
* @methodGroup Authorization
|
|
262
|
+
* @namedParams
|
|
263
|
+
* @param {string} idToken - An OAuth ID token
|
|
264
|
+
* @param {string=} tenantId - ID of tenant with which to associate the user. If marketplace info was set upon initialization, this will be determined automatically.
|
|
265
|
+
* @param {string=} email - Email address of the user. If not specified, this method will attempt to extract the email from the ID token.
|
|
266
|
+
* @param {boolean=} shareEmail=false - Whether or not the user consents to sharing their email
|
|
267
|
+
* @param {number=} tokenDuration=24 - Number of hours the generated authorization token will last before expiring
|
|
268
|
+
*
|
|
269
|
+
* @returns {Promise<Object>} - Returns an authorization tokens that can be used to initialize the client using <a href="#Authenticate">Authenticate</a>.
|
|
270
|
+
* Save this token to avoid having to reauthenticate with OAuth. This token expires after 24 hours.
|
|
271
|
+
*
|
|
272
|
+
* The result includes two tokens:
|
|
273
|
+
* - token - Standard client auth token used to access content and perform actions on behalf of the user.
|
|
274
|
+
* - signingToken - Identical to `authToken`, but also includes the ability to perform arbitrary signatures with the custodial wallet. This token should be protected and should not be
|
|
275
|
+
* shared with third parties.
|
|
276
|
+
*/
|
|
277
|
+
async AuthenticateOAuth({idToken, tenantId, email, shareEmail=false, tokenDuration=24}) {
|
|
278
|
+
if(!tenantId && this.selectedMarketplaceInfo) {
|
|
279
|
+
// Load tenant ID automatically from selected marketplace
|
|
280
|
+
await this.AvailableMarketplaces();
|
|
281
|
+
tenantId = this.selectedMarketplaceInfo.tenantId;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
await this.client.SetRemoteSigner({idToken, tenantId, extraData: { share_email: shareEmail }, unsignedPublicAuth: true});
|
|
285
|
+
|
|
286
|
+
const expiresAt = Date.now() + tokenDuration * 60 * 60 * 1000;
|
|
287
|
+
const fabricToken = await this.client.CreateFabricToken({duration: tokenDuration * 60 * 60 * 1000});
|
|
288
|
+
const address = this.client.utils.FormatAddress(this.client.CurrentAccountAddress());
|
|
289
|
+
|
|
290
|
+
if(!email) {
|
|
291
|
+
try {
|
|
292
|
+
const decodedToken = JSON.parse(this.utils.FromB64URL(idToken.split(".")[1]));
|
|
293
|
+
email = decodedToken.email;
|
|
294
|
+
} catch(error) {
|
|
295
|
+
throw Error("Failed to decode ID token");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
this.client.SetStaticToken({token: fabricToken});
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
authToken: this.SetAuthorization({
|
|
303
|
+
fabricToken,
|
|
304
|
+
tenantId,
|
|
305
|
+
address,
|
|
306
|
+
email,
|
|
307
|
+
expiresAt,
|
|
308
|
+
walletType: "Custodial",
|
|
309
|
+
walletName: "Eluvio"
|
|
310
|
+
}),
|
|
311
|
+
signingToken: this.SetAuthorization({
|
|
312
|
+
clusterToken: this.client.signer.authToken,
|
|
313
|
+
fabricToken,
|
|
314
|
+
tenantId,
|
|
315
|
+
address,
|
|
316
|
+
email,
|
|
317
|
+
expiresAt,
|
|
318
|
+
walletType: "Custodial",
|
|
319
|
+
walletName: "Eluvio"
|
|
320
|
+
})
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Authenticate with an external Ethereum compatible wallet, like Metamask.
|
|
326
|
+
*
|
|
327
|
+
* @methodGroup Authorization
|
|
328
|
+
* @namedParams
|
|
329
|
+
* @param {string} address - The address of the wallet
|
|
330
|
+
* @param {number=} tokenDuration=24 - Number of hours the generated authorization token will last before expiring
|
|
331
|
+
* @param {string=} walletName=Metamask - Name of the external wallet
|
|
332
|
+
* @param {function=} Sign - The method used for signing by the wallet. If not specified, will attempt to sign with Metamask.
|
|
333
|
+
*
|
|
334
|
+
* @returns {Promise<string>} - Returns an authorization token that can be used to initialize the client using <a href="#Authenticate">Authenticate</a>.
|
|
335
|
+
* Save this token to avoid having to reauthenticate. This token expires after 24 hours.
|
|
336
|
+
*/
|
|
337
|
+
async AuthenticateExternalWallet({address, tokenDuration=24, walletName="Metamask", Sign}) {
|
|
338
|
+
if(!address) {
|
|
339
|
+
address = window.ethereum.selectedAddress;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
address = this.utils.FormatAddress(address);
|
|
343
|
+
|
|
344
|
+
if(!Sign) {
|
|
345
|
+
Sign = async message => this.SignMetamask({message, address});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const expiresAt = Date.now() + tokenDuration * 60 * 60 * 1000;
|
|
349
|
+
const fabricToken = await this.client.CreateFabricToken({
|
|
350
|
+
address,
|
|
351
|
+
duration: tokenDuration * 60 * 60 * 1000,
|
|
352
|
+
Sign,
|
|
353
|
+
addEthereumPrefix: false
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
return this.SetAuthorization({fabricToken, address, expiresAt, walletType: "External", walletName});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* <b><i>Requires login</i></b>
|
|
361
|
+
*
|
|
362
|
+
* Retrieve the current client auth token
|
|
363
|
+
*
|
|
364
|
+
* @returns {string} - The client auth token
|
|
365
|
+
*/
|
|
366
|
+
ClientAuthToken() {
|
|
367
|
+
if(!this.loggedIn) { return ""; }
|
|
368
|
+
|
|
369
|
+
return this.utils.B58(JSON.stringify(this.__authorization));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
AuthToken() {
|
|
373
|
+
if(!this.loggedIn) {
|
|
374
|
+
return this.publicStaticToken;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return this.__authorization.fabricToken;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
SetAuthorization({clusterToken, fabricToken, tenantId, address, email, expiresAt, walletType, walletName}) {
|
|
381
|
+
address = this.client.utils.FormatAddress(address);
|
|
382
|
+
|
|
383
|
+
this.__authorization = {
|
|
384
|
+
fabricToken,
|
|
385
|
+
tenantId,
|
|
386
|
+
address,
|
|
387
|
+
email,
|
|
388
|
+
expiresAt,
|
|
389
|
+
walletType,
|
|
390
|
+
walletName
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
if(clusterToken) {
|
|
394
|
+
this.__authorization.clusterToken = clusterToken;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
this.loggedIn = true;
|
|
398
|
+
|
|
399
|
+
this.cachedMarketplaces = {};
|
|
400
|
+
|
|
401
|
+
const token = this.ClientAuthToken();
|
|
402
|
+
|
|
403
|
+
if(this.storeAuthToken && typeof localStorage !== "undefined") {
|
|
404
|
+
try {
|
|
405
|
+
localStorage.setItem(`__elv-token-${this.network}`, token);
|
|
406
|
+
// eslint-disable-next-line no-empty
|
|
407
|
+
} catch(error) {}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return token;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async SignMetamask({message, address}) {
|
|
414
|
+
if(!window.ethereum) {
|
|
415
|
+
throw Error("ElvWalletClient: Unable to initialize - Metamask not available");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
await window.ethereum.request({method: "eth_requestAccounts"});
|
|
419
|
+
const from = address || window.ethereum.selectedAddress;
|
|
420
|
+
return await window.ethereum.request({
|
|
421
|
+
method: "personal_sign",
|
|
422
|
+
params: [message, from, ""],
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
// Internal loading methods
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
// If marketplace slug is specified, load only that marketplace. Otherwise load all
|
|
433
|
+
async LoadAvailableMarketplaces(forceReload=false) {
|
|
434
|
+
if(!forceReload && Object.keys(this.availableMarketplaces) > 0) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const mainSiteHash = await this.client.LatestVersionHash({objectId: this.mainSiteId});
|
|
439
|
+
const metadata = await this.client.ContentObjectMetadata({
|
|
440
|
+
versionHash: mainSiteHash,
|
|
441
|
+
metadataSubtree: "public/asset_metadata/tenants",
|
|
442
|
+
resolveLinks: true,
|
|
443
|
+
linkDepthLimit: 2,
|
|
444
|
+
resolveIncludeSource: true,
|
|
445
|
+
resolveIgnoreErrors: true,
|
|
446
|
+
produceLinkUrls: true,
|
|
447
|
+
authorizationToken: this.publicStaticToken,
|
|
448
|
+
noAuth: true,
|
|
449
|
+
select: [
|
|
450
|
+
"*/.",
|
|
451
|
+
"*/marketplaces/*/.",
|
|
452
|
+
"*/marketplaces/*/info/tenant_id",
|
|
453
|
+
"*/marketplaces/*/info/tenant_name",
|
|
454
|
+
"*/marketplaces/*/info/branding"
|
|
455
|
+
],
|
|
456
|
+
remove: [
|
|
457
|
+
"*/marketplaces/*/info/branding/custom_css"
|
|
458
|
+
]
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
let availableMarketplaces = { ...(this.availableMarketplaces || {}) };
|
|
462
|
+
let availableMarketplacesById = { ...(this.availableMarketplacesById || {}) };
|
|
463
|
+
Object.keys(metadata || {}).forEach(tenantSlug => {
|
|
464
|
+
try {
|
|
465
|
+
availableMarketplaces[tenantSlug] = {
|
|
466
|
+
versionHash: metadata[tenantSlug]["."].source
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
Object.keys(metadata[tenantSlug].marketplaces || {}).forEach(marketplaceSlug => {
|
|
470
|
+
try {
|
|
471
|
+
const versionHash = metadata[tenantSlug].marketplaces[marketplaceSlug]["."].source;
|
|
472
|
+
const objectId = this.utils.DecodeVersionHash(versionHash).objectId;
|
|
473
|
+
|
|
474
|
+
availableMarketplaces[tenantSlug][marketplaceSlug] = {
|
|
475
|
+
...(metadata[tenantSlug].marketplaces[marketplaceSlug].info || {}),
|
|
476
|
+
tenantName: metadata[tenantSlug].marketplaces[marketplaceSlug].info.tenant_name,
|
|
477
|
+
tenantId: metadata[tenantSlug].marketplaces[marketplaceSlug].info.tenant_id,
|
|
478
|
+
tenantSlug,
|
|
479
|
+
marketplaceSlug,
|
|
480
|
+
marketplaceId: objectId,
|
|
481
|
+
marketplaceHash: versionHash,
|
|
482
|
+
order: Configuration.__MARKETPLACE_ORDER.findIndex(slug => slug === marketplaceSlug)
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
availableMarketplacesById[objectId] = availableMarketplaces[tenantSlug][marketplaceSlug];
|
|
486
|
+
|
|
487
|
+
this.marketplaceHashes[objectId] = versionHash;
|
|
488
|
+
|
|
489
|
+
// Fill out selected marketplace info
|
|
490
|
+
if(this.selectedMarketplaceInfo) {
|
|
491
|
+
if((this.selectedMarketplaceInfo.tenantSlug === tenantSlug && this.selectedMarketplaceInfo.marketplaceSlug === marketplaceSlug) || this.selectedMarketplaceInfo.marketplaceId === objectId) {
|
|
492
|
+
this.selectedMarketplaceInfo = availableMarketplaces[tenantSlug][marketplaceSlug];
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
} catch(error) {
|
|
496
|
+
this.Log(`Eluvio Wallet Client: Unable to load info for marketplace ${tenantSlug}/${marketplaceSlug}`, true);
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
} catch(error) {
|
|
500
|
+
this.Log(`Eluvio Wallet Client: Failed to load tenant info ${tenantSlug}`, true);
|
|
501
|
+
this.Log(error, true);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
this.availableMarketplaces = availableMarketplaces;
|
|
506
|
+
this.availableMarketplacesById = availableMarketplacesById;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Get the hash of the currently linked marketplace
|
|
510
|
+
async LatestMarketplaceHash({tenantSlug, marketplaceSlug}) {
|
|
511
|
+
const mainSiteHash = await this.client.LatestVersionHash({objectId: this.mainSiteId});
|
|
512
|
+
const marketplaceLink = await this.client.ContentObjectMetadata({
|
|
513
|
+
versionHash: mainSiteHash,
|
|
514
|
+
metadataSubtree: UrlJoin("/public", "asset_metadata", "tenants", tenantSlug, "marketplaces", marketplaceSlug),
|
|
515
|
+
resolveLinks: false
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
return LinkTargetHash(marketplaceLink);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async LoadMarketplace(marketplaceParams) {
|
|
522
|
+
const marketplaceInfo = this.MarketplaceInfo({marketplaceParams});
|
|
523
|
+
|
|
524
|
+
const marketplaceId = marketplaceInfo.marketplaceId;
|
|
525
|
+
const marketplaceHash = await this.LatestMarketplaceHash({tenantSlug: marketplaceInfo.tenantSlug, marketplaceSlug: marketplaceInfo.marketplaceSlug});
|
|
526
|
+
|
|
527
|
+
if(this.cachedMarketplaces[marketplaceId] && this.cachedMarketplaces[marketplaceId].versionHash !== marketplaceHash) {
|
|
528
|
+
delete this.cachedMarketplaces[marketplaceId];
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if(!this.cachedMarketplaces[marketplaceId]) {
|
|
532
|
+
let marketplace = await this.client.ContentObjectMetadata({
|
|
533
|
+
versionHash: marketplaceHash,
|
|
534
|
+
metadataSubtree: "public/asset_metadata/info",
|
|
535
|
+
linkDepthLimit: 2,
|
|
536
|
+
resolveLinks: true,
|
|
537
|
+
resolveIgnoreErrors: true,
|
|
538
|
+
resolveIncludeSource: true,
|
|
539
|
+
produceLinkUrls: true,
|
|
540
|
+
authorizationToken: this.publicStaticToken
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
marketplace.items = await Promise.all(
|
|
544
|
+
marketplace.items.map(async (item, index) => {
|
|
545
|
+
if(item.requires_permissions) {
|
|
546
|
+
if(!this.loggedIn) {
|
|
547
|
+
item.authorized = false;
|
|
548
|
+
} else {
|
|
549
|
+
try {
|
|
550
|
+
await this.client.ContentObjectMetadata({
|
|
551
|
+
versionHash: LinkTargetHash(item.nft_template),
|
|
552
|
+
metadataSubtree: "permissioned"
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
item.authorized = true;
|
|
556
|
+
} catch(error) {
|
|
557
|
+
item.authorized = false;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
item.nftTemplateMetadata = ((item.nft_template || {}).nft || {});
|
|
563
|
+
item.itemIndex = index;
|
|
564
|
+
|
|
565
|
+
return item;
|
|
566
|
+
})
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
marketplace.collections = (marketplace.collections || []).map((collection, collectionIndex) => ({
|
|
570
|
+
...collection,
|
|
571
|
+
collectionIndex
|
|
572
|
+
}));
|
|
573
|
+
|
|
574
|
+
marketplace.retrievedAt = Date.now();
|
|
575
|
+
marketplace.marketplaceId = marketplaceId;
|
|
576
|
+
marketplace.versionHash = marketplaceHash;
|
|
577
|
+
|
|
578
|
+
// Generate embed URLs for pack opening animations
|
|
579
|
+
["purchase_animation", "purchase_animation__mobile", "reveal_animation", "reveal_animation_mobile"].forEach(key => {
|
|
580
|
+
try {
|
|
581
|
+
if(marketplace.storefront[key]) {
|
|
582
|
+
let embedUrl = new URL("https://embed.v3.contentfabric.io");
|
|
583
|
+
const targetHash = LinkTargetHash(marketplace.storefront[key]);
|
|
584
|
+
embedUrl.searchParams.set("p", "");
|
|
585
|
+
embedUrl.searchParams.set("net", this.network === "main" ? "main" : "demo");
|
|
586
|
+
embedUrl.searchParams.set("ath", (this.__authorization || {}).authToken || this.publicStaticToken);
|
|
587
|
+
embedUrl.searchParams.set("vid", targetHash);
|
|
588
|
+
embedUrl.searchParams.set("ap", "");
|
|
589
|
+
|
|
590
|
+
if(!key.startsWith("reveal")) {
|
|
591
|
+
embedUrl.searchParams.set("m", "");
|
|
592
|
+
embedUrl.searchParams.set("lp", "");
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
marketplace.storefront[`${key}_embed_url`] = embedUrl.toString();
|
|
596
|
+
}
|
|
597
|
+
// eslint-disable-next-line no-empty
|
|
598
|
+
} catch(error) {
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
this.cachedMarketplaces[marketplaceId] = marketplace;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return this.cachedMarketplaces[marketplaceId];
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async FilteredQuery({
|
|
609
|
+
mode="listings",
|
|
610
|
+
sortBy="created",
|
|
611
|
+
sortDesc=false,
|
|
612
|
+
filter,
|
|
613
|
+
editionFilter,
|
|
614
|
+
attributeFilters,
|
|
615
|
+
contractAddress,
|
|
616
|
+
tokenId,
|
|
617
|
+
currency,
|
|
618
|
+
marketplaceParams,
|
|
619
|
+
tenantId,
|
|
620
|
+
collectionIndex=-1,
|
|
621
|
+
sellerAddress,
|
|
622
|
+
lastNDays=-1,
|
|
623
|
+
start=0,
|
|
624
|
+
limit=50
|
|
625
|
+
}={}) {
|
|
626
|
+
collectionIndex = parseInt(collectionIndex);
|
|
627
|
+
|
|
628
|
+
let params = {
|
|
629
|
+
sort_by: sortBy,
|
|
630
|
+
sort_descending: sortDesc,
|
|
631
|
+
start,
|
|
632
|
+
limit
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
let marketplaceInfo, marketplace;
|
|
636
|
+
if(marketplaceParams) {
|
|
637
|
+
marketplaceInfo = await this.MarketplaceInfo({marketplaceParams});
|
|
638
|
+
|
|
639
|
+
if(collectionIndex >= 0) {
|
|
640
|
+
marketplace = await this.Marketplace({marketplaceParams});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
try {
|
|
645
|
+
let filters = [];
|
|
646
|
+
|
|
647
|
+
if(sellerAddress) {
|
|
648
|
+
filters.push(`seller:eq:${this.client.utils.FormatAddress(sellerAddress)}`);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if(marketplace && collectionIndex >= 0) {
|
|
652
|
+
const collection = marketplace.collections[collectionIndex];
|
|
653
|
+
|
|
654
|
+
collection.items.forEach(sku => {
|
|
655
|
+
if(!sku) { return; }
|
|
656
|
+
|
|
657
|
+
const item = marketplace.items.find(item => item.sku === sku);
|
|
658
|
+
|
|
659
|
+
if(!item) { return; }
|
|
660
|
+
|
|
661
|
+
const address = Utils.SafeTraverse(item, "nft_template", "nft", "address");
|
|
662
|
+
|
|
663
|
+
if(address) {
|
|
664
|
+
filters.push(
|
|
665
|
+
`${mode === "owned" ? "contract_addr": "contract"}:eq:${Utils.FormatAddress(address)}`
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// No valid items, so there must not be anything relevant in the collection
|
|
671
|
+
if(filters.length === 0) {
|
|
672
|
+
if(mode.includes("stats")) {
|
|
673
|
+
return {};
|
|
674
|
+
} else {
|
|
675
|
+
return {
|
|
676
|
+
paging: {
|
|
677
|
+
start: params.start,
|
|
678
|
+
limit: params.limit,
|
|
679
|
+
total: 0,
|
|
680
|
+
more: false
|
|
681
|
+
},
|
|
682
|
+
results: []
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
} else if(mode !== "owned" && marketplaceInfo || tenantId) {
|
|
687
|
+
filters.push(`tenant:eq:${marketplaceInfo ? marketplaceInfo.tenantId : tenantId}`);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if(contractAddress) {
|
|
691
|
+
if(mode === "owned") {
|
|
692
|
+
filters.push(`contract_addr:eq:${Utils.FormatAddress(contractAddress)}`);
|
|
693
|
+
} else {
|
|
694
|
+
filters.push(`contract:eq:${Utils.FormatAddress(contractAddress)}`);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if(tokenId) {
|
|
698
|
+
filters.push(`token:eq:${tokenId}`);
|
|
699
|
+
}
|
|
700
|
+
} else if(filter) {
|
|
701
|
+
if(mode.includes("listing")) {
|
|
702
|
+
filters.push(`nft/display_name:eq:${filter}`);
|
|
703
|
+
} else if(mode === "owned") {
|
|
704
|
+
filters.push(`meta:@>:{"display_name":"${filter}"}`);
|
|
705
|
+
params.exact = false;
|
|
706
|
+
} else {
|
|
707
|
+
filters.push(`name:eq:${filter}`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if(editionFilter) {
|
|
712
|
+
if(mode.includes("listing")) {
|
|
713
|
+
filters.push(`nft/edition_name:eq:${editionFilter}`);
|
|
714
|
+
} else if(mode === "owned") {
|
|
715
|
+
filters.push(`meta:@>:{"edition_name":"${editionFilter}"}`);
|
|
716
|
+
params.exact = false;
|
|
717
|
+
} else {
|
|
718
|
+
filters.push(`edition:eq:${editionFilter}`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if(attributeFilters) {
|
|
723
|
+
attributeFilters.map(({name, value}) => {
|
|
724
|
+
if(!name || !value) { return; }
|
|
725
|
+
|
|
726
|
+
filters.push(`nft/attributes/${name}:eq:${value}`);
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if(currency) {
|
|
731
|
+
filters.push("link_type:eq:sol");
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if(lastNDays && lastNDays > 0) {
|
|
735
|
+
filters.push(`created:gt:${((Date.now() / 1000) - ( lastNDays * 24 * 60 * 60 )).toFixed(0)}`);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
let path;
|
|
739
|
+
switch(mode) {
|
|
740
|
+
case "owned":
|
|
741
|
+
path = UrlJoin("as", "wlt", "nfts");
|
|
742
|
+
|
|
743
|
+
if(marketplaceInfo) {
|
|
744
|
+
path = UrlJoin("as", "wlt", "nfts", marketplaceInfo.tenantId);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
break;
|
|
748
|
+
|
|
749
|
+
case "listings":
|
|
750
|
+
path = UrlJoin("as", "mkt", "f");
|
|
751
|
+
break;
|
|
752
|
+
|
|
753
|
+
case "sales":
|
|
754
|
+
path = UrlJoin("as", "mkt", "hst", "f");
|
|
755
|
+
filters.push("action:eq:SOLD");
|
|
756
|
+
break;
|
|
757
|
+
|
|
758
|
+
case "listing-stats":
|
|
759
|
+
path = UrlJoin("as", "mkt", "stats", "listed");
|
|
760
|
+
break;
|
|
761
|
+
|
|
762
|
+
case "sales-stats":
|
|
763
|
+
path = UrlJoin("as", "mkt", "stats", "sold");
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if(filters.length > 0) {
|
|
768
|
+
params.filter = filters;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if(mode.includes("stats")) {
|
|
772
|
+
return await Utils.ResponseToJson(
|
|
773
|
+
this.client.authClient.MakeAuthServiceRequest({
|
|
774
|
+
path,
|
|
775
|
+
method: "GET",
|
|
776
|
+
queryParams: params
|
|
777
|
+
})
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const { contents, paging } = await Utils.ResponseToJson(
|
|
782
|
+
await this.client.authClient.MakeAuthServiceRequest({
|
|
783
|
+
path,
|
|
784
|
+
method: "GET",
|
|
785
|
+
queryParams: params,
|
|
786
|
+
headers: mode === "owned" ?
|
|
787
|
+
{ Authorization: `Bearer ${this.AuthToken()}` } :
|
|
788
|
+
{}
|
|
789
|
+
})
|
|
790
|
+
) || [];
|
|
791
|
+
|
|
792
|
+
return {
|
|
793
|
+
paging: {
|
|
794
|
+
start: params.start,
|
|
795
|
+
limit: params.limit,
|
|
796
|
+
total: paging.total,
|
|
797
|
+
more: paging.total > start + limit
|
|
798
|
+
},
|
|
799
|
+
results: (contents || []).map(item => ["owned", "listings"].includes(mode) ? FormatNFT(item) : item)
|
|
800
|
+
};
|
|
801
|
+
} catch(error) {
|
|
802
|
+
if(error.status && error.status.toString() === "404") {
|
|
803
|
+
return {
|
|
804
|
+
paging: {
|
|
805
|
+
start: params.start,
|
|
806
|
+
limit: params.limit,
|
|
807
|
+
total: 0,
|
|
808
|
+
more: false
|
|
809
|
+
},
|
|
810
|
+
results: []
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
throw error;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
async MintingStatus({marketplaceParams, tenantId}) {
|
|
819
|
+
if(!tenantId) {
|
|
820
|
+
const marketplaceInfo = await this.MarketplaceInfo({marketplaceParams: marketplaceParams || this.selectedMarketplaceInfo});
|
|
821
|
+
tenantId = marketplaceInfo.tenantId;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
try {
|
|
825
|
+
const response = await Utils.ResponseToJson(
|
|
826
|
+
this.client.authClient.MakeAuthServiceRequest({
|
|
827
|
+
path: UrlJoin("as", "wlt", "status", "act", tenantId),
|
|
828
|
+
method: "GET",
|
|
829
|
+
headers: {
|
|
830
|
+
Authorization: `Bearer ${this.AuthToken()}`
|
|
831
|
+
}
|
|
832
|
+
})
|
|
833
|
+
);
|
|
834
|
+
|
|
835
|
+
return response
|
|
836
|
+
.map(status => {
|
|
837
|
+
let [op, address, id] = status.op.split(":");
|
|
838
|
+
address = address.startsWith("0x") ? Utils.FormatAddress(address) : address;
|
|
839
|
+
|
|
840
|
+
let confirmationId, tokenId;
|
|
841
|
+
if(op === "nft-buy") {
|
|
842
|
+
confirmationId = id;
|
|
843
|
+
} else if(op === "nft-claim") {
|
|
844
|
+
confirmationId = id;
|
|
845
|
+
status.marketplaceId = address;
|
|
846
|
+
|
|
847
|
+
if(status.extra && status.extra["0"]) {
|
|
848
|
+
address = status.extra.token_addr;
|
|
849
|
+
tokenId = status.extra.token_id_str;
|
|
850
|
+
}
|
|
851
|
+
} else if(op === "nft-redeem") {
|
|
852
|
+
confirmationId = status.op.split(":").slice(-1)[0];
|
|
853
|
+
} else {
|
|
854
|
+
tokenId = id;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if(op === "nft-transfer") {
|
|
858
|
+
confirmationId = status.extra && status.extra.trans_id;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
return {
|
|
862
|
+
...status,
|
|
863
|
+
timestamp: new Date(status.ts),
|
|
864
|
+
state: status.state && typeof status.state === "object" ? Object.values(status.state) : status.state,
|
|
865
|
+
extra: status.extra && typeof status.extra === "object" ? Object.values(status.extra) : status.extra,
|
|
866
|
+
confirmationId,
|
|
867
|
+
op,
|
|
868
|
+
address,
|
|
869
|
+
tokenId
|
|
870
|
+
};
|
|
871
|
+
})
|
|
872
|
+
.sort((a, b) => a.ts < b.ts ? 1 : -1);
|
|
873
|
+
} catch(error) {
|
|
874
|
+
this.Log("Failed to retrieve minting status", true);
|
|
875
|
+
this.Log(error);
|
|
876
|
+
|
|
877
|
+
return [];
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
Object.assign(ElvWalletClient.prototype, require("./ClientMethods"));
|
|
883
|
+
|
|
884
|
+
exports.ElvWalletClient = ElvWalletClient;
|