npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

liquid-accounts-evm

v0.1.0

Published

Use EVM wallets (MetaMask, etc.) to sign and send Algorand transactions using **EIP-712 typed structured data**.

Readme

liquid-accounts-evm

Use EVM wallets (MetaMask, etc.) to sign and send Algorand transactions using EIP-712 typed structured data.

The SDK compiles a per-address logic signature that validates EVM (secp256k1) signatures on-chain using the AVM's native ecdsa_pk_recover and keccak256 opcodes. Each EVM address maps deterministically to an Algorand lsig address that only that EVM private key can authorize.

Install

npm install liquid-accounts-evm

Peer dependencies:

npm install @algorandfoundation/algokit-utils algosdk

How it works

Algorand dApp                  EVM Wallet                     Algorand
─────────────                  ───────────                    ────────
1. Build Algorand txn
                               2. eth_signTypedData_v4(txnId)
3. Submit txn + lsig + sig ──────────────────────────────►  4. AVM recovers signer from sig
                                                             5. Checks recovered address == templated owner
                                                             6. Transaction executes

The logic signature:

  1. Takes the transaction ID (or group ID for atomic groups) as the payload
  2. Reads the EVM signature from arg[0] (65 bytes: R || S || V)
  3. Computes the EIP-712 digest: keccak256("\x19\x01" + domainSeparator + messageHash)
  4. Recovers the signer's public key via ecdsa_pk_recover (secp256k1)
  5. Derives the Ethereum address: keccak256(pubkeyX || pubkeyY)[12:32]
  6. Approves if the recovered address matches the templated owner

EIP-712 Domain

{
  name: "Algo x EVM",
  version: "1",
}

EIP-712 Types

{
  AlgorandTransaction: [{ name: "Transaction ID", type: "bytes32" }]
}

Usage

Setup

import { AlgorandClient } from "@algorandfoundation/algokit-utils"
import { LiquidEvmSdk, EIP712_DOMAIN, EIP712_TYPES } from "liquid-accounts-evm"
import { BrowserProvider } from "ethers"

const algorand = AlgorandClient.fromEnvironment()
const sdk = new LiquidEvmSdk({ algorand })

// MetaMask or other EVM wallet provider
const provider = new BrowserProvider(window.ethereum)
const evmAddress = (await provider.send("eth_requestAccounts", []))[0]

Get the Algorand address for an EVM account

const algoAddr = await sdk.getAddress({ evmAddress })
// => "ALGO..."

This is useful for checking balances or displaying the address before any signing is needed.

Get a TransactionSigner

getSigner returns an algokit-utils compatible { addr, signer } pair. Pass signMessage — a callback that receives the full EIP-712 typed data (domain, types, primaryType, message) and returns the signature.

import { ethers } from "ethers"
import type { SignTypedDataParams } from "liquid-accounts-evm"

const wallet = new ethers.Wallet(privateKey)

// signMessage receives all EIP-712 data — just forward to signTypedData
const signMessage = async ({ domain, types, message }: SignTypedDataParams) => {
  return wallet.signTypedData(domain, types, message)
}

const { addr, signer } = await sdk.getSigner({
  evmAddress,
  signMessage,
})

Or with MetaMask:

const signMessage = async ({ domain, types, primaryType, message }: SignTypedDataParams) => {
  const data = JSON.stringify({ domain, types, primaryType, message })
  return provider.send("eth_signTypedData_v4", [evmAddress, data])
}

const { addr, signer } = await sdk.getSigner({ evmAddress, signMessage })

Send a standalone transaction

await algorand.send.payment({
  sender: addr,
  receiver: addr,
  amount: (0).algos(),
  signer,
})

Send an atomic group

For grouped transactions of size > 1, the signer automatically signs the group ID instead of individual transaction IDs. The on-chain logic signature handles this automatically.

await algorand.newGroup()
  .addPayment({
    sender: addr,
    receiver: someReceiver,
    amount: (1).algos(),
    signer,
  })
  .addPayment({
    sender: otherAccount.addr,
    receiver: otherAccount.addr,
    amount: (0).algos(),
  })
  .send()

Sign raw algosdk transactions

For full control over transaction construction and group ID assignment, use signTxn. This is useful when working directly with algosdk.

Variant 1: With signMessage callback

import algosdk from "algosdk"
import type { SignTypedDataParams } from "liquid-accounts-evm"

const addr = await sdk.getAddress({ evmAddress })

const txn = await algorand.createTransaction.payment({
  sender: addr,
  receiver: addr,
  amount: (0).algos(),
})
const [gtxn] = algosdk.assignGroupID([txn])

const signMessage = async ({ domain, types, message }: SignTypedDataParams) => {
  return wallet.signTypedData(domain, types, message)
}

const [signed] = await sdk.signTxn({
  evmAddress,
  txns: [gtxn],
  signMessage,
})

await algorand.client.algod.sendRawTransaction(signed).do()

Variant 2: With pre-computed signature

import algosdk from "algosdk"
import { LiquidEvmSdk, buildTypedData } from "liquid-accounts-evm"

const addr = await sdk.getAddress({ evmAddress })

const txn = await algorand.createTransaction.payment({
  sender: addr,
  receiver: addr,
  amount: (0).algos(),
})
const [gtxn] = algosdk.assignGroupID([txn])

