New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More โ†’
Socket
Sign inDemoInstall
Socket

gridplus-sdk

Package Overview
Dependencies
Maintainers
1
Versions
182
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

gridplus-sdk

SDK to interact with GridPlus Lattice1 device

  • 1.1.0
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
1.2K
decreased by-25.2%
Maintainers
1
Weekly downloads
ย 
Created
Source

image

GridPlus Lattice1 SDK

  • For full API docs, see this
  • For Lattice docs, see this

This SDK is designed to facilitate communication with a user's Lattice1 hardware wallet. Once paired to a given Lattice, an instance of this SDK is used to make encrypted requests for things like getting addresses/public keys and making signatures.

The Lattice1 is an internet connected device which listens for requests and fills them in firmware. Web requests originate from this SDK and responses are returned asynchronously. Some requests require user authorization and may time out if the user does not approve them.

๐ŸŽฌ Getting Started

First install this SDK with:

npm install --save gridplus-sdk

To connect to a Lattice, you need to create an instance of Client:

import { Client } from 'gridplus-sdk'

You can use the following options:

ParamTypeRequiredDescription
namestringYA human readable name for your app. This will be displayed with your app's pairing on the user's Lattice.
privKeyBufferN32-byte buffer, not required but highly recommended. If none is specified, a random one will be created at initialization. This is used to build the encrypted channel. If you want to manually restart a connection with a new Client instance, you will need to use the same privKey in the constructor.
retryCountnumberNDefault is 3. Number of automatic retries allowed per request. Covers timeouts and certain device errors.
timeoutnumberNMilliseconds to timeout HTTP request. Defaults to 60s.
stateDatastringNUsed to rehydrate a session without other params. Result of call to getStateData().

๐Ÿ”— Connecting to a Lattice

Once Client is initialized, you need to connect to the target Lattice. This can happen one of three ways:

1๏ธโƒฃ Pairing with a new Lattice

If you have not setup a pairing with the Lattice in question, you will need to do that first.

  1. Call connect with the deviceId of the target Lattice
  2. The Lattice should generate and display a pairing code, valid for 60 seconds. Call pair with this code.
  3. If successful, you should now have a pairing between the SDK and the Lattice. This pairing maintains an encrypted channel. If that ever gets out of sync, it should repair automatically with a retry.

Example: pairing with a Lattice

const isPaired = await client.connect(deviceID)
if (!isPaired) {
  // Wait for the user to enter the pairing secret displayed on the device  
  const secret = await question('Enter pairing secret: ')
  await client.pair(secret)
}
2๏ธโƒฃ Connecting to a known Lattice

If the Lattice in question already has a pairing with your app (and therefore a recoverable encrypted channel), the connection process is easy.

  1. Call connect with the deviceId of the target Lattice

Example: connecting to a known Lattice

const isPaired = await client.connect(deviceID)

expect(isPaired).to.equal(true)
3๏ธโƒฃ Rehydrating an SDK session

You can always start a new SDK session with the same privKey in the constructor, which will always build an encrypted channel when you call connect on a paired Lattice. However, you can skip this step by exporting state data and then using that in the constructor. First you need to get the state data before you stop using the connection.

Example: drying and rehydrating a client session

// Fetch some addresses from existing client
const addrs1 = await client.getAddresses(addrReqData)

// Capture the state data from that client
const stateData = client.getStateData()

// Create a new client with the state data
const clientDos = new Client({ stateData })

// You can now call this without connecting
const addrs2 = await clientDos.getAddresses(addrReqData)

// The addresses should match and there should be no errors
expect(addrs1).to.equal(addrs2)

๐Ÿ”‘ Addresses and Public Keys

Once your Client instance is connected, you can request a few different address and key types from the Lattice.

Note: this section uses the following notation when discussing BIP32 derivation paths: [ purpose, coin_type, account, change, address ]. It also uses ' to represent a "hardened", index, which is just 0x80000000 + index.

ฮž Ethereum-type addresses

