Files
seedpgp-web/REFERENCE/bun-CompactseedQR-implement.md.rtf
LC mac aa06c9ae27 feat: fix CompactSeedQR binary QR code scanning with jsQR library
- Replace BarcodeDetector with jsQR for raw binary byte access
- BarcodeDetector forced UTF-8 decoding which corrupted binary data
- jsQR's binaryData property preserves raw bytes without text conversion
- Fix regex bug: use single backslash \x00 instead of \x00 for binary detection
- Add debug logging for scan data inspection
- QR generation already worked (Krux-compatible), only scanning was broken

Resolves binary QR code scanning for 12/24-word CompactSeedQR format.
Tested with Krux device - full bidirectional compatibility confirmed.
2026-02-07 04:22:56 +08:00

692 lines
27 KiB
Plaintext

{\rtf1\ansi\ansicpg1252\cocoartf2867
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\froman\fcharset0 Times-Roman;\f1\froman\fcharset0 Times-Bold;\f2\froman\fcharset0 TimesNewRomanPSMT;
\f3\fmodern\fcharset0 Courier-Bold;\f4\fnil\fcharset0 HelveticaNeue;\f5\fmodern\fcharset0 Courier;
\f6\fmodern\fcharset0 Courier-Oblique;\f7\fnil\fcharset0 Menlo-Italic;\f8\fnil\fcharset0 AppleColorEmoji;
\f9\fnil\fcharset0 Menlo-Regular;}
{\colortbl;\red255\green255\blue255;\red255\green255\blue255;\red0\green0\blue0;\red185\green188\blue186;
\red162\green127\blue173;\red166\green178\blue85;\red111\green144\blue176;\red212\green128\blue77;\red132\green134\blue132;
\red0\green0\blue0;\red0\green0\blue233;}
{\*\expandedcolortbl;;\cssrgb\c100000\c100000\c100000;\cssrgb\c0\c0\c0\c84706;\cssrgb\c77255\c78431\c77647;
\cssrgb\c69804\c58039\c73333;\cssrgb\c70980\c74118\c40784;\cssrgb\c50588\c63529\c74510;\cssrgb\c87059\c57647\c37255;\cssrgb\c58824\c59608\c58824;
\cssrgb\c0\c0\c0;\cssrgb\c0\c0\c93333;}
{\*\listtable{\list\listtemplateid1\listhybrid{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{decimal\}}{\leveltext\leveltemplateid1\'01\'00;}{\levelnumbers\'01;}\fi-360\li720\lin720 }{\listname ;}\listid1}}
{\*\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}}
\paperw11900\paperh16840\margl1440\margr1440\vieww11520\viewh8400\viewkind0
\deftab720
\pard\pardeftab720\sa240\partightenfactor0
\f0\fs24 \cf2 \expnd0\expndtw0\kerning0
\outl0\strokewidth0 \strokec2 You're right to clarify! The test code I provided
\f1\b only covers reading/decoding CompactSeedQR
\f0\b0 (direction 2). Let me provide a
\f1\b complete bidirectional test
\f0\b0 that covers both:\
\pard\tx220\tx720\pardeftab720\li720\fi-720\sa240\partightenfactor0
\ls1\ilvl0
\f1\b \cf2 \kerning1\expnd0\expndtw0 \outl0\strokewidth0 {\listtext 1 }\expnd0\expndtw0\kerning0
\outl0\strokewidth0 \strokec2 Encoding
\f0\b0 : Mnemonic
\f2 \uc0\u8594
\f0 CompactSeedQR (binary data that goes into QR code)\
\ls1\ilvl0
\f1\b \kerning1\expnd0\expndtw0 \outl0\strokewidth0 {\listtext 2 }\expnd0\expndtw0\kerning0
\outl0\strokewidth0 \strokec2 Decoding
\f0\b0 : CompactSeedQR
\f2 \uc0\u8594
\f0 Mnemonic (restore from QR)\
\pard\pardeftab720\sa298\partightenfactor0
\f1\b\fs36 \cf2 \strokec2 Complete Bidirectional Test (
\f3\fs39 compact-seedqr-complete.ts
\f1\fs36 )\
\pard\pardeftab720\qc\partightenfactor0
\f4\b0\fs22 \cf3 \strokec3 \
\pard\pardeftab720\partightenfactor0
\f5\fs26 \cf2 \strokec2 bash\
\pard\pardeftab720\partightenfactor0
\cf4 \strokec4 bun add bip39 qrcode\
\pard\pardeftab720\qc\partightenfactor0
\f4\fs22 \cf3 \strokec3 \
\pard\pardeftab720\partightenfactor0
\f5\fs26 \cf2 \strokec2 typescript\
\pard\pardeftab720\partightenfactor0
\f3\b \cf5 \strokec5 import
\f5\b0 \cf4 \strokec4 *
\f3\b \cf5 \strokec5 as
\f5\b0 \cf4 \strokec4 bip39
\f3\b \cf5 \strokec5 from
\f5\b0 \cf4 \strokec4 \cf6 \strokec6 "bip39"\cf4 \strokec4 ;\
\f3\b \cf5 \strokec5 import
\f5\b0 \cf4 \strokec4 QRCode
\f3\b \cf5 \strokec5 from
\f5\b0 \cf4 \strokec4 \cf6 \strokec6 "qrcode"\cf4 \strokec4 ;\
\
\f3\b \cf5 \strokec5 interface
\f5\b0 \cf4 \strokec4 \cf7 \strokec7 CompactSeedQRTestVector\cf4 \strokec4 \{\
name: \cf8 \strokec8 string\cf4 \strokec4 ;\
mnemonic: \cf8 \strokec8 string\cf4 \strokec4 ;\
hexEntropy: \cf8 \strokec8 string\cf4 \strokec4 ;\
wordCount: \cf8 \strokec8 12\cf4 \strokec4 | \cf8 \strokec8 24\cf4 \strokec4 ;\
\}\
\
\f3\b \cf5 \strokec5 const
\f5\b0 \cf4 \strokec4 TEST_VECTORS: CompactSeedQRTestVector[] = [\
\{\
name: \cf6 \strokec6 "Test Vector 1: 24-word"\cf4 \strokec4 ,\
mnemonic: \cf6 \strokec6 "attack pizza motion avocado network gather crop fresh patrol unusual wild holiday candy pony ranch winter theme error hybrid van cereal salon goddess expire"\cf4 \strokec4 ,\
hexEntropy: \cf6 \strokec6 "0e74b64107f94cc0ccfae6a13dcbec3662154fec67e0e00999c07892597d190a"\cf4 \strokec4 ,\
wordCount: \cf8 \strokec8 24\cf4 \strokec4 \
\},\
\{\
name: \cf6 \strokec6 "Test Vector 4: 12-word"\cf4 \strokec4 ,\
mnemonic: \cf6 \strokec6 "forum undo fragile fade shy sign arrest garment culture tube off merit"\cf4 \strokec4 ,\
hexEntropy: \cf6 \strokec6 "5bbd9d71a8ec799083laff359d456545"\cf4 \strokec4 ,\
wordCount: \cf8 \strokec8 12\cf4 \strokec4 \
\},\
\{\
name: \cf6 \strokec6 "Test Vector 6: 12-word (with null byte)"\cf4 \strokec4 ,\
mnemonic: \cf6 \strokec6 "approve fruit lens brass ring actual stool coin doll boss strong rate"\cf4 \strokec4 ,\
hexEntropy: \cf6 \strokec6 "0acbba008d9ba005f5996b40a3475cd9"\cf4 \strokec4 ,\
wordCount: \cf8 \strokec8 12\cf4 \strokec4 \
\}\
];\
\
\pard\pardeftab720\partightenfactor0
\f6\i \cf9 \strokec9 // ============================================================================
\f5\i0 \cf4 \strokec4 \
\f6\i \cf9 \strokec9 // DIRECTION 1: ENCODE - Mnemonic
\f7 \uc0\u8594
\f6 CompactSeedQR (Binary for QR Code)
\f5\i0 \cf4 \strokec4 \
\f6\i \cf9 \strokec9 // ============================================================================
\f5\i0 \cf4 \strokec4 \
\
\f6\i \cf9 \strokec9 /**\
* Convert mnemonic to CompactSeedQR format (raw entropy bytes)\
* This is what you encode into the QR code as BINARY data\
*/
\f5\i0 \cf4 \strokec4 \
\pard\pardeftab720\partightenfactor0
\f3\b \cf5 \strokec5 function
\f5\b0 \cf4 \strokec4 encodeToCompactSeedQR(mnemonic: \cf8 \strokec8 string\cf4 \strokec4 ): Buffer \{\
\f6\i \cf9 \strokec9 // Validate mnemonic
\f5\i0 \cf4 \strokec4 \
\f3\b \cf5 \strokec5 if
\f5\b0 \cf4 \strokec4 (!bip39.validateMnemonic(mnemonic)) \{\
\f3\b \cf5 \strokec5 throw
\f5\b0 \cf4 \strokec4
\f3\b \cf5 \strokec5 new
\f5\b0 \cf4 \strokec4 \cf7 \strokec7 Error\cf4 \strokec4 (\cf6 \strokec6 "Invalid mnemonic"\cf4 \strokec4 );\
\}\
\
\f6\i \cf9 \strokec9 // Extract entropy (WITHOUT checksum)
\f5\i0 \cf4 \strokec4 \
\f6\i \cf9 \strokec9 // bip39.mnemonicToEntropy returns hex string
\f5\i0 \cf4 \strokec4 \
\f3\b \cf5 \strokec5 const
\f5\b0 \cf4 \strokec4 entropyHex = bip39.mnemonicToEntropy(mnemonic);\
\f3\b \cf5 \strokec5 const
\f5\b0 \cf4 \strokec4 entropy = Buffer.from(entropyHex, \cf6 \strokec6 "hex"\cf4 \strokec4 );\
\
\f6\i \cf9 \strokec9 // Validate length
\f5\i0 \cf4 \strokec4 \
\f3\b \cf5 \strokec5 if
\f5\b0 \cf4 \strokec4 (entropy.length !== \cf8 \strokec8 16\cf4 \strokec4 && entropy.length !== \cf8 \strokec8 32\cf4 \strokec4 ) \{\
\f3\b \cf5 \strokec5 throw
\f5\b0 \cf4 \strokec4
\f3\b \cf5 \strokec5 new
\f5\b0 \cf4 \strokec4 \cf7 \strokec7 Error\cf4 \strokec4 (\cf6 \strokec6 `Invalid entropy length: \cf4 \strokec4 $\{entropy.length\}\cf6 \strokec6 `\cf4 \strokec4 );\
\}\
\
\f3\b \cf5 \strokec5 return
\f5\b0 \cf4 \strokec4 entropy;\
\}\
\
\pard\pardeftab720\partightenfactor0
\f6\i \cf9 \strokec9 /**\
* Generate a QR code from mnemonic (as PNG data URL)\
*/
\f5\i0 \cf4 \strokec4 \
\pard\pardeftab720\partightenfactor0
\f3\b \cf5 \strokec5 async
\f5\b0 \cf4 \strokec4
\f3\b \cf5 \strokec5 function
\f5\b0 \cf4 \strokec4 generateCompactSeedQR(mnemonic: \cf8 \strokec8 string\cf4 \strokec4 ): \cf8 \strokec8 Promise\cf4 \strokec4 <\cf8 \strokec8 string\cf4 \strokec4 > \{\
\f3\b \cf5 \strokec5 const
\f5\b0 \cf4 \strokec4 entropy = encodeToCompactSeedQR(mnemonic);\
\
\f6\i \cf9 \strokec9 // Generate QR code with BINARY data
\f5\i0 \cf4 \strokec4 \
\f6\i \cf9 \strokec9 // Important: Use 'byte' mode for raw binary data
\f5\i0 \cf4 \strokec4 \
\f3\b \cf5 \strokec5 const
\f5\b0 \cf4 \strokec4 qrDataURL =
\f3\b \cf5 \strokec5 await
\f5\b0 \cf4 \strokec4 QRCode.toDataURL([\{ data: entropy, mode: \cf6 \strokec6 'byte'\cf4 \strokec4 \}], \{\
errorCorrectionLevel: \cf6 \strokec6 'L'\cf4 \strokec4 ,
\f6\i \cf9 \strokec9 // SeedSigner uses Low error correction
\f5\i0 \cf4 \strokec4 \
type: \cf6 \strokec6 'image/png'\cf4 \strokec4 ,\
width: \cf8 \strokec8 300\cf4 \strokec4 \
\});\
\
\f3\b \cf5 \strokec5 return
\f5\b0 \cf4 \strokec4 qrDataURL;\
\}\
\
\pard\pardeftab720\partightenfactor0
\f6\i \cf9 \strokec9 // ============================================================================
\f5\i0 \cf4 \strokec4 \
\f6\i \cf9 \strokec9 // DIRECTION 2: DECODE - CompactSeedQR
\f7 \uc0\u8594
\f6 Mnemonic (Restore from QR)
\f5\i0 \cf4 \strokec4 \
\f6\i \cf9 \strokec9 // ============================================================================
\f5\i0 \cf4 \strokec4 \
\
\f6\i \cf9 \strokec9 /**\
* Parse CompactSeedQR from raw bytes (what QR scanner gives you)\
*/
\f5\i0 \cf4 \strokec4 \
\pard\pardeftab720\partightenfactor0
\f3\b \cf5 \strokec5 function
\f5\b0 \cf4 \strokec4 decodeCompactSeedQR(data: Buffer | Uint8Array): \cf8 \strokec8 string\cf4 \strokec4 \{\
\f3\b \cf5 \strokec5 const
\f5\b0 \cf4 \strokec4 entropy = Buffer.from(data);\
\
\f6\i \cf9 \strokec9 // Validate length
\f5\i0 \cf4 \strokec4 \
\f3\b \cf5 \strokec5 if
\f5\b0 \cf4 \strokec4 (entropy.length !== \cf8 \strokec8 16\cf4 \strokec4 && entropy.length !== \cf8 \strokec8 32\cf4 \strokec4 ) \{\
\f3\b \cf5 \strokec5 throw
\f5\b0 \cf4 \strokec4
\f3\b \cf5 \strokec5 new
\f5\b0 \cf4 \strokec4 \cf7 \strokec7 Error\cf4 \strokec4 (\
\cf6 \strokec6 `Invalid CompactSeedQR length: \cf4 \strokec4 $\{entropy.length\}\cf6 \strokec6 bytes. Must be 16 (12-word) or 32 (24-word).`\cf4 \strokec4 \
);\
\}\
\
\f6\i \cf9 \strokec9 // Convert entropy to mnemonic (automatically adds BIP39 checksum)
\f5\i0 \cf4 \strokec4 \
\f3\b \cf5 \strokec5 const
\f5\b0 \cf4 \strokec4 mnemonic = bip39.entropyToMnemonic(entropy);\
\
\f3\b \cf5 \strokec5 return
\f5\b0 \cf4 \strokec4 mnemonic;\
\}\
\
\pard\pardeftab720\partightenfactor0
\f6\i \cf9 \strokec9 /**\
* Parse from hex string (for testing)\
*/
\f5\i0 \cf4 \strokec4 \
\pard\pardeftab720\partightenfactor0
\f3\b \cf5 \strokec5 function
\f5\b0 \cf4 \strokec4 decodeCompactSeedQRFromHex(hexEntropy: \cf8 \strokec8 string\cf4 \strokec4 ): \cf8 \strokec8 string\cf4 \strokec4 \{\
\f3\b \cf5 \strokec5 const
\f5\b0 \cf4 \strokec4 entropy = Buffer.from(hexEntropy, \cf6 \strokec6 "hex"\cf4 \strokec4 );\
\f3\b \cf5 \strokec5 return
\f5\b0 \cf4 \strokec4 decodeCompactSeedQR(entropy);\
\}\
\
\pard\pardeftab720\partightenfactor0
\f6\i \cf9 \strokec9 // ============================================================================
\f5\i0 \cf4 \strokec4 \
\f6\i \cf9 \strokec9 // TESTS
\f5\i0 \cf4 \strokec4 \
\f6\i \cf9 \strokec9 // ============================================================================
\f5\i0 \cf4 \strokec4 \
\
\pard\pardeftab720\partightenfactor0
\f3\b \cf5 \strokec5 async
\f5\b0 \cf4 \strokec4
\f3\b \cf5 \strokec5 function
\f5\b0 \cf4 \strokec4 runBidirectionalTests() \{\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 "
\f8 \uc0\u55358 \u56810
\f5 CompactSeedQR Bidirectional Tests\\n"\cf4 \strokec4 );\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 "="\cf4 \strokec4 .repeat(\cf8 \strokec8 80\cf4 \strokec4 ));\
\
\f3\b \cf5 \strokec5 let
\f5\b0 \cf4 \strokec4 passed = \cf8 \strokec8 0\cf4 \strokec4 ;\
\f3\b \cf5 \strokec5 let
\f5\b0 \cf4 \strokec4 failed = \cf8 \strokec8 0\cf4 \strokec4 ;\
\
\f3\b \cf5 \strokec5 for
\f5\b0 \cf4 \strokec4 (
\f3\b \cf5 \strokec5 const
\f5\b0 \cf4 \strokec4 vector
\f3\b \cf5 \strokec5 of
\f5\b0 \cf4 \strokec4 TEST_VECTORS) \{\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 `\\n
\f8 \uc0\u55357 \u56523
\f5 \cf4 \strokec4 $\{vector.name\}\cf6 \strokec6 `\cf4 \strokec4 );\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 `Mnemonic: \cf4 \strokec4 $\{vector.mnemonic.slice(\cf8 \strokec8 0\cf4 \strokec4 , \cf8 \strokec8 50\cf4 \strokec4 )\}\cf6 \strokec6 ...`\cf4 \strokec4 );\
\
\f3\b \cf5 \strokec5 try
\f5\b0 \cf4 \strokec4 \{\
\f6\i \cf9 \strokec9 // ===== DIRECTION 1: ENCODE =====
\f5\i0 \cf4 \strokec4 \
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 "\\n
\f8 \uc0\u55357 \u56594
\f5 ENCODE: Mnemonic
\f9 \uc0\u8594
\f5 CompactSeedQR"\cf4 \strokec4 );\
\
\f3\b \cf5 \strokec5 const
\f5\b0 \cf4 \strokec4 encodedEntropy = encodeToCompactSeedQR(vector.mnemonic);\
\f3\b \cf5 \strokec5 const
\f5\b0 \cf4 \strokec4 encodedHex = encodedEntropy.toString(\cf6 \strokec6 "hex"\cf4 \strokec4 );\
\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` Generated entropy: \cf4 \strokec4 $\{encodedHex\}\cf6 \strokec6 `\cf4 \strokec4 );\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` Expected entropy: \cf4 \strokec4 $\{vector.hexEntropy\}\cf6 \strokec6 `\cf4 \strokec4 );\
\
\f3\b \cf5 \strokec5 const
\f5\b0 \cf4 \strokec4 encodeMatches = encodedHex === vector.hexEntropy;\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` \cf4 \strokec4 $\{encodeMatches ? \cf6 \strokec6 "
\f8 \uc0\u9989
\f5 "\cf4 \strokec4 : \cf6 \strokec6 "
\f8 \uc0\u10060
\f5 "\cf4 \strokec4 \}\cf6 \strokec6 Encode \cf4 \strokec4 $\{encodeMatches ? \cf6 \strokec6 "PASSED"\cf4 \strokec4 : \cf6 \strokec6 "FAILED"\cf4 \strokec4 \}\cf6 \strokec6 `\cf4 \strokec4 );\
\
\f6\i \cf9 \strokec9 // Generate actual QR code
\f5\i0 \cf4 \strokec4 \
\f3\b \cf5 \strokec5 const
\f5\b0 \cf4 \strokec4 qrDataURL =
\f3\b \cf5 \strokec5 await
\f5\b0 \cf4 \strokec4 generateCompactSeedQR(vector.mnemonic);\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 `
\f8 \uc0\u55357 \u56561
\f5 QR Code generated (\cf4 \strokec4 $\{qrDataURL.length\}\cf6 \strokec6 bytes PNG data)`\cf4 \strokec4 );\
\
\f6\i \cf9 \strokec9 // ===== DIRECTION 2: DECODE =====
\f5\i0 \cf4 \strokec4 \
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 "\\n
\f8 \uc0\u55357 \u56595
\f5 DECODE: CompactSeedQR
\f9 \uc0\u8594
\f5 Mnemonic"\cf4 \strokec4 );\
\
\f3\b \cf5 \strokec5 const
\f5\b0 \cf4 \strokec4 decodedMnemonic = decodeCompactSeedQRFromHex(vector.hexEntropy);\
\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` Decoded: \cf4 \strokec4 $\{decodedMnemonic.slice(\cf8 \strokec8 0\cf4 \strokec4 , \cf8 \strokec8 50\cf4 \strokec4 )\}\cf6 \strokec6 ...`\cf4 \strokec4 );\
\
\f3\b \cf5 \strokec5 const
\f5\b0 \cf4 \strokec4 decodeMatches = decodedMnemonic === vector.mnemonic;\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` \cf4 \strokec4 $\{decodeMatches ? \cf6 \strokec6 "
\f8 \uc0\u9989
\f5 "\cf4 \strokec4 : \cf6 \strokec6 "
\f8 \uc0\u10060
\f5 "\cf4 \strokec4 \}\cf6 \strokec6 Decode \cf4 \strokec4 $\{decodeMatches ? \cf6 \strokec6 "PASSED"\cf4 \strokec4 : \cf6 \strokec6 "FAILED"\cf4 \strokec4 \}\cf6 \strokec6 `\cf4 \strokec4 );\
\
\f6\i \cf9 \strokec9 // ===== ROUND-TRIP TEST =====
\f5\i0 \cf4 \strokec4 \
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 "\\n
\f8 \uc0\u55357 \u56580
\f5 ROUND-TRIP: Mnemonic
\f9 \uc0\u8594
\f5 QR
\f9 \uc0\u8594
\f5 Mnemonic"\cf4 \strokec4 );\
\
\f3\b \cf5 \strokec5 const
\f5\b0 \cf4 \strokec4 roundTripMnemonic = decodeCompactSeedQR(encodedEntropy);\
\f3\b \cf5 \strokec5 const
\f5\b0 \cf4 \strokec4 roundTripMatches = roundTripMnemonic === vector.mnemonic;\
\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` \cf4 \strokec4 $\{roundTripMatches ? \cf6 \strokec6 "
\f8 \uc0\u9989
\f5 "\cf4 \strokec4 : \cf6 \strokec6 "
\f8 \uc0\u10060
\f5 "\cf4 \strokec4 \}\cf6 \strokec6 Round-trip \cf4 \strokec4 $\{roundTripMatches ? \cf6 \strokec6 "PASSED"\cf4 \strokec4 : \cf6 \strokec6 "FAILED"\cf4 \strokec4 \}\cf6 \strokec6 `\cf4 \strokec4 );\
\
\f3\b \cf5 \strokec5 if
\f5\b0 \cf4 \strokec4 (encodeMatches && decodeMatches && roundTripMatches) \{\
passed++;\
\}
\f3\b \cf5 \strokec5 else
\f5\b0 \cf4 \strokec4 \{\
failed++;\
\}\
\
\}
\f3\b \cf5 \strokec5 catch
\f5\b0 \cf4 \strokec4 (error) \{\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 `
\f8 \uc0\u10060
\f5 ERROR: \cf4 \strokec4 $\{error\}\cf6 \strokec6 `\cf4 \strokec4 );\
failed++;\
\}\
\}\
\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 "\\n"\cf4 \strokec4 + \cf6 \strokec6 "="\cf4 \strokec4 .repeat(\cf8 \strokec8 80\cf4 \strokec4 ));\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 `\\n
\f8 \uc0\u55357 \u56522
\f5 Results: \cf4 \strokec4 $\{passed\}\cf6 \strokec6 /\cf4 \strokec4 $\{TEST_VECTORS.length\}\cf6 \strokec6 passed`\cf4 \strokec4 );\
\
\f3\b \cf5 \strokec5 if
\f5\b0 \cf4 \strokec4 (failed === \cf8 \strokec8 0\cf4 \strokec4 ) \{\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 "
\f8 \uc0\u55356 \u57225
\f5 ALL TESTS PASSED!"\cf4 \strokec4 );\
\}\
\}\
\
\pard\pardeftab720\partightenfactor0
\f6\i \cf9 \strokec9 // ============================================================================
\f5\i0 \cf4 \strokec4 \
\f6\i \cf9 \strokec9 // EXAMPLE USAGE
\f5\i0 \cf4 \strokec4 \
\f6\i \cf9 \strokec9 // ============================================================================
\f5\i0 \cf4 \strokec4 \
\
\pard\pardeftab720\partightenfactor0
\f3\b \cf5 \strokec5 async
\f5\b0 \cf4 \strokec4
\f3\b \cf5 \strokec5 function
\f5\b0 \cf4 \strokec4 demonstrateUsage() \{\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 "\\n\\n"\cf4 \strokec4 + \cf6 \strokec6 "="\cf4 \strokec4 .repeat(\cf8 \strokec8 80\cf4 \strokec4 ));\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 "
\f8 \uc0\u55357 \u56481
\f5 USAGE EXAMPLE\\n"\cf4 \strokec4 );\
\
\f3\b \cf5 \strokec5 const
\f5\b0 \cf4 \strokec4 exampleMnemonic = \cf6 \strokec6 "forum undo fragile fade shy sign arrest garment culture tube off merit"\cf4 \strokec4 ;\
\
\f6\i \cf9 \strokec9 // ===== CREATE QR CODE =====
\f5\i0 \cf4 \strokec4 \
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 "
\f8 1\uc0\u65039 \u8419
\f5 CREATE CompactSeedQR from mnemonic:"\cf4 \strokec4 );\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` Input: \cf4 \strokec4 $\{exampleMnemonic\}\cf6 \strokec6 `\cf4 \strokec4 );\
\
\f3\b \cf5 \strokec5 const
\f5\b0 \cf4 \strokec4 entropy = encodeToCompactSeedQR(exampleMnemonic);\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` Binary data (hex): \cf4 \strokec4 $\{entropy.toString(\cf6 \strokec6 "hex"\cf4 \strokec4 )\}\cf6 \strokec6 `\cf4 \strokec4 );\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` Binary data (bytes): [\cf4 \strokec4 $\{\cf8 \strokec8 Array\cf4 \strokec4 .from(entropy).join(\cf6 \strokec6 ", "\cf4 \strokec4 )\}\cf6 \strokec6 ]`\cf4 \strokec4 );\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` Length: \cf4 \strokec4 $\{entropy.length\}\cf6 \strokec6 bytes (\cf4 \strokec4 $\{entropy.length === \cf8 \strokec8 16\cf4 \strokec4 ? \cf6 \strokec6 "12-word"\cf4 \strokec4 : \cf6 \strokec6 "24-word"\cf4 \strokec4 \}\cf6 \strokec6 )`\cf4 \strokec4 );\
\
\f3\b \cf5 \strokec5 const
\f5\b0 \cf4 \strokec4 qrDataURL =
\f3\b \cf5 \strokec5 await
\f5\b0 \cf4 \strokec4 generateCompactSeedQR(exampleMnemonic);\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 `
\f8 \uc0\u9989
\f5 QR code generated!`\cf4 \strokec4 );\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 `
\f8 \uc0\u55357 \u56561
\f5 Display this QR: <img src="\cf4 \strokec4 $\{qrDataURL.slice(\cf8 \strokec8 0\cf4 \strokec4 , \cf8 \strokec8 50\cf4 \strokec4 )\}\cf6 \strokec6 ..." />`\cf4 \strokec4 );\
\
\f6\i \cf9 \strokec9 // ===== SCAN QR CODE =====
\f5\i0 \cf4 \strokec4 \
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 "\\n
\f8 2\uc0\u65039 \u8419
\f5 SCAN CompactSeedQR and restore mnemonic:"\cf4 \strokec4 );\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` Simulating QR scanner output...`\cf4 \strokec4 );\
\
\f6\i \cf9 \strokec9 // In real app, your QR scanner returns raw bytes:
\f5\i0 \cf4 \strokec4 \
\f6\i \cf9 \strokec9 // const scannedBytes = qrScanner.decode();
\f5\i0 \cf4 \strokec4 \
\f3\b \cf5 \strokec5 const
\f5\b0 \cf4 \strokec4 scannedBytes = entropy;
\f6\i \cf9 \strokec9 // Simulate scanning the QR we just created
\f5\i0 \cf4 \strokec4 \
\
\f3\b \cf5 \strokec5 const
\f5\b0 \cf4 \strokec4 restoredMnemonic = decodeCompactSeedQR(scannedBytes);\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` Restored: \cf4 \strokec4 $\{restoredMnemonic\}\cf6 \strokec6 `\cf4 \strokec4 );\
\cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 `
\f8 \uc0\u9989
\f5 Match: \cf4 \strokec4 $\{restoredMnemonic === exampleMnemonic\}\cf6 \strokec6 `\cf4 \strokec4 );\
\}\
\
\pard\pardeftab720\partightenfactor0
\f6\i \cf9 \strokec9 // Run everything
\f5\i0 \cf4 \strokec4 \
runBidirectionalTests().then(() => demonstrateUsage());\
\pard\pardeftab720\sa298\partightenfactor0
\f1\b\fs36 \cf2 \strokec2 Run Command\
\pard\pardeftab720\qc\partightenfactor0
\f4\b0\fs22 \cf3 \strokec3 \
\pard\pardeftab720\partightenfactor0
\f5\fs26 \cf2 \strokec2 bash\
\pard\pardeftab720\partightenfactor0
\cf4 \strokec4 bun run compact-seedqr-complete.ts\
\pard\pardeftab720\sa298\partightenfactor0
\f1\b\fs36 \cf2 \strokec2 Expected Output\
\pard\pardeftab720\qc\partightenfactor0
\f4\b0\fs22 \cf3 \strokec3 \
\pard\pardeftab720\partightenfactor0
\f5\fs26 \cf2 \strokec2 text\
\pard\pardeftab720\partightenfactor0
\f8 \cf4 \strokec4 \uc0\u55358 \u56810
\f5 CompactSeedQR Bidirectional Tests\
\
================================================================================\
\
\f8 \uc0\u55357 \u56523
\f5 Test Vector 1: 24-word\
Mnemonic: attack pizza motion avocado network gather crop fre...\
\
\f8 \uc0\u55357 \u56594
\f5 ENCODE: Mnemonic
\f9 \uc0\u8594
\f5 CompactSeedQR\
Generated entropy: 0e74b64107f94cc0ccfae6a13dcbec3662154fec67e0e00999c07892597d190a\
Expected entropy: 0e74b64107f94cc0ccfae6a13dcbec3662154fec67e0e00999c07892597d190a\
\f8 \uc0\u9989
\f5 Encode PASSED\
\f8 \uc0\u55357 \u56561
\f5 QR Code generated (2847 bytes PNG data)\
\
\f8 \uc0\u55357 \u56595
\f5 DECODE: CompactSeedQR
\f9 \uc0\u8594
\f5 Mnemonic\
Decoded: attack pizza motion avocado network gather crop fre...\
\f8 \uc0\u9989
\f5 Decode PASSED\
\
\f8 \uc0\u55357 \u56580
\f5 ROUND-TRIP: Mnemonic
\f9 \uc0\u8594
\f5 QR
\f9 \uc0\u8594
\f5 Mnemonic\
\f8 \uc0\u9989
\f5 Round-trip PASSED\
\
...\
\
================================================================================\
\
\f8 \uc0\u55357 \u56522
\f5 Results: 3/3 passed\
\f8 \uc0\u55356 \u57225
\f5 ALL TESTS PASSED!\
\
================================================================================\
\f8 \uc0\u55357 \u56481
\f5 USAGE EXAMPLE\
\
\f8 1\uc0\u65039 \u8419
\f5 CREATE CompactSeedQR from mnemonic:\
Input: forum undo fragile fade shy sign arrest garment culture tube off merit\
Binary data (hex): 5bbd9d71a8ec799083laff359d456545\
Binary data (bytes): [91, 189, 157, 113, 168, 236, 121, ...]\
Length: 16 bytes (12-word)\
\f8 \uc0\u9989
\f5 QR code generated!\
\
\f8 2\uc0\u65039 \u8419
\f5 SCAN CompactSeedQR and restore mnemonic:\
Simulating QR scanner output...\
Restored: forum undo fragile fade shy sign arrest garment culture tube off merit\
\f8 \uc0\u9989
\f5 Match: true\
\pard\pardeftab720\sa298\partightenfactor0
\f1\b\fs36 \cf2 \strokec2 Summary: What the Code Does\
\itap1\trowd \taflags0 \trgaph108\trleft-108 \trbrdrt\brdrnil \trbrdrl\brdrnil \trbrdrr\brdrnil
\clvertalc \clshdrawnil \clwWidth1000\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx2160
\clvertalc \clshdrawnil \clwWidth2033\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx4320
\clvertalc \clshdrawnil \clwWidth2779\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx6480
\clvertalc \clshdrawnil \clwWidth4979\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx8640
\pard\intbl\itap1\pardeftab720\qc\partightenfactor0
\fs24 \cf2 \strokec10 Direction\cf0 \cell
\pard\intbl\itap1\pardeftab720\qc\partightenfactor0
\cf2 Input\cf0 \cell
\pard\intbl\itap1\pardeftab720\qc\partightenfactor0
\cf2 Output\cf0 \cell
\pard\intbl\itap1\pardeftab720\qc\partightenfactor0
\cf2 What It Does\cf0 \cell \row
\itap1\trowd \taflags0 \trgaph108\trleft-108 \trbrdrl\brdrnil \trbrdrr\brdrnil
\clvertalc \clshdrawnil \clwWidth1000\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx2160
\clvertalc \clshdrawnil \clwWidth2033\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx4320
\clvertalc \clshdrawnil \clwWidth2779\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx6480
\clvertalc \clshdrawnil \clwWidth4979\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx8640
\pard\intbl\itap1\pardeftab720\partightenfactor0
\cf2 1. Encode
\f0\b0 \cf0 \cell
\pard\intbl\itap1\pardeftab720\partightenfactor0
\cf2 Mnemonic (words)\cf0 \cell
\pard\intbl\itap1\pardeftab720\partightenfactor0
\cf2 Binary entropy (16/32 bytes)\cf0 \cell
\pard\intbl\itap1\pardeftab720\partightenfactor0
\cf2 Strips checksum, creates raw bytes for QR\cf0 \cell \row
\itap1\trowd \taflags0 \trgaph108\trleft-108 \trbrdrl\brdrnil \trbrdrt\brdrnil \trbrdrr\brdrnil
\clvertalc \clshdrawnil \clwWidth1000\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx2160
\clvertalc \clshdrawnil \clwWidth2033\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx4320
\clvertalc \clshdrawnil \clwWidth2779\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx6480
\clvertalc \clshdrawnil \clwWidth4979\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx8640
\pard\intbl\itap1\pardeftab720\partightenfactor0
\f1\b \cf2 2. Decode
\f0\b0 \cf0 \cell
\pard\intbl\itap1\pardeftab720\partightenfactor0
\cf2 Binary data from QR\cf0 \cell
\pard\intbl\itap1\pardeftab720\partightenfactor0
\cf2 Mnemonic (words)\cf0 \cell
\pard\intbl\itap1\pardeftab720\partightenfactor0
\cf2 Reads raw bytes, adds checksum, converts to words\cf0 \cell \lastrow\row
\pard\pardeftab720\qc\partightenfactor0
\f4\fs22 \cf3 \strokec3 \
\
\pard\pardeftab720\sa240\partightenfactor0
\f0\fs24 \cf2 \strokec2 The QR code itself contains
\f1\b raw binary entropy
\f0\b0 (NOT encoded as hex or base64). This is why CompactSeedQR is smaller than Standard SeedQR!{\field{\*\fldinst{HYPERLINK "https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md"}}{\fldrslt \cf11 \ul \ulc11 \strokec11 github+1}}\
}