Skip to content

release: align SDK with EigenLayer-AVS PR #509 Transfer field semantics#207

Merged
chrisli30 merged 8 commits into
mainfrom
staging
Apr 9, 2026
Merged

release: align SDK with EigenLayer-AVS PR #509 Transfer field semantics#207
chrisli30 merged 8 commits into
mainfrom
staging

Conversation

@chrisli30

Copy link
Copy Markdown
Member

Summary

Documentation, type-comment, test-fixture, and changeset alignment with EigenLayer-AVS PR #509, which changed the semantics of TransferEventResponse.value for ERC-20 Transfer event triggers.

No production SDK source code reads transferData.value directly, so there is no compile-time API break. This is a documentation + consumer-guidance + test-fixture release.

What changed in EigenLayer-AVS

For ERC-20 Transfer event triggers, the operator's shared event enrichment now publishes:

Field Type Meaning
data.value string Raw uint256 base units (e.g. "1500000" for 1.5 USDC)
data.valueFormatted string Decimal-applied display string (e.g. "1.5")

Previously data.value held the formatted value when token metadata was available. SDK consumers that read data.value for display will now get raw base units and break.

Commits

SHA Type Summary
44f1764 docs Align SDK with PR #509: type comment, README section, example.ts comment, test fixtures, changeset
a2afd43 test Drop deprecated applyToFields ["Transfer.value"] from event trigger tests + add semantic regex assertions
668002b docs Scope Transfer value guidance to @remarks, not a field-level @deprecated (avoids strikethrough on 30+ legitimate non-Transfer usages)

Highlights

MethodCallType.applyToFields guidance, not deprecation (668002b)

The first attempt put @deprecated on MethodCallType.applyToFields. Audit found 30+ legitimate non-Transfer uses across contractRead.test.ts, loopNode.test.ts, eventTrigger.test.ts, uniswapv3_stoploss.test.ts, and the production wiring in models/trigger/event.ts / node/contractRead.ts / node/loop.ts (Chainlink AnswerUpdated.current/latestRoundData.answer, ERC-20 totalSupply, etc.). A blanket @deprecated would strikethrough all of them in IDE intellisense. The actual deprecation is value-specific (["Transfer.value"]), which TypeScript can't express at the type level. Replaced with an @remarks block that documents the Transfer-specific guidance without polluting non-Transfer usages.

Test cleanup + semantic contract assertion (a2afd43)

Removed the now-redundant methodCalls: [{ methodName: "decimals", applyToFields: ["Transfer.value"] }] blocks from tests/triggers/eventTrigger.test.ts (1 site) and tests/templates/telegram-alert-on-transfer.test.ts (2 sites). Added new semantic regex assertions in eventTrigger.test.ts that pin the PR #509 contract:

expect(transferData.value as string).toMatch(/^\d+$/);              // raw base units
expect(transferData.valueFormatted as string).toMatch(/^\d+(\.\d+)?$/); // decimal

These catch any future regression where the operator accidentally swaps the two fields back.

Documentation surface (44f1764)

  • packages/sdk-js/README.md — new "Event trigger output: value vs valueFormatted" section with field semantics table and breaking-change callout
  • examples/example.ts — comment on scheduleMonitorTransfer's ContractWrite step explaining when to use data.value (raw, for ERC-20 calldata) vs data.valueFormatted (display)
  • .changeset/transfer-value-semantics.md — minor bump for @avaprotocol/sdk-js and @avaprotocol/types with the migration note

Test plan

  • yarn build — both @avaprotocol/types and @avaprotocol/sdk-js build clean
  • Verified TransferEvent test fixtures use the new field semantics (value: "100500000" + valueFormatted: "100.5" for USDC)
  • Verified all applyToFields: ["Transfer.value"] configurations are removed from active test code (only mentioned in deprecation comments)
  • Verified all 30+ non-Transfer applyToFields usages still render normally (no @deprecated strikethrough)
  • Reviewers: confirm no in-flight workflow definitions in production rely on the old value semantics

The operator's shared event enrichment was updated in EigenLayer-AVS
PR #509 so that on ERC-20 Transfer event triggers:

  data.value          → raw uint256 base units (string, for math)
  data.valueFormatted → decimal-applied display string (for UI/messages)

Previously data.value held the formatted value when token metadata was
available. SDK consumers that read data.value for display will now get
raw base units and break.

Changes:

- packages/types/src/shared.ts — add @deprecated JSDoc to
  MethodCallType.applyToFields explaining the field is now redundant
  for Transfer.value (the operator publishes valueFormatted directly),
  while remaining valid for non-Transfer events such as Chainlink
  AnswerUpdated current/answer.

- packages/sdk-js/README.md — new "Event trigger output: value vs
  valueFormatted" section with field table, breaking change note, and
  applyToFields deprecation rationale.

- examples/example.ts — comment near scheduleMonitorTransfer's
  ContractWrite step explaining when to use data.value (raw, for
  ERC-20 calldata encoding) vs data.valueFormatted (display).

- tests/triggers/eventTrigger.test.ts — assert valueFormatted is
  defined and string-typed alongside value; update stale "Formatted
  value" comments.

- tests/templates/telegram-alert-on-transfer.test.ts — update mock
  fixture from value: "100.5" (formatted) to value: "100500000" /
  valueFormatted: "100.5" / tokenDecimals: 6; assert valueFormatted
  in the runTrigger output check.

- tests/executions/stepInput.test.ts — prefer eventData.valueFormatted
  in the CustomCode snippet, fall back to decoding rawData only when
  the new field is unavailable (simulation paths).

- .changeset/transfer-value-semantics.md — minor bump for
  @avaprotocol/sdk-js and @avaprotocol/types with the migration note.

No production SDK source reads transferData.value, so there is no
compile-time API break. This is a documentation and consumer-guidance
release.
…gger tests

Following the deprecation of MethodCallType.applyToFields for Transfer
events (the operator's shared event enrichment now publishes
valueFormatted automatically), remove the redundant methodCall blocks
from the SDK's transfer-related tests:

- tests/triggers/eventTrigger.test.ts:835 — drop the methodCalls
  block from the runTrigger Transfer enrichment test query. Add
  semantic regex assertions to pin the PR #509 contract: `value` must
  be digits-only (raw uint256 base units), `valueFormatted` must look
  like a decimal-formatted string. These catch any future operator
  regression that swaps the two fields.

- tests/templates/telegram-alert-on-transfer.test.ts:130, 177 —
  drop the methodCalls blocks from both outgoing and incoming
  transfer queries. Update the function-level docstring to remove
  the stale "applyDecimalsTo" reference and explain that shared
  enrichment publishes both value and valueFormatted automatically.

No functional change to the trigger setup — the operator emits the
same enriched data with or without the deprecated methodCall, just
without the now-redundant decimal lookup.
…precation

The previous commit added @deprecated to MethodCallType.applyToFields,
but applyToFields has 30+ legitimate non-Transfer usages across the
codebase: Chainlink AnswerUpdated current/answer, ERC-20 totalSupply,
latestRoundData.answer, and other contract read fields where shared
event enrichment does not pre-compute a formatted value. A blanket
field-level @deprecated would incorrectly strikethrough every one of
those uses in IDE intellisense and lint reports.

The actual deprecation is value-specific (the literal string
"Transfer.value"), which TypeScript cannot express at the type level.
Replace @deprecated with an @remarks block that documents the
Transfer-specific guidance without polluting non-Transfer usages.

Updates:

- packages/types/src/shared.ts — replace @deprecated with @remarks.
  The Transfer-specific guidance is preserved verbatim; the
  field-level type tag is gone so non-Transfer usages render normally.

- packages/sdk-js/README.md — soften "deprecated" framing to
  "do not use for Transfer events", emphasizing the field is still
  the correct mechanism for everything else.

- .changeset/transfer-value-semantics.md — clarify that this is a
  guidance change, not a type-level deprecation.
@claude

claude Bot commented Apr 8, 2026

Copy link
Copy Markdown

PR Review: align SDK with EigenLayer-AVS PR #509 Transfer field semantics

Overall this is a well-reasoned docs + test-fixture update. The commit history is clean, the @remarks decision (avoiding blanket @deprecated) is the right call, and the semantic regex assertions in eventTrigger.test.ts are a nice addition. A few issues to address:


Bug / Violation of Project Convention

stepInput.test.ts:522 — conditional fallback conflicts with test determinism rule

if (eventData.valueFormatted) {
  valueFormatted = parseFloat(eventData.valueFormatted);
} else {
  const rawValue = eventData.rawData || "0x0";
  const valueWei = parseInt(rawValue, 16);           // ⚠️ see precision note below
  valueFormatted = valueWei / Math.pow(10, 18);
}

