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

[DRAFT] feat: solana basic account actions #3241

Draft
wants to merge 5 commits into
base: feat/solana-initial-structure
Choose a base branch
from
Draft
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
430 changes: 404 additions & 26 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 9 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,11 @@
"@ionic/vue": "^8.2.6",
"@ionic/vue-router": "^8.2.6",
"@ngraveio/bc-ur": "^1.1.13",
"@solana/spl-token": "0.3.9",
"@solana/web3.js": "1.78.5",
"@trapezedev/configure": "^7.0.10",
"@vee-validate/i18n": "^4.13.2",
"@vee-validate/rules": "^4.13.2",
"@vue/vue3-jest": "^27.0.0",
"@walletconnect/core": "^2.14.0",
"@walletconnect/utils": "^2.14.0",
"@walletconnect/web3wallet": "^1.13.0",
Expand All @@ -83,13 +84,13 @@
"dayjs": "^1.11.12",
"detect-browser": "^5.3.0",
"ecpair": "^2.1.0",
"ed25519-hd-key": "^1.3.0",
"lodash-es": "^4.17.21",
"node-polyfill-webpack-plugin": "^3.0.0",
"qr-code-styling": "github:aeternity/qr-code-styling",
"rpc-websockets": "^7.11.0",
"satoshi-bitcoin": "^1.0.5",
"swagger-client": "3.18.4",
"swiper": "^11.0.7",
"ts-jest": "^27.1.5",
"tweetnacl": "^1.0.3",
"uuid": "^9.0.1",
"validator": "^13.12.0",
Expand Down Expand Up @@ -140,6 +141,7 @@
"event-hooks-webpack-plugin": "^2.3.0",
"fs-extra": "^11.2.0",
"identity-obj-proxy": "^3.0.0",
"node-polyfill-webpack-plugin": "^3.0.0",
"sass": "^1.77.8",
"sass-loader": "^16.0.0",
"standard-version": "^9.5.0",
Expand All @@ -157,6 +159,10 @@
"web-ext-types": "^3.2.1",
"xml-js": "^1.6.11"
},
"dependenciesComments": {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CedrikNikita @martinkaintas what do you think about this way of adding our custom comments to installed packages? I feel that it might be helpful as we sometimes add packages that we need to update or change in some conditions.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we would only benefit from the approach, good suggestion.

"//": "Add comments to installed packages here",
"rpc-websockets": "Remove this package after raising `typescript` to 5.x and updating `@solana` to newest versions"
},
"overrides": {
"proxy-agent": "^6.0.0",
"browserify-sign": "^4.2.2"
Expand Down
1 change: 1 addition & 0 deletions src/popup/components/Modals/AccountCreate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export default defineComponent({

case PROTOCOLS.bitcoin:
case PROTOCOLS.ethereum:
case PROTOCOLS.solana:
globalIdx = addRawAccount({
isRestored: false,
protocol,
Expand Down
6 changes: 3 additions & 3 deletions src/protocols/ethereum/libs/EthereumAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,8 @@ export class EthereumAdapter extends BaseProtocolAdapter {
accountIndex: number,
): IHdWalletAccount {
const hdNodeWallet = this.bip32.fromSeed(Buffer.from(seed));
const path = `m/44'/60'/${accountIndex}'/0/0`;
const childWallet = hdNodeWallet.derivePath(path);
const derivationPath = `m/44'/60'/${accountIndex}'/0/0`;
const childWallet = hdNodeWallet.derivePath(derivationPath);

const address = toChecksumAddress(privateKeyToAddress(childWallet.privateKey!).toString());

Expand Down Expand Up @@ -215,7 +215,7 @@ export class EthereumAdapter extends BaseProtocolAdapter {
idx,
...rawAccount,
...hdWallet,
} as IAccount;
};
}

override async discoverLastUsedAccountIndex(seed: Uint8Array): Promise<number> {
Expand Down
16 changes: 16 additions & 0 deletions src/protocols/solana/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import type { IDefaultNetworkTypeData } from '@/types';
import { NETWORK_TYPE_MAINNET, NETWORK_TYPE_TESTNET } from '@/constants';

export const SOL_PROTOCOL_NAME = 'Solana';

export const SOL_CONTRACT_ID = 'solana';
Expand All @@ -6,3 +9,16 @@ export const SOL_COIN_SYMBOL = 'SOL';
export const SOL_COIN_PRECISION = 18; // Amount of decimals

export const SOL_COINGECKO_COIN_ID = 'solana';

export const SOL_NETWORK_DEFAULT_SETTINGS: IDefaultNetworkTypeData = {
[NETWORK_TYPE_MAINNET]: {
nodeUrl: 'https://go.getblock.io/a9583f344e9e41e8a178738568430238', // TODO find working node API
},
[NETWORK_TYPE_TESTNET]: {
nodeUrl: 'https://api.testnet.solana.com', // TODO replace temp values - use our own node
},
};

export const SOL_NETWORK_DEFAULT_ENV_SETTINGS = (process.env.NETWORK === 'Testnet')
? SOL_NETWORK_DEFAULT_SETTINGS[NETWORK_TYPE_TESTNET]
: SOL_NETWORK_DEFAULT_SETTINGS[NETWORK_TYPE_MAINNET];
160 changes: 143 additions & 17 deletions src/protocols/solana/libs/SolanaAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,41 @@
/* eslint-disable class-methods-use-this */

import { PROTOCOLS } from '@/constants';
import {
Connection,
Keypair,
ParsedAccountData,
PublicKey,
} from '@solana/web3.js';
import { derivePath } from 'ed25519-hd-key';
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
import type {
AccountAddress,
AdapterNetworkSettingList,
IAccount,
IAccountRaw,
ICoin,
IFetchTransactionResult,
INetworkProtocolSettings,
IToken,
ITokenBalance,
ITransaction,
ITransactionApiPaginationParams,
ITransferResponse,
MarketData,
NetworkTypeDefault,
} from '@/types';
import { ACCOUNT_TYPES, PROTOCOLS } from '@/constants';
import { excludeFalsy, handleUnknownError, toShiftedBigNumber } from '@/utils';
import { useNetworks } from '@/composables';
import { tg } from '@/popup/plugins/i18n';
import { BaseProtocolAdapter } from '@/protocols/BaseProtocolAdapter';
import {
SOL_COIN_PRECISION,
SOL_COIN_SYMBOL,
SOL_COINGECKO_COIN_ID,
SOL_CONTRACT_ID,
SOL_NETWORK_DEFAULT_ENV_SETTINGS,
SOL_NETWORK_DEFAULT_SETTINGS,
SOL_PROTOCOL_NAME,
} from '@/protocols/solana/config';

Expand All @@ -41,16 +58,24 @@ export class SolanaAdapter extends BaseProtocolAdapter {

override mdwToNodeApproxDelayTime = 0; // TODO

private connection: Connection | null = null;

private networkSettings: AdapterNetworkSettingList = [
// TODO
{
key: 'nodeUrl',
testId: 'url',
defaultValue: SOL_NETWORK_DEFAULT_ENV_SETTINGS.nodeUrl,
getPlaceholder: () => tg('pages.network.networkUrlPlaceholder'),
getLabel: () => tg('pages.network.networkUrlLabel'),
},
];

override getAccountPrefix() {
return ''; // TODO
}

override getAmountPrecision(): number {
return 0; // TODO
return 9; // TODO
}

override getExplorer() {
Expand Down Expand Up @@ -80,12 +105,15 @@ export class SolanaAdapter extends BaseProtocolAdapter {
return this.networkSettings;
}

override getNetworkTypeDefaultValues(): INetworkProtocolSettings {
return {} as any; // TODO
override getNetworkTypeDefaultValues(networkType: NetworkTypeDefault): INetworkProtocolSettings {
return SOL_NETWORK_DEFAULT_SETTINGS[networkType];
}

override async fetchBalance(): Promise<string> {
return '0'; // TODO
override async fetchBalance(address: AccountAddress): Promise<string> {
const connection = this.getConnection();
const publicKey = new PublicKey(address);
const balance = await connection.getBalance(publicKey!);
return toShiftedBigNumber(balance, -this.getAmountPrecision()).toString();
}

override isAccountAddressValid() {
Expand All @@ -104,20 +132,68 @@ export class SolanaAdapter extends BaseProtocolAdapter {
return {} as any; // TODO
}

override resolveAccountRaw(): IAccount | null {
return null; // TODO
override resolveAccountRaw(
rawAccount: IAccountRaw,
idx: number,
globalIdx: number,
seed: Uint8Array,
): IAccount | null {
if (rawAccount.type === ACCOUNT_TYPES.hdWallet) {
const seedBuffer = Buffer.from(seed).toString('hex');
const derivationPath = `m/44'/501'/${idx}'/0'`;
const { key } = derivePath(derivationPath, seedBuffer);
const { publicKey, secretKey } = Keypair.fromSeed(key);
const address = publicKey.toString();

return {
...rawAccount,
globalIdx,
idx,
secretKey,
publicKey: publicKey.toBuffer(),
address,
};
}
return null;
}

override async discoverLastUsedAccountIndex(): Promise<number> {
return 0; // TODO
return -1; // TODO
}

override async fetchAvailableTokens(): Promise<IToken[] | null> {
return []; // TODO
}

override async fetchAccountTokenBalances(): Promise<ITokenBalance[] | null> {
return []; // TODO
override async fetchAccountTokenBalances(
address: AccountAddress,
): Promise<ITokenBalance[] | null> {
try {
const connection = this.getConnection();
const accounts = await connection.getParsedProgramAccounts(
TOKEN_PROGRAM_ID,
{
filters: [
{ dataSize: 165 },
{ memcmp: { offset: 32, bytes: address } },
],
},
);

return (accounts || []).map(({ account, pubkey }) => {
const { tokenAmount } = (account.data as ParsedAccountData)?.parsed?.info || {};
return {
address,
protocol: this.protocol,
contractId: pubkey.toString(),
amount: tokenAmount.amount || 0,
decimals: tokenAmount.decimals || this.coinPrecision,
};
});
} catch (error) {
handleUnknownError(error);
return null;
}
}

override async fetchTokenInfo(): Promise<IToken | undefined> {
Expand All @@ -136,11 +212,52 @@ export class SolanaAdapter extends BaseProtocolAdapter {
return {} as any; // TODO
}

override async fetchAccountTransactions(): Promise<IFetchTransactionResult> {
return {
regularTransactions: [], // TODO
paginationParams: {}, // TODO
};
override async fetchAccountTransactions(
address: AccountAddress,
{ lastTxId }: ITransactionApiPaginationParams = {},
): Promise<IFetchTransactionResult> {
const connection = this.getConnection();
const publicKey = new PublicKey(address);
try {
const rawTransactions = await connection.getSignaturesForAddress(publicKey, {
before: lastTxId,
});
const signatures = rawTransactions.map(({ signature }) => signature);
const transactions = await connection.getParsedTransactions(signatures, {
maxSupportedTransactionVersion: 0,
});

return {
regularTransactions: transactions
.filter(excludeFalsy)
.map(({ blockTime, meta, transaction }) => ({
hash: transaction.signatures[0] as any,
microTime: (blockTime || 0) * 1000,
pending: false,
protocol: this.protocol,
transactionOwner: address,
tx: {
contractId: this.coinContractId, // TODO update for token transfers
type: 'SpendTx', // TODO
amount: +toShiftedBigNumber(
(meta?.preBalances[0] || 0) - (meta?.postBalances[0] || 0),
-this.getAmountPrecision(),
),
fee: meta?.fee || 0,
log: meta?.logMessages || undefined,
},
})),
paginationParams: {
lastTxId: rawTransactions.at(-1)?.signature,
},
};
} catch (error: any) {
handleUnknownError(error);
return {
regularTransactions: [],
paginationParams: {},
};
}
}

override async fetchAccountAssetTransactions(): Promise<IFetchTransactionResult> {
Expand All @@ -161,4 +278,13 @@ export class SolanaAdapter extends BaseProtocolAdapter {
override async waitTransactionMined(): Promise<any> {
return null; // TODO
}

private getConnection() {
const { activeNetwork } = useNetworks();
const { nodeUrl } = activeNetwork.value.protocols.solana;
if (!this.connection || this.connection.rpcEndpoint !== nodeUrl) {
this.connection = new Connection(nodeUrl);
}
return this.connection;
}
}
57 changes: 57 additions & 0 deletions src/protocols/solana/views/AccountDetails.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<template>
<IonPage>
<IonContent class="account-ion-content">
<AccountDetailsBase
v-if="pageDidEnter"
class="account-details"
>
<template #navigation>
<AccountDetailsNavigation
:route-names="[
ROUTE_ACCOUNT_DETAILS,
ROUTE_ACCOUNT_DETAILS_ASSETS,
]"
/>
</template>
</AccountDetailsBase>
</IonContent>
</IonPage>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import { IonContent, IonPage } from '@ionic/vue';
import { PROTOCOL_VIEW_ACCOUNT_DETAILS } from '@/constants';
import { ROUTE_ACCOUNT_DETAILS, ROUTE_ACCOUNT_DETAILS_ASSETS } from '@/popup/router/routeNames';

import AccountDetailsBase from '@/popup/components/AccountDetailsBase.vue';
import AccountDetailsNavigation from '@/popup/components/AccountDetailsNavigation.vue';

export default defineComponent({
name: PROTOCOL_VIEW_ACCOUNT_DETAILS,
components: {
AccountDetailsBase,
IonPage,
IonContent,
AccountDetailsNavigation,
},
props: {
pageDidEnter: Boolean,
},
setup() {
return {
ROUTE_ACCOUNT_DETAILS,
ROUTE_ACCOUNT_DETAILS_ASSETS,
};
},
});
</script>

<style lang="scss" scoped>
@use '@/styles/variables' as *;

.account-ion-content {
overflow: hidden;
background-color: $color-bg-4;
}
</style>
Loading
Loading