gridplus-sdk
Advanced tools
Comparing version 0.10.5 to 1.0.0
@@ -10,2 +10,4 @@ "use strict"; | ||
var buffer_1 = require("buffer/"); | ||
var ripemd_1 = require("hash.js/lib/hash/ripemd"); | ||
var sha_1 = require("hash.js/lib/hash/sha"); | ||
var constants_1 = require("./constants"); | ||
@@ -136,3 +138,3 @@ var DEFAULT_SEQUENCE = 0xffffffff; | ||
var serializeTx = function (data) { | ||
var inputs = data.inputs, outputs = data.outputs, _a = data.lockTime, lockTime = _a === void 0 ? 0 : _a, crypto = data.crypto; | ||
var inputs = data.inputs, outputs = data.outputs, _a = data.lockTime, lockTime = _a === void 0 ? 0 : _a; | ||
var payload = buffer_1.Buffer.alloc(4); | ||
@@ -163,3 +165,3 @@ var off = 0; | ||
// Build a vector (varSlice of varSlice) containing the redeemScript | ||
var redeemScript = buildRedeemScript(input.pubkey, crypto); | ||
var redeemScript = buildRedeemScript(input.pubkey); | ||
var redeemScriptLen = getVarInt(redeemScript.length); | ||
@@ -243,6 +245,6 @@ var slice = buffer_1.Buffer.concat([redeemScriptLen, redeemScript]); | ||
//----------------------- | ||
function buildRedeemScript(pubkey, crypto) { | ||
function buildRedeemScript(pubkey) { | ||
var redeemScript = buffer_1.Buffer.alloc(22); | ||
var shaHash = crypto.createHash('sha256').update(pubkey).digest(); | ||
var pubkeyhash = crypto.createHash('rmd160').update(shaHash).digest(); | ||
var shaHash = buffer_1.Buffer.from((0, sha_1.sha256)().update(pubkey).digest('hex'), 'hex'); | ||
var pubkeyhash = buffer_1.Buffer.from((0, ripemd_1.ripemd160)().update(shaHash).digest('hex'), 'hex'); | ||
redeemScript.writeUInt8(OP.ZERO, 0); | ||
@@ -249,0 +251,0 @@ redeemScript.writeUInt8(pubkeyhash.length, 1); |
import { UInt4 } from 'bitwise/types'; | ||
import { Buffer } from 'buffer/'; | ||
import { KeyPair } from 'elliptic'; | ||
import { Crypto, KVRecord, ABIRecord, SignData, AddAbiDefsData, GetAbiRecordsData, GetKvRecordsData } from './types/client'; | ||
import { KVRecord, ABIRecord, SignData, AddAbiDefsData, GetAbiRecordsData, GetKvRecordsData } from './types/client'; | ||
/** | ||
@@ -14,3 +13,2 @@ * `Client` is a class-based interface for managing a Lattice device. | ||
private baseUrl; | ||
private crypto; | ||
private name; | ||
@@ -30,13 +28,9 @@ private key; | ||
*/ | ||
constructor({ baseUrl, crypto, name, privKey, stateData, timeout, retryCount }: { | ||
constructor({ baseUrl, name, privKey, stateData, timeout, retryCount }: { | ||
/** The base URL of the signing server. */ | ||
baseUrl?: string; | ||
/** The crypto library to use. Currently only 'secp256k1' is supported. */ | ||
crypto: Crypto; | ||
/** The name of the client. */ | ||
name: string; | ||
name?: string; | ||
/** The private key of the client.*/ | ||
privKey?: Buffer; | ||
/** The public key of the client. */ | ||
key?: KeyPair; | ||
/** Number of times to retry a request if it fails. */ | ||
@@ -59,3 +53,3 @@ retryCount?: number; | ||
*/ | ||
connect(deviceId: string, cb: (err?: string, isPaired?: boolean) => void): void; | ||
connect(deviceId: string, _cb?: (err?: string, isPaired?: boolean) => void): Promise<unknown>; | ||
/** | ||
@@ -68,3 +62,3 @@ * If a pairing secret is provided, `pair` uses it to sign a hash of the public key, name, and | ||
*/ | ||
pair(pairingSecret: string, cb: (err?: string, hasActiveWallet?: boolean) => void): void; | ||
pair(pairingSecret: string, _cb?: (err?: string, hasActiveWallet?: boolean) => void): Promise<unknown>; | ||
/** | ||
@@ -85,3 +79,3 @@ * `test` takes a data object with a testID and a payload, and sends them to the device. | ||
flag: UInt4; | ||
}, cb: (err?: string, data?: Buffer | string[]) => void): any; | ||
}, _cb?: (err?: string, data?: Buffer | string[]) => void): Promise<unknown>; | ||
/** | ||
@@ -95,3 +89,3 @@ * `sign` builds and sends a request for signing to the device. | ||
currency: string; | ||
}, cb: (err?: string, data?: SignData) => void, cachedData?: any, nextCode?: any): any; | ||
}, _cb?: (err?: string, data?: SignData) => void, cachedData?: any, nextCode?: any): Promise<unknown>; | ||
/** | ||
@@ -102,3 +96,3 @@ * `addAbiDefs` sends a list of ABI definitions to the device in chunks of up to `MAX_ABI_DEFS`. | ||
*/ | ||
addAbiDefs(defs: ABIRecord[], cb: (err?: string, data?: AddAbiDefsData) => void, nextCode?: any): any; | ||
addAbiDefs(defs: ABIRecord[], _cb?: (err?: string, data?: AddAbiDefsData) => void, nextCode?: any): Promise<unknown>; | ||
/** | ||
@@ -114,3 +108,3 @@ * `getAbiRecords` fetches a set of ABI records saved on the Lattice. You can fetch | ||
category: string; | ||
}, cb: (err?: string, data?: GetAbiRecordsData) => void, fetched?: GetAbiRecordsData): any; | ||
}, _cb?: (err?: string, data?: GetAbiRecordsData) => void, fetched?: GetAbiRecordsData): Promise<unknown>; | ||
/** | ||
@@ -124,3 +118,3 @@ * `removeAbiRecords` requests removal of ABI records on the device. You can request | ||
sigs: (number | string)[]; | ||
}, cb: (err?: string, data?: { | ||
}, _cb?: (err?: string, data?: { | ||
numRemoved: number; | ||
@@ -131,3 +125,3 @@ numTried: number; | ||
numTried: number; | ||
}): any; | ||
}): Promise<unknown>; | ||
/** | ||
@@ -144,3 +138,3 @@ * `addPermissionV0` takes in a currency, time window, spending limit, and decimals, and builds a | ||
asset: string; | ||
}, cb: (err?: string) => void): any; | ||
}, _cb?: (err?: string) => void): Promise<unknown>; | ||
/** | ||
@@ -154,3 +148,3 @@ * `getKvRecords` fetches a list of key-value records from the Lattice. | ||
start?: number; | ||
}, cb: (err?: string, data?: GetKvRecordsData) => void): any; | ||
}, _cb?: (err?: string, data?: GetKvRecordsData) => void): Promise<unknown>; | ||
/** | ||
@@ -166,3 +160,3 @@ * `addKvRecords` takes in a set of key-value records and sends a request to add them to the | ||
caseSensitive: boolean; | ||
}, cb: (err?: string) => void): any; | ||
}, _cb?: (err?: string) => void): Promise<unknown>; | ||
/** | ||
@@ -176,3 +170,3 @@ * `removeKvRecords` takes in an array of ids and sends a request to remove them from the Lattice. | ||
ids: number[]; | ||
}, cb: (err?: string) => void): any; | ||
}, _cb?: (err?: string) => void): Promise<unknown>; | ||
/** | ||
@@ -179,0 +173,0 @@ * Get the active wallet in the device. If we already have one recorded, we don't need to do |
import { Buffer } from 'buffer/'; | ||
import 'hash.js'; | ||
declare type Hash = { | ||
hash: Sha256Constructor | Ripemd160Constructor; | ||
update: (x: any) => Hash; | ||
digest: () => Buffer; | ||
}; | ||
export declare type Crypto = { | ||
get32RandomBytes: () => Buffer; | ||
generateEntropy: () => Buffer; | ||
randomBytes: (n: number) => Buffer; | ||
createHash: (type: any) => Hash; | ||
}; | ||
export declare type ABIRecord = { | ||
@@ -70,3 +59,2 @@ header: { | ||
}; | ||
export {}; | ||
//# sourceMappingURL=client.d.ts.map |
@@ -20,2 +20,8 @@ import { Buffer } from 'buffer/'; | ||
export declare const existsIn: (val: any, obj: any) => boolean; | ||
/** | ||
* `promisifyCb` accepts `resolve` and `reject` from a `Promise` and an optional callback. | ||
* It returns that callback if it exists, otherwise it resolves or rejects as a `Promise` | ||
*/ | ||
export declare const promisifyCb: (resolve: any, reject: any, cb: (err: string, ...cbParams: any[]) => void) => (err: any, ...params: any[]) => any; | ||
export declare const randomBytes: (n: any) => Buffer; | ||
//# sourceMappingURL=util.d.ts.map |
"use strict"; | ||
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { | ||
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { | ||
if (ar || !(i in from)) { | ||
if (!ar) ar = Array.prototype.slice.call(from, 0, i); | ||
ar[i] = from[i]; | ||
} | ||
} | ||
return to.concat(ar || Array.prototype.slice.call(from)); | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
@@ -6,3 +15,3 @@ return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.existsIn = exports.isAsciiStr = exports.buildSignerPathBuf = exports.getP256KeyPairFromPub = exports.getP256KeyPair = exports.parseDER = exports.aes256_decrypt = exports.aes256_encrypt = exports.fixLen = exports.ensureHexBuffer = exports.splitFrames = exports.isValidAssetPath = exports.toPaddedDER = exports.checksum = exports.parseLattice1Response = void 0; | ||
exports.randomBytes = exports.promisifyCb = exports.existsIn = exports.isAsciiStr = exports.buildSignerPathBuf = exports.getP256KeyPairFromPub = exports.getP256KeyPair = exports.parseDER = exports.aes256_decrypt = exports.aes256_encrypt = exports.fixLen = exports.ensureHexBuffer = exports.splitFrames = exports.isValidAssetPath = exports.toPaddedDER = exports.checksum = exports.parseLattice1Response = void 0; | ||
// Static utility functions | ||
@@ -267,1 +276,28 @@ var aes_js_1 = __importDefault(require("aes-js")); | ||
exports.existsIn = existsIn; | ||
/** | ||
* `promisifyCb` accepts `resolve` and `reject` from a `Promise` and an optional callback. | ||
* It returns that callback if it exists, otherwise it resolves or rejects as a `Promise` | ||
*/ | ||
var promisifyCb = function (resolve, reject, cb) { | ||
return function (err) { | ||
var params = []; | ||
for (var _i = 1; _i < arguments.length; _i++) { | ||
params[_i - 1] = arguments[_i]; | ||
} | ||
if (cb && typeof cb === 'function') | ||
return cb.apply(void 0, __spreadArray([err], params, false)); | ||
if (err && typeof err === 'string') | ||
return reject(err); | ||
return resolve.apply(void 0, params); | ||
}; | ||
}; | ||
exports.promisifyCb = promisifyCb; | ||
// Create a buffer of size `n` and fill it with random data | ||
var randomBytes = function (n) { | ||
var buf = buffer_1.Buffer.alloc(n); | ||
for (var i = 0; i < n; i++) { | ||
buf[i] = Math.round(Math.random() * 255); | ||
} | ||
return buf; | ||
}; | ||
exports.randomBytes = randomBytes; |
{ | ||
"name": "gridplus-sdk", | ||
"version": "0.10.5", | ||
"version": "1.0.0", | ||
"description": "SDK to interact with GridPlus Lattice1 device", | ||
@@ -59,7 +59,5 @@ "scripts": { | ||
"chai": "^4.2.0", | ||
"cli-interact": "^0.1.9", | ||
"ed25519-hd-key": "^1.2.0", | ||
"eslint": "^8.7.0", | ||
"ethereumjs-util": "^7.1.0", | ||
"it-each": "^0.4.0", | ||
"lodash": ">=4.17.21", | ||
@@ -66,0 +64,0 @@ "minimist": ">=0.2.1", |
513
README.md
@@ -0,96 +1,505 @@ | ||
 | ||
# GridPlus Lattice1 SDK | ||
The GridPlus SDK lets any application establish a connection and interact with a GridPlus Lattice1 device as a remote signer. With the Lattice1 as an extremely secure, connected keystore with signing capabilities, this SDK gives users the following functionality: | ||
* **For full API docs, see [this](https://gridplus.github.io/gridplus-sdk)** | ||
* **For Lattice docs, see [this](https://docs.gridplus.io)** | ||
* **Connect** to a Lattice1 device over the internet | ||
* **Pair** with a Lattice1 by exchanging keys and deriving a secret using an out-of-band secret displayed on the Lattice1. A pairing acts as a mechanism through which to derive shared encryption secrets for future requests. | ||
* Get **addresses** from the paired device (Bitcoin or Ethereum) | ||
* Request **signatures** on ETH or BTC transactions, which the Lattice1 owner must authorize on the device | ||
This SDK is designed to facilitate communication with a user's [Lattice1 hardware wallet](https://gridplus.io/lattice). 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. | ||
## [Documentation](https://gridplus-sdk.readthedocs.io) | ||
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. | ||
The documentation for this SDK can be found [here](https://gridplus-sdk.readthedocs.io). There you will find a complete quickstart guide and API docs for the above functionality. | ||
# ๐ฌ Getting Started | ||
## Testing | ||
First install this SDK with: | ||
If you have a Lattice1 device that is connected to the internet, you can run the full test suite with: | ||
``` | ||
npm install --save gridplus-sdk | ||
``` | ||
```sh | ||
npm test | ||
To connect to a Lattice, you need to create an instance of `Client`: | ||
``` | ||
import { Client } from 'gridplus-sdk' | ||
``` | ||
If you would like to run tests multiple times, you will need to re-pair with a fresh, random key pair using the above command. | ||
If you instead wish to quickly test non-pairing items, consider the following setup: | ||
You can use the following options: | ||
```sh | ||
# Pair with a hardcoded, re-usable test key. You only need to do this ONCE! | ||
env REUSE_KEY=1 npm test | ||
| 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()`. | | ||
# All subsequent tests will use the re-usable key if you specify your device ID | ||
# as an env variable | ||
env DEVICE_ID='my_device_id' npm test | ||
## ๐ 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) | ||
} | ||
``` | ||
> Note: By default, your Lattice will utilize its on-board wallet. If you wish to test against a SafeCard, you will need to insert it and PIN it (i.e. the card needs to be set up). If you reboot your unit, you will need to remove the card and re-insert (and re-authenticate) before testing against it. | ||
#### 2๏ธโฃ Connecting to a known Lattice | ||
### Signing tests | ||
If the Lattice in question already has a pairing with your app (and therefore a recoverable encrypted channel), the connection process is easy. | ||
Once you have paired with a device in a re-usable way (i.e. using the commands above ^), you can run more robust tests around signing. If you are testing with a dev Lattice, it is highly recommended that you compile the autosign flag into your firmware (or else you will need to press accept `n` times). | ||
1. Call `connect` with the `deviceId` of the target Lattice | ||
**ETH** | ||
*Example: connecting to a known Lattice* | ||
``` | ||
const isPaired = await client.connect(deviceID) | ||
Ethereum tests include both boundary checks on transaction params and randomized test vectors (20 by default). | ||
expect(isPaired).to.equal(true) | ||
``` | ||
*`env` options:* | ||
#### 3๏ธโฃ Rehydrating an SDK session | ||
* `N=<int>` (default=`3`) - number of random vectors to test | ||
* `SEED=<string>` (default=`myrandomseed`) - randomness for the pseudorandom number generator that builds deterministic test vectors | ||
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. | ||
Run the suite with: | ||
*Example: drying and rehydrating a client session* | ||
``` | ||
// Fetch some addresses from existing client | ||
const addrs1 = await client.getAddresses(addrReqData) | ||
```sh | ||
env DEVICE_ID='my_device_id' npm run test-eth | ||
// 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) | ||
``` | ||
If you wish to do more or fewer than 20 random transaction tests, you can specify the `N` param: | ||
# ๐ Addresses and Public Keys | ||
Once your `Client` instance is connected, you can request a few different address and key types from the Lattice. | ||
**BTC** | ||
> 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`. | ||
Bitcoin tests cover legacy, wrapped segwit, and segwit spending to all address types. Vectors are built deterministically using the seed and all permutations are tested. | ||
### ฮ Ethereum-type addresses | ||
*`env` options:* | ||
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'`). | ||
* `N=<int>` (default=`3`) - number of inputs per test. Note that if you choose e.g. `N=2` each test will first test one input, then will test two. Must be >0 and <11. | ||
* `SEED=<string>` (default=`myrandomseed`) - randomness for the pseudorandom number generator that builds deterministic test vectors | ||
* `TESTNET=<any>` (default=`false`) - if set to any value you will test all combinations for both mainnet and testnet transactions (doubles number of tests run) | ||
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): | ||
Run the tests with: | ||
> `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'` | ||
```sh | ||
env DEVICE_ID='my_device_id' npm run test-btc | ||
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) | ||
}; | ||
### Ethereum ABI Tests | ||
const addrs = await client.getAddresses(reqData); | ||
``` | ||
You may test functionality around loading Ethereum ABI definitions and displaying calldata in a markdwon screen with the following script: | ||
### โฟ Bitcoin addresses | ||
```sh | ||
env DEVICE_ID='my_device_id' N=<numRandomTests> npm run test-eth-abi | ||
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, | ||
] | ||
}; | ||
> Note that this test uses a random seed to generate data. You may include a `SEED=<mySeed>` if you want to use your own. | ||
// `n` will be set to 1 if not specified -> 1 address returned | ||
const addr0 = await client.getAddresses(reqData); | ||
``` | ||
### Test Harness | ||
### ๐๏ธ Public Keys | ||
We can test debug firmware builds using the `client.test` function in the SDK. This utilizes the firmware's test harness with an encrypted route. You can run these tests with the same `env DEVICE_ID='my_device_id` flag as some of the other tests. | ||
In addition to formatted addresses, the Lattice can return public keys on any supported curve for any BIP32 derivation path. | ||
> NOTE: Since these are encrypted routes, you need to be paired with your Lattice before you can run them (using `env REUSE_KEY=1 npm test` as before -- you still only need to do this once). | ||
> Note: Currently the derivation path must be at least 2 indices deep, but this restriction may be removed in the future. | ||
**Wallet Jobs** | ||
For requesting public keys it is best to import `Constants` with: | ||
Lattice firmware uses "wallet jobs" to interact with the SafeCard/Lattice wallet directly. The SDK does not have access to these methods in production builds, but for debug builds the test harness can be used to interact with them. | ||
``` | ||
import { Client, Constants } from 'gridplus-sdk' | ||
``` | ||
```sh | ||
env DEVICE_ID='my_device_id' npm run test-wallet-jobs | ||
#### 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 = { | ||
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 | ||
``` | ||
| Encoding | Description | | ||
|:---------|:------------| | ||
| `ASCII` | Default 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 | | ||
| `HEX` | Default encoding for non-ASCII messages. Can be specified to force display of a hex string on the Lattice | | ||
| `SOLANA` | Used 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. | ||
| 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)<br/>`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)<br/>`seed` (random string to seed a random number generator)<br/>`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)<br/>`seed` (random string to seed a random number generator)<br/>`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)<br/>`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)<br/>`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)<br/>`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 | |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
237618
24
4946
1
506