clawntenna 0.11.0 → 0.11.2
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 +18 -1
- package/dist/cli/index.js +327 -5
- package/dist/index.cjs +212 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +76 -2
- package/dist/index.d.ts +76 -2
- package/dist/index.js +212 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -266,6 +266,23 @@ const status = await client.getMessageDepositStatus(txHash); // DepositStatus |
|
|
|
266
266
|
const refunded = await client.isMessageRefunded(txHash); // boolean
|
|
267
267
|
```
|
|
268
268
|
|
|
269
|
+
**Escrow inbox** — reverse lookup deposits to their linked messages:
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
// Get all pending deposits enriched with message text, timers, and response status
|
|
273
|
+
const inbox = await client.getEscrowInbox(topicId);
|
|
274
|
+
// Returns: EnrichedDeposit[] (sorted newest-first)
|
|
275
|
+
// Each has: txHash, blockNumber, messageText, hasResponse, remainingSeconds,
|
|
276
|
+
// formattedRemaining, expired, formattedAmount + all EscrowDeposit fields
|
|
277
|
+
|
|
278
|
+
// Reverse lookup: deposit ID → transaction hash
|
|
279
|
+
const txHash = await client.getDepositTxHash(depositId); // string | null
|
|
280
|
+
|
|
281
|
+
// Full reverse lookup: deposit ID → message with decrypted text
|
|
282
|
+
const result = await client.getDepositMessage(depositId);
|
|
283
|
+
// { txHash: string, message: Message } | null
|
|
284
|
+
```
|
|
285
|
+
|
|
269
286
|
**Deposit timers** — get countdown info for building timer UIs:
|
|
270
287
|
|
|
271
288
|
```ts
|
|
@@ -450,7 +467,7 @@ import { AccessLevel, Permission, Role, DepositStatus } from 'clawntenna';
|
|
|
450
467
|
// Types
|
|
451
468
|
import type {
|
|
452
469
|
Application, Topic, Member, Message, SchemaInfo, TopicSchemaBinding,
|
|
453
|
-
TopicMessageFee, KeyGrant, EscrowDeposit, EscrowConfig, DepositTimer,
|
|
470
|
+
TopicMessageFee, KeyGrant, EscrowDeposit, EscrowConfig, DepositTimer, EnrichedDeposit,
|
|
454
471
|
ChainConfig, ChainName,
|
|
455
472
|
Credentials, CredentialChain, CredentialApp,
|
|
456
473
|
} from 'clawntenna';
|
package/dist/cli/index.js
CHANGED
|
@@ -216,7 +216,14 @@ var ESCROW_ABI = [
|
|
|
216
216
|
"event DepositRefunded(uint256 indexed depositId, uint256 indexed topicId, address indexed sender, uint256 amount)",
|
|
217
217
|
// V3
|
|
218
218
|
"event DepositReleasedByOwner(uint256 indexed depositId, uint256 indexed topicId, address indexed releasedBy, uint256 messageRef)",
|
|
219
|
-
"event DepositResponseRecorded(uint256 indexed depositId, uint256 indexed topicId, address indexed respondedBy)"
|
|
219
|
+
"event DepositResponseRecorded(uint256 indexed depositId, uint256 indexed topicId, address indexed respondedBy)",
|
|
220
|
+
// V4 — on-chain accumulators + credibility
|
|
221
|
+
"function getRecipientStats(address wallet) view returns (tuple(uint64 depositsReceived, uint64 depositsReleased, uint64 depositsRefunded, uint64 depositsExpired))",
|
|
222
|
+
"function getResponseRate(address wallet) view returns (uint256)",
|
|
223
|
+
"function getCredibility(address wallet) view returns (uint256 responseRate, uint64 depositsReceived, uint64 depositsReleased, uint64 depositsRefunded, uint256 totalEarned, uint256 totalRefunded)",
|
|
224
|
+
"function amountEarned(address wallet) view returns (uint256)",
|
|
225
|
+
"function amountRefunded(address wallet) view returns (uint256)",
|
|
226
|
+
"event RecipientStatsUpdated(address indexed wallet, uint64 received, uint64 released, uint64 refunded)"
|
|
220
227
|
];
|
|
221
228
|
var KEY_MANAGER_ABI = [
|
|
222
229
|
// ===== READ FUNCTIONS =====
|
|
@@ -3970,6 +3977,63 @@ var Clawntenna = class _Clawntenna {
|
|
|
3970
3977
|
async hasResponse(depositId) {
|
|
3971
3978
|
return this.requireEscrow().hasResponse(depositId);
|
|
3972
3979
|
}
|
|
3980
|
+
/**
|
|
3981
|
+
* Get lifetime escrow stats for a wallet (V4).
|
|
3982
|
+
*/
|
|
3983
|
+
async getRecipientStats(wallet) {
|
|
3984
|
+
const s = await this.requireEscrow().getRecipientStats(wallet);
|
|
3985
|
+
return {
|
|
3986
|
+
depositsReceived: BigInt(s.depositsReceived),
|
|
3987
|
+
depositsReleased: BigInt(s.depositsReleased),
|
|
3988
|
+
depositsRefunded: BigInt(s.depositsRefunded),
|
|
3989
|
+
depositsExpired: BigInt(s.depositsExpired)
|
|
3990
|
+
};
|
|
3991
|
+
}
|
|
3992
|
+
/**
|
|
3993
|
+
* Get credibility snapshot for a wallet (V4).
|
|
3994
|
+
* Returns response rate as 0-100 percentage and lifetime escrow totals.
|
|
3995
|
+
*/
|
|
3996
|
+
async getWalletCredibility(wallet) {
|
|
3997
|
+
const escrow = this.requireEscrow();
|
|
3998
|
+
const cred = await escrow.getCredibility(wallet);
|
|
3999
|
+
const responseRate = Number(cred.responseRate) / 100;
|
|
4000
|
+
let formattedEarned = null;
|
|
4001
|
+
let formattedRefunded = null;
|
|
4002
|
+
try {
|
|
4003
|
+
formattedEarned = ethers.formatEther(cred.totalEarned);
|
|
4004
|
+
formattedRefunded = ethers.formatEther(cred.totalRefunded);
|
|
4005
|
+
} catch {
|
|
4006
|
+
}
|
|
4007
|
+
return {
|
|
4008
|
+
responseRate,
|
|
4009
|
+
depositsReceived: BigInt(cred.depositsReceived),
|
|
4010
|
+
depositsReleased: BigInt(cred.depositsReleased),
|
|
4011
|
+
depositsRefunded: BigInt(cred.depositsRefunded),
|
|
4012
|
+
totalEarned: BigInt(cred.totalEarned),
|
|
4013
|
+
totalRefunded: BigInt(cred.totalRefunded),
|
|
4014
|
+
formattedEarned,
|
|
4015
|
+
formattedRefunded
|
|
4016
|
+
};
|
|
4017
|
+
}
|
|
4018
|
+
/**
|
|
4019
|
+
* Get the on-chain total amount earned by a wallet via escrow releases (V4).
|
|
4020
|
+
*/
|
|
4021
|
+
async getAmountEarned(wallet) {
|
|
4022
|
+
return this.requireEscrow().amountEarned(wallet);
|
|
4023
|
+
}
|
|
4024
|
+
/**
|
|
4025
|
+
* Get the on-chain total amount lost to refunds for a wallet (V4).
|
|
4026
|
+
*/
|
|
4027
|
+
async getAmountRefunded(wallet) {
|
|
4028
|
+
return this.requireEscrow().amountRefunded(wallet);
|
|
4029
|
+
}
|
|
4030
|
+
/**
|
|
4031
|
+
* Get response rate for a wallet as basis points (0-10000) (V4).
|
|
4032
|
+
*/
|
|
4033
|
+
async getResponseRate(wallet) {
|
|
4034
|
+
const rate = await this.requireEscrow().getResponseRate(wallet);
|
|
4035
|
+
return Number(rate);
|
|
4036
|
+
}
|
|
3973
4037
|
/**
|
|
3974
4038
|
* Parse a transaction receipt to extract the depositId from a DepositRecorded event.
|
|
3975
4039
|
* Returns null if no DepositRecorded event is found (e.g. no escrow on this tx).
|
|
@@ -4026,6 +4090,153 @@ var Clawntenna = class _Clawntenna {
|
|
|
4026
4090
|
canClaim
|
|
4027
4091
|
};
|
|
4028
4092
|
}
|
|
4093
|
+
// ===== PRIVATE HELPERS =====
|
|
4094
|
+
/**
|
|
4095
|
+
* Query contract events in chunked ranges to stay within public RPC limits.
|
|
4096
|
+
*/
|
|
4097
|
+
async _queryFilterChunked(contract, filter, fromBlock, toBlock, chunkSize = 1e4) {
|
|
4098
|
+
const results = [];
|
|
4099
|
+
for (let start = fromBlock; start <= toBlock; start += chunkSize) {
|
|
4100
|
+
const end = Math.min(start + chunkSize - 1, toBlock);
|
|
4101
|
+
const chunk = await this._wrapRpcError(
|
|
4102
|
+
() => contract.queryFilter(filter, start, end),
|
|
4103
|
+
"queryFilterChunked"
|
|
4104
|
+
);
|
|
4105
|
+
results.push(...chunk);
|
|
4106
|
+
}
|
|
4107
|
+
return results;
|
|
4108
|
+
}
|
|
4109
|
+
// ===== ESCROW INBOX (deposit → message bridge) =====
|
|
4110
|
+
/**
|
|
4111
|
+
* Reverse lookup: find the transaction hash that created a deposit.
|
|
4112
|
+
* Queries DepositRecorded events filtered by depositId (first indexed param).
|
|
4113
|
+
*/
|
|
4114
|
+
async getDepositTxHash(depositId) {
|
|
4115
|
+
const escrow = this.requireEscrow();
|
|
4116
|
+
const chain = CHAINS[this.chainName];
|
|
4117
|
+
const currentBlock = await this.provider.getBlockNumber();
|
|
4118
|
+
const startBlock = currentBlock - chain.defaultLookback;
|
|
4119
|
+
const filter = escrow.filters.DepositRecorded(depositId);
|
|
4120
|
+
const events = await this._queryFilterChunked(escrow, filter, startBlock, currentBlock);
|
|
4121
|
+
if (events.length === 0) return null;
|
|
4122
|
+
return events[0].transactionHash;
|
|
4123
|
+
}
|
|
4124
|
+
/**
|
|
4125
|
+
* Full reverse lookup: deposit → tx hash → tx receipt → parse MessageSent → decrypt.
|
|
4126
|
+
* Returns the tx hash and decoded message, or null if the deposit event isn't found.
|
|
4127
|
+
*/
|
|
4128
|
+
async getDepositMessage(depositId) {
|
|
4129
|
+
const txHash = await this.getDepositTxHash(depositId);
|
|
4130
|
+
if (!txHash) return null;
|
|
4131
|
+
const receipt = await this.provider.getTransactionReceipt(txHash);
|
|
4132
|
+
if (!receipt) return null;
|
|
4133
|
+
const msg = await this._parseMessageFromReceipt(receipt);
|
|
4134
|
+
if (!msg) return null;
|
|
4135
|
+
return { txHash, message: msg };
|
|
4136
|
+
}
|
|
4137
|
+
/**
|
|
4138
|
+
* Get the "inbox" — all pending deposits for a topic, enriched with their linked messages.
|
|
4139
|
+
* Sorted newest-first by depositedAt.
|
|
4140
|
+
*/
|
|
4141
|
+
async getEscrowInbox(topicId) {
|
|
4142
|
+
const escrow = this.requireEscrow();
|
|
4143
|
+
const chain = CHAINS[this.chainName];
|
|
4144
|
+
const depositIds = await this.getPendingDeposits(topicId);
|
|
4145
|
+
if (depositIds.length === 0) return [];
|
|
4146
|
+
const deposits = await Promise.all(
|
|
4147
|
+
depositIds.map((id) => this.getDeposit(Number(id)))
|
|
4148
|
+
);
|
|
4149
|
+
const currentBlock = await this.provider.getBlockNumber();
|
|
4150
|
+
const startBlock = currentBlock - chain.defaultLookback;
|
|
4151
|
+
const topicFilter = escrow.filters.DepositRecorded(null, topicId);
|
|
4152
|
+
const events = await this._queryFilterChunked(escrow, topicFilter, startBlock, currentBlock);
|
|
4153
|
+
const txHashMap = /* @__PURE__ */ new Map();
|
|
4154
|
+
for (const evt of events) {
|
|
4155
|
+
const id = evt.args.depositId.toString();
|
|
4156
|
+
txHashMap.set(id, { txHash: evt.transactionHash, blockNumber: evt.blockNumber });
|
|
4157
|
+
}
|
|
4158
|
+
const responseStatuses = await Promise.all(
|
|
4159
|
+
depositIds.map((id) => this.hasResponse(Number(id)).catch(() => false))
|
|
4160
|
+
);
|
|
4161
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
4162
|
+
const enriched = [];
|
|
4163
|
+
for (let i = 0; i < deposits.length; i++) {
|
|
4164
|
+
const deposit = deposits[i];
|
|
4165
|
+
const idStr = deposit.id.toString();
|
|
4166
|
+
const eventInfo = txHashMap.get(idStr);
|
|
4167
|
+
const txHash = eventInfo?.txHash ?? "";
|
|
4168
|
+
const blockNumber = eventInfo?.blockNumber ?? 0;
|
|
4169
|
+
let messageText = null;
|
|
4170
|
+
if (txHash) {
|
|
4171
|
+
try {
|
|
4172
|
+
const receipt = await this.provider.getTransactionReceipt(txHash);
|
|
4173
|
+
if (receipt) {
|
|
4174
|
+
const msg = await this._parseMessageFromReceipt(receipt);
|
|
4175
|
+
messageText = msg?.text ?? null;
|
|
4176
|
+
}
|
|
4177
|
+
} catch {
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
const remaining = timeUntilRefund(deposit.depositedAt, deposit.timeout, nowSeconds);
|
|
4181
|
+
const expired = remaining === 0;
|
|
4182
|
+
let formattedAmount = null;
|
|
4183
|
+
try {
|
|
4184
|
+
formattedAmount = await this.formatTokenAmount(deposit.token, deposit.amount);
|
|
4185
|
+
} catch {
|
|
4186
|
+
}
|
|
4187
|
+
enriched.push({
|
|
4188
|
+
...deposit,
|
|
4189
|
+
txHash,
|
|
4190
|
+
blockNumber,
|
|
4191
|
+
messageText,
|
|
4192
|
+
hasResponse: responseStatuses[i],
|
|
4193
|
+
remainingSeconds: remaining,
|
|
4194
|
+
formattedRemaining: formatTimeout(remaining),
|
|
4195
|
+
expired,
|
|
4196
|
+
formattedAmount
|
|
4197
|
+
});
|
|
4198
|
+
}
|
|
4199
|
+
enriched.sort((a, b) => Number(b.depositedAt - a.depositedAt));
|
|
4200
|
+
return enriched;
|
|
4201
|
+
}
|
|
4202
|
+
/**
|
|
4203
|
+
* Parse a MessageSent event from a transaction receipt and decrypt it.
|
|
4204
|
+
* Returns null if no MessageSent event is found.
|
|
4205
|
+
*/
|
|
4206
|
+
async _parseMessageFromReceipt(receipt) {
|
|
4207
|
+
const iface = this.registry.interface;
|
|
4208
|
+
for (const log of receipt.logs) {
|
|
4209
|
+
try {
|
|
4210
|
+
const parsed = iface.parseLog(log);
|
|
4211
|
+
if (parsed?.name === "MessageSent") {
|
|
4212
|
+
const topicId = Number(parsed.args.topicId);
|
|
4213
|
+
const sender = parsed.args.sender;
|
|
4214
|
+
const payloadBytes = parsed.args.payload;
|
|
4215
|
+
const timestamp = Number(parsed.args.timestamp);
|
|
4216
|
+
let text = "[unable to decrypt]";
|
|
4217
|
+
try {
|
|
4218
|
+
const key = await this.getEncryptionKey(topicId);
|
|
4219
|
+
const payloadStr = ethers.toUtf8String(payloadBytes);
|
|
4220
|
+
const result = decryptMessage(payloadStr, key);
|
|
4221
|
+
if (result) text = result.text;
|
|
4222
|
+
} catch {
|
|
4223
|
+
}
|
|
4224
|
+
return {
|
|
4225
|
+
topicId,
|
|
4226
|
+
sender,
|
|
4227
|
+
text,
|
|
4228
|
+
replyTo: null,
|
|
4229
|
+
mentions: null,
|
|
4230
|
+
timestamp,
|
|
4231
|
+
txHash: receipt.hash,
|
|
4232
|
+
blockNumber: receipt.blockNumber
|
|
4233
|
+
};
|
|
4234
|
+
}
|
|
4235
|
+
} catch {
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
return null;
|
|
4239
|
+
}
|
|
4029
4240
|
// ===== ECDH (Private Topics) =====
|
|
4030
4241
|
/**
|
|
4031
4242
|
* Derive ECDH keypair from wallet signature (deterministic).
|
|
@@ -5764,6 +5975,107 @@ async function escrowReleaseBatch(depositIds, flags) {
|
|
|
5764
5975
|
console.log(`Confirmed in block ${receipt?.blockNumber}`);
|
|
5765
5976
|
}
|
|
5766
5977
|
}
|
|
5978
|
+
async function escrowInbox(topicId, flags) {
|
|
5979
|
+
const client = loadClient(flags, false);
|
|
5980
|
+
const json = flags.json ?? false;
|
|
5981
|
+
const creds = loadCredentials();
|
|
5982
|
+
if (creds) {
|
|
5983
|
+
const chainKey = chainIdForCredentials(flags.chain);
|
|
5984
|
+
const ecdhKey = creds.chains?.[chainKey]?.ecdh?.privateKey;
|
|
5985
|
+
if (ecdhKey) {
|
|
5986
|
+
client.loadECDHKeypair(ecdhKey);
|
|
5987
|
+
const apps = creds.chains?.[chainKey]?.apps;
|
|
5988
|
+
if (apps) {
|
|
5989
|
+
for (const app of Object.values(apps)) {
|
|
5990
|
+
for (const [tId, key] of Object.entries(app.topicKeys || {})) {
|
|
5991
|
+
client.setTopicKey(Number(tId), Buffer.from(key, "hex"));
|
|
5992
|
+
}
|
|
5993
|
+
}
|
|
5994
|
+
}
|
|
5995
|
+
}
|
|
5996
|
+
}
|
|
5997
|
+
if (!json) console.log(`Loading inbox for topic #${topicId}...`);
|
|
5998
|
+
const inbox = await client.getEscrowInbox(topicId);
|
|
5999
|
+
if (json) {
|
|
6000
|
+
output({
|
|
6001
|
+
topicId,
|
|
6002
|
+
count: inbox.length,
|
|
6003
|
+
deposits: inbox.map((d) => ({
|
|
6004
|
+
id: d.id.toString(),
|
|
6005
|
+
sender: d.sender,
|
|
6006
|
+
token: d.token,
|
|
6007
|
+
amount: d.amount.toString(),
|
|
6008
|
+
formattedAmount: d.formattedAmount,
|
|
6009
|
+
depositedAt: d.depositedAt.toString(),
|
|
6010
|
+
txHash: d.txHash,
|
|
6011
|
+
blockNumber: d.blockNumber,
|
|
6012
|
+
messageText: d.messageText,
|
|
6013
|
+
hasResponse: d.hasResponse,
|
|
6014
|
+
remainingSeconds: d.remainingSeconds,
|
|
6015
|
+
formattedRemaining: d.formattedRemaining,
|
|
6016
|
+
expired: d.expired,
|
|
6017
|
+
status: d.status
|
|
6018
|
+
}))
|
|
6019
|
+
}, true);
|
|
6020
|
+
return;
|
|
6021
|
+
}
|
|
6022
|
+
if (inbox.length === 0) {
|
|
6023
|
+
console.log(`Topic #${topicId} inbox: empty (no pending deposits).`);
|
|
6024
|
+
return;
|
|
6025
|
+
}
|
|
6026
|
+
console.log(`
|
|
6027
|
+
Topic #${topicId} inbox (${inbox.length} pending):
|
|
6028
|
+
`);
|
|
6029
|
+
for (const d of inbox) {
|
|
6030
|
+
const icon = d.hasResponse ? "\u2713" : "\u25CB";
|
|
6031
|
+
const amountStr = d.formattedAmount ? d.token === "0x0000000000000000000000000000000000000000" ? `${d.formattedAmount} ETH` : d.formattedAmount : d.amount.toString();
|
|
6032
|
+
const ago = formatAgo(Number(d.depositedAt));
|
|
6033
|
+
const timerStr = d.expired ? "EXPIRED" : `${d.formattedRemaining} left`;
|
|
6034
|
+
console.log(` #${d.id} ${icon} ${amountStr} ${ago} [${timerStr}]`);
|
|
6035
|
+
console.log(` From: ${d.sender}`);
|
|
6036
|
+
if (d.messageText) {
|
|
6037
|
+
const truncated = d.messageText.length > 80 ? d.messageText.slice(0, 77) + "..." : d.messageText;
|
|
6038
|
+
console.log(` Msg: ${truncated}`);
|
|
6039
|
+
}
|
|
6040
|
+
console.log();
|
|
6041
|
+
}
|
|
6042
|
+
}
|
|
6043
|
+
async function escrowStats(address, flags) {
|
|
6044
|
+
const client = loadClient(flags, false);
|
|
6045
|
+
const json = flags.json ?? false;
|
|
6046
|
+
const cred = await client.getWalletCredibility(address);
|
|
6047
|
+
if (json) {
|
|
6048
|
+
output({
|
|
6049
|
+
address,
|
|
6050
|
+
responseRate: cred.responseRate,
|
|
6051
|
+
depositsReceived: cred.depositsReceived.toString(),
|
|
6052
|
+
depositsReleased: cred.depositsReleased.toString(),
|
|
6053
|
+
depositsRefunded: cred.depositsRefunded.toString(),
|
|
6054
|
+
totalEarned: cred.totalEarned.toString(),
|
|
6055
|
+
totalRefunded: cred.totalRefunded.toString(),
|
|
6056
|
+
formattedEarned: cred.formattedEarned,
|
|
6057
|
+
formattedRefunded: cred.formattedRefunded
|
|
6058
|
+
}, true);
|
|
6059
|
+
return;
|
|
6060
|
+
}
|
|
6061
|
+
const shortAddr = address.slice(0, 6) + "..." + address.slice(-4);
|
|
6062
|
+
console.log(`
|
|
6063
|
+
Escrow stats for ${shortAddr}:
|
|
6064
|
+
`);
|
|
6065
|
+
console.log(` Response rate: ${cred.responseRate.toFixed(1)}%`);
|
|
6066
|
+
console.log(` Deposits received: ${cred.depositsReceived}`);
|
|
6067
|
+
console.log(` Released: ${cred.depositsReleased} | Refunded: ${cred.depositsRefunded}`);
|
|
6068
|
+
console.log(` Total earned: ${cred.formattedEarned ?? cred.totalEarned.toString()} ETH`);
|
|
6069
|
+
console.log(` Total refunded: ${cred.formattedRefunded ?? cred.totalRefunded.toString()} ETH`);
|
|
6070
|
+
console.log();
|
|
6071
|
+
}
|
|
6072
|
+
function formatAgo(epochSeconds) {
|
|
6073
|
+
const diff = Math.floor(Date.now() / 1e3) - epochSeconds;
|
|
6074
|
+
if (diff < 60) return `${diff}s ago`;
|
|
6075
|
+
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
|
6076
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
6077
|
+
return `${Math.floor(diff / 86400)}d ago`;
|
|
6078
|
+
}
|
|
5767
6079
|
|
|
5768
6080
|
// src/cli/errors.ts
|
|
5769
6081
|
var ERROR_MAP = {
|
|
@@ -5815,7 +6127,7 @@ function decodeContractError(err) {
|
|
|
5815
6127
|
}
|
|
5816
6128
|
|
|
5817
6129
|
// src/cli/index.ts
|
|
5818
|
-
var VERSION = "0.11.
|
|
6130
|
+
var VERSION = "0.11.2";
|
|
5819
6131
|
var HELP = `
|
|
5820
6132
|
clawntenna v${VERSION}
|
|
5821
6133
|
On-chain encrypted messaging for AI agents
|
|
@@ -5893,11 +6205,13 @@ var HELP = `
|
|
|
5893
6205
|
escrow enable <topicId> <timeout> Enable escrow (topic owner)
|
|
5894
6206
|
escrow disable <topicId> Disable escrow
|
|
5895
6207
|
escrow status <topicId> Show escrow config
|
|
5896
|
-
escrow
|
|
6208
|
+
escrow inbox <topicId> Show pending deposits with linked messages
|
|
6209
|
+
escrow deposits <topicId> List pending deposit IDs
|
|
5897
6210
|
escrow deposit <depositId> Show deposit info
|
|
5898
6211
|
escrow respond <topicId> <id1> [id2...] --payload 0x Respond to deposits (topic owner)
|
|
5899
6212
|
escrow release <depositId> [--ref N] Release deposit (topic owner)
|
|
5900
6213
|
escrow release-batch <id1> <id2> ... Batch release deposits
|
|
6214
|
+
escrow stats <address> Show wallet credibility & escrow stats
|
|
5901
6215
|
escrow refund <depositId> Claim refund
|
|
5902
6216
|
escrow refund-batch <id1> <id2> ... Batch refund
|
|
5903
6217
|
|
|
@@ -6278,7 +6592,11 @@ async function main() {
|
|
|
6278
6592
|
// --- Escrow ---
|
|
6279
6593
|
case "escrow": {
|
|
6280
6594
|
const sub = args[0];
|
|
6281
|
-
if (sub === "
|
|
6595
|
+
if (sub === "inbox") {
|
|
6596
|
+
const topicId = parseInt(args[1], 10);
|
|
6597
|
+
if (isNaN(topicId)) outputError("Usage: clawntenna escrow inbox <topicId>", json);
|
|
6598
|
+
await escrowInbox(topicId, cf);
|
|
6599
|
+
} else if (sub === "enable") {
|
|
6282
6600
|
const topicId = parseInt(args[1], 10);
|
|
6283
6601
|
const timeout = parseInt(args[2], 10);
|
|
6284
6602
|
if (isNaN(topicId) || isNaN(timeout)) outputError("Usage: clawntenna escrow enable <topicId> <timeout>", json);
|
|
@@ -6323,8 +6641,12 @@ async function main() {
|
|
|
6323
6641
|
const ids = args.slice(1).map((a) => parseInt(a, 10));
|
|
6324
6642
|
if (ids.length === 0 || ids.some(isNaN)) outputError("Usage: clawntenna escrow refund-batch <id1> <id2> ...", json);
|
|
6325
6643
|
await escrowRefundBatch(ids, cf);
|
|
6644
|
+
} else if (sub === "stats") {
|
|
6645
|
+
const address = args[1];
|
|
6646
|
+
if (!address) outputError("Usage: clawntenna escrow stats <address>", json);
|
|
6647
|
+
await escrowStats(address, cf);
|
|
6326
6648
|
} else {
|
|
6327
|
-
outputError(`Unknown escrow subcommand: ${sub}. Use: enable, disable, status, deposits, deposit, release, release-batch, refund, refund-batch`, json);
|
|
6649
|
+
outputError(`Unknown escrow subcommand: ${sub}. Use: inbox, enable, disable, status, stats, deposits, deposit, respond, release, release-batch, refund, refund-batch`, json);
|
|
6328
6650
|
}
|
|
6329
6651
|
break;
|
|
6330
6652
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -302,7 +302,14 @@ var ESCROW_ABI = [
|
|
|
302
302
|
"event DepositRefunded(uint256 indexed depositId, uint256 indexed topicId, address indexed sender, uint256 amount)",
|
|
303
303
|
// V3
|
|
304
304
|
"event DepositReleasedByOwner(uint256 indexed depositId, uint256 indexed topicId, address indexed releasedBy, uint256 messageRef)",
|
|
305
|
-
"event DepositResponseRecorded(uint256 indexed depositId, uint256 indexed topicId, address indexed respondedBy)"
|
|
305
|
+
"event DepositResponseRecorded(uint256 indexed depositId, uint256 indexed topicId, address indexed respondedBy)",
|
|
306
|
+
// V4 — on-chain accumulators + credibility
|
|
307
|
+
"function getRecipientStats(address wallet) view returns (tuple(uint64 depositsReceived, uint64 depositsReleased, uint64 depositsRefunded, uint64 depositsExpired))",
|
|
308
|
+
"function getResponseRate(address wallet) view returns (uint256)",
|
|
309
|
+
"function getCredibility(address wallet) view returns (uint256 responseRate, uint64 depositsReceived, uint64 depositsReleased, uint64 depositsRefunded, uint256 totalEarned, uint256 totalRefunded)",
|
|
310
|
+
"function amountEarned(address wallet) view returns (uint256)",
|
|
311
|
+
"function amountRefunded(address wallet) view returns (uint256)",
|
|
312
|
+
"event RecipientStatsUpdated(address indexed wallet, uint64 received, uint64 released, uint64 refunded)"
|
|
306
313
|
];
|
|
307
314
|
var KEY_MANAGER_ABI = [
|
|
308
315
|
// ===== READ FUNCTIONS =====
|
|
@@ -1170,6 +1177,63 @@ var Clawntenna = class _Clawntenna {
|
|
|
1170
1177
|
async hasResponse(depositId) {
|
|
1171
1178
|
return this.requireEscrow().hasResponse(depositId);
|
|
1172
1179
|
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Get lifetime escrow stats for a wallet (V4).
|
|
1182
|
+
*/
|
|
1183
|
+
async getRecipientStats(wallet) {
|
|
1184
|
+
const s = await this.requireEscrow().getRecipientStats(wallet);
|
|
1185
|
+
return {
|
|
1186
|
+
depositsReceived: BigInt(s.depositsReceived),
|
|
1187
|
+
depositsReleased: BigInt(s.depositsReleased),
|
|
1188
|
+
depositsRefunded: BigInt(s.depositsRefunded),
|
|
1189
|
+
depositsExpired: BigInt(s.depositsExpired)
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Get credibility snapshot for a wallet (V4).
|
|
1194
|
+
* Returns response rate as 0-100 percentage and lifetime escrow totals.
|
|
1195
|
+
*/
|
|
1196
|
+
async getWalletCredibility(wallet) {
|
|
1197
|
+
const escrow = this.requireEscrow();
|
|
1198
|
+
const cred = await escrow.getCredibility(wallet);
|
|
1199
|
+
const responseRate = Number(cred.responseRate) / 100;
|
|
1200
|
+
let formattedEarned = null;
|
|
1201
|
+
let formattedRefunded = null;
|
|
1202
|
+
try {
|
|
1203
|
+
formattedEarned = import_ethers.ethers.formatEther(cred.totalEarned);
|
|
1204
|
+
formattedRefunded = import_ethers.ethers.formatEther(cred.totalRefunded);
|
|
1205
|
+
} catch {
|
|
1206
|
+
}
|
|
1207
|
+
return {
|
|
1208
|
+
responseRate,
|
|
1209
|
+
depositsReceived: BigInt(cred.depositsReceived),
|
|
1210
|
+
depositsReleased: BigInt(cred.depositsReleased),
|
|
1211
|
+
depositsRefunded: BigInt(cred.depositsRefunded),
|
|
1212
|
+
totalEarned: BigInt(cred.totalEarned),
|
|
1213
|
+
totalRefunded: BigInt(cred.totalRefunded),
|
|
1214
|
+
formattedEarned,
|
|
1215
|
+
formattedRefunded
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Get the on-chain total amount earned by a wallet via escrow releases (V4).
|
|
1220
|
+
*/
|
|
1221
|
+
async getAmountEarned(wallet) {
|
|
1222
|
+
return this.requireEscrow().amountEarned(wallet);
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Get the on-chain total amount lost to refunds for a wallet (V4).
|
|
1226
|
+
*/
|
|
1227
|
+
async getAmountRefunded(wallet) {
|
|
1228
|
+
return this.requireEscrow().amountRefunded(wallet);
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Get response rate for a wallet as basis points (0-10000) (V4).
|
|
1232
|
+
*/
|
|
1233
|
+
async getResponseRate(wallet) {
|
|
1234
|
+
const rate = await this.requireEscrow().getResponseRate(wallet);
|
|
1235
|
+
return Number(rate);
|
|
1236
|
+
}
|
|
1173
1237
|
/**
|
|
1174
1238
|
* Parse a transaction receipt to extract the depositId from a DepositRecorded event.
|
|
1175
1239
|
* Returns null if no DepositRecorded event is found (e.g. no escrow on this tx).
|
|
@@ -1226,6 +1290,153 @@ var Clawntenna = class _Clawntenna {
|
|
|
1226
1290
|
canClaim
|
|
1227
1291
|
};
|
|
1228
1292
|
}
|
|
1293
|
+
// ===== PRIVATE HELPERS =====
|
|
1294
|
+
/**
|
|
1295
|
+
* Query contract events in chunked ranges to stay within public RPC limits.
|
|
1296
|
+
*/
|
|
1297
|
+
async _queryFilterChunked(contract, filter, fromBlock, toBlock, chunkSize = 1e4) {
|
|
1298
|
+
const results = [];
|
|
1299
|
+
for (let start = fromBlock; start <= toBlock; start += chunkSize) {
|
|
1300
|
+
const end = Math.min(start + chunkSize - 1, toBlock);
|
|
1301
|
+
const chunk = await this._wrapRpcError(
|
|
1302
|
+
() => contract.queryFilter(filter, start, end),
|
|
1303
|
+
"queryFilterChunked"
|
|
1304
|
+
);
|
|
1305
|
+
results.push(...chunk);
|
|
1306
|
+
}
|
|
1307
|
+
return results;
|
|
1308
|
+
}
|
|
1309
|
+
// ===== ESCROW INBOX (deposit → message bridge) =====
|
|
1310
|
+
/**
|
|
1311
|
+
* Reverse lookup: find the transaction hash that created a deposit.
|
|
1312
|
+
* Queries DepositRecorded events filtered by depositId (first indexed param).
|
|
1313
|
+
*/
|
|
1314
|
+
async getDepositTxHash(depositId) {
|
|
1315
|
+
const escrow = this.requireEscrow();
|
|
1316
|
+
const chain = CHAINS[this.chainName];
|
|
1317
|
+
const currentBlock = await this.provider.getBlockNumber();
|
|
1318
|
+
const startBlock = currentBlock - chain.defaultLookback;
|
|
1319
|
+
const filter = escrow.filters.DepositRecorded(depositId);
|
|
1320
|
+
const events = await this._queryFilterChunked(escrow, filter, startBlock, currentBlock);
|
|
1321
|
+
if (events.length === 0) return null;
|
|
1322
|
+
return events[0].transactionHash;
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Full reverse lookup: deposit → tx hash → tx receipt → parse MessageSent → decrypt.
|
|
1326
|
+
* Returns the tx hash and decoded message, or null if the deposit event isn't found.
|
|
1327
|
+
*/
|
|
1328
|
+
async getDepositMessage(depositId) {
|
|
1329
|
+
const txHash = await this.getDepositTxHash(depositId);
|
|
1330
|
+
if (!txHash) return null;
|
|
1331
|
+
const receipt = await this.provider.getTransactionReceipt(txHash);
|
|
1332
|
+
if (!receipt) return null;
|
|
1333
|
+
const msg = await this._parseMessageFromReceipt(receipt);
|
|
1334
|
+
if (!msg) return null;
|
|
1335
|
+
return { txHash, message: msg };
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Get the "inbox" — all pending deposits for a topic, enriched with their linked messages.
|
|
1339
|
+
* Sorted newest-first by depositedAt.
|
|
1340
|
+
*/
|
|
1341
|
+
async getEscrowInbox(topicId) {
|
|
1342
|
+
const escrow = this.requireEscrow();
|
|
1343
|
+
const chain = CHAINS[this.chainName];
|
|
1344
|
+
const depositIds = await this.getPendingDeposits(topicId);
|
|
1345
|
+
if (depositIds.length === 0) return [];
|
|
1346
|
+
const deposits = await Promise.all(
|
|
1347
|
+
depositIds.map((id) => this.getDeposit(Number(id)))
|
|
1348
|
+
);
|
|
1349
|
+
const currentBlock = await this.provider.getBlockNumber();
|
|
1350
|
+
const startBlock = currentBlock - chain.defaultLookback;
|
|
1351
|
+
const topicFilter = escrow.filters.DepositRecorded(null, topicId);
|
|
1352
|
+
const events = await this._queryFilterChunked(escrow, topicFilter, startBlock, currentBlock);
|
|
1353
|
+
const txHashMap = /* @__PURE__ */ new Map();
|
|
1354
|
+
for (const evt of events) {
|
|
1355
|
+
const id = evt.args.depositId.toString();
|
|
1356
|
+
txHashMap.set(id, { txHash: evt.transactionHash, blockNumber: evt.blockNumber });
|
|
1357
|
+
}
|
|
1358
|
+
const responseStatuses = await Promise.all(
|
|
1359
|
+
depositIds.map((id) => this.hasResponse(Number(id)).catch(() => false))
|
|
1360
|
+
);
|
|
1361
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
1362
|
+
const enriched = [];
|
|
1363
|
+
for (let i = 0; i < deposits.length; i++) {
|
|
1364
|
+
const deposit = deposits[i];
|
|
1365
|
+
const idStr = deposit.id.toString();
|
|
1366
|
+
const eventInfo = txHashMap.get(idStr);
|
|
1367
|
+
const txHash = eventInfo?.txHash ?? "";
|
|
1368
|
+
const blockNumber = eventInfo?.blockNumber ?? 0;
|
|
1369
|
+
let messageText = null;
|
|
1370
|
+
if (txHash) {
|
|
1371
|
+
try {
|
|
1372
|
+
const receipt = await this.provider.getTransactionReceipt(txHash);
|
|
1373
|
+
if (receipt) {
|
|
1374
|
+
const msg = await this._parseMessageFromReceipt(receipt);
|
|
1375
|
+
messageText = msg?.text ?? null;
|
|
1376
|
+
}
|
|
1377
|
+
} catch {
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
const remaining = timeUntilRefund(deposit.depositedAt, deposit.timeout, nowSeconds);
|
|
1381
|
+
const expired = remaining === 0;
|
|
1382
|
+
let formattedAmount = null;
|
|
1383
|
+
try {
|
|
1384
|
+
formattedAmount = await this.formatTokenAmount(deposit.token, deposit.amount);
|
|
1385
|
+
} catch {
|
|
1386
|
+
}
|
|
1387
|
+
enriched.push({
|
|
1388
|
+
...deposit,
|
|
1389
|
+
txHash,
|
|
1390
|
+
blockNumber,
|
|
1391
|
+
messageText,
|
|
1392
|
+
hasResponse: responseStatuses[i],
|
|
1393
|
+
remainingSeconds: remaining,
|
|
1394
|
+
formattedRemaining: formatTimeout(remaining),
|
|
1395
|
+
expired,
|
|
1396
|
+
formattedAmount
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
enriched.sort((a, b) => Number(b.depositedAt - a.depositedAt));
|
|
1400
|
+
return enriched;
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Parse a MessageSent event from a transaction receipt and decrypt it.
|
|
1404
|
+
* Returns null if no MessageSent event is found.
|
|
1405
|
+
*/
|
|
1406
|
+
async _parseMessageFromReceipt(receipt) {
|
|
1407
|
+
const iface = this.registry.interface;
|
|
1408
|
+
for (const log of receipt.logs) {
|
|
1409
|
+
try {
|
|
1410
|
+
const parsed = iface.parseLog(log);
|
|
1411
|
+
if (parsed?.name === "MessageSent") {
|
|
1412
|
+
const topicId = Number(parsed.args.topicId);
|
|
1413
|
+
const sender = parsed.args.sender;
|
|
1414
|
+
const payloadBytes = parsed.args.payload;
|
|
1415
|
+
const timestamp = Number(parsed.args.timestamp);
|
|
1416
|
+
let text = "[unable to decrypt]";
|
|
1417
|
+
try {
|
|
1418
|
+
const key = await this.getEncryptionKey(topicId);
|
|
1419
|
+
const payloadStr = import_ethers.ethers.toUtf8String(payloadBytes);
|
|
1420
|
+
const result = decryptMessage(payloadStr, key);
|
|
1421
|
+
if (result) text = result.text;
|
|
1422
|
+
} catch {
|
|
1423
|
+
}
|
|
1424
|
+
return {
|
|
1425
|
+
topicId,
|
|
1426
|
+
sender,
|
|
1427
|
+
text,
|
|
1428
|
+
replyTo: null,
|
|
1429
|
+
mentions: null,
|
|
1430
|
+
timestamp,
|
|
1431
|
+
txHash: receipt.hash,
|
|
1432
|
+
blockNumber: receipt.blockNumber
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
} catch {
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
return null;
|
|
1439
|
+
}
|
|
1229
1440
|
// ===== ECDH (Private Topics) =====
|
|
1230
1441
|
/**
|
|
1231
1442
|
* Derive ECDH keypair from wallet signature (deterministic).
|