Skip to content

Commit

Permalink
wallet-ext: send keypair to UI instead of entropy (MystenLabs#6598)
Browse files Browse the repository at this point in the history
* ts-sdk: allow keypairs to be exported

* wallet-ext: send keypair instead of entropy to UI

* this will allow us to use different keypairs without making the UI aware of how the keypair was created
* this will help us to support multiple accounts
  • Loading branch information
pchrysochoidis authored Dec 6, 2022
1 parent 7671f88 commit 519e115
Show file tree
Hide file tree
Showing 15 changed files with 95 additions and 66 deletions.
5 changes: 5 additions & 0 deletions .changeset/friendly-impalas-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@mysten/sui.js": minor
---

Allow keypairs to be exported
5 changes: 1 addition & 4 deletions apps/wallet/src/background/connections/UiConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import Permissions from '_src/background/Permissions';
import Tabs from '_src/background/Tabs';
import Transactions from '_src/background/Transactions';
import Keyring from '_src/background/keyring';
import { entropyToSerialized } from '_src/shared/utils/bip39';

import type { Message } from '_messages';
import type { PortChannelName } from '_messaging/PortChannelName';
Expand Down Expand Up @@ -60,9 +59,7 @@ export class UiConnection extends Connection {
method: 'walletStatusUpdate',
return: {
isLocked,
entropy: Keyring.entropy
? entropyToSerialized(Keyring.entropy)
: undefined,
activeAccount: Keyring.keypair?.export(),
isInitialized: await Keyring.isWalletInitialized(),
},
})
Expand Down
26 changes: 3 additions & 23 deletions apps/wallet/src/background/keyring/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,22 +82,6 @@ class Keyring {
return this.#keypair;
}

public get entropy() {
return this.#vault?.entropy;
}

// sui address always prefixed with 0x
public get address() {
if (this.#keypair) {
let address = this.#keypair.getPublicKey().toSuiAddress();
if (!address.startsWith('0x')) {
address = `0x${address}`;
}
return address;
}
return null;
}

public on = this.#events.on;

public off = this.#events.off;
Expand All @@ -112,7 +96,7 @@ class Keyring {
const { password, importedEntropy } = payload.args;
await this.createVault(password, importedEntropy);
await this.unlock(password);
if (!this.#vault?.entropy) {
if (!this.#keypair) {
throw new Error('Error created vault is empty');
}
uiConnection.send(
Expand All @@ -121,9 +105,7 @@ class Keyring {
type: 'keyring',
method: 'create',
return: {
entropy: entropyToSerialized(
this.#vault.entropy
),
keypair: this.#keypair.export(),
},
},
id
Expand Down Expand Up @@ -161,9 +143,7 @@ class Keyring {
return: {
isLocked: this.isLocked,
isInitialized: await this.isWalletInitialized(),
entropy: this.#vault?.entropy
? entropyToSerialized(this.#vault.entropy)
: undefined,
activeAccount: this.#keypair?.export(),
},
},
id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@

import { isBasePayload } from '_payloads';

import type { ExportedKeypair } from '@mysten/sui.js';
import type { BasePayload, Payload } from '_payloads';

type MethodToPayloads = {
create: {
args: { password: string; importedEntropy?: string };
return: { entropy: string };
return: { keypair: ExportedKeypair };
};
getEntropy: {
args: string | undefined;
Expand All @@ -23,7 +24,8 @@ type MethodToPayloads = {
return: Partial<{
isLocked: boolean;
isInitialized: boolean;
entropy: string;
// we can replace keypair (once we stop signing from the UI) with the account address
activeAccount: ExportedKeypair;
}>;
};
lock: {
Expand Down
14 changes: 5 additions & 9 deletions apps/wallet/src/ui/app/KeypairVault.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { Ed25519Keypair } from '@mysten/sui.js';
import { fromExportedKeypair } from '@mysten/sui.js';

import { toEntropy, entropyToMnemonic } from '_shared/utils/bip39';

import type { Keypair } from '@mysten/sui.js';
import type { Keypair, ExportedKeypair } from '@mysten/sui.js';

export default class KeypairVault {
private _keypair: Keypair | null = null;

public set entropy(entropy: string) {
this._keypair = Ed25519Keypair.deriveKeypair(
entropyToMnemonic(toEntropy(entropy))
);
public set keypair(keypair: ExportedKeypair) {
this._keypair = fromExportedKeypair(keypair);
}

public getAccount(): string | null {
Expand All @@ -24,7 +20,7 @@ export default class KeypairVault {
return address;
}

public getKeyPair() {
public getKeypair() {
if (!this._keypair) {
throw new Error('Account keypair is not set');
}
Expand Down
2 changes: 1 addition & 1 deletion apps/wallet/src/ui/app/hooks/useSigner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ import { thunkExtras } from '_redux/store/thunk-extras';

export function useSigner() {
const { api, keypairVault } = thunkExtras;
return api.getSignerInstance(keypairVault.getKeyPair());
return api.getSignerInstance(keypairVault.getKeypair());
}
2 changes: 1 addition & 1 deletion apps/wallet/src/ui/app/pages/dapp-tx-approval/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ export function DappTxApprovalPage() {
queryFn: () => {
if (txRequest) {
const signer = thunkExtras.api.getSignerInstance(
thunkExtras.keypairVault.getKeyPair()
thunkExtras.keypairVault.getKeypair()
);
let txToEstimate: Parameters<
typeof signer.dryRunTransaction
Expand Down
15 changes: 10 additions & 5 deletions apps/wallet/src/ui/app/redux/slices/account/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,19 @@ import { isKeyringPayload } from '_payloads/keyring';
import { suiObjectsAdapterSelectors } from '_redux/slices/sui-objects';
import { Coin } from '_redux/slices/sui-objects/Coin';

import type { ObjectId, SuiAddress, SuiMoveObject } from '@mysten/sui.js';
import type {
ObjectId,
SuiAddress,
SuiMoveObject,
ExportedKeypair,
} from '@mysten/sui.js';
import type { PayloadAction, Reducer } from '@reduxjs/toolkit';
import type { KeyringPayload } from '_payloads/keyring';
import type { RootState } from '_redux/RootReducer';
import type { AppThunkConfig } from '_store/thunk-extras';

export const createVault = createAsyncThunk<
string,
ExportedKeypair,
{
importedEntropy?: string;
password: string;
Expand All @@ -37,10 +42,10 @@ export const createVault = createAsyncThunk<
if (!isKeyringPayload(payload, 'create')) {
throw new Error('Unknown payload');
}
if (!payload.return?.entropy) {
throw new Error('Empty entropy in payload');
if (!payload.return?.keypair) {
throw new Error('Empty keypair in payload');
}
return payload.return.entropy;
return payload.return.keypair;
}
);

Expand Down
4 changes: 2 additions & 2 deletions apps/wallet/src/ui/app/redux/slices/sui-objects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export const batchFetchObject = createAsyncThunk<
export const mintDemoNFT = createAsyncThunk<void, void, AppThunkConfig>(
'mintDemoNFT',
async (_, { extra: { api, keypairVault }, dispatch }) => {
const signer = api.getSignerInstance(keypairVault.getKeyPair());
const signer = api.getSignerInstance(keypairVault.getKeypair());
await ExampleNFT.mintExampleNFT(signer);
await dispatch(fetchAllOwnedAndRequiredObjects());
}
Expand All @@ -111,7 +111,7 @@ export const transferNFT = createAsyncThunk<
{ nftId: ObjectId; recipientAddress: SuiAddress },
AppThunkConfig
>('transferNFT', async (data, { extra: { api, keypairVault }, dispatch }) => {
const signer = api.getSignerInstance(keypairVault.getKeyPair());
const signer = api.getSignerInstance(keypairVault.getKeypair());
const txn = await signer.transferObject({
objectId: data.nftId,
recipient: data.recipientAddress,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export const deserializeTxn = createAsyncThunk<
'deserialize-transaction',
async (data, { dispatch, extra: { api, keypairVault } }) => {
const { id, serializedTxn } = data;
const signer = api.getSignerInstance(keypairVault.getKeyPair());
const signer = api.getSignerInstance(keypairVault.getKeypair());
const localSerializer = new LocalTxnDataSerializer(signer.provider);
const txnBytes = new Base64DataBuffer(serializedTxn);

Expand Down Expand Up @@ -138,7 +138,7 @@ export const respondToTransactionRequest = createAsyncThunk<
let txResult: SuiTransactionResponse | undefined = undefined;
let tsResultError: string | undefined;
if (approved) {
const signer = api.getSignerInstance(keypairVault.getKeyPair());
const signer = api.getSignerInstance(keypairVault.getKeypair());
try {
let response: SuiExecuteTransactionResponse;
if (
Expand Down
4 changes: 2 additions & 2 deletions apps/wallet/src/ui/app/redux/slices/transactions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const sendTokens = createAsyncThunk<
) => {
const state = getState();
const coins: SuiMoveObject[] = accountCoinsSelector(state);
const signer = api.getSignerInstance(keypairVault.getKeyPair());
const signer = api.getSignerInstance(keypairVault.getKeypair());
const response = await CoinAPI.transfer(
signer,
coins,
Expand Down Expand Up @@ -101,7 +101,7 @@ export const StakeTokens = createAsyncThunk<
const metadata = (first_validator as SuiMoveObject).fields.metadata;
const validatorAddress = (metadata as SuiMoveObject).fields.sui_address;
const response = await Coin.stakeCoin(
api.getSignerInstance(keypairVault.getKeyPair()),
api.getSignerInstance(keypairVault.getKeypair()),
coins,
amount,
validatorAddress
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import { isAnyOf } from '@reduxjs/toolkit';

import {
loadEntropyFromKeyring,
setAddress,
createVault,
setKeyringStatus,
Expand All @@ -15,7 +14,6 @@ import type { Middleware } from '@reduxjs/toolkit';

const keypairVault = thunkExtras.keypairVault;
const matchUpdateKeypairVault = isAnyOf(
loadEntropyFromKeyring.fulfilled,
createVault.fulfilled,
setKeyringStatus
);
Expand All @@ -25,19 +23,22 @@ export const KeypairVaultMiddleware: Middleware =
(next) =>
(action) => {
if (matchUpdateKeypairVault(action)) {
let entropy;
if (typeof action.payload === 'string') {
entropy = action.payload;
} else {
entropy = action.payload?.entropy;
let exportedKeypair;
if (action.payload) {
if ('activeAccount' in action.payload) {
exportedKeypair = action.payload.activeAccount;
} else if ('schema' in action.payload) {
exportedKeypair = action.payload;
}
}
if (entropy) {
keypairVault.entropy = entropy;
if (exportedKeypair) {
keypairVault.keypair = exportedKeypair;
dispatch(setAddress(keypairVault.getAccount()));
} else {
keypairVault.clear();
dispatch(setAddress(null));
}
entropy = null;
exportedKeypair = null;
}
return next(action);
};
21 changes: 17 additions & 4 deletions sdk/typescript/src/cryptography/ed25519-keypair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@

import nacl from 'tweetnacl';
import { Base64DataBuffer } from '../serialization/base64';
import { Keypair } from './keypair';
import type { ExportedKeypair, Keypair } from './keypair';
import { Ed25519PublicKey } from './ed25519-publickey';
import { SignatureScheme } from './publickey';
import { isValidHardenedPath, mnemonicToSeedHex } from './mnemonics';
import { derivePath, getPublicKey } from '../utils/ed25519-hd-key';
import { toB64 } from '@mysten/bcs';

export const DEFAULT_ED25519_DERIVATION_PATH = "m/44'/784'/0'/0'/0'";

Expand Down Expand Up @@ -64,16 +65,21 @@ export class Ed25519Keypair implements Keypair {
* @param secretKey secret key byte array
* @param options: skip secret key validation
*/
static fromSecretKey(secretKey: Uint8Array, options?: { skipValidation?: boolean }): Ed25519Keypair {
static fromSecretKey(
secretKey: Uint8Array,
options?: { skipValidation?: boolean }
): Ed25519Keypair {
const secretKeyLength = secretKey.length;
if (secretKeyLength != 64) {
// Many users actually wanted to invoke fromSeed(seed: Uint8Array), especially when reading from keystore.
if (secretKeyLength == 32) {
throw new Error(
'Wrong secretKey size. Expected 64 bytes, got 32. Similar function exists: fromSeed(seed: Uint8Array)'
'Wrong secretKey size. Expected 64 bytes, got 32. Similar function exists: fromSeed(seed: Uint8Array)'
);
}
throw new Error(`Wrong secretKey size. Expected 64 bytes, got ${secretKeyLength}.`);
throw new Error(
`Wrong secretKey size. Expected 64 bytes, got ${secretKeyLength}.`
);
}
const keypair = nacl.sign.keyPair.fromSecretKey(secretKey);
if (!options || !options.skipValidation) {
Expand Down Expand Up @@ -140,4 +146,11 @@ export class Ed25519Keypair implements Keypair {

return new Ed25519Keypair({ publicKey: pubkey, secretKey: fullPrivateKey });
}

export(): ExportedKeypair {
return {
schema: 'ED25519',
privateKey: toB64(this.keypair.secretKey),
};
}
}
22 changes: 22 additions & 0 deletions sdk/typescript/src/cryptography/keypair.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { fromB64 } from '@mysten/bcs';
import { Base64DataBuffer } from '../serialization/base64';
import { Ed25519Keypair } from './ed25519-keypair';
import { PublicKey, SignatureScheme } from './publickey';
import { Secp256k1Keypair } from './secp256k1-keypair';

export type ExportedKeypair = {
schema: SignatureScheme;
privateKey: string;
};

/**
* A keypair used for signing transactions.
Expand All @@ -22,4 +30,18 @@ export interface Keypair {
* Get the key scheme of the keypair: Secp256k1 or ED25519
*/
getKeyScheme(): SignatureScheme;

export(): ExportedKeypair;
}

export function fromExportedKeypair(keypair: ExportedKeypair): Keypair {
const secretKey = fromB64(keypair.privateKey);
switch (keypair.schema) {
case 'ED25519':
return Ed25519Keypair.fromSecretKey(secretKey);
case 'Secp256k1':
return Secp256k1Keypair.fromSecretKey(secretKey);
default:
throw new Error(`Invalid keypair schema ${keypair.schema}`);
}
}
Loading

0 comments on commit 519e115

Please sign in to comment.