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(connnect): solanaComposeTransaction method #16208

Merged
merged 5 commits into from
Jan 14, 2025
Merged
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
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
}
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
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',
mroz22 marked this conversation as resolved.
Show resolved Hide resolved
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',
},
},
},
},
],
};
3 changes: 3 additions & 0 deletions packages/connect/e2e/__wscache__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`];
}
Expand Down
7 changes: 7 additions & 0 deletions packages/connect/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
mroz22 marked this conversation as resolved.
Show resolved Hide resolved
"@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:*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
buildTokenTransferInstruction,
buildTokenTransferTransaction,
getMinimumRequiredTokenAccountsForTransfer,
getLamportsFromSol,
} from '../solanaUtils';

describe('solana utils', () => {
Expand Down Expand Up @@ -83,4 +84,9 @@ describe('solana utils', () => {
});
});
});

it('getLamportsFromSol', () => {
expect(getLamportsFromSol('1')).toEqual(1000000000n);
expect(getLamportsFromSol('0.000000001')).toEqual(1n);
});
});
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,
},
};
}
}
Loading
Loading