This guide walks you through connecting to the hardware wallet with the Openloop SDK, retrieving an address, and signing a transaction.
Install the core package plus the transport package for the connection method you plan to use.
# USB connection (Chrome/Edge on desktop)
npm install @openloop/sdk-core @openloop/transport-webhid
# Bluetooth connection (Chrome/Edge + Android)
npm install @openloop/sdk-core @openloop/transport-webble
# All browsers (via Openloop Connect)
npm install @openloop/sdk-core @openloop/transport-local
# iOS Safari
npm install @openloop/sdk-core @openloop/transport-safari
# Node.js / Electron
npm install @openloop/sdk-core @openloop/transport-usbThe same four steps apply to every connection method.
1. Connect transport → 2. Create app → 3. Operate → 4. Disconnect
import { EthereumApp, DEFAULT_ETH_PATH } from '@openloop/sdk-core'
import { WebHidTransport } from '@openloop/transport-webhid'
// 1. Connect the transport (must be called inside a user gesture)
const transport = await WebHidTransport.connect()
// 2. Create the app
const eth = new EthereumApp(transport)
// 3. Operate
const { address } = await eth.getAddress(DEFAULT_ETH_PATH)
console.log('Address:', address)
// 4. Disconnect
await transport.close()Note: Browser security rules require
WebHidTransport.connect()andWebBleTransport.connect()to be called inside a user gesture, such as a button click.
import {
EthereumApp,
DEFAULT_ETH_PATH,
type SignatureResult,
} from '@openloop/sdk-core'
import { WebHidTransport } from '@openloop/transport-webhid'
async function main() {
// Connect
const transport = await WebHidTransport.connect()
const eth = new EthereumApp(transport)
try {
// Get the address
const { address, publicKey } = await eth.getAddress(DEFAULT_ETH_PATH)
console.log('Address:', address)
console.log('Public Key:', publicKey)
// Sign a message (EIP-191 personal_sign)
const msgSig: SignatureResult = await eth.signPersonalMessage(
DEFAULT_ETH_PATH,
'Hello, Openloop!',
1 // chainId: Ethereum Mainnet
)
console.log('Message Signature:', { v: msgSig.v, r: msgSig.r, s: msgSig.s })
// Sign a transaction (pass the RLP-encoded raw tx)
const rawTx = 'f86c...' // RLP-encoded transaction hex
const txSig: SignatureResult = await eth.signTransaction(DEFAULT_ETH_PATH, rawTx)
console.log('TX Signature:', { v: txSig.v, r: txSig.r, s: txSig.s })
// Sign EIP-712 typed data
const typedSig: SignatureResult = await eth.signTypedData(
DEFAULT_ETH_PATH,
'aabbccdd...', // domainSeparatorHash (32 bytes hex)
'11223344...', // messageHash (32 bytes hex)
1
)
console.log('TypedData Signature:', typedSig)
} finally {
await transport.close()
}
}import {
BitcoinApp,
BTC_MAINNET_PATH,
type BtcMessageSignature,
} from '@openloop/sdk-core'
import { WebHidTransport } from '@openloop/transport-webhid'
async function main() {
const transport = await WebHidTransport.connect()
const btc = new BitcoinApp(transport)
try {
// Get the address (SegWit bech32)
const { address, publicKey, chainCode } = await btc.getAddress(false) // mainnet
console.log('Address:', address) // bc1q...
// Sign a message (BIP-137)
const msgSig: BtcMessageSignature = await btc.signMessage(
'Hello, Bitcoin!',
BTC_MAINNET_PATH
)
console.log('Signature (base64):', msgSig.signature)
// Sign a PSBT
const psbtHex = '70736274ff...' // PSBT binary as hex
const signedPsbtHex = await btc.signPsbtHex(psbtHex)
console.log('Signed PSBT:', signedPsbtHex)
// Sign a PSBT with explicit input paths
const signedWithPaths = await btc.signPsbtWithPaths(psbtHex, [
{ index: 0, path: "84'/0'/0'/0/0" },
{ index: 1, path: "84'/0'/0'/0/1" },
])
console.log('Signed PSBT with paths:', signedWithPaths)
} finally {
await transport.close()
}
}Just swap WebHID for WebBLE — the rest of the code is identical.
import { WebBleTransport } from '@openloop/transport-webble'
// Connect over Bluetooth (must be called inside a user gesture)
const transport = await WebBleTransport.connect()
// Everything from here on is the same as WebHID
const eth = new EthereumApp(transport)
const { address } = await eth.getAddress(DEFAULT_ETH_PATH)Connect through the Openloop Connect desktop app. This works even in Firefox and Safari.
import { LocalWsTransport } from '@openloop/transport-local'
// Check whether the Connect app is running
const available = await LocalWsTransport.isAvailable()
if (!available) {
console.log('Please start Openloop Connect')
}
// Connect
const transport = new LocalWsTransport()
await transport.open()
// Everything from here on is the same
const eth = new EthereumApp(transport)import { useState, useCallback } from 'react'
import { EthereumApp, DEFAULT_ETH_PATH, type ITransport } from '@openloop/sdk-core'
import { WebHidTransport } from '@openloop/transport-webhid'
function WalletButton() {
const [address, setAddress] = useState<string>('')
const [transport, setTransport] = useState<ITransport | null>(null)
const connect = useCallback(async () => {
const t = await WebHidTransport.connect()
setTransport(t)
const eth = new EthereumApp(t)
const { address } = await eth.getAddress(DEFAULT_ETH_PATH)
setAddress(address)
}, [])
const disconnect = useCallback(async () => {
await transport?.close()
setTransport(null)
setAddress('')
}, [transport])
return (
<div>
{address ? (
<>
<p>{address}</p>
<button onClick={disconnect}>Disconnect</button>
</>
) : (
<button onClick={connect}>Connect Wallet</button>
)}
</div>
)
}The sample app’s useOpenloop hook
(packages/sample-app/src/hooks/useOpenloop.ts) is a worked
example that covers switching between all transports, WalletConnect
integration, and automatic reconnection.
WebHID and WebBLE support reconnecting automatically to a device the user has previously authorized.
// Reconnect automatically to a previously authorized device (no user gesture needed)
const transport = await WebHidTransport.reconnect()
if (transport) {
// Reconnected successfully
const eth = new EthereumApp(transport)
} else {
// Device not found → pairing via connect() is required
}