Skip to content

feat(near): add a parser_cli token-metadata flag for NEAR - #437

Open
shahan-khatchadourian-anchorage wants to merge 3 commits into
shahankhatchadourian/near-d-token-metadatafrom
shahankhatchadourian/near-e-cli-token-metadata
Open

feat(near): add a parser_cli token-metadata flag for NEAR#437
shahan-khatchadourian-anchorage wants to merge 3 commits into
shahankhatchadourian/near-d-token-metadatafrom
shahankhatchadourian/near-e-cli-token-metadata

Conversation

@shahan-khatchadourian-anchorage

Copy link
Copy Markdown
Contributor

Adds the NEAR token-metadata flag parser_cli was missing, closing the CLI
parity gap with Ethereum (--abi-json-mappings) and Solana
(--idl-json-mappings).

Stacked on #432 (near-d-token-metadata), which adds the
ChainMetadata.near token-metadata plumbing this flag feeds.

The gap

NearPlugin::create_metadata accepts only --network. An asset outside the
compiled-in seed table renders its raw base-unit amount tagged
unresolved <asset id>, with no way to supply a symbol and decimals locally:

└─ Amount: 250000000 (unresolved nep141:my-token.near)

The flag

echo '{"symbol":"MYTOKEN","decimals":8}' > mytoken.json

parser_cli decode --chain near --output human \
  --near-token-metadata-mappings 'MyToken@mytoken.json@nep141:my-token.near' \
  -t '{"signer_id":"alice.near", ...}'
└─ Amount: 2.5 MYTOKEN

Two things differ from the sibling flags:

@ separates the fields, not :. NEAR Intents asset ids embed their own
colons (nep141:wrap.near), so the shared colon-delimited
mapping_parser::parse_mapping would truncate the id at its first embedded
colon. parse_near_mapping splits on @ into exactly three parts and takes
the asset id verbatim. A regression test pins this.

Each entry is signed with the CLI dev key. The NEAR plugin installs the
strict RequireAllowlistedSigner posture, so an unsigned entry is dropped —
signing is what makes the flag do anything at all. This follows Ethereum's
fuller template (mappings + signing) rather than Solana's unsigned-only one.
Three pieces make that work:

  • sign_token_metadata_for_cli is decoupled from the dev-signing cargo
    feature the same way sign_abi_for_cli is, so cli_plugin still compiles
    in a cli-plugin-without-dev-signing build (verified explicitly).
  • authorized_token_metadata_signers enrolls that dev key under
    dev-signing/cfg(test), matching visualsign-ethereum's
    authorized_abi_signers. Without it the CLI would sign entries its own
    decode path then rejects as an untrusted signer.
  • parser_cli's near feature enables visualsign-near/dev-signing, as its
    ethereum feature already does for visualsign-ethereum.

parser_app enables neither dev-signing nor diagnostics, so the enclave
binary carries no key material and no allowlist entry trusting the dev key.
make build splits parser_cli out of the workspace build to keep Cargo
feature unification from crossing that line, and the release image builds from
parser/app alone; this PR extends the Makefile comment enumerating those
features, which no longer listed all of them.

CLI-signed entries are always NEAR-origin (origin_chain unset).
Ethereum/Solana-origin CLI signing is not wired up.

Composition with --network

The flag composes with --network rather than replacing it. An invalid
network still errors before any mapping file is read, so a bad --network
can't be masked by a successful mapping load, and metadata is emitted when
either input yields something. None is returned only when neither does.

Coverage

  • 11 plugin-level cases: composition with --network in both directions, the
    colon-in-asset-id regression, duplicate asset ids, and partial failure
    (a malformed mapping and a missing file alongside a good one).
  • One end-to-end case proving a CLI-signed entry resolves through the posture
    register installs. This is the gate that fails if the dev key leaves the
    allowlist or the signing domain tag drifts — assertions on
    signature.is_some() and on unsigned-entry refusal both pass in that case.
  • Two parser_cli tests drive the real binary, so clap exposure and the
    dev-signing feature wiring are covered, not just direct NearArgs
    construction.
  • Docs: the new flag in docs/parser-cli.mdx (whose --chain row also did
    not list near) and a worked example in docs/chains/near.mdx, both run
    against the built binary before being written down.

make lint and make test are green across the workspace.

🤖 Generated with Claude Code

