@hashspan/core 0.8.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.cjs CHANGED
@@ -6,7 +6,7 @@ const ATTR_GEN_AI_AGENT_NAME = "gen_ai.agent.name";
6
6
  /**
7
7
  * Agent identity as GenAI attributes. A field set in the static identity always wins; Baggage, which a remote caller
8
8
  * can set, only fills fields it leaves unset, and is not read at all with `fromBaggage` false
9
- * (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.8.0/docs/adr/0011-agent-identity-precedence.md).
9
+ * (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.9.0/docs/adr/0011-agent-identity-precedence.md).
10
10
  */
11
11
  function agentAttributes(ctx, identity, fromBaggage = true) {
12
12
  const baggage = fromBaggage ? _opentelemetry_api.propagation.getBaggage(ctx) : void 0;
@@ -22,7 +22,7 @@ function agentAttributes(ctx, identity, fromBaggage = true) {
22
22
  /**
23
23
  * Attribute keys emitted by hashspan.
24
24
  *
25
- * Stability: development. See https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.8.0/docs/semconv.md for
25
+ * Stability: development. See https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.9.0/docs/semconv.md for
26
26
  * definitions and value types. These names are a public contract: changes follow the deprecation policy in AGENTS.md.
27
27
  */
28
28
  const ATTR_BLOCKCHAIN_SYSTEM = "blockchain.system";
@@ -33,6 +33,12 @@ const ATTR_BLOCKCHAIN_TX_FROM = "blockchain.tx.from";
33
33
  const ATTR_BLOCKCHAIN_TX_TO = "blockchain.tx.to";
34
34
  const ATTR_BLOCKCHAIN_TX_VALUE = "blockchain.tx.value";
35
35
  const ATTR_BLOCKCHAIN_TX_NONCE = "blockchain.tx.nonce";
36
+ /** Number of EIP-7702 authorizations a type 4 transaction carries. */
37
+ const ATTR_BLOCKCHAIN_TX_AUTHORIZATION_COUNT = "blockchain.tx.authorization.count";
38
+ /** Delegated contract address of each well-formed EIP-7702 authorization, per the address mode; at most 64. */
39
+ const ATTR_BLOCKCHAIN_TX_AUTHORIZATION_ADDRESSES = "blockchain.tx.authorization.addresses";
40
+ /** Chain id of each well-formed EIP-7702 authorization, aligned with the addresses; 0 means every chain. */
41
+ const ATTR_BLOCKCHAIN_TX_AUTHORIZATION_CHAIN_IDS = "blockchain.tx.authorization.chain_ids";
36
42
  const ATTR_BLOCKCHAIN_TX_STATUS = "blockchain.tx.status";
37
43
  const ATTR_BLOCKCHAIN_TX_GAS_USED = "blockchain.tx.gas.used";
38
44
  const ATTR_BLOCKCHAIN_TX_EFFECTIVE_GAS_PRICE = "blockchain.tx.effective_gas_price";
@@ -46,12 +52,12 @@ const ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_NAME = "blockchain.contract.function.nam
46
52
  const ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_SELECTOR = "blockchain.contract.function.selector";
47
53
  /**
48
54
  * Opt-in: decoded call arguments as a JSON array. See
49
- * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.8.0/docs/adr/0004-privacy-defaults.md.
55
+ * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.9.0/docs/adr/0004-privacy-defaults.md.
50
56
  */
51
57
  const ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_ARGUMENTS = "blockchain.contract.function.arguments";
52
58
  /**
53
59
  * Payments settled on chain by a party other than the agent, e.g. an x402 facilitator. See
54
- * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.8.0/docs/adr/0013-x402-payments.md.
60
+ * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.9.0/docs/adr/0013-x402-payments.md.
55
61
  */
56
62
  const ATTR_BLOCKCHAIN_PAYMENT_PROTOCOL = "blockchain.payment.protocol";
57
63
  const ATTR_BLOCKCHAIN_PAYMENT_PAYER = "blockchain.payment.payer";
@@ -64,7 +70,7 @@ const ATTR_BLOCKCHAIN_PAYMENT_SETTLED_AMOUNT = "blockchain.payment.settled_amoun
64
70
  /**
65
71
  * Whether the settlement transaction's receipt carries the payment, as checked by the adapter; absent when no check
66
72
  * was possible. See
67
- * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.8.0/docs/adr/0017-x402-payment-verification.md.
73
+ * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.9.0/docs/adr/0017-x402-payment-verification.md.
68
74
  */
69
75
  const ATTR_BLOCKCHAIN_PAYMENT_VERIFIED = "blockchain.payment.verified";
70
76
  /** x402's own payment fields. */
@@ -72,7 +78,7 @@ const ATTR_X402_SCHEME = "x402.scheme";
72
78
  const ATTR_X402_RESOURCE = "x402.resource";
73
79
  /**
74
80
  * User operations of ERC-4337 smart accounts. See
75
- * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.8.0/docs/adr/0021-user-operations.md.
81
+ * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.9.0/docs/adr/0021-user-operations.md.
76
82
  */
77
83
  const ATTR_BLOCKCHAIN_USER_OPERATION_HASH = "blockchain.user_operation.hash";
78
84
  const ATTR_BLOCKCHAIN_USER_OPERATION_SENDER = "blockchain.user_operation.sender";
@@ -85,12 +91,31 @@ const ATTR_BLOCKCHAIN_USER_OPERATION_GAS_USED = "blockchain.user_operation.gas.u
85
91
  const ATTR_BLOCKCHAIN_USER_OPERATION_GAS_COST = "blockchain.user_operation.gas.cost";
86
92
  const ATTR_BLOCKCHAIN_USER_OPERATION_PAYMASTER = "blockchain.user_operation.paymaster";
87
93
  /**
94
+ * The batch id the wallet returned, truncated after 256 characters. See
95
+ * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.9.0/docs/adr/0022-call-batches.md.
96
+ */
97
+ const ATTR_BLOCKCHAIN_CALL_BATCH_ID = "blockchain.call_batch.id";
98
+ const ATTR_BLOCKCHAIN_CALL_BATCH_SENDER = "blockchain.call_batch.sender";
99
+ const ATTR_BLOCKCHAIN_CALL_BATCH_CALL_COUNT = "blockchain.call_batch.call_count";
100
+ /** The outcome of a batch from chain data: `success`, `reverted` or `partially_reverted`. */
101
+ const ATTR_BLOCKCHAIN_CALL_BATCH_STATUS = "blockchain.call_batch.status";
102
+ /** The EIP-5792 status code of the batch as the wallet reported it, e.g. 200 confirmed or 500 reverted; spans only. */
103
+ const ATTR_BLOCKCHAIN_CALL_BATCH_STATUS_CODE = "blockchain.call_batch.status_code";
104
+ const ATTR_BLOCKCHAIN_CALL_BATCH_ATOMIC = "blockchain.call_batch.atomic";
105
+ /** Hashes of the transactions whose receipts the wallet reported for the batch. */
106
+ const ATTR_BLOCKCHAIN_CALL_BATCH_TRANSACTION_HASHES = "blockchain.call_batch.transaction_hashes";
107
+ /**
88
108
  * Metrics only: what a send, confirmation or fee sample is about. Recorded as `user_operation` on samples of user
89
- * operations, and absent on those of transactions.
109
+ * operations and `call_batch` on those of call batches, and absent on those of transactions.
90
110
  */
91
111
  const ATTR_BLOCKCHAIN_OPERATION_SUBJECT = "blockchain.operation.subject";
92
112
  /** Values for {@link ATTR_BLOCKCHAIN_OPERATION_SUBJECT}. */
93
113
  const BLOCKCHAIN_OPERATION_SUBJECT_VALUE_USER_OPERATION = "user_operation";
114
+ const BLOCKCHAIN_OPERATION_SUBJECT_VALUE_CALL_BATCH = "call_batch";
115
+ /** Values for {@link ATTR_BLOCKCHAIN_CALL_BATCH_STATUS}. */
116
+ const BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_SUCCESS = "success";
117
+ const BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_REVERTED = "reverted";
118
+ const BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_PARTIALLY_REVERTED = "partially_reverted";
94
119
  /** Values for {@link ATTR_BLOCKCHAIN_SYSTEM}. */
95
120
  const BLOCKCHAIN_SYSTEM_VALUE_EVM = "evm";
96
121
  /** Values for {@link ATTR_BLOCKCHAIN_OPERATION_NAME}. */
@@ -109,7 +134,7 @@ const BLOCKCHAIN_TX_STATUS_VALUE_REVERTED = "reverted";
109
134
  /**
110
135
  * @deprecated No longer recorded: a confirm span that gave up waiting records `error.type` `timeout` and no
111
136
  * `blockchain.tx.status`. The constant is removed in 1.0. See
112
- * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.8.0/docs/adr/0016-timeout-is-an-observer-outcome.md.
137
+ * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.9.0/docs/adr/0016-timeout-is-an-observer-outcome.md.
113
138
  */
114
139
  const BLOCKCHAIN_TX_STATUS_VALUE_TIMEOUT = "timeout";
115
140
  const BLOCKCHAIN_TX_STATUS_VALUE_REPLACED = "replaced";
@@ -332,7 +357,7 @@ function resolveAddressFormatter(option) {
332
357
  hash: void 0
333
358
  };
334
359
  switch (mode) {
335
- case "raw": return formatter((address) => address, false);
360
+ case "raw": return formatter((address) => address.toLowerCase(), false);
336
361
  case "off": return OFF_ADDRESS_FORMATTER;
337
362
  case "hashed": {
338
363
  const hashFn = hash ?? defaultHash();
@@ -356,6 +381,8 @@ const ADDRESS_LENGTH = 42;
356
381
  */
357
382
  const MAX_HEX_LENGTH = 66;
358
383
  const MAX_MESSAGE_LENGTH = 256;
384
+ /** A URL in free text: `scheme://` up to the next whitespace, quote or bracket; the bounded scheme keeps it linear. */
385
+ const URL_IN_TEXT = /\b[A-Za-z][A-Za-z0-9+.-]{0,31}:\/\/[^\s"'<>()[\]{}]+/g;
359
386
  /**
360
387
  * Rewrites every address in `text` with the address mode (`<address>` when it records none). In `off` and `hashed`
361
388
  * mode, hex values longer than an address become `<hex>`, because they can embed one.
@@ -367,11 +394,12 @@ function formatAddressesIn(text, formatAddress) {
367
394
  });
368
395
  }
369
396
  /**
370
- * First line of an error message with addresses per address mode and longer hex data (such as calldata)
371
- * replaced by `<hex>`. Best effort: other free text is kept, so the redaction hook still runs on the result.
397
+ * First line of an error message with URLs cut to their origin (an RPC URL can carry an API key in its path or
398
+ * query), addresses per address mode and longer hex data (such as calldata) replaced by `<hex>`. Best effort: other
399
+ * free text is kept, so the redaction hook still runs on the result.
372
400
  */
373
401
  function sanitizeErrorMessage(message, formatAddress) {
374
- const sanitized = formatAddressesIn(message.split("\n", 1)[0]?.trim() ?? "", formatAddress).replace(HEX, (hex) => hex.length > MAX_HEX_LENGTH ? "<hex>" : hex);
402
+ 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);
375
403
  return sanitized.length > MAX_MESSAGE_LENGTH ? `${sanitized.slice(0, MAX_MESSAGE_LENGTH)}...` : sanitized;
376
404
  }
377
405
  const MAX_ARGUMENTS_LENGTH = 4096;
@@ -474,7 +502,16 @@ function paymentResourceOf(resource, mode) {
474
502
  if (mode === "off" || hidesUserInfo(resource)) return void 0;
475
503
  const recorded = mode === "path" ? sanitizeResource(resource) : originOf(resource);
476
504
  if (recorded === void 0) return void 0;
477
- return recorded.length > MAX_RESOURCE_LENGTH ? `${recorded.slice(0, MAX_RESOURCE_LENGTH)}...` : recorded;
505
+ return cutAt(recorded, MAX_RESOURCE_LENGTH);
506
+ }
507
+ /**
508
+ * `text` cut to `max` characters, followed by `...`. A hex value that the cut splits is dropped whole: the part left
509
+ * is shorter than an address, so the address mode, applied later, would no longer recognise it as one.
510
+ */
511
+ function cutAt(text, max) {
512
+ if (text.length <= max) return text;
513
+ const head = text.slice(0, max);
514
+ return `${/^[0-9a-fA-F]/.test(text.slice(max)) ? head.replace(/0[xX][0-9a-fA-F]*$/, "") : head}...`;
478
515
  }
479
516
  /** Longest `x402.resource` recorded; the value comes from the server that asks for the payment. */
480
517
  const MAX_RESOURCE_LENGTH = 512;
@@ -508,7 +545,7 @@ function sanitizeResource(resource) {
508
545
  }
509
546
  //#endregion
510
547
  //#region src/version.ts
511
- const VERSION = "0.8.0";
548
+ const VERSION = "0.9.0";
512
549
  //#endregion
513
550
  //#region src/tracker.ts
514
551
  const INSTRUMENTATION_NAME = "@hashspan/core";
@@ -533,12 +570,20 @@ const NON_SENSITIVE_KEYS = /* @__PURE__ */ new Set([
533
570
  ATTR_BLOCKCHAIN_PAYMENT_VERIFIED,
534
571
  ATTR_BLOCKCHAIN_USER_OPERATION_HASH,
535
572
  ATTR_BLOCKCHAIN_USER_OPERATION_SUCCESS,
573
+ ATTR_BLOCKCHAIN_CALL_BATCH_ID,
574
+ ATTR_BLOCKCHAIN_CALL_BATCH_STATUS,
536
575
  ATTR_ERROR_TYPE,
537
576
  ATTR_EXCEPTION_TYPE
538
577
  ]);
539
578
  const TX_HASH = /^0x[0-9a-fA-F]{64}$/;
540
579
  const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
541
580
  const ZERO_ADDRESS = /^0x0{40}$/;
581
+ /** The value of an own data property of `target`; undefined for an accessor, so no getter of the caller runs. */
582
+ function ownValue(target, key) {
583
+ if (typeof target !== "object" || target === null) return void 0;
584
+ const descriptor = Object.getOwnPropertyDescriptor(target, key);
585
+ return descriptor !== void 0 && "value" in descriptor ? descriptor.value : void 0;
586
+ }
542
587
  /** A non-negative integer that fits in 256 bits. */
543
588
  const AMOUNT = /^(0|[1-9][0-9]{0,77})$/;
544
589
  /** A `0x` hex quantity of at most 256 bits, as JSON-RPC encodes integers. */
@@ -547,6 +592,18 @@ const MAX_UINT256 = 2n ** 256n - 1n;
547
592
  const MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER);
548
593
  /** `error.type` of a wait that gave up: a confirmation or a payment whose outcome was never learned. */
549
594
  const OBSERVER_TIMEOUT = "timeout";
595
+ /** `error.type` of a call batch that failed without being included (EIP-5792 status 400). */
596
+ const CALL_BATCH_FAILED = "failed";
597
+ /** A call batch id: `0x`-prefixed hex of at most 8194 characters, the bound EIP-5792 sets. */
598
+ const CALL_BATCH_ID = /^0x[0-9a-fA-F]{1,8192}$/;
599
+ /** Longest call batch id recorded as an attribute. */
600
+ const MAX_CALL_BATCH_ID_ATTRIBUTE_LENGTH = 256;
601
+ /** Most transaction hashes recorded for one call batch. */
602
+ const MAX_CALL_BATCH_TRANSACTION_HASHES = 64;
603
+ /** Most EIP-7702 authorizations listed on a send span; the count covers all of them. */
604
+ const MAX_AUTHORIZATIONS = 64;
605
+ /** EIP-5792 status code of a batch that is still pending. */
606
+ const CALL_BATCH_PENDING = 100;
550
607
  const PAYMENT_STATUSES = /* @__PURE__ */ new Set([
551
608
  BLOCKCHAIN_PAYMENT_STATUS_VALUE_SETTLED,
552
609
  BLOCKCHAIN_PAYMENT_STATUS_VALUE_PENDING,
@@ -584,6 +641,16 @@ const NOOP_USER_OPERATION_CONFIRM = {
584
641
  timeout: () => {},
585
642
  fail: () => {}
586
643
  };
644
+ const noopCallBatchSend = (parent) => ({
645
+ context: parent,
646
+ end: () => {},
647
+ fail: () => {}
648
+ });
649
+ const NOOP_CALL_BATCH_CONFIRM = {
650
+ end: () => {},
651
+ timeout: () => {},
652
+ fail: () => {}
653
+ };
587
654
  /** Runs `fn`, logging instead of throwing: instrumentation must never break the caller. */
588
655
  function safely(what, fn, fallback) {
589
656
  try {
@@ -627,7 +694,6 @@ function amount(value) {
627
694
  const text = typeof value === "bigint" ? value.toString() : value;
628
695
  return typeof text === "string" && AMOUNT.test(text) ? text : void 0;
629
696
  }
630
- /** The `error.type` for a failure: an adapter's override when it is a short identifier, else the class name. */
631
697
  /** A finite number, an `HrTime` pair or a `Date`: what the deprecated positional `endTime` argument takes. */
632
698
  function isTimeInput(value) {
633
699
  if (typeof value === "number") return Number.isFinite(value);
@@ -659,6 +725,7 @@ function handleOptions(second, third) {
659
725
  return {};
660
726
  }
661
727
  }
728
+ /** The `error.type` for a failure: an adapter's override when it is a short identifier, else the class name. */
662
729
  function reportedErrorType(error, options) {
663
730
  const override = options?.errorType;
664
731
  if (override === void 0) return errorType(error);
@@ -666,6 +733,14 @@ function reportedErrorType(error, options) {
666
733
  _opentelemetry_api.diag.debug("hashspan: ignoring an error type that is not a short identifier");
667
734
  return errorType(error);
668
735
  }
736
+ /** A call batch id the tracker keys and records. */
737
+ function isCallBatchId(id) {
738
+ return typeof id === "string" && CALL_BATCH_ID.test(id);
739
+ }
740
+ /** Whether `status` says the batch is still pending: its wait resolved before an outcome. */
741
+ function isPendingCallBatch(status) {
742
+ return status?.statusCode === CALL_BATCH_PENDING;
743
+ }
669
744
  /**
670
745
  * Joins the confirm span for `hash` in `registry`, opening it with `open` for the first handle; undefined when the
671
746
  * key recently got a receipt. A receipt from any handle ends the span; a timeout or failure only ends it when it is the
@@ -704,7 +779,7 @@ function joinConfirm(registry, chainId, hash, open) {
704
779
  }
705
780
  /**
706
781
  * Creates a tracker that records transactions as `send` and `confirm` spans, and payments as `payment` spans, with
707
- * `@opentelemetry/api` (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.8.0/docs/semconv.md). It makes
782
+ * `@opentelemetry/api` (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.9.0/docs/semconv.md). It makes
708
783
  * no network calls; the caller passes hashes and receipts. Its methods and handles never throw: failures are logged
709
784
  * via `diag`, and a method that fails returns a handle that records nothing.
710
785
  */
@@ -728,6 +803,14 @@ function createTxTracker(options = {}) {
728
803
  ttlMs: options.linkTtlMs ?? DEFAULT_LINK_TTL_MS,
729
804
  maxEntries: options.maxTrackedTransactions ?? DEFAULT_MAX_TRACKED
730
805
  });
806
+ const callBatchLinks = new LinkStore({
807
+ ttlMs: options.linkTtlMs ?? DEFAULT_LINK_TTL_MS,
808
+ maxEntries: options.maxTrackedTransactions ?? DEFAULT_MAX_TRACKED
809
+ });
810
+ const callBatchConfirmations = new ConfirmRegistry({
811
+ ttlMs: options.linkTtlMs ?? DEFAULT_LINK_TTL_MS,
812
+ maxEntries: options.maxTrackedTransactions ?? DEFAULT_MAX_TRACKED
813
+ });
731
814
  const txMetrics = createTxMetrics(options.meterProvider, INSTRUMENTATION_NAME, VERSION);
732
815
  /** Attributes of a metric: low-cardinality only, never an address, hash or agent identity. */
733
816
  const metricAttributes = (chainId, extra = {}) => ({
@@ -740,6 +823,11 @@ function createTxTracker(options = {}) {
740
823
  [ATTR_BLOCKCHAIN_OPERATION_SUBJECT]: BLOCKCHAIN_OPERATION_SUBJECT_VALUE_USER_OPERATION,
741
824
  ...extra
742
825
  });
826
+ /** Metric attributes of a call batch sample. */
827
+ const callBatchMetricAttributes = (chainId, extra = {}) => metricAttributes(chainId, {
828
+ [ATTR_BLOCKCHAIN_OPERATION_SUBJECT]: BLOCKCHAIN_OPERATION_SUBJECT_VALUE_CALL_BATCH,
829
+ ...extra
830
+ });
743
831
  const secondsSince = (startMs, endTime) => (toEpochMs(endTime) - startMs) / 1e3;
744
832
  let tracer;
745
833
  const getTracer = () => {
@@ -765,7 +853,7 @@ function createTxTracker(options = {}) {
765
853
  /**
766
854
  * Exception event attributes for `error`, per the error message mode. The error object itself is never handed to
767
855
  * the SDK: its message and stack can carry addresses and calldata
768
- * (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.8.0/docs/adr/0006-error-privacy.md).
856
+ * (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.9.0/docs/adr/0006-error-privacy.md).
769
857
  */
770
858
  const exceptionAttributes = (type, error) => {
771
859
  const attributes = { [ATTR_EXCEPTION_TYPE]: type };
@@ -820,6 +908,28 @@ function createTxTracker(options = {}) {
820
908
  const formatted = formatAddress(address);
821
909
  if (formatted !== void 0) attributes[key] = formatted;
822
910
  };
911
+ /**
912
+ * Records an EIP-7702 authorization list: its length, and for each well-formed entry (an address and a non-negative
913
+ * integer chain id, read from own data properties only) its address per the address mode and its chain id, at most
914
+ * {@link MAX_AUTHORIZATIONS}. The two lists stay aligned: an entry is listed in both or in neither.
915
+ */
916
+ const setAuthorizations = (attributes, list) => {
917
+ if (!Array.isArray(list) || list.length === 0) return;
918
+ attributes[ATTR_BLOCKCHAIN_TX_AUTHORIZATION_COUNT] = list.length;
919
+ const addresses = [];
920
+ const chainIds = [];
921
+ for (const entry of list.slice(0, MAX_AUTHORIZATIONS)) {
922
+ const address = ownValue(entry, "address");
923
+ const chainId = ownValue(entry, "chainId");
924
+ if (typeof address !== "string" || !ADDRESS.test(address)) continue;
925
+ if (typeof chainId !== "number" || !Number.isSafeInteger(chainId) || chainId < 0) continue;
926
+ const formatted = formatAddress(address);
927
+ if (formatted !== void 0) addresses.push(formatted);
928
+ chainIds.push(chainId);
929
+ }
930
+ if (addresses.length > 0) attributes[ATTR_BLOCKCHAIN_TX_AUTHORIZATION_ADDRESSES] = addresses;
931
+ if (chainIds.length > 0) attributes[ATTR_BLOCKCHAIN_TX_AUTHORIZATION_CHAIN_IDS] = chainIds;
932
+ };
823
933
  /** Records `address` only if it is one: payment and user operation addresses come from remote parties. */
824
934
  const setRemoteAddress = (attributes, key, address) => {
825
935
  if (typeof address === "string" && ADDRESS.test(address)) setAddress(attributes, key, address);
@@ -837,6 +947,11 @@ function createTxTracker(options = {}) {
837
947
  setAddress(attributes, ATTR_BLOCKCHAIN_TX_TO, input.to);
838
948
  if (input.value !== void 0) attributes[ATTR_BLOCKCHAIN_TX_VALUE] = input.value.toString();
839
949
  if (input.nonce !== void 0) attributes[ATTR_BLOCKCHAIN_TX_NONCE] = input.nonce;
950
+ try {
951
+ setAuthorizations(attributes, input.authorizations);
952
+ } catch (error) {
953
+ _opentelemetry_api.diag.debug(`hashspan: could not record authorizations (${errorType(error)})`);
954
+ }
840
955
  if (input.functionName !== void 0) attributes[ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_NAME] = input.functionName;
841
956
  if (input.functionSelector !== void 0) attributes[ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_SELECTOR] = input.functionSelector;
842
957
  if (options.recordFunctionArguments === true && input.functionArguments !== void 0) try {
@@ -957,7 +1072,7 @@ function createTxTracker(options = {}) {
957
1072
  };
958
1073
  /**
959
1074
  * Ends `shared` with `receipt`, attributing it to the transaction that was mined
960
- * (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.8.0/docs/adr/0008-replaced-transactions.md).
1075
+ * (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.9.0/docs/adr/0008-replaced-transactions.md).
961
1076
  */
962
1077
  const endWithReceipt = (chainId, hash, shared, receipt, endTime) => {
963
1078
  const mined = receipt.transactionHash;
@@ -1193,16 +1308,180 @@ function createTxTracker(options = {}) {
1193
1308
  }
1194
1309
  };
1195
1310
  };
1311
+ const startCallBatchSend = (input, parentCtx) => {
1312
+ const parent = parentCtx ?? _opentelemetry_api.context.active();
1313
+ const { chainId } = input;
1314
+ const attributes = baseAttributes(chainId, BLOCKCHAIN_OPERATION_NAME_VALUE_SEND, parent);
1315
+ setRemoteAddress(attributes, ATTR_BLOCKCHAIN_CALL_BATCH_SENDER, input.sender);
1316
+ const callCount = smallQuantity(input.callCount);
1317
+ if (callCount !== void 0) attributes[ATTR_BLOCKCHAIN_CALL_BATCH_CALL_COUNT] = callCount;
1318
+ const span = getTracer().startSpan(`send ${chainId}`, {
1319
+ kind: _opentelemetry_api.SpanKind.CLIENT,
1320
+ attributes: redact(attributes),
1321
+ ...input.startTime !== void 0 ? { startTime: input.startTime } : {}
1322
+ }, parent);
1323
+ const finish = finisher(span);
1324
+ const startMs = toEpochMs(input.startTime);
1325
+ const recordSend = (endTime, errorType) => txMetrics.sendDuration(secondsSince(startMs, endTime), callBatchMetricAttributes(chainId, errorType === void 0 ? {} : { [ATTR_ERROR_TYPE]: errorType }));
1326
+ return {
1327
+ context: _opentelemetry_api.trace.setSpan(parent, span),
1328
+ end: (result, second) => {
1329
+ const { endTime } = handleOptions(second);
1330
+ finish("record call batch id", () => {
1331
+ const id = result?.id;
1332
+ if (!isCallBatchId(id)) {
1333
+ _opentelemetry_api.diag.debug("hashspan: ending a send span without a valid call batch id");
1334
+ return;
1335
+ }
1336
+ const sent = {
1337
+ spanContext: span.spanContext(),
1338
+ parent
1339
+ };
1340
+ callBatchLinks.set(chainId, id, sent);
1341
+ const hashes = result.transactionHashes;
1342
+ if (Array.isArray(hashes)) {
1343
+ for (const hash of hashes) if (typeof hash === "string" && TX_HASH.test(hash) && !/^0x0+$/.test(hash)) links.set(chainId, hash, sent);
1344
+ }
1345
+ span.setAttributes(redact({ [ATTR_BLOCKCHAIN_CALL_BATCH_ID]: id.slice(0, MAX_CALL_BATCH_ID_ATTRIBUTE_LENGTH) }));
1346
+ recordSend(endTime);
1347
+ }, endTime);
1348
+ },
1349
+ fail: (error, second) => {
1350
+ const read = handleOptions(second);
1351
+ finish("record call batch send failure", () => recordSend(read.endTime, markError(span, reportedErrorType(error, read), error, errorType(error))), read.endTime);
1352
+ }
1353
+ };
1354
+ };
1355
+ /** Attributes of a call batch status. Its values come from a wallet: what is malformed is left out. */
1356
+ const callBatchStatusAttributes = (status) => {
1357
+ const attributes = {};
1358
+ const code = smallQuantity(status.statusCode);
1359
+ if (code !== void 0) attributes[ATTR_BLOCKCHAIN_CALL_BATCH_STATUS_CODE] = code;
1360
+ const atomic = status.atomic;
1361
+ if (typeof atomic === "boolean") attributes[ATTR_BLOCKCHAIN_CALL_BATCH_ATOMIC] = atomic;
1362
+ const receipts = status.receipts;
1363
+ if (Array.isArray(receipts)) {
1364
+ const hashes = [];
1365
+ const seen = /* @__PURE__ */ new Set();
1366
+ let block;
1367
+ for (const receipt of receipts) {
1368
+ const hash = receipt?.transactionHash;
1369
+ if (typeof hash === "string" && TX_HASH.test(hash) && !seen.has(hash.toLowerCase()) && hashes.length < MAX_CALL_BATCH_TRANSACTION_HASHES) {
1370
+ seen.add(hash.toLowerCase());
1371
+ hashes.push(hash);
1372
+ }
1373
+ const number = smallQuantity(receipt?.blockNumber);
1374
+ if (number !== void 0 && (block === void 0 || number > block)) block = number;
1375
+ }
1376
+ if (hashes.length > 0) attributes[ATTR_BLOCKCHAIN_CALL_BATCH_TRANSACTION_HASHES] = hashes;
1377
+ if (block !== void 0) attributes[ATTR_BLOCKCHAIN_BLOCK_NUMBER] = block;
1378
+ }
1379
+ return attributes;
1380
+ };
1381
+ /**
1382
+ * The outcome of an EIP-5792 status code (ADR 0022): `blockchain.call_batch.status` for outcomes from chain data,
1383
+ * `error.type` for the rest; a code the spec does not define, or none, is `_OTHER`.
1384
+ */
1385
+ const callBatchOutcome = (code) => {
1386
+ switch (code) {
1387
+ case 200: return { status: BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_SUCCESS };
1388
+ case 400: return { errorType: CALL_BATCH_FAILED };
1389
+ case 500: return {
1390
+ status: BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_REVERTED,
1391
+ errorType: BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_REVERTED
1392
+ };
1393
+ case 600: return {
1394
+ status: BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_PARTIALLY_REVERTED,
1395
+ errorType: BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_PARTIALLY_REVERTED
1396
+ };
1397
+ default: return { errorType: ERROR_TYPE_VALUE_OTHER };
1398
+ }
1399
+ };
1400
+ /** Opens the confirm span of a call batch. */
1401
+ const openCallBatchConfirm = (input, parentCtx) => {
1402
+ const { chainId, id } = input;
1403
+ const sent = callBatchLinks.get(chainId, id);
1404
+ const active = _opentelemetry_api.context.active();
1405
+ const parent = parentCtx ?? (_opentelemetry_api.trace.getSpan(active) ? active : sent?.parent ?? active);
1406
+ const attributes = baseAttributes(chainId, BLOCKCHAIN_OPERATION_NAME_VALUE_CONFIRM, parent);
1407
+ attributes[ATTR_BLOCKCHAIN_CALL_BATCH_ID] = id.slice(0, MAX_CALL_BATCH_ID_ATTRIBUTE_LENGTH);
1408
+ const span = getTracer().startSpan(`confirm ${chainId}`, {
1409
+ kind: _opentelemetry_api.SpanKind.CLIENT,
1410
+ attributes: redact(attributes),
1411
+ links: sent ? [{ context: sent.spanContext }] : [],
1412
+ ...input.startTime !== void 0 ? { startTime: input.startTime } : {}
1413
+ }, parent);
1414
+ const finish = finisher(span);
1415
+ const startMs = toEpochMs(input.startTime);
1416
+ const recordConfirmation = (endTime, outcome) => txMetrics.confirmationDuration(secondsSince(startMs, endTime), callBatchMetricAttributes(chainId, outcome));
1417
+ return {
1418
+ active: 0,
1419
+ ended: false,
1420
+ status: (status, endTime) => finish("record call batch status", () => {
1421
+ const attributes = callBatchStatusAttributes(status ?? {});
1422
+ if (isPendingCallBatch(status)) {
1423
+ span.setAttributes(redact(attributes));
1424
+ return;
1425
+ }
1426
+ const outcome = callBatchOutcome(status?.statusCode);
1427
+ if (outcome.status !== void 0) attributes[ATTR_BLOCKCHAIN_CALL_BATCH_STATUS] = outcome.status;
1428
+ span.setAttributes(redact(attributes));
1429
+ if (outcome.errorType !== void 0) markError(span, outcome.errorType);
1430
+ recordConfirmation(endTime, outcome.status !== void 0 ? { [ATTR_BLOCKCHAIN_CALL_BATCH_STATUS]: outcome.status } : { [ATTR_ERROR_TYPE]: outcome.errorType });
1431
+ }, endTime),
1432
+ timeout: (endTime) => finish("record call batch confirmation timeout", () => recordConfirmation(endTime, { [ATTR_ERROR_TYPE]: markError(span, OBSERVER_TIMEOUT) }), endTime),
1433
+ 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)
1434
+ };
1435
+ };
1436
+ const startCallBatchConfirm = (input, parentCtx) => {
1437
+ const { chainId, id } = input;
1438
+ if (!isCallBatchId(id)) {
1439
+ _opentelemetry_api.diag.debug("hashspan: not confirming a call batch without a valid id");
1440
+ return NOOP_CALL_BATCH_CONFIRM;
1441
+ }
1442
+ const claim = joinConfirm(callBatchConfirmations, chainId, id, () => openCallBatchConfirm(input, parentCtx));
1443
+ if (!claim) return NOOP_CALL_BATCH_CONFIRM;
1444
+ const { shared } = claim;
1445
+ return {
1446
+ end: (status, second) => {
1447
+ const { endTime } = handleOptions(second);
1448
+ if (isPendingCallBatch(status)) {
1449
+ claim.withdraw(() => shared.status(status, endTime));
1450
+ return;
1451
+ }
1452
+ if (!claim.receive()) return;
1453
+ callBatchConfirmations.settle(chainId, id, shared);
1454
+ shared.status(status, endTime);
1455
+ },
1456
+ timeout: (second) => {
1457
+ const { endTime } = handleOptions(second);
1458
+ claim.withdraw(() => shared.timeout(endTime));
1459
+ },
1460
+ fail: (error, second) => {
1461
+ const read = handleOptions(second);
1462
+ claim.withdraw(() => shared.fail(error, read));
1463
+ }
1464
+ };
1465
+ };
1196
1466
  return {
1197
1467
  startSend: (input, parent) => safely("start send span", () => startSend(input, parent), noopSend(parent ?? _opentelemetry_api.context.active())),
1198
1468
  startConfirm: (input, parent) => safely("start confirm span", () => startConfirm(input, parent), NOOP_CONFIRM),
1199
1469
  startPayment: (input, parent) => safely("start payment span", () => startPayment(input, parent), NOOP_PAYMENT),
1200
1470
  startUserOperationSend: (input, parent) => safely("start user operation send span", () => startUserOperationSend(input, parent), noopUserOperationSend(parent ?? _opentelemetry_api.context.active())),
1201
- startUserOperationConfirm: (input, parent) => safely("start user operation confirm span", () => startUserOperationConfirm(input, parent), NOOP_USER_OPERATION_CONFIRM)
1471
+ startUserOperationConfirm: (input, parent) => safely("start user operation confirm span", () => startUserOperationConfirm(input, parent), NOOP_USER_OPERATION_CONFIRM),
1472
+ startCallBatchSend: (input, parent) => safely("start call batch send span", () => startCallBatchSend(input, parent), noopCallBatchSend(parent ?? _opentelemetry_api.context.active())),
1473
+ startCallBatchConfirm: (input, parent) => safely("start call batch confirm span", () => startCallBatchConfirm(input, parent), NOOP_CALL_BATCH_CONFIRM)
1202
1474
  };
1203
1475
  }
1204
1476
  //#endregion
1205
1477
  exports.ATTR_BLOCKCHAIN_BLOCK_NUMBER = ATTR_BLOCKCHAIN_BLOCK_NUMBER;
1478
+ exports.ATTR_BLOCKCHAIN_CALL_BATCH_ATOMIC = ATTR_BLOCKCHAIN_CALL_BATCH_ATOMIC;
1479
+ exports.ATTR_BLOCKCHAIN_CALL_BATCH_CALL_COUNT = ATTR_BLOCKCHAIN_CALL_BATCH_CALL_COUNT;
1480
+ exports.ATTR_BLOCKCHAIN_CALL_BATCH_ID = ATTR_BLOCKCHAIN_CALL_BATCH_ID;
1481
+ exports.ATTR_BLOCKCHAIN_CALL_BATCH_SENDER = ATTR_BLOCKCHAIN_CALL_BATCH_SENDER;
1482
+ exports.ATTR_BLOCKCHAIN_CALL_BATCH_STATUS = ATTR_BLOCKCHAIN_CALL_BATCH_STATUS;
1483
+ exports.ATTR_BLOCKCHAIN_CALL_BATCH_STATUS_CODE = ATTR_BLOCKCHAIN_CALL_BATCH_STATUS_CODE;
1484
+ exports.ATTR_BLOCKCHAIN_CALL_BATCH_TRANSACTION_HASHES = ATTR_BLOCKCHAIN_CALL_BATCH_TRANSACTION_HASHES;
1206
1485
  exports.ATTR_BLOCKCHAIN_CHAIN_ID = ATTR_BLOCKCHAIN_CHAIN_ID;
1207
1486
  exports.ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_ARGUMENTS = ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_ARGUMENTS;
1208
1487
  exports.ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_NAME = ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_NAME;
@@ -1218,6 +1497,9 @@ exports.ATTR_BLOCKCHAIN_PAYMENT_SETTLED_AMOUNT = ATTR_BLOCKCHAIN_PAYMENT_SETTLED
1218
1497
  exports.ATTR_BLOCKCHAIN_PAYMENT_STATUS = ATTR_BLOCKCHAIN_PAYMENT_STATUS;
1219
1498
  exports.ATTR_BLOCKCHAIN_PAYMENT_VERIFIED = ATTR_BLOCKCHAIN_PAYMENT_VERIFIED;
1220
1499
  exports.ATTR_BLOCKCHAIN_SYSTEM = ATTR_BLOCKCHAIN_SYSTEM;
1500
+ exports.ATTR_BLOCKCHAIN_TX_AUTHORIZATION_ADDRESSES = ATTR_BLOCKCHAIN_TX_AUTHORIZATION_ADDRESSES;
1501
+ exports.ATTR_BLOCKCHAIN_TX_AUTHORIZATION_CHAIN_IDS = ATTR_BLOCKCHAIN_TX_AUTHORIZATION_CHAIN_IDS;
1502
+ exports.ATTR_BLOCKCHAIN_TX_AUTHORIZATION_COUNT = ATTR_BLOCKCHAIN_TX_AUTHORIZATION_COUNT;
1221
1503
  exports.ATTR_BLOCKCHAIN_TX_EFFECTIVE_GAS_PRICE = ATTR_BLOCKCHAIN_TX_EFFECTIVE_GAS_PRICE;
1222
1504
  exports.ATTR_BLOCKCHAIN_TX_FEE = ATTR_BLOCKCHAIN_TX_FEE;
1223
1505
  exports.ATTR_BLOCKCHAIN_TX_FROM = ATTR_BLOCKCHAIN_TX_FROM;
@@ -1245,9 +1527,13 @@ exports.ATTR_GEN_AI_AGENT_ID = ATTR_GEN_AI_AGENT_ID;
1245
1527
  exports.ATTR_GEN_AI_AGENT_NAME = ATTR_GEN_AI_AGENT_NAME;
1246
1528
  exports.ATTR_X402_RESOURCE = ATTR_X402_RESOURCE;
1247
1529
  exports.ATTR_X402_SCHEME = ATTR_X402_SCHEME;
1530
+ exports.BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_PARTIALLY_REVERTED = BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_PARTIALLY_REVERTED;
1531
+ exports.BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_REVERTED = BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_REVERTED;
1532
+ exports.BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_SUCCESS = BLOCKCHAIN_CALL_BATCH_STATUS_VALUE_SUCCESS;
1248
1533
  exports.BLOCKCHAIN_OPERATION_NAME_VALUE_CONFIRM = BLOCKCHAIN_OPERATION_NAME_VALUE_CONFIRM;
1249
1534
  exports.BLOCKCHAIN_OPERATION_NAME_VALUE_PAYMENT = BLOCKCHAIN_OPERATION_NAME_VALUE_PAYMENT;
1250
1535
  exports.BLOCKCHAIN_OPERATION_NAME_VALUE_SEND = BLOCKCHAIN_OPERATION_NAME_VALUE_SEND;
1536
+ exports.BLOCKCHAIN_OPERATION_SUBJECT_VALUE_CALL_BATCH = BLOCKCHAIN_OPERATION_SUBJECT_VALUE_CALL_BATCH;
1251
1537
  exports.BLOCKCHAIN_OPERATION_SUBJECT_VALUE_USER_OPERATION = BLOCKCHAIN_OPERATION_SUBJECT_VALUE_USER_OPERATION;
1252
1538
  exports.BLOCKCHAIN_PAYMENT_PROTOCOL_VALUE_X402 = BLOCKCHAIN_PAYMENT_PROTOCOL_VALUE_X402;
1253
1539
  exports.BLOCKCHAIN_PAYMENT_STATUS_VALUE_FAILED = BLOCKCHAIN_PAYMENT_STATUS_VALUE_FAILED;