Add a script to get subscribed and onboarded laconicd accounts (#22)
Part of [laconicd testnet validator enrollment](cerc-io/testnet-laconicd-stack#19) The script: - Fetches onboarded participants using the configured laconicd GQL endpoint - Reads a given CSV file with subscribers data - Fetches all the onboarding txs using given laconicd RPC endpoint - For each subscriber registered as a participant, maps the laconic pubkey and latest onboarding tx height - Outputs the result in a CSV Co-authored-by: Nabarun <nabarun@deepstacksoft.com> Reviewed-on: cerc-io/testnet-laconicd-stack#22 Co-authored-by: Prathamesh Musale <prathamesh.musale0@gmail.com> Co-committed-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
import * as fs from 'fs';
|
||||
import * as crypto from 'crypto';
|
||||
import * as path from 'path';
|
||||
import yargs from 'yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
import { parse as csvParse } from 'csv-parse';
|
||||
import * as csvWriter from 'csv-writer';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
import { StargateClient } from '@cosmjs/stargate';
|
||||
import { Registry } from '@cerc-io/registry-sdk';
|
||||
import { decodeTxRaw, decodePubkey } from '@cosmjs/proto-signing';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const LACONICD_GQL_ENDPOINT = process.env.LACONICD_GQL_ENDPOINT || 'https://laconicd.laconic.com/api';
|
||||
const LACONICD_RPC_ENDPOINT = process.env.LACONICD_RPC_ENDPOINT || 'https://laconicd.laconic.com';
|
||||
const LACONICD_CHAIN_ID = process.env.LACONICD_CHAIN_ID || 'laconic_9000-1';
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const argv = _getArgv();
|
||||
|
||||
const registry = new Registry(LACONICD_GQL_ENDPOINT, LACONICD_RPC_ENDPOINT, LACONICD_CHAIN_ID);
|
||||
const client = await StargateClient.connect(LACONICD_RPC_ENDPOINT);
|
||||
|
||||
console.time('time_taken_getParticipants');
|
||||
const participants = await registry.getParticipants();
|
||||
console.timeEnd('time_taken_getParticipants');
|
||||
|
||||
const subscribers = await readSubscribers(argv.subscribersCsv);
|
||||
console.log('Read subscribers, count:', subscribers.length);
|
||||
|
||||
await processSubscribers(client, participants, subscribers, argv.output);
|
||||
}
|
||||
|
||||
async function readSubscribers(subscribersCsvPath: string): Promise<any> {
|
||||
const fileContent = fs.readFileSync(path.resolve(subscribersCsvPath), { encoding: 'utf-8' });
|
||||
const headers = ['subscriber_id', 'email', 'status', 'premium?', 'created_at', 'api_subscription_id'];
|
||||
|
||||
return csvParse(fileContent, { delimiter: ',', columns: headers }).toArray();
|
||||
}
|
||||
|
||||
function hashSubscriberId(subscriberId: string): string {
|
||||
return '0x' + crypto.createHash('sha256').update(subscriberId).digest('hex');
|
||||
}
|
||||
|
||||
async function processSubscribers(client: StargateClient, participants: any[], subscribers: any[], outputPath: string) {
|
||||
// Map kyc_id to participant data
|
||||
const kycMap: Record<string, any> = {};
|
||||
participants.forEach((participant: any) => {
|
||||
kycMap[participant.kycId] = participant;
|
||||
});
|
||||
|
||||
const onboardingTxsHeightMap: Record<string, { txHeight: number, pubkey: string }> = {};
|
||||
console.time('time_taken_searchTx');
|
||||
const onboardingTxs = await client.searchTx(`message.action='/cerc.onboarding.v1.MsgOnboardParticipant'`);
|
||||
console.timeEnd('time_taken_searchTx');
|
||||
|
||||
console.log('Fetched onboardingTxs, count:', onboardingTxs.length);
|
||||
|
||||
console.time('time_taken_decodingTxs');
|
||||
onboardingTxs.forEach(onboardingTx => {
|
||||
const rawPubkey = decodeTxRaw(onboardingTx.tx).authInfo.signerInfos[0].publicKey;
|
||||
if (!rawPubkey) {
|
||||
console.error('pubkey not found in tx', onboardingTx.hash);
|
||||
return;
|
||||
}
|
||||
|
||||
const pubkey = decodePubkey(rawPubkey).value;
|
||||
|
||||
// Determine sender
|
||||
const onboardParticipantEvent = onboardingTx.events.find(event => event.type === 'onboard_participant');
|
||||
if (!onboardParticipantEvent) {
|
||||
console.error('onboard_participant event not found in tx', onboardingTx.hash);
|
||||
return;
|
||||
}
|
||||
|
||||
const sender = onboardParticipantEvent.attributes.find(attr => attr.key === 'signer')?.value;
|
||||
if (!sender) {
|
||||
console.error('sender not found in onboard_participant event for tx', onboardingTx.hash)
|
||||
return;
|
||||
}
|
||||
|
||||
// Update if already exists
|
||||
let latesTxHeight = onboardingTx.height;
|
||||
if (onboardingTxsHeightMap[sender]) {
|
||||
latesTxHeight = latesTxHeight > onboardingTxsHeightMap[sender].txHeight ? latesTxHeight : onboardingTxsHeightMap[sender].txHeight;
|
||||
}
|
||||
|
||||
onboardingTxsHeightMap[sender] = { txHeight: latesTxHeight, pubkey };
|
||||
});
|
||||
console.timeEnd('time_taken_decodingTxs');
|
||||
|
||||
const onboardedSubscribers: any[] = [];
|
||||
for (let i = 0; i < subscribers.length; i++) {
|
||||
const subscriber = subscribers[i];
|
||||
|
||||
const hashedSubscriberId = hashSubscriberId(subscriber['subscriber_id']);
|
||||
const participant = kycMap[hashedSubscriberId];
|
||||
if (!participant) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const participantAddresss = participant['cosmosAddress'];
|
||||
|
||||
// Skip participant if an onboarding tx not found
|
||||
if (!onboardingTxsHeightMap[participantAddresss]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const onboardedSubscriber = {
|
||||
subscriber_id: subscriber['subscriber_id'],
|
||||
email: subscriber['email'],
|
||||
status: subscriber['status'],
|
||||
'premium?': subscriber['premium?'],
|
||||
created_at: subscriber['created_at'],
|
||||
laconic_address: participantAddresss,
|
||||
nitro_address: participant['nitroAddress'],
|
||||
role: participant['role'],
|
||||
hashed_subscriber_id: participant['kycId'],
|
||||
laconic_pubkey: onboardingTxsHeightMap[participantAddresss].pubkey,
|
||||
onboarding_height: onboardingTxsHeightMap[participantAddresss].txHeight
|
||||
};
|
||||
|
||||
onboardedSubscribers.push(onboardedSubscriber);
|
||||
}
|
||||
|
||||
const writer = csvWriter.createObjectCsvWriter({
|
||||
path: path.resolve(outputPath),
|
||||
header: [
|
||||
{ id: 'subscriber_id', title: 'subscriber_id' },
|
||||
{ id: 'email', title: 'email' },
|
||||
{ id: 'status', title: 'status' },
|
||||
{ id: 'premium?', title: 'premium?' },
|
||||
{ id: 'created_at', title: 'created_at' },
|
||||
{ id: 'laconic_address', title: 'laconic_address' },
|
||||
{ id: 'nitro_address', title: 'nitro_address' },
|
||||
{ id: 'role', title: 'role' },
|
||||
{ id: 'hashed_subscriber_id', title: 'hashed_subscriber_id' },
|
||||
{ id: 'laconic_pubkey', title: 'laconic_pubkey' },
|
||||
{ id: 'onboarding_height', title: 'onboarding_height' },
|
||||
],
|
||||
alwaysQuote: true
|
||||
});
|
||||
|
||||
await writer.writeRecords(onboardedSubscribers);
|
||||
|
||||
console.log(`Data has been written to ${path.resolve(outputPath)}`);
|
||||
}
|
||||
|
||||
function _getArgv (): any {
|
||||
return yargs(hideBin(process.argv))
|
||||
.option('subscribersCsv', {
|
||||
alias: 's',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
describe: 'Path to the subscribers CSV file',
|
||||
})
|
||||
.option('output', {
|
||||
alias: 'o',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
describe: 'Path to the output CSV file',
|
||||
})
|
||||
.help()
|
||||
.argv;
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.log(err);
|
||||
});
|
||||
Reference in New Issue
Block a user