Skip to content

feat(wallet): eip712 variant - #325

Draft
frol-ai wants to merge 14 commits into
near:mainfrom
frol-ai:feat/wallet-eip712-variant
Draft

feat(wallet): eip712 variant#325
frol-ai wants to merge 14 commits into
near:mainfrom
frol-ai:feat/wallet-eip712-variant

Conversation

@frol-ai

@frol-ai frol-ai commented Jul 24, 2026

Copy link
Copy Markdown

Depends on #319 — this branch is stacked on top of it, so the diff below also contains #319's commits until it merges. Only the last commit (feat(wallet): eip712 variant) belongs to this PR.

Re-implementation of #256 on top of #319, adapted to the current wallet-contract architecture (per-variant crates + the SignatureSchema refactor) and extended to cover NEP-641 authorizations.

Adds an EIP-712 (eth_signTypedData_v4) wallet-contract variant, so any Ethereum wallet (MetaMask, WalletConnect, Ledger) can be the sole key of a NEAR wallet-contract account — with clear signing preserved: the wallet displays the structure being authorized, never an opaque digest.

Typed data

Both types live under the same domain and are separated by their primary type:

EIP712Domain(string name,string version)
  name = "NEAR Wallet Contract", version = "1"

WalletRequest(bool payForGas,string chainId,string signerId,uint32 nonce,string createdAt,uint32 timeoutSecs,string internal,string external)
WalletAuth(string chainId,string signer,string purpose,string recipient,string payload,string createdAt,uint32 timeoutSecs)

internal/external are the JSON-serialized wallet operations and promises of the request; signer is the JSON-serialized NEP-641 signer binding.

{
  "primaryType": "WalletRequest",
  "domain": { "name": "NEAR Wallet Contract", "version": "1" },
  "message": {
    "payForGas": false,
    "chainId": "mainnet",
    "signerId": "0se5eba21e8f191e1880e453794bc551dfa50a3419",
    "nonce": 42,
    "createdAt": "2026-07-16T12:34:56.789Z",
    "timeoutSecs": 300,
    "internal": "[{\"op\":\"add_extension\",\"payload\":{\"account_id\":\"extension.near\"}}]",
    "external": "[]"
  }
}

How the display is enforced

The contract does not trust what the client claims to have displayed: it checks every member of the signed typed data against the message it is about to act upon (Eip712RequestMessage::matches() / Eip712AuthMessage::matches()), so a proof whose typed data says anything other than the message is rejected. Members carrying nested structures (internal, external, signer) are compared semantically — parsed, then compared as values — so clients are free to pretty-print that JSON for display without breaking verification.

Tests cover this per field: tampering with any one of payForGas, chainId, signerId, nonce, createdAt, timeoutSecs, internal, external (and the auth analogues) makes the proof fail.

payForGas (#320) matters especially here: hash-based schemas bind it for free via the canonical digest, but a clear-signing schema compares fields explicitly, so it has to be a member of the typed data — otherwise a proof signed with payForGas: false would verify against a request that flips it on, and the wallet would pay for gas the signer never authorized.

verify_auth() on SignatureSchema

#319 hands schemas only the 32-byte digest (verify_hash), which is enough for blind-signing schemas but leaves nothing to display for w_resolve_auth(). This PR adds a verify_auth(public_key, msg: &AuthMessage, proof) provided method that defaults to verify_hash(&msg.hash(), proof) — every existing variant is unaffected — and w_resolve_auth() now calls it. WalletEip712 overrides both verify() and verify_auth(); its verify_hash() always returns false, since an opaque digest carries no message to show the signer.

Identity: 0x address in state

The contract stores the signer's Ethereum address (EthAddress = keccak256(public_key)[12..32], rendered as 0x<hex> by w_public_key()) instead of the public key, and derives it from the public key it recovers from each proof — so the proof still binds the key.

Ethereum wallets expose the address without any signing ceremony (eth_requestAccounts), while the public key can only be recovered from a signature. Since the NEP-616 deterministic AccountId commits to the initial state, a client that only knows the address can already derive which NEAR account it controls — one roundtrip less than a public-key-in-state design (test: account_id_derivable_from_address_alone). It also frees 44 bytes of the ZBA budget.

Address derivation is checked against an independent secp256k1 + keccak256 implementation, so wallets end up controlled by exactly the key behind the user's existing Ethereum address.

Proof

proof is a JSON-serialized SignedEip712: the typed data message plus a recoverable 65-byte secp256k1 signature (r ‖ s ‖ v, v ∈ {0,1}) as secp256k1:<base58> — the same signature encoding used elsewhere in this repo. Ethereum wallets return v as 27/28, so clients normalize by subtracting 27. Verification is a single ecrecover host call (malleable signatures rejected), compared against the public key in state.

Changes

  • New crate defuse-eip712 (crates/signatures/eip712): EIP-712 hashing primitives (type_hash, hash_struct, encode_bytes, encode_uint, 0x19 0x01 prehash, secp256k1 recover), following defuse-erc191/defuse-tip191. Domain is name+version only (NEAR has no EVM chain id and account ids don't fit address; network and account are bound by the message itself).
  • New contract variant defuse-wallet-eip712 (contracts/wallet/signatures/eip712): WalletEip712 schema, wallet-eip712 contract standard, typed-data types, EthAddress, and WalletEip712Signer (incl. sign_auth_msg()) for the SDK.
  • defuse-wallet: SignatureSchema::verify_auth() + w_resolve_auth() wiring.
  • Workspace members/deps, Makefile CONTRACT_CRATES, wallet README.

Tests

  • defuse-eip712: domain-separator, type-hash and uint-encoding known-answer vectors, sign/recover round-trip.
  • defuse-wallet-eip712: type-hash vectors for both primary types, EthAddress derivation/parsing vectors, account id derivable from the address alone, sign→verify round-trips for w_execute_signed and w_resolve_auth, 14 per-field tamper cases, cross-type replay (a request proof must not resolve an authorization and vice versa), wrong address, malformed/bare/other-curve proofs, and verify_hash() failing closed.
  • Shared JSON fixtures tests/fixtures/eip712-wallet-message.json (3 request + 2 auth vectors, each with the canonical message, the eth_signTypedData_v4 message, its prehash and the on-chain proof) pinning the wire format for cross-implementation clients.

Checked: cargo clippy --workspace --all-targets --all-features, cargo fmt --all --check, taplo format --check, make check-contracts/defuse-wallet-eip712/all, cargo test -p defuse-eip712 -p defuse-wallet -p defuse-wallet-eip712. No sandbox test added: the shared w_execute_signed/w_resolve_auth paths are already covered by #319's suite, and this variant only swaps signature verification.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PKxr8pX5kL97LHNZjHvPXR

frol-ai and others added 11 commits July 16, 2026 23:53
Implement the NEP-641 `w_resolve_auth(purpose, recipient, authorization)`
view method on all wallet contract variants:

- New `AuthMessage` envelope (domain `NEAR_WALLET_CONTRACT_AUTH/V1`,
  SHA3-256, no nonce — replay protection is layered via dApp payload
  freshness + recipient/purpose/chain_id/signer bindings + validity window)
- `AuthSignerBinding`: conventional `SignerId` binding, plus a `Code`
  binding to the account's NEP-616 `StateInit`. The envelope commits only
  to the *initial* state config: the contract reconstructs
  `StateInit { code: env::current_global_contract_id(), data: State {
  ..config, public_key } }` — taking the code identity from the code it
  currently runs under and the public key from its own storage — and
  verifies the derived deterministic account id equals
  `env::current_account_id()`. A match proves the envelope was intended
  for this exact account. The binding is constructible client-side before
  a WebAuthn ceremony reveals which passkey (or even which curve / wallet
  variant) answers, and survives post-creation config mutations — single
  ceremony on re-login, always
- `SignatureSchema::verify_hash()` refactor: schemas verify a 32-byte
  domain-separated digest; `verify(RequestMessage)` is now a provided
  method (existing mainnet test vectors unchanged)
- near-sdk 5.28.3 -> 5.29.0: required for the StateInit reconstruction —
  5.29.0 fixes `current_contract_code()` returning the current account's
  own id (instead of the global contract's account id) for
  GlobalByAccount deployments
- SDK: `WalletSigner::sign_auth_msg()` (incl. `WalletEd25519Signer` and
  `MockWalletWebauthnSigner`), `Wallet::sign_auth()` +
  `auth_message()`/`auth_message_code_binding()` builders,
  `w_resolve_auth` client bindings
- Tests: unit vectors (both bindings, both WebAuthn curves via the mock
  webauthn signer), sandbox suite (initial-state binding survives
  mutations, mutated-config envelopes rejected, subwallet isolation,
  CodeHash and AccountId code identities, same-key sibling accounts under
  different code accept the same blob by design, signature-disabled,
  no-sign), and shared JSON fixtures (tests/fixtures/nep641-auth.json)
  consumed by the near-connect-passkey executor to lock the wire format
  cross-repo

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ETHQdJcc4Rz4VTzhMaC3C
…ontract_code fix

near-sdk 5.29.0's generated wasm imports three gas-key host functions
that are not yet live on mainnet:

  - promise_batch_action_add_gas_key_with_full_access
  - promise_batch_action_add_gas_key_with_function_call
  - promise_batch_action_transfer_to_gas_key

A 5.29.0-built wallet contract therefore fails to link at call time:
`LinkError { msg: "unknown or invalid import" }` (reproduced on
near-sandbox 2.12 / protocol 84; the 5.28.x-built wasm links and runs).

Stay on near-sdk 5.28.x until that protocol change ships, and vendor the
`current_contract_code` fix that 5.29.0 would otherwise provide:

- `contracts/wallet/src/auth.rs`: `current_global_contract_id()` — a local
  copy of near/near-sdk-rs#1601. near-sdk 5.28.x reads the current account
  id (not the host-filled register) for the `GlobalByAccount` case, so it
  returns the wallet's own account id instead of the global contract's.
  The vendored version imports only `current_contract_code` (live on
  mainnet) via `near-sys` and reads the register directly.
- `contracts/wallet/src/contract.rs`: `w_resolve_auth` calls the vendored
  function instead of `env::current_global_contract_id()`.
- lockfile pinned to origin's known-good near-* set (near-global-contracts
  0.2.2, near-sys 0.2.12, near-sdk-env 0.1.4); newer versions pull a second
  near-primitives-core (0.37) that breaks near-sdk 5.28.x's unit-testing mock.

REVERT this commit — drop the vendored function, switch back to
`env::current_global_contract_id()`, and move to near-sdk 5.29.x — once the
gas-key host functions are live on mainnet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ETHQdJcc4Rz4VTzhMaC3C
Both deployed WebAuthn wallet variants (p256, ed25519) used
IgnoreUserVerification, so w_execute_signed / w_resolve_auth accepted
assertions with only the UP (user-present) flag — a bare touch, no
biometric/PIN. The passkey is the sole key over funds; the client
requests userVerification:"required" and re-checks the UV flag, but a
proof submitted directly to the relayer bypasses that. The contract must
not accept a user-presence-only assertion.

Switch both variants to RequireUserVerification: check_flags now requires
the UV bit. Trade-off: PIN-less FIDO U2F/CTAP1 keys can no longer sign;
platform passkeys (Apple/Google/Windows) always perform UV.

Account ids are unchanged (global-by-account code identity is stable
across wasm upgrades), so this needs no migration — redeploying the
factory wasm applies it to all existing instances.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMqbwtkoA9PwXo9iVsEDbG
The `Code` binding committed only to config, so a single signed AuthMessage
was accepted by any account keyed by the passkey under any code identity —
the sibling-account ambiguity. It could not commit the code id because the
executor builds the envelope before the discovery ceremony reveals the
curve (and thus which per-curve factory answers).

Fix: commit BOTH canonical factory ids (curve-independent constants known
before the ceremony) as `allowed_factory_ids`, and have each per-curve
`resolve_auth` require that its own running code is in the set. Because a
signature only verifies under its own curve's code, the accepting set
collapses to exactly one account per curve — the code-id binding restored
without unifying the contracts or a second ceremony.

`allowed_factory_ids` MUST list at most one factory per curve, else the
same signed message resolves against two accounts (cross-account replay);
documented on the field and pinned by test_resolve_auth_factory_allow_list.
Code binding now requires a by-account (canonical factory) deployment;
code-hash deployments must use the SignerId binding.

Wire format change: new field in AuthSignerBinding::Code; hash vectors and
the shared nep641-auth.json fixture recomputed (Rust and the executor
encoder agree). SDK `auth_message_code_binding` takes the allow-list.

Verified: wallet lib + fixture tests pass, contract wasm builds, SDK and
integration tests compile. The wallet auth integration tests need Docker
(near-kit sandbox), unavailable here, so were not executed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMqbwtkoA9PwXo9iVsEDbG
Make AuthSignerBinding deserialization strict. This lets a future
wallet-contract variant of the SAME signature curve be added to a Code
binding's allowed_factory_ids without re-introducing cross-account replay,
provided its Code binding carries a different (added, required) field: a
message shaped for the new variant fails to parse on the old contract
(unknown field), and a message for the old variant fails on the new one
(missing required field), so no single signed message resolves under both.

Without deny_unknown_fields serde silently ignores the extra field and the
old contract would accept the new variant's message — the replay the
per-curve invariant guards against.

Verified serde actually enforces it here (it has historically ignored
deny_unknown_fields on internally-tagged enums): binding_denies_unknown_field
rejects an extra field on both variants. Serialization/borsh/hashes are
unchanged, so the wire format and the executor are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMqbwtkoA9PwXo9iVsEDbG
…_SIGNATURE

test_resolve_auth_no_sign expected INVALID_SIGNATURE, but the no-sign wallet
sets signature_enabled=false and holds self as its only extension, so
is_signature_allowed() (signature_enabled || extensions.is_empty()) is false.
w_resolve_auth short-circuits at SignatureDisabled → INVALID_INPUT before ever
reaching signature verification — the same result as a SetSignatureMode(false)
wallet (test_resolve_auth_signature_disabled).

That short-circuit is required for safety: a signature-disabled wallet must be
rejected even if the signature would verify. So the contract is correct and
the test expectation was stale. Align it to INVALID_INPUT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMqbwtkoA9PwXo9iVsEDbG
origin/main refactored the wallet SDK (renamed WalletSigner::sign_request_msg
-> sign_wallet_msg, moved the relayer into the SDK crate, changed the Wallet
struct, added MPC support, switched to env::chain_id()/p256_verify()) and
removed the wallet integration tests (tests/src/tests/wallet/*).

Conflict resolution:
- Contract-side NEP-641 (auth.rs, contract.rs) auto-merged and is preserved in
  full: w_resolve_auth, AuthSignerBinding with allowed_factory_ids +
  deny_unknown_fields, the resolve_auth factory allow-list check, and the p256/
  ed25519 RequireUserVerification variants. Fixed one auto-merge artifact:
  resolve_auth now uses env::chain_id() (utils::chain_id() was removed on main).
- Took main's refactored SDK (signer.rs, lib.rs, Cargo.toml) and dropped the
  SDK-side NEP-641 *convenience* helpers that collided with the refactor
  (sign_auth_msg on the signers, auth_message/auth_message_code_binding/sign_auth
  on Wallet, WResolveAuthArgs::from_signed). These were only consumed by the
  now-deleted wallet integration tests; the deployed contract feature is
  unaffected. They should be re-added on the new signer/MPC/relayer API as a
  follow-up.
- Accepted main's deletion of tests/src/tests/wallet/*. Unrelated conflicts
  (root Cargo.toml versions, defuse core public_key.rs, Cargo.lock) took main.

Verified: full `cargo check --workspace` passes; defuse-wallet lib + NEP-641
fixture tests pass; clippy -D warnings clean on the wallet crates; p256 wasm
builds. Wallet integration tests need Docker (not run here).
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The wallet gains NEP-641 authorization resolution and digest-based signature verification. New EIP-712 hashing, recovery, wallet contract, signer, and fixture crates are added. Existing WebAuthn schemas require user verification, and the wallet SDK exposes the authorization-resolution view call.

Changes

Wallet authorization and signature support

Layer / File(s) Summary
NEP-641 authorization data model
contracts/wallet/src/auth.rs, contracts/wallet/src/lib.rs, contracts/wallet/tests/*, contracts/wallet/Cargo.toml
Adds authorization messages, signer bindings, resolution results, errors, canonical hashing, crate exports, and shared fixture validation.
Digest-based signature verification
contracts/wallet/src/schema.rs, contracts/wallet/signatures/{ed25519,no-sign,webauthn}/*, contracts/wallet/src/contract.rs
Adds SignatureSchema::verify_hash, updates existing schema implementations, and changes WebAuthn configurations to require user verification.
EIP-712 primitives and wallet schema
crates/signatures/eip712/*, contracts/wallet/signatures/eip712/*, Cargo.toml, Makefile, contracts/wallet/README.md
Adds EIP-712 hashing and recovery utilities, the wallet schema and signer, contract wiring, test vectors, and workspace/build integration.
Authorization resolution entrypoints
contracts/wallet/src/contract.rs, crates/wallet/sdk/src/client.rs
Adds w_resolve_auth, validates signed authorization payloads and bindings, verifies hashes, and exposes the corresponding SDK request type and result.
Estimated code review effort: 5 (Critical) ~120 minutes

Possibly related PRs

  • near/intents#270: Adds another wallet SignatureSchema implementation using the hash-based verification surface.
  • near/intents#319: Related NEP-641 authorization-resolution and wallet signature API changes.
  • near/intents#321: Related WalletSigner::sign_wallet_msg SDK method changes.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately points to the main change: adding an EIP-712 wallet variant.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
contracts/wallet/src/contract.rs (1)

292-298: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated validity-window formula.

This re-implements the same now - effective_timeout <= created_at <= now window check as Nonces::commit() (per the comment, "sans bitmap"). Consider factoring the window check itself into a shared helper (e.g. on Nonces or Timestamp) that both execute_signed's nonce commit and resolve_auth call, so the two authorization paths can't silently diverge if the window semantics change later.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/wallet/src/contract.rs` around lines 292 - 298, Factor the
validity-window condition from resolve_auth into a shared helper on the
appropriate nonce or timestamp type, then reuse that helper from both
resolve_auth and Nonces::commit(). Preserve the existing effective-timeout
calculation and ExpiredOrFuture behavior while ensuring both authorization paths
use identical window semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@contracts/wallet/src/contract.rs`:
- Around line 292-298: Factor the validity-window condition from resolve_auth
into a shared helper on the appropriate nonce or timestamp type, then reuse that
helper from both resolve_auth and Nonces::commit(). Preserve the existing
effective-timeout calculation and ExpiredOrFuture behavior while ensuring both
authorization paths use identical window semantics.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 32782069-86f3-47d7-b5d7-6bf36bc462a9

📥 Commits

Reviewing files that changed from the base of the PR and between 3b72cfd and d822edc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (24)
  • Cargo.toml
  • Makefile
  • contracts/wallet/Cargo.toml
  • contracts/wallet/README.md
  • contracts/wallet/signatures/ed25519/src/lib.rs
  • contracts/wallet/signatures/eip712/Cargo.toml
  • contracts/wallet/signatures/eip712/src/contract.rs
  • contracts/wallet/signatures/eip712/src/lib.rs
  • contracts/wallet/signatures/eip712/src/signer.rs
  • contracts/wallet/signatures/eip712/tests/fixtures.rs
  • contracts/wallet/signatures/eip712/tests/fixtures/eip712-wallet-message.json
  • contracts/wallet/signatures/no-sign/src/lib.rs
  • contracts/wallet/signatures/webauthn/ed25519/src/lib.rs
  • contracts/wallet/signatures/webauthn/p256/src/lib.rs
  • contracts/wallet/signatures/webauthn/src/lib.rs
  • contracts/wallet/src/auth.rs
  • contracts/wallet/src/contract.rs
  • contracts/wallet/src/lib.rs
  • contracts/wallet/src/schema.rs
  • contracts/wallet/tests/fixtures/nep641-auth.json
  • contracts/wallet/tests/nep641_fixtures.rs
  • crates/signatures/eip712/Cargo.toml
  • crates/signatures/eip712/src/lib.rs
  • crates/wallet/sdk/src/client.rs

@frol
frol marked this pull request as draft July 24, 2026 20:30
@frol-ai
frol-ai force-pushed the feat/wallet-eip712-variant branch from d822edc to 32ab972 Compare July 24, 2026 20:31
Add an EIP-712 (`eth_signTypedData_v4`) wallet-contract variant, so that any
Ethereum wallet (MetaMask, WalletConnect, Ledger) can be used as the sole key
of a NEAR wallet-contract account, with **clear signing**: the wallet displays
the structure being authorized instead of an opaque digest.

Re-implementation of near#256 on top of the current architecture (per-variant
crates + the `verify_hash()` signature schema refactor), extended to cover
NEP-641 authorizations as well.

New crates:
* `defuse-eip712` (`crates/signatures/eip712`): EIP-712 hashing primitives
  (`typeHash`, `hashStruct`, `encodeData` for dynamic/uint members, the
  `0x19 0x01` prehash and secp256k1 recovery), following the existing
  `defuse-erc191`/`defuse-tip191` crates.
* `defuse-wallet-eip712` (`contracts/wallet/signatures/eip712`): the
  `WalletEip712` signature schema, the contract variant (`wallet-eip712`
  standard) and a `WalletEip712Signer` for the SDK.

Typed data (both under domain `EIP712Domain(string name,string version)` with
name = "NEAR Wallet Contract", version = "1", separated by primary type):

    WalletRequest(string chainId,string signerId,uint32 nonce,string createdAt,
                  uint32 timeoutSecs,string internal,string external)
    WalletAuth(string chainId,string signer,string purpose,string recipient,
               string payload,string createdAt,uint32 timeoutSecs)

The contract does not trust the signed display: it checks every member of the
typed data against the message it is about to act upon, so a proof whose typed
data says anything other than the message is rejected. Members carrying nested
structures (`internal`, `external`, `signer`) are compared semantically, so
clients may format that JSON as they like.

`proof` is a JSON-serialized `SignedEip712`: the typed data `message` plus a
recoverable 65-byte secp256k1 `signature` as `secp256k1:<base58>`.

The contract stores the signer's `0x` **Ethereum address** (`EthAddress`,
keccak256(public_key)[12..32]) rather than the public key, and derives it from
the public key it recovers from each proof. Ethereum wallets expose the address
without a signing ceremony (the public key can only be recovered from a
signature), so a client knows the wallet's NEP-616 deterministic `AccountId` —
which commits to the initial state — as soon as the user connects, saving a
roundtrip. It also frees 44 bytes of the ZBA budget.

To let a schema bind the proof to the contents of an authorization message
(and not only to its digest), `SignatureSchema` gains a `verify_auth()`
provided method (defaults to `verify_hash(msg.hash())`, so all existing
variants are unaffected), which `w_resolve_auth()` now calls.

Tests: domain separator / type hash / uint encoding vectors, address
derivation vectors (cross-checked against an independent secp256k1 + keccak256
implementation), account id derivable from the address alone, sign-verify
round-trips for both entrypoints, per-field tamper cases (a proof must not
verify against any other message), cross-type replay, wrong address and
malformed proofs, plus shared JSON fixtures
(`tests/fixtures/eip712-wallet-message.json`) that lock the typed data and
proof wire format for cross-implementation clients.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKxr8pX5kL97LHNZjHvPXR
@frol-ai
frol-ai force-pushed the feat/wallet-eip712-variant branch from 32ab972 to 61c59f4 Compare July 24, 2026 20:43
frol-ai added 2 commits July 24, 2026 21:56
`RequestMessage` gained a `pay_for_gas` flag (near#320), which the wallet uses to
decide whether it pays for the whole transaction out of its own balance.

Hash-based schemas bind it for free via the canonical digest, but a
clear-signing schema compares fields explicitly, so the flag has to be part of
the typed data — otherwise a proof signed with `payForGas: false` would verify
against a request that flips it on, making the wallet pay for gas it never
authorized (and, the other way around, the signer would not see the flag at
all).

`WalletRequest` becomes:

    WalletRequest(bool payForGas,string chainId,string signerId,uint32 nonce,
                  string createdAt,uint32 timeoutSecs,string internal,
                  string external)

`defuse-eip712` gains `Eip712::encode_bool()` (`uint256` 0/1, per EIP-712)
for that member. Fixtures are regenerated (incl. a new `pay_for_gas` vector)
and a tamper case for the flag is added.
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