The Openloop SDK supports remote device access over WalletConnect v2 in two ways.
| Approach | Level | Description |
|---|---|---|
| WcTransport | Low-level | Relays APDU commands through the WalletConnect relay |
| AppKit | High-level | Standard multi-chain WC connection via Reown AppKit |
WcTransport implements ITransport and sends
APDU commands to a remote Openloop Connect app through the WalletConnect
v2 relay.
Hex-encoded APDUs are sent and received over the custom JSON-RPC
method openloop_exchange.
import { WcTransport, EthereumApp, DEFAULT_ETH_PATH } from '@openloop/sdk-core'
import SignClient from '@walletconnect/sign-client'
// Initialize the WalletConnect SignClient
const client = await SignClient.init({
projectId: 'your-project-id',
})
// Pair and establish a session
const { uri, approval } = await client.connect({
requiredNamespaces: {
eip155: {
methods: ['openloop_exchange', 'openloop_lock', 'openloop_unlock'],
chains: ['eip155:1'],
events: [],
},
},
})
// Show the URI as a QR code → the user scans it with Openloop Connect
console.log('Pairing URI:', uri)
// Wait for session approval
const session = await approval()
// Create the WcTransport
const transport = new WcTransport({
client,
session: { topic: session.topic },
chainId: 'eip155:1', // optional (default: 'eip155:1')
})
await transport.open()
// The regular App classes work as-is
const eth = new EthereumApp(transport)
const { address } = await eth.getAddress(DEFAULT_ETH_PATH)interface WcTransportOptions {
client: IWcSignClient // WalletConnect SignClient instance
session: WcSession // Active session { topic: string }
chainId?: string // Chain ID (default: "eip155:1")
}
interface IWcSignClient {
request<T>(params: {
topic: string
chainId: string
request: { method: string; params: unknown }
}): Promise<T>
}
interface WcSession {
topic: string
}| Method | Description |
|---|---|
openloop_exchange |
Send and receive APDU commands |
openloop_lock |
Acquire the exclusive device lock |
openloop_unlock |
Release the exclusive device lock |
ITransport interface, so every App class
works as-islock / unlock) also works
over the relayWith Reown AppKit you sign using WalletConnect’s standard methods —
eth_signTransaction, personal_sign, and so on.
This approach never handles APDUs directly; Openloop Connect drives the
device internally.
Chains: - eip155:1 — Ethereum Mainnet -
eip155:11155111 — Sepolia Testnet
Methods: | Method | Description | |———|——| |
eth_sendTransaction | Send a transaction | |
eth_signTransaction | Sign a transaction | |
eth_sign | Sign data | | personal_sign | Sign
an EIP-191 message | | eth_signTypedData | Sign EIP-712
typed data | | eth_signTypedData_v3 | EIP-712 v3 | |
eth_signTypedData_v4 | EIP-712 v4 | |
wallet_getCapabilities | Get the wallet’s capabilities
|
Chains (SUPPORTED_BIP122_CHAINS in
@openloop/sdk-core): -
bip122:000000000019d6689c085ae165831e93 — Bitcoin Mainnet -
bip122:000000000933ea01ad0ee984209779ba — Bitcoin
Testnet3
Methods (SUPPORTED_BIP122_METHODS): | Method |
Description | |———|——| | signPsbt | Sign a PSBT | |
getAccountAddresses | Get the account addresses | |
signMessage | Sign a message (BIP-137) | |
sendTransfer | Send a transfer |
import { SUPPORTED_BIP122_CHAINS, SUPPORTED_BIP122_METHODS } from '@openloop/sdk-core'
console.log(SUPPORTED_BIP122_CHAINS.length, SUPPORTED_BIP122_METHODS.join(', '))
// 2 "signPsbt, getAccountAddresses, signMessage, sendTransfer"Openloop Connect returns the signed PSBT (psbt in the
signPsbt result) and the BIP-137 signature
(signature in the signMessage result) as
base64. The SDK functions take hex, so convert before
passing them in.
import type { BtcMessageSignature } from '@openloop/sdk-core'
import { extractRawTx } from '@openloop/sdk-core/bitcoin'
import { verifyBtcMessage } from '@openloop/sdk-core/verify'
function base64ToBytes(b64: string): Uint8Array {
return Uint8Array.from(atob(b64), c => c.charCodeAt(0))
}
function toHex(bytes: Uint8Array): string {
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('')
}
// Result of signPsbt → raw transaction ready to broadcast
function rawTxFromSignPsbt(result: { psbt: string }) {
return extractRawTx(toHex(base64ToBytes(result.psbt))) // { rawTxHex, txid }
}
// Result of signMessage → verify the BIP-137 signature
function verifySignMessage(message: string, result: { address: string; signature: string }) {
const bytes = base64ToBytes(result.signature) // [v(1)][r(32)][s(32)]
const hex = toHex(bytes)
const sig: BtcMessageSignature = { v: bytes[0], r: hex.slice(2, 66), s: hex.slice(66, 130), signature: result.signature }
return verifyBtcMessage(message, sig, result.address)
}The range of v is the same as in BitcoinApp.signMessage
(31 to 42, depending on the address type). For the details of
extractRawTx and verifyBtcMessage, see the Bitcoin API.
▲
signMessagein Reown’s Bitcoin adapter (checked with@reown/appkit-adapter-bitcoin1.8.19) reads the signature as hex, so it returns an empty result for the base64 signature above. For signing, call the underlying WalletConnectprovider.request({ method: 'signMessage', params: { message, account, address } }, chainId)directly.
Chains: - solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp —
Mainnet Beta - solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1 —
Devnet
Methods: | Method | Description | |———|——| |
solana_signTransaction | Sign a transaction | |
solana_signAllTransactions | Sign multiple transactions | |
solana_signAndSendTransaction | Sign and send | |
solana_signMessage | Sign a message |
Chains: - tron:0x2b6653dc — TRON Mainnet -
tron:0x94a9059e — TRON Shasta Testnet
Methods: | Method | Description | |———|——| |
tron_signTransaction | Sign a transaction | |
tron_signMessage | Sign a message |
import { createAppKit } from '@reown/appkit/react'
import { WagmiAdapter } from '@reown/appkit-adapter-wagmi'
import { SolanaAdapter } from '@reown/appkit-adapter-solana'
import { BitcoinAdapter } from '@reown/appkit-adapter-bitcoin'
import { TronAdapter } from '@reown/appkit-adapter-tron'
import { mainnet, sepolia, bitcoin, bitcoinTestnet } from '@reown/appkit/networks'
const projectId = 'your-walletconnect-project-id'
// Create the adapters
const wagmiAdapter = new WagmiAdapter({ projectId, networks: [mainnet, sepolia] })
const solanaAdapter = new SolanaAdapter()
const bitcoinAdapter = new BitcoinAdapter()
const tronAdapter = new TronAdapter()
// Create the AppKit instance
const modal = createAppKit({
projectId,
adapters: [wagmiAdapter, solanaAdapter, bitcoinAdapter, tronAdapter],
networks: [mainnet, sepolia, bitcoin, bitcoinTestnet],
metadata: {
name: 'My dApp',
description: 'My dApp with Openloop',
url: 'https://my-dapp.com',
icons: ['https://my-dapp.com/icon.png'],
},
customWallets: [
{
id: 'openloop-connect',
name: 'Openloop Connect',
homepage: 'https://crypto.haudi.jp/openloop/',
mobile_link: 'openloop://',
desktop_link: 'openloop://',
image_url: 'https://crypto.haudi.jp/openloop/images/logo/openloop-icon.png',
},
],
})| Criterion | WcTransport | AppKit |
|---|---|---|
| APDU-level control | ✓ Full control | ✗ Abstracted away |
| Ease of adoption | Somewhat involved | Easy |
| UI components | None (build your own) | Modal included |
| Multi-chain | Manual switching | Automatic switching |
| EIP-7702 | ✓ Supported | ✗ Not supported |
| Supported wallets | Openloop Connect only | Every WC-compatible wallet |
Recommendation: - Integrating with the Openloop device is the main goal → WcTransport - You want to support multiple wallets → AppKit
exchange timeout of WcTransport
depends on the constraints of the WalletConnect relay