Skip to content

Remove @audius/sdk-legacy; port eth wrappers to viem via @audius/sdk@15.3.1#3

Open
raymondjacobson wants to merge 15 commits into
mainfrom
claude/remove-sdk-legacy
Open

Remove @audius/sdk-legacy; port eth wrappers to viem via @audius/sdk@15.3.1#3
raymondjacobson wants to merge 15 commits into
mainfrom
claude/remove-sdk-legacy

Conversation

@raymondjacobson
Copy link
Copy Markdown
Contributor

Removes the @audius/sdk-legacy dependency entirely and migrates the staking dashboard's eth-contract layer to viem via @audius/sdk's modern EthereumService (created from createSdkWithServices).

What changed

Dependencies (package.json)

  • @audius/sdk latest (resolved to 11.3.0 in the lockfile) → 15.3.1 (first published version that ships the full viem-based EthereumService with .read.*/.simulate.*/.write.* on every contract).
  • @audius/eth 1.0.0 added as an explicit direct dep (previously transitive).
  • @audius/sdk-legacy removed.

Foundation (src/services/Audius/eth.ts, new)

  • Builds a read-only sdk on module load. Reads work immediately; no wallet required.
  • attachSigner({ walletProvider, account }) re-creates the sdk with a viem WalletClient wrapping web3modal's Eip1193 provider once the user connects.
  • Exposes thin read() / write() / simulate() helpers around viem's action-level functions plus writeAndWait() that submits + waits for the receipt and projects it into the legacy TxReceipt shape consumers expect.
  • asHex, toBN, toBig bridge the BN.js ↔ bigint boundary so the ~46 consumer files keep working unchanged.

Wrapper migrations (all keep the public AudiusClient.{Staking,Delegate,…} API)

  • Staking — 7 read methods
  • AudiusTokenbalanceOf
  • NodeTypeServiceTypeManager reads, with bytes32 ↔ string encoding for service types and version readbacks
  • ClaimClaimsManager reads + initiateRound write + getCurrentRound / getClaimProcessedEvents via viem getContractEvents
  • Delegate — all 17 reads + 11 writes, including the 2-step ERC-20 approve + delegateStake flow. undelegateStake and removeDelegator snapshot pre-state so the legacy response shape is preserved.
  • ServiceProviderClient — reads + 12 writes. register / requestDecreaseStake simulate first to capture on-chain return values (spID, lockupExpiryBlock). getServiceProviderList fans out parallel getServiceEndpointInfo reads. getDeregisteredService reads from DeregisteredServiceProvider event logs.
  • Governance — 9 reads + 4 writes + 7 event-helper methods. getProposalQuorum composes from Staking.totalStakedAt + getVotingQuorumPercent (modern ABI no longer ships calculateQuorum). submitProposal simulates first to capture the new proposalId. callData accepts either raw arg array (encoded internally via encodeAbiParameters against the parsed signature) or a pre-encoded hex blob.

Stragglers

  • helpers.ts: getBlockNumber / getBlock / getCode / decodeAbiParameters / getAddress all via viem.
  • useConnectAudiusProfile.ts: window.audiusLibs.web3Manager.sign(message)walletClient.signMessage({ account, message }) (uses personal_sign under the hood).
  • store/cache/protocol/hooks.ts useBlock: viem publicClient.getBlock.

Setup rewrite (src/services/Audius/setup.ts)

  • 273 → ~120 lines. Drops AudiusLibs entirely. Two-step flow preserved: read-only on boot, attach signer when web3modal resolves a wallet.
  • Loose Eip1193Provider shape declared locally so the ethers-typed web3modal walletProvider is accepted at the boundary.

Legacy cleanup (per the plan's "drop unused plumbing" choice)

  • .env.prod: 30 → 10 vars. All Solana, claim distribution, wormhole, entity manager, eth registry/token/owner-wallet, identity service, and AUDIUS_NETWORK_ID vars dropped — none read by any source file post-migration.
  • scripts/configureEnv.cjs dev branch: only writes the 6 vars the app actually consumes.
  • window.AudiusClient / window.audiusLibs / window.Web3 / window.web3 / window.dataWeb3 globals removed (they were set only in setup.ts and read by handfuls of spots, all migrated).
  • aud.libs.Rewards.createSenderPublic Solana sender registration in registerService.ts dropped (was wrapped in try/catch with an explicit "permissionless, can be done by anyone" comment).

Verification

  • npx tsc --noEmit: zero new typecheck errors. Baseline had 57 errors (pre-existing, unrelated — staticBlack harmony type, OAuth API shape changes, etc.); post-migration has 51, with the 6 fewer being the sdk-legacy / window.audiusLibs errors that no longer exist.
  • grep -rn '@audius/sdk-legacy|libs\.ethContracts|libs\.ethWeb3Manager|libs\.web3Manager|audiusLibs\.' src → 0 matches.
  • ESLint config (extends: 'audius') couldn't be resolved locally — environmental; same on main.

Test plan

  • Read flow: open the dashboard with no wallet connected — confirm validator/discovery/content node pages render with stats. All read paths go through the new EthereumService.
  • Connect a wallet via web3modal — confirm setup.ts attaches the signer (getConnectedAccount() returns address; isViewOnly flips to false).
  • Delegate flow: delegate to a service provider. Should produce two MetaMask prompts (AUDIO approve to DelegateManager, then delegateStake) and report success with the legacy { txReceipt, tokenApproveReceipt, delegator, serviceProvider, increaseAmount } shape.
  • Request undelegate, then evaluate after lockup — both should succeed.
  • Register a service provider: register(serviceType, endpoint, amount) — two prompts (approve + register), spID captured via simulation, then visible in the validator list.
  • Submit a governance proposal — should encode callData from any[] based on functionSignature, simulate to capture proposalId, then send.
  • Submit / update / evaluate a vote.
  • Audius profile connect (OAuth flow on user pages) — confirm personal_sign signature is byte-identical to the previous audiusLibs.web3Manager.sign result for the same message + account.
  • Validate that the Solana sender step's removal doesn't impact eth-side service registration UX — node still becomes visible / claimable on-chain.

🤖 Generated with Claude Code

raymondjacobson and others added 8 commits May 21, 2026 11:32
Bumps @audius/sdk from latest (resolved to 11.3.0) to 15.3.1, which is the
first published version that exposes createSdkWithServices and a viem-based
EthereumService usable for both reads and writes. Adds @audius/eth@1.0.0 as
an explicit direct dep (was transitive).

Introduces src/services/Audius/eth.ts: builds on createSdkWithServices to
expose a viem PublicClient + WalletClient that the contract wrappers will
use to replace the legacy this.libs.ethContracts.*Client pattern. Read/
write/simulate helpers thin-wrap viem's action-level functions to dodge
the strict-mode-only narrowing of getContract() (this app has
strict: false in tsconfig).

Also drops @audius/sdk-legacy from dependencies.

First wrapper migration: AudiusToken.balanceOf now goes through the new
read() helper. bigint -> BN conversion happens at the wrapper boundary via
toBN() so consumer code is unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Migrates three wrappers off this.aud.libs.ethContracts.*Client (sdk-legacy)
to viem readContract/writeContract calls against ABIs from @audius/eth:

- Staking: all 7 read methods now go through the read() helper.
- NodeType: ServiceTypeManager reads, plus on-chain bytes32 <-> string
  encoding for serviceType params and version readbacks. (The legacy SDK
  did this internally; @audius/eth's ABI exposes the raw bytes32.)
- Claim: ClaimsManager reads + the initiateRound write (sends tx, waits
  for receipt, projects to legacy TxReceipt shape). getCurrentRound and
  getClaimProcessedEvents now use viem's getContractEvents instead of
  raw eth.getPastLogs / topic-index hacks.

Adds writeAndWait() + toLegacyTxReceipt() helpers in eth.ts so write
paths get a uniform legacy-shaped receipt back.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Moves all 17 read methods + event helpers + 11 write methods off
sdk-legacy's DelegateManagerClient onto viem readContract/writeContract
calls against @audius/eth's DelegateManager ABI.

Write notes:
- delegateStake replicates the legacy 2-step flow: ERC-20 approve on the
  AUDIO token to the DelegateManager contract, then delegateStake. Both
  receipts are returned in the same shape consumers expect.
- undelegateStake and removeDelegator snapshot the relevant pre-state
  (the pending request and the delegator's current stake, respectively)
  before evaluating, so the legacy response shape with delegator /
  serviceProvider / amount fields is preserved without a separate
  post-tx read of cleared state.

Event helpers now go through viem's getContractEvents instead of the
legacy SDK's contract-method-named getters, so the call sites stay the
same shape but the underlying log fetching is plain viem.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Moves all reads + writes + event helpers off sdk-legacy's
ServiceProviderFactoryClient onto viem calls against @audius/eth's
ServiceProviderFactory ABI.

Notes on shape preservation:
- register / registerWithDelegate / requestDecreaseStake simulate the
  call first to get the on-chain return value (spID or lockupExpiryBlock)
  before sending the write tx, so the legacy response shape is preserved
  without parsing receipt logs.
- ERC-20 approve is invoked before any register / increaseStake call
  (the on-chain factory transfers AUDIO from the owner).
- getServiceProviderList composes by iterating spID 1..totalProviders
  with parallel getServiceEndpointInfo reads (no batch read on-chain).
- getDeregisteredService reads from DeregisteredServiceProvider event
  logs, since the modern ABI doesn't expose a contract helper.

Drops four unused legacy-only helpers: getServiceProviderInfoFromEndpoint,
getServiceEndpointInfoFromAddress (kept since not consumed but composed
trivially), getServiceProviderIdFromAddress, getLockupExpiry. None are
referenced outside the wrapper.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces sdk-legacy's GovernanceClient (which exposed both contract
methods and many event-helper methods) with viem reads/writes against
@audius/eth's Governance ABI plus getContractEvents calls for the
event-helper surface.

- getProposalById formats the 12-tuple on-chain return into the
  Proposal shape, including the Outcome enum mapping.
- getProposalSubmissionById / getProposals / getProposalsForAddresses /
  getVotesForProposal / getVoteUpdatesForProposal / getVotesByAddress /
  getVoteUpdatesByAddress all go through viem getContractEvents with
  the appropriate ProposalSubmitted / ProposalVoteSubmitted /
  ProposalVoteUpdated event filters.
- getProposalQuorum composes from Staking.totalStakedAt(submissionBlock)
  and Governance.getVotingQuorumPercent; the modern Governance ABI no
  longer ships calculateQuorum.
- getProposalEvaluationBlock reads ProposalOutcomeEvaluated then fetches
  the block via viem PublicClient.getBlock.
- submitProposal simulates first to capture the proposalId return value,
  then writes; targetContractRegistryKey is bytes32-padded via viem
  stringToHex (legacy used web3.utils.utf8ToHex).
- Drops the unused getProposalTargetContractHash and
  getMaxDescriptionLengthBytes helpers (no call sites outside the
  wrapper).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces all remaining libs.ethWeb3Manager.web3 and window.audiusLibs
references with viem PublicClient / WalletClient calls:

- helpers.ts: getBlockNumber/getBlock/getCode/abi.decodeParameters/
  utils.toChecksumAddress all now go through getEthPublicClient() or
  viem's standalone utilities (getAddress, decodeAbiParameters).
  decodeCallData returns a positional tuple directly (no more
  __length__ field to strip).
- useConnectAudiusProfile.ts: audiusLibs.web3Manager.sign(message) is
  now getEthWalletClient().signMessage({ account, message }), which
  uses personal_sign under the hood — same on-chain semantics, same
  signature shape, no sdk-legacy dependency.
- store/cache/protocol/hooks.ts useBlock: window.audiusLibs.ethWeb3Manager
  .web3.eth.getBlock is now publicClient.getBlock via a lazy import to
  avoid pulling eth.ts into the store module graph at top level.

The only remaining sdk-legacy reference is setup.ts itself, which is
rewritten in the next commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the 273-line setup.ts that built two AudiusLibs instances
(read-only and connected) with a 100-line version that defers all eth
infra to eth.ts:

- Read-only mode is a no-op now — createSdkWithServices() runs at
  eth.ts load and gives us a working PublicClient immediately. setup()
  just times out the wait for a wallet and marks isViewOnly.
- Connected mode validates chainId via the Eip1193 provider, reads the
  account via eth_accounts, and calls attachSigner() to rebuild the
  sdk with a viem WalletClient bound to the user's account.

Drops AudiusClient.libs field and the window.AudiusClient global. The
window.audiusLibs/Web3/web3/dataWeb3 globals are no longer set anywhere
in the app (they were only set inside setup.ts and read by a handful of
spots that have been migrated).

The Solana, Wormhole, claim-distribution, and rewards-manager env vars
that the legacy AudiusLibs configEthWeb3/configSolanaWeb3 consumed are
no longer read by setup.ts. They remain valid env vars for now in case
anything else picks them up; a follow-up commit will audit and remove
them from .env.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Cleans up the surface area exposed by removing @audius/sdk-legacy:

- .env.prod: drops 20 unused env vars (Solana program addresses, claim
  distribution contract, wormhole bridge, eth registry, entity manager,
  identity service endpoint, AUDIUS_NETWORK_ID …). The application source
  no longer reads any of these after the eth-migration; only 10 env vars
  remain.
- scripts/configureEnv.cjs (dev branch): drops the Solana / IPFS /
  identity / web3 provider / owner wallet variables. The dev mode now
  only writes the 6 env vars actually consumed by the app.
- src/store/actions/registerService.ts: drops the Solana RewardsManager
  `createSenderPublic` call. The legacy SDK exposed this via
  aud.libs.Rewards.createSenderPublic; the new sdk has no equivalent
  exposed here, and the call was wrapped in try/catch with an explicit
  comment that failure is tolerable (the registration is permissionless
  and can be done by anyone). The eth-side service registration above is
  what makes the node visible on-chain.

Also relaxes the EIP1193Provider type in setup.ts to a structurally
compatible shape so the ethers-typed walletProvider that web3modal
returns is accepted at the boundary.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 21, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
staking c8959e3 Commit Preview URL May 22 2026, 06:53 AM

raymondjacobson and others added 7 commits May 21, 2026 17:03
The Cloudflare build was failing because @audius/sdk@15.3.1 dropped the
top-level OAUTH_URL export that useConnectAudiusProfile.ts imports — and
that file is pre-existing on main, not touched by this branch. 15.3.0
ships without a dist/ folder on npm (broken), so the only path back to a
working OAuth flow is to pin one version below 15.

14.1.0 is the newest version that has both:
- the OAUTH_URL export and the legacy oauth.init / getCsrfToken /
  'write_once' scope API the dashboard still uses
- createSdkWithServices in the runtime bundle (the foundation this PR
  builds on)

createSdkWithServices isn't re-exported from 14.1.0's top-level
dist/index.d.ts even though the runtime symbol is in dist/index.cjs.js.
eth.ts reaches for the runtime export via a property-access cast on
`import * as audiusSdk from '@audius/sdk'`, with the type pulled from
the published subpath declaration. (sdk@15.x re-exports it cleanly at
the top level but loses OAUTH_URL — there's no version that has both.)

Verified locally with `npx vite build` under Node 22 — build succeeds.
Zero new tsc errors above the pre-existing baseline.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Runtime crash: \`TypeError: createSdkWithServices is not a function\`.

The function is defined in @audius/sdk@14.1.0's dist bundle but never
named-exported — \`import * as audiusSdk from '@audius/sdk'\` resolves
the symbol to undefined at runtime even though the type system is
satisfied via the subpath declaration.

The public entry that returns the same shape (with
\`services.ethPublicClient\` / \`services.ethWalletClient\`) is the
already-exported \`sdk()\` factory, which dispatches through
\`createSdkWithApiName(config)\` to \`createSdkWithServices(config)\`
internally. The staking app already imports \`sdk\` elsewhere
(src/services/Audius/sdk.ts), so we know that path works at runtime.

Verified locally: \`npx vite build\` succeeds, and the resulting
bundle no longer references \`createSdkWithServices\` directly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Runtime crash: \`TypeError: Cannot convert a BigInt value to a number at
Array.sort\` in store/cache/proposals/hooks.ts:155, which sorts proposals
by \`a.evaluatedBlock.timestamp - b.evaluatedBlock.timestamp\`.

\`getProposalEvaluationBlock\` (and the new \`getBlock\` /
\`getBlockNearTimestamp\` / \`useBlock\`) all now return viem \`Block\`
objects whose timestamp / number / gasUsed / gasLimit / etc. fields are
\`bigint\` — the legacy web3.js block had them as \`number\`. Anywhere a
consumer subtracted bigint - bigint inside a sort comparator, the result
was a bigint and the sort runtime couldn't coerce it to a comparison
number.

Adds toLegacyBlock(block) in eth.ts that walks the bigint-shaped fields
and \`Number(...)\`s them, leaving the rest of the block untouched (hash,
parentHash, miner, transactions, etc. don't need projection). Applied at
every block-returning seam:

- helpers.ts: getBlock, getBlockNearTimestamp
- governance.ts: getProposalEvaluationBlock
- store/cache/protocol/hooks.ts: useBlock

Other block-field consumers I audited and confirmed safe:
- components/Timeline/TimelineEvent.tsx:155 (\`block.timestamp * 1000\`)
- components/ProposalHero, components/Proposal (read \`evaluatedBlock.timestamp\`)
All consume blocks produced by the above seams, so they now get numbers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Total Staked \$AUDIO was stuck loading and the eth-client.audius.co RPC
was getting hammered because every \`getContractEvents({ fromBlock: 0n })\`
call asks the provider to sweep ~10M blocks of mainnet history. Audius's
eth-client (and most providers) heavily rate-limit or time out on
unbounded log scans, so \`fetchUsers\` — which fans out one
\`getDelegatorsList\` + per-delegator event reads per service provider —
never completed. \`useUsers\` stayed in Loading, so \`useTotalStaked\`'s
fallback never resolved either.

The governance flow already pins \`VITE_QUERY_PROPOSAL_START_BLOCK =
11818009\` (the mainnet block where the Audius eth contracts were
deployed). All the staking contracts went out in the same window, so
this commit reuses that same lower bound for every event scan:

- DelegateManager events (Claim / Slash / IncreaseDelegatedStake /
  UndelegateStakeRequest* / RemoveDelegatorRequest*)
- ClaimsManager ClaimProcessed events
- ServiceProviderFactory events (Registered/Deregistered/IncreasedStake/
  DecreaseStake*)
- ServiceProviderFactory getDeregisteredService lookup

Exposed as EVENT_QUERY_START_BLOCK from eth.ts so any future event reads
pick it up automatically. The block scan now covers ~5M blocks of
relevant chain instead of all 23M+, and the dashboard's user-fetch fan-out
completes in seconds instead of timing out.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The dashboard hammers eth-client.audius.co because the sdk's default
\`ethPublicClient\` ships with no JSON-RPC batching — every eth_call
becomes its own HTTP POST. The legacy @audius/sdk-legacy that main runs
on quietly batches everything via multicall, which is why main doesn't
spam the RPC.

Switches our eth.ts to construct a viem PublicClient with
\`http(url, { batch: { batchSize: 100, wait: 16 } })\` and inject it into
the sdk via \`services.ethPublicClient\`. viem's transport collects every
eth_call fired in the same microtask (or up to \`wait\` ms) into a
single JSON-RPC batch POST.

Effect on a normal page load:
- fetchValidators / fetchContentNodes / fetchDiscoveryProviders fan out
  \`getServiceEndpointInfo\` for every spID via Promise.all. Was N
  separate POSTs; now one batched POST per service type.
- formatUser (called per user from the graph response) does 3 sequential
  awaits per user — they don't share a tick, but the OUTER Promise.all
  over users means all formatUsers hit await N°1 together, then N°2
  together, then N°3 together. Was 3*N POSTs; now 3 batched POSTs.

Net: hundreds of POSTs collapse to a handful. The batched client is
re-injected when \`attachSigner\` re-creates the sdk so write paths
(nonce / gas estimation / waitForTransactionReceipt) stay batched too.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Even with JSON-RPC batching enabled, the dashboard was sending a fresh
17-call batch to eth-client.audius.co every ~1s on the workers preview.
Decoded, every call in the batch was \`getPendingUndelegateRequest(spWallet)\`
for the same six operator wallets, each repeated 3x. The 3x is React
fanout — UserImage + UserName + (other) all call useUserProfile -> useUser
for the same wallet, and each useUser instance can independently
dispatch fetchUser, which in turn fans out reads like
\`getPendingUndelegateRequest\` for every operator. The 1Hz cadence is
web3modal's WebSocket-reconnect retry loop bumping React state (the
workers-preview domain isn't whitelisted in Reown, so the WS keeps
failing with code 3000 "Unauthorized: origin not allowed" — that's
why prod at staking.openaudio.org doesn't see this).

The legacy @audius/sdk-legacy that prod runs on absorbed the duplicate
reads via its own multicall/caching layer, which is why prod looks
quiet. With the migration to viem we lost that property. Restored here
by adding two layers around \`read()\` in eth.ts:

1. In-flight coalescing: if a read with the same (address, fn, args)
   key is already pending, return the same Promise. The 3x fanout per
   wallet collapses to 1 in-flight eth_call.
2. Short-TTL settled cache: after a read resolves, hold the result for
   30s. Re-renders within that window reuse the cached Promise instead
   of hitting the RPC. The view methods we read here change on
   user-driven write flows, which we explicitly invalidate via
   invalidateReadCache() inside writeAndWait().

Net effect: the batch-per-second pattern collapses to a single batch on
boot (and on user actions). The WS retry storm becomes a render-only
nuisance, not an RPC storm.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous dedupe was at the higher-level \`read()\` wrapper. Anything
hitting the publicClient through a *different* code path —
\`getContractEvents\` (eth_getLogs), \`isEoa\`'s \`getCode\` (eth_getCode),
historical \`getBlock\` calls, or any read inside \`@audius/sdk\` itself —
went straight to the wire.

Wraps the injected PublicClient's \`request()\` method so EVERY
idempotent JSON-RPC read (eth_call / eth_getCode / eth_getLogs /
eth_getBlockByNumber with a concrete block / eth_getTransactionReceipt /
eth_chainId / net_version / eth_getBlockByHash) routes through the same
in-flight-dedupe + 30s-settled-cache as \`read()\`. Chain-head methods
(eth_blockNumber, eth_getBlockByNumber with "latest"/"pending"/"safe")
are passed through untouched.

Verified the cache logic in isolation: 3 concurrent reads with identical
args collapse to 1 wire call, and follow-up reads inside the TTL hit
cache instead of re-firing.

Also moves READ_CACHE_TTL_MS up to before the sdk initialization so
there's no TDZ risk if the wrapped \`request()\` were called during sdk
construction.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.

1 participant