코인 레벨 QR 코드
코인 레벨 QR 코드는 다른 QR 서명 기기와 같은 UR 타입을 사용합니다(Blockchain Commons 및 Keystone 레지스트리를 따릅니다). 따라서 이미 Keystone 방식의 계정을 가져올 수 있는 지갑이라면 대개 수정 없이 읽을 수 있습니다. 앱에서는 Connect › Coin Level로 들어간 뒤 체인을 선택합니다.
| 체인 | 기본 내보내기 | 대안 |
|---|---|---|
| Bitcoin | 계정 xpub 하나를 담은 ur:crypto-account(Native SegWit, Taproot, Nested SegWit, Legacy 중 선택) | 일반 주소, 멀티시그 공동 서명자 xpub |
| EVM | ur:crypto-hdkey, m/44'/60'/0' 계정 노드 | 일반 주소(인덱스 0) |
| Solana | 프리셋 키 하나를 담은 ur:crypto-multi-accounts | 일반 주소 |
| TRON | 임의 인덱스 m/44'/195'/0'/0/i의 일반 주소 | — |
각 QR 코드에는 앱에 디코딩된 내용을 보여 주는 Payload 섹션이 있으며, 이 페이지의 JSON 보기와 정확히 같습니다. 정수 CBOR 키는 레지스트리 필드 이름으로 표시되며, 지문은 32비트 정수입니다(1942346250 = 0x73C5DA0A).
Bitcoin: crypto-account
crypto-account = {
1: master-fingerprint (uint32),
2: [ output-descriptor ] ; exactly one
}
output-descriptor = script tag(s) wrapping 303(crypto-hdkey)
| 주소 유형 | 계정 경로 | 디스크립터 태그 | 스크립트 |
|---|---|---|---|
| Native SegWit | m/84'/0'/0' | 404(303(key)) | wpkh |
| Taproot | m/86'/0'/0' | 409(303(key)) | tr (BIP86) |
| Nested SegWit | m/49'/0'/0' | 400(404(303(key))) | sh(wpkh) |
| Legacy | m/44'/0'/0' | 403(303(key)) | pkh |
안쪽의 crypto-hdkey에는 계정 공개 키(33바이트), 체인 코드, 소스 지문과 깊이를 포함한 origin 경로, 부모 지문, 그리고 이름 BitTool Signer가 들어 있습니다. 수신 주소는 …/0/i, 잔돈 주소는 …/1/i입니다.
직접 파싱한다면 두 가지를 주의하세요.
- 출력 디스크립터는 Keystone과 Sparrow가 쓰는 방식대로, BCR-2020-015의
308(crypto-output)래퍼 없이 작성됩니다. - Taproot는 tweak을 적용하지 않은 내부 키와 함께 태그 409 (
tr)를 사용합니다. 주소를 파생할 때 BIP86 tweak을 적용하세요. 오래된 레지스트리 라이브러리는 태그 409를 알지 못해 조용히 버립니다(그래서 아래 예제 디코더는 태그를 직접 읽습니다).
- Native SegWit
- Taproot
- Nested SegWit
- Legacy
{
"master-fingerprint": 1942346250,
"output-descriptors": [
{
"witness-public-key-hash": {
"crypto-hdkey": {
"is-private": false,
"key-data": "02707a62fdacc26ea9b63b1c197906f56ee0180d0bcf1966e1a2da34f5f3a09a9b",
"use-info": {
"crypto-coin-info": {}
},
"origin": {
"crypto-keypath": {
"components": [
84,
true,
0,
true,
0,
true
],
"source-fingerprint": 1942346250,
"depth": 3
}
},
"chain-code": "4a53a0ab21b9dc95869c4e92a161194e03c0ef3ff5014ac692f433c4765490fc",
"parent-fingerprint": 2129865691,
"name": "BitTool Signer",
"note": "BitTool Signer public account"
}
}
}
]
}
UR:CRYPTO-ACCOUNT/OEADCYJKSKTNBKAOLYTAADMWTAADDLPDAOWKAXHDCLAOJOKNIDZCPSSAJTPTRPFRCECFKKAMYKJTVTCSBTBDTKCFIYVYOETNEEYKWFNBNYNDAHTAADEHNBAMTAADDYOTADLNCSGHYKAEYKAEYKAOCYJKSKTNBKAXAXAAHDCXGEGUNBPYCLRHUOMDLNNSGLMOOYHSCFGLAXRTWSFHYKADGESWMOWKEOSSKOGHMHZTAYCYKBWFDNUYASJTFWINJYGHJLJLJZCXGUINIOJTIHJPBKKSCAFWINJYGHJLJLJZCXGUINIOJTIHJPCXJOKPIDJZINIACXHSIAIAJLKPJTJYDPCXPECF
{
"master-fingerprint": 1942346250,
"output-descriptors": [
{
"taproot": {
"crypto-hdkey": {
"is-private": false,
"key-data": "03418278a2885c8bb98148158d1474634097a179c642f23cf1cc04da629ac6f0fb",
"use-info": {
"crypto-coin-info": {}
},
"origin": {
"crypto-keypath": {
"components": [
86,
true,
0,
true,
0,
true
],
"source-fingerprint": 1942346250,
"depth": 3
}
},
"chain-code": "c61a8f27e98182314d2444da3e600eb5836ec8ad183c86c311f95df8082b18aa",
"parent-fingerprint": 55734490,
"name": "BitTool Signer",
"note": "BitTool Signer public account"
}
}
}
]
}
UR:CRYPTO-ACCOUNT/OEADCYJKSKTNBKAOLYTAADNLTAADDLPDAOWKAXHDCLAXFPLFKSOELOHHLURHLYFDBZLGBBJYIAFZMSOYKKSWFWWZFNWNSFAATNIDNYSWWTZOAHTAADEHNBAMTAADDYOTADLNCSHFYKAEYKAEYKAOCYJKSKTNBKAXAXAAHDCXSWCYMYDIWLLYLFEHGTDKFYTNFMHNBARELSJTSPPMCSFNLNSRBYYTHLYAAYDNCSPKAYCYAXGMJOTNASJTFWINJYGHJLJLJZCXGUINIOJTIHJPBKKSCAFWINJYGHJLJLJZCXGUINIOJTIHJPCXJOKPIDJZINIACXHSIAIAJLKPJTJYKSBBMKFG
{
"master-fingerprint": 1942346250,
"output-descriptors": [
{
"script-hash": {
"witness-public-key-hash": {
"crypto-hdkey": {
"is-private": false,
"key-data": "02f1f347891b20f7568eae3ec9869fbfb67bcab6f358326f10ecc42356bd55939d",
"use-info": {
"crypto-coin-info": {}
},
"origin": {
"crypto-keypath": {
"components": [
49,
true,
0,
true,
0,
true
],
"source-fingerprint": 1942346250,
"depth": 3
}
},
"chain-code": "6eaae365ae0e0a0aab84325cfe7cd76c3b909035f889e7d3f1b847a9a0797ecb",
"parent-fingerprint": 1023803253,
"name": "BitTool Signer",
"note": "BitTool Signer public account"
}
}
}
}
]
}
UR:CRYPTO-ACCOUNT/OEADCYJKSKTNBKAOLYTAADMHTAADMWTAADDLPDAOWKAXHDCLAOWNWFFLLDCWCXYLHFMNPLFMSOLNNERSRPKGSGRPWFHDEYJLBEWPSSCNHFRYGOMUNTAHTAADEHNBAMTAADDYOTADLNCSEHYKAEYKAEYKAOCYJKSKTNBKAXAXAAHDCXJTPKVLIHPLBABKBKPYLREYHHZEKETSJZFRMHMHECYALDVDTEWNROFLPTNBKKKBSBAYCYFSAHZMKPASJTFWINJYGHJLJLJZCXGUINIOJTIHJPBKKSCAFWINJYGHJLJLJZCXGUINIOJTIHJPCXJOKPIDJZINIACXHSIAIAJLKPJTJYZCOYAYKE
{
"master-fingerprint": 1942346250,
"output-descriptors": [
{
"public-key-hash": {
"crypto-hdkey": {
"is-private": false,
"key-data": "03774c910fcf07fa96886ea794f0d5caed9afe30b44b83f7e213bb92930e7df4bd",
"use-info": {
"crypto-coin-info": {}
},
"origin": {
"crypto-keypath": {
"components": [
44,
true,
0,
true,
0,
true
],
"source-fingerprint": 1942346250,
"depth": 3
}
},
"chain-code": "3da4bc190a2680111d31fadfdc905f2a7f6ce77c6f109919116f253d43445219",
"parent-fingerprint": 358337113,
"name": "BitTool Signer",
"note": "BitTool Signer public account"
}
}
}
]
}
UR:CRYPTO-ACCOUNT/OEADCYJKSKTNBKAOLYTAADMUTAADDLPDAOWKAXHDCLAXKTGSMEBSTKATZSMTLOJTOSMWWTTLSGWENYZEDYQZGRLSYLVOBWRKMOMUBAKIWKRYAHTAADEHNBAMTAADDYOTADLNCSDWYKAEYKAEYKAOCYJKSKTNBKAXAXAAHDCXFSOXRFCFBKDSLABYCAEHZSURUOMHHEDRLBJZVDKEJLBENLCFBYJLDAFSFXFYGMCFAYCYBZHPSGHKASJTFWINJYGHJLJLJZCXGUINIOJTIHJPBKKSCAFWINJYGHJLJLJZCXGUINIOJTIHJPCXJOKPIDJZINIACXHSIAIAJLKPJTJYGUTNWNDL
Bitcoin 멀티시그 공동 서명자
멀티시그 지갑용으로 BitTool Signer는 BIP48 경로의 공동 서명자 키 하나를 crypto-account로 내보냅니다.
| 유형 | 계정 경로 | 디스크립터 태그 |
|---|---|---|
| P2WSH | m/48'/0'/0'/2' | 401(303(key)) (wsh) |
| P2SH-P2WSH | m/48'/0'/0'/1' | 400(401(303(key))) (sh(wsh)) |
이것은 참여자 한 명의 키일 뿐입니다. 임계값, 다른 공동 서명자, 최종 주소는 포함되지 않습니다. 전체 정책(예: 정렬된 2-of-3 wsh(sortedmulti(…)))은 지갑이 모든 공동 서명자의 내보내기를 모아 구성합니다.
P2WSH 공동 서명자 테스트 벡터
{
"master-fingerprint": 1942346250,
"output-descriptors": [
{
"witness-script-hash": {
"crypto-hdkey": {
"is-private": false,
"key-data": "021a3bf5fbf737d0f36993fd46dc4913093beb532d654fe0dfd98bd27585dc9f29",
"use-info": {
"crypto-coin-info": {}
},
"origin": {
"crypto-keypath": {
"components": [
48,
true,
0,
true,
0,
true,
2,
true
],
"source-fingerprint": 1942346250,
"depth": 4
}
},
"chain-code": "bba0c7ca160a870efeb940ab90d0f4284fea1b5e0d2117677e823fc37e2d5763",
"parent-fingerprint": 485660438,
"name": "BitTool Signer",
"note": "BitTool Signer public account"
}
}
}
]
}
UR:CRYPTO-ACCOUNT/OEADCYJKSKTNBKAOLYTAADMETAADDLPDAOWKAXHDCLAOCYFRYKZOYLEMTIWFINMUZCFGUOGABWASFRWMGUDPIHGWVTURTALUTDKPLPUONEDTAHTAADEHNBAMTAADDYOTADLOCSDYYKAEYKAEYKAOYKAOCYJKSKTNBKAXAAAAHDCXRKNBSTSGCMBKLTBAZERHFZPYMHTIWKDEGWWDCWHYBTCLCHIOKBLFFHSRKBDPHGIAAYCYCEWZMSCMASJTFWINJYGHJLJLJZCXGUINIOJTIHJPBKKSCAFWINJYGHJLJLJZCXGUINIOJTIHJPCXJOKPIDJZINIACXHSIAIAJLKPJTJYVEGWJLEO
EVM: crypto-hdkey
EVM 내보내기는 계정 노드 m/44'/60'/0'을 최상위 crypto-hdkey(EIP-4527)로 담은 것으로, MetaMask, Rabby, imToken 등이 가져오는 형식입니다.
| 키 | 필드 | 값 |
|---|---|---|
| 2 | is-private | false |
| 3 | key-data | 33바이트 압축 공개 키 |
| 4 | chain-code | 32바이트 |
| 5 | use-info | 305({1: 60}) |
| 6 | origin | 304({1: [44, true, 60, true, 0, true], 2: fingerprint, 3: 3}) |
| 7 | children | 304({1: [0, false, [], false]}), 즉 0/* |
| 8 | parent-fingerprint | uint32 |
| 9, 10 | name, note | "BitTool Signer", "BitTool Signer public account" |
주소는 …/0/i로 파생하세요. 같은 키를 지원하는 모든 EVM 네트워크에서 사용합니다.
{
"is-private": false,
"key-data": "02eae4b876a8696134b868f88cc2f51f715f2dbedb7446b8e6edf3d4541c4eb67b",
"use-info": {
"crypto-coin-info": {
"type": 60
}
},
"origin": {
"crypto-keypath": {
"components": [
44,
true,
60,
true,
0,
true
],
"source-fingerprint": 1942346250,
"depth": 3
}
},
"chain-code": "d882718b7a42806803eeb17f7483f20620611adb88fc943c898dc5aba94c2819",
"parent-fingerprint": 3543024904,
"children": {
"crypto-keypath": {
"components": [
0,
false,
[],
false
]
}
},
"name": "BitTool Signer",
"note": "BitTool Signer public account"
}
UR:CRYPTO-HDKEY/PTAOWKAXHDCLAOWDVEROKOPDINHSEEROISYALKSAYKCTJSHEDPRNUYJYFGROVAWEWFTYGHCEGLRPKGAHTAADEHOYADCSFNAMTAADDYOTADLNCSDWYKCSFNYKAEYKAOCYJKSKTNBKAXAXAAHDCXTPLFJSLUKNFWLAISAXWYPALBJYLSWZAMCXHSCYUYLOZTMWFNLDLGSKPYPTGSDECFAYCYTEDMFEAYATTAADDYOYADLRAEWKLAWKASJTFWINJYGHJLJLJZCXGUINIOJTIHJPBKKSCAFWINJYGHJLJLJZCXGUINIOJTIHJPCXJOKPIDJZINIACXHSIAIAJLKPJTJYMUFLSAVD
Solana: crypto-multi-accounts
Solana 키는 모든 단계가 강화(hardened) 파생이므로 여기서 더 파생할 수 있는 것이 없습니다. 내보내기에는 앱에서 선택한 프리셋 키 하나가 32바이트 Ed25519 키를 담은 crypto-hdkey로 들어 있습니다.
crypto-multi-accounts = {
1: master-fingerprint,
2: [ 303({ 2: false, 3: <32-byte public key>, 5: 305({1: 501}), 6: 304(<path>), 9: name, 10: note }) ],
3: "BitTool Signer", ; device
5: "1.0.0" ; version
}
| 프리셋 | 경로 |
|---|---|
| Standard | m/44'/501'/0'/0' |
| Trust | m/44'/501'/0' |
| Legacy | m/44'/501' |
주소는 32바이트 키를 Base58로 인코딩한 값입니다.
{
"master-fingerprint": 1942346250,
"keys": [
{
"crypto-hdkey": {
"is-private": false,
"key-data": "f036276246a75b9de3349ed42b15e232f6518fc20f5fcd4f1d64e81f9bd258f7",
"use-info": {
"crypto-coin-info": {
"type": 501
}
},
"origin": {
"crypto-keypath": {
"components": [
44,
true,
501,
true,
0,
true,
0,
true
],
"source-fingerprint": 1942346250,
"depth": 4
}
},
"name": "BitTool Signer",
"note": "BitTool Signer public account"
}
}
],
"device": "BitTool Signer",
"version": "1.0.0"
}
UR:CRYPTO-MULTI-ACCOUNTS/OXADCYJKSKTNBKAOLYTAADDLOLAOWKAXHDCXWTENDIIDFGOSHPNTVLEENNTYDNBZVOEYYNGYMYSABSHESNGWCAIEVSCTNDTDHDYLAHTAADEHOYADCFADYKAMTAADDYOTADLOCSDWYKCFADYKYKAEYKAEYKAOCYJKSKTNBKAXAAASJTFWINJYGHJLJLJZCXGUINIOJTIHJPBKKSCAFWINJYGHJLJLJZCXGUINIOJTIHJPCXJOKPIDJZINIACXHSIAIAJLKPJTJYAXJTFWINJYGHJLJLJZCXGUINIOJTIHJPAHIHEHDMDYDMDYMSFNHFVA
일반 주소
Address 옵션은 주소만 QR 텍스트로 내보냅니다. URI 스킴, 금액, UR 래퍼가 없습니다. 키를 가져올 수 없는 보기 전용 지갑을 위한 옵션입니다. TRON은 항상 이 방식으로, 사용자가 고른 인덱스(m/44'/195'/0'/0/i, 0 ≤ i < 231)의 주소를 내보냅니다.
| 내보내기 | QR 텍스트 |
|---|---|
| Bitcoin (Native SegWit, 인덱스 0) | bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu |
| EVM (인덱스 0) | 0x9858EfFD232B4033E47d90003D41EC34EcaEda94 |
| Solana (standard) | HAgk14JpMQLgt6rVgv7cBQFJWFto5Dqxi472uT3DKpqk |
| TRON 인덱스 0 | TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH |
| TRON 인덱스 5 | TBdYXtwq18cAhi1BA574TrP6tw2G86anu1 |
주소만으로 페어링한 지갑은 서명 요청에 파생 데이터를 넣을 수 없습니다. BitTool Signer는 EVM 인덱스 0과 TRON 인덱스 0–99에 한해 주소만 있는 요청도 매칭할 수 있지만, 서명할 계획이라면 키 내보내기를 사용하세요.
모든 형식 디코딩
디스크립터 태그를 보존하도록 cborg를 사용해 모든 코인 레벨 내보내기를 처리하는 함수 하나입니다.
import { URDecoder } from '@ngraveio/bc-ur';
import { decode, Tagged } from 'cborg';
import { HDKey } from '@scure/bip32';
import { base58 } from '@scure/base';
// Output-descriptor tags (BCR-2020-010) → script names.
const SCRIPT = { 400: 'sh', 401: 'wsh', 403: 'pkh', 404: 'wpkh', 409: 'tr' };
const options = { useMaps: true, strict: true, tags: Tagged.preserve(303, 304, 305, 400, 401, 403, 404, 409) };
const fingerprint = (n) => n.toString(16).padStart(8, '0');
const pathOf = (keypath) => { // Tagged(304, Map{1: [index, hardened, …]})
const c = keypath.value.get(1), parts = [];
for (let i = 0; i < c.length; i += 2) parts.push(`${c[i]}${c[i + 1] ? "'" : ''}`);
return `m/${parts.join('/')}`;
};
const accountOf = (hdkey) => { // crypto-hdkey map: 3 key, 4 chain code, 6 origin, 8 parent fp
const origin = hdkey.get(6).value.get(1);
return {
path: pathOf(hdkey.get(6)),
masterFingerprint: fingerprint(hdkey.get(6).value.get(2)),
xpub: new HDKey({ publicKey: hdkey.get(3), chainCode: hdkey.get(4), depth: origin.length / 2,
index: origin.at(-2) + (origin.at(-1) ? 0x80000000 : 0), parentFingerprint: hdkey.get(8) }).publicExtendedKey,
};
};
/** Reads any coin-level export from BitTool Signer. */
export function readCoinLevelQR(frames) {
if (frames.length === 1 && !/^ur:/i.test(frames[0])) return { kind: 'address', address: frames[0].trim() };
const decoder = new URDecoder();
for (const frame of frames) decoder.receivePart(frame);
if (!decoder.isSuccess()) throw new Error('Incomplete or invalid QR');
const ur = decoder.resultUR();
const cbor = decode(ur.cbor, options);
switch (ur.type) {
case 'crypto-account': { // Bitcoin: {1: master fp, 2: [output descriptor]}
let node = cbor.get(2)[0];
const script = [];
while (node.tag !== 303) { script.push(SCRIPT[node.tag]); node = node.value; } // e.g. sh → wpkh
return { kind: 'bitcoin', script: script.join('-'), ...accountOf(node.value) };
}
case 'crypto-hdkey': // EVM account node
return { kind: 'evm', ...accountOf(cbor) };
case 'crypto-multi-accounts': { // Solana: {1: master fp, 2: [crypto-hdkey with a 32-byte key]}
const key = cbor.get(2)[0].value;
return { kind: 'solana', masterFingerprint: fingerprint(cbor.get(1)), path: pathOf(key.get(6)), address: base58.encode(key.get(3)) };
}
default:
throw new Error(`Not a pairing QR: ${ur.type}`);
}
}