Skip to content

feat: add NEAR Protocol chain support (Ed25519 + Borsh + NEP-413)#221

Merged
njdawn merged 1 commit into
open-wallet-standard:mainfrom
0xultravioleta:feat/near-chain-support
May 5, 2026
Merged

feat: add NEAR Protocol chain support (Ed25519 + Borsh + NEP-413)#221
njdawn merged 1 commit into
open-wallet-standard:mainfrom
0xultravioleta:feat/near-chain-support

Conversation

@0xultravioleta
Copy link
Copy Markdown
Contributor

@0xultravioleta 0xultravioleta commented May 3, 2026

Closes #219

Summary

Adds NEAR Protocol as a supported chain in OWS, following the canonical pattern from merged Nano PR #109 and in-flight Algorand PR #124. NEAR uses Ed25519 (no new curve) over Borsh-serialized transactions, with SHA-256 pre-hash before signing. The signer is genesis-agnostic — network binding lives inside Transaction.block_hash, the same model Algorand uses for gh.

Chain Specification

Property Value
ChainType Near
CAIP-2 near:mainnet, near:testnet
BIP-44 coin type 397 (SLIP-44)
Curve Ed25519 (existing Curve::Ed25519)
Default derivation path m/44'/397'/{index}' (NEAR Foundation / Sender Wallet, fully hardened)
Address (implicit) 64-char lowercase hex of the ed25519 pubkey
TX serialization Borsh (deterministic, NEAR-canonical)
TX signing preimage Ed25519(SHA-256(borsh(Transaction)))
encode_signed_transaction layout borsh(Transaction) ‖ 0x00 ‖ sig64
Network binding block_hash field within Transaction (genesis-agnostic signer)
signMessage (V1) Raw ed25519 over message bytes (parity with Solana). NEP-413 tracked as follow-up.
Default RPC https://rpc.mainnet.near.org, https://rpc.testnet.near.org

Files Changed (31 files, +696 / -49)

Core implementation

  • ows/crates/ows-signer/src/chains/near.rs (new) — NearSigner, 15 inline tests
  • ows/crates/ows-signer/src/chains/mod.rs — module + factory registration
  • ows/crates/ows-core/src/chain.rsChainType::Near variant + KNOWN_CHAINS rows + namespace + coin_type + serde + parse_chain test
  • ows/crates/ows-core/src/config.rs — default RPC URLs
  • ows/crates/ows-lib/src/near_rpc.rs (new) — broadcast_tx_commit JSON-RPC helper
  • ows/crates/ows-lib/src/lib.rs, ops.rs — module export + broadcast dispatch + 3 integration-test loops include near
  • ows/crates/ows-pay/src/discovery.rsformat_near() with yoctoNEAR divisor (10^24), wired into format_price(), 6 unit tests

Bindings tests

  • bindings/node/__test__/index.spec.mjsnear added to chain coverage loops; account count 10 → 12 (also fixed pre-existing Spark omission); spark: startswith assertion added
  • bindings/python/tests/test_bindings.py — same pattern; also fixed pre-existing missing xrpl in derive-all loop

Documentation

  • docs/07-supported-chains.md + website-docs/md/07-supported-chains.md — chain families table, non-EVM networks, shorthand aliases, HD derivation tree
  • docs/02-signing-interface.md + website-docs/md/02-signing-interface.md — chain-specific signMessage / signTransaction semantics for NEAR

README normalization (drive-by — fixed pre-existing gaps too)

  • ows/README.md, bindings/{node,python}/README.md, readme/templates/{ows,node,python}.md, readme/partials/{supported-chains,why-ows}.md, skills/ows/SKILL.md — chain enumerations now consistently include Spark, Nano, NEAR

Pre-existing fix (drive-by, single-line)

  • ows-core/src/chain.rs: ALL_CHAIN_TYPES: [ChainType; 10][ChainType; 12] — Spark was already a ChainType variant with a registered signer but missing from the universal-wallet derivation list. Fixed alongside NEAR addition since adding NEAR exposed the count mismatch in bindings tests. Easy to revert if maintainers prefer this in a separate PR.

