@hashspan/core 0.6.0 → 0.8.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.6.0/docs/adr/0011-agent-identity-precedence.md).
9
+ * (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.8.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.6.0/docs/semconv.md for
25
+ * Stability: development. See https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.8.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";
@@ -46,12 +46,12 @@ const ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_NAME = "blockchain.contract.function.nam
46
46
  const ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_SELECTOR = "blockchain.contract.function.selector";
47
47
  /**
48
48
  * Opt-in: decoded call arguments as a JSON array. See
49
- * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.6.0/docs/adr/0004-privacy-defaults.md.
49
+ * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.8.0/docs/adr/0004-privacy-defaults.md.
50
50
  */
51
51
  const ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_ARGUMENTS = "blockchain.contract.function.arguments";
52
52
  /**
53
53
  * 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.6.0/docs/adr/0013-x402-payments.md.
54
+ * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.8.0/docs/adr/0013-x402-payments.md.
55
55
  */
56
56
  const ATTR_BLOCKCHAIN_PAYMENT_PROTOCOL = "blockchain.payment.protocol";
57
57
  const ATTR_BLOCKCHAIN_PAYMENT_PAYER = "blockchain.payment.payer";
@@ -64,12 +64,33 @@ const ATTR_BLOCKCHAIN_PAYMENT_SETTLED_AMOUNT = "blockchain.payment.settled_amoun
64
64
  /**
65
65
  * Whether the settlement transaction's receipt carries the payment, as checked by the adapter; absent when no check
66
66
  * was possible. See
67
- * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.6.0/docs/adr/0017-x402-payment-verification.md.
67
+ * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.8.0/docs/adr/0017-x402-payment-verification.md.
68
68
  */
69
69
  const ATTR_BLOCKCHAIN_PAYMENT_VERIFIED = "blockchain.payment.verified";
70
70
  /** x402's own payment fields. */
71
71
  const ATTR_X402_SCHEME = "x402.scheme";
72
72
  const ATTR_X402_RESOURCE = "x402.resource";
73
+ /**
74
+ * 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.
76
+ */
77
+ const ATTR_BLOCKCHAIN_USER_OPERATION_HASH = "blockchain.user_operation.hash";
78
+ const ATTR_BLOCKCHAIN_USER_OPERATION_SENDER = "blockchain.user_operation.sender";
79
+ const ATTR_BLOCKCHAIN_USER_OPERATION_ENTRY_POINT = "blockchain.user_operation.entry_point";
80
+ const ATTR_BLOCKCHAIN_USER_OPERATION_CALL_COUNT = "blockchain.user_operation.call_count";
81
+ const ATTR_BLOCKCHAIN_USER_OPERATION_NONCE = "blockchain.user_operation.nonce";
82
+ const ATTR_BLOCKCHAIN_USER_OPERATION_SUCCESS = "blockchain.user_operation.success";
83
+ const ATTR_BLOCKCHAIN_USER_OPERATION_GAS_USED = "blockchain.user_operation.gas.used";
84
+ /** What the operation itself paid, in wei; the bundle transaction's fee covers every operation in the bundle. */
85
+ const ATTR_BLOCKCHAIN_USER_OPERATION_GAS_COST = "blockchain.user_operation.gas.cost";
86
+ const ATTR_BLOCKCHAIN_USER_OPERATION_PAYMASTER = "blockchain.user_operation.paymaster";
87
+ /**
88
+ * 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.
90
+ */
91
+ const ATTR_BLOCKCHAIN_OPERATION_SUBJECT = "blockchain.operation.subject";
92
+ /** Values for {@link ATTR_BLOCKCHAIN_OPERATION_SUBJECT}. */
93
+ const BLOCKCHAIN_OPERATION_SUBJECT_VALUE_USER_OPERATION = "user_operation";
73
94
  /** Values for {@link ATTR_BLOCKCHAIN_SYSTEM}. */
74
95
  const BLOCKCHAIN_SYSTEM_VALUE_EVM = "evm";
75
96
  /** Values for {@link ATTR_BLOCKCHAIN_OPERATION_NAME}. */
@@ -88,7 +109,7 @@ const BLOCKCHAIN_TX_STATUS_VALUE_REVERTED = "reverted";
88
109
  /**
89
110
  * @deprecated No longer recorded: a confirm span that gave up waiting records `error.type` `timeout` and no
90
111
  * `blockchain.tx.status`. The constant is removed in 1.0. See
91
- * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.6.0/docs/adr/0016-timeout-is-an-observer-outcome.md.
112
+ * https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.8.0/docs/adr/0016-timeout-is-an-observer-outcome.md.
92
113
  */
93
114
  const BLOCKCHAIN_TX_STATUS_VALUE_TIMEOUT = "timeout";
94
115
  const BLOCKCHAIN_TX_STATUS_VALUE_REPLACED = "replaced";
@@ -101,6 +122,107 @@ const ATTR_ERROR_TYPE = "error.type";
101
122
  /** Fallback {@link ATTR_ERROR_TYPE} value when the error has no name. */
102
123
  const ERROR_TYPE_VALUE_OTHER = "_OTHER";
103
124
  //#endregion
125
+ //#region src/metrics.ts
126
+ /**
127
+ * Duration of a send of a transaction or user operation: from the start of the sending call until the hash is known or
128
+ * the call failed.
129
+ */
130
+ const METRIC_BLOCKCHAIN_CLIENT_SEND_DURATION = "blockchain.client.send.duration";
131
+ /**
132
+ * Duration of a confirmation of a transaction or user operation: from the start of the wait until the receipt, a
133
+ * timeout or a failure.
134
+ */
135
+ const METRIC_BLOCKCHAIN_CLIENT_CONFIRMATION_DURATION = "blockchain.client.confirmation.duration";
136
+ /**
137
+ * Total fee of a mined transaction (execution fee plus L1 data fee), or the cost of a user operation, in the chain's
138
+ * smallest unit (wei).
139
+ */
140
+ const METRIC_BLOCKCHAIN_CLIENT_FEE = "blockchain.client.fee";
141
+ const DURATION_BUCKETS = [
142
+ .05,
143
+ .1,
144
+ .25,
145
+ .5,
146
+ 1,
147
+ 2,
148
+ 5,
149
+ 10,
150
+ 20,
151
+ 30,
152
+ 60,
153
+ 120,
154
+ 300
155
+ ];
156
+ const FEE_BUCKETS = Array.from({ length: 11 }, (_, i) => 10 ** (i + 8));
157
+ const ERROR_CLASS = /^[A-Z][A-Za-z]{0,62}Error$|^Error$/;
158
+ const ERROR_CODE = /^[a-z]{1,32}(_[a-z]{1,32}){0,7}$/;
159
+ /**
160
+ * `attributes` with `error.type` kept only if it is an error class name or a lower-case code, else `_OTHER`. Error
161
+ * names are free text (any `Error.name`), and metrics do not pass through the `redact` hook; the span keeps its own
162
+ * `error.type`.
163
+ */
164
+ function withMetricErrorType(attributes) {
165
+ const type = attributes[ATTR_ERROR_TYPE];
166
+ if (type === void 0) return attributes;
167
+ return typeof type === "string" && (type === "_OTHER" || ERROR_CLASS.test(type) || ERROR_CODE.test(type)) ? attributes : {
168
+ ...attributes,
169
+ [ATTR_ERROR_TYPE]: ERROR_TYPE_VALUE_OTHER
170
+ };
171
+ }
172
+ /** Milliseconds since the epoch of a span time, as the OpenTelemetry API accepts it. */
173
+ function toEpochMs(time) {
174
+ if (time === void 0) return Date.now();
175
+ if (time instanceof Date) return time.getTime();
176
+ if (Array.isArray(time)) return time[0] * 1e3 + time[1] / 1e6;
177
+ if (typeof time !== "number") return Date.now();
178
+ const origin = globalThis.performance?.timeOrigin;
179
+ return typeof origin === "number" && time < origin ? origin + time : time;
180
+ }
181
+ function createTxMetrics(meterProvider, name, version) {
182
+ let histograms;
183
+ const get = () => {
184
+ histograms ??= (() => {
185
+ const meter = (meterProvider ?? _opentelemetry_api.metrics.getMeterProvider()).getMeter(name, version);
186
+ return {
187
+ send: meter.createHistogram(METRIC_BLOCKCHAIN_CLIENT_SEND_DURATION, {
188
+ unit: "s",
189
+ description: "Duration of sending a transaction or user operation, until its hash is known",
190
+ advice: { explicitBucketBoundaries: DURATION_BUCKETS }
191
+ }),
192
+ confirmation: meter.createHistogram(METRIC_BLOCKCHAIN_CLIENT_CONFIRMATION_DURATION, {
193
+ unit: "s",
194
+ description: "Duration of waiting for a transaction or user operation receipt",
195
+ advice: { explicitBucketBoundaries: DURATION_BUCKETS }
196
+ }),
197
+ fee: meter.createHistogram(METRIC_BLOCKCHAIN_CLIENT_FEE, {
198
+ unit: "{wei}",
199
+ description: "Total fee of a mined transaction, or the cost of a user operation",
200
+ advice: { explicitBucketBoundaries: FEE_BUCKETS }
201
+ })
202
+ };
203
+ })();
204
+ return histograms;
205
+ };
206
+ const record = (what, run) => {
207
+ try {
208
+ run();
209
+ } catch (error) {
210
+ _opentelemetry_api.diag.error(`hashspan: failed to record the ${what} metric`, error);
211
+ }
212
+ };
213
+ return {
214
+ sendDuration: (seconds, attributes) => record("send duration", () => {
215
+ if (seconds >= 0) get().send.record(seconds, withMetricErrorType(attributes));
216
+ }),
217
+ confirmationDuration: (seconds, attributes) => record("confirmation duration", () => {
218
+ if (seconds >= 0) get().confirmation.record(seconds, withMetricErrorType(attributes));
219
+ }),
220
+ fee: (wei, attributes) => record("fee", () => {
221
+ if (wei >= 0n) get().fee.record(Number(wei), attributes);
222
+ })
223
+ };
224
+ }
225
+ //#endregion
104
226
  //#region src/confirm-registry.ts
105
227
  /**
106
228
  * Bounded registry from (chainId, tx hash) to its in-flight confirm span, or to "settled" for a while after a
@@ -386,7 +508,7 @@ function sanitizeResource(resource) {
386
508
  }
387
509
  //#endregion
388
510
  //#region src/version.ts
389
- const VERSION = "0.6.0";
511
+ const VERSION = "0.8.0";
390
512
  //#endregion
391
513
  //#region src/tracker.ts
392
514
  const INSTRUMENTATION_NAME = "@hashspan/core";
@@ -409,13 +531,20 @@ const NON_SENSITIVE_KEYS = /* @__PURE__ */ new Set([
409
531
  ATTR_BLOCKCHAIN_PAYMENT_PROTOCOL,
410
532
  ATTR_BLOCKCHAIN_PAYMENT_STATUS,
411
533
  ATTR_BLOCKCHAIN_PAYMENT_VERIFIED,
534
+ ATTR_BLOCKCHAIN_USER_OPERATION_HASH,
535
+ ATTR_BLOCKCHAIN_USER_OPERATION_SUCCESS,
412
536
  ATTR_ERROR_TYPE,
413
537
  ATTR_EXCEPTION_TYPE
414
538
  ]);
415
539
  const TX_HASH = /^0x[0-9a-fA-F]{64}$/;
416
540
  const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
541
+ const ZERO_ADDRESS = /^0x0{40}$/;
417
542
  /** A non-negative integer that fits in 256 bits. */
418
543
  const AMOUNT = /^(0|[1-9][0-9]{0,77})$/;
544
+ /** A `0x` hex quantity of at most 256 bits, as JSON-RPC encodes integers. */
545
+ const HEX_QUANTITY = /^0x[0-9a-fA-F]{1,64}$/;
546
+ const MAX_UINT256 = 2n ** 256n - 1n;
547
+ const MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER);
419
548
  /** `error.type` of a wait that gave up: a confirmation or a payment whose outcome was never learned. */
