clawntenna 0.8.5 → 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 +23 -6
- package/dist/cli/index.js +109 -4
- package/dist/index.cjs +67 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +67 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
//
|
|
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
|
@@ -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.
|
|
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().`
|
|
@@ -4199,6 +4264,11 @@ async function send(topicId, message, flags) {
|
|
|
4199
4264
|
const ecdhCreds = creds?.chains[chainId]?.ecdh;
|
|
4200
4265
|
if (ecdhCreds?.privateKey) {
|
|
4201
4266
|
client.loadECDHKeypair(ecdhCreds.privateKey);
|
|
4267
|
+
} else {
|
|
4268
|
+
try {
|
|
4269
|
+
await client.deriveECDHFromWallet();
|
|
4270
|
+
} catch {
|
|
4271
|
+
}
|
|
4202
4272
|
}
|
|
4203
4273
|
if (!json) console.log(`Sending to topic ${topicId} on ${flags.chain}...`);
|
|
4204
4274
|
const sendOptions = {
|
|
@@ -4245,6 +4315,11 @@ async function read(topicId, flags) {
|
|
|
4245
4315
|
const ecdhCreds = creds?.chains[chainId]?.ecdh;
|
|
4246
4316
|
if (ecdhCreds?.privateKey) {
|
|
4247
4317
|
client.loadECDHKeypair(ecdhCreds.privateKey);
|
|
4318
|
+
} else {
|
|
4319
|
+
try {
|
|
4320
|
+
await client.deriveECDHFromWallet();
|
|
4321
|
+
} catch {
|
|
4322
|
+
}
|
|
4248
4323
|
}
|
|
4249
4324
|
if (!json) console.log(`Reading topic ${topicId} on ${flags.chain} (last ${flags.limit} messages)...
|
|
4250
4325
|
`);
|
|
@@ -4904,7 +4979,7 @@ async function keysGrant(topicId, address, flags) {
|
|
|
4904
4979
|
await client.deriveECDHFromWallet();
|
|
4905
4980
|
}
|
|
4906
4981
|
if (!json) console.log(`Fetching topic key for topic ${topicId}...`);
|
|
4907
|
-
const topicKey = await client.
|
|
4982
|
+
const topicKey = await client.getOrInitializeTopicKey(topicId);
|
|
4908
4983
|
if (!json) console.log(`Granting key access to ${address}...`);
|
|
4909
4984
|
const tx = await client.grantKeyAccess(topicId, address, topicKey);
|
|
4910
4985
|
const receipt = await tx.wait();
|
|
@@ -4941,6 +5016,31 @@ async function keysRotate(topicId, flags) {
|
|
|
4941
5016
|
console.log(`Confirmed in block ${receipt?.blockNumber}`);
|
|
4942
5017
|
}
|
|
4943
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
|
+
}
|
|
4944
5044
|
async function keysHas(topicId, address, flags) {
|
|
4945
5045
|
const client = loadClient(flags, false);
|
|
4946
5046
|
const json = flags.json ?? false;
|
|
@@ -5103,7 +5203,7 @@ function decodeContractError(err) {
|
|
|
5103
5203
|
}
|
|
5104
5204
|
|
|
5105
5205
|
// src/cli/index.ts
|
|
5106
|
-
var VERSION = "0.8.
|
|
5206
|
+
var VERSION = "0.8.6";
|
|
5107
5207
|
var HELP = `
|
|
5108
5208
|
clawntenna v${VERSION}
|
|
5109
5209
|
On-chain encrypted messaging for AI agents
|
|
@@ -5170,6 +5270,7 @@ var HELP = `
|
|
|
5170
5270
|
keys revoke <topicId> <address> Revoke key access
|
|
5171
5271
|
keys rotate <topicId> Rotate topic key
|
|
5172
5272
|
keys has <topicId> <address> Check if user has key access
|
|
5273
|
+
keys pending <topicId> List members awaiting key grant
|
|
5173
5274
|
|
|
5174
5275
|
Fees:
|
|
5175
5276
|
fee topic-creation set <appId> <token> <amount> Set topic creation fee
|
|
@@ -5509,8 +5610,12 @@ async function main() {
|
|
|
5509
5610
|
const address = args[2];
|
|
5510
5611
|
if (isNaN(topicId) || !address) outputError("Usage: clawntenna keys has <topicId> <address>", json);
|
|
5511
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);
|
|
5512
5617
|
} else {
|
|
5513
|
-
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);
|
|
5514
5619
|
}
|
|
5515
5620
|
break;
|
|
5516
5621
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -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.
|
|
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().`
|