@qrkit/core 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -26
- package/dist/index.cjs +303 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +46 -4
- package/dist/index.d.ts +46 -4
- package/dist/index.js +296 -61
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Framework-agnostic protocol core for QR-based airgapped wallet flows. Designed for use in browser-based dApps.
|
|
4
4
|
|
|
5
|
-
Handles the ERC-4527 / UR / CBOR stack: decoding scanned QR exports, deriving EVM addresses, building sign requests, and parsing signature responses. No DOM, no React, no external services.
|
|
5
|
+
Handles the ERC-4527 / UR / CBOR stack: decoding scanned QR exports, deriving EVM and BTC addresses, building sign requests, carrying PSBTs, and parsing signature responses. No DOM, no React, no external services.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -14,41 +14,50 @@ pnpm add @qrkit/core
|
|
|
14
14
|
|
|
15
15
|
### 1. Parse the connection QR from the wallet
|
|
16
16
|
|
|
17
|
-
Scan the `crypto-hdkey` or `crypto-
|
|
17
|
+
Scan the `crypto-hdkey`, `crypto-account`, or `crypto-multi-accounts` QR exported by the hardware wallet, then derive the accounts you need:
|
|
18
18
|
|
|
19
19
|
```ts
|
|
20
|
-
import { parseConnection } from
|
|
20
|
+
import { parseConnection } from "@qrkit/core";
|
|
21
21
|
|
|
22
22
|
// scannedUR comes from a QR scanner — { type: string, cbor: Uint8Array }
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
account.
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
const accounts = parseConnection(scannedUR, { chains: ["evm", "btc"] });
|
|
24
|
+
const evm = accounts.find((account) => account.chain === "evm");
|
|
25
|
+
const btc = accounts.find((account) => account.chain === "btc");
|
|
26
|
+
|
|
27
|
+
evm?.address; // EIP-55 checksummed address
|
|
28
|
+
btc?.address; // BTC address, e.g. bc1q...
|
|
29
|
+
btc?.scriptType; // 'p2wpkh' | 'p2sh-p2wpkh' | 'p2pkh'
|
|
30
|
+
evm?.sourceFingerprint; // master key fingerprint — required for signing
|
|
28
31
|
```
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
`parseConnection()` can return multiple accounts from one scan, including mixed EVM and BTC results from `crypto-multi-accounts`.
|
|
34
|
+
|
|
35
|
+
### 2. Build an EVM sign request
|
|
31
36
|
|
|
32
37
|
Encode a message as animated UR parts to display as a QR code for the wallet to scan:
|
|
33
38
|
|
|
34
39
|
```ts
|
|
35
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
buildEthSignRequestURParts,
|
|
42
|
+
buildEthSignRequestUR,
|
|
43
|
+
EthDataType,
|
|
44
|
+
} from "@qrkit/core";
|
|
36
45
|
|
|
37
46
|
// Animated QR (multiple parts for long messages)
|
|
38
47
|
const parts = buildEthSignRequestURParts({
|
|
39
|
-
signData: message,
|
|
48
|
+
signData: message, // string (UTF-8 encoded) or Uint8Array (raw bytes)
|
|
40
49
|
dataType: EthDataType.PersonalMessage, // defaults to PersonalMessage if omitted
|
|
41
|
-
address:
|
|
42
|
-
sourceFingerprint:
|
|
43
|
-
})
|
|
50
|
+
address: evm.address,
|
|
51
|
+
sourceFingerprint: evm.sourceFingerprint,
|
|
52
|
+
});
|
|
44
53
|
// parts is string[] — cycle through them to animate the QR
|
|
45
54
|
|
|
46
55
|
// Single-frame QR (short messages)
|
|
47
56
|
const ur = buildEthSignRequestUR({
|
|
48
57
|
signData: message,
|
|
49
|
-
address:
|
|
50
|
-
sourceFingerprint:
|
|
51
|
-
})
|
|
58
|
+
address: evm.address,
|
|
59
|
+
sourceFingerprint: evm.sourceFingerprint,
|
|
60
|
+
});
|
|
52
61
|
```
|
|
53
62
|
|
|
54
63
|
### 3. Parse the wallet's signature response
|
|
@@ -56,21 +65,72 @@ const ur = buildEthSignRequestUR({
|
|
|
56
65
|
After the user scans the wallet's response QR, decode the signature:
|
|
57
66
|
|
|
58
67
|
```ts
|
|
59
|
-
import { parseEthSignature } from
|
|
68
|
+
import { parseEthSignature } from "@qrkit/core";
|
|
60
69
|
|
|
61
|
-
const signature = parseEthSignature(scannedResponseUR)
|
|
70
|
+
const signature = parseEthSignature(scannedResponseUR);
|
|
62
71
|
// → '0x...' hex string, ready for ethers / viem
|
|
63
72
|
```
|
|
64
73
|
|
|
74
|
+
### 4. Build a BTC message sign request
|
|
75
|
+
|
|
76
|
+
Direct BTC message signing uses `btc-sign-request` and returns `btc-signature`:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { buildBtcSignRequestURParts, parseBtcSignature } from "@qrkit/core";
|
|
80
|
+
|
|
81
|
+
const parts = buildBtcSignRequestURParts({
|
|
82
|
+
signData: "Hello Bitcoin",
|
|
83
|
+
address: btc.address,
|
|
84
|
+
scriptType: btc.scriptType,
|
|
85
|
+
sourceFingerprint: btc.sourceFingerprint,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const result = parseBtcSignature(scannedResponseUR);
|
|
89
|
+
result.signature; // base64 compact Bitcoin message signature (65 bytes)
|
|
90
|
+
result.publicKey; // compressed public key hex
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 5. Carry a BTC PSBT
|
|
94
|
+
|
|
95
|
+
Bitcoin transaction signing, and BIP-322-style message signing, use `crypto-psbt`.
|
|
96
|
+
qrkit carries PSBT bytes over QR; the airgapped wallet signs offline and returns a signed `crypto-psbt`.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { buildCryptoPsbtURParts, parseCryptoPsbt } from "@qrkit/core";
|
|
100
|
+
|
|
101
|
+
const parts = buildCryptoPsbtURParts(unsignedPsbtHex);
|
|
102
|
+
// render parts as QR codes
|
|
103
|
+
|
|
104
|
+
const signed = parseCryptoPsbt(scannedResponseUR);
|
|
105
|
+
signed.psbtHex; // signed PSBT hex
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Note: BIP-322-style message signing is also carried as `crypto-psbt`, but wallet review UX varies. Some wallets present it as a Bitcoin message flow, while others show only the proving address or a generic PSBT review.
|
|
109
|
+
|
|
65
110
|
## API
|
|
66
111
|
|
|
67
|
-
| Export
|
|
68
|
-
|
|
69
|
-
| `parseConnection(ur, options)`
|
|
70
|
-
| `buildEthSignRequestURParts(params)` | Build animated UR parts for a sign request (`EthSignRequestParams`)
|
|
71
|
-
| `buildEthSignRequestUR(params)`
|
|
72
|
-
| `EthDataType`
|
|
73
|
-
| `parseEthSignature(ur)`
|
|
112
|
+
| Export | Description |
|
|
113
|
+
| ------------------------------------ | ------------------------------------------------------------------------------- |
|
|
114
|
+
| `parseConnection(ur, options)` | Parse a connection UR into `Account[]` |
|
|
115
|
+
| `buildEthSignRequestURParts(params)` | Build animated UR parts for a sign request (`EthSignRequestParams`) |
|
|
116
|
+
| `buildEthSignRequestUR(params)` | Build a single-frame UR for a sign request (`EthSignRequestParams`) |
|
|
117
|
+
| `EthDataType` | Constants for ERC-4527 data types (1–4) |
|
|
118
|
+
| `parseEthSignature(ur)` | Decode an `eth-signature` UR into a `0x...` hex string |
|
|
119
|
+
| `buildBtcSignRequestURParts(params)` | Build animated UR parts for a BTC message sign request (`BtcSignRequestParams`) |
|
|
120
|
+
| `buildBtcSignRequestUR(params)` | Build a single-frame BTC message sign request UR |
|
|
121
|
+
| `BtcDataType` | Constants for BTC sign request data types |
|
|
122
|
+
| `parseBtcSignature(ur)` | Decode a `btc-signature` UR into a base64 signature and public key |
|
|
123
|
+
| `buildCryptoPsbtURParts(psbt)` | Build animated UR parts for a `crypto-psbt` request |
|
|
124
|
+
| `buildCryptoPsbtUR(psbt)` | Build a single-frame `crypto-psbt` UR |
|
|
125
|
+
| `parseCryptoPsbt(ur)` | Decode a `crypto-psbt` UR into PSBT bytes and hex |
|
|
126
|
+
|
|
127
|
+
## Examples
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
pnpm --filter @qrkit/core example:eth
|
|
131
|
+
pnpm --filter @qrkit/core example:btc-message
|
|
132
|
+
pnpm --filter @qrkit/core example:btc-psbt
|
|
133
|
+
```
|
|
74
134
|
|
|
75
135
|
## License
|
|
76
136
|
|
package/dist/index.cjs
CHANGED
|
@@ -30,14 +30,150 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
BtcDataType: () => BtcDataType,
|
|
33
34
|
EthDataType: () => EthDataType,
|
|
35
|
+
buildBtcSignRequestUR: () => buildBtcSignRequestUR,
|
|
36
|
+
buildBtcSignRequestURParts: () => buildBtcSignRequestURParts,
|
|
37
|
+
buildCryptoPsbtUR: () => buildCryptoPsbtUR,
|
|
38
|
+
buildCryptoPsbtURParts: () => buildCryptoPsbtURParts,
|
|
34
39
|
buildEthSignRequestUR: () => buildEthSignRequestUR,
|
|
35
40
|
buildEthSignRequestURParts: () => buildEthSignRequestURParts,
|
|
41
|
+
parseBtcSignature: () => parseBtcSignature,
|
|
36
42
|
parseConnection: () => parseConnection,
|
|
43
|
+
parseCryptoPsbt: () => parseCryptoPsbt,
|
|
37
44
|
parseEthSignature: () => parseEthSignature
|
|
38
45
|
});
|
|
39
46
|
module.exports = __toCommonJS(index_exports);
|
|
40
47
|
|
|
48
|
+
// src/bytes.ts
|
|
49
|
+
function bytesToBase64(bytes) {
|
|
50
|
+
let binary = "";
|
|
51
|
+
for (const byte of bytes) {
|
|
52
|
+
binary += String.fromCharCode(byte);
|
|
53
|
+
}
|
|
54
|
+
return btoa(binary);
|
|
55
|
+
}
|
|
56
|
+
function bytesToHex(bytes) {
|
|
57
|
+
return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
58
|
+
}
|
|
59
|
+
function hexToBytes(hex) {
|
|
60
|
+
const normalized = hex.trim().replace(/^0x/i, "");
|
|
61
|
+
if (normalized.length % 2 !== 0 || !/^[0-9a-f]*$/i.test(normalized)) {
|
|
62
|
+
throw new Error("Invalid hex string");
|
|
63
|
+
}
|
|
64
|
+
return new Uint8Array(
|
|
65
|
+
normalized.match(/.{2}/g)?.map((byte) => parseInt(byte, 16)) ?? []
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/btc/address.ts
|
|
70
|
+
var import_base = require("@scure/base");
|
|
71
|
+
var import_legacy = require("@noble/hashes/legacy.js");
|
|
72
|
+
var import_sha2 = require("@noble/hashes/sha2.js");
|
|
73
|
+
var base58check = (0, import_base.createBase58check)(import_sha2.sha256);
|
|
74
|
+
function hash160(bytes) {
|
|
75
|
+
return (0, import_legacy.ripemd160)((0, import_sha2.sha256)(bytes));
|
|
76
|
+
}
|
|
77
|
+
function pubKeyToP2wpkh(compressedPubKey) {
|
|
78
|
+
const h = hash160(compressedPubKey);
|
|
79
|
+
const words = import_base.bech32.toWords(h);
|
|
80
|
+
return import_base.bech32.encode("bc", [0, ...words]);
|
|
81
|
+
}
|
|
82
|
+
function pubKeyToP2shP2wpkh(compressedPubKey) {
|
|
83
|
+
const h = hash160(compressedPubKey);
|
|
84
|
+
const redeemScript = new Uint8Array(22);
|
|
85
|
+
redeemScript[0] = 0;
|
|
86
|
+
redeemScript[1] = 20;
|
|
87
|
+
redeemScript.set(h, 2);
|
|
88
|
+
const scriptHash = hash160(redeemScript);
|
|
89
|
+
const payload = new Uint8Array(21);
|
|
90
|
+
payload[0] = 5;
|
|
91
|
+
payload.set(scriptHash, 1);
|
|
92
|
+
return base58check.encode(payload);
|
|
93
|
+
}
|
|
94
|
+
function pubKeyToP2pkh(compressedPubKey) {
|
|
95
|
+
const h = hash160(compressedPubKey);
|
|
96
|
+
const payload = new Uint8Array(21);
|
|
97
|
+
payload[0] = 0;
|
|
98
|
+
payload.set(h, 1);
|
|
99
|
+
return base58check.encode(payload);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/btc/deriveAccount.ts
|
|
103
|
+
function firstChild(accountKey) {
|
|
104
|
+
return accountKey.deriveChild(0).deriveChild(0);
|
|
105
|
+
}
|
|
106
|
+
function scriptTypeFromPurpose(purpose) {
|
|
107
|
+
if (purpose === 84) return "p2wpkh";
|
|
108
|
+
if (purpose === 49) return "p2sh-p2wpkh";
|
|
109
|
+
if (purpose === 44) return "p2pkh";
|
|
110
|
+
return void 0;
|
|
111
|
+
}
|
|
112
|
+
function deriveAddress(pubKey, scriptType) {
|
|
113
|
+
if (scriptType === "p2wpkh") return pubKeyToP2wpkh(pubKey);
|
|
114
|
+
if (scriptType === "p2sh-p2wpkh") return pubKeyToP2shP2wpkh(pubKey);
|
|
115
|
+
return pubKeyToP2pkh(pubKey);
|
|
116
|
+
}
|
|
117
|
+
function deriveBtcAccount(parsed) {
|
|
118
|
+
const results = [];
|
|
119
|
+
for (const entry of parsed) {
|
|
120
|
+
const { hdKey, purpose, coinType, sourceFingerprint, name } = entry;
|
|
121
|
+
if (coinType !== 0) continue;
|
|
122
|
+
const scriptType = scriptTypeFromPurpose(purpose);
|
|
123
|
+
if (!scriptType) continue;
|
|
124
|
+
const child = firstChild(hdKey);
|
|
125
|
+
if (!child.publicKey) continue;
|
|
126
|
+
results.push({
|
|
127
|
+
address: deriveAddress(child.publicKey, scriptType),
|
|
128
|
+
scriptType,
|
|
129
|
+
publicKey: bytesToHex(child.publicKey),
|
|
130
|
+
sourceFingerprint,
|
|
131
|
+
device: name
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return results;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/eth/address.ts
|
|
138
|
+
var import_sha3 = require("@noble/hashes/sha3.js");
|
|
139
|
+
var secp = __toESM(require("@noble/secp256k1"), 1);
|
|
140
|
+
function pubKeyToEthAddress(compressedPubKey) {
|
|
141
|
+
const uncompressed = secp.Point.fromBytes(compressedPubKey).toBytes(false);
|
|
142
|
+
const hash = (0, import_sha3.keccak_256)(uncompressed.slice(1));
|
|
143
|
+
const hex = [...hash.slice(12)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
144
|
+
return toChecksumAddress(hex);
|
|
145
|
+
}
|
|
146
|
+
function toChecksumAddress(hex) {
|
|
147
|
+
const checksumHash = (0, import_sha3.keccak_256)(new TextEncoder().encode(hex));
|
|
148
|
+
return "0x" + [...hex].map((c, i) => {
|
|
149
|
+
if (c >= "0" && c <= "9") return c;
|
|
150
|
+
const nibble = i % 2 === 0 ? checksumHash[Math.floor(i / 2)] >> 4 & 15 : checksumHash[Math.floor(i / 2)] & 15;
|
|
151
|
+
return nibble >= 8 ? c.toUpperCase() : c.toLowerCase();
|
|
152
|
+
}).join("");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/eth/deriveAccount.ts
|
|
156
|
+
function firstChild2(accountKey) {
|
|
157
|
+
return accountKey.deriveChild(0).deriveChild(0);
|
|
158
|
+
}
|
|
159
|
+
function deriveEvmAccount(parsed) {
|
|
160
|
+
const results = [];
|
|
161
|
+
for (const entry of parsed) {
|
|
162
|
+
const { hdKey, purpose, coinType, type, sourceFingerprint, name } = entry;
|
|
163
|
+
const isEvm = purpose === 44 && coinType === 60 || purpose === void 0 && type === "xpub";
|
|
164
|
+
if (!isEvm) continue;
|
|
165
|
+
const child = firstChild2(hdKey);
|
|
166
|
+
if (!child.publicKey) continue;
|
|
167
|
+
results.push({
|
|
168
|
+
address: pubKeyToEthAddress(child.publicKey),
|
|
169
|
+
publicKey: bytesToHex(child.publicKey),
|
|
170
|
+
sourceFingerprint,
|
|
171
|
+
device: name
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return results;
|
|
175
|
+
}
|
|
176
|
+
|
|
41
177
|
// src/parseXpub.ts
|
|
42
178
|
var import_bip32 = require("@scure/bip32");
|
|
43
179
|
var import_cborg = require("cborg");
|
|
@@ -45,23 +181,46 @@ function get(m, k) {
|
|
|
45
181
|
if (m instanceof Map) return m.get(k);
|
|
46
182
|
return void 0;
|
|
47
183
|
}
|
|
184
|
+
function bytesFromCbor(value) {
|
|
185
|
+
if (value instanceof Uint8Array) return value;
|
|
186
|
+
if (value instanceof Map && value.get("type") === "Buffer") {
|
|
187
|
+
const data = value.get("data");
|
|
188
|
+
if (Array.isArray(data) && data.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
|
|
189
|
+
return new Uint8Array(data);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return void 0;
|
|
193
|
+
}
|
|
48
194
|
var passthrough = (v) => v;
|
|
49
195
|
function decodeCbor(cbor) {
|
|
196
|
+
const tags = new Proxy([], {
|
|
197
|
+
get(target, property, receiver) {
|
|
198
|
+
if (typeof property === "string" && /^\d+$/.test(property)) {
|
|
199
|
+
return passthrough;
|
|
200
|
+
}
|
|
201
|
+
return Reflect.get(target, property, receiver);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
50
204
|
return (0, import_cborg.decode)(cbor, {
|
|
51
205
|
useMaps: true,
|
|
52
|
-
tags
|
|
53
|
-
303: passthrough,
|
|
54
|
-
// crypto-hdkey
|
|
55
|
-
304: passthrough,
|
|
56
|
-
// crypto-keypath
|
|
57
|
-
305: passthrough
|
|
58
|
-
// crypto-coin-info
|
|
59
|
-
})
|
|
206
|
+
tags
|
|
60
207
|
});
|
|
61
208
|
}
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
209
|
+
function isCborMap(value) {
|
|
210
|
+
return value instanceof Map;
|
|
211
|
+
}
|
|
212
|
+
function assertCryptoHdKeyShape(value) {
|
|
213
|
+
if (!isCborMap(value)) {
|
|
214
|
+
throw new Error("crypto-hdkey entry must be a CBOR map");
|
|
215
|
+
}
|
|
216
|
+
if (!value.has(3) || !value.has(4)) {
|
|
217
|
+
throw new Error("crypto-hdkey missing key-data or chain-code");
|
|
218
|
+
}
|
|
219
|
+
return value;
|
|
220
|
+
}
|
|
221
|
+
function parseCryptoHdKey(map, raw, fallbackName) {
|
|
222
|
+
const keyData = bytesFromCbor(get(map, 3));
|
|
223
|
+
const chainCode = bytesFromCbor(get(map, 4));
|
|
65
224
|
if (!keyData || !chainCode) {
|
|
66
225
|
throw new Error("crypto-hdkey missing key-data or chain-code");
|
|
67
226
|
}
|
|
@@ -77,25 +236,28 @@ function parseCryptoHdKey(map, raw) {
|
|
|
77
236
|
}
|
|
78
237
|
sourceFingerprint = get(origin, 2);
|
|
79
238
|
}
|
|
80
|
-
const name = get(map, 9);
|
|
239
|
+
const name = get(map, 9) ?? fallbackName;
|
|
81
240
|
const hdKey = new import_bip32.HDKey({ publicKey: keyData, chainCode });
|
|
82
241
|
return { hdKey, type: "xpub", purpose, coinType, sourceFingerprint, name, raw };
|
|
83
242
|
}
|
|
84
243
|
function parseScannedUR(scanned) {
|
|
85
244
|
const { type, cbor } = scanned;
|
|
86
245
|
const raw = `ur:${type}`;
|
|
87
|
-
if (type !== "crypto-hdkey" && type !== "crypto-account") {
|
|
246
|
+
if (type !== "crypto-hdkey" && type !== "crypto-account" && type !== "crypto-multi-accounts") {
|
|
88
247
|
throw new Error(`Unsupported UR type: ${type}`);
|
|
89
248
|
}
|
|
90
249
|
const map = decodeCbor(cbor);
|
|
91
250
|
if (type === "crypto-hdkey") {
|
|
92
|
-
return parseCryptoHdKey(map, raw);
|
|
251
|
+
return parseCryptoHdKey(assertCryptoHdKeyShape(map), raw);
|
|
93
252
|
}
|
|
94
253
|
const accounts = map.get(2);
|
|
95
254
|
if (!Array.isArray(accounts) || accounts.length === 0) {
|
|
96
|
-
throw new Error(
|
|
255
|
+
throw new Error(`${type} contains no keys`);
|
|
97
256
|
}
|
|
98
|
-
|
|
257
|
+
const fallbackName = type === "crypto-multi-accounts" ? map.get(3) : void 0;
|
|
258
|
+
return accounts.map(
|
|
259
|
+
(entry) => parseCryptoHdKey(assertCryptoHdKeyShape(entry), raw, fallbackName)
|
|
260
|
+
);
|
|
99
261
|
}
|
|
100
262
|
function parseXpub(input) {
|
|
101
263
|
if (typeof input !== "string") {
|
|
@@ -115,48 +277,6 @@ function parseXpub(input) {
|
|
|
115
277
|
];
|
|
116
278
|
}
|
|
117
279
|
|
|
118
|
-
// src/eth/address.ts
|
|
119
|
-
var import_sha3 = require("@noble/hashes/sha3.js");
|
|
120
|
-
var secp = __toESM(require("@noble/secp256k1"), 1);
|
|
121
|
-
function pubKeyToEthAddress(compressedPubKey) {
|
|
122
|
-
const uncompressed = secp.Point.fromBytes(compressedPubKey).toBytes(false);
|
|
123
|
-
const hash = (0, import_sha3.keccak_256)(uncompressed.slice(1));
|
|
124
|
-
const hex = [...hash.slice(12)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
125
|
-
return toChecksumAddress(hex);
|
|
126
|
-
}
|
|
127
|
-
function toChecksumAddress(hex) {
|
|
128
|
-
const checksumHash = (0, import_sha3.keccak_256)(new TextEncoder().encode(hex));
|
|
129
|
-
return "0x" + [...hex].map((c, i) => {
|
|
130
|
-
if (c >= "0" && c <= "9") return c;
|
|
131
|
-
const nibble = i % 2 === 0 ? checksumHash[Math.floor(i / 2)] >> 4 & 15 : checksumHash[Math.floor(i / 2)] & 15;
|
|
132
|
-
return nibble >= 8 ? c.toUpperCase() : c.toLowerCase();
|
|
133
|
-
}).join("");
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// src/eth/deriveAccount.ts
|
|
137
|
-
function firstChild(accountKey) {
|
|
138
|
-
return accountKey.deriveChild(0).deriveChild(0);
|
|
139
|
-
}
|
|
140
|
-
function toHex(bytes) {
|
|
141
|
-
return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
142
|
-
}
|
|
143
|
-
function deriveEvmAccount(parsed) {
|
|
144
|
-
for (const entry of parsed) {
|
|
145
|
-
const { hdKey, purpose, coinType, type, sourceFingerprint, name } = entry;
|
|
146
|
-
const isEvm = purpose === 44 && coinType === 60 || purpose === void 0 && type === "xpub";
|
|
147
|
-
if (!isEvm) continue;
|
|
148
|
-
const child = firstChild(hdKey);
|
|
149
|
-
if (!child.publicKey) continue;
|
|
150
|
-
return {
|
|
151
|
-
address: pubKeyToEthAddress(child.publicKey),
|
|
152
|
-
publicKey: toHex(child.publicKey),
|
|
153
|
-
sourceFingerprint,
|
|
154
|
-
device: name
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
return void 0;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
280
|
// src/parseConnection.ts
|
|
161
281
|
var ALL_CHAINS = ["evm", "btc"];
|
|
162
282
|
function parseConnection(scannedUR, config = {}) {
|
|
@@ -164,11 +284,15 @@ function parseConnection(scannedUR, config = {}) {
|
|
|
164
284
|
const parsed = parseXpub(scannedUR);
|
|
165
285
|
const accounts = [];
|
|
166
286
|
if (chains.includes("evm")) {
|
|
167
|
-
const account
|
|
168
|
-
if (account) {
|
|
287
|
+
for (const account of deriveEvmAccount(parsed)) {
|
|
169
288
|
accounts.push({ chain: "evm", ...account });
|
|
170
289
|
}
|
|
171
290
|
}
|
|
291
|
+
if (chains.includes("btc")) {
|
|
292
|
+
for (const account of deriveBtcAccount(parsed)) {
|
|
293
|
+
accounts.push({ chain: "btc", ...account });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
172
296
|
return accounts;
|
|
173
297
|
}
|
|
174
298
|
|
|
@@ -305,14 +429,132 @@ function parseEthSignature(scanned) {
|
|
|
305
429
|
if (!sigBytes || sigBytes.length < 64) {
|
|
306
430
|
throw new Error("Invalid or missing signature bytes");
|
|
307
431
|
}
|
|
308
|
-
return
|
|
432
|
+
return `0x${bytesToHex(sigBytes)}`;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// src/btc/signRequest.ts
|
|
436
|
+
var BtcDataType = {
|
|
437
|
+
Message: 1
|
|
438
|
+
};
|
|
439
|
+
var TAG_KEYPATH2 = 304;
|
|
440
|
+
var PURPOSE_BY_SCRIPT_TYPE = {
|
|
441
|
+
p2wpkh: 84,
|
|
442
|
+
"p2sh-p2wpkh": 49,
|
|
443
|
+
p2pkh: 44
|
|
444
|
+
};
|
|
445
|
+
function randomBytes2(n) {
|
|
446
|
+
const buf = new Uint8Array(n);
|
|
447
|
+
crypto.getRandomValues(buf);
|
|
448
|
+
return buf;
|
|
449
|
+
}
|
|
450
|
+
function purposeFromScriptType(scriptType) {
|
|
451
|
+
return PURPOSE_BY_SCRIPT_TYPE[scriptType];
|
|
452
|
+
}
|
|
453
|
+
function buildKeypath2(scriptType, sourceFingerprint) {
|
|
454
|
+
const purpose = purposeFromScriptType(scriptType);
|
|
455
|
+
const components = [purpose, true, 0, true, 0, true, 0, false, 0, false];
|
|
456
|
+
const keypathMap = /* @__PURE__ */ new Map([[1, components]]);
|
|
457
|
+
if (sourceFingerprint !== void 0) {
|
|
458
|
+
keypathMap.set(2, sourceFingerprint);
|
|
459
|
+
}
|
|
460
|
+
return new CborTag(TAG_KEYPATH2, keypathMap);
|
|
461
|
+
}
|
|
462
|
+
function buildBtcSignRequestCbor(params) {
|
|
463
|
+
const { signData, address, scriptType, sourceFingerprint, origin = "qrkit" } = params;
|
|
464
|
+
const requestId = randomBytes2(16);
|
|
465
|
+
const signBytes = typeof signData === "string" ? new TextEncoder().encode(signData) : signData;
|
|
466
|
+
const keypath = buildKeypath2(scriptType, sourceFingerprint);
|
|
467
|
+
return encode(
|
|
468
|
+
/* @__PURE__ */ new Map([
|
|
469
|
+
[1, new CborTag(37, requestId)],
|
|
470
|
+
// request-id: uuid = #6.37(bstr)
|
|
471
|
+
[2, signBytes],
|
|
472
|
+
// sign-data
|
|
473
|
+
[3, BtcDataType.Message],
|
|
474
|
+
// data-type
|
|
475
|
+
[4, [keypath]],
|
|
476
|
+
// btc-derivation-paths
|
|
477
|
+
[5, [address]],
|
|
478
|
+
// btc-addresses
|
|
479
|
+
[6, origin]
|
|
480
|
+
// origin
|
|
481
|
+
])
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
function buildBtcSignRequestURParts(params) {
|
|
485
|
+
return encodeURParts(buildBtcSignRequestCbor(params), "btc-sign-request");
|
|
486
|
+
}
|
|
487
|
+
function buildBtcSignRequestUR(params) {
|
|
488
|
+
return encodeURParts(buildBtcSignRequestCbor(params), "btc-sign-request")[0];
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// src/btc/signature.ts
|
|
492
|
+
var import_cborg3 = require("cborg");
|
|
493
|
+
function parseBtcSignature(scanned) {
|
|
494
|
+
if (scanned.type !== "btc-signature") {
|
|
495
|
+
throw new Error(`Expected btc-signature, got: ${scanned.type}`);
|
|
496
|
+
}
|
|
497
|
+
const map = (0, import_cborg3.decode)(scanned.cbor, {
|
|
498
|
+
useMaps: true,
|
|
499
|
+
tags: Object.assign([], { 37: (v) => v })
|
|
500
|
+
});
|
|
501
|
+
const requestId = map.get(1);
|
|
502
|
+
const sigBytes = map.get(2);
|
|
503
|
+
const publicKeyBytes = map.get(3);
|
|
504
|
+
if (!sigBytes || sigBytes.length !== 65) {
|
|
505
|
+
throw new Error("Invalid or missing BTC signature bytes");
|
|
506
|
+
}
|
|
507
|
+
if (!publicKeyBytes || publicKeyBytes.length !== 33) {
|
|
508
|
+
throw new Error("Invalid or missing BTC public key bytes");
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
signature: bytesToBase64(sigBytes),
|
|
512
|
+
publicKey: bytesToHex(publicKeyBytes),
|
|
513
|
+
requestId: requestId ? bytesToHex(requestId) : void 0
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/btc/psbt.ts
|
|
518
|
+
var import_cborg4 = require("cborg");
|
|
519
|
+
function normalizePsbt(psbt) {
|
|
520
|
+
try {
|
|
521
|
+
return typeof psbt === "string" ? hexToBytes(psbt) : psbt;
|
|
522
|
+
} catch (error) {
|
|
523
|
+
throw new Error("Invalid PSBT hex", { cause: error });
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function parseCryptoPsbt(scanned) {
|
|
527
|
+
if (scanned.type !== "crypto-psbt") {
|
|
528
|
+
throw new Error(`Expected crypto-psbt, got: ${scanned.type}`);
|
|
529
|
+
}
|
|
530
|
+
const psbt = (0, import_cborg4.decode)(scanned.cbor);
|
|
531
|
+
if (!(psbt instanceof Uint8Array)) {
|
|
532
|
+
throw new Error("Invalid crypto-psbt payload");
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
psbt,
|
|
536
|
+
psbtHex: bytesToHex(psbt)
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function buildCryptoPsbtURParts(psbt) {
|
|
540
|
+
return encodeURParts(encode(normalizePsbt(psbt)), "crypto-psbt");
|
|
541
|
+
}
|
|
542
|
+
function buildCryptoPsbtUR(psbt) {
|
|
543
|
+
return buildCryptoPsbtURParts(psbt)[0];
|
|
309
544
|
}
|
|
310
545
|
// Annotate the CommonJS export names for ESM import in node:
|
|
311
546
|
0 && (module.exports = {
|
|
547
|
+
BtcDataType,
|
|
312
548
|
EthDataType,
|
|
549
|
+
buildBtcSignRequestUR,
|
|
550
|
+
buildBtcSignRequestURParts,
|
|
551
|
+
buildCryptoPsbtUR,
|
|
552
|
+
buildCryptoPsbtURParts,
|
|
313
553
|
buildEthSignRequestUR,
|
|
314
554
|
buildEthSignRequestURParts,
|
|
555
|
+
parseBtcSignature,
|
|
315
556
|
parseConnection,
|
|
557
|
+
parseCryptoPsbt,
|
|
316
558
|
parseEthSignature
|
|
317
559
|
});
|
|
318
560
|
//# sourceMappingURL=index.cjs.map
|