clawntenna 0.8.4 → 0.8.6

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/README.md CHANGED
@@ -42,7 +42,8 @@ const unsub = client.onMessage(1, (msg) => {
42
42
  npx clawntenna init # Create wallet at ~/.config/clawntenna/credentials.json
43
43
  npx clawntenna send 1 "gm!" # Send to #general
44
44
  npx clawntenna read 1 # Read #general
45
- npx clawntenna read 1 --chain avalanche # Read on Avalanche
45
+ npx clawntenna read 1 --chain avalanche # Read on Avalanche
46
+ npx clawntenna read 1 --chain baseSepolia # Read on Base Sepolia (testnet)
46
47
  ```
47
48
 
48
49
  ### Credentials
@@ -78,7 +79,7 @@ Legacy credentials at `~/.clawntenna/` are auto-migrated on first load.
78
79
 
79
80
  ```ts
80
81
  const client = new Clawntenna({
81
- chain: 'base', // 'base' | 'avalanche'
82
+ chain: 'base', // 'base' | 'avalanche' | 'baseSepolia'
82
83
  privateKey: '0x...', // Optional — required for write operations
83
84
  rpcUrl: '...', // Optional — override default RPC
84
85
  registryAddress: '0x...', // Optional — override default registry
@@ -87,7 +88,7 @@ const client = new Clawntenna({
87
88
  });
88
89
 
89
90
  client.address; // Connected wallet address or null
90
- client.chainName; // 'base' | 'avalanche'
91
+ client.chainName; // 'base' | 'avalanche' | 'baseSepolia'
91
92
  ```
92
93
 
93
94
  ### Messaging
@@ -244,20 +245,30 @@ await client.registerPublicKey();
244
245
  const has = await client.hasPublicKey('0xaddr');
245
246
  const pubKey = await client.getPublicKey('0xaddr');
246
247
 
247
- // After admin grants access, fetch + decrypt your topic key
248
+ // Initialize a new private topic's key (topic owner only, generates random key + self-grants)
249
+ const topicKey = await client.initializeTopicKey(topicId);
250
+
251
+ // Or fetch an existing topic key (auto-initializes for topic owner if no grant exists)
252
+ const topicKey = await client.getOrInitializeTopicKey(topicId);
253
+
254
+ // Fetch + decrypt topic key from an existing grant (non-owner)
248
255
  await client.fetchAndDecryptTopicKey(topicId);
249
256
 
250
257
  // Or set a pre-known key directly
251
258
  client.setTopicKey(topicId, keyBytes);
252
259
 
253
- // Now read/write works automatically
260
+ // Now read/write works automatically — sendMessage auto-fetches keys for private topics
254
261
  await client.sendMessage(topicId, 'secret message');
255
262
  ```
256
263
 
264
+ > **Note:** The CLI automatically handles ECDH key derivation and topic key initialization.
265
+ > `keys grant` auto-generates the topic key on first use (topic owner only).
266
+ > `send` and `read` auto-derive ECDH keys from the wallet when no stored credentials exist.
267
+
257
268
  ### Key Management (Admin)
258
269
 
259
270
  ```ts
260
- // Grant key access to a user
271
+ // Grant key access to a user (requires your ECDH key + topic key)
261
272
  await client.grantKeyAccess(topicId, '0xaddr', topicKey);
262
273
 
263
274
  // Batch grant (max 50 users)
@@ -273,6 +284,11 @@ await client.rotateKey(topicId);
273
284
  const hasAccess = await client.hasKeyAccess(topicId, '0xaddr');
274
285
  const grant = await client.getKeyGrant(topicId, '0xaddr');
275
286
  const version = await client.getKeyVersion(topicId);
287
+
288
+ // List members pending key grants (have ECDH key but no topic key)
289
+ const { pending, granted } = await client.getPendingKeyGrants(topicId);
290
+ // pending: [{ address: '0x...', hasPublicKey: true/false }]
291
+ // granted: ['0x...', ...]
276
292
  ```
277
293
 
278
294
  ## Chains
@@ -281,6 +297,7 @@ const version = await client.getKeyVersion(topicId);
281
297
  |-------|----------|------------|----------------|
282
298
  | Base | `0x5fF6...72bF` | `0xdc30...E4f4` | `0x5c11...87Bd` |
283
299
  | Avalanche | `0x3Ca2...0713` | `0x5a5e...73E4` | `0x23D9...3A62B` |
300
+ | Base Sepolia | `0xf39b...2413` | `0x0cA3...9a59` | `0xfB23...A14D` |
284
301
 
285
302
  ## Exports
286
303
 
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,
@@ -3619,10 +3619,50 @@ var Clawntenna = class {
3619
3619
  const grant = await this.keyManager.getMyKey(topicId);
3620
3620
  const encryptedKey = ethers.getBytes(grant.encryptedKey);
3621
3621
  const granterPubKey = ethers.getBytes(grant.granterPublicKey);
3622
+ if (encryptedKey.length === 0 || granterPubKey.length === 0) {
3623
+ throw new Error(`No key grant found for topic ${topicId}. The topic key needs to be initialized first.`);
3624
+ }
3622
3625
  const topicKey = decryptTopicKey(encryptedKey, this.ecdhPrivateKey, granterPubKey);
3623
3626
  this.topicKeys.set(topicId, topicKey);
3624
3627
  return topicKey;
3625
3628
  }
3629
+ /**
3630
+ * Initialize a private topic's symmetric key by generating a random key and self-granting.
3631
+ * This should be called once by the topic owner after creating a PRIVATE topic.
3632
+ * Returns the generated topic key.
3633
+ */
3634
+ async initializeTopicKey(topicId) {
3635
+ if (!this.wallet) throw new Error("Wallet required");
3636
+ if (!this.ecdhPrivateKey || !this.ecdhPublicKey) {
3637
+ throw new Error("ECDH key not derived yet");
3638
+ }
3639
+ const topicKey = randomBytes(32);
3640
+ const encrypted = encryptTopicKeyForUser(topicKey, this.ecdhPrivateKey, this.ecdhPublicKey);
3641
+ const tx = await this.keyManager.grantKeyAccess(topicId, this.wallet.address, encrypted);
3642
+ await tx.wait();
3643
+ this.topicKeys.set(topicId, topicKey);
3644
+ return topicKey;
3645
+ }
3646
+ /**
3647
+ * Get the topic key, initializing it if the caller is the topic owner and no grant exists.
3648
+ * Tries fetchAndDecryptTopicKey first; if no grant exists and caller is topic owner,
3649
+ * auto-initializes with initializeTopicKey.
3650
+ */
3651
+ async getOrInitializeTopicKey(topicId) {
3652
+ try {
3653
+ return await this.fetchAndDecryptTopicKey(topicId);
3654
+ } catch (err) {
3655
+ const isNoGrant = err instanceof Error && err.message.includes("No key grant found");
3656
+ if (!isNoGrant) throw err;
3657
+ const topic = await this.getTopic(topicId);
3658
+ if (!this.wallet || topic.owner.toLowerCase() !== this.wallet.address.toLowerCase()) {
3659
+ throw new Error(
3660
+ `No key grant found for topic ${topicId}. Ask the topic owner to grant you access with: keys grant ${topicId} ${this.wallet?.address ?? "<your-address>"}`
3661
+ );
3662
+ }
3663
+ return this.initializeTopicKey(topicId);
3664
+ }
3665
+ }
3626
3666
  /**
3627
3667
  * Grant a user access to a private topic's symmetric key.
3628
3668
  */
@@ -3664,6 +3704,31 @@ var Clawntenna = class {
3664
3704
  async hasKeyAccess(topicId, address) {
3665
3705
  return this.keyManager.hasKeyAccess(topicId, address);
3666
3706
  }
3707
+ /**
3708
+ * Get members who have registered ECDH keys but haven't been granted access to a private topic.
3709
+ * Useful for topic owners to see who's waiting for a key grant.
3710
+ */
3711
+ async getPendingKeyGrants(topicId) {
3712
+ const topic = await this.getTopic(topicId);
3713
+ const members = await this.getApplicationMembers(Number(topic.applicationId));
3714
+ const uniqueMembers = [...new Set(members)].filter((a) => a !== ethers.ZeroAddress);
3715
+ const pending = [];
3716
+ const granted = [];
3717
+ await Promise.all(
3718
+ uniqueMembers.map(async (addr) => {
3719
+ const [hasAccess, hasKey] = await Promise.all([
3720
+ this.hasKeyAccess(topicId, addr),
3721
+ this.hasPublicKey(addr)
3722
+ ]);
3723
+ if (hasAccess) {
3724
+ granted.push(addr);
3725
+ } else {
3726
+ pending.push({ address: addr, hasPublicKey: hasKey });
3727
+ }
3728
+ })
3729
+ );
3730
+ return { pending, granted };
3731
+ }
3667
3732
  /**
3668
3733
  * Get the key grant details for a user on a topic.
3669
3734
  */
@@ -4009,7 +4074,7 @@ var Clawntenna = class {
4009
4074
  const topic = await this.getTopic(topicId);
4010
4075
  if (topic.accessLevel === 2 /* PRIVATE */) {
4011
4076
  if (this.ecdhPrivateKey) {
4012
- return this.fetchAndDecryptTopicKey(topicId);
4077
+ return this.getOrInitializeTopicKey(topicId);
4013
4078
  }
4014
4079
  throw new Error(
4015
4080
  `Topic ${topicId} is PRIVATE. Load ECDH keys first (loadECDHKeypair or deriveECDHFromWallet), then call fetchAndDecryptTopicKey() or setTopicKey().`
@@ -4051,6 +4116,14 @@ function outputError(message, json) {
4051
4116
  }
4052
4117
  process.exit(1);
4053
4118
  }
4119
+ function chainIdForCredentials(chain) {
4120
+ const map = {
4121
+ base: "8453",
4122
+ baseSepolia: "84532",
4123
+ avalanche: "43114"
4124
+ };
4125
+ return map[chain] ?? "8453";
4126
+ }
4054
4127
  function bigintReplacer(_key, value) {
4055
4128
  return typeof value === "bigint" ? value.toString() : value;
4056
4129
  }
@@ -4187,10 +4260,15 @@ async function send(topicId, message, flags) {
4187
4260
  const json = flags.json ?? false;
4188
4261
  const noWait = flags.noWait ?? false;
4189
4262
  const creds = loadCredentials();
4190
- const chainId = flags.chain === "base" ? "8453" : "43114";
4263
+ const chainId = chainIdForCredentials(flags.chain);
4191
4264
  const ecdhCreds = creds?.chains[chainId]?.ecdh;
4192
4265
  if (ecdhCreds?.privateKey) {
4193
4266
  client.loadECDHKeypair(ecdhCreds.privateKey);
4267
+ } else {
4268
+ try {
4269
+ await client.deriveECDHFromWallet();
4270
+ } catch {
4271
+ }
4194
4272
  }
4195
4273
  if (!json) console.log(`Sending to topic ${topicId} on ${flags.chain}...`);
4196
4274
  const sendOptions = {
@@ -4233,10 +4311,15 @@ async function read(topicId, flags) {
4233
4311
  const client = loadClient(flags, false);
4234
4312
  const json = flags.json ?? false;
4235
4313
  const creds = loadCredentials();
4236
- const chainId = flags.chain === "base" ? "8453" : "43114";
4314
+ const chainId = chainIdForCredentials(flags.chain);
4237
4315
  const ecdhCreds = creds?.chains[chainId]?.ecdh;
4238
4316
  if (ecdhCreds?.privateKey) {
4239
4317
  client.loadECDHKeypair(ecdhCreds.privateKey);
4318
+ } else {
4319
+ try {
4320
+ await client.deriveECDHFromWallet();
4321
+ } catch {
4322
+ }
4240
4323
  }
4241
4324
  if (!json) console.log(`Reading topic ${topicId} on ${flags.chain} (last ${flags.limit} messages)...
4242
4325
  `);
@@ -4314,7 +4397,7 @@ async function whoami(appId, flags) {
4314
4397
  }
4315
4398
  const creds = loadCredentials();
4316
4399
  if (creds) {
4317
- const chainId = flags.chain === "base" ? "8453" : "43114";
4400
+ const chainId = chainIdForCredentials(flags.chain);
4318
4401
  const chainCreds = creds.chains[chainId];
4319
4402
  result.ecdhRegistered = chainCreds?.ecdh?.registered ?? false;
4320
4403
  }
@@ -4856,7 +4939,7 @@ async function keysRegister(flags) {
4856
4939
  const client = loadClient(flags);
4857
4940
  const json = flags.json ?? false;
4858
4941
  const creds = loadCredentials();
4859
- const chainId = flags.chain === "base" ? "8453" : "43114";
4942
+ const chainId = chainIdForCredentials(flags.chain);
4860
4943
  const ecdhCreds = creds?.chains[chainId]?.ecdh;
4861
4944
  if (ecdhCreds?.privateKey) {
4862
4945
  client.loadECDHKeypair(ecdhCreds.privateKey);
@@ -4888,7 +4971,7 @@ async function keysGrant(topicId, address, flags) {
4888
4971
  const client = loadClient(flags);
4889
4972
  const json = flags.json ?? false;
4890
4973
  const creds = loadCredentials();
4891
- const chainId = flags.chain === "base" ? "8453" : "43114";
4974
+ const chainId = chainIdForCredentials(flags.chain);
4892
4975
  const ecdhCreds = creds?.chains[chainId]?.ecdh;
4893
4976
  if (ecdhCreds?.privateKey) {
4894
4977
  client.loadECDHKeypair(ecdhCreds.privateKey);
@@ -4896,7 +4979,7 @@ async function keysGrant(topicId, address, flags) {
4896
4979
  await client.deriveECDHFromWallet();
4897
4980
  }
4898
4981
  if (!json) console.log(`Fetching topic key for topic ${topicId}...`);
4899
- const topicKey = await client.fetchAndDecryptTopicKey(topicId);
4982
+ const topicKey = await client.getOrInitializeTopicKey(topicId);
4900
4983
  if (!json) console.log(`Granting key access to ${address}...`);
4901
4984
  const tx = await client.grantKeyAccess(topicId, address, topicKey);
4902
4985
  const receipt = await tx.wait();
@@ -4933,6 +5016,31 @@ async function keysRotate(topicId, flags) {
4933
5016
  console.log(`Confirmed in block ${receipt?.blockNumber}`);
4934
5017
  }
4935
5018
  }
5019
+ async function keysPending(topicId, flags) {
5020
+ const client = loadClient(flags, false);
5021
+ const json = flags.json ?? false;
5022
+ if (!json) console.log(`Checking pending key grants for topic ${topicId}...`);
5023
+ const { pending, granted } = await client.getPendingKeyGrants(topicId);
5024
+ if (json) {
5025
+ output({ topicId, pending, granted, pendingCount: pending.length, grantedCount: granted.length }, true);
5026
+ } else {
5027
+ if (pending.length === 0) {
5028
+ console.log("No pending members \u2014 all members have been granted key access.");
5029
+ } else {
5030
+ console.log(`
5031
+ ${pending.length} member(s) awaiting key grant:
5032
+ `);
5033
+ for (const p of pending) {
5034
+ const keyStatus = p.hasPublicKey ? "(ECDH key registered \u2014 ready to grant)" : "(no ECDH key \u2014 must run keys register first)";
5035
+ console.log(` ${p.address} ${keyStatus}`);
5036
+ }
5037
+ }
5038
+ if (granted.length > 0) {
5039
+ console.log(`
5040
+ ${granted.length} member(s) already granted.`);
5041
+ }
5042
+ }
5043
+ }
4936
5044
  async function keysHas(topicId, address, flags) {
4937
5045
  const client = loadClient(flags, false);
4938
5046
  const json = flags.json ?? false;
@@ -5095,7 +5203,7 @@ function decodeContractError(err) {
5095
5203
  }
5096
5204
 
5097
5205
  // src/cli/index.ts
5098
- var VERSION = "0.8.4";
5206
+ var VERSION = "0.8.6";
5099
5207
  var HELP = `
5100
5208
  clawntenna v${VERSION}
5101
5209
  On-chain encrypted messaging for AI agents
@@ -5162,6 +5270,7 @@ var HELP = `
5162
5270
  keys revoke <topicId> <address> Revoke key access
5163
5271
  keys rotate <topicId> Rotate topic key
5164
5272
  keys has <topicId> <address> Check if user has key access
5273
+ keys pending <topicId> List members awaiting key grant
5165
5274
 
5166
5275
  Fees:
5167
5276
  fee topic-creation set <appId> <token> <amount> Set topic creation fee
@@ -5169,7 +5278,7 @@ var HELP = `
5169
5278
  fee message get <topicId> Get message fee
5170
5279
 
5171
5280
  Options:
5172
- --chain <base|avalanche> Chain to use (default: base)
5281
+ --chain <base|avalanche|baseSepolia> Chain to use (default: base)
5173
5282
  --key <privateKey> Private key (overrides credentials)
5174
5283
  --limit <N> Number of messages to read (default: 20)
5175
5284
  --json Output as JSON
@@ -5501,8 +5610,12 @@ async function main() {
5501
5610
  const address = args[2];
5502
5611
  if (isNaN(topicId) || !address) outputError("Usage: clawntenna keys has <topicId> <address>", json);
5503
5612
  await keysHas(topicId, address, cf);
5613
+ } else if (sub === "pending") {
5614
+ const topicId = parseInt(args[1], 10);
5615
+ if (isNaN(topicId)) outputError("Usage: clawntenna keys pending <topicId>", json);
5616
+ await keysPending(topicId, cf);
5504
5617
  } else {
5505
- outputError(`Unknown keys subcommand: ${sub}. Use: register, check, grant, revoke, rotate, has`, json);
5618
+ outputError(`Unknown keys subcommand: ${sub}. Use: register, check, grant, revoke, rotate, has, pending`, json);
5506
5619
  }
5507
5620
  break;
5508
5621
  }
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,
@@ -458,6 +458,7 @@ function hexToBytes(hex) {
458
458
  }
459
459
 
460
460
  // src/client.ts
461
+ var import_utils3 = require("@noble/hashes/utils");
461
462
  var Clawntenna = class {
462
463
  provider;
463
464
  wallet;
@@ -776,10 +777,50 @@ var Clawntenna = class {
776
777
  const grant = await this.keyManager.getMyKey(topicId);
777
778
  const encryptedKey = import_ethers.ethers.getBytes(grant.encryptedKey);
778
779
  const granterPubKey = import_ethers.ethers.getBytes(grant.granterPublicKey);
780
+ if (encryptedKey.length === 0 || granterPubKey.length === 0) {
781
+ throw new Error(`No key grant found for topic ${topicId}. The topic key needs to be initialized first.`);
782
+ }
779
783
  const topicKey = decryptTopicKey(encryptedKey, this.ecdhPrivateKey, granterPubKey);
780
784
  this.topicKeys.set(topicId, topicKey);
781
785
  return topicKey;
782
786
  }
787
+ /**
788
+ * Initialize a private topic's symmetric key by generating a random key and self-granting.
789
+ * This should be called once by the topic owner after creating a PRIVATE topic.
790
+ * Returns the generated topic key.
791
+ */
792
+ async initializeTopicKey(topicId) {
793
+ if (!this.wallet) throw new Error("Wallet required");
794
+ if (!this.ecdhPrivateKey || !this.ecdhPublicKey) {
795
+ throw new Error("ECDH key not derived yet");
796
+ }
797
+ const topicKey = (0, import_utils3.randomBytes)(32);
798
+ const encrypted = encryptTopicKeyForUser(topicKey, this.ecdhPrivateKey, this.ecdhPublicKey);
799
+ const tx = await this.keyManager.grantKeyAccess(topicId, this.wallet.address, encrypted);
800
+ await tx.wait();
801
+ this.topicKeys.set(topicId, topicKey);
802
+ return topicKey;
803
+ }
804
+ /**
805
+ * Get the topic key, initializing it if the caller is the topic owner and no grant exists.
806
+ * Tries fetchAndDecryptTopicKey first; if no grant exists and caller is topic owner,
807
+ * auto-initializes with initializeTopicKey.
808
+ */
809
+ async getOrInitializeTopicKey(topicId) {
810
+ try {
811
+ return await this.fetchAndDecryptTopicKey(topicId);
812
+ } catch (err) {
813
+ const isNoGrant = err instanceof Error && err.message.includes("No key grant found");
814
+ if (!isNoGrant) throw err;
815
+ const topic = await this.getTopic(topicId);
816
+ if (!this.wallet || topic.owner.toLowerCase() !== this.wallet.address.toLowerCase()) {
817
+ throw new Error(
818
+ `No key grant found for topic ${topicId}. Ask the topic owner to grant you access with: keys grant ${topicId} ${this.wallet?.address ?? "<your-address>"}`
819
+ );
820
+ }
821
+ return this.initializeTopicKey(topicId);
822
+ }
823
+ }
783
824
  /**
784
825
  * Grant a user access to a private topic's symmetric key.
785
826
  */
@@ -821,6 +862,31 @@ var Clawntenna = class {
821
862
  async hasKeyAccess(topicId, address) {
822
863
  return this.keyManager.hasKeyAccess(topicId, address);
823
864
  }
865
+ /**
866
+ * Get members who have registered ECDH keys but haven't been granted access to a private topic.
867
+ * Useful for topic owners to see who's waiting for a key grant.
868
+ */
869
+ async getPendingKeyGrants(topicId) {
870
+ const topic = await this.getTopic(topicId);
871
+ const members = await this.getApplicationMembers(Number(topic.applicationId));
872
+ const uniqueMembers = [...new Set(members)].filter((a) => a !== import_ethers.ethers.ZeroAddress);
873
+ const pending = [];
874
+ const granted = [];
875
+ await Promise.all(
876
+ uniqueMembers.map(async (addr) => {
877
+ const [hasAccess, hasKey] = await Promise.all([
878
+ this.hasKeyAccess(topicId, addr),
879
+ this.hasPublicKey(addr)
880
+ ]);
881
+ if (hasAccess) {
882
+ granted.push(addr);
883
+ } else {
884
+ pending.push({ address: addr, hasPublicKey: hasKey });
885
+ }
886
+ })
887
+ );
888
+ return { pending, granted };
889
+ }
824
890
  /**
825
891
  * Get the key grant details for a user on a topic.
826
892
  */
@@ -1166,7 +1232,7 @@ var Clawntenna = class {
1166
1232
  const topic = await this.getTopic(topicId);
1167
1233
  if (topic.accessLevel === 2 /* PRIVATE */) {
1168
1234
  if (this.ecdhPrivateKey) {
1169
- return this.fetchAndDecryptTopicKey(topicId);
1235
+ return this.getOrInitializeTopicKey(topicId);
1170
1236
  }
1171
1237
  throw new Error(
1172
1238
  `Topic ${topicId} is PRIVATE. Load ECDH keys first (loadECDHKeypair or deriveECDHFromWallet), then call fetchAndDecryptTopicKey() or setTopicKey().`