Skip to main content

WebUSB Notes

This guide covers direct browser access to UKey Wallet through WebUSB with @ukeyfe/hardware-common-connect-sdk. After the user authorizes and the device connects, BTC, EVM, Solana, and other chain APIs continue to use the same SDK entry.

The scope here is WebUSB only. For React Native BLE or Android/iOS native bridging, use the dedicated transport pages instead.

Part 1: Needs

  • The page must run under HTTPS or a local development secure context; plain HTTP cannot access WebUSB.
  • Use a browser with WebUSB support. Desktop Chrome or Edge is the safest target.
  • Connect a UKey Wallet device to the computer and follow the on-device unlock or confirmation prompts.

Part 2: Setup

npm i @ukeyfe/hardware-common-connect-sdk @ukeyfe/hardware-shared @ukeyfe/hardware-core

Part 3: Start Events

Attach event listeners right after SDK initialization. PIN, Passphrase, button confirmation, and device connect/disconnect all arrive through events; registering late can leave an in-flight request waiting for a response that no handler receives.

import ukeySdk from '@ukeyfe/hardware-common-connect-sdk';
import { UI_EVENT, UI_REQUEST, UI_RESPONSE, DEVICE } from '@ukeyfe/hardware-core';

export async function initializeWebUsbSession() {
await ukeySdk.init({
env: 'webusb',
debug: process.env.NODE_ENV !== 'production',
fetchConfig: true,
});

bindDeviceEventHandlers();
}

function bindDeviceEventHandlers() {
ukeySdk.on(UI_EVENT, async (uiPacket) => {
switch (uiPacket.type) {
case UI_REQUEST.REQUEST_PIN: {
// Open your PIN prompt here, then send the result back with uiResponse
const pinPromptState = await collectPinInput();
if (pinPromptState.mode === 'device') {
await ukeySdk.uiResponse({
type: UI_RESPONSE.RECEIVE_PIN,
payload: '@@UKEY_INPUT_PIN_IN_DEVICE',
});
} else {
await ukeySdk.uiResponse({
type: UI_RESPONSE.RECEIVE_PIN,
payload: pinPromptState.value,
});
}
break;
}

case UI_REQUEST.REQUEST_PASSPHRASE: {
const passphrasePromptState = await collectPassphraseInput();
if (passphrasePromptState.mode === 'device') {
await ukeySdk.uiResponse({
type: UI_RESPONSE.RECEIVE_PASSPHRASE,
payload: { passphraseOnDevice: true, value: '' },
});
} else {
await ukeySdk.uiResponse({
type: UI_RESPONSE.RECEIVE_PASSPHRASE,
payload: {
value: passphrasePromptState.value,
passphraseOnDevice: false,
save: passphrasePromptState.save,
},
});
}
break;
}

default:
break;
}
});

ukeySdk.on(DEVICE.CONNECT, (payload) => {
console.log('Connected device event:', payload);
});
ukeySdk.on(DEVICE.DISCONNECT, (payload) => {
console.log('Disconnected device event:', payload);
});
}

Key Notes

  • Subscribe to UI_EVENT early so PIN, Passphrase, and confirmation prompts always have a responder. See Configure events.
  • Keep PIN entry on the device whenever possible (@@UKEY_INPUT_PIN_IN_DEVICE); if your product offers software blind input, convert it with the fixed position map (7,8,9,4,5,6,1,2,3).
  • Passphrase can be handled by the device or collected in your software UI; save should be treated as current-session caching only.
  • Surface device connect/disconnect events in the UI so cable removal or reconnection is reflected immediately.

Part 4: Grant Access

Start WebUSB permission from an explicit user action, such as pressing a connect button. If the page opens the USB picker by itself, the browser will stop it.

Use UKEY_WEBUSB_FILTER to keep the chooser focused on UKey Wallet-compatible hardware and reduce the chance of selecting an unrelated USB device.

import { UKEY_WEBUSB_FILTER } from '@ukeyfe/hardware-shared';

export function WebUsbPermissionButton() {
return (
<button
onClick={async () => {
await navigator.usb.requestDevice({ filters: UKEY_WEBUSB_FILTER });
// After the browser grants access, continue with SDK device discovery
}}
>
Connect UKey Wallet by USB
</button>
);
}

Part 5: Pick Device

const deviceScanResponse = await ukeySdk.searchDevices();
if (!deviceScanResponse.success) throw new Error(deviceScanResponse.payload.error);

// This example uses the first device returned by discovery
const { connectId, deviceId } = deviceScanResponse.payload[0] ?? {};
  • WebUSB enumeration results will usually contain deviceId directly.
  • The subsequent chain API generally needs to pass in connectId and deviceId at the same time. Save them in the current session.

Part 6: Read Features

const featureSnapshot = await ukeySdk.getFeatures(connectId);
if (!featureSnapshot.success) throw new Error(featureSnapshot.payload.error);

const cachedDeviceId = featureSnapshot.payload.device_id;

