clawntenna 0.8.3 → 0.8.5

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.
package/dist/cli/index.js CHANGED
@@ -20,7 +20,7 @@ var CHAINS = {
20
20
  registry: "0xf39b193aedC1Ec9FD6C5ccc24fBAe58ba9f52413",
21
21
  keyManager: "0x5562B553a876CBdc8AA4B3fb0687f22760F4759e",
22
22
  schemaRegistry: "0xB7eB50e9058198b99b5b2589E6D70b2d99d5440a",
23
- identityRegistry: "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432"
23
+ identityRegistry: "0x8004AA63c570c570eBF15376c0dB199918BFe9Fb"
24
24
  },
25
25
  base: {
26
26
  chainId: 8453,
@@ -3374,14 +3374,23 @@ var Clawntenna = class {
3374
3374
  */
3375
3375
  async readMessages(topicId, options) {
3376
3376
  const limit = options?.limit ?? 50;
3377
- const fromBlock = options?.fromBlock ?? -1e5;
3378
3377
  const key = await this.getEncryptionKey(topicId);
3379
3378
  const filter = this.registry.filters.MessageSent(topicId);
3380
- const events = await this.registry.queryFilter(filter, fromBlock);
3379
+ const CHUNK_SIZE = 2e3;
3380
+ const currentBlock = await this.provider.getBlockNumber();
3381
+ const maxRange = options?.fromBlock != null ? currentBlock - options.fromBlock : 1e5;
3382
+ const startBlock = currentBlock - maxRange;
3383
+ const allEvents = [];
3384
+ let toBlock = currentBlock;
3385
+ while (toBlock > startBlock && allEvents.length < limit) {
3386
+ const chunkFrom = Math.max(toBlock - CHUNK_SIZE + 1, startBlock);
3387
+ const events = await this.registry.queryFilter(filter, chunkFrom, toBlock);
3388
+ allEvents.unshift(...events);
3389
+ toBlock = chunkFrom - 1;
3390
+ }
3391
+ const recent = allEvents.slice(-limit);
3381
3392
  const messages = [];
3382
- const recent = events.slice(-limit);
3383
- for (const event of recent) {
3384
- const log = event;
3393
+ for (const log of recent) {
3385
3394
  const payloadStr = ethers.toUtf8String(log.args.payload);
3386
3395
  const parsed = decryptMessage(payloadStr, key);
3387
3396
  messages.push({
@@ -3620,6 +3629,12 @@ var Clawntenna = class {
3620
3629
  async grantKeyAccess(topicId, userAddress, topicKey) {
3621
3630
  if (!this.wallet) throw new Error("Wallet required");
3622
3631
  if (!this.ecdhPrivateKey) throw new Error("ECDH key not derived yet");
3632
+ const hasKey = await this.keyManager.hasPublicKey(userAddress);
3633
+ if (!hasKey) {
3634
+ throw new Error(
3635
+ `User ${userAddress} has no ECDH public key registered. They must run 'keys register' first.`
3636
+ );
3637
+ }
3623
3638
  const userPubKeyBytes = ethers.getBytes(await this.keyManager.getPublicKey(userAddress));
3624
3639
  const encrypted = encryptTopicKeyForUser(topicKey, this.ecdhPrivateKey, userPubKeyBytes);
3625
3640
  return this.keyManager.grantKeyAccess(topicId, userAddress, encrypted);
@@ -3993,8 +4008,11 @@ var Clawntenna = class {
3993
4008
  if (storedKey) return storedKey;
3994
4009
  const topic = await this.getTopic(topicId);
3995
4010
  if (topic.accessLevel === 2 /* PRIVATE */) {
4011
+ if (this.ecdhPrivateKey) {
4012
+ return this.fetchAndDecryptTopicKey(topicId);
4013
+ }
3996
4014
  throw new Error(
3997
- `Topic ${topicId} is PRIVATE. Call fetchAndDecryptTopicKey() or setTopicKey() first.`
4015
+ `Topic ${topicId} is PRIVATE. Load ECDH keys first (loadECDHKeypair or deriveECDHFromWallet), then call fetchAndDecryptTopicKey() or setTopicKey().`
3998
4016
  );
3999
4017
  }
4000
4018
  return derivePublicTopicKey(topicId);
@@ -4033,6 +4051,14 @@ function outputError(message, json) {
4033
4051
  }
4034
4052
  process.exit(1);
4035
4053
  }
4054
+ function chainIdForCredentials(chain) {
4055
+ const map = {
4056
+ base: "8453",
4057
+ baseSepolia: "84532",
4058
+ avalanche: "43114"
4059
+ };
4060
+ return map[chain] ?? "8453";
4061
+ }
4036
4062
  function bigintReplacer(_key, value) {
4037
4063
  return typeof value === "bigint" ? value.toString() : value;
4038
4064
  }
@@ -4168,6 +4194,12 @@ async function send(topicId, message, flags) {
4168
4194
  const client = loadClient(flags);
4169
4195
  const json = flags.json ?? false;
4170
4196
  const noWait = flags.noWait ?? false;
4197
+ const creds = loadCredentials();
4198
+ const chainId = chainIdForCredentials(flags.chain);
4199
+ const ecdhCreds = creds?.chains[chainId]?.ecdh;
4200
+ if (ecdhCreds?.privateKey) {
4201
+ client.loadECDHKeypair(ecdhCreds.privateKey);
4202
+ }
4171
4203
  if (!json) console.log(`Sending to topic ${topicId} on ${flags.chain}...`);
4172
4204
  const sendOptions = {
4173
4205
  replyTo: flags.replyTo,
@@ -4208,6 +4240,12 @@ async function send(topicId, message, flags) {
4208
4240
  async function read(topicId, flags) {
4209
4241
  const client = loadClient(flags, false);
4210
4242
  const json = flags.json ?? false;
4243
+ const creds = loadCredentials();
4244
+ const chainId = chainIdForCredentials(flags.chain);
4245
+ const ecdhCreds = creds?.chains[chainId]?.ecdh;
4246
+ if (ecdhCreds?.privateKey) {
4247
+ client.loadECDHKeypair(ecdhCreds.privateKey);
4248
+ }
4211
4249
  if (!json) console.log(`Reading topic ${topicId} on ${flags.chain} (last ${flags.limit} messages)...
4212
4250
  `);
4213
4251
  const messages = await client.readMessages(topicId, { limit: flags.limit });
@@ -4284,7 +4322,7 @@ async function whoami(appId, flags) {
4284
4322
  }
4285
4323
  const creds = loadCredentials();
4286
4324
  if (creds) {
4287
- const chainId = flags.chain === "base" ? "8453" : "43114";
4325
+ const chainId = chainIdForCredentials(flags.chain);
4288
4326
  const chainCreds = creds.chains[chainId];
4289
4327
  result.ecdhRegistered = chainCreds?.ecdh?.registered ?? false;
4290
4328
  }
@@ -4305,6 +4343,7 @@ async function whoami(appId, flags) {
4305
4343
  }
4306
4344
 
4307
4345
  // src/cli/app.ts
4346
+ import { ethers as ethers4 } from "ethers";
4308
4347
  async function appInfo(appId, flags) {
4309
4348
  const client = loadClient(flags, false);
4310
4349
  const json = flags.json ?? false;
@@ -4341,9 +4380,22 @@ async function appCreate(name, description, url, isPublic, flags) {
4341
4380
  const tx = await client.createApplication(name, description, url, isPublic);
4342
4381
  if (!json) console.log(`TX submitted: ${tx.hash}`);
4343
4382
  const receipt = await tx.wait();
4383
+ let appId = null;
4384
+ if (receipt) {
4385
+ const iface = new ethers4.Interface(REGISTRY_ABI);
4386
+ const parsed = receipt.logs.map((l) => {
4387
+ try {
4388
+ return iface.parseLog(l);
4389
+ } catch {
4390
+ return null;
4391
+ }
4392
+ }).find((l) => l?.name === "ApplicationCreated");
4393
+ appId = parsed?.args?.applicationId?.toString() ?? null;
4394
+ }
4344
4395
  if (json) {
4345
- output({ txHash: tx.hash, blockNumber: receipt?.blockNumber, chain: flags.chain }, true);
4396
+ output({ txHash: tx.hash, blockNumber: receipt?.blockNumber, appId, chain: flags.chain }, true);
4346
4397
  } else {
4398
+ if (appId) console.log(`Application created with ID: ${appId}`);
4347
4399
  console.log(`Confirmed in block ${receipt?.blockNumber}`);
4348
4400
  }
4349
4401
  }
@@ -4362,6 +4414,7 @@ async function appUpdateUrl(appId, url, flags) {
4362
4414
  }
4363
4415
 
4364
4416
  // src/cli/topics.ts
4417
+ import { ethers as ethers5 } from "ethers";
4365
4418
  var ACCESS_NAMES = ["public", "limited", "private"];
4366
4419
  async function topicsList(appId, flags) {
4367
4420
  const client = loadClient(flags, false);
@@ -4441,9 +4494,22 @@ async function topicCreate(appId, name, description, access, flags) {
4441
4494
  if (!json) console.log(`Creating topic "${name}" in app ${appId} (${access})...`);
4442
4495
  const tx = await client.createTopic(appId, name, description, level);
4443
4496
  const receipt = await tx.wait();
4497
+ let topicId = null;
4498
+ if (receipt) {
4499
+ const iface = new ethers5.Interface(REGISTRY_ABI);
4500
+ const parsed = receipt.logs.map((l) => {
4501
+ try {
4502
+ return iface.parseLog(l);
4503
+ } catch {
4504
+ return null;
4505
+ }
4506
+ }).find((l) => l?.name === "TopicCreated");
4507
+ topicId = parsed?.args?.topicId?.toString() ?? null;
4508
+ }
4444
4509
  if (json) {
4445
- output({ txHash: tx.hash, blockNumber: receipt?.blockNumber, appId, access }, true);
4510
+ output({ txHash: tx.hash, blockNumber: receipt?.blockNumber, topicId, appId, access }, true);
4446
4511
  } else {
4512
+ if (topicId) console.log(`Topic created with ID: ${topicId}`);
4447
4513
  console.log(`TX: ${tx.hash}`);
4448
4514
  console.log(`Confirmed in block ${receipt?.blockNumber}`);
4449
4515
  }
@@ -4488,10 +4554,12 @@ async function nicknameClear(appId, flags) {
4488
4554
  }
4489
4555
 
4490
4556
  // src/cli/members.ts
4557
+ import { ethers as ethers6 } from "ethers";
4491
4558
  async function membersList(appId, flags) {
4492
4559
  const client = loadClient(flags, false);
4493
4560
  const json = flags.json ?? false;
4494
- const addresses = await client.getApplicationMembers(appId);
4561
+ const raw = await client.getApplicationMembers(appId);
4562
+ const addresses = [...new Set(raw)].filter((a) => a !== ethers6.ZeroAddress);
4495
4563
  const members = await Promise.all(
4496
4564
  addresses.map(async (addr) => {
4497
4565
  const m = await client.getMember(appId, addr);
@@ -4796,7 +4864,7 @@ async function keysRegister(flags) {
4796
4864
  const client = loadClient(flags);
4797
4865
  const json = flags.json ?? false;
4798
4866
  const creds = loadCredentials();
4799
- const chainId = flags.chain === "base" ? "8453" : "43114";
4867
+ const chainId = chainIdForCredentials(flags.chain);
4800
4868
  const ecdhCreds = creds?.chains[chainId]?.ecdh;
4801
4869
  if (ecdhCreds?.privateKey) {
4802
4870
  client.loadECDHKeypair(ecdhCreds.privateKey);
@@ -4828,7 +4896,7 @@ async function keysGrant(topicId, address, flags) {
4828
4896
  const client = loadClient(flags);
4829
4897
  const json = flags.json ?? false;
4830
4898
  const creds = loadCredentials();
4831
- const chainId = flags.chain === "base" ? "8453" : "43114";
4899
+ const chainId = chainIdForCredentials(flags.chain);
4832
4900
  const ecdhCreds = creds?.chains[chainId]?.ecdh;
4833
4901
  if (ecdhCreds?.privateKey) {
4834
4902
  client.loadECDHKeypair(ecdhCreds.privateKey);
@@ -4940,7 +5008,7 @@ async function subscribe(topicId, flags) {
4940
5008
  }
4941
5009
 
4942
5010
  // src/cli/fees.ts
4943
- import { ethers as ethers4 } from "ethers";
5011
+ import { ethers as ethers7 } from "ethers";
4944
5012
  async function feeTopicCreationSet(appId, token, amount, flags) {
4945
5013
  const client = loadClient(flags);
4946
5014
  const json = flags.json ?? false;
@@ -4971,7 +5039,7 @@ async function feeMessageGet(topicId, flags) {
4971
5039
  const client = loadClient(flags, false);
4972
5040
  const json = flags.json ?? false;
4973
5041
  const fee = await client.getTopicMessageFee(topicId);
4974
- const isZero = fee.token === ethers4.ZeroAddress && fee.amount === 0n;
5042
+ const isZero = fee.token === ethers7.ZeroAddress && fee.amount === 0n;
4975
5043
  if (json) {
4976
5044
  output({ topicId, token: fee.token, amount: fee.amount.toString() }, true);
4977
5045
  } else {
@@ -4985,8 +5053,57 @@ async function feeMessageGet(topicId, flags) {
4985
5053
  }
4986
5054
  }
4987
5055
 
5056
+ // src/cli/errors.ts
5057
+ var ERROR_MAP = {
5058
+ "0xea8e4eb5": "NotAuthorized \u2014 you lack permission for this action",
5059
+ "0x291fc442": "NotMember \u2014 address is not a member of this app",
5060
+ "0x810074be": "AlreadyMember \u2014 address is already a member",
5061
+ "0x5e03d55f": "CannotRemoveSelf \u2014 owner cannot remove themselves",
5062
+ "0x17b29d2e": "ApplicationNotFound \u2014 app ID does not exist",
5063
+ "0x04a29d55": "TopicNotFound \u2014 topic ID does not exist",
5064
+ "0x430f13b3": "InvalidName \u2014 name is empty or invalid",
5065
+ "0x9e4b2685": "NameTaken \u2014 that name is already in use",
5066
+ "0xa2d0fee8": "InvalidPublicKey \u2014 must be 33-byte compressed secp256k1 key",
5067
+ "0x16ea6d54": "PublicKeyNotRegistered \u2014 user has no ECDH key (run: keys register)",
5068
+ "0x5303c506": "InvalidEncryptedKey \u2014 encrypted key too short or malformed",
5069
+ "0xf4d678b8": "InsufficientBalance \u2014 not enough tokens",
5070
+ "0x13be252b": "InsufficientAllowance \u2014 token allowance too low",
5071
+ "0x0c79a8da": "InvalidAccessLevel \u2014 use public, limited, or private",
5072
+ "0x15b3521e": "NicknameCooldownActive \u2014 wait before changing nickname again",
5073
+ "0xae0ca2dd": "SchemaNotFound \u2014 schema ID does not exist",
5074
+ "0x03230700": "AppNameTaken \u2014 schema name already used in this app"
5075
+ };
5076
+ function decodeContractError(err) {
5077
+ if (!(err instanceof Error)) return String(err);
5078
+ const message = err.message;
5079
+ const dataMatch = message.match(/data="(0x[0-9a-fA-F]+)"/) ?? message.match(/error=\{[^}]*"data":"(0x[0-9a-fA-F]+)"/) ?? message.match(/(0x[0-9a-fA-F]{8})/);
5080
+ if (dataMatch) {
5081
+ const selector = dataMatch[1].slice(0, 10).toLowerCase();
5082
+ const decoded = ERROR_MAP[selector];
5083
+ if (decoded) return decoded;
5084
+ }
5085
+ const anyErr = err;
5086
+ if (typeof anyErr.data === "string" && anyErr.data.startsWith("0x")) {
5087
+ const selector = anyErr.data.slice(0, 10).toLowerCase();
5088
+ const decoded = ERROR_MAP[selector];
5089
+ if (decoded) return decoded;
5090
+ }
5091
+ if (anyErr.info && typeof anyErr.info === "object") {
5092
+ const info = anyErr.info;
5093
+ if (info.error && typeof info.error === "object") {
5094
+ const innerErr = info.error;
5095
+ if (typeof innerErr.data === "string" && innerErr.data.startsWith("0x")) {
5096
+ const selector = innerErr.data.slice(0, 10).toLowerCase();
5097
+ const decoded = ERROR_MAP[selector];
5098
+ if (decoded) return decoded;
5099
+ }
5100
+ }
5101
+ }
5102
+ return message;
5103
+ }
5104
+
4988
5105
  // src/cli/index.ts
4989
- var VERSION = "0.8.3";
5106
+ var VERSION = "0.8.4";
4990
5107
  var HELP = `
4991
5108
  clawntenna v${VERSION}
4992
5109
  On-chain encrypted messaging for AI agents
@@ -5060,7 +5177,7 @@ var HELP = `
5060
5177
  fee message get <topicId> Get message fee
5061
5178
 
5062
5179
  Options:
5063
- --chain <base|avalanche> Chain to use (default: base)
5180
+ --chain <base|avalanche|baseSepolia> Chain to use (default: base)
5064
5181
  --key <privateKey> Private key (overrides credentials)
5065
5182
  --limit <N> Number of messages to read (default: 20)
5066
5183
  --json Output as JSON
@@ -5433,7 +5550,7 @@ async function main() {
5433
5550
  outputError(`Unknown command: ${command}. Run 'clawntenna --help' for usage.`, json);
5434
5551
  }
5435
5552
  } catch (err) {
5436
- const message = err instanceof Error ? err.message : String(err);
5553
+ const message = decodeContractError(err);
5437
5554
  if (json) {
5438
5555
  console.error(JSON.stringify({ error: message }));
5439
5556
  } else {
package/dist/index.cjs CHANGED
@@ -75,7 +75,7 @@ var CHAINS = {
75
75
  registry: "0xf39b193aedC1Ec9FD6C5ccc24fBAe58ba9f52413",
76
76
  keyManager: "0x5562B553a876CBdc8AA4B3fb0687f22760F4759e",
77
77
  schemaRegistry: "0xB7eB50e9058198b99b5b2589E6D70b2d99d5440a",
78
- identityRegistry: "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432"
78
+ identityRegistry: "0x8004AA63c570c570eBF15376c0dB199918BFe9Fb"
79
79
  },
80
80
  base: {
81
81
  chainId: 8453,
@@ -531,14 +531,23 @@ var Clawntenna = class {
531
531
  */
532
532
  async readMessages(topicId, options) {
533
533
  const limit = options?.limit ?? 50;
534
- const fromBlock = options?.fromBlock ?? -1e5;
535
534
  const key = await this.getEncryptionKey(topicId);
536
535
  const filter = this.registry.filters.MessageSent(topicId);
537
- const events = await this.registry.queryFilter(filter, fromBlock);
536
+ const CHUNK_SIZE = 2e3;
537
+ const currentBlock = await this.provider.getBlockNumber();
538
+ const maxRange = options?.fromBlock != null ? currentBlock - options.fromBlock : 1e5;
539
+ const startBlock = currentBlock - maxRange;
540
+ const allEvents = [];
541
+ let toBlock = currentBlock;
542
+ while (toBlock > startBlock && allEvents.length < limit) {
543
+ const chunkFrom = Math.max(toBlock - CHUNK_SIZE + 1, startBlock);
544
+ const events = await this.registry.queryFilter(filter, chunkFrom, toBlock);
545
+ allEvents.unshift(...events);
546
+ toBlock = chunkFrom - 1;
547
+ }
548
+ const recent = allEvents.slice(-limit);
538
549
  const messages = [];
539
- const recent = events.slice(-limit);
540
- for (const event of recent) {
541
- const log = event;
550
+ for (const log of recent) {
542
551
  const payloadStr = import_ethers.ethers.toUtf8String(log.args.payload);
543
552
  const parsed = decryptMessage(payloadStr, key);
544
553
  messages.push({
@@ -777,6 +786,12 @@ var Clawntenna = class {
777
786
  async grantKeyAccess(topicId, userAddress, topicKey) {
778
787
  if (!this.wallet) throw new Error("Wallet required");
779
788
  if (!this.ecdhPrivateKey) throw new Error("ECDH key not derived yet");
789
+ const hasKey = await this.keyManager.hasPublicKey(userAddress);
790
+ if (!hasKey) {
791
+ throw new Error(
792
+ `User ${userAddress} has no ECDH public key registered. They must run 'keys register' first.`
793
+ );
794
+ }
780
795
  const userPubKeyBytes = import_ethers.ethers.getBytes(await this.keyManager.getPublicKey(userAddress));
781
796
  const encrypted = encryptTopicKeyForUser(topicKey, this.ecdhPrivateKey, userPubKeyBytes);
782
797
  return this.keyManager.grantKeyAccess(topicId, userAddress, encrypted);
@@ -1150,8 +1165,11 @@ var Clawntenna = class {
1150
1165
  if (storedKey) return storedKey;
1151
1166
  const topic = await this.getTopic(topicId);
1152
1167
  if (topic.accessLevel === 2 /* PRIVATE */) {
1168
+ if (this.ecdhPrivateKey) {
1169
+ return this.fetchAndDecryptTopicKey(topicId);
1170
+ }
1153
1171
  throw new Error(
1154
- `Topic ${topicId} is PRIVATE. Call fetchAndDecryptTopicKey() or setTopicKey() first.`
1172
+ `Topic ${topicId} is PRIVATE. Load ECDH keys first (loadECDHKeypair or deriveECDHFromWallet), then call fetchAndDecryptTopicKey() or setTopicKey().`
1155
1173
  );
1156
1174
  }
1157
1175
  return derivePublicTopicKey(topicId);