diff --git a/packages/connect-explorer/src/pages/methods/solana/solanaComposeTransaction.mdx b/packages/connect-explorer/src/pages/methods/solana/solanaComposeTransaction.mdx
new file mode 100644
index 00000000000..b1c63bbfe92
--- /dev/null
+++ b/packages/connect-explorer/src/pages/methods/solana/solanaComposeTransaction.mdx
@@ -0,0 +1,96 @@
+import { SolanaComposeTransaction } from '@trezor/connect/src/types/api/solana';
+
+import { ParamsTable } from '../../../components/ParamsTable';
+import { CommonParamsLink } from '../../../components/CommonParamsLink';
+import { ApiPlayground } from '../../../components/ApiPlayground';
+
+
+
+export const paramDescriptions = {
+ path: 'minimum length is `2`. [read more](/details/path)',
+ fromAddress: 'Sender address',
+ toAddress:
+ 'Recipient address. In case of token transfer, this still means the owner address of the token account. The associated token account is created automatically.',
+ amount: 'Amount to send in decimal string format',
+ priorityFees:
+ 'Fee configuration. If not set, it defaults to hardcoded values. It is recommended to simulate the transaction using `blockchainEstimateFee`',
+ token: 'Token details in case of token transfer',
+ blockHash: 'Recent Block hash',
+ lastValidBlockHeight: 'Recent Block height',
+ coin: '"SOL" for mainnet (default), "DSOL" for devnet',
+ identity: "Blockchain connection identity. It's used to separate multiple connections.",
+};
+
+## Solana: Compose transaction
+
+Compose a Solana transfer transaction that can be later signed on device using [solanaSignTransaction](/methods/solana/solanaSignTransaction/).
+
+The transaction may be a native SOL transfer or a token transfer.
+
+```javascript
+const result = await TrezorConnect.solanaComposeTransaction(params);
+```
+
+### Params
+
+
+
+#### SolanaComposeTransaction
+
+
+
+### Examples
+
+```javascript
+TrezorConnect.solanaComposeTransaction({
+ fromAddress: '...',
+ toAddress: '...',
+ amount: '0.1',
+ blockHash: '...',
+ lastValidBlockHeight: 123456,
+ coin: 'SOL',
+});
+```
+
+### Result
+
+[SolanaComposedTransaction type](https://github.com/trezor/trezor-suite/blob/develop/packages/connect/src/types/api/solana/index.ts)
+
+```javascript
+{
+ success: true,
+ payload: {
+ serializedTx: string,
+ additionalInfo: {
+ isCreatingAccount: boolean,
+ // in case of token transfer:
+ newTokenAccountProgramName: "spl-token" | "spl-token-2022",
+ tokenAccountInfo: {
+ baseAddress: string,
+ tokenProgram: string,
+ tokenMint: string,
+ tokenAccount: string,
+ },
+ }
+ }
+}
+```
+
+Error
+
+```javascript
+{
+ success: false,
+ payload: {
+ error: string // error message
+ }
+}
+```
diff --git a/packages/connect-explorer/src/pages/methods/solana/solanaSignTransaction.mdx b/packages/connect-explorer/src/pages/methods/solana/solanaSignTransaction.mdx
index 084f840aca5..e03ff9586f2 100644
--- a/packages/connect-explorer/src/pages/methods/solana/solanaSignTransaction.mdx
+++ b/packages/connect-explorer/src/pages/methods/solana/solanaSignTransaction.mdx
@@ -19,6 +19,8 @@ import signTransaction from '../../../data/methods/solana/signTransaction.ts';
export const paramDescriptions = {
path: 'minimum length is `2`. [read more](/details/path)',
serializedTx: '',
+ serialize:
+ 'If `true`, the transaction will be deserialized before signing and serialized back after signing. Without this option, the method will only return the signature by itself.',
};
## Solana: Sign transaction
diff --git a/packages/connect/e2e/__fixtures__/index.ts b/packages/connect/e2e/__fixtures__/index.ts
index f9df6ba282a..66a8af65409 100644
--- a/packages/connect/e2e/__fixtures__/index.ts
+++ b/packages/connect/e2e/__fixtures__/index.ts
@@ -56,6 +56,7 @@ export { default as signTransactionReplace } from './signTransactionReplace';
export { default as signTransactionSegwit } from './signTransactionSegwit';
export { default as signTransactionTaproot } from './signTransactionTaproot';
export { default as signTransactionZcash } from './signTransactionZcash';
+export { default as solanaComposeTransaction } from './solanaComposeTransaction';
export { default as solanaGetAddress } from './solanaGetAddress';
export { default as solanaGetPublicKey } from './solanaGetPublicKey';
export { default as solanaSignTransaction } from './solanaSignTransaction';
diff --git a/packages/connect/e2e/__fixtures__/solanaComposeTransaction.ts b/packages/connect/e2e/__fixtures__/solanaComposeTransaction.ts
new file mode 100644
index 00000000000..d570791679a
--- /dev/null
+++ b/packages/connect/e2e/__fixtures__/solanaComposeTransaction.ts
@@ -0,0 +1,97 @@
+export default {
+ method: 'solanaComposeTransaction',
+ setup: {
+ mnemonic: undefined, // device is not used in this test case
+ },
+ tests: [
+ {
+ description: 'Basic SOL transfer',
+ params: {
+ fromAddress: 'ANctUhC7YZPueiv4T8bkDcHYEAJ7Hwoxhvgnr2QkF8uR',
+ toAddress: '5Q9c3XoBef8BYA5RzSmogWnRrQas6HPwYuo4AYPafpom',
+ amount: '0.01',
+ blockHash: 'BXim2ZLR2UZ4JQxhNGaGngbRaySWej4x8zqaw7TJ4GLo',
+ lastValidBlockHeight: 290999279,
+ coin: 'sol',
+ },
+ result: {
+ serializedTx:
+ '0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010002048b42f51ed008e27e643818f0503bdfeb6535dc815e3a9fd41a7ce42344f888dc415cd5ca15bbbbcf07713fe3f690ebaab81c9cfa8f8a95f91b17deabb953b83400000000000000000000000000000000000000000000000000000000000000000306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a400000009c7382bc11ef07feb1a080bc78d2f7f6906ae7cb7c389057fd99ae3e97c811e00303000502400d030003000903a086010000000000020200010c020000008096980000000000',
+ additionalInfo: {
+ isCreatingAccount: false,
+ },
+ },
+ },
+ {
+ description: 'SOL token transfer',
+ params: {
+ fromAddress: 'ANctUhC7YZPueiv4T8bkDcHYEAJ7Hwoxhvgnr2QkF8uR',
+ toAddress: '5Q9c3XoBef8BYA5RzSmogWnRrQas6HPwYuo4AYPafpom',
+ amount: '1',
+ token: {
+ mint: 'HBoNJ5v8g71s2boRivrHnfSB5MVPLDHHyVjruPfhGkvL',
+ program: 'spl-token',
+ decimals: 1,
+ accounts: [
+ {
+ publicKey: '6EjZ73R3oEUHQL4zczkx3pRD3acysP3ug7hwMAdzdtNQ',
+ balance: '30',
+ },
+ ],
+ },
+ blockHash: 'HaVyjCbyqQa2sbwXTLKskNretcsRVws6VEBYP6xZMKm',
+ lastValidBlockHeight: 290999384,
+ coin: 'sol',
+ },
+ result: {
+ serializedTx:
+ '0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010003068b42f51ed008e27e643818f0503bdfeb6535dc815e3a9fd41a7ce42344f888dc4dcf19bed853ae158e8c5ad250530e09f8fb4b82bee35ec1a3c89d30b2b183f559e1abe20307a4f7ed6e586aea367e24b7d9f5c6d4d0969a72d9d0dd3436abe10306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a40000000f07f39f63a83ade085f851ca297f39b6993fd7b0fccba5e3aff7511fad40da3906ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9043f2bcc70fd3420cfbdcdd1da062d993dd46072ab2da56948d6ba1006138da00303000502400d030003000903a0860100000000000504010402000a0c0a0000000000000001',
+ additionalInfo: {
+ isCreatingAccount: false,
+ tokenAccountInfo: {
+ baseAddress: '5Q9c3XoBef8BYA5RzSmogWnRrQas6HPwYuo4AYPafpom',
+ tokenProgram: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
+ tokenMint: 'HBoNJ5v8g71s2boRivrHnfSB5MVPLDHHyVjruPfhGkvL',
+ tokenAccount: '73rsTqUoMd34Y3YwXtu4An2LkncLF9SeDY6TGUFfksfe',
+ },
+ },
+ },
+ },
+ {
+ description: 'SOL token transfer - new account',
+ params: {
+ fromAddress: 'ANctUhC7YZPueiv4T8bkDcHYEAJ7Hwoxhvgnr2QkF8uR',
+ toAddress: 'Aey9o8JXzTcQdjJVrV4Y56xzt5qHkLPWLgAQmaodUojm',
+ amount: '1',
+ token: {
+ mint: 'HBoNJ5v8g71s2boRivrHnfSB5MVPLDHHyVjruPfhGkvL',
+ program: 'spl-token',
+ decimals: 1,
+ accounts: [
+ {
+ publicKey: '6EjZ73R3oEUHQL4zczkx3pRD3acysP3ug7hwMAdzdtNQ',
+ balance: '30',
+ },
+ ],
+ },
+ blockHash: 'FuVcUvTCAEAefjSb7twrdnM98KFuxPhe9idGNZ3jb4zT',
+ lastValidBlockHeight: 290999698,
+ coin: 'sol',
+ },
+ result: {
+ serializedTx:
+ '0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010006098b42f51ed008e27e643818f0503bdfeb6535dc815e3a9fd41a7ce42344f888dc297187b6006964cafcb0a71cdf7d16cf80563debd388a20ed2beae6de8f0e23d4dcf19bed853ae158e8c5ad250530e09f8fb4b82bee35ec1a3c89d30b2b183f500000000000000000000000000000000000000000000000000000000000000008f7329c364a8e1d0376058cc672d81ce5ff8585a46c0adcb918ca7e3f0dc82a08c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f8590306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a40000000f07f39f63a83ade085f851ca297f39b6993fd7b0fccba5e3aff7511fad40da3906ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9dd762ba278d68a101267dbdcd6b7e6e1a84e080acdf553ba8caf0d072fbb6d200406000502400d030006000903a0860100000000000506000104070308000804020701000a0c0a0000000000000001',
+ additionalInfo: {
+ isCreatingAccount: true,
+ newTokenAccountProgramName: 'spl-token',
+ tokenAccountInfo: {
+ baseAddress: 'Aey9o8JXzTcQdjJVrV4Y56xzt5qHkLPWLgAQmaodUojm',
+ tokenProgram: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
+ tokenMint: 'HBoNJ5v8g71s2boRivrHnfSB5MVPLDHHyVjruPfhGkvL',
+ tokenAccount: '3nn86A71hFhoqYgPqLWSXdoxUwtfJNWoevBQUouAjSEg',
+ },
+ },
+ },
+ },
+ ],
+};
diff --git a/packages/connect/e2e/__wscache__/index.js b/packages/connect/e2e/__wscache__/index.js
index cad0fd21f42..e31228f6cd1 100644
--- a/packages/connect/e2e/__wscache__/index.js
+++ b/packages/connect/e2e/__wscache__/index.js
@@ -6,6 +6,9 @@ const transformCoinsJson = json => {
Object.keys(json).forEach(key => {
json[key].forEach(coin => {
if (coin.blockchain_link) {
+ // Skip for Solana, it uses a combination of HTTP and WebSocket, therefore it is not supported currently
+ if (coin.blockchain_link.type === 'solana') return;
+
const query = `?type=${coin.blockchain_link.type}&shortcut=${coin.shortcut}&suffix=/websocket`;
coin.blockchain_link.url = [`ws://localhost:18088/${query}`];
}
diff --git a/packages/connect/package.json b/packages/connect/package.json
index 2dc0beb449e..19d6b5d58e4 100644
--- a/packages/connect/package.json
+++ b/packages/connect/package.json
@@ -72,10 +72,17 @@
"@ethereumjs/common": "^4.4.0",
"@ethereumjs/tx": "^5.4.0",
"@fivebinaries/coin-selection": "3.0.0",
+ "@mobily/ts-belt": "^3.13.1",
"@noble/hashes": "^1.6.1",
"@scure/bip39": "^1.5.1",
+ "@solana-program/compute-budget": "^0.6.1",
+ "@solana-program/system": "^0.6.2",
+ "@solana-program/token": "^0.4.1",
+ "@solana-program/token-2022": "^0.3.1",
+ "@solana/web3.js": "^2.0.0",
"@trezor/blockchain-link": "workspace:*",
"@trezor/blockchain-link-types": "workspace:*",
+ "@trezor/blockchain-link-utils": "workspace:*",
"@trezor/connect-analytics": "workspace:*",
"@trezor/connect-common": "workspace:*",
"@trezor/crypto-utils": "workspace:*",
diff --git a/suite-common/wallet-utils/src/__fixtures__/solanaUtils.ts b/packages/connect/src/api/solana/__fixtures__/solanaUtils.ts
similarity index 100%
rename from suite-common/wallet-utils/src/__fixtures__/solanaUtils.ts
rename to packages/connect/src/api/solana/__fixtures__/solanaUtils.ts
diff --git a/suite-common/wallet-utils/src/__tests__/solanaUtils.test.ts b/packages/connect/src/api/solana/__tests__/solanaUtils.test.ts
similarity index 94%
rename from suite-common/wallet-utils/src/__tests__/solanaUtils.test.ts
rename to packages/connect/src/api/solana/__tests__/solanaUtils.test.ts
index cb41ad7bb7e..5e45c8c6507 100644
--- a/suite-common/wallet-utils/src/__tests__/solanaUtils.test.ts
+++ b/packages/connect/src/api/solana/__tests__/solanaUtils.test.ts
@@ -4,6 +4,7 @@ import {
buildTokenTransferInstruction,
buildTokenTransferTransaction,
getMinimumRequiredTokenAccountsForTransfer,
+ getLamportsFromSol,
} from '../solanaUtils';
describe('solana utils', () => {
@@ -83,4 +84,9 @@ describe('solana utils', () => {
});
});
});
+
+ it('getLamportsFromSol', () => {
+ expect(getLamportsFromSol('1')).toEqual(1000000000n);
+ expect(getLamportsFromSol('0.000000001')).toEqual(1n);
+ });
});
diff --git a/packages/connect/src/api/solana/api/index.ts b/packages/connect/src/api/solana/api/index.ts
index 163de912550..281e228608d 100644
--- a/packages/connect/src/api/solana/api/index.ts
+++ b/packages/connect/src/api/solana/api/index.ts
@@ -1,3 +1,4 @@
+export { default as solanaComposeTransaction } from './solanaComposeTransaction';
export { default as solanaGetAddress } from './solanaGetAddress';
export { default as solanaGetPublicKey } from './solanaGetPublicKey';
export { default as solanaSignTransaction } from './solanaSignTransaction';
diff --git a/packages/connect/src/api/solana/api/solanaComposeTransaction.ts b/packages/connect/src/api/solana/api/solanaComposeTransaction.ts
new file mode 100644
index 00000000000..831c7878b73
--- /dev/null
+++ b/packages/connect/src/api/solana/api/solanaComposeTransaction.ts
@@ -0,0 +1,117 @@
+import { Assert } from '@trezor/schema-utils';
+import { SYSTEM_PROGRAM_PUBLIC_KEY } from '@trezor/blockchain-link-utils/src/solana';
+
+import { AbstractMethod } from '../../../core/AbstractMethod';
+import { SolanaComposeTransaction as SolanaComposeTransactionSchema } from '../../../types/api/solana';
+import { ERRORS } from '../../../constants';
+import { CoinInfo } from '../../../types';
+import { initBlockchain, isBackendSupported } from '../../../backend/BlockchainLink';
+import { getCoinInfo } from '../../../data/coinInfo';
+import {
+ buildTokenTransferTransaction,
+ buildTransferTransaction,
+ dummyPriorityFeesForFeeEstimation,
+ fetchAccountOwnerAndTokenInfoForAddress,
+} from '../solanaUtils';
+
+type SolanaComposeTransactionParams = SolanaComposeTransactionSchema & {
+ coinInfo: CoinInfo;
+};
+
+export default class SolanaComposeTransaction extends AbstractMethod<
+ 'solanaComposeTransaction',
+ SolanaComposeTransactionParams
+> {
+ init() {
+ this.useDevice = false;
+ this.useUi = false;
+
+ const { payload } = this;
+
+ // validate bundle type
+ Assert(SolanaComposeTransactionSchema, payload);
+
+ const coinInfo = getCoinInfo(payload.coin || 'sol');
+ if (!coinInfo) {
+ throw ERRORS.TypedError('Method_UnknownCoin');
+ }
+ // validate backend
+ isBackendSupported(coinInfo);
+
+ this.params = {
+ coinInfo,
+ ...payload,
+ };
+ }
+
+ get info() {
+ return 'Compose Solana transaction';
+ }
+
+ async run() {
+ const backend = await initBlockchain(
+ this.params.coinInfo,
+ this.postMessage,
+ this.params.identity,
+ );
+
+ const [recipientAccountOwner, recipientTokenAccounts] = this.params.token
+ ? await fetchAccountOwnerAndTokenInfoForAddress(
+ backend,
+ this.params.toAddress,
+ this.params.token.mint,
+ this.params.token.program,
+ )
+ : [undefined, undefined];
+
+ const tokenTransferTxAndDestinationAddress =
+ this.params.token && this.params.token.accounts
+ ? await buildTokenTransferTransaction(
+ this.params.fromAddress,
+ this.params.toAddress,
+ recipientAccountOwner || SYSTEM_PROGRAM_PUBLIC_KEY, // toAddressOwner
+ this.params.token.mint,
+ this.params.amount || '0',
+ this.params.token.decimals,
+ this.params.token.accounts,
+ recipientTokenAccounts,
+ this.params.blockHash,
+ this.params.lastValidBlockHeight,
+ this.params.priorityFees || dummyPriorityFeesForFeeEstimation,
+ this.params.token.program,
+ )
+ : undefined;
+
+ if (this.params.token && !tokenTransferTxAndDestinationAddress)
+ throw ERRORS.TypedError('Method_InvalidParameter', 'Token accounts not found');
+
+ const tx = tokenTransferTxAndDestinationAddress
+ ? tokenTransferTxAndDestinationAddress.transaction
+ : await buildTransferTransaction(
+ this.params.fromAddress,
+ this.params.toAddress,
+ this.params.amount,
+ this.params.blockHash,
+ this.params.lastValidBlockHeight,
+ this.params.priorityFees || dummyPriorityFeesForFeeEstimation,
+ );
+
+ const isCreatingAccount =
+ this.params.token &&
+ recipientTokenAccounts === undefined &&
+ // if the recipient account has no owner, it means it's a new account and needs the token account to be created
+ (recipientAccountOwner === SYSTEM_PROGRAM_PUBLIC_KEY || recipientAccountOwner == null);
+ const newTokenAccountProgramName = isCreatingAccount
+ ? this.params.token?.program
+ : undefined;
+
+ return {
+ serializedTx: tx.serialize(),
+ additionalInfo: {
+ isCreatingAccount: !!isCreatingAccount,
+ newTokenAccountProgramName,
+ tokenAccountInfo: tokenTransferTxAndDestinationAddress?.tokenAccountInfo,
+ },
+ };
+ }
+}
diff --git a/packages/connect/src/api/solana/api/solanaSignTransaction.ts b/packages/connect/src/api/solana/api/solanaSignTransaction.ts
index 26a64d3fb5f..4bf818f0944 100644
--- a/packages/connect/src/api/solana/api/solanaSignTransaction.ts
+++ b/packages/connect/src/api/solana/api/solanaSignTransaction.ts
@@ -6,12 +6,12 @@ import { getFirmwareRange } from '../../common/paramsValidator';
import { getMiscNetwork } from '../../../data/coinInfo';
import { validatePath } from '../../../utils/pathUtils';
import { transformAdditionalInfo } from '../additionalInfo';
+import { createTransactionShimFromHex } from '../solanaUtils';
import { SolanaSignTransaction as SolanaSignTransactionSchema } from '../../../types/api/solana';
-export default class SolanaSignTransaction extends AbstractMethod<
- 'solanaSignTransaction',
- PROTO.SolanaSignTx
-> {
+type Params = PROTO.SolanaSignTx & { serialize: boolean };
+
+export default class SolanaSignTransaction extends AbstractMethod<'solanaSignTransaction', Params> {
init() {
this.requiredPermissions = ['read', 'write'];
this.requiredDeviceCapabilities = ['Capability_Solana'];
@@ -33,6 +33,7 @@ export default class SolanaSignTransaction extends AbstractMethod<
address_n: path,
serialized_tx: payload.serializedTx,
additional_info: transformAdditionalInfo(payload.additionalInfo),
+ serialize: !!payload.serialize,
};
}
@@ -42,6 +43,28 @@ export default class SolanaSignTransaction extends AbstractMethod<
async run() {
const cmd = this.device.getCommands();
+
+ if (this.params.serialize) {
+ const tx = await createTransactionShimFromHex(this.params.serialized_tx);
+
+ const { message } = await cmd.typedCall('SolanaSignTx', 'SolanaTxSignature', {
+ ...this.params,
+ serialized_tx: tx.serializeMessage(),
+ });
+
+ const addressCall = await cmd.typedCall('SolanaGetAddress', 'SolanaAddress', {
+ address_n: this.params.address_n,
+ show_display: false,
+ chunkify: false,
+ });
+ const { address } = addressCall.message;
+
+ tx.addSignature(address, message.signature);
+ const signedSerializedTx = tx.serialize();
+
+ return { signature: message.signature, serializedTx: signedSerializedTx };
+ }
+
const { message } = await cmd.typedCall('SolanaSignTx', 'SolanaTxSignature', this.params);
return { signature: message.signature };
diff --git a/suite-common/wallet-utils/src/solanaUtils.ts b/packages/connect/src/api/solana/solanaUtils.ts
similarity index 81%
rename from suite-common/wallet-utils/src/solanaUtils.ts
rename to packages/connect/src/api/solana/solanaUtils.ts
index 84c4eef6558..6edc746ec20 100644
--- a/suite-common/wallet-utils/src/solanaUtils.ts
+++ b/packages/connect/src/api/solana/solanaUtils.ts
@@ -4,6 +4,7 @@ import {
type Blockhash,
type CompilableTransactionMessage,
type TransactionMessage,
+ type Transaction,
} from '@solana/web3.js';
import { BigNumber } from '@trezor/utils/src/bigNumber';
@@ -11,26 +12,37 @@ import type { TokenAccount } from '@trezor/blockchain-link-types';
import { solanaUtils as SolanaBlockchainLinkUtils } from '@trezor/blockchain-link-utils';
import type { TokenProgramName } from '@trezor/blockchain-link-utils/src/solana';
-import { getLamportsFromSol } from './sendFormUtils';
+import { Blockchain } from '../../backend/Blockchain';
const { SYSTEM_PROGRAM_PUBLIC_KEY, tokenProgramsInfo } = SolanaBlockchainLinkUtils;
-const loadSolanaLib = async () => await import('@solana/web3.js');
+const loadSolanaLib = async () =>
+ await import(/* webpackChunkName: "vendor-solana-web3js" */ '@solana/web3.js');
const loadSolanaComputeBudgetProgramLib = async () =>
- await import('@solana-program/compute-budget');
-const loadSolanaSystemProgramLib = async () => await import('@solana-program/system');
+ await import(
+ /* webpackChunkName: "vendor-solana-program-compute-budget" */ '@solana-program/compute-budget'
+ );
+const loadSolanaSystemProgramLib = async () =>
+ await import(/* webpackChunkName: "vendor-solana-program-system" */ '@solana-program/system');
const loadSolanaTokenProgramLib = async (tokenProgramName: TokenProgramName) => {
switch (tokenProgramName) {
case 'spl-token':
- return await import('@solana-program/token');
+ return await import(
+ /* webpackChunkName: "vendor-solana-program-token" */ '@solana-program/token'
+ );
case 'spl-token-2022':
- return await import('@solana-program/token-2022');
+ return await import(
+ /* webpackChunkName: "vendor-solana-program-token-2022" */ '@solana-program/token-2022'
+ );
default:
throw new Error(`Unsupported token program: ${tokenProgramName}`);
}
};
+export const getLamportsFromSol = (amountInSol: string) =>
+ BigInt(new BigNumber(amountInSol).times(10 ** 9).toString());
+
type PriorityFees = { computeUnitPrice: string; computeUnitLimit: string };
export const dummyPriorityFeesForFeeEstimation: PriorityFees = {
@@ -38,10 +50,8 @@ export const dummyPriorityFeesForFeeEstimation: PriorityFees = {
computeUnitLimit: '200000',
};
-async function createTransactionShim(message: CompilableTransactionMessage) {
- const { compileTransaction, getBase16Codec, getTransactionEncoder } = await loadSolanaLib();
-
- let transaction = compileTransaction(message);
+async function createTransactionShimCommon(transaction: Transaction) {
+ const { getBase16Codec, getTransactionEncoder } = await loadSolanaLib();
return {
addSignature(signerPubKey: string, signatureHex: string) {
@@ -66,6 +76,25 @@ async function createTransactionShim(message: CompilableTransactionMessage) {
};
}
+export async function createTransactionShim(message: CompilableTransactionMessage) {
+ const { compileTransaction } = await loadSolanaLib();
+
+ const transaction = compileTransaction(message);
+
+ return createTransactionShimCommon(transaction);
+}
+
+export async function createTransactionShimFromHex(rawTx: string) {
+ const { getBase16Encoder, getCompiledTransactionMessageDecoder, decompileTransactionMessage } =
+ await loadSolanaLib();
+
+ const txByteArray = getBase16Encoder().encode(rawTx);
+ const compiledMessage = getCompiledTransactionMessageDecoder().decode(txByteArray);
+ const message = decompileTransactionMessage(compiledMessage);
+
+ return createTransactionShim(message);
+}
+
const addPriorityFees = async (
message: TMessage,
priorityFees: PriorityFees,
@@ -374,3 +403,35 @@ export const buildTokenTransferTransaction = async (
: undefined,
};
};
+
+export const fetchAccountOwnerAndTokenInfoForAddress = async (
+ blockchain: Blockchain,
+ address: string,
+ mint: string,
+ tokenProgram: TokenProgramName,
+) => {
+ // Fetch data about recipient account owner if this is a token transfer
+ // We need this in order to validate the address and ensure transfers go through
+ let accountOwner: string | undefined;
+ let tokenInfo: TokenAccount | undefined;
+
+ const accountInfoResponse = await blockchain.getAccountInfo({
+ descriptor: address,
+ details: 'tokens',
+ });
+
+ if (accountInfoResponse) {
+ const associatedTokenAccount = await getAssociatedTokenAccountAddress(
+ address,
+ mint,
+ tokenProgram,
+ );
+
+ accountOwner = accountInfoResponse?.misc?.owner;
+ tokenInfo = accountInfoResponse?.tokens
+ ?.find(token => token.contract === mint)
+ ?.accounts?.find(account => associatedTokenAccount.toString() === account.publicKey);
+ }
+
+ return [accountOwner, tokenInfo] as const;
+};
diff --git a/packages/connect/src/factory.ts b/packages/connect/src/factory.ts
index 531c4d6ede6..1f2d837f125 100644
--- a/packages/connect/src/factory.ts
+++ b/packages/connect/src/factory.ts
@@ -175,6 +175,8 @@ export const factory = <
signTransaction: params => call({ ...params, method: 'signTransaction' }),
+ solanaComposeTransaction: params => call({ ...params, method: 'solanaComposeTransaction' }),
+
solanaGetPublicKey: params => call({ ...params, method: 'solanaGetPublicKey' }),
solanaGetAddress: params => call({ ...params, method: 'solanaGetAddress' }),
diff --git a/packages/connect/src/types/api/index.ts b/packages/connect/src/types/api/index.ts
index 9e9ab63f5db..a9c417b11f7 100644
--- a/packages/connect/src/types/api/index.ts
+++ b/packages/connect/src/types/api/index.ts
@@ -73,6 +73,7 @@ import { setTransports } from './setTransports';
import { showDeviceTutorial } from './showDeviceTutorial';
import { signMessage } from './signMessage';
import { signTransaction } from './signTransaction';
+import { solanaComposeTransaction } from './solanaComposeTransaction';
import { solanaGetAddress } from './solanaGetAddress';
import { solanaGetPublicKey } from './solanaGetPublicKey';
import { solanaSignTransaction } from './solanaSignTransaction';
@@ -312,6 +313,9 @@ export interface TrezorConnect {
// https://connect.trezor.io/9/methods/bitcoin/signTransaction/
signTransaction: typeof signTransaction;
+ // https://connect.trezor.io/9/methods/solana/solanaComposeTransaction/
+ solanaComposeTransaction: typeof solanaComposeTransaction;
+
// https://connect.trezor.io/9/methods/solana/solanaGetPublicKey/
solanaGetPublicKey: typeof solanaGetPublicKey;
diff --git a/packages/connect/src/types/api/solana/index.ts b/packages/connect/src/types/api/solana/index.ts
index f1498e78359..9b6decfc51e 100644
--- a/packages/connect/src/types/api/solana/index.ts
+++ b/packages/connect/src/types/api/solana/index.ts
@@ -32,9 +32,49 @@ export const SolanaSignTransaction = Type.Object({
path: Type.Union([Type.String(), Type.Array(Type.Number())]),
serializedTx: Type.String(),
additionalInfo: Type.Optional(SolanaTxAdditionalInfo),
+ serialize: Type.Optional(Type.Boolean()),
});
export type SolanaSignedTransaction = Static;
export const SolanaSignedTransaction = Type.Object({
signature: Type.String(),
+ serializedTx: Type.Optional(Type.String()),
+});
+
+export type SolanaProgramName = Static;
+export const SolanaProgramName = Type.Union([
+ Type.Literal('spl-token'),
+ Type.Literal('spl-token-2022'),
+]);
+
+export type SolanaComposeTransaction = Static;
+export const SolanaComposeTransaction = Type.Object({
+ fromAddress: Type.String(),
+ toAddress: Type.String(),
+ amount: Type.String(),
+ blockHash: Type.String(),
+ lastValidBlockHeight: Type.Number(),
+ priorityFees: Type.Optional(
+ Type.Object({ computeUnitPrice: Type.String(), computeUnitLimit: Type.String() }),
+ ),
+ token: Type.Optional(
+ Type.Object({
+ mint: Type.String(),
+ program: SolanaProgramName,
+ decimals: Type.Number(),
+ accounts: Type.Array(Type.Object({ publicKey: Type.String(), balance: Type.String() })),
+ }),
+ ),
+ coin: Type.Optional(Type.String()),
+ identity: Type.Optional(Type.String()),
+});
+
+export type SolanaComposedTransaction = Static;
+export const SolanaComposedTransaction = Type.Object({
+ serializedTx: Type.String(),
+ additionalInfo: Type.Object({
+ isCreatingAccount: Type.Boolean(),
+ newTokenAccountProgramName: Type.Optional(SolanaProgramName),
+ tokenAccountInfo: Type.Optional(SolanaTxTokenAccountInfo),
+ }),
});
diff --git a/packages/connect/src/types/api/solanaComposeTransaction.ts b/packages/connect/src/types/api/solanaComposeTransaction.ts
new file mode 100644
index 00000000000..c0926eb7aa3
--- /dev/null
+++ b/packages/connect/src/types/api/solanaComposeTransaction.ts
@@ -0,0 +1,6 @@
+import type { Params, Response } from '../params';
+import type { SolanaComposeTransaction, SolanaComposedTransaction } from './solana';
+
+export declare function solanaComposeTransaction(
+ params: Params,
+): Response;
diff --git a/packages/connect/tsconfig.json b/packages/connect/tsconfig.json
index 08c3b1417df..59a02025eda 100644
--- a/packages/connect/tsconfig.json
+++ b/packages/connect/tsconfig.json
@@ -4,6 +4,7 @@
"references": [
{ "path": "../blockchain-link" },
{ "path": "../blockchain-link-types" },
+ { "path": "../blockchain-link-utils" },
{ "path": "../connect-analytics" },
{ "path": "../connect-common" },
{ "path": "../crypto-utils" },
diff --git a/packages/connect/tsconfig.lib.json b/packages/connect/tsconfig.lib.json
index 09960e56f47..9f31e0592a1 100644
--- a/packages/connect/tsconfig.lib.json
+++ b/packages/connect/tsconfig.lib.json
@@ -13,6 +13,9 @@
{
"path": "../blockchain-link-types"
},
+ {
+ "path": "../blockchain-link-utils"
+ },
{
"path": "../connect-analytics"
},
diff --git a/packages/suite/src/hooks/wallet/__fixtures__/useSendForm.ts b/packages/suite/src/hooks/wallet/__fixtures__/useSendForm.ts
index cef7e009ce7..d4bd9f0b76a 100644
--- a/packages/suite/src/hooks/wallet/__fixtures__/useSendForm.ts
+++ b/packages/suite/src/hooks/wallet/__fixtures__/useSendForm.ts
@@ -1219,7 +1219,21 @@ export const setMax = [
},
selectedAccount: SOL_ACCOUNT,
},
+ connect: [
+ undefined,
+ {
+ success: true,
+ payload: {
+ serializedTx: 'serializedABCD',
+ additionalInfo: {
+ isCreatingAccount: false,
+ newTokenAccountProgramName: undefined,
+ },
+ },
+ },
+ ],
finalResult: {
+ solanaComposeTransactionCalls: 1,
estimateFeeCalls: 1,
composedLevels: {
normal: {
diff --git a/packages/suite/src/hooks/wallet/__tests__/useSendForm.test.tsx b/packages/suite/src/hooks/wallet/__tests__/useSendForm.test.tsx
index c6f4893962f..57072beedd0 100644
--- a/packages/suite/src/hooks/wallet/__tests__/useSendForm.test.tsx
+++ b/packages/suite/src/hooks/wallet/__tests__/useSendForm.test.tsx
@@ -103,6 +103,7 @@ const Component = ({ callback }: { callback: TestCallback }) => {
interface Result {
composeTransactionCalls?: number;
composeTransactionParams?: any; // partial @trezor/connect params
+ solanaComposeTransactionCalls?: number;
estimateFeeCalls?: number; // used in ETH
estimateFeeParams?: any; // partial @trezor/connect params
getAccountInfoCalls?: number; // used in XRP
@@ -126,6 +127,11 @@ const actionCallback = (
result.composeTransactionCalls,
);
}
+ if (typeof result.solanaComposeTransactionCalls === 'number') {
+ expect(TrezorConnect.solanaComposeTransaction).toHaveBeenCalledTimes(
+ result.solanaComposeTransactionCalls,
+ );
+ }
if (typeof result.estimateFeeCalls === 'number') {
expect(TrezorConnect.blockchainEstimateFee).toHaveBeenCalledTimes(result.estimateFeeCalls);
}
diff --git a/scripts/ci/connect-test-matrix-generator.js b/scripts/ci/connect-test-matrix-generator.js
index f0373a0aa86..ba817838018 100644
--- a/scripts/ci/connect-test-matrix-generator.js
+++ b/scripts/ci/connect-test-matrix-generator.js
@@ -79,6 +79,12 @@ const groups = {
pattern: 'methods',
includeFilter: 'binanceGetAddress,binanceGetPublicKey,binanceSignTransaction',
},
+ solana: {
+ name: 'solana',
+ pattern: 'methods',
+ includeFilter:
+ 'solanaGetAddress,solanaGetPublicKey,solanaSignTransaction,solanaComposeTransaction',
+ },
};
const firmwares1 = ['1.9.0', '1-latest', '1-main'];
diff --git a/suite-common/wallet-core/src/send/sendFormSolanaThunks.ts b/suite-common/wallet-core/src/send/sendFormSolanaThunks.ts
index c6764b67d3e..9170850fbb2 100644
--- a/suite-common/wallet-core/src/send/sendFormSolanaThunks.ts
+++ b/suite-common/wallet-core/src/send/sendFormSolanaThunks.ts
@@ -1,11 +1,7 @@
import { BigNumber } from '@trezor/utils/src/bigNumber';
import TrezorConnect, { FeeLevel } from '@trezor/connect';
-import type { TokenInfo, TokenAccount } from '@trezor/blockchain-link-types';
-import {
- SYSTEM_PROGRAM_PUBLIC_KEY,
- TokenProgramName,
- tokenStandardToTokenProgramName,
-} from '@trezor/blockchain-link-utils/src/solana';
+import type { TokenInfo } from '@trezor/blockchain-link-types';
+import { tokenStandardToTokenProgramName } from '@trezor/blockchain-link-utils/src/solana';
import {
ExternalOutput,
PrecomposedTransaction,
@@ -18,12 +14,8 @@ import {
calculateMax,
calculateTotal,
formatAmount,
- getExternalComposeOutput,
- buildTransferTransaction,
- buildTokenTransferTransaction,
- getAssociatedTokenAccountAddress,
- dummyPriorityFeesForFeeEstimation,
getAccountIdentity,
+ getExternalComposeOutput,
} from '@suite-common/wallet-utils';
import { getNetworkDisplaySymbol } from '@suite-common/wallet-config';
@@ -114,39 +106,6 @@ const calculate = (
return payloadData;
};
-const fetchAccountOwnerAndTokenInfoForAddress = async (
- address: string,
- symbol: string,
- mint: string,
- tokenProgram: TokenProgramName,
-) => {
- // Fetch data about recipient account owner if this is a token transfer
- // We need this in order to validate the address and ensure transfers go through
- let accountOwner: string | undefined;
- let tokenInfo: TokenAccount | undefined;
-
- const accountInfoResponse = await TrezorConnect.getAccountInfo({
- coin: symbol,
- descriptor: address,
- details: 'tokens',
- });
-
- if (accountInfoResponse.success) {
- const associatedTokenAccount = await getAssociatedTokenAccountAddress(
- address,
- mint,
- tokenProgram,
- );
-
- accountOwner = accountInfoResponse.payload?.misc?.owner;
- tokenInfo = accountInfoResponse.payload?.tokens
- ?.find(token => token.contract === mint)
- ?.accounts?.find(account => associatedTokenAccount.toString() === account.publicKey);
- }
-
- return [accountOwner, tokenInfo] as const;
-};
-
function assertIsSolanaAccount(
account: Account,
): asserts account is Extract {
@@ -171,19 +130,8 @@ export const composeSolanaTransactionFeeLevelsThunk = createThunk<
const { output, decimals, tokenInfo } = composedOutput;
- const { blockhash, blockHeight: lastValidBlockHeight } = selectBlockchainBlockInfoBySymbol(
- getState(),
- account.symbol,
- );
-
- const [recipientAccountOwner, recipientTokenAccount] = tokenInfo
- ? await fetchAccountOwnerAndTokenInfoForAddress(
- formState.outputs[0].address,
- account.symbol,
- tokenInfo.contract,
- tokenStandardToTokenProgramName(tokenInfo.type),
- )
- : [undefined, undefined];
+ const { blockhash: blockHash, blockHeight: lastValidBlockHeight } =
+ selectBlockchainBlockInfoBySymbol(getState(), account.symbol);
// invalid token transfer -- should never happen
if (tokenInfo && !tokenInfo.accounts)
@@ -201,56 +149,43 @@ export const composeSolanaTransactionFeeLevelsThunk = createThunk<
}
}
- const tokenTransferTxAndDestinationAddress =
- tokenInfo && tokenInfo.accounts
- ? await buildTokenTransferTransaction(
- account.descriptor,
- formState.outputs[0].address || account.descriptor,
- recipientAccountOwner || SYSTEM_PROGRAM_PUBLIC_KEY,
- tokenInfo.contract,
- formState.outputs[0].amount || '0',
- tokenInfo.decimals,
- tokenInfo.accounts,
- recipientTokenAccount,
- blockhash,
- lastValidBlockHeight,
- dummyPriorityFeesForFeeEstimation,
- tokenStandardToTokenProgramName(tokenInfo.type),
- )
- : undefined;
-
// To estimate fees on Solana we need to turn a transaction into a message for which fees are estimated.
// Since all the values don't have to be filled in the form at the time of this function call, we use dummy values
// for the estimation, since these values don't affect the final fee.
// The real transaction is constructed in `signTransaction`, this one is used solely for fee estimation and is never submitted.
- const transferTx =
- tokenTransferTxAndDestinationAddress != null
- ? tokenTransferTxAndDestinationAddress.transaction
- : await buildTransferTransaction(
- account.descriptor,
- formState.outputs[0].address || account.descriptor,
- formState.outputs[0].amount || '0',
- blockhash,
- lastValidBlockHeight,
- dummyPriorityFeesForFeeEstimation,
- );
-
- const isCreatingAccount =
- tokenInfo &&
- recipientTokenAccount === undefined &&
- // if the recipient account has no owner, it means it's a new account and needs the token account to be created
- (recipientAccountOwner === SYSTEM_PROGRAM_PUBLIC_KEY || recipientAccountOwner == null);
- const newTokenAccountProgramName = isCreatingAccount
- ? tokenStandardToTokenProgramName(tokenInfo.type)
- : undefined;
+ const transaction = await TrezorConnect.solanaComposeTransaction({
+ fromAddress: account.descriptor,
+ toAddress: formState.outputs[0].address,
+ amount: formState.outputs[0].amount,
+ token: tokenInfo
+ ? {
+ mint: tokenInfo.contract,
+ program: tokenStandardToTokenProgramName(tokenInfo.type),
+ decimals: tokenInfo.decimals,
+ accounts: tokenInfo.accounts ?? [],
+ }
+ : undefined,
+ blockHash,
+ lastValidBlockHeight,
+ coin: account.symbol,
+ identity: getAccountIdentity(account),
+ });
+
+ if (!transaction.success) {
+ return rejectWithValue({
+ error: 'fee-levels-compose-failed',
+ message: transaction.payload.error,
+ });
+ }
const estimatedFee = await TrezorConnect.blockchainEstimateFee({
coin: account.symbol,
request: {
specific: {
- data: transferTx.serialize(),
- isCreatingAccount,
- newTokenAccountProgramName,
+ data: transaction.payload.serializedTx,
+ isCreatingAccount: transaction.payload.additionalInfo.isCreatingAccount,
+ newTokenAccountProgramName:
+ transaction.payload.additionalInfo.newTokenAccountProgramName,
},
},
});
@@ -353,61 +288,40 @@ export const signSolanaSendFormTransactionThunk = createThunk<
}
const { blockHash, blockHeight: lastValidBlockHeight } = blockchainInfo.payload;
- const [recipientAccountOwner, recipientTokenAccounts] = token
- ? await fetchAccountOwnerAndTokenInfoForAddress(
- formState.outputs[0].address,
- selectedAccount.symbol,
- token.contract,
- tokenStandardToTokenProgramName(token.type),
- )
- : [undefined, undefined];
-
if (token && !token.accounts)
rejectWithValue({
error: 'sign-transaction-failed',
message: 'Missing token accounts.',
});
- const tokenTransferTxAndDestinationAddress =
- token && token.accounts
- ? await buildTokenTransferTransaction(
- selectedAccount.descriptor,
- formState.outputs[0].address || selectedAccount.descriptor,
- recipientAccountOwner || SYSTEM_PROGRAM_PUBLIC_KEY,
- token.contract,
- formState.outputs[0].amount || '0',
- token.decimals,
- token.accounts,
- recipientTokenAccounts,
- blockHash,
- lastValidBlockHeight,
- {
- computeUnitPrice: precomposedTransaction.feePerByte,
- computeUnitLimit: precomposedTransaction.feeLimit,
- },
- tokenStandardToTokenProgramName(token.type),
- )
- : undefined;
-
- if (token && !tokenTransferTxAndDestinationAddress)
+ const transaction = await TrezorConnect.solanaComposeTransaction({
+ fromAddress: selectedAccount.descriptor,
+ toAddress: formState.outputs[0].address,
+ amount: formState.outputs[0].amount,
+ token: token
+ ? {
+ mint: token.contract,
+ program: tokenStandardToTokenProgramName(token.type),
+ decimals: token.decimals,
+ accounts: token.accounts ?? [],
+ }
+ : undefined,
+ blockHash,
+ lastValidBlockHeight,
+ priorityFees: {
+ computeUnitPrice: precomposedTransaction.feePerByte,
+ computeUnitLimit: precomposedTransaction.feeLimit,
+ },
+ coin: selectedAccount.symbol,
+ identity: getAccountIdentity(selectedAccount),
+ });
+
+ if (!transaction.success) {
return rejectWithValue({
error: 'sign-transaction-failed',
- message: 'Token transfer address missing.',
+ message: transaction.payload.error,
});
-
- const tx = tokenTransferTxAndDestinationAddress
- ? tokenTransferTxAndDestinationAddress.transaction
- : await buildTransferTransaction(
- selectedAccount.descriptor,
- formState.outputs[0].address,
- formState.outputs[0].amount,
- blockHash,
- lastValidBlockHeight,
- {
- computeUnitPrice: precomposedTransaction.feePerByte,
- computeUnitLimit: precomposedTransaction.feeLimit,
- },
- );
+ }
const response = await TrezorConnect.solanaSignTransaction({
device: {
@@ -417,16 +331,13 @@ export const signSolanaSendFormTransactionThunk = createThunk<
},
useEmptyPassphrase: device.useEmptyPassphrase,
path: selectedAccount.path,
- serializedTx: tx.serializeMessage(),
- additionalInfo:
- tokenTransferTxAndDestinationAddress &&
- tokenTransferTxAndDestinationAddress.tokenAccountInfo
- ? {
- tokenAccountsInfos: [
- tokenTransferTxAndDestinationAddress.tokenAccountInfo,
- ],
- }
- : undefined,
+ serializedTx: transaction.payload.serializedTx,
+ serialize: true,
+ additionalInfo: transaction.payload.additionalInfo.tokenAccountInfo
+ ? {
+ tokenAccountsInfos: [transaction.payload.additionalInfo.tokenAccountInfo],
+ }
+ : undefined,
});
if (!response.success) {
@@ -438,17 +349,6 @@ export const signSolanaSendFormTransactionThunk = createThunk<
});
}
- try {
- tx.addSignature(selectedAccount.descriptor, response.payload.signature);
- const signedSerializedTx = tx.serialize();
-
- return { serializedTx: signedSerializedTx };
- } catch (e) {
- return rejectWithValue({
- error: 'sign-transaction-failed',
- errorCode: e.code,
- message: e.error,
- });
- }
+ return { serializedTx: response.payload.serializedTx! };
},
);
diff --git a/suite-common/wallet-utils/package.json b/suite-common/wallet-utils/package.json
index 45d5b7299b1..569c876ee2f 100644
--- a/suite-common/wallet-utils/package.json
+++ b/suite-common/wallet-utils/package.json
@@ -13,12 +13,6 @@
},
"dependencies": {
"@everstake/wallet-sdk": "^1.0.7",
- "@mobily/ts-belt": "^3.13.1",
- "@solana-program/compute-budget": "^0.6.1",
- "@solana-program/system": "^0.6.2",
- "@solana-program/token": "^0.4.1",
- "@solana-program/token-2022": "^0.3.1",
- "@solana/web3.js": "^2.0.0",
"@suite-common/fiat-services": "workspace:*",
"@suite-common/metadata-types": "workspace:*",
"@suite-common/suite-config": "workspace:*",
diff --git a/suite-common/wallet-utils/src/__tests__/sendFormUtils.test.ts b/suite-common/wallet-utils/src/__tests__/sendFormUtils.test.ts
index 9950a92ee18..e6c1e5e2e1a 100644
--- a/suite-common/wallet-utils/src/__tests__/sendFormUtils.test.ts
+++ b/suite-common/wallet-utils/src/__tests__/sendFormUtils.test.ts
@@ -12,7 +12,6 @@ import {
getExcludedUtxos,
getExternalComposeOutput,
getInputState,
- getLamportsFromSol,
prepareEthereumTransaction,
restoreOrigOutputsOrder,
} from '../sendFormUtils';
@@ -393,9 +392,4 @@ describe('sendForm utils', () => {
expect(excludedUtxos[getUtxoOutpoint(lowAnonymityUtxo)]).toBe('low-anonymity');
expect(excludedUtxos[getUtxoOutpoint(spendableUtxo)]).toBe(undefined);
});
-
- it('getLamportsFromSol', () => {
- expect(getLamportsFromSol('1')).toEqual(1000000000n);
- expect(getLamportsFromSol('0.000000001')).toEqual(1n);
- });
});
diff --git a/suite-common/wallet-utils/src/index.ts b/suite-common/wallet-utils/src/index.ts
index c92c0ebd9b3..e829b76e3bc 100644
--- a/suite-common/wallet-utils/src/index.ts
+++ b/suite-common/wallet-utils/src/index.ts
@@ -13,7 +13,6 @@ export * from './localizePercentage';
export * from './networkUtils';
export * from './sendFormUtils';
export * from './settingsUtils';
-export * from './solanaUtils';
export * from './transactionUtils';
export * from './validationUtils';
export * from './ethereumStakingUtils';
diff --git a/suite-common/wallet-utils/src/sendFormUtils.ts b/suite-common/wallet-utils/src/sendFormUtils.ts
index 9b4e0be2320..4c962eb749c 100644
--- a/suite-common/wallet-utils/src/sendFormUtils.ts
+++ b/suite-common/wallet-utils/src/sendFormUtils.ts
@@ -489,8 +489,3 @@ export const getSendFormDraftKey = (
accountKey: AccountKey,
tokenAddress?: TokenAddress,
): SendFormDraftKey => (tokenAddress ? `${accountKey}-${tokenAddress}` : accountKey);
-
-// SOL Specific
-
-export const getLamportsFromSol = (amountInSol: string) =>
- BigInt(new BigNumber(amountInSol).times(10 ** 9).toString());
diff --git a/yarn.lock b/yarn.lock
index b7a32cb9523..ce31eb923b2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9834,12 +9834,6 @@ __metadata:
resolution: "@suite-common/wallet-utils@workspace:suite-common/wallet-utils"
dependencies:
"@everstake/wallet-sdk": "npm:^1.0.7"
- "@mobily/ts-belt": "npm:^3.13.1"
- "@solana-program/compute-budget": "npm:^0.6.1"
- "@solana-program/system": "npm:^0.6.2"
- "@solana-program/token": "npm:^0.4.1"
- "@solana-program/token-2022": "npm:^0.3.1"
- "@solana/web3.js": "npm:^2.0.0"
"@suite-common/fiat-services": "workspace:*"
"@suite-common/metadata-types": "workspace:*"
"@suite-common/suite-config": "workspace:*"
@@ -12032,10 +12026,17 @@ __metadata:
"@ethereumjs/common": "npm:^4.4.0"
"@ethereumjs/tx": "npm:^5.4.0"
"@fivebinaries/coin-selection": "npm:3.0.0"
+ "@mobily/ts-belt": "npm:^3.13.1"
"@noble/hashes": "npm:^1.6.1"
"@scure/bip39": "npm:^1.5.1"
+ "@solana-program/compute-budget": "npm:^0.6.1"
+ "@solana-program/system": "npm:^0.6.2"
+ "@solana-program/token": "npm:^0.4.1"
+ "@solana-program/token-2022": "npm:^0.3.1"
+ "@solana/web3.js": "npm:^2.0.0"
"@trezor/blockchain-link": "workspace:*"
"@trezor/blockchain-link-types": "workspace:*"
+ "@trezor/blockchain-link-utils": "workspace:*"
"@trezor/connect-analytics": "workspace:*"
"@trezor/connect-common": "workspace:*"
"@trezor/crypto-utils": "workspace:*"