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 000000000000..b1c63bbfe922
--- /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/e2e/__fixtures__/index.ts b/packages/connect/e2e/__fixtures__/index.ts
index f9df6ba282ad..66a8af65409e 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 000000000000..d570791679ac
--- /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/src/api/solana/api/index.ts b/packages/connect/src/api/solana/api/index.ts
index 163de9125502..281e228608dd 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 000000000000..831c7878b730
--- /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/solanaUtils.ts b/packages/connect/src/api/solana/solanaUtils.ts
index b20d214e3d3a..fdf0ea4b17fb 100644
--- a/packages/connect/src/api/solana/solanaUtils.ts
+++ b/packages/connect/src/api/solana/solanaUtils.ts
@@ -82,12 +82,14 @@ export async function createTransactionShim(message: CompilableTransactionMessag
}
export async function createTransactionShimFromHex(rawTx: string) {
- const { getBase16Encoder, getTransactionDecoder } = await loadSolanaLib();
+ const { getBase16Encoder, getCompiledTransactionMessageDecoder, decompileTransactionMessage } =
+ await loadSolanaLib();
const txByteArray = getBase16Encoder().encode(rawTx);
- const transaction = getTransactionDecoder().decode(txByteArray);
+ const compiledMessage = getCompiledTransactionMessageDecoder().decode(txByteArray);
+ const message = decompileTransactionMessage(compiledMessage);
- return createTransactionShimCommon(transaction);
+ return createTransactionShim(message);
}
const addPriorityFees = async (
diff --git a/packages/connect/src/factory.ts b/packages/connect/src/factory.ts
index 30083916adaf..29ed52a538f1 100644
--- a/packages/connect/src/factory.ts
+++ b/packages/connect/src/factory.ts
@@ -172,6 +172,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 12900e124d63..cba08692500d 100644
--- a/packages/connect/src/types/api/index.ts
+++ b/packages/connect/src/types/api/index.ts
@@ -72,6 +72,7 @@ import { setProxy } from './setProxy';
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';
@@ -308,6 +309,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 f1498e783599..9b6decfc51ee 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 000000000000..c0926eb7aa3a
--- /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;