@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.
- package/{src/modules/evolution → evolution}/evolution.models.ts +1 -1
- package/{src/modules/evolution → evolution}/evolution.router.ts +9 -9
- package/evolution/evolution.schema.ts +1 -0
- package/{src/modules/evolution → evolution}/evolution.types.ts +1 -1
- package/{src/index.ts → index.ts} +2 -2
- package/{src/modules/infinite → infinite}/infinite.models.ts +1 -1
- package/{src/modules/infinite → infinite}/infinite.router.ts +9 -9
- package/infinite/infinite.schema.ts +1 -0
- package/{src/modules/infinite → infinite}/infinite.types.ts +1 -1
- package/{src/modules/isles → isles}/isles.models.ts +1 -1
- package/{src/modules/isles → isles}/isles.router.ts +9 -9
- package/isles/isles.schema.ts +1 -0
- package/{src/modules/isles → isles}/isles.types.ts +1 -1
- package/{src/modules/oasis → oasis}/oasis.models.ts +1 -1
- package/{src/modules/oasis → oasis}/oasis.router.ts +2 -2
- package/oasis/oasis.schema.ts +1 -0
- package/{src/modules/oasis → oasis}/oasis.types.ts +1 -1
- package/package.json +3 -12
- package/{src/router.ts → router.ts} +23 -23
- package/trek/trek.models.ts +1 -0
- package/{src/modules/trek → trek}/trek.router.ts +1 -1
- package/trek/trek.schema.ts +1 -0
- package/trek/trek.types.ts +1 -0
- package/src/modules/evolution/evolution.schema.ts +0 -1
- package/src/modules/evolution/evolution.service.ts +0 -2000
- package/src/modules/infinite/infinite.schema.ts +0 -1
- package/src/modules/infinite/infinite.service.ts +0 -40
- package/src/modules/isles/isles.schema.ts +0 -1
- package/src/modules/isles/isles.service.ts +0 -40
- package/src/modules/oasis/oasis.schema.ts +0 -1
- package/src/modules/oasis/oasis.service.ts +0 -38
- package/src/modules/trek/trek.models.ts +0 -1
- package/src/modules/trek/trek.schema.ts +0 -1
- package/src/modules/trek/trek.service.ts +0 -1031
- package/src/modules/trek/trek.types.ts +0 -1
- /package/{src/modules/evolution → evolution}/index.ts +0 -0
- /package/{src/modules/infinite → infinite}/index.ts +0 -0
- /package/{src/modules/isles → isles}/index.ts +0 -0
- /package/{src/modules/oasis → oasis}/index.ts +0 -0
- /package/{src/modules/trek → trek}/index.ts +0 -0
- /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
|
-
}
|