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 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.0";
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 deposits <topicId> List pending deposits
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 === "enable") {
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).