Socket
Book a DemoInstallSign in
Socket

@ngraveio/ur-sync

Package Overview
Dependencies
Maintainers
5
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ngraveio/ur-sync

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

latest
Source
npmnpm
Version
2.0.0
Version published
Maintainers
5
Created
Source

Multi Layer Sync Protocol

This is the implementation of the Multi Layer Sync Protocol that supports multiple coins and accounts with different types via globally identifiable URs.

This package adds support for the following UR types:

TypeCBOR TagOwnerDescriptionDefinition
detailed-account41402NgraveImport multiple accounts with and without output descriptors and specify optionally tokens to synchronizeNBCR-2023-002
portfolio-coin41403NgraveAssociate several accounts to its coin identityNBCR-2023-002
portfolio41405NgraveAggregate the portfolio informationNBCR-2023-002

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

Installing

To install, run:

yarn add @ngraveio/ur-sync
npm install --save @ngraveio/ur-sync
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.

flowchart TB
    subgraph DetailedAccount
    direction TB
    DA[DetailedAccount] --> HD[HDKey]
    DA --> Token[Token IDs]
    end

CDDL Definition:

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

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

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.

flowchart TB
    subgraph PortfolioCoin
    direction TB
    PC[PortfolioCoin] --> CI[CoinIdentity]
    PC --> DA[DetailedAccount]
    end

CDDL Definition:

; 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

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

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.

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:

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

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

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.

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:

; 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

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

Keywords

ngrave

FAQs

Package last updated on 20 Feb 2025

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts