Skip to content

Commit

Permalink
feat(connect): solanaComposeTransaction method
Browse files Browse the repository at this point in the history
  • Loading branch information
martykan committed Jan 8, 2025
1 parent f819fc8 commit 4c88d90
Show file tree
Hide file tree
Showing 10 changed files with 369 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -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';

<ApiPlayground
options={[
{
title: 'Advanced schema',
method: 'solanaComposeTransaction',
schema: SolanaComposeTransaction,
},
]}
/>

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

<CommonParamsLink />

#### SolanaComposeTransaction

<ParamsTable schema={SolanaComposeTransaction} descriptions={paramDescriptions} />

### 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
}
}
```
1 change: 1 addition & 0 deletions packages/connect/e2e/__fixtures__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
97 changes: 97 additions & 0 deletions packages/connect/e2e/__fixtures__/solanaComposeTransaction.ts
Original file line number Diff line number Diff line change
@@ -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',
},
},
},
},
],
};
1 change: 1 addition & 0 deletions packages/connect/src/api/solana/api/index.ts
Original file line number Diff line number Diff line change
@@ -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';
117 changes: 117 additions & 0 deletions packages/connect/src/api/solana/api/solanaComposeTransaction.ts
Original file line number Diff line number Diff line change
@@ -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,
},
};
}
}
8 changes: 5 additions & 3 deletions packages/connect/src/api/solana/solanaUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <TMessage extends TransactionMessage>(
Expand Down
2 changes: 2 additions & 0 deletions packages/connect/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' }),
Expand Down
4 changes: 4 additions & 0 deletions packages/connect/src/types/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;

Expand Down
Loading

0 comments on commit 4c88d90

Please sign in to comment.