@arken/seer-protocol 0.1.0 → 0.1.1

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.
Files changed (41) hide show
  1. package/{src/modules/evolution → evolution}/evolution.models.ts +1 -1
  2. package/{src/modules/evolution → evolution}/evolution.router.ts +9 -9
  3. package/evolution/evolution.schema.ts +1 -0
  4. package/{src/modules/evolution → evolution}/evolution.types.ts +1 -1
  5. package/{src/index.ts → index.ts} +2 -2
  6. package/{src/modules/infinite → infinite}/infinite.models.ts +1 -1
  7. package/{src/modules/infinite → infinite}/infinite.router.ts +9 -9
  8. package/infinite/infinite.schema.ts +1 -0
  9. package/{src/modules/infinite → infinite}/infinite.types.ts +1 -1
  10. package/{src/modules/isles → isles}/isles.models.ts +1 -1
  11. package/{src/modules/isles → isles}/isles.router.ts +9 -9
  12. package/isles/isles.schema.ts +1 -0
  13. package/{src/modules/isles → isles}/isles.types.ts +1 -1
  14. package/{src/modules/oasis → oasis}/oasis.models.ts +1 -1
  15. package/{src/modules/oasis → oasis}/oasis.router.ts +2 -2
  16. package/oasis/oasis.schema.ts +1 -0
  17. package/{src/modules/oasis → oasis}/oasis.types.ts +1 -1
  18. package/package.json +3 -12
  19. package/{src/router.ts → router.ts} +23 -23
  20. package/trek/trek.models.ts +1 -0
  21. package/{src/modules/trek → trek}/trek.router.ts +1 -1
  22. package/trek/trek.schema.ts +1 -0
  23. package/trek/trek.types.ts +1 -0
  24. package/src/modules/evolution/evolution.schema.ts +0 -1
  25. package/src/modules/evolution/evolution.service.ts +0 -2000
  26. package/src/modules/infinite/infinite.schema.ts +0 -1
  27. package/src/modules/infinite/infinite.service.ts +0 -40
  28. package/src/modules/isles/isles.schema.ts +0 -1
  29. package/src/modules/isles/isles.service.ts +0 -40
  30. package/src/modules/oasis/oasis.schema.ts +0 -1
  31. package/src/modules/oasis/oasis.service.ts +0 -38
  32. package/src/modules/trek/trek.models.ts +0 -1
  33. package/src/modules/trek/trek.schema.ts +0 -1
  34. package/src/modules/trek/trek.service.ts +0 -1031
  35. package/src/modules/trek/trek.types.ts +0 -1
  36. /package/{src/modules/evolution → evolution}/index.ts +0 -0
  37. /package/{src/modules/infinite → infinite}/index.ts +0 -0
  38. /package/{src/modules/isles → isles}/index.ts +0 -0
  39. /package/{src/modules/oasis → oasis}/index.ts +0 -0
  40. /package/{src/modules/trek → trek}/index.ts +0 -0
  41. /package/{src/types.ts → types.ts} +0 -0
