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, }, },