Skip to content

Commit cff588f

Browse files
Solana ledger (#1363)
* solana ledger * create solana ledger wallet * sign transaction * Update search-ledger.tsx Co-authored-by: Andrei Novac <32679726+NovacAndrei@users.noreply.github.com>
1 parent 3ef51c2 commit cff588f

File tree

7 files changed

+229
-12
lines changed

7 files changed

+229
-12
lines changed

src/core/blockchain/near/account.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import { Blockchain, ChainIdType, IBlockchainAccountUtils } from '../types';
33
import { BigNumber } from 'bignumber.js';
44
import { config } from './config';
55
import { convert } from '../common/account';
6-
7-
import bs58 from 'bs58';
6+
import { encode as bs58Encode, decode as bs58Decode } from 'bs58';
87
import * as nacl from 'tweetnacl';
98
import { HDKeyEd25519 } from '../../wallet/hd-wallet/hd-key/hd-key-ed25519';
109
import { generateTokensConfig } from '../../../redux/tokens/static-selectors';
@@ -20,7 +19,7 @@ export class NearAccountUtils implements IBlockchainAccountUtils {
2019

2120
public getPrivateKeyFromDerived(derivedKey: HDKeyEd25519): string {
2221
const keyPair = nacl.sign.keyPair.fromSeed(derivedKey.key);
23-
return bs58.encode(Buffer.from(keyPair.secretKey));
22+
return bs58Encode(Buffer.from(keyPair.secretKey));
2423
}
2524

2625
public isValidChecksumAddress(address: string): boolean {
@@ -36,18 +35,18 @@ export class NearAccountUtils implements IBlockchainAccountUtils {
3635
}
3736

3837
public privateToPublic(privateKey: string): string {
39-
const keyPair = nacl.sign.keyPair.fromSecretKey(bs58.decode(privateKey));
40-
return 'ed25519:' + bs58.encode(Buffer.from(keyPair.publicKey));
38+
const keyPair = nacl.sign.keyPair.fromSecretKey(bs58Decode(privateKey));
39+
return 'ed25519:' + bs58Encode(Buffer.from(keyPair.publicKey));
4140
}
4241

4342
public privateToAddress(privateKey: string): string {
4443
return this.privateToPublic(privateKey);
4544
}
4645

4746
public getAccountFromPrivateKey(privateKey: string, index: number): IAccountState {
48-
const keyPair = nacl.sign.keyPair.fromSecretKey(bs58.decode(privateKey));
49-
const pk = bs58.encode(Buffer.from(keyPair.publicKey));
50-
const address = Buffer.from(bs58.decode(pk)).toString('hex');
47+
const keyPair = nacl.sign.keyPair.fromSecretKey(bs58Decode(privateKey));
48+
const pk = bs58Encode(Buffer.from(keyPair.publicKey));
49+
const address = Buffer.from(bs58Decode(pk)).toString('hex');
5150

5251
return {
5352
index: 0,

src/core/blockchain/solana/contracts/base-contract.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Solana } from '..';
66
import { IStakeAccountFormat } from '../types';
77
import { PosBasicActionType } from '../../types/token';
88
import CryptoJS from 'crypto-js';
9-
import bs58 from 'bs58';
9+
import { encode as bs58Encode, decode as bs58Decode } from 'bs58';
1010
import BigNumber from 'bignumber.js';
1111

1212
export const stakeProgramId = 'Stake11111111111111111111111111111111111111';
@@ -55,11 +55,11 @@ function hash(h, v) {
5555

5656
export const generateStakeAccount = (address: string, index: number): string => {
5757
const sha256 = CryptoJS.algo.SHA256.create();
58-
hash(sha256, bs58.decode(address));
58+
hash(sha256, bs58Decode(address));
5959
hash(sha256, `stake:${index}`);
60-
hash(sha256, bs58.decode(stakeProgramId));
60+
hash(sha256, bs58Decode(stakeProgramId));
6161
const pub = Buffer.from(sha256.finalize().toString(), 'hex');
62-
return bs58.encode(pub);
62+
return bs58Encode(pub);
6363
};
6464

6565
const stakeAccountWithExactAmount = (

src/core/wallet/hw-wallet/ledger/apps-factory.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Cosmos } from './apps/cosmos';
55
import { IHardwareWalletApp } from './types';
66
import { Celo } from './apps/celo';
77
import { Near } from './apps/near';
8+
import { Solana } from './apps/solana';
89

910
export class AppFactory {
1011
public static async get(
@@ -22,6 +23,8 @@ export class AppFactory {
2223
return new Celo(transport);
2324
case Blockchain.NEAR:
2425
return new Near(transport);
26+
case Blockchain.SOLANA:
27+
return new Solana(transport);
2528
default:
2629
return Promise.reject();
2730
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
const bs58 = require('bs58');
2+
3+
const INS_GET_PUBKEY = 0x05;
4+
const INS_GET_APP_CONFIGURATION = 0x04;
5+
const INS_SIGN_MESSAGE = 0x06;
6+
7+
const P1_NON_CONFIRM = 0x00;
8+
const P1_CONFIRM = 0x01;
9+
10+
const P2_EXTEND = 0x01;
11+
const P2_MORE = 0x02;
12+
13+
const MAX_PAYLOAD = 255;
14+
15+
const LEDGER_CLA = 0xe0;
16+
17+
const STATUS_OK = 0x9000;
18+
const BIP32_HARDENED_BIT = (1 << 31) >>> 0;
19+
20+
class Solana {
21+
constructor(transport) {
22+
this.transport = transport;
23+
}
24+
/*
25+
* Helper for chunked send of large payloads
26+
*/
27+
async solana_send(transport, instruction, p1, payload) {
28+
var p2 = 0;
29+
var payload_offset = 0;
30+
31+
if (payload.length > MAX_PAYLOAD) {
32+
while (payload.length - payload_offset > MAX_PAYLOAD) {
33+
const buf = payload.slice(payload_offset, payload_offset + MAX_PAYLOAD);
34+
payload_offset += MAX_PAYLOAD;
35+
const reply = await transport.send(LEDGER_CLA, instruction, p1, p2 | P2_MORE, buf);
36+
if (reply.length != 2) {
37+
throw new TransportError(
38+
'solana_send: Received unexpected reply payload',
39+
'UnexpectedReplyPayload'
40+
);
41+
}
42+
p2 |= P2_EXTEND;
43+
}
44+
}
45+
46+
const buf = payload.slice(payload_offset);
47+
const reply = await transport.send(LEDGER_CLA, instruction, p1, p2, buf);
48+
49+
return reply.slice(0, reply.length - 2);
50+
}
51+
52+
_harden(n) {
53+
return (n | BIP32_HARDENED_BIT) >>> 0;
54+
}
55+
56+
solana_derivation_path(account, change) {
57+
var length;
58+
if (typeof account === 'number') {
59+
if (typeof change === 'number') {
60+
length = 4;
61+
} else {
62+
length = 3;
63+
}
64+
} else {
65+
length = 2;
66+
}
67+
68+
var derivation_path = Buffer.alloc(1 + length * 4);
69+
var offset = 0;
70+
offset = derivation_path.writeUInt8(length, offset);
71+
offset = derivation_path.writeUInt32BE(this._harden(44), offset); // Using BIP44
72+
offset = derivation_path.writeUInt32BE(this._harden(501), offset); // Solana's BIP44 path
73+
74+
if (length > 2) {
75+
offset = derivation_path.writeUInt32BE(this._harden(account), offset);
76+
if (length == 4) {
77+
offset = derivation_path.writeUInt32BE(this._harden(change), offset);
78+
}
79+
}
80+
81+
return derivation_path;
82+
}
83+
84+
solana_ledger_get_pubkey() {
85+
return this.solana_send(
86+
this.transport,
87+
INS_GET_PUBKEY,
88+
P1_CONFIRM,
89+
this.solana_derivation_path()
90+
);
91+
}
92+
93+
async solana_ledger_get_version() {
94+
return this.solana_send(
95+
this.transport,
96+
INS_GET_APP_CONFIGURATION,
97+
P1_NON_CONFIRM,
98+
Buffer.from('')
99+
).then(info => {
100+
return `${info[2]}.${info[3]}.${info[4]}`;
101+
});
102+
}
103+
104+
solana_ledger_sign_transaction(transaction) {
105+
let msg_bytes;
106+
try {
107+
msg_bytes = transaction.serializeMessage();
108+
// XXX: Ledger app only supports a single derivation_path per call ATM
109+
var num_paths = Buffer.alloc(1);
110+
111+
num_paths.writeUInt8(1);
112+
113+
const payload = Buffer.concat([num_paths, this.solana_derivation_path(), msg_bytes]);
114+
115+
return this.solana_send(this.transport, INS_SIGN_MESSAGE, P1_CONFIRM, payload);
116+
} catch (e) {
117+
return;
118+
}
119+
}
120+
}
121+
122+
exports.default = Solana;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import SolanaApp from './solana-interface';
2+
import { IHardwareWalletApp } from '../types';
3+
import { IBlockchainTransaction } from '../../../../blockchain/types';
4+
import { StakeProgram } from '@solana/web3.js/src/stake-program';
5+
import { Transaction } from '@solana/web3.js/src/transaction';
6+
import { SolanaTransactionInstructionType } from '../../../../blockchain/solana/types';
7+
import { PublicKey } from '@solana/web3.js/src/publickey';
8+
import bs58 from 'bs58';
9+
10+
export class Solana implements IHardwareWalletApp {
11+
private app = null;
12+
constructor(transport) {
13+
this.app = new SolanaApp.default(transport);
14+
}
15+
16+
/**
17+
* @param {number} index index of account
18+
* @param {number} derivationIndex index of derivation for an account
19+
* @param {number} path derivation path, values accepted: live, legacy
20+
*/
21+
public async getAddress(index: number, derivationIndex: number = 0, path: string) {
22+
const pubkeyBytes = await this.app.solana_ledger_get_pubkey();
23+
const pubkey = bs58.encode(pubkeyBytes);
24+
return {
25+
address: pubkey,
26+
publicKey: pubkey
27+
};
28+
}
29+
30+
public signTransaction = async (
31+
index: number,
32+
derivationIndex: number = 0,
33+
path: string,
34+
tx: IBlockchainTransaction
35+
): Promise<any> => {
36+
// const client = Solana.getClient(tx.chainId) as SolanaClient;
37+
38+
let transaction;
39+
40+
switch (tx.additionalInfo.type) {
41+
case SolanaTransactionInstructionType.CREATE_ACCOUNT_WITH_SEED:
42+
transaction = StakeProgram.createAccountWithSeed(tx.additionalInfo.instructions[0]);
43+
break;
44+
case SolanaTransactionInstructionType.DELEGATE_STAKE:
45+
transaction = StakeProgram.delegate(tx.additionalInfo.instructions[0]);
46+
break;
47+
case SolanaTransactionInstructionType.UNSTAKE:
48+
transaction = StakeProgram.deactivate(tx.additionalInfo.instructions[0]);
49+
break;
50+
case SolanaTransactionInstructionType.SPLIT_STAKE:
51+
transaction = tx.additionalInfo.splitTransaction;
52+
break;
53+
case SolanaTransactionInstructionType.WITHDRAW:
54+
transaction = StakeProgram.withdraw(tx.additionalInfo.instructions[0]);
55+
break;
56+
57+
case SolanaTransactionInstructionType.TRANSFER:
58+
transaction = new Transaction();
59+
transaction.add(tx.additionalInfo.instructions[0]);
60+
break;
61+
}
62+
63+
const addressPublicKey = new PublicKey(tx.address);
64+
transaction.recentBlockhash = tx.additionalInfo.currentBlockHash;
65+
transaction.feePayer = addressPublicKey;
66+
67+
const sigBytes = await this.app.solana_ledger_sign_transaction(transaction);
68+
69+
transaction.addSignature(addressPublicKey, sigBytes);
70+
return transaction.serialize();
71+
};
72+
73+
public async getInfo() {
74+
return this.app.solana_ledger_get_version();
75+
}
76+
}

src/core/wallet/hw-wallet/ledger/config.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ export const ledgerConfigInternal: ILedgerTransportConfig = {
5353
NANO_X: {
5454
connectionTypes: [HWConnection.BLE, HWConnection.USB]
5555
}
56+
},
57+
SOLANA: {
58+
NANO_S: nanosConnectionConfig,
59+
NANO_X: {
60+
connectionTypes: [HWConnection.BLE, HWConnection.USB]
61+
}
5662
}
5763
},
5864
ios: {
@@ -67,6 +73,9 @@ export const ledgerConfigInternal: ILedgerTransportConfig = {
6773
},
6874
CELO: {
6975
NANO_X: nanoXConnectionConfigBLE
76+
},
77+
SOLANA: {
78+
NANO_X: nanoXConnectionConfigBLE
7079
}
7180
},
7281
web: {
@@ -97,6 +106,11 @@ export const ledgerSetupConfig = async (): Promise<ILedgerTransportConfig> => {
97106
delete ledgerConfigInternal.ios.CELO;
98107
}
99108

109+
if (!isFeatureActive(RemoteFeature.SOLANA)) {
110+
delete ledgerConfigInternal.android.SOLANA;
111+
delete ledgerConfigInternal.ios.SOLANA;
112+
}
113+
100114
return resolve(ledgerConfigInternal);
101115
});
102116
};

src/core/wallet/hw-wallet/ledger/ledger-wallet.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export class LedgerWallet implements IWallet {
102102
): Promise<any> {
103103
try {
104104
await this.onAppOpened(blockchain);
105+
105106
const transport = await this.getTransport();
106107
const app = await AppFactory.get(blockchain, transport);
107108

@@ -154,7 +155,9 @@ export class LedgerWallet implements IWallet {
154155

155156
// detect if app is opened
156157
const appOpenedTimeout = setTimeout(() => cb(LedgerSignEvent.OPEN_APP), 2000);
158+
157159
await this.onAppOpened(blockchain);
160+
158161
terminateIfNeeded();
159162
clearTimeout(appOpenedTimeout);
160163
cb(LedgerSignEvent.APP_OPENED);

0 commit comments

Comments
 (0)