These addresses are 20-byte hex strings prefixed with 0x. Lattice firmware places some restrictions based on derivation path, specifically that the coin_type must be supported (Ethereum uses coin type 60').

In practice, most apps just use the standard Ethereum coin_type (60') when requesting addresses for other networks, but we do support some others (a vestige of an integration -- you probably won't ever need to use these):

966', 700', 9006', 9005', 1007', 178', 137', 3731', 1010', 61', 108', 40', 889', 1987', 820', 6060', 1620', 1313114', 76', 246529', 246785', 1001', 227', 916', 464', 2221', 344', 73799', 246'

Keep in mind that changing the coin_type will change all the requested addresses relative to Ethereum. This is why, in practice, most apps just use the Ethereum path.

Example: requesting Ethereum addresses

const reqData = {
 startPath: [   // Derivation path of the first requested address
   0x80000000 + 44,
   0x80000000 + 60,
   0x80000000,
   0,
   0,
 ],
 n: 5,          // Number of sequential addresses on specified path to return (max 10)
};

const addrs = await client.getAddresses(reqData);

โ‚ฟ Bitcoin addresses

The Lattice can also export Bitcoin formatted addresses. There are three types of addresses that can be fetched and the type is determined by the purpose index of the BIP32 derivation path.

  • If purpose = 44', legacy addresses (beginning with 1) will be returned
  • If purpose = 49', wrapped segwit addresses (beginning with 3) will be returned
  • If purpose = 84', segwit v1 addresses (beginning with bc1) will be returned

Keep in mind that coin_type 0' is required when requesting BTC addresses.

Example: requesting BTC segwit addresse

const reqData = {
 startPath: [   // Derivation path of the first requested address
   0x80000000 + 84,
   0x80000000,
   0x80000000,
   0,
   0,
 ]
};

// `n` will be set to 1 if not specified -> 1 address returned
const addr0 = await client.getAddresses(reqData);

๐Ÿ—๏ธ Public Keys

In addition to formatted addresses, the Lattice can return public keys on any supported curve for any BIP32 derivation path.

Note: Currently the derivation path must be at least 2 indices deep, but this restriction may be removed in the future.

For requesting public keys it is best to import Constants with:

import { Client, Constants } from 'gridplus-sdk'
1๏ธโƒฃ secp256k1 curve

Used by Bitcoin, Ethereum, and most blockchains.

Pubkey size: 65 bytes

The public key has two 32 byte components and is of format: 04{X}{Y}, meaning every public key is prefixed with a 04 byte.

Example: requesting secp256k1 public key

const req = {
  startPath: [   // Derivation path of the first requested pubkey
    0x80000000 + 44,
    0x80000000 + 60,
    0x80000000,
    0,
    0,
  ],
  n: 3,
  flag: Constants.GET_ADDR_FLAGS.SECP256K1_PUB,
};

const pubkeys = await client.getAddresses(req);

NOTE: since startPath is the same, this example returns public keys which can be converted to Ethereum addresses to yield the same result as the above request to fetch Ethereum addresses.

2๏ธโƒฃ ed25519 curve

Used by Solana and a few others. Ed25519 requires all derivation path indices be hardened.

Pubkey size: 32 bytes

NOTE: Some libraries prefix these keys with a 00 byte (making them 33 bytes), but we do not return keys with this prefix.

const req = {
  startPath: [   // Derivation path of the first requested pubkey
    0x80000000 + 44,
    0x80000000 + 60,
    0x80000000,
  ],
  n: 3,
  flag: Constants.GET_ADDR_FLAGS.ED25519_PUB,
};

const pubkeys = await client.getAddresses(req);

๐Ÿงพ Signing Transactions and Messages

The Lattice1 is capable of signing messages on supported curves. For certain message types, it is capable of decoding and displaying the requests in more readable ways.

โœ๏ธ General Signing

This new signing mode was introduced Lattice firmare v0.14.0. GridPlus plans on deprecating the legacy signing mode and replacing it with general signing decoders. This document will be updated as that happens.

You should import Constants when using general signing:

import { Constants } from `gridplus-sdk`

๐Ÿ–Š๏ธ Requesting Signatures

General signing allows you to request a signature on any message from a private key derived on any supported curve. Some curves (e.g. secp256k1) require a hashing algorithm to be specified in order to hash the message before signing. Other curves (e.g. ed25519) do not expect hashed messages prior to signing.

ParamLocation in ConstantsOptionsDescription
CurveConstants.SIGNING.CURVESSECP256K1, ED25519Curve on which to derive the signer's private key
HashConstants.SIGNING.HASHESKECCAK256, SHA256, NONEHash to use prior to signing. Note that ED25519 requires NONE as messages are not prehashed.

Example: using generic signing

const msg = "I am the message to sign"
const req = {
  startPath: [
    0x80000000 + 44,
    0x80000000 + 60,
    0x80000000,
  ]
  curveType: Constants.SIGNING.CURVES.ED25519,
  hashType: Constants.SIGNING.HASHES.NONE,
  payload: msg
};

const sig = await client.sign(req)

๐Ÿ“ƒ Message Decoders

By default, the message will be displayed on the Lattice's screen in either ASCII or hex -- if the message contains only ASCII, it will be displayed as such; otherwise it will get printed as a hex string. This means the Lattice can produce a signature for any message you like. However, there are additional decoders that make the request more readable on the Lattice. These decoders can be accessed inside of Constants:

const encodings = Constants.SIGNING.ENCODINGS
EncodingDescription
ASCIIDefault encoding for ASCII messages. Can be specified to force display of ASCII characters on the Lattice, though non-ASCII characters may be displayed with specifial characters
HEXDefault encoding for non-ASCII messages. Can be specified to force display of a hex string on the Lattice
SOLANAUsed to decode a Solana transaction. Transactions that cannot be decoded will be rejected. See test/testGeneric.ts for an example.

If you do not wish to specify a decoder, you can leave this field empty and the message will display either as ASCII or a hex string on the device.

Example: using the Solana decoder

const msg = solTx.compileMessage().serialize()
const req = {
  startPath: [   // Derivation path of the first requested pubkey
    0x80000000 + 44,
    0x80000000 + 60,
    0x80000000,
  ]
  curveType: Constants.SIGNING.CURVES.ED25519,
  hashType: Constants.SIGNING.HASHES.NONE,
  encodingType: Constants.SIGNING.ENCODINGS.SOLANA,
  payload: msg
};

const sig = await client.sign(req)

๐Ÿ“œ Legacy Signing

Prior to general signing, request data was sent to the Lattice in preformatted ways and was used to build the transaction in firmware. We are phasing out this mechanism, but for now it is how you request Ethereum, Bitcoin, and Ethereum-Message signatures. These signing methods are accessed using the currency flag in the request data.

ฮž Ethereum (Transaction)

All six Ethereum transactions must be specified in the request data along with a signer path.

Example: requesting signature on Ethereum transaction

const txData = {
  nonce: '0x02',
  gasPrice: '0x1fe5d61a00',
  gasLimit: '0x034e97',
  to: '0x1af768c0a217804cfe1a0fb739230b546a566cd6',
  value: '0x01cba1761f7ab9870c',
  data: '0x17e914679b7e160613be4f8c2d3203d236286d74eb9192f6d6f71b9118a42bb033ccd8e8',
}

const reqData = {
  currency: 'ETH',
  data: {
    signerPath: [
      0x80000000 + 44,
      0x80000000 + 60,
      0x80000000,
      0,
      0,
    ],
    ...txData,
    chain: 5, // Defaults to 1 (i.e. mainnet)
  }
}

const sig = await client.sign(reqData)

ฮž Ethereum (Message)

Two message protocols are supported for Ethereum: personal_sign and sign_typed_data.

personal_sign

This is a protocol to display a simple, human readable message. It includes a prefix to avoid accidentally signing sensitive data. The message included should be a string.

protocol must be specified as "signPersonal".

Example: requesting signature on Ethereum personal_sign message

const reqData = {
  currency: 'ETH_MSG',
  data: {
    signerPath: [
      0x80000000 + 44,
      0x80000000 + 60,
      0x80000000,
      0,
      0,
    ],
    protocol: 'signPersonal' // You must use this string to specify this protocol
    payload: 'my message to sign'
  }
}

const sig = await client.sign(reqData)
sign_typed_data

This is used in protocols such as EIP712. It is meant to be an encoding for JSON-like data that can be more human readable.

NOTE: Only sign_typed_data V3 and V4 are supported.

protocol must be specified as "eip712".

const message = {
  hello: 'i am a message',
  goodbye: 1
}
const reqData = {
  currency: 'ETH_MSG',
  data: {
    signerPath: [
      0x80000000 + 44,
      0x80000000 + 60,
      0x80000000,
      0,
      0,
    ],
    protocol: 'eip712' // You must use this string to specify this protocol
    payload: message
  }
}

const sig = await client.sign(reqData)

โ‚ฟ Bitcoin

Bitcoin transactions can be requested by including a set of UTXOs, which include the signer derivation path and spend type. The same purpose values are used to determine how UTXOs should be signed:

  • If purpose = 44', the input will be signed with p2pkh
  • If purpose = 49', the input will signed with p2sh-p2wpkh
  • If purpose = 84', the input will be signed with p2wpkh

The purpose of the signerPath in the given previous output (a.k.a. UTXO) is used to make the above determination.

Example: requesting BTC transactions

const p2wpkhInputs = [
  {
    // Hash of transaction that produced this UTXO
    txHash: "2aba3db3dc5b1b3ded7231d90fe333e184d24672eb0b6466dbc86228b8996112",
    // Value of this UTXO in satoshis (1e8 sat = 1 BTC)
    value: 100000,
    // Index of this UTXO in the set of outputs in this transaction
    index: 3,
    // Owner of this UTXO. Since `purpose` is 84' this will be spent with p2wpkh,
    // meaning this is assumed to be a segwit address (starting with bc1)
    signerPath: [
      0x80000000 + 84,
      0x80000000,
      0x80000000,
      0,
      12
    ]
  }
]

const reqData = {
  currency: "BTC",
  data: {
    prevOuts: p2wpkhInputs,
    // Recipient can be any legacy, wrapped segwit, or segwit address
    recipient: "1FKpGnhtR3ZrVcU8hfEdMe8NpweFb2sj5F",
    // Value (in sats) must be <= (SUM(prevOuts) - fee)
    value: 50000,
    // Fee (in sats) goes to the miner
    fee: 20000,
    // SUM(prevOuts) - fee goes to the change recipient, which is an
    // address derived in the same wallet. Again, the `purpose` in this path 
    // determines what address the BTC will be sent to, or more accurately how 
    // the UTXO is locked -- e.g., p2wpkh unlocks differently than p2sh-p2wpkh
    changePath: [
      0x80000000 + 84,
      0x80000000,
      0x80000000,
      1, // Typically the change path includes a `1` here
      0
    ]
  }
}

const sig = await client.sign(reqData)

๐Ÿงช Testing

All functionality is tested in some script in /test. Please see those scripts for examples on functionality not documented in ths README.

Testing is only possible with a development Lattice, which GridPlus does not distribute publicly. Therefore, if you do not have a development Lattice, you will not be able to run many of these tests.

Setting up a test connection

Only one test can be run against an unpaired Lattice: npm run test. Therefore this must be run before running any other tests. If you wish to run additional tests, you need to specify the following:

env REUSE_KEY=1 npm run test

The REUSE_KEY will save the connection locally so you can run future tests. Running this test will ask for your device ID and will go through the pairing process. After pairing, the rest of the script will test a broad range of SDK functionality.

To use the connection you've established with any test (including this initial one), you need to include your DEVICE_ID as an env argument:

env DEVICE_ID='mydeviceid' npm run test

Global env Options

The following options can be used after env with any test.

ParamOptionsDescription
REUSE_KEYMust be 1Indicates we will be creating a new pairing with a Lattice and stashing that connection
DEVICE_IDA six character stringThe device ID of the target Lattice
nameAny 5-25 character string (default="SDK Test")The name of the pairing you will create
baseUrlAny URL (default="https://signing.gridplus.io")URL describing where to send HTTP requests. Should be changed if your Lattice is on non-default message routing infrastructure.

Firmware Test Runner

Several tests require dev Lattice firmware with the following flag in the root CMakeLists.txt:

FEATURE_TEST_RUNNER=1

See table in the next section.

Reference: Tests and Options

This section gives an overview of each test and which options can be passed for the specific test (in addition to global options)

TestDescriptionUses Test Runnerenv Options
npm run testSets up test connection. Tests getAddresses and sign.NoN/A
npm run test-abiTests Ethereum ABI routes and markdown of requests.NoN (number of random vectors to populate)
seed (random string to seed a random number generator)
npm run test-btcTests spending different types of BTC inputs. Signatures validated against bitcoinjs-lib using seed exported by test harness.YesN (number of random vectors to populate)
seed (random string to seed a random number generator)
testnet (if true, testnet addresses and transactions will also be tested)
npm run test-ethTests Ethereum transactions, specifically boundary conditions of tx params and new tx types like EIP1559.NoN (number of random vectors to populate)
seed (random string to seed a random number generator)
skip (if true, skip param boundary tests and only test random vectors)
npm run test-eth-msgTests Ethereum message requests signPersonal and signTypedData. Tests boundary conditions of EIP712 messages.NoN (number of random vectors to populate)
seed (random string to seed a random number generator)
npm run test-generic-signingTests different curves and hash types. Signatures validated using seed exported by test harness. Also tests decoders.YesN (number of random vectors to populate)
seed (random string to seed a random number generator)
npm run test-kvTests loading and using kv (key-value) files. These are used for address tags.NoN/A
npm run test-sigsTests determinism of signatures and validates derived signatures against reference vectors. Seed is exported by test harness for derivations + validations.YesN (number of random vectors to populate)
seed (random string to seed a random number generator)
npm run test-wallet-jobsTests exported addresses and public keys against those from reference libraries using seed exported by test harness.YesN/A

FAQs

Package last updated on 07 Mar 2022

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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with โšก๏ธ by Socket Inc