@unconfirmed/sui-ledger-signer
v0.1.0
Published
Sign Sui transactions with a Ledger from any TypeScript CLI or tool
Readme
@unconfirmed/sui-ledger-signer
Sign Sui transactions with a Ledger hardware wallet from any TypeScript CLI or tool. Prompt the user, wait for the on-device signature, then execute.
It builds on Mysten's official, transport-agnostic Ledger signer
(@mysten/ledger-signer) and adds the connection UX, account discovery, and a
Sui SDK client extension that wires up the whole connect → prompt → sign →
execute workflow.
📖 Full guide:
docs/client-extension.md— lifecycle, complete API reference, recipes, transports, and error handling.
Install
bun add @unconfirmed/sui-ledger-signer @mysten/sui @ledgerhq/hw-transport
# plus a transport for your environment, e.g. a CLI:
bun add @ledgerhq/hw-transport-node-hid@mysten/sui and @ledgerhq/hw-transport are peer dependencies. The library is
transport-agnostic — you supply an opened @ledgerhq/hw-transport (node-hid,
WebHID, WebUSB, speculos, …), so the same code runs in a CLI or a browser.
Quick start — client extension (recommended)
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid';
import { SuiGrpcClient } from '@mysten/sui/grpc';
import { Transaction } from '@mysten/sui/transactions';
import { ledgerExtension } from '@unconfirmed/sui-ledger-signer';
const client = new SuiGrpcClient({
network: 'testnet',
baseUrl: 'https://fullnode.testnet.sui.io:443',
}).$extend(
ledgerExtension({
transport: await TransportNodeHid.create(),
onApprove: ({ address }) => console.log(`Approve on your Ledger (${address})…`),
}),
);
const address = await client.ledger.getAddress();
const tx = new Transaction();
const [coin] = tx.splitCoins(tx.gas, [1]);
tx.transferObjects([coin], address);
// Prompts via onApprove, waits for the device, then broadcasts.
const { digest } = await client.ledger.signAndExecuteTransaction({ transaction: tx });
console.log('Executed:', digest);client.ledger API
| Method | Description |
| --- | --- |
| connect() | Open the device connection, verifying the Sui app is open. |
| getVersion() | Installed Sui app version. |
| getAddress(path?) | Sui address for a derivation path. |
| discoverAccounts({ count }) | Read a range of accounts for a picker. |
| getSigner(path?) | A standalone Sui Signer for that account. |
| signTransaction({ transaction, derivationPath?, onApprove? }) | Build + sign, no execute. |
| signAndExecuteTransaction({ transaction, … }) | Build + prompt + sign + execute + wait for commit. |
| disconnect() | Close the transport. |
signAndExecuteTransaction waits for the transaction to be committed and
queryable before resolving (via the SDK's waitForTransaction), so a read
immediately afterward won't race the indexer. Since Ledger signing is manual,
the extra poll is negligible. Opt out per call or globally with
waitForTransaction: false, and tune the timeout with waitTimeout.
Lower level — a plain Signer
If you don't want the extension, get a Sui Signer directly and use it with any
client method:
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid';
import { connectLedger, getLedgerSigner } from '@unconfirmed/sui-ledger-signer';
const connection = await connectLedger({ transport: await TransportNodeHid.create() });
const signer = await getLedgerSigner(connection, { client });
const { digest } = await client.signAndExecuteTransaction({ signer, transaction: tx });getLedgerSigner returns the official LedgerSigner, which implements the Sui
Signer interface (Ed25519, intent-message hashing, serialized signatures) and
also supports signPersonalMessage.
Errors
Failures are normalized to LedgerSignerError with a code:
DEVICE_NOT_CONNECTED, APP_NOT_OPEN, DEVICE_LOCKED, USER_REJECTED,
BLIND_SIGNING_DISABLED, UNKNOWN. Use isUserRejection(error) to special-case
a user pressing "Reject".
Example
bun run examples/cli.ts # connect, build, sign (no funds needed)
bun run examples/cli.ts --execute # also broadcast (needs testnet gas)The
@ledgerhq/hw-transport-node-hidnative addon is most reliable under Node. If Bun can't open the HID device, run the example under Node or use a WebHID transport in a browser. The library core never imports a transport.
Derivation paths
Sui uses m/44'/784'/{account}'/0'/0'. Helpers: suiDerivationPath(account),
DEFAULT_DERIVATION_PATH, isValidSuiDerivationPath.
Relationship to the Sui CLI external signers
The Rust sui CLI has its own external signers
feature (sui external-keys …, ≥ 1.66.2) that talks JSON-RPC over stdin/stdout
to a binary like rust-signers.
That's for the Rust CLI. This package is for TypeScript tools that drive
signing themselves via the SDK — no external binary or protocol needed. (A TS
port of that stdio protocol could be added later to plug into the Rust CLI too.)
Test / typecheck
bun test
bun run typecheck