UNPKG

@ngraveio/ur-sync

Version:

Provides BC-UR types for syncing multiple coins and accounts from cold wallets to watch only wallets.

507 lines (403 loc) 20.9 kB
# Multi Layer Sync Protocol This is the implementation of the [Multi Layer Sync Protocol](https://github.com/ngraveio/Research/blob/main/papers/nbcr-2023-002-multi-layer-sync.md) that supports multiple coins and accounts with different types via globally identifiable URs. This package adds support for the following UR types: | Type | [CBOR Tag](https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml) | Owner | Description | Definition | | ------------------------- | ---------------------------------------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | `detailed-account` | 41402 | Ngrave | Import multiple accounts with and without output descriptors and specify optionally tokens to synchronize | [NBCR-2023-002](https://github.com/ngraveio/Research/blob/main/papers/nbcr-2023-002-multi-layer-sync.md) | | `portfolio-coin` | 41403 | Ngrave | Associate several accounts to its coin identity | [NBCR-2023-002](https://github.com/ngraveio/Research/blob/main/papers/nbcr-2023-002-multi-layer-sync.md) | | `portfolio` | 41405 | Ngrave | Aggregate the portfolio information | [NBCR-2023-002](https://github.com/ngraveio/Research/blob/main/papers/nbcr-2023-002-multi-layer-sync.md) | ## Overview ### What is the Multi-Layer Sync Protocol? The **Multi-Layer Sync Protocol (MLSP)** is a universal syncing mechanism designed for managing multiple cryptocurrency accounts, tokens, and portfolio structures efficiently. Unlike traditional single-account syncing, MLSP allows for: - **Multi-coin synchronization** – Sync multiple cryptocurrencies within a single protocol instance. - **Detailed account structures** – Support for hierarchical accounts with HD keys, token IDs, and additional metadata. - **Efficient QR-based synchronization** – Enabling seamless transfer of account and portfolio data over **airgapped** systems using animated QR codes. ### Why was it created? The protocol addresses key limitations in existing wallet synchronization solutions, which often: - Require separate synchronization processes for different assets. - Lack a structured way to **group multiple accounts under a single identity**. - Do not efficiently handle **QR partitioning for large payloads**. MLSP solves these issues by introducing **globally identifiable URs** that allow seamless synchronization across multiple layers of portfolio data. --- ## Index - [Multi Layer Sync Protocol](#multi-layer-sync-protocol) - [Overview](#overview) - [What is the Multi-Layer Sync Protocol?](#what-is-the-multi-layer-sync-protocol) - [Why was it created?](#why-was-it-created) - [TOC](#toc) - [Installing](#installing) - [DetailedAccount](#detailedaccount) - [Explanation](#explanation) - [Construct a detailed account with HDKey](#construct-a-detailed-account-with-hdkey) - [Decode a detailed account with HDKey](#decode-a-detailed-account-with-hdkey) - [PortfolioCoin](#portfoliocoin) - [Explanation](#explanation-1) - [Create a PortfolioCoin with 2 detailed accounts with tokens](#create-a-portfoliocoin-with-2-detailed-accounts-with-tokens) - [Decode the PortfolioCoin with 2 detailed accounts with tokens](#decode-the-portfoliocoin-with-2-detailed-accounts-with-tokens) - [PortfolioMetadata](#portfoliometadata) - [Explanation](#explanation-2) - [Create a PortfolioMetadata](#create-a-portfoliometadata) - [Decode a PortfolioMetadata](#decode-a-portfoliometadata) - [Portfolio](#portfolio) - [Explanation](#explanation-3) - [Create a Portfolio with 4 coins and Metadata](#create-a-portfolio-with-4-coins-and-metadata) ## Installing To install, run: ```bash yarn add @ngraveio/ur-sync ``` ```bash npm install --save @ngraveio/ur-sync ``` ```typescript import { DetailedAccount, PortfolioCoin, PortfolioMetadata, Portfolio } from '@ngraveio/ur-sync'; ``` ## DetailedAccount ### Explanation The `DetailedAccount` class represents an account with detailed information, including the HDKey and optional token IDs. ```mermaid flowchart TB subgraph DetailedAccount direction TB DA[DetailedAccount] --> HD[HDKey] DA --> Token[Token IDs] end ``` **CDDL Definition:** ```CDDL account_exp = #6.40303(hdkey) / #6.40308(output-descriptor) ; Accounts are specified using either '#6.40303(hdkey)' or ; '#6.40308(output-descriptor)'. ; By default, '#6.40303(hdkey)' should be used to share public keys and ; extended public keys. ; '#6.308(output-descriptor)' should be used to share an output descriptor, ; e.g. for the different Bitcoin address formats (P2PKH, P2SH-P2WPKH, P2WPKH, P2TR). ; Optional 'token-ids' to indicate the synchronization of a list of tokens with ; the associated accounts ; 'token-id' is defined differently depending on the blockchain: ; - ERC20 tokens on EVM chains are identified by their contract addresses ; (e.g. `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48`) ; - ERC1155 tokens are identifed with their contract addresses followed by their ; ID with ':' as separator (e.g. `0xfaafdc07907ff5120a76b34b731b278c38d6043c: ; 508851954656174721695133294256171964208`) ; - ESDT tokens on MultiversX are by their name followed by their ID with `-` as ; separator (e.g. `USDC-c76f1f`) ; - SPL tokens on Solana are identified by their contract addresses ; (e.g. `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`) detailed-account = { account: account_exp, ? token-ids: [+ string / bytes] ; Specify multiple tokens associated to one account } account = 1 token-ids = 2 ``` ### Construct a detailed account with HDKey ```typescript import { HDKey, Keypath } from '@ngraveio/ur-blockchain-commons'; import { DetailedAccount } from '@ngraveio/ur-sync'; // Create a path component const originKeyPath = new Keypath({ path: "m/44'/501'/0'/0'" }); // Create a HDKey const cryptoHDKey = new HDKey({ isMaster: false, keyData: Buffer.from('02eae4b876a8696134b868f88cc2f51f715f2dbedb7446b8e6edf3d4541c4eb67b', 'hex'), origin: originKeyPath, }); // Create detailed account const detailedAccount = new DetailedAccount({ account: cryptoHDKey, }); const cbor = detailedAccount.toHex(); const ur = detailedAccount.toUr(); console.log(cbor); // 'a101d99d6fa301f403582102eae4b876a8696134b868f88cc2f51f715f2dbedb7446b8e6edf3d4541c4eb67b06d99d70a10188182cf51901f5f500f500f5' console.log(ur); // 'ur:detailed-account/oyadtantjlotadwkaxhdclaowdverokopdinhseeroisyalksaykctjshedprnuyjyfgrovawewftyghceglrpkgamtantjooyadlocsdwykcfadykykaeykaeykionnimfd' ``` ### Decode a detailed account with HDKey ```typescript import { DetailedAccount } from '@ngraveio/ur-sync'; // CBOR result after scanning the QR code const cbor = 'a101d99d6fa301f403582102eae4b876a8696134b868f88cc2f51f715f2dbedb7446b8e6edf3d4541c4eb67b06d99d70a10188182cf51901f5f500f500f5'; // Convert the CBOR data into the DetailedAccount const detailedAccount = DetailedAccount.fromHex(cbor); // Get HDKey const hdKey = detailedAccount.getAccount(); ``` ## PortfolioCoin ### Explanation The `PortfolioCoin` class represents a coin with multiple detailed accounts. ```mermaid flowchart TB subgraph PortfolioCoin direction TB PC[PortfolioCoin] --> CI[CoinIdentity] PC --> DA[DetailedAccount] end ``` **CDDL Definition:** ```CDDL ; Associate a coin identity to its accounts detailed_accounts = [+ #6.41402(detailed-account)] ; The accounts are listed using #6.41402(detailed-account) to share the maximum of information related to the accounts coin = { coin-id: #6.41401(coin-identity), accounts: accounts_exp, ? master-fingerprint: uint32 ; Master fingerprint (fingerprint for the master public key as per BIP32) } ; master-fingerprint must match the potential other fingerprints included in the other sub-UR types coin-id = 1 accounts = 2 ``` ### Create a PortfolioCoin with 2 detailed accounts with tokens ```typescript import { HDKey, Keypath } from '@ngraveio/ur-blockchain-commons'; import { DetailedAccount, PortfolioCoin } from '@ngraveio/ur-sync'; import { CoinIdentity, EllipticCurve } from '@ngraveio/ur-coin-identity'; // Create a coin identity const coinIdentity = new CoinIdentity(EllipticCurve.secp256k1, 60); // Create HDKeys const cryptoHDKey = new HDKey({ isMaster: false, keyData: Buffer.from('02d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f0', 'hex'), origin: new Keypath({ path: "m/60'/0'/0'/0/0" }), parentFingerprint: 2017537594, }); const tokenIds = ['0xdac17f958d2ee523a2206206994597c13d831ec7', '0xb8c77482e45f1f44de1745f52c74426c631bdd52']; const cryptoHDKey2 = HDKey.fromHex('A203582102EAE4B876A8696134B868F88CC2F51F715F2DBEDB7446B8E6EDF3D4541C4EB67B06D99D70A10188182CF51901F5F500F500F5'); const tokenIds2 = ['EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v']; // Create detailed accounts const detailedAccount = new DetailedAccount({ account: cryptoHDKey, tokenIds, }); const detailedAccount2 = new DetailedAccount({ account: cryptoHDKey2, tokenIds: tokenIds2, }); // Create a PortfolioCoin const portfolioCoin = new PortfolioCoin({ coinId: coinIdentity, accounts: [detailedAccount, detailedAccount2], }); const cbor = portfolioCoin.toHex(); const ur = portfolioCoin.toUr(); console.log(cbor); // 'a201d99d6fa2010802183c0282d99d70a201d99d6fa303582102d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f006d99d70a1018a183cf500f500f500f401f4021ad34db33f081a78412e3a0282d9010754dac17f958d2ee523a2206206994597c13d831ec7d9010754b8c77482e45f1f44de1745f52c74426c631bdd52d99d70a201d99d6fa203582102eae4b876a8696134b868f88cc2f51f715f2dbedb7446b8e6edf3d4541c4eb67b06d99d70a10188182cf51901f5f500f500f50281782c45506a465764643541756671535371654d32714e31787a7962617043384734774547476b5a77795444743176' console.log(ur); // 'ur:portfolio-coin/oeadtantjloxadwkaxhdclaotdqdinaeesjzmolfzsbbidlpiyhddlcximhltirfsptlvsmohscsamsgzoaxadwtamtantjooeadlecsfnykaeykaeykaewkadwkaocytegtqdfhaycyksfpdmftaolytaadatghnbroinmeswclluensettntgedmnnpftoenamwmfdjlfpcltt' ``` ### Decode the PortfolioCoin with 2 detailed accounts with tokens ```typescript import { PortfolioCoin } from '@ngraveio/ur-sync'; // CBOR taken from the example above const cbor = 'a201d99d6fa2010802183c0282d99d70a201d99d6fa303582102d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f006d99d70a1018a183cf500f500f500f401f4021ad34db33f081a78412e3a0282d9010754dac17f958d2ee523a2206206994597c13d831ec7d9010754b8c77482e45f1f44de1745f52c74426c631bdd52d99d70a201d99d6fa203582102eae4b876a8696134b868f88cc2f51f715f2dbedb7446b8e6edf3d4541c4eb67b06d99d70a10188182cf51901f5f500f500f50281782c45506a465764643541756671535371654d32714e31787a7962617043384734774547476b5a77795444743176'; const portfolioCoin = PortfolioCoin.fromHex(cbor); // Get the coin ID const coinID = portfolioCoin.getCoinId(); // Get the accounts const accounts = portfolioCoin.getAccounts(); ``` ## PortfolioMetadata ### Explanation The `PortfolioMetadata` class represents metadata associated with a portfolio, including sync ID, device information, language, firmware version, and additional data. ```mermaid flowchart TB subgraph PortfolioMetadata direction TB PM[PortfolioMetadata] --> SyncID[Sync ID] PM --> Device[Device] PM --> Language[Language] PM --> FirmwareVersion[Firmware Version] PM --> Data[Data] end ``` **CDDL Definition:** ```CDDL metadata = { ? sync_id: bytes .size 16 ; Generated by the hardware wallet to identify the device ? language: language_code, ; Indicates the selected language on the hardware wallet ? fw_version: string, ; Firmware version of the hardware wallet ? device: string ; Indicates the device name } sync_id = 1 language = 2 fw_version = 3 device = 4 language_code = string ; following [ISO 639-1] Code (e.g. "en" for English, "fr" for French, "nl" for Dutch and "es" for Spanish ``` ### Create a PortfolioMetadata ```typescript import { PortfolioMetadata } from '@ngraveio/ur-sync'; import { Buffer } from 'buffer'; // Create sync id const syncId = Buffer.from('babe0000babe00112233445566778899', 'hex'); // Create metadata const metadata = new PortfolioMetadata({ syncId, device: 'my-device', language: 'en', firmwareVersion: '1.0.0', string: 'hello world', number: 123, boolean: true, array: [1, 2, 3], object: { a: 1, b: 2 }, null: null, date: new Date('2021-01-01T00:00:00.000Z'), }); const cbor = metadata.toHex(); const ur = metadata.toUr(); console.log(cbor); // 'ab0150babe0000babe001122334455667788990262656e0365312e302e3004696d792d64657669636566737472696e676b68656c6c6f20776f726c64666e756d626572187b67626f6f6c65616ef565617272617983010203666f626a656374a2616101616202646e756c6cf66464617465c11a5fee6600' console.log(ur); // 'ur:portfolio-metadata/pyadgdrdrnaeaerdrnaebycpeofygoiyktlonlaoidihjtaxihehdmdydmdyaainjnkkdpieihkoiniaihiyjkjyjpinjtiojeisihjzjzjlcxktjljpjzieiyjtkpjnidihjpcskgioidjljljzihhsjtykihhsjpjphskklsadaoaxiyjlidimihiajyoehshsadhsidaoiejtkpjzjzynieiehsjyihsecyhewyiyaeahhngoeo' ``` ### Decode a PortfolioMetadata ```typescript import { PortfolioMetadata } from '@ngraveio/ur-sync'; // CBOR result after scanning the QR code const cbor = 'ab0150babe0000babe001122334455667788990262656e0365312e302e3004696d792d64657669636566737472696e676b68656c6c6f20776f726c64666e756d626572187b67626f6f6c65616ef565617272617983010203666f626a656374a2616101616202646e756c6cf66464617465c11a5fee6600'; // Convert the CBOR data into the PortfolioMetadata const metadata = PortfolioMetadata.fromHex(cbor); console.log(metadata.getSyncId()?.toString('hex')); // 'babe0000babe00112233445566778899' console.log(metadata.getlanguage()); // 'en' console.log(metadata.getDevice()); // 'my-device' console.log(metadata.getFirmwareVersion()); // '1.0.0' console.log(metadata.data); // { string: 'hello world', number: 123, boolean: true, array: [1, 2, 3], object: { a: 1, b: 2 }, null: null, date: new Date('2021-01-01T00:00:00.000Z') } ``` ## Portfolio ### Explanation The `Portfolio` class represents a collection of coins and associated metadata. ```mermaid flowchart TB %% portfolio breakdown Sync[(portfolio)] subgraph portfolio direction TB Sync --> Coins[[portfolio-coin]] Sync -.-> Meta[(portfolio-metadata)] end %% portfolio-metadata breakdown subgraph portfolio-metadata direction TB Meta -...-> sync_id Meta -...-> language Meta -...-> fw_version Meta -...-> device end %% portfolio-coin breakdown subgraph portfolio-coin direction TB Coins --> ID[(coin-identity)] Coins -.-> MF2[master-fingerprint] Coins --> DetAcc[[detailed-account]] end %% detailed-account breakdown subgraph detailed-account direction TB DetAcc --> key{Account} key --> HD[(hdkey)] key --> |If script type| Out[(output-descriptor)] DetAcc -.-> Token[[token-id]] end %% coin-identity breakdown subgraph coin-identity direction TB ID ---> elliptic_curve ID ---> coin_type ID -..-> Subtypes[[subtype]] end ``` **CDDL Definition:** ```CDDL ; Top level multi coin sync payload sync = { coins: [+ #6.41403(portfolio-coin)], ; Multiple coins with their respective accounts and coin identities ? metadata: #6.41404(portfolio-metadata) ; Optional wallet metadata } coins = 1 metadata = 2 ``` ### Create a Portfolio with 4 coins and Metadata ```typescript import { HDKey, Keypath, OutputDescriptor } from '@ngraveio/ur-blockchain-commons'; import { DetailedAccount, PortfolioCoin, Portfolio, PortfolioMetadata } from '@ngraveio/ur-sync'; import { CoinIdentity, EllipticCurve } from '@ngraveio/ur-coin-identity'; import { Buffer } from 'buffer'; // Create the coin identities of the 4 desired coins. const coinIdEth = new CoinIdentity(EllipticCurve.secp256k1, 60); const coinIdSol = new CoinIdentity(EllipticCurve.secp256k1, 501); const coinIdMatic = new CoinIdentity(EllipticCurve.secp256k1, 60, [137]); const coinIdBtc = new CoinIdentity(EllipticCurve.secp256k1, 0); // Create the accounts that will be included in the coins. // Ethereum with USDC ERC20 token const accountEth = new DetailedAccount({ account: new HDKey({ isMaster: false, keyData: Buffer.from('032503D7DCA4FF0594F0404D56188542A18D8E0784443134C716178BC1819C3DD4', 'hex'), chainCode: Buffer.from('D2B36900396C9282FA14628566582F206A5DD0BCC8D5E892611806CAFB0301F0', 'hex'), origin: new Keypath({ path: "m/44'/60'/0'" }), children: new Keypath({ path: "0/1" }), }), tokenIds: ['0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'], // USDC ERC20 token on Ethereum }); // Polygon with USDC ERC20 token const accountMatic = new DetailedAccount({ account: new HDKey({ isMaster: false, keyData: Buffer.from('032503D7DCA4FF0594F0404D56188542A18D8E0784443134C716178BC1819C3DD4', 'hex'), chainCode: Buffer.from('D2B36900396C9282fa14628566582F206A5DD0BCC8D5E892611806CAFB0301F0', 'hex'), origin: new Keypath({ path: "m/44'/60'/0'" }), children: new Keypath({ path: "0/1" }), }), tokenIds: ['2791Bca1f2de4661ED88A30C99A7a9449Aa84174'], // USDC ERC20 token on Polygon }); // Solana with USDC SPL token const accountSol = new DetailedAccount({ account: new HDKey({ isMaster: false, keyData: Buffer.from('02EAE4B876A8696134B868F88CC2F51F715F2DBEDB7446B8E6EDF3D4541C4EB67B', 'hex'), origin: new Keypath({ path: "m/44'/501'/0'/0" }), }), tokenIds: ['EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'], // USDC SPL token }); // Account with crypto-output public key hash const accountBtc = new DetailedAccount({ account: new OutputDescriptor({ source: 'pkh(@0)', keys: [ new HDKey({ isMaster: false, keyData: Buffer.from('03EB3E2863911826374DE86C231A4B76F0B89DFA174AFB78D7F478199884D9DD32', 'hex'), chainCode: Buffer.from('6456A5DF2DB0F6D9AF72B2A1AF4B25F45200ED6FCC29C3440B311D4796B70B5B', 'hex'), origin: new Keypath({ path: "m/44'/0'/0'/0/0" }), children: new Keypath({ path: "0/0" }), }), ], }), }); // Create the coins const cryptoCoinEth = new PortfolioCoin({ coinId: coinIdEth, accounts: [accountEth] }); const cryptoCoinSol = new PortfolioCoin({ coinId: coinIdSol, accounts: [accountSol] }); const cryptoCoinMatic = new PortfolioCoin({ coinId: coinIdMatic, accounts: [accountMatic] }); const cryptoCoinBtc = new PortfolioCoin({ coinId: coinIdBtc, accounts: [accountBtc] }); // Create the metadata. const metadata = new PortfolioMetadata({ syncId: Buffer.from('123456781234567802D9044FA3011A71', 'hex'), language: 'en', firmwareVersion: '1.2.1-1.rc', device: 'NGRAVE ZERO', }); // Create the Portfolio const portfolio = new Portfolio({ coins: [cryptoCoinEth, cryptoCoinSol, cryptoCoinMatic, cryptoCoinBtc], metadata }); const cbor = portfolio.toHex(); const ur = portfolio.toUr(); console.log(cbor); // 'a20184d9057ba201d90579a3010802183c03f70281d9057aa201d99d6fa4035821032503d7dca4ff0594f0404d56188542a18d8e0784443134c716178bc1819c3dd4045820d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f006d99d70a10186182cf5183cf500f507d99d70a1018400f401f40281d9010754a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48d9057ba201d90579a30108021901f503f70281d9057aa201d99d6fa203582102eae4b876a8696134b868f88cc2f51f715f2dbedb7446b8e6edf3d4541c4eb67b06d99d70a10188182cf51901f5f500f500f50281782c45506a465764643541756671535371654d32714e31787a7962617043384734774547476b5a77795444743176d9057ba201d90579a3010802183c038118890281d9057aa201d99d6fa4035821032503d7dca4ff0594f0404d56188542a18d8e0784443134c716178bc1819c3dd4045820d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f006d99d70a10186182cf5183cf500f507d99d70a1018400f401f40281d90107542791bca1f2de4661ed88a30c99a7a9449aa84174d9057ba201d90579a30108020003f70281d9057aa101d90134d90193d99d6fa403582103eb3e2863911826374de86c231a4b76f0b89dfa174afb78d7f478199884d9dd320458206456a5df2db0f6d9af72b2a1af4b25f45200ed6fcc29c3440b311d4796b70b5b06d99d70a10186182cf500f500f507d99d70a1018400f400f402d9057ca40150123456781234567802d9044fa3011a710262656e036a312e322e312d312e7263046b4e4752415645205a45524f' console.log(ur); // 'ur:portfolio/oeadtantjloxadwkaxhdclaotdqdinaeesjzmolfzsbbidlpiyhddlcximhltirfsptlvsmohscsamsgzoaxadwtamtantjooeadlecsfnykaeykaeykaewkadwkaocytegtqdfhaycyksfpdmftaolytaadatgh ```