Skip to content

Commit

Permalink
refactor(experimental): a function to convert a public key into a bas…
Browse files Browse the repository at this point in the history
…e 58 encoded address

## Summary

This is useful in cases where you, yourself, generated a keypair, and now you need to display the public key in your UI. Otherwise, you will typically already have the `Base58EncodedAddress` object.

## Test Plan

```
cd packages/keys/
pnpm test:unit:browser
pnpm test:unit:node
```
  • Loading branch information
steveluscher committed Jul 13, 2023
1 parent 6e4aa80 commit 9134ea7
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 2 deletions.
20 changes: 18 additions & 2 deletions packages/keys/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Client applications primarily deal with addresses and public keys in the form of
From time to time you might acquire a string, that you expect to validate as an address, from an untrusted network API or user input. To assert that such an arbitrary string is a base58-encoded address, use the `assertIsBase58EncodedAddress` function.

```ts
import { assertIsBase58EncodedAddress } from '@solana/web3.js`;
import { assertIsBase58EncodedAddress } from '@solana/keys`;

// Imagine a function that fetches an account's balance when a user submits a form.
function handleSubmit() {
Expand All @@ -51,6 +51,22 @@ function handleSubmit() {
}
```

### `generateKeyPair()`
### `generateKeypair()`

Generates an Ed25519 public/private keypair for use with other methods in this package that accept `CryptoKey` objects.

```ts
import { generateKeypair } from '@solana/keys';

const { privateKey, publicKey } = await generateKeypair();
```

### `getBase58EncodedAddressFromPublicKey()`

Given a public `CryptoKey`, this method will return its associated `Base58EncodedAddress`.

```ts
import { getBase58EncodedAddressFromPublicKey } from '@solana/keys';

const address = await getBase58EncodedAddressFromPublicKey(publicKey);
```
90 changes: 90 additions & 0 deletions packages/keys/src/__tests__/pubkey-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { getBase58EncodedAddressFromPublicKey } from '../pubkey';

// Corresponds to address `DcESq8KFcdTdpjWtr2DoGcvu5McM3VJoBetgM1X1vVct`
const MOCK_PUBLIC_KEY_BYTES = new Uint8Array([
0xbb, 0x52, 0xc6, 0x2d, 0x52, 0x4f, 0x7f, 0xea, 0x4f, 0x2c, 0x27, 0x13, 0xd6, 0x20, 0x80, 0xad, 0x6a, 0x36, 0x9a,
0x0e, 0x36, 0x71, 0x74, 0x32, 0x8d, 0x1a, 0xf7, 0xee, 0x7e, 0x04, 0x76, 0x19,
]);

describe('getBase58EncodedAddressFromPublicKey', () => {
let oldIsSecureContext: boolean;
beforeEach(() => {
oldIsSecureContext = globalThis.isSecureContext;
globalThis.isSecureContext = true;
});
afterEach(() => {
globalThis.isSecureContext = oldIsSecureContext;
});
it('returns the public key that corresponds to a given secret key', async () => {
expect.assertions(1);
const publicKey = await crypto.subtle.importKey(
'raw',
MOCK_PUBLIC_KEY_BYTES,
'Ed25519',
/* extractable */ true,
['verify']
);
await expect(getBase58EncodedAddressFromPublicKey(publicKey)).resolves.toBe(
'DcESq8KFcdTdpjWtr2DoGcvu5McM3VJoBetgM1X1vVct'
);
});
it('throws when the public key is non-extractable', async () => {
expect.assertions(1);
const publicKey = await crypto.subtle.importKey(
'raw',
MOCK_PUBLIC_KEY_BYTES,
'Ed25519',
/* extractable */ false,
['verify']
);
await expect(() => getBase58EncodedAddressFromPublicKey(publicKey)).rejects.toThrow();
});
it('throws when called with a secret', async () => {
expect.assertions(1);
const publicKey = await crypto.subtle.generateKey(
{
length: 256,
name: 'AES-GCM',
},
true,
['encrypt', 'decrypt']
);
await expect(() => getBase58EncodedAddressFromPublicKey(publicKey)).rejects.toThrow();
});
it.each([
{ __variant: 'P256', name: 'ECDSA', namedCurve: 'P-256' },
{ __variant: 'P384', name: 'ECDSA', namedCurve: 'P-384' } as EcKeyGenParams,
{ __variant: 'P521', name: 'ECDSA', namedCurve: 'P-521' } as EcKeyGenParams,
...['RSASSA-PKCS1-v1_5', 'RSA-PSS'].flatMap(rsaAlgoName =>
['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512'].map(
hashName =>
({
__variant: hashName,
hash: { name: hashName },
modulusLength: 2048,
name: rsaAlgoName,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
} as RsaHashedKeyGenParams)
)
),
])('throws when called with a $name/$__variant public key', async algorithm => {
expect.assertions(1);
const { publicKey } = await crypto.subtle.generateKey(algorithm, true, ['sign', 'verify']);
await expect(() => getBase58EncodedAddressFromPublicKey(publicKey)).rejects.toThrow();
});
it('throws when called with a private key', async () => {
expect.assertions(1);
const mockPrivateKey = await crypto.subtle.importKey(
'pkcs8',
new Uint8Array([
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20, 0xf2,
0x29, 0xe0, 0x33, 0x09, 0x44, 0x10, 0xd9, 0x64, 0x80, 0x42, 0x85, 0x9a, 0x18, 0x5c, 0x4a, 0x45, 0x45,
0xd9, 0xd1, 0x75, 0xeb, 0x30, 0x89, 0xb4, 0x2b, 0x7b, 0xe3, 0xca, 0xbf, 0x63, 0xc9,
]),
'Ed25519',
/* extractable */ false,
['sign']
);
await expect(() => getBase58EncodedAddressFromPublicKey(mockPrivateKey)).rejects.toThrow();
});
});
1 change: 1 addition & 0 deletions packages/keys/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './base58';
export * from './keypair';
export * from './pubkey';
13 changes: 13 additions & 0 deletions packages/keys/src/pubkey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Base58EncodedAddress, getBase58EncodedAddressCodec } from './base58';
import { assertKeyExporterIsAvailable } from './guard';

export async function getBase58EncodedAddressFromPublicKey(publicKey: CryptoKey): Promise<Base58EncodedAddress> {
await assertKeyExporterIsAvailable();
if (publicKey.type !== 'public' || publicKey.algorithm.name !== 'Ed25519') {
// TODO: Coded error.
throw new Error('The `CryptoKey` must be an `Ed25519` public key');
}
const publicKeyBytes = await crypto.subtle.exportKey('raw', publicKey);
const [base58EncodedAddress] = getBase58EncodedAddressCodec().deserialize(new Uint8Array(publicKeyBytes));
return base58EncodedAddress;
}

0 comments on commit 9134ea7

Please sign in to comment.