@net-protocol/cli 0.1.12 → 0.1.14

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.
@@ -4,19 +4,23 @@ import 'dotenv/config';
4
4
  import { Command } from 'commander';
5
5
  import { createRequire } from 'module';
6
6
  import chalk4 from 'chalk';
7
- import * as fs2 from 'fs';
7
+ import * as fs4 from 'fs';
8
8
  import { readFileSync } from 'fs';
9
9
  import { StorageClient, detectFileTypeFromBase64, base64ToDataUri, shouldSuggestXmlStorage, getStorageKeyBytes, encodeStorageKeyForUrl, chunkDataForStorage, CHUNKED_STORAGE_CONTRACT, STORAGE_CONTRACT as STORAGE_CONTRACT$1 } from '@net-protocol/storage';
10
10
  import { stringToHex, createWalletClient, http, hexToString, parseEther, encodeFunctionData, publicActions, defineChain } from 'viem';
11
11
  import { privateKeyToAccount } from 'viem/accounts';
12
- import { getNetContract, getChainName, getPublicClient, getChainRpcUrls, NetClient, toBytes32 } from '@net-protocol/core';
12
+ import { getNetContract, getChainName, getPublicClient, getChainRpcUrls, NetClient, toBytes32, NULL_ADDRESS } from '@net-protocol/core';
13
13
  import { createRelayX402Client, createRelaySession, checkBackendWalletBalance, fundBackendWallet, batchTransactions, submitTransactionsViaRelay, waitForConfirmations, retryFailedTransactions as retryFailedTransactions$1 } from '@net-protocol/relay';
14
+ import { FeedRegistryClient, FeedClient } from '@net-protocol/feeds';
14
15
  import { isNetrSupportedChain, NetrClient } from '@net-protocol/netr';
15
16
  import { PROFILE_PICTURE_STORAGE_KEY, PROFILE_METADATA_STORAGE_KEY, parseProfileMetadata, PROFILE_CANVAS_STORAGE_KEY, isValidUrl, getProfilePictureStorageArgs, STORAGE_CONTRACT, isValidXUsername, getProfileMetadataStorageArgs, isValidBio, isValidTokenAddress } from '@net-protocol/profiles';
16
17
  import { base } from 'viem/chains';
17
18
  import * as path from 'path';
18
19
  import { BazaarClient } from '@net-protocol/bazaar';
20
+ import * as os from 'os';
21
+ import * as readline from 'readline';
19
22
 
23
+ var DEFAULT_CHAIN_ID = 8453;
20
24
  function getRequiredChainId(optionValue) {
21
25
  const chainId = optionValue || (process.env.NET_CHAIN_ID ? parseInt(process.env.NET_CHAIN_ID, 10) : void 0);
22
26
  if (!chainId) {
@@ -29,9 +33,22 @@ function getRequiredChainId(optionValue) {
29
33
  }
30
34
  return chainId;
31
35
  }
36
+ function getChainIdWithDefault(optionValue) {
37
+ if (optionValue) {
38
+ return optionValue;
39
+ }
40
+ const envChainId = process.env.BOTCHAN_CHAIN_ID || process.env.NET_CHAIN_ID;
41
+ if (envChainId) {
42
+ return parseInt(envChainId, 10);
43
+ }
44
+ return DEFAULT_CHAIN_ID;
45
+ }
32
46
  function getRpcUrl(optionValue) {
33
47
  return optionValue || process.env.NET_RPC_URL;
34
48
  }
49
+ function getRpcUrlWithBotchanFallback(optionValue) {
50
+ return optionValue || process.env.BOTCHAN_RPC_URL || process.env.NET_RPC_URL;
51
+ }
35
52
  function parseCommonOptions(options, supportsEncodeOnly = false) {
36
53
  const privateKey = options.privateKey || process.env.NET_PRIVATE_KEY || process.env.PRIVATE_KEY;
37
54
  if (!privateKey) {
@@ -70,6 +87,44 @@ function parseReadOnlyOptions(options) {
70
87
  rpcUrl: getRpcUrl(options.rpcUrl)
71
88
  };
72
89
  }
90
+ function parseReadOnlyOptionsWithDefault(options) {
91
+ return {
92
+ chainId: getChainIdWithDefault(options.chainId),
93
+ rpcUrl: getRpcUrlWithBotchanFallback(options.rpcUrl)
94
+ };
95
+ }
96
+ function parseCommonOptionsWithDefault(options, supportsEncodeOnly = false) {
97
+ const privateKey = options.privateKey || process.env.BOTCHAN_PRIVATE_KEY || process.env.NET_PRIVATE_KEY || process.env.PRIVATE_KEY;
98
+ if (!privateKey) {
99
+ const encodeOnlyHint = supportsEncodeOnly ? ", or use --encode-only to output transaction data without submitting" : "";
100
+ console.error(
101
+ chalk4.red(
102
+ `Error: Private key is required. Provide via --private-key flag or NET_PRIVATE_KEY/BOTCHAN_PRIVATE_KEY environment variable${encodeOnlyHint}`
103
+ )
104
+ );
105
+ process.exit(1);
106
+ }
107
+ if (!privateKey.startsWith("0x") || privateKey.length !== 66) {
108
+ console.error(
109
+ chalk4.red(
110
+ "Error: Invalid private key format (must be 0x-prefixed, 66 characters)"
111
+ )
112
+ );
113
+ process.exit(1);
114
+ }
115
+ if (options.privateKey) {
116
+ console.warn(
117
+ chalk4.yellow(
118
+ "Warning: Private key provided via command line. Consider using NET_PRIVATE_KEY environment variable instead."
119
+ )
120
+ );
121
+ }
122
+ return {
123
+ privateKey,
124
+ chainId: getChainIdWithDefault(options.chainId),
125
+ rpcUrl: getRpcUrlWithBotchanFallback(options.rpcUrl)
126
+ };
127
+ }
73
128
  async function checkNormalStorageExists(params) {
74
129
  const { storageClient, storageKey, operatorAddress, expectedContent } = params;
75
130
  const existing = await storageClient.get({
@@ -1194,7 +1249,7 @@ async function encodeStorageUpload(options) {
1194
1249
  } else {
1195
1250
  operatorAddress = "0x0000000000000000000000000000000000000000";
1196
1251
  }
1197
- const fileContent = fs2.readFileSync(options.filePath, "utf-8");
1252
+ const fileContent = fs4.readFileSync(options.filePath, "utf-8");
1198
1253
  const fileSize = Buffer.byteLength(fileContent, "utf-8");
1199
1254
  const client = new StorageClient({
1200
1255
  chainId: readOnlyOptions.chainId
@@ -1478,8 +1533,8 @@ function registerStorageCommand(program2) {
1478
1533
  console.log(chalk4.blue(`\u{1F4C1} Reading file: ${options.file}`));
1479
1534
  console.log(chalk4.blue(`\u{1F517} Using relay API: ${options.apiUrl}`));
1480
1535
  const result = await uploadFileWithRelay(uploadRelayOptions);
1481
- const { privateKeyToAccount: privateKeyToAccount17 } = await import('viem/accounts');
1482
- const userAccount = privateKeyToAccount17(commonOptions.privateKey);
1536
+ const { privateKeyToAccount: privateKeyToAccount24 } = await import('viem/accounts');
1537
+ const userAccount = privateKeyToAccount24(commonOptions.privateKey);
1483
1538
  const storageUrl = generateStorageUrl(
1484
1539
  userAccount.address,
1485
1540
  commonOptions.chainId,
@@ -1549,6 +1604,18 @@ function registerStorageCommand(program2) {
1549
1604
  storageCommand.addCommand(uploadRelayCommand);
1550
1605
  storageCommand.addCommand(readCommand);
1551
1606
  }
1607
+ function createFeedClient(options) {
1608
+ return new FeedClient({
1609
+ chainId: options.chainId,
1610
+ overrides: options.rpcUrl ? { rpcUrls: [options.rpcUrl] } : void 0
1611
+ });
1612
+ }
1613
+ function createFeedRegistryClient(options) {
1614
+ return new FeedRegistryClient({
1615
+ chainId: options.chainId,
1616
+ overrides: options.rpcUrl ? { rpcUrls: [options.rpcUrl] } : void 0
1617
+ });
1618
+ }
1552
1619
  function createNetClient(options) {
1553
1620
  return new NetClient({
1554
1621
  chainId: options.chainId,
@@ -2755,10 +2822,10 @@ async function executeProfileSetCanvas(options) {
2755
2822
  let canvasContent;
2756
2823
  if (options.file) {
2757
2824
  const filePath = path.resolve(options.file);
2758
- if (!fs2.existsSync(filePath)) {
2825
+ if (!fs4.existsSync(filePath)) {
2759
2826
  exitWithError(`File not found: ${filePath}`);
2760
2827
  }
2761
- const buffer = fs2.readFileSync(filePath);
2828
+ const buffer = fs4.readFileSync(filePath);
2762
2829
  if (buffer.length > MAX_CANVAS_SIZE) {
2763
2830
  exitWithError(
2764
2831
  `File too large: ${buffer.length} bytes exceeds maximum of ${MAX_CANVAS_SIZE} bytes (60KB).`
@@ -2933,12 +3000,12 @@ async function executeProfileGetCanvas(options) {
2933
3000
  if (!path.extname(outputPath)) {
2934
3001
  finalPath = outputPath + getExtensionFromMimeType(mimeType);
2935
3002
  }
2936
- fs2.writeFileSync(finalPath, buffer);
3003
+ fs4.writeFileSync(finalPath, buffer);
2937
3004
  console.log(
2938
3005
  chalk4.green(`Canvas written to: ${finalPath} (${buffer.length} bytes)`)
2939
3006
  );
2940
3007
  } else {
2941
- fs2.writeFileSync(outputPath, canvasContent, "utf-8");
3008
+ fs4.writeFileSync(outputPath, canvasContent, "utf-8");
2942
3009
  console.log(
2943
3010
  chalk4.green(
2944
3011
  `Canvas written to: ${outputPath} (${canvasContent.length} bytes)`
@@ -4025,133 +4092,2293 @@ Owned Tokens (${tokenIds.length}):
4025
4092
  );
4026
4093
  }
4027
4094
  }
4028
-
4029
- // src/commands/bazaar/index.ts
4030
- var chainIdOption = [
4031
- "--chain-id <id>",
4032
- "Chain ID. Can also be set via NET_CHAIN_ID env var",
4033
- (value) => parseInt(value, 10)
4034
- ];
4035
- var rpcUrlOption = [
4036
- "--rpc-url <url>",
4037
- "Custom RPC URL. Can also be set via NET_RPC_URL env var"
4038
- ];
4039
- var privateKeyOption = [
4040
- "--private-key <key>",
4041
- "Private key (0x-prefixed hex). Can also be set via NET_PRIVATE_KEY env var"
4042
- ];
4043
- function registerBazaarCommand(program2) {
4044
- const bazaarCommand = program2.command("bazaar").description("Bazaar NFT trading operations");
4045
- const listListingsCommand = new Command("list-listings").description("List active NFT listings").option("--nft-address <address>", "NFT contract address (optional for cross-collection)").option(...chainIdOption).option(...rpcUrlOption).option("--json", "Output in JSON format").action(async (options) => {
4046
- await executeListListings({
4047
- nftAddress: options.nftAddress,
4048
- chainId: options.chainId,
4049
- rpcUrl: options.rpcUrl,
4050
- json: options.json
4051
- });
4052
- });
4053
- const listOffersCommand = new Command("list-offers").description("List active collection offers").requiredOption("--nft-address <address>", "NFT contract address").option(...chainIdOption).option(...rpcUrlOption).option("--json", "Output in JSON format").action(async (options) => {
4054
- await executeListOffers({
4055
- nftAddress: options.nftAddress,
4056
- chainId: options.chainId,
4057
- rpcUrl: options.rpcUrl,
4058
- json: options.json
4059
- });
4095
+ async function executeListErc20Listings(options) {
4096
+ const readOnlyOptions = parseReadOnlyOptions({
4097
+ chainId: options.chainId,
4098
+ rpcUrl: options.rpcUrl
4060
4099
  });
4061
- const listSalesCommand = new Command("list-sales").description("List recent sales").requiredOption("--nft-address <address>", "NFT contract address").option(...chainIdOption).option(...rpcUrlOption).option("--json", "Output in JSON format").action(async (options) => {
4062
- await executeListSales({
4063
- nftAddress: options.nftAddress,
4064
- chainId: options.chainId,
4065
- rpcUrl: options.rpcUrl,
4066
- json: options.json
4067
- });
4100
+ const bazaarClient = new BazaarClient({
4101
+ chainId: readOnlyOptions.chainId,
4102
+ rpcUrl: readOnlyOptions.rpcUrl
4068
4103
  });
4069
- const createListingCommand = new Command("create-listing").description("Create an NFT listing (with --private-key: full flow; without: output EIP-712 data)").requiredOption("--nft-address <address>", "NFT contract address").requiredOption("--token-id <id>", "Token ID to list").requiredOption("--price <eth>", "Price in ETH (e.g., 0.1)").option("--target-fulfiller <address>", "Make a private listing for this address").option("--offerer <address>", "Offerer address (required without --private-key)").option(...privateKeyOption).option(...chainIdOption).option(...rpcUrlOption).action(async (options) => {
4070
- await executeCreateListing({
4071
- nftAddress: options.nftAddress,
4072
- tokenId: options.tokenId,
4073
- price: options.price,
4074
- targetFulfiller: options.targetFulfiller,
4075
- offerer: options.offerer,
4076
- privateKey: options.privateKey,
4077
- chainId: options.chainId,
4078
- rpcUrl: options.rpcUrl
4104
+ try {
4105
+ const listings = await bazaarClient.getErc20Listings({
4106
+ tokenAddress: options.tokenAddress
4079
4107
  });
4108
+ if (options.json) {
4109
+ const output = listings.map((l) => ({
4110
+ orderHash: l.orderHash,
4111
+ maker: l.maker,
4112
+ tokenAddress: l.tokenAddress,
4113
+ tokenAmount: l.tokenAmount.toString(),
4114
+ price: l.price,
4115
+ priceWei: l.priceWei.toString(),
4116
+ pricePerToken: l.pricePerToken,
4117
+ pricePerTokenWei: l.pricePerTokenWei.toString(),
4118
+ currency: l.currency,
4119
+ expirationDate: l.expirationDate,
4120
+ orderStatus: l.orderStatus
4121
+ }));
4122
+ console.log(JSON.stringify(output, null, 2));
4123
+ return;
4124
+ }
4125
+ if (listings.length === 0) {
4126
+ console.log(chalk4.yellow("No active ERC-20 listings found"));
4127
+ return;
4128
+ }
4129
+ console.log(chalk4.white.bold(`
4130
+ ERC-20 Listings (${listings.length}):
4131
+ `));
4132
+ for (const listing of listings) {
4133
+ const expiry = new Date(listing.expirationDate * 1e3).toLocaleString();
4134
+ console.log(` ${chalk4.cyan("Order Hash:")} ${listing.orderHash}`);
4135
+ console.log(` ${chalk4.cyan("Seller:")} ${listing.maker}`);
4136
+ console.log(` ${chalk4.cyan("Token:")} ${listing.tokenAddress}`);
4137
+ console.log(` ${chalk4.cyan("Amount:")} ${listing.tokenAmount.toString()}`);
4138
+ console.log(` ${chalk4.cyan("Price:")} ${formatEthPrice(listing.price)} ${listing.currency.toUpperCase()}`);
4139
+ console.log(` ${chalk4.cyan("Price/Token:")} ${formatEthPrice(listing.pricePerToken)} ${listing.currency.toUpperCase()}`);
4140
+ console.log(` ${chalk4.cyan("Expires:")} ${expiry}`);
4141
+ console.log();
4142
+ }
4143
+ } catch (error) {
4144
+ exitWithError(
4145
+ `Failed to fetch ERC-20 listings: ${error instanceof Error ? error.message : String(error)}`
4146
+ );
4147
+ }
4148
+ }
4149
+ async function executeListErc20Offers(options) {
4150
+ const readOnlyOptions = parseReadOnlyOptions({
4151
+ chainId: options.chainId,
4152
+ rpcUrl: options.rpcUrl
4080
4153
  });
4081
- const createOfferCommand = new Command("create-offer").description("Create a collection offer (with --private-key: full flow; without: output EIP-712 data)").requiredOption("--nft-address <address>", "NFT contract address").requiredOption("--price <eth>", "Offer price in ETH (e.g., 0.1)").option("--offerer <address>", "Offerer address (required without --private-key)").option(...privateKeyOption).option(...chainIdOption).option(...rpcUrlOption).action(async (options) => {
4082
- await executeCreateOffer({
4083
- nftAddress: options.nftAddress,
4084
- price: options.price,
4085
- offerer: options.offerer,
4086
- privateKey: options.privateKey,
4087
- chainId: options.chainId,
4088
- rpcUrl: options.rpcUrl
4089
- });
4154
+ const bazaarClient = new BazaarClient({
4155
+ chainId: readOnlyOptions.chainId,
4156
+ rpcUrl: readOnlyOptions.rpcUrl
4090
4157
  });
4091
- const submitListingCommand = new Command("submit-listing").description("Submit a signed listing (follow-up to create-listing without --private-key)").requiredOption("--order-data <path>", "Path to order JSON file from create-listing output").requiredOption("--signature <sig>", "EIP-712 signature (0x-prefixed)").option(...privateKeyOption).option(...chainIdOption).option(...rpcUrlOption).option("--encode-only", "Output transaction data as JSON instead of executing").action(async (options) => {
4092
- await executeSubmitListing({
4093
- orderData: options.orderData,
4094
- signature: options.signature,
4095
- privateKey: options.privateKey,
4096
- chainId: options.chainId,
4097
- rpcUrl: options.rpcUrl,
4098
- encodeOnly: options.encodeOnly
4158
+ try {
4159
+ const offers = await bazaarClient.getErc20Offers({
4160
+ tokenAddress: options.tokenAddress
4099
4161
  });
4162
+ if (options.json) {
4163
+ const output = offers.map((o) => ({
4164
+ orderHash: o.orderHash,
4165
+ maker: o.maker,
4166
+ tokenAddress: o.tokenAddress,
4167
+ tokenAmount: o.tokenAmount.toString(),
4168
+ price: o.price,
4169
+ priceWei: o.priceWei.toString(),
4170
+ pricePerToken: o.pricePerToken,
4171
+ pricePerTokenWei: o.pricePerTokenWei.toString(),
4172
+ currency: o.currency,
4173
+ expirationDate: o.expirationDate,
4174
+ orderStatus: o.orderStatus
4175
+ }));
4176
+ console.log(JSON.stringify(output, null, 2));
4177
+ return;
4178
+ }
4179
+ if (offers.length === 0) {
4180
+ console.log(chalk4.yellow("No active ERC-20 offers found"));
4181
+ return;
4182
+ }
4183
+ console.log(chalk4.white.bold(`
4184
+ ERC-20 Offers (${offers.length}):
4185
+ `));
4186
+ for (const offer of offers) {
4187
+ const expiry = new Date(offer.expirationDate * 1e3).toLocaleString();
4188
+ console.log(` ${chalk4.cyan("Order Hash:")} ${offer.orderHash}`);
4189
+ console.log(` ${chalk4.cyan("Buyer:")} ${offer.maker}`);
4190
+ console.log(` ${chalk4.cyan("Token:")} ${offer.tokenAddress}`);
4191
+ console.log(` ${chalk4.cyan("Amount:")} ${offer.tokenAmount.toString()}`);
4192
+ console.log(` ${chalk4.cyan("Price:")} ${formatEthPrice(offer.price)} ${offer.currency.toUpperCase()}`);
4193
+ console.log(` ${chalk4.cyan("Price/Token:")} ${formatEthPrice(offer.pricePerToken)} ${offer.currency.toUpperCase()}`);
4194
+ console.log(` ${chalk4.cyan("Expires:")} ${expiry}`);
4195
+ console.log();
4196
+ }
4197
+ } catch (error) {
4198
+ exitWithError(
4199
+ `Failed to fetch ERC-20 offers: ${error instanceof Error ? error.message : String(error)}`
4200
+ );
4201
+ }
4202
+ }
4203
+ async function executeCreateErc20Listing(options) {
4204
+ const hasPrivateKey = !!(options.privateKey || process.env.NET_PRIVATE_KEY || process.env.PRIVATE_KEY);
4205
+ if (!hasPrivateKey) {
4206
+ await executeKeylessMode3(options);
4207
+ return;
4208
+ }
4209
+ const commonOptions = parseCommonOptions({
4210
+ privateKey: options.privateKey,
4211
+ chainId: options.chainId,
4212
+ rpcUrl: options.rpcUrl
4100
4213
  });
4101
- const submitOfferCommand = new Command("submit-offer").description("Submit a signed offer (follow-up to create-offer without --private-key)").requiredOption("--order-data <path>", "Path to order JSON file from create-offer output").requiredOption("--signature <sig>", "EIP-712 signature (0x-prefixed)").option(...privateKeyOption).option(...chainIdOption).option(...rpcUrlOption).option("--encode-only", "Output transaction data as JSON instead of executing").action(async (options) => {
4102
- await executeSubmitOffer({
4103
- orderData: options.orderData,
4104
- signature: options.signature,
4105
- privateKey: options.privateKey,
4106
- chainId: options.chainId,
4107
- rpcUrl: options.rpcUrl,
4108
- encodeOnly: options.encodeOnly
4109
- });
4214
+ const account = privateKeyToAccount(commonOptions.privateKey);
4215
+ const bazaarClient = new BazaarClient({
4216
+ chainId: commonOptions.chainId,
4217
+ rpcUrl: commonOptions.rpcUrl
4110
4218
  });
4111
- const buyListingCommand = new Command("buy-listing").description("Buy an NFT listing").requiredOption("--order-hash <hash>", "Order hash of the listing to buy").requiredOption("--nft-address <address>", "NFT contract address").option("--buyer <address>", "Buyer address (required with --encode-only)").option(...privateKeyOption).option(...chainIdOption).option(...rpcUrlOption).option("--encode-only", "Output transaction data as JSON instead of executing").action(async (options) => {
4112
- await executeBuyListing({
4113
- orderHash: options.orderHash,
4114
- nftAddress: options.nftAddress,
4115
- buyer: options.buyer,
4116
- privateKey: options.privateKey,
4117
- chainId: options.chainId,
4118
- rpcUrl: options.rpcUrl,
4119
- encodeOnly: options.encodeOnly
4219
+ const priceWei = parseEther(options.price);
4220
+ const tokenAmount = BigInt(options.tokenAmount);
4221
+ try {
4222
+ console.log(chalk4.blue("Preparing ERC-20 listing..."));
4223
+ const prepared = await bazaarClient.prepareCreateErc20Listing({
4224
+ tokenAddress: options.tokenAddress,
4225
+ tokenAmount,
4226
+ priceWei,
4227
+ offerer: account.address
4120
4228
  });
4121
- });
4122
- const acceptOfferCommand = new Command("accept-offer").description("Accept a collection offer by selling your NFT").requiredOption("--order-hash <hash>", "Order hash of the offer to accept").requiredOption("--nft-address <address>", "NFT contract address").requiredOption("--token-id <id>", "Token ID to sell").option("--seller <address>", "Seller address (required with --encode-only)").option(...privateKeyOption).option(...chainIdOption).option(...rpcUrlOption).option("--encode-only", "Output transaction data as JSON instead of executing").action(async (options) => {
4123
- await executeAcceptOffer({
4124
- orderHash: options.orderHash,
4125
- nftAddress: options.nftAddress,
4126
- tokenId: options.tokenId,
4127
- seller: options.seller,
4128
- privateKey: options.privateKey,
4129
- chainId: options.chainId,
4130
- rpcUrl: options.rpcUrl,
4131
- encodeOnly: options.encodeOnly
4229
+ const rpcUrls = getChainRpcUrls({
4230
+ chainId: commonOptions.chainId,
4231
+ rpcUrl: commonOptions.rpcUrl
4132
4232
  });
4133
- });
4134
- const ownedNftsCommand = new Command("owned-nfts").description("List NFTs owned by an address").requiredOption("--nft-address <address>", "NFT contract address").requiredOption("--owner <address>", "Owner address to check").option(...chainIdOption).option(...rpcUrlOption).option("--json", "Output in JSON format").option("--start-token-id <id>", "Start of token ID range (default: 0)").option("--end-token-id <id>", "End of token ID range (default: 10000)").action(async (options) => {
4135
- await executeOwnedNfts({
4136
- nftAddress: options.nftAddress,
4137
- owner: options.owner,
4138
- chainId: options.chainId,
4139
- rpcUrl: options.rpcUrl,
4140
- json: options.json,
4141
- startTokenId: options.startTokenId,
4142
- endTokenId: options.endTokenId
4233
+ const walletClient = createWalletClient({
4234
+ account,
4235
+ transport: http(rpcUrls[0])
4143
4236
  });
4144
- });
4145
- bazaarCommand.addCommand(listListingsCommand);
4146
- bazaarCommand.addCommand(listOffersCommand);
4147
- bazaarCommand.addCommand(listSalesCommand);
4148
- bazaarCommand.addCommand(createListingCommand);
4149
- bazaarCommand.addCommand(createOfferCommand);
4150
- bazaarCommand.addCommand(submitListingCommand);
4151
- bazaarCommand.addCommand(submitOfferCommand);
4152
- bazaarCommand.addCommand(buyListingCommand);
4153
- bazaarCommand.addCommand(acceptOfferCommand);
4154
- bazaarCommand.addCommand(ownedNftsCommand);
4237
+ for (const approval of prepared.approvals) {
4238
+ console.log(chalk4.blue("Sending approval transaction..."));
4239
+ const calldata = encodeFunctionData({
4240
+ abi: approval.abi,
4241
+ functionName: approval.functionName,
4242
+ args: approval.args
4243
+ });
4244
+ const approvalHash = await walletClient.sendTransaction({
4245
+ to: approval.to,
4246
+ data: calldata,
4247
+ chain: null,
4248
+ value: approval.value
4249
+ });
4250
+ console.log(chalk4.green(`Approval tx: ${approvalHash}`));
4251
+ }
4252
+ console.log(chalk4.blue("Signing order..."));
4253
+ const signature = await walletClient.signTypedData({
4254
+ domain: prepared.eip712.domain,
4255
+ types: prepared.eip712.types,
4256
+ primaryType: prepared.eip712.primaryType,
4257
+ message: prepared.eip712.message
4258
+ });
4259
+ console.log(chalk4.blue("Submitting ERC-20 listing..."));
4260
+ const submitTx = bazaarClient.prepareSubmitErc20Listing(
4261
+ prepared.eip712.orderParameters,
4262
+ prepared.eip712.counter,
4263
+ signature
4264
+ );
4265
+ const submitCalldata = encodeFunctionData({
4266
+ abi: submitTx.abi,
4267
+ functionName: submitTx.functionName,
4268
+ args: submitTx.args
4269
+ });
4270
+ const hash = await walletClient.sendTransaction({
4271
+ to: submitTx.to,
4272
+ data: submitCalldata,
4273
+ chain: null,
4274
+ value: submitTx.value
4275
+ });
4276
+ console.log(
4277
+ chalk4.green(
4278
+ `ERC-20 listing created successfully!
4279
+ Transaction: ${hash}
4280
+ Token: ${options.tokenAddress}
4281
+ Amount: ${options.tokenAmount}
4282
+ Price: ${options.price} ETH`
4283
+ )
4284
+ );
4285
+ } catch (error) {
4286
+ exitWithError(
4287
+ `Failed to create ERC-20 listing: ${error instanceof Error ? error.message : String(error)}`
4288
+ );
4289
+ }
4290
+ }
4291
+ async function executeKeylessMode3(options) {
4292
+ if (!options.offerer) {
4293
+ exitWithError("--offerer is required when not providing --private-key");
4294
+ }
4295
+ const readOnlyOptions = parseReadOnlyOptions({
4296
+ chainId: options.chainId,
4297
+ rpcUrl: options.rpcUrl
4298
+ });
4299
+ const bazaarClient = new BazaarClient({
4300
+ chainId: readOnlyOptions.chainId,
4301
+ rpcUrl: readOnlyOptions.rpcUrl
4302
+ });
4303
+ const priceWei = parseEther(options.price);
4304
+ const tokenAmount = BigInt(options.tokenAmount);
4305
+ try {
4306
+ const prepared = await bazaarClient.prepareCreateErc20Listing({
4307
+ tokenAddress: options.tokenAddress,
4308
+ tokenAmount,
4309
+ priceWei,
4310
+ offerer: options.offerer
4311
+ });
4312
+ const output = {
4313
+ eip712: {
4314
+ domain: prepared.eip712.domain,
4315
+ types: prepared.eip712.types,
4316
+ primaryType: prepared.eip712.primaryType,
4317
+ message: prepared.eip712.message
4318
+ },
4319
+ orderParameters: prepared.eip712.orderParameters,
4320
+ counter: prepared.eip712.counter.toString(),
4321
+ approvals: prepared.approvals.map((a) => ({
4322
+ to: a.to,
4323
+ data: encodeFunctionData({
4324
+ abi: a.abi,
4325
+ functionName: a.functionName,
4326
+ args: a.args
4327
+ }),
4328
+ description: `Approve ${a.functionName}`
4329
+ }))
4330
+ };
4331
+ console.log(JSON.stringify(output, bigintReplacer3, 2));
4332
+ } catch (error) {
4333
+ exitWithError(
4334
+ `Failed to prepare ERC-20 listing: ${error instanceof Error ? error.message : String(error)}`
4335
+ );
4336
+ }
4337
+ }
4338
+ function bigintReplacer3(_key, value) {
4339
+ if (typeof value === "bigint") {
4340
+ return value.toString();
4341
+ }
4342
+ return value;
4343
+ }
4344
+ async function executeCreateErc20Offer(options) {
4345
+ const hasPrivateKey = !!(options.privateKey || process.env.NET_PRIVATE_KEY || process.env.PRIVATE_KEY);
4346
+ if (!hasPrivateKey) {
4347
+ await executeKeylessMode4(options);
4348
+ return;
4349
+ }
4350
+ const commonOptions = parseCommonOptions({
4351
+ privateKey: options.privateKey,
4352
+ chainId: options.chainId,
4353
+ rpcUrl: options.rpcUrl
4354
+ });
4355
+ const account = privateKeyToAccount(commonOptions.privateKey);
4356
+ const bazaarClient = new BazaarClient({
4357
+ chainId: commonOptions.chainId,
4358
+ rpcUrl: commonOptions.rpcUrl
4359
+ });
4360
+ const priceWei = parseEther(options.price);
4361
+ const tokenAmount = BigInt(options.tokenAmount);
4362
+ try {
4363
+ console.log(chalk4.blue("Preparing ERC-20 offer..."));
4364
+ const prepared = await bazaarClient.prepareCreateErc20Offer({
4365
+ tokenAddress: options.tokenAddress,
4366
+ tokenAmount,
4367
+ priceWei,
4368
+ offerer: account.address
4369
+ });
4370
+ const rpcUrls = getChainRpcUrls({
4371
+ chainId: commonOptions.chainId,
4372
+ rpcUrl: commonOptions.rpcUrl
4373
+ });
4374
+ const walletClient = createWalletClient({
4375
+ account,
4376
+ transport: http(rpcUrls[0])
4377
+ });
4378
+ for (const approval of prepared.approvals) {
4379
+ console.log(chalk4.blue("Sending approval transaction..."));
4380
+ const calldata = encodeFunctionData({
4381
+ abi: approval.abi,
4382
+ functionName: approval.functionName,
4383
+ args: approval.args
4384
+ });
4385
+ const approvalHash = await walletClient.sendTransaction({
4386
+ to: approval.to,
4387
+ data: calldata,
4388
+ chain: null,
4389
+ value: approval.value
4390
+ });
4391
+ console.log(chalk4.green(`Approval tx: ${approvalHash}`));
4392
+ }
4393
+ console.log(chalk4.blue("Signing offer..."));
4394
+ const signature = await walletClient.signTypedData({
4395
+ domain: prepared.eip712.domain,
4396
+ types: prepared.eip712.types,
4397
+ primaryType: prepared.eip712.primaryType,
4398
+ message: prepared.eip712.message
4399
+ });
4400
+ console.log(chalk4.blue("Submitting ERC-20 offer..."));
4401
+ const submitTx = bazaarClient.prepareSubmitErc20Offer(
4402
+ prepared.eip712.orderParameters,
4403
+ prepared.eip712.counter,
4404
+ signature
4405
+ );
4406
+ const submitCalldata = encodeFunctionData({
4407
+ abi: submitTx.abi,
4408
+ functionName: submitTx.functionName,
4409
+ args: submitTx.args
4410
+ });
4411
+ const hash = await walletClient.sendTransaction({
4412
+ to: submitTx.to,
4413
+ data: submitCalldata,
4414
+ chain: null,
4415
+ value: submitTx.value
4416
+ });
4417
+ console.log(
4418
+ chalk4.green(
4419
+ `ERC-20 offer created successfully!
4420
+ Transaction: ${hash}
4421
+ Token: ${options.tokenAddress}
4422
+ Amount: ${options.tokenAmount}
4423
+ Price: ${options.price} ETH`
4424
+ )
4425
+ );
4426
+ } catch (error) {
4427
+ exitWithError(
4428
+ `Failed to create ERC-20 offer: ${error instanceof Error ? error.message : String(error)}`
4429
+ );
4430
+ }
4431
+ }
4432
+ async function executeKeylessMode4(options) {
4433
+ if (!options.offerer) {
4434
+ exitWithError("--offerer is required when not providing --private-key");
4435
+ }
4436
+ const readOnlyOptions = parseReadOnlyOptions({
4437
+ chainId: options.chainId,
4438
+ rpcUrl: options.rpcUrl
4439
+ });
4440
+ const bazaarClient = new BazaarClient({
4441
+ chainId: readOnlyOptions.chainId,
4442
+ rpcUrl: readOnlyOptions.rpcUrl
4443
+ });
4444
+ const priceWei = parseEther(options.price);
4445
+ const tokenAmount = BigInt(options.tokenAmount);
4446
+ try {
4447
+ const prepared = await bazaarClient.prepareCreateErc20Offer({
4448
+ tokenAddress: options.tokenAddress,
4449
+ tokenAmount,
4450
+ priceWei,
4451
+ offerer: options.offerer
4452
+ });
4453
+ const output = {
4454
+ eip712: {
4455
+ domain: prepared.eip712.domain,
4456
+ types: prepared.eip712.types,
4457
+ primaryType: prepared.eip712.primaryType,
4458
+ message: prepared.eip712.message
4459
+ },
4460
+ orderParameters: prepared.eip712.orderParameters,
4461
+ counter: prepared.eip712.counter.toString(),
4462
+ approvals: prepared.approvals.map((a) => ({
4463
+ to: a.to,
4464
+ data: encodeFunctionData({
4465
+ abi: a.abi,
4466
+ functionName: a.functionName,
4467
+ args: a.args
4468
+ }),
4469
+ description: `Approve ${a.functionName}`
4470
+ }))
4471
+ };
4472
+ console.log(JSON.stringify(output, bigintReplacer4, 2));
4473
+ } catch (error) {
4474
+ exitWithError(
4475
+ `Failed to prepare ERC-20 offer: ${error instanceof Error ? error.message : String(error)}`
4476
+ );
4477
+ }
4478
+ }
4479
+ function bigintReplacer4(_key, value) {
4480
+ if (typeof value === "bigint") {
4481
+ return value.toString();
4482
+ }
4483
+ return value;
4484
+ }
4485
+ async function executeSubmitErc20Listing(options) {
4486
+ let orderData;
4487
+ try {
4488
+ const raw = readFileSync(options.orderData, "utf-8");
4489
+ orderData = JSON.parse(raw);
4490
+ } catch (error) {
4491
+ exitWithError(
4492
+ `Failed to read order data file: ${error instanceof Error ? error.message : String(error)}`
4493
+ );
4494
+ }
4495
+ if (options.encodeOnly) {
4496
+ await executeEncodeOnly7(options, orderData);
4497
+ return;
4498
+ }
4499
+ const commonOptions = parseCommonOptions(
4500
+ {
4501
+ privateKey: options.privateKey,
4502
+ chainId: options.chainId,
4503
+ rpcUrl: options.rpcUrl
4504
+ },
4505
+ true
4506
+ );
4507
+ const account = privateKeyToAccount(commonOptions.privateKey);
4508
+ const bazaarClient = new BazaarClient({
4509
+ chainId: commonOptions.chainId,
4510
+ rpcUrl: commonOptions.rpcUrl
4511
+ });
4512
+ try {
4513
+ const submitTx = bazaarClient.prepareSubmitErc20Listing(
4514
+ orderData.orderParameters,
4515
+ BigInt(orderData.counter),
4516
+ options.signature
4517
+ );
4518
+ const rpcUrls = getChainRpcUrls({
4519
+ chainId: commonOptions.chainId,
4520
+ rpcUrl: commonOptions.rpcUrl
4521
+ });
4522
+ const walletClient = createWalletClient({
4523
+ account,
4524
+ transport: http(rpcUrls[0])
4525
+ });
4526
+ console.log(chalk4.blue("Submitting ERC-20 listing..."));
4527
+ const calldata = encodeFunctionData({
4528
+ abi: submitTx.abi,
4529
+ functionName: submitTx.functionName,
4530
+ args: submitTx.args
4531
+ });
4532
+ const hash = await walletClient.sendTransaction({
4533
+ to: submitTx.to,
4534
+ data: calldata,
4535
+ chain: null,
4536
+ value: submitTx.value
4537
+ });
4538
+ console.log(chalk4.green(`ERC-20 listing submitted successfully!
4539
+ Transaction: ${hash}`));
4540
+ } catch (error) {
4541
+ exitWithError(
4542
+ `Failed to submit ERC-20 listing: ${error instanceof Error ? error.message : String(error)}`
4543
+ );
4544
+ }
4545
+ }
4546
+ async function executeEncodeOnly7(options, orderData) {
4547
+ const readOnlyOptions = parseReadOnlyOptions({
4548
+ chainId: options.chainId,
4549
+ rpcUrl: options.rpcUrl
4550
+ });
4551
+ const bazaarClient = new BazaarClient({
4552
+ chainId: readOnlyOptions.chainId,
4553
+ rpcUrl: readOnlyOptions.rpcUrl
4554
+ });
4555
+ const submitTx = bazaarClient.prepareSubmitErc20Listing(
4556
+ orderData.orderParameters,
4557
+ BigInt(orderData.counter),
4558
+ options.signature
4559
+ );
4560
+ const encoded = encodeTransaction(
4561
+ { to: submitTx.to, functionName: submitTx.functionName, args: submitTx.args, abi: submitTx.abi, value: submitTx.value },
4562
+ readOnlyOptions.chainId
4563
+ );
4564
+ console.log(JSON.stringify(encoded, null, 2));
4565
+ }
4566
+ async function executeSubmitErc20Offer(options) {
4567
+ let orderData;
4568
+ try {
4569
+ const raw = readFileSync(options.orderData, "utf-8");
4570
+ orderData = JSON.parse(raw);
4571
+ } catch (error) {
4572
+ exitWithError(
4573
+ `Failed to read order data file: ${error instanceof Error ? error.message : String(error)}`
4574
+ );
4575
+ }
4576
+ if (options.encodeOnly) {
4577
+ await executeEncodeOnly8(options, orderData);
4578
+ return;
4579
+ }
4580
+ const commonOptions = parseCommonOptions(
4581
+ {
4582
+ privateKey: options.privateKey,
4583
+ chainId: options.chainId,
4584
+ rpcUrl: options.rpcUrl
4585
+ },
4586
+ true
4587
+ );
4588
+ const account = privateKeyToAccount(commonOptions.privateKey);
4589
+ const bazaarClient = new BazaarClient({
4590
+ chainId: commonOptions.chainId,
4591
+ rpcUrl: commonOptions.rpcUrl
4592
+ });
4593
+ try {
4594
+ const submitTx = bazaarClient.prepareSubmitErc20Offer(
4595
+ orderData.orderParameters,
4596
+ BigInt(orderData.counter),
4597
+ options.signature
4598
+ );
4599
+ const rpcUrls = getChainRpcUrls({
4600
+ chainId: commonOptions.chainId,
4601
+ rpcUrl: commonOptions.rpcUrl
4602
+ });
4603
+ const walletClient = createWalletClient({
4604
+ account,
4605
+ transport: http(rpcUrls[0])
4606
+ });
4607
+ console.log(chalk4.blue("Submitting ERC-20 offer..."));
4608
+ const calldata = encodeFunctionData({
4609
+ abi: submitTx.abi,
4610
+ functionName: submitTx.functionName,
4611
+ args: submitTx.args
4612
+ });
4613
+ const hash = await walletClient.sendTransaction({
4614
+ to: submitTx.to,
4615
+ data: calldata,
4616
+ chain: null,
4617
+ value: submitTx.value
4618
+ });
4619
+ console.log(chalk4.green(`ERC-20 offer submitted successfully!
4620
+ Transaction: ${hash}`));
4621
+ } catch (error) {
4622
+ exitWithError(
4623
+ `Failed to submit ERC-20 offer: ${error instanceof Error ? error.message : String(error)}`
4624
+ );
4625
+ }
4626
+ }
4627
+ async function executeEncodeOnly8(options, orderData) {
4628
+ const readOnlyOptions = parseReadOnlyOptions({
4629
+ chainId: options.chainId,
4630
+ rpcUrl: options.rpcUrl
4631
+ });
4632
+ const bazaarClient = new BazaarClient({
4633
+ chainId: readOnlyOptions.chainId,
4634
+ rpcUrl: readOnlyOptions.rpcUrl
4635
+ });
4636
+ const submitTx = bazaarClient.prepareSubmitErc20Offer(
4637
+ orderData.orderParameters,
4638
+ BigInt(orderData.counter),
4639
+ options.signature
4640
+ );
4641
+ const encoded = encodeTransaction(
4642
+ { to: submitTx.to, functionName: submitTx.functionName, args: submitTx.args, abi: submitTx.abi, value: submitTx.value },
4643
+ readOnlyOptions.chainId
4644
+ );
4645
+ console.log(JSON.stringify(encoded, null, 2));
4646
+ }
4647
+ async function executeBuyErc20Listing(options) {
4648
+ if (options.encodeOnly) {
4649
+ await executeEncodeOnly9(options);
4650
+ return;
4651
+ }
4652
+ const commonOptions = parseCommonOptions(
4653
+ {
4654
+ privateKey: options.privateKey,
4655
+ chainId: options.chainId,
4656
+ rpcUrl: options.rpcUrl
4657
+ },
4658
+ true
4659
+ );
4660
+ const account = privateKeyToAccount(commonOptions.privateKey);
4661
+ const bazaarClient = new BazaarClient({
4662
+ chainId: commonOptions.chainId,
4663
+ rpcUrl: commonOptions.rpcUrl
4664
+ });
4665
+ try {
4666
+ console.log(chalk4.blue("Fetching ERC-20 listing..."));
4667
+ const listings = await bazaarClient.getErc20Listings({
4668
+ tokenAddress: options.tokenAddress
4669
+ });
4670
+ const listing = listings.find(
4671
+ (l) => l.orderHash.toLowerCase() === options.orderHash.toLowerCase()
4672
+ );
4673
+ if (!listing) {
4674
+ exitWithError(`ERC-20 listing with order hash ${options.orderHash} not found or no longer active`);
4675
+ }
4676
+ console.log(chalk4.blue("Preparing fulfillment..."));
4677
+ const prepared = await bazaarClient.prepareFulfillErc20Listing(listing, account.address);
4678
+ const rpcUrls = getChainRpcUrls({
4679
+ chainId: commonOptions.chainId,
4680
+ rpcUrl: commonOptions.rpcUrl
4681
+ });
4682
+ const walletClient = createWalletClient({
4683
+ account,
4684
+ transport: http(rpcUrls[0])
4685
+ });
4686
+ for (const approval of prepared.approvals) {
4687
+ console.log(chalk4.blue("Sending approval transaction..."));
4688
+ const calldata2 = encodeFunctionData({
4689
+ abi: approval.abi,
4690
+ functionName: approval.functionName,
4691
+ args: approval.args
4692
+ });
4693
+ const approvalHash = await walletClient.sendTransaction({
4694
+ to: approval.to,
4695
+ data: calldata2,
4696
+ chain: null,
4697
+ value: approval.value
4698
+ });
4699
+ console.log(chalk4.green(`Approval tx: ${approvalHash}`));
4700
+ }
4701
+ console.log(chalk4.blue("Sending fulfillment transaction..."));
4702
+ const calldata = encodeFunctionData({
4703
+ abi: prepared.fulfillment.abi,
4704
+ functionName: prepared.fulfillment.functionName,
4705
+ args: prepared.fulfillment.args
4706
+ });
4707
+ const hash = await walletClient.sendTransaction({
4708
+ to: prepared.fulfillment.to,
4709
+ data: calldata,
4710
+ chain: null,
4711
+ value: prepared.fulfillment.value
4712
+ });
4713
+ console.log(
4714
+ chalk4.green(
4715
+ `ERC-20 listing fulfilled successfully!
4716
+ Transaction: ${hash}
4717
+ Token: ${listing.tokenAddress}
4718
+ Amount: ${listing.tokenAmount.toString()}
4719
+ Price: ${listing.price} ${listing.currency.toUpperCase()}`
4720
+ )
4721
+ );
4722
+ } catch (error) {
4723
+ exitWithError(
4724
+ `Failed to buy ERC-20 listing: ${error instanceof Error ? error.message : String(error)}`
4725
+ );
4726
+ }
4727
+ }
4728
+ async function executeEncodeOnly9(options) {
4729
+ if (!options.buyer) {
4730
+ exitWithError("--buyer is required when using --encode-only without --private-key");
4731
+ }
4732
+ const readOnlyOptions = parseReadOnlyOptions({
4733
+ chainId: options.chainId,
4734
+ rpcUrl: options.rpcUrl
4735
+ });
4736
+ const buyerAddress = options.buyer;
4737
+ const bazaarClient = new BazaarClient({
4738
+ chainId: readOnlyOptions.chainId,
4739
+ rpcUrl: readOnlyOptions.rpcUrl
4740
+ });
4741
+ try {
4742
+ const listings = await bazaarClient.getErc20Listings({
4743
+ tokenAddress: options.tokenAddress
4744
+ });
4745
+ const listing = listings.find(
4746
+ (l) => l.orderHash.toLowerCase() === options.orderHash.toLowerCase()
4747
+ );
4748
+ if (!listing) {
4749
+ exitWithError(`ERC-20 listing with order hash ${options.orderHash} not found or no longer active`);
4750
+ }
4751
+ const prepared = await bazaarClient.prepareFulfillErc20Listing(listing, buyerAddress);
4752
+ const result = {
4753
+ approvals: prepared.approvals.map(
4754
+ (a) => encodeTransaction(
4755
+ { to: a.to, functionName: a.functionName, args: a.args, abi: a.abi, value: a.value },
4756
+ readOnlyOptions.chainId
4757
+ )
4758
+ ),
4759
+ fulfillment: encodeTransaction(
4760
+ {
4761
+ to: prepared.fulfillment.to,
4762
+ functionName: prepared.fulfillment.functionName,
4763
+ args: prepared.fulfillment.args,
4764
+ abi: prepared.fulfillment.abi,
4765
+ value: prepared.fulfillment.value
4766
+ },
4767
+ readOnlyOptions.chainId
4768
+ )
4769
+ };
4770
+ console.log(JSON.stringify(result, null, 2));
4771
+ } catch (error) {
4772
+ exitWithError(
4773
+ `Failed to encode buy ERC-20 listing: ${error instanceof Error ? error.message : String(error)}`
4774
+ );
4775
+ }
4776
+ }
4777
+ async function executeAcceptErc20Offer(options) {
4778
+ if (options.encodeOnly) {
4779
+ await executeEncodeOnly10(options);
4780
+ return;
4781
+ }
4782
+ const commonOptions = parseCommonOptions(
4783
+ {
4784
+ privateKey: options.privateKey,
4785
+ chainId: options.chainId,
4786
+ rpcUrl: options.rpcUrl
4787
+ },
4788
+ true
4789
+ );
4790
+ const account = privateKeyToAccount(commonOptions.privateKey);
4791
+ const bazaarClient = new BazaarClient({
4792
+ chainId: commonOptions.chainId,
4793
+ rpcUrl: commonOptions.rpcUrl
4794
+ });
4795
+ try {
4796
+ console.log(chalk4.blue("Fetching ERC-20 offers..."));
4797
+ const offers = await bazaarClient.getErc20Offers({
4798
+ tokenAddress: options.tokenAddress
4799
+ });
4800
+ const offer = offers.find(
4801
+ (o) => o.orderHash.toLowerCase() === options.orderHash.toLowerCase()
4802
+ );
4803
+ if (!offer) {
4804
+ exitWithError(`ERC-20 offer with order hash ${options.orderHash} not found or no longer active`);
4805
+ }
4806
+ console.log(chalk4.blue("Preparing fulfillment..."));
4807
+ const prepared = await bazaarClient.prepareFulfillErc20Offer(
4808
+ offer,
4809
+ account.address
4810
+ );
4811
+ const rpcUrls = getChainRpcUrls({
4812
+ chainId: commonOptions.chainId,
4813
+ rpcUrl: commonOptions.rpcUrl
4814
+ });
4815
+ const walletClient = createWalletClient({
4816
+ account,
4817
+ transport: http(rpcUrls[0])
4818
+ });
4819
+ for (const approval of prepared.approvals) {
4820
+ console.log(chalk4.blue("Sending approval transaction..."));
4821
+ const calldata2 = encodeFunctionData({
4822
+ abi: approval.abi,
4823
+ functionName: approval.functionName,
4824
+ args: approval.args
4825
+ });
4826
+ const approvalHash = await walletClient.sendTransaction({
4827
+ to: approval.to,
4828
+ data: calldata2,
4829
+ chain: null,
4830
+ value: approval.value
4831
+ });
4832
+ console.log(chalk4.green(`Approval tx: ${approvalHash}`));
4833
+ }
4834
+ console.log(chalk4.blue("Sending fulfillment transaction..."));
4835
+ const calldata = encodeFunctionData({
4836
+ abi: prepared.fulfillment.abi,
4837
+ functionName: prepared.fulfillment.functionName,
4838
+ args: prepared.fulfillment.args
4839
+ });
4840
+ const hash = await walletClient.sendTransaction({
4841
+ to: prepared.fulfillment.to,
4842
+ data: calldata,
4843
+ chain: null,
4844
+ value: prepared.fulfillment.value
4845
+ });
4846
+ console.log(
4847
+ chalk4.green(
4848
+ `ERC-20 offer accepted successfully!
4849
+ Transaction: ${hash}
4850
+ Token: ${offer.tokenAddress}
4851
+ Amount: ${offer.tokenAmount.toString()}
4852
+ Price: ${offer.price} ${offer.currency.toUpperCase()}`
4853
+ )
4854
+ );
4855
+ } catch (error) {
4856
+ exitWithError(
4857
+ `Failed to accept ERC-20 offer: ${error instanceof Error ? error.message : String(error)}`
4858
+ );
4859
+ }
4860
+ }
4861
+ async function executeEncodeOnly10(options) {
4862
+ if (!options.seller) {
4863
+ exitWithError("--seller is required when using --encode-only without --private-key");
4864
+ }
4865
+ const readOnlyOptions = parseReadOnlyOptions({
4866
+ chainId: options.chainId,
4867
+ rpcUrl: options.rpcUrl
4868
+ });
4869
+ const sellerAddress = options.seller;
4870
+ const bazaarClient = new BazaarClient({
4871
+ chainId: readOnlyOptions.chainId,
4872
+ rpcUrl: readOnlyOptions.rpcUrl
4873
+ });
4874
+ try {
4875
+ const offers = await bazaarClient.getErc20Offers({
4876
+ tokenAddress: options.tokenAddress
4877
+ });
4878
+ const offer = offers.find(
4879
+ (o) => o.orderHash.toLowerCase() === options.orderHash.toLowerCase()
4880
+ );
4881
+ if (!offer) {
4882
+ exitWithError(`ERC-20 offer with order hash ${options.orderHash} not found or no longer active`);
4883
+ }
4884
+ const prepared = await bazaarClient.prepareFulfillErc20Offer(
4885
+ offer,
4886
+ sellerAddress
4887
+ );
4888
+ const result = {
4889
+ approvals: prepared.approvals.map(
4890
+ (a) => encodeTransaction(
4891
+ { to: a.to, functionName: a.functionName, args: a.args, abi: a.abi, value: a.value },
4892
+ readOnlyOptions.chainId
4893
+ )
4894
+ ),
4895
+ fulfillment: encodeTransaction(
4896
+ {
4897
+ to: prepared.fulfillment.to,
4898
+ functionName: prepared.fulfillment.functionName,
4899
+ args: prepared.fulfillment.args,
4900
+ abi: prepared.fulfillment.abi,
4901
+ value: prepared.fulfillment.value
4902
+ },
4903
+ readOnlyOptions.chainId
4904
+ )
4905
+ };
4906
+ console.log(JSON.stringify(result, null, 2));
4907
+ } catch (error) {
4908
+ exitWithError(
4909
+ `Failed to encode accept ERC-20 offer: ${error instanceof Error ? error.message : String(error)}`
4910
+ );
4911
+ }
4912
+ }
4913
+
4914
+ // src/commands/bazaar/index.ts
4915
+ var chainIdOption = [
4916
+ "--chain-id <id>",
4917
+ "Chain ID. Can also be set via NET_CHAIN_ID env var",
4918
+ (value) => parseInt(value, 10)
4919
+ ];
4920
+ var rpcUrlOption = [
4921
+ "--rpc-url <url>",
4922
+ "Custom RPC URL. Can also be set via NET_RPC_URL env var"
4923
+ ];
4924
+ var privateKeyOption = [
4925
+ "--private-key <key>",
4926
+ "Private key (0x-prefixed hex). Can also be set via NET_PRIVATE_KEY env var"
4927
+ ];
4928
+ function registerBazaarCommand(program2) {
4929
+ const bazaarCommand = program2.command("bazaar").description("Bazaar NFT trading operations");
4930
+ const listListingsCommand = new Command("list-listings").description("List active NFT listings").option("--nft-address <address>", "NFT contract address (optional for cross-collection)").option(...chainIdOption).option(...rpcUrlOption).option("--json", "Output in JSON format").action(async (options) => {
4931
+ await executeListListings({
4932
+ nftAddress: options.nftAddress,
4933
+ chainId: options.chainId,
4934
+ rpcUrl: options.rpcUrl,
4935
+ json: options.json
4936
+ });
4937
+ });
4938
+ const listOffersCommand = new Command("list-offers").description("List active collection offers").requiredOption("--nft-address <address>", "NFT contract address").option(...chainIdOption).option(...rpcUrlOption).option("--json", "Output in JSON format").action(async (options) => {
4939
+ await executeListOffers({
4940
+ nftAddress: options.nftAddress,
4941
+ chainId: options.chainId,
4942
+ rpcUrl: options.rpcUrl,
4943
+ json: options.json
4944
+ });
4945
+ });
4946
+ const listSalesCommand = new Command("list-sales").description("List recent sales").requiredOption("--nft-address <address>", "NFT contract address").option(...chainIdOption).option(...rpcUrlOption).option("--json", "Output in JSON format").action(async (options) => {
4947
+ await executeListSales({
4948
+ nftAddress: options.nftAddress,
4949
+ chainId: options.chainId,
4950
+ rpcUrl: options.rpcUrl,
4951
+ json: options.json
4952
+ });
4953
+ });
4954
+ const createListingCommand = new Command("create-listing").description("Create an NFT listing (with --private-key: full flow; without: output EIP-712 data)").requiredOption("--nft-address <address>", "NFT contract address").requiredOption("--token-id <id>", "Token ID to list").requiredOption("--price <eth>", "Price in ETH (e.g., 0.1)").option("--target-fulfiller <address>", "Make a private listing for this address").option("--offerer <address>", "Offerer address (required without --private-key)").option(...privateKeyOption).option(...chainIdOption).option(...rpcUrlOption).action(async (options) => {
4955
+ await executeCreateListing({
4956
+ nftAddress: options.nftAddress,
4957
+ tokenId: options.tokenId,
4958
+ price: options.price,
4959
+ targetFulfiller: options.targetFulfiller,
4960
+ offerer: options.offerer,
4961
+ privateKey: options.privateKey,
4962
+ chainId: options.chainId,
4963
+ rpcUrl: options.rpcUrl
4964
+ });
4965
+ });
4966
+ const createOfferCommand = new Command("create-offer").description("Create a collection offer (with --private-key: full flow; without: output EIP-712 data)").requiredOption("--nft-address <address>", "NFT contract address").requiredOption("--price <eth>", "Offer price in ETH (e.g., 0.1)").option("--offerer <address>", "Offerer address (required without --private-key)").option(...privateKeyOption).option(...chainIdOption).option(...rpcUrlOption).action(async (options) => {
4967
+ await executeCreateOffer({
4968
+ nftAddress: options.nftAddress,
4969
+ price: options.price,
4970
+ offerer: options.offerer,
4971
+ privateKey: options.privateKey,
4972
+ chainId: options.chainId,
4973
+ rpcUrl: options.rpcUrl
4974
+ });
4975
+ });
4976
+ const submitListingCommand = new Command("submit-listing").description("Submit a signed listing (follow-up to create-listing without --private-key)").requiredOption("--order-data <path>", "Path to order JSON file from create-listing output").requiredOption("--signature <sig>", "EIP-712 signature (0x-prefixed)").option(...privateKeyOption).option(...chainIdOption).option(...rpcUrlOption).option("--encode-only", "Output transaction data as JSON instead of executing").action(async (options) => {
4977
+ await executeSubmitListing({
4978
+ orderData: options.orderData,
4979
+ signature: options.signature,
4980
+ privateKey: options.privateKey,
4981
+ chainId: options.chainId,
4982
+ rpcUrl: options.rpcUrl,
4983
+ encodeOnly: options.encodeOnly
4984
+ });
4985
+ });
4986
+ const submitOfferCommand = new Command("submit-offer").description("Submit a signed offer (follow-up to create-offer without --private-key)").requiredOption("--order-data <path>", "Path to order JSON file from create-offer output").requiredOption("--signature <sig>", "EIP-712 signature (0x-prefixed)").option(...privateKeyOption).option(...chainIdOption).option(...rpcUrlOption).option("--encode-only", "Output transaction data as JSON instead of executing").action(async (options) => {
4987
+ await executeSubmitOffer({
4988
+ orderData: options.orderData,
4989
+ signature: options.signature,
4990
+ privateKey: options.privateKey,
4991
+ chainId: options.chainId,
4992
+ rpcUrl: options.rpcUrl,
4993
+ encodeOnly: options.encodeOnly
4994
+ });
4995
+ });
4996
+ const buyListingCommand = new Command("buy-listing").description("Buy an NFT listing").requiredOption("--order-hash <hash>", "Order hash of the listing to buy").requiredOption("--nft-address <address>", "NFT contract address").option("--buyer <address>", "Buyer address (required with --encode-only)").option(...privateKeyOption).option(...chainIdOption).option(...rpcUrlOption).option("--encode-only", "Output transaction data as JSON instead of executing").action(async (options) => {
4997
+ await executeBuyListing({
4998
+ orderHash: options.orderHash,
4999
+ nftAddress: options.nftAddress,
5000
+ buyer: options.buyer,
5001
+ privateKey: options.privateKey,
5002
+ chainId: options.chainId,
5003
+ rpcUrl: options.rpcUrl,
5004
+ encodeOnly: options.encodeOnly
5005
+ });
5006
+ });
5007
+ const acceptOfferCommand = new Command("accept-offer").description("Accept a collection offer by selling your NFT").requiredOption("--order-hash <hash>", "Order hash of the offer to accept").requiredOption("--nft-address <address>", "NFT contract address").requiredOption("--token-id <id>", "Token ID to sell").option("--seller <address>", "Seller address (required with --encode-only)").option(...privateKeyOption).option(...chainIdOption).option(...rpcUrlOption).option("--encode-only", "Output transaction data as JSON instead of executing").action(async (options) => {
5008
+ await executeAcceptOffer({
5009
+ orderHash: options.orderHash,
5010
+ nftAddress: options.nftAddress,
5011
+ tokenId: options.tokenId,
5012
+ seller: options.seller,
5013
+ privateKey: options.privateKey,
5014
+ chainId: options.chainId,
5015
+ rpcUrl: options.rpcUrl,
5016
+ encodeOnly: options.encodeOnly
5017
+ });
5018
+ });
5019
+ const ownedNftsCommand = new Command("owned-nfts").description("List NFTs owned by an address").requiredOption("--nft-address <address>", "NFT contract address").requiredOption("--owner <address>", "Owner address to check").option(...chainIdOption).option(...rpcUrlOption).option("--json", "Output in JSON format").option("--start-token-id <id>", "Start of token ID range (default: 0)").option("--end-token-id <id>", "End of token ID range (default: 10000)").action(async (options) => {
5020
+ await executeOwnedNfts({
5021
+ nftAddress: options.nftAddress,
5022
+ owner: options.owner,
5023
+ chainId: options.chainId,
5024
+ rpcUrl: options.rpcUrl,
5025
+ json: options.json,
5026
+ startTokenId: options.startTokenId,
5027
+ endTokenId: options.endTokenId
5028
+ });
5029
+ });
5030
+ const listErc20ListingsCommand = new Command("list-erc20-listings").description("List active ERC-20 token listings").requiredOption("--token-address <address>", "ERC-20 token contract address").option(...chainIdOption).option(...rpcUrlOption).option("--json", "Output in JSON format").action(async (options) => {
5031
+ await executeListErc20Listings({
5032
+ tokenAddress: options.tokenAddress,
5033
+ chainId: options.chainId,
5034
+ rpcUrl: options.rpcUrl,
5035
+ json: options.json
5036
+ });
5037
+ });
5038
+ const listErc20OffersCommand = new Command("list-erc20-offers").description("List active ERC-20 token offers").requiredOption("--token-address <address>", "ERC-20 token contract address").option(...chainIdOption).option(...rpcUrlOption).option("--json", "Output in JSON format").action(async (options) => {
5039
+ await executeListErc20Offers({
5040
+ tokenAddress: options.tokenAddress,
5041
+ chainId: options.chainId,
5042
+ rpcUrl: options.rpcUrl,
5043
+ json: options.json
5044
+ });
5045
+ });
5046
+ const createErc20ListingCommand = new Command("create-erc20-listing").description("Create an ERC-20 token listing (with --private-key: full flow; without: output EIP-712 data)").requiredOption("--token-address <address>", "ERC-20 token contract address").requiredOption("--token-amount <amount>", "Token amount in raw units (bigint string)").requiredOption("--price <eth>", "Total price in ETH for the token amount").option("--offerer <address>", "Offerer address (required without --private-key)").option(...privateKeyOption).option(...chainIdOption).option(...rpcUrlOption).action(async (options) => {
5047
+ await executeCreateErc20Listing({
5048
+ tokenAddress: options.tokenAddress,
5049
+ tokenAmount: options.tokenAmount,
5050
+ price: options.price,
5051
+ offerer: options.offerer,
5052
+ privateKey: options.privateKey,
5053
+ chainId: options.chainId,
5054
+ rpcUrl: options.rpcUrl
5055
+ });
5056
+ });
5057
+ const createErc20OfferCommand = new Command("create-erc20-offer").description("Create an ERC-20 token offer (with --private-key: full flow; without: output EIP-712 data)").requiredOption("--token-address <address>", "ERC-20 token contract address").requiredOption("--token-amount <amount>", "Token amount in raw units (bigint string)").requiredOption("--price <eth>", "Total price in ETH for the token amount").option("--offerer <address>", "Offerer address (required without --private-key)").option(...privateKeyOption).option(...chainIdOption).option(...rpcUrlOption).action(async (options) => {
5058
+ await executeCreateErc20Offer({
5059
+ tokenAddress: options.tokenAddress,
5060
+ tokenAmount: options.tokenAmount,
5061
+ price: options.price,
5062
+ offerer: options.offerer,
5063
+ privateKey: options.privateKey,
5064
+ chainId: options.chainId,
5065
+ rpcUrl: options.rpcUrl
5066
+ });
5067
+ });
5068
+ const submitErc20ListingCommand = new Command("submit-erc20-listing").description("Submit a signed ERC-20 listing (follow-up to create-erc20-listing without --private-key)").requiredOption("--order-data <path>", "Path to order JSON file from create-erc20-listing output").requiredOption("--signature <sig>", "EIP-712 signature (0x-prefixed)").option(...privateKeyOption).option(...chainIdOption).option(...rpcUrlOption).option("--encode-only", "Output transaction data as JSON instead of executing").action(async (options) => {
5069
+ await executeSubmitErc20Listing({
5070
+ orderData: options.orderData,
5071
+ signature: options.signature,
5072
+ privateKey: options.privateKey,
5073
+ chainId: options.chainId,
5074
+ rpcUrl: options.rpcUrl,
5075
+ encodeOnly: options.encodeOnly
5076
+ });
5077
+ });
5078
+ const submitErc20OfferCommand = new Command("submit-erc20-offer").description("Submit a signed ERC-20 offer (follow-up to create-erc20-offer without --private-key)").requiredOption("--order-data <path>", "Path to order JSON file from create-erc20-offer output").requiredOption("--signature <sig>", "EIP-712 signature (0x-prefixed)").option(...privateKeyOption).option(...chainIdOption).option(...rpcUrlOption).option("--encode-only", "Output transaction data as JSON instead of executing").action(async (options) => {
5079
+ await executeSubmitErc20Offer({
5080
+ orderData: options.orderData,
5081
+ signature: options.signature,
5082
+ privateKey: options.privateKey,
5083
+ chainId: options.chainId,
5084
+ rpcUrl: options.rpcUrl,
5085
+ encodeOnly: options.encodeOnly
5086
+ });
5087
+ });
5088
+ const buyErc20ListingCommand = new Command("buy-erc20-listing").description("Buy an ERC-20 token listing").requiredOption("--order-hash <hash>", "Order hash of the listing to buy").requiredOption("--token-address <address>", "ERC-20 token contract address").option("--buyer <address>", "Buyer address (required with --encode-only)").option(...privateKeyOption).option(...chainIdOption).option(...rpcUrlOption).option("--encode-only", "Output transaction data as JSON instead of executing").action(async (options) => {
5089
+ await executeBuyErc20Listing({
5090
+ orderHash: options.orderHash,
5091
+ tokenAddress: options.tokenAddress,
5092
+ buyer: options.buyer,
5093
+ privateKey: options.privateKey,
5094
+ chainId: options.chainId,
5095
+ rpcUrl: options.rpcUrl,
5096
+ encodeOnly: options.encodeOnly
5097
+ });
5098
+ });
5099
+ const acceptErc20OfferCommand = new Command("accept-erc20-offer").description("Accept an ERC-20 token offer by selling your tokens").requiredOption("--order-hash <hash>", "Order hash of the offer to accept").requiredOption("--token-address <address>", "ERC-20 token contract address").option("--seller <address>", "Seller address (required with --encode-only)").option(...privateKeyOption).option(...chainIdOption).option(...rpcUrlOption).option("--encode-only", "Output transaction data as JSON instead of executing").action(async (options) => {
5100
+ await executeAcceptErc20Offer({
5101
+ orderHash: options.orderHash,
5102
+ tokenAddress: options.tokenAddress,
5103
+ seller: options.seller,
5104
+ privateKey: options.privateKey,
5105
+ chainId: options.chainId,
5106
+ rpcUrl: options.rpcUrl,
5107
+ encodeOnly: options.encodeOnly
5108
+ });
5109
+ });
5110
+ bazaarCommand.addCommand(listListingsCommand);
5111
+ bazaarCommand.addCommand(listOffersCommand);
5112
+ bazaarCommand.addCommand(listSalesCommand);
5113
+ bazaarCommand.addCommand(createListingCommand);
5114
+ bazaarCommand.addCommand(createOfferCommand);
5115
+ bazaarCommand.addCommand(submitListingCommand);
5116
+ bazaarCommand.addCommand(submitOfferCommand);
5117
+ bazaarCommand.addCommand(buyListingCommand);
5118
+ bazaarCommand.addCommand(acceptOfferCommand);
5119
+ bazaarCommand.addCommand(ownedNftsCommand);
5120
+ bazaarCommand.addCommand(listErc20ListingsCommand);
5121
+ bazaarCommand.addCommand(listErc20OffersCommand);
5122
+ bazaarCommand.addCommand(createErc20ListingCommand);
5123
+ bazaarCommand.addCommand(createErc20OfferCommand);
5124
+ bazaarCommand.addCommand(submitErc20ListingCommand);
5125
+ bazaarCommand.addCommand(submitErc20OfferCommand);
5126
+ bazaarCommand.addCommand(buyErc20ListingCommand);
5127
+ bazaarCommand.addCommand(acceptErc20OfferCommand);
5128
+ }
5129
+ function truncateAddress(address) {
5130
+ if (address.length <= 12) return address;
5131
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
5132
+ }
5133
+ function formatTimestamp(timestamp) {
5134
+ return new Date(Number(timestamp) * 1e3).toISOString().replace("T", " ").slice(0, 19);
5135
+ }
5136
+ function parseTopic(topic) {
5137
+ const commentMatch = topic.match(/^(.+?):comments:/);
5138
+ if (commentMatch) {
5139
+ return { feedName: commentMatch[1], isComment: true };
5140
+ }
5141
+ return { feedName: topic, isComment: false };
5142
+ }
5143
+ function formatPost(post, index, options = {}) {
5144
+ const { commentCount, showTopic } = options;
5145
+ const timestamp = formatTimestamp(post.timestamp);
5146
+ const lines = [
5147
+ chalk4.cyan(`[${index}]`) + ` ${chalk4.gray(timestamp)}`,
5148
+ ` ${chalk4.white("Sender:")} ${post.sender}`,
5149
+ ` ${chalk4.white("Text:")} ${post.text}`
5150
+ ];
5151
+ if (showTopic && post.topic) {
5152
+ const { feedName, isComment } = parseTopic(post.topic);
5153
+ const prefix = isComment ? "Comment on" : "Feed";
5154
+ lines.push(` ${chalk4.white(prefix + ":")} ${chalk4.magenta(feedName)}`);
5155
+ }
5156
+ if (commentCount !== void 0) {
5157
+ lines.push(` ${chalk4.white("Comments:")} ${commentCount}`);
5158
+ }
5159
+ if (post.data && post.data !== "0x") {
5160
+ lines.push(` ${chalk4.white("Data:")} ${post.data}`);
5161
+ }
5162
+ return lines.join("\n");
5163
+ }
5164
+ function formatFeed(feed, index) {
5165
+ const timestamp = formatTimestamp(feed.timestamp);
5166
+ const lines = [
5167
+ chalk4.cyan(`[${index}]`) + ` ${chalk4.white(feed.feedName)}`,
5168
+ ` ${chalk4.gray("Registrant:")} ${feed.registrant}`,
5169
+ ` ${chalk4.gray("Registered:")} ${timestamp}`
5170
+ ];
5171
+ return lines.join("\n");
5172
+ }
5173
+ function formatComment(comment, depth) {
5174
+ const indent = " ".repeat(depth + 1);
5175
+ const timestamp = formatTimestamp(comment.timestamp);
5176
+ const lines = [
5177
+ `${indent}${chalk4.gray(timestamp)} ${chalk4.blue(truncateAddress(comment.sender))}`,
5178
+ `${indent}${comment.text}`
5179
+ ];
5180
+ return lines.join("\n");
5181
+ }
5182
+ function postToJson(post, index, commentCount) {
5183
+ const result = {
5184
+ index,
5185
+ sender: post.sender,
5186
+ text: post.text,
5187
+ timestamp: Number(post.timestamp),
5188
+ topic: post.topic
5189
+ };
5190
+ if (commentCount !== void 0) {
5191
+ result.commentCount = commentCount;
5192
+ }
5193
+ if (post.data && post.data !== "0x") {
5194
+ result.data = post.data;
5195
+ }
5196
+ return result;
5197
+ }
5198
+ function feedToJson(feed, index) {
5199
+ return {
5200
+ index,
5201
+ feedName: feed.feedName,
5202
+ registrant: feed.registrant,
5203
+ timestamp: feed.timestamp
5204
+ };
5205
+ }
5206
+ function commentToJson(comment, depth) {
5207
+ return {
5208
+ sender: comment.sender,
5209
+ text: comment.text,
5210
+ timestamp: Number(comment.timestamp),
5211
+ depth,
5212
+ data: comment.data !== "0x" ? comment.data : void 0
5213
+ };
5214
+ }
5215
+ function printJson(data) {
5216
+ console.log(JSON.stringify(data, null, 2));
5217
+ }
5218
+
5219
+ // src/commands/feed/list.ts
5220
+ async function executeFeedList(options) {
5221
+ const readOnlyOptions = parseReadOnlyOptionsWithDefault({
5222
+ chainId: options.chainId,
5223
+ rpcUrl: options.rpcUrl
5224
+ });
5225
+ const client = createFeedRegistryClient(readOnlyOptions);
5226
+ try {
5227
+ const feeds = await client.getRegisteredFeeds({
5228
+ maxFeeds: options.limit ?? 50
5229
+ });
5230
+ if (options.json) {
5231
+ printJson(feeds.map((feed, i) => feedToJson(feed, i)));
5232
+ } else {
5233
+ if (feeds.length === 0) {
5234
+ console.log(chalk4.yellow("No registered feeds found"));
5235
+ return;
5236
+ }
5237
+ console.log(chalk4.white(`Found ${feeds.length} registered feed(s):
5238
+ `));
5239
+ feeds.forEach((feed, i) => {
5240
+ console.log(formatFeed(feed, i));
5241
+ if (i < feeds.length - 1) {
5242
+ console.log();
5243
+ }
5244
+ });
5245
+ }
5246
+ } catch (error) {
5247
+ exitWithError(
5248
+ `Failed to fetch feeds: ${error instanceof Error ? error.message : String(error)}`
5249
+ );
5250
+ }
5251
+ }
5252
+ function registerFeedListCommand(parent, commandName = "list") {
5253
+ parent.command(commandName).description("List registered feeds").option(
5254
+ "--limit <n>",
5255
+ "Maximum number of feeds to display",
5256
+ (value) => parseInt(value, 10)
5257
+ ).option(
5258
+ "--chain-id <id>",
5259
+ "Chain ID (default: 8453 for Base)",
5260
+ (value) => parseInt(value, 10)
5261
+ ).option("--rpc-url <url>", "Custom RPC URL").option("--json", "Output in JSON format").action(async (options) => {
5262
+ await executeFeedList(options);
5263
+ });
5264
+ }
5265
+ var MAX_HISTORY_ENTRIES = 100;
5266
+ var STATE_DIR = path.join(os.homedir(), ".botchan");
5267
+ var STATE_FILE = path.join(STATE_DIR, "state.json");
5268
+ function ensureStateDir() {
5269
+ if (!fs4.existsSync(STATE_DIR)) {
5270
+ fs4.mkdirSync(STATE_DIR, { recursive: true });
5271
+ }
5272
+ }
5273
+ function loadState() {
5274
+ try {
5275
+ if (fs4.existsSync(STATE_FILE)) {
5276
+ const data = fs4.readFileSync(STATE_FILE, "utf-8");
5277
+ return JSON.parse(data);
5278
+ }
5279
+ } catch {
5280
+ }
5281
+ return { feeds: {} };
5282
+ }
5283
+ function saveState(state) {
5284
+ ensureStateDir();
5285
+ const tempFile = `${STATE_FILE}.tmp`;
5286
+ fs4.writeFileSync(tempFile, JSON.stringify(state, null, 2));
5287
+ fs4.renameSync(tempFile, STATE_FILE);
5288
+ }
5289
+ function getLastSeenTimestamp(feedName) {
5290
+ const state = loadState();
5291
+ return state.feeds[feedName]?.lastSeenTimestamp ?? null;
5292
+ }
5293
+ function setLastSeenTimestamp(feedName, timestamp) {
5294
+ const state = loadState();
5295
+ if (!state.feeds[feedName]) {
5296
+ state.feeds[feedName] = { lastSeenTimestamp: timestamp };
5297
+ } else {
5298
+ state.feeds[feedName].lastSeenTimestamp = timestamp;
5299
+ }
5300
+ saveState(state);
5301
+ }
5302
+ function markFeedSeen(feedName, posts) {
5303
+ if (posts.length === 0) return;
5304
+ const maxTimestamp = posts.reduce(
5305
+ (max, post) => post.timestamp > max ? post.timestamp : max,
5306
+ posts[0].timestamp
5307
+ );
5308
+ setLastSeenTimestamp(feedName, Number(maxTimestamp));
5309
+ }
5310
+ function getMyAddress() {
5311
+ const state = loadState();
5312
+ return state.myAddress ?? null;
5313
+ }
5314
+ function setMyAddress(address) {
5315
+ const state = loadState();
5316
+ state.myAddress = address.toLowerCase();
5317
+ saveState(state);
5318
+ }
5319
+ function clearMyAddress() {
5320
+ const state = loadState();
5321
+ delete state.myAddress;
5322
+ saveState(state);
5323
+ }
5324
+ function getFullState() {
5325
+ return loadState();
5326
+ }
5327
+ function resetState() {
5328
+ if (fs4.existsSync(STATE_FILE)) {
5329
+ fs4.unlinkSync(STATE_FILE);
5330
+ }
5331
+ }
5332
+ function getStateFilePath() {
5333
+ return STATE_FILE;
5334
+ }
5335
+ function addHistoryEntry(entry) {
5336
+ const state = loadState();
5337
+ const history = state.history ?? [];
5338
+ const newEntry = {
5339
+ ...entry,
5340
+ timestamp: Math.floor(Date.now() / 1e3)
5341
+ };
5342
+ history.unshift(newEntry);
5343
+ if (history.length > MAX_HISTORY_ENTRIES) {
5344
+ history.length = MAX_HISTORY_ENTRIES;
5345
+ }
5346
+ state.history = history;
5347
+ saveState(state);
5348
+ }
5349
+ function getHistory(limit) {
5350
+ const state = loadState();
5351
+ const history = state.history ?? [];
5352
+ return limit ? history.slice(0, limit) : history;
5353
+ }
5354
+ function getHistoryByType(type, limit) {
5355
+ const history = getHistory();
5356
+ const filtered = history.filter((entry) => entry.type === type);
5357
+ return limit ? filtered.slice(0, limit) : filtered;
5358
+ }
5359
+ function clearHistory() {
5360
+ const state = loadState();
5361
+ state.history = [];
5362
+ saveState(state);
5363
+ }
5364
+ function getHistoryCount() {
5365
+ const state = loadState();
5366
+ return state.history?.length ?? 0;
5367
+ }
5368
+ function isWalletAddress(feed) {
5369
+ return /^0x[a-fA-F0-9]{40}$/.test(feed);
5370
+ }
5371
+ function getContacts() {
5372
+ const history = getHistory();
5373
+ const contactMap = /* @__PURE__ */ new Map();
5374
+ for (const entry of history) {
5375
+ if (entry.type === "post" && isWalletAddress(entry.feed)) {
5376
+ const address = entry.feed.toLowerCase();
5377
+ const existing = contactMap.get(address);
5378
+ if (existing) {
5379
+ existing.interactionCount++;
5380
+ if (entry.timestamp > existing.lastInteraction) {
5381
+ existing.lastInteraction = entry.timestamp;
5382
+ }
5383
+ if (entry.timestamp < existing.firstInteraction) {
5384
+ existing.firstInteraction = entry.timestamp;
5385
+ }
5386
+ } else {
5387
+ contactMap.set(address, {
5388
+ address,
5389
+ lastInteraction: entry.timestamp,
5390
+ firstInteraction: entry.timestamp,
5391
+ interactionCount: 1
5392
+ });
5393
+ }
5394
+ }
5395
+ }
5396
+ return Array.from(contactMap.values()).sort(
5397
+ (a, b) => b.lastInteraction - a.lastInteraction
5398
+ );
5399
+ }
5400
+ function getActiveFeeds() {
5401
+ const history = getHistory();
5402
+ const feedMap = /* @__PURE__ */ new Map();
5403
+ for (const entry of history) {
5404
+ if (isWalletAddress(entry.feed)) continue;
5405
+ if (entry.type === "register") continue;
5406
+ const feedName = entry.feed.toLowerCase();
5407
+ const existing = feedMap.get(feedName);
5408
+ if (existing) {
5409
+ if (entry.type === "post") existing.postCount++;
5410
+ if (entry.type === "comment") existing.commentCount++;
5411
+ if (entry.timestamp > existing.lastActivity) {
5412
+ existing.lastActivity = entry.timestamp;
5413
+ }
5414
+ if (entry.timestamp < existing.firstActivity) {
5415
+ existing.firstActivity = entry.timestamp;
5416
+ }
5417
+ } else {
5418
+ feedMap.set(feedName, {
5419
+ feed: feedName,
5420
+ postCount: entry.type === "post" ? 1 : 0,
5421
+ commentCount: entry.type === "comment" ? 1 : 0,
5422
+ lastActivity: entry.timestamp,
5423
+ firstActivity: entry.timestamp
5424
+ });
5425
+ }
5426
+ }
5427
+ return Array.from(feedMap.values()).sort(
5428
+ (a, b) => b.lastActivity - a.lastActivity
5429
+ );
5430
+ }
5431
+
5432
+ // src/commands/feed/types.ts
5433
+ function normalizeFeedName(feed) {
5434
+ return feed.toLowerCase();
5435
+ }
5436
+
5437
+ // src/commands/feed/read.ts
5438
+ async function executeFeedRead(feed, options) {
5439
+ const normalizedFeed = normalizeFeedName(feed);
5440
+ const readOnlyOptions = parseReadOnlyOptionsWithDefault({
5441
+ chainId: options.chainId,
5442
+ rpcUrl: options.rpcUrl
5443
+ });
5444
+ const client = createFeedClient(readOnlyOptions);
5445
+ const limit = options.limit ?? 20;
5446
+ try {
5447
+ const count = await client.getFeedPostCount(normalizedFeed);
5448
+ if (count === 0) {
5449
+ if (options.json) {
5450
+ printJson([]);
5451
+ } else {
5452
+ console.log(chalk4.yellow(`No posts found in feed "${normalizedFeed}"`));
5453
+ }
5454
+ return;
5455
+ }
5456
+ const fetchLimit = options.sender ? Math.max(limit * 5, 100) : limit;
5457
+ let posts = await client.getFeedPosts({
5458
+ topic: normalizedFeed,
5459
+ maxPosts: fetchLimit
5460
+ });
5461
+ if (options.sender) {
5462
+ const senderLower = options.sender.toLowerCase();
5463
+ posts = posts.filter(
5464
+ (post) => post.sender.toLowerCase() === senderLower
5465
+ );
5466
+ posts = posts.slice(0, limit);
5467
+ }
5468
+ if (options.unseen) {
5469
+ const lastSeen = getLastSeenTimestamp(normalizedFeed);
5470
+ const myAddress = getMyAddress();
5471
+ posts = posts.filter((post) => {
5472
+ const isNew = lastSeen === null || Number(post.timestamp) > lastSeen;
5473
+ const isFromOther = !myAddress || post.sender.toLowerCase() !== myAddress;
5474
+ return isNew && isFromOther;
5475
+ });
5476
+ }
5477
+ if (options.markSeen) {
5478
+ const allPosts = await client.getFeedPosts({
5479
+ topic: normalizedFeed,
5480
+ maxPosts: 1
5481
+ // Just need the latest
5482
+ });
5483
+ if (allPosts.length > 0) {
5484
+ markFeedSeen(normalizedFeed, allPosts);
5485
+ }
5486
+ }
5487
+ const commentCounts = await Promise.all(
5488
+ posts.map((post) => client.getCommentCount(post))
5489
+ );
5490
+ if (options.json) {
5491
+ printJson(
5492
+ posts.map((post, i) => postToJson(post, i, commentCounts[i]))
5493
+ );
5494
+ } else {
5495
+ if (posts.length === 0) {
5496
+ const senderNote2 = options.sender ? ` by ${options.sender}` : "";
5497
+ console.log(chalk4.yellow(`No posts found in feed "${normalizedFeed}"${senderNote2}`));
5498
+ return;
5499
+ }
5500
+ const senderNote = options.sender ? ` by ${options.sender}` : "";
5501
+ console.log(
5502
+ chalk4.white(`Found ${posts.length} post(s) in feed "${normalizedFeed}"${senderNote}:
5503
+ `)
5504
+ );
5505
+ posts.forEach((post, i) => {
5506
+ console.log(formatPost(post, i, { commentCount: commentCounts[i] }));
5507
+ if (i < posts.length - 1) {
5508
+ console.log();
5509
+ }
5510
+ });
5511
+ }
5512
+ } catch (error) {
5513
+ exitWithError(
5514
+ `Failed to read feed: ${error instanceof Error ? error.message : String(error)}`
5515
+ );
5516
+ }
5517
+ }
5518
+ function registerFeedReadCommand(parent) {
5519
+ parent.command("read <feed>").description("Read posts from a feed").option(
5520
+ "--limit <n>",
5521
+ "Maximum number of posts to display",
5522
+ (value) => parseInt(value, 10)
5523
+ ).option(
5524
+ "--chain-id <id>",
5525
+ "Chain ID (default: 8453 for Base)",
5526
+ (value) => parseInt(value, 10)
5527
+ ).option("--rpc-url <url>", "Custom RPC URL").option("--sender <address>", "Filter posts by sender address").option("--unseen", "Only show posts not yet seen (newer than last --mark-seen)").option("--mark-seen", "Mark the feed as seen up to the latest post").option("--json", "Output in JSON format").action(async (feed, options) => {
5528
+ await executeFeedRead(feed, options);
5529
+ });
5530
+ }
5531
+ function createWallet(privateKey, chainId, rpcUrl) {
5532
+ const account = privateKeyToAccount(privateKey);
5533
+ const rpcUrls = getChainRpcUrls({
5534
+ chainId,
5535
+ rpcUrl
5536
+ });
5537
+ return createWalletClient({
5538
+ account,
5539
+ transport: http(rpcUrls[0])
5540
+ });
5541
+ }
5542
+ async function executeTransaction(walletClient, txConfig) {
5543
+ const hash = await walletClient.writeContract({
5544
+ address: txConfig.to,
5545
+ abi: txConfig.abi,
5546
+ functionName: txConfig.functionName,
5547
+ args: txConfig.args,
5548
+ value: txConfig.value,
5549
+ chain: null
5550
+ });
5551
+ return hash;
5552
+ }
5553
+
5554
+ // src/commands/feed/post.ts
5555
+ var MAX_MESSAGE_LENGTH = 4e3;
5556
+ async function executeFeedPost(feed, message, options) {
5557
+ const normalizedFeed = normalizeFeedName(feed);
5558
+ if (message.length === 0) {
5559
+ exitWithError("Message cannot be empty");
5560
+ }
5561
+ const fullMessage = options.body ? `${message}
5562
+
5563
+ ${options.body}` : message;
5564
+ if (fullMessage.length > MAX_MESSAGE_LENGTH) {
5565
+ exitWithError(
5566
+ `Message too long (${fullMessage.length} chars). Maximum is ${MAX_MESSAGE_LENGTH} characters.`
5567
+ );
5568
+ }
5569
+ if (options.encodeOnly) {
5570
+ const readOnlyOptions = parseReadOnlyOptionsWithDefault({
5571
+ chainId: options.chainId,
5572
+ rpcUrl: options.rpcUrl
5573
+ });
5574
+ const client2 = createFeedClient(readOnlyOptions);
5575
+ const txConfig2 = client2.preparePostToFeed({
5576
+ topic: normalizedFeed,
5577
+ text: fullMessage,
5578
+ data: options.data
5579
+ });
5580
+ const encoded = encodeTransaction(txConfig2, readOnlyOptions.chainId);
5581
+ printJson(encoded);
5582
+ return;
5583
+ }
5584
+ const commonOptions = parseCommonOptionsWithDefault(
5585
+ {
5586
+ privateKey: options.privateKey,
5587
+ chainId: options.chainId,
5588
+ rpcUrl: options.rpcUrl
5589
+ },
5590
+ true
5591
+ // supports --encode-only
5592
+ );
5593
+ const client = createFeedClient(commonOptions);
5594
+ const txConfig = client.preparePostToFeed({
5595
+ topic: normalizedFeed,
5596
+ text: fullMessage,
5597
+ data: options.data
5598
+ });
5599
+ const walletClient = createWallet(
5600
+ commonOptions.privateKey,
5601
+ commonOptions.chainId,
5602
+ commonOptions.rpcUrl
5603
+ );
5604
+ console.log(chalk4.blue(`Posting to feed "${normalizedFeed}"...`));
5605
+ try {
5606
+ const hash = await executeTransaction(walletClient, txConfig);
5607
+ const senderAddress = walletClient.account.address;
5608
+ let postId;
5609
+ try {
5610
+ const posts = await client.getFeedPosts({
5611
+ topic: normalizedFeed,
5612
+ maxPosts: 10
5613
+ });
5614
+ const ourPost = posts.find(
5615
+ (p) => p.sender.toLowerCase() === senderAddress.toLowerCase() && p.text === fullMessage
5616
+ );
5617
+ if (ourPost) {
5618
+ postId = `${ourPost.sender}:${ourPost.timestamp}`;
5619
+ }
5620
+ } catch {
5621
+ }
5622
+ addHistoryEntry({
5623
+ type: "post",
5624
+ txHash: hash,
5625
+ chainId: commonOptions.chainId,
5626
+ feed: normalizedFeed,
5627
+ sender: senderAddress,
5628
+ text: fullMessage,
5629
+ postId
5630
+ // Now we have the actual post ID for checking comments later
5631
+ });
5632
+ const displayText = options.body ? `${message} (+ body)` : message;
5633
+ const postIdInfo = postId ? `
5634
+ Post ID: ${postId}` : "";
5635
+ console.log(
5636
+ chalk4.green(
5637
+ `Message posted successfully!
5638
+ Transaction: ${hash}
5639
+ Feed: ${normalizedFeed}${postIdInfo}
5640
+ Text: ${displayText}`
5641
+ )
5642
+ );
5643
+ } catch (error) {
5644
+ exitWithError(
5645
+ `Failed to post message: ${error instanceof Error ? error.message : String(error)}`
5646
+ );
5647
+ }
5648
+ }
5649
+ function registerFeedPostCommand(parent) {
5650
+ parent.command("post <feed> <message>").description("Post a message to a feed").option(
5651
+ "--chain-id <id>",
5652
+ "Chain ID (default: 8453 for Base)",
5653
+ (value) => parseInt(value, 10)
5654
+ ).option("--rpc-url <url>", "Custom RPC URL").option("--private-key <key>", "Private key (0x-prefixed)").option(
5655
+ "--encode-only",
5656
+ "Output transaction data as JSON instead of executing"
5657
+ ).option("--data <data>", "Optional data to attach to the post").option("--body <text>", "Post body (message becomes the title)").action(async (feed, message, options) => {
5658
+ await executeFeedPost(feed, message, options);
5659
+ });
5660
+ }
5661
+
5662
+ // src/shared/postId.ts
5663
+ function parsePostId(postId) {
5664
+ const parts = postId.split(":");
5665
+ if (parts.length !== 2) {
5666
+ throw new Error(
5667
+ `Invalid post ID format. Expected {sender}:{timestamp}, got: ${postId}`
5668
+ );
5669
+ }
5670
+ const [sender, timestampStr] = parts;
5671
+ if (!sender.startsWith("0x") || sender.length !== 42) {
5672
+ throw new Error(`Invalid sender address in post ID: ${sender}`);
5673
+ }
5674
+ const timestamp = BigInt(timestampStr);
5675
+ if (timestamp <= 0) {
5676
+ throw new Error(`Invalid timestamp in post ID: ${timestampStr}`);
5677
+ }
5678
+ return {
5679
+ sender,
5680
+ timestamp
5681
+ };
5682
+ }
5683
+ function findPostByParsedId(posts, parsedId) {
5684
+ return posts.find(
5685
+ (p) => p.sender.toLowerCase() === parsedId.sender.toLowerCase() && p.timestamp === parsedId.timestamp
5686
+ );
5687
+ }
5688
+
5689
+ // src/commands/feed/comment-write.ts
5690
+ var MAX_MESSAGE_LENGTH2 = 4e3;
5691
+ async function executeFeedCommentWrite(feed, postId, message, options) {
5692
+ const normalizedFeed = normalizeFeedName(feed);
5693
+ if (message.length === 0) {
5694
+ exitWithError("Comment cannot be empty");
5695
+ }
5696
+ if (message.length > MAX_MESSAGE_LENGTH2) {
5697
+ exitWithError(
5698
+ `Comment too long (${message.length} chars). Maximum is ${MAX_MESSAGE_LENGTH2} characters.`
5699
+ );
5700
+ }
5701
+ let parsedId;
5702
+ try {
5703
+ parsedId = parsePostId(postId);
5704
+ } catch (error) {
5705
+ exitWithError(
5706
+ error instanceof Error ? error.message : "Invalid post ID format"
5707
+ );
5708
+ }
5709
+ const readOnlyOptions = parseReadOnlyOptionsWithDefault({
5710
+ chainId: options.chainId,
5711
+ rpcUrl: options.rpcUrl
5712
+ });
5713
+ const client = createFeedClient(readOnlyOptions);
5714
+ const count = await client.getFeedPostCount(normalizedFeed);
5715
+ if (count === 0) {
5716
+ exitWithError(
5717
+ `Feed "${normalizedFeed}" has no posts. Cannot find post ${postId}.`
5718
+ );
5719
+ }
5720
+ const posts = await client.getFeedPosts({
5721
+ topic: normalizedFeed,
5722
+ maxPosts: 100
5723
+ // Fetch enough to find the post
5724
+ });
5725
+ const targetPost = findPostByParsedId(posts, parsedId);
5726
+ if (!targetPost) {
5727
+ exitWithError(
5728
+ `Post not found with ID ${postId} in feed "${normalizedFeed}". Make sure the sender and timestamp are correct.`
5729
+ );
5730
+ }
5731
+ const txConfig = client.prepareComment({
5732
+ post: targetPost,
5733
+ text: message
5734
+ });
5735
+ if (options.encodeOnly) {
5736
+ const encoded = encodeTransaction(txConfig, readOnlyOptions.chainId);
5737
+ printJson(encoded);
5738
+ return;
5739
+ }
5740
+ const commonOptions = parseCommonOptionsWithDefault(
5741
+ {
5742
+ privateKey: options.privateKey,
5743
+ chainId: options.chainId,
5744
+ rpcUrl: options.rpcUrl
5745
+ },
5746
+ true
5747
+ // supports --encode-only
5748
+ );
5749
+ const walletClient = createWallet(
5750
+ commonOptions.privateKey,
5751
+ commonOptions.chainId,
5752
+ commonOptions.rpcUrl
5753
+ );
5754
+ console.log(chalk4.blue(`Commenting on post ${postId}...`));
5755
+ try {
5756
+ const hash = await executeTransaction(walletClient, txConfig);
5757
+ addHistoryEntry({
5758
+ type: "comment",
5759
+ txHash: hash,
5760
+ chainId: commonOptions.chainId,
5761
+ feed: normalizedFeed,
5762
+ sender: walletClient.account.address,
5763
+ text: message,
5764
+ postId
5765
+ });
5766
+ console.log(
5767
+ chalk4.green(
5768
+ `Comment posted successfully!
5769
+ Transaction: ${hash}
5770
+ Post: ${postId}
5771
+ Comment: ${message}`
5772
+ )
5773
+ );
5774
+ } catch (error) {
5775
+ exitWithError(
5776
+ `Failed to post comment: ${error instanceof Error ? error.message : String(error)}`
5777
+ );
5778
+ }
5779
+ }
5780
+ function registerFeedCommentWriteCommand(parent) {
5781
+ parent.command("comment <feed> <post-id> <message>").description(
5782
+ "Comment on a post. Post ID format: {sender}:{timestamp}"
5783
+ ).option(
5784
+ "--chain-id <id>",
5785
+ "Chain ID (default: 8453 for Base)",
5786
+ (value) => parseInt(value, 10)
5787
+ ).option("--rpc-url <url>", "Custom RPC URL").option("--private-key <key>", "Private key (0x-prefixed)").option(
5788
+ "--encode-only",
5789
+ "Output transaction data as JSON instead of executing"
5790
+ ).action(async (feed, postId, message, options) => {
5791
+ await executeFeedCommentWrite(feed, postId, message, options);
5792
+ });
5793
+ }
5794
+ async function executeFeedCommentRead(feed, postId, options) {
5795
+ const normalizedFeed = normalizeFeedName(feed);
5796
+ const readOnlyOptions = parseReadOnlyOptionsWithDefault({
5797
+ chainId: options.chainId,
5798
+ rpcUrl: options.rpcUrl
5799
+ });
5800
+ const client = createFeedClient(readOnlyOptions);
5801
+ let parsedId;
5802
+ try {
5803
+ parsedId = parsePostId(postId);
5804
+ } catch (error) {
5805
+ exitWithError(
5806
+ error instanceof Error ? error.message : "Invalid post ID format"
5807
+ );
5808
+ }
5809
+ try {
5810
+ const count = await client.getFeedPostCount(normalizedFeed);
5811
+ if (count === 0) {
5812
+ exitWithError(
5813
+ `Feed "${normalizedFeed}" has no posts. Cannot find post ${postId}.`
5814
+ );
5815
+ }
5816
+ const posts = await client.getFeedPosts({
5817
+ topic: normalizedFeed,
5818
+ maxPosts: 100
5819
+ // Fetch enough to find the post
5820
+ });
5821
+ const targetPost = findPostByParsedId(posts, parsedId);
5822
+ if (!targetPost) {
5823
+ exitWithError(
5824
+ `Post not found with ID ${postId} in feed "${normalizedFeed}". Make sure the sender and timestamp are correct.`
5825
+ );
5826
+ }
5827
+ const commentCount = await client.getCommentCount(targetPost);
5828
+ if (commentCount === 0) {
5829
+ if (options.json) {
5830
+ printJson([]);
5831
+ } else {
5832
+ console.log(chalk4.yellow(`No comments found for post ${postId}`));
5833
+ }
5834
+ return;
5835
+ }
5836
+ const comments = await client.getComments({
5837
+ post: targetPost,
5838
+ maxComments: options.limit ?? 50
5839
+ });
5840
+ const commentsWithDepth = comments.map((comment) => ({
5841
+ comment,
5842
+ depth: 0
5843
+ }));
5844
+ if (options.json) {
5845
+ printJson(
5846
+ commentsWithDepth.map(
5847
+ ({ comment, depth }) => commentToJson(comment, depth)
5848
+ )
5849
+ );
5850
+ } else {
5851
+ if (comments.length === 0) {
5852
+ console.log(chalk4.yellow(`No comments found for post ${postId}`));
5853
+ return;
5854
+ }
5855
+ console.log(
5856
+ chalk4.white(`Found ${comments.length} comment(s) for post ${postId}:
5857
+ `)
5858
+ );
5859
+ commentsWithDepth.forEach(({ comment, depth }, i) => {
5860
+ console.log(formatComment(comment, depth));
5861
+ if (i < commentsWithDepth.length - 1) {
5862
+ console.log();
5863
+ }
5864
+ });
5865
+ }
5866
+ } catch (error) {
5867
+ if (error.message?.includes("Post not found")) {
5868
+ throw error;
5869
+ }
5870
+ exitWithError(
5871
+ `Failed to fetch comments: ${error instanceof Error ? error.message : String(error)}`
5872
+ );
5873
+ }
5874
+ }
5875
+ function registerFeedCommentReadCommand(parent) {
5876
+ parent.command("comments <feed> <post-id>").description(
5877
+ "Read comments on a post. Post ID format: {sender}:{timestamp}"
5878
+ ).option(
5879
+ "--limit <n>",
5880
+ "Maximum number of comments to display",
5881
+ (value) => parseInt(value, 10)
5882
+ ).option(
5883
+ "--chain-id <id>",
5884
+ "Chain ID (default: 8453 for Base)",
5885
+ (value) => parseInt(value, 10)
5886
+ ).option("--rpc-url <url>", "Custom RPC URL").option("--json", "Output in JSON format").action(async (feed, postId, options) => {
5887
+ await executeFeedCommentRead(feed, postId, options);
5888
+ });
5889
+ }
5890
+ async function executeFeedRegister(feedName, options) {
5891
+ if (feedName.length > 64) {
5892
+ exitWithError("Feed name cannot exceed 64 characters");
5893
+ }
5894
+ if (feedName.length === 0) {
5895
+ exitWithError("Feed name cannot be empty");
5896
+ }
5897
+ if (options.encodeOnly) {
5898
+ const readOnlyOptions = parseReadOnlyOptionsWithDefault({
5899
+ chainId: options.chainId,
5900
+ rpcUrl: options.rpcUrl
5901
+ });
5902
+ const client2 = createFeedRegistryClient(readOnlyOptions);
5903
+ const txConfig2 = client2.prepareRegisterFeed({ feedName });
5904
+ const encoded = encodeTransaction(txConfig2, readOnlyOptions.chainId);
5905
+ printJson(encoded);
5906
+ return;
5907
+ }
5908
+ const commonOptions = parseCommonOptionsWithDefault(
5909
+ {
5910
+ privateKey: options.privateKey,
5911
+ chainId: options.chainId,
5912
+ rpcUrl: options.rpcUrl
5913
+ },
5914
+ true
5915
+ // supports --encode-only
5916
+ );
5917
+ const client = createFeedRegistryClient(commonOptions);
5918
+ const isRegistered = await client.isFeedRegistered(feedName);
5919
+ if (isRegistered) {
5920
+ exitWithError(`Feed "${feedName}" is already registered`);
5921
+ }
5922
+ const txConfig = client.prepareRegisterFeed({ feedName });
5923
+ const walletClient = createWallet(
5924
+ commonOptions.privateKey,
5925
+ commonOptions.chainId,
5926
+ commonOptions.rpcUrl
5927
+ );
5928
+ console.log(chalk4.blue(`Registering feed "${feedName}"...`));
5929
+ try {
5930
+ const hash = await executeTransaction(walletClient, txConfig);
5931
+ addHistoryEntry({
5932
+ type: "register",
5933
+ txHash: hash,
5934
+ chainId: commonOptions.chainId,
5935
+ feed: feedName,
5936
+ sender: walletClient.account.address
5937
+ });
5938
+ console.log(
5939
+ chalk4.green(
5940
+ `Feed registered successfully!
5941
+ Transaction: ${hash}
5942
+ Feed: ${feedName}`
5943
+ )
5944
+ );
5945
+ } catch (error) {
5946
+ exitWithError(
5947
+ `Failed to register feed: ${error instanceof Error ? error.message : String(error)}`
5948
+ );
5949
+ }
5950
+ }
5951
+ function registerFeedRegisterCommand(parent) {
5952
+ parent.command("register <feed-name>").description("Register a new feed").option(
5953
+ "--chain-id <id>",
5954
+ "Chain ID (default: 8453 for Base)",
5955
+ (value) => parseInt(value, 10)
5956
+ ).option("--rpc-url <url>", "Custom RPC URL").option("--private-key <key>", "Private key (0x-prefixed)").option(
5957
+ "--encode-only",
5958
+ "Output transaction data as JSON instead of executing"
5959
+ ).action(async (feedName, options) => {
5960
+ await executeFeedRegister(feedName, options);
5961
+ });
5962
+ }
5963
+ function truncateText(text, maxLen) {
5964
+ if (text.length <= maxLen) return text;
5965
+ return text.slice(0, maxLen) + "...";
5966
+ }
5967
+ async function executeFeedReplies(options) {
5968
+ const postHistory = getHistoryByType("post", options.limit ?? 10);
5969
+ const postsWithIds = postHistory.filter(
5970
+ (entry) => !!entry.postId
5971
+ );
5972
+ if (postsWithIds.length === 0) {
5973
+ if (options.json) {
5974
+ printJson([]);
5975
+ } else {
5976
+ console.log(chalk4.gray("No posts with trackable IDs found in history."));
5977
+ console.log(
5978
+ chalk4.gray("Post IDs are captured when you post with a wallet.")
5979
+ );
5980
+ }
5981
+ return;
5982
+ }
5983
+ const readOnlyOptions = parseReadOnlyOptionsWithDefault({
5984
+ chainId: options.chainId,
5985
+ rpcUrl: options.rpcUrl
5986
+ });
5987
+ const client = createFeedClient(readOnlyOptions);
5988
+ console.log(chalk4.blue(`Checking replies on ${postsWithIds.length} posts...
5989
+ `));
5990
+ const results = [];
5991
+ for (const entry of postsWithIds) {
5992
+ try {
5993
+ const [sender, timestampStr] = entry.postId.split(":");
5994
+ const timestamp = BigInt(timestampStr);
5995
+ const postObj = {
5996
+ sender,
5997
+ timestamp,
5998
+ text: entry.text ?? "",
5999
+ topic: entry.feed,
6000
+ app: "",
6001
+ data: "0x"
6002
+ };
6003
+ const commentCount = await client.getCommentCount(postObj);
6004
+ results.push({
6005
+ feed: entry.feed,
6006
+ postId: entry.postId,
6007
+ text: entry.text ?? "",
6008
+ postedAt: entry.timestamp,
6009
+ commentCount: Number(commentCount)
6010
+ });
6011
+ } catch {
6012
+ }
6013
+ }
6014
+ if (options.json) {
6015
+ printJson(results);
6016
+ return;
6017
+ }
6018
+ const postsWithReplies = results.filter((r) => r.commentCount > 0);
6019
+ const totalReplies = results.reduce((sum, r) => sum + r.commentCount, 0);
6020
+ console.log(
6021
+ chalk4.cyan(
6022
+ `Found ${totalReplies} total replies across ${postsWithReplies.length} posts
6023
+ `
6024
+ )
6025
+ );
6026
+ if (results.length === 0) {
6027
+ console.log(chalk4.gray("Could not check any posts."));
6028
+ return;
6029
+ }
6030
+ results.sort((a, b) => b.commentCount - a.commentCount);
6031
+ for (const post of results) {
6032
+ const timeAgo = formatTimestamp(post.postedAt);
6033
+ const replyText = post.commentCount === 0 ? chalk4.gray("no replies") : post.commentCount === 1 ? chalk4.green("1 reply") : chalk4.green(`${post.commentCount} replies`);
6034
+ console.log(
6035
+ `${chalk4.white(post.feed)} ${chalk4.gray("\u2022")} ${replyText} ${chalk4.gray(`\u2022 ${timeAgo}`)}`
6036
+ );
6037
+ console.log(` ${chalk4.gray(truncateText(post.text, 60))}`);
6038
+ if (post.commentCount > 0) {
6039
+ console.log(
6040
+ chalk4.blue(` \u2192 netp feed comments ${post.feed} ${post.postId}`)
6041
+ );
6042
+ }
6043
+ console.log("");
6044
+ }
6045
+ }
6046
+ function registerFeedRepliesCommand(parent) {
6047
+ parent.command("replies").description("Check for replies on your recent posts").option(
6048
+ "--chain-id <id>",
6049
+ "Chain ID (default: 8453 for Base)",
6050
+ (value) => parseInt(value, 10)
6051
+ ).option("--rpc-url <url>", "Custom RPC URL").option(
6052
+ "--limit <n>",
6053
+ "Number of recent posts to check (default: 10)",
6054
+ (value) => parseInt(value, 10)
6055
+ ).option("--json", "Output as JSON").action(async (options) => {
6056
+ await executeFeedReplies(options);
6057
+ });
6058
+ }
6059
+ async function executeFeedPosts(address, options) {
6060
+ if (!address.startsWith("0x") || address.length !== 42) {
6061
+ exitWithError("Invalid address format. Must be 0x-prefixed, 42 characters");
6062
+ }
6063
+ const readOnlyOptions = parseReadOnlyOptionsWithDefault({
6064
+ chainId: options.chainId,
6065
+ rpcUrl: options.rpcUrl
6066
+ });
6067
+ const client = createNetClient(readOnlyOptions);
6068
+ const limit = options.limit ?? 20;
6069
+ try {
6070
+ const count = await client.getMessageCount({
6071
+ filter: {
6072
+ appAddress: NULL_ADDRESS,
6073
+ maker: address
6074
+ }
6075
+ });
6076
+ if (count === 0) {
6077
+ if (options.json) {
6078
+ printJson([]);
6079
+ } else {
6080
+ console.log(chalk4.yellow(`No posts found for address ${address}`));
6081
+ }
6082
+ return;
6083
+ }
6084
+ const startIndex = count > limit ? count - limit : 0;
6085
+ const messages = await client.getMessages({
6086
+ filter: {
6087
+ appAddress: NULL_ADDRESS,
6088
+ maker: address
6089
+ },
6090
+ startIndex,
6091
+ endIndex: count
6092
+ });
6093
+ if (options.json) {
6094
+ printJson(messages.map((msg, i) => postToJson(msg, i)));
6095
+ } else {
6096
+ console.log(
6097
+ chalk4.white(`Found ${messages.length} post(s) by ${address}:
6098
+ `)
6099
+ );
6100
+ messages.forEach((msg, i) => {
6101
+ console.log(formatPost(msg, i, { showTopic: true }));
6102
+ if (i < messages.length - 1) {
6103
+ console.log();
6104
+ }
6105
+ });
6106
+ }
6107
+ } catch (error) {
6108
+ exitWithError(
6109
+ `Failed to fetch posts: ${error instanceof Error ? error.message : String(error)}`
6110
+ );
6111
+ }
6112
+ }
6113
+ function registerFeedPostsCommand(parent) {
6114
+ parent.command("posts <address>").description("View posts by an address").option(
6115
+ "--limit <n>",
6116
+ "Maximum number of posts to display",
6117
+ (value) => parseInt(value, 10)
6118
+ ).option(
6119
+ "--chain-id <id>",
6120
+ "Chain ID (default: 8453 for Base)",
6121
+ (value) => parseInt(value, 10)
6122
+ ).option("--rpc-url <url>", "Custom RPC URL").option("--json", "Output in JSON format").action(async (address, options) => {
6123
+ await executeFeedPosts(address, options);
6124
+ });
6125
+ }
6126
+ async function confirm(message) {
6127
+ const rl = readline.createInterface({
6128
+ input: process.stdin,
6129
+ output: process.stdout
6130
+ });
6131
+ return new Promise((resolve3) => {
6132
+ rl.question(`${message} (y/N): `, (answer) => {
6133
+ rl.close();
6134
+ resolve3(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
6135
+ });
6136
+ });
6137
+ }
6138
+
6139
+ // src/commands/feed/config.ts
6140
+ async function executeFeedConfig(options) {
6141
+ if (options.reset) {
6142
+ const statePath = getStateFilePath();
6143
+ console.log(chalk4.yellow(`This will delete all stored state at:`));
6144
+ console.log(chalk4.white(` ${statePath}`));
6145
+ console.log(chalk4.yellow(`
6146
+ This includes:`));
6147
+ console.log(chalk4.white(` - All "last seen" timestamps for feeds`));
6148
+ console.log(chalk4.white(` - Your configured address`));
6149
+ console.log(chalk4.white(` - Your activity history`));
6150
+ if (!options.force) {
6151
+ const confirmed = await confirm(chalk4.red("\nAre you sure you want to reset?"));
6152
+ if (!confirmed) {
6153
+ console.log(chalk4.gray("Cancelled."));
6154
+ return;
6155
+ }
6156
+ }
6157
+ resetState();
6158
+ console.log(chalk4.green("State reset successfully."));
6159
+ return;
6160
+ }
6161
+ if (options.myAddress) {
6162
+ if (!options.myAddress.match(/^0x[a-fA-F0-9]{40}$/)) {
6163
+ console.error(chalk4.red("Invalid address format. Expected 0x followed by 40 hex characters."));
6164
+ process.exit(1);
6165
+ }
6166
+ setMyAddress(options.myAddress);
6167
+ console.log(chalk4.green(`Set my address to: ${options.myAddress}`));
6168
+ return;
6169
+ }
6170
+ if (options.clearAddress) {
6171
+ clearMyAddress();
6172
+ console.log(chalk4.green("Cleared my address."));
6173
+ return;
6174
+ }
6175
+ const state = getFullState();
6176
+ const myAddress = getMyAddress();
6177
+ console.log(chalk4.cyan("Feed Configuration\n"));
6178
+ console.log(chalk4.white(`State file: ${getStateFilePath()}`));
6179
+ console.log(chalk4.white(`My address: ${myAddress ?? chalk4.gray("(not set)")}`));
6180
+ const feedCount = Object.keys(state.feeds).length;
6181
+ console.log(chalk4.white(`Tracked feeds: ${feedCount}`));
6182
+ const historyCount = getHistoryCount();
6183
+ console.log(chalk4.white(`History entries: ${historyCount}`));
6184
+ if (feedCount > 0 && feedCount <= 20) {
6185
+ console.log(chalk4.gray("\nLast seen timestamps:"));
6186
+ for (const [feed, data] of Object.entries(state.feeds)) {
6187
+ const date = new Date(data.lastSeenTimestamp * 1e3);
6188
+ console.log(chalk4.gray(` ${feed}: ${date.toLocaleString()}`));
6189
+ }
6190
+ } else if (feedCount > 20) {
6191
+ console.log(chalk4.gray(`
6192
+ (${feedCount} feeds tracked, use --json for full list)`));
6193
+ }
6194
+ const activeFeeds = getActiveFeeds();
6195
+ if (activeFeeds.length > 0) {
6196
+ console.log(chalk4.cyan("\nActive Feeds:"));
6197
+ const displayFeeds = activeFeeds.slice(0, 10);
6198
+ for (const feed of displayFeeds) {
6199
+ const activity = [];
6200
+ if (feed.postCount > 0) activity.push(`${feed.postCount} post${feed.postCount !== 1 ? "s" : ""}`);
6201
+ if (feed.commentCount > 0) activity.push(`${feed.commentCount} comment${feed.commentCount !== 1 ? "s" : ""}`);
6202
+ const lastActive = formatTimestamp(feed.lastActivity);
6203
+ console.log(chalk4.white(` ${feed.feed}`) + chalk4.gray(` \u2022 ${activity.join(", ")} \u2022 ${lastActive}`));
6204
+ }
6205
+ if (activeFeeds.length > 10) {
6206
+ console.log(chalk4.gray(` ... and ${activeFeeds.length - 10} more`));
6207
+ }
6208
+ }
6209
+ const contacts = getContacts();
6210
+ if (contacts.length > 0) {
6211
+ console.log(chalk4.cyan("\nRecent Contacts (DMs):"));
6212
+ const displayContacts = contacts.slice(0, 10);
6213
+ for (const contact of displayContacts) {
6214
+ const truncAddr = `${contact.address.slice(0, 6)}...${contact.address.slice(-4)}`;
6215
+ const msgCount = contact.interactionCount;
6216
+ const lastActive = formatTimestamp(contact.lastInteraction);
6217
+ console.log(
6218
+ chalk4.white(` ${truncAddr}`) + chalk4.gray(` \u2022 ${msgCount} message${msgCount !== 1 ? "s" : ""} \u2022 ${lastActive}`)
6219
+ );
6220
+ }
6221
+ if (contacts.length > 10) {
6222
+ console.log(chalk4.gray(` ... and ${contacts.length - 10} more`));
6223
+ }
6224
+ }
6225
+ }
6226
+ function registerFeedConfigCommand(parent) {
6227
+ parent.command("config").description("View or modify feed configuration").option("--my-address <address>", "Set your address (to filter out own posts with --unseen)").option("--clear-address", "Clear your configured address").option("--show", "Show current configuration (default)").option("--reset", "Reset all state (clears last-seen timestamps and address)").option("--force", "Skip confirmation prompt for --reset").action(async (options) => {
6228
+ await executeFeedConfig(options);
6229
+ });
6230
+ }
6231
+ function formatHistoryEntry(entry, index) {
6232
+ const timestamp = formatTimestamp(entry.timestamp);
6233
+ const typeColor = entry.type === "post" ? chalk4.green : entry.type === "comment" ? chalk4.blue : chalk4.yellow;
6234
+ const lines = [
6235
+ chalk4.cyan(`[${index}]`) + ` ${chalk4.gray(timestamp)} ` + typeColor(entry.type.toUpperCase()),
6236
+ ` ${chalk4.white("Feed:")} ${entry.feed}`,
6237
+ ` ${chalk4.white("Tx:")} ${entry.txHash}`
6238
+ ];
6239
+ if (entry.sender) {
6240
+ lines.push(` ${chalk4.white("Sender:")} ${truncateAddress(entry.sender)}`);
6241
+ }
6242
+ if (entry.text) {
6243
+ const truncatedText = entry.text.length > 80 ? entry.text.slice(0, 80) + "..." : entry.text;
6244
+ lines.push(` ${chalk4.white("Text:")} ${truncatedText}`);
6245
+ }
6246
+ if (entry.type === "post" && entry.postId) {
6247
+ lines.push(` ${chalk4.white("Post ID:")} ${entry.postId}`);
6248
+ } else if (entry.type === "comment" && entry.postId) {
6249
+ lines.push(` ${chalk4.white("Reply to:")} ${entry.postId}`);
6250
+ }
6251
+ if (entry.type === "post" && entry.postId) {
6252
+ lines.push(
6253
+ chalk4.gray(` \u2192 Check replies: netp feed comments ${entry.feed} ${entry.postId}`)
6254
+ );
6255
+ } else if (entry.type === "post" && entry.sender) {
6256
+ lines.push(
6257
+ chalk4.gray(` \u2192 Find post: netp feed read ${entry.feed} --sender ${entry.sender} --json`)
6258
+ );
6259
+ } else if (entry.type === "comment" && entry.postId) {
6260
+ lines.push(
6261
+ chalk4.gray(` \u2192 See thread: netp feed comments ${entry.feed} ${entry.postId}`)
6262
+ );
6263
+ }
6264
+ return lines.join("\n");
6265
+ }
6266
+ function historyEntryToJson(entry, index) {
6267
+ const result = {
6268
+ index,
6269
+ type: entry.type,
6270
+ timestamp: entry.timestamp,
6271
+ txHash: entry.txHash,
6272
+ chainId: entry.chainId,
6273
+ feed: entry.feed
6274
+ };
6275
+ if (entry.sender) {
6276
+ result.sender = entry.sender;
6277
+ }
6278
+ if (entry.text) {
6279
+ result.text = entry.text;
6280
+ }
6281
+ if (entry.postId) {
6282
+ result.postId = entry.postId;
6283
+ }
6284
+ return result;
6285
+ }
6286
+ function validateType(type) {
6287
+ const validTypes = ["post", "comment", "register"];
6288
+ if (!validTypes.includes(type)) {
6289
+ console.error(
6290
+ chalk4.red(
6291
+ `Invalid type "${type}". Must be one of: ${validTypes.join(", ")}`
6292
+ )
6293
+ );
6294
+ process.exit(1);
6295
+ }
6296
+ return type;
6297
+ }
6298
+ async function executeFeedHistory(options) {
6299
+ if (options.clear) {
6300
+ const count = getHistoryCount();
6301
+ if (count === 0) {
6302
+ console.log(chalk4.gray("History is already empty."));
6303
+ return;
6304
+ }
6305
+ console.log(chalk4.yellow(`This will delete ${count} history entries.`));
6306
+ if (!options.force) {
6307
+ const confirmed = await confirm(
6308
+ chalk4.red("\nAre you sure you want to clear history?")
6309
+ );
6310
+ if (!confirmed) {
6311
+ console.log(chalk4.gray("Cancelled."));
6312
+ return;
6313
+ }
6314
+ }
6315
+ clearHistory();
6316
+ console.log(chalk4.green("History cleared."));
6317
+ return;
6318
+ }
6319
+ let entries;
6320
+ if (options.type) {
6321
+ const validType = validateType(options.type);
6322
+ entries = getHistoryByType(validType, options.limit);
6323
+ } else {
6324
+ entries = getHistory(options.limit);
6325
+ }
6326
+ if (entries.length === 0) {
6327
+ if (options.json) {
6328
+ printJson([]);
6329
+ } else {
6330
+ console.log(chalk4.gray("No history entries found."));
6331
+ console.log(
6332
+ chalk4.gray(
6333
+ "History is recorded when you post, comment, or register feeds."
6334
+ )
6335
+ );
6336
+ }
6337
+ return;
6338
+ }
6339
+ if (options.json) {
6340
+ const jsonEntries = entries.map(
6341
+ (entry, idx) => historyEntryToJson(entry, idx)
6342
+ );
6343
+ printJson(jsonEntries);
6344
+ } else {
6345
+ const totalCount = getHistoryCount();
6346
+ const typeFilter = options.type ? ` (type: ${options.type})` : "";
6347
+ console.log(
6348
+ chalk4.cyan(`Feed History${typeFilter} (${entries.length} of ${totalCount})
6349
+ `)
6350
+ );
6351
+ for (let i = 0; i < entries.length; i++) {
6352
+ console.log(formatHistoryEntry(entries[i], i));
6353
+ if (i < entries.length - 1) {
6354
+ console.log("");
6355
+ }
6356
+ }
6357
+ }
6358
+ }
6359
+ function registerFeedHistoryCommand(parent) {
6360
+ parent.command("history").description("View feed activity history (posts, comments, registrations)").option(
6361
+ "--limit <n>",
6362
+ "Limit number of entries",
6363
+ (value) => parseInt(value, 10)
6364
+ ).option("--type <type>", "Filter by type: post, comment, or register").option("--json", "Output as JSON").option("--clear", "Clear all history").option("--force", "Skip confirmation prompt for --clear").action(async (options) => {
6365
+ await executeFeedHistory(options);
6366
+ });
6367
+ }
6368
+
6369
+ // src/commands/feed/index.ts
6370
+ function registerFeedCommand(program2) {
6371
+ const feedCommand = program2.command("feed").description("Feed operations (read/write posts, comments, manage feeds)");
6372
+ registerFeedListCommand(feedCommand);
6373
+ registerFeedReadCommand(feedCommand);
6374
+ registerFeedPostCommand(feedCommand);
6375
+ registerFeedCommentWriteCommand(feedCommand);
6376
+ registerFeedCommentReadCommand(feedCommand);
6377
+ registerFeedRegisterCommand(feedCommand);
6378
+ registerFeedRepliesCommand(feedCommand);
6379
+ registerFeedPostsCommand(feedCommand);
6380
+ registerFeedConfigCommand(feedCommand);
6381
+ registerFeedHistoryCommand(feedCommand);
4155
6382
  }
4156
6383
 
4157
6384
  // src/cli/index.ts
@@ -4171,6 +6398,7 @@ registerInfoCommand(program);
4171
6398
  registerTokenCommand(program);
4172
6399
  registerProfileCommand(program);
4173
6400
  registerBazaarCommand(program);
6401
+ registerFeedCommand(program);
4174
6402
  program.parse();
4175
6403
  //# sourceMappingURL=index.mjs.map
4176
6404
  //# sourceMappingURL=index.mjs.map