Skip to content

Commit

Permalink
added multiple keypairs/signers functions (#44)
Browse files Browse the repository at this point in the history
* chore: add assertions

* feat: generate extractable keypairs

* fix: type import

* feat: saving keypairs to files

* feat: added load leys from env

* feat: saving keys to env files

* docs: document in readme

* chore: changeset

* docs: readme and comment
  • Loading branch information
nickfrosty authored Feb 21, 2025
1 parent b9491e4 commit e18fc1b
Show file tree
Hide file tree
Showing 11 changed files with 427 additions and 14 deletions.
5 changes: 5 additions & 0 deletions .changeset/witty-tools-applaud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"gill": minor
---

added functions for generating extractable keypairs, saving keypairs to files, and loading/saving keypairs to env variables
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,7 @@ test-ledger/
.ghpages-deploy

# Codegenerated TypeDoc API
docs/
docs/

.env
.env.local
84 changes: 84 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,15 @@ yarn add gill
- [Get a transaction signature](#get-the-signature-from-a-signed-transaction)
- [Get a Solana Explorer link](#get-a-solana-explorer-link-for-transactions-accounts-or-blocks)
- [Calculate minimum rent balance for an account](#calculate-minimum-rent-for-an-account)
- [Generating keypairs and signers](#generating-keypairs-and-signers)
- [Generating extractable keypairs and signers](#generating-extractable-keypairs-and-signers)

You can also find some [NodeJS specific helpers](#node-specific-imports) like:

- [Loading a keypair from a file](#loading-a-keypair-from-a-file)
- [Saving a keypair to a file](#saving-a-keypair-to-a-file)
- [Loading a keypair from an environment variable](#loading-a-keypair-from-an-environment-variable)
- [Saving a keypair to an environment variable file](#saving-a-keypair-to-an-environment-file)

You can find [transaction builders](#transaction-builders) for common tasks, including:

Expand All @@ -70,6 +75,42 @@ For troubleshooting and debugging your Solana transactions, see [Debug mode](#de
> [JavaScript client](https://github.com/anza-xyz/solana-web3.js) library for more information and
> helpful resources.
### Generating keypairs and signers

For most "signing" operations, you will need a `KeyPairSigner` instance, which can be used to sign
transactions and messages.

To generate a random `KeyPairSigner`:

```typescript
import { generateKeyPairSigner } from "gill";

const signer: KeyPairSigner = generateKeyPairSigner();
```

> Note: These Signers are non-extractable, meaning there is no way to get the secret key material
> out of the instance. This is a more secure practice and highly recommended to be used over
> extractable keypairs, unless you REALLY need to be able to save the keypair for some reason.
### Generating extractable keypairs and signers

Extractable keypairs are less secure and should not be used unless you REALLY need to save the key
for some reason. Since there are a few useful cases for saving these keypairs, gill contains a
separate explicit function to generate these extractable keypairs.

To generate a random, **extractable** `KeyPairSigner`:

```typescript
import { generateExtractableKeyPairSigner } from "gill";

const signer: KeyPairSigner = generateExtractableKeyPairSigner();
```

> WARNING: Using **extractable** keypairs are inherently less-secure, since they allow the secret
> key material to be extracted. Obviously. As such, they should only be used sparingly and ONLY when
> you have an explicit reason you need extract the key material (like if you are going to save the
> key to a file).
### Create a Solana RPC connection

Create a Solana `rpc` and `rpcSubscriptions` client for any RPC URL or standard Solana network
Expand Down Expand Up @@ -428,6 +469,49 @@ const signer = await loadKeypairSignerFromFile("/path/to/your/keypair.json");
console.log("address:", signer.address);
```

### Saving a keypair to a file

> See [`saveKeypairSignerToEnvFile`](#saving-a-keypair-to-an-environment-file) for saving to an env
> file.
Save an **extractable** `KeyPairSigner` to a local json file (e.g. `keypair.json`).

```typescript
import { ... } from "gill/node";
const extractableSigner = generateExtractableKeyPairSigner();
await saveKeypairSignerToFile(extractableSigner, filePath);
```

See [`loadKeypairSignerFromFile`](#loading-a-keypair-from-a-file) for how to load keypairs from the
local filesystem.

### Loading a keypair from an environment variable

Load a `KeyPairSigner` from the bytes stored in the environment process (e.g.
`process.env[variableName]`)

```typescript
import { loadKeypairSignerFromEnvironment } from "gill/node";

// loads signer from bytes stored at `process.env[variableName]`
const signer = await loadKeypairSignerFromEnvironment(variableName);
console.log("address:", signer.address);
```

### Saving a keypair to an environment file

Save an **extractable** `KeyPairSigner` to a local environment variable file (e.g. `.env`).

```typescript
import { ... } from "gill/node";
const extractableSigner = generateExtractableKeyPairSigner();
// default: envPath = `.env` (in your current working directory)
await saveKeypairSignerToEnvFile(extractableSigner, variableName, envPath);
```

See [`loadKeypairSignerFromEnvironment`](#loading-a-keypair-from-an-environment-variable) for how to
load keypairs from environment variables.

## Transaction builders

To simplify the creation of common transactions, gill includes various "transaction builders" to
Expand Down
84 changes: 84 additions & 0 deletions packages/gill/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,15 @@ yarn add gill
- [Get a transaction signature](#get-the-signature-from-a-signed-transaction)
- [Get a Solana Explorer link](#get-a-solana-explorer-link-for-transactions-accounts-or-blocks)
- [Calculate minimum rent balance for an account](#calculate-minimum-rent-for-an-account)
- [Generating keypairs and signers](#generating-keypairs-and-signers)
- [Generating extractable keypairs and signers](#generating-extractable-keypairs-and-signers)

You can also find some [NodeJS specific helpers](#node-specific-imports) like:

- [Loading a keypair from a file](#loading-a-keypair-from-a-file)
- [Saving a keypair to a file](#saving-a-keypair-to-a-file)
- [Loading a keypair from an environment variable](#loading-a-keypair-from-an-environment-variable)
- [Saving a keypair to an environment variable file](#saving-a-keypair-to-an-environment-file)

You can find [transaction builders](#transaction-builders) for common tasks, including:

Expand All @@ -70,6 +75,42 @@ For troubleshooting and debugging your Solana transactions, see [Debug mode](#de
> [JavaScript client](https://github.com/anza-xyz/solana-web3.js) library for more information and
> helpful resources.
### Generating keypairs and signers

For most "signing" operations, you will need a `KeyPairSigner` instance, which can be used to sign
transactions and messages.

To generate a random `KeyPairSigner`:

```typescript
import { generateKeyPairSigner } from "gill";

const signer: KeyPairSigner = generateKeyPairSigner();
```

> Note: These Signers are non-extractable, meaning there is no way to get the secret key material
> out of the instance. This is a more secure practice and highly recommended to be used over
> extractable keypairs, unless you REALLY need to be able to save the keypair for some reason.
### Generating extractable keypairs and signers

Extractable keypairs are less secure and should not be used unless you REALLY need to save the key
for some reason. Since there are a few useful cases for saving these keypairs, gill contains a
separate explicit function to generate these extractable keypairs.

To generate a random, **extractable** `KeyPairSigner`:

```typescript
import { generateExtractableKeyPairSigner } from "gill";

const signer: KeyPairSigner = generateExtractableKeyPairSigner();
```

> WARNING: Using **extractable** keypairs are inherently less-secure, since they allow the secret
> key material to be extracted. Obviously. As such, they should only be used sparingly and ONLY when
> you have an explicit reason you need extract the key material (like if you are going to save the
> key to a file).
### Create a Solana RPC connection

Create a Solana `rpc` and `rpcSubscriptions` client for any RPC URL or standard Solana network
Expand Down Expand Up @@ -428,6 +469,49 @@ const signer = await loadKeypairSignerFromFile("/path/to/your/keypair.json");
console.log("address:", signer.address);
```

### Saving a keypair to a file

> See [`saveKeypairSignerToEnvFile`](#saving-a-keypair-to-an-environment-file) for saving to an env
> file.
Save an **extractable** `KeyPairSigner` to a local json file (e.g. `keypair.json`).

```typescript
import { ... } from "gill/node";
const extractableSigner = generateExtractableKeyPairSigner();
await saveKeypairSignerToFile(extractableSigner, filePath);
```

See [`loadKeypairSignerFromFile`](#loading-a-keypair-from-a-file) for how to load keypairs from the
local filesystem.

### Loading a keypair from an environment variable

Load a `KeyPairSigner` from the bytes stored in the environment process (e.g.
`process.env[variableName]`)

```typescript
import { loadKeypairSignerFromEnvironment } from "gill/node";

// loads signer from bytes stored at `process.env[variableName]`
const signer = await loadKeypairSignerFromEnvironment(variableName);
console.log("address:", signer.address);
```

### Saving a keypair to an environment file

Save an **extractable** `KeyPairSigner` to a local environment variable file (e.g. `.env`).

```typescript
import { ... } from "gill/node";
const extractableSigner = generateExtractableKeyPairSigner();
// default: envPath = `.env` (in your current working directory)
await saveKeypairSignerToEnvFile(extractableSigner, variableName, envPath);
```

See [`loadKeypairSignerFromEnvironment`](#loading-a-keypair-from-an-environment-variable) for how to
load keypairs from environment variables.

## Transaction builders

To simplify the creation of common transactions, gill includes various "transaction builders" to
Expand Down
1 change: 1 addition & 0 deletions packages/gill/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
"@solana-program/token-2022": "^0.3.4",
"@solana/accounts": "^2.0.0",
"@solana/addresses": "^2.0.0",
"@solana/assertions": "^2.0.0",
"@solana/codecs": "^2.0.0",
"@solana/errors": "^2.0.0",
"@solana/functional": "^2.0.0",
Expand Down
1 change: 1 addition & 0 deletions packages/gill/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from "./base64-transactions";
export * from "./prepare-transaction";
export * from "./create-solana-client";
export * from "./accounts";
export * from "./keypairs";
95 changes: 95 additions & 0 deletions packages/gill/src/core/keypairs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { assertKeyExporterIsAvailable, assertKeyGenerationIsAvailable } from "@solana/assertions";
import { createKeyPairFromBytes } from "@solana/keys";
import {
type KeyPairSigner,
createSignerFromKeyPair,
type createKeyPairSignerFromBytes,
} from "@solana/signers";

export function assertKeyPairIsExtractable(
keyPair: CryptoKeyPair,
): asserts keyPair is ExtractableCryptoKeyPair {
assertKeyExporterIsAvailable();

if (!keyPair.privateKey) {
throw new Error("Keypair is missing private key");
}

if (!keyPair.publicKey) {
throw new Error("Keypair is missing public key");
}

if (!keyPair.privateKey.extractable) {
throw new Error("Private key is not extractable");
}
}

type Extractable = { "~extractable": true };

type ExtractableCryptoKeyPair = CryptoKeyPair & Extractable;
type ExtractableKeyPairSigner = KeyPairSigner & Extractable;

/**
* Generates an extractable Ed25519 `CryptoKeyPair` capable of signing messages and transactions
* */
export async function generateExtractableKeyPair(): Promise<ExtractableCryptoKeyPair> {
await assertKeyGenerationIsAvailable();
return crypto.subtle.generateKey(
/* algorithm */ "Ed25519", // Native implementation status: https://github.com/WICG/webcrypto-secure-curves/issues/20
/* extractable */ true,
/* allowed uses */ ["sign", "verify"],
) as Promise<ExtractableCryptoKeyPair>;
}

/**
* Generates an extractable signer capable of signing messages and transactions using a Crypto KeyPair.
* */
export async function generateExtractableKeyPairSigner(): Promise<ExtractableKeyPairSigner> {
return createSignerFromKeyPair(
await generateExtractableKeyPair(),
) as Promise<ExtractableKeyPairSigner>;
}

/**
* Extracts the raw key material from an extractable Ed25519 CryptoKeyPair.
*
* @remarks
* - Requires a keypair generated with extractable=true. See {@link generateExtractableKeyPair}.
* - The extracted bytes can be used to reconstruct the `CryptoKeyPair` with {@link createKeyPairFromBytes}.
*
* @param keypair An extractable Ed25519 `CryptoKeyPair`
* @returns Raw key bytes as `Uint8Array`
*/
export async function extractBytesFromKeyPair(
keypair: ExtractableCryptoKeyPair | CryptoKeyPair,
): Promise<Uint8Array> {
assertKeyPairIsExtractable(keypair);

const [publicKeyBytes, privateKeyJwk] = await Promise.all([
crypto.subtle.exportKey("raw", keypair.publicKey),
crypto.subtle.exportKey("jwk", keypair.privateKey),
]);

if (!privateKeyJwk.d) throw new Error("Failed to get private key bytes");

return new Uint8Array([
...Buffer.from(privateKeyJwk.d, "base64"),
...new Uint8Array(publicKeyBytes),
]);
}

/**
* Extracts the raw key material from an extractable Ed25519 KeyPairSigner.
*
* @remarks
* - Requires a keypair generated with extractable=true. See {@link generateExtractableKeyPairSigner}.
* - The extracted bytes can be used to reconstruct the `CryptoKeyPair` with {@link createKeyPairSignerFromBytes}.
*
* @param keypairSigner An extractable Ed25519 `KeyPairSigner`
* @returns Raw key bytes as `Uint8Array`
*/
export async function extractBytesFromKeyPairSigner(
keypairSigner: ExtractableKeyPairSigner | KeyPairSigner,
): Promise<Uint8Array> {
return extractBytesFromKeyPair(keypairSigner.keyPair);
}
1 change: 1 addition & 0 deletions packages/gill/src/node/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./const";
export * from "./load-keypair";
export * from "./save-keypair";
Loading

0 comments on commit e18fc1b

Please sign in to comment.