@arcadiasol/sdk 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -0
- package/dist/esm/api-client.d.ts +54 -0
- package/dist/esm/api-client.d.ts.map +1 -0
- package/dist/esm/errors.d.ts +51 -0
- package/dist/esm/errors.d.ts.map +1 -0
- package/dist/esm/index.d.ts +13 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +929 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/messaging.d.ts +33 -0
- package/dist/esm/messaging.d.ts.map +1 -0
- package/dist/esm/payment.d.ts +35 -0
- package/dist/esm/payment.d.ts.map +1 -0
- package/dist/esm/sdk.d.ts +73 -0
- package/dist/esm/sdk.d.ts.map +1 -0
- package/dist/esm/types.d.ts +136 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/wallet.d.ts +48 -0
- package/dist/esm/wallet.d.ts.map +1 -0
- package/dist/umd/arcadia-game-sdk.js +949 -0
- package/dist/umd/arcadia-game-sdk.js.map +1 -0
- package/dist/umd/arcadia-game-sdk.min.js +1 -0
- package/examples/basic-integration.html +219 -0
- package/examples/payment-example.html +330 -0
- package/examples/wallet-example.html +298 -0
- package/package.json +53 -0
|
@@ -0,0 +1,949 @@
|
|
|
1
|
+
(function (global, factory) {
|
|
2
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
3
|
+
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
4
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ArcadiaGameSDK = {}));
|
|
5
|
+
})(this, (function (exports) { 'use strict';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Message types for postMessage communication
|
|
9
|
+
*/
|
|
10
|
+
var MessageType;
|
|
11
|
+
(function (MessageType) {
|
|
12
|
+
// SDK → Parent
|
|
13
|
+
MessageType["INIT_REQUEST"] = "INIT_REQUEST";
|
|
14
|
+
MessageType["GET_WALLET_ADDRESS"] = "GET_WALLET_ADDRESS";
|
|
15
|
+
MessageType["PAYMENT_REQUEST"] = "PAYMENT_REQUEST";
|
|
16
|
+
MessageType["GAME_READY"] = "GAME_READY";
|
|
17
|
+
// Parent → SDK
|
|
18
|
+
MessageType["INIT"] = "INIT";
|
|
19
|
+
MessageType["WALLET_ADDRESS_RESPONSE"] = "WALLET_ADDRESS_RESPONSE";
|
|
20
|
+
MessageType["WALLET_UPDATE"] = "WALLET_UPDATE";
|
|
21
|
+
MessageType["PAYMENT_RESPONSE"] = "PAYMENT_RESPONSE";
|
|
22
|
+
})(MessageType || (MessageType = {}));
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Base error class for all SDK errors
|
|
26
|
+
*/
|
|
27
|
+
class ArcadiaSDKError extends Error {
|
|
28
|
+
constructor(message, code) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.code = code;
|
|
31
|
+
this.name = 'ArcadiaSDKError';
|
|
32
|
+
Object.setPrototypeOf(this, ArcadiaSDKError.prototype);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Error thrown when wallet is not connected
|
|
37
|
+
*/
|
|
38
|
+
class WalletNotConnectedError extends ArcadiaSDKError {
|
|
39
|
+
constructor(message = 'Wallet is not connected. Please connect your wallet in Arcadia.') {
|
|
40
|
+
super(message, 'WALLET_NOT_CONNECTED');
|
|
41
|
+
this.name = 'WalletNotConnectedError';
|
|
42
|
+
Object.setPrototypeOf(this, WalletNotConnectedError.prototype);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Error thrown when payment fails
|
|
47
|
+
*/
|
|
48
|
+
class PaymentFailedError extends ArcadiaSDKError {
|
|
49
|
+
constructor(message = 'Payment failed', txSignature) {
|
|
50
|
+
super(message, 'PAYMENT_FAILED');
|
|
51
|
+
this.txSignature = txSignature;
|
|
52
|
+
this.name = 'PaymentFailedError';
|
|
53
|
+
Object.setPrototypeOf(this, PaymentFailedError.prototype);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Error thrown when request times out
|
|
58
|
+
*/
|
|
59
|
+
class TimeoutError extends ArcadiaSDKError {
|
|
60
|
+
constructor(message = 'Request timed out. Please try again.') {
|
|
61
|
+
super(message, 'TIMEOUT');
|
|
62
|
+
this.name = 'TimeoutError';
|
|
63
|
+
Object.setPrototypeOf(this, TimeoutError.prototype);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Error thrown when SDK configuration is invalid
|
|
68
|
+
*/
|
|
69
|
+
class InvalidConfigError extends ArcadiaSDKError {
|
|
70
|
+
constructor(message = 'Invalid SDK configuration') {
|
|
71
|
+
super(message, 'INVALID_CONFIG');
|
|
72
|
+
this.name = 'InvalidConfigError';
|
|
73
|
+
Object.setPrototypeOf(this, InvalidConfigError.prototype);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Error thrown when SDK is not running in iframe
|
|
78
|
+
*/
|
|
79
|
+
class NotInIframeError extends ArcadiaSDKError {
|
|
80
|
+
constructor(message = 'This function only works when the game is running in an Arcadia iframe') {
|
|
81
|
+
super(message, 'NOT_IN_IFRAME');
|
|
82
|
+
this.name = 'NotInIframeError';
|
|
83
|
+
Object.setPrototypeOf(this, NotInIframeError.prototype);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Error thrown when invalid amount is provided
|
|
88
|
+
*/
|
|
89
|
+
class InvalidAmountError extends ArcadiaSDKError {
|
|
90
|
+
constructor(message = 'Invalid payment amount. Amount must be greater than 0.') {
|
|
91
|
+
super(message, 'INVALID_AMOUNT');
|
|
92
|
+
this.name = 'InvalidAmountError';
|
|
93
|
+
Object.setPrototypeOf(this, InvalidAmountError.prototype);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Error thrown when invalid token is provided
|
|
98
|
+
*/
|
|
99
|
+
class InvalidTokenError extends ArcadiaSDKError {
|
|
100
|
+
constructor(message = "Invalid token. Must be 'SOL' or 'USDC'.") {
|
|
101
|
+
super(message, 'INVALID_TOKEN');
|
|
102
|
+
this.name = 'InvalidTokenError';
|
|
103
|
+
Object.setPrototypeOf(this, InvalidTokenError.prototype);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Handles postMessage communication with parent window
|
|
109
|
+
*/
|
|
110
|
+
class MessageHandler {
|
|
111
|
+
constructor(parentOrigin = '*', timeout = 30000) {
|
|
112
|
+
this.messageId = 0;
|
|
113
|
+
this.pendingRequests = new Map();
|
|
114
|
+
this.messageListener = null;
|
|
115
|
+
this.parentOrigin = parentOrigin;
|
|
116
|
+
this.timeout = timeout;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Initialize message listener
|
|
120
|
+
*/
|
|
121
|
+
init() {
|
|
122
|
+
if (this.messageListener) {
|
|
123
|
+
return; // Already initialized
|
|
124
|
+
}
|
|
125
|
+
this.messageListener = (event) => {
|
|
126
|
+
this.handleMessage(event);
|
|
127
|
+
};
|
|
128
|
+
window.addEventListener('message', this.messageListener);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Cleanup message listener
|
|
132
|
+
*/
|
|
133
|
+
destroy() {
|
|
134
|
+
if (this.messageListener) {
|
|
135
|
+
window.removeEventListener('message', this.messageListener);
|
|
136
|
+
this.messageListener = null;
|
|
137
|
+
}
|
|
138
|
+
// Clear all pending requests
|
|
139
|
+
this.pendingRequests.forEach((request) => {
|
|
140
|
+
clearTimeout(request.timeout);
|
|
141
|
+
request.reject(new Error('Message handler destroyed'));
|
|
142
|
+
});
|
|
143
|
+
this.pendingRequests.clear();
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Send message to parent window and wait for response
|
|
147
|
+
*/
|
|
148
|
+
sendMessage(type, payload) {
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
const id = ++this.messageId;
|
|
151
|
+
// Set up timeout
|
|
152
|
+
const timeout = setTimeout(() => {
|
|
153
|
+
this.pendingRequests.delete(id);
|
|
154
|
+
reject(new TimeoutError(`Request ${type} timed out after ${this.timeout}ms`));
|
|
155
|
+
}, this.timeout);
|
|
156
|
+
// Store request
|
|
157
|
+
this.pendingRequests.set(id, {
|
|
158
|
+
resolve: (value) => {
|
|
159
|
+
clearTimeout(timeout);
|
|
160
|
+
resolve(value);
|
|
161
|
+
},
|
|
162
|
+
reject: (error) => {
|
|
163
|
+
clearTimeout(timeout);
|
|
164
|
+
reject(error);
|
|
165
|
+
},
|
|
166
|
+
timeout,
|
|
167
|
+
});
|
|
168
|
+
// Send message to parent
|
|
169
|
+
const message = {
|
|
170
|
+
type,
|
|
171
|
+
id,
|
|
172
|
+
payload,
|
|
173
|
+
};
|
|
174
|
+
if (window.parent) {
|
|
175
|
+
window.parent.postMessage(message, this.parentOrigin);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
clearTimeout(timeout);
|
|
179
|
+
this.pendingRequests.delete(id);
|
|
180
|
+
reject(new Error('No parent window found'));
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Handle incoming messages from parent
|
|
186
|
+
*/
|
|
187
|
+
handleMessage(event) {
|
|
188
|
+
// Validate origin if specified
|
|
189
|
+
if (this.parentOrigin !== '*' && event.origin !== this.parentOrigin) {
|
|
190
|
+
return; // Ignore messages from unauthorized origins
|
|
191
|
+
}
|
|
192
|
+
const message = event.data;
|
|
193
|
+
// Handle response messages (have ID)
|
|
194
|
+
if (message.id !== undefined) {
|
|
195
|
+
const request = this.pendingRequests.get(message.id);
|
|
196
|
+
if (request) {
|
|
197
|
+
this.pendingRequests.delete(message.id);
|
|
198
|
+
if (message.type === MessageType.PAYMENT_RESPONSE) {
|
|
199
|
+
const response = message.payload;
|
|
200
|
+
if (response.success) {
|
|
201
|
+
request.resolve(response);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
request.reject(new Error(response.error || 'Payment failed'));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else if (message.type === MessageType.WALLET_ADDRESS_RESPONSE) {
|
|
208
|
+
request.resolve(message.payload);
|
|
209
|
+
}
|
|
210
|
+
else if (message.type === MessageType.INIT) {
|
|
211
|
+
request.resolve(message.payload);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
// Generic response - resolve with payload
|
|
215
|
+
request.resolve(message.payload);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Send one-way message (no response expected)
|
|
222
|
+
*/
|
|
223
|
+
sendOneWay(type, payload) {
|
|
224
|
+
const message = {
|
|
225
|
+
type,
|
|
226
|
+
payload,
|
|
227
|
+
};
|
|
228
|
+
if (window.parent) {
|
|
229
|
+
window.parent.postMessage(message, this.parentOrigin);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Wallet address management
|
|
236
|
+
*/
|
|
237
|
+
class WalletManager {
|
|
238
|
+
constructor(messageHandler, isIframe, apiClient) {
|
|
239
|
+
this.walletAddress = null;
|
|
240
|
+
this.walletConnected = false;
|
|
241
|
+
this.apiClient = null;
|
|
242
|
+
this.walletChangeCallbacks = [];
|
|
243
|
+
this.messageHandler = messageHandler;
|
|
244
|
+
this.isIframe = isIframe;
|
|
245
|
+
this.apiClient = apiClient || null;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Get wallet address from parent window (iframe) or API (non-iframe)
|
|
249
|
+
*/
|
|
250
|
+
async getWalletAddress() {
|
|
251
|
+
if (this.isIframe) {
|
|
252
|
+
// Iframe mode: use postMessage
|
|
253
|
+
try {
|
|
254
|
+
const response = await this.messageHandler.sendMessage(MessageType.GET_WALLET_ADDRESS);
|
|
255
|
+
this.walletAddress = response.walletAddress;
|
|
256
|
+
this.walletConnected = response.connected;
|
|
257
|
+
return this.walletAddress;
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
// If request fails, assume wallet not connected
|
|
261
|
+
this.walletAddress = null;
|
|
262
|
+
this.walletConnected = false;
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
// Non-iframe mode: use API
|
|
268
|
+
if (!this.apiClient) {
|
|
269
|
+
throw new Error('API client not initialized. Ensure apiBaseURL is configured in SDK config.');
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
const response = await this.apiClient.getWalletAddress();
|
|
273
|
+
this.walletAddress = response.walletAddress;
|
|
274
|
+
this.walletConnected = response.connected;
|
|
275
|
+
return this.walletAddress;
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
this.walletAddress = null;
|
|
279
|
+
this.walletConnected = false;
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Check if wallet is connected
|
|
286
|
+
*/
|
|
287
|
+
async isWalletConnected() {
|
|
288
|
+
// If we have cached address, return true
|
|
289
|
+
if (this.walletAddress) {
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
// Otherwise, fetch from parent (iframe) or API (non-iframe)
|
|
293
|
+
const address = await this.getWalletAddress();
|
|
294
|
+
return address !== null;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Listen for wallet connection changes
|
|
298
|
+
*/
|
|
299
|
+
onWalletChange(callback) {
|
|
300
|
+
this.walletChangeCallbacks.push(callback);
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Remove wallet change listener
|
|
304
|
+
*/
|
|
305
|
+
offWalletChange(callback) {
|
|
306
|
+
const index = this.walletChangeCallbacks.indexOf(callback);
|
|
307
|
+
if (index > -1) {
|
|
308
|
+
this.walletChangeCallbacks.splice(index, 1);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Update wallet status (called by SDK when receiving WALLET_UPDATE message)
|
|
313
|
+
*/
|
|
314
|
+
updateWalletStatus(walletInfo) {
|
|
315
|
+
const oldAddress = this.walletAddress;
|
|
316
|
+
const oldConnected = this.walletConnected;
|
|
317
|
+
this.walletAddress = walletInfo.address;
|
|
318
|
+
this.walletConnected = walletInfo.connected;
|
|
319
|
+
// Notify callbacks if status changed
|
|
320
|
+
if (oldAddress !== this.walletAddress || oldConnected !== this.walletConnected) {
|
|
321
|
+
this.walletChangeCallbacks.forEach((callback) => {
|
|
322
|
+
try {
|
|
323
|
+
callback(this.walletConnected, this.walletAddress);
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
console.error('Error in wallet change callback:', error);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Get current cached wallet address (without fetching)
|
|
333
|
+
*/
|
|
334
|
+
getCachedWalletAddress() {
|
|
335
|
+
return this.walletAddress;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Get current cached connection status (without fetching)
|
|
339
|
+
*/
|
|
340
|
+
getCachedConnectionStatus() {
|
|
341
|
+
return this.walletConnected;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Clear cached wallet data
|
|
345
|
+
*/
|
|
346
|
+
clearCache() {
|
|
347
|
+
this.walletAddress = null;
|
|
348
|
+
this.walletConnected = false;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Payment processing
|
|
354
|
+
*/
|
|
355
|
+
class PaymentManager {
|
|
356
|
+
constructor(gameId, isIframe, messageHandler, walletManager, apiClient) {
|
|
357
|
+
this.apiClient = null;
|
|
358
|
+
this.gameId = gameId;
|
|
359
|
+
this.isIframe = isIframe;
|
|
360
|
+
this.messageHandler = messageHandler;
|
|
361
|
+
this.walletManager = walletManager;
|
|
362
|
+
this.apiClient = apiClient || null;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Pay to play - one-time payment to access game
|
|
366
|
+
* @param amount - Payment amount
|
|
367
|
+
* @param token - Token type: 'SOL', 'USDC', or custom token symbol/mint address
|
|
368
|
+
* @param txSignature - Transaction signature (required for non-iframe mode)
|
|
369
|
+
*/
|
|
370
|
+
async payToPlay(amount, token, txSignature) {
|
|
371
|
+
this.validatePaymentParams(amount, token);
|
|
372
|
+
// Check wallet connection
|
|
373
|
+
const isConnected = await this.walletManager.isWalletConnected();
|
|
374
|
+
if (!isConnected) {
|
|
375
|
+
throw new WalletNotConnectedError();
|
|
376
|
+
}
|
|
377
|
+
const request = {
|
|
378
|
+
amount,
|
|
379
|
+
token,
|
|
380
|
+
type: 'pay_to_play',
|
|
381
|
+
gameId: this.gameId,
|
|
382
|
+
txSignature, // Optional for iframe mode, required for non-iframe
|
|
383
|
+
};
|
|
384
|
+
if (this.isIframe) {
|
|
385
|
+
// Iframe mode: use postMessage
|
|
386
|
+
try {
|
|
387
|
+
const response = await this.messageHandler.sendMessage(MessageType.PAYMENT_REQUEST, request);
|
|
388
|
+
if (!response.success) {
|
|
389
|
+
throw new PaymentFailedError(response.error || 'Payment failed', response.txSignature);
|
|
390
|
+
}
|
|
391
|
+
if (!response.txSignature) {
|
|
392
|
+
throw new PaymentFailedError('Payment succeeded but no transaction signature received');
|
|
393
|
+
}
|
|
394
|
+
if (!response.amount || !response.token || !response.timestamp) {
|
|
395
|
+
throw new PaymentFailedError('Payment succeeded but incomplete payment details received');
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
success: true,
|
|
399
|
+
txSignature: response.txSignature,
|
|
400
|
+
amount: response.amount,
|
|
401
|
+
token: response.token,
|
|
402
|
+
timestamp: response.timestamp,
|
|
403
|
+
purchaseId: response.purchaseId,
|
|
404
|
+
platformFee: response.platformFee,
|
|
405
|
+
developerAmount: response.developerAmount,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
if (error instanceof PaymentFailedError || error instanceof WalletNotConnectedError) {
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
throw new PaymentFailedError(error instanceof Error ? error.message : 'Payment failed');
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
// Non-iframe mode: use API
|
|
417
|
+
if (!this.apiClient) {
|
|
418
|
+
throw new Error('API client not initialized. Ensure apiBaseURL is configured in SDK config.');
|
|
419
|
+
}
|
|
420
|
+
if (!txSignature) {
|
|
421
|
+
throw new PaymentFailedError('Transaction signature is required for non-iframe payments. ' +
|
|
422
|
+
'Please sign the transaction first and provide the signature.');
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
const response = await this.apiClient.sendPaymentRequest(this.gameId, {
|
|
426
|
+
...request,
|
|
427
|
+
txSignature,
|
|
428
|
+
});
|
|
429
|
+
if (!response.success) {
|
|
430
|
+
throw new PaymentFailedError(response.error || 'Payment failed', response.txSignature);
|
|
431
|
+
}
|
|
432
|
+
if (!response.txSignature) {
|
|
433
|
+
throw new PaymentFailedError('Payment succeeded but no transaction signature received');
|
|
434
|
+
}
|
|
435
|
+
if (!response.amount || !response.token || !response.timestamp) {
|
|
436
|
+
throw new PaymentFailedError('Payment succeeded but incomplete payment details received');
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
success: true,
|
|
440
|
+
txSignature: response.txSignature,
|
|
441
|
+
amount: response.amount,
|
|
442
|
+
token: response.token,
|
|
443
|
+
timestamp: response.timestamp,
|
|
444
|
+
purchaseId: response.purchaseId,
|
|
445
|
+
platformFee: response.platformFee,
|
|
446
|
+
developerAmount: response.developerAmount,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
if (error instanceof PaymentFailedError || error instanceof WalletNotConnectedError) {
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
throw new PaymentFailedError(error instanceof Error ? error.message : 'Payment failed');
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Purchase in-game item
|
|
459
|
+
* @param itemId - Item identifier
|
|
460
|
+
* @param amount - Payment amount
|
|
461
|
+
* @param token - Token type: 'SOL', 'USDC', or custom token symbol/mint address
|
|
462
|
+
* @param txSignature - Transaction signature (required for non-iframe mode)
|
|
463
|
+
*/
|
|
464
|
+
async purchaseItem(itemId, amount, token, txSignature) {
|
|
465
|
+
this.validatePaymentParams(amount, token);
|
|
466
|
+
if (!itemId || typeof itemId !== 'string' || itemId.trim().length === 0) {
|
|
467
|
+
throw new Error('Item ID is required and must be a non-empty string');
|
|
468
|
+
}
|
|
469
|
+
// Check wallet connection
|
|
470
|
+
const isConnected = await this.walletManager.isWalletConnected();
|
|
471
|
+
if (!isConnected) {
|
|
472
|
+
throw new WalletNotConnectedError();
|
|
473
|
+
}
|
|
474
|
+
const request = {
|
|
475
|
+
amount,
|
|
476
|
+
token,
|
|
477
|
+
type: 'in_game_purchase',
|
|
478
|
+
gameId: this.gameId,
|
|
479
|
+
itemId: itemId.trim(),
|
|
480
|
+
txSignature, // Optional for iframe mode, required for non-iframe
|
|
481
|
+
};
|
|
482
|
+
if (this.isIframe) {
|
|
483
|
+
// Iframe mode: use postMessage
|
|
484
|
+
try {
|
|
485
|
+
const response = await this.messageHandler.sendMessage(MessageType.PAYMENT_REQUEST, request);
|
|
486
|
+
if (!response.success) {
|
|
487
|
+
throw new PaymentFailedError(response.error || 'Purchase failed', response.txSignature);
|
|
488
|
+
}
|
|
489
|
+
if (!response.txSignature) {
|
|
490
|
+
throw new PaymentFailedError('Purchase succeeded but no transaction signature received');
|
|
491
|
+
}
|
|
492
|
+
if (!response.amount || !response.token || !response.timestamp) {
|
|
493
|
+
throw new PaymentFailedError('Purchase succeeded but incomplete payment details received');
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
success: true,
|
|
497
|
+
txSignature: response.txSignature,
|
|
498
|
+
amount: response.amount,
|
|
499
|
+
token: response.token,
|
|
500
|
+
timestamp: response.timestamp,
|
|
501
|
+
purchaseId: response.purchaseId,
|
|
502
|
+
platformFee: response.platformFee,
|
|
503
|
+
developerAmount: response.developerAmount,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
if (error instanceof PaymentFailedError || error instanceof WalletNotConnectedError) {
|
|
508
|
+
throw error;
|
|
509
|
+
}
|
|
510
|
+
throw new PaymentFailedError(error instanceof Error ? error.message : 'Purchase failed');
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
// Non-iframe mode: use API
|
|
515
|
+
if (!this.apiClient) {
|
|
516
|
+
throw new Error('API client not initialized. Ensure apiBaseURL is configured in SDK config.');
|
|
517
|
+
}
|
|
518
|
+
if (!txSignature) {
|
|
519
|
+
throw new PaymentFailedError('Transaction signature is required for non-iframe purchases. ' +
|
|
520
|
+
'Please sign the transaction first and provide the signature.');
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
const response = await this.apiClient.sendPaymentRequest(this.gameId, {
|
|
524
|
+
...request,
|
|
525
|
+
txSignature,
|
|
526
|
+
});
|
|
527
|
+
if (!response.success) {
|
|
528
|
+
throw new PaymentFailedError(response.error || 'Purchase failed', response.txSignature);
|
|
529
|
+
}
|
|
530
|
+
if (!response.txSignature) {
|
|
531
|
+
throw new PaymentFailedError('Purchase succeeded but no transaction signature received');
|
|
532
|
+
}
|
|
533
|
+
if (!response.amount || !response.token || !response.timestamp) {
|
|
534
|
+
throw new PaymentFailedError('Purchase succeeded but incomplete payment details received');
|
|
535
|
+
}
|
|
536
|
+
return {
|
|
537
|
+
success: true,
|
|
538
|
+
txSignature: response.txSignature,
|
|
539
|
+
amount: response.amount,
|
|
540
|
+
token: response.token,
|
|
541
|
+
timestamp: response.timestamp,
|
|
542
|
+
purchaseId: response.purchaseId,
|
|
543
|
+
platformFee: response.platformFee,
|
|
544
|
+
developerAmount: response.developerAmount,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
if (error instanceof PaymentFailedError || error instanceof WalletNotConnectedError) {
|
|
549
|
+
throw error;
|
|
550
|
+
}
|
|
551
|
+
throw new PaymentFailedError(error instanceof Error ? error.message : 'Purchase failed');
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Validate payment parameters
|
|
557
|
+
*/
|
|
558
|
+
validatePaymentParams(amount, token) {
|
|
559
|
+
if (typeof amount !== 'number' || isNaN(amount) || amount <= 0) {
|
|
560
|
+
throw new InvalidAmountError();
|
|
561
|
+
}
|
|
562
|
+
if (!token || typeof token !== 'string' || token.trim().length === 0) {
|
|
563
|
+
throw new InvalidTokenError();
|
|
564
|
+
}
|
|
565
|
+
// SOL and USDC are always valid
|
|
566
|
+
// Custom tokens will be validated by the backend
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* API Client for non-iframe environments
|
|
572
|
+
* Handles direct REST API calls when SDK is not running in iframe
|
|
573
|
+
*/
|
|
574
|
+
class APIClient {
|
|
575
|
+
constructor(baseURL) {
|
|
576
|
+
this.authToken = null;
|
|
577
|
+
this.walletAddress = null;
|
|
578
|
+
// Use provided base URL or detect from environment
|
|
579
|
+
this.baseURL = baseURL || this.detectBaseURL();
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Detect Arcadia base URL from environment or use default
|
|
583
|
+
*/
|
|
584
|
+
detectBaseURL() {
|
|
585
|
+
// Check for explicit base URL in config
|
|
586
|
+
if (typeof window !== 'undefined' && window.ARCADIA_API_URL) {
|
|
587
|
+
return window.ARCADIA_API_URL;
|
|
588
|
+
}
|
|
589
|
+
// Try to detect from current page (if game is hosted on Arcadia subdomain)
|
|
590
|
+
if (typeof window !== 'undefined') {
|
|
591
|
+
const hostname = window.location.hostname;
|
|
592
|
+
// If on arcadia.com or subdomain, use same origin
|
|
593
|
+
if (hostname.includes('arcadia')) {
|
|
594
|
+
return `${window.location.protocol}//${hostname}`;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
// Default fallback (should be configured by developer)
|
|
598
|
+
return 'https://arcadia.com';
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Set authentication token (from wallet authentication)
|
|
602
|
+
*/
|
|
603
|
+
setAuthToken(token) {
|
|
604
|
+
this.authToken = token;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Set wallet address (for wallet-based auth)
|
|
608
|
+
*/
|
|
609
|
+
setWalletAddress(address) {
|
|
610
|
+
this.walletAddress = address;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Authenticate with wallet
|
|
614
|
+
* Returns auth token for subsequent API calls
|
|
615
|
+
*/
|
|
616
|
+
async authenticateWithWallet(walletAddress, signature, message) {
|
|
617
|
+
const response = await this.request('/api/auth/wallet/connect', {
|
|
618
|
+
method: 'POST',
|
|
619
|
+
body: {
|
|
620
|
+
walletAddress,
|
|
621
|
+
signature,
|
|
622
|
+
message,
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
// Store auth token from response
|
|
626
|
+
// Note: You may need to extract token from response based on your auth implementation
|
|
627
|
+
if (response.token) {
|
|
628
|
+
this.authToken = response.token;
|
|
629
|
+
}
|
|
630
|
+
return response;
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Get wallet address (for non-iframe)
|
|
634
|
+
* Requires authentication
|
|
635
|
+
*/
|
|
636
|
+
async getWalletAddress() {
|
|
637
|
+
if (!this.authToken && !this.walletAddress) {
|
|
638
|
+
return {
|
|
639
|
+
walletAddress: null,
|
|
640
|
+
connected: false,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
// If we have wallet address cached, return it
|
|
644
|
+
if (this.walletAddress) {
|
|
645
|
+
return {
|
|
646
|
+
walletAddress: this.walletAddress,
|
|
647
|
+
connected: true,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
// Otherwise, fetch from profile endpoint
|
|
651
|
+
try {
|
|
652
|
+
const profile = await this.request('/api/profile', {
|
|
653
|
+
method: 'GET',
|
|
654
|
+
});
|
|
655
|
+
if (profile?.walletAddress) {
|
|
656
|
+
this.walletAddress = profile.walletAddress;
|
|
657
|
+
return {
|
|
658
|
+
walletAddress: profile.walletAddress,
|
|
659
|
+
connected: true,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
console.warn('Failed to fetch wallet address:', error);
|
|
665
|
+
}
|
|
666
|
+
return {
|
|
667
|
+
walletAddress: null,
|
|
668
|
+
connected: false,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Send payment request via API
|
|
673
|
+
*/
|
|
674
|
+
async sendPaymentRequest(gameId, request) {
|
|
675
|
+
const endpoint = request.type === 'pay_to_play'
|
|
676
|
+
? `/api/games/${gameId}/pay-to-play`
|
|
677
|
+
: `/api/games/${gameId}/purchase-item`;
|
|
678
|
+
return await this.request(endpoint, {
|
|
679
|
+
method: 'POST',
|
|
680
|
+
body: {
|
|
681
|
+
amount: request.amount,
|
|
682
|
+
token: request.token,
|
|
683
|
+
txSignature: request.txSignature, // App must provide this
|
|
684
|
+
itemId: request.itemId,
|
|
685
|
+
},
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Update playtime/stats
|
|
690
|
+
*/
|
|
691
|
+
async updatePlaytime(gameId, playtimeHours, status) {
|
|
692
|
+
await this.request('/api/library', {
|
|
693
|
+
method: 'POST',
|
|
694
|
+
body: {
|
|
695
|
+
gameId,
|
|
696
|
+
playtimeHours,
|
|
697
|
+
status: status || 'playing',
|
|
698
|
+
},
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Update online status
|
|
703
|
+
*/
|
|
704
|
+
async updateOnlineStatus(isOnline, gameId) {
|
|
705
|
+
await this.request('/api/online-status', {
|
|
706
|
+
method: 'POST',
|
|
707
|
+
body: {
|
|
708
|
+
isOnline,
|
|
709
|
+
gameId,
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Generic request method
|
|
715
|
+
*/
|
|
716
|
+
async request(endpoint, options = {}) {
|
|
717
|
+
const url = `${this.baseURL}${endpoint}`;
|
|
718
|
+
const headers = {
|
|
719
|
+
'Content-Type': 'application/json',
|
|
720
|
+
...options.headers,
|
|
721
|
+
};
|
|
722
|
+
// Add auth token if available
|
|
723
|
+
if (this.authToken) {
|
|
724
|
+
headers['Authorization'] = `Bearer ${this.authToken}`;
|
|
725
|
+
}
|
|
726
|
+
const response = await fetch(url, {
|
|
727
|
+
method: options.method || 'GET',
|
|
728
|
+
headers,
|
|
729
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
730
|
+
});
|
|
731
|
+
if (!response.ok) {
|
|
732
|
+
const error = await response.json().catch(() => ({ error: 'Request failed' }));
|
|
733
|
+
throw new Error(error.error || `Request failed with status ${response.status}`);
|
|
734
|
+
}
|
|
735
|
+
return await response.json();
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Main Arcadia Game SDK class
|
|
741
|
+
*/
|
|
742
|
+
class ArcadiaSDK {
|
|
743
|
+
/**
|
|
744
|
+
* Create new SDK instance
|
|
745
|
+
*/
|
|
746
|
+
constructor(config) {
|
|
747
|
+
this.initialized = false;
|
|
748
|
+
this.apiClient = null;
|
|
749
|
+
// Validate config
|
|
750
|
+
if (!config || !config.gameId || typeof config.gameId !== 'string' || config.gameId.trim().length === 0) {
|
|
751
|
+
throw new InvalidConfigError('gameId is required and must be a non-empty string');
|
|
752
|
+
}
|
|
753
|
+
this.config = {
|
|
754
|
+
gameId: config.gameId.trim(),
|
|
755
|
+
parentOrigin: config.parentOrigin || '*',
|
|
756
|
+
timeout: config.timeout || 30000,
|
|
757
|
+
};
|
|
758
|
+
// Detect iframe environment
|
|
759
|
+
this.isIframe = typeof window !== 'undefined' && window.self !== window.top;
|
|
760
|
+
// Initialize message handler
|
|
761
|
+
this.messageHandler = new MessageHandler(this.config.parentOrigin, this.config.timeout);
|
|
762
|
+
// Initialize managers
|
|
763
|
+
this.walletManager = new WalletManager(this.messageHandler, this.isIframe, this.apiClient);
|
|
764
|
+
this.paymentManager = new PaymentManager(this.config.gameId, this.isIframe, this.messageHandler, this.walletManager, this.apiClient);
|
|
765
|
+
// Set up message listener if in iframe
|
|
766
|
+
if (this.isIframe) {
|
|
767
|
+
this.messageHandler.init();
|
|
768
|
+
this.setupMessageListener();
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
// Initialize API client for non-iframe environments
|
|
772
|
+
this.apiClient = new APIClient(this.config.apiBaseURL);
|
|
773
|
+
// Set auth token if provided
|
|
774
|
+
if (this.config.authToken) {
|
|
775
|
+
this.apiClient.setAuthToken(this.config.authToken);
|
|
776
|
+
}
|
|
777
|
+
// Set wallet address if provided
|
|
778
|
+
if (this.config.walletAddress) {
|
|
779
|
+
this.apiClient.setWalletAddress(this.config.walletAddress);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Initialize SDK and request initialization data from parent (iframe) or API (non-iframe)
|
|
785
|
+
*/
|
|
786
|
+
async init() {
|
|
787
|
+
if (this.initialized) {
|
|
788
|
+
return; // Already initialized
|
|
789
|
+
}
|
|
790
|
+
if (this.isIframe) {
|
|
791
|
+
// Iframe mode: use postMessage
|
|
792
|
+
try {
|
|
793
|
+
// Request initialization from parent
|
|
794
|
+
const initData = await this.messageHandler.sendMessage(MessageType.INIT_REQUEST);
|
|
795
|
+
// Update wallet status from init data
|
|
796
|
+
if (initData.wallet) {
|
|
797
|
+
this.walletManager.updateWalletStatus(initData.wallet);
|
|
798
|
+
}
|
|
799
|
+
// Notify parent that game is ready
|
|
800
|
+
this.messageHandler.sendOneWay(MessageType.GAME_READY);
|
|
801
|
+
this.initialized = true;
|
|
802
|
+
}
|
|
803
|
+
catch (error) {
|
|
804
|
+
// Initialization failed, but SDK can still be used
|
|
805
|
+
// Wallet functions will fetch on demand
|
|
806
|
+
this.initialized = true;
|
|
807
|
+
console.warn('SDK initialization failed:', error);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
else {
|
|
811
|
+
// Non-iframe mode: use API
|
|
812
|
+
if (!this.apiClient) {
|
|
813
|
+
throw new Error('API client not initialized. Ensure apiBaseURL is configured.');
|
|
814
|
+
}
|
|
815
|
+
try {
|
|
816
|
+
// Fetch wallet address from API
|
|
817
|
+
const walletResponse = await this.apiClient.getWalletAddress();
|
|
818
|
+
// Update wallet manager with API response
|
|
819
|
+
this.walletManager.updateWalletStatus({
|
|
820
|
+
connected: walletResponse.connected,
|
|
821
|
+
address: walletResponse.walletAddress,
|
|
822
|
+
});
|
|
823
|
+
this.initialized = true;
|
|
824
|
+
}
|
|
825
|
+
catch (error) {
|
|
826
|
+
// Initialization failed, but SDK can still be used
|
|
827
|
+
this.initialized = true;
|
|
828
|
+
console.warn('SDK API initialization failed:', error);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Get wallet address - use this as user identifier
|
|
834
|
+
* Games should link all save data to this wallet address
|
|
835
|
+
* Returns null if wallet not connected
|
|
836
|
+
*/
|
|
837
|
+
async getWalletAddress() {
|
|
838
|
+
return this.walletManager.getWalletAddress();
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Check if wallet is connected
|
|
842
|
+
*/
|
|
843
|
+
async isWalletConnected() {
|
|
844
|
+
return this.walletManager.isWalletConnected();
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Listen for wallet connection changes
|
|
848
|
+
*/
|
|
849
|
+
onWalletChange(callback) {
|
|
850
|
+
this.walletManager.onWalletChange(callback);
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Remove wallet change listener
|
|
854
|
+
*/
|
|
855
|
+
offWalletChange(callback) {
|
|
856
|
+
this.walletManager.offWalletChange(callback);
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Payment methods
|
|
860
|
+
*/
|
|
861
|
+
get payment() {
|
|
862
|
+
return {
|
|
863
|
+
/**
|
|
864
|
+
* Pay to play - one-time payment to start/access game
|
|
865
|
+
*/
|
|
866
|
+
payToPlay: (amount, token) => {
|
|
867
|
+
return this.paymentManager.payToPlay(amount, token);
|
|
868
|
+
},
|
|
869
|
+
/**
|
|
870
|
+
* In-game purchase - buy items, upgrades, etc.
|
|
871
|
+
*/
|
|
872
|
+
purchaseItem: (itemId, amount, token) => {
|
|
873
|
+
return this.paymentManager.purchaseItem(itemId, amount, token);
|
|
874
|
+
},
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Get SDK configuration
|
|
879
|
+
*/
|
|
880
|
+
getConfig() {
|
|
881
|
+
return { ...this.config };
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Check if SDK is running in iframe
|
|
885
|
+
*/
|
|
886
|
+
isInIframe() {
|
|
887
|
+
return this.isIframe;
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Check if SDK is initialized
|
|
891
|
+
*/
|
|
892
|
+
isInitialized() {
|
|
893
|
+
return this.initialized;
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Cleanup SDK resources
|
|
897
|
+
*/
|
|
898
|
+
destroy() {
|
|
899
|
+
this.messageHandler.destroy();
|
|
900
|
+
this.walletManager.clearCache();
|
|
901
|
+
this.initialized = false;
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Set up message listener for wallet updates
|
|
905
|
+
*/
|
|
906
|
+
setupMessageListener() {
|
|
907
|
+
// Additional listener for wallet updates (not request/response)
|
|
908
|
+
const listener = (event) => {
|
|
909
|
+
// Validate origin if specified
|
|
910
|
+
if (this.config.parentOrigin !== '*' && event.origin !== this.config.parentOrigin) {
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
const message = event.data;
|
|
914
|
+
// Handle wallet update messages
|
|
915
|
+
if (message.type === MessageType.WALLET_UPDATE && message.payload) {
|
|
916
|
+
const walletInfo = message.payload;
|
|
917
|
+
this.walletManager.updateWalletStatus(walletInfo);
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
window.addEventListener('message', listener);
|
|
921
|
+
// Store listener for cleanup (though SDK doesn't typically need cleanup)
|
|
922
|
+
// This is handled by messageHandler.destroy()
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Arcadia Game SDK
|
|
928
|
+
*
|
|
929
|
+
* SDK for integrating Arcadia wallet and payment features into Web3 games.
|
|
930
|
+
* Works in iframe environments by communicating with parent window via postMessage.
|
|
931
|
+
*/
|
|
932
|
+
// Main SDK class
|
|
933
|
+
|
|
934
|
+
exports.APIClient = APIClient;
|
|
935
|
+
exports.ArcadiaSDK = ArcadiaSDK;
|
|
936
|
+
exports.ArcadiaSDKError = ArcadiaSDKError;
|
|
937
|
+
exports.InvalidAmountError = InvalidAmountError;
|
|
938
|
+
exports.InvalidConfigError = InvalidConfigError;
|
|
939
|
+
exports.InvalidTokenError = InvalidTokenError;
|
|
940
|
+
exports.NotInIframeError = NotInIframeError;
|
|
941
|
+
exports.PaymentFailedError = PaymentFailedError;
|
|
942
|
+
exports.TimeoutError = TimeoutError;
|
|
943
|
+
exports.WalletNotConnectedError = WalletNotConnectedError;
|
|
944
|
+
exports.default = ArcadiaSDK;
|
|
945
|
+
|
|
946
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
947
|
+
|
|
948
|
+
}));
|
|
949
|
+
//# sourceMappingURL=arcadia-game-sdk.js.map
|