Using Ledger wallet with ethers on Sei
Prerequisites & Setup
-
Ledger App Installation
- On your Ledger device, open the Manager in Ledger Live and install either the Sei app or the Ethereum app.
- The Ethereum app on Ledger supports any EVM chain (like Sei), but the native Sei app may provide optimal compatibility.
-
Enable Blind Signing
- Still in the Ledger device’s Ethereum or Sei app settings, enable “Blind signing”.
- This permits the device to sign arbitrary EVM transactions and contract calls (e.g. precompiles), which would otherwise be rejected for safety.
-
USB Permissions on Linux
- If you’re on Linux, you’ll likely need a udev rule so your process can talk to the Ledger over USB/HID.
- Clone the official rules from → https://github.com/LedgerHQ/udev-rules
- Copy the provided
49-ledger.rules
into/etc/udev/rules.d/
, thensudo udevadm control --reload-rules
and replug your Ledger.
-
Install Dependencies
npm install ethers @ethers-ext/signer-ledger @sei-js/evm @ledgerhq/hw-transport-node-hid
@ethers-ext/signer-ledger
provides aLedgerSigner
that wraps Ledger’s USB/HID transport for Ethers.js.@sei-js/evm
exports each precompile’s address & ABI.
Connecting Your Ledger to Sei via Ethers.js
Why this matters:
- The provider connects to the blockchain RPC.
- The LedgerSigner injects
signTransaction
/signMessage
calls to your device. - Every contract call or tx will require your physical confirmation on the Ledger.
import { LedgerSigner } from '@ethers-ext/signer-ledger';
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid';
import { ethers } from 'ethers';
// 1. Create an RPC provider (here: Sei testnet)
const rpcUrl = 'https://evm-rpc-testnet.sei-apis.com';
const provider = new ethers.JsonRpcProvider(rpcUrl);
// 2. Initialize the LedgerSigner
// - TransportNodeHid handles the low-level USB/HID connection.
// - The signer will prompt your Ledger for each transaction.
const signer = new LedgerSigner(TransportNodeHid, provider);
// 3. (Optional) Confirm the address on-device
(async () => {
const addr = await signer.getAddress();
console.log('Using Ledger address:', addr);
})();
Governance Precompile Example
Key points before you code:
- Precompiles are optimized, built-in contracts—no external contract deployment needed.
- You must supply the
value
field in overrides to attach SEI. - Every transaction pauses to let you approve on Ledger.
import { GOVERNANCE_PRECOMPILE_ADDRESS, GOVERNANCE_PRECOMPILE_ABI } from '@sei-js/evm';
import { ethers } from 'ethers';
/**
* Deposit SEI into a governance proposal.
*
* @param signer – an initialized LedgerSigner
* @param proposalId – the on-chain ID of the proposal you’re participating in
* @param amount – how many SEI to lock (as a decimal string, e.g. "5")
*/
async function depositToProposal(signer: LedgerSigner, proposalId: number, amount: string) {
// 1. Instantiate the precompile contract
const gov = new ethers.Contract(GOVERNANCE_PRECOMPILE_ADDRESS, GOVERNANCE_PRECOMPILE_ABI, signer);
// 2. Prepare overrides to include SEI value
const overrides = {
value: ethers.parseEther(amount)
};
try {
// 3. Send the tx (this will prompt your Ledger to confirm)
const tx = await gov.deposit(proposalId, overrides);
console.log('Deposit TX sent:', tx.hash);
await tx.wait(); // wait for on-chain confirmation
console.log('✅ Deposit confirmed!');
} catch (err) {
console.error('❌ Deposit failed:', err);
}
}
/**
* Cast a vote on a governance proposal.
*
* @param signer – LedgerSigner
* @param proposalId – the proposal ID
* @param option – vote choice (1=Yes, 2=Abstain, 3=No, 4=NoWithVeto)
*/
async function castVote(signer: LedgerSigner, proposalId: number, option: number) {
const gov = new ethers.Contract(GOVERNANCE_PRECOMPILE_ADDRESS, GOVERNANCE_PRECOMPILE_ABI, signer);
try {
const tx = await gov.vote(proposalId, option);
console.log('Vote TX sent:', tx.hash);
await tx.wait();
console.log('✅ Vote recorded!');
} catch (err) {
console.error('❌ Vote failed:', err);
}
}
// Bring it all together
async function runGovernanceDemo() {
const proposalId = 1; // ← your target proposal
const depositAmount = '5'; // ← e.g. "5" SEI
const voteOption = 1; // ← Yes
// Deposit then vote
await depositToProposal(signer, proposalId, depositAmount);
await castVote(signer, proposalId, voteOption);
}
runGovernanceDemo();
Staking via Precompile Example
Before you run the code:
- The staking precompile is simply another in-chain contract that handles delegation logic.
- You must set both
value
(SEI to lock) andfrom
(your EVM address). - Gas limits on precompiles can be very low—start small and raise if you hit errors.
import { STAKING_PRECOMPILE_ADDRESS, STAKING_PRECOMPILE_ABI } from '@sei-js/evm';
/**
* Stake SEI to a validator via the staking precompile.
*
* @param signer – LedgerSigner
* @param amount – decimal string of SEI to stake
* @param fromAddress – your EVM account address (for ‘from’ override)
* @param validatorAddress – the `seivaloper…` address of your validator
*/
async function stake(signer: LedgerSigner, amount: string, fromAddress: string, validatorAddress: string) {
const staking = new ethers.Contract(STAKING_PRECOMPILE_ADDRESS, STAKING_PRECOMPILE_ABI, signer);
// Attach SEI and explicitly set the ‘from’ so the precompile knows who's delegating
const overrides = {
from: fromAddress,
value: ethers.parseEther(amount),
// A minimal gas limit; adjust if you get underpriced errors
gasLimit: 100_000
};
console.log(`Staking ${amount} SEI → ${validatorAddress}`);
try {
const tx = await staking.delegate(validatorAddress, overrides);
console.log('Stake TX sent:', tx.hash);
await tx.wait();
console.log('✅ Stake confirmed!');
} catch (err) {
console.error('❌ Staking failed:', err);
}
}
// Demo runner
async function runStakingDemo() {
const validatorAddress = 'seivaloper…'; // ← your chosen validator
const defaultAddress = await signer.getAddress();
// Stake 5 SEI
await stake(signer, '5', defaultAddress, validatorAddress);
}
runStakingDemo();
Additional Tips
-
Error Handling:
- If the Ledger times out, increase the HID timeouts or ensure the device stays awake.
- If you see “blind signing required” errors, double-check the app settings on the device itself.
-
Reading Events & Logs:
You can attach anon("receipt", …)
or useprovider.getTransactionReceipt(tx.hash)
to inspect events emitted by the precompile for richer UX. -
Security Reminder:
Always verify the contract address and ABI match official Sei docs. Using a wrong address can lead to lost funds.
Last updated on