intuition-cli
Version:
A CLI for the Intuition protocol.
356 lines (355 loc) • 16 kB
JavaScript
/* eslint-disable max-params */
/* eslint-disable no-await-in-loop */
import { batchCreateAtomsFromEthereumAccounts, batchCreateAtomsFromIpfsUris, batchCreateAtomsFromSmartContracts, batchCreateAtomsFromThings, batchCreateTripleStatements, intuitionDeployments, } from '@0xintuition/sdk';
import select from '@inquirer/select';
import { Command, Flags } from '@oclif/core';
import chalk from 'chalk';
import csv from 'csv-parser';
import { createObjectCsvWriter } from 'csv-writer';
import fs from 'node:fs';
import { createPublicClient, createWalletClient, getAddress, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { getAccounts, getDefaultAccount, getDefaultNetwork } from '../../../config.js';
import { base, baseSepolia, getNetworkByName } from '../../../networks.js';
export default class BatchStart extends Command {
static description = 'Batch create atoms using a CSV file';
static examples = ['<%= config.bin %> <%= command.id %> --name my-batch.csv'];
static flags = {
count: Flags.string({
char: 'c',
default: '50',
description: 'Amount to batch together. Default is 50',
}),
list: Flags.string({ char: 'l', description: 'Add atoms to a list.' }),
name: Flags.string({
char: 'n',
description: 'Filename to load. Default is intuition-data.csv',
}),
network: Flags.string({ description: 'Network to use.' }),
};
async run() {
const { flags } = await this.parse(BatchStart);
// 1. Setup clients
const clientSetup = await this.setupClients(flags.network);
if (!clientSetup)
return;
const { contractAddress, publicClient, walletClient } = clientSetup;
// 2. Select atom type
const atomType = await this.getAtomType();
// 3. Load and prepare CSV data
const fileName = flags.name ?? 'intuition-data.csv';
const { headers, rows: allRows } = await this.loadCsv(fileName);
const vaultIdCol = headers.find((h) => h.toLowerCase().includes('vaultid')) || 'vaultId';
const unprocessedRows = allRows.filter((row) => !row[vaultIdCol] || row[vaultIdCol].trim() === '');
if (unprocessedRows.length === 0) {
this.log(chalk.yellow('All rows have already been processed.'));
return;
}
this.log(chalk.blue(`📄 ${allRows.length} rows loaded, ${unprocessedRows.length} to process.`));
// 4. Process batches
const atomConfig = {
address: contractAddress,
publicClient,
walletClient,
};
const processedCount = await this.processBatches({ allRows, headers, unprocessedRows }, { atomConfig, atomType, flags });
this.log(chalk.green(`🎉 Done! ${processedCount} atoms processed and CSV updated.`));
}
async batchTagAtomsInList(atomConfig, vaultIds, listFlag) {
const listIds = listFlag
.split(',')
.map((id) => id.trim())
.filter(Boolean);
// @DEV: This is a very hacky way to assign the "has tag" atom.
// Base: The Atom ID is 4.
// Base Sepolia: The Atom ID is 3.
// @TODO: It gets the job done, but fragile. It should probably be a constant in the protocol module.
const hasTagIdFromNetwork = atomConfig.walletClient.chain?.id === base.id ? 4 : 3;
const hasTagId = Array.from({ length: vaultIds.length }).fill(hasTagIdFromNetwork);
for (const listIdValue of listIds) {
const listIdArr = Array.from({ length: vaultIds.length }).fill(listIdValue);
// @ts-expect-error SDK types need to be updated
await batchCreateTripleStatements(atomConfig, [vaultIds, hasTagId, listIdArr]);
this.log(chalk.green(`✅ New atoms have been tagged in list ${listIdValue}`));
}
}
async getAtomType() {
const atomType = await select({
choices: [
{
description: 'Batch create atoms from Things',
name: 'Thing',
value: 'thing',
},
{
description: 'Batch create atoms from Ethereum addresses',
name: 'Ethereum Account',
value: 'ethereum-account',
},
{
description: 'Batch create atoms from IPFS URIs',
name: 'IPFS URI',
value: 'ipfs-uri',
},
{
description: 'Batch create atoms from Smart Contracts',
name: 'Smart Contract',
value: 'smart-contract',
},
],
message: 'Select atom type to batch create:',
});
this.log(chalk.green(`✅ Selected: ${atomType.replace('-', ' ')}`));
return atomType;
}
async loadCsv(fileName) {
const rows = [];
const headers = [];
await new Promise((resolve, reject) => {
fs.createReadStream(fileName)
.on('error', reject)
.pipe(csv())
.on('headers', (hdrs) => headers.push(...hdrs))
.on('data', (data) => rows.push(data))
.on('end', resolve)
.on('error', reject);
});
return { headers, rows };
}
async processBatches({ allRows, headers, unprocessedRows }, { atomConfig, atomType, flags, }) {
const { count, list, name: fileName } = flags;
const batchSize = Number.parseInt(count, 10);
let processedCount = 0;
for (let i = 0; i < unprocessedRows.length; i += batchSize) {
const batch = unprocessedRows.slice(i, i + batchSize);
this.log(chalk.blue(`🚀 Processing batch ${Math.floor(i / batchSize) + 1} (${batch.length} atoms)...`));
try {
switch (atomType) {
case 'ethereum-account': {
await this.processEthereumAccountBatch(batch, allRows, headers, atomConfig, list);
break;
}
case 'ipfs-uri': {
await this.processIpfsUriBatch(batch, allRows, headers, atomConfig, list);
break;
}
case 'smart-contract': {
await this.processSmartContractBatch(batch, allRows, headers, atomConfig, list);
break;
}
case 'thing': {
await this.processThingBatch(batch, allRows, headers, atomConfig, list);
break;
}
default: {
this.log(chalk.yellow('This atom type is not yet supported for batch creation.'));
}
}
}
catch (error) {
this.log(chalk.red(`❌ Error processing batch: ${error instanceof Error ? error.message : String(error)}`));
break; // Exit on error
}
processedCount += batch.length;
// Write progress to CSV after each batch
const csvWriter = createObjectCsvWriter({
alwaysQuote: true,
header: headers.map((h) => ({ id: h, title: h })),
path: fileName ?? 'intuition-data.csv',
});
await csvWriter.writeRecords(allRows);
this.log(chalk.gray(`Progress saved to ${fileName ?? 'intuition-data.csv'}`));
}
return processedCount;
}
async processEthereumAccountBatch(batch, allRows, headers, atomConfig, listFlag) {
// 1. Ensure required columns
const requiredCols = ['vaultId'];
for (const col of requiredCols) {
if (!headers.includes(col)) {
headers.push(col);
for (const row of allRows) {
if (!(col in row))
row[col] = '';
}
}
}
// 2. Prepare batch input
const batchInput = batch.map((row) => getAddress(row.address));
// 3. Call SDK batch function
const result = await batchCreateAtomsFromEthereumAccounts(atomConfig, batchInput);
const { state } = result;
// 4. Update allRows and collect vaultIds
let idx = 0;
const vaultIds = [];
for (let j = 0; j < allRows.length && idx < batch.length; j++) {
const row = allRows[j];
if ((!row.vaultId || row.vaultId.trim() === '') && row.address && batchInput.includes(getAddress(row.address))) {
row.vaultId = state[idx]?.vaultId?.toString?.() || '';
vaultIds.push(row.vaultId);
this.log(chalk.green(`✅ Created atom for address: ${row.address} (VaultId: ${row.vaultId})`));
idx++;
}
}
// 5. Tag list if needed
if (listFlag && vaultIds.length > 0) {
await this.batchTagAtomsInList(atomConfig, vaultIds, listFlag);
}
}
async processIpfsUriBatch(batch, allRows, headers, atomConfig, listFlag) {
// 1. Ensure required columns
const requiredCols = ['vaultId'];
for (const col of requiredCols) {
if (!headers.includes(col)) {
headers.push(col);
for (const row of allRows) {
if (!(col in row))
row[col] = '';
}
}
}
// 2. Prepare batch input
const batchInput = batch.map((row) => row.ipfsUri);
// 3. Call SDK batch function
const result = await batchCreateAtomsFromIpfsUris(atomConfig, batchInput);
const { state } = result;
// 4. Update allRows and collect vaultIds
let idx = 0;
const vaultIds = [];
for (let j = 0; j < allRows.length && idx < batch.length; j++) {
const row = allRows[j];
if ((!row.vaultId || row.vaultId.trim() === '') && row.ipfsUri && batchInput.includes(row.ipfsUri)) {
row.vaultId = state[idx]?.vaultId?.toString?.() || '';
vaultIds.push(row.vaultId);
this.log(chalk.green(`✅ Created atom for IPFS URI: ${row.ipfsUri} (VaultId: ${row.vaultId})`));
idx++;
}
}
// 5. Tag list if needed
if (listFlag && vaultIds.length > 0) {
await this.batchTagAtomsInList(atomConfig, vaultIds, listFlag);
}
}
async processSmartContractBatch(batch, allRows, headers, atomConfig, listFlag) {
// 1. Ensure required columns
const requiredCols = ['vaultId'];
for (const col of requiredCols) {
if (!headers.includes(col)) {
headers.push(col);
for (const row of allRows) {
if (!(col in row))
row[col] = '';
}
}
}
// 2. Prepare batch input
const batchInput = batch.map((row) => ({
address: getAddress(row.address),
chainId: Number(row.chainId),
}));
// 3. Call SDK batch function
const result = await batchCreateAtomsFromSmartContracts(atomConfig, batchInput);
const { state } = result;
// 4. Update allRows and collect vaultIds
let idx = 0;
const vaultIds = [];
for (let j = 0; j < allRows.length && idx < batch.length; j++) {
const row = allRows[j];
if ((!row.vaultId || row.vaultId.trim() === '') &&
row.address &&
row.chainId &&
batchInput.some((input) => input.address === getAddress(row.address) && input.chainId === Number(row.chainId))) {
row.vaultId = state[idx]?.vaultId?.toString?.() || '';
vaultIds.push(row.vaultId);
this.log(chalk.green(`✅ Created atom for smart contract: ${row.address} (ChainId: ${row.chainId}, VaultId: ${row.vaultId})`));
idx++;
}
}
// 5. Tag list if needed
if (listFlag && vaultIds.length > 0) {
await this.batchTagAtomsInList(atomConfig, vaultIds, listFlag);
}
}
async processThingBatch(batch, allRows, headers, atomConfig, listFlag) {
// 1. Ensure required columns
const requiredCols = ['vaultId', 'ipfsUri'];
for (const col of requiredCols) {
if (!headers.includes(col)) {
headers.push(col);
for (const row of allRows) {
if (!(col in row))
row[col] = '';
}
}
}
// 2. Prepare batch input
const batchInput = batch.map((row) => ({
description: row.description,
image: row.image,
name: row.name,
url: row.url,
}));
// 3. Call SDK batch function
const result = await batchCreateAtomsFromThings(atomConfig, batchInput);
const { state, uris } = result;
// 4. Update allRows and collect vaultIds
let idx = 0;
const vaultIds = [];
for (let j = 0; j < allRows.length && idx < batch.length; j++) {
const row = allRows[j];
if ((!row.vaultId || row.vaultId.trim() === '') &&
row.name &&
batchInput.some((input) => input.name === row.name)) {
row.vaultId = state[idx]?.vaultId?.toString?.() || '';
if ('ipfsUri' in row && uris && uris[idx]) {
row.ipfsUri = uris[idx];
}
vaultIds.push(row.vaultId);
this.log(chalk.green(`✅ Created atom for thing: ${row.name} (VaultId: ${row.vaultId})`));
idx++;
}
}
// 5. Tag list if needed
if (listFlag && vaultIds.length > 0) {
await this.batchTagAtomsInList(atomConfig, vaultIds, listFlag);
}
}
// #region Private Helper Methods
async setupClients(networkFlag) {
// 1. Determine and validate network
const networkName = networkFlag || getDefaultNetwork() || 'base';
const network = getNetworkByName(networkName);
if (!network) {
this.log(chalk.red(`❌ Unsupported network: ${networkName}`));
this.log(chalk.gray('Supported: base, base-sepolia'));
return;
}
this.log(chalk.blue(`🌐 Using network: ${network.name}`));
// 2. Get default account
const defaultAccountAddress = getDefaultAccount();
const accounts = getAccounts();
const defaultAccount = accounts.find((acc) => acc.address.toLowerCase() === (defaultAccountAddress || '').toLowerCase());
if (!defaultAccount) {
this.log(chalk.red('❌ No default account found.'));
this.log(chalk.gray('Set a default account: intuition account set-default <address>'));
this.log(chalk.gray('Or import an account: intuition account import <private-key>'));
return;
}
// 3. Create Viem clients
const account = privateKeyToAccount(defaultAccount.privateKey);
const chain = network.id === base.id ? base : baseSepolia;
const walletClient = createWalletClient({
account,
chain,
transport: http(),
});
const publicClient = createPublicClient({ chain, transport: http() });
// 4. Get contract address
const contractAddress = intuitionDeployments.EthMultiVault?.[network.id];
if (!contractAddress) {
this.log(chalk.red(`❌ No contract deployment found for network: ${network.name}`));
return;
}
return { account, contractAddress, network, publicClient, walletClient };
}
}