Skip to content

Commit

Permalink
updates docs
Browse files Browse the repository at this point in the history
  • Loading branch information
netbonus committed Jun 4, 2024
1 parent a8453fd commit cb9d250
Show file tree
Hide file tree
Showing 18 changed files with 11,894 additions and 21,560 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/docs-build-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v1
with:
node-version: 16.x
node-version: 18.x

- name: Install NPM packages for SDK
run: npm ci
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
docs/_build
docs/docs/reference
.DS_Store
dist
node_modules
Expand Down
68 changes: 28 additions & 40 deletions docs/docs/addresses.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
id: "addresses"
id: 'addresses'
sidebar_position: 2
---

# 🔑 Addresses and Public Keys

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

:::note

Expand All @@ -17,7 +17,7 @@ This section uses the following notation when discussing BIP32 derivation paths:

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):
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'`
Expand All @@ -27,44 +27,40 @@ Keep in mind that changing the `coin_type` will change all the requested address

```ts
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)
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);
const addrs = await fetchAddresses(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
- 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 addresses

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

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

## 🗝️ Public Keys
Expand All @@ -78,7 +74,7 @@ Currently the derivation path must be at least 2 indices deep, but this restrict
For requesting public keys it is best to import `Constants` with:

```ts
import { Client, Constants } from 'gridplus-sdk'
import { Constants } from 'gridplus-sdk';
```

### 1️⃣ `secp256k1` curve
Expand All @@ -93,18 +89,13 @@ The public key has two 32 byte components and is of format: `04{X}{Y}`, meaning

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

const pubkeys = await client.getAddresses(req);
const pubkeys = await fetchAddresses(req);
```

:::note
Expand All @@ -113,7 +104,7 @@ Since `startPath` is the same, this example returns public keys which can be con

### 2️⃣ `ed25519` curve

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

**Pubkey size: 32 bytes**

Expand All @@ -123,14 +114,11 @@ Some libraries prefix these keys with a `00` byte (making them 33 bytes), but we

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

const pubkeys = await client.getAddresses(req);
const pubkeys = await fetchAddresses(req);
```
175 changes: 124 additions & 51 deletions docs/docs/intro.md
Original file line number Diff line number Diff line change
@@ -1,94 +1,167 @@
---
id: "index"
title: "👋 Getting Started"
slug: "/"
id: 'index'
title: '👋 Getting Started'
slug: '/'
sidebar_position: 0
custom_edit_url: null
---

# GridPlus SDK

The [GridPlus SDK](https://github.com/GridPlus/gridplus-sdk) is designed to facilitate communication between an app or service and a user's [Lattice1 hardware wallet](https://gridplus.io/lattice).
The [GridPlus SDK](https://github.com/GridPlus/gridplus-sdk) is the bridge between software wallets, like MetaMask or Frame, and the [Lattice1 hardware wallet](https://gridplus.io/lattice).

:::note
The [Lattice1](https://gridplus.io/lattice) is an Internet-connected device which listens for end-to-end encrypted requests. HTTPS requests originate from this SDK and responses are returned **asynchronously**. Some requests require user authorization and will time out if the user does not approve them.

If you are using the `gridplus-sdk` in a Node.js application with a version of Node lower than v18, you will need to patch the `fetch()` API in the global scope. One solution is to use the `node-fetch` package. See [the `node-fetch` README](https://github.com/node-fetch/node-fetch#installation) for instructions. Other options are available on NPM.

:::

## Installing
## Getting Started

First install this SDK with:
### Installation

```bash
Install the package with your package manager. For example, with npm:

```sh
npm install --save gridplus-sdk
```

## Connecting to a Lattice
### Connecting to a Lattice

To initiate the connection, you'll call `setup`. This function takes an object with the following properties:

- `name` - the name of the wallet or app you're connecting from, e.g. `MetaMask`
- `deviceId` - the device ID of the Lattice you're connecting to
- `password` - an arbitrary string that is used to encrypt/decrypt data for transport
- `getStoredClient` - a function that returns the stored client data
- `setStoredClient` - a function that stores the client data

#### Setup Example

`setup()` will return a `boolean` that tells you whether the device has already been paired. If it has not, you will need to pair the device by calling `pair()`.

