initial commit
This commit is contained in:
commit
2b140e16df
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
|
||||
# Temporary folders
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
./generated
|
||||
27
Dockerfile
Normal file
27
Dockerfile
Normal file
@ -0,0 +1,27 @@
|
||||
# Build stage
|
||||
FROM node:22.17.0-alpine3.22 AS builder
|
||||
|
||||
RUN apk --update --no-cache add git python3 alpine-sdk jq curl bash
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN echo "Building mock-lockdrop-watcher" && \
|
||||
yarn && yarn build
|
||||
|
||||
# Production stage
|
||||
FROM node:22.17.0-alpine3.22
|
||||
|
||||
RUN apk --update --no-cache add git python3 alpine-sdk jq curl bash
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
# Install only production dependencies
|
||||
RUN yarn install --production --frozen-lockfile
|
||||
|
||||
# Copy built application from builder stage
|
||||
COPY --from=builder /app/dist ./dist
|
||||
29
package.json
Normal file
29
package.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "mock-lockdrop-watcher",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Mock GraphQL server for lockdrop watcher testing",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"postbuild": "cp src/schema.graphql dist/schema.graphql",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "ts-node src/index.ts",
|
||||
"generate-participants": "node dist/participant-generator.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"apollo-server-express": "^3.12.1",
|
||||
"express": "^4.19.2",
|
||||
"graphql": "^16.9.0",
|
||||
"@cosmjs/crypto": "^0.33.1",
|
||||
"@cosmjs/encoding": "^0.33.1",
|
||||
"ethers": "^6.13.4",
|
||||
"urbit-ob": "^5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.14.10",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.5.3"
|
||||
}
|
||||
}
|
||||
148
src/data-generator.ts
Normal file
148
src/data-generator.ts
Normal file
@ -0,0 +1,148 @@
|
||||
import * as fs from 'fs';
|
||||
|
||||
interface VerifiedParticipant {
|
||||
attestation: {
|
||||
payload: {
|
||||
address: string;
|
||||
msg: string;
|
||||
payload: {
|
||||
address: string;
|
||||
msg: string;
|
||||
owned_points: {
|
||||
galaxy: string;
|
||||
stars: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
signatures: string[];
|
||||
};
|
||||
role: string;
|
||||
}
|
||||
|
||||
interface BlockData {
|
||||
hash: string;
|
||||
number: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface PointLockedEvent {
|
||||
__typename: "PointLockedEvent";
|
||||
azimuth_id: string;
|
||||
point: string;
|
||||
lock_period: number;
|
||||
}
|
||||
|
||||
interface LockdropClosedEvent {
|
||||
__typename: "LockdropClosedEvent";
|
||||
ok: boolean;
|
||||
}
|
||||
|
||||
interface EventInRange {
|
||||
block: BlockData;
|
||||
event: PointLockedEvent | LockdropClosedEvent;
|
||||
}
|
||||
|
||||
interface GeneratedData {
|
||||
data: {
|
||||
eventsInRange: EventInRange[];
|
||||
};
|
||||
}
|
||||
|
||||
function generateMockBlock(): BlockData {
|
||||
const sixMonthsAgo = Math.floor(Date.now() / 1000) - (6 * 30 * 24 * 60 * 60);
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const randomTimestamp = Math.floor(Math.random() * (now - sixMonthsAgo)) + sixMonthsAgo;
|
||||
const randomBlockNumber = Math.floor(Math.random() * 1000000) + 22000000;
|
||||
|
||||
return {
|
||||
hash: `0x${Math.random().toString(16).substr(2, 64)}`,
|
||||
number: randomBlockNumber,
|
||||
timestamp: randomTimestamp,
|
||||
};
|
||||
}
|
||||
|
||||
function generateRandomLockPeriod(): number {
|
||||
return Math.floor(Math.random() * 5) + 1; // 1 to 5 years
|
||||
}
|
||||
|
||||
export function generateDataFromParticipants(verifiedParticipantsPath: string): GeneratedData {
|
||||
const participantsData = JSON.parse(fs.readFileSync(verifiedParticipantsPath, 'utf8')) as VerifiedParticipant[];
|
||||
|
||||
const events: EventInRange[] = [];
|
||||
let latestBlock: BlockData | null = null;
|
||||
|
||||
let hasGalaxyWith5Years = false;
|
||||
let hasStarWith5Years = false;
|
||||
|
||||
// Generate PointLockedEvent for each point in each participant
|
||||
for (const participant of participantsData) {
|
||||
const azimuthId = participant.attestation.payload.address;
|
||||
const ownedPoints = participant.attestation.payload.payload.owned_points;
|
||||
|
||||
// Collect all points (galaxy + stars)
|
||||
const allPoints: { point: string; isGalaxy: boolean }[] = [];
|
||||
if (ownedPoints.galaxy && ownedPoints.galaxy !== '-') {
|
||||
allPoints.push({ point: ownedPoints.galaxy, isGalaxy: true });
|
||||
}
|
||||
ownedPoints.stars.forEach(star => {
|
||||
allPoints.push({ point: star, isGalaxy: false });
|
||||
});
|
||||
|
||||
// Generate random block for this participant (all points from same participant use same block)
|
||||
const block = generateMockBlock();
|
||||
|
||||
// Track the latest block
|
||||
if (!latestBlock || block.timestamp > latestBlock.timestamp) {
|
||||
latestBlock = block;
|
||||
}
|
||||
|
||||
// Generate events for each point
|
||||
for (const { point, isGalaxy } of allPoints) {
|
||||
let lockPeriod = generateRandomLockPeriod();
|
||||
|
||||
// Ensure we have at least one galaxy and one star with 5-year lock period
|
||||
if (isGalaxy && !hasGalaxyWith5Years) {
|
||||
lockPeriod = 5;
|
||||
hasGalaxyWith5Years = true;
|
||||
}
|
||||
|
||||
if (!isGalaxy && !hasStarWith5Years) {
|
||||
lockPeriod = 5;
|
||||
hasStarWith5Years = true;
|
||||
}
|
||||
|
||||
const event: EventInRange = {
|
||||
block,
|
||||
event: {
|
||||
__typename: "PointLockedEvent",
|
||||
azimuth_id: azimuthId,
|
||||
point,
|
||||
lock_period: lockPeriod,
|
||||
},
|
||||
};
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a LockdropClosedEvent using the latest block from PointLockedEvents
|
||||
if (latestBlock) {
|
||||
events.push({
|
||||
block: latestBlock,
|
||||
event: {
|
||||
__typename: "LockdropClosedEvent",
|
||||
ok: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
eventsInRange: events,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function saveGeneratedData(data: GeneratedData, outputPath: string): void {
|
||||
fs.writeFileSync(outputPath, JSON.stringify(data, null, 2));
|
||||
}
|
||||
29
src/index.ts
Normal file
29
src/index.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import express from 'express';
|
||||
import { ApolloServer } from 'apollo-server-express';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { resolvers } from './resolvers';
|
||||
|
||||
async function startServer() {
|
||||
const typeDefs = readFileSync(join(__dirname, 'schema.graphql'), 'utf8');
|
||||
|
||||
const server = new ApolloServer({
|
||||
typeDefs,
|
||||
resolvers,
|
||||
});
|
||||
|
||||
await server.start();
|
||||
|
||||
const app = express();
|
||||
server.applyMiddleware({ app: app as any });
|
||||
|
||||
const PORT = process.env.PORT || 6000;
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`);
|
||||
});
|
||||
}
|
||||
|
||||
startServer().catch(error => {
|
||||
console.error('Error starting server:', error);
|
||||
});
|
||||
413
src/participant-generator.ts
Normal file
413
src/participant-generator.ts
Normal file
@ -0,0 +1,413 @@
|
||||
import { Wallet } from 'ethers';
|
||||
import { patp } from 'urbit-ob';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { Secp256k1, sha256, Random, ripemd160 } from '@cosmjs/crypto';
|
||||
import { toBech32 } from '@cosmjs/encoding';
|
||||
|
||||
const MAX_GALAXIES = 256;
|
||||
const MAX_STARS = 65280;
|
||||
|
||||
// Generate real Cosmos address with zenith prefix using standard derivation
|
||||
async function generateCosmosAddress(): Promise<{ address: string; privateKey: string }> {
|
||||
const privkeyBytes = Random.getBytes(32);
|
||||
const keypair = await Secp256k1.makeKeypair(privkeyBytes);
|
||||
const pubkey = Secp256k1.compressPubkey(keypair.pubkey);
|
||||
|
||||
// Standard Cosmos address derivation: RIPEMD160(SHA256(compressed_pubkey))
|
||||
const sha256Hash = sha256(pubkey);
|
||||
const addressBytes = ripemd160(sha256Hash);
|
||||
const address = toBech32('zenith', addressBytes);
|
||||
|
||||
return {
|
||||
address,
|
||||
privateKey: Buffer.from(keypair.privkey).toString('hex')
|
||||
};
|
||||
}
|
||||
|
||||
// Generate real Ethereum address
|
||||
function generateEthereumAddress(): { address: string; privateKey: string } {
|
||||
const wallet = Wallet.createRandom();
|
||||
return {
|
||||
address: wallet.address,
|
||||
privateKey: wallet.privateKey
|
||||
};
|
||||
}
|
||||
|
||||
// Generate Urbit point names using urbit-ob
|
||||
function getUrbitPointName(id: number): string {
|
||||
return patp(id.toString());
|
||||
}
|
||||
|
||||
interface GeneratorConfig {
|
||||
totalParticipants: number;
|
||||
galaxyCount: number;
|
||||
starCount: number;
|
||||
outputDir: string;
|
||||
}
|
||||
|
||||
interface GeneratedAccount {
|
||||
zenithAddress: string;
|
||||
zenithPrivateKey: string;
|
||||
ethereumAddress: string;
|
||||
ethereumPrivateKey: string;
|
||||
}
|
||||
|
||||
interface VerifiedParticipant {
|
||||
attestation: {
|
||||
payload: {
|
||||
address: string;
|
||||
msg: string;
|
||||
payload: {
|
||||
address: string;
|
||||
msg: string;
|
||||
owned_points: {
|
||||
galaxy: string;
|
||||
stars: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
signatures: string[];
|
||||
};
|
||||
role: string;
|
||||
}
|
||||
|
||||
interface GenerationStats {
|
||||
totalParticipants: number;
|
||||
validators: number;
|
||||
delegators: number;
|
||||
galaxiesAllocated: number;
|
||||
starsAllocated: number;
|
||||
config: GeneratorConfig;
|
||||
}
|
||||
|
||||
export class ParticipantGenerator {
|
||||
private config: GeneratorConfig;
|
||||
private accounts: GeneratedAccount[] = [];
|
||||
private usedGalaxies: Set<number> = new Set();
|
||||
private usedStars: Set<number> = new Set();
|
||||
|
||||
constructor(config: GeneratorConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
private shuffleArray<T>(array: T[]): T[] {
|
||||
const shuffled = [...array];
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||
}
|
||||
return shuffled;
|
||||
}
|
||||
|
||||
private getAvailableGalaxies(): number[] {
|
||||
const galaxies = Array.from({ length: MAX_GALAXIES }, (_, i) => i);
|
||||
return this.shuffleArray(galaxies).slice(0, this.config.galaxyCount);
|
||||
}
|
||||
|
||||
private getAvailableStars(): number[] {
|
||||
// Stars start from MAX_GALAXIES (after galaxies)
|
||||
const stars = Array.from({ length: MAX_STARS }, (_, i) => i + MAX_GALAXIES);
|
||||
return this.shuffleArray(stars).slice(0, this.config.starCount);
|
||||
}
|
||||
|
||||
// Distribute all available galaxies among first participants (they become validators)
|
||||
private distributeGalaxies(availableGalaxies: number[]): number[] {
|
||||
const galaxyAllocation: number[] = new Array(this.config.totalParticipants).fill(null);
|
||||
|
||||
// First galaxyCount participants get galaxies (and become validators)
|
||||
for (let i = 0; i < availableGalaxies.length && i < this.config.totalParticipants; i++) {
|
||||
galaxyAllocation[i] = availableGalaxies[i];
|
||||
}
|
||||
|
||||
return galaxyAllocation;
|
||||
}
|
||||
|
||||
// Distribute all available stars randomly among all participants (each gets at least 1)
|
||||
private distributeStars(availableStars: number[]): number[][] {
|
||||
const starAllocation: number[][] = Array.from({ length: this.config.totalParticipants }, () => []);
|
||||
const shuffledStars = this.shuffleArray([...availableStars]);
|
||||
|
||||
// First, give each participant 1 star (minimum requirement)
|
||||
for (let i = 0; i < this.config.totalParticipants; i++) {
|
||||
starAllocation[i].push(shuffledStars[i]);
|
||||
}
|
||||
|
||||
// Randomly distribute remaining stars
|
||||
for (let i = this.config.totalParticipants; i < shuffledStars.length; i++) {
|
||||
const randomParticipant = Math.floor(Math.random() * this.config.totalParticipants);
|
||||
starAllocation[randomParticipant].push(shuffledStars[i]);
|
||||
}
|
||||
|
||||
return starAllocation;
|
||||
}
|
||||
|
||||
async generate(): Promise<{ participants: VerifiedParticipant[]; accounts: GeneratedAccount[]; stats: GenerationStats }> {
|
||||
const participants: VerifiedParticipant[] = [];
|
||||
this.accounts = [];
|
||||
this.usedGalaxies.clear();
|
||||
this.usedStars.clear();
|
||||
|
||||
const availableGalaxies = this.getAvailableGalaxies();
|
||||
const availableStars = this.getAvailableStars();
|
||||
|
||||
console.log(`Generating ${this.config.totalParticipants} participants...`);
|
||||
console.log(`- Validators (with galaxies): ${this.config.galaxyCount}`);
|
||||
console.log(`- Delegators (stars only): ${this.config.totalParticipants - this.config.galaxyCount}`);
|
||||
console.log(`- Galaxies to allocate: ${availableGalaxies.length}`);
|
||||
console.log(`- Stars to allocate: ${availableStars.length}`);
|
||||
|
||||
// Pre-allocate all points to ensure they're all used
|
||||
const galaxyAllocation = this.distributeGalaxies(availableGalaxies);
|
||||
const starAllocation = this.distributeStars(availableStars);
|
||||
|
||||
for (let i = 0; i < this.config.totalParticipants; i++) {
|
||||
const cosmosAccount = await generateCosmosAddress();
|
||||
const ethereumAccount = generateEthereumAddress();
|
||||
|
||||
this.accounts.push({
|
||||
zenithAddress: cosmosAccount.address,
|
||||
zenithPrivateKey: cosmosAccount.privateKey,
|
||||
ethereumAddress: ethereumAccount.address,
|
||||
ethereumPrivateKey: ethereumAccount.privateKey
|
||||
});
|
||||
|
||||
const isValidator = i < this.config.galaxyCount;
|
||||
|
||||
// Get pre-allocated galaxy for validators
|
||||
let galaxy = '-';
|
||||
if (isValidator && galaxyAllocation[i] !== null) {
|
||||
galaxy = getUrbitPointName(galaxyAllocation[i]);
|
||||
this.usedGalaxies.add(galaxyAllocation[i]);
|
||||
}
|
||||
|
||||
// Get pre-allocated stars
|
||||
const stars = starAllocation[i].map(starId => {
|
||||
this.usedStars.add(starId);
|
||||
return getUrbitPointName(starId);
|
||||
});
|
||||
|
||||
const participant: VerifiedParticipant = {
|
||||
attestation: {
|
||||
payload: {
|
||||
address: ethereumAccount.address,
|
||||
msg: "Onboarding my Azimuth ID onto ZenithChain",
|
||||
payload: {
|
||||
address: cosmosAccount.address,
|
||||
msg: "Onboarding my validator onto ZenithChain",
|
||||
owned_points: {
|
||||
galaxy,
|
||||
stars
|
||||
}
|
||||
}
|
||||
},
|
||||
signatures: [
|
||||
`dummy_azimuth_sig_${i}_${Math.random().toString(36).substring(2, 15)}`,
|
||||
`dummy_zenith_sig_${i}_${Math.random().toString(36).substring(2, 15)}`
|
||||
]
|
||||
},
|
||||
role: isValidator ? "validator" : "delegator"
|
||||
};
|
||||
|
||||
participants.push(participant);
|
||||
|
||||
if ((i + 1) % 50 === 0) {
|
||||
console.log(`Generated ${i + 1}/${this.config.totalParticipants} participants...`);
|
||||
}
|
||||
}
|
||||
|
||||
const stats: GenerationStats = {
|
||||
totalParticipants: this.config.totalParticipants,
|
||||
validators: this.config.galaxyCount,
|
||||
delegators: this.config.totalParticipants - this.config.galaxyCount,
|
||||
galaxiesAllocated: this.usedGalaxies.size,
|
||||
starsAllocated: this.usedStars.size,
|
||||
config: this.config
|
||||
};
|
||||
|
||||
console.log('\nGeneration completed!');
|
||||
console.log(`- Galaxies allocated: ${stats.galaxiesAllocated}/${availableGalaxies.length}`);
|
||||
console.log(`- Stars allocated: ${stats.starsAllocated}/${availableStars.length}\n`);
|
||||
|
||||
return { participants, accounts: this.accounts, stats };
|
||||
}
|
||||
|
||||
saveToFiles(outputDir: string, participants: VerifiedParticipant[], accounts: GeneratedAccount[], stats: GenerationStats): void {
|
||||
// Ensure output directory exists
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Save participants
|
||||
const participantsPath = path.join(outputDir, 'generated-participants.json');
|
||||
fs.writeFileSync(participantsPath, JSON.stringify(participants, null, 2));
|
||||
console.log(`Participants saved to: ${participantsPath}`);
|
||||
|
||||
// Save accounts
|
||||
const accountsPath = path.join(outputDir, 'generated-accounts.json');
|
||||
fs.writeFileSync(accountsPath, JSON.stringify(accounts, null, 2));
|
||||
console.log(`Accounts saved to: ${accountsPath}`);
|
||||
|
||||
// Save stats
|
||||
const statsPath = path.join(outputDir, 'point-allocation-stats.json');
|
||||
fs.writeFileSync(statsPath, JSON.stringify(stats, null, 2));
|
||||
console.log(`Statistics saved to: ${statsPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validation function
|
||||
function validateConfig(config: GeneratorConfig): void {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (config.totalParticipants <= 0) {
|
||||
errors.push('Total participants must be greater than 0');
|
||||
}
|
||||
|
||||
if (config.galaxyCount < 0) {
|
||||
errors.push('Galaxy count cannot be negative');
|
||||
}
|
||||
|
||||
if (config.galaxyCount > config.totalParticipants) {
|
||||
errors.push('Galaxy count cannot exceed total participants');
|
||||
}
|
||||
|
||||
if (config.galaxyCount > MAX_GALAXIES) {
|
||||
errors.push(`Galaxy count cannot exceed ${MAX_GALAXIES} (maximum available galaxies)`);
|
||||
}
|
||||
|
||||
if (config.starCount <= 0) {
|
||||
errors.push('Star count must be greater than 0');
|
||||
}
|
||||
|
||||
if (config.starCount < config.totalParticipants) {
|
||||
errors.push('Star count must be at least equal to total participants (each participant needs at least 1 star)');
|
||||
}
|
||||
|
||||
if (config.starCount > MAX_STARS) {
|
||||
errors.push(`Star count cannot exceed ${MAX_STARS} (maximum available stars)`);
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error('\n❌ Configuration validation failed:');
|
||||
errors.forEach(error => console.error(` - ${error}`));
|
||||
console.error('\nUse --help for usage information.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// CLI interface
|
||||
function parseArgs(): GeneratorConfig {
|
||||
const args = process.argv.slice(2);
|
||||
const config: GeneratorConfig = {
|
||||
totalParticipants: 400,
|
||||
galaxyCount: 200,
|
||||
starCount: 2000,
|
||||
outputDir: path.join(process.cwd(), 'generated')
|
||||
};
|
||||
|
||||
for (let i = 0; i < args.length; i += 2) {
|
||||
const key = args[i];
|
||||
const value = args[i + 1];
|
||||
|
||||
switch (key) {
|
||||
case '--participants':
|
||||
config.totalParticipants = parseInt(value, 10);
|
||||
break;
|
||||
case '--galaxy-count':
|
||||
config.galaxyCount = parseInt(value, 10);
|
||||
break;
|
||||
case '--star-count':
|
||||
config.starCount = parseInt(value, 10);
|
||||
break;
|
||||
case '--output-dir':
|
||||
config.outputDir = value;
|
||||
break;
|
||||
case '-h':
|
||||
case '--help':
|
||||
console.log(`
|
||||
Usage: yarn generate-participants -- [options]
|
||||
|
||||
Options:
|
||||
--participants <number> Number of participants to generate (default: 400)
|
||||
--galaxy-count <number> Number of galaxies to allocate (default: 200, max: ${MAX_GALAXIES})
|
||||
--star-count <number> Number of stars to allocate (default: 2000, max: ${MAX_STARS})
|
||||
--output-dir <path> Output directory for generated files (default: <cwd>/generated)
|
||||
--help Show this help message
|
||||
|
||||
Constraints:
|
||||
- galaxy-count ≤ participants (each participant can have at most 1 galaxy)
|
||||
- galaxy-count ≤ ${MAX_GALAXIES} (maximum available galaxies)
|
||||
- star-count ≥ participants (each participant needs at least 1 star)
|
||||
- star-count ≤ ${MAX_STARS} (maximum available stars)
|
||||
|
||||
Examples:
|
||||
yarn generate-participants
|
||||
yarn generate-participants -- --participants 200 --galaxy-count 150 --star-count 1000
|
||||
yarn generate-participants -- --galaxy-count 100 --star-count 500
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate configuration
|
||||
validateConfig(config);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// Main execution
|
||||
if (require.main === module) {
|
||||
const config = parseArgs();
|
||||
|
||||
console.log('Participant Generator Configuration:');
|
||||
console.log(JSON.stringify(config, null, 2));
|
||||
|
||||
// Check for existing files and warn user
|
||||
const participantsPath = path.join(config.outputDir, 'generated-participants.json');
|
||||
const accountsPath = path.join(config.outputDir, 'generated-accounts.json');
|
||||
const statsPath = path.join(config.outputDir, 'point-allocation-stats.json');
|
||||
|
||||
const existingFiles = [];
|
||||
if (fs.existsSync(participantsPath)) existingFiles.push('generated-participants.json');
|
||||
if (fs.existsSync(accountsPath)) existingFiles.push('generated-accounts.json');
|
||||
if (fs.existsSync(statsPath)) existingFiles.push('point-allocation-stats.json');
|
||||
|
||||
if (existingFiles.length > 0) {
|
||||
console.warn(`\nWARNING: The following files will be overwritten:`);
|
||||
existingFiles.forEach(file => console.warn(` - ${path.join(config.outputDir, file)}`));
|
||||
console.warn(` This will replace all existing generated participant data.`);
|
||||
console.warn(` Press Ctrl+C to cancel or any key to continue...`);
|
||||
|
||||
// Wait for user input
|
||||
process.stdin.setRawMode(true);
|
||||
process.stdin.resume();
|
||||
process.stdin.once('data', (data) => {
|
||||
process.stdin.setRawMode(false);
|
||||
process.stdin.pause();
|
||||
|
||||
// Check for Ctrl+C (ASCII code 3)
|
||||
if (data[0] === 3) {
|
||||
console.log('\n\nOperation cancelled by user.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
startGeneration();
|
||||
});
|
||||
} else {
|
||||
startGeneration();
|
||||
}
|
||||
|
||||
function startGeneration() {
|
||||
console.log('\nStarting generation...\n');
|
||||
|
||||
const generator = new ParticipantGenerator(config);
|
||||
generator.generate().then(({ participants, accounts, stats }) => {
|
||||
generator.saveToFiles(config.outputDir, participants, accounts, stats);
|
||||
console.log('\nParticipants generated successfully!');
|
||||
}).catch(error => {
|
||||
console.error('Error during generation:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
57
src/resolvers.ts
Normal file
57
src/resolvers.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { generateDataFromParticipants, saveGeneratedData } from './data-generator';
|
||||
|
||||
function loadOrGenerateData() {
|
||||
const outputDir = process.env.GENERATED_WATCHER_EVENTS_OUTPUT_PATH || path.join(process.cwd(), 'generated');
|
||||
|
||||
// Ensure output directory exists
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
const dataPath = path.join(outputDir, 'watcher-events.json');
|
||||
const verifiedParticipantsPath = process.env.VERIFIED_PARTICIPANTS_PATH;
|
||||
|
||||
// Fallback to existing data
|
||||
if (fs.existsSync(dataPath)) {
|
||||
console.log('Using existing watcher-events.json...');
|
||||
return JSON.parse(fs.readFileSync(dataPath, 'utf8'));
|
||||
}
|
||||
|
||||
// Check if verified-participants.json path is provided
|
||||
if (verifiedParticipantsPath) {
|
||||
if (!fs.existsSync(verifiedParticipantsPath)) {
|
||||
throw new Error(`Verified participants file not found at: ${verifiedParticipantsPath}`);
|
||||
}
|
||||
|
||||
console.log(`Found verified participants at ${verifiedParticipantsPath}, generating watcher-events.json...`);
|
||||
const generatedData = generateDataFromParticipants(verifiedParticipantsPath);
|
||||
saveGeneratedData(generatedData, dataPath);
|
||||
return generatedData;
|
||||
}
|
||||
|
||||
throw new Error('No participants file found. Please provide VERIFIED_PARTICIPANTS_PATH, or ensure watcher-events.json exists.');
|
||||
}
|
||||
|
||||
const data = loadOrGenerateData();
|
||||
|
||||
export const resolvers = {
|
||||
Query: {
|
||||
eventsInRange: (_: any, args: { fromBlockNumber: number; toBlockNumber: number; name?: string }) => {
|
||||
const events = data.data.eventsInRange;
|
||||
|
||||
return events.filter((event: any) => {
|
||||
const blockNumber = event.block.number;
|
||||
const inRange = blockNumber >= args.fromBlockNumber && blockNumber <= args.toBlockNumber;
|
||||
|
||||
if (args.name && args.name !== event.event.__typename) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return inRange;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
27
src/schema.graphql
Normal file
27
src/schema.graphql
Normal file
@ -0,0 +1,27 @@
|
||||
type _Block_ {
|
||||
hash: String!
|
||||
number: Int!
|
||||
timestamp: Int!
|
||||
}
|
||||
|
||||
union Event = PointLockedEvent | LockdropClosedEvent
|
||||
|
||||
type PointLockedEvent {
|
||||
azimuth_id: String!
|
||||
point: String!
|
||||
lock_period: Int!
|
||||
}
|
||||
|
||||
type LockdropClosedEvent {
|
||||
ok: Boolean! # placeholder, GQL doesn't allow empty types
|
||||
}
|
||||
|
||||
type ResultEvent {
|
||||
block: _Block_!
|
||||
contract: String!
|
||||
event: Event!
|
||||
}
|
||||
|
||||
type Query {
|
||||
eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!, name: String): [ResultEvent!]
|
||||
}
|
||||
11
src/types/urbit-ob.d.ts
vendored
Normal file
11
src/types/urbit-ob.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
declare module 'urbit-ob' {
|
||||
/**
|
||||
* Converts a decimal string to a `@p` name (e.g., '0' → '~zod').
|
||||
*/
|
||||
export function patp(n: string): string;
|
||||
|
||||
/**
|
||||
* Converts a `@p` name to a decimal string (e.g., '~zod' → '0').
|
||||
*/
|
||||
export function patp2dec(n: string): string;
|
||||
}
|
||||
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user