@@ -1,2000 +0,0 @@
1
- // packages/seer/packages/protocol/src/modules/evolution/evolution.service.ts
2
- //
3
- import type { RouterContext, RouterInput, RouterOutput } from './evolution.types';
4
- import * as Arken from '@arken/node';
5
- import { generateShortId } from '@arken/node/util/db';
6
- import contractInfo from '@arken/node/legacy/contractInfo';
7
- import { ARXError } from '@arken/node/util/rpc';
8
- import { iterateBlocks, getAddress, getSignedRequest } from '@arken/node/util/web3';
9
- import { sleep } from '@arken/node/util/time';
10
- import * as ethers from 'ethers';
11
- import Web3 from 'web3';
12
- import dayjs from 'dayjs';
13
- import { getTime, log, logError, random, toLong } from '@arken/node/util';
14
- import { getFilter } from '@arken/node/util/api';
15
- import { getTokenIdFromItem, normalizeItem } from '@arken/node/util/decoder';
16
- import { awaitEnter } from '@arken/node/util/process';
17
- import { Schema, serialize } from 'borsh';
18
- import get from 'lodash/get';
19
- import set from 'lodash/set';
20
- import type { PatchOp, EntityPatch } from '@arken/node/types';
21
- import { applyPatchesWithInventoryViaMail } from '@arken/node/modules/core/mail/applyPatchesOrMail';
22
-
23
- // -----------------------
24
- // Clean patch architecture
25
- // -----------------------
26
-
27
- function hasPerm(perms: Record<string, any> | undefined, key: string): boolean {
28
- if (!perms) return false;
29
- return !!perms[key];
30
- }
31
-
32
- /**
33
- * Generic, future-proof permission gate:
34
- * - Broad: `${target}.write`
35
- * - Prefix: `${target}.write:<prefix>`
36
- *
37
- * Examples:
38
- * - character.data.write
39
- * - character.data.write:character.quest.evolution.
40
- * - profile.meta.write:meta.reputation.
41
- */
42
- function hasWriteAccess(perms: Record<string, any> | undefined, target: string, path: string) {
43
- if (hasPerm(perms, `${target}.write`)) return true;
44
-
45
- const entries = Object.keys(perms || {});
46
- const wantedPrefix = `${target}.write:`;
47
- for (const p of entries) {
48
- if (!p.startsWith(wantedPrefix)) continue;
49
- const allowedPrefix = p.slice(wantedPrefix.length);
50
- if (path.startsWith(allowedPrefix)) return true;
51
- }
52
- return false;
53
- }
54
-
55
- function applyPatchToObject(obj: any, patch: PatchOp[]) {
56
- for (const p of patch) {
57
- if (p.op === 'set') {
58
- set(obj, p.key, p.value);
59
- } else if (p.op === 'unset') {
60
- // lodash unset is separate; simplest:
61
- set(obj, p.key, undefined);
62
- } else if (p.op === 'inc') {
63
- const cur = Number(get(obj, p.key)) || 0;
64
- set(obj, p.key, cur + Number(p.value || 0));
65
- } else if (p.op === 'push') {
66
- const cur = get(obj, p.key);
67
- const arr = Array.isArray(cur) ? cur : [];
68
- arr.push(p.value);
69
- set(obj, p.key, arr);
70
- } else if (p.op === 'merge') {
71
- const cur = get(obj, p.key);
72
- const base = cur && typeof cur === 'object' ? cur : {};
73
- set(obj, p.key, { ...base, ...(p.value || {}) });
74
- }
75
- }
76
- }
77
-
78
- /**
79
- * Canonical target namespace used for permission checks.
80
- * We keep this intentionally simple and stringly-typed.
81
- */
82
- function entityPatchTarget(patch: EntityPatch): string {
83
- const t = patch.entityType || '';
84
- // recommended canonical values:
85
- // - 'profile.meta'
86
- // - 'character.data'
87
- // - 'character.inventory'
88
- // but allow any string
89
- return t;
90
- }
91
-
92
- /**
93
- * Inventory normalization is NOT legacy—it's required because shards cannot read mongo.
94
- * Canonical contract:
95
- * - shard may push inventory items as `{ itemKey, x, y }`
96
- * - seer resolves itemKey -> itemId and stores `{ itemId, x, y }`
97
- */
98
- async function normalizeInventoryPatch(ctx: RouterContext, patch: PatchOp[]): Promise<PatchOp[]> {
99
- const out: PatchOp[] = [];
100
-
101
- for (const p of patch) {
102
- if (p.op === 'push' && (p.key === 'inventory.0.items' || p.key.startsWith('inventory.0.items'))) {
103
- const v: any = p.value || {};
104
- if (v.itemId) {
105
- out.push(p);
106
- continue;
107
- }
108
-
109
- if (v.itemKey) {
110
- const item = await ctx.app.model.Item.findOne({ key: v.itemKey }).exec();
111
- if (!item) {
112
- // Fail hard: itemKey should be authoritative content
113
- throw new Error(`Unknown itemKey in inventory patch: ${v.itemKey}`);
114
- }
115
-
116
- out.push({
117
- op: 'push',
118
- key: p.key,
119
- value: {
120
- itemId: item._id,
121
- x: v.x ?? 1,
122
- y: v.y ?? 1,
123
- },
124
- });
125
- continue;
126
- }
127
- }
128
-
129
- out.push(p);
130
- }
131
-
132
- return out;
133
- }
134
-
135
- function assertEntityPatch(op: any): asserts op is EntityPatch {
136
- if (!op || typeof op !== 'object') throw new Error('Invalid op');
137
- if (typeof op.entityType !== 'string') throw new Error('Invalid op.entityType');
138
- if (typeof op.entityId !== 'string') throw new Error('Invalid op.entityId');
139
- if (!Array.isArray(op.ops)) throw new Error('Invalid op.ops');
140
- }
141
-
142
- // -----------------------
143
- // Existing constants (unchanged)
144
- // -----------------------
145
-
146
- const allowedAdminAddresses = [
147
- '0xa987f487639920A3c2eFe58C8FBDedB96253ed9B',
148
- '0x82b644E1B2164F5B81B3e7F7518DdE8E515A419d',
149
- '0xD934BAD7bCaAfdfdEbad863Bc9964B82197cBCc3',
150
- ];
151
-
152
- const usdPairs = ['busd', 'usdc', 'usdt'];
153
-
154
- const contractAddressToKey: any = {};
155
- for (const contractKey of Object.keys(contractInfo)) {
156
- contractAddressToKey[(contractInfo as any)[contractKey][56]] = contractKey;
157
- }
158
-
159
- // TODO: check history, reasonable amount? does it deplete like it should?
160
- function isClaimProblem(payment) {
161
- if (payment.owner.name === 'goldenlion') {
162
- return 'Claim seems suspicious - contact support';
163
- }
164
-
165
- const allowedItemKeys = ['zavox'];
166
-
167
- for (const index in payment.meta.tokenAmounts) {
168
- const tokenAmount = payment.meta.tokenAmounts[index];
169
- const tokenKey = payment.meta.tokenKeys[index];
170
-
171
- if (!payment.owner.meta.rewards || payment.owner.meta.rewards.tokens[tokenKey] < tokenAmount) {
172
- return 'Profile does not have enough funds for this payment';
173
- }
174
- }
175
-
176
- for (const index in payment.meta.tokenIds) {
177
- const item = payment.meta.tokenIds[index];
178
- const reward = payment.owner.meta.rewards?.items?.[item.rewardId];
179
-
180
- if (!allowedAdminAddresses.includes(payment.owner.address)) {
181
- return 'Invalid request';
182
- }
183
- }
184
-
185
- for (const index in payment.meta.itemIds) {
186
- const item = payment.meta.itemIds[index];
187
- const reward = payment.owner.meta.rewards?.items?.[item.rewardId];
188
-
189
- if (!allowedItemKeys.includes(item.id) && !allowedAdminAddresses.includes(payment.owner.address)) {
190
- return 'Invalid req';
191
- }
192
- if (!reward || reward.rarity !== item.rewardRarity || reward.name !== item.rewardName) {
193
- return 'Profile does not have a matching reward in database';
194
- }
195
- }
196
- }
197
-
198
- interface ItemData {
199
- tokenAddresses: string[];
200
- tokenAmounts: string[];
201
- tokenIds: string[];
202
- itemIds: string[];
203
- purportedSigner: string;
204
- to: string;
205
- nonce: string;
206
- expiry: string;
207
- requestId: string;
208
- }
209
-
210
- function getTokenIdsFromItems(requestedItems) {
211
- const tokenIds = requestedItems.map((requestedItem) => {
212
- return getTokenIdFromItem(requestedItem, random(0, 999));
213
- });
214
- return tokenIds;
215
- }
216
-
217
- export class Service {
218
- // -----------------------
219
- // (unchanged methods above saveRound)
220
- // -----------------------
221
-
222
- async updateConfig(input: RouterInput['updateConfig'], ctx: RouterContext): Promise<RouterOutput['updateConfig']> {
223
- /* unchanged */
224
- if (!input) throw new Error('Input should not be void');
225
- const game = await ctx.app.model.Game.findOne({ key: input.gameKey });
226
- for (const key in input) {
227
- console.log('Setting', key, input[key]);
228
- game.meta[key] = input[key];
229
- }
230
- game.meta = { ...game.meta };
231
- game.markModified('meta');
232
- await game.save();
233
- return game.meta;
234
- }
235
-
236
- async updateGameStats(
237
- input: RouterInput['updateGameStats'],
238
- ctx: RouterContext
239
- ): Promise<RouterOutput['updateGameStats']> {
240
- /* unchanged */
241
- const { Game, GameStat } = ctx.app.model;
242
- const game = await Game.findOne({ key: 'evolution' }).populate('stat').exec();
243
- let latestRecord = game.stat;
244
- try {
245
- if (latestRecord) {
246
- const dayAgo = dayjs().subtract(1, 'day');
247
- if (!dayjs(latestRecord.createdDate).isAfter(dayAgo)) {
248
- latestRecord = undefined;
249
- }
250
- }
251
- } catch (e) {
252
- console.log('Error getting latest stat record', e);
253
- }
254
-
255
- const meta: any = { clientCount: game.meta.clientCount };
256
-
257
- if (latestRecord) {
258
- await GameStat.updateOne({ _id: latestRecord.id }, { ...latestRecord, meta }).exec();
259
- } else {
260
- const gameStat = await GameStat.create({ gameId: game.id, meta });
261
- game.statId = gameStat.id;
262
- }
263
-
264
- game.meta = { ...game.meta };
265
- await game.save();
266
- }
267
-
268
- // -----------------------
269
- // ✅ CLEAN saveRound
270
- // -----------------------
271
- /**
272
- * Clean contract:
273
- * - input.round.clients[].ops is EntityPatch[]
274
- * - each client also includes permissions snapshot used for gating
275
- * - seer applies entity patches to mongo with dedupe per profile.meta.opIds
276
- *
277
- * Notes:
278
- * - shard is responsible for validation, quest logic, ordering checks, etc.
279
- * - seer is responsible for:
280
- * - permission gate
281
- * - dedupe (op id)
282
- * - persistence
283
- */
284
-
285
- // {
286
- // "shardId": "676cca983a7ff7b07727361a",
287
- // "round": {
288
- // "id": "676ccad6040be4e1feb4fb07",
289
- // "startedAt": 1735183062,
290
- // "endedAt": 1735183092,
291
- // "clients": [
292
- // {
293
- // "id": "sa00CgPFKF606oOSAAAD",
294
- // "name": "returnportal",
295
- // "joinedRoundAt": 1735183062215,
296
- // "points": 0,
297
- // "kills": 0,
298
- // "killStreak": 0,
299
- // "deaths": 0,
300
- // "evolves": 1,
301
- // "rewards": 0,
302
- // "orbs": 0,
303
- // "powerups": 10,
304
- // "baseSpeed": 0.8,
305
- // "decayPower": 1,
306
- // "pickups": [],
307
- // "xp": 22.560000000000606,
308
- // "maxHp": 100,
309
- // "avatar": 0,
310
- // "speed": 3.2,
311
- // "cameraSize": 2.5,
312
- // "log": {
313
- // "kills": [],
314
- // "deaths": [],
315
- // "revenge": 0,
316
- // "resetPosition": 0,
317
- // "phases": 0,
318
- // "stuck": 0,
319
- // "collided": 0,
320
- // "timeoutDisconnect": 0,
321
- // "speedProblem": 0,
322
- // "clientDistanceProblem": 0,
323
- // "outOfBounds": 0,
324
- // "ranOutOfHealth": 0,
325
- // "notReallyTrying": 0,
326
- // "tooManyKills": 0,
327
- // "killingThemselves": 0,
328
- // "sameNetworkDisconnect": 0,
329
- // "connectedTooSoon": 0,
330
- // "clientDisconnected": 0,
331
- // "positionJump": 0,
332
- // "pauses": 0,
333
- // "connects": 0,
334
- // "path": "",
335
- // "positions": 368,
336
- // "spectating": 0,
337
- // "recentJoinProblem": 0,
338
- // "usernameProblem": 0,
339
- // "maintenanceJoin": 0,
340
- // "signatureProblem": 0,
341
- // "signinProblem": 0,
342
- // "versionProblem": 0,
343
- // "failedRealmCheck": 0,
344
- // "addressProblem": 0,
345
- // "replay": []
346
- // }
347
- // }
348
- // ],
349
- // "events": [],
350
- // "states": []
351
- // },
352
- // "rewardWinnerAmount": 100,
353
- // "lastClients": [
354
- // {
355
- // "id": "7qYr9V8pTyZtbUlRAAAB",
356
- // "name": "Unknown812",
357
- // "joinedRoundAt": 1735183032188,
358
- // "points": 0,
359
- // "kills": 0,
360
- // "killStreak": 0,
361
- // "deaths": 0,
362
- // "evolves": 0,
363
- // "rewards": 0,
364
- // "orbs": 0,
365
- // "powerups": 0,
366
- // "baseSpeed": 0.8,
367
- // "decayPower": 1,
368
- // "pickups": [],
369
- // "xp": 50,
370
- // "maxHp": 100,
371
- // "avatar": 0,
372
- // "speed": 2.4,
373
- // "cameraSize": 3,
374
- // "log": {
375
- // "kills": [],
376
- // "deaths": [],
377
- // "revenge": 0,
378
- // "resetPosition": 0,
379
- // "phases": 0,
380
- // "stuck": 0,
381
- // "collided": 0,
382
- // "timeoutDisconnect": 0,
383
- // "speedProblem": 0,
384
- // "clientDistanceProblem": 0,
385
- // "outOfBounds": 0,
386
- // "ranOutOfHealth": 0,
387
- // "notReallyTrying": 0,
388
- // "tooManyKills": 0,
389
- // "killingThemselves": 0,
390
- // "sameNetworkDisconnect": 0,
391
- // "connectedTooSoon": 0,
392
- // "clientDisconnected": 0,
393
- // "positionJump": 0,
394
- // "pauses": 0,
395
- // "connects": 0,
396
- // "path": "",
397
- // "positions": 0,
398
- // "spectating": 0,
399
- // "recentJoinProblem": 0,
400
- // "usernameProblem": 0,
401
- // "maintenanceJoin": 0,
402
- // "signatureProblem": 0,
403
- // "signinProblem": 0,
404
- // "versionProblem": 0,
405
- // "failedRealmCheck": 0,
406
- // "addressProblem": 0,
407
- // "replay": []
408
- // },
409
- // "shardId": "676cca983a7ff7b07727361a"
410
- // },
411
- // {
412
- // "id": "sa00CgPFKF606oOSAAAD",
413
- // "name": "returnportal",
414
- // "joinedRoundAt": 1735183032189,
415
- // "points": 68,
416
- // "kills": 0,
417
- // "killStreak": 0,
418
- // "deaths": 0,
419
- // "evolves": 7,
420
- // "rewards": 1,
421
- // "orbs": 0,
422
- // "powerups": 56,
423
- // "baseSpeed": 0.8,
424
- // "decayPower": 1,
425
- // "pickups": [
426
- // {
427
- // "type": "token",
428
- // "symbol": "pepe",
429
- // "quantity": 1,
430
- // "rewardItemType": 0,
431
- // "id": "676ccaa32b9c5454607eaa05",
432
- // "enabledDate": 1735183011161,
433
- // "rewardItemName": "pepe",
434
- // "position": {
435
- // "x": -9.420004,
436
- // "y": -6.517404
437
- // },
438
- // "winner": "returnportal"
439
- // }
440
- // ],
441
- // "xp": 93.60000000000002,
442
- // "maxHp": 100,
443
- // "avatar": 1,
444
- // "speed": 2.4,
445
- // "cameraSize": 3,
446
- // "log": {
447
- // "kills": [],
448
- // "deaths": [],
449
- // "revenge": 0,
450
- // "resetPosition": 0,
451
- // "phases": 0,
452
- // "stuck": 0,
453
- // "collided": 0,
454
- // "timeoutDisconnect": 0,
455
- // "speedProblem": 0,
456
- // "clientDistanceProblem": 0,
457
- // "outOfBounds": 0,
458
- // "ranOutOfHealth": 0,
459
- // "notReallyTrying": 0,
460
- // "tooManyKills": 0,
461
- // "killingThemselves": 0,
462
- // "sameNetworkDisconnect": 0,
463
- // "connectedTooSoon": 0,
464
- // "clientDisconnected": 0,
465
- // "positionJump": 0,
466
- // "pauses": 0,
467
- // "connects": 0,
468
- // "path": "",
469
- // "positions": 368,
470
- // "spectating": 0,
471
- // "recentJoinProblem": 0,
472
- // "usernameProblem": 0,
473
- // "maintenanceJoin": 0,
474
- // "signatureProblem": 0,
475
- // "signinProblem": 0,
476
- // "versionProblem": 0,
477
- // "failedRealmCheck": 0,
478
- // "addressProblem": 0,
479
- // "replay": []
480
- // },
481
- // "shardId": "676cca983a7ff7b07727361a"
482
- // }
483
- // ]
484
- // }
485
- async saveRound(input: RouterInput['saveRound'], ctx: RouterContext): Promise<RouterOutput['saveRound']> {
486
- if (!input) throw new Error('Input should not be void');
487
-
488
- console.log('Evolution.Service.saveRound', input);
489
-
490
- // realm/shard identity check (keep yours)
491
- if (!ctx.client?.roles?.includes('admin')) throw new Error('Not authorized');
492
-
493
- const game = await ctx.app.model.Game.findOne({ key: input.gameKey }).exec();
494
- if (!game) throw new Error('Game not found');
495
-
496
- if (input.round.id !== game.meta.roundId) throw new Error('Invalid Round ID');
497
-
498
- const session = await ctx.app.db.mongoose.startSession();
499
- session.startTransaction();
500
-
501
- try {
502
- const oldGameRound = await ctx.app.model.GameRound.findById(input.round.id).exec();
503
- if (oldGameRound) {
504
- oldGameRound.meta = input.round;
505
- oldGameRound.markModified('meta');
506
- await oldGameRound.save();
507
- } else {
508
- log('Could not find game round: ', input.round.id);
509
- }
510
-
511
- const newGameRound = await ctx.app.model.GameRound.create({ gameId: game.id, meta: input.round });
512
-
513
- game.meta = {
514
- ...game.meta,
515
- clientCount: input.round.clients.length,
516
- roundId: newGameRound._id.toString(),
517
- };
518
- game.markModified('meta');
519
- await game.save();
520
-
521
- const res = { roundId: game.meta.roundId };
522
-
523
- // ----- rewards table (unchanged) -----
524
- if (input.round.clients.length > 0) {
525
- const rewardWinnerMap = {
526
- 0: Math.round(game.meta.rewardWinnerAmount * 1 * 1000) / 1000,
527
- 1: Math.round(game.meta.rewardWinnerAmount * 0.25 * 1000) / 1000,
528
- 2: Math.round(game.meta.rewardWinnerAmount * 0.15 * 1000) / 1000,
529
- 3: Math.round(game.meta.rewardWinnerAmount * 0.05 * 1000) / 1000,
530
- 4: Math.round(game.meta.rewardWinnerAmount * 0.05 * 1000) / 1000,
531
- 5: Math.round(game.meta.rewardWinnerAmount * 0.05 * 1000) / 1000,
532
- 6: Math.round(game.meta.rewardWinnerAmount * 0.05 * 1000) / 1000,
533
- 7: Math.round(game.meta.rewardWinnerAmount * 0.05 * 1000) / 1000,
534
- 8: Math.round(game.meta.rewardWinnerAmount * 0.05 * 1000) / 1000,
535
- 9: Math.round(game.meta.rewardWinnerAmount * 0.05 * 1000) / 1000,
536
- };
537
-
538
- const winners = input.round.clients.sort((a, b) => b.points - a.points);
539
-
540
- // iterate clients, save rewards + apply ops
541
- for (const client of input.round.clients as any[]) {
542
- if (!client?.address) continue;
543
-
544
- const winnerIndex = winners.findIndex((w: any) => w.address === client.address);
545
-
546
- const profile = await ctx.app.model.Profile.findOne({ address: client.address })
547
- .populate('characters')
548
- .exec();
549
- if (!profile) continue;
550
-
551
- const character = profile.characters?.[0];
552
-
553
- // ✅ IMPORTANT: permission snapshot comes from shard payload, not ctx.client (realm admin)
554
- const perms: Record<string, any> = client.permissions || {};
555
-
556
- // dedupe store
557
- if (!profile.meta) profile.meta = {};
558
- if (!Array.isArray(profile.meta.opIds)) profile.meta.opIds = [];
559
- const applied = new Set<string>(profile.meta.opIds);
560
-
561
- // ─────────────────────────────────────────────
562
- // 1) Collect ALL allowed shard patches (deduped) into ONE claim bundle
563
- // ─────────────────────────────────────────────
564
- const claimableOps: EntityPatch[] = [];
565
- const ops: EntityPatch[] = Array.isArray(client.ops) ? client.ops : [];
566
-
567
- for (const rawOp of ops) {
568
- assertEntityPatch(rawOp);
569
-
570
- const opId = `${rawOp.entityType}:${rawOp.entityId}:${rawOp.baseVersion ?? 'na'}:${JSON.stringify(
571
- rawOp.ops
572
- )}`;
573
- if (applied.has(opId)) continue;
574
-
575
- const target = entityPatchTarget(rawOp);
576
-
577
- // Permission gate: every key must be allowed
578
- let allowed = true;
579
- for (const p of rawOp.ops) {
580
- const path = p.key;
581
- if (!hasWriteAccess(perms, target, path)) {
582
- allowed = false;
583
- break;
584
- }
585
- }
586
- if (!allowed) continue;
587
-
588
- // mark applied (so retry won't re-mail)
589
- profile.meta.opIds.push(opId);
590
- applied.add(opId);
591
-
592
- // ✅ mail-gate any patch (claimable)
593
- claimableOps.push({ ...rawOp, claimable: true });
594
- }
595
-
596
- // bound dedupe list
597
- profile.meta.opIds = profile.meta.opIds.slice(-2000);
598
-
599
- // ─────────────────────────────────────────────
600
- // 2) Immediate stats (unchanged behavior)
601
- // ─────────────────────────────────────────────
602
- if (!profile.meta.ap) profile.meta.ap = 0;
603
- if (!profile.meta.bp) profile.meta.bp = 0;
604
-
605
- profile.meta.ap += client.powerups || 0;
606
- profile.meta.bp += client.kills || 0;
607
-
608
- profile.markModified('meta');
609
- await profile.save();
610
-
611
- // ─────────────────────────────────────────────
612
- // 3) Token rewards → add as a claimable patch (bundled into same mail)
613
- // ─────────────────────────────────────────────
614
- const tokenGrants: Record<string, number> = {};
615
- const pepeGrant = winnerIndex <= 9 ? rewardWinnerMap[winnerIndex] : 0;
616
- if (pepeGrant > 0) tokenGrants['pepe'] = (tokenGrants['pepe'] ?? 0) + pepeGrant;
617
-
618
- for (const pickup of client.pickups || []) {
619
- if (pickup.type === 'token') {
620
- const tokenSymbol = String(pickup.rewardItemName || '').toLowerCase();
621
-
622
- if (!game.meta.rewards.tokens.find((t: any) => t.symbol === tokenSymbol)) {
623
- throw new Error('Problem finding a reward token');
624
- }
625
-
626
- const qty = Number(pickup.quantity || 0);
627
- if (qty > 0) tokenGrants[tokenSymbol] = (tokenGrants[tokenSymbol] ?? 0) + qty;
628
- } else if (pickup.name === 'Santa Christmas Ticket') {
629
- const year = new Date().getFullYear();
630
- const k = `christmas${year}`;
631
- tokenGrants[k] = (tokenGrants[k] ?? 0) + 1;
632
- }
633
- }
634
-
635
- const rewardOps: PatchOp[] = Object.entries(tokenGrants)
636
- .filter(([, amt]) => Number(amt) > 0)
637
- .map(([sym, amt]) => ({ op: 'inc', key: `rewards.tokens.${sym}`, value: Number(amt) }));
638
-
639
- if (rewardOps.length > 0) {
640
- claimableOps.push({
641
- entityType: 'profile.meta',
642
- entityId: profile._id.toString(),
643
- claimable: true,
644
- ops: rewardOps,
645
- });
646
- }
647
-
648
- // ─────────────────────────────────────────────
649
- // 4) Send ONE message per client if anything claimable exists
650
- // ─────────────────────────────────────────────
651
- if (claimableOps.length > 0) {
652
- const tokenSummary = Object.entries(tokenGrants)
653
- .filter(([, amt]) => Number(amt) > 0)
654
- .map(([sym, amt]) => `${amt} ${sym}`)
655
- .join(', ');
656
-
657
- const title = 'Round Rewards';
658
- const bodyParts: string[] = [];
659
- if (rewardOps.length > 0) bodyParts.push(`Rewards: ${tokenSummary}`);
660
- if (claimableOps.length > (rewardOps.length > 0 ? 1 : 0))
661
- bodyParts.push(`Updates: ${claimableOps.length} patch(es)`);
662
- const body = bodyParts.join(' • ') || `Round updates ready to claim.`;
663
-
664
- await applyPatchesWithInventoryViaMail({
665
- ctx,
666
- profile,
667
- character,
668
- patches: claimableOps,
669
- mail: {
670
- profileId: profile._id.toString(),
671
- kind: 'mail',
672
- conversationKey: 'battles',
673
- source: 'evolution.round.rewards',
674
- title,
675
- body,
676
- ui: rewardOps.length
677
- ? {
678
- rewards: Object.entries(tokenGrants)
679
- .filter(([, amt]) => Number(amt) > 0)
680
- .map(([sym, amt]) => ({ type: 'token', id: sym, qty: Number(amt) })),
681
- }
682
- : undefined,
683
- // ✅ one dedupe key per client per round bundle
684
- dedupeKey: `evolution:round:${input.round.id}:${client.address}:rewards`,
685
- },
686
- });
687
- }
688
- }
689
-
690
- await this.processWorldRecords(input, ctx);
691
- } else {
692
- console.log('No clients this round.');
693
- }
694
-
695
- await session.commitTransaction();
696
- return res;
697
- } catch (error) {
698
- await session.abortTransaction();
699
- throw error;
700
- } finally {
701
- session.endSession();
702
- }
703
- }
704
-
705
- async processWorldRecords(input: RouterInput['saveRound'], ctx: RouterContext): Promise<RouterOutput['saveRound']> {
706
- /* unchanged (your existing implementation) */
707
- if (!input) throw new Error('Input should not be void');
708
-
709
- console.log('Evolution.Service.processWorldRecords', input);
710
-
711
- const game = await ctx.app.model.Game.findOne({ key: input.gameKey });
712
-
713
- {
714
- const record = await ctx.app.model.WorldRecord.findOne({
715
- gameId: game.id,
716
- name: `${input.round.gameMode} Points`,
717
- })
718
- .sort({ score: -1 })
719
- .limit(1);
720
-
721
- const winner = input.round.clients.sort((a, b) => b.points - a.points)[0];
722
- const profile = await ctx.app.model.Profile.findOne({ address: winner.address });
723
-
724
- if (!record || winner.points > record.score) {
725
- await ctx.app.model.WorldRecord.create({
726
- gameId: game.id,
727
- holderId: profile.id,
728
- score: winner.points,
729
- });
730
- }
731
- }
732
-
733
- {
734
- const record = await ctx.app.model.WorldRecord.findOne({ gameId: game.id, name: 'Highest Score' })
735
- .sort({ score: -1 })
736
- .limit(1);
737
-
738
- const winner = input.round.clients.sort((a, b) => b.points - a.points)[0];
739
- const profile = await ctx.app.model.Profile.findOne({ address: winner.address });
740
-
741
- if (!record || winner.points > record.score) {
742
- await ctx.app.model.WorldRecord.create({
743
- gameId: game.id,
744
- holderId: profile.id,
745
- score: winner.points,
746
- });
747
- }
748
- }
749
-
750
- {
751
- const record = await ctx.app.model.WorldRecord.findOne({ gameId: game.id, name: 'Most Kills' })
752
- .sort({ score: -1 })
753
- .limit(1);
754
-
755
- const winner = input.round.clients.sort((a, b) => b.kills - a.kills)[0];
756
- const profile = await ctx.app.model.Profile.findOne({ address: winner.address });
757
-
758
- if (!record || winner.kills > record.score) {
759
- await ctx.app.model.WorldRecord.create({
760
- gameId: game.id,
761
- holderId: profile.id,
762
- score: winner.kills,
763
- });
764
- }
765
- }
766
-
767
- return { roundId: game.meta.roundId };
768
- }
769
-
770
- async interact(input: RouterInput['interact'], ctx: RouterContext): Promise<RouterOutput['interact']> {
771
- console.log('Evolution.Service.interact', input);
772
- }
773
-
774
- async getScene(input: RouterInput['getScene'], ctx: RouterContext): Promise<RouterOutput['getScene']> {
775
- /* unchanged */
776
- if (!input) throw new Error('Input should not be void');
777
- console.log('Evolution.Service.getScene', input);
778
-
779
- let data = {};
780
-
781
- if (input.applicationId === '668e4e805f9a03927caf883b') {
782
- data = {
783
- ...data,
784
- objects: [
785
- {
786
- id: 'axl',
787
- file: 'axl.fbx',
788
- position: { x: 1000, y: 1000, z: 1000 },
789
- },
790
- ] as unknown as Arken.Core.Types.Object,
791
- };
792
- }
793
-
794
- return data as any;
795
- }
796
-
797
- // -----------------------
798
- // (all other methods below unchanged: payments, chest, parties, etc.)
799
- // -----------------------
800
-
801
- // async updateConfig(input: RouterInput['updateConfig'], ctx: RouterContext): Promise<RouterOutput['updateConfig']> {
802
- // if (!input) throw new Error('Input should not be void');
803
-
804
- // const game = await ctx.app.model.Game.findOne({ key: input.gameKey });
805
-
806
- // // if (!game.meta)
807
- // // game.meta = {
808
- // // noDecay: false,
809
- // // isBattleRoyale: false,
810
- // // roundLoopSeconds: 0,
811
- // // totalLegitPlayers: 0,
812
- // // checkPositionDistance: 0,
813
- // // baseSpeed: 0,
814
- // // maxClients: 100,
815
- // // rewardItemAmount: 1,
816
- // // mapBoundary: { x: { min: 0, max: 0 }, y: { min: 0, max: 0 } },
817
- // // orbTimeoutSeconds: 0,
818
- // // preventBadKills: false,
819
- // // rewardWinnerAmountMax: 300,
820
- // // maxEvolves: 0,
821
- // // spawnBoundary1: { x: { min: 0, max: 0 }, y: { min: 0, max: 0 } },
822
- // // isRoundPaused: false,
823
- // // gameMode: 'Deathmatch',
824
- // // antifeed1: false,
825
- // // level2open: false,
826
- // // rewardWinnerAmount: 1000,
827
- // // orbCutoffSeconds: 0,
828
- // // antifeed2: false,
829
- // // rewardSpawnLoopSeconds: 0,
830
- // // rewardItemAmountPerLegitPlayer: 0.0010000000000000000208,
831
- // // antifeed3: false,
832
- // // checkInterval: 0,
833
- // // isGodParty: false,
834
- // // pointsPerEvolve: 0,
835
- // // pointsPerKill: 0,
836
- // // loggableEvents: [],
837
- // // drops: { guardian: 0, santa: 0, earlyAccess: 0, trinket: 0 },
838
- // // hideMap: false,
839
- // // rewards: {
840
- // // characters: [{ type: 'character', tokenId: '1' }],
841
- // // items: [],
842
- // // tokens: [
843
- // // { symbol: 'pepe', value: 0.000020000000000000001636, quantity: 10000000, type: 'token' },
844
- // // { symbol: 'doge', value: 0.2999999999999999889, quantity: 0, type: 'token' },
845
- // // { symbol: 'harold', value: 0.012999999999999999403, quantity: 100000, type: 'token' },
846
- // // ],
847
- // // },
848
- // // decayPower: 0,
849
- // // spawnBoundary2: { x: { max: 0, min: 0 }, y: { min: 0, max: 0 } },
850
- // // rewardWinnerAmountPerLegitPlayer: 0.0010000000000000000208,
851
- // // dynamicDecayPower: true,
852
- // // leadercap: false,
853
- // // rewardItemAmountMax: 1,
854
- // // resetInterval: 0,
855
- // // orbOnDeathPercent: 0,
856
- // // roundId: '6774d64608c05476fc257d1a',
857
- // // noBoot: false,
858
- // // fastLoopSeconds: 0,
859
- // // };
860
-
861
- // for (const key in input) {
862
- // console.log('Setting', key, input[key]);
863
- // game.meta[key] = input[key];
864
- // }
865
-
866
- // game.meta = { ...game.meta };
867
- // game.markModified('meta');
868
-
869
- // await game.save();
870
-
871
- // return game.meta;
872
- // }
873
-
874
- // async updateGameStats(
875
- // input: RouterInput['updateGameStats'],
876
- // ctx: RouterContext
877
- // ): Promise<RouterOutput['updateGameStats']> {
878
- // const { Game, GameStat } = ctx.app.model;
879
-
880
- // const game = await Game.findOne({ key: 'evolution' }).populate('stat').exec();
881
- // let latestRecord = game.stat;
882
-
883
- // try {
884
- // if (latestRecord) {
885
- // const dayAgo = dayjs().subtract(1, 'day');
886
-
887
- // // Unset the latest record if it's older than one day, so a new one is created
888
- // if (!dayjs(latestRecord.createdDate).isAfter(dayAgo)) {
889
- // latestRecord = undefined;
890
- // }
891
- // }
892
- // } catch (e) {
893
- // console.log('Error getting latest stat record', e);
894
- // }
895
-
896
- // const meta: any = {
897
- // clientCount: game.meta.clientCount,
898
- // };
899
-
900
- // if (latestRecord) {
901
- // await GameStat.updateOne(
902
- // { _id: latestRecord.id },
903
- // {
904
- // ...latestRecord,
905
- // meta,
906
- // }
907
- // ).exec();
908
- // } else {
909
- // const gameStat = await GameStat.create({
910
- // gameId: game.id,
911
- // meta,
912
- // });
913
-
914
- // game.statId = gameStat.id;
915
- // }
916
-
917
- // game.meta = { ...game.meta };
918
-
919
- // await game.save();
920
- // }
921
-
922
- async getAllChestEvents(app, retry = false) {
923
- if (app.data.chest.updating) return;
924
-
925
- log('[Chest] Updating');
926
-
927
- app.data.chest.updating = true;
928
-
929
- try {
930
- const payments = await app.model.Payment.find();
931
-
932
- const iface = new ethers.Interface(app.contractMetadata.bsc.ArkenChest.abi);
933
-
934
- // @ts-ignore
935
- async function processLog(log2, updateConfig = true) {
936
- try {
937
- const e = iface.parseLog(log2);
938
-
939
- console.log(e.name, e);
940
- const profile = await app.model.Profile.findOne({ address: e.args.to });
941
-
942
- const payment = await app.model.Payment.findOne({ id: e.args.requestId });
943
-
944
- if (!payment) {
945
- throw new Error('Could not find a payment that appeared onchain.');
946
- }
947
-
948
- payment.status = 'Completed';
949
- } catch (ex) {
950
- log(ex);
951
- log('Error parsing log: ', log2);
952
- await sleep(1000);
953
- }
954
- }
955
-
956
- const blockNumber = await app.web3.bsc.eth.getBlockNumber();
957
-
958
- if (parseInt(blockNumber) > 10000) {
959
- const events = ['ItemsSent(address,uint256,string)'];
960
-
961
- for (const event of events) {
962
- await iterateBlocks(
963
- app,
964
- `Chest Events: ${event}`,
965
- getAddress(app.contractInfo.bsc.chest),
966
- app.data.chest.lastBlock[event] || 15000000,
967
- blockNumber,
968
- app.contracts.bsc.chest.filters[event](),
969
- processLog,
970
- async function (blockNumber2) {
971
- app.data.chest.lastBlock[event] = blockNumber2;
972
- // await saveConfig()
973
- }
974
- );
975
- }
976
- } else {
977
- log('Error parsing block number', blockNumber);
978
- }
979
-
980
- log('Finished getting events');
981
- } catch (e) {
982
- log('Error', e);
983
- await sleep(1000);
984
- }
985
-
986
- app.data.chest.updating = false;
987
- app.data.chest.updatedDate = new Date().toString();
988
- app.data.chest.updatedTimestamp = new Date().getTime();
989
-
990
- if (retry) {
991
- setTimeout(() => this.getAllChestEvents(app, retry), 5 * 60 * 1000);
992
- }
993
- }
994
-
995
- async monitorChest(input: RouterInput['monitorChest'], ctx: RouterContext): Promise<RouterOutput['monitorChest']> {
996
- ctx.app.data.chest = {
997
- lastBlock: {
998
- 'ItemsSent(address,uint256,string)': 45299534,
999
- },
1000
- };
1001
-
1002
- await this.getAllChestEvents(ctx.app);
1003
-
1004
- ctx.app.contracts.bsc.chest.on('ItemsSent', async () => {
1005
- await this.getAllChestEvents(ctx.app);
1006
- });
1007
- }
1008
-
1009
- async info(input: RouterInput['info'], ctx: RouterContext): Promise<RouterOutput['info']> {
1010
- console.log('Evolution.Service.info', input);
1011
-
1012
- if (!ctx.client?.roles?.includes('admin')) throw new Error('Not authorized');
1013
-
1014
- // const gameData = await ctx.app.model.Data.findOne({ key: 'evolution', mod: 'evolution' });
1015
-
1016
- // gameData.data = {
1017
- // roundId: generateShortId(),
1018
- // maxClients: 100,
1019
- // rewardItemAmount: 1,
1020
- // rewardWinnerAmount: 300,
1021
- // rewardItemAmountPerLegitPlayer: 0.001,
1022
- // rewardItemAmountMax: 1,
1023
- // rewardWinnerAmountPerLegitPlayer: 0.001,
1024
- // rewardWinnerAmountMax: 300,
1025
- // drops: {
1026
- // guardian: 0,
1027
- // earlyAccess: 0,
1028
- // trinket: 0,
1029
- // santa: 0,
1030
- // },
1031
- // totalLegitPlayers: 0,
1032
- // isBattleRoyale: false,
1033
- // isGodParty: false,
1034
- // level2open: false,
1035
- // isRoundPaused: false,
1036
- // gameMode: 'Deathmatch',
1037
- // maxEvolves: 0,
1038
- // pointsPerEvolve: 0,
1039
- // pointsPerKill: 0,
1040
- // decayPower: 0,
1041
- // dynamicDecayPower: true,
1042
- // baseSpeed: 0,
1043
- // avatarSpeedMultiplier: {},
1044
- // avatarDecayPower: {},
1045
- // preventBadKills: false,
1046
- // antifeed1: false,
1047
- // antifeed2: false,
1048
- // antifeed3: false,
1049
- // noDecay: false,
1050
- // noBoot: false,
1051
- // rewardSpawnLoopSeconds: 0,
1052
- // orbOnDeathPercent: 0,
1053
- // orbTimeoutSeconds: 0,
1054
- // orbCutoffSeconds: 0,
1055
- // orbLookup: {},
1056
- // roundLoopSeconds: 0,
1057
- // fastLoopSeconds: 0,
1058
- // leadercap: false,
1059
- // hideMap: false,
1060
- // checkPositionDistance: 0,
1061
- // checkInterval: 0,
1062
- // resetInterval: 0,
1063
- // loggableEvents: [],
1064
- // mapBoundary: {
1065
- // x: { min: 0, max: 0 },
1066
- // y: { min: 0, max: 0 },
1067
- // },
1068
- // spawnBoundary1: {
1069
- // x: { min: 0, max: 0 },
1070
- // y: { min: 0, max: 0 },
1071
- // },
1072
- // spawnBoundary2: {
1073
- // x: { min: 0, max: 0 },
1074
- // y: { min: 0, max: 0 },
1075
- // },
1076
- // rewards: {
1077
- // tokens: [
1078
- // {
1079
- // type: 'token',
1080
- // symbol: 'pepe',
1081
- // quantity: 10000000,
1082
- // value: 0.00002,
1083
- // },
1084
- // {
1085
- // type: 'token',
1086
- // symbol: 'doge',
1087
- // quantity: 1000,
1088
- // value: 0.3,
1089
- // },
1090
- // {
1091
- // type: 'token',
1092
- // symbol: 'harold',
1093
- // quantity: 100000,
1094
- // value: 0.013,
1095
- // },
1096
- // ],
1097
- // items: [],
1098
- // characters: [
1099
- // {
1100
- // type: 'character',
1101
- // tokenId: '1',
1102
- // },
1103
- // ],
1104
- // },
1105
- // ...gameData.data,
1106
- // };
1107
-
1108
- // for (const key in data) {
1109
- // if (!gameData.data[key]) {
1110
- // gameData.data[key] = data[key];
1111
- // }
1112
- // }
1113
-
1114
- // gameData.markModified('data');
1115
-
1116
- // await gameData.save();
1117
-
1118
- const game = await ctx.app.model.Game.findOne({ key: input.gameKey });
1119
-
1120
- return game.meta;
1121
- }
1122
-
1123
- async monitorParties(
1124
- input: RouterInput['monitorParties'],
1125
- ctx: RouterContext
1126
- ): Promise<RouterOutput['monitorParties']> {
1127
- async function processParties() {
1128
- const parties = await ctx.app.model.Party.find().populate('members');
1129
-
1130
- log('Total Parties: ' + parties.length);
1131
-
1132
- // Remove empty parties
1133
- // Remove players who have been offline for X length
1134
- // Check requirements
1135
- // Check other issues
1136
-
1137
- setTimeout(processParties, 60 * 60 * 1000);
1138
- }
1139
-
1140
- processParties();
1141
- }
1142
-
1143
- async getParties(input: RouterInput['getParties'], ctx: RouterContext): Promise<RouterOutput['getParties']> {
1144
- if (!input) throw new ARXError('NO_INPUT');
1145
-
1146
- log('Evolution.Service.getParties', input);
1147
-
1148
- const filter = getFilter(input);
1149
- const parties = await ctx.app.model.Party.find(filter).populate('members').lean();
1150
-
1151
- return parties;
1152
- }
1153
-
1154
- async createParty(input: RouterInput['createParty'], ctx: RouterContext): Promise<RouterOutput['createParty']> {
1155
- if (!input) throw new ARXError('NO_INPUT');
1156
-
1157
- log('Evolution.Service.createParty', input);
1158
-
1159
- const profile = await ctx.app.model.Profile.findById(ctx.client.profile.id);
1160
-
1161
- if (profile.partyId) throw new Error('Already in a party');
1162
-
1163
- const party = await ctx.app.model.Party.create({
1164
- name: profile.name,
1165
- targetAreaId: null,
1166
- limit: 6,
1167
- isPublic: true,
1168
- isVisibleToEnemies: true,
1169
- isApprovalRequired: false,
1170
- isNonLeaderInviteAllowed: false,
1171
- isCombatEnabled: true,
1172
- isFriendlyFireEnabled: true,
1173
- isLocalQuestShared: true,
1174
- isGlobalQuestShared: true,
1175
- isMergeEnabled: false,
1176
- isRejoinEnabled: false,
1177
- itemDistribution: 'Random',
1178
- leaderId: profile.id,
1179
- powerRequired: 1,
1180
- levelRequired: 1,
1181
- approvalMethod: 'Auto Accept',
1182
- // memberIds: [profile.id],
1183
- // assistantIds: [],
1184
- // pendingMemberIds: [],
1185
- // blockedMemberIds: [],
1186
- });
1187
-
1188
- profile.partyId = ctx.client.profile.partyId = party.id;
1189
-
1190
- await profile.save();
1191
-
1192
- return {
1193
- id: party.id,
1194
- };
1195
- }
1196
-
1197
- async joinParty(input: RouterInput['joinParty'], ctx: RouterContext): Promise<RouterOutput['joinParty']> {
1198
- if (!input) throw new ARXError('NO_INPUT');
1199
-
1200
- log('Evolution.Service.joinParty', input);
1201
-
1202
- // TODO: if no ID, matchmaking
1203
-
1204
- const filter = getFilter(input);
1205
- const party = await ctx.app.model.Party.findOne(filter).populate('members');
1206
-
1207
- if (!party) throw new Error('Party does not exist');
1208
-
1209
- if (party.members.length >= party.limit) throw new Error('Party is full');
1210
-
1211
- const profile = await ctx.app.model.Profile.findById(ctx.client.profile.id);
1212
-
1213
- if (party.members.find((m) => m.id === profile.id)) throw new Error('Already in this party');
1214
-
1215
- if (profile.partyId) throw new Error('Already in a party');
1216
-
1217
- profile.partyId = ctx.client.profile.partyId = party.id;
1218
-
1219
- await profile.save();
1220
-
1221
- return {
1222
- id: party.id,
1223
- };
1224
- }
1225
-
1226
- async leaveParty(input: RouterInput['leaveParty'], ctx: RouterContext): Promise<RouterOutput['leaveParty']> {
1227
- if (!input) throw new ARXError('NO_INPUT');
1228
-
1229
- log('Evolution.Service.leaveParty', input);
1230
-
1231
- const filter = getFilter(input);
1232
- const party = await ctx.app.model.Party.findOne(filter).populate('members');
1233
-
1234
- if (!party) throw new Error('Party does not exist');
1235
-
1236
- const profile = await ctx.app.model.Profile.findById(ctx.client.profile.id);
1237
-
1238
- if (profile.partyId) throw new Error('Not in a party');
1239
-
1240
- if (!party.members.find((m) => m.id === profile.id)) throw new Error('Not in this party');
1241
-
1242
- profile.partyId = ctx.client.profile.partyId = undefined;
1243
-
1244
- await profile.save();
1245
-
1246
- return {
1247
- id: party.id,
1248
- };
1249
- }
1250
-
1251
- async updateSettings(
1252
- input: RouterInput['updateSettings'],
1253
- ctx: RouterContext
1254
- ): Promise<RouterOutput['updateSettings']> {
1255
- log('Evolution.Service.updateSettings', input);
1256
-
1257
- if (!ctx.client.profile) return;
1258
- if (!ctx.client.profile.meta.evolution) ctx.client.profile.meta.evolution = {};
1259
- if (!ctx.client.profile.meta.evolution.settings) ctx.client.profile.meta.evolution.settings = {};
1260
-
1261
- const validKeys = ['zoom', 'opacity'];
1262
-
1263
- // TODO: sanitize settings
1264
- const settings = input as unknown as Array<any>;
1265
-
1266
- for (const key in settings) {
1267
- if (!validKeys.includes(key)) continue;
1268
-
1269
- ctx.client.profile.meta.evolution.settings[key] = input[key];
1270
- }
1271
-
1272
- ctx.client.profile.markModified('meta');
1273
-
1274
- await ctx.client.profile.saveQueued();
1275
- }
1276
-
1277
- async getPayments(input: RouterInput['getPayments'], ctx: RouterContext): Promise<RouterOutput['getPayments']> {
1278
- log('Evolution.Service.getPayments', input);
1279
-
1280
- if (!ctx.client.profile) return [];
1281
-
1282
- const payments = await ctx.app.model.Payment.find({
1283
- ownerId: ctx.client.profile.id,
1284
- })
1285
- .sort({ createdDate: -1 })
1286
- .lean();
1287
-
1288
- return payments;
1289
- }
1290
-
1291
- async cancelPaymentRequest(
1292
- input: RouterInput['cancelPaymentRequest'],
1293
- ctx: RouterContext
1294
- ): Promise<RouterOutput['cancelPaymentRequest']> {
1295
- if (!input) throw new ARXError('NO_INPUT');
1296
-
1297
- log('Evolution.Service.cancelPaymentRequest', input);
1298
-
1299
- await ctx.app.model.Payment.updateMany({ ownerId: ctx.client.profile.id }, { $set: { status: 'Voided' } });
1300
- }
1301
-
1302
- async createPaymentRequest(
1303
- input: RouterInput['createPaymentRequest'],
1304
- ctx: RouterContext
1305
- ): Promise<RouterOutput['createPaymentRequest']> {
1306
- if (!input) throw new ARXError('NO_INPUT');
1307
-
1308
- log('Evolution.Service.createPaymentRequest', input);
1309
-
1310
- const existingPayment = await ctx.app.model.Payment.findOne({
1311
- ownerId: ctx.client.profile.id,
1312
- status: 'Processing',
1313
- });
1314
-
1315
- if (existingPayment) throw new Error('Payment is already processing');
1316
-
1317
- await ctx.app.model.Payment.updateMany({ ownerId: ctx.client.profile.id }, { $set: { status: 'Voided' } });
1318
-
1319
- await ctx.app.model.Payment.create({
1320
- // applicationId: this.cache.Application.Arken.id,
1321
- // name: ctx.client.profile.name,
1322
- status: 'Processing',
1323
- ownerId: ctx.client.profile.id,
1324
- meta: {
1325
- chain: input.chain,
1326
- tokenKeys: input.tokenKeys,
1327
- tokenAmounts: input.tokenAmounts,
1328
- to: input.to,
1329
- // tokenIds: input.tokenIds || [],
1330
- // itemIds: input.itemIds || [],
1331
- // signedData: null,
1332
- },
1333
- });
1334
-
1335
- const notice = await ctx.app.model.Conversation.create({
1336
- profileId: ctx.app.cache.Profile.admin.id,
1337
- });
1338
-
1339
- await ctx.app.model.Message.create({
1340
- name: 'Payment Created',
1341
- content: `Payment request by ${ctx.client.profile.name} (${ctx.client.profile.address})
1342
- Details: ${JSON.stringify(input, null, 2)}`,
1343
- conversationId: notice.id,
1344
- });
1345
- }
1346
-
1347
- async processBinanceSmartChainPayment(payment: any, ctx: any) {
1348
- const items = await ctx.app.model.Item.find({ key: { $in: payment.meta.itemIds || [] } });
1349
-
1350
- // #region Build ItemData
1351
- let rewardData: ItemData = {} as any;
1352
-
1353
- const tokenDecimals: Record<string, number> = {
1354
- '0xbA2aE424d960c26247Dd6c32edC70B295c744C43': 8,
1355
- };
1356
-
1357
- // payment.meta.tokenAddresses = ['0xbA2aE424d960c26247Dd6c32edC70B295c744C43'];
1358
- // payment.meta.tokenAmounts = [1];
1359
-
1360
- /*
1361
- * `payment.tokenAddresses` contains the contract addresses of the tokens the user is claiming.
1362
- *
1363
- * If the user isn't claiming any tokens (only item mints), this is the empty array.
1364
- */
1365
- rewardData.tokenAddresses = payment.meta.tokenKeys.map((token) =>
1366
- token === 'usd' ? ctx.app.contractInfo.bsc.busd[56] : ctx.app.contractInfo.bsc[token][56]
1367
- );
1368
- /*
1369
- * `payment.tokenAmounts` contains the amounts (in wei) of the tokens the user is claiming.
1370
- *
1371
- * If the user isn't claiming any tokens (only item mints), this is the empty array.
1372
- *
1373
- * The length of this array must equal the length of `payment.tokenAddresses` and must be ordered the same.
1374
- */
1375
- rewardData.tokenAmounts = (rewardData.tokenAddresses || []).map(
1376
- (r, i) =>
1377
- toLong((payment.meta.tokenAmounts[i] + '').slice(0, tokenDecimals[r] || 16), tokenDecimals[r] || 16) + ''
1378
- );
1379
- /*
1380
- * `payment.tokenIds` contains the token ids of any reward items (trinkets, Santa hats, cubes, etc.) the user
1381
- * is claiming.
1382
- *
1383
- * If the user isn't claiming any item mints (only tokens), this is the empty array.
1384
- *
1385
- * The contract will ensure token uniqueness, but the three digit serial should be randomised to prevent excess
1386
- * gas spending.
1387
- */
1388
-
1389
- rewardData.tokenIds = ctx.client.roles.includes('admin') ? getTokenIdsFromItems(items) : [];
1390
- /*
1391
- * `payment.itemIds` contains the item ids of any reward items (trinkets, Santa hats, cubes, etc.) the user
1392
- * is claiming.
1393
- *
1394
- * If the user isn't claiming any item mints (only tokens), this is the empty array.
1395
- *
1396
- * The length of this array must equal the length of `payment.tokenIds` and must be ordered the same.
1397
- */
1398
- rewardData.itemIds = ctx.client.roles.includes('admin') ? items.map((requestedItem) => requestedItem.id) : [];
1399
-
1400
- rewardData.purportedSigner = ctx.app.signers.wallet.address; // '0x81F8C054667046171C0EAdC73063Da557a828d6f'; // arken dev 2 #0
1401
-
1402
- /*
1403
- * `payment.to` contains the address of the user who should receive the rewards.
1404
- */
1405
- rewardData.to = payment.meta.to;
1406
-
1407
- /*
1408
- * Get next nonce and expiry for the user who should receive the rewards.
1409
- */
1410
- const nextNonceAndExpiry = await ctx.app.contracts.bsc.chest.getNextNonceAndExpiry(rewardData.to);
1411
- const nonce = nextNonceAndExpiry.nextNonce.toNumber();
1412
- const expiry = nextNonceAndExpiry.expiry.toNumber();
1413
-
1414
- rewardData.nonce = nonce;
1415
- rewardData.expiry = expiry;
1416
-
1417
- /*
1418
- * RE request id: suggest it's the keccak256 hash of all rewardData structure, but we can make it something else
1419
- * if needed (think you mentioned doing something similar to AWS's payment ids at one point)
1420
- */
1421
- rewardData.requestId = payment.id + ''; // (ctx.app.web3.bsc as Web3).utils.keccak256(JSON.stringify(rewardData));
1422
-
1423
- /* --- IMPORTANT ---
1424
- *
1425
- * We need to store mappings between address and nonce pairs and the rewards we're authorising issuance of.
1426
- *
1427
- * Let's say this is the first time the user has claimed.
1428
- *
1429
- * We get the next nonce from the contract and see it is 0, as expected. The user is eligible for 5 Tir and a
1430
- * mysterious trinket (for example). We need to store the mapping between (user's address, nonce 0) and the
1431
- * reward. When we can then issue a permit for the user to get 5 Tir and their trinket.
1432
- *
1433
- * Let's say the user doesn't use their permit, but hits the service again. We look up the fact that the nonce is
1434
- * still 0 in the contract, and we can reissue another permit ad libitum. It doesn't matter if the user comes back
1435
- * and requests a million permits - as soon as they give one of them to the contract and make a claim, the nonce
1436
- * counter increments, and all of the other permits are invalidated. So even if someone thinks they're being
1437
- * clever, gets ten permits, stores then, then submits them at once, only one will succeed.
1438
- *
1439
- * What we *do* need to be careful of is what happens when we check the nonce with the contract and it's now 1.
1440
- * That means one of the tickets went through. Now we need to compare it against the stored nonce for the user,
1441
- * see that it's higher than the stored number, take the mapped reward and ask the coordinator to subtract those
1442
- * rewards from the user's pending balance. Then, assuming there's anything left, we can issue the permit for
1443
- * whatever is new under the new nonce.
1444
- *
1445
- * I'll skip the logic for this as I haven't had a chance to familiarise myself properly with the coordinator,
1446
- * and just assume the nonce check is ok here.
1447
- */
1448
- const domain = {
1449
- name: 'ArkenChest',
1450
- version: '1',
1451
- chainId: 56,
1452
- verifyingContract: getAddress(contractInfo.chest),
1453
- };
1454
-
1455
- const rewardDataTypes = {
1456
- // EIP712Domain: [
1457
- // { name: 'name', type: 'string' },
1458
- // { name: 'version', type: 'string' },
1459
- // { name: 'chainId', type: 'uint256' },
1460
- // { name: 'verifyingContract', type: 'address' },
1461
- // ],
1462
- ItemData: [
1463
- { name: 'tokenAddresses', type: 'address[]' },
1464
- { name: 'tokenAmounts', type: 'uint256[]' },
1465
- { name: 'tokenIds', type: 'uint256[]' },
1466
- { name: 'itemIds', type: 'uint16[]' },
1467
-
1468
- { name: 'purportedSigner', type: 'address' },
1469
- { name: 'to', type: 'address' },
1470
- { name: 'nonce', type: 'uint256' },
1471
- { name: 'expiry', type: 'uint256' },
1472
- { name: 'requestId', type: 'string' },
1473
- ],
1474
- };
1475
-
1476
- // const profile = await ctx.app.model.Profile.findOne({ id: payment.ownerId });
1477
-
1478
- for (const i in payment.meta.tokenKeys) {
1479
- const key = payment.meta.tokenKeys[i];
1480
-
1481
- if (!contractInfo[key]) throw new Error('Invalid token');
1482
-
1483
- const value = payment.owner.meta.rewards.tokens[key];
1484
-
1485
- if (!payment.owner.meta.rewards.tokens[key]) throw new Error('Invalid reward token');
1486
-
1487
- console.log(
1488
- payment.owner.meta.rewards.tokens,
1489
- payment.owner.meta.rewards.tokens[key],
1490
- payment.meta.tokenAmounts[i]
1491
- );
1492
-
1493
- payment.owner.meta.rewards.tokens[key] -= payment.meta.tokenAmounts[i];
1494
-
1495
- if (payment.owner.meta.rewards.tokens[key] < -0.000000001) {
1496
- console.log(payment.owner.meta.rewards.tokens[key]);
1497
- throw new Error('Invalid reward amount');
1498
-
1499
- // payment.owner.meta.rewards.tokens[key] = 0;
1500
- }
1501
- }
1502
-
1503
- // payment.owner.meta = { ...payment.owner.meta };
1504
-
1505
- // TODO: refetch profile and confirm balances?
1506
- console.log(222, domain, rewardDataTypes, rewardData);
1507
-
1508
- const signature = await ctx.app.signers.wallet._signTypedData(domain, rewardDataTypes, rewardData);
1509
-
1510
- // This should be the calldata we need to return to the user
1511
- payment.meta.signedData = ctx.app.contracts.bsc.chest.interface.encodeFunctionData('sendItems', [
1512
- rewardData,
1513
- signature,
1514
- ]);
1515
- console.log(333, payment.meta.signedData);
1516
- }
1517
-
1518
- async processSolanaPayment(payment: any, ctx: any) {
1519
- const anchor = require('@project-serum/anchor');
1520
- const { PublicKey, Keypair, SystemProgram } = anchor.web3;
1521
- const fs = require('fs');
1522
- const nacl = require('tweetnacl');
1523
- const bs58 = require('bs58');
1524
-
1525
- // Configure the client to use the Solana cluster
1526
- const provider = anchor.AnchorProvider.env();
1527
- anchor.setProvider(provider);
1528
-
1529
- // Load authority keypair (authorized signer)
1530
- const authoritySecret = JSON.parse(process.env.SOLANA_CHEST_AUTHORITY);
1531
- const authority = Keypair.fromSecretKey(new Uint8Array(authoritySecret));
1532
-
1533
- // Define token mint and from token account
1534
- const tokenMint = '3vgopg7xm3EWkXfxmWPUpcf7g939hecfqg18sLuXDzVt'; // Replace with your existing token mint address
1535
-
1536
- // Define recipient
1537
- const recipient = '9hdMuARXF4VXg3LsJJirkye8QNjY79iznTzNRtL7JFYa'; // Replace with recipient's public key
1538
-
1539
- const items = await ctx.app.model.Item.find({ key: { $in: payment.meta.itemIds || [] } });
1540
-
1541
- // #region Build ItemData
1542
- let rewardData: ItemData = {} as any;
1543
-
1544
- const tokenDecimals: Record<string, number> = {
1545
- '0xbA2aE424d960c26247Dd6c32edC70B295c744C43': 8,
1546
- };
1547
-
1548
- // payment.meta.tokenAddresses = ['0xbA2aE424d960c26247Dd6c32edC70B295c744C43'];
1549
- // payment.meta.tokenAmounts = [1];
1550
-
1551
- /*
1552
- * `payment.tokenAddresses` contains the contract addresses of the tokens the user is claiming.
1553
- *
1554
- * If the user isn't claiming any tokens (only item mints), this is the empty array.
1555
- */
1556
- rewardData.tokenAddresses = payment.meta.tokenAddresses;
1557
- /*
1558
- * `payment.tokenAmounts` contains the amounts (in wei) of the tokens the user is claiming.
1559
- *
1560
- * If the user isn't claiming any tokens (only item mints), this is the empty array.
1561
- *
1562
- * The length of this array must equal the length of `payment.tokenAddresses` and must be ordered the same.
1563
- */
1564
- rewardData.tokenAmounts = (payment.meta.tokenAddresses || []).map(
1565
- (r, i) =>
1566
- toLong((payment.meta.tokenAmounts[i] + '').slice(0, tokenDecimals[r] || 16), tokenDecimals[r] || 16) + ''
1567
- );
1568
- /*
1569
- * `payment.tokenIds` contains the token ids of any reward items (trinkets, Santa hats, cubes, etc.) the user
1570
- * is claiming.
1571
- *
1572
- * If the user isn't claiming any item mints (only tokens), this is the empty array.
1573
- *
1574
- * The contract will ensure token uniqueness, but the three digit serial should be randomised to prevent excess
1575
- * gas spending.
1576
- */
1577
-
1578
- rewardData.tokenIds = ctx.client.roles.includes('admin') ? getTokenIdsFromItems(items) : [];
1579
- /*
1580
- * `payment.itemIds` contains the item ids of any reward items (trinkets, Santa hats, cubes, etc.) the user
1581
- * is claiming.
1582
- *
1583
- * If the user isn't claiming any item mints (only tokens), this is the empty array.
1584
- *
1585
- * The length of this array must equal the length of `payment.tokenIds` and must be ordered the same.
1586
- */
1587
- rewardData.itemIds = ctx.client.roles.includes('admin') ? items.map((requestedItem) => requestedItem.id) : [];
1588
-
1589
- rewardData.purportedSigner = ctx.app.signers.wallet.address; // '0x81F8C054667046171C0EAdC73063Da557a828d6f'; // arken dev 2 #0
1590
-
1591
- /*
1592
- * `payment.to` contains the address of the user who should receive the rewards.
1593
- */
1594
- rewardData.to = payment.meta.to;
1595
-
1596
- /*
1597
- * Get next nonce and expiry for the user who should receive the rewards.
1598
- */
1599
- const nextNonceAndExpiry = await ctx.app.contracts.bsc.chest.getNextNonceAndExpiry(rewardData.to);
1600
- const nonce = nextNonceAndExpiry.nextNonce.toNumber();
1601
- const expiry = nextNonceAndExpiry.expiry.toNumber();
1602
-
1603
- rewardData.nonce = nonce;
1604
-
1605
- // Define ItemData
1606
- const itemData = {
1607
- token_addresses: [tokenMint],
1608
- token_amounts: [1], // Amounts corresponding to each token address
1609
- token_ids: [], // Ignored as per instructions
1610
- item_ids: [], // Ignored as per instructions
1611
- purported_signer: authority.publicKey,
1612
- to: recipient,
1613
- nonce: 0, // Fetch current nonce from state if necessary
1614
- expiry: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
1615
- request_id: payment.id + '',
1616
- };
1617
-
1618
- // Serialize ItemData using Borsh
1619
- class ItemDataSchema {
1620
- token_addresses: string[];
1621
- token_amounts: number[];
1622
- token_ids: number[];
1623
- item_ids: number[];
1624
- purported_signer: string;
1625
- to: string;
1626
- nonce: number;
1627
- expiry: number;
1628
- request_id: string;
1629
-
1630
- constructor(fields: Partial<ItemDataSchema>) {
1631
- Object.assign(this, fields);
1632
- }
1633
- }
1634
-
1635
- const schema: any = new Map([
1636
- [
1637
- ItemDataSchema,
1638
- {
1639
- kind: 'struct',
1640
- fields: [
1641
- ['token_addresses', ['pubkey']],
1642
- ['token_amounts', ['u64']],
1643
- ['token_ids', ['u64']],
1644
- ['item_ids', ['u16']],
1645
- ['purported_signer', 'pubkey'],
1646
- ['to', 'pubkey'],
1647
- ['nonce', 'u64'],
1648
- ['expiry', 'u64'],
1649
- ['request_id', 'string'],
1650
- ],
1651
- },
1652
- ],
1653
- ]);
1654
-
1655
- const message = serialize(schema, new ItemDataSchema(itemData));
1656
-
1657
- // Sign the message with the authorized signer
1658
- const signature = nacl.sign.detached(message, authority.secretKey);
1659
-
1660
- payment.meta.signedData = Array.from(signature);
1661
- }
1662
-
1663
- async processPayments(
1664
- input: RouterInput['processPayments'],
1665
- ctx: RouterContext
1666
- ): Promise<RouterOutput['processPayments']> {
1667
- log('Evolution.Service.processPayments', input);
1668
-
1669
- const banList = [
1670
- '0x3c261982d5721eeb91f23fa573a1d883118f7473',
1671
- '0x92153ac2f8172ed736ed8b09df917f65e7a587b6',
1672
- '0x2a6ec632d61a3726e5bf8882a39596b2cb6e75c3',
1673
- '0x63b6643276c704933b3cf5adb1677466b4bae2f7',
1674
- '0xf37e0370877739b643a83ed46358011f434fdb14',
1675
- '0x08e5f31d6d2255222c234f1ab7ab0dfdb8225a3e',
1676
- '0x7e351f42c33d2e3e342f66fa67da1b71a86f6e50',
1677
- '0x180961cb73ba789f9e078ae0f5cf35dae0449490',
1678
- '0x01b446e7c93d7acb5caa41e22d5ab257867eda2c',
1679
- '0x1f1bd54ecae5f400bddba27bf079ed44f181df4a',
1680
- '0xc6e18f15b6386540d58c5ee414ef4766fdf3f45f',
1681
- '0x6062329fd2f2d6437c723d05c00215d9c339f03f',
1682
- '0x5008276ae16e7287c56630ce269724af893c2f44',
1683
- '0x04919f45b95dd0ac6e8a6fc52b1a57d6108d6f61',
1684
- '0xe339454722d68696fc78ebcf6f858005ee489f13',
1685
- '0x12333aba4243e1f99a3f09b5cefef98ddbe442ff',
1686
- '0x92a556cef6ffd02f15983ba90f8d4a336d41e1de',
1687
- '0x227ad0e0d3834c3c036c4fcab6dff6d8ff8a5d1b',
1688
- '0x9a69400865fa141040d45a126a45ee6b6655c578',
1689
- '0xf37e0370877739b643a83ed46358011f434fdb14',
1690
- '0x419c59f3fe3cd4ad6344a112ae005c04219dd495',
1691
- '0x1d984fd2af11da709ffbc5c76df4604647f77030',
1692
- '0x92153ac2f8172ed736ed8b09df917f65e7a587b6',
1693
- '0x3c261982d5721eeb91f23fa573a1d883118f7473',
1694
- '0x6b6fb8d96bd2ddcb09ea8529b6070408dc334a6d',
1695
- '0xc84ce216fef4ec8957bd0fb966bb3c3e2c938082',
1696
- '0xb0c8a8503c114e101248d2fdf401a36423116cb2',
1697
- '0x11f36b4166815bacc110c3af3f910e3c03ec8bf5',
1698
- '0x69cb24da962f70ea452c14f3b0fae35ebf0afe04',
1699
- '0x18e62da83931a24151856e39da75862e1ce723f1',
1700
- '0xe041d38f2c48315fd9bbe63f6e6ef4d07b00c46c',
1701
- '0xa1f610a78e2df017adeec80a618ce6a60d9f113b',
1702
- '0xd973812e8c327a8c5a0dafc200f9b73fd86dc352',
1703
- '0x101b680f6c87ab5ca72d4e25f770ee7f9257c21e',
1704
- '0xf6c80422a9d7d20992bf042aa9ab21af86bd7e70',
1705
- '0xbae1754a1e80cf4a7ac344ea4a6560bf6fb32dad',
1706
- '0x9d1d5a42c7d842bd1ebe525576fec84bd963b868',
1707
- '0xe1cdef10655bb49175847d039e20b907e092254b',
1708
- '0xceec48bb1377c6b5a1f0df99c8060b01986e7496',
1709
- '0x1f647fdff890089bb9157e626fb2263ffae90585',
1710
- '0xfe5e35ff26dbd5666072cc98570074aadbcf10eb',
1711
- '0x9568b75583b42bd2fabfa3e11b5dd28035eeac51',
1712
- '0x93fa31ab82db283295a0cb72dfd447264acda407',
1713
- '0x3780db47c9663d2b17447c814436f40fda2378d6',
1714
- '0xa205b1ed9754bb9b66b39282a918ccca00af3d38',
1715
- '0x272012a0253bf90263037e7c72dace0349334762',
1716
- '0xb17373a4342dac4abfef7f378a2960e4c297becd',
1717
- '0x27e3717ba664ca97da86177705eb8efb02610be6',
1718
- '0x63513545e403878249ddc07dd6711a7ab1ef7867',
1719
- '0xc964eb11ebbefaf8a7e4a1953fa8cee1d31fc27c',
1720
- '0x32ea1081d93834367584b7bf661c51fe93cc52af',
1721
- '0x6f42dbb23389fb8cb33a1a9dc8a16073961f5a8a',
1722
- '0x472a375ea5d7067c4cf89c4e00ba9c49aedf4db3',
1723
- '0xe50706c991544cafe35de4b58342598381ba6b21',
1724
- '0x2711997db0f84220cf479bda03d99440cdbf1dab',
1725
- '0x8aeedc8d20a349736940cff5aa6ced3bc7ccbf8f',
1726
- ];
1727
-
1728
- const session = await ctx.app.db.mongoose.startSession();
1729
- session.startTransaction();
1730
-
1731
- const payments = await ctx.app.model.Payment.find({ status: { $in: ['Processing', 'Refunding'] } }).populate(
1732
- 'owner'
1733
- );
1734
-
1735
- const totals = {};
1736
-
1737
- for (const payment of payments) {
1738
- console.log('Payment to: ' + payment.meta.to);
1739
-
1740
- for (const i in payment.meta.tokenAddresses) {
1741
- const address = payment.meta.tokenAddresses[i];
1742
- const key = contractAddressToKey[address];
1743
-
1744
- if (!totals[key]) totals[key] = 0;
1745
- totals[key] += payment.meta.tokenAmounts[i];
1746
- }
1747
-
1748
- console.log('Totals: ', totals);
1749
- }
1750
-
1751
- // await awaitEnter('Approve?');
1752
- // return;
1753
-
1754
- for (const payment of payments) {
1755
- try {
1756
- if (payment.meta?.signedData) {
1757
- console.log('Payment already processed');
1758
-
1759
- continue;
1760
- }
1761
- console.log(payment.createdDate, dayjs().subtract(24, 'hour').toDate());
1762
- // Void any submit more than 24 hours ago (so there is 24 hours to finalize)
1763
- if (dayjs(payment.createdDate).isBefore(dayjs().subtract(24, 'hour'))) {
1764
- // payment.owner.meta.rewards = {};
1765
- // await payment.owner.save();
1766
-
1767
- payment.status = 'Expired';
1768
- await payment.save();
1769
-
1770
- continue;
1771
- }
1772
-
1773
- if (banList.includes(payment.owner.address.toLowerCase())) {
1774
- payment.owner.meta.rewards = {};
1775
- // await payment.owner.save();
1776
-
1777
- payment.status = 'Denied';
1778
- await payment.save();
1779
-
1780
- continue;
1781
- }
1782
-
1783
- if (payment.status === 'Refunding') {
1784
- console.log('Refunding payment...');
1785
-
1786
- for (const i in payment.meta.tokenKeys) {
1787
- const key = payment.meta.tokenKeys[i];
1788
-
1789
- if (!contractInfo[key]) throw new Error('Invalid token');
1790
-
1791
- if (!payment.owner.meta.rewards.tokens[key]) throw new Error('Invalid reward token');
1792
-
1793
- payment.owner.meta.rewards.tokens[key] += payment.meta.tokenAmounts[i];
1794
- }
1795
-
1796
- const profile = await ctx.app.model.Profile.findOne({ _id: payment.ownerId });
1797
-
1798
- profile.meta = { ...payment.owner.meta };
1799
-
1800
- profile.markModified('meta');
1801
-
1802
- await profile.save();
1803
-
1804
- payment.status = 'Refunded';
1805
- await payment.save();
1806
-
1807
- console.log('Refunded.');
1808
-
1809
- continue;
1810
- }
1811
-
1812
- const problem = isClaimProblem(payment);
1813
- if (problem) throw new Error(problem);
1814
-
1815
- if (!payment.meta.network) payment.meta.network = 'bsc';
1816
-
1817
- if (payment.meta.network === 'bsc') {
1818
- await this.processBinanceSmartChainPayment(payment, ctx);
1819
- } else if (payment.meta.network === 'solana') {
1820
- await this.processSolanaPayment(payment, ctx);
1821
- }
1822
-
1823
- if (!payment.meta.signedData) {
1824
- console.error('No signed set was set, aborting', payment);
1825
-
1826
- await session.abortTransaction();
1827
-
1828
- payment.status = 'Failed';
1829
-
1830
- await payment.save();
1831
-
1832
- break;
1833
- }
1834
-
1835
- console.log(payment.owner.meta.rewards.tokens);
1836
-
1837
- // await ctx.app.model.Profile.updateOne({ id: payment.ownerId + '' }, { $set: { meta: payment.owner.meta } });
1838
-
1839
- const profile = await ctx.app.model.Profile.findOne({ _id: payment.ownerId });
1840
-
1841
- profile.meta = { ...payment.owner.meta };
1842
-
1843
- profile.markModified('meta');
1844
-
1845
- await profile.save();
1846
-
1847
- // const updatedProfile = await ctx.app.model.Profile.findOne({ _id: payment.ownerId });
1848
-
1849
- await ctx.app.model.Payment.updateMany({ ownerId: payment.ownerId }, { $set: { status: 'Voided' } });
1850
-
1851
- // payment.owner.meta = ctx.client.profile.meta;
1852
- payment.status = 'Processed';
1853
-
1854
- payment.meta = { ...payment.meta };
1855
-
1856
- payment.markModified('meta');
1857
-
1858
- await payment.save();
1859
-
1860
- await session.commitTransaction();
1861
- } catch (e) {
1862
- console.log('Payment failed', e);
1863
- payment.status = 'Failed';
1864
- await payment.save();
1865
-
1866
- await session.abortTransaction();
1867
- throw e;
1868
- } finally {
1869
- session.endSession();
1870
- }
1871
- }
1872
- }
1873
-
1874
- // async processWorldRecords(input: RouterInput['saveRound'], ctx: RouterContext): Promise<RouterOutput['saveRound']> {
1875
- // if (!input) throw new Error('Input should not be void');
1876
-
1877
- // console.log('Evolution.Service.processWorldRecords', input);
1878
-
1879
- // const game = await ctx.app.model.Game.findOne({ key: input.gameKey });
1880
-
1881
- // {
1882
- // const record = await ctx.app.model.WorldRecord.findOne({
1883
- // gameId: game.id,
1884
- // name: `${input.round.gameMode} Points`,
1885
- // })
1886
- // .sort({ score: -1 })
1887
- // .limit(1);
1888
-
1889
- // const winner = input.round.clients.sort((a, b) => b.points - a.points)[0];
1890
- // const profile = await ctx.app.model.Profile.findOne({ address: winner.address });
1891
-
1892
- // if (!record || winner.points > record.score) {
1893
- // await ctx.app.model.WorldRecord.create({
1894
- // gameId: game.id,
1895
- // holderId: profile.id,
1896
- // score: winner.points,
1897
- // });
1898
-
1899
- // // Announce to all game shards
1900
- // }
1901
- // }
1902
-
1903
- // {
1904
- // const record = await ctx.app.model.WorldRecord.findOne({ gameId: game.id, name: 'Highest Score' })
1905
- // .sort({ score: -1 })
1906
- // .limit(1);
1907
-
1908
- // const winner = input.round.clients.sort((a, b) => b.points - a.points)[0];
1909
- // const profile = await ctx.app.model.Profile.findOne({ address: winner.address });
1910
-
1911
- // if (!record || winner.points > record.score) {
1912
- // await ctx.app.model.WorldRecord.create({
1913
- // gameId: game.id,
1914
- // holderId: profile.id,
1915
- // score: winner.points,
1916
- // });
1917
- // }
1918
- // }
1919
-
1920
- // // {
1921
- // // const record = await ctx.app.model.WorldRecord.findOne({ gameId: game.id, name: 'Quickest Kill' })
1922
- // // .sort({ score: -1 })
1923
- // // .limit(1);
1924
-
1925
- // // const winner = input.round.clients.sort((a, b) => b.quickestKill - a.quickestKill)[0];
1926
- // // const profile = await ctx.app.model.Profile.findOne({ address: winner.address });
1927
-
1928
- // // if (!record || winner.quickestKill < record.score) {
1929
- // // await ctx.app.model.WorldRecord.create({
1930
- // // gameId: game.id,
1931
- // // holderId: profile.id,
1932
- // // });
1933
- // // }
1934
- // // }
1935
-
1936
- // // const record = await ctx.app.model.WorldRecord.findOne({
1937
- // // gameId: game.id,
1938
- // // name: 'Quickest Death',
1939
- // // })
1940
- // // .sort({ score: -1 })
1941
- // // .limit(1);
1942
-
1943
- // // const winner = input.round.clients.sort((a, b) => b.quickestDeath - a.quickestDeath)[0];
1944
- // // const profile = await ctx.app.model.Profile.findOne({ address: winner.address });
1945
-
1946
- // // if (!record || winner.quickestDeath < record.score) {
1947
- // // await ctx.app.model.WorldRecord.create({
1948
- // // gameId: game.id,
1949
- // // holderId: profile.id,
1950
- // // });
1951
- // // }
1952
-
1953
- // {
1954
- // const record = await ctx.app.model.WorldRecord.findOne({ gameId: game.id, name: 'Most Kills' })
1955
- // .sort({ score: -1 })
1956
- // .limit(1);
1957
-
1958
- // const winner = input.round.clients.sort((a, b) => b.kills - a.kills)[0];
1959
- // const profile = await ctx.app.model.Profile.findOne({ address: winner.address });
1960
-
1961
- // if (!record || winner.kills > record.score) {
1962
- // await ctx.app.model.WorldRecord.create({
1963
- // gameId: game.id,
1964
- // holderId: profile.id,
1965
- // score: winner.kills,
1966
- // });
1967
- // }
1968
- // }
1969
- // }
1970
-
1971
- // async interact(input: RouterInput['interact'], ctx: RouterContext): Promise<RouterOutput['interact']> {
1972
- // console.log('Evolution.Service.interact', input);
1973
- // }
1974
-
1975
- // async getScene(input: RouterInput['getScene'], ctx: RouterContext): Promise<RouterOutput['getScene']> {
1976
- // if (!input) throw new Error('Input should not be void');
1977
- // console.log('Evolution.Service.getScene', input);
1978
-
1979
- // let data = {};
1980
-
1981
- // if (input.applicationId === '668e4e805f9a03927caf883b') {
1982
- // data = {
1983
- // ...data,
1984
- // objects: [
1985
- // {
1986
- // id: 'axl',
1987
- // file: 'axl.fbx',
1988
- // position: {
1989
- // x: 1000,
1990
- // y: 1000,
1991
- // z: 1000,
1992
- // },
1993
- // },
1994
- // ] as Arken.Core.Types.Object,
1995
- // };
1996
- // }
1997
-
1998
- // return data;
1999
- // }
2000
- }