420
549
  const OBSERVER_TIMEOUT = "timeout";
421
550
  const PAYMENT_STATUSES = /* @__PURE__ */ new Set([
@@ -434,6 +563,11 @@ const noopSend = (parent) => ({
434
563
  end: () => {},
435
564
  fail: () => {}
436
565
  });
566
+ const noopUserOperationSend = (parent) => ({
567
+ context: parent,
568
+ end: () => {},
569
+ fail: () => {}
570
+ });
437
571
  const NOOP_PAYMENT = {
438
572
  end: () => {},
439
573
  fail: () => {},
@@ -445,6 +579,11 @@ const NOOP_CONFIRM = {
445
579
  timeout: () => {},
446
580
  fail: () => {}
447
581
  };
582
+ const NOOP_USER_OPERATION_CONFIRM = {
583
+ end: () => {},
584
+ timeout: () => {},
585
+ fail: () => {}
586
+ };
448
587
  /** Runs `fn`, logging instead of throwing: instrumentation must never break the caller. */
449
588
  function safely(what, fn, fallback) {
450
589
  try {
@@ -467,6 +606,22 @@ const ERROR_TYPE_OVERRIDE = /^[A-Za-z0-9_.-]{1,64}$/;
467
606
  function identifier(value) {
468
607
  return typeof value === "string" && ERROR_TYPE_OVERRIDE.test(value) ? value : void 0;
469
608
  }
609
+ /**
610
+ * A non-negative integer of at most 256 bits, from a bigint, a safe integer, or a decimal or `0x` hex string, as
611
+ * bundlers return them; undefined for anything else.
612
+ */
613
+ function quantity(value) {
614
+ let parsed;
615
+ if (typeof value === "bigint") parsed = value;
616
+ else if (typeof value === "number" && Number.isSafeInteger(value)) parsed = BigInt(value);
617
+ else if (typeof value === "string" && (AMOUNT.test(value) || HEX_QUANTITY.test(value))) parsed = BigInt(value);
618
+ return parsed !== void 0 && parsed >= 0n && parsed <= MAX_UINT256 ? parsed : void 0;
619
+ }
620
+ /** A quantity as a number, or undefined when it is none or too large to be one exactly. */
621
+ function smallQuantity(value) {
622
+ const parsed = quantity(value);
623
+ return parsed !== void 0 && parsed <= MAX_SAFE_INTEGER ? Number(parsed) : void 0;
624
+ }
470
625
  /** A decimal amount, or undefined when `value` is not a non-negative integer. */
471
626
  function amount(value) {
472
627
  const text = typeof value === "bigint" ? value.toString() : value;
@@ -512,8 +667,44 @@ function reportedErrorType(error, options) {
512
667
  return errorType(error);
513
668
  }
514
669
  /**
670
+ * Joins the confirm span for `hash` in `registry`, opening it with `open` for the first handle; undefined when the
671
+ * key recently got a receipt. A receipt from any handle ends the span; a timeout or failure only ends it when it is the
672
+ * last handle still waiting.
673
+ */
674
+ function joinConfirm(registry, chainId, hash, open) {
675
+ const current = registry.get(chainId, hash);
676
+ if (current === "settled") return void 0;
677
+ let confirm = current;
678
+ if (!confirm) {
679
+ confirm = open();
680
+ registry.start(chainId, hash, confirm);
681
+ }
682
+ const shared = confirm;
683
+ shared.active += 1;
684
+ let done = false;
685
+ return {
686
+ shared,
687
+ receive: () => {
688
+ if (done || shared.ended) return false;
689
+ done = true;
690
+ shared.active -= 1;
691
+ shared.ended = true;
692
+ return true;
693
+ },
694
+ withdraw: (end) => {
695
+ if (done || shared.ended) return;
696
+ done = true;
697
+ shared.active -= 1;
698
+ if (shared.active > 0) return;
699
+ shared.ended = true;
700
+ registry.release(chainId, hash, shared);
701
+ end();
702
+ }
703
+ };
704
+ }
705
+ /**
515
706
  * Creates a tracker that records transactions as `send` and `confirm` spans, and payments as `payment` spans, with
516
- * `@opentelemetry/api` (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.6.0/docs/semconv.md). It makes
707
+ * `@opentelemetry/api` (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.8.0/docs/semconv.md). It makes
517
708
  * no network calls; the caller passes hashes and receipts. Its methods and handles never throw: failures are logged
518
709
  * via `diag`, and a method that fails returns a handle that records nothing.
519
710
  */
@@ -529,6 +720,27 @@ function createTxTracker(options = {}) {
529
720
  const formatAddress = safely("configure address mode", () => resolveAddressFormatter(options.address), OFF_ADDRESS_FORMATTER);
530
721
  const errorMessages = safely("configure error message mode", () => resolveErrorMessageMode(options.errorMessages), "off");
531
722
  const paymentResource = safely("configure payment resource mode", () => resolvePaymentResourceMode(options.paymentResource), "off");
723
+ const userOperationLinks = new LinkStore({
724
+ ttlMs: options.linkTtlMs ?? DEFAULT_LINK_TTL_MS,
725
+ maxEntries: options.maxTrackedTransactions ?? DEFAULT_MAX_TRACKED
726
+ });
727
+ const userOperationConfirmations = new ConfirmRegistry({
728
+ ttlMs: options.linkTtlMs ?? DEFAULT_LINK_TTL_MS,
729
+ maxEntries: options.maxTrackedTransactions ?? DEFAULT_MAX_TRACKED
730
+ });
731
+ const txMetrics = createTxMetrics(options.meterProvider, INSTRUMENTATION_NAME, VERSION);
732
+ /** Attributes of a metric: low-cardinality only, never an address, hash or agent identity. */
733
+ const metricAttributes = (chainId, extra = {}) => ({
734
+ [ATTR_BLOCKCHAIN_SYSTEM]: "evm",
735
+ [ATTR_BLOCKCHAIN_CHAIN_ID]: chainId,
736
+ ...extra
737
+ });
738
+ /** Metric attributes of a user operation sample: as for transactions, plus what the sample is about. */
739
+ const userOperationMetricAttributes = (chainId, extra = {}) => metricAttributes(chainId, {
740
+ [ATTR_BLOCKCHAIN_OPERATION_SUBJECT]: BLOCKCHAIN_OPERATION_SUBJECT_VALUE_USER_OPERATION,
741
+ ...extra
742
+ });
743
+ const secondsSince = (startMs, endTime) => (toEpochMs(endTime) - startMs) / 1e3;
532
744
  let tracer;
533
745
  const getTracer = () => {
534
746
  tracer ??= (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(INSTRUMENTATION_NAME, VERSION);
@@ -553,7 +765,7 @@ function createTxTracker(options = {}) {
553
765
  /**
554
766
  * Exception event attributes for `error`, per the error message mode. The error object itself is never handed to
555
767
  * the SDK: its message and stack can carry addresses and calldata
556
- * (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.6.0/docs/adr/0006-error-privacy.md).
768
+ * (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.8.0/docs/adr/0006-error-privacy.md).
557
769
  */
558
770
  const exceptionAttributes = (type, error) => {
559
771
  const attributes = { [ATTR_EXCEPTION_TYPE]: type };
@@ -586,6 +798,7 @@ function createTxTracker(options = {}) {
586
798
  code: _opentelemetry_api.SpanStatusCode.ERROR,
587
799
  ...message !== void 0 ? { message } : {}
588
800
  });
801
+ return type;
589
802
  };
590
803
  /** Ends a span exactly once; the span is always ended even if recording attributes fails. */
591
804
  const finisher = (span) => {
@@ -607,8 +820,8 @@ function createTxTracker(options = {}) {
607
820
  const formatted = formatAddress(address);
608
821
  if (formatted !== void 0) attributes[key] = formatted;
609
822
  };
610
- /** Records `address` only if it is one: payment addresses come from remote parties. */
611
- const setPaymentAddress = (attributes, key, address) => {
823
+ /** Records `address` only if it is one: payment and user operation addresses come from remote parties. */
824
+ const setRemoteAddress = (attributes, key, address) => {
612
825
  if (typeof address === "string" && ADDRESS.test(address)) setAddress(attributes, key, address);
613
826
  };
614
827
  const baseAttributes = (chainId, operation, ctx) => ({
@@ -637,6 +850,8 @@ function createTxTracker(options = {}) {
637
850
  ...input.startTime !== void 0 ? { startTime: input.startTime } : {}
638
851
  }, parent);
639
852
  const finish = finisher(span);
853
+ const startMs = toEpochMs(input.startTime);
854
+ const recordSend = (endTime, errorType) => txMetrics.sendDuration(secondsSince(startMs, endTime), metricAttributes(input.chainId, errorType === void 0 ? {} : { [ATTR_ERROR_TYPE]: errorType }));
640
855
  return {
641
856
  context: _opentelemetry_api.trace.setSpan(parent, span),
642
857
  end: (result, second) => finish("record transaction hash", () => {
@@ -650,10 +865,11 @@ function createTxTracker(options = {}) {
650
865
  parent
651
866
  });
652
867
  span.setAttributes(redact({ [ATTR_BLOCKCHAIN_TX_HASH]: hash }));
868
+ recordSend(handleOptions(second).endTime);
653
869
  }, handleOptions(second).endTime),
654
870
  fail: (error, second, third) => {
655
871
  const options = handleOptions(second, third);
656
- finish("record send failure", () => markError(span, reportedErrorType(error, options), error, errorType(error)), options.endTime);
872
+ finish("record send failure", () => recordSend(options.endTime, markError(span, reportedErrorType(error, options), error, errorType(error))), options.endTime);
657
873
  }
658
874
  };
659
875
  };
@@ -689,6 +905,8 @@ function createTxTracker(options = {}) {
689
905
  ...explicitStart !== void 0 ? { startTime: explicitStart } : {}
690
906
  }, parent);
691
907
  const finish = finisher(span);
908
+ const startMs = toEpochMs(explicitStart);
909
+ const recordConfirmation = (endTime, outcome) => txMetrics.confirmationDuration(secondsSince(startMs, endTime), metricAttributes(input.chainId, outcome));
692
910
  return {
693
911
  active: 0,
694
912
  ended: false,
@@ -698,11 +916,16 @@ function createTxTracker(options = {}) {
698
916
  links: [{ context: span.spanContext() }, ...sent ? [{ context: sent.spanContext }] : []]
699
917
  },
700
918
  receipt: (receipt, endTime) => finish("record receipt", () => {
701
- span.setAttributes(redact(receiptAttributes(receipt)));
919
+ const attributes = receiptAttributes(receipt);
920
+ span.setAttributes(redact(attributes));
702
921
  if (receipt.status === "reverted") markError(span, BLOCKCHAIN_TX_STATUS_VALUE_REVERTED);
922
+ const status = { [ATTR_BLOCKCHAIN_TX_STATUS]: attributes[ATTR_BLOCKCHAIN_TX_STATUS] };
923
+ recordConfirmation(endTime, status);
924
+ const fee = attributes[ATTR_BLOCKCHAIN_TX_FEE];
925
+ if (typeof fee === "string") txMetrics.fee(BigInt(fee), metricAttributes(input.chainId, status));
703
926
  }, endTime),
704
- timeout: (endTime) => finish("record confirmation timeout", () => markError(span, OBSERVER_TIMEOUT), endTime),
705
- fail: (error, endTime) => finish("record confirmation failure", () => markError(span, errorType(error), error), endTime),
927
+ timeout: (endTime) => finish("record confirmation timeout", () => recordConfirmation(endTime, { [ATTR_ERROR_TYPE]: markError(span, OBSERVER_TIMEOUT) }), endTime),
928
+ fail: (error, endTime) => finish("record confirmation failure", () => recordConfirmation(endTime, { [ATTR_ERROR_TYPE]: markError(span, errorType(error), error) }), endTime),
706
929
  replaced: (hash, reason, endTime) => finish("record replacement", () => {
707
930
  const attributes = {
708
931
  [ATTR_BLOCKCHAIN_TX_STATUS]: BLOCKCHAIN_TX_STATUS_VALUE_REPLACED,
@@ -710,8 +933,9 @@ function createTxTracker(options = {}) {
710
933
  };
711
934
  if (reason !== void 0 && REPLACEMENT_REASONS.has(reason)) attributes[ATTR_BLOCKCHAIN_TX_REPLACEMENT_REASON] = reason;
712
935
  span.setAttributes(redact(attributes));
936
+ recordConfirmation(endTime, { [ATTR_BLOCKCHAIN_TX_STATUS]: BLOCKCHAIN_TX_STATUS_VALUE_REPLACED });
713
937
  }, endTime),
714
- unattributable: (endTime) => finish("record unattributable receipt", () => markError(span, ERROR_TYPE_VALUE_OTHER), endTime)
938
+ unattributable: (endTime) => finish("record unattributable receipt", () => recordConfirmation(endTime, { [ATTR_ERROR_TYPE]: markError(span, ERROR_TYPE_VALUE_OTHER) }), endTime)
715
939
  };
716
940
  };
717
941
  /**
@@ -733,7 +957,7 @@ function createTxTracker(options = {}) {
733
957
  };
734
958
  /**
735
959
  * Ends `shared` with `receipt`, attributing it to the transaction that was mined
736
- * (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.6.0/docs/adr/0008-replaced-transactions.md).
960
+ * (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.8.0/docs/adr/0008-replaced-transactions.md).
737
961
  */
738
962
  const endWithReceipt = (chainId, hash, shared, receipt, endTime) => {
739
963
  const mined = receipt.transactionHash;
@@ -763,41 +987,22 @@ function createTxTracker(options = {}) {
763
987
  */
764
988
  const startConfirm = (input, parentCtx) => {
765
989
  const { chainId, hash } = input;
766
- const current = confirmations.get(chainId, hash);
767
- if (current === "settled") return NOOP_CONFIRM;
768
- let confirm = current;
769
- if (!confirm) {
770
- confirm = openConfirm(input, parentCtx);
771
- confirmations.start(chainId, hash, confirm);
772
- }
773
- const shared = confirm;
774
- shared.active += 1;
775
- let done = false;
776
- const withdraw = (end) => {
777
- if (done || shared.ended) return;
778
- done = true;
779
- shared.active -= 1;
780
- if (shared.active > 0) return;
781
- shared.ended = true;
782
- confirmations.release(chainId, hash, shared);
783
- end();
784
- };
990
+ const claim = joinConfirm(confirmations, chainId, hash, () => openConfirm(input, parentCtx));
991
+ if (!claim) return NOOP_CONFIRM;
992
+ const { shared } = claim;
785
993
  return {
786
994
  end: (receipt, second) => {
787
995
  const { endTime } = handleOptions(second);
788
- if (done || shared.ended) return;
789
- done = true;
790
- shared.active -= 1;
791
- shared.ended = true;
996
+ if (!claim.receive()) return;
792
997
  safely("record receipt", () => endWithReceipt(chainId, hash, shared, receipt, endTime), void 0);
793
998
  },
794
999
  timeout: (second) => {
795
1000
  const { endTime } = handleOptions(second);
796
- withdraw(() => shared.timeout(endTime));
1001
+ claim.withdraw(() => shared.timeout(endTime));
797
1002
  },
798
1003
  fail: (error, second) => {
799
1004
  const { endTime } = handleOptions(second);
800
- withdraw(() => shared.fail(error, endTime));
1005
+ claim.withdraw(() => shared.fail(error, endTime));
801
1006
  }
802
1007
  };
803
1008
  };
@@ -806,10 +1011,10 @@ function createTxTracker(options = {}) {
806
1011
  const attributes = baseAttributes(input.chainId, BLOCKCHAIN_OPERATION_NAME_VALUE_PAYMENT, parent);
807
1012
  const protocol = identifier(input.protocol);
808
1013
  if (protocol !== void 0) attributes[ATTR_BLOCKCHAIN_PAYMENT_PROTOCOL] = protocol;
809
- setPaymentAddress(attributes, ATTR_BLOCKCHAIN_PAYMENT_PAYER, input.payer);
1014
+ setRemoteAddress(attributes, ATTR_BLOCKCHAIN_PAYMENT_PAYER, input.payer);
810
1015
  const knownPayer = typeof input.payer === "string" && ADDRESS.test(input.payer);
811
- setPaymentAddress(attributes, ATTR_BLOCKCHAIN_PAYMENT_RECIPIENT, input.recipient);
812
- setPaymentAddress(attributes, ATTR_BLOCKCHAIN_PAYMENT_ASSET, input.asset);
1016
+ setRemoteAddress(attributes, ATTR_BLOCKCHAIN_PAYMENT_RECIPIENT, input.recipient);
1017
+ setRemoteAddress(attributes, ATTR_BLOCKCHAIN_PAYMENT_ASSET, input.asset);
813
1018
  const paid = amount(input.amount);
814
1019
  if (paid !== void 0) attributes[ATTR_BLOCKCHAIN_PAYMENT_AMOUNT] = paid;
815
1020
  const scheme = identifier(input.x402?.scheme);
@@ -841,7 +1046,7 @@ function createTxTracker(options = {}) {
841
1046
  const settled = { [ATTR_BLOCKCHAIN_PAYMENT_STATUS]: status };
842
1047
  const hash = settlement.hash;
843
1048
  if (linkHash(hash)) settled[ATTR_BLOCKCHAIN_TX_HASH] = hash;
844
- if (!knownPayer) setPaymentAddress(settled, ATTR_BLOCKCHAIN_PAYMENT_PAYER, settlement.payer);
1049
+ if (!knownPayer) setRemoteAddress(settled, ATTR_BLOCKCHAIN_PAYMENT_PAYER, settlement.payer);
845
1050
  const settledAmount = amount(settlement.amount);
846
1051
  if (settledAmount !== void 0) {
847
1052
  settled[ATTR_BLOCKCHAIN_PAYMENT_SETTLED_AMOUNT] = settledAmount;
@@ -862,10 +1067,138 @@ function createTxTracker(options = {}) {
862
1067
  link: (hash) => safely("link the payment span", () => void linkHash(hash), void 0)
863
1068
  };
864
1069
  };
1070
+ const startUserOperationSend = (input, parentCtx) => {
1071
+ const parent = parentCtx ?? _opentelemetry_api.context.active();
1072
+ const { chainId } = input;
1073
+ const attributes = baseAttributes(chainId, BLOCKCHAIN_OPERATION_NAME_VALUE_SEND, parent);
1074
+ setRemoteAddress(attributes, ATTR_BLOCKCHAIN_USER_OPERATION_SENDER, input.sender);
1075
+ setRemoteAddress(attributes, ATTR_BLOCKCHAIN_USER_OPERATION_ENTRY_POINT, input.entryPoint);
1076
+ const callCount = smallQuantity(input.callCount);
1077
+ if (callCount !== void 0) attributes[ATTR_BLOCKCHAIN_USER_OPERATION_CALL_COUNT] = callCount;
1078
+ const span = getTracer().startSpan(`send ${chainId}`, {
1079
+ kind: _opentelemetry_api.SpanKind.CLIENT,
1080
+ attributes: redact(attributes),
1081
+ ...input.startTime !== void 0 ? { startTime: input.startTime } : {}
1082
+ }, parent);
1083
+ const finish = finisher(span);
1084
+ const startMs = toEpochMs(input.startTime);
1085
+ const recordSend = (endTime, errorType) => txMetrics.sendDuration(secondsSince(startMs, endTime), userOperationMetricAttributes(chainId, errorType === void 0 ? {} : { [ATTR_ERROR_TYPE]: errorType }));
1086
+ return {
1087
+ context: _opentelemetry_api.trace.setSpan(parent, span),
1088
+ end: (result, second) => {
1089
+ const { endTime } = handleOptions(second);
1090
+ finish("record user operation hash", () => {
1091
+ const hash = result?.userOpHash;
1092
+ if (typeof hash !== "string" || !TX_HASH.test(hash)) {
1093
+ _opentelemetry_api.diag.debug("hashspan: ending a send span without a valid user operation hash");
1094
+ return;
1095
+ }
1096
+ userOperationLinks.set(chainId, hash, {
1097
+ spanContext: span.spanContext(),
1098
+ parent
1099
+ });
1100
+ span.setAttributes(redact({ [ATTR_BLOCKCHAIN_USER_OPERATION_HASH]: hash }));
1101
+ recordSend(endTime);
1102
+ }, endTime);
1103
+ },
1104
+ fail: (error, second) => {
1105
+ const read = handleOptions(second);
1106
+ finish("record user operation send failure", () => recordSend(read.endTime, markError(span, reportedErrorType(error, read), error, errorType(error))), read.endTime);
1107
+ }
1108
+ };
1109
+ };
1110
+ /**
1111
+ * Attributes of a user operation receipt. Its values come from a bundler: what is malformed is left out. The bundle
1112
+ * transaction's status, gas and fee are not recorded: they cover every operation in the bundle (ADR 0021).
1113
+ */
1114
+ const userOperationReceiptAttributes = (receipt) => {
1115
+ const attributes = {};
1116
+ const success = receipt.success;
1117
+ if (typeof success === "boolean") attributes[ATTR_BLOCKCHAIN_USER_OPERATION_SUCCESS] = success;
1118
+ const gasUsed = smallQuantity(receipt.actualGasUsed);
1119
+ if (gasUsed !== void 0) attributes[ATTR_BLOCKCHAIN_USER_OPERATION_GAS_USED] = gasUsed;
1120
+ const gasCost = quantity(receipt.actualGasCost);
1121
+ if (gasCost !== void 0) attributes[ATTR_BLOCKCHAIN_USER_OPERATION_GAS_COST] = gasCost.toString();
1122
+ setRemoteAddress(attributes, ATTR_BLOCKCHAIN_USER_OPERATION_SENDER, receipt.sender);
1123
+ const nonce = quantity(receipt.nonce);
1124
+ if (nonce !== void 0) attributes[ATTR_BLOCKCHAIN_USER_OPERATION_NONCE] = nonce.toString();
1125
+ const paymaster = receipt.paymaster;
1126
+ if (typeof paymaster === "string" && !ZERO_ADDRESS.test(paymaster)) setRemoteAddress(attributes, ATTR_BLOCKCHAIN_USER_OPERATION_PAYMASTER, paymaster);
1127
+ setRemoteAddress(attributes, ATTR_BLOCKCHAIN_USER_OPERATION_ENTRY_POINT, receipt.entryPoint);
1128
+ const reason = receipt.revertReason;
1129
+ if (typeof reason === "string") attributes[ATTR_BLOCKCHAIN_TX_REVERT_REASON] = formatAddressesIn(reason, formatAddress);
1130
+ const bundle = receipt.transactionHash;
1131
+ if (typeof bundle === "string" && TX_HASH.test(bundle)) attributes[ATTR_BLOCKCHAIN_TX_HASH] = bundle;
1132
+ const block = smallQuantity(receipt.blockNumber);
1133
+ if (block !== void 0) attributes[ATTR_BLOCKCHAIN_BLOCK_NUMBER] = block;
1134
+ return attributes;
1135
+ };
1136
+ /** Opens the confirm span of a user operation. */
1137
+ const openUserOperationConfirm = (input, parentCtx) => {
1138
+ const { chainId, userOpHash } = input;
1139
+ const sent = userOperationLinks.get(chainId, userOpHash);
1140
+ const active = _opentelemetry_api.context.active();
1141
+ const parent = parentCtx ?? (_opentelemetry_api.trace.getSpan(active) ? active : sent?.parent ?? active);
1142
+ const attributes = baseAttributes(chainId, BLOCKCHAIN_OPERATION_NAME_VALUE_CONFIRM, parent);
1143
+ attributes[ATTR_BLOCKCHAIN_USER_OPERATION_HASH] = userOpHash;
1144
+ const span = getTracer().startSpan(`confirm ${chainId}`, {
1145
+ kind: _opentelemetry_api.SpanKind.CLIENT,
1146
+ attributes: redact(attributes),
1147
+ links: sent ? [{ context: sent.spanContext }] : [],
1148
+ ...input.startTime !== void 0 ? { startTime: input.startTime } : {}
1149
+ }, parent);
1150
+ const finish = finisher(span);
1151
+ const startMs = toEpochMs(input.startTime);
1152
+ const recordConfirmation = (endTime, outcome) => txMetrics.confirmationDuration(secondsSince(startMs, endTime), userOperationMetricAttributes(chainId, outcome));
1153
+ return {
1154
+ active: 0,
1155
+ ended: false,
1156
+ receipt: (receipt, endTime) => finish("record user operation receipt", () => {
1157
+ const attributes = userOperationReceiptAttributes(receipt ?? {});
1158
+ span.setAttributes(redact(attributes));
1159
+ const success = attributes[ATTR_BLOCKCHAIN_USER_OPERATION_SUCCESS];
1160
+ if (success === false) markError(span, BLOCKCHAIN_TX_STATUS_VALUE_REVERTED);
1161
+ const outcome = typeof success === "boolean" ? { [ATTR_BLOCKCHAIN_USER_OPERATION_SUCCESS]: success } : {};
1162
+ recordConfirmation(endTime, outcome);
1163
+ const cost = attributes[ATTR_BLOCKCHAIN_USER_OPERATION_GAS_COST];
1164
+ if (typeof cost === "string") txMetrics.fee(BigInt(cost), userOperationMetricAttributes(chainId, outcome));
1165
+ }, endTime),
1166
+ timeout: (endTime) => finish("record user operation confirmation timeout", () => recordConfirmation(endTime, { [ATTR_ERROR_TYPE]: markError(span, OBSERVER_TIMEOUT) }), endTime),
1167
+ 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)
1168
+ };
1169
+ };
1170
+ const startUserOperationConfirm = (input, parentCtx) => {
1171
+ const { chainId, userOpHash } = input;
1172
+ if (typeof userOpHash !== "string" || !TX_HASH.test(userOpHash)) {
1173
+ _opentelemetry_api.diag.debug("hashspan: not confirming a user operation without a valid hash");
1174
+ return NOOP_USER_OPERATION_CONFIRM;
1175
+ }
1176
+ const claim = joinConfirm(userOperationConfirmations, chainId, userOpHash, () => openUserOperationConfirm(input, parentCtx));
1177
+ if (!claim) return NOOP_USER_OPERATION_CONFIRM;
1178
+ const { shared } = claim;
1179
+ return {
1180
+ end: (receipt, second) => {
1181
+ const { endTime } = handleOptions(second);
1182
+ if (!claim.receive()) return;
1183
+ userOperationConfirmations.settle(chainId, userOpHash, shared);
1184
+ shared.receipt(receipt, endTime);
1185
+ },
1186
+ timeout: (second) => {
1187
+ const { endTime } = handleOptions(second);
1188
+ claim.withdraw(() => shared.timeout(endTime));
1189
+ },
1190
+ fail: (error, second) => {
1191
+ const read = handleOptions(second);
1192
+ claim.withdraw(() => shared.fail(error, read));
1193
+ }
1194
+ };
1195
+ };
865
1196
  return {
866
1197
  startSend: (input, parent) => safely("start send span", () => startSend(input, parent), noopSend(parent ?? _opentelemetry_api.context.active())),
867
1198
  startConfirm: (input, parent) => safely("start confirm span", () => startConfirm(input, parent), NOOP_CONFIRM),
868
- startPayment: (input, parent) => safely("start payment span", () => startPayment(input, parent), NOOP_PAYMENT)
1199
+ startPayment: (input, parent) => safely("start payment span", () => startPayment(input, parent), NOOP_PAYMENT),
1200
+ 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)
869
1202
  };
870
1203
  }
871
1204
  //#endregion
@@ -875,6 +1208,7 @@ exports.ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_ARGUMENTS = ATTR_BLOCKCHAIN_CONTRACT_F
875
1208
  exports.ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_NAME = ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_NAME;
876
1209
  exports.ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_SELECTOR = ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_SELECTOR;
877
1210
  exports.ATTR_BLOCKCHAIN_OPERATION_NAME = ATTR_BLOCKCHAIN_OPERATION_NAME;
1211
+ exports.ATTR_BLOCKCHAIN_OPERATION_SUBJECT = ATTR_BLOCKCHAIN_OPERATION_SUBJECT;
878
1212
  exports.ATTR_BLOCKCHAIN_PAYMENT_AMOUNT = ATTR_BLOCKCHAIN_PAYMENT_AMOUNT;
879
1213
  exports.ATTR_BLOCKCHAIN_PAYMENT_ASSET = ATTR_BLOCKCHAIN_PAYMENT_ASSET;
880
1214
  exports.ATTR_BLOCKCHAIN_PAYMENT_PAYER = ATTR_BLOCKCHAIN_PAYMENT_PAYER;
@@ -897,6 +1231,15 @@ exports.ATTR_BLOCKCHAIN_TX_REVERT_REASON = ATTR_BLOCKCHAIN_TX_REVERT_REASON;
897
1231
  exports.ATTR_BLOCKCHAIN_TX_STATUS = ATTR_BLOCKCHAIN_TX_STATUS;
898
1232
  exports.ATTR_BLOCKCHAIN_TX_TO = ATTR_BLOCKCHAIN_TX_TO;
899
1233
  exports.ATTR_BLOCKCHAIN_TX_VALUE = ATTR_BLOCKCHAIN_TX_VALUE;
1234
+ exports.ATTR_BLOCKCHAIN_USER_OPERATION_CALL_COUNT = ATTR_BLOCKCHAIN_USER_OPERATION_CALL_COUNT;
1235
+ exports.ATTR_BLOCKCHAIN_USER_OPERATION_ENTRY_POINT = ATTR_BLOCKCHAIN_USER_OPERATION_ENTRY_POINT;
1236
+ exports.ATTR_BLOCKCHAIN_USER_OPERATION_GAS_COST = ATTR_BLOCKCHAIN_USER_OPERATION_GAS_COST;
1237
+ exports.ATTR_BLOCKCHAIN_USER_OPERATION_GAS_USED = ATTR_BLOCKCHAIN_USER_OPERATION_GAS_USED;
1238
+ exports.ATTR_BLOCKCHAIN_USER_OPERATION_HASH = ATTR_BLOCKCHAIN_USER_OPERATION_HASH;
1239
+ exports.ATTR_BLOCKCHAIN_USER_OPERATION_NONCE = ATTR_BLOCKCHAIN_USER_OPERATION_NONCE;
1240
+ exports.ATTR_BLOCKCHAIN_USER_OPERATION_PAYMASTER = ATTR_BLOCKCHAIN_USER_OPERATION_PAYMASTER;
1241
+ exports.ATTR_BLOCKCHAIN_USER_OPERATION_SENDER = ATTR_BLOCKCHAIN_USER_OPERATION_SENDER;
1242
+ exports.ATTR_BLOCKCHAIN_USER_OPERATION_SUCCESS = ATTR_BLOCKCHAIN_USER_OPERATION_SUCCESS;
900
1243
  exports.ATTR_ERROR_TYPE = ATTR_ERROR_TYPE;
901
1244
  exports.ATTR_GEN_AI_AGENT_ID = ATTR_GEN_AI_AGENT_ID;
902
1245
  exports.ATTR_GEN_AI_AGENT_NAME = ATTR_GEN_AI_AGENT_NAME;
@@ -905,6 +1248,7 @@ exports.ATTR_X402_SCHEME = ATTR_X402_SCHEME;
905
1248
  exports.BLOCKCHAIN_OPERATION_NAME_VALUE_CONFIRM = BLOCKCHAIN_OPERATION_NAME_VALUE_CONFIRM;
906
1249
  exports.BLOCKCHAIN_OPERATION_NAME_VALUE_PAYMENT = BLOCKCHAIN_OPERATION_NAME_VALUE_PAYMENT;
907
1250
  exports.BLOCKCHAIN_OPERATION_NAME_VALUE_SEND = BLOCKCHAIN_OPERATION_NAME_VALUE_SEND;
1251
+ exports.BLOCKCHAIN_OPERATION_SUBJECT_VALUE_USER_OPERATION = BLOCKCHAIN_OPERATION_SUBJECT_VALUE_USER_OPERATION;
908
1252
  exports.BLOCKCHAIN_PAYMENT_PROTOCOL_VALUE_X402 = BLOCKCHAIN_PAYMENT_PROTOCOL_VALUE_X402;
909
1253
  exports.BLOCKCHAIN_PAYMENT_STATUS_VALUE_FAILED = BLOCKCHAIN_PAYMENT_STATUS_VALUE_FAILED;
910
1254
  exports.BLOCKCHAIN_PAYMENT_STATUS_VALUE_PENDING = BLOCKCHAIN_PAYMENT_STATUS_VALUE_PENDING;
@@ -918,5 +1262,8 @@ exports.BLOCKCHAIN_TX_STATUS_VALUE_REVERTED = BLOCKCHAIN_TX_STATUS_VALUE_REVERTE
918
1262
  exports.BLOCKCHAIN_TX_STATUS_VALUE_SUCCESS = BLOCKCHAIN_TX_STATUS_VALUE_SUCCESS;
919
1263
  exports.BLOCKCHAIN_TX_STATUS_VALUE_TIMEOUT = BLOCKCHAIN_TX_STATUS_VALUE_TIMEOUT;
920
1264
  exports.ERROR_TYPE_VALUE_OTHER = ERROR_TYPE_VALUE_OTHER;
1265
+ exports.METRIC_BLOCKCHAIN_CLIENT_CONFIRMATION_DURATION = METRIC_BLOCKCHAIN_CLIENT_CONFIRMATION_DURATION;
1266
+ exports.METRIC_BLOCKCHAIN_CLIENT_FEE = METRIC_BLOCKCHAIN_CLIENT_FEE;
1267
+ exports.METRIC_BLOCKCHAIN_CLIENT_SEND_DURATION = METRIC_BLOCKCHAIN_CLIENT_SEND_DURATION;
921
1268
  exports.VERSION = VERSION;
922
1269
  exports.createTxTracker = createTxTracker;