Skip to content

Commit

Permalink
feat: add solana to ledger
Browse files Browse the repository at this point in the history
  • Loading branch information
RyukTheCoder authored and RanGojo committed May 13, 2024
1 parent 81a2902 commit 77b6695
Show file tree
Hide file tree
Showing 23 changed files with 613 additions and 133 deletions.
8 changes: 5 additions & 3 deletions wallets/core/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Network } from '@rango-dev/wallets-shared';
import { Options } from './wallet';
import type { Options } from './wallet';
import type { Network } from '@rango-dev/wallets-shared';

export function formatAddressWithNetwork(
address: string,
Expand All @@ -12,7 +12,9 @@ export function accountAddressesWithNetwork(
addresses: string[] | null,
network?: Network | null
) {
if (!addresses) return [];
if (!addresses) {
return [];
}

return addresses.map((address) => {
return formatAddressWithNetwork(address, network);
Expand Down
2 changes: 2 additions & 0 deletions wallets/core/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { State as WalletState } from './wallet';
import type {
Namespace,
Network,
WalletInfo,
WalletType,
Expand Down Expand Up @@ -59,6 +60,7 @@ export type Connect = (options: {
instance: any;
network?: Network;
meta: BlockchainMeta[];
namespaces?: Namespace[];
}) => Promise<ProviderConnectResult | ProviderConnectResult[]>;

export type Disconnect = (options: {
Expand Down
5 changes: 3 additions & 2 deletions wallets/core/src/wallet.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { GetInstanceOptions, WalletActions, WalletConfig } from './types';
import type { Network, WalletType } from '@rango-dev/wallets-shared';
import type { Namespace, Network, WalletType } from '@rango-dev/wallets-shared';
import type { BlockchainMeta } from 'rango-types';

import { getBlockChainNameFromId, Networks } from '@rango-dev/wallets-shared';
Expand Down Expand Up @@ -76,7 +76,7 @@ class Wallet<InstanceType = any> {
return await this.connect(network);
}

async connect(network?: Network) {
async connect(network?: Network, namespaces?: Namespace[]) {
// If it's connecting, nothing do.
if (this.state.connecting) {
throw new Error('Connecting...');
Expand Down Expand Up @@ -165,6 +165,7 @@ class Wallet<InstanceType = any> {
instance,
network: requestedNetwork || undefined,
meta: this.info.supportedBlockchains || [],
namespaces,
});
} catch (e) {
this.resetState();
Expand Down
11 changes: 8 additions & 3 deletions wallets/provider-ledger/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,18 @@
"lint": "eslint \"**/*.{ts,tsx}\" --ignore-path ../../.eslintignore"
},
"dependencies": {
"@ledgerhq/hw-app-eth": "^6.35.7",
"@ledgerhq/hw-transport-webhid": "^6.28.5",
"@ledgerhq/errors": "^6.16.4",
"@ledgerhq/hw-app-eth": "^6.36.0",
"@ledgerhq/hw-app-solana": "^7.1.6",
"@ledgerhq/hw-transport-webhid": "^6.28.6",
"@rango-dev/wallets-shared": "^0.32.1-next.1",
"@rango-dev/signer-solana": "^0.27.1-next.0",
"@solana/web3.js": "^1.91.4",
"bs58": "^5.0.0",
"ethers": "^6.11.1",
"rango-types": "^0.1.59"
},
"publishConfig": {
"access": "public"
}
}
}
76 changes: 64 additions & 12 deletions wallets/provider-ledger/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,59 @@
import type Transport from '@ledgerhq/hw-transport';

import { getAltStatusMessage } from '@ledgerhq/errors';
import { Networks } from '@rango-dev/wallets-shared';
import bs58 from 'bs58';

const ETHEREUM_CHAIN_ID = '0x1';

export const ETH_BIP32_PATH = "44'/60'/0'/0/0";
export const SOLANA_BIP32_PATH = "44'/501'/0'";

export const HEXADECIMAL_BASE = 16;

const ledgerErrorMessages: { [statusCode: number | string]: string } = {
21781: 'The device is locked',
25871: 'Related application is not ready on your device',
27013: 'Action denied by user',
INSUFFICIENT_FUNDS: 'Insufficient funds for transaction',
const ledgerFrequentErrorMessages: { [statusCode: number]: string } = {
0x5515: 'The device is locked',
0x650f: 'Related application is not ready on your device',
0x6985: 'Action denied by user',
};

function getLedgerErrorMessage(statusCode: number): string {
if (ledgerFrequentErrorMessages[statusCode]) {
return ledgerFrequentErrorMessages[statusCode];
} else if (getAltStatusMessage(statusCode)) {
return getAltStatusMessage(statusCode) as string;
}

return `Ledger device unknown error 0x${statusCode.toString(
HEXADECIMAL_BASE
)}`; // Hexadecimal numbers are more commonly recognized and utilized for representing ledger error codes
}

export function getLedgerError(error: any) {
if (error?.statusCode) {
return new Error(getLedgerErrorMessage(error.statusCode));
}

if (error?.code === 'INSUFFICIENT_FUNDS') {
return new Error('Insufficient funds for transaction');
}
return error;
}

export function getLedgerInstance() {
/*
* Instances have a required property which is `chainId` and is using in swap execution.
* Here we are setting it as Ethereum always since we are supporting only eth for now.
*/
return { chainId: ETHEREUM_CHAIN_ID };
const instances = new Map();

instances.set(Networks.ETHEREUM, { chainId: ETHEREUM_CHAIN_ID });
instances.set(Networks.SOLANA, { chainId: Networks.SOLANA });

return instances;
}

export async function getLedgerAccounts(): Promise<{
export async function getEthereumAccounts(): Promise<{
accounts: string[];
chainId: string;
}> {
Expand All @@ -44,13 +78,31 @@ export async function getLedgerAccounts(): Promise<{
}
}

export function getLedgerError(error: any) {
const errorCode = error?.statusCode || error?.code; // ledger error || broadcast error
export async function getSolanaAccounts(): Promise<{
accounts: string[];
chainId: string;
}> {
try {
const transport = await transportConnect();

const solana = new (await import('@ledgerhq/hw-app-solana')).default(
transport
);

const accounts: string[] = [];

const result = await solana.getAddress(SOLANA_BIP32_PATH);
accounts.push(bs58.encode(result.address));

if (errorCode && !!ledgerErrorMessages[errorCode]) {
return new Error(ledgerErrorMessages[errorCode]);
return {
accounts: accounts,
chainId: Networks.SOLANA,
};
} catch (error: any) {
throw getLedgerError(error);
} finally {
await transportDisconnect();
}
return error;
}

let transportConnection: Transport | null = null;
Expand Down
37 changes: 26 additions & 11 deletions wallets/provider-ledger/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,28 @@ import type {
Disconnect,
WalletInfo,
} from '@rango-dev/wallets-shared';
import type { BlockchainMeta, SignerFactory } from 'rango-types';

import { Networks, WalletTypes } from '@rango-dev/wallets-shared';
import { Namespace, Networks, WalletTypes } from '@rango-dev/wallets-shared';
import { type BlockchainMeta, type SignerFactory } from 'rango-types';

import {
getLedgerAccounts,
getEthereumAccounts,
getLedgerInstance,
getSolanaAccounts,
transportDisconnect,
} from './helpers';
import signer from './signer';

const WALLET = WalletTypes.LEDGER;

export const config = {
type: WALLET,
type: WalletTypes.LEDGER,
};

export const getInstance = getLedgerInstance;
export const connect: Connect = async () => {
const ledgerAccounts = await getLedgerAccounts();

return ledgerAccounts;
export const connect: Connect = async ({ namespaces }) => {
if (namespaces?.includes(Namespace.Solana)) {
return await getSolanaAccounts();
}
return await getEthereumAccounts();
};

export const disconnect: Disconnect = async () => {
Expand All @@ -36,9 +36,22 @@ export const getSigners: (provider: any) => SignerFactory = signer;
export const getWalletInfo: (allBlockChains: BlockchainMeta[]) => WalletInfo = (
allBlockChains
) => {
const supportedChains: BlockchainMeta[] = [];

const ethereumBlockchain = allBlockChains.find(
(chain) => chain.name === Networks.ETHEREUM
);
if (ethereumBlockchain) {
supportedChains.push(ethereumBlockchain);
}

const solanaBlockchain = allBlockChains.find(
(chain) => chain.name === Networks.SOLANA
);
if (solanaBlockchain) {
supportedChains.push(solanaBlockchain);
}

return {
name: 'Ledger',
img: 'https://raw.githubusercontent.com/rango-exchange/assets/main/wallets/ledger/icon.svg',
Expand All @@ -47,6 +60,8 @@ export const getWalletInfo: (allBlockChains: BlockchainMeta[]) => WalletInfo = (
'https://support.ledger.com/hc/en-us/articles/4404389606417-Download-and-install-Ledger-Live?docs=true',
},
color: 'black',
supportedChains: ethereumBlockchain ? [ethereumBlockchain] : [],
supportedChains,
namespaces: [Namespace.Evm, Namespace.Solana],
singleNamespace: true,
};
};
2 changes: 2 additions & 0 deletions wallets/provider-ledger/src/signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import type { SignerFactory } from 'rango-types';
import { DefaultSignerFactory, TransactionType as TxType } from 'rango-types';

import { EthereumSigner } from './signers/ethereum';
import { SolanaSigner } from './signers/solana';

export default function getSigners(): SignerFactory {
const signers = new DefaultSignerFactory();
signers.registerSigner(TxType.EVM, new EthereumSigner());
signers.registerSigner(TxType.SOLANA, new SolanaSigner());
return signers;
}
74 changes: 74 additions & 0 deletions wallets/provider-ledger/src/signers/solana.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { SolanaWeb3Signer } from '@rango-dev/signer-solana';
import type { Transaction, VersionedTransaction } from '@solana/web3.js';
import type { GenericSigner, SolanaTransaction } from 'rango-types';

import { generalSolanaTransactionExecutor } from '@rango-dev/signer-solana';
import { PublicKey } from '@solana/web3.js';
import { SignerError } from 'rango-types';

import {
getLedgerError,
SOLANA_BIP32_PATH,
transportConnect,
transportDisconnect,
} from '../helpers';

export function isVersionedTransaction(
transaction: Transaction | VersionedTransaction
): transaction is VersionedTransaction {
return 'version' in transaction;
}

export class SolanaSigner implements GenericSigner<SolanaTransaction> {
async signMessage(): Promise<string> {
throw SignerError.UnimplementedError('signMessage');
}

async signAndSendTx(tx: SolanaTransaction): Promise<{ hash: string }> {
try {
const DefaultSolanaSigner: SolanaWeb3Signer = async (
solanaWeb3Transaction: Transaction | VersionedTransaction
) => {
const transport = await transportConnect();
const solana = new (await import('@ledgerhq/hw-app-solana')).default(
transport
);

let signResult;
if (isVersionedTransaction(solanaWeb3Transaction)) {
signResult = await solana.signTransaction(
SOLANA_BIP32_PATH,
solanaWeb3Transaction.message.serialize() as Buffer
);
} else {
signResult = await solana.signTransaction(
SOLANA_BIP32_PATH,
solanaWeb3Transaction.serialize()
);
}

const addressResult = await solana.getAddress(SOLANA_BIP32_PATH);

const publicKey = new PublicKey(addressResult.address);

solanaWeb3Transaction.addSignature(
publicKey,
Buffer.from(signResult.signature)
);

const serializedTx = solanaWeb3Transaction.serialize();

return serializedTx;
};
const hash = await generalSolanaTransactionExecutor(
tx,
DefaultSolanaSigner
);
return { hash };
} catch (error) {
throw getLedgerError(error);
} finally {
await transportDisconnect();
}
}
}
4 changes: 2 additions & 2 deletions wallets/react/src/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ function Provider(props: ProviderProps) {
// Final API we put in context and it will be available to use for users.
// eslint-disable-next-line react/jsx-no-constructed-context-values
const api: ProviderContext = {
async connect(type, network) {
async connect(type, network, namespaces) {
const wallet = wallets.get(type);
if (!wallet) {
throw new Error(`You should add ${type} to provider first.`);
}
const walletInstance = getWalletInstance(wallet);
const result = await walletInstance.connect(network);
const result = await walletInstance.connect(network, namespaces);
if (props.autoConnect) {
void tryPersistWallet({
type,
Expand Down
7 changes: 6 additions & 1 deletion wallets/react/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
State as WalletState,
} from '@rango-dev/wallets-core';
import type {
Namespace,
Network,
WalletInfo,
WalletType,
Expand All @@ -23,7 +24,11 @@ export type ConnectResult = {
export type Providers = { [type in WalletType]?: any };

export type ProviderContext = {
connect(type: WalletType, network?: Network): Promise<ConnectResult>;
connect(
type: WalletType,
network?: Network,
namespaces?: Namespace[]
): Promise<ConnectResult>;
disconnect(type: WalletType): Promise<void>;
disconnectAll(): Promise<PromiseSettledResult<any>[]>;
state(type: WalletType): WalletState;
Expand Down
2 changes: 1 addition & 1 deletion wallets/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ For better user experience, wallet provider tries to connect to a wallet only wh
| Halo | - | - | &cross; | https://halo.social/ |
| Keplr | Cosmos | - | &cross; | https://www.keplr.app/ |
| Leap Cosmos | Cosmos | Cosmos | &cross; | https://www.leapwallet.io/cosmos |
| Ledger | Ethereum |- | &cross; | https://www.ledger.com/ |
| Ledger | Ethereum,Solana |- | &cross; | https://www.ledger.com/ |
| Math Wallet | BTC,EVM,Solana,Aptos,Tron,Polkadot,Cosmos | BTC,Aptos,Tron,Polkadot,Cosmos | &check; | https://mathwallet.org/en-us/ |
| Metamask | EVM | - | &check; | - |
| OKX | EVM,Solana,Cosmos | Cosmos | &check; | https://www.okx.com/web3 |
Expand Down
Loading

0 comments on commit 77b6695

Please sign in to comment.