diff --git a/packages/starknet-snap/index.html b/packages/starknet-snap/index.html
index 5b8b031e..e6385545 100644
--- a/packages/starknet-snap/index.html
+++ b/packages/starknet-snap/index.html
@@ -612,13 +612,13 @@
Hello, Snaps!
await callSnap('starkNet_createAccount', { addressIndex, deploy });
}
- // here we call the snap's "starkNet_extractPrivateKey" method
+ // here we call the snap's "starkNet_displayPrivateKey" method
async function retrievePrivateKeyFromAddress(e) {
e.preventDefault(); // to prevent default form behavior
const userAddress = document.getElementById('userAddress').value;
- await callSnap('starkNet_extractPrivateKey', { userAddress });
+ await callSnap('starkNet_displayPrivateKey', { userAddress });
}
// here we call the snap's "starkNet_extractPublicKey" method
diff --git a/packages/starknet-snap/openrpc/starknet_snap_api_openrpc.json b/packages/starknet-snap/openrpc/starknet_snap_api_openrpc.json
index 70a219fa..0dd14653 100644
--- a/packages/starknet-snap/openrpc/starknet_snap_api_openrpc.json
+++ b/packages/starknet-snap/openrpc/starknet_snap_api_openrpc.json
@@ -8,7 +8,7 @@
"servers": [],
"methods": [
{
- "name": "starkNet_extractPrivateKey",
+ "name": "starkNet_displayPrivateKey",
"summary": "Extract private key from deployed Starknet account and display in MetaMask",
"paramStructure": "by-name",
"params": [
diff --git a/packages/starknet-snap/src/extractPrivateKey.ts b/packages/starknet-snap/src/extractPrivateKey.ts
deleted file mode 100644
index 69739c2e..00000000
--- a/packages/starknet-snap/src/extractPrivateKey.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import { copyable, panel, text, DialogType } from '@metamask/snaps-sdk';
-
-import type {
- ApiParamsWithKeyDeriver,
- ExtractPrivateKeyRequestParams,
-} from './types/snapApi';
-import { logger } from './utils/logger';
-import { toJson } from './utils/serializer';
-import {
- getNetworkFromChainId,
- verifyIfAccountNeedUpgradeOrDeploy,
-} from './utils/snapUtils';
-import {
- validateAndParseAddress,
- getKeysFromAddress,
-} from './utils/starknetUtils';
-/**
- *
- * @param params
- */
-export async function extractPrivateKey(params: ApiParamsWithKeyDeriver) {
- try {
- const { state, wallet, keyDeriver, requestParams } = params;
- const requestParamsObj = requestParams as ExtractPrivateKeyRequestParams;
- const network = getNetworkFromChainId(state, requestParamsObj.chainId);
- const { userAddress } = requestParamsObj;
- if (!userAddress) {
- throw new Error(
- `The given user address need to be non-empty string, got: ${toJson(
- userAddress,
- )}`,
- );
- }
-
- try {
- validateAndParseAddress(userAddress);
- } catch (error) {
- throw new Error(`The given user address is invalid: ${userAddress}`);
- }
-
- const { privateKey: userPrivateKey, publicKey } = await getKeysFromAddress(
- keyDeriver,
- network,
- state,
- userAddress,
- );
-
- await verifyIfAccountNeedUpgradeOrDeploy(
- network,
- userAddress,
- publicKey,
- false,
- );
-
- const response = await wallet.request({
- method: 'snap_dialog',
- params: {
- type: DialogType.Confirmation,
- content: panel([text('Do you want to export your private Key ?')]),
- },
- });
-
- if (response === true) {
- await wallet.request({
- method: 'snap_dialog',
- params: {
- type: DialogType.Alert,
- content: panel([
- text('Starknet Account Private Key'),
- copyable(userPrivateKey),
- ]),
- },
- });
- }
-
- return null;
- } catch (error) {
- logger.error(`Problem found:`, error);
- throw error;
- }
-}
diff --git a/packages/starknet-snap/src/index.ts b/packages/starknet-snap/src/index.ts
index d0856933..a1ebc65d 100644
--- a/packages/starknet-snap/src/index.ts
+++ b/packages/starknet-snap/src/index.ts
@@ -23,7 +23,6 @@ import { estimateAccDeployFee } from './estimateAccountDeployFee';
import { estimateFee } from './estimateFee';
import { estimateFees } from './estimateFees';
import { executeTxn } from './executeTxn';
-import { extractPrivateKey } from './extractPrivateKey';
import { extractPublicKey } from './extractPublicKey';
import { getCurrentNetwork } from './getCurrentNetwork';
import { getErc20TokenBalance } from './getErc20TokenBalance';
@@ -36,8 +35,12 @@ import { getTransactions } from './getTransactions';
import { getTransactionStatus } from './getTransactionStatus';
import { getValue } from './getValue';
import { recoverAccounts } from './recoverAccounts';
-import type { SignMessageParams, SignTransactionParams } from './rpcs';
-import { signMessage, signTransaction } from './rpcs';
+import type {
+ DisplayPrivateKeyParams,
+ SignMessageParams,
+ SignTransactionParams,
+} from './rpcs';
+import { displayPrivateKey, signMessage, signTransaction } from './rpcs';
import { sendTransaction } from './sendTransaction';
import { signDeclareTransaction } from './signDeclareTransaction';
import { signDeployAccountTransaction } from './signDeployAccountTransaction';
@@ -161,10 +164,9 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => {
case 'starkNet_getStoredUserAccounts':
return await getStoredUserAccounts(apiParams);
- case 'starkNet_extractPrivateKey':
- apiParams.keyDeriver = await getAddressKeyDeriver(snap);
- return await extractPrivateKey(
- apiParams as unknown as ApiParamsWithKeyDeriver,
+ case 'starkNet_displayPrivateKey':
+ return await displayPrivateKey.execute(
+ apiParams.requestParams as unknown as DisplayPrivateKeyParams,
);
case 'starkNet_extractPublicKey':
diff --git a/packages/starknet-snap/src/rpcs/__tests__/helper.ts b/packages/starknet-snap/src/rpcs/__tests__/helper.ts
index 79be082b..baa20a9c 100644
--- a/packages/starknet-snap/src/rpcs/__tests__/helper.ts
+++ b/packages/starknet-snap/src/rpcs/__tests__/helper.ts
@@ -48,10 +48,21 @@ export function prepareMockAccount(account: StarknetAccount, state: SnapState) {
/**
*
*/
-export function prepareSignConfirmDialog() {
+export function prepareConfirmDialog() {
const confirmDialogSpy = jest.spyOn(snapHelper, 'confirmDialog');
confirmDialogSpy.mockResolvedValue(true);
return {
confirmDialogSpy,
};
}
+
+/**
+ *
+ */
+export function prepareAlertDialog() {
+ const alertDialogSpy = jest.spyOn(snapHelper, 'alertDialog');
+ alertDialogSpy.mockResolvedValue(true);
+ return {
+ alertDialogSpy,
+ };
+}
diff --git a/packages/starknet-snap/src/rpcs/displayPrivateKey.test.ts b/packages/starknet-snap/src/rpcs/displayPrivateKey.test.ts
new file mode 100644
index 00000000..f7ac8f0a
--- /dev/null
+++ b/packages/starknet-snap/src/rpcs/displayPrivateKey.test.ts
@@ -0,0 +1,116 @@
+import {
+ InvalidParamsError,
+ UserRejectedRequestError,
+} from '@metamask/snaps-sdk';
+import { constants } from 'starknet';
+
+import type { SnapState } from '../types/snapState';
+import { STARKNET_SEPOLIA_TESTNET_NETWORK } from '../utils/constants';
+import {
+ mockAccount,
+ prepareAlertDialog,
+ prepareMockAccount,
+ prepareConfirmDialog,
+} from './__tests__/helper';
+import { displayPrivateKey } from './displayPrivateKey';
+import type { DisplayPrivateKeyParams } from './displayPrivateKey';
+
+describe('displayPrivateKey', () => {
+ const state: SnapState = {
+ accContracts: [],
+ erc20Tokens: [],
+ networks: [STARKNET_SEPOLIA_TESTNET_NETWORK],
+ transactions: [],
+ };
+
+ const createRequestParam = (
+ chainId: constants.StarknetChainId,
+ address: string,
+ ): DisplayPrivateKeyParams => {
+ const request: DisplayPrivateKeyParams = {
+ chainId,
+ address,
+ };
+ return request;
+ };
+
+ it('displays private key correctly', async () => {
+ const chainId = constants.StarknetChainId.SN_SEPOLIA;
+ const account = await mockAccount(chainId);
+ prepareMockAccount(account, state);
+ prepareConfirmDialog();
+ const { alertDialogSpy } = prepareAlertDialog();
+
+ const request = createRequestParam(chainId, account.address);
+
+ await displayPrivateKey.execute(request);
+
+ expect(alertDialogSpy).toHaveBeenCalledTimes(1);
+
+ const calls = alertDialogSpy.mock.calls[0][0];
+
+ expect(calls).toStrictEqual([
+ { type: 'text', value: 'Starknet Account Private Key' },
+ { type: 'copyable', value: account.privateKey },
+ ]);
+ });
+
+ it('renders confirmation dialog', async () => {
+ const chainId = constants.StarknetChainId.SN_SEPOLIA;
+ const account = await mockAccount(chainId);
+ prepareMockAccount(account, state);
+ const { confirmDialogSpy } = prepareConfirmDialog();
+ prepareAlertDialog();
+
+ const request = createRequestParam(chainId, account.address);
+
+ await displayPrivateKey.execute(request);
+
+ expect(confirmDialogSpy).toHaveBeenCalledTimes(1);
+
+ const calls = confirmDialogSpy.mock.calls[0][0];
+
+ expect(calls).toStrictEqual([
+ { type: 'text', value: 'Do you want to export your private key?' },
+ ]);
+ });
+
+ it('throws `UserRejectedRequestError` if user denies the operation', async () => {
+ const chainId = constants.StarknetChainId.SN_SEPOLIA;
+ const account = await mockAccount(chainId);
+ prepareMockAccount(account, state);
+ const { confirmDialogSpy } = prepareConfirmDialog();
+ prepareAlertDialog();
+
+ confirmDialogSpy.mockResolvedValue(false);
+
+ const request = createRequestParam(chainId, account.address);
+
+ await expect(displayPrivateKey.execute(request)).rejects.toThrow(
+ UserRejectedRequestError,
+ );
+ });
+
+ it.each([
+ {
+ case: 'user address is omitted',
+ request: {
+ chainId: constants.StarknetChainId.SN_SEPOLIA,
+ },
+ },
+ {
+ case: 'user address is invalid',
+ request: {
+ chainId: constants.StarknetChainId.SN_SEPOLIA,
+ address: 'invalid_address',
+ },
+ },
+ ])(
+ 'throws `InvalidParamsError` when $case',
+ async ({ request }: { request: unknown }) => {
+ await expect(
+ displayPrivateKey.execute(request as DisplayPrivateKeyParams),
+ ).rejects.toThrow(InvalidParamsError);
+ },
+ );
+});
diff --git a/packages/starknet-snap/src/rpcs/displayPrivateKey.ts b/packages/starknet-snap/src/rpcs/displayPrivateKey.ts
new file mode 100644
index 00000000..38f5092d
--- /dev/null
+++ b/packages/starknet-snap/src/rpcs/displayPrivateKey.ts
@@ -0,0 +1,77 @@
+import { copyable, text, UserRejectedRequestError } from '@metamask/snaps-sdk';
+import { type Infer, object, literal, assign } from 'superstruct';
+
+import {
+ AccountRpcController,
+ AddressStruct,
+ confirmDialog,
+ alertDialog,
+ BaseRequestStruct,
+} from '../utils';
+
+export const DisplayPrivateKeyRequestStruct = assign(
+ object({
+ address: AddressStruct,
+ }),
+ BaseRequestStruct,
+);
+
+export const DisplayPrivateKeyResponseStruct = literal(null);
+
+export type DisplayPrivateKeyParams = Infer<
+ typeof DisplayPrivateKeyRequestStruct
+>;
+
+export type DisplayPrivateKeyResponse = Infer<
+ typeof DisplayPrivateKeyResponseStruct
+>;
+
+/**
+ * The RPC handler to display a private key.
+ */
+export class DisplayPrivateKeyRpc extends AccountRpcController<
+ DisplayPrivateKeyParams,
+ DisplayPrivateKeyResponse
+> {
+ protected requestStruct = DisplayPrivateKeyRequestStruct;
+
+ protected responseStruct = DisplayPrivateKeyResponseStruct;
+
+ /**
+ * Execute the display private key request handler.
+ * The private key will be display via a confirmation dialog.
+ *
+ * @param params - The parameters of the request.
+ * @param params.address - The account address.
+ * @param params.chainId - The chain id of the network.
+ */
+ async execute(
+ params: DisplayPrivateKeyParams,
+ ): Promise {
+ return super.execute(params);
+ }
+
+ protected async handleRequest(
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ params: DisplayPrivateKeyParams,
+ ): Promise {
+ const confirmComponents = [text('Do you want to export your private key?')];
+
+ if (!(await confirmDialog(confirmComponents))) {
+ throw new UserRejectedRequestError() as unknown as Error;
+ }
+
+ const alertComponents = [
+ text('Starknet Account Private Key'),
+ copyable(this.account.privateKey),
+ ];
+
+ await alertDialog(alertComponents);
+
+ return null;
+ }
+}
+
+export const displayPrivateKey = new DisplayPrivateKeyRpc({
+ showInvalidAccountAlert: false,
+});
diff --git a/packages/starknet-snap/src/rpcs/index.ts b/packages/starknet-snap/src/rpcs/index.ts
index db76b966..62815702 100644
--- a/packages/starknet-snap/src/rpcs/index.ts
+++ b/packages/starknet-snap/src/rpcs/index.ts
@@ -1,2 +1,3 @@
export * from './signMessage';
+export * from './displayPrivateKey';
export * from './signTransaction';
diff --git a/packages/starknet-snap/src/rpcs/signTransaction.test.ts b/packages/starknet-snap/src/rpcs/signTransaction.test.ts
index d71c919b..ca892fc0 100644
--- a/packages/starknet-snap/src/rpcs/signTransaction.test.ts
+++ b/packages/starknet-snap/src/rpcs/signTransaction.test.ts
@@ -13,7 +13,7 @@ import * as starknetUtils from '../utils/starknetUtils';
import {
mockAccount,
prepareMockAccount,
- prepareSignConfirmDialog,
+ prepareConfirmDialog,
} from './__tests__/helper';
import { signTransaction } from './signTransaction';
import type { SignTransactionParams } from './signTransaction';
@@ -48,7 +48,7 @@ describe('signTransaction', () => {
const chainId = constants.StarknetChainId.SN_SEPOLIA;
const account = await mockAccount(chainId);
prepareMockAccount(account, state);
- prepareSignConfirmDialog();
+ prepareConfirmDialog();
const request = createRequestParam(chainId, account.address);
const expectedResult = await starknetUtils.signTransactions(
@@ -66,7 +66,7 @@ describe('signTransaction', () => {
const chainId = constants.StarknetChainId.SN_SEPOLIA;
const account = await mockAccount(chainId);
prepareMockAccount(account, state);
- const { confirmDialogSpy } = prepareSignConfirmDialog();
+ const { confirmDialogSpy } = prepareConfirmDialog();
const request = createRequestParam(chainId, account.address, true);
await signTransaction.execute(request);
@@ -108,7 +108,7 @@ describe('signTransaction', () => {
const chainId = constants.StarknetChainId.SN_SEPOLIA;
const account = await mockAccount(chainId);
prepareMockAccount(account, state);
- const { confirmDialogSpy } = prepareSignConfirmDialog();
+ const { confirmDialogSpy } = prepareConfirmDialog();
const request = createRequestParam(chainId, account.address, false);
await signTransaction.execute(request);
@@ -120,7 +120,7 @@ describe('signTransaction', () => {
const chainId = constants.StarknetChainId.SN_SEPOLIA;
const account = await mockAccount(chainId);
prepareMockAccount(account, state);
- const { confirmDialogSpy } = prepareSignConfirmDialog();
+ const { confirmDialogSpy } = prepareConfirmDialog();
confirmDialogSpy.mockResolvedValue(false);
const request = createRequestParam(chainId, account.address, true);
diff --git a/packages/starknet-snap/test/src/extractPrivateKey.test.ts b/packages/starknet-snap/test/src/extractPrivateKey.test.ts
deleted file mode 100644
index 81cd2f95..00000000
--- a/packages/starknet-snap/test/src/extractPrivateKey.test.ts
+++ /dev/null
@@ -1,211 +0,0 @@
-import chai, { expect } from 'chai';
-import sinon from 'sinon';
-import sinonChai from 'sinon-chai';
-import { WalletMock } from '../wallet.mock.test';
-import { SnapState } from '../../src/types/snapState';
-import { extractPrivateKey } from '../../src/extractPrivateKey';
-import { STARKNET_SEPOLIA_TESTNET_NETWORK } from '../../src/utils/constants';
-import {
- account1,
- getBip44EntropyStub,
- unfoundUserAddress,
-} from '../constants.test';
-import { getAddressKeyDeriver } from '../../src/utils/keyPair';
-import * as utils from '../../src/utils/starknetUtils';
-import { Mutex } from 'async-mutex';
-import {
- ApiParamsWithKeyDeriver,
- ExtractPrivateKeyRequestParams,
-} from '../../src/types/snapApi';
-import { UpgradeRequiredError } from '../../src/utils/exceptions';
-
-chai.use(sinonChai);
-const sandbox = sinon.createSandbox();
-
-describe('Test function: extractPrivateKey', function () {
- const walletStub = new WalletMock();
- const state: SnapState = {
- accContracts: [account1],
- erc20Tokens: [],
- networks: [STARKNET_SEPOLIA_TESTNET_NETWORK],
- transactions: [],
- };
-
- let apiParams: ApiParamsWithKeyDeriver;
-
- const requestObject: ExtractPrivateKeyRequestParams = {
- userAddress: account1.address,
- };
-
- beforeEach(async function () {
- walletStub.rpcStubs.snap_getBip44Entropy.callsFake(getBip44EntropyStub);
- apiParams = {
- state,
- requestParams: {},
- wallet: walletStub,
- saveMutex: new Mutex(),
- keyDeriver: await getAddressKeyDeriver(walletStub),
- };
- });
-
- afterEach(function () {
- walletStub.reset();
- sandbox.restore();
- });
-
- describe('when request param validation fail', function () {
- let invalidRequest = Object.assign({}, requestObject);
-
- afterEach(async function () {
- invalidRequest = Object.assign({}, requestObject);
- });
-
- it('should throw an error if the user address is undefined', async function () {
- invalidRequest.userAddress = undefined as unknown as string;
- apiParams.requestParams = invalidRequest;
- let result;
- try {
- result = await extractPrivateKey(apiParams);
- } catch (err) {
- result = err;
- } finally {
- expect(result).to.be.an('Error');
- }
- });
-
- it('should throw an error if the user address is invalid', async function () {
- invalidRequest.userAddress = 'wrongAddress';
- apiParams.requestParams = invalidRequest;
- let result;
- try {
- result = await extractPrivateKey(apiParams);
- } catch (err) {
- result = err;
- } finally {
- expect(result).to.be.an('Error');
- }
- });
- });
-
- describe('when request param validation pass', function () {
- beforeEach(async function () {
- apiParams.requestParams = Object.assign({}, requestObject);
- });
-
- afterEach(async function () {
- apiParams.requestParams = Object.assign({}, requestObject);
- });
-
- describe('when validateAccountRequireUpgradeOrDeploy fail', function () {
- it('should throw error', async function () {
- const validateAccountRequireUpgradeOrDeployStub = sandbox
- .stub(utils, 'validateAccountRequireUpgradeOrDeploy')
- .throws('network error');
- let result;
- try {
- result = await extractPrivateKey(apiParams);
- } catch (err) {
- result = err;
- } finally {
- expect(
- validateAccountRequireUpgradeOrDeployStub,
- ).to.have.been.calledOnceWith(
- STARKNET_SEPOLIA_TESTNET_NETWORK,
- account1.address,
- account1.publicKey,
- );
- expect(result).to.be.an('Error');
- }
- });
- });
-
- describe('when account require upgrade', function () {
- let validateAccountRequireUpgradeOrDeployStub: sinon.SinonStub;
- beforeEach(async function () {
- validateAccountRequireUpgradeOrDeployStub = sandbox
- .stub(utils, 'validateAccountRequireUpgradeOrDeploy')
- .throws(new UpgradeRequiredError('Upgrade Required'));
- });
-
- it('should throw error if upgrade required', async function () {
- let result;
- try {
- result = await extractPrivateKey(apiParams);
- } catch (err) {
- result = err;
- } finally {
- expect(
- validateAccountRequireUpgradeOrDeployStub,
- ).to.have.been.calledOnceWith(
- STARKNET_SEPOLIA_TESTNET_NETWORK,
- account1.address,
- account1.publicKey,
- );
- expect(result).to.be.an('Error');
- }
- });
- });
-
- describe('when account is not require upgrade', function () {
- beforeEach(async function () {
- sandbox
- .stub(utils, 'validateAccountRequireUpgradeOrDeploy')
- .resolvesThis();
- });
-
- it('should get the private key of the specified user account correctly', async function () {
- walletStub.rpcStubs.snap_dialog.resolves(true);
- const requestObject: ExtractPrivateKeyRequestParams = {
- userAddress: account1.address,
- };
- apiParams.requestParams = requestObject;
- const result = await extractPrivateKey(apiParams);
- expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledTwice;
- expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called;
- expect(result).to.be.equal(null);
- });
-
- it('should get the private key of the unfound user account correctly', async function () {
- walletStub.rpcStubs.snap_dialog.resolves(true);
- const requestObject: ExtractPrivateKeyRequestParams = {
- userAddress: unfoundUserAddress,
- };
- apiParams.requestParams = requestObject;
- const result = await extractPrivateKey(apiParams);
- expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledTwice;
- expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called;
- expect(result).to.be.eql(null);
- });
-
- it('should not get the private key of the specified user account if user rejected', async function () {
- walletStub.rpcStubs.snap_dialog.resolves(false);
- const requestObject: ExtractPrivateKeyRequestParams = {
- userAddress: account1.address,
- };
- apiParams.requestParams = requestObject;
- const result = await extractPrivateKey(apiParams);
- expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce;
- expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called;
- expect(result).to.be.equal(null);
- });
-
- it('should throw error if getKeysFromAddress failed', async function () {
- sandbox.stub(utils, 'getKeysFromAddress').throws(new Error());
- walletStub.rpcStubs.snap_dialog.resolves(true);
- const requestObject: ExtractPrivateKeyRequestParams = {
- userAddress: account1.address,
- };
- apiParams.requestParams = requestObject;
-
- let result;
- try {
- await extractPrivateKey(apiParams);
- } catch (err) {
- result = err;
- } finally {
- expect(result).to.be.an('Error');
- }
- });
- });
- });
-});
diff --git a/packages/wallet-ui/src/services/useStarkNetSnap.ts b/packages/wallet-ui/src/services/useStarkNetSnap.ts
index 3756d92a..3ebbf2a6 100644
--- a/packages/wallet-ui/src/services/useStarkNetSnap.ts
+++ b/packages/wallet-ui/src/services/useStarkNetSnap.ts
@@ -52,7 +52,7 @@ export const useStarkNetSnap = () => {
const debugLevel =
process.env.REACT_APP_DEBUG_LEVEL !== undefined
? process.env.REACT_APP_DEBUG_LEVEL
- : 'all';
+ : 'ALL';
const START_SCAN_INDEX = 0;
const MAX_SCANNED = 1;
const MAX_MISSED = 1;
@@ -312,10 +312,10 @@ export const useStarkNetSnap = () => {
params: {
snapId,
request: {
- method: 'starkNet_extractPrivateKey',
+ method: 'starkNet_displayPrivateKey',
params: {
...defaultParam,
- userAddress: address,
+ address: address,
chainId,
},
},