
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:
Param | Type | Required | Description |
---|
name | string | Y | A human readable name for your app. This will be displayed with your app's pairing on the user's Lattice. |
privKey | Buffer | N | 32-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. |
retryCount | number | N | Default is 3. Number of automatic retries allowed per request. Covers timeouts and certain device errors. |
timeout | number | N | Milliseconds to timeout HTTP request. Defaults to 60s. |
stateData | string | N | Used 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.
- Call
connect
with the deviceId
of the target Lattice - The Lattice should generate and display a pairing code, valid for 60 seconds. Call
pair
with this code. - 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.
- 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.
Param | Location in Constants | Options | Description |
---|
Curve | Constants.SIGNING.CURVES | SECP256K1 , ED25519 | Curve on which to derive the signer's private key |
Hash | Constants.SIGNING.HASHES | KECCAK256 , SHA256 , NONE | Hash 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 = {
signerPath: [
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
Encoding | Description |
---|
NONE | Can also use null or not specify the encodingType . Lattice will display either an ASCII or a hex string depending on the payload. |
SOLANA | Used to decode a Solana transaction. Transactions that cannot be decoded will be rejected. See test/testGeneric.ts for an example. |
TERRA | Used to decode a Terra transaction. Only MsgSend , MsgMultiSend , and MsgExecuteContract are supported, but any transaction with unsupported message types will still decode -- the message type and calldata will be displayed raw. |
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 = {
signerPath: [ // 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.
Param | Options | Description |
---|
REUSE_KEY | Must be 1 | Indicates we will be creating a new pairing with a Lattice and stashing that connection |
DEVICE_ID | A six character string | The device ID of the target Lattice |
name | Any 5-25 character string (default="SDK Test") | The name of the pairing you will create |
baseUrl | Any 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)
Test | Description | Uses Test Runner | env Options |
---|
npm run test | Sets up test connection. Tests getAddresses and sign . | No | N/A |
npm run test-abi | Tests Ethereum ABI routes and markdown of requests. | No | N (number of random vectors to populate)
seed (random string to seed a random number generator) |
npm run test-btc | Tests spending different types of BTC inputs. Signatures validated against bitcoinjs-lib using seed exported by test harness. | Yes | N (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-eth | Tests Ethereum transactions, specifically boundary conditions of tx params and new tx types like EIP1559. | No | N (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-msg | Tests Ethereum message requests signPersonal and signTypedData . Tests boundary conditions of EIP712 messages. | No | N (number of random vectors to populate)
seed (random string to seed a random number generator) |
npm run test-generic-signing | Tests different curves and hash types. Signatures validated using seed exported by test harness. Also tests decoders. | Yes | N (number of random vectors to populate)
seed (random string to seed a random number generator) |
npm run test-kv | Tests loading and using kv (key-value) files. These are used for address tags. | No | N/A |
npm run test-sigs | Tests determinism of signatures and validates derived signatures against reference vectors. Seed is exported by test harness for derivations + validations. | Yes | N (number of random vectors to populate)
seed (random string to seed a random number generator) |
npm run test-wallet-jobs | Tests exported addresses and public keys against those from reference libraries using seed exported by test harness. | Yes | N/A |