https://github.com/dereckscompany/ethsign
Local Ethereum and EVM wallet signing in pure R: keccak-256 hashing, secp256k1 ECDSA with Ethereum recovery id and low-s, EIP-712 typed-data signing, and address derivation. The cryptographic primitives needed to authenticate and sign orders on EVM venues such as Hyperliquid and Polymarket.
https://github.com/dereckscompany/ethsign
pkgdown r rcpp testthat
Last synced: 5 days ago
JSON representation
Local Ethereum and EVM wallet signing in pure R: keccak-256 hashing, secp256k1 ECDSA with Ethereum recovery id and low-s, EIP-712 typed-data signing, and address derivation. The cryptographic primitives needed to authenticate and sign orders on EVM venues such as Hyperliquid and Polymarket.
- Host: GitHub
- URL: https://github.com/dereckscompany/ethsign
- Owner: dereckscompany
- License: mit
- Created: 2026-06-07T18:10:21.000Z (20 days ago)
- Default Branch: master
- Last Pushed: 2026-06-07T20:11:17.000Z (20 days ago)
- Last Synced: 2026-06-07T20:15:17.624Z (20 days ago)
- Topics: pkgdown, r, rcpp, testthat
- Language: R
- Size: 76.2 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.Rmd
- Changelog: NEWS.md
- License: LICENSE
Awesome Lists containing this project
README
---
output: github_document
---
```{r setup, include = FALSE}
knitr::opts_chunk$set(
warning = FALSE,
message = FALSE,
fig.path = "./man/figures/README-",
fig.align = "center",
collapse = TRUE,
comment = "#>"
)
# Every example below runs at render time. Signing is OFFLINE and deterministic
# (RFC 6979), so the document executes with no network, no funds, and no chain
# connection -- the printed signatures are the real, reproducible output.
box::use(ethsign[eth_signer, eth_address, keccak256, eip712_digest, ecrecover, as_rsv, as_hex])
```
# ethsign 
**ethsign creates the cryptographic signature a crypto wallet makes -- the digital "stamp" that proves a request came from your wallet and authorizes it -- directly from R.**
Pure-R Ethereum and EVM wallet signing primitives: keccak-256 hashing,
secp256k1 ECDSA with the Ethereum recovery id and low-s normalisation, EIP-712
typed-data signing, EIP-191 `personal_sign`, and address derivation. These are
the cryptographic primitives needed to authenticate and sign orders on EVM
venues such as Hyperliquid and Polymarket, with no native dependencies beyond
`gmp` and `openssl`.
> **A note on responsibility.** This package handles private keys and produces
> signatures that can authorize real transactions. You are responsible for how
> you use it and for keeping your keys safe.
## What this is — and what it is NOT
`ethsign` is the signing **maths** plus a small signer object that holds a key
in memory. That is the whole scope.
It is **NOT** a wallet application. It does not:
- hold or move funds,
- read balances,
- connect to any chain or RPC node,
- broadcast, submit, or track transactions.
It **does**:
- derive an Ethereum address from a private key,
- hash data with keccak-256,
- build the EIP-712 typed-data digest,
- produce a canonical `(r, s, v)` ECDSA signature, deterministically, and
- recover the signing address from a signature (`ecrecover`).
What you do with the resulting signature -- post it to a venue, embed it in a
raw transaction, present it as a login -- is up to you and your other tooling.
## Installation
```{r install, eval = FALSE}
renv::install("dereckscompany/ethsign")
# or, if you use remotes instead of renv:
# install.packages("remotes")
# remotes::install_github("dereckscompany/ethsign")
```
## Quick start
Every block below is executed at render time against no network. We use the
well-known `0x0123…0123` test key (the canonical eth_account / Hyperliquid
example key) so the output is reproducible; in real use, read your key from the
`ETH_PRIVATE_KEY` environment variable (`eth_signer()` with no argument) or
generate a throwaway one with `eth_signer_random()`.
### Create a signer
```{r signer}
signer <- eth_signer(
"0x0123456789012345678901234567890123456789012345678901234567890123"
)
# The address is derived from the key; the private key is never printed.
signer$address
signer
```
### Sign EIP-712 typed data
A single struct over atomic field types is all most venues need. Here is a
Hyperliquid `usdSend` user action (an exact reproduction of the official
Hyperliquid Python SDK testnet vector):
```{r typed-data}
domain <- list(
name = "HyperliquidSignTransaction",
version = "1",
chainId = 421614,
verifyingContract = "0x0000000000000000000000000000000000000000"
)
types <- list(
list(name = "hyperliquidChain", type = "string"),
list(name = "destination", type = "string"),
list(name = "amount", type = "string"),
list(name = "time", type = "uint64")
)
message <- list(
hyperliquidChain = "Testnet",
destination = "0x5e9ee1089755c3435139848e47e6635505d5a13a",
amount = "1",
time = 1687816341423
)
sig <- signer$sign_typed_data(domain, "HyperliquidTransaction:UsdSend", types, message)
sig
```
### Two wire formats
The same signature serializes to either venue convention:
```{r serialize}
# {r, s, v} object form, e.g. Hyperliquid
as_rsv(sig)
# 65-byte concatenated hex r || s || v, e.g. Polymarket
as_hex(sig)
```
### Verify with `ecrecover`
Recompute the digest, recover the address, and confirm it is the signer's --
the inverse check that any venue (or you) can run on a signature:
```{r recover}
digest <- eip712_digest(domain, "HyperliquidTransaction:UsdSend", types, message)
rsv <- as_rsv(sig)
ecrecover(digest, rsv$r, rsv$s, rsv$v) == signer$address
```
### Hashing and addresses
```{r hashing}
# keccak-256 of a string (the empty-string Ethereum vector) or raw bytes
keccak256("")
keccak256(as.raw(c(0x12, 0x34)))
# derive an address from a key without constructing a signer
eth_address("0x0123456789012345678901234567890123456789012345678901234567890123")
```
### Sign a login message (SIWE)
`$sign_message()` applies the EIP-191 `personal_sign` prefix -- the digest used
by Sign-In with Ethereum and most "sign this message to log in" flows:
```{r login}
login <- "example.com wants you to sign in with your Ethereum account"
msig <- signer$sign_message(login)
as_hex(msig)
```
## Use cases
The same `sign_typed_data` / `sign_message` / `sign_digest` primitives cover the
EIP-712 and EIP-191 signing required by, among others:
- **Hyperliquid** user actions (`usdSend`, `withdraw`, approve-agent, and other
user-signed actions) -- verified against the official SDK vectors.
- **Polymarket** order signing (CLOB orders, the 65-byte hex form via
`as_hex()`).
- **0x**, **CoW Protocol**, **1inch**, and **Seaport** (OpenSea) order/intent
signing.
- **ERC-2612** and **Permit2** token permits.
- **Gnosis Safe** transaction hashes.
- **Sign-In with Ethereum** (SIWE) and generic `personal_sign` login messages.
- **Raw EVM transaction** signing (sign the transaction's keccak-256 digest with
`$sign_digest()`; RLP encoding is supplied by your tooling).
### Out of scope
- **Hyperliquid order placement** (the `l1` actions) is msgpack-wrapped and
needs an external msgpack encoder before hashing; the signing step itself is
in scope, the encoding is not.
- **StarkEx / zk / Cosmos venues** -- dYdX, Paradex, ApeX, Lighter -- use
different cryptography (Stark-friendly curves, Cosmos ADR-036) and are not
EVM secp256k1 signing.
## License
MIT © Dereck Mezquita. Provided "as is", without warranty of any kind; see
`LICENSE`. You are responsible for how you use this software and for the safe
custody of your private keys.