// Pre-compute the signature
const payload = LiquidEvmSdk.getSignPayload([gtxn])
const { domain, types, message } = buildTypedData(payload)
const signature = await wallet.signTypedData(domain, types, message)

// Sign using pre-computed signature
const [signed] = await sdk.signTxn({
  evmAddress,
  txns: [gtxn],
  signature,
})

await algorand.client.algod.sendRawTransaction(signed).do()

API

LiquidEvmSdk

constructor({ algorand: AlgorandClient })

Creates an SDK instance. The AlgorandClient is used to compile the TEAL template. Compiled programs are cached per EVM address.

getAddress({ evmAddress: string }): Promise<string>

Returns the Algorand lsig address for the given EVM address. The evmAddress should be a hex string (with or without 0x prefix).

getSigner({ evmAddress, signMessage }): Promise<{ addr: string; signer: TransactionSigner }>

Returns the Algorand address and a TransactionSigner that can be passed directly to any algokit-utils send method.

  • evmAddress — hex string (with or without 0x prefix)
  • signMessage(typedData: SignTypedDataParams) => Promise<string> — receives the full EIP-712 typed data (domain, types, primaryType, message) and should return the signature.

The signer automatically determines what to sign: the transaction ID for standalone transactions, or the group ID for atomic groups.

signTxn({ evmAddress, txns, signMessage }): Promise<Uint8Array[]>

signTxn({ evmAddress, txns, signature }): Promise<Uint8Array[]>

Signs one or more algosdk Transaction objects with the EVM lsig. Returns an array of signed transaction blobs ready for sendRawTransaction.

Parameters:

  • evmAddress — hex string (with or without 0x prefix)
  • txns — algosdk Transaction[] to sign (must already have group ID assigned via algosdk.assignGroupID if grouped)
  • signMessage — (variant 1) (typedData: SignTypedDataParams) => Promise<string> — receives the full EIP-712 typed data and should return the signature
  • signature — (variant 2) string — pre-computed EIP-712 signature (0x-prefixed 65-byte hex string)

The payload signed is the group ID if there are more than 1 transactions, otherwise the transaction ID.

Example with callback:

await sdk.signTxn({ evmAddress, txns, signMessage })

Example with pre-computed signature:

const payload = LiquidEvmSdk.getSignPayload(txns)
const signature = await getSignature(payload)
await sdk.signTxn({ evmAddress, txns, signature })

Types

SignTypedDataParams

Interface for the EIP-712 typed data passed to signMessage callbacks:

interface SignTypedDataParams {
  domain: { name: string; version: string }
  types: {
    EIP712Domain: Array<{ name: string; type: string }>
    AlgorandTransaction: Array<{ name: string; type: string }>
  }
  primaryType: "AlgorandTransaction"
  message: { "Transaction ID": string }
}

Utilities

buildTypedData(payload: Uint8Array): SignTypedDataParams

Builds a complete EIP-712 typed data object from a raw transaction/group ID payload. Useful when pre-computing signatures outside the SDK callbacks.

const payload = LiquidEvmSdk.getSignPayload(txns)
const { domain, types, message } = buildTypedData(payload)
const signature = await wallet.signTypedData(domain, types, message)

formatEIP712Message(payload: Uint8Array): { "Transaction ID": string }

Formats a raw transaction/group ID payload as an EIP-712 typed data message. Helper for use with signing callbacks.

const payload = new Uint8Array(32) // transaction or group ID
const message = formatEIP712Message(payload)
// => { "Transaction ID": "0x..." }

parseEvmSignature(sigHex: string): Uint8Array

Parses a 0x-prefixed 65-byte EVM signature hex string into a Uint8Array of R(32) || S(32) || V(1).

Security: Automatically normalizes signatures to lower-S form because the AVM only accepts lower-S signatures. If s > n/2 (where n is the secp256k1 curve order), the signature is normalized to n - s with the recovery ID flipped.

const sig = parseEvmSignature("0x1234...") // 65-byte signature
// Returns normalized signature with s in lower half

EIP712_DOMAIN

The EIP-712 domain used for signing:

{
  name: "Algo x EVM",
  version: "1",
}

EIP712_TYPES

The EIP-712 types used for signing:

{
  AlgorandTransaction: [{ name: "Transaction ID", type: "bytes32" }]
}

ALGORAND_CHAIN_ID

The Algorand chain ID constant: 4160 (used for EVM wallet chain registration)

hexToBytes(hex: string): Uint8Array

Converts a hex string to a Uint8Array.

Security

  • Each EVM address maps to a unique Algorand lsig address. Only the holder of the corresponding EVM private key can authorize transactions from that address.
  • The signature covers the transaction ID (standalone) or group ID (atomic group), preventing replay across different transactions.
  • EIP-712 provides domain separation, preventing cross-app and cross-network replay attacks.
  • Users see human-readable transaction data in their wallet (MetaMask, etc.) instead of raw hex.
  • Lower-S normalization: All signatures are automatically normalized to lower-S form because the AVM only accepts lower-S signatures. If s > n/2, the signature is converted to (r, n - s) with the recovery ID flipped.

License

MIT