@imtbl/minting-backend 2.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,618 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/index.ts
2
+ var _blockchaindata = require('@imtbl/blockchain-data');
3
+ var _webhook = require('@imtbl/webhook');
4
+ var _metrics = require('@imtbl/metrics');
5
+
6
+ // src/analytics/index.ts
7
+
8
+ var moduleName = "minting_backend_sdk";
9
+ var trackInitializePersistencePG = () => {
10
+ try {
11
+ _metrics.track.call(void 0, moduleName, "initializePersistencePG");
12
+ } catch (e3) {
13
+ }
14
+ };
15
+ var trackInitializePersistencePrismaSqlite = () => {
16
+ try {
17
+ _metrics.track.call(void 0, moduleName, "initializePersistencePrismaSqlite");
18
+ } catch (e4) {
19
+ }
20
+ };
21
+ var trackSubmitMintingRequests = () => {
22
+ try {
23
+ _metrics.track.call(void 0, moduleName, "submitMintingRequests");
24
+ } catch (e5) {
25
+ }
26
+ };
27
+ var trackProcessMint = () => {
28
+ try {
29
+ _metrics.track.call(void 0, moduleName, "processMint");
30
+ } catch (e6) {
31
+ }
32
+ };
33
+ var trackRecordMint = () => {
34
+ try {
35
+ _metrics.track.call(void 0, moduleName, "recordMint");
36
+ } catch (e7) {
37
+ }
38
+ };
39
+ var trackError = (error) => {
40
+ try {
41
+ _metrics.track.call(void 0, moduleName, "error", {
42
+ name: error.name,
43
+ message: error.message
44
+ });
45
+ } catch (e8) {
46
+ }
47
+ };
48
+ var trackUncaughtException = (error, origin) => {
49
+ try {
50
+ _metrics.track.call(void 0, moduleName, "error", {
51
+ name: error.name,
52
+ message: error.message,
53
+ origin
54
+ });
55
+ } catch (e9) {
56
+ }
57
+ };
58
+
59
+ // src/persistence/pg/postgres.ts
60
+ var mintingPersistence = (client) => {
61
+ trackInitializePersistencePG();
62
+ return {
63
+ recordMint: async (request) => {
64
+ const r = await client.query(
65
+ `
66
+ INSERT INTO im_assets (asset_id, contract_address, owner_address, metadata, amount, token_id)
67
+ VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (asset_id, contract_address) DO NOTHING;
68
+ `,
69
+ [
70
+ request.asset_id,
71
+ request.contract_address,
72
+ request.owner_address,
73
+ request.metadata,
74
+ request.amount,
75
+ request.token_id
76
+ ]
77
+ );
78
+ if (r.rowCount === 0) {
79
+ throw new Error("Duplicated mint");
80
+ }
81
+ },
82
+ getNextBatchForSubmission: async (limit) => {
83
+ const res = await client.query(`
84
+ WITH limited_assets AS (
85
+ SELECT id
86
+ FROM im_assets
87
+ WHERE minting_status IS NULL
88
+ LIMIT $1
89
+ FOR UPDATE SKIP LOCKED
90
+ )
91
+ UPDATE im_assets
92
+ SET minting_status = 'submitting'
93
+ WHERE minting_status IS NULL
94
+ AND id IN (SELECT id FROM limited_assets)
95
+ RETURNING *;
96
+ `, [limit]);
97
+ return res.rows;
98
+ },
99
+ updateMintingStatusToSubmitted: async (ids) => {
100
+ await client.query(`
101
+ UPDATE im_assets SET minting_status = $2 WHERE id = ANY($1);
102
+ `, [ids, "submitted"]);
103
+ },
104
+ syncMintingStatus: async (submittedMintRequest) => {
105
+ await client.query(`
106
+ INSERT INTO im_assets (
107
+ asset_id,
108
+ contract_address,
109
+ owner_address,
110
+ token_id,
111
+ minting_status,
112
+ metadata_id,
113
+ last_imtbl_zkevm_mint_request_updated_id,
114
+ error,
115
+ amount
116
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (asset_id, contract_address)
117
+ DO UPDATE SET
118
+ owner_address = $3,
119
+ token_id = $4,
120
+ minting_status = $5,
121
+ metadata_id = $6,
122
+ last_imtbl_zkevm_mint_request_updated_id = $7,
123
+ error = $8
124
+ where (
125
+ im_assets.last_imtbl_zkevm_mint_request_updated_id < $7 OR
126
+ im_assets.last_imtbl_zkevm_mint_request_updated_id is null
127
+ );
128
+ `, [
129
+ submittedMintRequest.assetId,
130
+ submittedMintRequest.contractAddress,
131
+ submittedMintRequest.ownerAddress,
132
+ submittedMintRequest.tokenId,
133
+ submittedMintRequest.status,
134
+ submittedMintRequest.metadataId,
135
+ submittedMintRequest.imtblZkevmMintRequestUpdatedId,
136
+ submittedMintRequest.error,
137
+ submittedMintRequest.amount
138
+ ]);
139
+ },
140
+ markAsConflict: async (assetIds, contractAddress) => {
141
+ await client.query(`
142
+ UPDATE im_assets
143
+ SET minting_status = 'conflicting'
144
+ WHERE asset_id = ANY($1)
145
+ AND contract_address = $2;
146
+ `, [assetIds, contractAddress]);
147
+ },
148
+ resetMintingStatus: async (ids) => {
149
+ await client.query(`
150
+ UPDATE im_assets SET minting_status = null WHERE id = ANY($1);
151
+ `, [ids]);
152
+ },
153
+ markForRetry: async (ids) => {
154
+ await client.query(`
155
+ UPDATE im_assets
156
+ SET minting_status = null, tried_count = tried_count + 1 WHERE id = ANY($1);
157
+ `, [ids]);
158
+ },
159
+ updateMintingStatusToSubmissionFailed: async (ids) => {
160
+ await client.query(`
161
+ UPDATE im_assets SET minting_status = 'submission_failed' WHERE id = ANY($1);
162
+ `, [ids]);
163
+ },
164
+ getMintingRequest: async (contractAddress, referenceId) => {
165
+ const res = await client.query(`
166
+ SELECT * FROM im_assets WHERE contract_address = $1 and asset_id = $2;
167
+ `, [contractAddress, referenceId]);
168
+ return res.rows[0] || null;
169
+ }
170
+ };
171
+ };
172
+
173
+ // src/persistence/prismaSqlite/sqlite.ts
174
+ var mintingPersistence2 = (client) => {
175
+ trackInitializePersistencePrismaSqlite();
176
+ return {
177
+ recordMint: async (request) => {
178
+ const result = await client.imAssets.upsert({
179
+ where: {
180
+ im_assets_uindex: {
181
+ assetId: request.asset_id,
182
+ contractAddress: request.contract_address
183
+ }
184
+ },
185
+ update: {},
186
+ // Do nothing on conflict
187
+ create: {
188
+ assetId: request.asset_id,
189
+ contractAddress: request.contract_address,
190
+ ownerAddress: request.owner_address,
191
+ metadata: JSON.stringify(request.metadata),
192
+ // Serialize JSON metadata
193
+ amount: request.amount || null,
194
+ tokenId: request.token_id || null
195
+ }
196
+ });
197
+ if (!result) {
198
+ throw new Error("Duplicated mint");
199
+ }
200
+ },
201
+ // WARNING: this is NOT concurrency safe. Please only call this method one at a time.
202
+ getNextBatchForSubmission: async (limit) => {
203
+ const assets = await client.imAssets.findMany({
204
+ where: {
205
+ mintingStatus: null
206
+ },
207
+ take: limit
208
+ });
209
+ const assetIds = assets.map((asset) => asset.id);
210
+ await client.imAssets.updateMany({
211
+ where: {
212
+ id: {
213
+ in: assetIds
214
+ }
215
+ },
216
+ data: {
217
+ mintingStatus: "submitting"
218
+ }
219
+ });
220
+ const updatedAssets = await client.imAssets.findMany({
221
+ where: {
222
+ id: {
223
+ in: assetIds
224
+ }
225
+ }
226
+ });
227
+ return updatedAssets.map((asset) => ({
228
+ id: asset.id,
229
+ contract_address: asset.contractAddress,
230
+ wallet_address: asset.ownerAddress,
231
+ asset_id: asset.assetId,
232
+ metadata: asset.metadata ? JSON.parse(asset.metadata) : null,
233
+ owner_address: asset.ownerAddress,
234
+ tried_count: asset.triedCount,
235
+ amount: asset.amount || null,
236
+ token_id: asset.tokenId || null
237
+ }));
238
+ },
239
+ updateMintingStatusToSubmitted: async (ids) => {
240
+ await client.imAssets.updateMany({
241
+ where: {
242
+ id: {
243
+ in: ids
244
+ }
245
+ },
246
+ data: {
247
+ mintingStatus: "submitted"
248
+ }
249
+ });
250
+ },
251
+ syncMintingStatus: async (submittedMintRequest) => {
252
+ const existingAsset = await client.imAssets.findUnique({
253
+ where: {
254
+ im_assets_uindex: {
255
+ assetId: submittedMintRequest.assetId,
256
+ contractAddress: submittedMintRequest.contractAddress
257
+ }
258
+ }
259
+ });
260
+ if (existingAsset && (existingAsset.lastImtblZkevmMintRequestUpdatedId === null || existingAsset.lastImtblZkevmMintRequestUpdatedId < submittedMintRequest.imtblZkevmMintRequestUpdatedId)) {
261
+ await client.imAssets.update({
262
+ where: {
263
+ im_assets_uindex: {
264
+ assetId: submittedMintRequest.assetId,
265
+ contractAddress: submittedMintRequest.contractAddress
266
+ }
267
+ },
268
+ data: {
269
+ ownerAddress: submittedMintRequest.ownerAddress,
270
+ tokenId: submittedMintRequest.tokenId,
271
+ mintingStatus: submittedMintRequest.status,
272
+ metadataId: submittedMintRequest.metadataId,
273
+ lastImtblZkevmMintRequestUpdatedId: submittedMintRequest.imtblZkevmMintRequestUpdatedId,
274
+ error: submittedMintRequest.error,
275
+ amount: submittedMintRequest.amount || null
276
+ }
277
+ });
278
+ } else if (!existingAsset) {
279
+ await client.imAssets.create({
280
+ data: {
281
+ assetId: submittedMintRequest.assetId,
282
+ contractAddress: submittedMintRequest.contractAddress,
283
+ ownerAddress: submittedMintRequest.ownerAddress,
284
+ tokenId: submittedMintRequest.tokenId,
285
+ mintingStatus: submittedMintRequest.status,
286
+ metadataId: submittedMintRequest.metadataId,
287
+ lastImtblZkevmMintRequestUpdatedId: submittedMintRequest.imtblZkevmMintRequestUpdatedId,
288
+ error: submittedMintRequest.error,
289
+ amount: submittedMintRequest.amount || null
290
+ }
291
+ });
292
+ }
293
+ },
294
+ updateMintingStatusToSubmissionFailed: async (ids) => {
295
+ await client.imAssets.updateMany({
296
+ where: {
297
+ id: {
298
+ in: ids
299
+ }
300
+ },
301
+ data: {
302
+ mintingStatus: "submission_failed"
303
+ }
304
+ });
305
+ },
306
+ markAsConflict: async (assetIds, contractAddress) => {
307
+ await client.imAssets.updateMany({
308
+ where: {
309
+ assetId: {
310
+ in: assetIds
311
+ // Targets assets where assetId is in the provided list
312
+ },
313
+ contractAddress
314
+ // Additional condition for contract address
315
+ },
316
+ data: {
317
+ mintingStatus: "conflicting"
318
+ // Set the new status
319
+ }
320
+ });
321
+ },
322
+ resetMintingStatus: async (ids) => {
323
+ await client.imAssets.updateMany({
324
+ where: {
325
+ id: {
326
+ in: ids
327
+ // Condition to match ids
328
+ }
329
+ },
330
+ data: {
331
+ mintingStatus: null
332
+ // Setting minting_status to null
333
+ }
334
+ });
335
+ },
336
+ // this method is not concurrency safe
337
+ markForRetry: async (ids) => {
338
+ const assets = await client.imAssets.findMany({
339
+ where: {
340
+ id: {
341
+ in: ids
342
+ }
343
+ },
344
+ select: {
345
+ id: true,
346
+ triedCount: true
347
+ // Assuming the field is named triedCount
348
+ }
349
+ });
350
+ for (const asset of assets) {
351
+ await client.imAssets.update({
352
+ where: {
353
+ id: asset.id
354
+ },
355
+ data: {
356
+ mintingStatus: null,
357
+ triedCount: asset.triedCount + 1
358
+ }
359
+ });
360
+ }
361
+ },
362
+ getMintingRequest: async (contractAddress, referenceId) => {
363
+ const asset = await client.imAssets.findFirst({
364
+ where: {
365
+ contractAddress,
366
+ assetId: referenceId
367
+ }
368
+ });
369
+ if (!asset) {
370
+ return null;
371
+ }
372
+ return {
373
+ asset_id: asset.assetId,
374
+ contract_address: asset.contractAddress,
375
+ id: asset.id,
376
+ metadata: asset.metadata ? JSON.parse(asset.metadata) : null,
377
+ owner_address: asset.ownerAddress,
378
+ tried_count: asset.triedCount,
379
+ wallet_address: asset.ownerAddress,
380
+ amount: asset.amount || null
381
+ };
382
+ }
383
+ };
384
+ };
385
+
386
+ // src/minting.ts
387
+ var recordMint = async (mintingPersistence3, mintRequest) => {
388
+ trackRecordMint();
389
+ mintingPersistence3.recordMint(mintRequest);
390
+ };
391
+ var defaultMintingDelay = 1e3;
392
+ var submitMintingRequests = async (mintingPersistence3, blockchainDataSDKClient, {
393
+ defaultBatchSize = 1e3,
394
+ chainName = "imtbl-zkevm-testnet",
395
+ maxNumberOfTries = 3
396
+ }, logger = console, maxLoops = Infinity) => {
397
+ trackSubmitMintingRequests();
398
+ let mintingResponse;
399
+ let numberOfLoops = 0;
400
+ while (numberOfLoops++ < maxLoops) {
401
+ await new Promise((resolve) => {
402
+ setTimeout(resolve, defaultMintingDelay);
403
+ });
404
+ let batchSize = Math.min(
405
+ _optionalChain([mintingResponse, 'optionalAccess', _ => _.imx_remaining_mint_requests]) ? parseInt(mintingResponse.imx_remaining_mint_requests, 10) : defaultBatchSize,
406
+ defaultBatchSize
407
+ );
408
+ if (batchSize === 0 && mintingResponse && new Date(mintingResponse.imx_mint_requests_limit_reset) > /* @__PURE__ */ new Date()) {
409
+ logger.info(
410
+ `minting limit reached, waiting for reset at ${_optionalChain([mintingResponse, 'optionalAccess', _2 => _2.imx_mint_requests_limit_reset])}`
411
+ );
412
+ continue;
413
+ }
414
+ if (batchSize === 0) {
415
+ logger.info(
416
+ `minting limit has been reset, use default batch size: ${defaultBatchSize}`
417
+ );
418
+ mintingResponse = void 0;
419
+ batchSize = defaultBatchSize;
420
+ }
421
+ const pendingMints = await mintingPersistence3.getNextBatchForSubmission(
422
+ batchSize
423
+ );
424
+ if (pendingMints.length === 0) {
425
+ logger.info("no assets to mint");
426
+ continue;
427
+ }
428
+ const chunkedAssets = pendingMints.sort(
429
+ (a, b) => a.contract_address > b.contract_address ? 1 : -1
430
+ ).reduce((acc, row) => {
431
+ if (acc.length === 0) {
432
+ return [{ contractAddress: row.contract_address, assets: [row] }];
433
+ }
434
+ const lastBatch = acc[acc.length - 1];
435
+ if (lastBatch.contractAddress === row.contract_address && lastBatch.assets.length < 100) {
436
+ return [...acc.slice(0, -1), { ...lastBatch, assets: [...lastBatch.assets, row] }];
437
+ }
438
+ return [...acc, { contractAddress: row.contract_address, assets: [row] }];
439
+ }, []);
440
+ const mintingResults = await Promise.allSettled(
441
+ chunkedAssets.map(
442
+ async ({ contractAddress, assets }) => {
443
+ const mintingRequest = {
444
+ chainName,
445
+ contractAddress,
446
+ createMintRequestRequest: {
447
+ assets: assets.map((row) => ({
448
+ reference_id: row.asset_id,
449
+ owner_address: row.owner_address,
450
+ metadata: row.metadata,
451
+ token_id: row.token_id,
452
+ amount: row.amount ? `${row.amount}` : null
453
+ }))
454
+ }
455
+ };
456
+ try {
457
+ const response = await blockchainDataSDKClient.createMintRequest(
458
+ mintingRequest
459
+ );
460
+ logger.info(
461
+ `mintingResponse: ${JSON.stringify(response, null, 2)}`
462
+ );
463
+ await mintingPersistence3.updateMintingStatusToSubmitted(
464
+ assets.map(({ id }) => id)
465
+ );
466
+ return response;
467
+ } catch (e) {
468
+ logger.error(e);
469
+ trackError(e);
470
+ if (e.code === "CONFLICT_ERROR" && _optionalChain([e, 'access', _3 => _3.details, 'optionalAccess', _4 => _4.id]) === "reference_id") {
471
+ try {
472
+ await mintingPersistence3.markAsConflict(
473
+ e.details.values,
474
+ contractAddress
475
+ );
476
+ await mintingPersistence3.resetMintingStatus(
477
+ assets.map(({ id }) => id).filter((id) => !e.details.values.includes(id))
478
+ );
479
+ } catch (e2) {
480
+ logger.error(e2);
481
+ trackError(e);
482
+ }
483
+ } else {
484
+ const { assetsToRetry, assetsExceededMaxNumberOfTries } = assets.reduce(
485
+ (acc, { tried_count = 0, id }) => {
486
+ if (tried_count < maxNumberOfTries) {
487
+ acc.assetsToRetry.push(id);
488
+ } else {
489
+ acc.assetsExceededMaxNumberOfTries.push(id);
490
+ }
491
+ return acc;
492
+ },
493
+ {
494
+ assetsToRetry: [],
495
+ assetsExceededMaxNumberOfTries: []
496
+ }
497
+ );
498
+ await mintingPersistence3.markForRetry(
499
+ assetsToRetry
500
+ );
501
+ await mintingPersistence3.updateMintingStatusToSubmissionFailed(
502
+ assetsExceededMaxNumberOfTries
503
+ );
504
+ }
505
+ return e;
506
+ }
507
+ }
508
+ )
509
+ );
510
+ mintingResponse = _optionalChain([mintingResults, 'access', _5 => _5.reverse, 'call', _6 => _6(), 'access', _7 => _7.find, 'call', _8 => _8(
511
+ (r) => r.status === "fulfilled"
512
+ ), 'optionalAccess', _9 => _9.value]);
513
+ }
514
+ };
515
+ var processMint = async (mintingPersistence3, event, logger = console) => {
516
+ trackProcessMint();
517
+ if (event.event_name !== "imtbl_zkevm_mint_request_updated") {
518
+ logger.info(
519
+ `${event.event_name} is not imtbl_zkevm_mint_request_updated, skip.`
520
+ );
521
+ return;
522
+ }
523
+ const referenceId = event.data.reference_id;
524
+ if (!referenceId) {
525
+ throw new Error("reference_id not found in webhook event");
526
+ }
527
+ const contractAddress = event.data.contract_address;
528
+ if (!contractAddress) {
529
+ throw new Error("contract_address not found in webhook event");
530
+ }
531
+ if (event.data.status === "failed") {
532
+ logger.error(`mint failed: ${JSON.stringify(event.data, null, 2)}`);
533
+ }
534
+ const mintReq = await mintingPersistence3.getMintingRequest(
535
+ contractAddress,
536
+ referenceId
537
+ );
538
+ if (!mintReq) {
539
+ logger.info(
540
+ `minting request not found in the database, ${JSON.stringify(event.data)}`
541
+ );
542
+ }
543
+ const ownerAddress = _optionalChain([mintReq, 'optionalAccess', _10 => _10.wallet_address]) || event.data.owner_address;
544
+ if (!ownerAddress) {
545
+ logger.error("owner_address missing");
546
+ throw new Error("owner_address missing");
547
+ }
548
+ await mintingPersistence3.syncMintingStatus({
549
+ tokenId: event.data.token_id,
550
+ status: event.data.status,
551
+ assetId: referenceId,
552
+ contractAddress,
553
+ ownerAddress,
554
+ metadataId: event.data.metadata_id,
555
+ imtblZkevmMintRequestUpdatedId: event.event_id,
556
+ error: event.data.error ? JSON.stringify(event.data.error) : null,
557
+ amount: event.data.amount || null
558
+ });
559
+ };
560
+
561
+ // src/index.ts
562
+ var noopHandlers = {
563
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
564
+ zkevmMintRequestUpdated: async (event) => {
565
+ }
566
+ };
567
+ var MintingBackendModule = class {
568
+
569
+
570
+
571
+
572
+ constructor(config) {
573
+ this.baseConfig = config.baseConfig;
574
+ this.persistence = config.persistence;
575
+ this.logger = config.logger || console;
576
+ this.blockchainDataClient = new (0, _blockchaindata.BlockchainData)({
577
+ baseConfig: config.baseConfig
578
+ });
579
+ _metrics.setEnvironment.call(void 0, this.baseConfig.environment);
580
+ if (this.baseConfig.publishableKey) {
581
+ _metrics.setPublishableApiKey.call(void 0, this.baseConfig.publishableKey);
582
+ }
583
+ }
584
+ async recordMint(mintRequest) {
585
+ await recordMint(this.persistence, mintRequest);
586
+ }
587
+ async submitMintingRequests(config) {
588
+ await submitMintingRequests(
589
+ this.persistence,
590
+ this.blockchainDataClient,
591
+ config
592
+ );
593
+ }
594
+ async processMint(body, otherHandlers = noopHandlers) {
595
+ await _webhook.handle.call(void 0, body, this.baseConfig.environment, {
596
+ zkevmMintRequestUpdated: async (event) => {
597
+ await processMint(this.persistence, event, this.logger);
598
+ if (otherHandlers.zkevmMintRequestUpdated) {
599
+ otherHandlers.zkevmMintRequestUpdated(event);
600
+ }
601
+ }
602
+ });
603
+ }
604
+ };
605
+ if (typeof process !== "undefined" && process.on) {
606
+ try {
607
+ process.on("uncaughtExceptionMonitor", trackUncaughtException);
608
+ } catch (e10) {
609
+ }
610
+ }
611
+
612
+
613
+
614
+
615
+
616
+
617
+
618
+ exports.MintingBackendModule = MintingBackendModule; exports.mintingPersistencePg = mintingPersistence; exports.mintingPersistencePrismaSqlite = mintingPersistence2; exports.processMint = processMint; exports.recordMint = recordMint; exports.submitMintingRequests = submitMintingRequests;