feat(near): add baseline NEAR transaction parser - #426
Conversation
a23f854 to
f9246e6
Compare
f9246e6 to
ee2adaf
Compare
There was a problem hiding this comment.
Pull request overview
Ports a baseline NEAR chain parser into the workspace’s VisualSign conventions by adding NEAR transaction envelope decoding, per-action rendering, and a NearVisualSignConverter that assembles NEAR transactions into SignablePayload fields.
Changes:
- Added NEAR transaction decoding from hex/base64 (including
0x-prefixed hex) into aNearTransactionwrapper. - Implemented baseline NEAR action rendering (Transfer + FunctionCall with fail-closed decoding for select NEP-141 methods) and payload assembly via
NearVisualSignConverter. - Removed now-dead NEAR-specific error type and dropped the
thiserrordependency.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/chain_parsers/visualsign-near/src/tx.rs | Adds NEAR borsh decoding from hex/base64 into NearTransaction + regression tests. |
| src/chain_parsers/visualsign-near/src/actions.rs | Renders NEAR Action variants into VisualSign fields; includes selective JSON args decoding for common FT methods. |
| src/chain_parsers/visualsign-near/src/convert.rs | Converts NearTransaction into SignablePayload with network/from/to headers and per-action fields. |
| src/chain_parsers/visualsign-near/src/fmt.rs | Adds yoctoNEAR + Tgas fixed-decimal formatting helpers and tests. |
| src/chain_parsers/visualsign-near/src/networks.rs | Introduces NearNetwork enum and display names + tests. |
| src/chain_parsers/visualsign-near/src/lib.rs | Exposes NEAR modules and re-exports NearVisualSignConverter/NearTransaction; removes dead error enum. |
| src/chain_parsers/visualsign-near/Cargo.toml | Drops thiserror dependency from the NEAR chain crate. |
| src/Cargo.lock | Removes thiserror from the NEAR crate dependency list. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| impl NearVisualSignConverter { | ||
| /// Construct a converter for mainnet (the default for wallet display). | ||
| #[must_use] | ||
| pub fn new() -> Self { | ||
| Self::default() | ||
| } | ||
| } |
There was a problem hiding this comment.
Added NearVisualSignConverter::with_network(NearNetwork) so external callers can construct a testnet converter without making the field public. See 6f3ba4e.
| @@ -0,0 +1,65 @@ | |||
| //! yoctoNEAR, Tgas, base58 formatting helpers. | |||
There was a problem hiding this comment.
Fixed -- the doc comment claimed a base58 helper that doesn't exist in this file (bs58 is used later, in the intents preset's tests, not here). Updated to "yoctoNEAR, Tgas formatting helpers." See 6f3ba4e.
| @@ -0,0 +1,39 @@ | |||
| //! NearNetwork enum and system-account recognition. | |||
There was a problem hiding this comment.
Fixed -- removed the stale "system-account recognition" claim; this file only defines NearNetwork and its display names. See 6f3ba4e.
6f3ba4e to
f274f66
Compare
f274f66 to
84a822d
Compare
84a822d to
0c40eab
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/chain_parsers/visualsign-near/src/convert.rs:19
payload_typeis used elsewhere as a chain-specific tag (e.g. "EthereumTx", "SolanaTx", "TronTx"). Using the generic string "VisualSign" here makes NEAR payloads inconsistent and harder to identify downstream (CLI output, consumers that switch onpayload_type). Consider switching this constant to a NEAR-specific tag (e.g. "NearTx").
/// Payload version emitted for NEAR transactions.
const PAYLOAD_VERSION: i64 = 0;
/// Payload type tag emitted for NEAR transactions.
const PAYLOAD_TYPE: &str = "VisualSign";
src/chain_parsers/visualsign-near/src/convert.rs:62
- When a NEAR transaction contains multiple actions,
Transferrenders only an "Amount" field andFunctionCallrenders "Method"/"Deposit"/"Gas" without an action-kind field. In multi-action transactions this can produce repeated/ambiguous labels (e.g. multiple "Amount" fields) with no clear action boundary. Consider injecting an "Action" label for these variants whentx.actions().len() > 1.
for action in tx.actions() {
fields.extend(render_action(action)?);
}
prasanna-anchorage
left a comment
There was a problem hiding this comment.
Read through the baseline and exercised it locally against real borsh-encoded transactions -- a 2.5 NEAR transfer renders Network / From / To / Amount 2.5 NEAR with title Transfer, ft_transfer renders Method / Recipient / Amount / Memo / Deposit / Gas with 30 Tgas correct, a genuinely signed SignedTransaction round-trips through the signed-first path, and trailing garbage is rejected (borsh's Not all bytes read covers it, so from_string needs no explicit length check). 24 tests pass, clippy --all-targets -D warnings clean.
I also byte-decoded TRANSFER_HEX against the borsh wire format independently of near-primitives: 120 bytes, signer/pubkey/nonce/receiver/block_hash/1 action, deposit exactly 10^24 yocto, zero trailing. That was the one thing in the PR a compiler can't check, and it's correct.
Three comments below. Only the second is a design question; the others are small.
| let tx = NearTransaction::from_string(TRANSFER_HEX).expect("decode hex"); | ||
| assert_eq!(tx.inner.signer_id().as_str(), "alice.near"); | ||
| assert_eq!(tx.inner.receiver_id().as_str(), "bob.near"); | ||
| assert_eq!(tx.inner.actions().len(), 1); |
There was a problem hiding this comment.
These assertions cover signer, receiver, and the action variant, but never the deposit -- so the amount encoded in TRANSFER_HEX is untested.
That matters more here than it looks: this fixture is the only hand-encoded wire bytes in the crate (everything else builds Action values through near-primitives, which can't disagree with itself). The amount is also the single field a signer is most likely to be defrauded on. As written, the fixture's deposit bytes could be edited to any value and this test stays green.
I decoded the bytes by hand and they are right -- exactly 10^24 yoctoNEAR -- so this is just pinning down what's already correct:
let near_primitives::action::Action::Transfer(transfer) = &tx.inner.actions()[0] else {
panic!("expected Transfer");
};
assert_eq!(transfer.deposit.as_yoctonear(), 1_000_000_000_000_000_000_000_000);There was a problem hiding this comment.
Added the missing assertion — transfer.deposit.as_yoctonear() == 1_000_000_000_000_000_000_000_000, matching your independent hand-decode. TRANSFER_HEX's amount is now actually pinned, not just its variant.
| fn to_visual_sign_payload( | ||
| &self, | ||
| transaction: NearTransaction, | ||
| _options: VisualSignOptions, |
There was a problem hiding this comment.
_options is discarded, and the Network field on the next few lines comes from self.network instead -- fixed when the converter is constructed. I think that's the wrong channel, because options.metadata is exactly how the requester already supplies this everywhere else.
Compare the two existing patterns:
- Ethereum (multi-network) reads it per request:
networks::extract_chain_id_from_metadata(options.metadata.as_ref())inlib.rs:457. - Tron (single network) hardcodes
create_text_field("Network", "Tron")-- fine, because there's nothing to get wrong.
NEAR is multi-network but takes neither route. The problem is that with_network can't actually be used per request under the current architecture: TransactionConverterRegistry holds one Box<dyn VisualSignConverterAny> per chain (registry.rs:120), registered once at startup (parser/app/src/registry.rs:18-41). So whatever network the instance is built with at boot is baked in for the process lifetime, and a testnet transaction renders a confident NEAR Mainnet header with no request-level way to correct it. An incorrect network label on a signing screen is the kind of thing that reads as authoritative.
The blocker is that ChainMetadata's oneof only has Ethereum and Solana variants (generated/parser.rs:80-85), so there's no Near variant to read yet -- which is presumably why this went to constructor state. Options as I see them, roughly in order of preference:
- Add a
Near(NearMetadata { network })variant to the proto and read it fromoptions.metadatahere, matching Ethereum. Most consistent, but pulls a codegen change into A2. - Keep
with_networkas-is for now but drop theNetworkfield from the payload entirely until the metadata path exists -- omitting the field is safer than asserting a possibly-wrong one. - Land as-is with a
TODOand an explicit note in stage B that registration must not assume mainnet.
Happy with any of these, and (1) is a reasonable thing to defer to B if you'd rather not grow this PR -- but I'd like the decision recorded rather than left implicit in the constructor. What's the plan for how B wires this?
There was a problem hiding this comment.
Went with option 1: added NearMetadata { network_id } as a new variant on ChainMetadata's oneof (proto + codegen regen), and convert.rs now reads options.metadata per-request via a new extract_network_from_metadata — mirrors visualsign-ethereum's extract_chain_id_from_metadata almost line for line. Falls back to whatever network the converter was constructed with when metadata is absent or belongs to another chain.
This means stage B needs no changes: NearVisualSignConverter::new(), registered once at startup, is already the right pattern once this lands — same shape as EthereumVisualSignConverter::new(), a single per-process instance whose network is resolved per-request from metadata rather than baked in at construction. B's existing registration code was accidentally already correct.
Also refactored the one exhaustive match on the oneof (visualsign-ethereum's extract_chain_id_from_metadata) to a let-else, so it's non-exhaustive by construction — adding Near didn't require touching that crate, and neither will any future chain variant.
The REST gateway's ChainMetadataInput JSON discriminator doesn't need adding here either — stage D already has a CHAIN_NEAR variant wired to NearMetadata, written ahead of this proto change landing. It'll just compile once D rebases onto this.
| create_address_field("To", tx.receiver_id().as_str(), None, None, None, None)? | ||
| .signable_payload_field, | ||
| ); | ||
| let total_actions = tx.actions().len(); |
There was a problem hiding this comment.
A transaction with zero actions produces a complete, valid-looking payload: Network / From / To, titled NEAR Transaction (via the _ => arm in title_for), and no indication that nothing happens. I confirmed this renders rather than erroring.
An empty actions vec is well-formed borsh but not a meaningful thing to sign, and the rendered result is indistinguishable at a glance from a transaction whose actions simply weren't decoded -- which is the more dangerous reading, since the fallback arm in render_action deliberately renders unknown variants as a bare Action label. A signer can't tell "this does nothing" from "we didn't understand this."
Suggest rejecting it outright:
if tx.actions().is_empty() {
return Err(VisualSignError::…("NEAR transaction has no actions".into()));
}If there's a reason to keep rendering it, an explicit No actions field would at least make the emptiness visible.
There was a problem hiding this comment.
Fixed — to_visual_sign_payload now rejects a NEAR transaction with zero actions (VisualSignError::ValidationError) instead of rendering a normal-looking payload. Added a test (rejects_transaction_with_no_actions) covering it.
1c2f96e to
88ce10b
Compare
Ports the baseline chain-parsing code: borsh Transaction/SignedTransaction decode via near-primitives (tx.rs), per-action rendering plus a fail-closed args decoder for ft_transfer/ft_transfer_call/ft_withdraw (actions.rs), the NearVisualSignConverter (convert.rs), and yoctoNEAR/Tgas formatting (fmt.rs, networks.rs). Two deliberate deviations from a verbatim port: - tx.rs decodes hex/base64 input via the shared visualsign::encodings::SupportedEncodings::detect + decode_hex convention (matching visualsign-solana) instead of hand-rolled 0x-prefix stripping. - convert.rs omits near-intents-feature-gated execute_intents decoding; that depends on presets/intents/, which lands in a later PR, compiled unconditionally rather than behind a feature flag. A1's placeholder NearParserError is removed as dead code now that this PR uses visualsign::vsptrait::TransactionParseError directly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Add NearVisualSignConverter::with_network so external callers (e.g. parser_app) can construct a converter for testnet without making the network field public. - Drop stale doc-comment claims: fmt.rs has no base58 helper (bs58 is used later, in the intents preset's tests, not here), and networks.rs has no system-account recognition, only the NearNetwork enum and its display names. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…on fields - PAYLOAD_TYPE was the generic "VisualSign" string; every other chain crate uses a chain-specific tag (visualsign-solana: "SolanaTx", visualsign-tron: "TronTx"). Switch to "NearTx" to match. - render_action() rendered Transfer/FunctionCall with no action-kind marker, unlike the fallback branch for undecoded variants (which already prints an "Action" field). A multi-action transaction with e.g. two transfers rendered two identically-labelled "Amount" fields with nothing indicating which action each belongs to. Thread total_actions through render_action and prepend the same "Action" boundary label when there is more than one action; single-action transactions are unaffected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- tx.rs: TRANSFER_HEX's deposit was never asserted, only the action
variant -- the one hand-encoded wire-format fixture in the crate,
editable to any deposit value and still green. Assert the decoded
amount (10^24 yoctoNEAR).
- convert.rs: reject a NEAR transaction with zero actions instead of
rendering a valid-looking payload indistinguishable from a decode
failure.
- Network selection moves from constructor-baked to per-request:
add a NearMetadata { network_id } variant to ChainMetadata's proto
oneof (regenerated via `make generated`), and add
networks::extract_network_from_metadata mirroring
visualsign-ethereum's extract_chain_id_from_metadata. convert.rs
now reads options.metadata first, falling back to whatever network
the converter was constructed with. Stage B's registration
(NearVisualSignConverter::new()) needs no changes: it's already the
same per-process-instance-plus-per-request-metadata pattern
Ethereum uses.
- visualsign-ethereum/networks.rs: switch
extract_chain_id_from_metadata's exhaustive match on the oneof to a
let-else, so adding Near (or any future chain) doesn't require
touching this crate again.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- tx.rs: gate signed-transaction decoding behind developer_config.allow_signed_transactions, mirroring visualsign-ethereum; decode unsigned first and bind its error instead of discarding it. Make the inner Transaction field private with new()/inner() accessors, matching the other chain wrappers. - actions.rs: render each fallback action variant's own fields (DeleteAccount's beneficiary, AddKey/DeleteKey's public key and permission, Stake/TransferToGasKey/WithdrawFromGasKey/ DeterministicStateInit's balances) instead of a bare label; refuse Delegate (NEP-366 meta-transactions) rather than partially render a nested action batch; mark DeployContract/DeployGlobalContract/ UseGlobalContract's label "(not fully decoded)". - actions.rs: push a raw-args field when decode_known_method_args can't interpret a FunctionCall's args, instead of silently rendering just Method+Gas. - actions.rs: sanitize method_name/memo/msg through a charset_safe filter before inserting them into text fields, closing a newline- injection path that could spoof extra fields on the signing screen. - convert.rs/networks.rs: reject an unparseable network_id instead of falling back to Mainnet, and reject a signer account whose top-level suffix (.testnet/.near) contradicts the resolved network. - Cargo.toml: move hex to dev-dependencies (only used in test code); note near-primitives' ~34-crate transitive surface (serde_yaml/ unsafe-libyaml, arbitrary) since it has no default-features knob to shed it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…or fix An Opus-model review of commit 6bcc1da against pepe-anchor's original 9 comments found 4 residual gaps before sending the PR back for re-review: - convert.rs: NearVisualSignConverter never overrode to_visual_sign_payload_from_string, so the default impl called NearTransaction::from_string (developer_config hardcoded to None) -- the only production dispatch path. This silently broke the CLI's allow_signed_transactions posture entirely, the exact override pepe named by name and asked for. Added the override, mirroring EthereumVisualSignConverter's, plus converter-level tests for both postures. - actions.rs: DeterministicStateInit dropped state_init (the derived account's code/data) with no indication anything was left out, unlike every other undecodable-content variant. Appended a "State Init" "(not fully decoded)" field, matching the Deploy*/UseGlobalContract pattern. - actions.rs: decode_known_method_args's doc comment still said unparsed args render "nothing extra", stale since the raw-data fallback fix landed in the same commit. - actions.rs: FtTransferArgs/FtWithdrawArgs' receiver_id/token are attacker-controlled JSON, same as memo/msg/method_name, but weren't charset_safe'd before reaching create_address_field -- same field-spoofing injection class pepe's comment 4 was about, just not the specific fields he named. Added a regression test with an embedded newline in both fields. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`AddKey` collapsed every `FunctionCallPermission` to the fixed label
"Function Call (Restricted)". Both of that permission's bounds widen the
grant when absent -- `method_names: []` allows any method, `allowance:
None` allows unlimited spend -- so a key scoped to one method with a cap
and a key that can call anything on a contract without limit rendered
identically, under a word that contradicts the second.
`push_permission_fields` replaces the label with the fields themselves:
`Permission`, `Contract`, `Allowed Methods` ("Any method" when
unrestricted), and `Allowance` ("Unlimited" when absent). The two gas-key
variants additionally render the `GasKeyInfo` balance and nonce count
they were discarding. `Allowed Methods` joins names with a newline rather
than a comma, since a method name may itself contain a comma.
Also in the display path:
- `TransferToGasKey`/`WithdrawFromGasKey` render the `public_key` naming
which gas key is funded or drained; an account can hold several.
- `charset_safe` keeps double quotes. `validate_charset` permits `\"`
precisely so field text can carry embedded JSON, and
`ft_transfer_call`'s `msg` is such a field. A literal backslash is
still stripped: it serializes as `\\`, putting a
`FORBIDDEN_JSON_ESCAPES` substring in the payload and failing the whole
transaction.
- `FtTransferArgs`/`FtWithdrawArgs` type `receiver_id`/`token` as
`AccountId`, so an id the chain would reject fails the decode and drops
to the raw-args field rather than rendering filtered.
- The network-suffix check runs on the receiver as well as the signer,
and names which account failed.
- `NearMetadata` gets `BORSH_ENUM_DISC_ATTR` alongside its `BORSH_DERIVE`,
matching its Ethereum and Solana siblings. Generated output is
unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Display and decode changes from the second review pass: - Decoding a signed transaction under `allow_signed_transactions` logs a `tracing::warn!`. Production callers pass `None`, so reaching that branch in production is a misconfiguration and now leaves a trail. - When both the unsigned and signed decodes fail, the error carries both causes instead of only the unsigned one. - `title_for` applies the `(not fully decoded)` qualifier that the corresponding field already carries, so a single-action payload's headline cannot claim more than its body. Both sites read the variant set from `is_partially_decoded`. - `CreateAccount` records why it renders its label directly rather than through `action_boundary_field`: its label is the whole render, so there is nothing for a boundary marker to separate. `NearMetadata` no longer takes `BORSH_ENUM_DISC_ATTR`. The attribute applies to enums nested in the named message, and `NearMetadata` holds a single optional string -- as do `EthereumMetadata` and `SolanaMetadata`, whose own nested types `Abi` and `Idl` carry the attribute themselves. The call generates nothing, so it is dropped rather than kept as a pattern that implies an effect it does not have. `extract_chain_id_from_metadata` states why its non-Ethereum arm is a catch-all: this crate reads Ethereum metadata and does not name other chains' variants. Tests: appending a byte to a valid transaction is rejected, pinning that `borsh::from_slice` requires the whole buffer to be consumed; and a 64-hex implicit account renders under either network, pinning that the account-suffix check does not reach accounts that carry no suffix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
314d912 to
f9e649f
Compare
…trip - cli_plugin.rs: create_metadata no longer silently drops --network. Now that #426 added the Near oneof variant, this builds a real ChainMetadata::Near(NearMetadata { network_id }) instead of the interim "reject the flag" fallback proposed in review -- validated via NearNetwork::from_network_id, rejecting unrecognized values with a clear error rather than rendering a confident but wrong "NEAR Mainnet" on a signing screen. Verified end-to-end via the CLI: no flag defaults to mainnet, --network NEAR_TESTNET renders "NEAR Testnet", an invalid value errors before rendering anything. - registry.rs: add Chain::Near to test_chain_from_str/test_chain_as_str, the only chain missing round-trip coverage after this PR wired it in. The e2e PayloadType: "VisualSign" vs "NearTx" mismatch flagged in review is already fixed on this branch (fix(near): update e2e fixtures to match NearTx payload_type) -- no change needed here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rters (#428) Third PR of the NEAR chain-support stack (stacked on #426). Adds the NEAR Intents preset -- decode + render for both the signed-envelope and pre-signature flows -- and merges the crate's two input-format handlers into one. Stack: A1 (#425) -> A2 (#426) -> **A3 (this)** -> A4 (#429) -> B (identity + wiring) -> C (docs site) -> D (wallet-signed token metadata). ## The preset `chain_parsers/visualsign-near/src/presets/intents/`: - `mod.rs` -- entry points (`try_decode_execute_intents` for a signed batch, `try_render_single_intent` for the pre-signature view), `NearIntentsError`, `NearTokenRegistry`/`TokenMeta`. The public surface is `visualsign` types plus primitives; `defuse-*` and `near-sdk` types stay behind this module. - `args.rs` -- JSON deserialization of `execute_intents` args (`{"signed": [...]}`). - `tokens.rs` -- seeded NEP-141 metadata and amount formatting. - `render.rs` -- all 11 intent variants, plus the envelope and signature sections. - `verify.rs` -- signature verification and payload extraction, reported independently of each other. ## The converter merge `tx.rs`'s `NearTransaction` is an enum: ```rust pub enum NearTransaction { OnChain(near_primitives::transaction::Transaction), Intent(String), // validated DefusePayload JSON, kept as text } ``` `from_string` tries borsh first (via the existing hex/base64 detection); on failure it validates the input as a `DefusePayload` JSON envelope, and rejects anything that is neither. Borsh bytes are never valid JSON, so the discrimination is unambiguous. One chain identity, format-discriminated, rather than a converter per input format under its own custom chain name. `convert.rs` dispatches on the variant: `OnChain` renders A2's baseline fields, plus the decoded intents when the action is a `FunctionCall` to `intents.near`/`execute_intents`; `Intent` renders via `try_render_single_intent`, titled "NEAR Intent", with no signature section because nothing is signed at that stage. ## What the signing screen states - **Verification and extraction are independent.** A bad signature still renders the decoded intents, flagged at the signature line; a body that fails to extract surfaces the reason as a diagnostic rather than silently omitting the envelope. - **Every field that changes what is authorized renders.** The three withdraws' `storage_deposit` (an unconditional, unrefundable wNEAR debit alongside the withdraw) and `msg` (switches the call into its `_transfer_call` form); `Transfer`'s flattened notification, whose `msg` calls `mt_on_transfer` on the receiver; and the NEP-616 `state_init` on both `Transfer`'s notification and `AuthCall`, which initializes the receiver's contract in the same receipt. `state_init`'s code and data have no cheap field-level render, so it is flagged `(not fully decoded)`, matching `actions.rs`'s handling of NEAR's own `DeterministicStateInit`. `min_gas` is the only field omitted, consistently across the four variants that carry it. - **`mt_withdraw` rejects a `token_ids`/`amounts` length mismatch** rather than rendering the `zip` overlap and dropping the rest. - **Signature outcomes are three, not two.** `Valid`, `Invalid`, and `MalformedEncoding` -- a signature that is cryptographically sound but carries Ethereum's `v = recovery_id + 27` against the 0-3 this wire format expects. Reporting that as `Invalid` would render "signature verification failed", which reads as tampering. A separate guard rejects out-of-range recovery ids before `verify()`, because near-crypto's native `ecrecover` panics on them rather than returning `None`, and a signing service must treat malformed input as invalid, never as a crash. The variant list is explicit, so a future secp256k1-backed standard is a build break rather than a latent panic. - **The account-binding fields claim only what was checked.** When `signer_id` has an implicit-account shape, the recovered key's implied account id is compared against it and the result stated literally. Neither outcome settles authorization: the contract accepts a key that either derives the account id *or* sits in the account's on-chain key set (`Account::has_public_key`, `defuse/v0.4.2`), so a derivation match can still be rejected on-chain and a non-match is expected for any account that added keys via `AddPublicKey`. A named account has nothing to compare against, so no claim is made. - **Deadline expiry is advisory.** `has_expired()` reads the system clock, which is not necessarily trustworthy inside the enclave; the contract enforces the real deadline. - **Diagnostics carry in both build shapes.** With the `diagnostics` feature a soft finding is a structured `Diagnostic` field; by default it is the same information as a `Warning`-labelled text field, leaving the production payload shape unchanged. ## Token metadata `SEEDS` is a compiled-in table of entries verified against the token contract's own `ft_metadata`; anything unverified is omitted and falls back to the raw asset id, since a wrong `decimals` silently misrenders amounts. Request-scoped overrides layer on top via `LayeredRegistry`. `decimals` above 38 does not resolve at all: `format_units` scales by `10^decimals`, which exceeds `u128` past that point, so an out-of-range value would either overflow or -- with overflow checks off -- divide by a wrapped-around scale and misrender the amount being signed. ## Dependencies `defuse-core`/`defuse-crypto`/`defuse-deadline`, `near-sdk`, `chrono`, `bs58` and `hex` land here, where they are first used. near-sdk 5.27+ (what this workspace resolves) fixes the native-linking gap on the panic path, so `visualsign-near` keeps the workspace's `unsafe_code = "forbid"` with no carve-out and no shim. ## Verification - `cargo build -p visualsign-near` -- clean. - `cargo test -p visualsign-near` -- 94/94 pass, under both default and `--features diagnostics`. - `cargo clippy -p visualsign-near --all-targets -- -D warnings`, `cargo fmt --check` -- clean. - `cargo clippy -p parser_app --no-default-features --features near` -- clean (narrow-build variant). - `make -C src lint` -- clean; `make -C src test` -- clean (workspace suite, including the 9 gRPC integration tests).
#429) Fourth PR of the NEAR chain-support stack (stacked on #428). Adds the signed-vector test coverage for the intents preset's `verify.rs`, which #428 leaves untested because `render.rs` needs those functions to compile. Stack: A1 (#425) -> A2 (#426) -> A3 (#428) -> **A4 (this)** -> B (identity + wiring) -> C (docs site) -> D (wallet-signed token metadata). Entirely `#[cfg(test)]`-scoped: the diff adds a test module to `verify.rs`, two fixture files, and `k256` as a dev-dependency. No production code, so no production attack surface. ## Coverage **raw_ed25519** (`tests/fixtures/_vector_raw_ed25519.input`) -- a signed vector verbatim from `defuse/v0.4.2`, `core/src/payload/multi.rs:120`. Proves defuse's canonical verification runs off-chain (pure-Rust crypto via near-sdk's non-contract-usage mode) and fails closed rather than panicking on a tampered signature. The vector's inner body carries a pre-v0.4.x `"deadline":{"timestamp":...}` that the pinned `Deadline` cannot parse, and the signature covers that body byte-for-byte, so it exercises verification only: the test asserts extraction fails with the format reason instead of discarding it, and names `pipeline_decodes_and_renders_intent_section` as where current-format ed25519 extraction is covered. **ERC-191** (`tests/fixtures/_vector_erc191.input`) -- a deterministically generated vector (fixed test key, RFC6979 signing via `k256`). The three verification tests read the committed fixture, so a generator regression shows up as a signature that no longer verifies rather than as a fixture that rewrites itself and still passes; `build_erc191_vector()` backs only the drift check. Covers a valid signature recovering exactly the signing key, and a tampered payload recovering a *different* key -- ERC-191 "verification" is recovery, so tampering does not fail verification, which is why account binding is the comparison against the claimed signer rather than the recovery itself. **The recovery-id guard** -- near-crypto's native `ecrecover` panics on an out-of-range recovery id instead of returning `None`. Both an arbitrary out-of-range value and Ethereum's v=27 are pinned to `MalformedEncoding`. ## Real-wallet reference vectors Both come verbatim from the pinned dependency's own test suites, cited in-file so provenance is checkable by diffing against those files rather than by trusting this description. Both wallets signed with the same private key, so they share one reference public key. **ERC-191 / MetaMask** (`defuse/v0.4.2`, `erc191/src/lib.rs:72-95`, documented there as a signature MetaMask produced) is used three ways: - this crate's ERC-191 signing path must reproduce those exact bytes -- meaningful across implementations because MetaMask/ethereumjs and `k256` both use deterministic ECDSA (RFC6979), so the same key and message must produce byte-identical output; - those same bytes must recover the reference public key through `decode_args` -> `verify_and_extract`, so the pin covers the production verification path and not just the signing helper; - the same signature with its v byte left as the wallet emits it (28) must report `MalformedEncoding`, which backs the guard with a real wallet's encoding rather than a synthetic value. **TIP-191 / TRON** (`defuse/v0.4.2`, `tip191/src/lib.rs:83-95`) gets the same end-to-end recovery and guard pair. TIP-191 shares the recovery-id guard with ERC-191 and otherwise has no coverage, and since it is the same key signed under TRON's prefix, a prehash regression in either standard is caught. The private key is a long-published throwaway test key from that external suite, not a secret generated or exposed here. ## Standards covered `RawEd25519` and `Erc191` (signed vectors plus the real-wallet pin), `Tip191` (real-wallet pin). Not covered: `Nep413` -- no upstream vector exists, so one has to be generated -- along with `Sep53`, `TonConnect` and `WebAuthn`. ## Verification - `cargo build --locked -p visualsign-near` -- clean. - `cargo test -p visualsign-near` -- 106/106 pass, under both default and `--features diagnostics`. - `cargo clippy -p visualsign-near --all-targets -- -D warnings`, `cargo fmt --check` -- clean. - `cargo clippy -p parser_app --no-default-features --features near --all-targets -- -D warnings` -- clean (narrow-build variant). - Review re-derived both cryptographic vectors against the pinned upstream source byte-for-byte, and traced the guard through to the panic it prevents: bypassing it panics with `InvalidRecoveryId` at both v=27 and v=29, while `RecoveryId::from_i32` accepts 0..=3 and v in {2,3} returns `Err` without panicking -- so the `< 4` threshold is exact, not conservative. - No new crates enter the graph: `k256 0.13.4` was already in `Cargo.lock`; the only lockfile change is `signature 2.0.0` appearing in k256's dep list from the newly enabled signing feature.
…trip - cli_plugin.rs: create_metadata no longer silently drops --network. Now that #426 added the Near oneof variant, this builds a real ChainMetadata::Near(NearMetadata { network_id }) instead of the interim "reject the flag" fallback proposed in review -- validated via NearNetwork::from_network_id, rejecting unrecognized values with a clear error rather than rendering a confident but wrong "NEAR Mainnet" on a signing screen. Verified end-to-end via the CLI: no flag defaults to mainnet, --network NEAR_TESTNET renders "NEAR Testnet", an invalid value errors before rendering anything. - registry.rs: add Chain::Near to test_chain_from_str/test_chain_as_str, the only chain missing round-trip coverage after this PR wired it in. The e2e PayloadType: "VisualSign" vs "NearTx" mismatch flagged in review is already fixed on this branch (fix(near): update e2e fixtures to match NearTx payload_type) -- no change needed here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…erage (#430) * feat(near): add CHAIN_NEAR identity, registry/CLI wiring, and e2e coverage Gives NEAR a first-class chain identity, replacing the placeholder scaffolding from earlier PRs in this stack: - proto/parser/parser.proto: CHAIN_NEAR = 6, regenerated via make -C src generated. - visualsign::registry::Chain gains a Near variant (as_str/FromStr). - parser_app: chain_conversion.rs maps CHAIN_NEAR <-> Chain::Near; create_registry() registers the NearVisualSignConverter; near joins parser_app's default features. - visualsign-near gains a cli-plugin feature (default-on, mirroring every other chain crate) with a NearPlugin implementing parser_cli_core::ChainPlugin, and near joins parser_cli's default features with the plugin wired into ChainArgs/build_plugins. - parser_cli_core::chains.rs's chain_string_mapping gains an entry for "near"; the test asserting an arbitrary string falls back to Chain::Custom now uses a string that is actually still unmapped. - integration/tests/parser.rs: three e2e tests against the real parser_app binary over gRPC -- a native transfer, a pre-signature intents envelope (proving the format-discrimination in tx.rs works end-to-end under the one CHAIN_NEAR identity), and a rejection case for input that is neither. Verified end-to-end via parser_cli decode --chain near for both input formats before writing the e2e assertions. Full workspace make lint and make test pass; the near-only narrow-build variant (parser_app --no-default-features --features near) passes standalone. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(near): update e2e fixtures to match NearTx payload_type Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(near): wire --network into NearMetadata, cover Chain::Near round-trip - cli_plugin.rs: create_metadata no longer silently drops --network. Now that #426 added the Near oneof variant, this builds a real ChainMetadata::Near(NearMetadata { network_id }) instead of the interim "reject the flag" fallback proposed in review -- validated via NearNetwork::from_network_id, rejecting unrecognized values with a clear error rather than rendering a confident but wrong "NEAR Mainnet" on a signing screen. Verified end-to-end via the CLI: no flag defaults to mainnet, --network NEAR_TESTNET renders "NEAR Testnet", an invalid value errors before rendering anything. - registry.rs: add Chain::Near to test_chain_from_str/test_chain_as_str, the only chain missing round-trip coverage after this PR wired it in. The e2e PayloadType: "VisualSign" vs "NearTx" mismatch flagged in review is already fixed on this branch (fix(near): update e2e fixtures to match NearTx payload_type) -- no change needed here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(near): propagate diagnostics to visualsign-near, cover NearMetadata e2e parser_cli and parser_app both listed visualsign-solana in the diagnostics feature but not visualsign-near, the only other chain crate that declares one. A no-op today, since both also enable visualsign/diagnostics directly, but it silently drops any diagnostics-gated behaviour visualsign-near adds. The three NEAR integration tests all sent chain_metadata: None, so the ChainMetadata -> NearMetadata -> extract_network_from_metadata path was never exercised through the real gRPC pipeline. Two tests now cover it: a .testnet transaction with NEAR_TESTNET metadata renders "NEAR Testnet", and a .near transaction declared as Testnet is rejected naming the offending account -- the second is what distinguishes a metadata value that reached the converter from one that merely deserialized. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Second PR of the NEAR chain-support stack (stacked on #425). Implements baseline NEAR transaction parsing in this crate's conventions.
Stack: A1 (#425) -> A2 (this) -> A3 (intents preset) -> A4 (signature verification) -> B (identity + wiring) -> C (docs site) -> D (wallet-signed token metadata).
What this PR does
Adds baseline NEAR chain decoding to
chain_parsers/visualsign-near:tx.rs: borsh-decodes a NEARTransaction/SignedTransactionfrom hex or base64 input.actions.rs: renders eachActionvariant (Transfer, FunctionCall with a fail-closed JSON args decoder forft_transfer/ft_transfer_call/ft_withdraw, generic label fallback for the rest).convert.rs: assembles the final payload (Network/From/To + per-action fields) viaNearVisualSignConverter.fmt.rs/networks.rs: yoctoNEAR/Tgas decimal formatting, Mainnet/Testnet display.Two design decisions worth calling out:
tx.rs's input decoding uses this repo's sharedvisualsign::encodings::SupportedEncodings::detect+decode_hexconvention (matchingvisualsign-solana's ownfrom_string) rather than hand-rolling0x-prefix stripping -- this repo's convention is to reuse the shared mechanism rather than hand-roll per chain. Added a regression test for the0x-prefixed case.convert.rsomitsexecute_intentsdecoding for now -- that code depends onpresets/intents/, which lands in A3, compiled unconditionally there (no feature gate in the final design, since every other preset in this repo is unconditional too).A1's placeholder
NearParserErrorenum is removed as dead code in favor ofvisualsign::vsptrait::TransactionParseErrordirectly; the now-unusedthiserrordependency is dropped with it.Review fixes folded in
NearVisualSignConverter::with_network(NearNetwork): external callers (e.g.parser_app) can now construct a testnet converter without making thenetworkfield public.fmt.rsclaimed a base58 helper that doesn't exist in this file;networks.rsclaimed "system-account recognition" that was never implemented.payload_typeswitched from the generic"VisualSign"to"NearTx", matching every other chain crate's convention (visualsign-solana:"SolanaTx",visualsign-tron:"TronTx")."Action"boundary label ahead ofTransfer's/FunctionCall's own fields when there's more than one action -- without it, e.g. two transfers in one transaction rendered two identically-labelled"Amount"fields with nothing distinguishing which action each belonged to.Verification
cargo test -p visualsign-near-- 24/24 pass.cargo clippy -p visualsign-near --all-targets -- -D warnings,cargo fmt --check-- clean.cargo check/clippy -p parser_app --no-default-features --features near-- clean (narrow-build variant).make -C src lint,make -C src test-- clean (default features; stock builds unaffected).