CLAUDE.md states: "All test assertions must be deterministic — never use conditional checks that allow tests to pass on errors." Even though this conditional lives inside the CustomCode template string (not directly in an expect() call), it makes the test pass via two different code paths. The simulation path silently falls back to rawData decoding; the live-operator path uses valueFormatted. A regression that drops valueFormatted in the simulation would still produce a passing test via the fallback.

Since this is a simulation test, it will always hit the else branch (valueFormatted is not populated by the simulator). The if branch is dead code in this context. Consider either:

  • removing the fallback entirely and asserting valueFormatted is defined upfront (deterministic), or
  • adding a separate test that explicitly asserts valueFormatted is present in the simulation output and then uses it unconditionally.

Precision Issue in Fallback Code

stepInput.test.ts:526parseInt(rawValue, 16) silently loses precision for uint256 values above Number.MAX_SAFE_INTEGER (~9 × 10¹⁵). For a token with 18 decimals, 100 ETH = 100 * 10^18 = 10^20, which already exceeds the safe integer limit. This is pre-existing code, but this PR is adding context around numeric semantics so it's a good time to fix:

// Before:
const valueWei = parseInt(rawValue, 16);
const valueFormatted = valueWei / Math.pow(10, 18);

// After:
const valueWei = BigInt(rawValue);
const valueFormatted = Number(valueWei) / Math.pow(10, 18); // acceptable for display
// or more precisely:
const valueFormatted = Number(valueWei * 10000n / BigInt(10 ** 18)) / 10000;

Same note applies to the parseFloat(eventData.valueFormatted) call: for tokens with many decimals and large amounts the float representation may lose the last few digits. For display with _.floor(..., 4) it's probably fine in practice, but worth a comment.


Comment Inconsistency ("deprecated" language)

The third commit (668002b) was specifically about removing @deprecated to avoid IDE strikethrough pollution. However, two inline comments still use the word "deprecated":

  • tests/templates/telegram-alert-on-transfer.test.ts:133-134:
    // `valueFormatted` automatically. applyToFields ["Transfer.value"]
    // is deprecated for this case.
  • tests/triggers/eventTrigger.test.ts:838-839:
    // applyToFields: ["Transfer.value"] is deprecated for this
    // use case (see MethodCallType JSDoc and SDK README).

These contradict the deliberate @remarks-not-@deprecated decision. Suggest changing to "is no longer needed for this case" or "should not be used for Transfer events" to stay consistent with the JSDoc and changeset framing.


Minor: Changeset Semver

This is marked minor for both packages. The runtime behavior of data.value has changed from formatted → raw base units — consumers reading data.value for display will silently show wrong values (not a compile error). The SDK itself doesn't read this field, so there's no API surface break, but the practical impact for end consumers is a major-level behavioral change. Worth discussing whether this should be major or at least flagging it prominently in the release notes (the changeset body already does a good job of this).


Suggestion: Add a TransferEventData type

Now that value, valueFormatted, tokenSymbol, and tokenDecimals are well-defined and documented, a typed interface would help consumers catch misuse at compile time:

// packages/types/src/trigger.ts (or shared.ts)
export interface TransferEventData {
  value: string;           // raw uint256 base units
  valueFormatted: string;  // decimal-applied display string
  tokenSymbol: string;
  tokenDecimals: number;
  fromAddress: string;
  toAddress: string;
  blockNumber: number;
  transactionHash: string;
}

Not a blocker for this PR, but a natural follow-up given the documentation work done here.


Nit: README version reference is vague

**Breaking change in v1.x**: Prior versions populated `data.value` with...

"v1.x" doesn't tell consumers which specific version introduced this. Consider pinning the exact version (e.g., "as of v1.4.0") once the changeset lands, or replacing with "as of EigenLayer-AVS PR #509 / SDK v<X.Y.Z>".


Summary: The core approach is solid. The two required fixes before merge are (1) the determinism violation in stepInput.test.ts and (2) aligning the inline "deprecated" comments with the @remarks framing. The precision issue is worth addressing since this PR is already touching numeric semantics in that file.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Aligns the SDK’s documentation, type guidance, and test fixtures with EigenLayer-AVS PR #509, which changed ERC-20 Transfer event trigger output semantics so data.value is always raw base units and data.valueFormatted is the human-readable decimal string.

Changes:

  • Updated trigger/template tests and fixtures to expect value as raw uint256 base-units string and to include/validate valueFormatted.
  • Clarified MethodCallType.applyToFields JSDoc/README guidance to discourage ["Transfer.value"] usage for ERC-20 Transfer events (shared enrichment now provides valueFormatted).
  • Added a changeset entry documenting the semantics/migration guidance.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/triggers/eventTrigger.test.ts Removes Transfer decimals methodCalls and adds semantic assertions for value vs valueFormatted.
tests/templates/telegram-alert-on-transfer.test.ts Removes applyToFields: ["Transfer.value"] and updates mocked Transfer output to include valueFormatted.
tests/executions/stepInput.test.ts Prefers valueFormatted when present, preserving simulation fallback behavior.
packages/types/src/shared.ts Adds @remarks guidance clarifying applyToFields usage and Transfer-specific caveat.
packages/sdk-js/README.md Documents value vs valueFormatted semantics and migration guidance.
examples/example.ts Adds comments describing new semantics near ERC-20 calldata encoding example.
.changeset/transfer-value-semantics.md Adds release notes describing the semantics change and guidance.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/sdk-js/README.md Outdated
| `data.tokenSymbol` | `string` | Token symbol from contract metadata | Display |
| `data.tokenDecimals` | `number` | Token decimals from contract metadata | Reference |

**Breaking change in v1.x**: Prior versions populated `data.value` with the decimal-formatted amount when token metadata was available. As of EigenLayer-AVS PR #509, `data.value` is **always raw uint256 base units**, and `data.valueFormatted` is the new field for human-readable amounts. SDK consumers that read `data.value` for display **must migrate to `data.valueFormatted`**.

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README calls this a "Breaking change in v1.x", but @avaprotocol/sdk-js is currently v2.x (package.json shows 2.15.0). This version reference is inaccurate/misleading; consider changing it to "Breaking change relative to previous operator output" (or update it to the correct major) so readers don’t assume this only affects v1 consumers.

Suggested change
**Breaking change in v1.x**: Prior versions populated `data.value` with the decimal-formatted amount when token metadata was available. As of EigenLayer-AVS PR #509, `data.value` is **always raw uint256 base units**, and `data.valueFormatted` is the new field for human-readable amounts. SDK consumers that read `data.value` for display **must migrate to `data.valueFormatted`**.
**Breaking change relative to previous operator output**: Prior versions populated `data.value` with the decimal-formatted amount when token metadata was available. As of EigenLayer-AVS PR #509, `data.value` is **always raw uint256 base units**, and `data.valueFormatted` is the new field for human-readable amounts. SDK consumers that read `data.value` for display **must migrate to `data.valueFormatted`**.

Copilot uses AI. Check for mistakes.
Comment thread examples/example.ts Outdated
Comment on lines 1156 to 1157
callData:
"0xa9059cbb000000000000000000000000e0f7d11fd714674722d325cd86062a5f1882e13a{{ Number(demoTriggerName.data.value).toString(16).padStart(64, '0') }}",

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new comment states data.value is correct for ERC-20 transfer calldata, but the example still encodes it via Number(demoTriggerName.data.value). ERC-20 amounts are uint256 and can exceed JS safe-integer range, so Number(...) will silently lose precision and generate incorrect calldata. Use an integer-safe approach (e.g., BigInt-based) for hex encoding, and consider noting this constraint in the surrounding guidance.

Suggested change
callData:
"0xa9059cbb000000000000000000000000e0f7d11fd714674722d325cd86062a5f1882e13a{{ Number(demoTriggerName.data.value).toString(16).padStart(64, '0') }}",
// Encode it with an integer-safe path (`BigInt`), because ERC-20 amounts can exceed
// JavaScript's safe integer range and `Number(...)` would silently lose precision.
callData:
"0xa9059cbb000000000000000000000000e0f7d11fd714674722d325cd86062a5f1882e13a{{ BigInt(demoTriggerName.data.value).toString(16).padStart(64, '0') }}",

Copilot uses AI. Check for mistakes.
Copilot + Claude review on PR #207:

1. tests/executions/stepInput.test.ts — replace the if/else fallback
   that violated the test determinism rule (CLAUDE.md). The CustomCode
   snippet now reads eventData.valueFormatted directly. Per
   EigenLayer-AVS PR #509 the operator always publishes valueFormatted
   for ERC-20 Transfer events when token decimals are known, so the
   fallback was dead code in practice and would have hidden a future
   regression that drops the field. Bonus: this also removes the
   parseInt(rawValue, 16) precision-loss path Claude flagged.

