Skip to content

Ship typed ABIs for viem consumers - #9

Merged
Alexey1100 merged 2 commits into
mainfrom
feature/typed-abis
Sep 22, 2026
Merged

Alexey1100 merged 2 commits into
mainfrom
feature/typed-abis

Conversation

@Alexey1100

Copy link
Copy Markdown
Contributor

Why

viem and abitype read event and function names out of the ABI type. A JSON import widens every "type": "uint256" to string, which no longer satisfies Abi, and every name goes with it. A consumer that wanted the names had to keep a hand-written as const copy of the ABI plus a drift check against this package.

What

Each release ships two more modules:

Export Contents
<release>/abis Each contract ABI, typed
<release>/abis/merged The merged call surfaces, typed
import { restrictedLockupTokenAbi } from '@upsideos/evm-rwa-artifacts/v5.1/abis/merged'

// "Transfer" | "AddressFrozen" | ..., not string
type Events = ExtractAbiEventNames<typeof restrictedLockupTokenAbi>

The declaration carries the literal type. The runtime file re-exports the JSON the release already ships, so the ABI bytes appear once in the package however many modules name them: merged.js is 8 lines.

Keeping them honest

  • pack writes the modules alongside the ABIs they read, so a release cannot ship one without the other.
  • audit regenerates them and compares, so a declaration left behind by a repack fails CI. Verified by hand: editing one event name in a declaration turns the audit red.
  • A test compiles a generated declaration against abitype and asserts the names come back as literals, including a negative case for a name the contract does not have. Verified that it fails if the generator ever widens the type.

Size

The declarations add about 1 MB to the package, the largest single module being 438 KB. Typechecking a consumer that imports the merged surface takes about 1.5s.

Checks

npm test (60 tests), lint:ci, format:ci, build and audit all pass.

Made with Cursor

viem and abitype read event and function names out of the ABI type. A
JSON import widens every "type": "uint256" to string, which no longer
satisfies Abi, so a consumer lost every name and had to keep its own
hand-written copy of the ABI and a drift check against this package.

Each release now also ships a typed module per ABI set: `<release>/abis`
for the contracts and `<release>/abis/merged` for the call surfaces. The
declaration carries the literal type. The runtime file re-exports the
JSON the release already ships, so the ABI bytes stay in the package
once however many modules name them.

Packing writes the modules next to the ABIs they read, and the audit
compares them against that JSON, so a declaration cannot outlive the
contract it describes.

Co-authored-by: Cursor <cursoragent@cursor.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 92859a4. Configure here.

const releaseDir = resolveReleaseDir(entry)
if (releaseDir === undefined || entry.releaseDir === undefined) {
throw new Error(`Release ${entry.releaseId} has no packed directory`)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Audit throws without packed directory

Medium Severity

auditRelease always calls auditTypedAbis, and generateTypedAbisForRelease throws when releaseDir is missing instead of returning a finding. auditPackedRelease already reports that case, so a release without a packed directory now crashes the whole audit, including auditAll for other releases.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 92859a4. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, fixed.

auditTypedAbis now returns no findings when the release has no packed directory or no manifest. auditPackedRelease already reports both cases, so adding a second finding would only have been noise, and the throw meant one unpacked release ended the audit of every other one.

Covered by a test that builds a config with a release that has artifactsDir but no releaseDir and asserts auditRelease reports release has no packed directory rather than throwing. Removing the guard makes it fail.

auditTypedAbis generated the modules to compare them, and generating
throws when the release has nothing packed. auditPackedRelease already
reports that case, so the throw ended the audit of every other release
instead of adding anything.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Alexey1100
Alexey1100 merged commit cbbed3d into main Sep 22, 2026
6 checks passed
@Alexey1100
Alexey1100 deleted the feature/typed-abis branch September 22, 2026 16:13
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