Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/implement solana staking #15409

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion jest.config.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,5 @@ module.exports = {
'<rootDir>/../../node_modules/react-native-gesture-handler/jestSetup.js',
'<rootDir>/../../suite-native/test-utils/src/atomsMock.js',
'<rootDir>/../../suite-native/test-utils/src/expoMock.js',
'<rootDir>/../../suite-native/test-utils/src/walletSdkMock.js',
],
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/blockchain-link-types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*"
Expand Down
3 changes: 3 additions & 0 deletions packages/blockchain-link-types/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
ContractInfo,
StakingPool,
} from './blockbook-api';
import type { SolanaStakingAccount } from './solana';

/* Common types used in both params and responses */

Expand Down Expand Up @@ -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)
Expand All @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions packages/blockchain-link-types/src/solana.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
GetTransactionApi,
Signature,
} from '@solana/web3.js';
import { SolDelegation } from '@everstake/wallet-sdk';

import type {
GetObjectWithKey,
Expand Down Expand Up @@ -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 } };
1 change: 1 addition & 0 deletions packages/blockchain-link-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*"
Expand Down
18 changes: 5 additions & 13 deletions packages/blockchain-link-utils/src/blockbook.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ETH_NETWORK_ADDRESSES } from '@everstake/wallet-sdk';

import { BigNumber } from '@trezor/utils/src/bigNumber';
import type {
Utxo,
Expand Down Expand Up @@ -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 = (
Expand Down
1 change: 1 addition & 0 deletions packages/blockchain-link/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
10 changes: 9 additions & 1 deletion packages/blockchain-link/src/workers/solana/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -203,7 +204,11 @@ const pushTransaction = async (request: Request<MessageTypes.PushTransaction>) =
}
};

const getAccountInfo = async (request: Request<MessageTypes.GetAccountInfo>) => {
const getAccountInfo = async (
request: Request<MessageTypes.GetAccountInfo>,
// TODO: uncomment when solana staking accounts are supported
// isTestnet: boolean,
) => {
const { payload } = request;
const { details = 'basic' } = payload;
const api = await request.connect();
Expand Down Expand Up @@ -333,9 +338,12 @@ const getAccountInfo = async (request: Request<MessageTypes.GetAccountInfo>) =>
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: [],
};
}
}
Expand Down
22 changes: 22 additions & 0 deletions packages/blockchain-link/src/workers/utils.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
};
3 changes: 2 additions & 1 deletion packages/connect-iframe/webpack/base.webpack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
3 changes: 2 additions & 1 deletion packages/suite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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/[email protected]",
"@suite-common/analytics": "workspace:*",
"@suite-common/assets": "workspace:*",
"@suite-common/connect-init": "workspace:*",
Expand Down
156 changes: 156 additions & 0 deletions packages/suite/src/actions/wallet/stake/stakeFormActions.ts
Original file line number Diff line number Diff line change
@@ -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;
};
Loading