```ts
import { setup } from 'gridplus-sdk';

const isPaired = await setup({
name: 'My Wallet',
deviceId: 'XXXXXX',
password: 'password',
getStoredClient: () => localStorage.getItem('client'),
setStoredClient: (client) => localStorage.setItem('client', client),
});
```

### Pairing

To pair the device with your application, you'll call `pair` with the pairing code displayed on the Lattice screen. This code is a 6-digit number that is displayed on the Lattice screen when you attempt to connect to it.

#### Pairing Example

You first need to instantiate a new [`Client`](./api/classes/client.Client) object with, at a minimum, the name of your requesting app (see [`Client` doc](./api/classes/client.Client) for a full list of options). The `name` used to instantiate `Client` will show up on the desired Lattice when pairing, so you should name it something relevant to your app/service.
`pair()` also returns a `boolean` that tells you whether the pairing was successful.

```ts
import { Client } from 'gridplus-sdk';
import { pair } from 'gridplus-sdk';

const client = new Client({ name: 'SDK Connectooor' });
const isPaired = await pair('123456');
```

You can now use your `client` object to connect to a specific Lattice1 device, which should have a unique `deviceID`, discoverable through the client's `baseUrl`.
### Fetching Addresses

:::info
Lattices are discoverable over a combination of `deviceID` and `baseUrl`. By default, `baseUrl` (an attribute of `Client` and a config option when creating an instance) points to the GridPlus routing cloud service, but you can also create your own endpoint using [Lattice Connect](https://github.com/GridPlus/lattice-connect-v2).
Once you're connected to the Lattice, you can fetch addresses from it. This is done by calling `fetchAddresses()`.

When a Lattice connects to a routing service (located at some `baseUrl`) for the first time, that server should generate a `deviceID` for the connecting Lattice. At this point, the Lattice will save the newly issued `deviceID` and will listen for corresponding messages coming from `baseUrl` (these messages are always **end-to-end encrypted**). The Lattice should be permanently discoverable at this `baseUrl`/`deviceID` combination unless/until its user resets the Lattice Router or switches the device to a new routing service.
:::
#### Fetch Addresses Example

### Pairing vs Connecting
`fetchAddresses()` returns an array of addresses.

The connection process depends on whether your app/service is already **paired** to the target Lattice. **Pairing** is a process involving key exchange and [ECDH](https://en.wikipedia.org/wiki/Elliptic-curve_Diffie%E2%80%93Hellman) shared key generation between the target Lattice and your `Client` instance, such that future messages can be end-to-end encrypted.
```ts
import { fetchAddresses } from 'gridplus-sdk';

:::caution
`Client` has a `privKey` attribute (a 32-byte buffer or hex string), which is used to encrypt/decrypt messages. By default, `privKey` is generated randomly, but it is **highly recommended** you generate your own private key deterministically or [stash and rehydrate](#stashing-and-rehydrating-an-sdk-instance) the instance if you wish to re-use the app/service with the target Lattice(s). If you naively create a new `Client` instance with a random `privKey`, it will force a re-pairing with the target Lattice(s).
:::
const addresses = await fetchAddresses();
```

If you are **not** paired to the target Lattice already, the connection request will cause the Lattice to generate a new **pairing code** and display that on the device's screen. That code must be entered into the `Client` instance within 60 seconds, i.e. before it expires. This process only happens **once per pairing**, so subsequent `connect` requests should reach the target Lattice without having to re-pair. However, any Lattice user may remove any pairing from their device at any time. If this happens, you will need to re-pair with the device in order to make any new requests.
By default, this function returns the first 10 addresses at the standard EVM derivation path. You can specify the number of addresses you want to fetch by passing a number as an argument to `fetchAddresses()`.

```ts
import { Client, Constants, Utils } from 'gridplus-sdk';
import { question } from 'readline-sync';
const deviceID = 'XXXXXX';
const numValidators = 5;

// Instantiate the `Client` object with a name. Here we will use the
// default `baseUrl`, i.e. GridPlus routing service.
const client = new Client({ name: 'SDK Connectooor' });

// Call `connect` to determine if we are already paired
const isPaired = await client.connect(deviceID);

if (!isPaired) {
// If not paired, the secret needs to get sent to `pair`
const secret = await question('Enter pairing secret: ');
await client.pair(secret);
}
const addresses = await fetchAddresses(5);
```

If you're working with another blockchain, you can specify the derivation path by passing an object as an argument to `fetchAddresses()` that has the key of `startPath` which is an array that represents the derivation path.

### Stashing and Rehydrating an SDK Instance
:::note
The derivation path is an array of integers that represents the path to the address you want to fetch. The first element of the array is the purpose, the second is the coin type, the third is the account index, the fourth is the change index, and the fifth is the address index.

As mentioned above, naively generating new `Client` instances without deterministically generating a `privKey` will require a pairing with target Lattice(s). If you don't want to deterministically generate and set the `privKey` attribute, you can also let `Client` generate a random one and then stash your `Client` instance:
Also, some values will need to be "hardened" by adding 0x80000000 to them. For example, the purpose for Ethereum is `44`, so the hardened value would be `44 + 0x80000000 = 2147483692`. This library exports a constant of `HARDENED_OFFSET` which is `0x80000000` for your convenience.
:::

```ts
const clientStash = client.getStateData();
const client2 = new Client({ stateData: clientStash, });
// default EVM
const addresses = await fetchAddresses({
startPath: [
HARDENED_OFFSET + 44,
HARDENED_OFFSET + 60,
HARDENED_OFFSET,
0,
0,
],
});

// Legacy BTC
const addresses = await fetchAddresses({
startPath: [HARDENED_OFFSET + 44, HARDENED_OFFSET + 0, HARDENED_OFFSET, 0, 0],
});

// Segwit BTC
const addresses = await fetchAddresses({
startPath: [HARDENED_OFFSET + 84, HARDENED_OFFSET, HARDENED_OFFSET, 0, 0],
});
```

## Now What?
The library also exports convenience functions for fetching addresses for specific derivation paths:

Once your `client` is paired or otherwise connected to the target Lattice, you can make full use of the SDK.
```ts
import {
fetchBtcLegacyAddresses,
fetchBtcSegwitAddresses,
fetchBtcWrappedSegwitAddresses,
fetchSolanaAddresses,
} from 'gridplus-sdk';

const btcLegacyAddresses = await fetchBtcLegacyAddresses();
const btcSegwitAddresses = await fetchBtcSegwitAddresses();
const btcWrappedSegwitAddresses = await fetchBtcWrappedSegwitAddresses();
const solanaAddresses = await fetchSolanaAddresses();
```

### Signing Transactions

To sign a transaction, you'll call `sign` with the transaction data with the chain information and signing scheme. This function returns the signed transaction data.

Some actions, such as requesting signatures, require **user authorization** on the device or they will time out. Other actions, such as fetching public keys, can be made as long as there is a pairing with the target Lattice.
#### Signing Example

The rest of these docs will cover basic functionality (e.g. [getting addresses](./addresses) and [making signatures](./signing)) as well as tutorials on more advanced topics, which would typically be built into a UI such as the [Lattice Manager](https://lattice.gridplus.io) or an integrated app such as [MetaMask](https://metamask.io).
For an Ethereum transaction, sing the `ethers.js` library, version 5, to build the transaction data and sign with the `gridplus-sdk` would look like this:

```ts
import { sign } from 'gridplus-sdk';

const txData = {
type: 1,
maxFeePerGas: 1200000000,
maxPriorityFeePerGas: 1200000000,
nonce: 0,
gasLimit: 50000,
to: '0xe242e54155b1abc71fc118065270cecaaf8b7768',
value: 1000000000000,
data: '0x17e914679b7e160613be4f8c2d3203d236286d74eb9192f6d6f71b9118a42bb033ccd8e8',
gasPrice: 1200000000,
};

const common = new Common({
chain: Chain.Mainnet,
hardfork: Hardfork.London,
});
const tx = TransactionFactory.fromTxData(txData, { common });
const payload = tx.getMessageToSign(false);

const signedTx = await sign(reqData);
```

You can always consult the [API Docs](./api/classes/client.Client) for more specific information on options related to various SDK functions.
For more complex signing examples or signing for other chains, please refer to the [Signing](/signing.md) page.
Loading

0 comments on commit cb9d250

Please sign in to comment.