Why these design choices

  • No new dependencies. ed25519-dalek, sha2, hex, bs58, base64 are all already in the workspace. We do not pull near-primitives (heavy: tokio + dozens of transitive crates the wallet doesn't need). The minimal NearSigner is ~330 LoC.
  • Implicit account, not named. derive_address returns the implicit account ID (hex(pubkey)). Named accounts (alice.near) require on-chain registration and are out of scope for a stateless signer primitive.
  • HD derivation reuses existing SLIP-10. NEAR's hardened-only path m/44'/397'/{index}' works through the existing ed25519 SLIP-10 implementation in hd.rs. No new HD machinery (in contrast to Algorand PR Feat: add algorand chain and AVM chain type support #124 which needed BIP32-Ed25519 with Peikert).
  • Stateless signing. block_hash inside the Transaction binds it to a network, so the signer doesn't need a network parameter at signing time. Same model Algorand PR Feat: add algorand chain and AVM chain type support #124 uses.

Production Reference

A production x402 facilitator with full NEAR support runs at facilitator.ultravioletadao.xyz, source at UltravioletaDAO/x402-rs. It uses NEP-366 SignedDelegateAction for gasless meta-transactions on the facilitator side; this PR provides the underlying signer primitives so OWS wallets can produce the bytes that facilitator settles. Meta-tx wrapping belongs in ows-pay as a follow-up.

Test Strategy

Unit tests (15 inline in chains/near.rs)

  • Trait properties (chain_type, curve, coin_type, derivation path)
  • Implicit-address derivation against RFC 8032 vector 1 (verifies our hex(pubkey) lowercase produces the canonical 64-char NEAR implicit account ID)
  • ed25519 sign/verify roundtrip with ed25519-dalek::Verifier
  • Deterministic signing (RFC 8032 ed25519 is deterministic)
  • sign_message parity with sign (V1)
  • Invalid private key length rejection
  • sign_transaction MUST sha256 the input first (verified by signing both ways and asserting the signatures differ; then verifying the sign_transaction output matches Ed25519(SHA-256(tx_bytes)))
  • Empty input rejection
  • encode_signed_transaction byte layout (1-byte ED25519 enum tag + 64-byte sig)
  • Wrong sig length rejection
  • Full pipeline extract → sign → encode with signature verifiable against sha256(tx_bytes)

Byte-parity test against near-api-js

test_borsh_byte_parity_with_near_api_js_transaction1 hard-codes the canonical Transaction borsh hex from near/near-api-js test/unit/transactions/data/transaction1.json (transfer of 1 yoctoNEAR test.nearwhatever.near, nonce=1). Verifies that extract_signable_bytes is identity AND that encode_signed_transaction emits exactly tx_bytes ‖ 0x00 ‖ sig64 matching near-api-js's borsh(SignedTransaction) layout.

Integration tests in ops.rs

NEAR added to all 3 cross-chain loops:

  • derive_address_all_chains
  • mnemonic_wallet_sign_message_all_chains
  • mnemonic_wallet_sign_tx_all_chains

format_near unit tests (6)

Whole NEAR / fractional / zero / one yoctoNEAR / typical NEP-141 storage deposit / non-numeric input rejection.

CI gates (verified locally)

cargo fmt --all --check                                  clean
cargo clippy --workspace --all-targets -- -D warnings    clean
cargo test --workspace                                   604 passed, 0 failed
.githooks/pre-commit                                     pass

Out of scope (tracked as follow-ups)

  • NEP-413 prefixed message signing (tag 2147484061 ‖ borsh({message, nonce, recipient, callbackUrl?})) — the V1 sign_message here is raw ed25519 over message bytes for parity with Solana. NEP-413 is a structurally distinct flow with required fields; better as an opt-in API in a follow-up.
  • NEP-366 SignedDelegateAction (gasless meta-transactions) — belongs in ows-pay, not the signer crate.
  • Named-account resolution (alice.near → AccountId) — requires RPC, not a pure stateless signer concern.
  • Live testnet broadcast smoke test — Nano feat: add Nano (XNO) chain support #109 didn't include one either; can be added behind #[ignore] later.

References

cc @njdawn


Note

Medium Risk
Adds a new chain implementation and RPC broadcasting path, which touches signing/encoding and expands universal wallet derivation; mistakes could cause invalid signatures/tx bytes or mis-broadcasts for NEAR.

Overview
Adds NEAR Protocol support end-to-end: introduces ChainType::Near with near:mainnet/near:testnet, default coin type/derivation path, and a new NearSigner that signs transactions as Ed25519(SHA-256(borsh(Transaction))) and encodes signed tx bytes as borsh(Transaction) || 0x00 || sig64.

Wires NEAR into runtime flows by adding default NEAR RPC URLs, adding a curl-based broadcast_tx_commit implementation for signAndSend/broadcast dispatch, and updating ows-pay price formatting to render yoctoNEAR amounts.

Updates Node/Python bindings tests and universal-wallet expectations (account count 10 → 12, adds near coverage and a spark: assertion), and refreshes specs/READMEs to list NEAR and document its message/transaction signing conventions and aliases.

Reviewed by Cursor Bugbot for commit 16c6caf. Bugbot is set up for automated code reviews on this repo. Configure here.

@0xultravioleta 0xultravioleta requested a review from njdawn as a code owner May 3, 2026 00:28
@vercel
Copy link
Copy Markdown

vercel Bot commented May 3, 2026

@0xultravioleta is attempting to deploy a commit to the MoonPay Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

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 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 9a48ecd. Configure here.

Comment thread ows/crates/ows-lib/src/near_rpc.rs Outdated
Copy link
Copy Markdown
Contributor

@njdawn njdawn left a comment

Choose a reason for hiding this comment

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

everything looks good! some ci issues to fix, then can merge

Closes open-wallet-standard#219

Adds NEAR Protocol as a supported chain in OWS. Ed25519 over canonical
Borsh transactions, with SHA-256 pre-hash before signing. Genesis-agnostic
signer (network binding lives in Transaction.block_hash, same model
Algorand uses for `gh`).

## Chain spec

  ChainType:                 Near
  CAIP-2:                    near:mainnet, near:testnet
  BIP-44 coin type:          397 (SLIP-44)
  Curve:                     Ed25519 (existing Curve::Ed25519)
  Default derivation path:   m/44'/397'/{index}' (NEAR Foundation, hardened)
  Address (implicit):        64-char lowercase hex of the ed25519 pubkey
  TX serialization:          Borsh
  TX signing preimage:       Ed25519(SHA-256(borsh(Transaction)))
  encode_signed_transaction: borsh(Transaction) || 0x00 || sig64
  signMessage (V1):          raw ed25519 over message bytes (parity with
                             Solana). NEP-413 follow-up tracked.
  Default RPC:               https://rpc.{mainnet,testnet}.near.org

## Files (23 changed, +702 / -50)

Core implementation:
- ows-signer/src/chains/near.rs (new) — NearSigner, 15 inline tests
- ows-signer/src/chains/mod.rs — module + factory registration
- ows-core/src/chain.rs — ChainType::Near, KNOWN_CHAINS rows, namespace,
  coin_type, FromStr, Display, serde tests, parse_chain test. Also fixes
  pre-existing Spark omission from ALL_CHAIN_TYPES (was [10] now [12]).
- ows-core/src/config.rs — default RPC URLs
- ows-lib/src/near_rpc.rs (new) — broadcast_tx_commit JSON-RPC helper.
  Sanitized error messages: never embeds raw RPC response payload in
  Display output (would leak operational data — tx details, account
  identifiers — into logs/UI).
- ows-lib/src/lib.rs, ops.rs — module export, broadcast dispatch,
  3 integration-test loops include "near".
- ows-pay/src/discovery.rs — format_near() with yoctoNEAR divisor
  (10^24 = 1 NEAR), wired into format_price(). 6 unit tests.

Bindings tests:
- bindings/node/__test__/index.spec.mjs — "near" in chain coverage,
  account count 10 -> 12 (Spark omission fix), spark:* assertion.
- bindings/python/tests/test_bindings.py — same pattern, also fixes
  pre-existing missing "xrpl" in derive-all loop.

Documentation (DUAL — docs/ AND website-docs/md/ kept in sync):
- 07-supported-chains.md: chain families table, non-EVM networks,
  shorthand aliases, HD derivation tree.
- 02-signing-interface.md: chain-specific signMessage / signTransaction
  semantics for NEAR.

README normalization:
- ows/README.md, bindings/{node,python}/README.md,
  readme/templates/{ows,node,python}.md,
  readme/partials/{supported-chains,why-ows}.md,
  skills/ows/SKILL.md — chain enumerations now consistently include
  Spark, Nano, NEAR (also fixed pre-existing Nano omissions).

## Why these design choices

- No new dependencies. ed25519-dalek, sha2, hex, bs58, base64 are
  already in the workspace. We do NOT pull near-primitives (heavy:
  tokio + dozens of transitive crates). NearSigner is ~330 LoC.
- Implicit account, not named. derive_address returns the implicit
  account ID (hex(pubkey)). Named accounts (alice.near) require
  on-chain registration and are out of scope for a stateless signer.
- HD derivation reuses existing SLIP-10. NEAR's hardened-only path
  works through the existing ed25519 SLIP-10 implementation in hd.rs.
  No new HD machinery (in contrast to Algorand PR open-wallet-standard#124 which needed
  BIP32-Ed25519 with Peikert).
- Stateless signing. block_hash inside the Transaction binds it to a
  network, so the signer doesn't need a network parameter at signing
  time. Same model Algorand PR open-wallet-standard#124 uses.

## Production reference

A production x402 facilitator with full NEAR support runs at
facilitator.ultravioletadao.xyz, source at UltravioletaDAO/x402-rs
(Rust, ~600 LoC NEAR implementation). It uses NEP-366 SignedDelegateAction
for gasless meta-transactions on the facilitator side; this PR provides
the underlying signer primitives. Meta-tx wrapping belongs in ows-pay
as a follow-up.

## Test strategy

- 15 unit tests inline in chains/near.rs covering trait properties,
  derivation, RFC 8032 vector 1 implicit address, sign/verify roundtrip,
  determinism, sign_message parity, invalid key length, sha256 prehash
  semantics, encode_signed layout, sig length rejection, empty input
  rejection, full extract -> sign -> encode pipeline.
- Byte-parity test against near-api-js test/unit/transactions/data/
  transaction1.json: hard-coded canonical Transaction borsh hex (transfer
  test.near -> whatever.near, nonce=1). Verifies extract_signable_bytes
  is identity AND encode_signed_transaction emits exactly
  tx_bytes || 0x00 || sig64 matching near-api-js's borsh(SignedTransaction)
  layout.
- 6 format_near unit tests: whole / fractional / zero / one yoctoNEAR /
  typical NEP-141 storage deposit / non-numeric input.
- 3 ops.rs integration loops include "near" (derive_address_all_chains,
  mnemonic_wallet_sign_message_all_chains, mnemonic_wallet_sign_tx_all_chains).

CI gates verified locally:
  cargo fmt --all --check                                    clean
  cargo clippy --workspace --all-targets -- -D warnings      clean
  cargo test --workspace                                     604 passed
  .githooks/pre-commit                                       pass

## Out of scope (tracked as follow-ups)

- NEP-413 prefixed message signing (V1 sign_message is raw ed25519
  for parity with Solana; NEP-413 is a structurally distinct flow).
- NEP-366 SignedDelegateAction (belongs in ows-pay, not the signer).
- Named-account resolution (alice.near -> AccountId requires RPC).
- Live testnet broadcast smoke test (Nano open-wallet-standard#109 didn't include one
  either; can be added behind #[ignore] later).

References:
- near-api-js: https://github.com/near/near-api-js
- NEP-413: https://github.com/near/NEPs/blob/master/neps/nep-0413.md
- NEP-366: https://github.com/near/NEPs/blob/master/neps/nep-0366.md
- SLIP-44 coin type 397: https://github.com/satoshilabs/slips/blob/master/slip-0044.md
- Nano PR open-wallet-standard#109 (merged) — closest structural template
- Algorand PR open-wallet-standard#124 (open) — sibling ed25519 chain addition

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@0xultravioleta 0xultravioleta force-pushed the feat/near-chain-support branch from 9c93175 to 16c6caf Compare May 5, 2026 17:10
@njdawn njdawn merged commit f220204 into open-wallet-standard:main May 5, 2026
9 of 10 checks passed
@0xultravioleta
Copy link
Copy Markdown
Contributor Author

Thanks for the review @njdawn! CI is now green on 16c6caf.

The failure was the readme job — I had hand-edited the four sub-crate READMEs (ows-{cli,core,lib,signer}/README.md) instead of running ./readme/generate.sh, so the generated output diverged from the templates. Fixed by regenerating from readme/templates/ows.md + readme/partials/. Verified locally with ./readme/generate.sh --check before pushing.

All 9 checks now pass on the new head:

  • rust, node, node-cross-compile, python, readme
  • Cursor Bugbot, Socket Security (×2), semgrep ✅

PR is still a single squashed commit. Ready to merge when you are.

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.

feat: Add NEAR Protocol chain support (Ed25519 + Borsh + NEP-413)

2 participants