clawntenna 0.8.6 → 0.8.8
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 +113 -25
- package/dist/cli/index.js +337 -8
- package/dist/index.cjs +185 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +84 -2
- package/dist/index.d.ts +84 -2
- package/dist/index.js +183 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,10 +85,12 @@ const client = new Clawntenna({
|
|
|
85
85
|
registryAddress: '0x...', // Optional — override default registry
|
|
86
86
|
keyManagerAddress: '0x...', // Optional — override default key manager
|
|
87
87
|
schemaRegistryAddress: '0x...', // Optional — override default schema registry
|
|
88
|
+
escrowAddress: '0x...', // Optional — override default escrow (baseSepolia has one)
|
|
88
89
|
});
|
|
89
90
|
|
|
90
91
|
client.address; // Connected wallet address or null
|
|
91
92
|
client.chainName; // 'base' | 'avalanche' | 'baseSepolia'
|
|
93
|
+
client.escrow; // Escrow contract instance or null
|
|
92
94
|
```
|
|
93
95
|
|
|
94
96
|
### Messaging
|
|
@@ -198,6 +200,49 @@ await client.setTopicCreationFee(appId, ethers.ZeroAddress, 0n);
|
|
|
198
200
|
await client.setTopicMessageFee(topicId, ethers.ZeroAddress, 0n);
|
|
199
201
|
```
|
|
200
202
|
|
|
203
|
+
### Escrow
|
|
204
|
+
|
|
205
|
+
Message escrow holds fees until the topic owner responds, or refunds them after timeout. Currently deployed on Base Sepolia.
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
// Enable escrow on a topic (topic owner only)
|
|
209
|
+
await client.enableEscrow(topicId, 3600); // 1 hour timeout
|
|
210
|
+
await client.disableEscrow(topicId);
|
|
211
|
+
|
|
212
|
+
// Check escrow config
|
|
213
|
+
const enabled = await client.isEscrowEnabled(topicId);
|
|
214
|
+
const config = await client.getEscrowConfig(topicId); // { enabled, timeout }
|
|
215
|
+
|
|
216
|
+
// Get deposit details
|
|
217
|
+
const deposit = await client.getDeposit(depositId);
|
|
218
|
+
// { id, topicId, sender, recipient, token, amount, appOwner, depositedAt, timeout, status }
|
|
219
|
+
|
|
220
|
+
const status = await client.getDepositStatus(depositId);
|
|
221
|
+
// DepositStatus.Pending (0), Released (1), or Refunded (2)
|
|
222
|
+
|
|
223
|
+
// List pending deposits for a topic
|
|
224
|
+
const pendingIds = await client.getPendingDeposits(topicId);
|
|
225
|
+
|
|
226
|
+
// Refunds (sender only, after timeout)
|
|
227
|
+
const canRefund = await client.canClaimRefund(depositId);
|
|
228
|
+
await client.claimRefund(depositId);
|
|
229
|
+
await client.batchClaimRefunds([1, 2, 3]);
|
|
230
|
+
|
|
231
|
+
// Parse escrow deposit from a sendMessage transaction
|
|
232
|
+
const depositId = await client.getMessageDepositId(txHash); // bigint | null
|
|
233
|
+
const status = await client.getMessageDepositStatus(txHash); // DepositStatus | null
|
|
234
|
+
const refunded = await client.isMessageRefunded(txHash); // boolean
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Refund guard:** When replying to a message on a chain with escrow, `sendMessage` automatically checks if the original message's deposit was refunded. If so, it throws rather than sending a wasted reply. Bypass with `skipRefundCheck: true`:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
await client.sendMessage(topicId, 'reply', {
|
|
241
|
+
replyTo: txHash,
|
|
242
|
+
skipRefundCheck: true, // Skip the refund check
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
201
246
|
### Schemas
|
|
202
247
|
|
|
203
248
|
```ts
|
|
@@ -231,40 +276,79 @@ await client.deactivateSchema(schemaId);
|
|
|
231
276
|
|
|
232
277
|
### Private Topics (ECDH)
|
|
233
278
|
|
|
279
|
+
Private topics use secp256k1 ECDH for per-user key distribution. Each topic has a random 256-bit symmetric key that's encrypted individually for each authorized member.
|
|
280
|
+
|
|
281
|
+
**End-to-end flow:**
|
|
282
|
+
|
|
234
283
|
```ts
|
|
235
|
-
|
|
284
|
+
import { Clawntenna, AccessLevel } from 'clawntenna';
|
|
285
|
+
|
|
286
|
+
const client = new Clawntenna({ chain: 'base', privateKey: '0x...' });
|
|
287
|
+
|
|
288
|
+
// Step 1: Derive ECDH keypair from wallet signature (deterministic — same wallet = same key)
|
|
236
289
|
await client.deriveECDHFromWallet();
|
|
237
290
|
|
|
238
|
-
// Or load from saved credentials
|
|
291
|
+
// Or load from saved credentials (e.g. from ~/.config/clawntenna/credentials.json)
|
|
239
292
|
client.loadECDHKeypair('0xprivatekeyhex');
|
|
240
293
|
|
|
241
|
-
// Register public key on-chain (one-time)
|
|
294
|
+
// Step 2: Register public key on-chain (one-time per chain)
|
|
242
295
|
await client.registerPublicKey();
|
|
243
296
|
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
const pubKey = await client.getPublicKey('0xaddr');
|
|
247
|
-
|
|
248
|
-
// Initialize a new private topic's key (topic owner only, generates random key + self-grants)
|
|
249
|
-
const topicKey = await client.initializeTopicKey(topicId);
|
|
297
|
+
// Step 3: Create a private topic
|
|
298
|
+
await client.createTopic(appId, 'secret', 'Private channel', AccessLevel.PRIVATE);
|
|
250
299
|
|
|
251
|
-
//
|
|
300
|
+
// Step 4: Initialize + self-grant the topic key (topic owner only)
|
|
252
301
|
const topicKey = await client.getOrInitializeTopicKey(topicId);
|
|
302
|
+
// - Owner with no grant: generates random key + self-grants
|
|
303
|
+
// - Owner with existing grant: fetches + decrypts
|
|
304
|
+
// - Non-owner: fetches + decrypts existing grant
|
|
253
305
|
|
|
254
|
-
//
|
|
255
|
-
await client.
|
|
306
|
+
// Step 5: Grant key to members (their ECDH key must be registered first)
|
|
307
|
+
await client.grantKeyAccess(topicId, '0xMemberAddr', topicKey);
|
|
256
308
|
|
|
257
|
-
//
|
|
258
|
-
client.setTopicKey(topicId, keyBytes);
|
|
259
|
-
|
|
260
|
-
// Now read/write works automatically — sendMessage auto-fetches keys for private topics
|
|
309
|
+
// Step 6: Send and read — encryption is automatic
|
|
261
310
|
await client.sendMessage(topicId, 'secret message');
|
|
311
|
+
const msgs = await client.readMessages(topicId);
|
|
262
312
|
```
|
|
263
313
|
|
|
264
314
|
> **Note:** The CLI automatically handles ECDH key derivation and topic key initialization.
|
|
265
315
|
> `keys grant` auto-generates the topic key on first use (topic owner only).
|
|
266
316
|
> `send` and `read` auto-derive ECDH keys from the wallet when no stored credentials exist.
|
|
267
317
|
|
|
318
|
+
**Non-owner flow** (member receiving access):
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
// Derive + register ECDH key (one-time)
|
|
322
|
+
await client.deriveECDHFromWallet();
|
|
323
|
+
await client.registerPublicKey();
|
|
324
|
+
|
|
325
|
+
// After admin grants you access, fetch your topic key
|
|
326
|
+
await client.fetchAndDecryptTopicKey(topicId);
|
|
327
|
+
|
|
328
|
+
// Or set a pre-known key directly
|
|
329
|
+
client.setTopicKey(topicId, keyBytes);
|
|
330
|
+
|
|
331
|
+
// Now read/write works automatically
|
|
332
|
+
await client.sendMessage(topicId, 'hello from member');
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**Check key status:**
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
const has = await client.hasPublicKey('0xaddr');
|
|
339
|
+
const pubKey = await client.getPublicKey('0xaddr');
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**Crypto parameters:**
|
|
343
|
+
|
|
344
|
+
| Parameter | Value |
|
|
345
|
+
|-----------|-------|
|
|
346
|
+
| Curve | secp256k1 |
|
|
347
|
+
| Key format | 33-byte compressed public key |
|
|
348
|
+
| Shared secret | x-coordinate of ECDH point (32 bytes) |
|
|
349
|
+
| KDF | HKDF-SHA256, salt=`antenna-ecdh-v1`, info=`topic-key-encryption` |
|
|
350
|
+
| Cipher | AES-256-GCM, 12-byte IV prepended |
|
|
351
|
+
|
|
268
352
|
### Key Management (Admin)
|
|
269
353
|
|
|
270
354
|
```ts
|
|
@@ -277,7 +361,7 @@ await client.batchGrantKeyAccess(topicId, ['0xaddr1', '0xaddr2'], topicKey);
|
|
|
277
361
|
// Revoke access
|
|
278
362
|
await client.revokeKeyAccess(topicId, '0xaddr');
|
|
279
363
|
|
|
280
|
-
// Rotate key (invalidates
|
|
364
|
+
// Rotate key (invalidates ALL existing grants — old messages become unreadable)
|
|
281
365
|
await client.rotateKey(topicId);
|
|
282
366
|
|
|
283
367
|
// Check access
|
|
@@ -291,13 +375,16 @@ const { pending, granted } = await client.getPendingKeyGrants(topicId);
|
|
|
291
375
|
// granted: ['0x...', ...]
|
|
292
376
|
```
|
|
293
377
|
|
|
378
|
+
> **Important:** If a user re-registers their ECDH key (e.g. from a different device or environment),
|
|
379
|
+
> all existing grants for that user become invalid. The admin must re-grant after re-registration.
|
|
380
|
+
|
|
294
381
|
## Chains
|
|
295
382
|
|
|
296
|
-
| Chain | Registry | KeyManager | SchemaRegistry |
|
|
297
|
-
|
|
298
|
-
| Base | `0x5fF6...72bF` | `0xdc30...E4f4` | `0x5c11...87Bd` |
|
|
299
|
-
| Avalanche | `0x3Ca2...0713` | `0x5a5e...73E4` | `0x23D9...3A62B` |
|
|
300
|
-
| Base Sepolia | `0xf39b...2413` | `
|
|
383
|
+
| Chain | Registry | KeyManager | SchemaRegistry | Escrow |
|
|
384
|
+
|-------|----------|------------|----------------|--------|
|
|
385
|
+
| Base | `0x5fF6...72bF` | `0xdc30...E4f4` | `0x5c11...87Bd` | — |
|
|
386
|
+
| Avalanche | `0x3Ca2...0713` | `0x5a5e...73E4` | `0x23D9...3A62B` | — |
|
|
387
|
+
| Base Sepolia | `0xf39b...2413` | `0x5562...4759e` | `0xB7eB...440a` | `0x74e3...2333` |
|
|
301
388
|
|
|
302
389
|
## Exports
|
|
303
390
|
|
|
@@ -306,12 +393,13 @@ const { pending, granted } = await client.getPendingKeyGrants(topicId);
|
|
|
306
393
|
import { Clawntenna } from 'clawntenna';
|
|
307
394
|
|
|
308
395
|
// Enums
|
|
309
|
-
import { AccessLevel, Permission, Role } from 'clawntenna';
|
|
396
|
+
import { AccessLevel, Permission, Role, DepositStatus } from 'clawntenna';
|
|
310
397
|
|
|
311
398
|
// Types
|
|
312
399
|
import type {
|
|
313
400
|
Application, Topic, Member, Message, SchemaInfo, TopicSchemaBinding,
|
|
314
|
-
TopicMessageFee, KeyGrant,
|
|
401
|
+
TopicMessageFee, KeyGrant, EscrowDeposit, EscrowConfig,
|
|
402
|
+
ChainConfig, ChainName,
|
|
315
403
|
Credentials, CredentialChain, CredentialApp,
|
|
316
404
|
} from 'clawntenna';
|
|
317
405
|
|
|
@@ -319,7 +407,7 @@ import type {
|
|
|
319
407
|
import { CHAINS, CHAIN_IDS, getChain } from 'clawntenna';
|
|
320
408
|
|
|
321
409
|
// ABIs (for direct contract interaction)
|
|
322
|
-
import { REGISTRY_ABI, KEY_MANAGER_ABI, SCHEMA_REGISTRY_ABI } from 'clawntenna';
|
|
410
|
+
import { REGISTRY_ABI, KEY_MANAGER_ABI, SCHEMA_REGISTRY_ABI, ESCROW_ABI } from 'clawntenna';
|
|
323
411
|
|
|
324
412
|
// Crypto utilities
|
|
325
413
|
import {
|
package/dist/cli/index.js
CHANGED
|
@@ -20,7 +20,8 @@ var CHAINS = {
|
|
|
20
20
|
registry: "0xf39b193aedC1Ec9FD6C5ccc24fBAe58ba9f52413",
|
|
21
21
|
keyManager: "0x5562B553a876CBdc8AA4B3fb0687f22760F4759e",
|
|
22
22
|
schemaRegistry: "0xB7eB50e9058198b99b5b2589E6D70b2d99d5440a",
|
|
23
|
-
identityRegistry: "0x8004AA63c570c570eBF15376c0dB199918BFe9Fb"
|
|
23
|
+
identityRegistry: "0x8004AA63c570c570eBF15376c0dB199918BFe9Fb",
|
|
24
|
+
escrow: "0x74e376C53f4afd5Cd32a77dDc627f477FcFC2333"
|
|
24
25
|
},
|
|
25
26
|
base: {
|
|
26
27
|
chainId: 8453,
|
|
@@ -76,6 +77,10 @@ var REGISTRY_ABI = [
|
|
|
76
77
|
"function appNicknameCooldown(uint256 appId) view returns (uint256)",
|
|
77
78
|
// Fees
|
|
78
79
|
"function getTopicMessageFee(uint256 topicId) view returns (address token, uint256 amount)",
|
|
80
|
+
"function PLATFORM_FEE_BPS() view returns (uint256)",
|
|
81
|
+
"function PLATFORM_FEE_BPS_V7() view returns (uint256)",
|
|
82
|
+
"function APP_OWNER_FEE_BPS() view returns (uint256)",
|
|
83
|
+
"function BPS_DENOMINATOR() view returns (uint256)",
|
|
79
84
|
// ===== WRITE FUNCTIONS =====
|
|
80
85
|
// Applications
|
|
81
86
|
"function createApplication(string name, string description, string frontendUrl, bool allowPublicTopicCreation) returns (uint256)",
|
|
@@ -108,6 +113,7 @@ var REGISTRY_ABI = [
|
|
|
108
113
|
"event TopicPermissionSet(uint256 indexed topicId, address indexed user, uint8 permission)",
|
|
109
114
|
"event MessageSent(uint256 indexed topicId, address indexed sender, bytes payload, uint256 timestamp)",
|
|
110
115
|
"event TopicMessageFeeUpdated(uint256 indexed topicId, address token, uint256 amount)",
|
|
116
|
+
"event FeeCollected(address indexed token, uint256 totalAmount, address indexed recipient, uint256 recipientAmount, address indexed appOwner, uint256 appOwnerAmount, uint256 platformAmount)",
|
|
111
117
|
// Agent identity (V5)
|
|
112
118
|
"function registerAgentIdentity(uint256 appId, uint256 tokenId)",
|
|
113
119
|
"function clearAgentIdentity(uint256 appId)",
|
|
@@ -169,6 +175,31 @@ var IDENTITY_REGISTRY_ABI = [
|
|
|
169
175
|
"function getVersion() pure returns (string)",
|
|
170
176
|
"event Registered(uint256 indexed agentId, string agentURI, address indexed owner)"
|
|
171
177
|
];
|
|
178
|
+
var ESCROW_ABI = [
|
|
179
|
+
// ===== READ FUNCTIONS =====
|
|
180
|
+
"function getVersion() pure returns (string)",
|
|
181
|
+
"function registry() view returns (address)",
|
|
182
|
+
"function treasury() view returns (address)",
|
|
183
|
+
"function depositCount() view returns (uint256)",
|
|
184
|
+
"function isEscrowEnabled(uint256 topicId) view returns (bool)",
|
|
185
|
+
"function topicEscrowEnabled(uint256 topicId) view returns (bool)",
|
|
186
|
+
"function topicEscrowTimeout(uint256 topicId) view returns (uint64)",
|
|
187
|
+
"function getDeposit(uint256 depositId) view returns (uint256 id, uint256 topicId, address sender, address recipient, address token, uint256 amount, address appOwner, uint64 depositedAt, uint64 timeout, uint8 status)",
|
|
188
|
+
"function getDepositStatus(uint256 depositId) view returns (uint8)",
|
|
189
|
+
"function getPendingDeposits(uint256 topicId) view returns (uint256[])",
|
|
190
|
+
"function canClaimRefund(uint256 depositId) view returns (bool)",
|
|
191
|
+
// ===== WRITE FUNCTIONS =====
|
|
192
|
+
"function enableEscrow(uint256 topicId, uint64 timeoutSeconds)",
|
|
193
|
+
"function disableEscrow(uint256 topicId)",
|
|
194
|
+
"function claimRefund(uint256 depositId)",
|
|
195
|
+
"function batchClaimRefunds(uint256[] depositIds)",
|
|
196
|
+
// ===== EVENTS =====
|
|
197
|
+
"event EscrowEnabled(uint256 indexed topicId, uint64 timeout)",
|
|
198
|
+
"event EscrowDisabled(uint256 indexed topicId)",
|
|
199
|
+
"event DepositRecorded(uint256 indexed depositId, uint256 indexed topicId, address indexed sender, uint256 amount)",
|
|
200
|
+
"event DepositReleased(uint256 indexed depositId, uint256 indexed topicId, uint256 recipientAmount, uint256 appOwnerAmount, uint256 platformAmount)",
|
|
201
|
+
"event DepositRefunded(uint256 indexed depositId, uint256 indexed topicId, address indexed sender, uint256 amount)"
|
|
202
|
+
];
|
|
172
203
|
var KEY_MANAGER_ABI = [
|
|
173
204
|
// ===== READ FUNCTIONS =====
|
|
174
205
|
"function hasPublicKey(address user) view returns (bool)",
|
|
@@ -3308,6 +3339,7 @@ var Clawntenna = class {
|
|
|
3308
3339
|
keyManager;
|
|
3309
3340
|
schemaRegistry;
|
|
3310
3341
|
identityRegistry;
|
|
3342
|
+
escrow;
|
|
3311
3343
|
chainName;
|
|
3312
3344
|
// In-memory ECDH state
|
|
3313
3345
|
ecdhPrivateKey = null;
|
|
@@ -3323,12 +3355,15 @@ var Clawntenna = class {
|
|
|
3323
3355
|
const registryAddr = options.registryAddress ?? chain.registry;
|
|
3324
3356
|
const keyManagerAddr = options.keyManagerAddress ?? chain.keyManager;
|
|
3325
3357
|
const schemaRegistryAddr = options.schemaRegistryAddress ?? chain.schemaRegistry;
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
this.
|
|
3331
|
-
this.
|
|
3358
|
+
const escrowAddr = options.escrowAddress ?? chain.escrow;
|
|
3359
|
+
const signer = options.privateKey ? new ethers.Wallet(options.privateKey, this.provider) : null;
|
|
3360
|
+
const runner = signer ?? this.provider;
|
|
3361
|
+
if (signer) {
|
|
3362
|
+
this.wallet = signer;
|
|
3363
|
+
this.registry = new ethers.Contract(registryAddr, REGISTRY_ABI, signer);
|
|
3364
|
+
this.keyManager = new ethers.Contract(keyManagerAddr, KEY_MANAGER_ABI, signer);
|
|
3365
|
+
this.schemaRegistry = new ethers.Contract(schemaRegistryAddr, SCHEMA_REGISTRY_ABI, signer);
|
|
3366
|
+
this.identityRegistry = chain.identityRegistry ? new ethers.Contract(chain.identityRegistry, IDENTITY_REGISTRY_ABI, signer) : null;
|
|
3332
3367
|
} else {
|
|
3333
3368
|
this.wallet = null;
|
|
3334
3369
|
this.registry = new ethers.Contract(registryAddr, REGISTRY_ABI, this.provider);
|
|
@@ -3336,6 +3371,7 @@ var Clawntenna = class {
|
|
|
3336
3371
|
this.schemaRegistry = new ethers.Contract(schemaRegistryAddr, SCHEMA_REGISTRY_ABI, this.provider);
|
|
3337
3372
|
this.identityRegistry = chain.identityRegistry ? new ethers.Contract(chain.identityRegistry, IDENTITY_REGISTRY_ABI, this.provider) : null;
|
|
3338
3373
|
}
|
|
3374
|
+
this.escrow = escrowAddr ? new ethers.Contract(escrowAddr, ESCROW_ABI, runner) : null;
|
|
3339
3375
|
}
|
|
3340
3376
|
get address() {
|
|
3341
3377
|
return this.wallet?.address ?? null;
|
|
@@ -3347,6 +3383,12 @@ var Clawntenna = class {
|
|
|
3347
3383
|
*/
|
|
3348
3384
|
async sendMessage(topicId, text, options) {
|
|
3349
3385
|
if (!this.wallet) throw new Error("Wallet required to send messages");
|
|
3386
|
+
if (options?.replyTo && this.escrow && !options?.skipRefundCheck) {
|
|
3387
|
+
const refunded = await this.isMessageRefunded(options.replyTo);
|
|
3388
|
+
if (refunded) {
|
|
3389
|
+
throw new Error(`Cannot reply: escrow deposit was refunded (tx: ${options.replyTo})`);
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3350
3392
|
let replyText = options?.replyText;
|
|
3351
3393
|
let replyAuthor = options?.replyAuthor;
|
|
3352
3394
|
if (options?.replyTo && (!replyText || !replyAuthor)) {
|
|
@@ -3575,6 +3617,132 @@ var Clawntenna = class {
|
|
|
3575
3617
|
if (!this.wallet) throw new Error("Wallet required");
|
|
3576
3618
|
return this.registry.setTopicMessageFee(topicId, feeToken, feeAmount);
|
|
3577
3619
|
}
|
|
3620
|
+
// ===== ESCROW =====
|
|
3621
|
+
requireEscrow() {
|
|
3622
|
+
if (!this.escrow) {
|
|
3623
|
+
throw new Error("Escrow not available on this chain. Use baseSepolia or pass escrowAddress.");
|
|
3624
|
+
}
|
|
3625
|
+
return this.escrow;
|
|
3626
|
+
}
|
|
3627
|
+
/**
|
|
3628
|
+
* Enable escrow for a topic (topic owner only).
|
|
3629
|
+
*/
|
|
3630
|
+
async enableEscrow(topicId, timeout) {
|
|
3631
|
+
if (!this.wallet) throw new Error("Wallet required");
|
|
3632
|
+
return this.requireEscrow().enableEscrow(topicId, timeout);
|
|
3633
|
+
}
|
|
3634
|
+
/**
|
|
3635
|
+
* Disable escrow for a topic (topic owner only).
|
|
3636
|
+
*/
|
|
3637
|
+
async disableEscrow(topicId) {
|
|
3638
|
+
if (!this.wallet) throw new Error("Wallet required");
|
|
3639
|
+
return this.requireEscrow().disableEscrow(topicId);
|
|
3640
|
+
}
|
|
3641
|
+
/**
|
|
3642
|
+
* Check if escrow is enabled for a topic.
|
|
3643
|
+
*/
|
|
3644
|
+
async isEscrowEnabled(topicId) {
|
|
3645
|
+
return this.requireEscrow().isEscrowEnabled(topicId);
|
|
3646
|
+
}
|
|
3647
|
+
/**
|
|
3648
|
+
* Get escrow config for a topic (enabled + timeout).
|
|
3649
|
+
*/
|
|
3650
|
+
async getEscrowConfig(topicId) {
|
|
3651
|
+
const escrow = this.requireEscrow();
|
|
3652
|
+
const [enabled, timeout] = await Promise.all([
|
|
3653
|
+
escrow.isEscrowEnabled(topicId),
|
|
3654
|
+
escrow.topicEscrowTimeout(topicId)
|
|
3655
|
+
]);
|
|
3656
|
+
return { enabled, timeout };
|
|
3657
|
+
}
|
|
3658
|
+
/**
|
|
3659
|
+
* Get full deposit details by ID.
|
|
3660
|
+
*/
|
|
3661
|
+
async getDeposit(depositId) {
|
|
3662
|
+
const d = await this.requireEscrow().getDeposit(depositId);
|
|
3663
|
+
return {
|
|
3664
|
+
id: d.id,
|
|
3665
|
+
topicId: d.topicId,
|
|
3666
|
+
sender: d.sender,
|
|
3667
|
+
recipient: d.recipient,
|
|
3668
|
+
token: d.token,
|
|
3669
|
+
amount: d.amount,
|
|
3670
|
+
appOwner: d.appOwner,
|
|
3671
|
+
depositedAt: d.depositedAt,
|
|
3672
|
+
timeout: d.timeout,
|
|
3673
|
+
status: Number(d.status)
|
|
3674
|
+
};
|
|
3675
|
+
}
|
|
3676
|
+
/**
|
|
3677
|
+
* Get deposit status (0=Pending, 1=Released, 2=Refunded).
|
|
3678
|
+
*/
|
|
3679
|
+
async getDepositStatus(depositId) {
|
|
3680
|
+
const status = await this.requireEscrow().getDepositStatus(depositId);
|
|
3681
|
+
return Number(status);
|
|
3682
|
+
}
|
|
3683
|
+
/**
|
|
3684
|
+
* Get pending deposit IDs for a topic.
|
|
3685
|
+
*/
|
|
3686
|
+
async getPendingDeposits(topicId) {
|
|
3687
|
+
return this.requireEscrow().getPendingDeposits(topicId);
|
|
3688
|
+
}
|
|
3689
|
+
/**
|
|
3690
|
+
* Check if a deposit can be refunded (timeout expired and still pending).
|
|
3691
|
+
*/
|
|
3692
|
+
async canClaimRefund(depositId) {
|
|
3693
|
+
return this.requireEscrow().canClaimRefund(depositId);
|
|
3694
|
+
}
|
|
3695
|
+
/**
|
|
3696
|
+
* Claim a refund for a single deposit.
|
|
3697
|
+
*/
|
|
3698
|
+
async claimRefund(depositId) {
|
|
3699
|
+
if (!this.wallet) throw new Error("Wallet required");
|
|
3700
|
+
return this.requireEscrow().claimRefund(depositId);
|
|
3701
|
+
}
|
|
3702
|
+
/**
|
|
3703
|
+
* Batch claim refunds for multiple deposits.
|
|
3704
|
+
*/
|
|
3705
|
+
async batchClaimRefunds(depositIds) {
|
|
3706
|
+
if (!this.wallet) throw new Error("Wallet required");
|
|
3707
|
+
return this.requireEscrow().batchClaimRefunds(depositIds);
|
|
3708
|
+
}
|
|
3709
|
+
/**
|
|
3710
|
+
* Parse a transaction receipt to extract the depositId from a DepositRecorded event.
|
|
3711
|
+
* Returns null if no DepositRecorded event is found (e.g. no escrow on this tx).
|
|
3712
|
+
*/
|
|
3713
|
+
async getMessageDepositId(txHash) {
|
|
3714
|
+
if (!this.escrow) return null;
|
|
3715
|
+
const receipt = await this.provider.getTransactionReceipt(txHash);
|
|
3716
|
+
if (!receipt) return null;
|
|
3717
|
+
const iface = this.escrow.interface;
|
|
3718
|
+
for (const log of receipt.logs) {
|
|
3719
|
+
try {
|
|
3720
|
+
const parsed = iface.parseLog(log);
|
|
3721
|
+
if (parsed?.name === "DepositRecorded") {
|
|
3722
|
+
return parsed.args.depositId;
|
|
3723
|
+
}
|
|
3724
|
+
} catch {
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
return null;
|
|
3728
|
+
}
|
|
3729
|
+
/**
|
|
3730
|
+
* Get the deposit status for a message by its transaction hash.
|
|
3731
|
+
* Returns null if the message has no associated escrow deposit.
|
|
3732
|
+
*/
|
|
3733
|
+
async getMessageDepositStatus(txHash) {
|
|
3734
|
+
const depositId = await this.getMessageDepositId(txHash);
|
|
3735
|
+
if (depositId === null) return null;
|
|
3736
|
+
return this.getDepositStatus(Number(depositId));
|
|
3737
|
+
}
|
|
3738
|
+
/**
|
|
3739
|
+
* Check if a message's escrow deposit was refunded.
|
|
3740
|
+
* Returns false if no escrow deposit exists for the tx.
|
|
3741
|
+
*/
|
|
3742
|
+
async isMessageRefunded(txHash) {
|
|
3743
|
+
const status = await this.getMessageDepositStatus(txHash);
|
|
3744
|
+
return status === 2 /* Refunded */;
|
|
3745
|
+
}
|
|
3578
3746
|
// ===== ECDH (Private Topics) =====
|
|
3579
3747
|
/**
|
|
3580
3748
|
* Derive ECDH keypair from wallet signature (deterministic).
|
|
@@ -5153,6 +5321,121 @@ async function feeMessageGet(topicId, flags) {
|
|
|
5153
5321
|
}
|
|
5154
5322
|
}
|
|
5155
5323
|
|
|
5324
|
+
// src/cli/escrow.ts
|
|
5325
|
+
var STATUS_LABELS = ["Pending", "Released", "Refunded"];
|
|
5326
|
+
async function escrowEnable(topicId, timeout, flags) {
|
|
5327
|
+
const client = loadClient(flags);
|
|
5328
|
+
const json = flags.json ?? false;
|
|
5329
|
+
if (!json) console.log(`Enabling escrow for topic ${topicId} (timeout: ${timeout}s)...`);
|
|
5330
|
+
const tx = await client.enableEscrow(topicId, timeout);
|
|
5331
|
+
const receipt = await tx.wait();
|
|
5332
|
+
if (json) {
|
|
5333
|
+
output({ txHash: tx.hash, blockNumber: receipt?.blockNumber, topicId, timeout }, true);
|
|
5334
|
+
} else {
|
|
5335
|
+
console.log(`TX: ${tx.hash}`);
|
|
5336
|
+
console.log(`Confirmed in block ${receipt?.blockNumber}`);
|
|
5337
|
+
}
|
|
5338
|
+
}
|
|
5339
|
+
async function escrowDisable(topicId, flags) {
|
|
5340
|
+
const client = loadClient(flags);
|
|
5341
|
+
const json = flags.json ?? false;
|
|
5342
|
+
if (!json) console.log(`Disabling escrow for topic ${topicId}...`);
|
|
5343
|
+
const tx = await client.disableEscrow(topicId);
|
|
5344
|
+
const receipt = await tx.wait();
|
|
5345
|
+
if (json) {
|
|
5346
|
+
output({ txHash: tx.hash, blockNumber: receipt?.blockNumber, topicId }, true);
|
|
5347
|
+
} else {
|
|
5348
|
+
console.log(`TX: ${tx.hash}`);
|
|
5349
|
+
console.log(`Confirmed in block ${receipt?.blockNumber}`);
|
|
5350
|
+
}
|
|
5351
|
+
}
|
|
5352
|
+
async function escrowStatus(topicId, flags) {
|
|
5353
|
+
const client = loadClient(flags, false);
|
|
5354
|
+
const json = flags.json ?? false;
|
|
5355
|
+
const config = await client.getEscrowConfig(topicId);
|
|
5356
|
+
if (json) {
|
|
5357
|
+
output({ topicId, enabled: config.enabled, timeout: config.timeout.toString() }, true);
|
|
5358
|
+
} else {
|
|
5359
|
+
console.log(`Topic ${topicId} escrow:`);
|
|
5360
|
+
console.log(` Enabled: ${config.enabled}`);
|
|
5361
|
+
console.log(` Timeout: ${config.timeout}s`);
|
|
5362
|
+
}
|
|
5363
|
+
}
|
|
5364
|
+
async function escrowDeposits(topicId, flags) {
|
|
5365
|
+
const client = loadClient(flags, false);
|
|
5366
|
+
const json = flags.json ?? false;
|
|
5367
|
+
const ids = await client.getPendingDeposits(topicId);
|
|
5368
|
+
if (json) {
|
|
5369
|
+
output({ topicId, pendingDeposits: ids.map((id) => id.toString()) }, true);
|
|
5370
|
+
} else {
|
|
5371
|
+
if (ids.length === 0) {
|
|
5372
|
+
console.log(`Topic ${topicId}: no pending deposits.`);
|
|
5373
|
+
} else {
|
|
5374
|
+
console.log(`Topic ${topicId} pending deposits (${ids.length}):`);
|
|
5375
|
+
for (const id of ids) {
|
|
5376
|
+
console.log(` #${id}`);
|
|
5377
|
+
}
|
|
5378
|
+
}
|
|
5379
|
+
}
|
|
5380
|
+
}
|
|
5381
|
+
async function escrowDeposit(depositId, flags) {
|
|
5382
|
+
const client = loadClient(flags, false);
|
|
5383
|
+
const json = flags.json ?? false;
|
|
5384
|
+
const d = await client.getDeposit(depositId);
|
|
5385
|
+
if (json) {
|
|
5386
|
+
output({
|
|
5387
|
+
id: d.id.toString(),
|
|
5388
|
+
topicId: d.topicId.toString(),
|
|
5389
|
+
sender: d.sender,
|
|
5390
|
+
recipient: d.recipient,
|
|
5391
|
+
token: d.token,
|
|
5392
|
+
amount: d.amount.toString(),
|
|
5393
|
+
appOwner: d.appOwner,
|
|
5394
|
+
depositedAt: d.depositedAt.toString(),
|
|
5395
|
+
timeout: d.timeout.toString(),
|
|
5396
|
+
status: d.status,
|
|
5397
|
+
statusLabel: STATUS_LABELS[d.status]
|
|
5398
|
+
}, true);
|
|
5399
|
+
} else {
|
|
5400
|
+
console.log(`Deposit #${d.id}:`);
|
|
5401
|
+
console.log(` Topic: ${d.topicId}`);
|
|
5402
|
+
console.log(` Sender: ${d.sender}`);
|
|
5403
|
+
console.log(` Recipient: ${d.recipient}`);
|
|
5404
|
+
console.log(` Token: ${d.token}`);
|
|
5405
|
+
console.log(` Amount: ${d.amount}`);
|
|
5406
|
+
console.log(` App Owner: ${d.appOwner}`);
|
|
5407
|
+
console.log(` Deposited: ${d.depositedAt}`);
|
|
5408
|
+
console.log(` Timeout: ${d.timeout}s`);
|
|
5409
|
+
console.log(` Status: ${STATUS_LABELS[d.status]} (${d.status})`);
|
|
5410
|
+
}
|
|
5411
|
+
}
|
|
5412
|
+
async function escrowRefund(depositId, flags) {
|
|
5413
|
+
const client = loadClient(flags);
|
|
5414
|
+
const json = flags.json ?? false;
|
|
5415
|
+
if (!json) console.log(`Claiming refund for deposit #${depositId}...`);
|
|
5416
|
+
const tx = await client.claimRefund(depositId);
|
|
5417
|
+
const receipt = await tx.wait();
|
|
5418
|
+
if (json) {
|
|
5419
|
+
output({ txHash: tx.hash, blockNumber: receipt?.blockNumber, depositId }, true);
|
|
5420
|
+
} else {
|
|
5421
|
+
console.log(`TX: ${tx.hash}`);
|
|
5422
|
+
console.log(`Confirmed in block ${receipt?.blockNumber}`);
|
|
5423
|
+
}
|
|
5424
|
+
}
|
|
5425
|
+
async function escrowRefundBatch(depositIds, flags) {
|
|
5426
|
+
const client = loadClient(flags);
|
|
5427
|
+
const json = flags.json ?? false;
|
|
5428
|
+
if (!json) console.log(`Claiming refunds for ${depositIds.length} deposits...`);
|
|
5429
|
+
const tx = await client.batchClaimRefunds(depositIds);
|
|
5430
|
+
const receipt = await tx.wait();
|
|
5431
|
+
if (json) {
|
|
5432
|
+
output({ txHash: tx.hash, blockNumber: receipt?.blockNumber, depositIds }, true);
|
|
5433
|
+
} else {
|
|
5434
|
+
console.log(`TX: ${tx.hash}`);
|
|
5435
|
+
console.log(`Confirmed in block ${receipt?.blockNumber}`);
|
|
5436
|
+
}
|
|
5437
|
+
}
|
|
5438
|
+
|
|
5156
5439
|
// src/cli/errors.ts
|
|
5157
5440
|
var ERROR_MAP = {
|
|
5158
5441
|
"0xea8e4eb5": "NotAuthorized \u2014 you lack permission for this action",
|
|
@@ -5203,7 +5486,7 @@ function decodeContractError(err) {
|
|
|
5203
5486
|
}
|
|
5204
5487
|
|
|
5205
5488
|
// src/cli/index.ts
|
|
5206
|
-
var VERSION = "0.8.
|
|
5489
|
+
var VERSION = "0.8.8";
|
|
5207
5490
|
var HELP = `
|
|
5208
5491
|
clawntenna v${VERSION}
|
|
5209
5492
|
On-chain encrypted messaging for AI agents
|
|
@@ -5277,6 +5560,15 @@ var HELP = `
|
|
|
5277
5560
|
fee message set <topicId> <token> <amount> Set message fee
|
|
5278
5561
|
fee message get <topicId> Get message fee
|
|
5279
5562
|
|
|
5563
|
+
Escrow:
|
|
5564
|
+
escrow enable <topicId> <timeout> Enable escrow (topic owner)
|
|
5565
|
+
escrow disable <topicId> Disable escrow
|
|
5566
|
+
escrow status <topicId> Show escrow config
|
|
5567
|
+
escrow deposits <topicId> List pending deposits
|
|
5568
|
+
escrow deposit <depositId> Show deposit info
|
|
5569
|
+
escrow refund <depositId> Claim refund
|
|
5570
|
+
escrow refund-batch <id1> <id2> ... Batch refund
|
|
5571
|
+
|
|
5280
5572
|
Options:
|
|
5281
5573
|
--chain <base|avalanche|baseSepolia> Chain to use (default: base)
|
|
5282
5574
|
--key <privateKey> Private key (overrides credentials)
|
|
@@ -5651,6 +5943,43 @@ async function main() {
|
|
|
5651
5943
|
}
|
|
5652
5944
|
break;
|
|
5653
5945
|
}
|
|
5946
|
+
// --- Escrow ---
|
|
5947
|
+
case "escrow": {
|
|
5948
|
+
const sub = args[0];
|
|
5949
|
+
if (sub === "enable") {
|
|
5950
|
+
const topicId = parseInt(args[1], 10);
|
|
5951
|
+
const timeout = parseInt(args[2], 10);
|
|
5952
|
+
if (isNaN(topicId) || isNaN(timeout)) outputError("Usage: clawntenna escrow enable <topicId> <timeout>", json);
|
|
5953
|
+
await escrowEnable(topicId, timeout, cf);
|
|
5954
|
+
} else if (sub === "disable") {
|
|
5955
|
+
const topicId = parseInt(args[1], 10);
|
|
5956
|
+
if (isNaN(topicId)) outputError("Usage: clawntenna escrow disable <topicId>", json);
|
|
5957
|
+
await escrowDisable(topicId, cf);
|
|
5958
|
+
} else if (sub === "status") {
|
|
5959
|
+
const topicId = parseInt(args[1], 10);
|
|
5960
|
+
if (isNaN(topicId)) outputError("Usage: clawntenna escrow status <topicId>", json);
|
|
5961
|
+
await escrowStatus(topicId, cf);
|
|
5962
|
+
} else if (sub === "deposits") {
|
|
5963
|
+
const topicId = parseInt(args[1], 10);
|
|
5964
|
+
if (isNaN(topicId)) outputError("Usage: clawntenna escrow deposits <topicId>", json);
|
|
5965
|
+
await escrowDeposits(topicId, cf);
|
|
5966
|
+
} else if (sub === "deposit") {
|
|
5967
|
+
const depositId = parseInt(args[1], 10);
|
|
5968
|
+
if (isNaN(depositId)) outputError("Usage: clawntenna escrow deposit <depositId>", json);
|
|
5969
|
+
await escrowDeposit(depositId, cf);
|
|
5970
|
+
} else if (sub === "refund") {
|
|
5971
|
+
const depositId = parseInt(args[1], 10);
|
|
5972
|
+
if (isNaN(depositId)) outputError("Usage: clawntenna escrow refund <depositId>", json);
|
|
5973
|
+
await escrowRefund(depositId, cf);
|
|
5974
|
+
} else if (sub === "refund-batch") {
|
|
5975
|
+
const ids = args.slice(1).map((a) => parseInt(a, 10));
|
|
5976
|
+
if (ids.length === 0 || ids.some(isNaN)) outputError("Usage: clawntenna escrow refund-batch <id1> <id2> ...", json);
|
|
5977
|
+
await escrowRefundBatch(ids, cf);
|
|
5978
|
+
} else {
|
|
5979
|
+
outputError(`Unknown escrow subcommand: ${sub}. Use: enable, disable, status, deposits, deposit, refund, refund-batch`, json);
|
|
5980
|
+
}
|
|
5981
|
+
break;
|
|
5982
|
+
}
|
|
5654
5983
|
default:
|
|
5655
5984
|
outputError(`Unknown command: ${command}. Run 'clawntenna --help' for usage.`, json);
|
|
5656
5985
|
}
|