@shahan-khatchadourian-anchorage
shahan-khatchadourian-anchorage force-pushed the shahankhatchadourian/near-d-token-metadata branch from b11d4f0 to 85272a0 Compare August 5, 2026 00:43
@shahan-khatchadourian-anchorage
shahan-khatchadourian-anchorage force-pushed the shahankhatchadourian/near-e-cli-token-metadata branch from b9b8c2d to 40c3f20 Compare August 5, 2026 00:46
@shahan-khatchadourian-anchorage
shahan-khatchadourian-anchorage force-pushed the shahankhatchadourian/near-d-token-metadata branch from 85272a0 to 88874d6 Compare August 5, 2026 02:26
@shahan-khatchadourian-anchorage
shahan-khatchadourian-anchorage force-pushed the shahankhatchadourian/near-e-cli-token-metadata branch from 40c3f20 to 485a961 Compare August 5, 2026 02:26

@pepe-anchor pepe-anchor 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.

Peer Review Summary — NEAR stack #437 (COMMENT)\n\n5 findings: 5 LOW. Detailed inline comments below.\n\nKey concern: @ in file paths breaks mapping parsing. Dev-signing edge cases.


AI Review on behalf of @pepe-anchor. Please flag any inaccuracies.

return Err(format!(
"Invalid mapping format (expected Name@FilePath@AssetId): {mapping_str}"
));
};

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.

[LOW] @ in file paths breaks Name@FilePath@AssetId parsing because splitn(3, '@') consumes the path component greedily, and the error message doesn't surface which component is the problem

parse_near_mapping uses splitn(3, '@') which splits on the first two @ characters regardless of position. A file path containing @ (valid on Linux/macOS) causes mis-parsing: /tmp/my@dir/token.json yields name='/tmp/my', path='dir/token.json', asset_id='<rest>' instead of a clear error. The format is documented as Name@FilePath@AssetId and the help text expects @/path/to/file.json@AssetId (implying absolute paths with a leading @ that disambiguates), but a relative path with an embedded @ may still confuse users — consider adding a validation step that rejects a path component containing @ and points to the third @ as the intended separator.


Authored by Claude on behalf of @pepe-anchor. Please flag any inaccuracies, I'm not always right.

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 in f21237c -- and the mis-parse was silent, which made it worse than a bad error message.

parse_near_mapping now splits on every @ and requires exactly three components. A NEAR Intents asset id never contains @, so a fourth component can only mean the path did, and the error says so: it names the component count and states that a path containing @ can't be used in this format.

Before this, MyToken@/tmp/my@dir/token.json@nep141:wrap.near parsed as path /tmp/my and asset id dir/token.json@nep141:wrap.near with no complaint at all -- it would then fail as a missing file, pointing at the wrong thing. Test parse_near_mapping_rejects_an_at_sign_in_the_path pins that exact string.

On the leading-@ reading of the help text: the @ there is the separator, not part of the path, so absolute and relative paths are both fine -- it's only an embedded @ that's unrepresentable.

asset_id,
value,
&DEV_NEAR_SIGNING_KEY_SEED,
visualsign::signing::near_token_metadata_prehash,

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.

[LOW] DEV_NEAR_SIGNING_KEY_SEED is a deterministic all-0x51 seed — consistent with DEV_ETHEREUM_SIGNING_KEY_SEED (0x52) and DEV_SOLANA_SIGNING_KEY_SEED (0x53) but worth re-confirming these are intentionally non-random for dev-only use and would never reach a production enclave binary

The seed [0x51u8; 32] is hardcoded, gated behind #[cfg(any(test, feature = "dev-signing"))], and allowlisted only under the same cfg. The split-build Makefile strategy prevents unification, so parser_app never links the key. However, if anyone changes the build to a single cargo build --workspace invocation (e.g. for a CI optimization), the key and its allowlist entry would silently leak into the production enclave — there's no compile-time assertion or build-script check that dev-signing is absent from the parser_app/grpc-server dependency graph. Consider adding a compile-time guard (e.g. a compile_error! in a non-dev-signing parser_app module that depends on visualsign-near but is #[cfg(not(dev_signing))]) or a CI-only check in narrow-build-check.


Authored by Claude on behalf of @pepe-anchor. Please flag any inaccuracies, I'm not always right.

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.

Confirmed non-random and dev-only, and you're right that nothing enforced it -- added a CI gate in f21237c.

narrow-build-check now runs dev-signing-absent-check, which fails if any --workspace recipe in src/Makefile lacks --exclude parser_cli. That's the actual invariant: Cargo unifies features within a single invocation, so the only thing keeping dev-signing off parser_app/grpc-server is that build compiles parser_cli separately. A future cargo build --workspace covering both now fails the build rather than quietly linking a dev key into an attested binary.

Worth recording why it isn't the graph probe you suggested, since I tried that first: cargo tree -p parser_app -e features does not print enabled features that carry no dependency edges, and dev-signing = [] is exactly that. The probe matched nothing even against parser_cli, which does enable it -- so it would have passed unconditionally and looked like coverage. I verified the replacement fires by injecting a violating recipe.

It also covers visualsign-ethereum/dev-signing, which has the same exposure and predates NEAR.

}
};
if NearNetwork::from_network_id(&network).is_none() {
return Err(format!(

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.

[LOW] create_chain_metadata returns Ok(None) when all mappings fail to load, but test_cli_near_token_metadata_invalid_file_still_parses only covers the file-not-found case — add a test for a file that exists but contains invalid JSON (e.g. {) to ensure the serde_json parse failure path in load_json_file is exercised

load_json_file returns Err(...) when serde_json::from_str::<Value> fails (invalid JSON). The CLI test test_cli_near_token_metadata_invalid_file_still_parses uses /nonexistent/token.json which exercises the File::open error path but not the serde_json parse error path. A separate test with a real but syntactically invalid JSON file would close this coverage gap — important because the error message shape (Invalid JSON in file {path}: ...) is user-visible and worth snapshotting.


Authored by Claude on behalf of @pepe-anchor. Please flag any inaccuracies, I'm not always right.

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.

Agreed, added in f21237c. build_token_mappings_skips_a_file_whose_json_is_invalid writes a real file containing {, so File::open succeeds and the failure comes from serde_json in load_json_file -- the branch the existing /nonexistent/token.json test can't reach.

Asserts both halves of the outcome: the returned map is empty and valid_count is 0, so a future change that logged the error but still registered the entry would fail here.

"near",
"--output",
"json",
"--near-token-metadata-mappings",

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.

[LOW] CLI integration tests exercise the happy path and file-not-found error, but not the dev-signing-disabled error path — add a test with an intentionally disabled feature to catch the regression where sign_token_metadata_for_cli returns Err and the entry loads unsigned then gets silently dropped

The two new CLI tests (test_cli_near_token_metadata_mappings and test_cli_near_token_metadata_invalid_file_still_parses) cover happy-path resolution and file-not-found. But neither exercises what happens when dev-signing is absent: sign_token_metadata_for_cli returns Err(...), build_token_mappings_from_files inserts the entry unsigned, and RequireAllowlistedSigner drops it. A #[cfg(not(feature = "dev-signing"))] test (or a dedicated test binary without the feature) that asserts the entry is dropped (symbol stays unresolved) would prevent a future regression where the feature gate is accidentally removed or broken.


Authored by Claude on behalf of @pepe-anchor. Please flag any inaccuracies, I'm not always right.


Authored by Claude on behalf of @pepe-anchor. Please flag any inaccuracies, I'm not always right.

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.

Added in 2834460 -- and my first read of this was wrong, so worth spelling out why it is testable.

You're right that #[cfg(not(feature = "dev-signing"))] doesn't work from the crate's own test module: the gate is cfg(any(test, feature = "dev-signing")), so under cargo test the signing variant is always linked and its Err arm is unreachable. But an integration test compiles the library without cfg(test), so with dev-signing off the error-returning twin at token_signature.rs:646 is what links -- no separate test binary needed.

tests/unsigned_without_dev_signing.rs drives the real public path (NearPlugin::create_metadata with a mapping file) and asserts the entry registers with signature: None and its value carried verbatim -- i.e. unsigned rather than dropped, which is the half nothing else covered. I checked it isn't vacuous two ways: it reports 1 passed rather than being silently cfg-ed out, and with the gate temporarily removed and --features dev-signing on it fails against a real ed25519 signature.

make test already runs it in the right configuration -- the workspace pass excludes parser_cli, the only crate that enables dev-signing, so unification can't switch it back on.

Scoped deliberately to the unsigned-registration half. Asserting the subsequent drop would need #439's TokenMetadataExtraction API, which doesn't exist on this PR -- and that behaviour is already covered by token_signature.rs's tests, which don't depend on this feature.

parser_cli has no way to supply NEAR token metadata, unlike Ethereum
(--abi-json-mappings, signed with the CLI dev key) or Solana
(--idl-json-mappings, unsigned only). Assets outside the compiled-in seed
table therefore render as raw base units tagged `unresolved <asset id>`
with no local override available.

- --near-token-metadata-mappings takes `Name@/path/to/token.json@AssetId`.
  `@` is the field separator, not `:` (the convention the other two mapping
  flags use), because NEAR Intents asset ids embed their own colons
  (nep141:wrap.near), which would make the identifier ambiguous under a
  colon-delimited format.
- Each loaded entry is signed with the CLI's local dev key (NEAR-origin
  ed25519). The CLI installs the strict RequireAllowlistedSigner posture, so
  an unsigned entry is dropped; signing is what makes the flag do anything.
  This follows Ethereum's fuller template rather than Solana's unsigned-only
  one.
- authorized_token_metadata_signers enrolls that dev key under
  `dev-signing`/`cfg(test)`, matching visualsign-ethereum's
  authorized_abi_signers. parser_cli's `near` feature enables
  visualsign-near/dev-signing, as its `ethereum` feature already does for
  visualsign-ethereum. parser_app enables neither, so the enclave binary
  still carries no key material.
- sign_token_metadata_for_cli is decoupled from the `dev-signing` feature the
  same way sign_abi_for_cli is, so cli_plugin compiles in a
  cli-plugin-without-dev-signing build.
- CLI-signed entries are always NEAR-origin (origin_chain unset);
  Ethereum/Solana-origin CLI signing is not wired up.

The flag composes with --network rather than replacing it: an invalid network
still errors before any mapping file is read, and metadata is emitted when
either input yields something.

Tests: 11 plugin-level cases (composition with --network, the colon-in-asset-id
regression, partial-failure handling) plus an end-to-end case proving a
CLI-signed entry resolves through the posture `register` installs -- the gate
that fails if the dev key leaves the allowlist or the domain tag drifts. Two
parser_cli tests drive the real binary, so clap exposure and the dev-signing
feature wiring are covered too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…enclave

parse_near_mapping used splitn(3, '@'), so a path containing '@' -- legal on
Linux/macOS -- was absorbed into the asset id: "MyToken@/tmp/a@b/t.json@ID"
parsed as path "/tmp/a" and asset id "b/t.json@ID" with no complaint. It now
splits on every '@' and reports which component is at fault, since an asset
id never contains one.

narrow-build-check gains dev-signing-absent-check. dev-signing carries
hardcoded signing-key seeds and allowlists them; only parser_cli enables it,
and the sole thing keeping it off parser_app/grpc-server is that `build`
compiles parser_cli in a separate cargo invocation. Nothing enforced that,
so a --workspace recipe covering both now fails the build. Asserted against
the recipes, not the feature graph: `cargo tree -e features` omits enabled
features carrying no dependency edges, and `dev-signing = []` is one, so a
graph probe matches nothing and passes regardless.

Adds the coverage gap for a file that exists but holds invalid JSON, which
exercises load_json_file's serde_json branch rather than File::open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sign_token_metadata_for_cli is gated on cfg(any(test, feature =
"dev-signing")), so the crate's own tests always link the signing variant
and its Err arm is unreachable there. An integration test compiles the
library without cfg(test), so with dev-signing off the error-returning twin
links instead -- the configuration a shipped binary has.

Asserts the half nothing else covered: an entry registers unsigned, value
carried verbatim, rather than being dropped when signing is unavailable.
What happens to an unsigned entry afterwards is already covered by
token_signature.rs's tests, which don't depend on this feature.

make test runs it in the right shape: the workspace pass excludes
parser_cli, the only crate enabling dev-signing, so feature unification
cannot switch it back on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shahan-khatchadourian-anchorage
shahan-khatchadourian-anchorage force-pushed the shahankhatchadourian/near-d-token-metadata branch from 88874d6 to 7883584 Compare August 6, 2026 14:12
@shahan-khatchadourian-anchorage
shahan-khatchadourian-anchorage force-pushed the shahankhatchadourian/near-e-cli-token-metadata branch from 485a961 to 2834460 Compare August 6, 2026 14:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants