-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathanalyze-successful-mints.ts
More file actions
944 lines (822 loc) · 39 KB
/
analyze-successful-mints.ts
File metadata and controls
944 lines (822 loc) · 39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
/**
* Analyze successful mint transactions to learn patterns
* - Gas prices used by successful minters
* - Timing patterns (when did they send relative to unlock)
* - Transaction inclusion patterns
* - Block timing analysis
*/
import 'dotenv/config'
import { createPublicClient, http, getContract, decodeEventLog, parseAbi } from 'viem'
import { loadConfig, beraChain } from './config.js'
// Beratrail API base URL
const BERATRAIL_API = 'https://api.beratrail.io'
const MITEDDY_ADDR = '0x111111111fd1a588bdb8254e3af1fc2fb0d9078a' as const
const STEADY_TEDDYS = '0x88888888A9361f15AAdBAca355A6B2938C6A674e' as const
const MIBERA = '0x6666397DFe9a8c469BF65dc744CB1C733416c420' as const
const MINT_ABI = parseAbi([
'function mint(uint256 teddyId, uint256 miberaId) public returns (uint256)',
'event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)',
])
const TRANSFER_ABI = parseAbi([
'event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)',
])
interface SuccessfulMint {
txHash: string
blockNumber: bigint
blockTimestamp: bigint
from: string
gasPrice: bigint
maxFeePerGas: bigint | null
maxPriorityFeePerGas: bigint | null
gasUsed: bigint
gasLimit: bigint
nonce: number
positionInBlock: number
effectiveGasPrice: bigint
timeSinceBlockStart: number // ms since block timestamp
// Timing data
txTimestamp: number // When transaction was actually sent (estimated from block time)
blockTimeMs: number // Block timestamp in milliseconds
previousBlockTime: number | null // Previous block timestamp
timeBetweenBlocks: number | null // Time between this block and previous
// Additional useful data
calldataLength: number
inputData: string
value: bigint
transactionIndex: number
logIndex: number
contractAddress: string // Which contract was called
}
async function analyzeSuccessfulMints(maxBlocksToSearch: number = 1000000) {
// Use public RPCs for analysis to avoid rate limiting user's RPC
const publicRPCs = [
'https://rpc.berachain.com',
'https://berachain-rpc.publicnode.com',
'https://berachain.drpc.org',
]
// Try public RPCs first, fall back to user's RPC if needed
let client: ReturnType<typeof createPublicClient>
let rpcUsed = ''
for (const rpc of publicRPCs) {
try {
const testClient = createPublicClient({
chain: beraChain,
transport: http(rpc, { timeout: 10000 }),
})
await testClient.getBlockNumber() // Test connection
client = testClient
rpcUsed = rpc
break
} catch (e) {
// Try next RPC
continue
}
}
// Fallback to user's RPC if all public RPCs fail
if (!client!) {
const cfg = loadConfig()
client = createPublicClient({
chain: beraChain,
transport: http(cfg.rpcs[0], { timeout: 30000 }),
})
rpcUsed = cfg.rpcs[0]
}
console.log(`📡 Using RPC: ${rpcUsed}\n`)
console.log('🔍 Analyzing successful mint transactions...\n')
// Get ALL events from both contracts and analyze them
console.log('🔍 Fetching ALL events from Steady Teddys and MiTeddy contracts...\n')
let allSteadyTeddyLogs: any[] = []
let allMiTeddyLogs: any[] = []
let currentBlock = await client.getBlockNumber()
let fromBlock = currentBlock - BigInt(maxBlocksToSearch)
console.log(`📦 Block range: ${fromBlock} to ${currentBlock} (${maxBlocksToSearch} blocks)\n`)
// Fetch ALL logs from Steady Teddys contract
console.log('📋 Fetching ALL events from Steady Teddys contract...\n')
const batchSize = 10000n
let processed = 0
for (let blockStart = fromBlock; blockStart <= currentBlock; blockStart += batchSize) {
const blockEnd = blockStart + batchSize - 1n > currentBlock ? currentBlock : blockStart + batchSize - 1n
try {
const logs = await client.getLogs({
address: STEADY_TEDDYS,
fromBlock: blockStart,
toBlock: blockEnd,
})
allSteadyTeddyLogs.push(...logs)
processed += Number(blockEnd - blockStart + 1n)
process.stdout.write(`\r Searched ${processed.toLocaleString()}/${maxBlocksToSearch.toLocaleString()} blocks... Found ${allSteadyTeddyLogs.length} total events`)
await new Promise(resolve => setTimeout(resolve, 200))
} catch (error: any) {
if (error.message?.includes('limit') || error.message?.includes('range') || error.message?.includes('413')) {
// Try smaller batches
const smallerBatch = 1000n
for (let b = blockStart; b <= blockEnd && b <= currentBlock; b += smallerBatch) {
const bEnd = b + smallerBatch - 1n > blockEnd ? blockEnd : b + smallerBatch - 1n
try {
const logs = await client.getLogs({
address: STEADY_TEDDYS,
fromBlock: b,
toBlock: bEnd,
})
allSteadyTeddyLogs.push(...logs)
processed += Number(bEnd - b + 1n)
process.stdout.write(`\r Searched ${processed.toLocaleString()}/${maxBlocksToSearch.toLocaleString()} blocks... Found ${allSteadyTeddyLogs.length} total events`)
await new Promise(resolve => setTimeout(resolve, 100))
} catch (e) {
// Skip
}
}
}
}
}
console.log(`\n✅ Found ${allSteadyTeddyLogs.length} total events from Steady Teddys contract\n`)
// Fetch ALL logs from MiTeddy contract
console.log('📋 Fetching ALL events from MiTeddy contract...\n')
processed = 0
for (let blockStart = fromBlock; blockStart <= currentBlock; blockStart += batchSize) {
const blockEnd = blockStart + batchSize - 1n > currentBlock ? currentBlock : blockStart + batchSize - 1n
try {
const logs = await client.getLogs({
address: MITEDDY_ADDR,
fromBlock: blockStart,
toBlock: blockEnd,
})
allMiTeddyLogs.push(...logs)
processed += Number(blockEnd - blockStart + 1n)
process.stdout.write(`\r Searched ${processed.toLocaleString()}/${maxBlocksToSearch.toLocaleString()} blocks... Found ${allMiTeddyLogs.length} total events`)
await new Promise(resolve => setTimeout(resolve, 200))
} catch (error: any) {
if (error.message?.includes('limit') || error.message?.includes('range') || error.message?.includes('413')) {
const smallerBatch = 1000n
for (let b = blockStart; b <= blockEnd && b <= currentBlock; b += smallerBatch) {
const bEnd = b + smallerBatch - 1n > blockEnd ? blockEnd : b + smallerBatch - 1n
try {
const logs = await client.getLogs({
address: MITEDDY_ADDR,
fromBlock: b,
toBlock: bEnd,
})
allMiTeddyLogs.push(...logs)
processed += Number(bEnd - b + 1n)
process.stdout.write(`\r Searched ${processed.toLocaleString()}/${maxBlocksToSearch.toLocaleString()} blocks... Found ${allMiTeddyLogs.length} total events`)
await new Promise(resolve => setTimeout(resolve, 100))
} catch (e) {
// Skip
}
}
}
}
}
console.log(`\n✅ Found ${allMiTeddyLogs.length} total events from MiTeddy contract\n`)
// Analyze all events to find mints
console.log('🔍 Analyzing all events to identify mints...\n')
// Get unique transaction hashes from Steady Teddys logs
const steadyTeddyTxHashes = new Set(allSteadyTeddyLogs.map(l => l.transactionHash.toLowerCase()))
const miTeddyTxHashes = new Set(allMiTeddyLogs.map(l => l.transactionHash.toLowerCase()))
console.log(` Steady Teddys: ${allSteadyTeddyLogs.length} events across ${steadyTeddyTxHashes.size} transactions`)
console.log(` MiTeddy: ${allMiTeddyLogs.length} events across ${miTeddyTxHashes.size} transactions\n`)
// Analyze Steady Teddys events - look for ALL Transfer events and analyze them
const transferLogs: any[] = []
const allTransferLogs: any[] = []
const eventTypes = new Map<string, number>()
const transferFromCounts = new Map<string, number>()
const transferToCounts = new Map<string, number>()
for (const log of allSteadyTeddyLogs) {
// Try to decode as Transfer event
try {
const decoded = decodeEventLog({
abi: TRANSFER_ABI,
data: log.data,
topics: log.topics,
})
eventTypes.set('Transfer', (eventTypes.get('Transfer') || 0) + 1)
allTransferLogs.push({ log, decoded })
const from = decoded.args.from?.toLowerCase() || 'unknown'
const to = decoded.args.to?.toLowerCase() || 'unknown'
transferFromCounts.set(from, (transferFromCounts.get(from) || 0) + 1)
transferToCounts.set(to, (transferToCounts.get(to) || 0) + 1)
// Check if it's a mint (from zero address)
if (from === '0x0000000000000000000000000000000000000000') {
transferLogs.push(log)
}
} catch (e) {
// Not a Transfer event - identify by topic[0] (event signature)
const eventSig = log.topics[0] || 'unknown'
eventTypes.set(eventSig, (eventTypes.get(eventSig) || 0) + 1)
}
}
console.log(`📊 Event breakdown from Steady Teddys:`)
for (const [event, count] of Array.from(eventTypes.entries()).sort((a, b) => b[1] - a[1])) {
console.log(` ${event}: ${count}`)
}
console.log(`\n📊 Transfer event analysis:`)
console.log(` Total Transfer events: ${allTransferLogs.length}`)
console.log(` Transfers from zero address (mints): ${transferLogs.length}`)
console.log(`\n Top 'from' addresses:`)
for (const [addr, count] of Array.from(transferFromCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10)) {
const isZero = addr === '0x0000000000000000000000000000000000000000'
console.log(` ${addr}${isZero ? ' (ZERO - MINT!)' : ''}: ${count}`)
}
console.log(`\n Top 'to' addresses:`)
for (const [addr, count] of Array.from(transferToCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10)) {
console.log(` ${addr}: ${count}`)
}
console.log(`\n✅ Found ${transferLogs.length} mint events (Transfer from zero address)\n`)
// If no zero-address mints, check other patterns
if (transferLogs.length === 0 && allTransferLogs.length > 0) {
console.log('🔍 No zero-address mints found. Checking other mint patterns...\n')
const miTeddyAddrLower = MITEDDY_ADDR.toLowerCase()
// Check transfers TO MiTeddy contract (these might be successful mints!)
const transfersToMiTeddy = allTransferLogs.filter(({ decoded }) =>
decoded.args.to?.toLowerCase() === miTeddyAddrLower
)
if (transfersToMiTeddy.length > 0) {
console.log(` ✅ Found ${transfersToMiTeddy.length} transfers TO MiTeddy contract - these are likely successful mints!`)
console.log(`\n 📋 Mint Transaction Hashes:`)
for (const { log, decoded } of transfersToMiTeddy) {
console.log(` ${log.transactionHash}`)
console.log(` Block: ${log.blockNumber}`)
console.log(` From: ${decoded.args.from}`)
console.log(` To: ${decoded.args.to}`)
if ('tokenId' in decoded.args && decoded.args.tokenId !== undefined) {
console.log(` Token ID: ${decoded.args.tokenId}`)
}
}
console.log(``)
transferLogs.push(...transfersToMiTeddy.map(({ log }) => log))
}
// Also check transfers FROM MiTeddy contract
const mintsFromMiTeddy = allTransferLogs.filter(({ decoded }) =>
decoded.args.from?.toLowerCase() === miTeddyAddrLower
)
if (mintsFromMiTeddy.length > 0) {
console.log(` Found ${mintsFromMiTeddy.length} transfers FROM MiTeddy contract`)
// Don't add these as they might be burns/transfers out
}
}
if (transferLogs.length === 0) {
console.log('⚠️ No mints found via Transfer events. Checking for other patterns...\n')
// Check if any transactions to MiTeddy contract resulted in Steady Teddys events
console.log(' Checking transactions to MiTeddy contract that also have Steady Teddys events...')
const intersectingTxs = Array.from(miTeddyTxHashes).filter(hash => steadyTeddyTxHashes.has(hash))
console.log(` Found ${intersectingTxs.length} transactions that interacted with both contracts\n`)
if (intersectingTxs.length === 0) {
console.log('⚠️ No mints found. This could mean:')
console.log(' - No mints have occurred yet')
console.log(' - The block range is too limited')
console.log(' - The RPC has restrictions')
console.log('\n💡 Try checking Beratrail.io manually:')
console.log(` Steady Teddys: https://beratrail.io/address/${STEADY_TEDDYS}`)
console.log(` MiTeddy Contract: https://beratrail.io/address/${MITEDDY_ADDR}`)
return
}
// Use intersecting transactions as potential mints
for (const txHash of intersectingTxs.slice(0, 100)) {
const log = allSteadyTeddyLogs.find(l => l.transactionHash.toLowerCase() === txHash)
if (log) {
transferLogs.push(log)
}
}
console.log(`✅ Using ${transferLogs.length} transactions that interacted with both contracts as potential mints\n`)
}
// Get transaction details for each mint
const mints: SuccessfulMint[] = []
const failedTxs: Array<{ hash: string; blockNumber: bigint; error?: string }> = []
const partialMints: Array<{ txHash: string; blockNumber: bigint; error?: string }> = []
console.log(`📋 Analyzing transaction details for ${transferLogs.length} mint transactions...\n`)
// Process Transfer event logs - these are the mints
for (let i = 0; i < transferLogs.length; i++) {
const log = transferLogs[i]
console.log(` [${i + 1}/${transferLogs.length}] Analyzing ${log.transactionHash}...`)
try {
const tx = await client.getTransaction({ hash: log.transactionHash })
const receipt = await client.getTransactionReceipt({ hash: log.transactionHash })
const block = await client.getBlock({ blockNumber: log.blockNumber })
// These are already identified as mints (transfers to MiTeddy contract)
// So we should analyze them regardless
// But let's verify it's actually a successful transaction
if (receipt.status !== 'success') {
console.log(` ⚠️ Transaction failed/reverted, skipping`)
continue
}
// Check if this transaction has a Transfer event to the MiTeddy contract
const hasTransferToMiTeddy = receipt.logs.some(log => {
const addr = log.address.toLowerCase()
if (addr === STEADY_TEDDYS.toLowerCase()) {
try {
const decoded = decodeEventLog({
abi: TRANSFER_ABI,
data: log.data,
topics: log.topics,
})
return decoded.args.to?.toLowerCase() === MITEDDY_ADDR.toLowerCase()
} catch (e) {
return false
}
}
return false
})
if (!hasTransferToMiTeddy) {
console.log(` ⚠️ No transfer to MiTeddy contract found in receipt, skipping`)
continue
}
// Get position in block (approximate by checking transaction index)
const blockTxs = await client.getBlock({ blockNumber: log.blockNumber, includeTransactions: true })
const positionInBlock = blockTxs.transactions.findIndex(t =>
typeof t === 'object' && 'hash' in t && t.hash === log.transactionHash
)
const effectiveGasPrice = receipt.effectiveGasPrice || tx.gasPrice || 0n
const maxFeePerGas = ('maxFeePerGas' in tx && tx.maxFeePerGas) ? tx.maxFeePerGas : null
const maxPriorityFeePerGas = ('maxPriorityFeePerGas' in tx && tx.maxPriorityFeePerGas) ? tx.maxPriorityFeePerGas : null
const gasPrice = tx.gasPrice || 0n
// Calculate timing data
const blockTimeMs = Number(block.timestamp) * 1000
const blockStartTime = blockTimeMs
const timeSinceBlockStart = 0 // Transactions are included at block creation time
// Get previous block for timing analysis
let previousBlockTime: number | null = null
let timeBetweenBlocks: number | null = null
try {
if (log.blockNumber > 0n) {
const prevBlock = await client.getBlock({ blockNumber: log.blockNumber - 1n })
previousBlockTime = Number(prevBlock.timestamp) * 1000
timeBetweenBlocks = blockTimeMs - previousBlockTime
}
} catch (e) {
// Ignore if can't get previous block
}
// Estimate when transaction was sent (block time is when it was included)
// For EIP-1559, transactions are typically sent slightly before block time
// We'll use block time as proxy, but note this is when included, not sent
const txTimestamp = blockTimeMs
// Get additional transaction data
const calldataLength = tx.input ? tx.input.length : 0
const inputData = tx.input || '0x'
const value = tx.value || 0n
const transactionIndex = receipt.transactionIndex
const logIndex = log.logIndex || 0
const contractAddress = tx.to || ''
mints.push({
txHash: log.transactionHash,
blockNumber: log.blockNumber,
blockTimestamp: block.timestamp,
from: tx.from,
gasPrice,
maxFeePerGas,
maxPriorityFeePerGas,
gasUsed: receipt.gasUsed,
gasLimit: tx.gas,
nonce: tx.nonce,
positionInBlock: positionInBlock >= 0 ? positionInBlock : 999,
effectiveGasPrice,
timeSinceBlockStart,
// Timing data
txTimestamp,
blockTimeMs,
previousBlockTime,
timeBetweenBlocks,
// Additional data
calldataLength,
inputData,
value,
transactionIndex,
logIndex,
contractAddress,
})
console.log(` ✅ Successfully analyzed mint #${i + 1}`)
// Add delay to avoid rate limiting
await new Promise(resolve => setTimeout(resolve, 500))
} catch (error: any) {
if (error.message?.includes('429') || error.message?.includes('Too Many Requests')) {
console.error(`\n ⚠️ Rate limited. Waiting 5 seconds and trying next RPC...`)
await new Promise(resolve => setTimeout(resolve, 5000))
// Try switching to a different RPC
const nextRPC = publicRPCs.find(rpc => rpc !== rpcUsed) || publicRPCs[0]
if (nextRPC !== rpcUsed) {
console.log(` Switching to RPC: ${nextRPC}`)
client = createPublicClient({
chain: beraChain,
transport: http(nextRPC, { timeout: 10000 }),
})
rpcUsed = nextRPC
}
// Retry this transaction
i--
continue
}
console.error(`\n ⚠️ Error: ${error.message}`)
partialMints.push({ txHash: log.transactionHash, blockNumber: log.blockNumber, error: error.message })
continue
}
}
console.log(`\n✅ Completed analysis of ${mints.length} mints (${partialMints.length} partial/failed)\n`)
if (partialMints.length > 0) {
console.log(`⚠️ Could not fully analyze ${partialMints.length} mints due to errors:`)
for (const partial of partialMints) {
console.log(` ${partial.txHash} (Block ${partial.blockNumber})${partial.error ? ` - ${partial.error}` : ''}`)
}
console.log(``)
}
// Use MiTeddy transactions from logs if available, otherwise query via RPC
let miTeddyTxs: Array<{ hash: string; blockNumber: bigint }> = Array.from(miTeddyTxHashes).map(hash => {
const log = allMiTeddyLogs.find(l => l.transactionHash.toLowerCase() === hash)
return { hash, blockNumber: log?.blockNumber || 0n }
})
if (miTeddyTxs.length === 0) {
console.log('\n🔎 Querying transactions to MiTeddy contract via RPC...')
currentBlock = await client.getBlockNumber()
fromBlock = currentBlock - BigInt(maxBlocksToSearch)
const miTeddyBatchSize = 10000n
for (let blockStart = fromBlock; blockStart <= currentBlock; blockStart += miTeddyBatchSize) {
const blockEnd = blockStart + miTeddyBatchSize - 1n > currentBlock ? currentBlock : blockStart + miTeddyBatchSize - 1n
try {
const logs = await client.getLogs({
address: MITEDDY_ADDR,
fromBlock: blockStart,
toBlock: blockEnd,
})
const txHashes = new Set(logs.map(l => l.transactionHash.toLowerCase()))
for (const hash of txHashes) {
const log = logs.find(l => l.transactionHash.toLowerCase() === hash)
if (log) {
miTeddyTxs.push({ hash, blockNumber: log.blockNumber })
}
}
process.stdout.write(`\r Found ${miTeddyTxs.length} transactions to MiTeddy contract...`)
await new Promise(resolve => setTimeout(resolve, 200))
} catch (error: any) {
// Continue
}
}
console.log(`\n`)
}
console.log(`✅ Found ${miTeddyTxs.length} total transactions to MiTeddy contract\n`)
// Analyze failed transactions (those to MiTeddy contract but no mint event)
if (miTeddyTxs.length > 0 && mints.length > 0) {
const successfulHashes = new Set(mints.map(m => m.txHash.toLowerCase()))
const additionalFailed = miTeddyTxs.filter((tx: { hash: string; blockNumber: bigint }) => !successfulHashes.has(tx.hash.toLowerCase())).slice(0, 100)
if (additionalFailed.length > 0) {
console.log(`📋 Analyzing ${additionalFailed.length} failed transactions...`)
for (const txInfo of additionalFailed) {
try {
const receipt = await client.getTransactionReceipt({ hash: txInfo.hash as `0x${string}` })
if (receipt.status === 'reverted') {
failedTxs.push({ hash: txInfo.hash, blockNumber: txInfo.blockNumber, error: 'reverted' })
}
} catch (e) {
// Skip
}
}
if (failedTxs.length > 0) {
console.log(` Found ${failedTxs.length} failed/reverted transactions\n`)
}
}
}
console.log(`\n\n✅ Analyzed ${mints.length} successful mint transactions`)
if (failedTxs.length > 0) {
console.log(` Also found ${failedTxs.length} failed transactions\n`)
} else {
console.log('\n')
}
if (mints.length === 0) {
console.log('⚠️ No successful mint transactions found in analyzed data')
if (miTeddyTxs.length > 0) {
console.log(`\n📊 Found ${miTeddyTxs.length} transactions to MiTeddy contract`)
console.log(' These might be failed attempts or pending transactions')
console.log(' This suggests high competition - many bots trying to mint')
}
return
}
// Analyze patterns
console.log('📊 ANALYSIS RESULTS\n')
console.log('═'.repeat(80))
// 1. Gas Price Analysis
console.log('\n💰 GAS PRICE ANALYSIS\n')
const gasPrices = mints.map(m => Number(m.effectiveGasPrice) / 1e9)
const priorityFees = mints
.map(m => m.maxPriorityFeePerGas ? Number(m.maxPriorityFeePerGas) / 1e9 : null)
.filter((f): f is number => f !== null)
const maxFees = mints
.map(m => m.maxFeePerGas ? Number(m.maxFeePerGas) / 1e9 : null)
.filter((f): f is number => f !== null)
if (gasPrices.length > 0) {
gasPrices.sort((a, b) => a - b)
console.log(`Effective Gas Price (Gwei):`)
console.log(` Min: ${gasPrices[0].toFixed(2)}`)
console.log(` P25: ${gasPrices[Math.floor(gasPrices.length * 0.25)].toFixed(2)}`)
console.log(` Median: ${gasPrices[Math.floor(gasPrices.length * 0.5)].toFixed(2)}`)
console.log(` P75: ${gasPrices[Math.floor(gasPrices.length * 0.75)].toFixed(2)}`)
console.log(` Max: ${gasPrices[gasPrices.length - 1].toFixed(2)}`)
console.log(` Mean: ${(gasPrices.reduce((a, b) => a + b, 0) / gasPrices.length).toFixed(2)}`)
}
if (priorityFees.length > 0) {
priorityFees.sort((a, b) => a - b)
console.log(`\nPriority Fee (Gwei):`)
console.log(` Min: ${priorityFees[0].toFixed(2)}`)
console.log(` P25: ${priorityFees[Math.floor(priorityFees.length * 0.25)].toFixed(2)}`)
console.log(` Median: ${priorityFees[Math.floor(priorityFees.length * 0.5)].toFixed(2)}`)
console.log(` P75: ${priorityFees[Math.floor(priorityFees.length * 0.75)].toFixed(2)}`)
console.log(` Max: ${priorityFees[priorityFees.length - 1].toFixed(2)}`)
console.log(` Mean: ${(priorityFees.reduce((a, b) => a + b, 0) / priorityFees.length).toFixed(2)}`)
}
if (maxFees.length > 0) {
maxFees.sort((a, b) => a - b)
console.log(`\nMax Fee Per Gas (Gwei):`)
console.log(` Min: ${maxFees[0].toFixed(2)}`)
console.log(` P25: ${maxFees[Math.floor(maxFees.length * 0.25)].toFixed(2)}`)
console.log(` Median: ${maxFees[Math.floor(maxFees.length * 0.5)].toFixed(2)}`)
console.log(` P75: ${maxFees[Math.floor(maxFees.length * 0.75)].toFixed(2)}`)
console.log(` Max: ${maxFees[maxFees.length - 1].toFixed(2)}`)
console.log(` Mean: ${(maxFees.reduce((a, b) => a + b, 0) / maxFees.length).toFixed(2)}`)
}
// 2. Block Position Analysis
console.log('\n\n📍 BLOCK POSITION ANALYSIS\n')
const positions = mints.map(m => m.positionInBlock).filter(p => p < 999)
if (positions.length > 0) {
positions.sort((a, b) => a - b)
console.log(`Position in Block:`)
console.log(` Min: ${positions[0]}`)
console.log(` P25: ${positions[Math.floor(positions.length * 0.25)]}`)
console.log(` Median: ${positions[Math.floor(positions.length * 0.5)]}`)
console.log(` P75: ${positions[Math.floor(positions.length * 0.75)]}`)
console.log(` Max: ${positions[positions.length - 1]}`)
console.log(` Mean: ${(positions.reduce((a, b) => a + b, 0) / positions.length).toFixed(1)}`)
// Count how many were in first 10 positions
const first10 = positions.filter(p => p < 10).length
console.log(`\n First 10 positions: ${first10}/${positions.length} (${((first10 / positions.length) * 100).toFixed(1)}%)`)
}
// 3. Gas Usage Analysis
console.log('\n\n⛽ GAS USAGE ANALYSIS\n')
const gasUsed = mints.map(m => Number(m.gasUsed))
const gasLimits = mints.map(m => Number(m.gasLimit))
const utilization = mints.map(m => (Number(m.gasUsed) / Number(m.gasLimit)) * 100)
gasUsed.sort((a, b) => a - b)
console.log(`Gas Used:`)
console.log(` Min: ${gasUsed[0].toLocaleString()}`)
console.log(` Median: ${gasUsed[Math.floor(gasUsed.length * 0.5)].toLocaleString()}`)
console.log(` Max: ${gasUsed[gasUsed.length - 1].toLocaleString()}`)
console.log(` Mean: ${Math.round(gasUsed.reduce((a, b) => a + b, 0) / gasUsed.length).toLocaleString()}`)
utilization.sort((a, b) => a - b)
console.log(`\nGas Utilization (% of limit):`)
console.log(` Min: ${utilization[0].toFixed(1)}%`)
console.log(` Median: ${utilization[Math.floor(utilization.length * 0.5)].toFixed(1)}%`)
console.log(` Max: ${utilization[utilization.length - 1].toFixed(1)}%`)
console.log(` Mean: ${(utilization.reduce((a, b) => a + b, 0) / utilization.length).toFixed(1)}%`)
// 4. Top Performers (highest gas, earliest in block)
console.log('\n\n🏆 TOP PERFORMERS\n')
const sortedByGas = [...mints].sort((a, b) =>
Number(b.effectiveGasPrice) - Number(a.effectiveGasPrice)
)
console.log('Highest Gas Prices:')
for (let i = 0; i < Math.min(5, sortedByGas.length); i++) {
const m = sortedByGas[i]
const gasGwei = Number(m.effectiveGasPrice) / 1e9
const priorityGwei = m.maxPriorityFeePerGas ? Number(m.maxPriorityFeePerGas) / 1e9 : 'N/A'
console.log(` ${i + 1}. ${m.txHash.slice(0, 10)}... | Gas: ${gasGwei.toFixed(2)} Gwei | Priority: ${priorityGwei} | Block: ${m.blockNumber} | Pos: ${m.positionInBlock}`)
}
const sortedByPosition = [...mints].sort((a, b) => a.positionInBlock - b.positionInBlock)
console.log('\nEarliest in Block:')
for (let i = 0; i < Math.min(5, sortedByPosition.length); i++) {
const m = sortedByPosition[i]
const gasGwei = Number(m.effectiveGasPrice) / 1e9
console.log(` ${i + 1}. ${m.txHash.slice(0, 10)}... | Pos: ${m.positionInBlock} | Gas: ${gasGwei.toFixed(2)} Gwei | Block: ${m.blockNumber}`)
}
// 5. Timing Analysis
console.log('\n\n⏰ TIMING ANALYSIS\n')
// Analyze if mints happen in bursts (multiple in same block)
const mintsByBlock = new Map<bigint, number>()
mints.forEach(m => {
const count = mintsByBlock.get(m.blockNumber) || 0
mintsByBlock.set(m.blockNumber, count + 1)
})
const blocksWithMultipleMints = Array.from(mintsByBlock.values()).filter(count => count > 1)
const timeBetweenBlocksData = mints
.map(m => m.timeBetweenBlocks)
.filter((t): t is number => t !== null)
if (timeBetweenBlocksData.length > 0) {
timeBetweenBlocksData.sort((a, b) => a - b)
console.log(`Block Time (ms between blocks):`)
console.log(` Min: ${timeBetweenBlocksData[0].toFixed(0)}ms`)
console.log(` P25: ${timeBetweenBlocksData[Math.floor(timeBetweenBlocksData.length * 0.25)].toFixed(0)}ms`)
console.log(` Median: ${timeBetweenBlocksData[Math.floor(timeBetweenBlocksData.length * 0.5)].toFixed(0)}ms`)
console.log(` P75: ${timeBetweenBlocksData[Math.floor(timeBetweenBlocksData.length * 0.75)].toFixed(0)}ms`)
console.log(` Max: ${timeBetweenBlocksData[timeBetweenBlocksData.length - 1].toFixed(0)}ms`)
console.log(` Mean: ${(timeBetweenBlocksData.reduce((a, b) => a + b, 0) / timeBetweenBlocksData.length).toFixed(0)}ms`)
}
// Analyze transaction timing patterns - time between mint windows
const blockTimestamps = mints.map(m => m.blockTimeMs)
if (blockTimestamps.length > 1) {
blockTimestamps.sort((a, b) => a - b)
const timeDiffs: number[] = []
for (let i = 1; i < blockTimestamps.length; i++) {
timeDiffs.push(blockTimestamps[i] - blockTimestamps[i - 1])
}
if (timeDiffs.length > 0) {
timeDiffs.sort((a, b) => a - b)
const meanMs = timeDiffs.reduce((a, b) => a + b, 0) / timeDiffs.length
const meanHours = meanMs / (1000 * 60 * 60)
const meanMinutes = (meanMs / (1000 * 60)) % 60
console.log(`\nTime Between Mint Windows:`)
console.log(` Min: ${(timeDiffs[0] / (1000 * 60 * 60)).toFixed(2)} hours (${(timeDiffs[0] / 1000).toFixed(0)} seconds)`)
console.log(` Median: ${(timeDiffs[Math.floor(timeDiffs.length * 0.5)] / (1000 * 60 * 60)).toFixed(2)} hours (${(timeDiffs[Math.floor(timeDiffs.length * 0.5)] / 1000).toFixed(0)} seconds)`)
console.log(` Max: ${(timeDiffs[timeDiffs.length - 1] / (1000 * 60 * 60)).toFixed(2)} hours (${(timeDiffs[timeDiffs.length - 1] / 1000).toFixed(0)} seconds)`)
console.log(` Mean: ${meanHours.toFixed(2)} hours (${(meanMs / 1000).toFixed(0)} seconds)`)
console.log(`\n 💡 Mint window appears to open approximately every ${meanHours.toFixed(1)} hours`)
}
}
if (blocksWithMultipleMints.length > 0) {
console.log(`\nMint Distribution:`)
console.log(` Blocks with 1 mint: ${Array.from(mintsByBlock.values()).filter(c => c === 1).length}`)
console.log(` Blocks with 2+ mints: ${blocksWithMultipleMints.length}`)
console.log(` Max mints in one block: ${Math.max(...Array.from(mintsByBlock.values()))}`)
}
// 6. Calldata Analysis
console.log('\n\n📦 CALLDATA ANALYSIS\n')
const calldataLengths = mints.map(m => m.calldataLength)
if (calldataLengths.length > 0) {
calldataLengths.sort((a, b) => a - b)
console.log(`Calldata Length (bytes):`)
console.log(` Min: ${calldataLengths[0]}`)
console.log(` Median: ${calldataLengths[Math.floor(calldataLengths.length * 0.5)]}`)
console.log(` Max: ${calldataLengths[calldataLengths.length - 1]}`)
console.log(` Mean: ${Math.round(calldataLengths.reduce((a: number, b: number) => a + b, 0) / calldataLengths.length)}`)
}
// Check if all use same function signature
const functionSignatures = new Set<string>()
mints.forEach(m => {
if (m.inputData && m.inputData.length >= 10) {
const sig = m.inputData.slice(0, 10) // First 4 bytes = function selector
functionSignatures.add(sig)
}
})
console.log(`\nFunction Signatures Used: ${functionSignatures.size}`)
if (functionSignatures.size > 1) {
console.log(` ⚠️ Multiple function signatures detected - check contract`)
}
// 7. Nonce Analysis
console.log('\n\n🔢 NONCE ANALYSIS\n')
const nonces = mints.map(m => m.nonce)
nonces.sort((a, b) => a - b)
console.log(`Nonce Range:`)
console.log(` Min: ${nonces[0]}`)
console.log(` Max: ${nonces[nonces.length - 1]}`)
console.log(` Median: ${nonces[Math.floor(nonces.length * 0.5)]}`)
// Check for nonce patterns (replace vs increment)
const nonceDiffs: number[] = []
for (let i = 1; i < nonces.length; i++) {
nonceDiffs.push(nonces[i] - nonces[i - 1])
}
if (nonceDiffs.length > 0) {
const avgDiff = nonceDiffs.reduce((a, b) => a + b, 0) / nonceDiffs.length
console.log(`\nNonce Strategy:`)
console.log(` Average difference: ${avgDiff.toFixed(2)}`)
if (avgDiff < 1.5) {
console.log(` Pattern: Replace strategy (same nonce, different gas)`)
} else {
console.log(` Pattern: Increment strategy (new nonce each time)`)
}
}
// 8. Recommendations
console.log('\n\n💡 RECOMMENDATIONS\n')
console.log('═'.repeat(80))
if (priorityFees.length > 0) {
const p75Priority = priorityFees[Math.floor(priorityFees.length * 0.75)]
const p90Priority = priorityFees[Math.floor(priorityFees.length * 0.9)]
const maxPriority = priorityFees[priorityFees.length - 1]
console.log(`\nPriority Fee Strategy:`)
console.log(` Competitive (P75): ${p75Priority.toFixed(2)} Gwei`)
console.log(` Aggressive (P90): ${p90Priority.toFixed(2)} Gwei`)
console.log(` Maximum: ${maxPriority.toFixed(2)} Gwei`)
// Load config only for comparison
try {
const cfg = loadConfig()
console.log(`\n Your current: ${cfg.priorityFeeGwei} Gwei`)
if (cfg.priorityFeeGwei < p75Priority) {
console.log(` ⚠️ Your priority fee is below P75 - consider increasing to ${Math.ceil(p75Priority)} Gwei`)
} else if (cfg.priorityFeeGwei < p90Priority) {
console.log(` ⚠️ Your priority fee is below P90 - consider increasing to ${Math.ceil(p90Priority)} Gwei`)
} else {
console.log(` ✅ Your priority fee is competitive`)
}
} catch (e) {
// Config not available, skip comparison
}
}
if (maxFees.length > 0) {
const p75MaxFee = maxFees[Math.floor(maxFees.length * 0.75)]
const p90MaxFee = maxFees[Math.floor(maxFees.length * 0.9)]
console.log(`\nMax Fee Strategy:`)
console.log(` Competitive (P75): ${p75MaxFee.toFixed(2)} Gwei`)
console.log(` Aggressive (P90): ${p90MaxFee.toFixed(2)} Gwei`)
// Load config only for comparison
try {
const cfg = loadConfig()
console.log(`\n Your current max: ${cfg.maxFeePerGasGwei} Gwei`)
if (cfg.maxFeePerGasGwei < p75MaxFee) {
console.log(` ⚠️ Your max fee is below P75 - consider increasing to ${Math.ceil(p75MaxFee)} Gwei`)
} else {
console.log(` ✅ Your max fee is competitive`)
}
} catch (e) {
// Config not available, skip comparison
}
}
// 6. Export detailed data
console.log('\n\n📄 Exporting detailed data to successful-mints-analysis.json...')
const analysis = {
summary: {
totalMints: mints.length,
blocksAnalyzed: maxBlocksToSearch,
blockRange: { from: fromBlock.toString(), to: currentBlock.toString() },
},
gasStats: {
effectiveGasPrice: {
min: gasPrices[0],
p25: gasPrices[Math.floor(gasPrices.length * 0.25)],
median: gasPrices[Math.floor(gasPrices.length * 0.5)],
p75: gasPrices[Math.floor(gasPrices.length * 0.75)],
max: gasPrices[gasPrices.length - 1],
mean: gasPrices.reduce((a, b) => a + b, 0) / gasPrices.length,
},
priorityFee: priorityFees.length > 0 ? {
min: priorityFees[0],
p25: priorityFees[Math.floor(priorityFees.length * 0.25)],
median: priorityFees[Math.floor(priorityFees.length * 0.5)],
p75: priorityFees[Math.floor(priorityFees.length * 0.75)],
max: priorityFees[priorityFees.length - 1],
mean: priorityFees.reduce((a, b) => a + b, 0) / priorityFees.length,
} : null,
maxFee: maxFees.length > 0 ? {
min: maxFees[0],
p25: maxFees[Math.floor(maxFees.length * 0.25)],
median: maxFees[Math.floor(maxFees.length * 0.5)],
p75: maxFees[Math.floor(maxFees.length * 0.75)],
max: maxFees[maxFees.length - 1],
mean: maxFees.reduce((a, b) => a + b, 0) / maxFees.length,
} : null,
},
blockPosition: positions.length > 0 ? {
min: positions[0],
p25: positions[Math.floor(positions.length * 0.25)],
median: positions[Math.floor(positions.length * 0.5)],
p75: positions[Math.floor(positions.length * 0.75)],
max: positions[positions.length - 1],
mean: positions.reduce((a, b) => a + b, 0) / positions.length,
first10Count: positions.filter(p => p < 10).length,
first10Percent: (positions.filter(p => p < 10).length / positions.length) * 100,
} : null,
timing: {
blockTime: timeBetweenBlocksData.length > 0 ? {
min: timeBetweenBlocksData[0],
p25: timeBetweenBlocksData[Math.floor(timeBetweenBlocksData.length * 0.25)],
median: timeBetweenBlocksData[Math.floor(timeBetweenBlocksData.length * 0.5)],
p75: timeBetweenBlocksData[Math.floor(timeBetweenBlocksData.length * 0.75)],
max: timeBetweenBlocksData[timeBetweenBlocksData.length - 1],
mean: timeBetweenBlocksData.reduce((a: number, b: number) => a + b, 0) / timeBetweenBlocksData.length,
} : null,
mintsByBlock: Object.fromEntries(mintsByBlock),
blocksWithMultipleMints: blocksWithMultipleMints.length,
},
competition: miTeddyTxs.length > 0 ? {
totalAttempts: miTeddyTxs.length,
successfulMints: mints.length,
successRate: mints.length > 0 ? (mints.length / miTeddyTxs.length) * 100 : 0,
failedTransactions: failedTxs.length,
} : null,
mints: mints.map(m => ({
txHash: m.txHash,
blockNumber: m.blockNumber.toString(),
blockTimestamp: m.blockTimestamp.toString(),
blockTimeMs: m.blockTimeMs,
from: m.from,
effectiveGasPriceGwei: Number(m.effectiveGasPrice) / 1e9,
priorityFeeGwei: m.maxPriorityFeePerGas ? Number(m.maxPriorityFeePerGas) / 1e9 : null,
maxFeeGwei: m.maxFeePerGas ? Number(m.maxFeePerGas) / 1e9 : null,
gasUsed: m.gasUsed.toString(),
gasLimit: m.gasLimit.toString(),
positionInBlock: m.positionInBlock,
nonce: m.nonce,
calldataLength: m.calldataLength,
functionSelector: m.inputData.slice(0, 10),
timeBetweenBlocks: m.timeBetweenBlocks,
transactionIndex: m.transactionIndex,
logIndex: m.logIndex,
})),
}
const fs = await import('fs')
fs.writeFileSync(
'successful-mints-analysis.json',
JSON.stringify(analysis, null, 2)
)
console.log('✅ Analysis complete!')
console.log('\n💡 Next steps:')
console.log(' 1. Review the recommendations above')
console.log(' 2. Check successful-mints-analysis.json for detailed data')
console.log(' 3. Adjust your gas settings if needed')
}
// Run analysis
const blocksToAnalyze = process.argv[2] ? parseInt(process.argv[2]) : 100
analyzeSuccessfulMints(blocksToAnalyze).catch(error => {
console.error('❌ Analysis failed:', error)
process.exit(1)
})