Part 7: First Call

const btcAddressReply = await ukeySdk.btcGetAddress(connectId, cachedDeviceId, {
path: "m/84'/0'/3'/0/1",
coin: 'btc',
showOnUKey: false,
});

if (btcAddressReply.success) {
console.log('Resolved BTC address:', btcAddressReply.payload.address);
} else {
console.error('Request failed:', btcAddressReply.payload.error, btcAddressReply.payload.code);
}

SDK Method Map

Once the connection is successful, you can start the verification process from these common methods.

actiondescriptionSample
evmGetAddressGet EVM addresspath: "m/44'/60'/6'/0/2"
evmSignMessageSign EVM messagesmessageHex: '...'
evmSignTransactionSign EVM transactiontransaction: { ... }
btcGetAddressGet BTC addresscoin: 'btc', path: "m/84'/0'/2'/0/0"
solanaGetAddressGet Solana addresspath: "m/44'/501'/3'/0'"
getFeaturesGet device detailsconnectId
deviceSettingsModify device settings (such as name)label: 'My UKey Wallet'

Appendix: UI Helpers

Below are minimalist versions of collectPinInput and collectPassphraseInput illustrating how events are translated into the user interface. For the production environment, please add error handling, cancellation processes, and accessibility support based on your product design.

// Blind keyboard map aligned with hardware key positions
const PIN_SLOT_DIGITS = ['7', '8', '9', '4', '5', '6', '1', '2', '3'];

function collectPinInput() {
return new Promise(resolve => {
const backdrop = document.createElement('div');
backdrop.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:9999;';

const panel = document.createElement('div');
panel.style.cssText = 'background:white;padding:20px;border-radius:8px;max-width:400px;width:100%;';
panel.innerHTML = `
<h3 style="margin-top:0">Provide PIN</h3>
<p>For better security, prefer device entry, or use the blind keyboard here.</p>
<div id="pin-display" style="font-size: 24px; margin: 10px 0; min-height: 1.2em; border-bottom: 1px solid #ccc;"></div>
<div id="keypad" style="display:grid; grid-template-columns: repeat(3, 1fr); gap: 10px;"></div>
<div style="margin-top: 20px; display:flex; gap:10px;">
<button id="use-device" style="flex:1">Use device input instead</button>
<button id="clear-pin">Reset entry</button>
<button id="confirm-pin" style="background:#007aff;color:white;border:none;padding:5px 15px;border-radius:4px;">Submit PIN</button>
</div>
`;

const enteredDigits = [];
const keypad = panel.querySelector('#keypad');
PIN_SLOT_DIGITS.forEach((mappedDigit, slotIndex) => {
const btn = document.createElement('button');
btn.textContent = slotIndex + 1;
btn.style.padding = '10px';
btn.onclick = () => {
enteredDigits.push(mappedDigit);
panel.querySelector('#pin-display').textContent = '•'.repeat(enteredDigits.length);
};
keypad.appendChild(btn);
});

panel.querySelector('#use-device').onclick = () => {
document.body.removeChild(backdrop);
resolve({ mode: 'device' });
};
panel.querySelector('#clear-pin').onclick = () => {
enteredDigits.length = 0;
panel.querySelector('#pin-display').textContent = '';
};
panel.querySelector('#confirm-pin').onclick = () => {
document.body.removeChild(backdrop);
resolve({ mode: 'software', value: enteredDigits.join('') });
};

backdrop.appendChild(panel);
document.body.appendChild(backdrop);
});
}

function collectPassphraseInput() {
return new Promise(resolve => {
const backdrop = document.createElement('div');
backdrop.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:9999;';

const panel = document.createElement('div');
panel.style.cssText = 'background:white;padding:20px;border-radius:8px;max-width:400px;width:100%;';
panel.innerHTML = `
<h3 style="margin-top:0">Provide passphrase</h3>
<input type="password" id="passphrase-input" placeholder="Leave empty to use the default wallet" style="width:100%;padding:8px;margin-bottom:10px;box-sizing:border-box;" />
<slot style="display:block;margin-bottom:15px;"><input type="checkbox" id="save-passphrase" /> Keep it for this session only</slot>
<div style="display:flex; gap:10px;">
<button id="use-device-pass" style="flex:1">Use device input instead</button>
<button id="submit-pass" style="background:#007aff;color:white;border:none;padding:5px 15px;border-radius:4px;">Submit passphrase</button>
</div>
`;

panel.querySelector('#use-device-pass').onclick = () => {
document.body.removeChild(backdrop);
resolve({ mode: 'device', value: '', save: false });
};
panel.querySelector('#submit-pass').onclick = () => {
const passphraseValue = panel.querySelector('#passphrase-input').value;
const rememberSession = panel.querySelector('#save-passphrase').checked;
document.body.removeChild(backdrop);
resolve({ mode: 'software', value: passphraseValue, save: rememberSession });
};

backdrop.appendChild(panel);
document.body.appendChild(backdrop);
});
}