Skip to content

Commit

Permalink
Merge pull request #22 from CoachChuckFF/add-init-keypair
Browse files Browse the repository at this point in the history
Added one-line function initializeKeypair for quick startup for dev labs
  • Loading branch information
mikemaccana authored Mar 13, 2024
2 parents 59d7f14 + 5304ce5 commit 1434be0
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 20 deletions.
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ Eventually most of these will end up in `@solana/web3.js`.

[Add a new keypair to an env file](#addkeypairtoenvfile)

[Load and airdrop to a keypair](#initializekeypairconnection-options)

## Installation

```bash
Expand Down Expand Up @@ -228,6 +230,56 @@ await addKeypairToEnvFile(testKeypair, "SECRET_KEY", ".env.local");

This will also reload the env file

### initializeKeypair(connection, options)

Loads in a keypair from the filesystem, or environment and then airdrops to it if needed.

How the keypair is initialized is dependant on the `initializeKeypairOptions`:

```typescript
interface initializeKeypairOptions {
envFileName?: string;
envVariableName?: string;
airdropAmount?: number;
minimumBalance?: number;
keypairPath?: string;
}
```

By default, the keypair will be retrieved from the `.env` file. If a `.env` file does not exist, this function will create one with a new keypair under the optional `envVariableName`.

To load the keypair from the filesystem, pass in the `keypairPath`.

After the keypair has been loaded, it will check the account's balance. If the balance is below the `minimumBalance`, it will airdrop the account `airdropAmount`.

To initialize a keypair from the `.env` file, and airdrop it 1 sol if it's beneath 0.5 sol:
```typescript
const keypair = await initializeKeypair(connection);
```

To initialize a keypair from the `.env` file under a different variable name:
```typescript
const keypair = await initializeKeypair(connection, {
envVariableName: 'TEST_KEYPAIR'
});
```

To initialize a keypair from the filesystem, and airdrop it 3 sol:
```typescript
const keypair = await initializeKeypair(connection, {
keypairPath: '~/.config/solana/id.json',
airdropAmount: LAMPORTS_PER_SOL * 3
});
```

The default options are as follows:

```typescript
const DEFAULT_AIRDROP_AMOUNT = 1 * LAMPORTS_PER_SOL;
const DEFAULT_MINIMUM_BALANCE = 0.5 * LAMPORTS_PER_SOL;
const DEFAULT_ENV_KEYPAIR_VARIABLE_NAME = "PRIVATE_KEY";
```

## Secret key format

Secret keys can be read in either the more compact base58 format (`base58.encode(randomKeypair.secretKey);`), like:
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"contributors": [
"Nick Frostbutter",
"John Liu",
"Steven Luscher"
"Steven Luscher",
"Christian Krueger"
],
"license": "MIT",
"dependencies": {
Expand Down
66 changes: 48 additions & 18 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
getExplorerLink,
confirmTransaction,
makeKeypairs,
initializeKeypairOptions,
initializeKeypair,
getLogs,
} from "./index";
import {
Expand All @@ -28,9 +30,6 @@ import dotenv from "dotenv";
const exec = promisify(execNoPromises);

const LOCALHOST = "http://127.0.0.1:8899";

const log = console.log;

const TEMP_DIR = "src/temp";

describe(`getCustomErrorMessage`, () => {
Expand Down Expand Up @@ -182,6 +181,49 @@ describe("addKeypairToEnvFile", () => {
});
});

describe("initializeKeypair", () => {
const connection = new Connection(LOCALHOST);
const keypairVariableName = "INITIALIZE_KEYPAIR_TEST";

test("generates a new keypair and airdrops needed amount", async () => {
const options: initializeKeypairOptions = {
envVariableName: keypairVariableName,
};

const signerFirstLoad = await initializeKeypair(connection, options);

// Check balance
const firstBalanceLoad = await connection.getBalance(
signerFirstLoad.publicKey,
);
assert.ok(firstBalanceLoad > 0);

// Check that the environment variable was created
dotenv.config();
const secretKeyString = process.env[keypairVariableName];
if (!secretKeyString) {
throw new Error(`${secretKeyString} not found in environment`);
}

// Now reload the environment and check it matches our test keypair
const signerSecondLoad = await initializeKeypair(connection, options);

// Check the keypair is the same
assert.ok(signerFirstLoad.publicKey.equals(signerSecondLoad.publicKey));

// Check balance has not changed
const secondBalanceLoad = await connection.getBalance(
signerSecondLoad.publicKey,
);
assert.equal(firstBalanceLoad, secondBalanceLoad);

// Check there is a secret key
assert.ok(signerSecondLoad.secretKey);

await deleteFile(".env");
});
});

describe("airdropIfRequired", () => {
test("Checking the balance after airdropIfRequired", async () => {
const keypair = Keypair.generate();
Expand Down Expand Up @@ -318,15 +360,10 @@ describe("makeKeypairs", () => {
assert.equal(keypairs.length, KEYPAIRS_TO_MAKE);
assert.ok(keypairs[KEYPAIRS_TO_MAKE - 1].secretKey);
});
});

describe("makeKeypairs", () => {
test("makeKeypairs makes exactly the amount of keypairs requested", () => {
// We could test more, but keypair generation takes time and slows down tests
const KEYPAIRS_TO_MAKE = 3;
const keypairs = makeKeypairs(KEYPAIRS_TO_MAKE);
assert.equal(keypairs.length, KEYPAIRS_TO_MAKE);
assert.ok(keypairs[KEYPAIRS_TO_MAKE - 1].secretKey);
test("makeKeypairs() creates the correct number of keypairs", () => {
const keypairs = makeKeypairs(3);
assert.equal(keypairs.length, 3);
});
});

Expand Down Expand Up @@ -357,13 +394,6 @@ describe("confirmTransaction", () => {
});
});

describe("makeKeypairs", () => {
test("makeKeypairs() creates the correct number of keypairs", () => {
const keypairs = makeKeypairs(3);
assert.equal(keypairs.length, 3);
});
});

describe(`getLogs`, () => {
test(`getLogs works`, async () => {
const connection = new Connection(LOCALHOST);
Expand Down
55 changes: 54 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { Cluster, Connection, Keypair, PublicKey } from "@solana/web3.js";
import {
Cluster,
Connection,
Keypair,
PublicKey,
LAMPORTS_PER_SOL,
} from "@solana/web3.js";
import base58 from "bs58";
import path from "path";
import { readFile, appendFile } from "fs/promises";
Expand All @@ -7,6 +13,10 @@ const log = console.log;

// Default value from Solana CLI
const DEFAULT_FILEPATH = "~/.config/solana/id.json";
const DEFAULT_AIRDROP_AMOUNT = 1 * LAMPORTS_PER_SOL;
const DEFAULT_MINIMUM_BALANCE = 0.5 * LAMPORTS_PER_SOL;
const DEFAULT_ENV_KEYPAIR_VARIABLE_NAME = "PRIVATE_KEY";


export const keypairToSecretKeyJSON = (keypair: Keypair): string => {
return JSON.stringify(Array.from(keypair.secretKey));
Expand Down Expand Up @@ -149,6 +159,49 @@ export const addKeypairToEnvFile = async (
await appendFile(fileName, `\n${variableName}=${secretKeyString}`);
};

export interface initializeKeypairOptions {
envFileName?: string;
envVariableName?: string;
airdropAmount?: number;
minimumBalance?: number;
keypairPath?: string;
}

export const initializeKeypair = async (
connection: Connection,
options?: initializeKeypairOptions,
): Promise<Keypair> => {

let {
envFileName,
envVariableName,
airdropAmount,
minimumBalance,
keypairPath,
} = options || {};

let keypair: Keypair;
envVariableName = envVariableName || DEFAULT_ENV_KEYPAIR_VARIABLE_NAME;

if (keypairPath) {
keypair = await getKeypairFromFile(keypairPath);
} else if (process.env[envVariableName]) {
keypair = getKeypairFromEnvironment(envVariableName);
} else {
keypair = Keypair.generate();
await addKeypairToEnvFile(keypair, envVariableName, envFileName);
}

await airdropIfRequired(
connection,
keypair.publicKey,
airdropAmount || DEFAULT_AIRDROP_AMOUNT,
minimumBalance || DEFAULT_MINIMUM_BALANCE,
);

return keypair;
};

// Not exported as we don't want to encourage people to
// request airdrops when they don't need them, ie - don't bother
// the faucet unless you really need to!
Expand Down

0 comments on commit 1434be0

Please sign in to comment.