GridPlus SDK¶
The GridPlus SDK allows any application to establish a connection and interact with a GridPlus Lattice device.
Installation¶
This SDK is currently only available as a node.js
module. You can add it to your project with:
npm install gridplus-sdk
You can then import a new client with:
import { Client } from 'gridplus-sdk';
or, for older style syntax:
const Sdk = require('gridplus-sdk').Client;
Instantiating a Client¶
Once imported, you can instantiate your SDK client with a clientConfig
object, which at minimum requires the name of your app (name
) and a private key with which to sign requests (privKey
). The latter is not meant to e.g. hold onto any cryptocurrencies; it is simply a way of maintaining a secure communication channel between the device and your application.
const crypto = require('crypto');
const clientConfig = {
name: 'MyApp',
crypto: crypto,
privKey: crypto.randomBytes(32).toString('hex')
}
Client options¶
Param | Type | Default | Description |
---|---|---|---|
name |
string | None | Name of the app. This will appear on the Lattice screen for requests. Not required, but strongly suggested. |
privKey |
buffer | None | Private key buffer used for encryption/decryption of Lattice messages. A random private key will be generated and stored if none is provided. Note that you will need to persist the private key between SDK sessions! |
crypto |
object | None | Crypto function package (e.g. node.js ' native crypto module) |
timeout |
number | 60000 | Number of milliseconds to needed to timeout on a Lattice request |
baseUrl |
string | https://signing.gridpl.us |
Hostname of Lattice request handlerName of the app. You probably don't need to ever change this. |
Connecting to a Lattice¶
With the clientConfig
filled out, you can instantiate a new SDK object:
const client = new Client(clientConfig);
With the client object, you can make a connection to any Lattice device which is connected to the internet:
const deviceId = 'MY_LATTICE';
client.connect(deviceId, (err, isPaired) => {
...
});
If you get a non-error response, it means you can talk to the device. Note that the response also tells you whether you are paired with the device.
ThedeviceId
is listed on your Lattice underSettings->Device Info
Canceling a Pairing Request¶
If you get isPaired = false
in the callback, this request will have started the pairing request with the specified device, which will now be showing a random 8 character pairing code for 60 seconds.
If you wish to cancel this request, you may call pair()
with an empty string ''
as the first argument. This will
gracefully cancel the request. You may also call pair()
with any random string which will also cancel the request, but
the Lattice will show an error screen.
Pairing with a Lattice¶
This function requires the user to interact with the Lattice. It therefore uses your client’s timeout to sever the request if needed.
When connect
is called, your Lattice will draw a random, six digit secret on the screen. The SDK uses
this to “pair” with the device:
client.pair('SECRET', (err, hasActiveWallet) => {
...
});
A non-error response indicates you may now make encrypted requests.
IfhasActiveWallet = false
, it means there was an error fetching the current wallet on the device. This could mean the device has not been set up or that a SafeCard is inserted which has not been set up. It could also mean there was an error with the connection. If you try to get addresses or sign without an active wallet saved (it is saved automatically ifhasActiveWallet = true
), the SDK will automatically retry fetching the active wallet before making the original request.
Getting Addresses¶
If the SDK is connected to the wrong wallet or if the device has no current active wallet, this request will take additional time to complete.
You may retrieve some number of addresses for supported cryptocurrencies. The Lattice uses BIP44-compliant highly-deterministic (HD) wallets for generating addresses. You may request a set of contiguous addresses (e.g. indices 5 to 10 or 33 to 36) based on a currency (ETH
or BTC
). For now, you may only request a maximum of 10 addresses at a time from the Lattice per request.
NOTE: For BTC, the type of address returned will be based on the user’s setting. For example, if the user’s latter is configured to return segwit addresses, you will get addresses that start with3
.
An example request looks like:
const HARDENED_OFFSET = 0x80000000;
const req = {
// -- m/49'/0'/0'/0/0, i.e. first BTC address
startPath: [HARDENED_OFFSET+49, HARDENED_OFFSET, HARDENED_OFFSET, 0, 0],
n: 4
};
client.addresses(req, (err, res) => {
...
})
NOTE: For v1, the Lattice1 only supportsp2sh-p2wpkh
BTC addresses, which require a49'
purpose, per BIP49. Ethereum addresses use the legacy44'
purpose.
Options:
Param | Type | Default | Options | Description |
---|---|---|---|---|
startPath |
Array | none | n/a | First address path in BIP44 tree to return. You must provide 5 indices to form the path. |
n |
number | 1 | n/a | Number of subsequent addresses after start to derive. These will increment over the final index in the path |
Response:
Returns an array of address strings (if the user’s Lattice is configured to return segwit addresses):
res = [
'3PKEDaainApM4u5Tqm1nn3txzZWbtFXUQ2',
'3He2JrsT33DEnjCgdpPgc6RXD3UogALCNF',
'3QybQyM8i9YR9e9Tgb1zLsYHHRXWF1eDAR',
'3PNwCSHKNfCjzvcU8XE9N8wp8DRxrUzsyL'
]
Requesting Signatures¶
This function requires the user to interact with the Lattice. It therefore uses your client’s timeout to sever the request if needed. If the SDK is connected to the wrong wallet or if the device has no current active wallet, this request will take additional time to complete.
The Lattice device, at its core, is a tightly controlled, highly configurable, cryptographic signing machine. By default, each pairing (the persistent association between your app and a user’s lattice) allows the app an ability to request signatures that the user must manually authorize.
Request Types¶
The following types of requests are currently supported by the Lattice. These correspond to the currency
param in the sign
options (signOpts
below)
ETH
(Ethereum transaction)¶
Ethereum transactions consist of six fields. An example payload looks as follows:
const data = {
nonce: '0x01',
gasLimit: '0x61a8,
gasPrice: '0x2540be400,
to: '0xe242e54155b1abc71fc118065270cecaaf8b7768',
value: 0,
data: '0x12345678'
// -- m/44'/60'/0'/0/0
signerPath: [HARDENED_OFFSET+44, HARDENED_OFFSET+60, HARDENED_OFFSET, 0, 0],
chainId: 'rinkeby',
useEIP155: false,
}
const signOpts = {
currency: 'ETH',
data: data,
}
Param | Type | Restrictions |
---|---|---|
nonce |
hex string or number | None |
gasLimit |
hex string or number | Must be >=22000 |
gasPrice |
hex string or number | Must be >0 |
to |
hex string | Must be 20 bytes (excluding optional 0x prefix) |
value |
hex string or number | None |
data |
hex string | Must be <557 bytes |
signerPath |
Array | Address path from which to sign this transaction. NOTE: Ethereum wallets typically use the path specified in the example above for all transactions. |
chainId |
hex string or number | Can be hex string, number, or name. See name options below. Default=mainnet |
eip155 |
bool | Optional. Set the value you want to override the default EIP155 usage of the given chain (see below) |
Chain ID¶
The chainId
param is used to provide replay protectin for most Ethereum-based chains. We allow several ways to specify this:
- A “named” chain, with options being:
mainnet
,ropsten
,rinkeby
,kovan
,goerli
- An integer (only recommended for small numbers – see below section)
- A hex string (e.g.
0x1234
)
Hex strings are strongly recommended
Generally, we recommend not using Javascript integers and never using them for fields that may contain large values, such as value
(which is measured in units of wei, where 10**18 wei = 1 ether). We recommend using hex strings instead, as shown in the example above. Consider the following dummy code in node.js
:
> new bn(2).pow(64).toString(16)
'10000000000000000'
> (2**64).toString(16)
'10000000000000000'
> (2**64-2).toString(16)
'10000000000000000'
> new bn(2**64).toString(16)
'10000000000000180'
> 2**64
18446744073709552000
> new bn(18446744073709552000-2).toString(16)
'10000000000000180'
As you can see, all sorts of problems arise from large Javascript integers. Don’t use them!
Note that in the gridplus-sdk
, all numerical inputs are converted to big numbers, but we still recommend avoiding them.
“Named” chainId
s
We support a hand full of human-readable strings for specifying a network. These include the Ethereum mainnet and current widely used testnets. It is important to note that some networks use EIP155 by default and others don’t. You can, of course, specify whether you want to use EIP155 or not explicitly using the eip155
param. Please see the following table for EIP155 defaults:
Network | Number | Uses EIP155 by default |
---|---|---|
mainnet |
1 | Yes |
ropsten |
3 | No |
rinkeby |
4 | No |
kovan |
42 | Yes |
goerli |
5 | Yes |
Others | n/a | Yes |
ETH_MSG
(Ethereum message)¶
In addition to transactions, we support signing ETH messages, e.g.:
const data = {
protocol: 'signPersonal',
payload: '0xdeadbeef',
signerPath: [HARDENED_OFFSET+44, HARDENED_OFFSET+60, HARDENED_OFFSET, 0, 0],
}
const signOpts = {
currency: 'ETH_MSG',
data: data,
}
Param | Type | Restrictions |
---|---|---|
protocol |
string | Must be one of supported protocols specified below |
payload |
Buffer or string | Must be hex string or buffer type. Raw, serialized data to be signed. Please do not include protocol headers in this data. |
signerPath |
Array | Address path from which to sign this transaction. NOTE: Ethereum wallets typically use the path specified in the example above for all transactions. |
BTC
(Bitcoin transaction)¶
Bitcoin transactions are constructed by referencing a set of inputs to spend and a recipient + output value. You should also specify a change address path (defaults to m/44'/0'/0'/1/0
):
const data = {
prevOuts: [
{
txHash: '08911991c5659349fa507419a20fd398d66d59e823bca1b1b94f8f19e21be44c',
value: 3469416,
index: 1,
signerPath: [HARDENED_OFFSET+49, HARDENED_OFFSET, HARDENED_OFFSET, 1, 0],
},
{
txHash: '19e7aa056a82b790c478e619153c35195211b58923a8e74d3540f8ff1f25ecef',
value: 3461572,
index: 0,
signerPath: [HARDENED_OFFSET+49, HARDENED_OFFSET, HARDENED_OFFSET, 0, 5],
}
],
recipient: 'mhifA1DwiMPHTjSJM8FFSL8ibrzWaBCkVT',
value: 1000,
fee: 1000,
isSegwit: true,
changePath: [HARDENED_OFFSET+49, HARDENED_OFFSET, HARDENED_OFFSET, 1, 1],
=}
const signOpts = {
currency: 'BTC',
data: data,
}
Param | Type | Restrictions | Description |
---|---|---|---|
prevOuts->txHash |
string | Must be 32 bytes | Transaction hash of the previous output |
prevOuts->value |
number | Must be >0 | Value of the previous output |
prevOuts->index |
number | Must be <255 | Index of this previous output in the transaction |
prevOuts->signerPath |
Array | Must have 5x 4-byte numbers | BIP44 address path needed to sign this input |
recipient |
string | Must be a valid address | Address you are sending to |
value |
number | Must be >0 | Number of satoshis you are sending to recipient |
fee |
number | Must be >0 | Number of satoshis reserved for the transaction fee |
isSegwit |
bool | Must be true/false | True if the inputs are encumbered by P2SH(P2WPKH), i.e. segwit |
changePath |
Array | Must have 5x 4-byte numbers | BIP44 address path to which the change will go |
Requesting the Signature¶
Once you build the data needed, you can request a signature using the following pattern:
client.sign(signOpts, (err, signedTx) => {
})
Response
The returned signedTx
object has the following properties:
Currency | Param | Type | Description |
---|---|---|---|
ETH / BTC | tx |
string | Ready-to-broadcast, serialized transaction + signature payload |
ETH / BTC | txHash |
string | Hash of the transaction for lookup on the relevant block explorer |
ETH | sig |
object | Contains v (int), r (string), and s (string) signature params |
BTC | changeRecipient |
string | Lattice wallet address that recieved the BTC change |
Getting Active Wallets¶
The Lattice1 has two wallet “slots”: an internal wallet that is always the same for a given device and an external slot for SafeCard wallets. When a SafeCard is inserted or removed, the external slot is updated. If a wallet is present in a given slot, the device will allow paired requesters to get the “wallet UID”, against which addresses or signatures may be requested. This UID is a permanent identifier for a given wallet (i.e. every SafeCard, once setup, will have a permanent UID that maps directly to a wallet seed and, therefore, to a set of addresses).
Although these requests are abstracted from the user of this SDK, you may look at the active wallets currently known by the SDK. This may be useful for determining if there is a SafeCard inserted.
const wallet = client.getActiveWallet();
This will return an object containing:
uid // 32 byte buffer id
name // 20 char (max) string
capabilities // 4 byte flag
external // boolean
Where uid
is a 32-byte buffer containing the wallet UID discussed above and external
is true
if the active wallet is a SafeCard.
**NOTE: If a SafeCard is inserted, this will be the data returned from getActiveWallet()
. When it is removed, you will get the internal wallet data.
Currently, name
and capabilities
are not used.
Detecting Card Insertion/Removal¶
When a card is inserted or removed, this will affect the active wallet of the device. If you want to stay up to date on the latest wallet state, you will need to refresh the active wallet. You can do this by “re-connecting”:
client.connect((err) => {
activeWallet = client.getActiveWallet();
})
Note that you may only call connect
with one argument once a deviceID
has been saved, i.e. after you’ve called connect
once with the device ID as the first argument.