diff --git a/packages/starknet-snap/index.html b/packages/starknet-snap/index.html index 2913a5a9..5b8b031e 100644 --- a/packages/starknet-snap/index.html +++ b/packages/starknet-snap/index.html @@ -359,6 +359,15 @@

Hello, Snaps!

+
+
+ Upgrade Cairo Version from 0 to 1 + +
+ +
+
+ + + diff --git a/packages/starknet-snap/openrpc/starknet_snap_api_openrpc.json b/packages/starknet-snap/openrpc/starknet_snap_api_openrpc.json index b50508c4..6dea641f 100644 --- a/packages/starknet-snap/openrpc/starknet_snap_api_openrpc.json +++ b/packages/starknet-snap/openrpc/starknet_snap_api_openrpc.json @@ -367,6 +367,49 @@ }, "errors": [] }, + { + "name": "starkNet_upgradeAccContract", + "summary": "Upgrade Account Contract", + "paramStructure": "by-name", + "params": [ + { + "name": "contractAddress", + "summary": "Address of the target contract", + "description": "Address of the target contract", + "required": true, + "schema": { + "$ref": "#/components/schemas/ADDRESS" + } + }, + { + "name": "maxFee", + "summary": "Maximum gas fee allowed from the sender", + "description": "Maximum gas fee allowed from the sender (if not given, the max fee will be automatically calculated)", + "required": false, + "schema": { + "$ref": "#/components/schemas/NUM_AS_HEX" + } + }, + { + "name": "chainId", + "summary": "Id of the target Starknet network", + "description": "Id of the target Starknet network (default to Starknet Goerli Testnet)", + "required": false, + "schema": { + "$ref": "#/components/schemas/CHAIN_ID" + } + } + ], + "result": { + "name": "result", + "summary": "Response from Starknet’s \"gateway/call_contract\" endpoint", + "description": "Response from Starknet’s \"gateway/call_contract\" endpoint", + "schema": { + "$ref": "#/components/schemas/SEND_TRANSACTION_RESULT" + } + }, + "errors": [] + }, { "name": "starkNet_getTransactionStatus", "summary": "Gets the status of a transaction", diff --git a/packages/starknet-snap/package.json b/packages/starknet-snap/package.json index 79c1e9d2..4cf0df8b 100644 --- a/packages/starknet-snap/package.json +++ b/packages/starknet-snap/package.json @@ -24,6 +24,7 @@ "lint:fix": "eslint '**/*.{js,ts,tsx}' --fix", "test": "yarn run test:unit && yarn run cover:report", "test:unit": "nyc --check-coverage --statements 80 --branches 80 --functions 80 --lines 80 mocha --colors -r ts-node/register \"test/**/*.test.ts\"", + "test:unit:one": "nyc --check-coverage --statements 80 --branches 80 --functions 80 --lines 80 mocha --colors -r ts-node/register", "cover:report": "nyc report --reporter=lcov --reporter=text" }, "keywords": [], diff --git a/packages/starknet-snap/snap.manifest.json b/packages/starknet-snap/snap.manifest.json index 5d11164d..170837ee 100644 --- a/packages/starknet-snap/snap.manifest.json +++ b/packages/starknet-snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/ConsenSys/starknet-snap.git" }, "source": { - "shasum": "n98JYVZFBDx4LYHqusYAftu/diScJU/ePb8FTYHZXsU=", + "shasum": "kl5iM2GRyXtFYwezb2vvrh2iTNC4Gw4K1I3PVOttXJ4=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/starknet-snap/src/createAccount.ts b/packages/starknet-snap/src/createAccount.ts index 735fdc2f..0ce385e9 100644 --- a/packages/starknet-snap/src/createAccount.ts +++ b/packages/starknet-snap/src/createAccount.ts @@ -3,9 +3,9 @@ import { getKeysFromAddressIndex, getAccContractAddressAndCallData, deployAccount, - callContract, + getBalance, estimateAccountDeployFee, - getSigner, + isAccountDeployed, waitForTransaction, } from './utils/starknetUtils'; import { @@ -53,8 +53,6 @@ export async function createAccount(params: ApiParams, silentMode = false, waitM let failureReason = ''; let estimateDeployFee: EstimateFee; - let signerAssigned = true; - let signer = ''; if (deploy) { if (!silentMode) { @@ -80,24 +78,16 @@ export async function createAccount(params: ApiParams, silentMode = false, waitM }; } - try { - signer = await getSigner(contractAddress, network); - logger.log(`createAccount:\ngetSigner: contractAddress = ${contractAddress}, signerPublicKey= ${signer}`); - failureReason = 'The account address had already been deployed'; - } catch (err) { - signerAssigned = false; - logger.log(`createAccount:\ngetSigner: err in get signer: ${toJson(err)}`); - } + const signerAssigned = await isAccountDeployed(network, contractAddress); if (!signerAssigned) { try { - const getBalanceResp = await callContract( - network, + const balance = await getBalance( getEtherErc20Token(state, network.chainId)?.address, - 'balanceOf', - [num.toBigInt(contractAddress).toString(10)], + num.toBigInt(contractAddress).toString(10), + network, ); - logger.log(`createAccount:\ngetBalanceResp: ${toJson(getBalanceResp)}`); + logger.log(`createAccount:\ngetBalanceResp: ${balance}`); estimateDeployFee = await estimateAccountDeployFee( network, contractAddress, @@ -106,7 +96,7 @@ export async function createAccount(params: ApiParams, silentMode = false, waitM privateKey, ); logger.log(`createAccount:\nestimateDeployFee: ${toJson(estimateDeployFee)}`); - if (Number(getBalanceResp[0]) < Number(estimateDeployFee.suggestedMaxFee)) { + if (Number(balance) < Number(estimateDeployFee.suggestedMaxFee)) { const gasFeeStr = ethers.utils.formatUnits(estimateDeployFee.suggestedMaxFee.toString(10), 18); const gasFeeFloat = parseFloat(gasFeeStr).toFixed(6); // 6 decimal places for ether const gasFeeInEther = Number(gasFeeFloat) === 0 ? '0.000001' : gasFeeFloat; @@ -118,9 +108,17 @@ export async function createAccount(params: ApiParams, silentMode = false, waitM } } - const deployResp = await deployAccount(network, contractAddress, contractCallData, publicKey, privateKey, { - maxFee: estimateDeployFee?.suggestedMaxFee, - }); + const deployResp = await deployAccount( + network, + contractAddress, + contractCallData, + publicKey, + privateKey, + undefined, + { + maxFee: estimateDeployFee?.suggestedMaxFee, + }, + ); if (deployResp.contract_address && deployResp.transaction_hash) { const userAccount: AccContract = { diff --git a/packages/starknet-snap/src/estimateFee.ts b/packages/starknet-snap/src/estimateFee.ts index f86db237..c917c6e4 100644 --- a/packages/starknet-snap/src/estimateFee.ts +++ b/packages/starknet-snap/src/estimateFee.ts @@ -11,17 +11,22 @@ import { estimateFeeBulk, addFeesFromAllTransactions, isAccountDeployed, + isUpgradeRequired, } from './utils/starknetUtils'; - -import { PROXY_CONTRACT_HASH } from './utils/constants'; +import { ACCOUNT_CLASS_HASH } from './utils/constants'; import { logger } from './utils/logger'; export async function estimateFee(params: ApiParams) { try { const { state, keyDeriver, requestParams } = params; const requestParamsObj = requestParams as EstimateFeeRequestParams; + const contractAddress = requestParamsObj.contractAddress; + const contractFuncName = requestParamsObj.contractFuncName; + const contractCallData = getCallDataArray(requestParamsObj.contractCallData); + const senderAddress = requestParamsObj.senderAddress; + const network = getNetworkFromChainId(state, requestParamsObj.chainId); - if (!requestParamsObj.contractAddress || !requestParamsObj.senderAddress || !requestParamsObj.contractFuncName) { + if (!contractAddress || !requestParamsObj.senderAddress || !contractFuncName) { throw new Error( `The given contract address, sender address, and function name need to be non-empty string, got: ${toJson( requestParamsObj, @@ -30,21 +35,20 @@ export async function estimateFee(params: ApiParams) { } try { - validateAndParseAddress(requestParamsObj.contractAddress); + validateAndParseAddress(contractAddress); } catch (err) { - throw new Error(`The given contract address is invalid: ${requestParamsObj.contractAddress}`); + throw new Error(`The given contract address is invalid: ${contractAddress}`); } try { - validateAndParseAddress(requestParamsObj.senderAddress); + validateAndParseAddress(senderAddress); } catch (err) { - throw new Error(`The given sender address is invalid: ${requestParamsObj.senderAddress}`); + throw new Error(`The given sender address is invalid: ${senderAddress}`); + } + + if (await isUpgradeRequired(network, senderAddress)) { + throw new Error('Upgrade required'); } - const contractAddress = requestParamsObj.contractAddress; - const contractFuncName = requestParamsObj.contractFuncName; - const contractCallData = getCallDataArray(requestParamsObj.contractCallData); - const senderAddress = requestParamsObj.senderAddress; - const network = getNetworkFromChainId(state, requestParamsObj.chainId); const { privateKey: senderPrivateKey, publicKey } = await getKeysFromAddress( keyDeriver, network, @@ -61,7 +65,7 @@ export async function estimateFee(params: ApiParams) { logger.log(`estimateFee:\ntxnInvocation: ${toJson(txnInvocation)}`); //Estimate deploy account fee if the signer has not been deployed yet - const accountDeployed = await isAccountDeployed(network, publicKey); + const accountDeployed = await isAccountDeployed(network, senderAddress); let bulkTransactions: Invocations = [ { type: TransactionType.INVOKE, @@ -71,7 +75,7 @@ export async function estimateFee(params: ApiParams) { if (!accountDeployed) { const { callData } = getAccContractAddressAndCallData(publicKey); const deployAccountpayload = { - classHash: PROXY_CONTRACT_HASH, + classHash: ACCOUNT_CLASS_HASH, contractAddress: senderAddress, constructorCalldata: callData, addressSalt: publicKey, diff --git a/packages/starknet-snap/src/estimateFees.ts b/packages/starknet-snap/src/estimateFees.ts index 624bbd5a..74888465 100644 --- a/packages/starknet-snap/src/estimateFees.ts +++ b/packages/starknet-snap/src/estimateFees.ts @@ -20,7 +20,7 @@ export async function estimateFees(params: ApiParams) { senderAddress, senderPrivateKey, requestParamsObj.invocations, - requestParamsObj.invocationDetails ? requestParamsObj.invocationDetails : undefined, + requestParamsObj.invocationDetails, ); return fees.map((fee) => ({ diff --git a/packages/starknet-snap/src/extractPrivateKey.ts b/packages/starknet-snap/src/extractPrivateKey.ts index 4b875bc4..77480e65 100644 --- a/packages/starknet-snap/src/extractPrivateKey.ts +++ b/packages/starknet-snap/src/extractPrivateKey.ts @@ -2,7 +2,7 @@ import { toJson } from './utils/serializer'; import { validateAndParseAddress } from '../src/utils/starknetUtils'; import { ApiParams, ExtractPrivateKeyRequestParams } from './types/snapApi'; import { getNetworkFromChainId } from './utils/snapUtils'; -import { getKeysFromAddress } from './utils/starknetUtils'; +import { getKeysFromAddress, isUpgradeRequired } from './utils/starknetUtils'; import { DialogType } from '@metamask/rpc-methods'; import { copyable, panel, text } from '@metamask/snaps-sdk'; import { logger } from './utils/logger'; @@ -11,17 +11,20 @@ export async function extractPrivateKey(params: ApiParams) { try { const { state, wallet, keyDeriver, requestParams } = params; const requestParamsObj = requestParams as ExtractPrivateKeyRequestParams; - - if (!requestParamsObj.userAddress) { - throw new Error( - `The given user address need to be non-empty string, got: ${toJson(requestParamsObj.userAddress)}`, - ); + const network = getNetworkFromChainId(state, requestParamsObj.chainId); + const userAddress = requestParamsObj.userAddress; + if (!userAddress) { + throw new Error(`The given user address need to be non-empty string, got: ${toJson(userAddress)}`); } try { - validateAndParseAddress(requestParamsObj.userAddress); + validateAndParseAddress(userAddress); } catch (err) { - throw new Error(`The given user address is invalid: ${requestParamsObj.userAddress}`); + throw new Error(`The given user address is invalid: ${userAddress}`); + } + + if (await isUpgradeRequired(network, userAddress)) { + throw new Error('Upgrade required'); } const response = await wallet.request({ @@ -33,8 +36,6 @@ export async function extractPrivateKey(params: ApiParams) { }); if (response === true) { - const userAddress = requestParamsObj.userAddress; - const network = getNetworkFromChainId(state, requestParamsObj.chainId); const { privateKey: userPrivateKey } = await getKeysFromAddress(keyDeriver, network, state, userAddress); await wallet.request({ diff --git a/packages/starknet-snap/src/extractPublicKey.ts b/packages/starknet-snap/src/extractPublicKey.ts index 7b9e5960..1fa67fe5 100644 --- a/packages/starknet-snap/src/extractPublicKey.ts +++ b/packages/starknet-snap/src/extractPublicKey.ts @@ -1,6 +1,6 @@ import { toJson } from './utils/serializer'; import { constants, num } from 'starknet'; -import { validateAndParseAddress } from '../src/utils/starknetUtils'; +import { validateAndParseAddress, isUpgradeRequired } from '../src/utils/starknetUtils'; import { ApiParams, ExtractPublicKeyRequestParams } from './types/snapApi'; import { getAccount, getNetworkFromChainId } from './utils/snapUtils'; import { getKeysFromAddress } from './utils/starknetUtils'; @@ -26,6 +26,10 @@ export async function extractPublicKey(params: ApiParams) { throw new Error(`The given user address is invalid: ${requestParamsObj.userAddress}`); } + if (await isUpgradeRequired(network, userAddress)) { + throw new Error('Upgrade required'); + } + let userPublicKey; const accContract = getAccount(state, userAddress, network.chainId); if (!accContract?.publicKey || num.toBigInt(accContract.publicKey) === constants.ZERO) { diff --git a/packages/starknet-snap/src/index.ts b/packages/starknet-snap/src/index.ts index 96c9a58e..0561c0be 100644 --- a/packages/starknet-snap/src/index.ts +++ b/packages/starknet-snap/src/index.ts @@ -45,6 +45,7 @@ import { estimateFees } from './estimateFees'; import { declareContract } from './declareContract'; import { signDeclareTransaction } from './signDeclareTransaction'; import { signDeployAccountTransaction } from './signDeployAccountTransaction'; +import { upgradeAccContract } from './upgradeAccContract'; import { logger } from './utils/logger'; import { getStarkName } from './getStarkName'; @@ -211,6 +212,10 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ origin, request }) => apiParams.keyDeriver = await getAddressKeyDeriver(snap); return await estimateFees(apiParams); + case 'starkNet_upgradeAccContract': + apiParams.keyDeriver = await getAddressKeyDeriver(snap); + return upgradeAccContract(apiParams); + case 'starkNet_declareContract': apiParams.keyDeriver = await getAddressKeyDeriver(snap); return await declareContract(apiParams); @@ -248,7 +253,7 @@ export const onInstall: OnInstallHandler = async () => { export const onUpdate: OnUpdateHandler = async () => { const component = panel([ text('Features released with this update:'), - text('Deprecation of the Starknet Goerli Testnet'), + text('Cairo contract upgrade support.'), ]); await snap.request({ diff --git a/packages/starknet-snap/src/recoverAccounts.ts b/packages/starknet-snap/src/recoverAccounts.ts index fb65da5a..fb4cef06 100644 --- a/packages/starknet-snap/src/recoverAccounts.ts +++ b/packages/starknet-snap/src/recoverAccounts.ts @@ -1,6 +1,6 @@ import { toJson } from './utils/serializer'; import { num } from 'starknet'; -import { getSigner, getKeysFromAddressIndex, getAccContractAddressAndCallData } from './utils/starknetUtils'; +import { getKeysFromAddressIndex, getCorrectContractAddress } from './utils/starknetUtils'; import { getNetworkFromChainId, getValidNumber, upsertAccount } from './utils/snapUtils'; import { AccContract } from './types/snapState'; import { ApiParams, RecoverAccountsRequestParams } from './types/snapApi'; @@ -29,20 +29,17 @@ export async function recoverAccounts(params: ApiParams) { state, i, ); - const { address: contractAddress } = getAccContractAddressAndCallData(publicKey); - logger.log(`recoverAccounts: index ${i}:\ncontractAddress = ${contractAddress}\npublicKey = ${publicKey}`); - - let signerPublicKey = ''; - - try { - signerPublicKey = await getSigner(contractAddress, network); - logger.log(`recoverAccounts: index ${i}\nsignerPublicKey: ${signerPublicKey}`); - } catch (err) { - logger.log(`recoverAccounts: index ${i}\nerr in get signer: ${toJson(err)}`); - signerPublicKey = ''; - } + const { + address: contractAddress, + signerPubKey: signerPublicKey, + upgradeRequired, + } = await getCorrectContractAddress(network, publicKey); + logger.log( + `recoverAccounts: index ${i}:\ncontractAddress = ${contractAddress}\npublicKey = ${publicKey}\nisUpgradeRequired = ${upgradeRequired}`, + ); if (signerPublicKey) { + logger.log(`recoverAccounts: index ${i}:\ncontractAddress = ${contractAddress}\n`); if (num.toBigInt(signerPublicKey) === num.toBigInt(publicKey)) { logger.log(`recoverAccounts: index ${i} matched\npublicKey: ${publicKey}`); } @@ -59,6 +56,7 @@ export async function recoverAccounts(params: ApiParams) { derivationPath, deployTxnHash: '', chainId: network.chainId, + upgradeRequired: upgradeRequired, }; logger.log(`recoverAccounts: index ${i}\nuserAccount: ${toJson(userAccount)}`); diff --git a/packages/starknet-snap/src/sendTransaction.ts b/packages/starknet-snap/src/sendTransaction.ts index 219c37f3..b3870fe3 100644 --- a/packages/starknet-snap/src/sendTransaction.ts +++ b/packages/starknet-snap/src/sendTransaction.ts @@ -4,7 +4,13 @@ import { validateAndParseAddress } from '../src/utils/starknetUtils'; import { estimateFee } from './estimateFee'; import { Transaction, TransactionStatus, VoyagerTransactionType } from './types/snapState'; import { getNetworkFromChainId, getSendTxnText, upsertTransaction } from './utils/snapUtils'; -import { getKeysFromAddress, getCallDataArray, executeTxn, isAccountDeployed } from './utils/starknetUtils'; +import { + getKeysFromAddress, + getCallDataArray, + executeTxn, + isAccountDeployed, + isUpgradeRequired, +} from './utils/starknetUtils'; import { ApiParams, SendTransactionRequestParams } from './types/snapApi'; import { createAccount } from './createAccount'; import { DialogType } from '@metamask/rpc-methods'; @@ -40,11 +46,17 @@ export async function sendTransaction(params: ApiParams) { const contractCallData = getCallDataArray(requestParamsObj.contractCallData); const senderAddress = requestParamsObj.senderAddress; const network = getNetworkFromChainId(state, requestParamsObj.chainId); - const { - privateKey: senderPrivateKey, - publicKey, - addressIndex, - } = await getKeysFromAddress(keyDeriver, network, state, senderAddress); + + if (await isUpgradeRequired(network, senderAddress)) { + throw new Error('Upgrade required'); + } + + const { privateKey: senderPrivateKey, addressIndex } = await getKeysFromAddress( + keyDeriver, + network, + state, + senderAddress, + ); let maxFee = requestParamsObj.maxFee ? num.toBigInt(requestParamsObj.maxFee) : constants.ZERO; if (maxFee === constants.ZERO) { const { suggestedMaxFee } = await estimateFee(params); @@ -77,7 +89,7 @@ export async function sendTransaction(params: ApiParams) { logger.log(`sendTransaction:\ntxnInvocation: ${toJson(txnInvocation)}\nmaxFee: ${maxFee.toString()}}`); - const accountDeployed = await isAccountDeployed(network, publicKey); + const accountDeployed = await isAccountDeployed(network, senderAddress); if (!accountDeployed) { //Deploy account before sending the transaction logger.log('sendTransaction:\nFirst transaction : send deploy transaction'); @@ -116,7 +128,8 @@ export async function sendTransaction(params: ApiParams) { try { return num.toHex(num.toBigInt(data)); } catch (e) { - throw new Error(`contractCallData could not be converted, ${e.message || e}`); + //data is already send to chain, hence we should not throw error + return '0x0'; } }), finalityStatus: TransactionStatus.RECEIVED, diff --git a/packages/starknet-snap/src/signMessage.ts b/packages/starknet-snap/src/signMessage.ts index bea26970..d4693f2c 100644 --- a/packages/starknet-snap/src/signMessage.ts +++ b/packages/starknet-snap/src/signMessage.ts @@ -1,5 +1,5 @@ import { toJson } from './utils/serializer'; -import { signMessage as signMessageUtil, getKeysFromAddress } from './utils/starknetUtils'; +import { signMessage as signMessageUtil, getKeysFromAddress, isUpgradeRequired } from './utils/starknetUtils'; import { getNetworkFromChainId, addDialogTxt } from './utils/snapUtils'; import { ApiParams, SignMessageRequestParams } from './types/snapApi'; import { validateAndParseAddress } from '../src/utils/starknetUtils'; @@ -11,24 +11,25 @@ export async function signMessage(params: ApiParams) { try { const { state, wallet, keyDeriver, requestParams } = params; const requestParamsObj = requestParams as SignMessageRequestParams; + const signerAddress = requestParamsObj.signerAddress; + const typedDataMessage = requestParamsObj.typedDataMessage; + const network = getNetworkFromChainId(state, requestParamsObj.chainId); - if (!requestParamsObj.signerAddress) { - throw new Error( - `The given signer address need to be non-empty string, got: ${toJson(requestParamsObj.signerAddress)}`, - ); + logger.log(`signMessage:\nsignerAddress: ${signerAddress}\ntypedDataMessage: ${toJson(typedDataMessage)}`); + + if (!signerAddress) { + throw new Error(`The given signer address need to be non-empty string, got: ${toJson(signerAddress)}`); } try { - validateAndParseAddress(requestParamsObj.signerAddress); + validateAndParseAddress(signerAddress); } catch (err) { - throw new Error(`The given signer address is invalid: ${requestParamsObj.signerAddress}`); + throw new Error(`The given signer address is invalid: ${signerAddress}`); } - const signerAddress = requestParamsObj.signerAddress; - const typedDataMessage = requestParamsObj.typedDataMessage; - const network = getNetworkFromChainId(state, requestParamsObj.chainId); - - logger.log(`signMessage:\nsignerAddress: ${signerAddress}\ntypedDataMessage: ${toJson(typedDataMessage)}`); + if (await isUpgradeRequired(network, signerAddress)) { + throw new Error('Upgrade required'); + } const components = []; addDialogTxt(components, 'Message', toJson(typedDataMessage)); diff --git a/packages/starknet-snap/src/types/snapApi.ts b/packages/starknet-snap/src/types/snapApi.ts index 40a19510..027f305e 100644 --- a/packages/starknet-snap/src/types/snapApi.ts +++ b/packages/starknet-snap/src/types/snapApi.ts @@ -102,6 +102,11 @@ export interface SendTransactionRequestParams extends BaseRequestParams { maxFee?: string; } +export interface UpgradeTransactionRequestParams extends BaseRequestParams { + contractAddress: string; + maxFee?: string; +} + export interface GetValueRequestParams extends BaseRequestParams { contractAddress: string; contractFuncName: string; diff --git a/packages/starknet-snap/src/types/snapState.ts b/packages/starknet-snap/src/types/snapState.ts index d563640e..041cfcbf 100644 --- a/packages/starknet-snap/src/types/snapState.ts +++ b/packages/starknet-snap/src/types/snapState.ts @@ -16,6 +16,7 @@ export interface AccContract { derivationPath: string; deployTxnHash: string; // in hex chainId: string; // in hex + upgradeRequired?: boolean; } export interface Erc20Token { diff --git a/packages/starknet-snap/src/upgradeAccContract.ts b/packages/starknet-snap/src/upgradeAccContract.ts new file mode 100644 index 00000000..d9f3bd11 --- /dev/null +++ b/packages/starknet-snap/src/upgradeAccContract.ts @@ -0,0 +1,121 @@ +import { toJson } from './utils/serializer'; +import { num, constants, CallData } from 'starknet'; +import { Transaction, TransactionStatus, VoyagerTransactionType } from './types/snapState'; +import { + getKeysFromAddress, + validateAndParseAddress, + isUpgradeRequired, + executeTxn, + isAccountDeployed, + estimateFee, +} from './utils/starknetUtils'; +import { getNetworkFromChainId, upsertTransaction, getSendTxnText } from './utils/snapUtils'; +import { ApiParams, UpgradeTransactionRequestParams } from './types/snapApi'; +import { ACCOUNT_CLASS_HASH, CAIRO_VERSION_LEGACY } from './utils/constants'; +import { DialogType } from '@metamask/rpc-methods'; +import { heading, panel } from '@metamask/snaps-sdk'; +import { logger } from './utils/logger'; + +export async function upgradeAccContract(params: ApiParams) { + try { + const { state, wallet, saveMutex, keyDeriver, requestParams } = params; + const requestParamsObj = requestParams as UpgradeTransactionRequestParams; + const contractAddress = requestParamsObj.contractAddress; + const chainId = requestParamsObj.chainId; + + if (!contractAddress) { + throw new Error(`The given contract address need to be non-empty string, got: ${toJson(requestParamsObj)}`); + } + try { + validateAndParseAddress(contractAddress); + } catch (err) { + throw new Error(`The given contract address is invalid: ${contractAddress}`); + } + + const network = getNetworkFromChainId(state, chainId); + + if (!(await isAccountDeployed(network, contractAddress))) { + throw new Error('Contract has not deployed'); + } + + if (!(await isUpgradeRequired(network, contractAddress))) { + throw new Error('Upgrade is not required'); + } + + const { privateKey } = await getKeysFromAddress(keyDeriver, network, state, contractAddress); + + const method = 'upgrade'; + + const calldata = CallData.compile({ + implementation: ACCOUNT_CLASS_HASH, + calldata: [0], + }); + + const txnInvocation = { + contractAddress, + entrypoint: method, + calldata, + }; + + let maxFee = requestParamsObj.maxFee ? num.toBigInt(requestParamsObj.maxFee) : constants.ZERO; + if (maxFee === constants.ZERO) { + const estFeeResp = await estimateFee(network, contractAddress, privateKey, txnInvocation, CAIRO_VERSION_LEGACY); + maxFee = num.toBigInt(estFeeResp.suggestedMaxFee.toString(10) ?? '0'); + } + + const dialogComponents = getSendTxnText(state, contractAddress, method, calldata, contractAddress, maxFee, network); + + const response = await wallet.request({ + method: 'snap_dialog', + params: { + type: DialogType.Confirmation, + content: panel([heading('Do you want to sign this transaction ?'), ...dialogComponents]), + }, + }); + + if (!response) return false; + + logger.log(`sendTransaction:\ntxnInvocation: ${toJson(txnInvocation)}\nmaxFee: ${maxFee.toString()}}`); + + const txnResp = await executeTxn( + network, + contractAddress, + privateKey, + txnInvocation, + undefined, + { + maxFee, + }, + CAIRO_VERSION_LEGACY, + ); + + logger.log(`sendTransaction:\ntxnResp: ${toJson(txnResp)}`); + + if (!txnResp?.transaction_hash) { + throw new Error(`Transaction hash is not found`); + } + + const txn: Transaction = { + txnHash: txnResp.transaction_hash, + txnType: VoyagerTransactionType.INVOKE, + chainId: network.chainId, + senderAddress: contractAddress, + contractAddress, + contractFuncName: 'upgrade', + contractCallData: CallData.compile(calldata), + finalityStatus: TransactionStatus.RECEIVED, + executionStatus: TransactionStatus.RECEIVED, + status: '', //DEPRECATED LATER + failureReason: '', + eventIds: [], + timestamp: Math.floor(Date.now() / 1000), + }; + + await upsertTransaction(txn, wallet, saveMutex); + + return txnResp; + } catch (err) { + logger.error(`Problem found: ${err}`); + throw err; + } +} diff --git a/packages/starknet-snap/src/utils/constants.ts b/packages/starknet-snap/src/utils/constants.ts index 866a5de9..a365bbe3 100644 --- a/packages/starknet-snap/src/utils/constants.ts +++ b/packages/starknet-snap/src/utils/constants.ts @@ -12,8 +12,10 @@ export const MAXIMUM_TOKEN_NAME_LENGTH = 64; export const MAXIMUM_TOKEN_SYMBOL_LENGTH = 16; export const TRANSFER_SELECTOR_HEX = '0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e'; +export const UPGRADE_SELECTOR_HEX = '0xf2f7c15cbe06c8d94597cd91fd7f3369eae842359235712def5584f8d270cd'; -export const ACCOUNT_CLASS_HASH_V0 = '0x033434ad846cdd5f23eb73ff09fe6fddd568284a0fb7d1be20ee482f044dabe2'; // from argent-x repo +export const ACCOUNT_CLASS_HASH_LEGACY = '0x033434ad846cdd5f23eb73ff09fe6fddd568284a0fb7d1be20ee482f044dabe2'; // from argent-x repo +export const ACCOUNT_CLASS_HASH = '0x29927c8af6bccf3f6fda035981e765a7bdbf18a2dc0d630494f8758aa908e2b'; // from argent-x repo interface IDappConfig { dev: string; @@ -33,7 +35,7 @@ export const STARKNET_MAINNET_NETWORK: Network = { baseUrl: 'https://alpha-mainnet.starknet.io', nodeUrl: 'https://starknet-mainnet.infura.io/v3/60c7253fb48147658095fe0460ac9ee9', voyagerUrl: 'https://voyager.online', - accountClassHash: '', // from argent-x repo + accountClassHash: '', }; // Keep this constants for unit test @@ -70,7 +72,7 @@ export const STARKNET_INTEGRATION_NETWORK: Network = { baseUrl: 'https://external.integration.starknet.io', nodeUrl: '', voyagerUrl: '', - accountClassHash: '', // from argent-x repo + accountClassHash: '', }; export const ETHER_MAINNET: Erc20Token = { @@ -178,4 +180,10 @@ export const PRELOADED_NETWORKS = [ STARKNET_INTEGRATION_NETWORK, ]; -export const PROXY_CONTRACT_HASH = '0x25ec026985a3bf9d0cc1fe17326b245dfdc3ff89b8fde106542a3ea56c5a918'; // from argent-x repo +export const PROXY_CONTRACT_HASH = '0x25ec026985a3bf9d0cc1fe17326b245dfdc3ff89b8fde106542a3ea56c5a918'; // for cairo 0 proxy contract + +export const MIN_ACC_CONTRACT_VERSION = [0, 3, 0]; + +export const CAIRO_VERSION = '1'; + +export const CAIRO_VERSION_LEGACY = '0'; diff --git a/packages/starknet-snap/src/utils/formatterUtils.ts b/packages/starknet-snap/src/utils/formatterUtils.ts new file mode 100644 index 00000000..de0aabb8 --- /dev/null +++ b/packages/starknet-snap/src/utils/formatterUtils.ts @@ -0,0 +1,9 @@ +export const hexToString = (hex) => { + let str = ''; + for (let i = 0; i < hex.length; i += 2) { + const hexValue = hex.substr(i, 2); + const decimalValue = parseInt(hexValue, 16); + str += String.fromCharCode(decimalValue); + } + return str; +}; diff --git a/packages/starknet-snap/src/utils/snapUtils.ts b/packages/starknet-snap/src/utils/snapUtils.ts index ff873d1d..f0c63ae5 100644 --- a/packages/starknet-snap/src/utils/snapUtils.ts +++ b/packages/starknet-snap/src/utils/snapUtils.ts @@ -369,6 +369,7 @@ export async function upsertAccount(userAccount: AccContract, wallet, mutex: Mut storedAccount.derivationPath = userAccount.derivationPath; storedAccount.publicKey = userAccount.publicKey; storedAccount.deployTxnHash = userAccount.deployTxnHash || storedAccount.deployTxnHash; + storedAccount.upgradeRequired = userAccount.upgradeRequired; } await wallet.request({ diff --git a/packages/starknet-snap/src/utils/starknetUtils.ts b/packages/starknet-snap/src/utils/starknetUtils.ts index 920f9aeb..c1d2abf9 100644 --- a/packages/starknet-snap/src/utils/starknetUtils.ts +++ b/packages/starknet-snap/src/utils/starknetUtils.ts @@ -31,13 +31,22 @@ import { UniversalDetails, DeclareSignerDetails, DeployAccountSignerDetails, + CairoVersion, InvocationsSignerDetails, ProviderInterface, - CairoVersion, GetTransactionReceiptResponse, } from 'starknet'; import { Network, SnapState, Transaction, TransactionType } from '../types/snapState'; -import { ACCOUNT_CLASS_HASH_V0, PROXY_CONTRACT_HASH, TRANSFER_SELECTOR_HEX } from './constants'; +import { + PROXY_CONTRACT_HASH, + TRANSFER_SELECTOR_HEX, + UPGRADE_SELECTOR_HEX, + MIN_ACC_CONTRACT_VERSION, + ACCOUNT_CLASS_HASH_LEGACY, + ACCOUNT_CLASS_HASH, + CAIRO_VERSION, + CAIRO_VERSION_LEGACY, +} from './constants'; import { getAddressKey } from './keyPair'; import { getAccount, @@ -49,6 +58,7 @@ import { } from './snapUtils'; import { logger } from './logger'; import { RpcV4GetTransactionReceiptResponse } from '../types/snapApi'; +import { hexToString } from './formatterUtils'; export const getCallDataArray = (callDataStr: string): string[] => { return (callDataStr ?? '') @@ -65,6 +75,16 @@ export const getProvider = (network: Network): ProviderInterface => { return new Provider(providerParam); }; +export const getAccountInstance = ( + network: Network, + userAddress: string, + privateKey: string | Uint8Array, + cairoVersion?: CairoVersion, +): Account => { + const provider = getProvider(network); + return new Account(provider, userAddress, privateKey, cairoVersion ?? CAIRO_VERSION); +}; + export const callContract = async ( network: Network, contractAddress: string, @@ -82,16 +102,29 @@ export const callContract = async ( ); }; +export const waitForTransaction = async ( + network: Network, + senderAddress: string, + privateKey: string | Uint8Array, + txnHash: num.BigNumberish, + cairoVersion?: CairoVersion, +): Promise => { + return getAccountInstance(network, senderAddress, privateKey, cairoVersion).waitForTransaction(txnHash); +}; + export const declareContract = async ( network: Network, senderAddress: string, privateKey: string | Uint8Array, contractPayload: DeclareContractPayload, invocationsDetails?: UniversalDetails, + cairoVersion?: CairoVersion, ): Promise => { - const provider = getProvider(network); - const account = new Account(provider, senderAddress, privateKey, '0'); - return account.declare(contractPayload, { ...invocationsDetails, skipValidate: false, blockIdentifier: 'latest' }); + return getAccountInstance(network, senderAddress, privateKey, cairoVersion).declare(contractPayload, { + ...invocationsDetails, + skipValidate: false, + blockIdentifier: 'latest', + }); }; export const estimateFee = async ( @@ -99,39 +132,25 @@ export const estimateFee = async ( senderAddress: string, privateKey: string | Uint8Array, txnInvocation: Call | Call[], + cairoVersion?: CairoVersion, invocationsDetails?: UniversalDetails, ): Promise => { - const provider = getProvider(network); - const account = new Account(provider, senderAddress, privateKey, '0'); - return account.estimateInvokeFee(txnInvocation, { + return getAccountInstance(network, senderAddress, privateKey, cairoVersion).estimateInvokeFee(txnInvocation, { ...invocationsDetails, skipValidate: false, blockIdentifier: 'latest', }); }; -export const waitForTransaction = async ( - network: Network, - senderAddress: string, - privateKey: string | Uint8Array, - txnHash: num.BigNumberish, - cairoVersion?: CairoVersion, -): Promise => { - const provider = getProvider(network); - const account = new Account(provider, senderAddress, privateKey, cairoVersion ?? '0'); - return account.waitForTransaction(txnHash); -}; - export const estimateFeeBulk = async ( network: Network, senderAddress: string, privateKey: string | Uint8Array, txnInvocation: Invocations, invocationsDetails?: UniversalDetails, + cairoVersion?: CairoVersion, ): Promise => { - const provider = getProvider(network); - const account = new Account(provider, senderAddress, privateKey, '0'); - return account.estimateFeeBulk(txnInvocation, { + return getAccountInstance(network, senderAddress, privateKey, cairoVersion).estimateFeeBulk(txnInvocation, { ...invocationsDetails, skipValidate: false, blockIdentifier: 'latest', @@ -145,10 +164,9 @@ export const executeTxn = async ( txnInvocation: Call | Call[], abis?: Abi[], invocationsDetails?: UniversalDetails, + cairoVersion?: CairoVersion, ): Promise => { - const provider = getProvider(network); - const account = new Account(provider, senderAddress, privateKey, '0'); - return account.execute(txnInvocation, abis, { + return getAccountInstance(network, senderAddress, privateKey, cairoVersion).execute(txnInvocation, abis, { ...invocationsDetails, skipValidate: false, blockIdentifier: 'latest', @@ -161,17 +179,16 @@ export const deployAccount = async ( contractCallData: RawCalldata, addressSalt: num.BigNumberish, privateKey: string | Uint8Array, + cairoVersion?: CairoVersion, invocationsDetails?: UniversalDetails, ): Promise => { - const provider = getProvider(network); - const account = new Account(provider, contractAddress, privateKey, '0'); const deployAccountPayload = { - classHash: PROXY_CONTRACT_HASH, + classHash: ACCOUNT_CLASS_HASH, contractAddress: contractAddress, constructorCalldata: contractCallData, addressSalt, }; - return account.deployAccount(deployAccountPayload, { + return getAccountInstance(network, contractAddress, privateKey, cairoVersion).deployAccount(deployAccountPayload, { ...invocationsDetails, skipValidate: false, blockIdentifier: 'latest', @@ -184,21 +201,23 @@ export const estimateAccountDeployFee = async ( contractCallData: RawCalldata, addressSalt: num.BigNumberish, privateKey: string | Uint8Array, + cairoVersion?: CairoVersion, invocationsDetails?: UniversalDetails, ): Promise => { - const provider = getProvider(network); - const account = new Account(provider, contractAddress, privateKey, '0'); const deployAccountPayload = { - classHash: PROXY_CONTRACT_HASH, + classHash: ACCOUNT_CLASS_HASH, contractAddress: contractAddress, constructorCalldata: contractCallData, addressSalt, }; - return account.estimateAccountDeployFee(deployAccountPayload, { - ...invocationsDetails, - skipValidate: false, - blockIdentifier: 'latest', - }); + return getAccountInstance(network, contractAddress, privateKey, cairoVersion).estimateAccountDeployFee( + deployAccountPayload, + { + ...invocationsDetails, + skipValidate: false, + blockIdentifier: 'latest', + }, + ); }; export const getSigner = async (userAccAddress: string, network: Network): Promise => { @@ -206,6 +225,29 @@ export const getSigner = async (userAccAddress: string, network: Network): Promi return resp[0]; }; +export const getVersion = async (userAccAddress: string, network: Network): Promise => { + const resp = await callContract(network, userAccAddress, 'getVersion'); + return resp[0]; +}; + +export const getOwner = async (userAccAddress: string, network: Network): Promise => { + const resp = await callContract(network, userAccAddress, 'get_owner'); + return resp[0]; +}; + +export const getContractOwner = async ( + userAccAddress: string, + network: Network, + version: CairoVersion, +): Promise => { + return version === '0' ? getSigner(userAccAddress, network) : getOwner(userAccAddress, network); +}; + +export const getBalance = async (address: string, tokenAddress: string, network: Network) => { + const resp = await callContract(network, tokenAddress, 'balanceOf', [num.toBigInt(address).toString(10)]); + return resp[0]; +}; + export const getTransactionStatus = async (transactionHash: num.BigNumberish, network: Network) => { const provider = getProvider(network); const receipt = (await provider.getTransactionReceipt(transactionHash)) as RpcV4GetTransactionReceiptResponse; @@ -320,8 +362,11 @@ export const getMassagedTransactions = async ( ); const bigIntTransferSelectorHex = num.toBigInt(TRANSFER_SELECTOR_HEX); + const bigIntUpgradeSelectorHex = num.toBigInt(UPGRADE_SELECTOR_HEX); let massagedTxns = await Promise.all( txns.map(async (txn) => { + logger.log(`getMassagedTransactions: txn:\n${toJson(txn)}`); + let txnResp: GetTransactionResponse; let statusResp; try { @@ -343,12 +388,20 @@ export const getMassagedTransactions = async ( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore contractAddress: txnResp.calldata?.[1] || txnResp.contract_address || txn.contract_address || '', + + contractFuncName: + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + num.toBigInt(txnResp.calldata?.[2] || '') === bigIntTransferSelectorHex + ? 'transfer' + : // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + num.toBigInt(txnResp.calldata?.[2] || '') === bigIntUpgradeSelectorHex + ? 'upgrade' + : txn.operations ?? '', // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - contractFuncName: num.toBigInt(txnResp.calldata?.[2] || '') === bigIntTransferSelectorHex ? 'transfer' : '', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - contractCallData: txnResp.calldata?.slice(6, txnResp.calldata?.length - 1) || [], + contractCallData: txnResp.calldata || [], timestamp: txn.timestamp, status: '', //DEPRECATION finalityStatus: statusResp.finalityStatus || '', @@ -369,6 +422,7 @@ export const getMassagedTransactions = async ( massagedTxns = massagedTxns.filter( (massagedTxn) => num.toBigInt(massagedTxn.contractAddress) === bigIntContractAddress || + massagedTxn.contractFuncName === 'upgrade' || deployTxns.find((deployTxn) => deployTxn.hash === massagedTxn.txnHash), ); } @@ -442,9 +496,38 @@ export const getNextAddressIndex = (chainId: string, state: SnapState, derivatio return uninitializedAccount?.addressIndex ?? accounts.length; }; +/** + * calculate contract address by publicKey + * + * @param publicKey - address's publicKey. + * @returns - address and calldata. + */ export const getAccContractAddressAndCallData = (publicKey) => { const callData = CallData.compile({ - implementation: ACCOUNT_CLASS_HASH_V0, + signer: publicKey, + guardian: '0', + }); + + let address = hash.calculateContractAddressFromHash(publicKey, ACCOUNT_CLASS_HASH, callData, 0); + + if (address.length < 66) { + address = address.replace('0x', '0x' + '0'.repeat(66 - address.length)); + } + return { + address, + callData, + }; +}; + +/** + * calculate contract address by publicKey + * + * @param publicKey - address's publicKey. + * @returns - address and calldata. + */ +export const getAccContractAddressAndCallDataLegacy = (publicKey) => { + const callData = CallData.compile({ + implementation: ACCOUNT_CLASS_HASH_LEGACY, selector: hash.getSelectorFromName('initialize'), calldata: CallData.compile({ signer: publicKey, guardian: '0' }), }); @@ -471,23 +554,10 @@ export const getKeysFromAddress = async ( addressIndex = acc.addressIndex; logger.log(`getNextAddressIndex:\nFound address in state: ${addressIndex} ${address}`); } else { - const bigIntAddress = num.toBigInt(address); - for (let i = 0; i < maxScan; i++) { - const { publicKey } = await getKeysFromAddressIndex(keyDeriver, network.chainId, state, i); - const { address: calculatedAddress } = getAccContractAddressAndCallData(publicKey); - if (num.toBigInt(calculatedAddress) === bigIntAddress) { - addressIndex = i; - logger.log(`getNextAddressIndex:\nFound address in scan: ${addressIndex} ${address}`); - break; - } - } + const result = await findAddressIndex(network.chainId, address, keyDeriver, state, maxScan); + addressIndex = result.index; } - - if (!isNaN(addressIndex)) { - return getKeysFromAddressIndex(keyDeriver, network.chainId, state, addressIndex); - } - logger.log(`getNextAddressIndex:\nAddress not found: ${address}`); - throw new Error(`Address not found: ${address}`); + return getKeysFromAddressIndex(keyDeriver, network.chainId, state, addressIndex); }; export const getKeysFromAddressIndex = async ( @@ -513,16 +583,23 @@ export const getKeysFromAddressIndex = async ( }; }; -export const isAccountDeployed = async (network: Network, publicKey: string) => { - let accountDeployed = true; +/** + * Check address is deployed by using getVersion + * + * @param network - Network. + * @param address - Input address. + * @returns - boolean. + */ +export const isAccountDeployed = async (network: Network, address: string) => { try { - const { address: signerContractAddress } = getAccContractAddressAndCallData(publicKey); - await getSigner(signerContractAddress, network); + await getVersion(address, network); + return true; } catch (err) { - accountDeployed = false; + if (!err.message.includes('Contract not found')) { + throw err; + } + return false; } - logger.log(`isAccountDeployed: ${accountDeployed}`); - return accountDeployed; }; export const addFeesFromAllTransactions = (fees: EstimateFee[]): Partial => { @@ -548,6 +625,143 @@ export const validateAndParseAddress = (address: num.BigNumberish, length = 63) return _validateAndParseAddressFn(address); }; +/** + * Find address index from the keyDeriver + * + * @param chainId - Network ChainId. + * @param address - Input address. + * @param keyDeriver - keyDeriver from MetaMask wallet. + * @param state - MetaMask Snap state. + * @param maxScan - Number of scaning in the keyDeriver. + * @returns - address index and cairoVersion. + */ +export const findAddressIndex = async ( + chainId: string, + address: string, + keyDeriver, + state: SnapState, + maxScan = 20, +) => { + const bigIntAddress = num.toBigInt(address); + for (let i = 0; i < maxScan; i++) { + const { publicKey } = await getKeysFromAddressIndex(keyDeriver, chainId, state, i); + const { address: calculatedAddress, addressLegacy: calculatedAddressLegacy } = getPermutationAddresses(publicKey); + + if (num.toBigInt(calculatedAddress) === bigIntAddress || num.toBigInt(calculatedAddressLegacy) === bigIntAddress) { + logger.log(`findAddressIndex:\nFound address in scan: ${i} ${address}`); + return { + index: i, + cairoVersion: num.toBigInt(calculatedAddress) === bigIntAddress ? 1 : 0, + }; + } + } + throw new Error(`Address not found: ${address}`); +}; + +/** + * Get address permutation by public key + * + * @param pk - Public key. + * @returns - address and addressLegacy. + */ +export const getPermutationAddresses = (pk: string) => { + const { address } = getAccContractAddressAndCallData(pk); + const { address: addressLegacy } = getAccContractAddressAndCallDataLegacy(pk); + + return { + address, + addressLegacy, + }; +}; + +/** + * Check address needed upgrade by using getVersion and compare with MIN_ACC_CONTRACT_VERSION + * + * @param network - Network. + * @param address - Input address. + * @returns - boolean. + */ +export const isUpgradeRequired = async (network: Network, address: string) => { + try { + logger.log(`isUpgradeRequired: address = ${address}`); + const hexResp = await getVersion(address, network); + return isGTEMinVersion(hexToString(hexResp)) ? false : true; + } catch (err) { + if (!err.message.includes('Contract not found')) { + throw err; + } + //[TODO] if address is cairo0 but not deployed we throw error + return false; + } +}; + +/** + * Compare version number with MIN_ACC_CONTRACT_VERSION + * + * @param version - version, e.g (2.3.0). + * @returns - boolean. + */ +export const isGTEMinVersion = (version: string) => { + logger.log(`isGTEMinVersion: version = ${version}`); + const versionArr = version.split('.'); + return Number(versionArr[1]) >= MIN_ACC_CONTRACT_VERSION[1]; +}; + +/** + * Get user address by public key, return address if the address has deployed + * + * @param network - Network. + * @param publicKey - address's public key. + * @returns - address and address's public key. + */ +export const getCorrectContractAddress = async (network: Network, publicKey: string) => { + const { address: contractAddress, addressLegacy: contractAddressLegacy } = getPermutationAddresses(publicKey); + + logger.log( + `getContractAddressByKey: contractAddress = ${contractAddress}\ncontractAddressLegacy = ${contractAddressLegacy}\npublicKey = ${publicKey}`, + ); + + let address = contractAddress; + let upgradeRequired = false; + let pk = ''; + + try { + await getVersion(contractAddress, network); + pk = await getContractOwner(address, network, CAIRO_VERSION); + } catch (e) { + if (!e.message.includes('Contract not found')) { + throw e; + } + + logger.log( + `getContractAddressByKey: cairo ${CAIRO_VERSION} contract cant found, try cairo ${CAIRO_VERSION_LEGACY}`, + ); + + try { + const version = await getVersion(contractAddressLegacy, network); + upgradeRequired = isGTEMinVersion(hexToString(version)) ? false : true; + pk = await getContractOwner( + contractAddressLegacy, + network, + upgradeRequired ? CAIRO_VERSION_LEGACY : CAIRO_VERSION, + ); + address = contractAddressLegacy; + } catch (e) { + if (!e.message.includes('Contract not found')) { + throw e; + } + + logger.log(`getContractAddressByKey: no deployed contract found, fallback to cairo ${CAIRO_VERSION}`); + } + } + + return { + address, + signerPubKey: pk, + upgradeRequired: upgradeRequired, + }; +}; + export const signTransactions = async ( privateKey: string, transactions: Call[], diff --git a/packages/starknet-snap/src/verifySignedMessage.ts b/packages/starknet-snap/src/verifySignedMessage.ts index e3475d6a..8917852e 100644 --- a/packages/starknet-snap/src/verifySignedMessage.ts +++ b/packages/starknet-snap/src/verifySignedMessage.ts @@ -4,6 +4,7 @@ import { verifyTypedDataMessageSignature, getFullPublicKeyPairFromPrivateKey, getKeysFromAddress, + isUpgradeRequired, } from './utils/starknetUtils'; import { getNetworkFromChainId } from './utils/snapUtils'; import { ApiParams, VerifySignedMessageRequestParams } from './types/snapApi'; @@ -14,19 +15,6 @@ export async function verifySignedMessage(params: ApiParams) { try { const { state, keyDeriver, requestParams } = params; const requestParamsObj = requestParams as VerifySignedMessageRequestParams; - - if (!requestParamsObj.signerAddress || !requestParamsObj.signature) { - throw new Error( - `The given signer address and signature need to be non-empty string, got: ${toJson(requestParamsObj)}`, - ); - } - - try { - validateAndParseAddress(requestParamsObj.signerAddress); - } catch (err) { - throw new Error(`The given signer address is invalid: ${requestParamsObj.signerAddress}`); - } - const verifySignerAddress = requestParamsObj.signerAddress; const verifySignature = requestParamsObj.signature; const verifyTypedDataMessage = requestParamsObj.typedDataMessage @@ -40,6 +28,22 @@ export async function verifySignedMessage(params: ApiParams) { )}`, ); + if (!verifySignerAddress || !verifySignature) { + throw new Error( + `The given signer address and signature need to be non-empty string, got: ${toJson(requestParamsObj)}`, + ); + } + + try { + validateAndParseAddress(verifySignerAddress); + } catch (err) { + throw new Error(`The given signer address is invalid: ${verifySignerAddress}`); + } + + if (await isUpgradeRequired(network, verifySignerAddress)) { + throw new Error('Upgrade required'); + } + const { privateKey: signerPrivateKey } = await getKeysFromAddress(keyDeriver, network, state, verifySignerAddress); const fullPublicKey = getFullPublicKeyPairFromPrivateKey(signerPrivateKey); diff --git a/packages/starknet-snap/test/constants.test.ts b/packages/starknet-snap/test/constants.test.ts index a5cdf30e..95c8e1f0 100644 --- a/packages/starknet-snap/test/constants.test.ts +++ b/packages/starknet-snap/test/constants.test.ts @@ -1,5 +1,5 @@ import { JsonBIP44CoinTypeNode } from '@metamask/key-tree'; -import { EstimateFee, GetTransactionResponse, num } from 'starknet'; +import { EstimateFee, GetTransactionResponse, constants, num } from 'starknet'; import { AccContract, Erc20Token, @@ -60,6 +60,16 @@ export const account4: AccContract = { chainId: STARKNET_SEPOLIA_TESTNET_NETWORK.chainId, }; +export const Cairo1Account1: AccContract = { + address: '0x0404d766fd6db2c23177e5ea289af99e81e5c4a7badae588950ad0f8572c49b9', + addressSalt: '0x019e59f349e1aa813ab4556c5836d0472e5e1ae82d1e5c3b3e8aabfeb290befd', + addressIndex: 1, + derivationPath: "m / bip32:44' / bip32:9004' / bip32:0' / bip32:0", + deployTxnHash: '0x5bc00132b8f2fc0f673dc232594b26727e712b204a2716f9dc28a8c5f607b5e', + publicKey: '0x019e59f349e1aa813ab4556c5836d0472e5e1ae82d1e5c3b3e8aabfeb290befd', + chainId: constants.StarknetChainId.SN_GOERLI, +}; + export const token1: Erc20Token = { address: '0x244c20d51109adcf604fde1bbf878e5dcd549b3877ac87911ec6a158bd7aa62', name: 'Starknet ERC-20 sample', @@ -87,15 +97,19 @@ export const token3: Erc20Token = { export const signature1 = '3044022001bbc0696d02f85696608c9029973d7d5cf714be2a8188424578c40016262fff022004e388edeb3ceb1fd023b165c9a91cc39b97d58f77beb53b6b90ee9261d9f90c'; export const signature2 = - '304402200510bd78f928984364253c04201185ab6ccc386278c8fe1aeda0deab7a476e3f02200442916a82f917f520071da038d6dc3eb4446824ce26893355ad4c4a9343729c'; + '30440220052956ac852275b6004c4e8042450f6dce83059f068029b037cc47338c80d062022002bc0e712f03e341bb3532fc356b779d84fcb4dbfe8ed34de2db66e121971d92'; +export const signature4Cairo1SignMessage = [ + '1011319195017091294626264310379704228916787561535736421454347559326036897966', + '953080452563745534645084375499931001089185216376442413556466035688849743177', +]; export const signature4SignMessage = [ '784041227270069705374122994163964526105670242785431143890307285886848872447', '2211270729821731368290303126976610283184761443640531855459727543936510195980', ]; export const signature4SignMessageWithUnfoundAddress = [ - '2291141487445389914420608491513144291926464341525100982346025850159785012799', - '1926866925376139342818732291555402078512683709189543755386120411261784257180', + '1011319195017091294626264310379704228916787561535736421454347559326036897966', + '953080452563745534645084375499931001089185216376442413556466035688849743177', ]; export const signature3 = [ @@ -240,7 +254,18 @@ export const RejectedTxn2: Transaction = { export const unsettedTransactionInMassagedTxn: Transaction = { chainId: STARKNET_SEPOLIA_TESTNET_NETWORK.chainId, contractAddress: '0x7394cbe418daa16e42b87ba67372d4ab4a5df0b05c6e554d158458ce245bc10', - contractCallData: ['0x14361d05e560796ad3152e083b609f5205f3bd76039327326746ba7f769a666', '0xde0b6b3a7640000', '0x0'], + contractCallData: [ + '0x1', + '0x7394cbe418daa16e42b87ba67372d4ab4a5df0b05c6e554d158458ce245bc10', + '0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e', + '0x0', + '0x3', + '0x3', + '0x14361d05e560796ad3152e083b609f5205f3bd76039327326746ba7f769a666', + '0xde0b6b3a7640000', + '0x0', + '0x1', + ], contractFuncName: 'transfer', senderAddress: '0x5a98ec74a40383cf99896bfea2ec5e6aad16c7eed50025a5f569d585ebb13a2', timestamp: 1655109666, @@ -375,9 +400,9 @@ export const estimateFeeResp2 = { suggestedMaxFee: num.toBigInt('0x14a5d6744ed9'), } as EstimateFee; -export const unfoundUserAddress = '0x018dfa1955a0154524203f81c5668d6a78c708375ee8908dcb55a49c6ec87190'; -export const unfoundUserPrivateKey = '0x610d87a5c02459f8643f9ad6a9bc70597d1a8a0ab4d645346b7eadc5266ad4d'; -export const unfoundUserPublicKey = '0x4b36a2b0a1e9d2af3416914798de776e37d9e0ab9a50d2dec30485dca64bb8'; +export const unfoundUserAddress = '0x0404d766fd6db2c23177e5ea289af99e81e5c4a7badae588950ad0f8572c49b9'; +export const unfoundUserPrivateKey = '0x38d36fc25592257d913d143d37e12533dba9f6721db6fa954ed513b0dc3d68b'; +export const unfoundUserPublicKey = '0x154c7b20442ee954f50831702ca844ec185ad484c21719575d351583deec90b'; export const foundUserPrivateKey = '0x3cddbb7f3694ce84bd9598820834015d979d78e63474a5b00e59b41b0563f4e'; export const testnetPublicKeys = [ @@ -716,7 +741,18 @@ export const expectedMassagedTxns: Transaction[] = [ senderAddress: '0x5a98ec74a40383cf99896bfea2ec5e6aad16c7eed50025a5f569d585ebb13a2', contractAddress: '0x7394cbe418daa16e42b87ba67372d4ab4a5df0b05c6e554d158458ce245bc10', contractFuncName: 'transfer', - contractCallData: ['0x14361d05e560796ad3152e083b609f5205f3bd76039327326746ba7f769a666', '0xde0b6b3a7640000', '0x0'], + contractCallData: [ + '0x1', + '0x7394cbe418daa16e42b87ba67372d4ab4a5df0b05c6e554d158458ce245bc10', + '0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e', + '0x0', + '0x3', + '0x3', + '0x14361d05e560796ad3152e083b609f5205f3bd76039327326746ba7f769a666', + '0xde0b6b3a7640000', + '0x0', + '0x1', + ], timestamp: 1655109666, status: '', finalityStatus: 'ACCEPTED_ON_L1', diff --git a/packages/starknet-snap/test/src/createAccount.test.ts b/packages/starknet-snap/test/src/createAccount.test.ts index 2680dc5a..fa8995b9 100644 --- a/packages/starknet-snap/test/src/createAccount.test.ts +++ b/packages/starknet-snap/test/src/createAccount.test.ts @@ -17,7 +17,6 @@ import { estimateDeployFeeResp, getBalanceResp, account1, - account2, estimateDeployFeeResp2, estimateDeployFeeResp3, } from '../constants.test'; @@ -33,7 +32,7 @@ describe('Test function: createAccount', function () { this.timeout(10000); const walletStub = new WalletMock(); let waitForTransactionStub; - const state: SnapState = { + let state: SnapState = { accContracts: [], erc20Tokens: [], networks: [STARKNET_MAINNET_NETWORK, STARKNET_SEPOLIA_TESTNET_NETWORK], @@ -59,10 +58,16 @@ describe('Test function: createAccount', function () { afterEach(function () { walletStub.reset(); sandbox.restore(); + state = { + accContracts: [], + erc20Tokens: [], + networks: [STARKNET_MAINNET_NETWORK, STARKNET_SEPOLIA_TESTNET_NETWORK], + transactions: [], + }; }); it('should only return derived address without sending deploy txn correctly in mainnet if deploy is false', async function () { - sandbox.stub(utils, 'getSigner').throws(new Error()); + sandbox.stub(utils, 'isAccountDeployed').resolves(false); const requestObject: CreateAccountRequestParams = { chainId: STARKNET_MAINNET_NETWORK.chainId, }; @@ -106,9 +111,9 @@ describe('Test function: createAccount', function () { sandbox.stub(utils, 'deployAccount').callsFake(async () => { return createAccountProxyMainnetResp; }); - sandbox.stub(utils, 'getSigner').throws(new Error()); - sandbox.stub(utils, 'callContract').callsFake(async () => { - return getBalanceResp; + sandbox.stub(utils, 'isAccountDeployed').resolves(false); + sandbox.stub(utils, 'getBalance').callsFake(async () => { + return getBalanceResp[0]; }); sandbox.stub(utils, 'estimateAccountDeployFee').callsFake(async () => { return estimateDeployFeeResp; @@ -136,12 +141,14 @@ describe('Test function: createAccount', function () { }); it('should create and store an user account of specific address index with proxy in state correctly in mainnet', async function () { + state.accContracts.push(account1); + state.transactions.push(createAccountProxyTxn); sandbox.stub(utils, 'deployAccount').callsFake(async () => { return createAccountProxyMainnetResp2; }); - sandbox.stub(utils, 'getSigner').throws(new Error()); - sandbox.stub(utils, 'callContract').callsFake(async () => { - return getBalanceResp; + sandbox.stub(utils, 'isAccountDeployed').resolves(false); + sandbox.stub(utils, 'getBalance').callsFake(async () => { + return getBalanceResp[0]; }); sandbox.stub(utils, 'estimateAccountDeployFee').callsFake(async () => { return estimateDeployFeeResp; @@ -174,9 +181,9 @@ describe('Test function: createAccount', function () { sandbox.stub(utils, 'deployAccount').callsFake(async () => { return createAccountProxyResp; }); - sandbox.stub(utils, 'getSigner').throws(new Error()); - sandbox.stub(utils, 'callContract').callsFake(async () => { - return getBalanceResp; + sandbox.stub(utils, 'isAccountDeployed').resolves(false); + sandbox.stub(utils, 'getBalance').callsFake(async () => { + return getBalanceResp[0]; }); sandbox.stub(utils, 'estimateAccountDeployFee').callsFake(async () => { return estimateDeployFeeResp; @@ -193,34 +200,39 @@ describe('Test function: createAccount', function () { expect(walletStub.rpcStubs.snap_manageState).to.have.been.callCount(4); expect(result.address).to.be.eq(createAccountProxyResp.contract_address); expect(result.transaction_hash).to.be.eq(createAccountProxyResp.transaction_hash); - expect(state.accContracts.length).to.be.eq(3); - expect(state.accContracts[2].address).to.be.eq(createAccountProxyResp.contract_address); - expect(state.accContracts[2].deployTxnHash).to.be.eq(createAccountProxyResp.transaction_hash); - expect(state.accContracts[2].publicKey).to.be.eq(expectedPublicKey); - expect(state.accContracts[2].addressSalt).to.be.eq(expectedPublicKey); - expect(state.transactions.length).to.be.eq(3); + expect(state.accContracts.length).to.be.eq(1); + expect(state.accContracts[0].address).to.be.eq(createAccountProxyResp.contract_address); + expect(state.accContracts[0].deployTxnHash).to.be.eq(createAccountProxyResp.transaction_hash); + expect(state.accContracts[0].publicKey).to.be.eq(expectedPublicKey); + expect(state.accContracts[0].addressSalt).to.be.eq(expectedPublicKey); + expect(state.transactions.length).to.be.eq(1); }); it('should not create any user account with proxy in state in SN_SEPOLIA if not in silentMode and user rejected', async function () { + sandbox.stub(utils, 'getAccContractAddressAndCallData').callsFake(() => { + return { + address: account1.address, + callData: [], + }; + }); walletStub.rpcStubs.snap_dialog.resolves(false); const requestObject: CreateAccountRequestParams = { deploy: true }; apiParams.requestParams = requestObject; + const result = await createAccount(apiParams); expect(walletStub.rpcStubs.snap_manageState).to.have.been.callCount(0); - expect(result.address).to.be.eq(account2.address); - expect(state.accContracts.length).to.be.eq(3); - expect(state.transactions.length).to.be.eq(3); + expect(result.address).to.be.eq(account1.address); + expect(state.accContracts.length).to.be.eq(0); + expect(state.transactions.length).to.be.eq(0); }); - it('should not create any user account with proxy in state in SN_SEPOLIA if account already initialized with a signer', async function () { + it('should not create any user account with proxy in state in SN_SEPOLIA if account already deployed', async function () { sandbox.stub(utils, 'deployAccount').callsFake(async () => { return createAccountProxyResp; }); - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account1.publicKey; - }); - sandbox.stub(utils, 'callContract').callsFake(async () => { - return getBalanceResp; + sandbox.stub(utils, 'isAccountDeployed').resolves(true); + sandbox.stub(utils, 'getBalance').callsFake(async () => { + return getBalanceResp[0]; }); sandbox.stub(utils, 'estimateAccountDeployFee').callsFake(async () => { return estimateDeployFeeResp; @@ -231,17 +243,17 @@ describe('Test function: createAccount', function () { expect(walletStub.rpcStubs.snap_manageState).to.have.been.callCount(4); expect(result.address).to.be.eq(createAccountProxyResp.contract_address); expect(result.transaction_hash).to.be.eq(createAccountProxyResp.transaction_hash); - expect(state.accContracts.length).to.be.eq(3); - expect(state.transactions.length).to.be.eq(3); + expect(state.accContracts.length).to.be.eq(1); + expect(state.transactions.length).to.be.eq(1); }); it('should not create any user account with proxy in state in SN_SEPOLIA if account does not have enough ETH balance', async function () { sandbox.stub(utils, 'deployAccount').callsFake(async () => { return createAccountProxyResp; }); - sandbox.stub(utils, 'getSigner').throws(new Error()); - sandbox.stub(utils, 'callContract').callsFake(async () => { - return getBalanceResp; + sandbox.stub(utils, 'isAccountDeployed').resolves(false); + sandbox.stub(utils, 'getBalance').callsFake(async () => { + return getBalanceResp[0]; }); sandbox.stub(utils, 'estimateAccountDeployFee').callsFake(async () => { return estimateDeployFeeResp2; @@ -249,20 +261,20 @@ describe('Test function: createAccount', function () { const requestObject: CreateAccountRequestParams = { deploy: true }; apiParams.requestParams = requestObject; const result = await createAccount(apiParams); - expect(walletStub.rpcStubs.snap_manageState).to.have.been.callCount(3); + expect(walletStub.rpcStubs.snap_manageState).to.have.been.callCount(4); expect(result.address).to.be.eq(createAccountProxyResp.contract_address); expect(result.transaction_hash).to.be.eq(createAccountProxyResp.transaction_hash); - expect(state.accContracts.length).to.be.eq(3); - expect(state.transactions.length).to.be.eq(3); + expect(state.accContracts.length).to.be.eq(1); + expect(state.transactions.length).to.be.eq(1); }); it('should not create any user account with proxy in state in SN_SEPOLIA if account does not have enough ETH balance for suggestedMaxFee > 0.000001 ETH', async function () { sandbox.stub(utils, 'deployAccount').callsFake(async () => { return createAccountProxyResp; }); - sandbox.stub(utils, 'getSigner').throws(new Error()); - sandbox.stub(utils, 'callContract').callsFake(async () => { - return getBalanceResp; + sandbox.stub(utils, 'isAccountDeployed').resolves(false); + sandbox.stub(utils, 'getBalance').callsFake(async () => { + return getBalanceResp[0]; }); sandbox.stub(utils, 'estimateAccountDeployFee').callsFake(async () => { return estimateDeployFeeResp3; @@ -270,36 +282,37 @@ describe('Test function: createAccount', function () { const requestObject: CreateAccountRequestParams = { deploy: true }; apiParams.requestParams = requestObject; const result = await createAccount(apiParams); - expect(walletStub.rpcStubs.snap_manageState).to.have.been.callCount(3); + expect(walletStub.rpcStubs.snap_manageState).to.have.been.callCount(4); expect(result.address).to.be.eq(createAccountProxyResp.contract_address); expect(result.transaction_hash).to.be.eq(createAccountProxyResp.transaction_hash); - expect(state.accContracts.length).to.be.eq(3); - expect(state.transactions.length).to.be.eq(3); + expect(state.accContracts.length).to.be.eq(1); + expect(state.transactions.length).to.be.eq(1); }); it('should not create any user account with proxy in state in SN_SEPOLIA if get account ETH balance throws error', async function () { sandbox.stub(utils, 'deployAccount').callsFake(async () => { return createAccountProxyResp; }); - sandbox.stub(utils, 'getSigner').throws(new Error()); - sandbox.stub(utils, 'callContract').throws(new Error()); + sandbox.stub(utils, 'isAccountDeployed').resolves(false); + sandbox.stub(utils, 'getBalance').throws(new Error()); sandbox.stub(utils, 'estimateAccountDeployFee').callsFake(async () => { return estimateDeployFeeResp2; }); const requestObject: CreateAccountRequestParams = { deploy: true }; apiParams.requestParams = requestObject; const result = await createAccount(apiParams); - expect(walletStub.rpcStubs.snap_manageState).to.have.been.callCount(3); + expect(walletStub.rpcStubs.snap_manageState).to.have.been.callCount(4); expect(result.address).to.be.eq(createAccountProxyResp.contract_address); expect(result.transaction_hash).to.be.eq(createAccountProxyResp.transaction_hash); - expect(state.accContracts.length).to.be.eq(3); - expect(state.transactions.length).to.be.eq(3); + expect(state.accContracts.length).to.be.eq(1); + expect(state.transactions.length).to.be.eq(1); }); it('should skip upsert account and transaction if deployTxn response code has no transaction_hash in SN_SEPOLIA', async function () { sandbox.stub(utils, 'deployAccount').callsFake(async () => { return createAccountFailedProxyResp; }); + sandbox.stub(utils, 'isAccountDeployed').resolves(false); sandbox.stub(utils, 'callContract').resolves(getBalanceResp); sandbox.stub(utils, 'getSigner').throws(new Error()); sandbox.stub(utils, 'estimateAccountDeployFee').callsFake(async () => { @@ -311,8 +324,8 @@ describe('Test function: createAccount', function () { expect(walletStub.rpcStubs.snap_manageState).to.have.been.callCount(0); expect(result.address).to.be.eq(createAccountFailedProxyResp.contract_address); expect(result.transaction_hash).to.be.eq(createAccountFailedProxyResp.transaction_hash); - expect(state.accContracts.length).to.be.eq(3); - expect(state.transactions.length).to.be.eq(3); + expect(state.accContracts.length).to.be.eq(0); + expect(state.transactions.length).to.be.eq(0); }); it('should throw error if upsertAccount failed', async function () { @@ -320,6 +333,7 @@ describe('Test function: createAccount', function () { sandbox.stub(utils, 'deployAccount').callsFake(async () => { return createAccountProxyResp; }); + sandbox.stub(utils, 'isAccountDeployed').resolves(false); sandbox.stub(utils, 'callContract').resolves(getBalanceResp); sandbox.stub(utils, 'getSigner').throws(new Error()); sandbox.stub(utils, 'estimateAccountDeployFee').callsFake(async () => { @@ -337,4 +351,26 @@ describe('Test function: createAccount', function () { expect(result).to.be.an('Error'); } }); + + it('should throw error if isAccountDeployed failed', async function () { + const isAccountAddressDeployedStub = sandbox.stub(utils, 'isAccountDeployed').throws(new Error()); + const deployAccountStub = sandbox.stub(utils, 'deployAccount'); + const estimateAccountDeployFeeStub = sandbox.stub(utils, 'estimateAccountDeployFee'); + const getBalanceStub = sandbox.stub(utils, 'getBalance'); + const requestObject: CreateAccountRequestParams = { deploy: true }; + apiParams.requestParams = requestObject; + + let result; + try { + await createAccount(apiParams); + } catch (err) { + result = err; + } finally { + expect(isAccountAddressDeployedStub).to.have.been.callCount(1); + expect(deployAccountStub).to.have.been.callCount(0); + expect(estimateAccountDeployFeeStub).to.have.been.callCount(0); + expect(getBalanceStub).to.have.been.callCount(0); + expect(result).to.be.an('Error'); + } + }); }); diff --git a/packages/starknet-snap/test/src/declareContract.test.ts b/packages/starknet-snap/test/src/declareContract.test.ts index 1dfbda95..385e55e1 100644 --- a/packages/starknet-snap/test/src/declareContract.test.ts +++ b/packages/starknet-snap/test/src/declareContract.test.ts @@ -18,7 +18,7 @@ describe('Test function: declareContract', function () { this.timeout(10000); const walletStub = new WalletMock(); const state: SnapState = { - accContracts: [], + accContracts: [account1], erc20Tokens: [], networks: [STARKNET_MAINNET_NETWORK, STARKNET_SEPOLIA_TESTNET_NETWORK], transactions: [], @@ -31,7 +31,7 @@ describe('Test function: declareContract', function () { }; const requestObject: DeclareContractRequestParams = { - chainId: STARKNET_MAINNET_NETWORK.chainId, + chainId: STARKNET_SEPOLIA_TESTNET_NETWORK.chainId, senderAddress: account1.address, contractPayload: { contract: 'TestContract', @@ -63,7 +63,7 @@ describe('Test function: declareContract', function () { const result = await declareContract(apiParams); const { privateKey } = await utils.getKeysFromAddress( apiParams.keyDeriver, - STARKNET_MAINNET_NETWORK, + STARKNET_SEPOLIA_TESTNET_NETWORK, state, account1.address, ); @@ -74,7 +74,7 @@ describe('Test function: declareContract', function () { }); expect(declareContractStub).to.have.been.calledOnce; expect(declareContractStub).to.have.been.calledWith( - STARKNET_MAINNET_NETWORK, + STARKNET_SEPOLIA_TESTNET_NETWORK, account1.address, privateKey, { contract: 'TestContract' }, @@ -86,7 +86,7 @@ describe('Test function: declareContract', function () { const declareContractStub = sandbox.stub(utils, 'declareContract').rejects('error'); const { privateKey } = await utils.getKeysFromAddress( apiParams.keyDeriver, - STARKNET_MAINNET_NETWORK, + STARKNET_SEPOLIA_TESTNET_NETWORK, state, account1.address, ); @@ -99,7 +99,7 @@ describe('Test function: declareContract', function () { expect(result).to.be.an('Error'); expect(declareContractStub).to.have.been.calledOnce; expect(declareContractStub).to.have.been.calledWith( - STARKNET_MAINNET_NETWORK, + STARKNET_SEPOLIA_TESTNET_NETWORK, account1.address, privateKey, { contract: 'TestContract' }, diff --git a/packages/starknet-snap/test/src/estimateFee.test.ts b/packages/starknet-snap/test/src/estimateFee.test.ts index 2d5908d4..a7f49327 100644 --- a/packages/starknet-snap/test/src/estimateFee.test.ts +++ b/packages/starknet-snap/test/src/estimateFee.test.ts @@ -5,18 +5,19 @@ import { WalletMock } from '../wallet.mock.test'; import * as utils from '../../src/utils/starknetUtils'; import { estimateFee } from '../../src/estimateFee'; import { SnapState } from '../../src/types/snapState'; -import { STARKNET_SEPOLIA_TESTNET_NETWORK } from '../../src/utils/constants'; +import { ACCOUNT_CLASS_HASH, STARKNET_SEPOLIA_TESTNET_NETWORK } from '../../src/utils/constants'; import { getAddressKeyDeriver } from '../../src/utils/keyPair'; import { account2, + Cairo1Account1, estimateDeployFeeResp4, estimateFeeResp, - estimateFeeResp2, getBip44EntropyStub, getBalanceResp, } from '../constants.test'; import { Mutex } from 'async-mutex'; import { ApiParams, EstimateFeeRequestParams } from '../../src/types/snapApi'; +import { TransactionType } from 'starknet'; chai.use(sinonChai); const sandbox = sinon.createSandbox(); @@ -25,7 +26,7 @@ describe('Test function: estimateFee', function () { const walletStub = new WalletMock(); const state: SnapState = { - accContracts: [account2], + accContracts: [], erc20Tokens: [], networks: [STARKNET_SEPOLIA_TESTNET_NETWORK], transactions: [], @@ -54,122 +55,177 @@ describe('Test function: estimateFee', function () { sandbox.restore(); }); - it('should estimate the fee correctly', async function () { - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account2.publicKey; - }); - sandbox.stub(utils, 'estimateFee').callsFake(async () => { - return estimateFeeResp; + describe('when request param validation fail', function () { + let invalidRequest = Object.assign({}, requestObject); + + afterEach(async function () { + invalidRequest = Object.assign({}, requestObject); }); - // The following will be commented out later when starknet.js - // supports estimateFeeBulk in rpc mode - // sandbox.stub(utils, 'estimateFeeBulk').callsFake(async () => { - // return [estimateFeeResp]; - // }); - const result = await estimateFee(apiParams); - expect(result.suggestedMaxFee).to.be.eq(estimateFeeResp.suggestedMaxFee.toString(10)); - }); - it('should estimate the fee including deploy txn correctly', async function () { - sandbox.stub(utils, 'getSigner').throws(new Error()); - sandbox.stub(utils, 'estimateFeeBulk').callsFake(async () => { - return [estimateDeployFeeResp4, estimateFeeResp]; + it('should throw an error if the function name is undefined', async function () { + invalidRequest.contractFuncName = undefined; + apiParams.requestParams = invalidRequest; + let result; + try { + result = await estimateFee(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + } }); - const expectedSuggestedMaxFee = estimateDeployFeeResp4.suggestedMaxFee + estimateFeeResp.suggestedMaxFee; - const result = await estimateFee(apiParams); - expect(result.suggestedMaxFee).to.be.eq(expectedSuggestedMaxFee.toString(10)); - }); - it('should estimate the fee without gas consumed and gas price correctly', async function () { - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account2.publicKey; + it('should throw an error if the contract address is invalid', async function () { + invalidRequest.contractAddress = 'wrongAddress'; + apiParams.requestParams = invalidRequest; + let result; + try { + result = await estimateFee(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + } }); - sandbox.stub(utils, 'estimateFee').callsFake(async () => { - return estimateFeeResp2; + + it('should throw an error if the sender address is invalid', async function () { + invalidRequest.senderAddress = 'wrongAddress'; + apiParams.requestParams = invalidRequest; + let result; + try { + result = await estimateFee(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + } }); - // The following will be commented out later when starknet.js - // supports estimateFeeBulk in rpc mode - // sandbox.stub(utils, 'estimateFeeBulk').callsFake(async () => { - // return [estimateFeeResp2]; - // }); - const result = await estimateFee(apiParams); - expect(result.suggestedMaxFee).to.be.eq(estimateFeeResp.suggestedMaxFee.toString(10)); }); - it('should throw error if estimateFee failed', async function () { - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account2.publicKey; + describe('when request param validation pass', function () { + beforeEach(async function () { + apiParams.requestParams = Object.assign({}, requestObject); }); - sandbox.stub(utils, 'estimateFee').throws(new Error()); - apiParams.requestParams = requestObject; - - let result; - try { - await estimateFee(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); - it('should throw an error if the function name is undefined', async function () { - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account2.publicKey; + afterEach(async function () { + apiParams.requestParams = Object.assign({}, requestObject); }); - apiParams.requestParams = { - contractAddress: '0x07394cbe418daa16e42b87ba67372d4ab4a5df0b05c6e554d158458ce245bc10', - contractFuncName: undefined, - contractCallData: '0x7426b2da7a8978e0d472d64f15f984d658226cb55a4fd8aa7689688a7eab37b', - senderAddress: account2.address, - }; - let result; - try { - result = await estimateFee(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); - it('should throw an error if the contract address is invalid', async function () { - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account2.publicKey; + describe('when account require upgrade', function () { + let isUpgradeRequiredStub: sinon.SinonStub; + beforeEach(async function () { + isUpgradeRequiredStub = sandbox.stub(utils, 'isUpgradeRequired').resolves(true); + }); + + it('should throw error if upgrade required', async function () { + let result; + try { + result = await estimateFee(apiParams); + } catch (err) { + result = err; + } finally { + expect(isUpgradeRequiredStub).to.have.been.calledOnceWith(STARKNET_SEPOLIA_TESTNET_NETWORK, account2.address); + expect(result).to.be.an('Error'); + } + }); }); - apiParams.requestParams = { - contractAddress: 'wrongAddress', - contractFuncName: 'balanceOf', - contractCallData: '0x7426b2da7a8978e0d472d64f15f984d658226cb55a4fd8aa7689688a7eab37b', - senderAddress: account2.address, - }; - let result; - try { - result = await estimateFee(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); - it('should throw an error if the sender address is invalid', async function () { - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account2.publicKey; + describe('when account is not require upgrade', function () { + let estimateFeeBulkStub: sinon.SinonStub; + let estimateFeeStub: sinon.SinonStub; + + beforeEach(async function () { + sandbox.stub(utils, 'isUpgradeRequired').resolves(false); + apiParams.requestParams = { + ...apiParams.requestParams, + senderAddress: Cairo1Account1.address, + }; + }); + + describe('when account is deployed', function () { + beforeEach(async function () { + estimateFeeBulkStub = sandbox.stub(utils, 'estimateFeeBulk'); + sandbox.stub(utils, 'isAccountDeployed').resolves(true); + }); + + it('should estimate the fee correctly', async function () { + estimateFeeStub = sandbox.stub(utils, 'estimateFee').resolves(estimateFeeResp); + const result = await estimateFee(apiParams); + expect(result.suggestedMaxFee).to.be.eq(estimateFeeResp.suggestedMaxFee.toString(10)); + expect(estimateFeeStub).callCount(1); + expect(estimateFeeBulkStub).callCount(0); + }); + }); + + describe('when account is not deployed', function () { + beforeEach(async function () { + estimateFeeStub = sandbox.stub(utils, 'estimateFee'); + sandbox.stub(utils, 'isAccountDeployed').resolves(false); + }); + + it('should estimate the fee including deploy txn correctly', async function () { + estimateFeeBulkStub = sandbox + .stub(utils, 'estimateFeeBulk') + .resolves([estimateFeeResp, estimateDeployFeeResp4]); + const expectedSuggestedMaxFee = estimateDeployFeeResp4.suggestedMaxFee + estimateFeeResp.suggestedMaxFee; + const result = await estimateFee(apiParams); + + const { privateKey, publicKey } = await utils.getKeysFromAddress( + apiParams.keyDeriver, + STARKNET_SEPOLIA_TESTNET_NETWORK, + state, + Cairo1Account1.address, + ); + const { callData } = utils.getAccContractAddressAndCallData(publicKey); + const apiRequest = apiParams.requestParams as EstimateFeeRequestParams; + + const expectedBulkTransaction = [ + { + type: TransactionType.DEPLOY_ACCOUNT, + payload: { + classHash: ACCOUNT_CLASS_HASH, + contractAddress: Cairo1Account1.address, + constructorCalldata: callData, + addressSalt: publicKey, + }, + }, + { + type: TransactionType.INVOKE, + payload: { + contractAddress: apiRequest.contractAddress, + entrypoint: apiRequest.contractFuncName, + calldata: utils.getCallDataArray(apiRequest.contractCallData), + }, + }, + ]; + + expect(result.suggestedMaxFee).to.be.eq(expectedSuggestedMaxFee.toString(10)); + expect(estimateFeeStub).callCount(0); + expect(estimateFeeBulkStub).callCount(1); + expect(estimateFeeBulkStub).to.be.calledWith( + STARKNET_SEPOLIA_TESTNET_NETWORK, + Cairo1Account1.address, + privateKey, + expectedBulkTransaction, + ); + }); + + it('should throw error if estimateFee failed', async function () { + estimateFeeBulkStub = sandbox.stub(utils, 'estimateFeeBulk').throws('Error'); + apiParams.requestParams = requestObject; + + let result; + try { + await estimateFee(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + expect(estimateFeeStub).callCount(0); + expect(estimateFeeBulkStub).callCount(1); + } + }); + }); }); - apiParams.requestParams = { - contractAddress: '0x07394cbe418daa16e42b87ba67372d4ab4a5df0b05c6e554d158458ce245bc10', - contractFuncName: 'balanceOf', - contractCallData: '0x7426b2da7a8978e0d472d64f15f984d658226cb55a4fd8aa7689688a7eab37b', - senderAddress: 'wrongAddress', - }; - let result; - try { - result = await estimateFee(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } }); }); diff --git a/packages/starknet-snap/test/src/estimateFees.test.ts b/packages/starknet-snap/test/src/estimateFees.test.ts index 5e557ce4..cf797293 100644 --- a/packages/starknet-snap/test/src/estimateFees.test.ts +++ b/packages/starknet-snap/test/src/estimateFees.test.ts @@ -40,11 +40,9 @@ describe('Test function: estimateFees', function () { sandbox.restore(); }); - it('should estimate the fee including deploy txn correctly', async function () { + it('should estimate fees correctly', async function () { const feeResult = [estimateDeployFeeResp2, estimateDeployFeeResp3]; - sandbox.stub(utils, 'estimateFeeBulk').callsFake(async () => { - return feeResult; - }); + sandbox.stub(utils, 'estimateFeeBulk').resolves(feeResult); apiParams.requestParams = { senderAddress: account2.address, chainId: STARKNET_SEPOLIA_TESTNET_NETWORK.chainId, @@ -58,6 +56,9 @@ describe('Test function: estimateFees', function () { }, }, ], + invocationsDetails: { + nonce: '1', + }, }; const expectedResult = feeResult.map((fee) => ({ overall_fee: fee.overall_fee.toString(10) || '0', @@ -65,14 +66,13 @@ describe('Test function: estimateFees', function () { gas_price: fee.gas_price.toString(10) || '0', suggestedMaxFee: fee.suggestedMaxFee.toString(10) || '0', })); + const result = await estimateFees(apiParams); + expect(result).to.eql(expectedResult); }); it('should throw error if estimateFee failed', async function () { - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account2.publicKey; - }); sandbox.stub(utils, 'estimateFeeBulk').throws(new Error()); apiParams.requestParams = { senderAddress: account2.address, diff --git a/packages/starknet-snap/test/src/executeTxn.test.ts b/packages/starknet-snap/test/src/executeTxn.test.ts index 9c579be2..667c0eab 100644 --- a/packages/starknet-snap/test/src/executeTxn.test.ts +++ b/packages/starknet-snap/test/src/executeTxn.test.ts @@ -294,6 +294,7 @@ describe('Test function: executeTxn', function () { }); it('should return false if user rejected to sign the transaction', async function () { + sandbox.stub(utils, 'isAccountDeployed').resolves(true); walletStub.rpcStubs.snap_dialog.resolves(false); const stub = sandbox.stub(utils, 'executeTxn').resolves({ transaction_hash: 'transaction_hash', diff --git a/packages/starknet-snap/test/src/extractPrivateKey.test.ts b/packages/starknet-snap/test/src/extractPrivateKey.test.ts index f3a65eb9..b7f0e591 100644 --- a/packages/starknet-snap/test/src/extractPrivateKey.test.ts +++ b/packages/starknet-snap/test/src/extractPrivateKey.test.ts @@ -22,6 +22,7 @@ describe('Test function: extractPrivateKey', function () { networks: [STARKNET_SEPOLIA_TESTNET_NETWORK], transactions: [], }; + const apiParams: ApiParams = { state, requestParams: {}, @@ -29,6 +30,10 @@ describe('Test function: extractPrivateKey', function () { saveMutex: new Mutex(), }; + const requestObject: ExtractPrivateKeyRequestParams = { + userAddress: account1.address, + }; + beforeEach(async function () { walletStub.rpcStubs.snap_getBip44Entropy.callsFake(getBip44EntropyStub); apiParams.keyDeriver = await getAddressKeyDeriver(walletStub); @@ -39,87 +44,141 @@ describe('Test function: extractPrivateKey', function () { sandbox.restore(); }); - 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); - }); + describe('when request param validation fail', function () { + let invalidRequest = Object.assign({}, requestObject); - 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); - }); + afterEach(async function () { + invalidRequest = Object.assign({}, requestObject); + }); - 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 an error if the user address is undefined', async function () { + invalidRequest.userAddress = undefined; + apiParams.requestParams = invalidRequest; + let result; + try { + result = await extractPrivateKey(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + } + }); - 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'); - } + 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'); + } + }); }); - it('should throw an error if the user address is undefined', async function () { - const requestObject: ExtractPrivateKeyRequestParams = { - userAddress: undefined, - }; - apiParams.requestParams = requestObject; - 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 require upgrade checking fail', function () { + it('should throw error', async function () { + const isUpgradeRequiredStub = sandbox.stub(utils, 'isUpgradeRequired').throws('network error'); + let result; + try { + result = await extractPrivateKey(apiParams); + } catch (err) { + result = err; + } finally { + expect(isUpgradeRequiredStub).to.have.been.calledOnceWith(STARKNET_SEPOLIA_TESTNET_NETWORK, account1.address); + expect(result).to.be.an('Error'); + } + }); + }); + + describe('when account require upgrade', function () { + let isUpgradeRequiredStub: sinon.SinonStub; + beforeEach(async function () { + isUpgradeRequiredStub = sandbox.stub(utils, 'isUpgradeRequired').resolves(true); + }); + + it('should throw error if upgrade required', async function () { + let result; + try { + result = await extractPrivateKey(apiParams); + } catch (err) { + result = err; + } finally { + expect(isUpgradeRequiredStub).to.have.been.calledOnceWith(STARKNET_SEPOLIA_TESTNET_NETWORK, account1.address); + expect(result).to.be.an('Error'); + } + }); + }); + + describe('when account is not require upgrade', function () { + beforeEach(async function () { + sandbox.stub(utils, 'isUpgradeRequired').resolves(false); + }); + + 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; - it('should throw an error if the user address is invalid', async function () { - const requestObject: ExtractPrivateKeyRequestParams = { - userAddress: 'wrongAddress', - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await extractPrivateKey(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } + let result; + try { + await extractPrivateKey(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + } + }); + }); }); }); diff --git a/packages/starknet-snap/test/src/extractPublicKey.test.ts b/packages/starknet-snap/test/src/extractPublicKey.test.ts index 1204a754..8dd022fd 100644 --- a/packages/starknet-snap/test/src/extractPublicKey.test.ts +++ b/packages/starknet-snap/test/src/extractPublicKey.test.ts @@ -29,6 +29,10 @@ describe('Test function: extractPublicKey', function () { saveMutex: new Mutex(), }; + const requestObject: ExtractPublicKeyRequestParams = { + userAddress: account1.address, + }; + beforeEach(async function () { walletStub.rpcStubs.snap_getBip44Entropy.callsFake(getBip44EntropyStub); apiParams.keyDeriver = await getAddressKeyDeriver(walletStub); @@ -39,70 +43,124 @@ describe('Test function: extractPublicKey', function () { sandbox.restore(); }); - it('should get the public key of the specified user account correctly', async function () { - const requestObject: ExtractPublicKeyRequestParams = { - userAddress: account1.address, - }; - apiParams.requestParams = requestObject; - const result = await extractPublicKey(apiParams); - expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called; - expect(result).to.be.eql(account1.publicKey); - }); + describe('when request param validation fail', function () { + let invalidRequest = Object.assign({}, requestObject); - it('should get the public key of the unfound user account correctly', async function () { - const requestObject: ExtractPublicKeyRequestParams = { - userAddress: unfoundUserAddress, - }; - apiParams.requestParams = requestObject; - const result = await extractPublicKey(apiParams); - expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called; - expect(result).to.be.eql(unfoundUserPublicKey); - }); + afterEach(async function () { + invalidRequest = Object.assign({}, requestObject); + }); - it('should throw error if getKeysFromAddress failed', async function () { - sandbox.stub(utils, 'getKeysFromAddress').throws(new Error()); - const requestObject: ExtractPublicKeyRequestParams = { - userAddress: unfoundUserAddress, - }; - apiParams.requestParams = requestObject; - - let result; - try { - await extractPublicKey(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); + it('should throw error if param userAddress is undefined', async function () { + invalidRequest.userAddress = undefined; + apiParams.requestParams = invalidRequest; + let result; + try { + result = await extractPublicKey(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + } + }); - it('should throw error if param userAddress is undefined', async function () { - const requestObject: ExtractPublicKeyRequestParams = { - userAddress: undefined, - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await extractPublicKey(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } + it('should throw error if param userAddress is invalid', async function () { + invalidRequest.userAddress = 'wrongAddress'; + apiParams.requestParams = invalidRequest; + let result; + try { + result = await extractPublicKey(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + } + }); }); - it('should throw error if param userAddress is invalid', async function () { - const requestObject: ExtractPublicKeyRequestParams = { - userAddress: 'wrongAddress', - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await extractPublicKey(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 require upgrade checking fail', function () { + it('should throw error', async function () { + const isUpgradeRequiredStub = sandbox.stub(utils, 'isUpgradeRequired').throws('network error'); + let result; + try { + result = await extractPublicKey(apiParams); + } catch (err) { + result = err; + } finally { + expect(isUpgradeRequiredStub).to.have.been.calledOnceWith(STARKNET_SEPOLIA_TESTNET_NETWORK, account1.address); + expect(result).to.be.an('Error'); + } + }); + }); + + describe('when account require upgrade', function () { + let isUpgradeRequiredStub: sinon.SinonStub; + beforeEach(async function () { + isUpgradeRequiredStub = sandbox.stub(utils, 'isUpgradeRequired').resolves(true); + }); + + it('should throw error if upgrade required', async function () { + let result; + try { + result = await extractPublicKey(apiParams); + } catch (err) { + result = err; + } finally { + expect(isUpgradeRequiredStub).to.have.been.calledOnceWith(STARKNET_SEPOLIA_TESTNET_NETWORK, account1.address); + expect(result).to.be.an('Error'); + } + }); + }); + + describe('when account is not require upgrade', function () { + beforeEach(async function () { + sandbox.stub(utils, 'isUpgradeRequired').resolves(false); + }); + + it('should get the public key of the specified user account correctly', async function () { + const requestObject: ExtractPublicKeyRequestParams = { + userAddress: account1.address, + }; + apiParams.requestParams = requestObject; + const result = await extractPublicKey(apiParams); + expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called; + expect(result).to.be.eql(account1.publicKey); + }); + + it('should get the public key of the unfound user account correctly', async function () { + const requestObject: ExtractPublicKeyRequestParams = { + userAddress: unfoundUserAddress, + }; + apiParams.requestParams = requestObject; + const result = await extractPublicKey(apiParams); + expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called; + expect(result).to.be.eql(unfoundUserPublicKey); + }); + + it('should throw error if getKeysFromAddress failed', async function () { + sandbox.stub(utils, 'getKeysFromAddress').throws(new Error()); + const requestObject: ExtractPublicKeyRequestParams = { + userAddress: unfoundUserAddress, + }; + apiParams.requestParams = requestObject; + + let result; + try { + await extractPublicKey(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + } + }); + }); }); }); diff --git a/packages/starknet-snap/test/src/recoverAccounts.test.ts b/packages/starknet-snap/test/src/recoverAccounts.test.ts index 0902f054..1ff1d2e6 100644 --- a/packages/starknet-snap/test/src/recoverAccounts.test.ts +++ b/packages/starknet-snap/test/src/recoverAccounts.test.ts @@ -27,7 +27,7 @@ const sandbox = sinon.createSandbox(); describe('Test function: recoverAccounts', function () { this.timeout(5000); const walletStub = new WalletMock(); - const state: SnapState = { + let state: SnapState = { accContracts: [], erc20Tokens: [], networks: [STARKNET_MAINNET_NETWORK, STARKNET_SEPOLIA_TESTNET_NETWORK, INVALID_NETWORK], @@ -50,19 +50,33 @@ describe('Test function: recoverAccounts', function () { afterEach(function () { walletStub.reset(); sandbox.restore(); + state = { + accContracts: [], + erc20Tokens: [], + networks: [STARKNET_SEPOLIA_TESTNET_NETWORK, STARKNET_MAINNET_NETWORK, INVALID_NETWORK], + transactions: [], + }; }); it('should recover accounts in mainnet correctly', async function () { - state.accContracts = []; const maxScanned = 5; const maxMissed = 3; const validPublicKeys = 2; - const getSignerStub = sandbox.stub(utils, 'getSigner'); - for (let i = 0; i < validPublicKeys; i++) { - getSignerStub.onCall(i).resolves(mainnetPublicKeys[i]); + const getCorrectContractAddressStub = sandbox.stub(utils, 'getCorrectContractAddress'); + + for (let i = 0; i < maxScanned; i++) { + if (i < validPublicKeys) { + getCorrectContractAddressStub + .onCall(i) + .resolves({ address: mainnetAccAddresses[i], signerPubKey: mainnetPublicKeys[i], upgradeRequired: false }); + } else { + getCorrectContractAddressStub.onCall(i).resolves({ + address: mainnetAccAddresses[i], + signerPubKey: num.toHex(constants.ZERO), + upgradeRequired: false, + }); + } } - getSignerStub.onCall(validPublicKeys).resolves(num.toHex(constants.ZERO)); - getSignerStub.resolves(num.toHex(constants.ZERO)); const requestObject: RecoverAccountsRequestParams = { startScanIndex: 0, @@ -74,6 +88,7 @@ describe('Test function: recoverAccounts', function () { const result = await recoverAccounts(apiParams); const expectedCalledTimes = validPublicKeys + maxMissed; + expect(walletStub.rpcStubs.snap_manageState.callCount).to.be.eq(expectedCalledTimes * 2); expect(result.length).to.be.eq(expectedCalledTimes); expect(state.accContracts.map((acc) => acc.address)).to.be.eql(mainnetAccAddresses.slice(0, expectedCalledTimes)); @@ -91,11 +106,21 @@ describe('Test function: recoverAccounts', function () { const maxScanned = 5; const maxMissed = 3; const validPublicKeys = 2; - const getSignerStub = sandbox.stub(utils, 'getSigner'); - for (let i = 0; i < validPublicKeys; i++) { - getSignerStub.onCall(i).resolves(testnetPublicKeys[i]); + const getCorrectContractAddressStub = sandbox.stub(utils, 'getCorrectContractAddress'); + + for (let i = 0; i < maxScanned; i++) { + if (i < validPublicKeys) { + getCorrectContractAddressStub + .onCall(i) + .resolves({ address: testnetAccAddresses[i], signerPubKey: testnetPublicKeys[i], upgradeRequired: false }); + } else { + getCorrectContractAddressStub.onCall(i).resolves({ + address: testnetAccAddresses[i], + signerPubKey: num.toHex(constants.ZERO), + upgradeRequired: false, + }); + } } - getSignerStub.throws(new Error()); const requestObject: RecoverAccountsRequestParams = { startScanIndex: 0, @@ -106,6 +131,7 @@ describe('Test function: recoverAccounts', function () { apiParams.requestParams = requestObject; const result = await recoverAccounts(apiParams); const expectedCalledTimes = validPublicKeys + maxMissed; + expect(walletStub.rpcStubs.snap_manageState.callCount).to.be.eq(expectedCalledTimes * 2); expect(result.length).to.be.eq(expectedCalledTimes); expect(state.accContracts.map((acc) => acc.address)).to.be.eql(testnetAccAddresses.slice(0, expectedCalledTimes)); @@ -118,35 +144,35 @@ describe('Test function: recoverAccounts', function () { expect(state.accContracts.length).to.be.eq(expectedCalledTimes); }); - it('should recover accounts in SN_SEPOLIA with same parameters correctly', async function () { + it('should throw error if getCorrectContractAddress throw error', async function () { const maxScanned = 5; const maxMissed = 3; - const validPublicKeys = 2; - const getSignerStub = sandbox.stub(utils, 'getSigner'); - for (let i = 0; i < validPublicKeys; i++) { - getSignerStub.onCall(i).resolves(testnetPublicKeys[i]); - } - getSignerStub.throws(new Error()); - + const getCorrectContractAddressStub = sandbox.stub(utils, 'getCorrectContractAddress'); + getCorrectContractAddressStub.callsFake(async () => { + throw new Error('network error'); + }); + const isUpgradeRequiredStub = sandbox.stub(utils, 'isUpgradeRequired'); const requestObject: RecoverAccountsRequestParams = { startScanIndex: 0, maxScanned, maxMissed, + chainId: STARKNET_MAINNET_NETWORK.chainId, }; - apiParams.requestParams = requestObject; - const result = await recoverAccounts(apiParams); - const expectedCalledTimes = validPublicKeys + maxMissed; - expect(walletStub.rpcStubs.snap_manageState.callCount).to.be.eq(expectedCalledTimes); - expect(result.length).to.be.eq(expectedCalledTimes); - expect(state.accContracts.map((acc) => acc.address)).to.be.eql(testnetAccAddresses.slice(0, expectedCalledTimes)); - expect(state.accContracts.map((acc) => acc.addressSalt)).to.be.eql(testnetPublicKeys.slice(0, expectedCalledTimes)); - expect( - state.accContracts - .filter((acc) => acc.publicKey && acc.publicKey !== num.toHex(constants.ZERO)) - .map((acc) => acc.publicKey), - ).to.be.eql(testnetPublicKeys.slice(0, validPublicKeys)); - expect(state.accContracts.length).to.be.eq(expectedCalledTimes); + + let result = null; + + try { + await recoverAccounts(apiParams); + } catch (e) { + result = e; + } finally { + expect(getCorrectContractAddressStub.callCount).to.be.eq(1); + expect(isUpgradeRequiredStub.callCount).to.be.eq(0); + expect(walletStub.rpcStubs.snap_manageState.callCount).to.be.eq(0); + expect(result).to.be.an('Error'); + expect(result.message).to.be.eq('network error'); + } }); it('should throw error if upsertAccount failed', async function () { @@ -154,12 +180,21 @@ describe('Test function: recoverAccounts', function () { const maxScanned = 5; const maxMissed = 3; const validPublicKeys = 2; - const getSignerStub = sandbox.stub(utils, 'getSigner'); - for (let i = 0; i < validPublicKeys; i++) { - getSignerStub.onCall(i).resolves(mainnetPublicKeys[i]); + const getCorrectContractAddressStub = sandbox.stub(utils, 'getCorrectContractAddress'); + + for (let i = 0; i < maxScanned; i++) { + if (i < validPublicKeys) { + getCorrectContractAddressStub + .onCall(i) + .resolves({ address: mainnetAccAddresses[i], signerPubKey: mainnetPublicKeys[i], upgradeRequired: false }); + } else { + getCorrectContractAddressStub.onCall(i).resolves({ + address: mainnetAccAddresses[i], + signerPubKey: num.toHex(constants.ZERO), + upgradeRequired: false, + }); + } } - getSignerStub.onCall(validPublicKeys).resolves(num.toHex(constants.ZERO)); - getSignerStub.resolves(num.toHex(constants.ZERO)); const requestObject: RecoverAccountsRequestParams = { startScanIndex: 0, diff --git a/packages/starknet-snap/test/src/sendTransaction.test.ts b/packages/starknet-snap/test/src/sendTransaction.test.ts index 131087a1..9d684ce2 100644 --- a/packages/starknet-snap/test/src/sendTransaction.test.ts +++ b/packages/starknet-snap/test/src/sendTransaction.test.ts @@ -7,7 +7,7 @@ import * as utils from '../../src/utils/starknetUtils'; import * as snapUtils from '../../src/utils/snapUtils'; import { SnapState } from '../../src/types/snapState'; import { sendTransaction } from '../../src/sendTransaction'; - +import * as estimateFeeSnap from '../../src/estimateFee'; import { STARKNET_SEPOLIA_TESTNET_NETWORK } from '../../src/utils/constants'; import { account1, @@ -21,6 +21,7 @@ import { token2, token3, unfoundUserAddress, + Cairo1Account1, } from '../constants.test'; import { getAddressKeyDeriver } from '../../src/utils/keyPair'; import { Mutex } from 'async-mutex'; @@ -35,7 +36,7 @@ describe('Test function: sendTransaction', function () { this.timeout(5000); const walletStub = new WalletMock(); const state: SnapState = { - accContracts: [account1], + accContracts: [], erc20Tokens: [token2, token3], networks: [STARKNET_SEPOLIA_TESTNET_NETWORK], transactions: [], @@ -46,24 +47,18 @@ describe('Test function: sendTransaction', function () { wallet: walletStub, saveMutex: new Mutex(), }; - let executeTxnResp; + + const requestObject: SendTransactionRequestParams = { + contractAddress: '0x07394cbe418daa16e42b87ba67372d4ab4a5df0b05c6e554d158458ce245bc10', + contractFuncName: 'transfer', + contractCallData: '0x0256d8f49882cc9366037415f48fa9fd2b5b7344ded7573ebfcef7c90e3e6b75,100000000000000000000,0', + senderAddress: account1.address, + chainId: STARKNET_SEPOLIA_TESTNET_NETWORK.chainId, + }; beforeEach(async function () { walletStub.rpcStubs.snap_getBip44Entropy.callsFake(getBip44EntropyStub); apiParams.keyDeriver = await getAddressKeyDeriver(walletStub); - sandbox.stub(utils, 'estimateFeeBulk').callsFake(async () => { - return [estimateFeeResp]; - }); - sandbox.stub(utils, 'estimateFee').callsFake(async () => { - return estimateFeeResp; - }); - executeTxnResp = sendTransactionResp; - sandbox.stub(utils, 'executeTxn').callsFake(async () => { - return executeTxnResp; - }); - walletStub.rpcStubs.snap_dialog.resolves(true); - walletStub.rpcStubs.snap_manageState.resolves(state); - sandbox.stub(utils, 'waitForTransaction').resolves({} as unknown as GetTransactionReceiptResponse); }); afterEach(function () { @@ -71,376 +66,345 @@ describe('Test function: sendTransaction', function () { sandbox.restore(); }); - it('should send a transaction for transferring 10 tokens correctly', async function () { - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account1.publicKey; - }); - const requestObject: SendTransactionRequestParams = { - contractAddress: '0x07394cbe418daa16e42b87ba67372d4ab4a5df0b05c6e554d158458ce245bc10', - contractFuncName: 'transfer', - contractCallData: '0x0256d8f49882cc9366037415f48fa9fd2b5b7344ded7573ebfcef7c90e3e6b75,100000000000000000000,0', - senderAddress: account1.address, - }; - apiParams.requestParams = requestObject; - const result = await sendTransaction(apiParams); - expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; - expect(walletStub.rpcStubs.snap_manageState).to.have.been.called; - expect(result).to.be.eql(sendTransactionResp); - }); + describe('when request param validation fail', function () { + let invalidRequest: SendTransactionRequestParams = Object.assign({}, requestObject); - it('should trigger a deploy txn and send a transaction for transferring 10 tokens correctly', async function () { - sandbox.stub(utils, 'getSigner').throws(new Error()); - sandbox.stub(utils, 'deployAccount').callsFake(async () => { - return createAccountProxyResp; - }); - sandbox.stub(utils, 'callContract').callsFake(async () => { - return getBalanceResp; + afterEach(function () { + invalidRequest = Object.assign({}, requestObject); + apiParams.requestParams = requestObject; }); - sandbox.stub(utils, 'estimateAccountDeployFee').callsFake(async () => { - return estimateDeployFeeResp; - }); - const requestObject: SendTransactionRequestParams = { - contractAddress: '0x07394cbe418daa16e42b87ba67372d4ab4a5df0b05c6e554d158458ce245bc10', - contractFuncName: 'transfer', - contractCallData: '0x0256d8f49882cc9366037415f48fa9fd2b5b7344ded7573ebfcef7c90e3e6b75,100000000000000000000,0', - senderAddress: account1.address, - }; - apiParams.requestParams = requestObject; - const result = await sendTransaction(apiParams); - expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; - expect(walletStub.rpcStubs.snap_manageState).to.have.been.called; - expect(result).to.be.eql(sendTransactionResp); - }); - it('should return false if user rejected to sign the transaction', async function () { - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account1.publicKey; + it('should show error when request contractAddress is not given', async function () { + invalidRequest.contractAddress = undefined; + apiParams.requestParams = invalidRequest; + let result; + try { + result = await sendTransaction(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + expect(result.message).to.be.include( + 'The given contract address, sender address, and function name need to be non-empty string', + ); + } }); - walletStub.rpcStubs.snap_dialog.resolves(false); - const requestObject: SendTransactionRequestParams = { - contractAddress: '0x07394cbe418daa16e42b87ba67372d4ab4a5df0b05c6e554d158458ce245bc10', - contractFuncName: 'transfer', - contractCallData: '0x0256d8f49882cc9366037415f48fa9fd2b5b7344ded7573ebfcef7c90e3e6b75,100000000000000000000,0', - senderAddress: account1.address, - }; - apiParams.requestParams = requestObject; - const result = await sendTransaction(apiParams); - expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; - expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called; - expect(result).to.be.eql(false); - }); - it('should send a transaction for transferring 10 tokens but not update snap state if transaction_hash is missing from response', async function () { - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account1.publicKey; + it('should show error when request contractAddress is invalid', async function () { + invalidRequest.contractAddress = '0x0'; + apiParams.requestParams = invalidRequest; + let result; + try { + result = await sendTransaction(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + expect(result.message).to.be.include('The given contract address is invalid'); + } }); - executeTxnResp = sendTransactionFailedResp; - const requestObject: SendTransactionRequestParams = { - contractAddress: '0x07394cbe418daa16e42b87ba67372d4ab4a5df0b05c6e554d158458ce245bc10', - contractFuncName: 'transfer', - contractCallData: '0x0256d8f49882cc9366037415f48fa9fd2b5b7344ded7573ebfcef7c90e3e6b75,100000000000000000000,0', - senderAddress: account1.address, - }; - apiParams.requestParams = requestObject; - const result = await sendTransaction(apiParams); - expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; - expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called; - expect(result).to.be.eql(sendTransactionFailedResp); - }); - it('should send a transaction with given max fee for transferring 10 tokens correctly', async function () { - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account1.publicKey; + it('should show error when request senderAddress is not given', async function () { + invalidRequest.senderAddress = undefined; + apiParams.requestParams = invalidRequest; + let result; + try { + result = await sendTransaction(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + expect(result.message).to.be.include( + 'The given contract address, sender address, and function name need to be non-empty string', + ); + } }); - const requestObject: SendTransactionRequestParams = { - contractAddress: '0x07394cbe418daa16e42b87ba67372d4ab4a5df0b05c6e554d158458ce245bc10', - contractFuncName: 'transfer', - contractCallData: '0x0256d8f49882cc9366037415f48fa9fd2b5b7344ded7573ebfcef7c90e3e6b75,100000000000000000000,0', - senderAddress: account1.address, - maxFee: '15135825227039', - }; - apiParams.requestParams = requestObject; - const result = await sendTransaction(apiParams); - expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; - expect(walletStub.rpcStubs.snap_manageState).to.have.been.called; - expect(result).to.be.eql(sendTransactionResp); - }); - it('should send a transfer transaction for empty call data', async function () { - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account1.publicKey; + it('should show error when request contractAddress is invalid', async function () { + invalidRequest.senderAddress = '0x0'; + apiParams.requestParams = invalidRequest; + let result; + try { + result = await sendTransaction(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + expect(result.message).to.be.include('The given sender address is invalid'); + } }); - const requestObject: SendTransactionRequestParams = { - contractAddress: '0x07394cbe418daa16e42b87ba67372d4ab4a5df0b05c6e554d158458ce245bc10', - contractFuncName: 'transfer', - contractCallData: undefined, - senderAddress: account1.address, - }; - apiParams.requestParams = requestObject; - const result = await sendTransaction(apiParams); - expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; - expect(walletStub.rpcStubs.snap_manageState).to.have.been.called; - expect(result).to.be.eql(sendTransactionResp); - }); - it('should send a transaction for empty call data', async function () { - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account1.publicKey; + it('should show error when request contractFuncName is not given', async function () { + invalidRequest.contractFuncName = undefined; + apiParams.requestParams = invalidRequest; + let result; + try { + result = await sendTransaction(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + expect(result.message).to.be.include( + 'The given contract address, sender address, and function name need to be non-empty string', + ); + } }); - const requestObject: SendTransactionRequestParams = { - contractAddress: account1.address, - contractFuncName: 'get_signer', - contractCallData: undefined, - senderAddress: account1.address, - }; - apiParams.requestParams = requestObject; - const result = await sendTransaction(apiParams); - expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; - expect(walletStub.rpcStubs.snap_manageState).to.have.been.called; - expect(result).to.be.eql(sendTransactionResp); - }); - it('should use heading, text and copyable component', async function () { - executeTxnResp = sendTransactionFailedResp; - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account1.publicKey; + it('should show error when request network not found', async function () { + invalidRequest.chainId = '0x0'; + apiParams.requestParams = invalidRequest; + let result; + try { + result = await sendTransaction(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + } }); - const requestObject: SendTransactionRequestParams = { - contractAddress: account1.address, - contractFuncName: 'get_signer', - contractCallData: '**foo**', - senderAddress: account1.address, - }; - apiParams.requestParams = requestObject; - await sendTransaction(apiParams); - const expectedDialogParams = { - type: 'confirmation', - content: { - type: 'panel', - children: [ - { type: 'heading', value: 'Do you want to sign this transaction ?' }, - { - type: 'text', - value: `**Signer Address:**`, - }, - { - type: 'copyable', - value: '0x04882a372da3dfe1c53170ad75893832469bf87b62b13e84662565c4a88f25cd', - }, - { - type: 'text', - value: `**Contract:**`, - }, - { - type: 'copyable', - value: '0x04882a372da3dfe1c53170ad75893832469bf87b62b13e84662565c4a88f25cd', - }, - { - type: 'text', - value: `**Call Data:**`, - }, - { - type: 'copyable', - value: '[**foo**]', - }, - { - type: 'text', - value: `**Estimated Gas Fee(ETH):**`, - }, - { - type: 'copyable', - value: '0.000022702500105945', - }, - { - type: 'text', - value: `**Network:**`, - }, - { - type: 'copyable', - value: 'Sepolia Testnet', - }, - ], - }, - }; - expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; - expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledWith(expectedDialogParams); }); - it('should send a transaction for transferring 10 tokens from an unfound user correctly', async function () { - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account1.publicKey; + describe('when request param validation pass', function () { + beforeEach(async function () { + apiParams.requestParams = Object.assign({}, requestObject); }); - const requestObject: SendTransactionRequestParams = { - contractAddress: '0x07394cbe418daa16e42b87ba67372d4ab4a5df0b05c6e554d158458ce245bc10', - contractFuncName: 'transfer', - contractCallData: '0x0256d8f49882cc9366037415f48fa9fd2b5b7344ded7573ebfcef7c90e3e6b75,100000000000000000000,0', - senderAddress: unfoundUserAddress, - }; - apiParams.requestParams = requestObject; - const result = await sendTransaction(apiParams); - expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; - expect(walletStub.rpcStubs.snap_manageState).to.have.been.called; - expect(result).to.be.eql(sendTransactionResp); - }); - it('should send a transaction for transferring 10 tokens (token of 10 decimal places) from an unfound user correctly', async function () { - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account1.publicKey; + afterEach(async function () { + apiParams.requestParams = Object.assign({}, requestObject); }); - const requestObject: SendTransactionRequestParams = { - contractAddress: '0x06a09ccb1caaecf3d9683efe335a667b2169a409d19c589ba1eb771cd210af75', - contractFuncName: 'transfer', - contractCallData: '0x0256d8f49882cc9366037415f48fa9fd2b5b7344ded7573ebfcef7c90e3e6b75,100000000000000000000,0', - senderAddress: unfoundUserAddress, - }; - apiParams.requestParams = requestObject; - const result = await sendTransaction(apiParams); - expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; - expect(walletStub.rpcStubs.snap_manageState).to.have.been.called; - expect(result).to.be.eql(sendTransactionResp); - }); - it('should throw error if upsertTransaction failed', async function () { - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account1.publicKey; + describe('when require upgrade checking fail', function () { + it('should throw error', async function () { + const isUpgradeRequiredStub = sandbox.stub(utils, 'isUpgradeRequired').throws('network error'); + let result; + try { + result = await sendTransaction(apiParams); + } catch (err) { + result = err; + } finally { + expect(isUpgradeRequiredStub).to.have.been.calledOnceWith(STARKNET_SEPOLIA_TESTNET_NETWORK, account1.address); + expect(result).to.be.an('Error'); + } + }); }); - sandbox.stub(snapUtils, 'upsertTransaction').throws(new Error()); - const requestObject: SendTransactionRequestParams = { - contractAddress: '0x07394cbe418daa16e42b87ba67372d4ab4a5df0b05c6e554d158458ce245bc10', - contractFuncName: 'transfer', - contractCallData: '0x0256d8f49882cc9366037415f48fa9fd2b5b7344ded7573ebfcef7c90e3e6b75,100000000000000000000,0', - senderAddress: unfoundUserAddress, - }; - apiParams.requestParams = requestObject; - let result; - try { - await sendTransaction(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); + describe('when account require upgrade', function () { + let isUpgradeRequiredStub: sinon.SinonStub; + beforeEach(async function () { + isUpgradeRequiredStub = sandbox.stub(utils, 'isUpgradeRequired').resolves(true); + }); - it('should throw an error if contract address is undefined', async function () { - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account1.publicKey; + it('should throw error if upgrade required', async function () { + let result; + try { + result = await sendTransaction(apiParams); + } catch (err) { + result = err; + } finally { + expect(isUpgradeRequiredStub).to.have.been.calledOnceWith(STARKNET_SEPOLIA_TESTNET_NETWORK, account1.address); + expect(result).to.be.an('Error'); + } + }); }); - const requestObject: SendTransactionRequestParams = { - contractAddress: undefined, - contractFuncName: 'transfer', - contractCallData: '0x0256d8f49882cc9366037415f48fa9fd2b5b7344ded7573ebfcef7c90e3e6b75,100000000000000000000,0', - senderAddress: unfoundUserAddress, - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await sendTransaction(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); - it('should throw an error if function name is undefined', async function () { - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account1.publicKey; - }); - const requestObject: SendTransactionRequestParams = { - contractAddress: '0x07394cbe418daa16e42b87ba67372d4ab4a5df0b05c6e554d158458ce245bc10', - contractFuncName: undefined, - contractCallData: '0x0256d8f49882cc9366037415f48fa9fd2b5b7344ded7573ebfcef7c90e3e6b75,100000000000000000000,0', - senderAddress: unfoundUserAddress, - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await sendTransaction(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); + describe('when account do not require upgrade', function () { + let executeTxnResp; + let executeTxnStub: sinon.SinonStub; + beforeEach(async function () { + apiParams.requestParams = { + ...apiParams.requestParams, + senderAddress: Cairo1Account1.address, + }; + sandbox.stub(utils, 'isUpgradeRequired').resolves(false); + sandbox.stub(estimateFeeSnap, 'estimateFee').resolves({ + suggestedMaxFee: estimateFeeResp.suggestedMaxFee.toString(10), + overallFee: estimateFeeResp.overall_fee.toString(10), + gasConsumed: '0', + gasPrice: '0', + unit: 'wei', + includeDeploy: true, + }); + executeTxnResp = sendTransactionResp; + executeTxnStub = sandbox.stub(utils, 'executeTxn').resolves(executeTxnResp); + walletStub.rpcStubs.snap_manageState.resolves(state); + walletStub.rpcStubs.snap_dialog.resolves(true); + sandbox.stub(utils, 'waitForTransaction').resolves({} as unknown as GetTransactionReceiptResponse); + }); - it('should throw an error if sender address is undefined', async function () { - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account1.publicKey; - }); - const requestObject: SendTransactionRequestParams = { - contractAddress: '0x07394cbe418daa16e42b87ba67372d4ab4a5df0b05c6e554d158458ce245bc10', - contractFuncName: 'transfer', - contractCallData: '0x0256d8f49882cc9366037415f48fa9fd2b5b7344ded7573ebfcef7c90e3e6b75,100000000000000000000,0', - senderAddress: undefined, - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await sendTransaction(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); + describe('when account is deployed', function () { + beforeEach(async function () { + sandbox.stub(utils, 'isAccountDeployed').resolves(true); + }); - it('should throw an error if contract address is invalid', async function () { - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account1.publicKey; - }); - const requestObject: SendTransactionRequestParams = { - contractAddress: 'wrongAddress', - contractFuncName: 'transfer', - contractCallData: '0x0256d8f49882cc9366037415f48fa9fd2b5b7344ded7573ebfcef7c90e3e6b75,100000000000000000000,0', - senderAddress: account1.address, - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await sendTransaction(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); + it('should send a transaction for transferring 10 tokens correctly', async function () { + const result = await sendTransaction(apiParams); + expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; + expect(walletStub.rpcStubs.snap_manageState).to.have.been.called; + expect(result).to.be.eql(sendTransactionResp); + }); - it('should throw an error if sender address is invalid', async function () { - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account1.publicKey; - }); - const requestObject: SendTransactionRequestParams = { - contractAddress: '0x07394cbe418daa16e42b87ba67372d4ab4a5df0b05c6e554d158458ce245bc10', - contractFuncName: 'transfer', - contractCallData: '0x0256d8f49882cc9366037415f48fa9fd2b5b7344ded7573ebfcef7c90e3e6b75,100000000000000000000,0', - senderAddress: 'wrongAddress', - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await sendTransaction(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); + it('should send a transaction for transferring 10 tokens but not update snap state if transaction_hash is missing from response', async function () { + executeTxnStub.restore(); + executeTxnStub = sandbox.stub(utils, 'executeTxn').resolves(sendTransactionFailedResp); + const result = await sendTransaction(apiParams); + expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; + expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called; + expect(result).to.be.eql(sendTransactionFailedResp); + }); + + it('should send a transaction with given max fee for transferring 10 tokens correctly', async function () { + const apiRequest = apiParams.requestParams as SendTransactionRequestParams; + apiRequest.maxFee = '15135825227039'; + const result = await sendTransaction(apiParams); + expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; + expect(walletStub.rpcStubs.snap_manageState).to.have.been.called; + expect(result).to.be.eql(sendTransactionResp); + }); + + it('should send a transfer transaction for empty call data', async function () { + const apiRequest = apiParams.requestParams as SendTransactionRequestParams; + apiRequest.contractCallData = undefined; + const result = await sendTransaction(apiParams); + expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; + expect(walletStub.rpcStubs.snap_manageState).to.have.been.called; + expect(result).to.be.eql(sendTransactionResp); + }); + + it('should send a transaction for empty call data', async function () { + const apiRequest = apiParams.requestParams as SendTransactionRequestParams; + apiRequest.contractCallData = undefined; + apiRequest.contractFuncName = 'get_signer'; + const result = await sendTransaction(apiParams); + expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; + expect(walletStub.rpcStubs.snap_manageState).to.have.been.called; + expect(result).to.be.eql(sendTransactionResp); + }); + + it('should send a transaction for transferring 10 tokens from an unfound user correctly', async function () { + const apiRequest = apiParams.requestParams as SendTransactionRequestParams; + apiRequest.senderAddress = unfoundUserAddress; + const result = await sendTransaction(apiParams); + expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; + expect(walletStub.rpcStubs.snap_manageState).to.have.been.called; + expect(result).to.be.eql(sendTransactionResp); + }); + + it('should throw error if upsertTransaction failed', async function () { + sandbox.stub(snapUtils, 'upsertTransaction').throws(new Error()); + let result; + try { + await sendTransaction(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + } + }); + + it('should return false if user rejected to sign the transaction', async function () { + walletStub.rpcStubs.snap_dialog.resolves(false); + const result = await sendTransaction(apiParams); + expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; + expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called; + expect(result).to.be.eql(false); + }); + + it('should use heading, text and copyable component', async function () { + executeTxnResp = sendTransactionFailedResp; + sandbox.stub(utils, 'getSigner').callsFake(async () => { + return account1.publicKey; + }); + const requestObject: SendTransactionRequestParams = { + contractAddress: account1.address, + contractFuncName: 'get_signer', + contractCallData: '**foo**', + senderAddress: account1.address, + }; + apiParams.requestParams = requestObject; + await sendTransaction(apiParams); + const expectedDialogParams = { + type: 'confirmation', + content: { + type: 'panel', + children: [ + { type: 'heading', value: 'Do you want to sign this transaction ?' }, + { + type: 'text', + value: `**Signer Address:**`, + }, + { + type: 'copyable', + value: '0x04882a372da3dfe1c53170ad75893832469bf87b62b13e84662565c4a88f25cd', + }, + { + type: 'text', + value: `**Contract:**`, + }, + { + type: 'copyable', + value: '0x04882a372da3dfe1c53170ad75893832469bf87b62b13e84662565c4a88f25cd', + }, + { + type: 'text', + value: `**Call Data:**`, + }, + { + type: 'copyable', + value: '[**foo**]', + }, + { + type: 'text', + value: `**Estimated Gas Fee(ETH):**`, + }, + { + type: 'copyable', + value: '0.000022702500105945', + }, + { + type: 'text', + value: `**Network:**`, + }, + { + type: 'copyable', + value: 'Sepolia Testnet', + }, + ], + }, + }; + expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; + expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledWith(expectedDialogParams); + }); + }); + + describe('when account is not deployed', function () { + beforeEach(async function () { + sandbox.stub(utils, 'isAccountDeployed').resolves(false); + }); - it('should throw an error when call data entries can not be converted to a bigNumber', async function () { - sandbox.stub(utils, 'getSigner').callsFake(async () => { - return account1.publicKey; + it('send a transaction for transferring 10 tokens and a transaction for deploy correctly', async function () { + sandbox.stub(utils, 'deployAccount').callsFake(async () => { + return createAccountProxyResp; + }); + sandbox.stub(utils, 'getBalance').callsFake(async () => { + return getBalanceResp[0]; + }); + sandbox.stub(utils, 'estimateAccountDeployFee').callsFake(async () => { + return estimateDeployFeeResp; + }); + const requestObject: SendTransactionRequestParams = { + contractAddress: '0x07394cbe418daa16e42b87ba67372d4ab4a5df0b05c6e554d158458ce245bc10', + contractFuncName: 'transfer', + contractCallData: + '0x0256d8f49882cc9366037415f48fa9fd2b5b7344ded7573ebfcef7c90e3e6b75,100000000000000000000,0', + senderAddress: account1.address, + }; + apiParams.requestParams = requestObject; + const result = await sendTransaction(apiParams); + expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; + expect(walletStub.rpcStubs.snap_manageState).to.have.been.called; + expect(result).to.be.eql(sendTransactionResp); + }); + }); }); - const requestObject: SendTransactionRequestParams = { - contractAddress: account1.address, - contractFuncName: 'get_signer', - contractCallData: '**foo**', - senderAddress: account1.address, - }; - apiParams.requestParams = requestObject; - await expect(sendTransaction(apiParams)).to.be.rejectedWith( - 'contractCallData could not be converted, Cannot convert **foo** to a BigInt', - ); }); }); diff --git a/packages/starknet-snap/test/src/signDeclareTransaction.test.ts b/packages/starknet-snap/test/src/signDeclareTransaction.test.ts index e5353871..3274ae17 100644 --- a/packages/starknet-snap/test/src/signDeclareTransaction.test.ts +++ b/packages/starknet-snap/test/src/signDeclareTransaction.test.ts @@ -41,7 +41,7 @@ describe('Test function: signDeclareTransaction', function () { } as unknown as DeclareSignerDetails; const requestObject: SignDeclareTransactionRequestParams = { - chainId: STARKNET_MAINNET_NETWORK.chainId, + chainId: STARKNET_SEPOLIA_TESTNET_NETWORK.chainId, signerAddress: account1.address, transaction: declarePayload, enableAuthorize: true, diff --git a/packages/starknet-snap/test/src/signDeployAccountTransaction.test.ts b/packages/starknet-snap/test/src/signDeployAccountTransaction.test.ts index 90818e04..e8de8315 100644 --- a/packages/starknet-snap/test/src/signDeployAccountTransaction.test.ts +++ b/packages/starknet-snap/test/src/signDeployAccountTransaction.test.ts @@ -43,7 +43,7 @@ describe('Test function: signDeployAccountTransaction', function () { } as unknown as DeployAccountSignerDetails; const requestObject: SignDeployAccountTransactionRequestParams = { - chainId: STARKNET_MAINNET_NETWORK.chainId, + chainId: STARKNET_SEPOLIA_TESTNET_NETWORK.chainId, signerAddress: account1.address, transaction: declareNDeployPayload, enableAuthorize: true, diff --git a/packages/starknet-snap/test/src/signMessage.test.ts b/packages/starknet-snap/test/src/signMessage.test.ts index c0b96adc..9f6fca21 100644 --- a/packages/starknet-snap/test/src/signMessage.test.ts +++ b/packages/starknet-snap/test/src/signMessage.test.ts @@ -1,4 +1,3 @@ -import { toJson } from '../../src/utils/serializer'; import chai, { expect } from 'chai'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; @@ -6,14 +5,14 @@ import { WalletMock } from '../wallet.mock.test'; import { SnapState } from '../../src/types/snapState'; import { signMessage } from '../../src/signMessage'; import typedDataExample from '../../src/typedData/typedDataExample.json'; -import { ArraySignatureType } from 'starknet'; import { STARKNET_SEPOLIA_TESTNET_NETWORK } from '../../src/utils/constants'; import { account1, + Cairo1Account1, getBip44EntropyStub, signature4SignMessageWithUnfoundAddress, unfoundUserAddress, - signature4SignMessage, + signature4Cairo1SignMessage, } from '../constants.test'; import { getAddressKeyDeriver } from '../../src/utils/keyPair'; import * as utils from '../../src/utils/starknetUtils'; @@ -27,7 +26,7 @@ describe('Test function: signMessage', function () { this.timeout(5000); const walletStub = new WalletMock(); const state: SnapState = { - accContracts: [account1], + accContracts: [], erc20Tokens: [], networks: [STARKNET_SEPOLIA_TESTNET_NETWORK], transactions: [], @@ -58,120 +57,196 @@ describe('Test function: signMessage', function () { sandbox.restore(); }); - it('should sign a message from an user account correctly', async function () { - const result: boolean | ArraySignatureType = await signMessage(apiParams); - const expectedDialogParams = { - type: 'confirmation', - content: { - type: 'panel', - children: [ - { type: 'heading', value: 'Do you want to sign this message?' }, - - { - type: 'text', - value: `**Message:**`, - }, - { - type: 'copyable', - value: toJson(typedDataExample), - }, - { - type: 'text', - value: `**Signer Address:**`, - }, - { - type: 'copyable', - value: `${account1.address}`, - }, - ], - }, - }; - expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; - expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledWith(expectedDialogParams); - expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called; - expect(result).to.be.eql(signature4SignMessage); - }); + describe('when request param validation fail', function () { + let invalidRequest = Object.assign({}, requestObject); + + afterEach(async function () { + invalidRequest = Object.assign({}, requestObject); + apiParams.requestParams = requestObject; + }); + + it('should throw an error if the signerAddress is undefined', async function () { + invalidRequest.signerAddress = undefined; + apiParams.requestParams = invalidRequest; + let result; + try { + result = await signMessage(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + } + }); - it('should sign a message from an unfound user account correctly', async function () { - const requestObject = apiParams.requestParams as SignMessageRequestParams; - requestObject.signerAddress = unfoundUserAddress; - const result = await signMessage(apiParams); - expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; - expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called; - expect(result).to.be.eql(signature4SignMessageWithUnfoundAddress); - requestObject.signerAddress = account1.address; + it('should throw an error if the signerAddress is an invalid address', async function () { + invalidRequest.signerAddress = 'wrongAddress'; + apiParams.requestParams = invalidRequest; + let result; + try { + result = await signMessage(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + } + }); }); - it('should throw error if getKeysFromAddress failed', async function () { - sandbox.stub(utils, 'getKeysFromAddress').throws(new Error()); - let result; - try { + describe('when request param validation pass', function () { + beforeEach(async function () { + apiParams.requestParams = Object.assign({}, requestObject); + }); + + afterEach(async function () { + apiParams.requestParams = Object.assign({}, requestObject); + }); + + it('skip dialog if enableAuthorize is false or omit', async function () { + sandbox.stub(utils, 'isUpgradeRequired').resolves(false); + const paramsObject = apiParams.requestParams as SignMessageRequestParams; + + paramsObject.enableAuthorize = false; await signMessage(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; - expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called; - }); + expect(walletStub.rpcStubs.snap_dialog).to.have.been.callCount(0); - it('should return false if the user not confirmed', async function () { - walletStub.rpcStubs.snap_dialog.resolves(false); - const result = await signMessage(apiParams); - expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; - expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called; - expect(result).to.be.eql(false); - }); + paramsObject.enableAuthorize = undefined; + await signMessage(apiParams); + expect(walletStub.rpcStubs.snap_dialog).to.have.been.callCount(0); - it('should throw an error if the signerAddress is undefined', async function () { - const requestObject: SignMessageRequestParams = { - signerAddress: undefined, - typedDataMessage: typedDataExample, - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await signMessage(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); + paramsObject.enableAuthorize = true; + }); - it('should throw an error if the signerAddress is an invalid address', async function () { - const invalidAddress = 'wrongAddress'; - const requestObject: SignMessageRequestParams = { - signerAddress: invalidAddress, - typedDataMessage: typedDataExample, - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await signMessage(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); + describe('when require upgrade checking fail', function () { + it('should throw error', async function () { + const isUpgradeRequiredStub = sandbox.stub(utils, 'isUpgradeRequired').throws('network error'); + let result; + try { + result = await signMessage(apiParams); + } catch (err) { + result = err; + } finally { + expect(isUpgradeRequiredStub).to.have.been.calledOnceWith(STARKNET_SEPOLIA_TESTNET_NETWORK, account1.address); + expect(result).to.be.an('Error'); + } + }); + }); - it('should skip dialog if enableAuthorize is false', async function () { - const paramsObject = apiParams.requestParams as SignMessageRequestParams; - paramsObject.enableAuthorize = false; - const result = await signMessage(apiParams); - expect(walletStub.rpcStubs.snap_dialog).to.have.been.callCount(0); - expect(result).to.be.eql(signature4SignMessage); - paramsObject.enableAuthorize = true; - }); + describe('when account require upgrade', function () { + let isUpgradeRequiredStub: sinon.SinonStub; + beforeEach(async function () { + isUpgradeRequiredStub = sandbox.stub(utils, 'isUpgradeRequired').resolves(true); + }); + + it('should throw error if upgrade required', async function () { + let result; + try { + result = await signMessage(apiParams); + } catch (err) { + result = err; + } finally { + expect(isUpgradeRequiredStub).to.have.been.calledOnceWith(STARKNET_SEPOLIA_TESTNET_NETWORK, account1.address); + expect(result).to.be.an('Error'); + } + }); + }); + + describe('when account is not require upgrade', function () { + beforeEach(async function () { + apiParams.requestParams = { + ...apiParams.requestParams, + signerAddress: Cairo1Account1.address, + }; + sandbox.stub(utils, 'isUpgradeRequired').resolves(false); + }); + + it('should sign a message from an user account correctly', async function () { + const result = await signMessage(apiParams); + expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; + expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called; + expect(result).to.be.eql(signature4Cairo1SignMessage); + }); + + it('should sign a message from an unfound user account correctly', async function () { + const paramsObject = apiParams.requestParams as SignMessageRequestParams; + paramsObject.signerAddress = unfoundUserAddress; + const result = await signMessage(apiParams); + expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; + expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called; + expect(result).to.be.eql(signature4SignMessageWithUnfoundAddress); + }); + + it('should throw error if getKeysFromAddress failed', async function () { + sandbox.stub(utils, 'getKeysFromAddress').throws(new Error()); + let result; + try { + await signMessage(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + } + expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; + expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called; + }); + + it('should return false if the user not confirmed', async function () { + walletStub.rpcStubs.snap_dialog.resolves(false); + const result = await signMessage(apiParams); + expect(walletStub.rpcStubs.snap_dialog).to.have.been.calledOnce; + expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called; + expect(result).to.be.eql(false); + }); + + it('should throw an error if the signerAddress is undefined', async function () { + const requestObject: SignMessageRequestParams = { + signerAddress: undefined, + typedDataMessage: typedDataExample, + }; + apiParams.requestParams = requestObject; + let result; + try { + result = await signMessage(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + } + }); + + it('should throw an error if the signerAddress is an invalid address', async function () { + const invalidAddress = 'wrongAddress'; + const requestObject: SignMessageRequestParams = { + signerAddress: invalidAddress, + typedDataMessage: typedDataExample, + }; + apiParams.requestParams = requestObject; + let result; + try { + result = await signMessage(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + } + }); + + it('should skip dialog if enableAuthorize is false', async function () { + const paramsObject = apiParams.requestParams as SignMessageRequestParams; + paramsObject.enableAuthorize = false; + const result = await signMessage(apiParams); + expect(walletStub.rpcStubs.snap_dialog).to.have.been.callCount(0); + expect(result).to.be.eql(signature4Cairo1SignMessage); + paramsObject.enableAuthorize = true; + }); - it('should skip dialog if enableAuthorize is omit', async function () { - const paramsObject = apiParams.requestParams as SignMessageRequestParams; - paramsObject.enableAuthorize = undefined; - const result = await signMessage(apiParams); - expect(walletStub.rpcStubs.snap_dialog).to.have.been.callCount(0); - expect(result).to.be.eql(signature4SignMessage); - paramsObject.enableAuthorize = true; + it('should skip dialog if enableAuthorize is omit', async function () { + const paramsObject = apiParams.requestParams as SignMessageRequestParams; + paramsObject.enableAuthorize = undefined; + const result = await signMessage(apiParams); + expect(walletStub.rpcStubs.snap_dialog).to.have.been.callCount(0); + expect(result).to.be.eql(signature4Cairo1SignMessage); + paramsObject.enableAuthorize = true; + }); + }); }); }); diff --git a/packages/starknet-snap/test/src/signTransaction.test.ts b/packages/starknet-snap/test/src/signTransaction.test.ts index 1a85b782..76a7883d 100644 --- a/packages/starknet-snap/test/src/signTransaction.test.ts +++ b/packages/starknet-snap/test/src/signTransaction.test.ts @@ -32,7 +32,7 @@ describe('Test function: signMessage', function () { }; const requestObject: SignTransactionRequestParams = { - chainId: STARKNET_MAINNET_NETWORK.chainId, + chainId: STARKNET_SEPOLIA_TESTNET_NETWORK.chainId, signerAddress: account1.address, transactions: [ { diff --git a/packages/starknet-snap/test/src/upgradeAccContract.test.ts b/packages/starknet-snap/test/src/upgradeAccContract.test.ts new file mode 100644 index 00000000..bd3ed17a --- /dev/null +++ b/packages/starknet-snap/test/src/upgradeAccContract.test.ts @@ -0,0 +1,285 @@ +import chai, { expect } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { WalletMock } from '../wallet.mock.test'; +import * as utils from '../../src/utils/starknetUtils'; +import * as snapUtils from '../../src/utils/snapUtils'; +import { SnapState, VoyagerTransactionType, TransactionStatus } from '../../src/types/snapState'; +import { upgradeAccContract } from '../../src/upgradeAccContract'; +import { STARKNET_SEPOLIA_TESTNET_NETWORK } from '../../src/utils/constants'; +import { account1, estimateFeeResp, getBip44EntropyStub, sendTransactionResp } from '../constants.test'; +import { getAddressKeyDeriver } from '../../src/utils/keyPair'; +import { Mutex } from 'async-mutex'; +import { ApiParams, UpgradeTransactionRequestParams } from '../../src/types/snapApi'; +import { CAIRO_VERSION_LEGACY, ACCOUNT_CLASS_HASH } from '../../src/utils/constants'; +import { CallData, num } from 'starknet'; + +chai.use(sinonChai); +chai.use(chaiAsPromised); +const sandbox = sinon.createSandbox(); + +describe('Test function: upgradeAccContract', function () { + this.timeout(5000); + let walletStub: WalletMock; + let apiParams: ApiParams; + let state: SnapState; + + beforeEach(async function () { + const requestObject = { + contractAddress: account1.address, + chainId: STARKNET_SEPOLIA_TESTNET_NETWORK.chainId, + }; + + walletStub = new WalletMock(); + walletStub.rpcStubs.snap_getBip44Entropy.callsFake(getBip44EntropyStub); + + state = { + accContracts: [account1], + erc20Tokens: [], + networks: [STARKNET_SEPOLIA_TESTNET_NETWORK], + transactions: [], + }; + + apiParams = { + state, + requestParams: requestObject, + wallet: walletStub, + saveMutex: new Mutex(), + keyDeriver: await getAddressKeyDeriver(walletStub), + }; + }); + + afterEach(function () { + walletStub.reset(); + sandbox.restore(); + }); + + describe('when validation fail', function () { + it('should show error when request contractAddress is not given', async function () { + (apiParams.requestParams as UpgradeTransactionRequestParams).contractAddress = undefined; + + let result; + try { + result = await upgradeAccContract(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + expect(result.message).to.be.include('The given contract address need to be non-empty string'); + } + }); + + it('should show error when request contractAddress is invalid', async function () { + (apiParams.requestParams as UpgradeTransactionRequestParams).contractAddress = '0x0'; + + let result; + try { + result = await upgradeAccContract(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + expect(result.message).to.be.include('The given contract address is invalid'); + } + }); + + it('should show error when account is not deployed', async function () { + sandbox.stub(utils, 'isAccountDeployed').resolves(false); + + let result; + try { + result = await upgradeAccContract(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + expect(result.message).to.be.include('Contract has not deployed'); + } + }); + + it('should show error when account is not required to upgrade', async function () { + sandbox.stub(utils, 'isAccountDeployed').resolves(true); + sandbox.stub(utils, 'isUpgradeRequired').resolves(false); + + let result; + try { + result = await upgradeAccContract(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + expect(result.message).to.be.include('Upgrade is not required'); + } + }); + }); + + describe('when validation pass', function () { + let upsertTransactionStub: sinon.SinonStub; + let executeTxnStub: sinon.SinonStub; + let estimateFeeStub: sinon.SinonStub; + + beforeEach(async function () { + sandbox.stub(utils, 'isAccountDeployed').resolves(true); + sandbox.stub(utils, 'isUpgradeRequired').resolves(true); + sandbox.stub(utils, 'getKeysFromAddress').resolves({ + privateKey: 'pk', + publicKey: account1.publicKey, + addressIndex: account1.addressIndex, + derivationPath: `m / bip32:1' / bip32:1' / bip32:1' / bip32:1'`, + }); + upsertTransactionStub = sandbox.stub(snapUtils, 'upsertTransaction'); + executeTxnStub = sandbox.stub(utils, 'executeTxn'); + estimateFeeStub = sandbox.stub(utils, 'estimateFee'); + }); + + it('should use provided max fee to execute txn when max fee provided', async function () { + (apiParams.requestParams as UpgradeTransactionRequestParams).maxFee = '10000'; + walletStub.rpcStubs.snap_dialog.resolves(true); + executeTxnStub.resolves(sendTransactionResp); + + const address = (apiParams.requestParams as UpgradeTransactionRequestParams).contractAddress; + const calldata = CallData.compile({ + implementation: ACCOUNT_CLASS_HASH, + calldata: [0], + }); + + const txnInvocation = { + contractAddress: address, + entrypoint: 'upgrade', + calldata, + }; + + const result = await upgradeAccContract(apiParams); + + expect(executeTxnStub).to.calledOnce; + expect(executeTxnStub).to.calledWith( + STARKNET_SEPOLIA_TESTNET_NETWORK, + address, + 'pk', + txnInvocation, + undefined, + { + maxFee: num.toBigInt(10000), + }, + CAIRO_VERSION_LEGACY, + ); + expect(result).to.be.equal(sendTransactionResp); + }); + + it('should use calculated max fee to execute txn when max fee not provided', async function () { + walletStub.rpcStubs.snap_dialog.resolves(true); + executeTxnStub.resolves(sendTransactionResp); + estimateFeeStub.resolves(estimateFeeResp); + + const address = (apiParams.requestParams as UpgradeTransactionRequestParams).contractAddress; + const calldata = CallData.compile({ + implementation: ACCOUNT_CLASS_HASH, + calldata: [0], + }); + + const txnInvocation = { + contractAddress: address, + entrypoint: 'upgrade', + calldata, + }; + + const result = await upgradeAccContract(apiParams); + + expect(executeTxnStub).to.calledOnce; + expect(executeTxnStub).to.calledWith( + STARKNET_SEPOLIA_TESTNET_NETWORK, + address, + 'pk', + txnInvocation, + undefined, + { + maxFee: num.toBigInt(estimateFeeResp.suggestedMaxFee), + }, + CAIRO_VERSION_LEGACY, + ); + expect(result).to.be.equal(sendTransactionResp); + }); + + it('should return executed txn result when user accept to sign the transaction', async function () { + executeTxnStub.resolves(sendTransactionResp); + estimateFeeStub.resolves(estimateFeeResp); + walletStub.rpcStubs.snap_dialog.resolves(true); + + const result = await upgradeAccContract(apiParams); + + expect(result).to.be.equal(sendTransactionResp); + expect(walletStub.rpcStubs.snap_dialog).to.calledOnce; + expect(executeTxnStub).to.calledOnce; + }); + + it('should return false when user rejected to sign the transaction', async function () { + executeTxnStub.resolves(sendTransactionResp); + estimateFeeStub.resolves(estimateFeeResp); + walletStub.rpcStubs.snap_dialog.resolves(false); + + const result = await upgradeAccContract(apiParams); + + expect(result).to.be.equal(false); + expect(walletStub.rpcStubs.snap_dialog).to.calledOnce; + expect(executeTxnStub).to.not.called; + }); + + it('should return executed txn result when execute transaction success', async function () { + executeTxnStub.resolves(sendTransactionResp); + estimateFeeStub.resolves(estimateFeeResp); + walletStub.rpcStubs.snap_dialog.resolves(true); + + const result = await upgradeAccContract(apiParams); + + expect(result).to.be.equal(sendTransactionResp); + expect(walletStub.rpcStubs.snap_dialog).to.calledOnce; + expect(executeTxnStub).to.calledOnce; + }); + + it('should throw exception when execute transaction result null', async function () { + executeTxnStub.resolves(null); + estimateFeeStub.resolves(estimateFeeResp); + walletStub.rpcStubs.snap_dialog.resolves(true); + + let result; + try { + result = await upgradeAccContract(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + expect(result.message).to.be.include('Transaction hash is not found'); + } + }); + + it('should save transaction when execute transaction success', async function () { + executeTxnStub.resolves(sendTransactionResp); + estimateFeeStub.resolves(estimateFeeResp); + walletStub.rpcStubs.snap_dialog.resolves(true); + const address = (apiParams.requestParams as UpgradeTransactionRequestParams).contractAddress; + const calldata = CallData.compile({ + implementation: ACCOUNT_CLASS_HASH, + calldata: [0], + }); + const txn = { + txnHash: sendTransactionResp.transaction_hash, + txnType: VoyagerTransactionType.INVOKE, + chainId: STARKNET_SEPOLIA_TESTNET_NETWORK.chainId, + senderAddress: address, + contractAddress: address, + contractFuncName: 'upgrade', + contractCallData: CallData.compile(calldata), + finalityStatus: TransactionStatus.RECEIVED, + executionStatus: TransactionStatus.RECEIVED, + status: '', + failureReason: '', + eventIds: [], + }; + + const result = await upgradeAccContract(apiParams); + expect(result).to.be.equal(sendTransactionResp); + expect(upsertTransactionStub).to.calledOnceWith(sinon.match(txn)); + }); + }); +}); diff --git a/packages/starknet-snap/test/src/verifySignedMessage.test.ts b/packages/starknet-snap/test/src/verifySignedMessage.test.ts index 8d3cc22b..5e1aa2ea 100644 --- a/packages/starknet-snap/test/src/verifySignedMessage.test.ts +++ b/packages/starknet-snap/test/src/verifySignedMessage.test.ts @@ -1,13 +1,11 @@ -import { toJson } from '../../src/utils/serializer'; 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 typedDataExample from '../../src/typedData/typedDataExample.json'; import { verifySignedMessage } from '../../src/verifySignedMessage'; import { STARKNET_SEPOLIA_TESTNET_NETWORK } from '../../src/utils/constants'; -import { account1, getBip44EntropyStub, signature1, signature2, unfoundUserAddress } from '../constants.test'; +import { account1, getBip44EntropyStub, signature1 } from '../constants.test'; import { getAddressKeyDeriver } from '../../src/utils/keyPair'; import * as utils from '../../src/utils/starknetUtils'; import { Mutex } from 'async-mutex'; @@ -25,6 +23,7 @@ describe('Test function: verifySignedMessage', function () { networks: [STARKNET_SEPOLIA_TESTNET_NETWORK], transactions: [], }; + const apiParams: ApiParams = { state, requestParams: {}, @@ -32,6 +31,12 @@ describe('Test function: verifySignedMessage', function () { saveMutex: new Mutex(), }; + const requestObject: VerifySignedMessageRequestParams = { + signerAddress: account1.address, + typedDataMessage: undefined, // will use typedDataExample.json + signature: signature1, + }; + beforeEach(async function () { walletStub.rpcStubs.snap_getBip44Entropy.callsFake(getBip44EntropyStub); apiParams.keyDeriver = await getAddressKeyDeriver(walletStub); @@ -42,81 +47,120 @@ describe('Test function: verifySignedMessage', function () { sandbox.restore(); }); - it('should verify a signed message from an user account correctly', async function () { - const requestObject: VerifySignedMessageRequestParams = { - signerAddress: account1.address, - typedDataMessage: undefined, // will use typedDataExample.json - signature: signature1, - }; - apiParams.requestParams = requestObject; - const result = await verifySignedMessage(apiParams); - expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called; - expect(result).to.be.eql(true); - }); + describe('when request param validation fail', function () { + let invalidRequest = Object.assign({}, requestObject); - it('should verify a signed message from an unfound user account correctly', async function () { - const requestObject: VerifySignedMessageRequestParams = { - signerAddress: unfoundUserAddress, - typedDataMessage: toJson(typedDataExample), - signature: signature2, - }; - apiParams.requestParams = requestObject; - const result = await verifySignedMessage(apiParams); - expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called; - expect(result).to.be.eql(true); - }); + afterEach(async function () { + invalidRequest = Object.assign({}, requestObject); + apiParams.requestParams = requestObject; + }); - it('should throw error if getKeysFromAddress failed', async function () { - sandbox.stub(utils, 'getKeysFromAddress').throws(new Error()); - const requestObject: VerifySignedMessageRequestParams = { - signerAddress: account1.address, - typedDataMessage: undefined, // will use typedDataExample.json - signature: signature1, - }; - apiParams.requestParams = requestObject; - - let result; - try { - await verifySignedMessage(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called; - }); + it('should throw an error if the signerAddress is an invalid address', async function () { + invalidRequest.signerAddress = 'wrongAddress'; + apiParams.requestParams = invalidRequest; + let result; + try { + result = await verifySignedMessage(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + } + }); - it('should throw an error if the signerAddress is an invalid address', async function () { - const requestObject: VerifySignedMessageRequestParams = { - signerAddress: 'wrongAddress', - typedDataMessage: undefined, // will use typedDataExample.json - signature: signature1, - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await verifySignedMessage(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } + it('should throw an error if the signature is undefined', async function () { + invalidRequest.signature = undefined; + apiParams.requestParams = invalidRequest; + let result; + try { + result = await verifySignedMessage(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + } + }); }); - it('should throw an error if the signature is undefined', async function () { - const requestObject: VerifySignedMessageRequestParams = { - signerAddress: account1.address, - typedDataMessage: undefined, // will use typedDataExample.json - signature: undefined, - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await verifySignedMessage(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 require upgrade checking fail', function () { + it('should throw error', async function () { + const isUpgradeRequiredStub = sandbox.stub(utils, 'isUpgradeRequired').throws('network error'); + let result; + try { + result = await verifySignedMessage(apiParams); + } catch (err) { + result = err; + } finally { + expect(isUpgradeRequiredStub).to.have.been.calledOnceWith(STARKNET_SEPOLIA_TESTNET_NETWORK, account1.address); + expect(result).to.be.an('Error'); + } + }); + }); + + describe('when account require upgrade', function () { + let isUpgradeRequiredStub: sinon.SinonStub; + beforeEach(async function () { + isUpgradeRequiredStub = sandbox.stub(utils, 'isUpgradeRequired').resolves(true); + }); + + it('should throw error if upgrade required', async function () { + let result; + try { + result = await verifySignedMessage(apiParams); + } catch (err) { + result = err; + } finally { + expect(isUpgradeRequiredStub).to.have.been.calledOnceWith(STARKNET_SEPOLIA_TESTNET_NETWORK, account1.address); + expect(result).to.be.an('Error'); + } + }); + }); + + describe('when account is not require upgrade', function () { + beforeEach(async function () { + sandbox.stub(utils, 'isUpgradeRequired').resolves(false); + }); + + it('should verify a signed message from an user account correctly', async function () { + const requestObject: VerifySignedMessageRequestParams = { + signerAddress: account1.address, + typedDataMessage: undefined, // will use typedDataExample.json + signature: signature1, + }; + apiParams.requestParams = requestObject; + const result = await verifySignedMessage(apiParams); + expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called; + expect(result).to.be.eql(true); + }); + + it('should throw error if getKeysFromAddress failed', async function () { + sandbox.stub(utils, 'getKeysFromAddress').throws(new Error()); + const requestObject: VerifySignedMessageRequestParams = { + signerAddress: account1.address, + typedDataMessage: undefined, // will use typedDataExample.json + signature: signature1, + }; + apiParams.requestParams = requestObject; + + let result; + try { + await verifySignedMessage(apiParams); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + } + expect(walletStub.rpcStubs.snap_manageState).not.to.have.been.called; + }); + }); }); }); diff --git a/packages/starknet-snap/test/utils/starknetUtils.test.ts b/packages/starknet-snap/test/utils/starknetUtils.test.ts index f1ff6c8d..7ef654f2 100644 --- a/packages/starknet-snap/test/utils/starknetUtils.test.ts +++ b/packages/starknet-snap/test/utils/starknetUtils.test.ts @@ -3,7 +3,7 @@ import sinon from 'sinon'; import sinonChai from 'sinon-chai'; import { WalletMock } from '../wallet.mock.test'; import * as utils from '../../src/utils/starknetUtils'; -import { STARKNET_SEPOLIA_TESTNET_NETWORK } from '../../src/utils/constants'; +import { STARKNET_SEPOLIA_TESTNET_NETWORK, CAIRO_VERSION, CAIRO_VERSION_LEGACY } from '../../src/utils/constants'; import { getAddressKeyDeriver } from '../../src/utils/keyPair'; import { getTxnFromVoyagerResp1, @@ -11,13 +11,120 @@ import { getBip44EntropyStub, account1, account2, + account3, } from '../constants.test'; import { SnapState } from '../../src/types/snapState'; -import { Calldata, GetTransactionReceiptResponse } from 'starknet'; +import { Calldata, num, Account, Provider, GetTransactionReceiptResponse } from 'starknet'; +import { hexToString } from '../../src/utils/formatterUtils'; chai.use(sinonChai); const sandbox = sinon.createSandbox(); +describe('Test function: getAccountInstance', function () { + const provider = {} as Provider; + + beforeEach(function () { + sandbox.stub(utils, 'getProvider').returns(provider); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('should return account instance with default cairo version', async function () { + const result = await utils.getAccountInstance( + STARKNET_SEPOLIA_TESTNET_NETWORK, + account1.address, + account1.publicKey, + ); + expect(result).to.be.instanceOf(Account); + expect(result.cairoVersion).to.equal(CAIRO_VERSION); + }); + + it('should return account instance with provided cairo version', async function () { + const result = await utils.getAccountInstance( + STARKNET_SEPOLIA_TESTNET_NETWORK, + account1.address, + account1.publicKey, + CAIRO_VERSION_LEGACY, + ); + expect(result).to.be.instanceOf(Account); + expect(result.cairoVersion).to.equal(CAIRO_VERSION_LEGACY); + }); +}); + +describe('Test function: findAddressIndex', function () { + const state: SnapState = { + accContracts: [], + erc20Tokens: [], + networks: [STARKNET_SEPOLIA_TESTNET_NETWORK], + transactions: [], + }; + + beforeEach(function () { + sandbox.stub(utils, 'getKeysFromAddressIndex').resolves({ + privateKey: 'pk', + publicKey: 'pubkey', + addressIndex: 1, + derivationPath: `m / bip32:1' / bip32:1' / bip32:1' / bip32:1'`, + }); + sandbox.stub(utils, 'getPermutationAddresses').returns({ + address: account1.address, + addressLegacy: account2.address, + }); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('should find address index', async function () { + const result = await utils.findAddressIndex( + STARKNET_SEPOLIA_TESTNET_NETWORK.chainId, + account1.address, + 'div', + state, + 1, + ); + expect(result).to.be.contains({ + index: 0, + cairoVersion: 1, + }); + }); + + it('should find address index address match account legacy', async function () { + const result = await utils.findAddressIndex( + STARKNET_SEPOLIA_TESTNET_NETWORK.chainId, + account2.address, + 'div', + state, + 1, + ); + expect(result).to.be.contains({ + index: 0, + cairoVersion: 0, + }); + }); + + it('should throw error if address not found', async function () { + let result = null; + try { + result = await utils.findAddressIndex( + STARKNET_SEPOLIA_TESTNET_NETWORK.chainId, + account3.address, + 'div', + state, + 1, + ); + } catch (err) { + result = err; + } finally { + expect(result).to.be.an('Error'); + expect(result?.message).to.be.eq(`Address not found: ${account3.address}`); + } + }); +}); + describe('Test function: callContract', function () { const walletStub = new WalletMock(); const userAddress = '0x27f204588cadd08a7914f6a9808b34de0cbfc4cb53aa053663e7fd3a34dbc26'; @@ -130,6 +237,349 @@ describe('Test function: validateAndParseAddress', function () { }); }); +describe('Test function: getPermutationAddresses', function () { + let getAccContractAddressAndCallDataStub: sinon.SinonStub; + let getAccContractAddressAndCallDataLegacy: sinon.SinonStub; + const PK = 'PK'; + + beforeEach(function () { + getAccContractAddressAndCallDataStub = sandbox + .stub(utils, 'getAccContractAddressAndCallData') + .returns({ address: account1.address, callData: [] as Calldata }); + getAccContractAddressAndCallDataLegacy = sandbox + .stub(utils, 'getAccContractAddressAndCallDataLegacy') + .returns({ address: account2.address, callData: [] as Calldata }); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('should return all addresses', async function () { + const result = await utils.getPermutationAddresses(PK); + expect(result).to.be.contains({ + address: account1.address, + addressLegacy: account2.address, + }); + expect(getAccContractAddressAndCallDataStub).to.have.been.calledOnceWith(PK); + expect(getAccContractAddressAndCallDataLegacy).to.have.been.calledOnceWith(PK); + }); +}); + +describe('Test function: getVersion', function () { + let callContractStub: sinon.SinonStub; + const expected = '0.3.0'; + + beforeEach(function () { + callContractStub = sandbox.stub(utils, 'callContract').callsFake(async () => [expected]); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('should trigger callContract correct', async function () { + const result = await utils.getVersion(account1.address, STARKNET_SEPOLIA_TESTNET_NETWORK); + expect(result).to.be.eq(expected); + expect(callContractStub).to.have.been.calledOnceWith( + STARKNET_SEPOLIA_TESTNET_NETWORK, + account1.address, + 'getVersion', + ); + }); +}); + +describe('Test function: getOwner', function () { + let callContractStub: sinon.SinonStub; + const expected = 'pk'; + + beforeEach(function () { + callContractStub = sandbox.stub(utils, 'callContract').callsFake(async () => [expected]); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('should trigger callContract correct', async function () { + const result = await utils.getOwner(account1.address, STARKNET_SEPOLIA_TESTNET_NETWORK); + expect(result).to.be.eq(expected); + expect(callContractStub).to.have.been.calledOnceWith( + STARKNET_SEPOLIA_TESTNET_NETWORK, + account1.address, + 'get_owner', + ); + }); +}); + +describe('Test function: getBalance', function () { + let callContractStub: sinon.SinonStub; + const expected = 'pk'; + + beforeEach(function () { + callContractStub = sandbox.stub(utils, 'callContract').callsFake(async () => [expected]); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('should trigger callContract correct', async function () { + const result = await utils.getBalance(account1.address, account1.address, STARKNET_SEPOLIA_TESTNET_NETWORK); + expect(result).to.be.eq(expected); + expect(callContractStub).to.have.been.calledOnceWith( + STARKNET_SEPOLIA_TESTNET_NETWORK, + account1.address, + 'balanceOf', + [num.toBigInt(account1.address).toString(10)], + ); + }); +}); + +describe('Test function: isUpgradeRequired', function () { + const walletStub = new WalletMock(); + const userAddress = '0x27f204588cadd08a7914f6a9808b34de0cbfc4cb53aa053663e7fd3a34dbc26'; + + afterEach(function () { + walletStub.reset(); + sandbox.restore(); + }); + + it('should return true when upgrade is required', async function () { + sandbox.stub(utils, 'getVersion').callsFake(async () => '0x302e322e33'); + const result = await utils.isUpgradeRequired(STARKNET_SEPOLIA_TESTNET_NETWORK, userAddress); + expect(result).to.be.eq(true); + }); + + it('should return false when upgrade is not required', async function () { + sandbox.stub(utils, 'getVersion').callsFake(async () => '0x302e332e30'); + const result = await utils.isUpgradeRequired(STARKNET_SEPOLIA_TESTNET_NETWORK, userAddress); + expect(result).to.be.eq(false); + }); + + it('should return false when contract is not deployed', async function () { + sandbox.stub(utils, 'getVersion').callsFake(async () => { + throw new Error('Contract not found'); + }); + const result = await utils.isUpgradeRequired(STARKNET_SEPOLIA_TESTNET_NETWORK, userAddress); + expect(result).to.be.eq(false); + }); + + it('should throw err when getVersion is throwing unknown error', async function () { + sandbox.stub(utils, 'getVersion').callsFake(async () => { + throw new Error('network error'); + }); + let result = null; + try { + await utils.isUpgradeRequired(STARKNET_SEPOLIA_TESTNET_NETWORK, userAddress); + } catch (e) { + result = e; + } finally { + expect(result).to.be.an('Error'); + expect(result?.message).to.be.eq('network error'); + } + }); +}); + +describe('Test function: isGTEMinVersion', function () { + const cairoVersionHex = '0x302e332e30'; + const cairoVersionLegacyHex = '302e322e30a'; + + it(`should return true when version greater than or equal to min version`, function () { + expect(utils.isGTEMinVersion(hexToString(cairoVersionHex))).to.be.eq(true); + }); + + it(`should return false when version smaller than min version`, function () { + expect(utils.isGTEMinVersion(hexToString(cairoVersionLegacyHex))).to.be.eq(false); + }); +}); + +describe('Test function: getContractOwner', function () { + let getOwnerStub: sinon.SinonStub; + let getSignerStub: sinon.SinonStub; + + beforeEach(function () { + getOwnerStub = sandbox.stub(utils, 'getOwner'); + getSignerStub = sandbox.stub(utils, 'getSigner'); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it(`should call getOwner when cairo version is ${CAIRO_VERSION}`, async function () { + await utils.getContractOwner(account1.address, STARKNET_SEPOLIA_TESTNET_NETWORK, CAIRO_VERSION); + + expect(getOwnerStub).to.have.been.callCount(1); + expect(getSignerStub).to.have.been.callCount(0); + }); + + it(`should call getSigner when cairo version is ${CAIRO_VERSION_LEGACY}`, async function () { + await utils.getContractOwner(account1.address, STARKNET_SEPOLIA_TESTNET_NETWORK, CAIRO_VERSION_LEGACY); + + expect(getOwnerStub).to.have.been.callCount(0); + expect(getSignerStub).to.have.been.callCount(1); + }); +}); + +describe('Test function: getCorrectContractAddress', function () { + const walletStub = new WalletMock(); + let getAccContractAddressAndCallDataStub: sinon.SinonStub; + let getAccContractAddressAndCallDataLegacyStub: sinon.SinonStub; + let getOwnerStub: sinon.SinonStub; + let getSignerStub: sinon.SinonStub; + let getVersionStub: sinon.SinonStub; + + const PK = 'pk'; + const cairoVersionHex = '0x302e332e30'; + const cairoVersionLegacyHex = '302e322e30a'; + + beforeEach(function () { + getAccContractAddressAndCallDataStub = sandbox + .stub(utils, 'getAccContractAddressAndCallData') + .returns({ address: account1.address, callData: [] as Calldata }); + getAccContractAddressAndCallDataLegacyStub = sandbox + .stub(utils, 'getAccContractAddressAndCallDataLegacy') + .returns({ address: account2.address, callData: [] as Calldata }); + }); + + afterEach(function () { + walletStub.reset(); + sandbox.restore(); + }); + + it(`should permutation both Cairo${CAIRO_VERSION_LEGACY} and Cairo${CAIRO_VERSION} address`, async function () { + sandbox.stub(utils, 'getOwner').callsFake(async () => PK); + sandbox.stub(utils, 'getSigner').callsFake(async () => PK); + sandbox.stub(utils, 'getVersion').callsFake(async () => cairoVersionHex); + + await utils.getCorrectContractAddress(STARKNET_SEPOLIA_TESTNET_NETWORK, PK); + expect(getAccContractAddressAndCallDataStub).to.have.been.calledOnceWith(PK); + expect(getAccContractAddressAndCallDataLegacyStub).to.have.been.calledOnceWith(PK); + }); + + it('should throw error when getOwner is throwing unknown error', async function () { + sandbox.stub(utils, 'getVersion').resolves(cairoVersionHex); + getOwnerStub = sandbox.stub(utils, 'getOwner').rejects(new Error('network error for getOwner')); + getSignerStub = sandbox.stub(utils, 'getSigner').callsFake(async () => PK); + + let result = null; + try { + await utils.getCorrectContractAddress(STARKNET_SEPOLIA_TESTNET_NETWORK, PK); + } catch (e) { + result = e; + } finally { + expect(getOwnerStub).to.have.been.calledOnceWith(account1.address, STARKNET_SEPOLIA_TESTNET_NETWORK); + expect(getSignerStub).to.have.been.callCount(0); + expect(result).to.be.an('Error'); + expect(result?.message).to.be.eq('network error for getOwner'); + } + }); + + it('should throw error when getSigner is throwing unknown error', async function () { + sandbox + .stub(utils, 'getVersion') + .withArgs(account1.address, STARKNET_SEPOLIA_TESTNET_NETWORK) + .rejects(new Error('Contract not found')) + .withArgs(account2.address, STARKNET_SEPOLIA_TESTNET_NETWORK) + .resolves(cairoVersionLegacyHex); + + getSignerStub = sandbox.stub(utils, 'getSigner').rejects(new Error('network error for getSigner')); + + let result = null; + try { + await utils.getCorrectContractAddress(STARKNET_SEPOLIA_TESTNET_NETWORK, PK); + } catch (e) { + result = e; + } finally { + expect(getSignerStub).to.have.been.calledOnceWith(account2.address, STARKNET_SEPOLIA_TESTNET_NETWORK); + expect(result).to.be.an('Error'); + expect(result?.message).to.be.eq('network error for getSigner'); + } + }); + + describe(`when contact is Cairo${CAIRO_VERSION} has deployed`, function () { + it(`should return Cairo${CAIRO_VERSION} address with pubic key`, async function () { + getVersionStub = sandbox.stub(utils, 'getVersion').resolves(cairoVersionHex); + getSignerStub = sandbox.stub(utils, 'getSigner').resolves(PK); + getOwnerStub = sandbox.stub(utils, 'getOwner').resolves(PK); + + const result = await utils.getCorrectContractAddress(STARKNET_SEPOLIA_TESTNET_NETWORK, PK); + expect(getVersionStub).to.have.been.calledOnceWith(account1.address, STARKNET_SEPOLIA_TESTNET_NETWORK); + expect(getOwnerStub).to.have.been.calledOnceWith(account1.address, STARKNET_SEPOLIA_TESTNET_NETWORK); + expect(getSignerStub).to.have.been.callCount(0); + expect(result.address).to.be.eq(account1.address); + expect(result.signerPubKey).to.be.eq(PK); + expect(result.upgradeRequired).to.be.eq(false); + }); + }); + + describe(`when contact is Cairo${CAIRO_VERSION} has not deployed`, function () { + describe(`when when is Cairo${CAIRO_VERSION_LEGACY} has deployed`, function () { + describe(`when when is Cairo${CAIRO_VERSION_LEGACY} has upgraded`, function () { + it(`should return Cairo${CAIRO_VERSION_LEGACY} address with upgrade = false`, async function () { + sandbox + .stub(utils, 'getVersion') + .withArgs(account1.address, STARKNET_SEPOLIA_TESTNET_NETWORK) + .rejects(new Error('Contract not found')) + .withArgs(account2.address, STARKNET_SEPOLIA_TESTNET_NETWORK) + .resolves(cairoVersionHex); + + getSignerStub = sandbox.stub(utils, 'getSigner').resolves(PK); + getOwnerStub = sandbox.stub(utils, 'getOwner').resolves(PK); + + const result = await utils.getCorrectContractAddress(STARKNET_SEPOLIA_TESTNET_NETWORK, PK); + + expect(getOwnerStub).to.have.been.calledOnceWith(account2.address, STARKNET_SEPOLIA_TESTNET_NETWORK); + expect(getSignerStub).to.have.been.callCount(0); + expect(result.address).to.be.eq(account2.address); + expect(result.signerPubKey).to.be.eq(PK); + expect(result.upgradeRequired).to.be.eq(false); + }); + }); + + describe(`when when is Cairo${CAIRO_VERSION_LEGACY} has not upgraded`, function () { + it(`should return Cairo${CAIRO_VERSION_LEGACY} address with upgrade = true`, async function () { + sandbox + .stub(utils, 'getVersion') + .withArgs(account1.address, STARKNET_SEPOLIA_TESTNET_NETWORK) + .rejects(new Error('Contract not found')) + .withArgs(account2.address, STARKNET_SEPOLIA_TESTNET_NETWORK) + .resolves(cairoVersionLegacyHex); + + getSignerStub = sandbox.stub(utils, 'getSigner').resolves(PK); + getOwnerStub = sandbox.stub(utils, 'getOwner').resolves(PK); + + const result = await utils.getCorrectContractAddress(STARKNET_SEPOLIA_TESTNET_NETWORK, PK); + + expect(getSignerStub).to.have.been.calledOnceWith(account2.address, STARKNET_SEPOLIA_TESTNET_NETWORK); + expect(getOwnerStub).to.have.been.callCount(0); + expect(result.address).to.be.eq(account2.address); + expect(result.signerPubKey).to.be.eq(PK); + expect(result.upgradeRequired).to.be.eq(true); + }); + }); + }); + + describe(`when when is Cairo${CAIRO_VERSION_LEGACY} has not deployed`, function () { + it(`should return Cairo${CAIRO_VERSION} address with upgrade = false`, async function () { + sandbox.stub(utils, 'getVersion').rejects(new Error('Contract not found')); + + getSignerStub = sandbox.stub(utils, 'getSigner').resolves(PK); + getOwnerStub = sandbox.stub(utils, 'getOwner').resolves(PK); + + const result = await utils.getCorrectContractAddress(STARKNET_SEPOLIA_TESTNET_NETWORK, PK); + + expect(getSignerStub).to.have.been.callCount(0); + expect(getOwnerStub).to.have.been.callCount(0); + expect(result.address).to.be.eq(account1.address); + expect(result.signerPubKey).to.be.eq(''); + expect(result.upgradeRequired).to.be.eq(false); + }); + }); + }); +}); + describe('Test function: waitForTransaction', function () { const walletStub = new WalletMock(); const userAddress = '0x27f204588cadd08a7914f6a9808b34de0cbfc4cb53aa053663e7fd3a34dbc26'; diff --git a/packages/wallet-ui/package.json b/packages/wallet-ui/package.json index 05af15db..f5657cd7 100644 --- a/packages/wallet-ui/package.json +++ b/packages/wallet-ui/package.json @@ -26,7 +26,7 @@ "react-qr-code": "^2.0.7", "react-redux": "^8.0.1", "redux-persist": "^6.0.0", - "semver": "^7.3.7", + "semver": "^7.5.2", "starknet": "^4.22.0", "styled-components": "^5.3.5", "toastr2": "^3.0.0-alpha.18", diff --git a/packages/wallet-ui/src/App.tsx b/packages/wallet-ui/src/App.tsx index dabd8ba9..d37475e8 100644 --- a/packages/wallet-ui/src/App.tsx +++ b/packages/wallet-ui/src/App.tsx @@ -14,6 +14,7 @@ import { ConnectModal } from 'components/ui/organism/ConnectModal'; import { PopIn } from 'components/ui/molecule/PopIn'; import { LoadingBackdrop } from 'components/ui/molecule/LoadingBackdrop'; import { ConnectInfoModal } from 'components/ui/organism/ConnectInfoModal'; +import { UpgradeModel } from 'components/ui/organism/UpgradeModel'; import 'toastr2/dist/toastr.min.css'; import { NoMetamaskModal } from 'components/ui/organism/NoMetamaskModal'; import { MinVersionModal } from './components/ui/organism/MinVersionModal'; @@ -25,7 +26,7 @@ library.add(fas, far); function App() { const { initSnap, getWalletData, checkConnection } = useStarkNetSnap(); const { connected, forceReconnect, provider } = useAppSelector((state) => state.wallet); - const { infoModalVisible, minVersionModalVisible } = useAppSelector((state) => state.modals); + const { infoModalVisible, minVersionModalVisible, upgradeModalVisible } = useAppSelector((state) => state.modals); const { loader } = useAppSelector((state) => state.UI); const networks = useAppSelector((state) => state.networks); const { accounts } = useAppSelector((state) => state.wallet); @@ -72,6 +73,9 @@ function App() { + + + {loading && {loader.loadingMessage}} diff --git a/packages/wallet-ui/src/components/pages/Home/Home.view.tsx b/packages/wallet-ui/src/components/pages/Home/Home.view.tsx index 318e190a..7be15eb6 100644 --- a/packages/wallet-ui/src/components/pages/Home/Home.view.tsx +++ b/packages/wallet-ui/src/components/pages/Home/Home.view.tsx @@ -10,14 +10,15 @@ interface Props { export const HomeView = ({ address }: Props) => { const { erc20TokenBalanceSelected, transactions } = useAppSelector((state) => state.wallet); const loader = useAppSelector((state) => state.UI.loader); + const { upgradeModalVisible } = useAppSelector((state) => state.modals); return ( - {Object.keys(erc20TokenBalanceSelected).length > 0 &&
} - - {Object.keys(transactions).length === 0 && !loader.isLoading && ( + {!upgradeModalVisible && Object.keys(erc20TokenBalanceSelected).length > 0 &&
} + {!upgradeModalVisible && } + {!upgradeModalVisible && Object.keys(transactions).length === 0 && !loader.isLoading && ( You have no transactions )} diff --git a/packages/wallet-ui/src/components/ui/molecule/TransactionsList/TransactionListItem/types.ts b/packages/wallet-ui/src/components/ui/molecule/TransactionsList/TransactionListItem/types.ts index db89eeb7..856d9435 100644 --- a/packages/wallet-ui/src/components/ui/molecule/TransactionsList/TransactionListItem/types.ts +++ b/packages/wallet-ui/src/components/ui/molecule/TransactionsList/TransactionListItem/types.ts @@ -21,6 +21,8 @@ export const getTxnName = (transaction: Transaction): string => { if (transaction.txnType.toLowerCase() === VoyagerTransactionType.INVOKE) { if (transaction.contractFuncName.toLowerCase() === 'transfer') { return 'Send'; + } else if (transaction.contractFuncName.toLowerCase() === 'upgrade') { + return 'Upgrade Account'; } } else if (transaction.txnType.toLowerCase() === VoyagerTransactionType.DEPLOY) { return 'Deploy'; @@ -92,7 +94,10 @@ export const getTxnValues = (transaction: Transaction, decimals: number = 18, to switch (txnName) { case 'Send': case 'Receive': - txnValue = ethers.utils.formatUnits(transaction.contractCallData[1].toString(), decimals); + txnValue = ethers.utils.formatUnits( + transaction.contractCallData[transaction.contractCallData.length - 2].toString(), + decimals, + ); txnUsdValue = (parseFloat(txnValue) * toUsdRate).toFixed(2); break; default: diff --git a/packages/wallet-ui/src/components/ui/organism/UpgradeModel/UpgradeModel.stories.tsx b/packages/wallet-ui/src/components/ui/organism/UpgradeModel/UpgradeModel.stories.tsx new file mode 100644 index 00000000..4e3f0acb --- /dev/null +++ b/packages/wallet-ui/src/components/ui/organism/UpgradeModel/UpgradeModel.stories.tsx @@ -0,0 +1,23 @@ +import { Meta } from '@storybook/react'; +import { useState } from 'react'; +import { PopIn } from 'components/ui/molecule/PopIn'; +import { UpgradeModelView } from './UpgradeModel.view'; + +export default { + title: 'Organism/UpgradeModel', + component: UpgradeModelView, +} as Meta; + +export const ContentOnly = () => ; + +export const WithModal = () => { + let [isOpen, setIsOpen] = useState(false); + return ( + <> + + + + + + ); +}; diff --git a/packages/wallet-ui/src/components/ui/organism/UpgradeModel/UpgradeModel.style.ts b/packages/wallet-ui/src/components/ui/organism/UpgradeModel/UpgradeModel.style.ts new file mode 100644 index 00000000..4627b0db --- /dev/null +++ b/packages/wallet-ui/src/components/ui/organism/UpgradeModel/UpgradeModel.style.ts @@ -0,0 +1,64 @@ +import styled from 'styled-components'; +import starknetSrc from 'assets/images/starknet-logo.svg'; +import { Button } from 'components/ui/atom/Button'; + +export const Wrapper = styled.div` + display: flex; + flex-direction: column; + background-color: ${(props) => props.theme.palette.grey.white}; + width: ${(props) => props.theme.modal.base}; + padding: ${(props) => props.theme.spacing.base}; + padding-top: 40px; + border-radius: 8px; + align-items: center; +`; + +export const StarknetLogo = styled.img.attrs(() => ({ + src: starknetSrc, +}))` + width: 158px; + height: 32px; + margin-bottom: 32px; +`; + +export const Title = styled.div` + text-align: center; + font-weight: ${(props) => props.theme.typography.h3.fontWeight}; + font-size: ${(props) => props.theme.typography.h3.fontSize}; + font-family: ${(props) => props.theme.typography.h3.fontFamily}; + line-height: ${(props) => props.theme.typography.h3.lineHeight}; + margin-bottom: 8px; +`; + +export const Description = styled.div` + font-size: ${(props) => props.theme.typography.p2.fontSize}; + color: ${(props) => props.theme.palette.grey.grey1}; +`; + +export const DescriptionCentered = styled(Description)` + text-align: center; + width: 264px; +`; + +export const Txnlink = styled.div` + margin-top: 12px; + font-size: ${(props) => props.theme.typography.p2.fontSize}; + color: ${(props) => props.theme.palette.primary.main}; + font-weight: ${(props) => props.theme.typography.bold.fontWeight}; + font-family: ${(props) => props.theme.typography.bold.fontFamily}; + text-decoration: underline; + cursor: pointer; +`; + +export const UpgradeButton = styled(Button).attrs((props) => ({ + textStyle: { + fontSize: props.theme.typography.p1.fontSize, + fontWeight: 900, + }, + upperCaseOnly: false, + backgroundTransparent: true, +}))` + box-shadow: 0px 14px 24px -6px rgba(106, 115, 125, 0.2); + padding-top: 16px; + padding-bottom: 16px; +`; diff --git a/packages/wallet-ui/src/components/ui/organism/UpgradeModel/UpgradeModel.view.tsx b/packages/wallet-ui/src/components/ui/organism/UpgradeModel/UpgradeModel.view.tsx new file mode 100644 index 00000000..55602fdb --- /dev/null +++ b/packages/wallet-ui/src/components/ui/organism/UpgradeModel/UpgradeModel.view.tsx @@ -0,0 +1,122 @@ +import { useEffect, useState } from 'react'; +import { useStarkNetSnap } from 'services'; +import { useAppSelector, useAppDispatch } from 'hooks/redux'; +import Toastr from 'toastr2'; + +import { setUpgradeModalVisible } from 'slices/modalSlice'; +import { openExplorerTab, shortenAddress } from '../../../../utils/utils'; +import { UpgradeButton, StarknetLogo, Title, Wrapper, DescriptionCentered, Txnlink } from './UpgradeModel.style'; + +interface Props { + address: string; +} + +enum Stage { + INIT = 0, + WAITING_FOR_TXN = 1, + SUCCESS = 2, + FAIL = 3, +} + +export const UpgradeModelView = ({ address }: Props) => { + const dispatch = useAppDispatch(); + const { upgradeAccount, waitForAccountUpdate } = useStarkNetSnap(); + const [txnHash, setTxnHash] = useState(''); + const [stage, setStage] = useState(Stage.INIT); + const networks = useAppSelector((state) => state.networks); + const chainId = networks?.items[networks.activeNetwork]?.chainId; + const toastr = new Toastr(); + + const onUpgrade = async () => { + try { + const resp = await upgradeAccount(address, '0', chainId); + + if (resp === false) { + return; + } + + if (resp.transaction_hash) { + setTxnHash(resp.transaction_hash); + } else { + throw new Error('no transaction hash'); + } + } catch (err) { + //eslint-disable-next-line no-console + console.error(err); + toastr.error(`Upgrade account failed`); + } + }; + + useEffect(() => { + if (txnHash) { + setStage(Stage.WAITING_FOR_TXN); + waitForAccountUpdate(txnHash, address, chainId) + .then((resp) => { + setStage(resp === true ? Stage.SUCCESS : Stage.FAIL); + }) + .catch(() => { + setStage(Stage.FAIL); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [txnHash, address, chainId]); + + useEffect(() => { + if (stage === Stage.SUCCESS) { + toastr.success(`Account upgraded successfully`); + dispatch(setUpgradeModalVisible(false)); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [stage, dispatch]); + + const renderComponent = () => { + switch (stage) { + case Stage.INIT: + return ( + <> + + A new version of the smart contract
+ is necessary to proceed with the Snap. +
+
+ New enhancements will come with
+ this version. +
+
+ Click on the "Upgrade" button to install it. +
+ Thank you! +
+ Upgrade + + ); + case Stage.WAITING_FOR_TXN: + return Waiting for transaction to be complete.; + case Stage.SUCCESS: + return Account upgraded successfully.; + default: + return ( + + Transaction Hash:
{' '} + openExplorerTab(txnHash, 'tx', chainId)}>{shortenAddress(txnHash)} +
+ Unfortunately, you reached the maximum number of upgrade tentatives allowed. +
+
+ Please try again in a couple of hours. +
+
+ Thank you for your comprehension. +
+ ); + } + }; + + return ( + + + Upgrade Account + {renderComponent()} + + ); +}; diff --git a/packages/wallet-ui/src/components/ui/organism/UpgradeModel/index.ts b/packages/wallet-ui/src/components/ui/organism/UpgradeModel/index.ts new file mode 100644 index 00000000..632833b1 --- /dev/null +++ b/packages/wallet-ui/src/components/ui/organism/UpgradeModel/index.ts @@ -0,0 +1 @@ +export { UpgradeModelView as UpgradeModel } from './UpgradeModel.view'; diff --git a/packages/wallet-ui/src/services/useStarkNetSnap.ts b/packages/wallet-ui/src/services/useStarkNetSnap.ts index f1130384..04ed4b2b 100644 --- a/packages/wallet-ui/src/services/useStarkNetSnap.ts +++ b/packages/wallet-ui/src/services/useStarkNetSnap.ts @@ -1,4 +1,4 @@ -import { setInfoModalVisible, setMinVersionModalVisible } from 'slices/modalSlice'; +import { setInfoModalVisible, setMinVersionModalVisible, setUpgradeModalVisible } from 'slices/modalSlice'; import { setNetworks } from 'slices/networkSlice'; import { useAppDispatch, useAppSelector } from 'hooks/redux'; import { @@ -11,7 +11,7 @@ import { setForceReconnect, } from '../slices/walletSlice'; import Toastr from 'toastr2'; -import { addMissingPropertiesToToken } from '../utils/utils'; +import { addMissingPropertiesToToken, hexToString, retry, isGTEMinVersion } from '../utils/utils'; import { setWalletConnection } from '../slices/walletSlice'; import { Network, VoyagerTransactionType } from '../types'; import { Account } from '../types'; @@ -31,6 +31,9 @@ export const useStarkNetSnap = () => { const snapVersion = process.env.REACT_APP_SNAP_VERSION ? process.env.REACT_APP_SNAP_VERSION : '*'; const minSnapVersion = process.env.REACT_APP_MIN_SNAP_VERSION ? process.env.REACT_APP_MIN_SNAP_VERSION : '2.0.1'; const debugLevel = process.env.REACT_APP_DEBUG_LEVEL !== undefined ? process.env.REACT_APP_DEBUG_LEVEL : 'all'; + const START_SCAN_INDEX = 0; + const MAX_SCANNED = 1; + const MAX_MISSED = 1; const defaultParam = { debugLevel, @@ -114,10 +117,12 @@ export const useStarkNetSnap = () => { return tokens; }; - const recoverAccounts = async (chainId: string) => { - const START_SCAN_INDEX = 0; - const MAX_SCANNED = 1; - const MAX_MISSED = 1; + const recoverAccounts = async ( + chainId: string, + start: number = START_SCAN_INDEX, + maxScan: number = MAX_SCANNED, + maxMiss: number = MAX_MISSED, + ) => { const scannedAccounts = (await provider.request({ method: 'wallet_invokeSnap', params: { @@ -126,9 +131,9 @@ export const useStarkNetSnap = () => { method: 'starkNet_recoverAccounts', params: { ...defaultParam, - startScanIndex: START_SCAN_INDEX, - maxScanned: MAX_SCANNED, - maxMissed: MAX_MISSED, + startScanIndex: start, + maxScanned: maxScan, + maxMissed: maxMiss, chainId, }, }, @@ -163,7 +168,7 @@ export const useStarkNetSnap = () => { method: 'starkNet_createAccount', params: { ...defaultParam, - addressIndex: 0, + addressIndex: START_SCAN_INDEX, chainId, deploy: false, }, @@ -229,9 +234,14 @@ export const useStarkNetSnap = () => { } const tokens = await getTokens(chainId); let acc: Account[] | Account = await recoverAccounts(chainId); + let upgradeRequired = false; + if (!acc || acc.length === 0 || !acc[0].publicKey) { acc = await addAccount(chainId); + } else { + upgradeRequired = (Array.isArray(acc) ? acc[0].upgradeRequired : (acc as Account).upgradeRequired) ?? false; } + const tokenBalances = await Promise.all( tokens.map(async (token) => { const accountAddr = Array.isArray(acc) ? acc[0].address : acc.address; @@ -259,6 +269,7 @@ export const useStarkNetSnap = () => { if (!Array.isArray(acc)) { dispatch(setInfoModalVisible(true)); } + dispatch(setUpgradeModalVisible(upgradeRequired)); dispatch(disableLoading()); }; @@ -358,6 +369,80 @@ export const useStarkNetSnap = () => { } } + const getTransactionStatus = async (transactionHash: string, chainId: string) => { + try { + const response = await provider.request({ + method: 'wallet_invokeSnap', + params: { + snapId, + request: { + method: 'starkNet_getTransactionStatus', + params: { + ...defaultParam, + transactionHash, + chainId, + }, + }, + }, + }); + return response; + } catch (err) { + //eslint-disable-next-line no-console + console.error(err); + } + }; + + const readContract = async (contractAddress: string, contractFuncName: string) => { + try { + const response = await provider.request({ + method: 'wallet_invokeSnap', + params: { + snapId, + request: { + method: 'starkNet_getValue', + params: { + ...defaultParam, + contractAddress, + contractFuncName, + }, + }, + }, + }); + return response; + } catch (err) { + //eslint-disable-next-line no-console + console.error(err); + } + }; + + const upgradeAccount = async (contractAddress: string, maxFee: string, chainId: string) => { + dispatch(enableLoadingWithMessage('Upgrading account...')); + try { + const response = await provider.request({ + method: 'wallet_invokeSnap', + params: { + snapId, + request: { + method: 'starkNet_upgradeAccContract', + params: { + ...defaultParam, + contractAddress, + maxFee, + chainId, + }, + }, + }, + }); + dispatch(disableLoading()); + return response; + } catch (err) { + dispatch(disableLoading()); + //eslint-disable-next-line no-console + console.error(err); + throw err; + } + }; + const getTransactions = async ( senderAddress: string, contractAddress: string, @@ -532,6 +617,77 @@ export const useStarkNetSnap = () => { } }; + const waitForTransaction = async (transactionHash: string, chainId: string) => { + let txStatus; + const successStates = ['ACCEPTED_ON_L2', 'ACCEPTED_ON_L1']; + const errorStates = ['REJECTED', 'NOT_RECEIVED']; + + const executeFn = async () => { + txStatus = await getTransactionStatus(transactionHash, chainId); + + if (!txStatus || !('executionStatus' in txStatus) || !('finalityStatus' in txStatus)) { + return false; + } + + if (txStatus.finalityStatus && successStates.includes(txStatus.finalityStatus)) { + return true; + } else if (txStatus.executionStatus && errorStates.includes(txStatus.executionStatus)) { + const message = txStatus.executionStatus; + throw new Error(message); + } + + return false; + }; + + await retry(executeFn); + + return txStatus; + }; + + const waitForAccountUpdate = async (transactionHash: string, accountAddress: string, chainId: string) => { + dispatch(enableLoadingWithMessage('Waiting for transaction finalize...')); + const toastr = new Toastr(); + let result = false; + + try { + // read transaction to check if the txn is ready + await waitForTransaction(transactionHash, chainId); + } catch (e) { + //eslint-disable-next-line no-console + console.log(`error while wait for transaction: ${e}`); + } + + try { + const executeFn = async (): Promise => { + // read contract to check if upgrade is required + const resp = await readContract(accountAddress, 'getVersion'); + if (!resp || !resp[0]) { + return false; + } + + if (!isGTEMinVersion(hexToString(resp[0]))) { + return false; + } + + // recover accounts to update snap state + await recoverAccounts(chainId); + return true; + }; + + result = await retry(executeFn, { + maxAttempts: 20, + }); + } catch (e: any) { + //eslint-disable-next-line no-console + console.log(`error while processing waitForAccountUpdate: ${e}`); + toastr.error('Snap is unable to verify the contract upgrade process'); + } + + dispatch(disableLoading()); + + return result; + }; + const switchNetwork = async (chainId: string) => { dispatch(enableLoadingWithMessage('Switching Network...')); try { @@ -605,7 +761,12 @@ export const useStarkNetSnap = () => { getPrivateKeyFromAddress, estimateFees, sendTransaction, + upgradeAccount, getTransactions, + getTransactionStatus, + recoverAccounts, + waitForTransaction, + waitForAccountUpdate, updateTokenBalance, getTokenBalance, addErc20Token, @@ -614,6 +775,7 @@ export const useStarkNetSnap = () => { initSnap, getWalletData, refreshTokensUSDPrice, + readContract, switchNetwork, getCurrentNetwork, getStarkName, diff --git a/packages/wallet-ui/src/slices/modalSlice.ts b/packages/wallet-ui/src/slices/modalSlice.ts index c4dcda5b..329e8314 100644 --- a/packages/wallet-ui/src/slices/modalSlice.ts +++ b/packages/wallet-ui/src/slices/modalSlice.ts @@ -3,11 +3,13 @@ import { createSlice } from '@reduxjs/toolkit'; export interface modalState { infoModalVisible: boolean; minVersionModalVisible: boolean; + upgradeModalVisible: boolean; } const initialState: modalState = { infoModalVisible: false, minVersionModalVisible: false, + upgradeModalVisible: false, }; export const modalSlice = createSlice({ @@ -18,12 +20,15 @@ export const modalSlice = createSlice({ setInfoModalVisible: (state, { payload }) => { state.infoModalVisible = payload; }, + setUpgradeModalVisible: (state, { payload }) => { + state.upgradeModalVisible = payload; + }, setMinVersionModalVisible: (state, { payload }) => { state.minVersionModalVisible = payload; }, }, }); -export const { setInfoModalVisible, setMinVersionModalVisible } = modalSlice.actions; +export const { setInfoModalVisible, setMinVersionModalVisible, setUpgradeModalVisible } = modalSlice.actions; export default modalSlice.reducer; diff --git a/packages/wallet-ui/src/types/index.ts b/packages/wallet-ui/src/types/index.ts index cc65ed4a..5a5855e3 100644 --- a/packages/wallet-ui/src/types/index.ts +++ b/packages/wallet-ui/src/types/index.ts @@ -1,7 +1,7 @@ import * as Types from '@consensys/starknet-snap/src/types/snapState'; import { BigNumber } from 'ethers'; -export type Account = Pick; +export type Account = Pick; export type Network = Pick; export interface Erc20TokenBalance extends Types.Erc20Token { diff --git a/packages/wallet-ui/src/utils/constants.ts b/packages/wallet-ui/src/utils/constants.ts index 8dfaeaf4..db47e350 100644 --- a/packages/wallet-ui/src/utils/constants.ts +++ b/packages/wallet-ui/src/utils/constants.ts @@ -58,4 +58,5 @@ export const TOKEN_BALANCE_REFRESH_FREQUENCY = 60000; export const TIMEOUT_DURATION = 10000; +export const MIN_ACC_CONTRACT_VERSION = [0, 3, 0]; export const DUMMY_ADDRESS = '0xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; diff --git a/packages/wallet-ui/src/utils/utils.ts b/packages/wallet-ui/src/utils/utils.ts index 7aaa7a98..0eb54b6b 100644 --- a/packages/wallet-ui/src/utils/utils.ts +++ b/packages/wallet-ui/src/utils/utils.ts @@ -6,6 +6,7 @@ import { STARKNET_SEPOLIA_TESTNET_EXPLORER, SEPOLIA_CHAINID, TIMEOUT_DURATION, + MIN_ACC_CONTRACT_VERSION, } from './constants'; import { Erc20Token, Erc20TokenBalance } from 'types'; import { constants } from 'starknet'; @@ -118,6 +119,56 @@ export const fetchWithTimeout = async (resource: string, options = { timeout: TI return response; }; +export const isGTEMinVersion = (version: string) => { + const versionArr = version.split('.'); + return Number(versionArr[1]) >= MIN_ACC_CONTRACT_VERSION[1]; +}; + +export const hexToString = (hex: string): string => { + let str = ''; + for (let i = 0; i < hex.length; i += 2) { + const hexValue = hex.substr(i, 2); + const decimalValue = parseInt(hexValue, 16); + str += String.fromCharCode(decimalValue); + } + return str; +}; + +export const wait = (delay: number) => { + return new Promise((res) => { + setTimeout(res, delay); + }); +}; + +export const retry = async ( + fn: () => Promise, + options?: { delay?: number; maxAttempts?: number; onFailedAttempt?: CallableFunction }, +): Promise => { + let retry = options?.maxAttempts ?? 10; + const delay = options?.delay ?? 1000; + + while (retry > 0) { + try { + // read contract to check if upgrade is required + const result = await fn(); + if (result) { + return result; + } + } catch (e) { + if (options?.onFailedAttempt && typeof options?.onFailedAttempt === 'function') { + options.onFailedAttempt(e); + } else { + //eslint-disable-next-line no-console + console.log(`error while processing retry: ${e}`); + } + } finally { + await wait(delay); + retry -= 1; + } + } + return false; +}; + export const shortenDomain = (domain: string, maxLength = 18) => { if (!domain) return ''; const ellipsis = '...'; diff --git a/yarn.lock b/yarn.lock index a67a6192..8606aacc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3248,7 +3248,7 @@ __metadata: "@consensys/starknet-snap@file:../starknet-snap::locator=wallet-ui%40workspace%3Apackages%2Fwallet-ui": version: 2.7.0 - resolution: "@consensys/starknet-snap@file:../starknet-snap#../starknet-snap::hash=da052f&locator=wallet-ui%40workspace%3Apackages%2Fwallet-ui" + resolution: "@consensys/starknet-snap@file:../starknet-snap#../starknet-snap::hash=64cb92&locator=wallet-ui%40workspace%3Apackages%2Fwallet-ui" dependencies: "@metamask/snaps-sdk": 3.0.1 async-mutex: ^0.3.2 @@ -3257,7 +3257,7 @@ __metadata: ethers: ^5.5.1 starknet: 6.7.0 starknet_v4.22.0: "npm:starknet@4.22.0" - checksum: d9b1b74d87bd0062457119a8a47fde7ca7914e5e235d2bda51b6f99f74f6a621d6134845518e0c4c969cc84eeaca24bc0cf4ee91245a69c98dc4e4a0a955caab + checksum: 12c71045fd6bd5d137d51a9fb9a92b3aae206b7b1b1a036bd8fe4ddbf953d94967be56ecb170c4e871a30fa065ba64571dca714180522b89b5a9bf4cc6476b41 languageName: node linkType: hard @@ -25348,6 +25348,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.5.2": + version: 7.6.2 + resolution: "semver@npm:7.6.2" + bin: + semver: bin/semver.js + checksum: 40f6a95101e8d854357a644da1b8dd9d93ce786d5c6a77227bc69dbb17bea83d0d1d1d7c4cd5920a6df909f48e8bd8a5909869535007f90278289f2451d0292d + languageName: node + linkType: hard + "semver@npm:^7.5.4": version: 7.6.0 resolution: "semver@npm:7.6.0" @@ -28383,7 +28392,7 @@ __metadata: react-scripts: 5.0.1 redux-persist: ^6.0.0 rimraf: ^3.0.2 - semver: ^7.3.7 + semver: ^7.5.2 starknet: ^4.22.0 styled-components: ^5.3.5 toastr2: ^3.0.0-alpha.18