2. examples/example.ts — switch the ContractWrite calldata template
   from Number(...) to BigInt(...) for hex encoding the transfer
   amount. ERC-20 amounts can exceed JS safe-integer range
   (~9e15), and Number(...) silently truncates, producing incorrect
   calldata for any token amount above that. Add a comment
   explaining the constraint.

3. packages/sdk-js/README.md — replace "Breaking change in v1.x"
   (incorrect — SDK is currently v2.x) with version-agnostic wording
   "Breaking change in operator output (EigenLayer-AVS PR #509)".

4. tests/triggers/eventTrigger.test.ts and
   tests/templates/telegram-alert-on-transfer.test.ts — soften
   inline "is deprecated" comments to "is no longer needed for
   Transfer events". This aligns the inline guidance with the
   @remarks (not @deprecated) framing committed in 668002b, and
   explicitly notes the field is still valid for non-Transfer cases.

Skipped: Claude's suggestion of a typed TransferEventData interface
is a natural follow-up but out of scope for this docs/test PR. The
minor-vs-major changeset semver debate is deferred to the release
lead — the changeset body already calls out the consumer-visible
behavior change prominently.
Patch bump for @avaprotocol/sdk-js covering the four review-fix
changes in commit 8a29747:

1. examples/example.ts BigInt fix (real precision-loss bug for ERC-20
   amounts > ~9e15 base units)
2. stepInput.test.ts determinism fix
3. README version-string fix (v1.x → operator-output framing)
4. Test comment language alignment with the @remarks decision

@avaprotocol/types is not bumped since this commit doesn't touch
the types package.
…511 fix

Three loopNode tests were silently passing because the operator returned
`success: false` whenever any iteration errored, which caused the tests'
`if (result.success && result.data)` blocks to be skipped entirely. The
broken assertions inside were never reached.

After EigenLayer-AVS issue #511 was fixed, the loop runner correctly
reports `success: true` with the per-iteration results array preserved
(including null entries for failed iterations). This exposed three
pre-existing test bugs that now fail loudly:

1. "should support sequential execution mode with timing verification"
   and "should default to sequential execution mode when not specified"
   - Used the wrong response shape: `result.data.data` instead of
     `result.data` directly. The same file documents the correct
     contract on line ~2000: "runNodeWithInputs returns data directly
     as array, not wrapped in RunNodeResponseData".
   - Used `item` and `index` in CustomCode source instead of `value`
     (matching iterVal: "value"). `item` was always undefined in goja,
     so each iteration silently produced `{item: undefined, ...}` which
     pre-fix was discarded entirely (leading to `success: false`).
   - Fix: read `result.data` directly as the iteration array; use the
     correct variable names matching the loop config; remove the
     synthetic `await new Promise(setTimeout(...))` line which goja
     does not reliably support.

3. "should handle invalid runner configuration gracefully"
   - Expected `success: false` for an "invalid runner configuration",
     but the runner config (CustomCode that throws) is structurally
     valid — the inner source just throws on every iteration.
   - With #511 fix, the operator now correctly reports the loop as
     having run to completion with all-null results. The semantic
     question of whether all-iterations-failed should be `success: false`
     or `success: true` with an all-null array is open and tracked in
     a follow-up on EigenLayer-AVS.
   - Fix: accept either outcome — the test verifies that the failure
     is observable either via the top-level success flag OR via all-null
     entries in the data array.

No SDK source changes. Pure test cleanup.
The withdraw tests perform real Sepolia UserOp round-trips (bundler →
EntryPoint → on-chain confirmation) and use TimeoutPresets.SLOW
(3 minutes) on the gRPC client. The default TIMEOUT_DURATION (60s)
races the gRPC timeout and aborts the test before the chain call can
complete, even when the underlying operation succeeds.

Bump to 4 minutes to give the gRPC SLOW preset its full budget plus
some headroom. This is structural mismatch in the test file, not
related to any specific bug — the file's own comment "60 seconds to
reduce flaky timeouts" was actively producing the flakes it tried to
prevent.
Releases:
  @avaprotocol/sdk-js@2.16.0
  @avaprotocol/types@2.12.0

[skip ci]
@chrisli30 chrisli30 merged commit cb81c6e into main Apr 9, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants