diff --git a/jest.config.native.js b/jest.config.native.js index 66cf9fa3196..a62817379a5 100644 --- a/jest.config.native.js +++ b/jest.config.native.js @@ -41,6 +41,5 @@ module.exports = { '/../../node_modules/react-native-gesture-handler/jestSetup.js', '/../../suite-native/test-utils/src/atomsMock.js', '/../../suite-native/test-utils/src/expoMock.js', - '/../../suite-native/test-utils/src/walletSdkMock.js', ], }; diff --git a/package.json b/package.json index d0e4841542d..8167bc01628 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,8 @@ "@types/react": "18.2.55", "bn.js": "5.2.1", "node-gyp": "10.2.0", - "reselect": "5.1.1" + "reselect": "5.1.1", + "@everstake/wallet-sdk/@solana/web3.js": "1.95.8" }, "devDependencies": { "@babel/cli": "^7.23.9", diff --git a/packages/blockchain-link-types/package.json b/packages/blockchain-link-types/package.json index da7f46e7df0..1b2008ceed9 100644 --- a/packages/blockchain-link-types/package.json +++ b/packages/blockchain-link-types/package.json @@ -19,6 +19,7 @@ "prepublish": "yarn tsx ../../scripts/prepublish.js" }, "dependencies": { + "@everstake/wallet-sdk": "^1.0.5", "@solana/web3.js": "^2.0.0", "@trezor/type-utils": "workspace:*", "@trezor/utxo-lib": "workspace:*" diff --git a/packages/blockchain-link-types/src/common.ts b/packages/blockchain-link-types/src/common.ts index 75004777e2c..65df60ab1e7 100644 --- a/packages/blockchain-link-types/src/common.ts +++ b/packages/blockchain-link-types/src/common.ts @@ -7,6 +7,7 @@ import type { ContractInfo, StakingPool, } from './blockbook-api'; +import type { SolanaStakingAccount } from './solana'; /* Common types used in both params and responses */ @@ -197,6 +198,7 @@ export interface AccountInfo { availableBalance: string; empty: boolean; tokens?: TokenInfo[]; // ethereum and blockfrost tokens + addresses?: AccountAddresses; // bitcoin and blockfrost addresses history: { total: number; // total transactions (unknown in ripple) @@ -211,6 +213,7 @@ export interface AccountInfo { nonce?: string; contractInfo?: ContractInfo; stakingPools?: StakingPool[]; + solStakingAccounts?: SolanaStakingAccount[]; // solana staking accounts addressAliases?: { [key: string]: AddressAlias }; // XRP sequence?: number; diff --git a/packages/blockchain-link-types/src/solana.ts b/packages/blockchain-link-types/src/solana.ts index 330c6e5d700..217b03b3648 100644 --- a/packages/blockchain-link-types/src/solana.ts +++ b/packages/blockchain-link-types/src/solana.ts @@ -7,6 +7,7 @@ import type { GetTransactionApi, Signature, } from '@solana/web3.js'; +import { SolDelegation } from '@everstake/wallet-sdk'; import type { GetObjectWithKey, @@ -70,4 +71,6 @@ export type AccountInfo< export type { Address } from '@solana/web3.js'; +export type SolanaStakingAccount = SolDelegation; + export type TokenDetailByMint = { [mint: string]: { name: string; symbol: string } }; diff --git a/packages/blockchain-link-utils/package.json b/packages/blockchain-link-utils/package.json index b636f26f447..d3161fa1be0 100644 --- a/packages/blockchain-link-utils/package.json +++ b/packages/blockchain-link-utils/package.json @@ -20,6 +20,7 @@ "prepublish": "yarn tsx ../../scripts/prepublish.js" }, "dependencies": { + "@everstake/wallet-sdk": "^1.0.5", "@mobily/ts-belt": "^3.13.1", "@trezor/env-utils": "workspace:*", "@trezor/utils": "workspace:*" diff --git a/packages/blockchain-link-utils/src/blockbook.ts b/packages/blockchain-link-utils/src/blockbook.ts index 470ffbd9cea..288a5e598c4 100644 --- a/packages/blockchain-link-utils/src/blockbook.ts +++ b/packages/blockchain-link-utils/src/blockbook.ts @@ -1,3 +1,5 @@ +import { ETH_NETWORK_ADDRESSES } from '@everstake/wallet-sdk'; + import { BigNumber } from '@trezor/utils/src/bigNumber'; import type { Utxo, @@ -87,21 +89,11 @@ export const filterTokenTransfers = ( }); }; -const ethereumStakingAddresses = { - poolInstance: [ - '0xD523794C879D9eC028960a231F866758e405bE34', - '0xAFA848357154a6a624686b348303EF9a13F63264', - ], - withdrawTreasury: [ - '0x19449f0f696703Aa3b1485DfA2d855F33659397a', - '0x66cb3AeD024740164EBcF04e292dB09b5B63A2e1', - ], -}; - export const isEthereumStakingInternalTransfer = (from: string, to: string) => { - const { poolInstance, withdrawTreasury } = ethereumStakingAddresses; + const poolInstances = Object.values(ETH_NETWORK_ADDRESSES).map(network => network.addressContractPool); + const withdrawTreasuries = Object.values(ETH_NETWORK_ADDRESSES).map(network => network.addressContractWithdrawTreasury); - return poolInstance.includes(from) && withdrawTreasury.includes(to); + return poolInstances.includes(from) && withdrawTreasuries.includes(to); }; export const filterEthereumInternalTransfers = ( diff --git a/packages/blockchain-link/package.json b/packages/blockchain-link/package.json index 30c611eff16..2c53877908e 100644 --- a/packages/blockchain-link/package.json +++ b/packages/blockchain-link/package.json @@ -76,6 +76,7 @@ "worker-loader": "^3.0.8" }, "dependencies": { + "@everstake/wallet-sdk": "^1.0.5", "@solana-program/token": "^0.4.1", "@solana/web3.js": "^2.0.0", "@trezor/blockchain-link-types": "workspace:*", diff --git a/packages/blockchain-link/src/workers/solana/index.ts b/packages/blockchain-link/src/workers/solana/index.ts index b44e9ca02f3..bf657b585fc 100644 --- a/packages/blockchain-link/src/workers/solana/index.ts +++ b/packages/blockchain-link/src/workers/solana/index.ts @@ -59,6 +59,7 @@ import { IntervalId } from '@trezor/type-utils'; import { getBaseFee, getPriorityFee } from './fee'; import { BaseWorker, ContextType, CONTEXT } from '../baseWorker'; +// import { getSolanaStakingAccounts } from '../utils'; export type SolanaAPI = Readonly<{ clusterUrl: ClusterUrl; @@ -203,7 +204,11 @@ const pushTransaction = async (request: Request) = } }; -const getAccountInfo = async (request: Request) => { +const getAccountInfo = async ( + request: Request, + // TODO: uncomment when solana staking accounts are supported + // isTestnet: boolean, +) => { const { payload } = request; const { details = 'basic' } = payload; const api = await request.connect(); @@ -333,9 +338,12 @@ const getAccountInfo = async (request: Request) => const accountDataBytes = getBase64Encoder().encode(accountDataEncoded); const accountDataLength = BigInt(accountDataBytes.byteLength); const rent = await api.rpc.getMinimumBalanceForRentExemption(accountDataLength).send(); + // TODO: uncomment when solana staking accounts are supported + // const stakingAccounts = await getSolanaStakingAccounts(payload.descriptor, isTestnet); misc = { owner: accountInfo?.owner, rent: Number(rent), + solStakingAccounts: [], }; } } diff --git a/packages/blockchain-link/src/workers/utils.ts b/packages/blockchain-link/src/workers/utils.ts index a180fd49834..a47e5db756c 100644 --- a/packages/blockchain-link/src/workers/utils.ts +++ b/packages/blockchain-link/src/workers/utils.ts @@ -1,5 +1,9 @@ +import { Solana, SolNetwork } from '@everstake/wallet-sdk'; + import { parseHostname } from '@trezor/utils'; +import config from './../ui/config'; + /** * Sorts array of backend urls so the localhost addresses are first, * then onion addresses and then the rest. Apart from that it will @@ -20,3 +24,21 @@ export const prioritizeEndpoints = (urls: string[]) => }) .sort(([, a], [, b]) => b - a) .map(([url]) => url); + +export const getSolanaStakingAccounts = async (descriptor: string, isTestnet: boolean) => { + const blockchainEnvironment = isTestnet ? 'devnet' : 'mainnet'; + + // Find the blockchain configuration for the specified chain and environment + const blockchainConfig = config.find(c => + c.blockchain.name.toLowerCase().includes(`solana ${blockchainEnvironment}`) + ); + const serverUrl = blockchainConfig?.blockchain.server[0]; + const network = isTestnet ? SolNetwork.Devnet : SolNetwork.Mainnet; + + const solanaClient = new Solana(network, serverUrl); + + const delegations = await solanaClient.getDelegations(descriptor); + const { result: stakingAccounts } = delegations; + + return stakingAccounts; +}; diff --git a/packages/connect-iframe/webpack/base.webpack.config.ts b/packages/connect-iframe/webpack/base.webpack.config.ts index 92fdba35a73..95fcea747ce 100644 --- a/packages/connect-iframe/webpack/base.webpack.config.ts +++ b/packages/connect-iframe/webpack/base.webpack.config.ts @@ -111,10 +111,11 @@ export const config: webpack.Configuration = { }), // provide fallback for global objects. // resolve.fallback will not work since those objects are not imported as modules. + // process/browser needs explicit .js extension new webpack.ProvidePlugin({ Buffer: ['buffer', 'Buffer'], Promise: ['es6-promise', 'Promise'], - process: 'process/browser', + process: 'process/browser.js', }), // resolve @trezor/connect modules as "browser" new webpack.NormalModuleReplacementPlugin(/\/workers\/workers$/, resource => { diff --git a/packages/suite/package.json b/packages/suite/package.json index 06a49b28158..23f0575ca73 100644 --- a/packages/suite/package.json +++ b/packages/suite/package.json @@ -18,7 +18,7 @@ "test-unit:watch": "yarn g:jest -o --watch" }, "dependencies": { - "@everstake/wallet-sdk": "^0.3.66", + "@everstake/wallet-sdk": "^1.0.5", "@floating-ui/react": "^0.26.9", "@formatjs/intl": "2.10.0", "@hookform/resolvers": "3.9.1", @@ -27,6 +27,7 @@ "@sentry/core": "^7.100.1", "@solana/buffer-layout": "^4.0.1", "@solana/web3.js": "^2.0.0", + "@solana/web3.js-version1": "npm:@solana/web3.js@1.95.8", "@suite-common/analytics": "workspace:*", "@suite-common/assets": "workspace:*", "@suite-common/connect-init": "workspace:*", diff --git a/packages/suite/src/actions/wallet/stake/stakeFormActions.ts b/packages/suite/src/actions/wallet/stake/stakeFormActions.ts new file mode 100644 index 00000000000..23bc83d27f7 --- /dev/null +++ b/packages/suite/src/actions/wallet/stake/stakeFormActions.ts @@ -0,0 +1,156 @@ +import { BigNumber } from '@trezor/utils/src/bigNumber'; +import { FeeLevel } from '@trezor/connect'; +import { + calculateTotal, + calculateMax, + getExternalComposeOutput, + formatAmount, +} from '@suite-common/wallet-utils'; +import { + StakeFormState, + PrecomposedLevels, + PrecomposedTransaction, + ExternalOutput, +} from '@suite-common/wallet-types'; +import { ComposeActionContext } from '@suite-common/wallet-core'; +import { NetworkSymbol } from '@suite-common/wallet-config'; + +type StakingParams = { + feeInBaseUnits: string; + minBalanceForStakingInBaseUnits: string; + minAmountForStakingInBaseUnits: string; + minAmountForWithdrawalInBaseUnits: string; +}; + +export const calculate = ( + availableBalance: string, + output: ExternalOutput, + feeLevel: FeeLevel, + compareWithAmount = true, + symbol: NetworkSymbol, + stakingParams: StakingParams, +): PrecomposedTransaction => { + const { + feeInBaseUnits, + minBalanceForStakingInBaseUnits, + minAmountForStakingInBaseUnits, + minAmountForWithdrawalInBaseUnits, + } = stakingParams; + + let amount: string; + let max: string | undefined; + + if (output.type === 'send-max' || output.type === 'send-max-noaddress') { + const minAmountWithFeeInBaseUnits = new BigNumber(minBalanceForStakingInBaseUnits).plus( + feeInBaseUnits, + ); + + if (new BigNumber(availableBalance).lt(minAmountWithFeeInBaseUnits)) { + max = minAmountForStakingInBaseUnits; + } else { + max = new BigNumber(calculateMax(availableBalance, feeInBaseUnits)) + .minus(minAmountForWithdrawalInBaseUnits) + .toString(); + } + + amount = max; + } else { + amount = output.amount; + } + + const totalSpent = new BigNumber(calculateTotal(amount, feeInBaseUnits)); + + if ( + new BigNumber(feeInBaseUnits).gt(availableBalance) || + (compareWithAmount && totalSpent.isGreaterThan(availableBalance)) + ) { + const error = 'TR_STAKE_NOT_ENOUGH_FUNDS'; + + // errorMessage declared later + return { + type: 'error', + error, + errorMessage: { id: error, values: { symbol: symbol.toUpperCase() } }, + } as const; + } + + const payloadData = { + type: 'nonfinal' as const, + totalSpent: totalSpent.toString(), + max, + fee: feeInBaseUnits, + feePerByte: feeLevel.feePerUnit, + feeLimit: feeLevel.feeLimit, + bytes: 0, + inputs: [], + }; + + if (output.type === 'send-max' || output.type === 'payment') { + return { + ...payloadData, + type: 'final', + // compatibility with BTC PrecomposedTransaction from @trezor/connect + inputs: [], + outputsPermutation: [0], + outputs: [ + { + address: output.address, + amount, + script_type: 'PAYTOADDRESS', + }, + ], + }; + } + + return payloadData; +}; + +export const composeStakingTransaction = ( + formValues: StakeFormState, + formState: ComposeActionContext, + predefinedLevels: FeeLevel[], + calculateTransaction: ( + availableBalance: string, + output: ExternalOutput, + feeLevel: FeeLevel, + compareWithAmount: boolean, + symbol: NetworkSymbol, + ) => PrecomposedTransaction, + customFeeLimit?: string, +) => { + const { account, network } = formState; + const composeOutputs = getExternalComposeOutput(formValues, account, network); + if (!composeOutputs) return; // no valid Output + + const { output, decimals } = composeOutputs; + const { availableBalance } = account; + + // wrap response into PrecomposedLevels object where key is a FeeLevel label + const wrappedResponse: PrecomposedLevels = {}; + const compareWithAmount = formValues.stakeType === 'stake'; + const response = predefinedLevels.map(level => + calculateTransaction(availableBalance, output, level, compareWithAmount, account.symbol), + ); + response.forEach((tx, index) => { + const feeLabel = predefinedLevels[index].label as FeeLevel['label']; + wrappedResponse[feeLabel] = tx; + }); + + // format max (calculate sends it as satoshi) + // update errorMessage values (symbol) + Object.keys(wrappedResponse).forEach(key => { + const tx = wrappedResponse[key]; + if (tx.type !== 'error') { + tx.max = tx.max ? formatAmount(tx.max, decimals) : undefined; + tx.estimatedFeeLimit = customFeeLimit ?? tx.estimatedFeeLimit; + } + if (tx.type === 'error' && tx.error === 'AMOUNT_NOT_ENOUGH_CURRENCY_FEE') { + tx.errorMessage = { + id: 'AMOUNT_NOT_ENOUGH_CURRENCY_FEE', + values: { symbol: network.symbol.toUpperCase() }, + }; + } + }); + + return wrappedResponse; +}; diff --git a/packages/suite/src/actions/wallet/stake/stakeFormEthereumActions.ts b/packages/suite/src/actions/wallet/stake/stakeFormEthereumActions.ts index c351be1772e..512b8b85903 100644 --- a/packages/suite/src/actions/wallet/stake/stakeFormEthereumActions.ts +++ b/packages/suite/src/actions/wallet/stake/stakeFormEthereumActions.ts @@ -3,18 +3,9 @@ import { toWei } from 'web3-utils'; import { BigNumber } from '@trezor/utils/src/bigNumber'; import TrezorConnect, { FeeLevel } from '@trezor/connect'; import { notificationsActions } from '@suite-common/toast-notifications'; -import { - calculateTotal, - calculateMax, - calculateEthFee, - getExternalComposeOutput, - formatAmount, - isPending, - getAccountIdentity, -} from '@suite-common/wallet-utils'; +import { calculateEthFee, isPending, getAccountIdentity } from '@suite-common/wallet-utils'; import { StakeFormState, - PrecomposedLevels, PrecomposedTransaction, PrecomposedTransactionFinal, ExternalOutput, @@ -36,9 +27,11 @@ import { prepareClaimEthTx, prepareStakeEthTx, prepareUnstakeEthTx, -} from 'src/utils/suite/stake'; +} from 'src/utils/suite/ethereumStaking'; + +import { calculate, composeStakingTransaction } from './stakeFormActions'; -const calculate = ( +const calculateTransaction = ( availableBalance: string, output: ExternalOutput, feeLevel: FeeLevel, @@ -47,88 +40,27 @@ const calculate = ( ): PrecomposedTransaction => { const feeInWei = calculateEthFee(toWei(feeLevel.feePerUnit, 'gwei'), feeLevel.feeLimit || '0'); - let amount: string; - let max: string | undefined; - - if (output.type === 'send-max' || output.type === 'send-max-noaddress') { - const minEthBalanceForStakingWei = toWei(MIN_ETH_BALANCE_FOR_STAKING.toString(), 'ether'); - const minAmountWithFeeWei = new BigNumber(minEthBalanceForStakingWei).plus(feeInWei); - - if (new BigNumber(availableBalance).lt(minAmountWithFeeWei)) { - max = toWei(MIN_ETH_AMOUNT_FOR_STAKING.toString(), 'ether'); - } else { - max = new BigNumber(calculateMax(availableBalance, feeInWei)) - .minus(toWei(MIN_ETH_FOR_WITHDRAWALS.toString(), 'ether')) - .toString(); - } - - amount = max; - } else { - amount = output.amount; - } - - // total ETH spent (amount + fee), in ERC20 only fee - const totalSpent = new BigNumber(calculateTotal(amount, feeInWei)); - - if ( - new BigNumber(feeInWei).gt(availableBalance) || - (compareWithAmount && totalSpent.isGreaterThan(availableBalance)) - ) { - const error = 'TR_STAKE_NOT_ENOUGH_FUNDS'; - - // errorMessage declared later - return { - type: 'error', - error, - errorMessage: { id: error, values: { symbol: symbol.toUpperCase() } }, - } as const; - } - - const payloadData = { - type: 'nonfinal' as const, - totalSpent: totalSpent.toString(), - max, - fee: feeInWei, - feePerByte: feeLevel.feePerUnit, - feeLimit: feeLevel.feeLimit, - bytes: 0, // TODO: calculate - inputs: [], + const stakingParams = { + feeInBaseUnits: feeInWei, + minBalanceForStakingInBaseUnits: toWei(MIN_ETH_BALANCE_FOR_STAKING.toString(), 'ether'), + minAmountForStakingInBaseUnits: toWei(MIN_ETH_AMOUNT_FOR_STAKING.toString(), 'ether'), + minAmountForWithdrawalInBaseUnits: toWei(MIN_ETH_FOR_WITHDRAWALS.toString(), 'ether'), }; - if (output.type === 'send-max' || output.type === 'payment') { - return { - ...payloadData, - type: 'final', - // compatibility with BTC PrecomposedTransaction from @trezor/connect - inputs: [], - outputsPermutation: [0], - outputs: [ - { - address: output.address, - amount, - script_type: 'PAYTOADDRESS', - }, - ], - }; - } - - return payloadData; + return calculate(availableBalance, output, feeLevel, compareWithAmount, symbol, stakingParams); }; export const composeTransaction = (formValues: StakeFormState, formState: ComposeActionContext) => async () => { - const { account, network, feeInfo } = formState; - const composeOutputs = getExternalComposeOutput(formValues, account, network); - if (!composeOutputs) return; // no valid Output + const { account, feeInfo } = formState; + if (!account || !feeInfo) return; - const { output, decimals } = composeOutputs; - const { availableBalance } = account; const { amount } = formValues.outputs[0]; // gasLimit calculation based on account.descriptor and amount - const { ethereumStakeType } = formValues; + const { stakeType } = formValues; const stakeTxGasLimit = await getStakeTxGasLimit({ - ethereumStakeType, + stakeType, from: account.descriptor, amount, symbol: account.symbol, @@ -156,34 +88,13 @@ export const composeTransaction = }); } - // wrap response into PrecomposedLevels object where key is a FeeLevel label - const wrappedResponse: PrecomposedLevels = {}; - const compareWithAmount = formValues.ethereumStakeType === 'stake'; - const response = predefinedLevels.map(level => - calculate(availableBalance, output, level, compareWithAmount, account.symbol), + return composeStakingTransaction( + formValues, + formState, + predefinedLevels, + calculateTransaction, + customFeeLimit, ); - response.forEach((tx, index) => { - const feeLabel = predefinedLevels[index].label as FeeLevel['label']; - wrappedResponse[feeLabel] = tx; - }); - - // format max (calculate sends it as satoshi) - // update errorMessage values (symbol) - Object.keys(wrappedResponse).forEach(key => { - const tx = wrappedResponse[key]; - if (tx.type !== 'error') { - tx.max = tx.max ? formatAmount(tx.max, decimals) : undefined; - tx.estimatedFeeLimit = customFeeLimit; - } - if (tx.type === 'error' && tx.error === 'AMOUNT_NOT_ENOUGH_CURRENCY_FEE') { - tx.errorMessage = { - id: 'AMOUNT_NOT_ENOUGH_CURRENCY_FEE', - values: { symbol: network.symbol.toUpperCase() }, - }; - } - }); - - return wrappedResponse; }; export const signTransaction = @@ -227,9 +138,9 @@ export const signTransaction = const identity = getAccountIdentity(account); // transform to TrezorConnect.ethereumSignTransaction params - const { ethereumStakeType } = formValues; + const { stakeType } = formValues; let txData; - if (ethereumStakeType === 'stake') { + if (stakeType === 'stake') { txData = await prepareStakeEthTx({ symbol: account.symbol, from: account.descriptor, @@ -240,7 +151,7 @@ export const signTransaction = chainId: network.chainId, }); } - if (ethereumStakeType === 'unstake') { + if (stakeType === 'unstake') { txData = await prepareUnstakeEthTx({ symbol: account.symbol, from: account.descriptor, @@ -252,7 +163,7 @@ export const signTransaction = interchanges: UNSTAKE_INTERCHANGES, }); } - if (ethereumStakeType === 'claim') { + if (stakeType === 'claim') { txData = await prepareClaimEthTx({ symbol: account.symbol, from: account.descriptor, diff --git a/packages/suite/src/actions/wallet/stake/stakeFormSolanaActions.ts b/packages/suite/src/actions/wallet/stake/stakeFormSolanaActions.ts new file mode 100644 index 00000000000..f08aa48d237 --- /dev/null +++ b/packages/suite/src/actions/wallet/stake/stakeFormSolanaActions.ts @@ -0,0 +1,152 @@ +import { BigNumber } from '@trezor/utils/src/bigNumber'; +import TrezorConnect, { FeeLevel } from '@trezor/connect'; +import { notificationsActions } from '@suite-common/toast-notifications'; +import { networkAmountToSmallestUnit } from '@suite-common/wallet-utils'; +import { + StakeFormState, + PrecomposedTransaction, + PrecomposedTransactionFinal, + ExternalOutput, + AddressDisplayOptions, +} from '@suite-common/wallet-types'; +import { selectDevice, ComposeActionContext } from '@suite-common/wallet-core'; +import { NetworkSymbol } from '@suite-common/wallet-config'; +import { + MIN_SOL_AMOUNT_FOR_STAKING, + MIN_SOL_BALANCE_FOR_STAKING, + MIN_SOL_FOR_WITHDRAWALS, +} from '@suite-common/wallet-constants'; + +import { Dispatch, GetState } from 'src/types/suite'; +import { selectAddressDisplayType } from 'src/reducers/suite/suiteReducer'; +import { getPubKeyFromAddress, prepareStakeSolTx } from 'src/utils/suite/solanaStaking'; + +import { calculate, composeStakingTransaction } from './stakeFormActions'; + +const calculateTransaction = ( + availableBalance: string, + output: ExternalOutput, + feeLevel: FeeLevel, + compareWithAmount = true, + symbol: NetworkSymbol, +): PrecomposedTransaction => { + const feeInLamports = new BigNumber(feeLevel.feePerTx ?? '0').toString(); + + const stakingParams = { + feeInBaseUnits: feeInLamports, + minBalanceForStakingInBaseUnits: networkAmountToSmallestUnit(MIN_SOL_BALANCE_FOR_STAKING.toString(), symbol), + minAmountForStakingInBaseUnits: networkAmountToSmallestUnit(MIN_SOL_AMOUNT_FOR_STAKING.toString(), symbol), + minAmountForWithdrawalInBaseUnits: networkAmountToSmallestUnit(MIN_SOL_FOR_WITHDRAWALS.toString(), symbol), + }; + + return calculate(availableBalance, output, feeLevel, compareWithAmount, symbol, stakingParams); +}; + +export const composeTransaction = + (formValues: StakeFormState, formState: ComposeActionContext) => () => { + const { feeInfo } = formState; + if (!feeInfo) return; + + const { levels } = feeInfo; + const predefinedLevels = levels.filter(l => l.label !== 'custom'); + + return composeStakingTransaction( + formValues, + formState, + predefinedLevels, + calculateTransaction, + undefined, + ); + }; + +export const signTransaction = + (formValues: StakeFormState, transactionInfo: PrecomposedTransactionFinal) => + async (dispatch: Dispatch, getState: GetState) => { + const { selectedAccount, blockchain } = getState().wallet; + + const device = selectDevice(getState()); + if ( + selectedAccount.status !== 'loaded' || + !device || + !transactionInfo || + transactionInfo.type !== 'final' + ) + return; + + const { account } = selectedAccount; + if (account.networkType !== 'solana') return; + + const selectedBlockchain = blockchain[account.symbol]; + const addressDisplayType = selectAddressDisplayType(getState()); + const { stakeType } = formValues; + + let txData; + if (stakeType === 'stake') { + txData = await prepareStakeSolTx({ + from: account.descriptor, + path: account.path, + amount: formValues.outputs[0].amount, + symbol: account.symbol, + selectedBlockchain, + }); + } + + if (!txData) { + dispatch( + notificationsActions.addToast({ + type: 'sign-tx-error', + error: 'Unknown stake action', + }), + ); + + return; + } + + if (!txData.success) { + dispatch( + notificationsActions.addToast({ + type: 'sign-tx-error', + error: txData.errorMessage, + }), + ); + + return; + } + + const signedTx = await TrezorConnect.solanaSignTransaction({ + device: { + path: device.path, + instance: device.instance, + state: device.state, + }, + useEmptyPassphrase: device.useEmptyPassphrase, + path: account.path, + serializedTx: txData.tx.serializedTx, + chunkify: addressDisplayType === AddressDisplayOptions.CHUNKED, + }); + + if (!signedTx.success) { + // catch manual error from TransactionReviewModal + if (signedTx.payload.error === 'tx-cancelled') return; + dispatch( + notificationsActions.addToast({ + type: 'sign-tx-error', + error: signedTx.payload.error, + }), + ); + + return; + } + + const signerPubKey = getPubKeyFromAddress(account.descriptor); + + txData.tx.versionedTx.addSignature( + signerPubKey, + Uint8Array.from(Buffer.from(signedTx.payload.signature, 'hex')), + ); + + const serializedVersiondeTx = txData.tx.versionedTx.serialize(); + const signedSerializedTx = Buffer.from(serializedVersiondeTx).toString('hex'); + + return signedSerializedTx; + }; diff --git a/packages/suite/src/actions/wallet/stakeActions.ts b/packages/suite/src/actions/wallet/stakeActions.ts index af0a0640b14..3a43216423e 100644 --- a/packages/suite/src/actions/wallet/stakeActions.ts +++ b/packages/suite/src/actions/wallet/stakeActions.ts @@ -11,6 +11,8 @@ import { notificationsActions } from '@suite-common/toast-notifications'; import { formatNetworkAmount, isRbfTransaction, + isSupportedEthStakingNetworkSymbol, + isSupportedSolStakingNetworkSymbol, tryGetAccountIdentity, } from '@suite-common/wallet-utils'; import { StakeFormState, PrecomposedTransactionFinal, StakeType } from '@suite-common/wallet-types'; @@ -19,15 +21,21 @@ import { Dispatch, GetState } from 'src/types/suite'; import * as modalActions from '../suite/modalActions'; import * as stakeFormEthereumActions from './stake/stakeFormEthereumActions'; +import * as stakeFormSolanaActions from './stake/stakeFormSolanaActions'; import { openModal } from '../suite/modalActions'; export const composeTransaction = (formValues: StakeFormState, formState: ComposeActionContext) => (dispatch: Dispatch) => { const { account } = formState; - if (account.networkType === 'ethereum') { + + if (isSupportedEthStakingNetworkSymbol(account.symbol)) { return dispatch(stakeFormEthereumActions.composeTransaction(formValues, formState)); } + if (isSupportedSolStakingNetworkSymbol(account.symbol)) { + return dispatch(stakeFormSolanaActions.composeTransaction(formValues, formState)); + } + return Promise.resolve(undefined); }; @@ -45,9 +53,9 @@ export const cancelSignTx = (isSuccessTx?: boolean) => (dispatch: Dispatch, getS // otherwise just close modal and open stake modal dispatch(modalActions.onCancel()); - const { ethereumStakeType } = precomposedForm ?? {}; - if (ethereumStakeType && !isSuccessTx) { - dispatch(openModal({ type: ethereumStakeType })); + const { stakeType } = precomposedForm ?? {}; + if (stakeType && !isSuccessTx) { + dispatch(openModal({ type: stakeType })); } }; @@ -170,19 +178,25 @@ export const signTransaction = // signTransaction by Trezor let serializedTx: string | undefined; - if (account.networkType === 'ethereum') { + if (isSupportedEthStakingNetworkSymbol(account.symbol)) { serializedTx = await dispatch( stakeFormEthereumActions.signTransaction(formValues, enhancedTxInfo), ); } + if (isSupportedSolStakingNetworkSymbol(account.symbol)) { + serializedTx = await dispatch( + stakeFormSolanaActions.signTransaction(formValues, enhancedTxInfo), + ); + } + if (!serializedTx) { // close modal manually since UI.CLOSE_UI.WINDOW was blocked dispatch(modalActions.onCancel()); - const { ethereumStakeType } = formValues; - if (ethereumStakeType) { - dispatch(openModal({ type: ethereumStakeType })); + const { stakeType } = formValues; + if (stakeType) { + dispatch(openModal({ type: stakeType })); } return; @@ -202,6 +216,6 @@ export const signTransaction = ); if (decision) { // push tx to the network - return dispatch(pushTransaction(formValues.ethereumStakeType)); + return dispatch(pushTransaction(formValues.stakeType)); } }; diff --git a/packages/suite/src/components/suite/StakingProcess/StakingInfo.tsx b/packages/suite/src/components/suite/StakingProcess/StakingInfo.tsx index 4c83cdab32b..f8ac34af289 100644 --- a/packages/suite/src/components/suite/StakingProcess/StakingInfo.tsx +++ b/packages/suite/src/components/suite/StakingProcess/StakingInfo.tsx @@ -11,7 +11,7 @@ import { } from '@suite-common/wallet-core'; import { Translation } from 'src/components/suite'; -import { getDaysToAddToPool } from 'src/utils/suite/stake'; +import { getDaysToAddToPool } from 'src/utils/suite/ethereumStaking'; import { CoinjoinRootState } from 'src/reducers/wallet/coinjoinReducer'; import { InfoRow } from './InfoRow'; diff --git a/packages/suite/src/components/suite/StakingProcess/UnstakingInfo.tsx b/packages/suite/src/components/suite/StakingProcess/UnstakingInfo.tsx index 6c1a45ae4ea..46bf2f4e5a5 100644 --- a/packages/suite/src/components/suite/StakingProcess/UnstakingInfo.tsx +++ b/packages/suite/src/components/suite/StakingProcess/UnstakingInfo.tsx @@ -10,7 +10,7 @@ import { } from '@suite-common/wallet-core'; import { Translation } from 'src/components/suite'; -import { getDaysToUnstake } from 'src/utils/suite/stake'; +import { getDaysToUnstake } from 'src/utils/suite/ethereumStaking'; import { CoinjoinRootState } from 'src/reducers/wallet/coinjoinReducer'; import { InfoRow } from './InfoRow'; diff --git a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewEvmExplanation.tsx b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewEvmExplanation.tsx index 02077fb292d..7ad012d947a 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewEvmExplanation.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewEvmExplanation.tsx @@ -8,16 +8,16 @@ import { Translation } from 'src/components/suite'; type TransactionReviewEvmExplanationProps = { account: Account; - ethereumStakeType: StakeType | null; + stakeType: StakeType | null; }; export const TransactionReviewEvmExplanation = ({ account, - ethereumStakeType, + stakeType, }: TransactionReviewEvmExplanationProps) => { const network = networks[account.symbol]; - if (network.networkType !== 'ethereum' || ethereumStakeType) { + if (network.networkType !== 'ethereum' || stakeType) { return null; } diff --git a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewModalContent.tsx b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewModalContent.tsx index 81491dbc229..38f1e7be8f8 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewModalContent.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewModalContent.tsx @@ -50,7 +50,7 @@ const isStakeState = (state: SendState | StakeState): state is StakeState => { }; const isStakeForm = (form: FormState | StakeFormState): form is StakeFormState => { - return 'ethereumStakeType' in form; + return 'stakeType' in form; }; interface TransactionReviewModalContentProps { @@ -108,8 +108,8 @@ export const TransactionReviewModalContent = ({ }); // for bump fee we have to analyze tx data which are in outputs[0] - const ethereumStakeType = isStakeForm(precomposedForm) - ? precomposedForm.ethereumStakeType + const stakeType = isStakeForm(precomposedForm) + ? precomposedForm.stakeType : getTxStakeNameByDataHex(outputs[0]?.value); // get estimate mining time @@ -153,9 +153,9 @@ export const TransactionReviewModalContent = ({ broadcast={precomposedForm.options.includes('broadcast')} detailsOpen={detailsOpen} onDetailsClick={() => setDetailsOpen(!detailsOpen)} - ethereumStakeType={ethereumStakeType} + stakeType={stakeType} actionText={getTransactionReviewModalActionText({ - ethereumStakeType, + stakeType, isRbfAction, })} /> @@ -170,18 +170,15 @@ export const TransactionReviewModalContent = ({ buttonRequestsCount={buttonRequestsCount} isRbfAction={isRbfAction} actionText={getTransactionReviewModalActionText({ - ethereumStakeType, + stakeType, isRbfAction, isSending, })} isSending={isSending} setIsSending={() => setIsSending(true)} - ethereumStakeType={ethereumStakeType || undefined} - /> - + ); }; diff --git a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutput.tsx b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutput.tsx index 505776af8be..002c5369b24 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutput.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutput.tsx @@ -54,17 +54,16 @@ export type TransactionReviewOutputProps = { symbol: NetworkSymbol; account: Account; isRbf: boolean; - ethereumStakeType?: StakeType; + stakeType?: StakeType; } & ReviewOutput; export const TransactionReviewOutput = forwardRef( (props, ref) => { - const { type, state, label, value, symbol, token, account, ethereumStakeType, isRbf } = - props; + const { type, state, label, value, symbol, token, account, stakeType, isRbf } = props; let outputLabel: ReactNode = label; const { networkType } = account; const { translationString } = useTranslation(); - const displayMode = useDisplayMode({ ethereumStakeType, type }); + const displayMode = useDisplayMode({ stakeType, type }); if (type === 'locktime') { const isTimestamp = new BigNumber(value).gte(BTC_LOCKTIME_VALUE); @@ -157,7 +156,7 @@ export const TransactionReviewOutput = forwardRef void; - ethereumStakeType?: StakeType; + stakeType?: StakeType; } export const TransactionReviewOutputList = ({ @@ -115,7 +115,7 @@ export const TransactionReviewOutputList = ({ actionText, isSending, setIsSending, - ethereumStakeType, + stakeType, }: TransactionReviewOutputListProps) => { const dispatch = useDispatch(); const { networkType } = account; @@ -225,7 +225,7 @@ export const TransactionReviewOutputList = ({ symbol={symbol} account={account} isRbf={isRbfAction} - ethereumStakeType={ethereumStakeType} + stakeType={stakeType} /> ); })} @@ -237,7 +237,7 @@ export const TransactionReviewOutputList = ({ outputs={outputs} buttonRequestsCount={buttonRequestsCount} precomposedTx={precomposedTx} - ethereumStakeType={ethereumStakeType} + stakeType={stakeType} isRbfAction={isRbfAction} /> )} diff --git a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewTotalOutput.tsx b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewTotalOutput.tsx index 2e9c18ed4d1..54e0e40020c 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewTotalOutput.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewTotalOutput.tsx @@ -47,7 +47,7 @@ const getLines = ( symbol: TransactionReviewOutputListProps['account']['symbol'], precomposedTx: TransactionReviewOutputListProps['precomposedTx'], isRbfAction?: boolean, - ethereumStakeType?: StakeType, + stakeType?: StakeType, ): Array => { const isUpdatedSendFlow = getIsUpdatedSendFlow(device); const isUpdatedEthereumSendFlow = getIsUpdatedEthereumSendFlow(device, networkType); @@ -70,7 +70,7 @@ const getLines = ( .toString(); if (isUpdatedEthereumSendFlow) { - const isUnknownStakingClaimValue = isRbfAction && ethereumStakeType === 'claim'; + const isUnknownStakingClaimValue = isRbfAction && stakeType === 'claim'; const amountLine = { id: 'amount', // In updated ethereum send flow there is no total amount shown, only amount without fee label: , @@ -120,15 +120,7 @@ export const TransactionReviewTotalOutput = forwardRef< TransactionReviewTotalOutputProps >( ( - { - account, - signedTx, - outputs, - buttonRequestsCount, - precomposedTx, - ethereumStakeType, - isRbfAction, - }, + { account, signedTx, outputs, buttonRequestsCount, precomposedTx, stakeType, isRbfAction }, ref, ) => { const device = useSelector(selectDevice); @@ -139,14 +131,7 @@ export const TransactionReviewTotalOutput = forwardRef< const { symbol, networkType } = account; - const lines = getLines( - device, - networkType, - symbol, - precomposedTx, - isRbfAction, - ethereumStakeType, - ); + const lines = getLines(device, networkType, symbol, precomposedTx, isRbfAction, stakeType); return ( void; - ethereumStakeType?: StakeType | null; + stakeType?: StakeType | null; actionText: TranslationKey; } @@ -221,7 +221,7 @@ export const TransactionReviewSummary = ({ broadcast, detailsOpen, onDetailsClick, - ethereumStakeType, + stakeType, actionText, }: TransactionReviewSummaryProps) => { const drafts = useSelector(state => state.wallet.send.drafts); @@ -327,7 +327,7 @@ export const TransactionReviewSummary = ({ )} - {!ethereumStakeType && ( + {!stakeType && ( diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeEthInANutshellModal/StakeEthInANutshellModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeEthInANutshellModal/StakeEthInANutshellModal.tsx index af32c823c73..d0a36e3871b 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeEthInANutshellModal/StakeEthInANutshellModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeEthInANutshellModal/StakeEthInANutshellModal.tsx @@ -21,7 +21,7 @@ import { Translation } from 'src/components/suite'; import { useDispatch, useSelector } from 'src/hooks/suite'; import { openModal } from 'src/actions/suite/modalActions'; import { selectSelectedAccount } from 'src/reducers/wallet/selectedAccountReducer'; -import { getUnstakingPeriodInDays } from 'src/utils/suite/stake'; +import { getUnstakingPeriodInDays } from 'src/utils/suite/ethereumStaking'; import { StakingInfo } from 'src/components/suite/StakingProcess/StakingInfo'; import { UnstakingInfo } from 'src/components/suite/StakingProcess/UnstakingInfo'; diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeEthForm/ConfirmStakeEthModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeEthForm/ConfirmStakeEthModal.tsx index 3917e5b1348..45dcb7f6ed1 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeEthForm/ConfirmStakeEthModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeEthForm/ConfirmStakeEthModal.tsx @@ -3,13 +3,20 @@ import { useState } from 'react'; import { Checkbox, NewModal, Column, Banner, Card } from '@trezor/components'; import { spacings } from '@trezor/theme'; import { selectValidatorsQueueData } from '@suite-common/wallet-core'; +import { NetworkType } from '@suite-common/wallet-config'; import { HELP_CENTER_ETH_STAKING } from '@trezor/urls'; import { Translation, TrezorLink } from 'src/components/suite'; import { useDispatch, useSelector } from 'src/hooks/suite'; import { openModal } from 'src/actions/suite/modalActions'; import { selectSelectedAccount } from 'src/reducers/wallet/selectedAccountReducer'; -import { getDaysToAddToPoolInitial } from 'src/utils/suite/stake'; +import { getDaysToAddToPoolInitial } from 'src/utils/suite/ethereumStaking'; + +const getStakeEnteringMessage = (networkType?: NetworkType) => { + if (networkType === 'ethereum') return 'TR_STAKE_ENTERING_POOL_MAY_TAKE'; + + return 'TR_STAKE_ACTIVATION_COULD_TAKE'; +}; interface ConfirmStakeEthModalProps { isLoading: boolean; @@ -60,7 +67,7 @@ export const ConfirmStakeEthModal = ({ { diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeModal.tsx index 01f910f0056..8042c5840c3 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeModal.tsx @@ -29,7 +29,12 @@ export const StakeModal = ({ onCancel }: StakeModalModalProps) => { } + heading={ + + } onCancel={onCancel} bottomContent={} > diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakingInfoCards/EstimatedGains.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakingInfoCards/EstimatedGains.tsx index 9f65c58fa24..e172a8371ca 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakingInfoCards/EstimatedGains.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakingInfoCards/EstimatedGains.tsx @@ -10,7 +10,7 @@ import { Translation } from 'src/components/suite/Translation'; import { useStakeEthFormContext } from 'src/hooks/wallet/useStakeEthForm'; import { CRYPTO_INPUT } from 'src/types/wallet/stakeForms'; import { FiatValue, FormattedCryptoAmount, TrezorLink } from 'src/components/suite'; -import { calculateGains } from 'src/utils/suite/stake'; +import { calculateGains } from 'src/utils/suite/ethereumStaking'; export const EstimatedGains = () => { const { account, getValues, formState } = useStakeEthFormContext(); diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UnstakeModal/EverstakeModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UnstakeModal/EverstakeModal.tsx index 18aab999bb4..4fae6e5af0a 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UnstakeModal/EverstakeModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UnstakeModal/EverstakeModal.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { Checkbox, NewModal, Column, Banner, Card } from '@trezor/components'; +import { Checkbox, NewModal, Column, Banner, Card, IconName } from '@trezor/components'; import { spacings } from '@trezor/theme'; import { Translation } from 'src/components/suite'; @@ -22,9 +22,51 @@ export const EverstakeModal = ({ onCancel }: EverstakeModalProps) => { dispatch(openModal({ type: 'stake' })); }; + const banners: { + icon: IconName; + message: JSX.Element; + }[] = [ + { + icon: 'fileFilled', + message: ( + {text}, + }} + /> + ), + }, + { + icon: 'shieldWarningFilled', + message: ( + + ), + }, + ]; + return ( } + heading={ + + } description={} onCancel={onCancel} size="small" @@ -40,18 +82,11 @@ export const EverstakeModal = ({ onCancel }: EverstakeModalProps) => { } > - - {text}, - }} - /> - - - - + {banners.map(({ icon, message }, index) => ( + + {message} + + ))} { + const isDebugModeActive = useSelector(selectIsDebugModeActive); + const account = useSelector(selectSelectedAccount); const routerParams = useSelector(state => state.router.params) as WalletParams; const dispatch = useDispatch(); @@ -60,7 +63,10 @@ export const AccountNavigation = () => { goToWithAnalytics('wallet-staking', { preserveParams: true }); }, title: , - isHidden: !hasNetworkFeatures(account, 'staking'), + // TODO: remove 'solana' and debug mode check from the condition when staking will be ready for launch + isHidden: + !hasNetworkFeatures(account, 'staking') || + (!isDebugModeActive && 'solana' === networkType), 'data-testid': '@wallet/menu/staking', }, { diff --git a/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountItemsGroup.tsx b/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountItemsGroup.tsx index 6d88c558c90..373850bb912 100644 --- a/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountItemsGroup.tsx +++ b/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountItemsGroup.tsx @@ -87,7 +87,7 @@ export const AccountItemsGroup = ({ account={account} type="staking" isSelected={selected && routeName === 'wallet-staking'} - formattedBalance={stakingBalance} + formattedBalance={stakingBalance ?? '0'} isGroup isGroupSelected={selected} dataTestKey={`${dataTestKey}/staking`} diff --git a/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountSection.tsx b/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountSection.tsx index 06dc023a1d5..0d2b9d65bbc 100644 --- a/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountSection.tsx +++ b/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountSection.tsx @@ -1,10 +1,11 @@ import { Account } from '@suite-common/wallet-types'; import { selectCoinDefinitions } from '@suite-common/token-definitions'; -import { selectAccountHasStaked } from '@suite-common/wallet-core'; -import { isSupportedEthStakingNetworkSymbol } from '@suite-common/wallet-utils'; +import { selectAccountHasStaked, selectStakingAccounts } from '@suite-common/wallet-core'; +import { isSupportedStakingNetworkSymbol } from '@suite-common/wallet-utils'; import { useSelector } from 'src/hooks/suite'; import { getTokens } from 'src/utils/wallet/tokenUtils'; +import { selectIsDebugModeActive } from 'src/reducers/suite/suiteReducer'; import { AccountItem } from './AccountItem/AccountItem'; import { AccountItemsGroup } from './AccountItemsGroup'; @@ -32,10 +33,16 @@ export const AccountSection = ({ tokens: accountTokens = [], } = account; + const isDebugModeActive = useSelector(selectIsDebugModeActive); + const coinDefinitions = useSelector(state => selectCoinDefinitions(state, symbol)); const hasStaked = useSelector(state => selectAccountHasStaked(state, account.key)); + const stakingAccounts = useSelector(state => selectStakingAccounts(state, account.key)); + // TODO: remove isDebugModeActive when staking will be ready for launch + const hasStakingAccount = !!stakingAccounts?.length && isDebugModeActive; // for solana - const isStakeShown = isSupportedEthStakingNetworkSymbol(symbol) && hasStaked; + const isStakeShown = + isSupportedStakingNetworkSymbol(symbol) && (hasStaked || hasStakingAccount); const showGroup = ['ethereum', 'solana', 'cardano'].includes(networkType); diff --git a/packages/suite/src/components/wallet/WalletLayout/WalletLayout.tsx b/packages/suite/src/components/wallet/WalletLayout/WalletLayout.tsx index 92c022f4221..e9bd0106a83 100644 --- a/packages/suite/src/components/wallet/WalletLayout/WalletLayout.tsx +++ b/packages/suite/src/components/wallet/WalletLayout/WalletLayout.tsx @@ -3,6 +3,7 @@ import { ReactNode } from 'react'; import styled from 'styled-components'; import { SkeletonRectangle } from '@trezor/components'; +import { PrimitiveType } from '@trezor/type-utils'; import { AppState, ExtendedMessageDescriptor } from 'src/types/suite'; import { useTranslation, useLayout } from 'src/hooks/suite'; @@ -38,6 +39,7 @@ const WalletPageHeader = ({ isSubpage }: WalletPageHeaderProps) => { type WalletLayoutProps = { title: ExtendedMessageDescriptor['id']; + titleValues?: Record; account: AppState['wallet']['selectedAccount']; isSubpage?: boolean; showEmptyHeaderPlaceholder?: boolean; @@ -48,13 +50,14 @@ type WalletLayoutProps = { export const WalletLayout = ({ showEmptyHeaderPlaceholder = false, title, + titleValues, account, isSubpage, className, children, }: WalletLayoutProps) => { const { translationString } = useTranslation(); - const l10nTitle = translationString(title); + const l10nTitle = translationString(title, titleValues); useLayout(l10nTitle, ); diff --git a/packages/suite/src/hooks/suite/useDisplayMode.ts b/packages/suite/src/hooks/suite/useDisplayMode.ts index eb591836333..b02c4f3d06e 100644 --- a/packages/suite/src/hooks/suite/useDisplayMode.ts +++ b/packages/suite/src/hooks/suite/useDisplayMode.ts @@ -9,15 +9,15 @@ import { useSelector } from './useSelector'; type UseDisplayModeProps = { type: ReviewOutput['type']; - ethereumStakeType?: StakeType; + stakeType?: StakeType; }; -export const useDisplayMode = ({ type, ethereumStakeType }: UseDisplayModeProps) => { +export const useDisplayMode = ({ type, stakeType }: UseDisplayModeProps) => { const account = useSelector(selectSelectedAccount); const unavailableCapabilities = useSelector(selectDeviceUnavailableCapabilities); const addressDisplayType = useSelector(selectAddressDisplayType); - if (ethereumStakeType || ['data', 'opreturn'].includes(type)) { + if (stakeType || ['data', 'opreturn'].includes(type)) { return DisplayMode.SINGLE_WRAPPED_TEXT; } diff --git a/packages/suite/src/hooks/wallet/useClaimEthForm.ts b/packages/suite/src/hooks/wallet/useClaimEthForm.ts index 646afd34292..b3ad2c35365 100644 --- a/packages/suite/src/hooks/wallet/useClaimEthForm.ts +++ b/packages/suite/src/hooks/wallet/useClaimEthForm.ts @@ -1,8 +1,6 @@ import { createContext, useCallback, useContext, useEffect, useMemo } from 'react'; import { useForm } from 'react-hook-form'; -import { selectNetwork } from '@everstake/wallet-sdk/ethereum'; - import { getFeeLevels } from '@suite-common/wallet-utils'; import { PrecomposedTransactionFinal } from '@suite-common/wallet-types'; @@ -10,7 +8,10 @@ import { useDispatch, useSelector } from 'src/hooks/suite'; import { CRYPTO_INPUT, OUTPUT_AMOUNT, UseStakeFormsProps } from 'src/types/wallet/stakeForms'; import { selectLocalCurrency } from 'src/reducers/wallet/settingsReducer'; import { signTransaction } from 'src/actions/wallet/stakeActions'; -import { getEthNetworkForWalletSdk, getStakeFormsDefaultValues } from 'src/utils/suite/stake'; +import { + getEthNetworkAddresses, + getStakeFormsDefaultValues, +} from 'src/utils/suite/ethereumStaking'; import { ClaimContextValues, ClaimFormState } from 'src/types/wallet/claimForm'; import { useFees } from './form/useFees'; @@ -28,14 +29,13 @@ export const useClaimEthForm = ({ selectedAccount }: UseStakeFormsProps): ClaimC const symbolFees = useSelector(state => state.wallet.fees[account.symbol]); const defaultValues = useMemo(() => { - const { address_accounting: accountingAddress } = selectNetwork( - getEthNetworkForWalletSdk(account.symbol), - ); + + const { addressContractAccounting } = getEthNetworkAddresses(account.symbol); return { ...getStakeFormsDefaultValues({ - address: accountingAddress, - ethereumStakeType: 'claim', + address: addressContractAccounting, + stakeType: 'claim', }), } as ClaimFormState; }, [account.symbol]); diff --git a/packages/suite/src/hooks/wallet/useStakeEthForm.ts b/packages/suite/src/hooks/wallet/useStakeEthForm.ts index 67315892f97..3b0771097ec 100644 --- a/packages/suite/src/hooks/wallet/useStakeEthForm.ts +++ b/packages/suite/src/hooks/wallet/useStakeEthForm.ts @@ -3,7 +3,6 @@ import { useForm, useWatch } from 'react-hook-form'; import useDebounce from 'react-use/lib/useDebounce'; import { fromWei } from 'web3-utils'; -import { selectNetwork } from '@everstake/wallet-sdk/ethereum'; import { BigNumber } from '@trezor/utils/src/bigNumber'; import { @@ -31,7 +30,10 @@ import { } from 'src/types/wallet/stakeForms'; import { selectLocalCurrency } from 'src/reducers/wallet/settingsReducer'; import { signTransaction } from 'src/actions/wallet/stakeActions'; -import { getEthNetworkForWalletSdk, getStakeFormsDefaultValues } from 'src/utils/suite/stake'; +import { + getEthNetworkAddresses, + getStakeFormsDefaultValues, +} from 'src/utils/suite/ethereumStaking'; import type { CryptoAmountLimitProps } from 'src/utils/suite/validation'; import { useStakeCompose } from './form/useStakeCompose'; @@ -41,6 +43,7 @@ import { useFees } from './form/useFees'; export const StakeEthFormContext = createContext(null); StakeEthFormContext.displayName = 'StakeEthFormContext'; +// TODO: refactor this hook to support both ethereum and solana export const useStakeEthForm = ({ selectedAccount }: UseStakeFormsProps): StakeContextValues => { const dispatch = useDispatch(); @@ -61,14 +64,13 @@ export const useStakeEthForm = ({ selectedAccount }: UseStakeFormsProps): StakeC }; const defaultValues = useMemo(() => { - const { address_pool: poolAddress } = selectNetwork( - getEthNetworkForWalletSdk(account.symbol), - ); + // TODO: get the address for solana here + const { addressContractPool } = getEthNetworkAddresses(account.symbol); return { ...getStakeFormsDefaultValues({ - address: poolAddress, - ethereumStakeType: 'stake', + address: addressContractPool, + stakeType: 'stake', }), setMaxOutputId: undefined, } as StakeFormState; @@ -353,7 +355,7 @@ export const useStakeEthForm = ({ selectedAccount }: UseStakeFormsProps): StakeC const signTx = useCallback(async () => { const values = getValues(); const composedTx = composedLevels ? composedLevels[selectedFee] : undefined; - if (composedTx && composedTx.type === 'final') { + if (composedTx && composedTx.type === 'final') { setIsLoading(true); const result = await dispatch( signTransaction(values, composedTx as PrecomposedTransactionFinal), diff --git a/packages/suite/src/hooks/wallet/useUnstakeEthForm.ts b/packages/suite/src/hooks/wallet/useUnstakeEthForm.ts index 67018e01ad4..94ea4298d29 100644 --- a/packages/suite/src/hooks/wallet/useUnstakeEthForm.ts +++ b/packages/suite/src/hooks/wallet/useUnstakeEthForm.ts @@ -2,7 +2,6 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useState } import { useForm, useWatch } from 'react-hook-form'; import useDebounce from 'react-use/lib/useDebounce'; -import { selectNetwork } from '@everstake/wallet-sdk/ethereum'; import { fromFiatCurrency, @@ -30,10 +29,10 @@ import { import { selectLocalCurrency } from 'src/reducers/wallet/settingsReducer'; import { signTransaction } from 'src/actions/wallet/stakeActions'; import { - getEthNetworkForWalletSdk, + getEthNetworkAddresses, getStakeFormsDefaultValues, simulateUnstake, -} from 'src/utils/suite/stake'; +} from 'src/utils/suite/ethereumStaking'; import type { AmountLimitProps } from 'src/utils/suite/validation'; import { useStakeCompose } from './form/useStakeCompose'; @@ -78,14 +77,12 @@ export const useUnstakeEthForm = ({ }; const defaultValues = useMemo(() => { - const { address_pool: poolAddress } = selectNetwork( - getEthNetworkForWalletSdk(account.symbol), - ); + const { addressContractPool } = getEthNetworkAddresses(account.symbol); return { ...getStakeFormsDefaultValues({ - address: poolAddress, - ethereumStakeType: 'unstake', + address: addressContractPool, + stakeType: 'unstake', amount: autocompoundBalance, }), } as UnstakeFormState; diff --git a/packages/suite/src/support/messages.ts b/packages/suite/src/support/messages.ts index 0a52c6784ce..80686842088 100644 --- a/packages/suite/src/support/messages.ts +++ b/packages/suite/src/support/messages.ts @@ -8560,9 +8560,9 @@ export default defineMessages({ id: 'TR_TO', defaultMessage: 'To', }, - TR_STAKE_ETH: { - id: 'TR_STAKE_ETH', - defaultMessage: 'Stake Ethereum', + TR_STAKE_NETWORK: { + id: 'TR_STAKE_NETWORK', + defaultMessage: 'Stake {networkSymbol}', }, TR_STAKE_RESTAKED_BADGE: { id: 'TR_STAKE_RESTAKED_BADGE', @@ -8580,9 +8580,10 @@ export default defineMessages({ id: 'TR_STAKE_ETH_SEE_MONEY_DANCE', defaultMessage: 'Watch your money dance', }, - TR_STAKE_ETH_SEE_MONEY_DANCE_DESC: { - id: 'TR_STAKE_ETH_SEE_MONEY_DANCE_DESC', - defaultMessage: 'Earn {apyPercent}% APY by staking your Ethereum with Trezor.', + TR_STAKE_NETWORK_SEE_MONEY_DANCE_DESC: { + id: 'TR_STAKE_NETWORK_SEE_MONEY_DANCE_DESC', + defaultMessage: + 'Earn {apyPercent}% APY by staking your {networkSymbol} with Trezor.', }, TR_STAKE_APY_DESC: { id: 'TR_STAKE_APY_DESC', @@ -8631,7 +8632,7 @@ export default defineMessages({ TR_STAKE_STAKING_IS: { id: 'TR_STAKE_STAKING_IS', defaultMessage: - "Staking involves temporarily locking your Ethereum assets to support the blockchain's operation. In return, you'll earn additional Ethereum as a reward.", + "Staking involves temporarily locking your {networkSymbol} to support the blockchain's operation. In return, you'll earn additional {networkSymbol} as a reward.", }, TR_STAKE_ANY_AMOUNT_ETH: { id: 'TR_STAKE_ANY_AMOUNT_ETH', @@ -8736,6 +8737,10 @@ export default defineMessages({ defaultMessage: 'Entering the staking pool may take up to {count, plural, one {# day} other {# days}}', }, + TR_STAKE_ACTIVATION_COULD_TAKE: { + id: 'TR_STAKE_ACTIVATION_COULD_TAKE', + defaultMessage: 'Stake activation usually takes 1 epoch (~3 days)', + }, TR_STAKE_ETH_WILL_BE_BLOCKED: { id: 'TR_STAKE_ETH_WILL_BE_BLOCKED', defaultMessage: @@ -8947,13 +8952,23 @@ export default defineMessages({ TR_STAKE_EVERSTAKE_MANAGES: { id: 'TR_STAKE_EVERSTAKE_MANAGES', defaultMessage: - 'Everstake maintains and protects your staked {symbol} with their smart contracts, infrastructure, and technology.', + 'Everstake maintains and protects your staked {networkSymbol} with their smart contracts, infrastructure, and technology.', }, TR_STAKE_TREZOR_NO_LIABILITY: { id: 'TR_STAKE_TREZOR_NO_LIABILITY', defaultMessage: "When staking, the responsibility for your funds' security transitions from your Trezor to Everstake.", }, + TR_STAKE_BY_STAKING_YOU_CAN_EARN_REWARDS: { + id: 'TR_STAKE_BY_STAKING_YOU_CAN_EARN_REWARDS', + defaultMessage: + 'By staking your {networkSymbol}, you can earn rewards while contributing to the security and stability of the network.', + }, + TR_STAKE_SECURELY_DELEGATE_TO_EVERSTAKE: { + id: 'TR_STAKE_SECURELY_DELEGATE_TO_EVERSTAKE', + defaultMessage: + 'With Trezor Suite, you can effortlessly and securely delegate your {networkSymbol} to Everstake validator node for staking. Enjoy competitive rewards, rely on a trusted validator, and maintain full ownership of your coins.', + }, TR_STAKE_CONSENT_TO_STAKING_WITH_EVERSTAKE: { id: 'TR_STAKE_CONSENT_TO_STAKING_WITH_EVERSTAKE', defaultMessage: 'I acknowledge and consent to staking with Everstake', diff --git a/packages/suite/src/utils/suite/__fixtures__/stake.ts b/packages/suite/src/utils/suite/__fixtures__/ethereumStaking.ts similarity index 99% rename from packages/suite/src/utils/suite/__fixtures__/stake.ts rename to packages/suite/src/utils/suite/__fixtures__/ethereumStaking.ts index cb56784bb95..9e41a200270 100644 --- a/packages/suite/src/utils/suite/__fixtures__/stake.ts +++ b/packages/suite/src/utils/suite/__fixtures__/ethereumStaking.ts @@ -404,7 +404,7 @@ export const getStakeFormsDefaultValuesFixture = [ description: 'should return default values for stake forms', args: { address: '0xfB0bc552ab5Fa1971E8530852753c957e29eEEFC', - ethereumStakeType: 'stake', + stakeType: 'stake', amount: '0.1', }, result: { @@ -422,7 +422,7 @@ export const getStakeFormsDefaultValuesFixture = [ }, ], options: ['broadcast'], - ethereumStakeType: 'stake', + stakeType: 'stake', ethereumNonce: '', ethereumDataAscii: '', ethereumDataHex: '', @@ -440,7 +440,7 @@ export const getStakeFormsDefaultValuesFixture = [ 'should return default values for stake forms with empty amount when amount is invalid', args: { address: '0xfB0bc552ab5Fa1971E8530852753c957e29eEEFC', - ethereumStakeType: 'stake', + stakeType: 'stake', amount: undefined, }, result: { @@ -458,7 +458,7 @@ export const getStakeFormsDefaultValuesFixture = [ }, ], options: ['broadcast'], - ethereumStakeType: 'stake', + stakeType: 'stake', ethereumNonce: '', ethereumDataAscii: '', ethereumDataHex: '', @@ -477,7 +477,7 @@ export const getStakeTxGasLimitFixture = [ { description: 'should return correct gasLimit', args: { - ethereumStakeType: 'stake', + stakeType: 'stake', from: '0xfB0bc552ab5Fa1971E8530852753c957e29eEEFC', amount: '0.1', // eth symbol: 'eth', @@ -497,7 +497,7 @@ export const getStakeTxGasLimitFixture = [ { description: 'should throw an error when stake type is empty', args: { - ethereumStakeType: '', + stakeType: '', from: '0xfB0bc552ab5Fa1971E8530852753c957e29eEEFC', amount: '0.1', // eth symbol: 'eth', diff --git a/packages/suite/src/utils/suite/__tests__/stake.test.ts b/packages/suite/src/utils/suite/__tests__/ethereumStaking.test.ts similarity index 99% rename from packages/suite/src/utils/suite/__tests__/stake.test.ts rename to packages/suite/src/utils/suite/__tests__/ethereumStaking.test.ts index c21e68d1f36..e3cc1ffc9df 100644 --- a/packages/suite/src/utils/suite/__tests__/stake.test.ts +++ b/packages/suite/src/utils/suite/__tests__/ethereumStaking.test.ts @@ -31,7 +31,7 @@ import { getInstantStakeType, getChangedInternalTx, simulateUnstake, -} from '../stake'; +} from '../ethereumStaking'; import { transformTxFixtures, stakeFixture, @@ -51,7 +51,7 @@ import { getInstantStakeTypeFixture, getChangedInternalTxFixture, simulateUnstakeFixture, -} from '../__fixtures__/stake'; +} from '../__fixtures__/ethereumStaking'; describe('transformTx', () => { transformTxFixtures.forEach(test => { diff --git a/packages/suite/src/utils/suite/stake.ts b/packages/suite/src/utils/suite/ethereumStaking.ts similarity index 88% rename from packages/suite/src/utils/suite/stake.ts rename to packages/suite/src/utils/suite/ethereumStaking.ts index 71854bc9e81..da4aa6c4f2d 100644 --- a/packages/suite/src/utils/suite/stake.ts +++ b/packages/suite/src/utils/suite/ethereumStaking.ts @@ -1,4 +1,4 @@ -import { selectNetwork } from '@everstake/wallet-sdk/ethereum'; +import { Ethereum, ETH_NETWORK_ADDRESSES, EthNetworkAddresses } from '@everstake/wallet-sdk'; import { fromWei, numberToHex, toWei } from 'web3-utils'; import { @@ -13,6 +13,8 @@ import { MIN_ETH_AMOUNT_FOR_STAKING, MAX_ETH_AMOUNT_FOR_STAKING, UNSTAKE_INTERCHANGES, + WALLET_SDK_SOURCE, + UNSTAKING_ETH_PERIOD, } from '@suite-common/wallet-constants'; import type { NetworkSymbol } from '@suite-common/wallet-config'; import { getEthereumEstimateFeeParams, isPending, sanitizeHex } from '@suite-common/wallet-utils'; @@ -24,15 +26,6 @@ import { PartialRecord } from '@trezor/type-utils'; import { TranslationFunction } from 'src/hooks/suite/useTranslation'; -// source is a required parameter for some functions in the Everstake Wallet SDK. -// This parameter is used for some contract calls. -// It is a constant which allows the SDK to define which app calls its functions. -// Each app which integrates the SDK has its own source, e.g. source for Trezor Suite is '1'. -export const WALLET_SDK_SOURCE = '1'; - -// Used when Everstake unstaking period is not available from the API. -export const UNSTAKING_ETH_PERIOD = 3; - const secondsToDays = (seconds: number) => Math.round(seconds / 60 / 60 / 24); type EthNetwork = 'holesky' | 'mainnet'; @@ -49,6 +42,16 @@ export const getEthNetworkForWalletSdk = ( return network!; }; +export const getEthNetworkAddresses = (symbol: NetworkSymbol): EthNetworkAddresses => { + const defaultAddresses = ETH_NETWORK_ADDRESSES['mainnet']; + const ethNetwork = getEthNetworkForWalletSdk(symbol); + + if (!ethNetwork) return defaultAddresses; + + return ETH_NETWORK_ADDRESSES[ethNetwork] ?? defaultAddresses; + +}; + export const getAdjustedGasLimitConsumption = (estimatedFee: Success) => new BigNumber(estimatedFee.payload.levels[0].feeLimit || '') .plus(STAKE_GAS_LIMIT_RESERVE) @@ -77,9 +80,11 @@ export const stake = async ({ try { const ethNetwork = getEthNetworkForWalletSdk(symbol); - const { contract_pool: contractPool } = selectNetwork(ethNetwork); - const contractPoolAddress = contractPool.options.address; - const data = contractPool.methods.stake(WALLET_SDK_SOURCE).encodeABI(); + const ethereumClient = new Ethereum(ethNetwork); + const { addressContractPool } = getEthNetworkAddresses(symbol);; + + const contractPoolAddress = ethereumClient.contractPool.options.address; + const data = ethereumClient.contractPool.methods.stake(WALLET_SDK_SOURCE).encodeABI(); // gasLimit calculation based on address, amount and data size // amount is essential for a proper calculation of gasLimit (via blockbook/geth) @@ -90,7 +95,7 @@ export const stake = async ({ blocks: [2], specific: { from, - ...getEthereumEstimateFeeParams(contractPoolAddress, amount, undefined, data), + ...getEthereumEstimateFeeParams(addressContractPool, amount, undefined, data), }, }, }); @@ -151,9 +156,10 @@ export const unstake = async ({ const amountWei = toWei(amount, 'ether'); const ethNetwork = getEthNetworkForWalletSdk(symbol); - const { contract_pool: contractPool } = selectNetwork(ethNetwork); - const contractPoolAddress = contractPool.options.address; - const data = contractPool.methods + const ethereumClient = new Ethereum(ethNetwork); + const { addressContractPool } = getEthNetworkAddresses(symbol);; + const contractPoolAddress = ethereumClient.contractPool.options.address; + const data = ethereumClient.contractPool.methods .unstake(amountWei, interchanges, WALLET_SDK_SOURCE) .encodeABI(); @@ -166,7 +172,7 @@ export const unstake = async ({ blocks: [2], specific: { from, - ...getEthereumEstimateFeeParams(contractPoolAddress, '0', undefined, data), + ...getEthereumEstimateFeeParams(addressContractPool, '0', undefined, data), }, }, }); @@ -213,9 +219,11 @@ export const claimWithdrawRequest = async ({ from, symbol, identity }: StakeTxBa if (!readyForClaim.eq(requested)) throw new Error('Unstake request not filled yet'); const ethNetwork = getEthNetworkForWalletSdk(symbol); - const { contract_accounting: contractAccounting } = selectNetwork(ethNetwork); - const contractAccountingAddress = contractAccounting.options.address; - const data = contractAccounting.methods.claimWithdrawRequest().encodeABI(); + const ethereumClient = new Ethereum(ethNetwork); + const { addressContractAccounting } = getEthNetworkAddresses(symbol);; + + const contractAccountingAddress = ethereumClient.contractAccounting.options.address; + const data = ethereumClient.contractAccounting.methods.claimWithdrawRequest().encodeABI(); // gasLimit calculation based on address, amount and data size // amount is essential for a proper calculation of gasLimit (via blockbook/geth) @@ -227,7 +235,7 @@ export const claimWithdrawRequest = async ({ from, symbol, identity }: StakeTxBa specific: { from, ...getEthereumEstimateFeeParams( - contractAccountingAddress, + addressContractAccounting, '0', undefined, data, @@ -253,13 +261,13 @@ export const claimWithdrawRequest = async ({ from, symbol, identity }: StakeTxBa export interface GetStakeFormsDefaultValuesParams { address: string; - ethereumStakeType: StakeFormState['ethereumStakeType']; + stakeType: StakeFormState['stakeType']; amount?: string; } export const getStakeFormsDefaultValues = ({ address, - ethereumStakeType, + stakeType, amount, }: GetStakeFormsDefaultValuesParams) => ({ fiatInput: '', @@ -273,7 +281,7 @@ export const getStakeFormsDefaultValues = ({ ], options: ['broadcast'], - ethereumStakeType, + stakeType, ethereumNonce: '', ethereumDataAscii: '', ethereumDataHex: '', @@ -427,7 +435,7 @@ export const prepareClaimEthTx = async ({ }; export interface GetStakeTxGasLimitParams { - ethereumStakeType: StakeType | undefined; + stakeType: StakeType | undefined; from: string; amount: string; symbol: NetworkSymbol; @@ -445,7 +453,7 @@ export type GetStakeTxGasLimitResponse = }; export const getStakeTxGasLimit = async ({ - ethereumStakeType, + stakeType, from, amount, symbol, @@ -459,7 +467,7 @@ export const getStakeTxGasLimit = async ({ }, }; - if (!ethereumStakeType) { + if (!stakeType) { return { success: false, error: genericError, @@ -468,10 +476,10 @@ export const getStakeTxGasLimit = async ({ try { let txData; - if (ethereumStakeType === 'stake') { + if (stakeType === 'stake') { txData = await stake({ from, amount, symbol, identity }); } - if (ethereumStakeType === 'unstake') { + if (stakeType === 'unstake') { // Increase allowedInterchangeNum to enable instant unstaking. txData = await unstake({ from, @@ -481,7 +489,7 @@ export const getStakeTxGasLimit = async ({ identity, }); } - if (ethereumStakeType === 'claim') { + if (stakeType === 'claim') { txData = await claimWithdrawRequest({ from, symbol, identity }); } @@ -578,19 +586,17 @@ export const getInstantStakeType = ( ): StakeType | null => { if (!address || !symbol) return null; const { from, to } = internalTransfer; - const ethNetwork = getEthNetworkForWalletSdk(symbol); - const { address_pool: poolAddress, address_withdraw_treasury: withdrawTreasuryAddress } = - selectNetwork(ethNetwork); + const { addressContractPool, addressContractWithdrawTreasury } = getEthNetworkAddresses(symbol); - if (from === poolAddress && to === withdrawTreasuryAddress) { + if (from === addressContractPool && to === addressContractWithdrawTreasury) { return 'stake'; } - if (from === poolAddress && to === address) { + if (from === addressContractPool && to === address) { return 'unstake'; } - if (from === withdrawTreasuryAddress && to === address) { + if (from === addressContractWithdrawTreasury && to === address) { return 'claim'; } @@ -645,13 +651,14 @@ export const simulateUnstake = async ({ symbol, }: StakeTxBaseArgs & { amount: string }) => { const ethNetwork = getEthNetworkForWalletSdk(symbol); - const { address_pool: poolAddress, contract_pool: contractPool } = selectNetwork(ethNetwork); + const ethereumClient = new Ethereum(ethNetwork); + const { addressContractPool } = getEthNetworkAddresses(symbol); if (!amount || !from || !symbol) return null; const amountWei = toWei(amount, 'ether'); - const data = contractPool.methods + const data = ethereumClient.contractPool.methods .unstake(amountWei, UNSTAKE_INTERCHANGES, WALLET_SDK_SOURCE) .encodeABI(); if (!data) return null; @@ -659,7 +666,7 @@ export const simulateUnstake = async ({ const ethereumData = await TrezorConnect.blockchainEvmRpcCall({ coin: symbol, from, - to: poolAddress, + to: addressContractPool, data, }); diff --git a/packages/suite/src/utils/suite/solanaStaking.ts b/packages/suite/src/utils/suite/solanaStaking.ts new file mode 100644 index 00000000000..6d543c5cb8d --- /dev/null +++ b/packages/suite/src/utils/suite/solanaStaking.ts @@ -0,0 +1,85 @@ +import { VersionedTransaction, PublicKey } from '@solana/web3.js-version1'; + +import { NetworkSymbol } from '@suite-common/wallet-config'; +import { LAMPORTS_PER_SOL, WALLET_SDK_SOURCE } from '@suite-common/wallet-constants'; +import { selectSolanaWalletSdkNetwork } from '@suite-common/wallet-utils'; +import { BigNumber } from '@trezor/utils'; +import type { SolanaSignTransaction } from '@trezor/connect'; +import { Blockchain } from '@suite-common/wallet-types'; + +type SolanaTx = SolanaSignTransaction & { + versionedTx: VersionedTransaction; +}; + +export const transformTx = ( + tx: VersionedTransaction, + path: string | number[], + tokenAccountsInfos?: { + baseAddress: string; + tokenProgram: string; + tokenMint: string; + tokenAccount: string; + }[], +): SolanaTx => { + const serializedMessage = new Uint8Array(tx.message.serialize()); + const serializedTxHex = Buffer.from(serializedMessage).toString('hex'); + + const transformedTx = { + path, + serializedTx: serializedTxHex, + additionalInfo: tokenAccountsInfos ? { tokenAccountsInfos } : undefined, + versionedTx: tx, + }; + + return transformedTx; +}; + +export const getPubKeyFromAddress = (address: string) => { + + return new PublicKey(address); +}; + +interface PrepareStakeSolTxParams { + from: string; + path: string | number[]; + amount: string; + symbol: NetworkSymbol; + selectedBlockchain: Blockchain +} +export type PrepareStakeSolTxResponse = + | { + success: true; + tx: SolanaTx; + } + | { + success: false; + errorMessage: string; + }; + +export const prepareStakeSolTx = async ({ + from, + path, + amount, + symbol, + selectedBlockchain, +}: PrepareStakeSolTxParams): Promise => { + try { + const solanaClient = selectSolanaWalletSdkNetwork(symbol, selectedBlockchain.url); + + const lamports = new BigNumber(LAMPORTS_PER_SOL).multipliedBy(amount).toNumber(); // stake method expects lamports as a number + const tx = await solanaClient.stake(from, lamports, WALLET_SDK_SOURCE); + const transformedTx = transformTx(tx.result, path); + + return { + success: true, + tx: transformedTx, + }; + } catch (e) { + console.error(e); + + return { + success: false, + errorMessage: e.message, + }; + } +}; diff --git a/packages/suite/src/utils/suite/transactionReview.ts b/packages/suite/src/utils/suite/transactionReview.ts index ffe85a9e729..8aa29ac9e56 100644 --- a/packages/suite/src/utils/suite/transactionReview.ts +++ b/packages/suite/src/utils/suite/transactionReview.ts @@ -2,17 +2,17 @@ import { TranslationKey } from '@suite-common/intl-types'; import { StakeFormState } from '@suite-common/wallet-types'; interface getTransactionReviewModalActionTextParams { - ethereumStakeType: StakeFormState['ethereumStakeType'] | null; + stakeType: StakeFormState['stakeType'] | null; isRbfAction: boolean; isSending?: boolean; } export const getTransactionReviewModalActionText = ({ - ethereumStakeType, + stakeType, isRbfAction, isSending, }: getTransactionReviewModalActionTextParams): TranslationKey => { - switch (ethereumStakeType) { + switch (stakeType) { case 'stake': return 'TR_STAKE_STAKE'; case 'unstake': diff --git a/packages/suite/src/views/dashboard/AssetsView/assetsViewUtils.ts b/packages/suite/src/views/dashboard/AssetsView/assetsViewUtils.ts index 3dae8884a23..34f95e26467 100644 --- a/packages/suite/src/views/dashboard/AssetsView/assetsViewUtils.ts +++ b/packages/suite/src/views/dashboard/AssetsView/assetsViewUtils.ts @@ -21,7 +21,7 @@ export const handleTokensAndStakingData = ( currentFiatRates?: RatesByKey, ) => { const assetStakingBalance = accountsThatStaked.reduce((total, account) => { - return total.plus(getAccountTotalStakingBalance(account)); + return total.plus(getAccountTotalStakingBalance(account) ?? '0'); }, new BigNumber(0)); const tokens = getTokens(assetTokens ?? [], symbol, coinDefinitions); const tokensWithRates = enhanceTokensWithRates( diff --git a/packages/suite/src/views/dashboard/StakeEthCard/StakeEthCard.tsx b/packages/suite/src/views/dashboard/StakeEthCard/StakeEthCard.tsx index d390fca2e26..c5d5947e6b7 100644 --- a/packages/suite/src/views/dashboard/StakeEthCard/StakeEthCard.tsx +++ b/packages/suite/src/views/dashboard/StakeEthCard/StakeEthCard.tsx @@ -70,9 +70,10 @@ export const StakeEthCard = () => { title: , description: ( ( { return ( <> - }> + + } + > diff --git a/packages/suite/src/views/wallet/staking/WalletStaking.tsx b/packages/suite/src/views/wallet/staking/WalletStaking.tsx index 9b9990e25de..95355c335a0 100644 --- a/packages/suite/src/views/wallet/staking/WalletStaking.tsx +++ b/packages/suite/src/views/wallet/staking/WalletStaking.tsx @@ -5,7 +5,8 @@ import { AccountExceptionLayout, WalletLayout } from 'src/components/wallet'; import { useSelector } from 'src/hooks/suite'; import { CardanoStakingDashboard } from './components/CardanoStakingDashboard'; -import { EthStakingDashboard } from './components/EthStakingDashboard/EthStakingDashboard'; +import { EthStakingDashboard } from './components/EthStakingDashboard/components/EthStakingDashboard'; +import { SolStakingDashboard } from './components/SolStakingDashboard/SolStakingDashboard'; export const WalletStaking = () => { const { selectedAccount } = useSelector(state => state.wallet); @@ -26,6 +27,8 @@ export const WalletStaking = () => { return ; case 'ethereum': return ; + case 'solana': + return ; // no default } } diff --git a/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/EthStakingDashboard.tsx b/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/EthStakingDashboard.tsx deleted file mode 100644 index b1dd8fa6acb..00000000000 --- a/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/EthStakingDashboard.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { selectAccountHasStaked } from '@suite-common/wallet-core'; -import { SelectedAccountLoaded } from '@suite-common/wallet-types'; - -import { WalletLayout } from 'src/components/wallet'; -import { useSelector } from 'src/hooks/suite'; - -import { EmptyStakingCard } from './components/EmptyStakingCard'; -import { StakingDashboard } from './components/StakingDashboard'; -import { EverstakeFooter } from './components/EverstakeFooter'; - -interface EthStakingDashboardProps { - selectedAccount: SelectedAccountLoaded; -} - -export const EthStakingDashboard = ({ selectedAccount }: EthStakingDashboardProps) => { - const hasStaked = useSelector(state => - selectAccountHasStaked(state, selectedAccount.account.key), - ); - - return ( - - {hasStaked ? : } - - - - ); -}; diff --git a/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/StakingDashboard.tsx b/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/EthStakingDashboard.tsx similarity index 52% rename from packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/StakingDashboard.tsx rename to packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/EthStakingDashboard.tsx index b7ba6c2cc47..19368208792 100644 --- a/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/StakingDashboard.tsx +++ b/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/EthStakingDashboard.tsx @@ -11,12 +11,12 @@ import { selectValidatorsQueue, } from '@suite-common/wallet-core'; import { getAccountEverstakeStakingPool } from '@suite-common/wallet-utils'; +import { SelectedAccountStatus } from '@suite-common/wallet-types'; -import { selectSelectedAccount } from 'src/reducers/wallet/selectedAccountReducer'; import { useDispatch, useSelector } from 'src/hooks/suite'; import { Translation } from 'src/components/suite'; import { DashboardSection } from 'src/components/dashboard'; -import { getDaysToAddToPool, getDaysToUnstake } from 'src/utils/suite/stake'; +import { getDaysToAddToPool, getDaysToUnstake } from 'src/utils/suite/ethereumStaking'; import { StakingCard } from './StakingCard'; import { ApyCard } from './ApyCard'; @@ -24,9 +24,14 @@ import { PayoutCard } from './PayoutCard'; import { ClaimCard } from './ClaimCard'; import { Transactions } from './Transactions'; import { InstantStakeBanner } from './InstantStakeBanner'; +import { StakingDashboard } from '../../StakingDashboard/StakingDashboard'; -export const StakingDashboard = () => { - const account = useSelector(selectSelectedAccount); +interface EthStakingDashboardProps { + selectedAccount: SelectedAccountStatus; +} + +export const EthStakingDashboard = ({ selectedAccount }: EthStakingDashboardProps) => { + const { account } = selectedAccount; const accountKey = account?.key ?? ''; const isBelowLaptop = useMediaQuery(`(max-width: ${variables.SCREEN_SIZE.LG})`); @@ -62,34 +67,46 @@ export const StakingDashboard = () => { const { canClaim = false } = getAccountEverstakeStakingPool(account) ?? {}; return ( - - }> - - - - - - - + + } + > + + - - - - - + + + + + + + + + + - - + + + } + /> ); }; diff --git a/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/InstantStakeBanner.tsx b/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/InstantStakeBanner.tsx index 885d6df5704..5c2752f5858 100644 --- a/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/InstantStakeBanner.tsx +++ b/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/InstantStakeBanner.tsx @@ -9,7 +9,7 @@ import { InternalTransfer } from '@trezor/connect'; import { useSelector } from 'src/hooks/suite'; import { Translation } from 'src/components/suite'; -import { getChangedInternalTx, getInstantStakeType } from 'src/utils/suite/stake'; +import { getChangedInternalTx, getInstantStakeType } from 'src/utils/suite/ethereumStaking'; import { selectSelectedAccount } from 'src/reducers/wallet/selectedAccountReducer'; const getSubheadingTranslationId = (stakeType: StakeType) => { diff --git a/packages/suite/src/views/wallet/staking/components/SolStakingDashboard/SolStakingDashboard.tsx b/packages/suite/src/views/wallet/staking/components/SolStakingDashboard/SolStakingDashboard.tsx new file mode 100644 index 00000000000..98db57dae5b --- /dev/null +++ b/packages/suite/src/views/wallet/staking/components/SolStakingDashboard/SolStakingDashboard.tsx @@ -0,0 +1,17 @@ +import { SelectedAccountStatus } from '@suite-common/wallet-types'; + +import { StakingDashboard } from '../StakingDashboard/StakingDashboard'; + +interface SolStakingDashboardProps { + selectedAccount: SelectedAccountStatus; +} + +export const SolStakingDashboard = ({ selectedAccount }: SolStakingDashboardProps) => { + return ( + } + /> + ); +}; diff --git a/packages/suite/src/views/wallet/staking/components/StakingDashboard/StakingDashboard.tsx b/packages/suite/src/views/wallet/staking/components/StakingDashboard/StakingDashboard.tsx new file mode 100644 index 00000000000..832cf448dd1 --- /dev/null +++ b/packages/suite/src/views/wallet/staking/components/StakingDashboard/StakingDashboard.tsx @@ -0,0 +1,33 @@ +import { selectAccountHasStaked } from '@suite-common/wallet-core'; +import { SelectedAccountStatus } from '@suite-common/wallet-types'; + +import { WalletLayout } from 'src/components/wallet'; +import { useSelector } from 'src/hooks/suite'; + +import { EmptyStakingCard } from './components/EmptyStakingCard'; +import { EverstakeFooter } from './components/EverstakeFooter'; + +interface StakingDashboardProps { + selectedAccount: SelectedAccountStatus; + dashboard: React.ReactElement; +} + +export const StakingDashboard = ({ selectedAccount, dashboard }: StakingDashboardProps) => { + const hasStaked = useSelector(state => + selectAccountHasStaked(state, selectedAccount?.account?.key ?? ''), + ); + + if (!selectedAccount) return null; + + return ( + + {hasStaked ? dashboard : } + + + + ); +}; diff --git a/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/EmptyStakingCard.tsx b/packages/suite/src/views/wallet/staking/components/StakingDashboard/components/EmptyStakingCard.tsx similarity index 89% rename from packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/EmptyStakingCard.tsx rename to packages/suite/src/views/wallet/staking/components/StakingDashboard/components/EmptyStakingCard.tsx index 7a712d7cc2d..7b74c5b9f2c 100644 --- a/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/EmptyStakingCard.tsx +++ b/packages/suite/src/views/wallet/staking/components/StakingDashboard/components/EmptyStakingCard.tsx @@ -26,9 +26,11 @@ import { useMessageSystemStaking } from 'src/hooks/suite/useMessageSystemStaking export const EmptyStakingCard = () => { const isBelowLaptop = useMediaQuery(`(max-width: ${variables.SCREEN_SIZE.LG})`); const account = useSelector(selectSelectedAccount); + const { isStakingDisabled, stakingMessageContent } = useMessageSystemStaking(); const ethApy = useSelector(state => selectPoolStatsApyData(state, account?.symbol)); + // TODO: calc solApy const dispatch = useDispatch(); const openStakingEthInANutshellModal = () => { @@ -45,9 +47,10 @@ export const EmptyStakingCard = () => { title: , description: ( ( { description: , }, ], - [ethApy], + [ethApy, account?.symbol], ); return ( - }> + + } + >
@@ -88,7 +98,7 @@ export const EmptyStakingCard = () => {
diff --git a/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/EverstakeFooter.tsx b/packages/suite/src/views/wallet/staking/components/StakingDashboard/components/EverstakeFooter.tsx similarity index 100% rename from packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/EverstakeFooter.tsx rename to packages/suite/src/views/wallet/staking/components/StakingDashboard/components/EverstakeFooter.tsx diff --git a/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/EverstakeLogo.tsx b/packages/suite/src/views/wallet/staking/components/StakingDashboard/components/EverstakeLogo.tsx similarity index 100% rename from packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/EverstakeLogo.tsx rename to packages/suite/src/views/wallet/staking/components/StakingDashboard/components/EverstakeLogo.tsx diff --git a/suite-common/wallet-config/src/networksConfig.ts b/suite-common/wallet-config/src/networksConfig.ts index e232797a873..f278be7cddd 100644 --- a/suite-common/wallet-config/src/networksConfig.ts +++ b/suite-common/wallet-config/src/networksConfig.ts @@ -198,7 +198,7 @@ export const networks = { bip43Path: "m/44'/501'/i'/0'", // phantom - bip44Change decimals: 9, testnet: false, - features: ['tokens', 'coin-definitions' /*, 'staking' */], + features: ['tokens', 'coin-definitions', 'staking'], explorer: { tx: 'https://solscan.io/tx/', account: 'https://solscan.io/account/', @@ -616,7 +616,7 @@ export const networks = { bip43Path: "m/44'/501'/i'/0'", decimals: 9, testnet: true, - features: ['tokens' /* , 'staking' */], + features: ['tokens', 'staking'], explorer: { tx: 'https://solscan.io/tx/', account: 'https://solscan.io/account/', diff --git a/suite-common/wallet-constants/src/ethereumStakingConstants.ts b/suite-common/wallet-constants/src/ethereumStakingConstants.ts index a75b5464815..4dfd43481ea 100644 --- a/suite-common/wallet-constants/src/ethereumStakingConstants.ts +++ b/suite-common/wallet-constants/src/ethereumStakingConstants.ts @@ -16,3 +16,6 @@ export const MIN_ETH_BALANCE_FOR_STAKING = MIN_ETH_AMOUNT_FOR_STAKING.plus(MIN_E export const UNSTAKE_INTERCHANGES = 5; export const BACKUP_REWARD_PAYOUT_DAYS = 7; + +// Used when Everstake unstaking period is not available from the API. +export const UNSTAKING_ETH_PERIOD = 3; diff --git a/suite-common/wallet-constants/src/index.ts b/suite-common/wallet-constants/src/index.ts index 0eea39efe42..ae12d0e9465 100644 --- a/suite-common/wallet-constants/src/index.ts +++ b/suite-common/wallet-constants/src/index.ts @@ -2,3 +2,5 @@ export * from './formDraft'; export * from './sendForm'; export * from './discovery'; export * from './ethereumStakingConstants'; +export * from './solanaStakingConstants'; +export * from './stakingConstants'; diff --git a/suite-common/wallet-constants/src/solanaStakingConstants.ts b/suite-common/wallet-constants/src/solanaStakingConstants.ts new file mode 100644 index 00000000000..02b4d4f12ac --- /dev/null +++ b/suite-common/wallet-constants/src/solanaStakingConstants.ts @@ -0,0 +1,9 @@ +import { BigNumber } from '@trezor/utils'; + +// TODO: change to solana constants +export const MIN_SOL_AMOUNT_FOR_STAKING = new BigNumber(0.1); +export const MAX_SOL_AMOUNT_FOR_STAKING = new BigNumber(1_000_000); +export const MIN_SOL_FOR_WITHDRAWALS = new BigNumber(0.03); +export const MIN_SOL_BALANCE_FOR_STAKING = MIN_SOL_AMOUNT_FOR_STAKING.plus(MIN_SOL_FOR_WITHDRAWALS); + +export const LAMPORTS_PER_SOL = 1_000_000_000; diff --git a/suite-common/wallet-constants/src/stakingConstants.ts b/suite-common/wallet-constants/src/stakingConstants.ts new file mode 100644 index 00000000000..f9edbf858a2 --- /dev/null +++ b/suite-common/wallet-constants/src/stakingConstants.ts @@ -0,0 +1,5 @@ +// source is a required parameter for some functions in the Everstake Wallet SDK. +// This parameter is used for some contract calls. +// It is a constant which allows the SDK to define which app calls its functions. +// Each app which integrates the SDK has its own source, e.g. source for Trezor Suite is '1'. +export const WALLET_SDK_SOURCE = '1'; diff --git a/suite-common/wallet-core/src/accounts/accountsReducer.ts b/suite-common/wallet-core/src/accounts/accountsReducer.ts index 97c61d827b7..07b67031bad 100644 --- a/suite-common/wallet-core/src/accounts/accountsReducer.ts +++ b/suite-common/wallet-core/src/accounts/accountsReducer.ts @@ -475,3 +475,9 @@ export const selectIsDeviceNotEmpty = createMemoizedSelector( return isNotEmpty; }, ); + +export const selectStakingAccounts = createMemoizedSelector([selectAccountByKey], account => { + if (!account || account.networkType !== 'solana') return null; + + return account.misc.solStakingAccounts ?? []; +}); diff --git a/suite-common/wallet-core/src/accounts/accountsThunks.ts b/suite-common/wallet-core/src/accounts/accountsThunks.ts index bc9f9a29546..f194d7ac9e5 100644 --- a/suite-common/wallet-core/src/accounts/accountsThunks.ts +++ b/suite-common/wallet-core/src/accounts/accountsThunks.ts @@ -109,6 +109,7 @@ export const fetchAndUpdateAccountThunk = createThunk( const accountOutdated = isAccountOutdated(account, basic.payload); const accountTransactions = selectTransactions(getState()); const accountTxs = getAccountTransactions(account.key, accountTransactions); + // stop here if account is not outdated and there are no pending transactions if (!accountOutdated && !accountTxs.find(isPending)) { diff --git a/suite-common/wallet-types/src/account.ts b/suite-common/wallet-types/src/account.ts index 89583e9e8de..03f7989f742 100644 --- a/suite-common/wallet-types/src/account.ts +++ b/suite-common/wallet-types/src/account.ts @@ -6,6 +6,7 @@ import { ContractInfo, StakingPool, } from '@trezor/blockchain-link-types/src/blockbook-api'; +import { SolanaStakingAccount } from '@trezor/blockchain-link-types/src/solana'; export type MetadataItem = string; export type XpubAddress = string; @@ -61,6 +62,7 @@ type AccountNetworkSpecific = networkType: 'solana'; misc: { rent?: number; + solStakingAccounts?: SolanaStakingAccount[]; }; marker: undefined; page: AccountInfo['page']; diff --git a/suite-common/wallet-types/src/index.ts b/suite-common/wallet-types/src/index.ts index a9ba9c73350..256f4f0fef3 100644 --- a/suite-common/wallet-types/src/index.ts +++ b/suite-common/wallet-types/src/index.ts @@ -11,3 +11,4 @@ export * from './transaction'; export * from './ethereumStaking'; export * from './stakeForm'; export * from './send'; +export * from './solanaStaking'; diff --git a/suite-common/wallet-types/src/solanaStaking.ts b/suite-common/wallet-types/src/solanaStaking.ts new file mode 100644 index 00000000000..72bdf56ee69 --- /dev/null +++ b/suite-common/wallet-types/src/solanaStaking.ts @@ -0,0 +1,3 @@ +export const supportedSolanaNetworkSymbols = ['sol', 'dsol'] as const; + +export type SupportedSolanaNetworkSymbols = (typeof supportedSolanaNetworkSymbols)[number]; diff --git a/suite-common/wallet-types/src/stakeForm.ts b/suite-common/wallet-types/src/stakeForm.ts index b261927ba66..2efa22354f9 100644 --- a/suite-common/wallet-types/src/stakeForm.ts +++ b/suite-common/wallet-types/src/stakeForm.ts @@ -4,5 +4,5 @@ import { FormState } from './sendForm'; export interface StakeFormState extends FormState { fiatInput?: string; cryptoInput?: string; - ethereumStakeType: StakeType; + stakeType: StakeType; } diff --git a/suite-common/wallet-utils/package.json b/suite-common/wallet-utils/package.json index cf8880bf642..5e975b4158f 100644 --- a/suite-common/wallet-utils/package.json +++ b/suite-common/wallet-utils/package.json @@ -12,6 +12,7 @@ "test-unit:watch": "yarn g:jest -c ../../jest.config.base.js -o --watch" }, "dependencies": { + "@everstake/wallet-sdk": "^1.0.5", "@mobily/ts-belt": "^3.13.1", "@solana-program/compute-budget": "^0.6.1", "@solana-program/system": "^0.6.2", diff --git a/suite-common/wallet-utils/src/__fixtures__/ethereumStakingUtils.ts b/suite-common/wallet-utils/src/__fixtures__/ethereumStakingUtils.ts index 227e2304a87..2622a70e756 100644 --- a/suite-common/wallet-utils/src/__fixtures__/ethereumStakingUtils.ts +++ b/suite-common/wallet-utils/src/__fixtures__/ethereumStakingUtils.ts @@ -144,7 +144,7 @@ export const getAccountAutocompoundBalanceFixtures = [ }, ]; -export const getAccountTotalStakingBalanceFixtures = [ +export const getEthAccountTotalStakingBalanceFixtures = [ { description: 'Ethereum account with valid Everstake pool', account: { diff --git a/suite-common/wallet-utils/src/__tests__/ethereumStakingUtils.test.ts b/suite-common/wallet-utils/src/__tests__/ethereumStakingUtils.test.ts index 508a0697750..8a6d464da27 100644 --- a/suite-common/wallet-utils/src/__tests__/ethereumStakingUtils.test.ts +++ b/suite-common/wallet-utils/src/__tests__/ethereumStakingUtils.test.ts @@ -2,14 +2,14 @@ import { Account } from '@suite-common/wallet-types'; import { getAccountAutocompoundBalance, - getAccountTotalStakingBalance, + getEthAccountTotalStakingBalance, getAccountEverstakeStakingPool, getUnstakeAmountByEthereumDataHex, } from '../ethereumStakingUtils'; import { getAccountAutocompoundBalanceFixtures, getAccountEverstakeStakingPoolFixtures, - getAccountTotalStakingBalanceFixtures, + getEthAccountTotalStakingBalanceFixtures, getUnstakeAmountByEthereumDataHexFixtures, } from '../__fixtures__/ethereumStakingUtils'; @@ -31,13 +31,15 @@ describe('getAccountAutocompoundBalance', () => { }); }); -describe('getAccountTotalStakingBalance', () => { - getAccountTotalStakingBalanceFixtures.forEach(({ description, account, expectedBalance }) => { - it(description, () => { - const result = getAccountTotalStakingBalance(account as unknown as Account); - expect(result).toEqual(expectedBalance); - }); - }); +describe('getEthAccountTotalStakingBalance', () => { + getEthAccountTotalStakingBalanceFixtures.forEach( + ({ description, account, expectedBalance }) => { + it(description, () => { + const result = getEthAccountTotalStakingBalance(account as unknown as Account); + expect(result).toEqual(expectedBalance); + }); + }, + ); }); describe('getUnstakeAmountByEthereumDataHex', () => { diff --git a/suite-common/wallet-utils/src/accountUtils.ts b/suite-common/wallet-utils/src/accountUtils.ts index cec5a92689e..40224a6c832 100644 --- a/suite-common/wallet-utils/src/accountUtils.ts +++ b/suite-common/wallet-utils/src/accountUtils.ts @@ -42,8 +42,8 @@ import { formatTokenSymbol } from '@trezor/blockchain-link-utils'; import { toFiatCurrency } from './fiatConverterUtils'; import { getFiatRateKey } from './fiatRatesUtils'; -import { getAccountTotalStakingBalance } from './ethereumStakingUtils'; import { isRbfTransaction } from './transactionUtils'; +import { getAccountTotalStakingBalance } from './stakingUtils'; export const isUtxoBased = (account: Account) => account.networkType === 'bitcoin' || account.networkType === 'cardano'; @@ -693,9 +693,9 @@ export const getAssetTokensFiatBalance = ( }; export const getStakingFiatBalance = (account: Account, rate: number | undefined) => { - const balanceInEther = getAccountTotalStakingBalance(account); + const balance = getAccountTotalStakingBalance(account) ?? '0'; - return toFiatCurrency(balanceInEther, rate, 2); + return toFiatCurrency(balance, rate, 2); }; export const getAccountFiatBalance = ({ @@ -866,7 +866,7 @@ export const getAccountSpecific = (accountInfo: Partial, networkTyp if (networkType === 'solana') { return { networkType, - misc: { rent: misc?.rent }, + misc: { rent: misc?.rent, solStakingAccounts: misc?.solStakingAccounts }, marker: undefined, page: accountInfo.page, }; diff --git a/suite-common/wallet-utils/src/ethereumStakingUtils.ts b/suite-common/wallet-utils/src/ethereumStakingUtils.ts index 42b4ff2b4cc..d7efe7bcb89 100644 --- a/suite-common/wallet-utils/src/ethereumStakingUtils.ts +++ b/suite-common/wallet-utils/src/ethereumStakingUtils.ts @@ -51,7 +51,7 @@ export const getAccountAutocompoundBalance = (account?: Account) => { return pool?.autocompoundBalance ?? '0'; }; -export const getAccountTotalStakingBalance = (account?: Account) => { +export const getEthAccountTotalStakingBalance = (account?: Account) => { const pool = getAccountEverstakeStakingPool(account); return new BigNumber(pool?.autocompoundBalance ?? '0') @@ -62,7 +62,7 @@ export const getAccountTotalStakingBalance = (account?: Account) => { }; export const getEthereumCryptoBalanceWithStaking = (account: Account) => { - const stakingBalance = getAccountTotalStakingBalance(account); + const stakingBalance = getEthAccountTotalStakingBalance(account); return new BigNumber(account.formattedBalance).plus(stakingBalance).toString(); }; diff --git a/suite-common/wallet-utils/src/index.ts b/suite-common/wallet-utils/src/index.ts index 3222d029aeb..b211ee9a62f 100644 --- a/suite-common/wallet-utils/src/index.ts +++ b/suite-common/wallet-utils/src/index.ts @@ -20,3 +20,5 @@ export * from './ethereumStakingUtils'; export * from './reviewTransactionUtils'; export * from './filterReceiveAccounts'; export * from './tokenUtils'; +export * from './solanaStakingUtils'; +export * from './stakingUtils'; diff --git a/suite-common/wallet-utils/src/solanaStakingUtils.ts b/suite-common/wallet-utils/src/solanaStakingUtils.ts new file mode 100644 index 00000000000..111e4637813 --- /dev/null +++ b/suite-common/wallet-utils/src/solanaStakingUtils.ts @@ -0,0 +1,71 @@ +import { Solana, SolNetwork } from '@everstake/wallet-sdk'; + +import { NetworkSymbol } from '@suite-common/wallet-config'; +import { BigNumber, isArrayMember } from '@trezor/utils'; +import { SolanaStakingAccount } from '@trezor/blockchain-link-types/src/solana'; +import { + Account, + supportedSolanaNetworkSymbols, + SupportedSolanaNetworkSymbols, +} from '@suite-common/wallet-types'; +import { LAMPORTS_PER_SOL } from '@suite-common/wallet-constants'; + +export function isSupportedSolStakingNetworkSymbol( + networkSymbol: NetworkSymbol, +): networkSymbol is SupportedSolanaNetworkSymbols { + return isArrayMember(networkSymbol, supportedSolanaNetworkSymbols); +} + +export const formatSolanaStakingAmount = (amount: string | null) => { + if (!amount) return null; + + return new BigNumber(amount).div(LAMPORTS_PER_SOL).toFixed(9); +}; + +interface SolNetworkConfig { + network: SolNetwork; +} + +export const getSolNetworkForWalletSdk = (symbol: NetworkSymbol): SolNetworkConfig => { + const solNetworks: { [key in NetworkSymbol]?: SolNetworkConfig } = { + dsol: { network: SolNetwork.Devnet }, + sol: { network: SolNetwork.Mainnet }, + }; + + return solNetworks[symbol] || solNetworks.sol!; +}; + +export const selectSolanaWalletSdkNetwork = (symbol: NetworkSymbol, url?: string) => { + const { network } = getSolNetworkForWalletSdk(symbol); + + return new Solana(network, url); +}; + +export const calculateTotalSolStakingBalance = (stakingAccounts: SolanaStakingAccount[]) => { + if (!stakingAccounts?.length) return null; + + const totalAmount = stakingAccounts.reduce((acc, solAccount) => { + const { account } = solAccount; + + if ('parsed' in account.data) { + return acc.plus(account.data.parsed.info.stake.delegation.stake); + } + + return acc; + }, new BigNumber(0)); + + return totalAmount.toString(); +}; + +export const getSolAccountTotalStakingBalance = (account: Account) => { + if (!account || account.networkType !== 'solana') { + return null; + } + + const { solStakingAccounts } = account.misc; + if (!solStakingAccounts) return null; + + const totalStakingBalance = calculateTotalSolStakingBalance(solStakingAccounts); + + return formatSolanaStakingAmount(totalStakingBalance); +}; diff --git a/suite-common/wallet-utils/src/stakingUtils.ts b/suite-common/wallet-utils/src/stakingUtils.ts new file mode 100644 index 00000000000..74bcdaf427a --- /dev/null +++ b/suite-common/wallet-utils/src/stakingUtils.ts @@ -0,0 +1,27 @@ +import { Account } from '@suite-common/wallet-types'; +import { NetworkSymbol } from '@suite-common/wallet-config'; + +import { + getEthAccountTotalStakingBalance, + isSupportedEthStakingNetworkSymbol, +} from './ethereumStakingUtils'; +import { + getSolAccountTotalStakingBalance, + isSupportedSolStakingNetworkSymbol, +} from './solanaStakingUtils'; + +export const getAccountTotalStakingBalance = (account: Account) => { + if (!account) return null; + + if (account.networkType === 'ethereum') { + return getEthAccountTotalStakingBalance(account); + } + + if (account.networkType === 'solana') { + return getSolAccountTotalStakingBalance(account); + } +}; + +export const isSupportedStakingNetworkSymbol = (symbol: NetworkSymbol) => { + return isSupportedEthStakingNetworkSymbol(symbol) || isSupportedSolStakingNetworkSymbol(symbol); +}; diff --git a/suite-native/accounts/src/selectors.ts b/suite-native/accounts/src/selectors.ts index f2f4aa9f2c9..3bf58f6a709 100644 --- a/suite-native/accounts/src/selectors.ts +++ b/suite-native/accounts/src/selectors.ts @@ -109,7 +109,7 @@ export const getAccountListSections = ( const canHasTokens = isCoinWithTokens(account.symbol); const tokens = filterKnownTokens(tokenDefinitions, account.symbol, account.tokens ?? []); const hasAnyKnownTokens = canHasTokens && !!tokens.length; - const stakingBalance = getAccountTotalStakingBalance(account); + const stakingBalance = getAccountTotalStakingBalance(account) ?? '0'; const hasStaking = stakingBalance !== '0'; if (canHasTokens) { diff --git a/suite-native/test-utils/src/walletSdkMock.js b/suite-native/test-utils/src/walletSdkMock.js deleted file mode 100644 index 7d5094a446a..00000000000 --- a/suite-native/test-utils/src/walletSdkMock.js +++ /dev/null @@ -1,8 +0,0 @@ -jest.mock('@everstake/wallet-sdk/ethereum', () => ({ - selectNetwork: jest.fn().mockImplementation(network => { - return { - address_pool: `mocked_pool_${network}`, - address_withdraw_treasury: `mocked_treasury_${network}`, - }; - }), -})); diff --git a/yarn.lock b/yarn.lock index 4f14bc3b2e6..269d157867f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -55,16 +55,6 @@ __metadata: languageName: node linkType: hard -"@aptos-labs/aptos-client@npm:^0.1.0": - version: 0.1.0 - resolution: "@aptos-labs/aptos-client@npm:0.1.0" - dependencies: - axios: "npm:1.6.2" - got: "npm:^11.8.6" - checksum: 10/8cb6330a25f5579acbc6fdca1f293d887ba390f4730248adcc38aa869ede10c9bf29e75aca0d3ab0d74fe82c01520376e91ca899292aff5aa30ccba588dad53d - languageName: node - linkType: hard - "@asamuzakjp/dom-selector@npm:^2.0.1": version: 2.0.2 resolution: "@asamuzakjp/dom-selector@npm:2.0.2" @@ -2048,154 +2038,6 @@ __metadata: languageName: node linkType: hard -"@confio/ics23@npm:^0.6.8": - version: 0.6.8 - resolution: "@confio/ics23@npm:0.6.8" - dependencies: - "@noble/hashes": "npm:^1.0.0" - protobufjs: "npm:^6.8.8" - checksum: 10/5bffe3b9549eafe627aabdad8951c989cecb9d841f9e646ef3c916ae4cd2c582076f937684ff1b686dc67ec7176c82faf15c221dc9c86990cfbfe13c2add9473 - languageName: node - linkType: hard - -"@cosmjs/amino@npm:^0.30.1": - version: 0.30.1 - resolution: "@cosmjs/amino@npm:0.30.1" - dependencies: - "@cosmjs/crypto": "npm:^0.30.1" - "@cosmjs/encoding": "npm:^0.30.1" - "@cosmjs/math": "npm:^0.30.1" - "@cosmjs/utils": "npm:^0.30.1" - checksum: 10/c80859749aae9d6e971380e50e8a3438d2889caeaa56433d448985710bcbb4da62937a5ce94ceb65afc5d6fc92b7daf3596e7bd9d31c3d76d08dea4beedf191b - languageName: node - linkType: hard - -"@cosmjs/crypto@npm:^0.30.1": - version: 0.30.1 - resolution: "@cosmjs/crypto@npm:0.30.1" - dependencies: - "@cosmjs/encoding": "npm:^0.30.1" - "@cosmjs/math": "npm:^0.30.1" - "@cosmjs/utils": "npm:^0.30.1" - "@noble/hashes": "npm:^1" - bn.js: "npm:^5.2.0" - elliptic: "npm:^6.5.4" - libsodium-wrappers: "npm:^0.7.6" - checksum: 10/1f52d4b0bd3efa62ffdd4c37c81b52c615f9fd463734f46935de0baa47eac3ba24f30180dec72a18e7596bf190a9e9cd3b781984eb697cbe7dc7e77c591f4e2c - languageName: node - linkType: hard - -"@cosmjs/encoding@npm:^0.30.1": - version: 0.30.1 - resolution: "@cosmjs/encoding@npm:0.30.1" - dependencies: - base64-js: "npm:^1.3.0" - bech32: "npm:^1.1.4" - readonly-date: "npm:^1.0.0" - checksum: 10/620b3bd575f370d6c515413bab5d38a02506c2f16cc0a51e64448cafe438b31592873eac8aefe549ef71f9756ac24c6450e43a466e1e9067638f0fee625638c3 - languageName: node - linkType: hard - -"@cosmjs/json-rpc@npm:^0.30.1": - version: 0.30.1 - resolution: "@cosmjs/json-rpc@npm:0.30.1" - dependencies: - "@cosmjs/stream": "npm:^0.30.1" - xstream: "npm:^11.14.0" - checksum: 10/9dd3a915f6cc1beff14fee60fe5fe10776f768efa3344d334cd2f332a14c951613ba0da38571e8a3360bc77283d6653b9ced198ae7187cb52bc2769e57754fbd - languageName: node - linkType: hard - -"@cosmjs/math@npm:^0.30.1": - version: 0.30.1 - resolution: "@cosmjs/math@npm:0.30.1" - dependencies: - bn.js: "npm:^5.2.0" - checksum: 10/bf273c61936d4102a4083c29d6ac445287443b7e62ce700754eb376c0ece11edaa1370cb82288352b497a6779613c85f69fb79961ee46400f2fe0665d18afdda - languageName: node - linkType: hard - -"@cosmjs/proto-signing@npm:^0.30.1": - version: 0.30.1 - resolution: "@cosmjs/proto-signing@npm:0.30.1" - dependencies: - "@cosmjs/amino": "npm:^0.30.1" - "@cosmjs/crypto": "npm:^0.30.1" - "@cosmjs/encoding": "npm:^0.30.1" - "@cosmjs/math": "npm:^0.30.1" - "@cosmjs/utils": "npm:^0.30.1" - cosmjs-types: "npm:^0.7.1" - long: "npm:^4.0.0" - checksum: 10/214871f1c9f2997ec58af278b790cd533a761420ff569bed436b8f43856249d0698645082818470ddcd09f33417d7a945f58ab52d40b2d21656648f57ede11c8 - languageName: node - linkType: hard - -"@cosmjs/socket@npm:^0.30.1": - version: 0.30.1 - resolution: "@cosmjs/socket@npm:0.30.1" - dependencies: - "@cosmjs/stream": "npm:^0.30.1" - isomorphic-ws: "npm:^4.0.1" - ws: "npm:^7" - xstream: "npm:^11.14.0" - checksum: 10/2fbee08d34ca2dfed08bbe4282ee600dc6aaa892aec5aa24286e648ef29e64d8e0a003824894e03332202b8ff48b7b08b8726f8d09ad0897437b3d059434ffe7 - languageName: node - linkType: hard - -"@cosmjs/stargate@npm:^0.30.1": - version: 0.30.1 - resolution: "@cosmjs/stargate@npm:0.30.1" - dependencies: - "@confio/ics23": "npm:^0.6.8" - "@cosmjs/amino": "npm:^0.30.1" - "@cosmjs/encoding": "npm:^0.30.1" - "@cosmjs/math": "npm:^0.30.1" - "@cosmjs/proto-signing": "npm:^0.30.1" - "@cosmjs/stream": "npm:^0.30.1" - "@cosmjs/tendermint-rpc": "npm:^0.30.1" - "@cosmjs/utils": "npm:^0.30.1" - cosmjs-types: "npm:^0.7.1" - long: "npm:^4.0.0" - protobufjs: "npm:~6.11.3" - xstream: "npm:^11.14.0" - checksum: 10/5b1eb882029102fcc7d0021a9c9893c8fdd43de1f8662872b477f47cd719a632a3cc7e32d45857b7e3d1169cba436292d3416067a0e2439dd95218c6617df38f - languageName: node - linkType: hard - -"@cosmjs/stream@npm:^0.30.1": - version: 0.30.1 - resolution: "@cosmjs/stream@npm:0.30.1" - dependencies: - xstream: "npm:^11.14.0" - checksum: 10/f9e48a8377c2d3cfbf288fcf4fad745905c042dabc442d2cbb93d4280033e3c8e493a3328f58c0b645b60f9c2188d14603b2bb37a174bc0619686c5e70b13dca - languageName: node - linkType: hard - -"@cosmjs/tendermint-rpc@npm:^0.30.1": - version: 0.30.1 - resolution: "@cosmjs/tendermint-rpc@npm:0.30.1" - dependencies: - "@cosmjs/crypto": "npm:^0.30.1" - "@cosmjs/encoding": "npm:^0.30.1" - "@cosmjs/json-rpc": "npm:^0.30.1" - "@cosmjs/math": "npm:^0.30.1" - "@cosmjs/socket": "npm:^0.30.1" - "@cosmjs/stream": "npm:^0.30.1" - "@cosmjs/utils": "npm:^0.30.1" - axios: "npm:^0.21.2" - readonly-date: "npm:^1.0.0" - xstream: "npm:^11.14.0" - checksum: 10/7bf1fd6b12725cf506bcaefaa4507e7d653b8dfb8faa8eddd83d0db3522308e9de5e0bbe228dabf41d5491e2e1efe32b9eaac4453a9854ead703b882e502ce0c - languageName: node - linkType: hard - -"@cosmjs/utils@npm:^0.30.1": - version: 0.30.1 - resolution: "@cosmjs/utils@npm:0.30.1" - checksum: 10/64ea16cdeba64d2b346a0b45ca47059ab4297fdf5c4e5fd89ec262eec488807f49f94dcdc294628142015ce4669c4eaf7426d1f8a6538146da5601dcc484cb19 - languageName: node - linkType: hard - "@craftzdog/react-native-buffer@npm:^6.0.5": version: 6.0.5 resolution: "@craftzdog/react-native-buffer@npm:6.0.5" @@ -3307,21 +3149,15 @@ __metadata: languageName: node linkType: hard -"@everstake/wallet-sdk@npm:^0.3.66": - version: 0.3.66 - resolution: "@everstake/wallet-sdk@npm:0.3.66" +"@everstake/wallet-sdk@npm:^1.0.5": + version: 1.0.5 + resolution: "@everstake/wallet-sdk@npm:1.0.5" dependencies: - "@cosmjs/stargate": "npm:^0.30.1" - "@mysten/sui.js": "npm:^0.46.1" - "@noble/hashes": "npm:^1.3.2" "@solana/web3.js": "npm:^1.75.0" - aptos: "npm:^1.8.4" - axios: "npm:^1.3.5" bignumber.js: "npm:^9.1.2" - bip39: "npm:^3.1.0" - bs58: "npm:^5.0.0" + superstruct: "npm:^0.16.7" web3: "npm:^4.14.0" - checksum: 10/035e4ac89b5d2d48c4731382e4bf03bc3b6c7e66a5c72a58629f3528eb8b6c6ad91f0eae392e6cdcd5b601d3b72d73f1b94cb8266c64267546a9ebda7189cf3d + checksum: 10/00715f194f44f2c9b2631b0d5e4b3c31be1483b2e750f8a1c9554ca3cc601f2756798ae5e78c7eb4ccc88c2e5c585e6442b654e6081570232489d667d241f4f9 languageName: node linkType: hard @@ -4767,33 +4603,6 @@ __metadata: languageName: node linkType: hard -"@mysten/bcs@npm:0.8.1": - version: 0.8.1 - resolution: "@mysten/bcs@npm:0.8.1" - dependencies: - bs58: "npm:^5.0.0" - checksum: 10/b2142572abc415a149ba9da4c9e93cba0ac1fe0c7c6108188c0c28133edad050556a60edc45c192446a0822e223a4ed33ec05e0994979e457b57a5ded1045b14 - languageName: node - linkType: hard - -"@mysten/sui.js@npm:^0.46.1": - version: 0.46.1 - resolution: "@mysten/sui.js@npm:0.46.1" - dependencies: - "@mysten/bcs": "npm:0.8.1" - "@noble/curves": "npm:^1.1.0" - "@noble/hashes": "npm:^1.3.1" - "@open-rpc/client-js": "npm:^1.8.1" - "@scure/bip32": "npm:^1.3.1" - "@scure/bip39": "npm:^1.2.1" - "@suchipi/femver": "npm:^1.0.0" - events: "npm:^3.3.0" - superstruct: "npm:^1.0.3" - tweetnacl: "npm:^1.0.3" - checksum: 10/0dc485608adbe587433b45285aa9439836310c55880c0550f523957ab5533fc6a79ce0f339432cc5c09f8f6cb62917fd790fbe9f62a7e27c58538978070d5101 - languageName: node - linkType: hard - "@napi-rs/simple-git-android-arm-eabi@npm:0.1.16": version: 0.1.16 resolution: "@napi-rs/simple-git-android-arm-eabi@npm:0.1.16" @@ -5001,7 +4810,7 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:1.4.2, @noble/curves@npm:^1.1.0, @noble/curves@npm:^1.4.2, @noble/curves@npm:~1.4.0": +"@noble/curves@npm:1.4.2, @noble/curves@npm:^1.4.2, @noble/curves@npm:~1.4.0": version: 1.4.2 resolution: "@noble/curves@npm:1.4.2" dependencies: @@ -5010,14 +4819,7 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:1.3.3, @noble/hashes@npm:~1.3.0": - version: 1.3.3 - resolution: "@noble/hashes@npm:1.3.3" - checksum: 10/1025ddde4d24630e95c0818e63d2d54ee131b980fe113312d17ed7468bc18f54486ac86c907685759f8a7e13c2f9b9e83ec7b67d1cc20836f36b5e4a65bb102d - languageName: node - linkType: hard - -"@noble/hashes@npm:1.4.0, @noble/hashes@npm:^1, @noble/hashes@npm:^1.0.0, @noble/hashes@npm:^1.2.0, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.3.2, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:~1.4.0": +"@noble/hashes@npm:1.4.0, @noble/hashes@npm:^1.2.0, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:~1.4.0": version: 1.4.0 resolution: "@noble/hashes@npm:1.4.0" checksum: 10/e156e65794c473794c52fa9d06baf1eb20903d0d96719530f523cc4450f6c721a957c544796e6efd0197b2296e7cd70efeb312f861465e17940a3e3c7e0febc6 @@ -5473,18 +5275,6 @@ __metadata: languageName: node linkType: hard -"@open-rpc/client-js@npm:^1.8.1": - version: 1.8.1 - resolution: "@open-rpc/client-js@npm:1.8.1" - dependencies: - isomorphic-fetch: "npm:^3.0.0" - isomorphic-ws: "npm:^5.0.0" - strict-event-emitter-types: "npm:^2.0.0" - ws: "npm:^7.0.0" - checksum: 10/f3cf39963ecdefd9b57a027e292588e7ba859887231bed7f8358f7813a6159e9f14bd8d61aecdcea968a9e968779f2db300ed7127f046a78b4b85dc174754609 - languageName: node - linkType: hard - "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -7061,14 +6851,14 @@ __metadata: languageName: node linkType: hard -"@scure/base@npm:^1.1.3, @scure/base@npm:~1.1.0, @scure/base@npm:~1.1.3, @scure/base@npm:~1.1.6": +"@scure/base@npm:^1.1.3, @scure/base@npm:~1.1.3, @scure/base@npm:~1.1.6": version: 1.1.9 resolution: "@scure/base@npm:1.1.9" checksum: 10/f0ab7f687bbcdee2a01377fe3cd808bf63977999672751295b6a92625d5322f4754a96d40f6bd579bc367aad48ecf8a4e6d0390e70296e6ded1076f52adb16bb languageName: node linkType: hard -"@scure/bip32@npm:1.4.0, @scure/bip32@npm:^1.3.1": +"@scure/bip32@npm:1.4.0": version: 1.4.0 resolution: "@scure/bip32@npm:1.4.0" dependencies: @@ -7079,17 +6869,7 @@ __metadata: languageName: node linkType: hard -"@scure/bip39@npm:1.2.1": - version: 1.2.1 - resolution: "@scure/bip39@npm:1.2.1" - dependencies: - "@noble/hashes": "npm:~1.3.0" - "@scure/base": "npm:~1.1.0" - checksum: 10/2ea368bbed34d6b1701c20683bf465e147f231a9e37e639b8c82f585d6f978bb0f3855fca7ceff04954ae248b3e313f5d322d0210614fb7acb402739415aaf31 - languageName: node - linkType: hard - -"@scure/bip39@npm:1.3.0, @scure/bip39@npm:^1.2.1": +"@scure/bip39@npm:1.3.0": version: 1.3.0 resolution: "@scure/bip39@npm:1.3.0" dependencies: @@ -8307,9 +8087,9 @@ __metadata: languageName: node linkType: hard -"@solana/web3.js@npm:^1.75.0": - version: 1.95.4 - resolution: "@solana/web3.js@npm:1.95.4" +"@solana/web3.js-version1@npm:@solana/web3.js@1.95.8, @solana/web3.js@npm:1.95.8": + version: 1.95.8 + resolution: "@solana/web3.js@npm:1.95.8" dependencies: "@babel/runtime": "npm:^7.25.0" "@noble/curves": "npm:^1.4.2" @@ -8326,7 +8106,7 @@ __metadata: node-fetch: "npm:^2.7.0" rpc-websockets: "npm:^9.0.2" superstruct: "npm:^2.0.2" - checksum: 10/353e04ac1110035ff108f16af4029c7a98f71cce841d45877c9bc4a354cdc58a051681603c92289b81e3dc5ef6b1567c6f866e4ba56a434db145e38a5a41d276 + checksum: 10/25fb38f46f4ba47019f17f686219a75f821455737bbf1153deb8b3f1141c3996c1ac0dc8603bbac50cd04f61058e472772d866aa38d01aef4e1609e53e442075 languageName: node linkType: hard @@ -9123,13 +8903,6 @@ __metadata: languageName: node linkType: hard -"@suchipi/femver@npm:^1.0.0": - version: 1.0.0 - resolution: "@suchipi/femver@npm:1.0.0" - checksum: 10/26139f9c54932ac94cc7bae8451d38a0752f8d8b634ef42bbfd3b85cd16dee31a259f1fc1e634b3096328c2db5f68d34be13578aa4bbbcccbd9b121610abcba1 - languageName: node - linkType: hard - "@suite-common/analytics@workspace:*, @suite-common/analytics@workspace:suite-common/analytics": version: 0.0.0-use.local resolution: "@suite-common/analytics@workspace:suite-common/analytics" @@ -9538,6 +9311,7 @@ __metadata: version: 0.0.0-use.local resolution: "@suite-common/wallet-utils@workspace:suite-common/wallet-utils" dependencies: + "@everstake/wallet-sdk": "npm:^1.0.5" "@mobily/ts-belt": "npm:^3.13.1" "@solana-program/compute-budget": "npm:^0.6.1" "@solana-program/system": "npm:^0.6.2" @@ -11292,6 +11066,7 @@ __metadata: version: 0.0.0-use.local resolution: "@trezor/blockchain-link-types@workspace:packages/blockchain-link-types" dependencies: + "@everstake/wallet-sdk": "npm:^1.0.5" "@solana/web3.js": "npm:^2.0.0" "@trezor/type-utils": "workspace:*" "@trezor/utxo-lib": "workspace:*" @@ -11305,6 +11080,7 @@ __metadata: version: 0.0.0-use.local resolution: "@trezor/blockchain-link-utils@workspace:packages/blockchain-link-utils" dependencies: + "@everstake/wallet-sdk": "npm:^1.0.5" "@mobily/ts-belt": "npm:^3.13.1" "@trezor/blockchain-link-types": "workspace:*" "@trezor/env-utils": "workspace:*" @@ -11321,6 +11097,7 @@ __metadata: version: 0.0.0-use.local resolution: "@trezor/blockchain-link@workspace:packages/blockchain-link" dependencies: + "@everstake/wallet-sdk": "npm:^1.0.5" "@solana-program/token": "npm:^0.4.1" "@solana/web3.js": "npm:^2.0.0" "@trezor/blockchain-link-types": "workspace:*" @@ -12258,7 +12035,7 @@ __metadata: resolution: "@trezor/suite@workspace:packages/suite" dependencies: "@crowdin/cli": "npm:^4.0.0" - "@everstake/wallet-sdk": "npm:^0.3.66" + "@everstake/wallet-sdk": "npm:^1.0.5" "@floating-ui/react": "npm:^0.26.9" "@formatjs/cli": "npm:^6.2.7" "@formatjs/intl": "npm:2.10.0" @@ -12268,6 +12045,7 @@ __metadata: "@sentry/core": "npm:^7.100.1" "@solana/buffer-layout": "npm:^4.0.1" "@solana/web3.js": "npm:^2.0.0" + "@solana/web3.js-version1": "npm:@solana/web3.js@1.95.8" "@suite-common/analytics": "workspace:*" "@suite-common/assets": "workspace:*" "@suite-common/connect-init": "workspace:*" @@ -13505,13 +13283,6 @@ __metadata: languageName: node linkType: hard -"@types/long@npm:^4.0.1": - version: 4.0.2 - resolution: "@types/long@npm:4.0.2" - checksum: 10/68afa05fb20949d88345876148a76f6ccff5433310e720db51ac5ca21cb8cc6714286dbe04713840ddbd25a8b56b7a23aa87d08472fabf06463a6f2ed4967707 - languageName: node - linkType: hard - "@types/markdown-it@npm:^14.1.1": version: 14.1.1 resolution: "@types/markdown-it@npm:14.1.1" @@ -15233,20 +15004,6 @@ __metadata: languageName: node linkType: hard -"aptos@npm:^1.8.4": - version: 1.21.0 - resolution: "aptos@npm:1.21.0" - dependencies: - "@aptos-labs/aptos-client": "npm:^0.1.0" - "@noble/hashes": "npm:1.3.3" - "@scure/bip39": "npm:1.2.1" - eventemitter3: "npm:^5.0.1" - form-data: "npm:4.0.0" - tweetnacl: "npm:1.0.3" - checksum: 10/6200ea828d719c6adc38e884441b3e1a634ce1fb77b1d33459dc24704525623b266e849508102bc74a6fca7cb9f6f447d8e478f436910bc88258f845889d497e - languageName: node - linkType: hard - "arch@npm:^2.1.0, arch@npm:^2.2.0": version: 2.2.0 resolution: "arch@npm:2.2.0" @@ -15714,27 +15471,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:1.6.2": - version: 1.6.2 - resolution: "axios@npm:1.6.2" - dependencies: - follow-redirects: "npm:^1.15.0" - form-data: "npm:^4.0.0" - proxy-from-env: "npm:^1.1.0" - checksum: 10/612bc93f8f738a518e7c5f9de9cc782bcd36aac6bae279160ef6a10260378e21c1786520eab3336898e3d66e0839ebdf739f327fb6d0431baa4d3235703a7652 - languageName: node - linkType: hard - -"axios@npm:^0.21.2": - version: 0.21.4 - resolution: "axios@npm:0.21.4" - dependencies: - follow-redirects: "npm:^1.14.0" - checksum: 10/da644592cb6f8f9f8c64fdabd7e1396d6769d7a4c1ea5f8ae8beb5c2eb90a823e3a574352b0b934ac62edc762c0f52647753dc54f7d07279127a7e5c4cd20272 - languageName: node - linkType: hard - -"axios@npm:^1.3.5, axios@npm:^1.6.0, axios@npm:^1.6.1, axios@npm:^1.6.7, axios@npm:^1.7.7": +"axios@npm:^1.6.0, axios@npm:^1.6.1, axios@npm:^1.6.7, axios@npm:^1.7.7": version: 1.7.7 resolution: "axios@npm:1.7.7" dependencies: @@ -16095,13 +15832,6 @@ __metadata: languageName: node linkType: hard -"base-x@npm:^4.0.0": - version: 4.0.0 - resolution: "base-x@npm:4.0.0" - checksum: 10/b25db9e07eb1998472a20557c7f00c797dc0595f79df95155ab74274e7fa98b9f2659b3ee547ac8773666b7f69540656793aeb97ad2b1ceccdb6fa5faaf69ac0 - languageName: node - linkType: hard - "base-x@npm:^5.0.0": version: 5.0.0 resolution: "base-x@npm:5.0.0" @@ -16192,13 +15922,6 @@ __metadata: languageName: node linkType: hard -"bech32@npm:^1.1.4": - version: 1.1.4 - resolution: "bech32@npm:1.1.4" - checksum: 10/63ff37c0ce43be914c685ce89700bba1589c319af0dac1ea04f51b33d0e5ecfd40d14c24f527350b94f0a4e236385373bb9122ec276410f354ddcdbf29ca13f4 - languageName: node - linkType: hard - "bech32@npm:^2.0.0": version: 2.0.0 resolution: "bech32@npm:2.0.0" @@ -16285,15 +16008,6 @@ __metadata: languageName: node linkType: hard -"bip39@npm:^3.1.0": - version: 3.1.0 - resolution: "bip39@npm:3.1.0" - dependencies: - "@noble/hashes": "npm:^1.2.0" - checksum: 10/406c0b5bdab0d43df2ff8916c5b57bb7f65cd36a115cbd7620a75b972638f8511840bfc27bfc68946027086fd33ed3050d8cd304e85fd527503b23d857a8486e - languageName: node - linkType: hard - "bip66@npm:^2.0.0": version: 2.0.0 resolution: "bip66@npm:2.0.0" @@ -16698,15 +16412,6 @@ __metadata: languageName: node linkType: hard -"bs58@npm:^5.0.0": - version: 5.0.0 - resolution: "bs58@npm:5.0.0" - dependencies: - base-x: "npm:^4.0.0" - checksum: 10/2475cb0684e07077521aac718e604a13e0f891d58cff923d437a2f7e9e28703ab39fce9f84c7c703ab369815a675f11e3bd394d38643bfe8969fbe42e6833d45 - languageName: node - linkType: hard - "bs58@npm:^6.0.0": version: 6.0.0 resolution: "bs58@npm:6.0.0" @@ -18481,16 +18186,6 @@ __metadata: languageName: node linkType: hard -"cosmjs-types@npm:^0.7.1": - version: 0.7.2 - resolution: "cosmjs-types@npm:0.7.2" - dependencies: - long: "npm:^4.0.0" - protobufjs: "npm:~6.11.2" - checksum: 10/28144cfdce38dafedd702b90a625fb5822ddc0abb63cca4eaf25e5c7b1ef80b0186248f1cf302f181b96f8394fa1e103c30817a61250a9cfec4502b84f997252 - languageName: node - linkType: hard - "crc-32@npm:^1.2.0, crc-32@npm:^1.2.2": version: 1.2.2 resolution: "crc-32@npm:1.2.2" @@ -23594,7 +23289,7 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.14.0, follow-redirects@npm:^1.15.0, follow-redirects@npm:^1.15.6": +"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.15.6": version: 1.15.6 resolution: "follow-redirects@npm:1.15.6" peerDependenciesMeta: @@ -23660,25 +23355,25 @@ __metadata: languageName: node linkType: hard -"form-data@npm:4.0.0, form-data@npm:^4.0.0": - version: 4.0.0 - resolution: "form-data@npm:4.0.0" +"form-data@npm:^3.0.0, form-data@npm:^3.0.1": + version: 3.0.1 + resolution: "form-data@npm:3.0.1" dependencies: asynckit: "npm:^0.4.0" combined-stream: "npm:^1.0.8" mime-types: "npm:^2.1.12" - checksum: 10/7264aa760a8cf09482816d8300f1b6e2423de1b02bba612a136857413fdc96d7178298ced106817655facc6b89036c6e12ae31c9eb5bdc16aabf502ae8a5d805 + checksum: 10/944b40ff63b9cb1ca7a97e70f72104c548e0b0263e3e817e49919015a0d687453086259b93005389896dbffd3777cccea2e67c51f4e827590e5979b14ff91bf7 languageName: node linkType: hard -"form-data@npm:^3.0.0, form-data@npm:^3.0.1": - version: 3.0.1 - resolution: "form-data@npm:3.0.1" +"form-data@npm:^4.0.0": + version: 4.0.0 + resolution: "form-data@npm:4.0.0" dependencies: asynckit: "npm:^0.4.0" combined-stream: "npm:^1.0.8" mime-types: "npm:^2.1.12" - checksum: 10/944b40ff63b9cb1ca7a97e70f72104c548e0b0263e3e817e49919015a0d687453086259b93005389896dbffd3777cccea2e67c51f4e827590e5979b14ff91bf7 + checksum: 10/7264aa760a8cf09482816d8300f1b6e2423de1b02bba612a136857413fdc96d7178298ced106817655facc6b89036c6e12ae31c9eb5bdc16aabf502ae8a5d805 languageName: node linkType: hard @@ -24519,7 +24214,7 @@ __metadata: languageName: node linkType: hard -"got@npm:^11.7.0, got@npm:^11.8.5, got@npm:^11.8.6": +"got@npm:^11.7.0, got@npm:^11.8.5": version: 11.8.6 resolution: "got@npm:11.8.6" dependencies: @@ -26647,16 +26342,6 @@ __metadata: languageName: node linkType: hard -"isomorphic-fetch@npm:^3.0.0": - version: 3.0.0 - resolution: "isomorphic-fetch@npm:3.0.0" - dependencies: - node-fetch: "npm:^2.6.1" - whatwg-fetch: "npm:^3.4.1" - checksum: 10/568fe0307528c63405c44dd3873b7b6c96c0d19ff795cb15846e728b6823bdbc68cc8c97ac23324509661316f12f551e43dac2929bc7030b8bc4d6aa1158b857 - languageName: node - linkType: hard - "isomorphic-ws@npm:^4.0.1": version: 4.0.1 resolution: "isomorphic-ws@npm:4.0.1" @@ -28515,22 +28200,6 @@ __metadata: languageName: node linkType: hard -"libsodium-wrappers@npm:^0.7.6": - version: 0.7.13 - resolution: "libsodium-wrappers@npm:0.7.13" - dependencies: - libsodium: "npm:^0.7.13" - checksum: 10/e5f7f5baf37095a764e3f5037ab47d65343db0c0efca4add80a5c2037ac2cb9fb05f00646f7b75220bd4f30bccc02e9742603ceb8eb97e5c305103cd4f86392d - languageName: node - linkType: hard - -"libsodium@npm:^0.7.13": - version: 0.7.13 - resolution: "libsodium@npm:0.7.13" - checksum: 10/0105523135c1ed1c4ee853a36cd3b3b562ed150f5888ae61be4b2945f0b1010e7ecaad4cdc9333beac034a88bdf470a93f22609877a06f15f5d690a9591e0d7c - languageName: node - linkType: hard - "lie@npm:3.1.1": version: 3.1.1 resolution: "lie@npm:3.1.1" @@ -34535,30 +34204,6 @@ __metadata: languageName: node linkType: hard -"protobufjs@npm:^6.8.8, protobufjs@npm:~6.11.2, protobufjs@npm:~6.11.3": - version: 6.11.4 - resolution: "protobufjs@npm:6.11.4" - dependencies: - "@protobufjs/aspromise": "npm:^1.1.2" - "@protobufjs/base64": "npm:^1.1.2" - "@protobufjs/codegen": "npm:^2.0.4" - "@protobufjs/eventemitter": "npm:^1.1.0" - "@protobufjs/fetch": "npm:^1.1.0" - "@protobufjs/float": "npm:^1.0.2" - "@protobufjs/inquire": "npm:^1.1.0" - "@protobufjs/path": "npm:^1.1.2" - "@protobufjs/pool": "npm:^1.1.0" - "@protobufjs/utf8": "npm:^1.1.0" - "@types/long": "npm:^4.0.1" - "@types/node": "npm:>=13.7.0" - long: "npm:^4.0.0" - bin: - pbjs: bin/pbjs - pbts: bin/pbts - checksum: 10/6b7fd7540d74350d65c38f69f398c9995ae019da070e79d9cd464a458c6d19b40b07c9a026be4e10704c824a344b603307745863310c50026ebd661ce4da0663 - languageName: node - linkType: hard - "protocols@npm:^2.0.0, protocols@npm:^2.0.1": version: 2.0.1 resolution: "protocols@npm:2.0.1" @@ -35887,13 +35532,6 @@ __metadata: languageName: node linkType: hard -"readonly-date@npm:^1.0.0": - version: 1.0.0 - resolution: "readonly-date@npm:1.0.0" - checksum: 10/70a42fd3dbc94a2823e79a415fa46ced7d961b39e33fc0654b2cc774ee56e67a7a6fa3d268ba0f5d2268fdd21b65512142c6fa91e5c21ad526fc3c1b7c019e07 - languageName: node - linkType: hard - "real-require@npm:^0.2.0": version: 0.2.0 resolution: "real-require@npm:0.2.0" @@ -38412,13 +38050,6 @@ __metadata: languageName: node linkType: hard -"strict-event-emitter-types@npm:^2.0.0": - version: 2.0.0 - resolution: "strict-event-emitter-types@npm:2.0.0" - checksum: 10/d7b28708bf09648302e8ea5a6c4999d7e3186a06f68568a6196a71e315bf4ef44f78011db11576b0b4ed45df4a8f0baf0d64232d2a6eaf21fa92c7ec4085f481 - languageName: node - linkType: hard - "strict-uri-encode@npm:^2.0.0": version: 2.0.0 resolution: "strict-uri-encode@npm:2.0.0" @@ -38931,10 +38562,10 @@ __metadata: languageName: node linkType: hard -"superstruct@npm:^1.0.3": - version: 1.0.3 - resolution: "superstruct@npm:1.0.3" - checksum: 10/632b6171ac136b6750e62a55f806cc949b3dbf2b4a7dc70cc85f54adcdf19d21eab9711f04e8a643b7dd622bbd8658366ead924f467adaccb2c8005c133b7976 +"superstruct@npm:^0.16.7": + version: 0.16.7 + resolution: "superstruct@npm:0.16.7" + checksum: 10/2f3fb30f8fa3e012fb74fc6f8c850b13b6f6c4b8a412186996b55da8292586205dda8495bcd2e850436a6476dd25f7d0d226905f1ed97162903dcf4cce59d2b8 languageName: node linkType: hard @@ -39112,13 +38743,6 @@ __metadata: languageName: node linkType: hard -"symbol-observable@npm:^2.0.3": - version: 2.0.3 - resolution: "symbol-observable@npm:2.0.3" - checksum: 10/15ca0de42b490172e901b4cf61b35da9d5ad752afcb5ef6dc9c998a0e9b414dfa26aa9f0dde11d235dfa96731e26a6521c98738e90143bf8efe65bfdc1633be1 - languageName: node - linkType: hard - "symbol-tree@npm:^3.2.4": version: 3.2.4 resolution: "symbol-tree@npm:3.2.4" @@ -40193,13 +39817,6 @@ __metadata: languageName: node linkType: hard -"tweetnacl@npm:1.0.3, tweetnacl@npm:^1.0.3": - version: 1.0.3 - resolution: "tweetnacl@npm:1.0.3" - checksum: 10/ca122c2f86631f3c0f6d28efb44af2a301d4a557a62a3e2460286b08e97567b258c2212e4ad1cfa22bd6a57edcdc54ba76ebe946847450ab0999e6d48ccae332 - languageName: node - linkType: hard - "tweetnacl@npm:^0.14.3, tweetnacl@npm:~0.14.0": version: 0.14.5 resolution: "tweetnacl@npm:0.14.5" @@ -40207,6 +39824,13 @@ __metadata: languageName: node linkType: hard +"tweetnacl@npm:^1.0.3": + version: 1.0.3 + resolution: "tweetnacl@npm:1.0.3" + checksum: 10/ca122c2f86631f3c0f6d28efb44af2a301d4a557a62a3e2460286b08e97567b258c2212e4ad1cfa22bd6a57edcdc54ba76ebe946847450ab0999e6d48ccae332 + languageName: node + linkType: hard + "type-check@npm:^0.4.0, type-check@npm:~0.4.0": version: 0.4.0 resolution: "type-check@npm:0.4.0" @@ -42420,7 +42044,7 @@ __metadata: languageName: node linkType: hard -"whatwg-fetch@npm:^3.0.0, whatwg-fetch@npm:^3.4.1": +"whatwg-fetch@npm:^3.0.0": version: 3.6.20 resolution: "whatwg-fetch@npm:3.6.20" checksum: 10/2b4ed92acd6a7ad4f626a6cb18b14ec982bbcaf1093e6fe903b131a9c6decd14d7f9c9ca3532663c2759d1bdf01d004c77a0adfb2716a5105465c20755a8c57c @@ -42848,16 +42472,6 @@ __metadata: languageName: node linkType: hard -"xstream@npm:^11.14.0": - version: 11.14.0 - resolution: "xstream@npm:11.14.0" - dependencies: - globalthis: "npm:^1.0.1" - symbol-observable: "npm:^2.0.3" - checksum: 10/935b1f2b4dd79ef77de3fb0153af03ffbbc93fed0c2e0f1afcf3771f462e1c3126430ce6767f9ca1d374b2ced4beb8d3d6c05fb4c0cfd92a0f208b068fb87779 - languageName: node - linkType: hard - "xtend@npm:^4.0.0, xtend@npm:^4.0.2, xtend@npm:~4.0.1": version: 4.0.2 resolution: "xtend@npm:4.0.2"