diff --git a/.nvmrc b/.nvmrc index 5edcff036..9e15be387 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v16 \ No newline at end of file +v16.20.0 diff --git a/package-lock.json b/package-lock.json index da0b0280b..12719ed3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,19 @@ { "name": "Casper Wallet", - "version": "1.6.0", + "version": "1.6.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "Casper Wallet", - "version": "1.6.0", + "version": "1.6.3", "dependencies": { "@formatjs/intl": "2.6.2", "@hookform/resolvers": "2.9.10", "@hot-loader/react-dom": "^17.0.1", "@lapo/asn1js": "1.2.4", "@make-software/ces-js-parser": "1.3.2", + "@noble/ciphers": "^0.3.0", "@scure/bip32": "1.3.2", "@scure/bip39": "1.2.1", "@types/argon2-browser": "1.18.1", @@ -4269,6 +4270,14 @@ "integrity": "sha512-CD/2ai1W45cDN/zN2AcYduDavU+nq9aStyQizi4MHxnwkRvS/H24WIjgc1qD8CISoqXa8AAIe+A+zpWxwV7a2Q==", "dev": true }, + "node_modules/@noble/ciphers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.3.0.tgz", + "integrity": "sha512-ldbrnOjmNRwFdXcTM6uXDcxpMIFrbzAWNnpBPp4oTJTFF0XByGD6vf45WrehZGXRQTRVV+Zm8YP+EgEf+e4cWA==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/curves": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", @@ -32070,6 +32079,11 @@ "integrity": "sha512-CD/2ai1W45cDN/zN2AcYduDavU+nq9aStyQizi4MHxnwkRvS/H24WIjgc1qD8CISoqXa8AAIe+A+zpWxwV7a2Q==", "dev": true }, + "@noble/ciphers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.3.0.tgz", + "integrity": "sha512-ldbrnOjmNRwFdXcTM6uXDcxpMIFrbzAWNnpBPp4oTJTFF0XByGD6vf45WrehZGXRQTRVV+Zm8YP+EgEf+e4cWA==" + }, "@noble/curves": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", diff --git a/package.json b/package.json index 0ce54135f..0e71df484 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "Casper Wallet", "description": "Securely manage your CSPR tokens and interact with dapps with the self-custody wallet for the Casper blockchain.", - "version": "1.6.2", + "version": "1.6.3", "author": "MAKE LLC", "scripts": { "devtools:redux": "redux-devtools --hostname=localhost", @@ -61,6 +61,7 @@ "@hot-loader/react-dom": "^17.0.1", "@lapo/asn1js": "1.2.4", "@make-software/ces-js-parser": "1.3.2", + "@noble/ciphers": "^0.3.0", "@scure/bip32": "1.3.2", "@scure/bip39": "1.2.1", "@types/argon2-browser": "1.18.1", diff --git a/src/apps/popup/app-router.tsx b/src/apps/popup/app-router.tsx index 2be8040ae..983098e99 100644 --- a/src/apps/popup/app-router.tsx +++ b/src/apps/popup/app-router.tsx @@ -34,6 +34,7 @@ import { TokenDetailPage } from 'src/apps/popup/pages/token-details'; import { ActivityDetailsPage } from '@popup/pages/activity-details'; import { ReceivePage } from '@popup/pages/receive'; import { NftDetailsPage } from '@popup/pages/nft-details'; +import { WalletQrCodePage } from '@popup/pages/wallet-qr-code'; import { TransferNftPage } from '@popup/pages/transfer-nft'; import { ChangePasswordPage } from '@popup/pages/change-password'; @@ -242,6 +243,10 @@ function AppRoutes() { } /> } /> } /> + } + /> } /> void; + onClick?: (password: string) => Promise; + loading?: boolean; } export const BackupSecretPhrasePasswordPage = ({ - setPasswordConfirmed + setPasswordConfirmed, + onClick, + loading = false }: BackupSecretPhrasePasswordPageType) => { const { t } = useTranslation(); @@ -40,7 +44,8 @@ export const BackupSecretPhrasePasswordPage = ({ const { register, handleSubmit, - formState: { errors, isDirty } + formState: { errors, isDirty }, + getValues } = useUnlockWalletForm(passwordHash, passwordSaltHash); const isSubmitButtonDisabled = calculateSubmitButtonDisabled({ @@ -48,8 +53,17 @@ export const BackupSecretPhrasePasswordPage = ({ }); const onSubmit = () => { - setPasswordConfirmed(); - dispatchToMainStore(loginRetryCountReseted()); + if (onClick) { + const { password } = getValues(); + + onClick(password).then(() => { + setPasswordConfirmed(); + dispatchToMainStore(loginRetryCountReseted()); + }); + } else { + setPasswordConfirmed(); + dispatchToMainStore(loginRetryCountReseted()); + } }; return ( @@ -75,8 +89,8 @@ export const BackupSecretPhrasePasswordPage = ({ )} renderFooter={() => ( - )} diff --git a/src/apps/popup/pages/home/components/tokens-list/utils.ts b/src/apps/popup/pages/home/components/tokens-list/utils.ts index 61f7aa679..70c578ed3 100644 --- a/src/apps/popup/pages/home/components/tokens-list/utils.ts +++ b/src/apps/popup/pages/home/components/tokens-list/utils.ts @@ -32,8 +32,8 @@ export const formatErc20TokenBalance = ( name: token.contract_name, balance: token.balance, amount: erc20Amount, - symbol: token.metadata?.symbol || '', - decimals: token.metadata?.decimals, + symbol: token?.metadata?.symbol || '', + decimals: token?.metadata?.decimals, amountFiat: null, icon: token.icon_url || 'assets/icons/erc20-avatar.svg' }; diff --git a/src/apps/popup/pages/navigation-menu/index.tsx b/src/apps/popup/pages/navigation-menu/index.tsx index b53f95f58..4f434b3e9 100644 --- a/src/apps/popup/pages/navigation-menu/index.tsx +++ b/src/apps/popup/pages/navigation-menu/index.tsx @@ -183,6 +183,18 @@ export function NavigationMenuPageContent() { }, { id: 2, + title: t('Generate wallet QR code'), + description: t('Scan to import your wallet on mobile'), + iconPath: 'assets/icons/qr.svg', + disabled: false, + handleOnClick: () => { + closeNavigationMenu(); + + navigate(RouterPath.GenerateWalletQRCode); + } + }, + { + id: 3, title: t('Download account keys'), description: t('For all accounts imported via file'), iconPath: 'assets/icons/download.svg', diff --git a/src/apps/popup/pages/receive/content.tsx b/src/apps/popup/pages/receive/content.tsx index 18d9cb92d..a7cc7584b 100644 --- a/src/apps/popup/pages/receive/content.tsx +++ b/src/apps/popup/pages/receive/content.tsx @@ -58,7 +58,7 @@ export const ReceivePageContent = () => { const tokens = useSelector(selectErc20Tokens, shallowEqual); useEffect(() => { - if (tokenData.symbol === 'CSPR') { + if (tokenData?.symbol === 'CSPR') { const balance = (csprBalance.amountMotes && motesToCSPR(csprBalance.amountMotes)) || '0'; @@ -66,10 +66,10 @@ export const ReceivePageContent = () => { } else { const erc20Tokens = formatErc20TokenBalance(tokens); const balance = - erc20Tokens?.find(t => t.symbol === tokenData.symbol)?.amount ?? '0'; + erc20Tokens?.find(t => t?.symbol === tokenData?.symbol)?.amount ?? '0'; setTokenData(prev => ({ ...prev, balance })); } - }, [csprBalance, tokenData.symbol, tokens]); + }, [csprBalance, tokenData?.symbol, tokens]); return ( @@ -80,7 +80,7 @@ export const ReceivePageContent = () => { diff --git a/src/apps/popup/pages/token-details/token.tsx b/src/apps/popup/pages/token-details/token.tsx index 84b751a2e..f8fc7cc3f 100644 --- a/src/apps/popup/pages/token-details/token.tsx +++ b/src/apps/popup/pages/token-details/token.tsx @@ -66,7 +66,7 @@ export const Token = ({ erc20Tokens }: TokenProps) => { if (casperToken && activeAccount) { setTokenData(casperToken); setTokenInfoList([ - { id: 1, name: 'Symbol', value: casperToken.symbol } + { id: 1, name: 'Symbol', value: casperToken?.symbol } ]); } } else { @@ -78,8 +78,8 @@ export const Token = ({ erc20Tokens }: TokenProps) => { if (token) { setTokenData(token); setTokenInfoList([ - { id: 1, name: 'Symbol', value: token.symbol }, - { id: 2, name: 'Decimals', value: (token.decimals || 0).toString() } + { id: 1, name: 'Symbol', value: token?.symbol }, + { id: 2, name: 'Decimals', value: (token?.decimals || 0).toString() } ]); } else { setTokenData(prev => (prev ? { ...prev, amount: '0' } : null)); diff --git a/src/apps/popup/pages/transfer/confirm-step.tsx b/src/apps/popup/pages/transfer/confirm-step.tsx index 0c71a3445..d7aed6b45 100644 --- a/src/apps/popup/pages/transfer/confirm-step.tsx +++ b/src/apps/popup/pages/transfer/confirm-step.tsx @@ -134,7 +134,7 @@ export const ConfirmStep = ({ {listItems.text} - {`${listItems.amount} ${listItems.symbol}`} + {`${listItems.amount} ${listItems?.symbol}`} {listItems.fiatPrice == null ? null diff --git a/src/apps/popup/pages/transfer/index.tsx b/src/apps/popup/pages/transfer/index.tsx index 3830daa79..4f4d7e6ef 100644 --- a/src/apps/popup/pages/transfer/index.tsx +++ b/src/apps/popup/pages/transfer/index.tsx @@ -88,7 +88,6 @@ export const TransferPage = () => { const { amountForm, recipientForm } = useTransferForm( erc20Balance, - erc20Decimals, isErc20Transfer, csprBalance.amountMotes, paymentAmount diff --git a/src/apps/popup/pages/wallet-qr-code/content.tsx b/src/apps/popup/pages/wallet-qr-code/content.tsx new file mode 100644 index 000000000..49f3febde --- /dev/null +++ b/src/apps/popup/pages/wallet-qr-code/content.tsx @@ -0,0 +1,73 @@ +import React, { useEffect, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { QRCodeCanvas } from 'qrcode.react'; +import styled, { useTheme } from 'styled-components'; + +import { + CenteredFlexRow, + ContentContainer, + ParagraphContainer, + SpacingSize +} from '@libs/layout'; +import { Typography } from '@libs/ui'; + +const QRContainer = styled(CenteredFlexRow)` + padding: 20px 16px; + background-color: ${({ theme }) => theme.color.backgroundPrimary}; + border-radius: ${({ theme }) => theme.borderRadius.base}px; + margin-top: 20px; +`; + +interface WalletQrCodePageContentProps { + qrStrings: string[]; +} + +export const WalletQrCodePageContent = ({ + qrStrings +}: WalletQrCodePageContentProps) => { + const theme = useTheme(); + const [currentQrIndex, setCurrentQrIndex] = useState(0); + + useEffect(() => { + const int = setInterval(() => { + setCurrentQrIndex(prev => { + const next = prev + 1; + + if (next === qrStrings.length) { + return 0; + } + + return next; + }); + }, 500); + + return () => clearInterval(int); + }, [qrStrings.length]); + + const { t } = useTranslation(); + + return ( + + + + QR code is ready! + + + + + Scan this with your Casper Wallet app. + + + + + + + ); +}; diff --git a/src/apps/popup/pages/wallet-qr-code/index.tsx b/src/apps/popup/pages/wallet-qr-code/index.tsx new file mode 100644 index 000000000..4c3e4539f --- /dev/null +++ b/src/apps/popup/pages/wallet-qr-code/index.tsx @@ -0,0 +1,89 @@ +import React, { useCallback, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { shallowEqual, useSelector } from 'react-redux'; + +import { + FooterButtonsContainer, + HeaderSubmenuBarNavLink, + PopupHeader, + PopupLayout +} from '@libs/layout'; +import { WalletQrCodePageContent } from '@popup/pages/wallet-qr-code/content'; +import { RouterPath, useTypedNavigate } from '@popup/router'; +import { Button } from '@libs/ui'; +import { + selectSecretPhrase, + selectVaultDerivedAccounts, + selectVaultImportedAccounts +} from '@background/redux/vault/selectors'; +import { generateSyncWalletQrData } from '@libs/crypto'; +import { BackupSecretPhrasePasswordPage } from '@popup/pages/backup-secret-phrase-password'; + +export const WalletQrCodePage = () => { + const [qrStrings, setQrStrings] = useState([]); + const [isPasswordConfirmed, setIsPasswordConfirmed] = + useState(false); + const [loading, setLoading] = useState(false); + + const secretPhrase = useSelector(selectSecretPhrase); + const derivedAccounts = useSelector(selectVaultDerivedAccounts, shallowEqual); + const importedAccounts = useSelector( + selectVaultImportedAccounts, + shallowEqual + ); + + const navigate = useTypedNavigate(); + const { t } = useTranslation(); + + const setPasswordConfirmed = useCallback(() => { + setIsPasswordConfirmed(true); + }, []); + + const generateQRCode = async (password: string) => { + if (secretPhrase) { + setLoading(true); + const data = await generateSyncWalletQrData( + password, + secretPhrase, + derivedAccounts, + importedAccounts + ); + + setLoading(false); + setQrStrings(data); + } + }; + + if (!isPasswordConfirmed) { + return ( + + ); + } + + return ( + ( + ( + + )} + /> + )} + renderContent={() => } + renderFooter={() => ( + + + + )} + /> + ); +}; diff --git a/src/apps/popup/router/paths.ts b/src/apps/popup/router/paths.ts index 178e4544f..cc2e6eba6 100644 --- a/src/apps/popup/router/paths.ts +++ b/src/apps/popup/router/paths.ts @@ -20,6 +20,7 @@ export enum RouterPath { Token = '/token/:tokenName', Receive = '/receive', NftDetails = '/nft-details/:contractPackageHash/nfts/:tokenId', + GenerateWalletQRCode = '/generate-wallet-qr-code', TransferNft = '/transfer-nft/:contractPackageHash/nfts/:tokenId', ChangePassword = '/change-password' } diff --git a/src/assets/icons/qr.svg b/src/assets/icons/qr.svg new file mode 100644 index 000000000..816d0fec3 --- /dev/null +++ b/src/assets/icons/qr.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/libs/crypto/hashing.ts b/src/libs/crypto/hashing.ts index 65b855fa0..625053790 100644 --- a/src/libs/crypto/hashing.ts +++ b/src/libs/crypto/hashing.ts @@ -11,7 +11,7 @@ export function generateRandomSaltHex() { return convertBytesToHex(generateRandomSaltBytes()); } -const createScryptOptions = () => { +export const createScryptOptions = () => { const options = { N: 2 ** 18, r: 8, p: 1, dkLen: 32 }; return options; }; diff --git a/src/libs/crypto/index.ts b/src/libs/crypto/index.ts index b7ec89731..95030aebf 100644 --- a/src/libs/crypto/index.ts +++ b/src/libs/crypto/index.ts @@ -4,3 +4,4 @@ export * from './bip39'; export * from './bip44'; export * from './parse-secret-key-string'; export * from './sign-deploy'; +export * from './sync-wallet-qr'; diff --git a/src/libs/crypto/sync-wallet-qr.ts b/src/libs/crypto/sync-wallet-qr.ts new file mode 100644 index 000000000..7a3e3c775 --- /dev/null +++ b/src/libs/crypto/sync-wallet-qr.ts @@ -0,0 +1,52 @@ +import { randomBytes } from '@noble/hashes/utils'; +import { aes_256_cbc } from '@noble/ciphers/webcrypto/aes'; +import { scryptAsync } from '@noble/hashes/scrypt'; +import { CLPublicKey } from 'casper-js-sdk'; + +import { Account } from '@background/redux/vault/types'; + +import { convertBytesToBase64 } from './utils'; +import { createScryptOptions } from './hashing'; + +export const generateSyncWalletQrData = async ( + password: string, + secretPhrase: string[], + derivedAccounts: Account[], + importedAccounts: Account[] +) => { + const salt = randomBytes(16); + const iv = randomBytes(16); + + const key = await scryptAsync(password, salt, createScryptOptions()); + + const qrDataString = JSON.stringify([ + secretPhrase.join(' '), + derivedAccounts.map(da => da.name), + [ + ...importedAccounts.map(acc => ({ + secretKey: acc.secretKey, + label: acc.name, + publicKeyTag: CLPublicKey.fromHex(acc.publicKey).getTag() + })) + ] + ]); + + const data = Uint8Array.from(Buffer.from(qrDataString)); + + const stream = aes_256_cbc(key, iv); + const cipher = await stream.encrypt(data); + + const qrString = JSON.stringify([ + convertBytesToBase64(cipher), + convertBytesToBase64(salt), + convertBytesToBase64(iv) + ]); + + const qrBytes = Uint8Array.from(Buffer.from(qrString)); + const qrData = convertBytesToBase64(qrBytes); + const qrDataArray = qrData.match(/.{1,200}/g); + + return (qrDataArray ?? [qrData]).map( + (qr, i, arr) => `${i + 1}$${arr.length}$${qr}` + ); +}; diff --git a/src/libs/ui/components/erc20-token-activity-list/erc20-token-activity-list.tsx b/src/libs/ui/components/erc20-token-activity-list/erc20-token-activity-list.tsx index 81df9fdc3..fe13e4c25 100644 --- a/src/libs/ui/components/erc20-token-activity-list/erc20-token-activity-list.tsx +++ b/src/libs/ui/components/erc20-token-activity-list/erc20-token-activity-list.tsx @@ -61,8 +61,8 @@ export const Erc20TokenActivityList = () => { args: transaction.deploy?.args || '-', status: transaction.deploy?.status || '-', errorMessage: transaction.deploy?.error_message || null, - decimals: transaction.contract_package?.metadata.decimals, - symbol: transaction.contract_package?.metadata.symbol, + decimals: transaction.contract_package?.metadata?.decimals, + symbol: transaction.contract_package?.metadata?.symbol, toPublicKey: transaction?.to_public_key, fromPublicKey: transaction?.from_public_key || null, contractPackage: transaction?.contract_package, diff --git a/src/libs/ui/forms/form-validation-rules.ts b/src/libs/ui/forms/form-validation-rules.ts index 504243a8b..e5ef55850 100644 --- a/src/libs/ui/forms/form-validation-rules.ts +++ b/src/libs/ui/forms/form-validation-rules.ts @@ -1,6 +1,7 @@ import * as Yup from 'yup'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; +import Big from 'big.js'; import { verifyPasswordAgainstHash } from '@src/libs/crypto/hashing'; import { dispatchToMainStore } from '@src/background/redux/utils'; @@ -12,10 +13,10 @@ import { TRANSFER_COST_MOTES } from '@src/constants'; import { isValidPublicKey, isValidU64 } from '@src/utils'; -import Big from 'big.js'; import { CSPRtoMotes, motesToCSPR } from '@libs/ui/utils/formatters'; export const minPasswordLength = 16; +export const maxQrCodePasswordLength = 8; const ERROR_DISPLAYED_BEFORE_ATTEMPT_IS_DECREMENTED = 1; @@ -180,10 +181,7 @@ export const useCsprAmountRule = (amountMotes: string | null) => { }); }; -export const useErc20AmountRule = ( - amount: string | null, - decimals: number | null -) => { +export const useErc20AmountRule = (amount: string | null) => { const { t } = useTranslation(); const maxAmount: string = amount == null ? '0' : Big(amount).toString(); diff --git a/src/libs/ui/forms/transfer.ts b/src/libs/ui/forms/transfer.ts index 8536453e9..4adaf0d6d 100644 --- a/src/libs/ui/forms/transfer.ts +++ b/src/libs/ui/forms/transfer.ts @@ -25,7 +25,6 @@ export type TransferAmountFormValues = { export function useTransferForm( erc20Balance: string | null, - decimals: number | null, isErc20: boolean, amountMotes: string | null, paymentAmount: string @@ -41,7 +40,7 @@ export function useTransferForm( }; const erc20AmountFormSchema = Yup.object().shape({ - amount: useErc20AmountRule(erc20Balance, decimals), + amount: useErc20AmountRule(erc20Balance), paymentAmount: usePaymentAmountRule(amountMotes), transferIdMemo: useTransferIdMemoRule() }); diff --git a/xcode-project/Casper Wallet/Casper Wallet.xcodeproj/project.pbxproj b/xcode-project/Casper Wallet/Casper Wallet.xcodeproj/project.pbxproj index 30570fdf1..ba296e19e 100644 --- a/xcode-project/Casper Wallet/Casper Wallet.xcodeproj/project.pbxproj +++ b/xcode-project/Casper Wallet/Casper Wallet.xcodeproj/project.pbxproj @@ -500,7 +500,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = "Casper Wallet Extension/Casper_Wallet_Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 22; + CURRENT_PROJECT_VERSION = 23; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Casper Wallet Extension/Info.plist"; @@ -512,7 +512,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 1.5.2; + MARKETING_VERSION = 1.6.0; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -530,7 +530,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = "Casper Wallet Extension/Casper_Wallet_Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 22; + CURRENT_PROJECT_VERSION = 23; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Casper Wallet Extension/Info.plist"; @@ -542,7 +542,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 1.5.2; + MARKETING_VERSION = 1.6.0; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -564,7 +564,7 @@ CODE_SIGN_ENTITLEMENTS = "Casper Wallet/Casper Wallet.entitlements"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 35; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Casper Wallet/Info.plist"; @@ -578,7 +578,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 1.6.2; + MARKETING_VERSION = 1.6.3; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -601,7 +601,7 @@ CODE_SIGN_ENTITLEMENTS = "Casper Wallet/Casper Wallet.entitlements"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 35; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Casper Wallet/Info.plist"; @@ -615,7 +615,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 1.6.2; + MARKETING_VERSION = 1.6.3; OTHER_LDFLAGS = ( "-framework", SafariServices,