@hashspan/core 0.7.0 → 0.9.0

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/dist/index.mjs CHANGED
@@ -5,7 +5,7 @@ const ATTR_GEN_AI_AGENT_NAME = "gen_ai.agent.name";
5
5
  /**
6
6
  * Agent identity as GenAI attributes. A field set in the static identity always wins; Baggage, which a remote caller
7
7
  * can set, only fills fields it leaves unset, and is not read at all with `fromBaggage` false
8
- * (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0011-agent-identity-precedence.md).
8
+ * (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.9.0/docs/adr/0011-agent-identity-precedence.md).
9
9
  */
10
10
  function agentAttributes(ctx, identity, fromBaggage = true) {
11
11
  const baggage = fromBaggage ? propagation.getBaggage(ctx) : void 0;
@@ -21,7 +21,7 @@ function agentAttributes(ctx, identity, fromBaggage = true) {
21
21
  /**
22
22
  * Attribute keys emitted by hashspan.
23
23
  *
24
- * Stability: development. See https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/semconv.md for
24
+ * Stability: development. See https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.9.0/docs/semconv.md for
25
25
  * definitions and value types. These names are a public contract: changes follow the deprecation policy in AGENTS.md.
26
26
  */
27
27
  const ATTR_BLOCKCHAIN_SYSTEM = "blockchain.system";
@@ -32,6 +32,12 @@ const ATTR_BLOCKCHAIN_TX_FROM = "blockchain.tx.from";
32
32
  const ATTR_BLOCKCHAIN_TX_TO = "blockchain.tx.to";
33
33
  const ATTR_BLOCKCHAIN_TX_VALUE = "blockchain.tx.value";
34
34
  const ATTR_BLOCKCHAIN_TX_NONCE = "blockchain.tx.nonce";
35
+ /** Number of EIP-7702 authorizations a type 4 transaction carries. */
36
+ const ATTR_BLOCKCHAIN_TX_AUTHORIZATION_COUNT = "blockchain.tx.authorization.count";
37
+ /** Delegated contract address of each well-formed EIP-7702 authorization, per the address mode; at most 64. */
38
+ const ATTR_BLOCKCHAIN_TX_AUTHORIZATION_ADDRESSES = "blockchain.tx.authorization.addresses";
39
+ /** Chain id of each well-formed EIP-7702 authorization, aligned with the addresses; 0 means every chain. */
40
+ const ATTR_BLOCKCHAIN_TX_AUTHORIZATION_CHAIN_IDS = "blockchain.tx.authorization.chain_ids";
35
41
  const ATTR_BLOCKCHAIN_TX_STATUS = "blockchain.tx.status";
36
42
  const ATTR_BLOCKCHAIN_TX_GAS_USED = "blockchain.tx.gas.used";
37
43
  const ATTR_BLOCKCHAIN_TX_EFFECTIVE_GAS_PRICE = "blockchain.tx.effective_gas_price";
@@ -45,12 +51,12 @@ const ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_NAME = "blockchain.contract.function.nam
45
51
  const ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_SELECTOR = "blockchain.contract.function.selector";
46
52
  /**
47
53
  * Opt-in: decoded call arguments as a JSON array. See
48
- * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0004-privacy-defaults.md.
54
+ * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.9.0/docs/adr/0004-privacy-defaults.md.
49
55
  */
50
56
  const ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_ARGUMENTS = "blockchain.contract.function.arguments";
51
57
  /**
52
58
  * Payments settled on chain by a party other than the agent, e.g. an x402 facilitator. See
53
- * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0013-x402-payments.md.
59
+ * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.9.0/docs/adr/0013-x402-payments.md.
54
60
  */
55
61
  const ATTR_BLOCKCHAIN_PAYMENT_PROTOCOL = "blockchain.payment.protocol";
56
62
  const ATTR_BLOCKCHAIN_PAYMENT_PAYER = "blockchain.payment.payer";
@@ -63,12 +69,52 @@ const ATTR_BLOCKCHAIN_PAYMENT_SETTLED_AMOUNT = "blockchain.payment.settled_amoun
63
69
  /**
64
70
  * Whether the settlement transaction's receipt carries the payment, as checked by the adapter; absent when no check
65
71
  * was possible. See
66
- * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0017-x402-payment-verification.md.
72
+ * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.9.0/docs/adr/0017-x402-payment-verification.md.
67
73
  */
68
74
  const ATTR_BLOCKCHAIN_PAYMENT_VERIFIED = "blockchain.payment.verified";
69
75
  /** x402's own payment fields. */
70
76
  const ATTR_X402_SCHEME = "x402.scheme";
71
77
  const ATTR_X402_RESOURCE = "x402.resource";
78
+ /**
79
+ * User operations of ERC-4337 smart accounts. See
80
+ * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.9.0/docs/adr/0021-user-operations.md.
81
+ */
82
+ const ATTR_BLOCKCHAIN_USER_OPERATION_HASH = "blockchain.user_operation.hash";
83
+ const ATTR_BLOCKCHAIN_USER_OPERATION_SENDER = "blockchain.user_operation.sender";
84
+ const ATTR_BLOCKCHAIN_USER_OPERATION_ENTRY_POINT = "blockchain.user_operation.entry_point";
85
+ const ATTR_BLOCKCHAIN_USER_OPERATION_CALL_COUNT = "blockchain.user_operation.call_count";
86
+ const ATTR_BLOCKCHAIN_USER_OPERATION_NONCE = "blockchain.user_operation.nonce";
87
+ const ATTR_BLOCKCHAIN_USER_OPERATION_SUCCESS = "blockchain.user_operation.success";
88
+ const ATTR_BLOCKCHAIN_USER_OPERATION_GAS_USED = "blockchain.user_operation.gas.used";
89
+ /** What the operation itself paid, in wei; the bundle transaction's fee covers every operation in the bundle. */
90
+ const ATTR_BLOCKCHAIN_USER_OPERATION_GAS_COST = "blockchain.user_operation.gas.cost";
91
+ const ATTR_BLOCKCHAIN_USER_OPERATION_PAYMASTER = "blockchain.user_operation.paymaster";
92
+ /**
93
+ * The batch id the wallet returned, truncated after 256 characters. See
94
+ * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.9.0/docs/adr/0022-call-batches.md.
95
+ */
96
+ const ATTR_BLOCKCHAIN_CALL_BATCH_ID = "blockchain.call_batch.id";
97
+ const ATTR_BLOCKCHAIN_CALL_BATCH_SENDER = "blockchain.call_batch.sender";
98
+ const ATTR_BLOCKCHAIN_CALL_BATCH_CALL_COUNT = "blockchain.call_batch.call_count";
99
+ /** The outcome of a batch from chain data: `success`, `reverted` or `partially_reverted`. */
100
+ const ATTR_BLOCKCHAIN_CALL_BATCH_STATUS = "blockchain.call_batch.status";
101
+ /** The EIP-5792 status code of the batch as the wallet reported it, e.g. 200 confirmed or 500 reverted; spans only. */
102
+ const ATTR_BLOCKCHAIN_CALL_BATCH_STATUS_CODE = "blockchain.call_batch.status_code";
103
+ const ATTR_BLOCKCHAIN_CALL_BATCH_ATOMIC = "blockchain.call_batch.atomic";
104
+ /** Hashes of the transactions whose receipts the wallet reported for the batch. */
105
+ const ATTR_BLOCKCHAIN_CALL_BATCH_TRANSACTION_HASHES = "blockchain.call_batch.transaction_hashes";
106
+ /**
107
+ * Metrics only: what a send, confirmation or fee sample is about. Recorded as `user_operation` on samples of user
108
+ * operations and `call_batch` on those of call batches, and absent on those of transactions.
109
+ */
110
+ const ATTR_BLOCKCHAIN_OPERATION_SUBJECT = "blockchain.operation.subject";
111
+ /** Values for {@link ATTR_BLOCKCHAIN_OPERATION_SUBJECT}. */
112
+ const BLOCKCHAIN_OPERATION_SUBJECT_VALUE_USER_OPERATION = "user_operation";
113
+ const BLOCKCHAIN_OPERATION_SUBJECT_VALUE_CALL_BATCH = "call_batch";
114
+ /** Values for {@link ATTR_BLOCKCHAIN_CALL_BATCH_STATUS}. */
115
+ const BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_SUCCESS = "success";
116
+ const BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_REVERTED = "reverted";
117
+ const BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_PARTIALLY_REVERTED = "partially_reverted";
72
118
  /** Values for {@link ATTR_BLOCKCHAIN_SYSTEM}. */
73
119
  const BLOCKCHAIN_SYSTEM_VALUE_EVM = "evm";
74
120
  /** Values for {@link ATTR_BLOCKCHAIN_OPERATION_NAME}. */
@@ -87,7 +133,7 @@ const BLOCKCHAIN_TX_STATUS_VALUE_REVERTED = "reverted";
87
133
  /**
88
134
  * @deprecated No longer recorded: a confirm span that gave up waiting records `error.type` `timeout` and no
89
135
  * `blockchain.tx.status`. The constant is removed in 1.0. See
90
- * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0016-timeout-is-an-observer-outcome.md.
136
+ * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.9.0/docs/adr/0016-timeout-is-an-observer-outcome.md.
91
137
  */
92
138
  const BLOCKCHAIN_TX_STATUS_VALUE_TIMEOUT = "timeout";
93
139
  const BLOCKCHAIN_TX_STATUS_VALUE_REPLACED = "replaced";
@@ -101,11 +147,20 @@ const ATTR_ERROR_TYPE = "error.type";
101
147
  const ERROR_TYPE_VALUE_OTHER = "_OTHER";
102
148
  //#endregion
103
149
  //#region src/metrics.ts
104
- /** Duration of a send: from the start of the sending call until the hash is known or the call failed. */
150
+ /**
151
+ * Duration of a send of a transaction or user operation: from the start of the sending call until the hash is known or
152
+ * the call failed.
153
+ */
105
154
  const METRIC_BLOCKCHAIN_CLIENT_SEND_DURATION = "blockchain.client.send.duration";
106
- /** Duration of a confirmation: from the start of the wait until the receipt, a timeout or a failure. */
155
+ /**
156
+ * Duration of a confirmation of a transaction or user operation: from the start of the wait until the receipt, a
157
+ * timeout or a failure.
158
+ */
107
159
  const METRIC_BLOCKCHAIN_CLIENT_CONFIRMATION_DURATION = "blockchain.client.confirmation.duration";
108
- /** Total fee of a mined transaction (execution fee plus L1 data fee), in the chain's smallest unit (wei). */
160
+ /**
161
+ * Total fee of a mined transaction (execution fee plus L1 data fee), or the cost of a user operation, in the chain's
162
+ * smallest unit (wei).
163
+ */
109
164
  const METRIC_BLOCKCHAIN_CLIENT_FEE = "blockchain.client.fee";
110
165
  const DURATION_BUCKETS = [
111
166
  .05,
@@ -123,6 +178,21 @@ const DURATION_BUCKETS = [
123
178
  300
124
179
  ];
125
180
  const FEE_BUCKETS = Array.from({ length: 11 }, (_, i) => 10 ** (i + 8));
181
+ const ERROR_CLASS = /^[A-Z][A-Za-z]{0,62}Error$|^Error$/;
182
+ const ERROR_CODE = /^[a-z]{1,32}(_[a-z]{1,32}){0,7}$/;
183
+ /**
184
+ * `attributes` with `error.type` kept only if it is an error class name or a lower-case code, else `_OTHER`. Error
185
+ * names are free text (any `Error.name`), and metrics do not pass through the `redact` hook; the span keeps its own
186
+ * `error.type`.
187
+ */
188
+ function withMetricErrorType(attributes) {
189
+ const type = attributes[ATTR_ERROR_TYPE];
190
+ if (type === void 0) return attributes;
191
+ return typeof type === "string" && (type === "_OTHER" || ERROR_CLASS.test(type) || ERROR_CODE.test(type)) ? attributes : {
192
+ ...attributes,
193
+ [ATTR_ERROR_TYPE]: ERROR_TYPE_VALUE_OTHER
194
+ };
195
+ }
126
196
  /** Milliseconds since the epoch of a span time, as the OpenTelemetry API accepts it. */
127
197
  function toEpochMs(time) {
128
198
  if (time === void 0) return Date.now();
@@ -140,17 +210,17 @@ function createTxMetrics(meterProvider, name, version) {
140
210
  return {
141
211
  send: meter.createHistogram(METRIC_BLOCKCHAIN_CLIENT_SEND_DURATION, {
142
212
  unit: "s",
143
- description: "Duration of sending a transaction, until its hash is known",
213
+ description: "Duration of sending a transaction or user operation, until its hash is known",
144
214
  advice: { explicitBucketBoundaries: DURATION_BUCKETS }
145
215
  }),
146
216
  confirmation: meter.createHistogram(METRIC_BLOCKCHAIN_CLIENT_CONFIRMATION_DURATION, {
147
217
  unit: "s",
148
- description: "Duration of waiting for a transaction receipt",
218
+ description: "Duration of waiting for a transaction or user operation receipt",
149
219
  advice: { explicitBucketBoundaries: DURATION_BUCKETS }
150
220
  }),
151
221
  fee: meter.createHistogram(METRIC_BLOCKCHAIN_CLIENT_FEE, {
152
222
  unit: "{wei}",
153
- description: "Total fee of a mined transaction",
223
+ description: "Total fee of a mined transaction, or the cost of a user operation",
154
224
  advice: { explicitBucketBoundaries: FEE_BUCKETS }
155
225
  })
156
226
  };
@@ -166,10 +236,10 @@ function createTxMetrics(meterProvider, name, version) {
166
236
  };
167
237
  return {
168
238
  sendDuration: (seconds, attributes) => record("send duration", () => {
169
- if (seconds >= 0) get().send.record(seconds, attributes);
239
+ if (seconds >= 0) get().send.record(seconds, withMetricErrorType(attributes));
170
240
  }),
171
241
  confirmationDuration: (seconds, attributes) => record("confirmation duration", () => {
172
- if (seconds >= 0) get().confirmation.record(seconds, attributes);
242
+ if (seconds >= 0) get().confirmation.record(seconds, withMetricErrorType(attributes));
173
243
  }),
174
244
  fee: (wei, attributes) => record("fee", () => {
175
245
  if (wei >= 0n) get().fee.record(Number(wei), attributes);
@@ -286,7 +356,7 @@ function resolveAddressFormatter(option) {
286
356
  hash: void 0
287
357
  };
288
358
  switch (mode) {
289
- case "raw": return formatter((address) => address, false);
359
+ case "raw": return formatter((address) => address.toLowerCase(), false);
290
360
  case "off": return OFF_ADDRESS_FORMATTER;
291
361
  case "hashed": {
292
362
  const hashFn = hash ?? defaultHash();
@@ -310,6 +380,8 @@ const ADDRESS_LENGTH = 42;
310
380
  */
311
381
  const MAX_HEX_LENGTH = 66;
312
382
  const MAX_MESSAGE_LENGTH = 256;
383
+ /** A URL in free text: `scheme://` up to the next whitespace, quote or bracket; the bounded scheme keeps it linear. */
384
+ const URL_IN_TEXT = /\b[A-Za-z][A-Za-z0-9+.-]{0,31}:\/\/[^\s"'<>()[\]{}]+/g;
313
385
  /**
314
386
  * Rewrites every address in `text` with the address mode (`<address>` when it records none). In `off` and `hashed`
315
387
  * mode, hex values longer than an address become `<hex>`, because they can embed one.
@@ -321,11 +393,12 @@ function formatAddressesIn(text, formatAddress) {
321
393
  });
322
394
  }
323
395
  /**
324
- * First line of an error message with addresses per address mode and longer hex data (such as calldata)
325
- * replaced by `<hex>`. Best effort: other free text is kept, so the redaction hook still runs on the result.
396
+ * First line of an error message with URLs cut to their origin (an RPC URL can carry an API key in its path or
397
+ * query), addresses per address mode and longer hex data (such as calldata) replaced by `<hex>`. Best effort: other
398
+ * free text is kept, so the redaction hook still runs on the result.
326
399
  */
327
400
  function sanitizeErrorMessage(message, formatAddress) {
328
- const sanitized = formatAddressesIn(message.split("\n", 1)[0]?.trim() ?? "", formatAddress).replace(HEX, (hex) => hex.length > MAX_HEX_LENGTH ? "<hex>" : hex);
401
+ const sanitized = formatAddressesIn((message.split("\n", 1)[0]?.trim() ?? "").replace(URL_IN_TEXT, (url) => hidesUserInfo(url) ? "<url>" : originOf(url) ?? "<url>"), formatAddress).replace(HEX, (hex) => hex.length > MAX_HEX_LENGTH ? "<hex>" : hex);
329
402
  return sanitized.length > MAX_MESSAGE_LENGTH ? `${sanitized.slice(0, MAX_MESSAGE_LENGTH)}...` : sanitized;
330
403
  }
331
404
  const MAX_ARGUMENTS_LENGTH = 4096;
@@ -428,7 +501,16 @@ function paymentResourceOf(resource, mode) {
428
501
  if (mode === "off" || hidesUserInfo(resource)) return void 0;
429
502
  const recorded = mode === "path" ? sanitizeResource(resource) : originOf(resource);
430
503
  if (recorded === void 0) return void 0;
431
- return recorded.length > MAX_RESOURCE_LENGTH ? `${recorded.slice(0, MAX_RESOURCE_LENGTH)}...` : recorded;
504
+ return cutAt(recorded, MAX_RESOURCE_LENGTH);
505
+ }
506
+ /**
507
+ * `text` cut to `max` characters, followed by `...`. A hex value that the cut splits is dropped whole: the part left
508
+ * is shorter than an address, so the address mode, applied later, would no longer recognise it as one.
509
+ */
510
+ function cutAt(text, max) {
511
+ if (text.length <= max) return text;
512
+ const head = text.slice(0, max);
513
+ return `${/^[0-9a-fA-F]/.test(text.slice(max)) ? head.replace(/0[xX][0-9a-fA-F]*$/, "") : head}...`;
432
514
  }
433
515
  /** Longest `x402.resource` recorded; the value comes from the server that asks for the payment. */
434
516
  const MAX_RESOURCE_LENGTH = 512;
@@ -462,7 +544,7 @@ function sanitizeResource(resource) {
462
544
  }
463
545
  //#endregion
464
546
  //#region src/version.ts
465
- const VERSION = "0.7.0";
547
+ const VERSION = "0.9.0";
466
548
  //#endregion
467
549
  //#region src/tracker.ts
468
550
  const INSTRUMENTATION_NAME = "@hashspan/core";
@@ -485,15 +567,42 @@ const NON_SENSITIVE_KEYS = /* @__PURE__ */ new Set([
485
567
  ATTR_BLOCKCHAIN_PAYMENT_PROTOCOL,
486
568
  ATTR_BLOCKCHAIN_PAYMENT_STATUS,
487
569
  ATTR_BLOCKCHAIN_PAYMENT_VERIFIED,
570
+ ATTR_BLOCKCHAIN_USER_OPERATION_HASH,
571
+ ATTR_BLOCKCHAIN_USER_OPERATION_SUCCESS,
572
+ ATTR_BLOCKCHAIN_CALL_BATCH_ID,
573
+ ATTR_BLOCKCHAIN_CALL_BATCH_STATUS,
488
574
  ATTR_ERROR_TYPE,
489
575
  ATTR_EXCEPTION_TYPE
490
576
  ]);
491
577
  const TX_HASH = /^0x[0-9a-fA-F]{64}$/;
492
578
  const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
579
+ const ZERO_ADDRESS = /^0x0{40}$/;
580
+ /** The value of an own data property of `target`; undefined for an accessor, so no getter of the caller runs. */
581
+ function ownValue(target, key) {
582
+ if (typeof target !== "object" || target === null) return void 0;
583
+ const descriptor = Object.getOwnPropertyDescriptor(target, key);
584
+ return descriptor !== void 0 && "value" in descriptor ? descriptor.value : void 0;
585
+ }
493
586
  /** A non-negative integer that fits in 256 bits. */
494
587
  const AMOUNT = /^(0|[1-9][0-9]{0,77})$/;
588
+ /** A `0x` hex quantity of at most 256 bits, as JSON-RPC encodes integers. */
589
+ const HEX_QUANTITY = /^0x[0-9a-fA-F]{1,64}$/;
590
+ const MAX_UINT256 = 2n ** 256n - 1n;
591
+ const MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER);
495
592
  /** `error.type` of a wait that gave up: a confirmation or a payment whose outcome was never learned. */
496
593
  const OBSERVER_TIMEOUT = "timeout";
594
+ /** `error.type` of a call batch that failed without being included (EIP-5792 status 400). */
595
+ const CALL_BATCH_FAILED = "failed";
596
+ /** A call batch id: `0x`-prefixed hex of at most 8194 characters, the bound EIP-5792 sets. */
597
+ const CALL_BATCH_ID = /^0x[0-9a-fA-F]{1,8192}$/;
598
+ /** Longest call batch id recorded as an attribute. */
599
+ const MAX_CALL_BATCH_ID_ATTRIBUTE_LENGTH = 256;
600
+ /** Most transaction hashes recorded for one call batch. */
601
+ const MAX_CALL_BATCH_TRANSACTION_HASHES = 64;
602
+ /** Most EIP-7702 authorizations listed on a send span; the count covers all of them. */
603
+ const MAX_AUTHORIZATIONS = 64;
604
+ /** EIP-5792 status code of a batch that is still pending. */
605
+ const CALL_BATCH_PENDING = 100;
497
606
  const PAYMENT_STATUSES = /* @__PURE__ */ new Set([
498
607
  BLOCKCHAIN_PAYMENT_STATUS_VALUE_SETTLED,
499
608
  BLOCKCHAIN_PAYMENT_STATUS_VALUE_PENDING,
@@ -510,6 +619,11 @@ const noopSend = (parent) => ({
510
619
  end: () => {},
511
620
  fail: () => {}
512
621
  });
622
+ const noopUserOperationSend = (parent) => ({
623
+ context: parent,
624
+ end: () => {},
625
+ fail: () => {}
626
+ });
513
627
  const NOOP_PAYMENT = {
514
628
  end: () => {},
515
629
  fail: () => {},
@@ -521,6 +635,21 @@ const NOOP_CONFIRM = {
521
635
  timeout: () => {},
522
636
  fail: () => {}
523
637
  };
638
+ const NOOP_USER_OPERATION_CONFIRM = {
639
+ end: () => {},
640
+ timeout: () => {},
641
+ fail: () => {}
642
+ };
643
+ const noopCallBatchSend = (parent) => ({
644
+ context: parent,
645
+ end: () => {},
646
+ fail: () => {}
647
+ });
648
+ const NOOP_CALL_BATCH_CONFIRM = {
649
+ end: () => {},
650
+ timeout: () => {},
651
+ fail: () => {}
652
+ };
524
653
  /** Runs `fn`, logging instead of throwing: instrumentation must never break the caller. */
525
654
  function safely(what, fn, fallback) {
526
655
  try {
@@ -543,12 +672,27 @@ const ERROR_TYPE_OVERRIDE = /^[A-Za-z0-9_.-]{1,64}$/;
543
672
  function identifier(value) {
544
673
  return typeof value === "string" && ERROR_TYPE_OVERRIDE.test(value) ? value : void 0;
545
674
  }
675
+ /**
676
+ * A non-negative integer of at most 256 bits, from a bigint, a safe integer, or a decimal or `0x` hex string, as
677
+ * bundlers return them; undefined for anything else.
678
+ */
679
+ function quantity(value) {
680
+ let parsed;
681
+ if (typeof value === "bigint") parsed = value;
682
+ else if (typeof value === "number" && Number.isSafeInteger(value)) parsed = BigInt(value);
683
+ else if (typeof value === "string" && (AMOUNT.test(value) || HEX_QUANTITY.test(value))) parsed = BigInt(value);
684
+ return parsed !== void 0 && parsed >= 0n && parsed <= MAX_UINT256 ? parsed : void 0;
685
+ }
686
+ /** A quantity as a number, or undefined when it is none or too large to be one exactly. */
687
+ function smallQuantity(value) {
688
+ const parsed = quantity(value);
689
+ return parsed !== void 0 && parsed <= MAX_SAFE_INTEGER ? Number(parsed) : void 0;
690
+ }
546
691
  /** A decimal amount, or undefined when `value` is not a non-negative integer. */
547
692
  function amount(value) {
548
693
  const text = typeof value === "bigint" ? value.toString() : value;
549
694
  return typeof text === "string" && AMOUNT.test(text) ? text : void 0;
550
695
  }
551
- /** The `error.type` for a failure: an adapter's override when it is a short identifier, else the class name. */
552
696
  /** A finite number, an `HrTime` pair or a `Date`: what the deprecated positional `endTime` argument takes. */
553
697
  function isTimeInput(value) {
554
698
  if (typeof value === "number") return Number.isFinite(value);
@@ -580,6 +724,7 @@ function handleOptions(second, third) {
580
724
  return {};
581
725
  }
582
726
  }
727
+ /** The `error.type` for a failure: an adapter's override when it is a short identifier, else the class name. */
583
728
  function reportedErrorType(error, options) {
584
729
  const override = options?.errorType;
585
730
  if (override === void 0) return errorType(error);
@@ -587,9 +732,53 @@ function reportedErrorType(error, options) {
587
732
  diag.debug("hashspan: ignoring an error type that is not a short identifier");
588
733
  return errorType(error);
589
734
  }
735
+ /** A call batch id the tracker keys and records. */
736
+ function isCallBatchId(id) {
737
+ return typeof id === "string" && CALL_BATCH_ID.test(id);
738
+ }
739
+ /** Whether `status` says the batch is still pending: its wait resolved before an outcome. */
740
+ function isPendingCallBatch(status) {
741
+ return status?.statusCode === CALL_BATCH_PENDING;
742
+ }
743
+ /**
744
+ * Joins the confirm span for `hash` in `registry`, opening it with `open` for the first handle; undefined when the
745
+ * key recently got a receipt. A receipt from any handle ends the span; a timeout or failure only ends it when it is the
746
+ * last handle still waiting.
747
+ */
748
+ function joinConfirm(registry, chainId, hash, open) {
749
+ const current = registry.get(chainId, hash);
750
+ if (current === "settled") return void 0;
751
+ let confirm = current;
752
+ if (!confirm) {
753
+ confirm = open();
754
+ registry.start(chainId, hash, confirm);
755
+ }
756
+ const shared = confirm;
757
+ shared.active += 1;
758
+ let done = false;
759
+ return {
760
+ shared,
761
+ receive: () => {
762
+ if (done || shared.ended) return false;
763
+ done = true;
764
+ shared.active -= 1;
765
+ shared.ended = true;
766
+ return true;
767
+ },
768
+ withdraw: (end) => {
769
+ if (done || shared.ended) return;
770
+ done = true;
771
+ shared.active -= 1;
772
+ if (shared.active > 0) return;
773
+ shared.ended = true;
774
+ registry.release(chainId, hash, shared);
775
+ end();
776
+ }
777
+ };
778
+ }
590
779
  /**
591
780
  * Creates a tracker that records transactions as `send` and `confirm` spans, and payments as `payment` spans, with
592
- * `@opentelemetry/api` (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/semconv.md). It makes
781
+ * `@opentelemetry/api` (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.9.0/docs/semconv.md). It makes
593
782
  * no network calls; the caller passes hashes and receipts. Its methods and handles never throw: failures are logged
594
783
  * via `diag`, and a method that fails returns a handle that records nothing.
595
784
  */
@@ -605,6 +794,22 @@ function createTxTracker(options = {}) {
605
794
  const formatAddress = safely("configure address mode", () => resolveAddressFormatter(options.address), OFF_ADDRESS_FORMATTER);
606
795
  const errorMessages = safely("configure error message mode", () => resolveErrorMessageMode(options.errorMessages), "off");
607
796
  const paymentResource = safely("configure payment resource mode", () => resolvePaymentResourceMode(options.paymentResource), "off");
797
+ const userOperationLinks = new LinkStore({
798
+ ttlMs: options.linkTtlMs ?? DEFAULT_LINK_TTL_MS,
799
+ maxEntries: options.maxTrackedTransactions ?? DEFAULT_MAX_TRACKED
800
+ });
801
+ const userOperationConfirmations = new ConfirmRegistry({
802
+ ttlMs: options.linkTtlMs ?? DEFAULT_LINK_TTL_MS,
803
+ maxEntries: options.maxTrackedTransactions ?? DEFAULT_MAX_TRACKED
804
+ });
805
+ const callBatchLinks = new LinkStore({
806
+ ttlMs: options.linkTtlMs ?? DEFAULT_LINK_TTL_MS,
807
+ maxEntries: options.maxTrackedTransactions ?? DEFAULT_MAX_TRACKED
808
+ });
809
+ const callBatchConfirmations = new ConfirmRegistry({
810
+ ttlMs: options.linkTtlMs ?? DEFAULT_LINK_TTL_MS,
811
+ maxEntries: options.maxTrackedTransactions ?? DEFAULT_MAX_TRACKED
812
+ });
608
813
  const txMetrics = createTxMetrics(options.meterProvider, INSTRUMENTATION_NAME, VERSION);
609
814
  /** Attributes of a metric: low-cardinality only, never an address, hash or agent identity. */
610
815
  const metricAttributes = (chainId, extra = {}) => ({
@@ -612,6 +817,16 @@ function createTxTracker(options = {}) {
612
817
  [ATTR_BLOCKCHAIN_CHAIN_ID]: chainId,
613
818
  ...extra
614
819
  });
820
+ /** Metric attributes of a user operation sample: as for transactions, plus what the sample is about. */
821
+ const userOperationMetricAttributes = (chainId, extra = {}) => metricAttributes(chainId, {
822
+ [ATTR_BLOCKCHAIN_OPERATION_SUBJECT]: BLOCKCHAIN_OPERATION_SUBJECT_VALUE_USER_OPERATION,
823
+ ...extra
824
+ });
825
+ /** Metric attributes of a call batch sample. */
826
+ const callBatchMetricAttributes = (chainId, extra = {}) => metricAttributes(chainId, {
827
+ [ATTR_BLOCKCHAIN_OPERATION_SUBJECT]: BLOCKCHAIN_OPERATION_SUBJECT_VALUE_CALL_BATCH,
828
+ ...extra
829
+ });
615
830
  const secondsSince = (startMs, endTime) => (toEpochMs(endTime) - startMs) / 1e3;
616
831
  let tracer;
617
832
  const getTracer = () => {
@@ -637,7 +852,7 @@ function createTxTracker(options = {}) {
637
852
  /**
638
853
  * Exception event attributes for `error`, per the error message mode. The error object itself is never handed to
639
854
  * the SDK: its message and stack can carry addresses and calldata
640
- * (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0006-error-privacy.md).
855
+ * (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.9.0/docs/adr/0006-error-privacy.md).
641
856
  */
642
857
  const exceptionAttributes = (type, error) => {
643
858
  const attributes = { [ATTR_EXCEPTION_TYPE]: type };
@@ -692,8 +907,30 @@ function createTxTracker(options = {}) {
692
907
  const formatted = formatAddress(address);
693
908
  if (formatted !== void 0) attributes[key] = formatted;
694
909
  };
695
- /** Records `address` only if it is one: payment addresses come from remote parties. */
696
- const setPaymentAddress = (attributes, key, address) => {
910
+ /**
911
+ * Records an EIP-7702 authorization list: its length, and for each well-formed entry (an address and a non-negative
912
+ * integer chain id, read from own data properties only) its address per the address mode and its chain id, at most
913
+ * {@link MAX_AUTHORIZATIONS}. The two lists stay aligned: an entry is listed in both or in neither.
914
+ */
915
+ const setAuthorizations = (attributes, list) => {
916
+ if (!Array.isArray(list) || list.length === 0) return;
917
+ attributes[ATTR_BLOCKCHAIN_TX_AUTHORIZATION_COUNT] = list.length;
918
+ const addresses = [];
919
+ const chainIds = [];
920
+ for (const entry of list.slice(0, MAX_AUTHORIZATIONS)) {
921
+ const address = ownValue(entry, "address");
922
+ const chainId = ownValue(entry, "chainId");
923
+ if (typeof address !== "string" || !ADDRESS.test(address)) continue;
924
+ if (typeof chainId !== "number" || !Number.isSafeInteger(chainId) || chainId < 0) continue;
925
+ const formatted = formatAddress(address);
926
+ if (formatted !== void 0) addresses.push(formatted);
927
+ chainIds.push(chainId);
928
+ }
929
+ if (addresses.length > 0) attributes[ATTR_BLOCKCHAIN_TX_AUTHORIZATION_ADDRESSES] = addresses;
930
+ if (chainIds.length > 0) attributes[ATTR_BLOCKCHAIN_TX_AUTHORIZATION_CHAIN_IDS] = chainIds;
931
+ };
932
+ /** Records `address` only if it is one: payment and user operation addresses come from remote parties. */
933
+ const setRemoteAddress = (attributes, key, address) => {
697
934
  if (typeof address === "string" && ADDRESS.test(address)) setAddress(attributes, key, address);
698
935
  };
699
936
  const baseAttributes = (chainId, operation, ctx) => ({
@@ -709,6 +946,11 @@ function createTxTracker(options = {}) {
709
946
  setAddress(attributes, ATTR_BLOCKCHAIN_TX_TO, input.to);
710
947
  if (input.value !== void 0) attributes[ATTR_BLOCKCHAIN_TX_VALUE] = input.value.toString();
711
948
  if (input.nonce !== void 0) attributes[ATTR_BLOCKCHAIN_TX_NONCE] = input.nonce;
949
+ try {
950
+ setAuthorizations(attributes, input.authorizations);
951
+ } catch (error) {
952
+ diag.debug(`hashspan: could not record authorizations (${errorType(error)})`);
953
+ }
712
954
  if (input.functionName !== void 0) attributes[ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_NAME] = input.functionName;
713
955
  if (input.functionSelector !== void 0) attributes[ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_SELECTOR] = input.functionSelector;
714
956
  if (options.recordFunctionArguments === true && input.functionArguments !== void 0) try {
@@ -829,7 +1071,7 @@ function createTxTracker(options = {}) {
829
1071
  };
830
1072
  /**
831
1073
  * Ends `shared` with `receipt`, attributing it to the transaction that was mined
832
- * (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0008-replaced-transactions.md).
1074
+ * (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.9.0/docs/adr/0008-replaced-transactions.md).
833
1075
  */
834
1076
  const endWithReceipt = (chainId, hash, shared, receipt, endTime) => {
835
1077
  const mined = receipt.transactionHash;
@@ -859,41 +1101,22 @@ function createTxTracker(options = {}) {
859
1101
  */
860
1102
  const startConfirm = (input, parentCtx) => {
861
1103
  const { chainId, hash } = input;
862
- const current = confirmations.get(chainId, hash);
863
- if (current === "settled") return NOOP_CONFIRM;
864
- let confirm = current;
865
- if (!confirm) {
866
- confirm = openConfirm(input, parentCtx);
867
- confirmations.start(chainId, hash, confirm);
868
- }
869
- const shared = confirm;
870
- shared.active += 1;
871
- let done = false;
872
- const withdraw = (end) => {
873
- if (done || shared.ended) return;
874
- done = true;
875
- shared.active -= 1;
876
- if (shared.active > 0) return;
877
- shared.ended = true;
878
- confirmations.release(chainId, hash, shared);
879
- end();
880
- };
1104
+ const claim = joinConfirm(confirmations, chainId, hash, () => openConfirm(input, parentCtx));
1105
+ if (!claim) return NOOP_CONFIRM;
1106
+ const { shared } = claim;
881
1107
  return {
882
1108
  end: (receipt, second) => {
883
1109
  const { endTime } = handleOptions(second);
884
- if (done || shared.ended) return;
885
- done = true;
886
- shared.active -= 1;
887
- shared.ended = true;
1110
+ if (!claim.receive()) return;
888
1111
  safely("record receipt", () => endWithReceipt(chainId, hash, shared, receipt, endTime), void 0);
889
1112
  },
890
1113
  timeout: (second) => {
891
1114
  const { endTime } = handleOptions(second);
892
- withdraw(() => shared.timeout(endTime));
1115
+ claim.withdraw(() => shared.timeout(endTime));
893
1116
  },
894
1117
  fail: (error, second) => {
895
1118
  const { endTime } = handleOptions(second);
896
- withdraw(() => shared.fail(error, endTime));
1119
+ claim.withdraw(() => shared.fail(error, endTime));
897
1120
  }
898
1121
  };
899
1122
  };
@@ -902,10 +1125,10 @@ function createTxTracker(options = {}) {
902
1125
  const attributes = baseAttributes(input.chainId, BLOCKCHAIN_OPERATION_NAME_VALUE_PAYMENT, parent);
903
1126
  const protocol = identifier(input.protocol);
904
1127
  if (protocol !== void 0) attributes[ATTR_BLOCKCHAIN_PAYMENT_PROTOCOL] = protocol;
905
- setPaymentAddress(attributes, ATTR_BLOCKCHAIN_PAYMENT_PAYER, input.payer);
1128
+ setRemoteAddress(attributes, ATTR_BLOCKCHAIN_PAYMENT_PAYER, input.payer);
906
1129
  const knownPayer = typeof input.payer === "string" && ADDRESS.test(input.payer);
907
- setPaymentAddress(attributes, ATTR_BLOCKCHAIN_PAYMENT_RECIPIENT, input.recipient);
908
- setPaymentAddress(attributes, ATTR_BLOCKCHAIN_PAYMENT_ASSET, input.asset);
1130
+ setRemoteAddress(attributes, ATTR_BLOCKCHAIN_PAYMENT_RECIPIENT, input.recipient);
1131
+ setRemoteAddress(attributes, ATTR_BLOCKCHAIN_PAYMENT_ASSET, input.asset);
909
1132
  const paid = amount(input.amount);
910
1133
  if (paid !== void 0) attributes[ATTR_BLOCKCHAIN_PAYMENT_AMOUNT] = paid;
911
1134
  const scheme = identifier(input.x402?.scheme);
@@ -937,7 +1160,7 @@ function createTxTracker(options = {}) {
937
1160
  const settled = { [ATTR_BLOCKCHAIN_PAYMENT_STATUS]: status };
938
1161
  const hash = settlement.hash;
939
1162
  if (linkHash(hash)) settled[ATTR_BLOCKCHAIN_TX_HASH] = hash;
940
- if (!knownPayer) setPaymentAddress(settled, ATTR_BLOCKCHAIN_PAYMENT_PAYER, settlement.payer);
1163
+ if (!knownPayer) setRemoteAddress(settled, ATTR_BLOCKCHAIN_PAYMENT_PAYER, settlement.payer);
941
1164
  const settledAmount = amount(settlement.amount);
942
1165
  if (settledAmount !== void 0) {
943
1166
  settled[ATTR_BLOCKCHAIN_PAYMENT_SETTLED_AMOUNT] = settledAmount;
@@ -958,11 +1181,296 @@ function createTxTracker(options = {}) {
958
1181
  link: (hash) => safely("link the payment span", () => void linkHash(hash), void 0)
959
1182
  };
960
1183
  };
1184
+ const startUserOperationSend = (input, parentCtx) => {
1185
+ const parent = parentCtx ?? context.active();
1186
+ const { chainId } = input;
1187
+ const attributes = baseAttributes(chainId, BLOCKCHAIN_OPERATION_NAME_VALUE_SEND, parent);
1188
+ setRemoteAddress(attributes, ATTR_BLOCKCHAIN_USER_OPERATION_SENDER, input.sender);
1189
+ setRemoteAddress(attributes, ATTR_BLOCKCHAIN_USER_OPERATION_ENTRY_POINT, input.entryPoint);
1190
+ const callCount = smallQuantity(input.callCount);
1191
+ if (callCount !== void 0) attributes[ATTR_BLOCKCHAIN_USER_OPERATION_CALL_COUNT] = callCount;
1192
+ const span = getTracer().startSpan(`send ${chainId}`, {
1193
+ kind: SpanKind.CLIENT,
1194
+ attributes: redact(attributes),
1195
+ ...input.startTime !== void 0 ? { startTime: input.startTime } : {}
1196
+ }, parent);
1197
+ const finish = finisher(span);
1198
+ const startMs = toEpochMs(input.startTime);
1199
+ const recordSend = (endTime, errorType) => txMetrics.sendDuration(secondsSince(startMs, endTime), userOperationMetricAttributes(chainId, errorType === void 0 ? {} : { [ATTR_ERROR_TYPE]: errorType }));
1200
+ return {
1201
+ context: trace.setSpan(parent, span),
1202
+ end: (result, second) => {
1203
+ const { endTime } = handleOptions(second);
1204
+ finish("record user operation hash", () => {
1205
+ const hash = result?.userOpHash;
1206
+ if (typeof hash !== "string" || !TX_HASH.test(hash)) {
1207
+ diag.debug("hashspan: ending a send span without a valid user operation hash");
1208
+ return;
1209
+ }
1210
+ userOperationLinks.set(chainId, hash, {
1211
+ spanContext: span.spanContext(),
1212
+ parent
1213
+ });
1214
+ span.setAttributes(redact({ [ATTR_BLOCKCHAIN_USER_OPERATION_HASH]: hash }));
1215
+ recordSend(endTime);
1216
+ }, endTime);
1217
+ },
1218
+ fail: (error, second) => {
1219
+ const read = handleOptions(second);
1220
+ finish("record user operation send failure", () => recordSend(read.endTime, markError(span, reportedErrorType(error, read), error, errorType(error))), read.endTime);
1221
+ }
1222
+ };
1223
+ };
1224
+ /**
1225
+ * Attributes of a user operation receipt. Its values come from a bundler: what is malformed is left out. The bundle
1226
+ * transaction's status, gas and fee are not recorded: they cover every operation in the bundle (ADR 0021).
1227
+ */
1228
+ const userOperationReceiptAttributes = (receipt) => {
1229
+ const attributes = {};
1230
+ const success = receipt.success;
1231
+ if (typeof success === "boolean") attributes[ATTR_BLOCKCHAIN_USER_OPERATION_SUCCESS] = success;
1232
+ const gasUsed = smallQuantity(receipt.actualGasUsed);
1233
+ if (gasUsed !== void 0) attributes[ATTR_BLOCKCHAIN_USER_OPERATION_GAS_USED] = gasUsed;
1234
+ const gasCost = quantity(receipt.actualGasCost);
1235
+ if (gasCost !== void 0) attributes[ATTR_BLOCKCHAIN_USER_OPERATION_GAS_COST] = gasCost.toString();
1236
+ setRemoteAddress(attributes, ATTR_BLOCKCHAIN_USER_OPERATION_SENDER, receipt.sender);
1237
+ const nonce = quantity(receipt.nonce);
1238
+ if (nonce !== void 0) attributes[ATTR_BLOCKCHAIN_USER_OPERATION_NONCE] = nonce.toString();
1239
+ const paymaster = receipt.paymaster;
1240
+ if (typeof paymaster === "string" && !ZERO_ADDRESS.test(paymaster)) setRemoteAddress(attributes, ATTR_BLOCKCHAIN_USER_OPERATION_PAYMASTER, paymaster);
1241
+ setRemoteAddress(attributes, ATTR_BLOCKCHAIN_USER_OPERATION_ENTRY_POINT, receipt.entryPoint);
1242
+ const reason = receipt.revertReason;
1243
+ if (typeof reason === "string") attributes[ATTR_BLOCKCHAIN_TX_REVERT_REASON] = formatAddressesIn(reason, formatAddress);
1244
+ const bundle = receipt.transactionHash;
1245
+ if (typeof bundle === "string" && TX_HASH.test(bundle)) attributes[ATTR_BLOCKCHAIN_TX_HASH] = bundle;
1246
+ const block = smallQuantity(receipt.blockNumber);
1247
+ if (block !== void 0) attributes[ATTR_BLOCKCHAIN_BLOCK_NUMBER] = block;
1248
+ return attributes;
1249
+ };
1250
+ /** Opens the confirm span of a user operation. */
1251
+ const openUserOperationConfirm = (input, parentCtx) => {
1252
+ const { chainId, userOpHash } = input;
1253
+ const sent = userOperationLinks.get(chainId, userOpHash);
1254
+ const active = context.active();
1255
+ const parent = parentCtx ?? (trace.getSpan(active) ? active : sent?.parent ?? active);
1256
+ const attributes = baseAttributes(chainId, BLOCKCHAIN_OPERATION_NAME_VALUE_CONFIRM, parent);
1257
+ attributes[ATTR_BLOCKCHAIN_USER_OPERATION_HASH] = userOpHash;
1258
+ const span = getTracer().startSpan(`confirm ${chainId}`, {
1259
+ kind: SpanKind.CLIENT,
1260
+ attributes: redact(attributes),
1261
+ links: sent ? [{ context: sent.spanContext }] : [],
1262
+ ...input.startTime !== void 0 ? { startTime: input.startTime } : {}
1263
+ }, parent);
1264
+ const finish = finisher(span);
1265
+ const startMs = toEpochMs(input.startTime);
1266
+ const recordConfirmation = (endTime, outcome) => txMetrics.confirmationDuration(secondsSince(startMs, endTime), userOperationMetricAttributes(chainId, outcome));
1267
+ return {
1268
+ active: 0,
1269
+ ended: false,
1270
+ receipt: (receipt, endTime) => finish("record user operation receipt", () => {
1271
+ const attributes = userOperationReceiptAttributes(receipt ?? {});
1272
+ span.setAttributes(redact(attributes));
1273
+ const success = attributes[ATTR_BLOCKCHAIN_USER_OPERATION_SUCCESS];
1274
+ if (success === false) markError(span, BLOCKCHAIN_TX_STATUS_VALUE_REVERTED);
1275
+ const outcome = typeof success === "boolean" ? { [ATTR_BLOCKCHAIN_USER_OPERATION_SUCCESS]: success } : {};
1276
+ recordConfirmation(endTime, outcome);
1277
+ const cost = attributes[ATTR_BLOCKCHAIN_USER_OPERATION_GAS_COST];
1278
+ if (typeof cost === "string") txMetrics.fee(BigInt(cost), userOperationMetricAttributes(chainId, outcome));
1279
+ }, endTime),
1280
+ timeout: (endTime) => finish("record user operation confirmation timeout", () => recordConfirmation(endTime, { [ATTR_ERROR_TYPE]: markError(span, OBSERVER_TIMEOUT) }), endTime),
1281
+ fail: (error, read) => finish("record user operation confirmation failure", () => recordConfirmation(read.endTime, { [ATTR_ERROR_TYPE]: markError(span, reportedErrorType(error, read), error, errorType(error)) }), read.endTime)
1282
+ };
1283
+ };
1284
+ const startUserOperationConfirm = (input, parentCtx) => {
1285
+ const { chainId, userOpHash } = input;
1286
+ if (typeof userOpHash !== "string" || !TX_HASH.test(userOpHash)) {
1287
+ diag.debug("hashspan: not confirming a user operation without a valid hash");
1288
+ return NOOP_USER_OPERATION_CONFIRM;
1289
+ }
1290
+ const claim = joinConfirm(userOperationConfirmations, chainId, userOpHash, () => openUserOperationConfirm(input, parentCtx));
1291
+ if (!claim) return NOOP_USER_OPERATION_CONFIRM;
1292
+ const { shared } = claim;
1293
+ return {
1294
+ end: (receipt, second) => {
1295
+ const { endTime } = handleOptions(second);
1296
+ if (!claim.receive()) return;
1297
+ userOperationConfirmations.settle(chainId, userOpHash, shared);
1298
+ shared.receipt(receipt, endTime);
1299
+ },
1300
+ timeout: (second) => {
1301
+ const { endTime } = handleOptions(second);
1302
+ claim.withdraw(() => shared.timeout(endTime));
1303
+ },
1304
+ fail: (error, second) => {
1305
+ const read = handleOptions(second);
1306
+ claim.withdraw(() => shared.fail(error, read));
1307
+ }
1308
+ };
1309
+ };
1310
+ const startCallBatchSend = (input, parentCtx) => {
1311
+ const parent = parentCtx ?? context.active();
1312
+ const { chainId } = input;
1313
+ const attributes = baseAttributes(chainId, BLOCKCHAIN_OPERATION_NAME_VALUE_SEND, parent);
1314
+ setRemoteAddress(attributes, ATTR_BLOCKCHAIN_CALL_BATCH_SENDER, input.sender);
1315
+ const callCount = smallQuantity(input.callCount);
1316
+ if (callCount !== void 0) attributes[ATTR_BLOCKCHAIN_CALL_BATCH_CALL_COUNT] = callCount;
1317
+ const span = getTracer().startSpan(`send ${chainId}`, {
1318
+ kind: SpanKind.CLIENT,
1319
+ attributes: redact(attributes),
1320
+ ...input.startTime !== void 0 ? { startTime: input.startTime } : {}
1321
+ }, parent);
1322
+ const finish = finisher(span);
1323
+ const startMs = toEpochMs(input.startTime);
1324
+ const recordSend = (endTime, errorType) => txMetrics.sendDuration(secondsSince(startMs, endTime), callBatchMetricAttributes(chainId, errorType === void 0 ? {} : { [ATTR_ERROR_TYPE]: errorType }));
1325
+ return {
1326
+ context: trace.setSpan(parent, span),
1327
+ end: (result, second) => {
1328
+ const { endTime } = handleOptions(second);
1329
+ finish("record call batch id", () => {
1330
+ const id = result?.id;
1331
+ if (!isCallBatchId(id)) {
1332
+ diag.debug("hashspan: ending a send span without a valid call batch id");
1333
+ return;
1334
+ }
1335
+ const sent = {
1336
+ spanContext: span.spanContext(),
1337
+ parent
1338
+ };
1339
+ callBatchLinks.set(chainId, id, sent);
1340
+ const hashes = result.transactionHashes;
1341
+ if (Array.isArray(hashes)) {
1342
+ for (const hash of hashes) if (typeof hash === "string" && TX_HASH.test(hash) && !/^0x0+$/.test(hash)) links.set(chainId, hash, sent);
1343
+ }
1344
+ span.setAttributes(redact({ [ATTR_BLOCKCHAIN_CALL_BATCH_ID]: id.slice(0, MAX_CALL_BATCH_ID_ATTRIBUTE_LENGTH) }));
1345
+ recordSend(endTime);
1346
+ }, endTime);
1347
+ },
1348
+ fail: (error, second) => {
1349
+ const read = handleOptions(second);
1350
+ finish("record call batch send failure", () => recordSend(read.endTime, markError(span, reportedErrorType(error, read), error, errorType(error))), read.endTime);
1351
+ }
1352
+ };
1353
+ };
1354
+ /** Attributes of a call batch status. Its values come from a wallet: what is malformed is left out. */
1355
+ const callBatchStatusAttributes = (status) => {
1356
+ const attributes = {};
1357
+ const code = smallQuantity(status.statusCode);
1358
+ if (code !== void 0) attributes[ATTR_BLOCKCHAIN_CALL_BATCH_STATUS_CODE] = code;
1359
+ const atomic = status.atomic;
1360
+ if (typeof atomic === "boolean") attributes[ATTR_BLOCKCHAIN_CALL_BATCH_ATOMIC] = atomic;
1361
+ const receipts = status.receipts;
1362
+ if (Array.isArray(receipts)) {
1363
+ const hashes = [];
1364
+ const seen = /* @__PURE__ */ new Set();
1365
+ let block;
1366
+ for (const receipt of receipts) {
1367
+ const hash = receipt?.transactionHash;
1368
+ if (typeof hash === "string" && TX_HASH.test(hash) && !seen.has(hash.toLowerCase()) && hashes.length < MAX_CALL_BATCH_TRANSACTION_HASHES) {
1369
+ seen.add(hash.toLowerCase());
1370
+ hashes.push(hash);
1371
+ }
1372
+ const number = smallQuantity(receipt?.blockNumber);
1373
+ if (number !== void 0 && (block === void 0 || number > block)) block = number;
1374
+ }
1375
+ if (hashes.length > 0) attributes[ATTR_BLOCKCHAIN_CALL_BATCH_TRANSACTION_HASHES] = hashes;
1376
+ if (block !== void 0) attributes[ATTR_BLOCKCHAIN_BLOCK_NUMBER] = block;
1377
+ }
1378
+ return attributes;
1379
+ };
1380
+ /**
1381
+ * The outcome of an EIP-5792 status code (ADR 0022): `blockchain.call_batch.status` for outcomes from chain data,
1382
+ * `error.type` for the rest; a code the spec does not define, or none, is `_OTHER`.
1383
+ */
1384
+ const callBatchOutcome = (code) => {
1385
+ switch (code) {
1386
+ case 200: return { status: BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_SUCCESS };
1387
+ case 400: return { errorType: CALL_BATCH_FAILED };
1388
+ case 500: return {
1389
+ status: BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_REVERTED,
1390
+ errorType: BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_REVERTED
1391
+ };
1392
+ case 600: return {
1393
+ status: BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_PARTIALLY_REVERTED,
1394
+ errorType: BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_PARTIALLY_REVERTED
1395
+ };
1396
+ default: return { errorType: ERROR_TYPE_VALUE_OTHER };
1397
+ }
1398
+ };
1399
+ /** Opens the confirm span of a call batch. */
1400
+ const openCallBatchConfirm = (input, parentCtx) => {
1401
+ const { chainId, id } = input;
1402
+ const sent = callBatchLinks.get(chainId, id);
1403
+ const active = context.active();
1404
+ const parent = parentCtx ?? (trace.getSpan(active) ? active : sent?.parent ?? active);
1405
+ const attributes = baseAttributes(chainId, BLOCKCHAIN_OPERATION_NAME_VALUE_CONFIRM, parent);
1406
+ attributes[ATTR_BLOCKCHAIN_CALL_BATCH_ID] = id.slice(0, MAX_CALL_BATCH_ID_ATTRIBUTE_LENGTH);
1407
+ const span = getTracer().startSpan(`confirm ${chainId}`, {
1408
+ kind: SpanKind.CLIENT,
1409
+ attributes: redact(attributes),
1410
+ links: sent ? [{ context: sent.spanContext }] : [],
1411
+ ...input.startTime !== void 0 ? { startTime: input.startTime } : {}
1412
+ }, parent);
1413
+ const finish = finisher(span);
1414
+ const startMs = toEpochMs(input.startTime);
1415
+ const recordConfirmation = (endTime, outcome) => txMetrics.confirmationDuration(secondsSince(startMs, endTime), callBatchMetricAttributes(chainId, outcome));
1416
+ return {
1417
+ active: 0,
1418
+ ended: false,
1419
+ status: (status, endTime) => finish("record call batch status", () => {
1420
+ const attributes = callBatchStatusAttributes(status ?? {});
1421
+ if (isPendingCallBatch(status)) {
1422
+ span.setAttributes(redact(attributes));
1423
+ return;
1424
+ }
1425
+ const outcome = callBatchOutcome(status?.statusCode);
1426
+ if (outcome.status !== void 0) attributes[ATTR_BLOCKCHAIN_CALL_BATCH_STATUS] = outcome.status;
1427
+ span.setAttributes(redact(attributes));
1428
+ if (outcome.errorType !== void 0) markError(span, outcome.errorType);
1429
+ recordConfirmation(endTime, outcome.status !== void 0 ? { [ATTR_BLOCKCHAIN_CALL_BATCH_STATUS]: outcome.status } : { [ATTR_ERROR_TYPE]: outcome.errorType });
1430
+ }, endTime),
1431
+ timeout: (endTime) => finish("record call batch confirmation timeout", () => recordConfirmation(endTime, { [ATTR_ERROR_TYPE]: markError(span, OBSERVER_TIMEOUT) }), endTime),
1432
+ fail: (error, read) => finish("record call batch confirmation failure", () => recordConfirmation(read.endTime, { [ATTR_ERROR_TYPE]: markError(span, reportedErrorType(error, read), error, errorType(error)) }), read.endTime)
1433
+ };
1434
+ };
1435
+ const startCallBatchConfirm = (input, parentCtx) => {
1436
+ const { chainId, id } = input;
1437
+ if (!isCallBatchId(id)) {
1438
+ diag.debug("hashspan: not confirming a call batch without a valid id");
1439
+ return NOOP_CALL_BATCH_CONFIRM;
1440
+ }
1441
+ const claim = joinConfirm(callBatchConfirmations, chainId, id, () => openCallBatchConfirm(input, parentCtx));
1442
+ if (!claim) return NOOP_CALL_BATCH_CONFIRM;
1443
+ const { shared } = claim;
1444
+ return {
1445
+ end: (status, second) => {
1446
+ const { endTime } = handleOptions(second);
1447
+ if (isPendingCallBatch(status)) {
1448
+ claim.withdraw(() => shared.status(status, endTime));
1449
+ return;
1450
+ }
1451
+ if (!claim.receive()) return;
1452
+ callBatchConfirmations.settle(chainId, id, shared);
1453
+ shared.status(status, endTime);
1454
+ },
1455
+ timeout: (second) => {
1456
+ const { endTime } = handleOptions(second);
1457
+ claim.withdraw(() => shared.timeout(endTime));
1458
+ },
1459
+ fail: (error, second) => {
1460
+ const read = handleOptions(second);
1461
+ claim.withdraw(() => shared.fail(error, read));
1462
+ }
1463
+ };
1464
+ };
961
1465
  return {
962
1466
  startSend: (input, parent) => safely("start send span", () => startSend(input, parent), noopSend(parent ?? context.active())),
963
1467
  startConfirm: (input, parent) => safely("start confirm span", () => startConfirm(input, parent), NOOP_CONFIRM),
964
- startPayment: (input, parent) => safely("start payment span", () => startPayment(input, parent), NOOP_PAYMENT)
1468
+ startPayment: (input, parent) => safely("start payment span", () => startPayment(input, parent), NOOP_PAYMENT),
1469
+ startUserOperationSend: (input, parent) => safely("start user operation send span", () => startUserOperationSend(input, parent), noopUserOperationSend(parent ?? context.active())),
1470
+ startUserOperationConfirm: (input, parent) => safely("start user operation confirm span", () => startUserOperationConfirm(input, parent), NOOP_USER_OPERATION_CONFIRM),
1471
+ startCallBatchSend: (input, parent) => safely("start call batch send span", () => startCallBatchSend(input, parent), noopCallBatchSend(parent ?? context.active())),
1472
+ startCallBatchConfirm: (input, parent) => safely("start call batch confirm span", () => startCallBatchConfirm(input, parent), NOOP_CALL_BATCH_CONFIRM)
965
1473
  };
966
1474
  }
967
1475
  //#endregion
968
- export { ATTR_BLOCKCHAIN_BLOCK_NUMBER, ATTR_BLOCKCHAIN_CHAIN_ID, ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_ARGUMENTS, ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_NAME, ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_SELECTOR, ATTR_BLOCKCHAIN_OPERATION_NAME, ATTR_BLOCKCHAIN_PAYMENT_AMOUNT, ATTR_BLOCKCHAIN_PAYMENT_ASSET, ATTR_BLOCKCHAIN_PAYMENT_PAYER, ATTR_BLOCKCHAIN_PAYMENT_PROTOCOL, ATTR_BLOCKCHAIN_PAYMENT_RECIPIENT, ATTR_BLOCKCHAIN_PAYMENT_SETTLED_AMOUNT, ATTR_BLOCKCHAIN_PAYMENT_STATUS, ATTR_BLOCKCHAIN_PAYMENT_VERIFIED, ATTR_BLOCKCHAIN_SYSTEM, ATTR_BLOCKCHAIN_TX_EFFECTIVE_GAS_PRICE, ATTR_BLOCKCHAIN_TX_FEE, ATTR_BLOCKCHAIN_TX_FROM, ATTR_BLOCKCHAIN_TX_GAS_USED, ATTR_BLOCKCHAIN_TX_HASH, ATTR_BLOCKCHAIN_TX_L1_FEE, ATTR_BLOCKCHAIN_TX_NONCE, ATTR_BLOCKCHAIN_TX_REPLACEMENT_HASH, ATTR_BLOCKCHAIN_TX_REPLACEMENT_REASON, ATTR_BLOCKCHAIN_TX_REVERT_REASON, ATTR_BLOCKCHAIN_TX_STATUS, ATTR_BLOCKCHAIN_TX_TO, ATTR_BLOCKCHAIN_TX_VALUE, ATTR_ERROR_TYPE, ATTR_GEN_AI_AGENT_ID, ATTR_GEN_AI_AGENT_NAME, ATTR_X402_RESOURCE, ATTR_X402_SCHEME, BLOCKCHAIN_OPERATION_NAME_VALUE_CONFIRM, BLOCKCHAIN_OPERATION_NAME_VALUE_PAYMENT, BLOCKCHAIN_OPERATION_NAME_VALUE_SEND, BLOCKCHAIN_PAYMENT_PROTOCOL_VALUE_X402, BLOCKCHAIN_PAYMENT_STATUS_VALUE_FAILED, BLOCKCHAIN_PAYMENT_STATUS_VALUE_PENDING, BLOCKCHAIN_PAYMENT_STATUS_VALUE_SETTLED, BLOCKCHAIN_SYSTEM_VALUE_EVM, BLOCKCHAIN_TX_REPLACEMENT_REASON_VALUE_CANCELLED, BLOCKCHAIN_TX_REPLACEMENT_REASON_VALUE_REPLACED, BLOCKCHAIN_TX_REPLACEMENT_REASON_VALUE_REPRICED, BLOCKCHAIN_TX_STATUS_VALUE_REPLACED, BLOCKCHAIN_TX_STATUS_VALUE_REVERTED, BLOCKCHAIN_TX_STATUS_VALUE_SUCCESS, BLOCKCHAIN_TX_STATUS_VALUE_TIMEOUT, ERROR_TYPE_VALUE_OTHER, METRIC_BLOCKCHAIN_CLIENT_CONFIRMATION_DURATION, METRIC_BLOCKCHAIN_CLIENT_FEE, METRIC_BLOCKCHAIN_CLIENT_SEND_DURATION, VERSION, createTxTracker };
1476
+ export { ATTR_BLOCKCHAIN_BLOCK_NUMBER, ATTR_BLOCKCHAIN_CALL_BATCH_ATOMIC, ATTR_BLOCKCHAIN_CALL_BATCH_CALL_COUNT, ATTR_BLOCKCHAIN_CALL_BATCH_ID, ATTR_BLOCKCHAIN_CALL_BATCH_SENDER, ATTR_BLOCKCHAIN_CALL_BATCH_STATUS, ATTR_BLOCKCHAIN_CALL_BATCH_STATUS_CODE, ATTR_BLOCKCHAIN_CALL_BATCH_TRANSACTION_HASHES, ATTR_BLOCKCHAIN_CHAIN_ID, ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_ARGUMENTS, ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_NAME, ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_SELECTOR, ATTR_BLOCKCHAIN_OPERATION_NAME, ATTR_BLOCKCHAIN_OPERATION_SUBJECT, ATTR_BLOCKCHAIN_PAYMENT_AMOUNT, ATTR_BLOCKCHAIN_PAYMENT_ASSET, ATTR_BLOCKCHAIN_PAYMENT_PAYER, ATTR_BLOCKCHAIN_PAYMENT_PROTOCOL, ATTR_BLOCKCHAIN_PAYMENT_RECIPIENT, ATTR_BLOCKCHAIN_PAYMENT_SETTLED_AMOUNT, ATTR_BLOCKCHAIN_PAYMENT_STATUS, ATTR_BLOCKCHAIN_PAYMENT_VERIFIED, ATTR_BLOCKCHAIN_SYSTEM, ATTR_BLOCKCHAIN_TX_AUTHORIZATION_ADDRESSES, ATTR_BLOCKCHAIN_TX_AUTHORIZATION_CHAIN_IDS, ATTR_BLOCKCHAIN_TX_AUTHORIZATION_COUNT, ATTR_BLOCKCHAIN_TX_EFFECTIVE_GAS_PRICE, ATTR_BLOCKCHAIN_TX_FEE, ATTR_BLOCKCHAIN_TX_FROM, ATTR_BLOCKCHAIN_TX_GAS_USED, ATTR_BLOCKCHAIN_TX_HASH, ATTR_BLOCKCHAIN_TX_L1_FEE, ATTR_BLOCKCHAIN_TX_NONCE, ATTR_BLOCKCHAIN_TX_REPLACEMENT_HASH, ATTR_BLOCKCHAIN_TX_REPLACEMENT_REASON, ATTR_BLOCKCHAIN_TX_REVERT_REASON, ATTR_BLOCKCHAIN_TX_STATUS, ATTR_BLOCKCHAIN_TX_TO, ATTR_BLOCKCHAIN_TX_VALUE, ATTR_BLOCKCHAIN_USER_OPERATION_CALL_COUNT, ATTR_BLOCKCHAIN_USER_OPERATION_ENTRY_POINT, ATTR_BLOCKCHAIN_USER_OPERATION_GAS_COST, ATTR_BLOCKCHAIN_USER_OPERATION_GAS_USED, ATTR_BLOCKCHAIN_USER_OPERATION_HASH, ATTR_BLOCKCHAIN_USER_OPERATION_NONCE, ATTR_BLOCKCHAIN_USER_OPERATION_PAYMASTER, ATTR_BLOCKCHAIN_USER_OPERATION_SENDER, ATTR_BLOCKCHAIN_USER_OPERATION_SUCCESS, ATTR_ERROR_TYPE, ATTR_GEN_AI_AGENT_ID, ATTR_GEN_AI_AGENT_NAME, ATTR_X402_RESOURCE, ATTR_X402_SCHEME, BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_PARTIALLY_REVERTED, BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_REVERTED, BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_SUCCESS, BLOCKCHAIN_OPERATION_NAME_VALUE_CONFIRM, BLOCKCHAIN_OPERATION_NAME_VALUE_PAYMENT, BLOCKCHAIN_OPERATION_NAME_VALUE_SEND, BLOCKCHAIN_OPERATION_SUBJECT_VALUE_CALL_BATCH, BLOCKCHAIN_OPERATION_SUBJECT_VALUE_USER_OPERATION, BLOCKCHAIN_PAYMENT_PROTOCOL_VALUE_X402, BLOCKCHAIN_PAYMENT_STATUS_VALUE_FAILED, BLOCKCHAIN_PAYMENT_STATUS_VALUE_PENDING, BLOCKCHAIN_PAYMENT_STATUS_VALUE_SETTLED, BLOCKCHAIN_SYSTEM_VALUE_EVM, BLOCKCHAIN_TX_REPLACEMENT_REASON_VALUE_CANCELLED, BLOCKCHAIN_TX_REPLACEMENT_REASON_VALUE_REPLACED, BLOCKCHAIN_TX_REPLACEMENT_REASON_VALUE_REPRICED, BLOCKCHAIN_TX_STATUS_VALUE_REPLACED, BLOCKCHAIN_TX_STATUS_VALUE_REVERTED, BLOCKCHAIN_TX_STATUS_VALUE_SUCCESS, BLOCKCHAIN_TX_STATUS_VALUE_TIMEOUT, ERROR_TYPE_VALUE_OTHER, METRIC_BLOCKCHAIN_CLIENT_CONFIRMATION_DURATION, METRIC_BLOCKCHAIN_CLIENT_FEE, METRIC_BLOCKCHAIN_CLIENT_SEND_DURATION, VERSION, createTxTracker };