Ledger support (#54)
* feat: add https for local development * feat: add support for ledger * feat: add ledger support * feat: add ledger support * feat: add ledger support * feat: add ledger support * feat: ledger support * feat: add leadger support * feat: ledger support * Some updates * Reset back to master * Revert login page logic * Fix various things * working * some updates * Fix unlock error silent failing * dapp signatures working * touch up Co-authored-by: Bartosz Lipinski <264380+bartosz-lipinski@users.noreply.github.com>
This commit is contained in:
parent
32358afde2
commit
f22090c503
|
@ -0,0 +1,27 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIEozCCAwugAwIBAgIRALVgQ4iLzJxtipCRuRZ9FWMwDQYJKoZIhvcNAQELBQAw
|
||||
gbkxHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTFHMEUGA1UECww+YmFy
|
||||
dG9zei5saXBpbnNraUBCYXJ0b3N6cy1NYWNCb29rLVByby5sb2NhbCAoQmFydG9z
|
||||
eiBMaXBpbnNraSkxTjBMBgNVBAMMRW1rY2VydCBiYXJ0b3N6LmxpcGluc2tpQEJh
|
||||
cnRvc3pzLU1hY0Jvb2stUHJvLmxvY2FsIChCYXJ0b3N6IExpcGluc2tpKTAeFw0x
|
||||
OTA2MDEwMDAwMDBaFw0zMDEwMTgwMjU2MzhaMHIxJzAlBgNVBAoTHm1rY2VydCBk
|
||||
ZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTFHMEUGA1UECww+YmFydG9zei5saXBpbnNr
|
||||
aUBCYXJ0b3N6cy1NYWNCb29rLVByby5sb2NhbCAoQmFydG9zeiBMaXBpbnNraSkw
|
||||
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDOwT5doIalTreoX71W6TLr
|
||||
V2tijuSHLRmIDcDutPM/cPYNUHgkGthZveMdrcoaDqveHZdGjWY39U+8kzrdcbV2
|
||||
7oXNd4ivC5acS8DIJNCO3G1JcNSYnxmZXEaPAHXaVke+SMXVTWbUvA8Rkyor9hPe
|
||||
KW8gtFqm2IT/klRBfWuYLO24dILrCfYkqJkZ6g++X7pBp1R/8h9SYdHWbHxIDk2d
|
||||
CWaHNA7v8g1bMw2ZmxICwgbsARplLgIU/ZWRKQik2axOIeHDpoeV9/hj4SXvs1bA
|
||||
yvO8oNMjYuekkAs117NAfJb9oR6p/iet39IfzY4zQmBqwFDuAu7+nZq1NfFLajor
|
||||
AgMBAAGjbDBqMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAM
|
||||
BgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFLRNFQImfwsJQApnvaPukIsWedjpMBQG
|
||||
A1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAYEAgm1tjv0cRoBR
|
||||
lBzIajECMHVK1dHYARaaFrG4ll1DpWS38Cyn2FN/YIIdTrzh8vFIkV/Leeozcjp8
|
||||
nE84zziw9nYvX86SfKtv8uVaVLPoNm2hl9JQS/19dMrB25vpeDGJqmnF3/n7oVEC
|
||||
SY6fY2xLMx47tpnT5P9NuXgP6Zz6KVQp+CPEfoIkTo+dU0Kk67K5Q9OR42SyiRG2
|
||||
JxSBymbPV/mHwOxAS2M6QMODPt+FpVYeiz+iM6d1lL2NGs2CnyBFaFLNlmxij9yL
|
||||
rZoU+Om5LrhgY5CL7/DVkU6xZC0VI9AZvV3eV5ouPv4ofH47BSOkxwLj5V4xFGTf
|
||||
+1YRuvqE12EecBpz5g/33LzrkipA7G6P1Oca38g7Xkv6+XKALXIZ2dtcXUp7s7Ty
|
||||
Cf6Nydlgeqe6Ik6+OKgWIwCahr2cWWVKC/JFMqOYugD4dYcyvbv+V06YEQvYJP5B
|
||||
yhVdn4uVAfrGGFUCRm9ZM3EhSPZlNwVcG6l5jaV/48L+Uy32VGvJ
|
||||
-----END CERTIFICATE-----
|
|
@ -0,0 +1,28 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDOwT5doIalTreo
|
||||
X71W6TLrV2tijuSHLRmIDcDutPM/cPYNUHgkGthZveMdrcoaDqveHZdGjWY39U+8
|
||||
kzrdcbV27oXNd4ivC5acS8DIJNCO3G1JcNSYnxmZXEaPAHXaVke+SMXVTWbUvA8R
|
||||
kyor9hPeKW8gtFqm2IT/klRBfWuYLO24dILrCfYkqJkZ6g++X7pBp1R/8h9SYdHW
|
||||
bHxIDk2dCWaHNA7v8g1bMw2ZmxICwgbsARplLgIU/ZWRKQik2axOIeHDpoeV9/hj
|
||||
4SXvs1bAyvO8oNMjYuekkAs117NAfJb9oR6p/iet39IfzY4zQmBqwFDuAu7+nZq1
|
||||
NfFLajorAgMBAAECggEBAMuSWeW1+N0q9IpEOhko44n1OTaBm2G9djYP1Lc0U41T
|
||||
m/DgGmryQ7OY09aVFzkw2OiKGjjNYKgYUbpK/Nqs6w9/Kx9zYpF3x4N80wQ9u1vu
|
||||
jWySO8FKZdoqkQ6cVW31Jg6leKTc4TL1N6EGVa+TS1yjT1fUPK2q4skBOxSAeUAK
|
||||
t5OvL5eeuumu+Ya3nrwQ1wp1ZBWhkeDIGjED8SDPfT0kU4v8dKTJcXk7ooF5s1OZ
|
||||
H6HSzEej8eZSfBLDTwPsHylaGTcTi9Tgi8CnjAVdBLaFJlS+bkvv7m5/jnuEpofX
|
||||
HkyAW5LDnIvHLycE3Ql9BQsR/DF9uzQvn9VRwPGQm4ECgYEA6/eyWhQfl4rDfjQi
|
||||
fzQgjEYnTbE69R3LUUKulUQQeBYcZO2JAAGZ45G6lZ1zEWFmXuj0aQRYpj798cTg
|
||||
0ZyH7Dllae8GPZlQ0u/Pxp03DzrvNGbHiTEBYqyVB+gJFn9Robm1eG+kP3QVgaIq
|
||||
iOXwnUGL74MaJKdwi9ruhzLaQwUCgYEA4E6qJwkBRkGfpjSc0stwTEd+04nSPU1u
|
||||
V3jFkxS/MNHCF/FUOGBKvrEbbedZ4DRiHjmQ6+zsvl+5jW75bw2rrxxxjkvpNjZe
|
||||
BOKhac7c+jHXEAm4xuP6lUnPOenBhy/AvOJMm+6wzxdvTzp2gfnfIUOld24bdSXP
|
||||
29yOzOpYb28CgYAlDn0f0FE1x0D0LNPODi2eWdYKSW7s14T6efJY1puPgEltQDBn
|
||||
o9i6+EPJAzTy4czl0sevRlN1qCbRNQ3pXR+rZUgb3sGoIs+ikK6cjkv7RFIUdJ+Z
|
||||
V+zTxi6RU0s6ETyMnVF2XHH61Qwbk5ACd7nVuFl1f603XGQ8UmFrMf080QKBgQDQ
|
||||
a2eg87YScOGGDvb0ywFib0BCEJqgSXVQo7B5lNp94zmFA8EszRRGkcwZ19DkCeht
|
||||
izHEdhYYYlvINihg7wPqpvRAsvpUXDoKMganiQY9F9hsV4wwih8JXlbFyhT/pvhg
|
||||
yalDbostMepEZN8+sE2K3A9ApLewp1y3Pv4VG17m0wKBgB7GIrI2Jx5CG4yFoM8y
|
||||
kB5rbvNlGAER58AMhzMebs7e2pQg0GxMpaNwqSZyVG0kLq7ayXn9ExA+uQpAG6cR
|
||||
fnBWVflYf0hKIC1M6U9kOle6lZ/Nbdo80wGa5z3Ieup6w9WB0pbtifvbsnv6RLeA
|
||||
S7MNQDcNXJxQulqppa+1BOTl
|
||||
-----END PRIVATE KEY-----
|
|
@ -0,0 +1,15 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{js}]
|
||||
charset = utf-8
|
||||
|
||||
[src/**.js]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[{package.json,.travis.yml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -0,0 +1,2 @@
|
|||
HTTPS=true
|
||||
SSL_CRT_FILE=./.cert/cert.pem SSL_KEY_FILE=./.cert/key.pem
|
|
@ -3,6 +3,7 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ledgerhq/hw-transport-webusb": "^5.34.0",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@project-serum/serum": "^0.13.11",
|
||||
|
|
|
@ -38,16 +38,17 @@ export default function App() {
|
|||
<Suspense fallback={<LoadingIndicator />}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
|
||||
<ConnectionProvider>
|
||||
<WalletProvider>
|
||||
<SnackbarProvider maxSnack={5} autoHideDuration={8000}>
|
||||
<SnackbarProvider maxSnack={5} autoHideDuration={8000}>
|
||||
<WalletProvider>
|
||||
<NavigationFrame>
|
||||
<Suspense fallback={<LoadingIndicator />}>
|
||||
<PageContents />
|
||||
</Suspense>
|
||||
</NavigationFrame>
|
||||
</SnackbarProvider>
|
||||
</WalletProvider>
|
||||
</WalletProvider>
|
||||
</SnackbarProvider>
|
||||
</ConnectionProvider>
|
||||
</ThemeProvider>
|
||||
</Suspense>
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
import React, {useEffect, useState} from 'react';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogForm from './DialogForm';
|
||||
import {LedgerWalletProvider} from "../utils/walletProvider/ledger";
|
||||
import CircularProgress from "@material-ui/core/CircularProgress";
|
||||
import {useSnackbar} from "notistack";
|
||||
|
||||
export default function AddHardwareWalletDialog({ open, onAdd, onClose }) {
|
||||
const [pubKey, setPubKey] = useState();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
useEffect(() => {( async () => {
|
||||
if (open) {
|
||||
try {
|
||||
const provider = new LedgerWalletProvider();
|
||||
await provider.init();
|
||||
setPubKey(provider.publicKey);
|
||||
} catch (err) {
|
||||
console.log(`received error when attempting to connect ledger: ${err}`);
|
||||
if (err.statusCode === 0x6804) {
|
||||
enqueueSnackbar('Unlock ledger device', { variant: 'error' })
|
||||
}
|
||||
setPubKey(undefined)
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
})();}, [open, onClose])
|
||||
|
||||
return (
|
||||
<DialogForm
|
||||
open={open}
|
||||
onEnter={() => {}}
|
||||
onClose={() => {
|
||||
setPubKey(undefined);
|
||||
onClose();
|
||||
}}
|
||||
onSubmit={() => {
|
||||
setPubKey(undefined);
|
||||
onAdd(pubKey);
|
||||
onClose();
|
||||
}}
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Add hardware wallet</DialogTitle>
|
||||
<DialogContent style={{ paddingTop: 16 }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{pubKey
|
||||
? (
|
||||
<>
|
||||
<b>Hardware wallet detected:</b>
|
||||
<div>{pubKey.toString()}</div>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<b>Connect your ledger and open the Solana application</b>
|
||||
<CircularProgress />
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => {
|
||||
setPubKey(undefined);
|
||||
onClose();
|
||||
}}>Close</Button>
|
||||
<Button type="submit" color="primary" disabled={!pubKey}>
|
||||
Add
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogForm>
|
||||
);
|
||||
}
|
|
@ -184,6 +184,7 @@ function BalanceListItemDetails({ publicKey, balanceInfo }) {
|
|||
closeTokenAccountDialogOpen,
|
||||
setCloseTokenAccountDialogOpen,
|
||||
] = useState(false);
|
||||
const wallet = useWallet()
|
||||
|
||||
if (!balanceInfo) {
|
||||
return <LoadingIndicator delay={0} />;
|
||||
|
@ -197,10 +198,12 @@ function BalanceListItemDetails({ publicKey, balanceInfo }) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<ExportAccountDialog
|
||||
onClose={() => setExportAccDialogOpen(false)}
|
||||
open={exportAccDialogOpen}
|
||||
/>
|
||||
{wallet.allowsExport &&
|
||||
<ExportAccountDialog
|
||||
onClose={() => setExportAccDialogOpen(false)}
|
||||
open={exportAccDialogOpen}
|
||||
/>
|
||||
}
|
||||
<div className={classes.itemDetails}>
|
||||
<div className={classes.buttonContainer}>
|
||||
{!publicKey.equals(owner) && showTokenInfoDialog ? (
|
||||
|
@ -270,7 +273,7 @@ function BalanceListItemDetails({ publicKey, balanceInfo }) {
|
|||
</Link>
|
||||
</Typography>
|
||||
</div>
|
||||
{exportNeedsDisplay && (
|
||||
{exportNeedsDisplay && wallet.allowsExport && (
|
||||
<div>
|
||||
<Typography variant="body2">
|
||||
<Link href={'#'} onClick={(e) => setExportAccDialogOpen(true)}>
|
||||
|
|
|
@ -20,7 +20,7 @@ export default function DebugButtons() {
|
|||
const wallet = useWallet();
|
||||
const updateTokenName = useUpdateTokenName();
|
||||
const { endpoint } = useConnectionConfig();
|
||||
const balanceInfo = useBalanceInfo(wallet.account.publicKey);
|
||||
const balanceInfo = useBalanceInfo(wallet.publicKey);
|
||||
const [sendTransaction, sending] = useSendTransaction();
|
||||
const callAsync = useCallAsync();
|
||||
|
||||
|
@ -29,13 +29,13 @@ export default function DebugButtons() {
|
|||
function requestAirdrop() {
|
||||
callAsync(
|
||||
wallet.connection.requestAirdrop(
|
||||
wallet.account.publicKey,
|
||||
wallet.publicKey,
|
||||
LAMPORTS_PER_SOL,
|
||||
),
|
||||
{
|
||||
onSuccess: async () => {
|
||||
await sleep(5000);
|
||||
refreshAccountInfo(wallet.connection, wallet.account.publicKey);
|
||||
refreshAccountInfo(wallet.connection, wallet.publicKey);
|
||||
},
|
||||
successMessage:
|
||||
'Success! Please wait up to 30 seconds for the SOL tokens to appear in your wallet.',
|
||||
|
@ -53,7 +53,7 @@ export default function DebugButtons() {
|
|||
sendTransaction(
|
||||
createAndInitializeMint({
|
||||
connection: wallet.connection,
|
||||
owner: wallet.account,
|
||||
owner: wallet,
|
||||
mint,
|
||||
amount: 1000,
|
||||
decimals: 2,
|
||||
|
|
|
@ -24,7 +24,7 @@ export default function ExportAccountDialog({ open, onClose }) {
|
|||
type={isHidden && 'password'}
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
value={bs58.encode(wallet.account.secretKey)}
|
||||
value={bs58.encode(wallet.provider.account.secretKey)}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
|
|
|
@ -14,6 +14,7 @@ import CheckIcon from '@material-ui/icons/Check';
|
|||
import AddIcon from '@material-ui/icons/Add';
|
||||
import ExitToApp from '@material-ui/icons/ExitToApp';
|
||||
import AccountIcon from '@material-ui/icons/AccountCircle';
|
||||
import UsbIcon from '@material-ui/icons/Usb';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import Hidden from '@material-ui/core/Hidden';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
|
@ -22,6 +23,7 @@ import CodeIcon from '@material-ui/icons/Code';
|
|||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import AddAccountDialog from './AddAccountDialog';
|
||||
import DeleteAccountDialog from "./DeleteAccountDialog";
|
||||
import AddHardwareWalletDialog from "./AddHarwareWalletDialog";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
content: {
|
||||
|
@ -131,6 +133,7 @@ function WalletSelector() {
|
|||
const { accounts, setWalletSelector, addAccount } = useWalletSelector();
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [addAccountOpen, setAddAccountOpen] = useState(false);
|
||||
const [addHardwareWalletDialogOpen, setAddHardwareWalletDialogOpen] = useState(false);
|
||||
const [deleteAccountOpen, setDeleteAccountOpen] = useState(false);
|
||||
const [isDeleteAccountEnabled, setIsDeleteAccountEnabled] = useState(false);
|
||||
const classes = useStyles();
|
||||
|
@ -141,6 +144,18 @@ function WalletSelector() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<AddHardwareWalletDialog
|
||||
open={addHardwareWalletDialogOpen}
|
||||
onClose={() => setAddHardwareWalletDialogOpen(false)}
|
||||
onAdd={(pubKey) => {
|
||||
addAccount({ name: 'Hardware wallet', importedAccount: pubKey.toString(), ledger: true });
|
||||
setWalletSelector({
|
||||
walletIndex: undefined,
|
||||
importedPubkey: pubKey.toString(),
|
||||
ledger: true
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<AddAccountDialog
|
||||
open={addAccountOpen}
|
||||
onClose={() => setAddAccountOpen(false)}
|
||||
|
@ -151,6 +166,7 @@ function WalletSelector() {
|
|||
importedPubkey: importedAccount
|
||||
? importedAccount.publicKey.toString()
|
||||
: undefined,
|
||||
ledger: false,
|
||||
});
|
||||
setAddAccountOpen(false);
|
||||
}}
|
||||
|
@ -208,6 +224,14 @@ function WalletSelector() {
|
|||
</MenuItem>
|
||||
))}
|
||||
<Divider />
|
||||
<MenuItem
|
||||
onClick={() => setAddHardwareWalletDialogOpen(true)}
|
||||
>
|
||||
<ListItemIcon className={classes.menuItemIcon}>
|
||||
<UsbIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
Import Hardware Wallet
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setAnchorEl(null);
|
||||
|
|
|
@ -72,7 +72,7 @@ export default function PopupPage({ opener }) {
|
|||
useEffect(() => {
|
||||
if (
|
||||
connectedAccount &&
|
||||
!connectedAccount.publicKey.equals(wallet.publicKey)
|
||||
!connectedAccount.equals(wallet.publicKey)
|
||||
) {
|
||||
setConnectedAccount(null);
|
||||
}
|
||||
|
@ -95,11 +95,11 @@ export default function PopupPage({ opener }) {
|
|||
|
||||
if (
|
||||
!connectedAccount ||
|
||||
!connectedAccount.publicKey.equals(wallet.publicKey)
|
||||
!connectedAccount.equals(wallet.publicKey)
|
||||
) {
|
||||
// Approve the parent page to connect to this wallet.
|
||||
function connect(autoApprove) {
|
||||
setConnectedAccount(wallet.account);
|
||||
setConnectedAccount(wallet.publicKey);
|
||||
postMessage({
|
||||
method: 'connected',
|
||||
params: { publicKey: wallet.publicKey.toBase58(), autoApprove },
|
||||
|
@ -116,13 +116,11 @@ export default function PopupPage({ opener }) {
|
|||
assert(request.method === 'signTransaction');
|
||||
const message = bs58.decode(request.params.message);
|
||||
|
||||
function sendSignature() {
|
||||
async function sendSignature() {
|
||||
setRequests((requests) => requests.slice(1));
|
||||
postMessage({
|
||||
result: {
|
||||
signature: bs58.encode(
|
||||
nacl.sign.detached(message, wallet.account.secretKey),
|
||||
),
|
||||
signature: await wallet.createSignature(message),
|
||||
publicKey: wallet.publicKey.toBase58(),
|
||||
},
|
||||
id: request.id,
|
||||
|
|
|
@ -39,7 +39,8 @@ export function useIsProdNetwork() {
|
|||
}
|
||||
|
||||
export function useSolanaExplorerUrlSuffix() {
|
||||
const endpoint = useContext(ConnectionContext).endpoint;
|
||||
const context = useContext(ConnectionContext);
|
||||
const endpoint = context.endpoint;
|
||||
if (endpoint === clusterApiUrl('devnet')) {
|
||||
return '?cluster=devnet';
|
||||
} else if (endpoint === clusterApiUrl('testnet')) {
|
||||
|
|
|
@ -34,9 +34,9 @@ export async function getOwnedTokenAccounts(connection, publicKey) {
|
|||
if (resp.error) {
|
||||
throw new Error(
|
||||
'failed to get token accounts owned by ' +
|
||||
publicKey.toBase58() +
|
||||
': ' +
|
||||
resp.error.message,
|
||||
publicKey.toBase58() +
|
||||
': ' +
|
||||
resp.error.message,
|
||||
);
|
||||
}
|
||||
return resp.result
|
||||
|
@ -68,9 +68,39 @@ export async function getOwnedTokenAccounts(connection, publicKey) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function signAndSendTransaction(connection, transaction, wallet, signers) {
|
||||
transaction.recentBlockhash = (await connection.getRecentBlockhash('max')).blockhash;
|
||||
transaction.setSigners(
|
||||
// fee payed by the wallet owner
|
||||
wallet.publicKey,
|
||||
...signers.map(s => s.publicKey)
|
||||
);
|
||||
|
||||
if (signers.length > 0) {
|
||||
transaction.partialSign(...signers);
|
||||
}
|
||||
|
||||
transaction = await wallet.signTransaction(transaction);
|
||||
const rawTransaction = transaction.serialize();
|
||||
return await connection.sendRawTransaction(rawTransaction, {
|
||||
preflightCommitment: 'single',
|
||||
});
|
||||
}
|
||||
|
||||
export async function nativeTransfer(connection, wallet, destination, amount) {
|
||||
const tx = new Transaction().add(
|
||||
SystemProgram.transfer({
|
||||
fromPubkey: wallet.publicKey,
|
||||
toPubkey: destination,
|
||||
lamports: amount,
|
||||
}),
|
||||
);
|
||||
return await signAndSendTransaction(connection, tx, wallet, []);
|
||||
}
|
||||
|
||||
export async function createAndInitializeMint({
|
||||
connection,
|
||||
owner, // Account for paying fees and allowed to mint new tokens
|
||||
owner, // Wallet for paying fees and allowed to mint new tokens
|
||||
mint, // Account to hold token information
|
||||
amount, // Number of tokens to issue
|
||||
decimals,
|
||||
|
@ -95,7 +125,7 @@ export async function createAndInitializeMint({
|
|||
mintAuthority: owner.publicKey,
|
||||
}),
|
||||
);
|
||||
let signers = [owner, mint];
|
||||
let signers = [mint];
|
||||
if (amount > 0) {
|
||||
transaction.add(
|
||||
SystemProgram.createAccount({
|
||||
|
@ -125,9 +155,8 @@ export async function createAndInitializeMint({
|
|||
}),
|
||||
);
|
||||
}
|
||||
return await connection.sendTransaction(transaction, signers, {
|
||||
preflightCommitment: 'single',
|
||||
});
|
||||
|
||||
return await signAndSendTransaction(connection, transaction, owner, signers);
|
||||
}
|
||||
|
||||
export async function createAndInitializeTokenAccount({
|
||||
|
@ -155,10 +184,9 @@ export async function createAndInitializeTokenAccount({
|
|||
owner: payer.publicKey,
|
||||
}),
|
||||
);
|
||||
let signers = [payer, newAccount];
|
||||
return await connection.sendTransaction(transaction, signers, {
|
||||
preflightCommitment: 'single',
|
||||
});
|
||||
|
||||
let signers = [newAccount];
|
||||
return await signAndSendTransaction(connection, transaction, payer, signers);
|
||||
}
|
||||
|
||||
export async function transferTokens({
|
||||
|
@ -218,7 +246,7 @@ export async function transferTokens({
|
|||
}
|
||||
|
||||
function createTransferBetweenSplTokenAccountsInstruction({
|
||||
owner,
|
||||
ownerPublicKey,
|
||||
sourcePublicKey,
|
||||
destinationPublicKey,
|
||||
amount,
|
||||
|
@ -228,7 +256,7 @@ function createTransferBetweenSplTokenAccountsInstruction({
|
|||
transfer({
|
||||
source: sourcePublicKey,
|
||||
destination: destinationPublicKey,
|
||||
owner: owner.publicKey,
|
||||
owner: ownerPublicKey,
|
||||
amount,
|
||||
}),
|
||||
);
|
||||
|
@ -247,16 +275,14 @@ async function transferBetweenSplTokenAccounts({
|
|||
memo,
|
||||
}) {
|
||||
const transaction = createTransferBetweenSplTokenAccountsInstruction({
|
||||
owner,
|
||||
ownerPublicKey: owner.publicKey,
|
||||
sourcePublicKey,
|
||||
destinationPublicKey,
|
||||
amount,
|
||||
memo,
|
||||
});
|
||||
let signers = [owner];
|
||||
return await connection.sendTransaction(transaction, signers, {
|
||||
preflightCommitment: 'single',
|
||||
});
|
||||
let signers = [];
|
||||
return await signAndSendTransaction(connection, transaction, owner, signers)
|
||||
}
|
||||
|
||||
async function createAndTransferToAccount({
|
||||
|
@ -296,7 +322,7 @@ async function createAndTransferToAccount({
|
|||
);
|
||||
const transferBetweenAccountsTxn = createTransferBetweenSplTokenAccountsInstruction(
|
||||
{
|
||||
owner,
|
||||
ownerPublicKey: owner.publicKey,
|
||||
sourcePublicKey,
|
||||
destinationPublicKey: newAccount.publicKey,
|
||||
amount,
|
||||
|
@ -304,10 +330,8 @@ async function createAndTransferToAccount({
|
|||
},
|
||||
);
|
||||
transaction.add(transferBetweenAccountsTxn);
|
||||
let signers = [owner, newAccount];
|
||||
return await connection.sendTransaction(transaction, signers, {
|
||||
preflightCommitment: 'single',
|
||||
});
|
||||
let signers = [newAccount];
|
||||
return await signAndSendTransaction(connection, transaction, owner, signers)
|
||||
}
|
||||
|
||||
export async function closeTokenAccount({
|
||||
|
@ -322,8 +346,6 @@ export async function closeTokenAccount({
|
|||
owner: owner.publicKey,
|
||||
}),
|
||||
);
|
||||
let signers = [owner];
|
||||
return await connection.sendTransaction(transaction, signers, {
|
||||
preflightCommitment: 'single',
|
||||
});
|
||||
let signers = [];
|
||||
return await signAndSendTransaction(connection, transaction, owner, signers);
|
||||
}
|
||||
|
|
|
@ -1,61 +1,55 @@
|
|||
import React, {useContext, useMemo, useState} from 'react';
|
||||
import * as bip32 from 'bip32';
|
||||
import React, {useContext, useEffect, useMemo, useState} from 'react';
|
||||
import * as bs58 from 'bs58';
|
||||
import {
|
||||
Account,
|
||||
SystemProgram,
|
||||
Transaction,
|
||||
PublicKey,
|
||||
} from '@solana/web3.js';
|
||||
import {Account, PublicKey,} from '@solana/web3.js';
|
||||
import nacl from 'tweetnacl';
|
||||
import {
|
||||
setInitialAccountInfo,
|
||||
useAccountInfo,
|
||||
useConnection,
|
||||
} from './connection';
|
||||
import {setInitialAccountInfo, useAccountInfo, useConnection,} from './connection';
|
||||
import {
|
||||
closeTokenAccount,
|
||||
createAndInitializeTokenAccount,
|
||||
getOwnedTokenAccounts,
|
||||
nativeTransfer,
|
||||
transferTokens,
|
||||
} from './tokens';
|
||||
import { TOKEN_PROGRAM_ID, WRAPPED_SOL_MINT } from './tokens/instructions';
|
||||
import {
|
||||
ACCOUNT_LAYOUT,
|
||||
parseMintData,
|
||||
parseTokenAccountData,
|
||||
} from './tokens/data';
|
||||
import { useListener, useLocalStorageState } from './utils';
|
||||
import { useTokenName } from './tokens/names';
|
||||
import { refreshCache, useAsyncData } from './fetch-loop';
|
||||
import { getUnlockedMnemonicAndSeed, walletSeedChanged } from './wallet-seed';
|
||||
import {TOKEN_PROGRAM_ID, WRAPPED_SOL_MINT} from './tokens/instructions';
|
||||
import {ACCOUNT_LAYOUT, parseMintData, parseTokenAccountData,} from './tokens/data';
|
||||
import {useListener, useLocalStorageState} from './utils';
|
||||
import {useTokenName} from './tokens/names';
|
||||
import {refreshCache, useAsyncData} from './fetch-loop';
|
||||
import {getUnlockedMnemonicAndSeed, walletSeedChanged} from './wallet-seed';
|
||||
import {WalletProviderFactory} from "./walletProvider/factory";
|
||||
import {getAccountFromSeed} from "./walletProvider/localStorage";
|
||||
|
||||
const DEFAULT_WALLET_SELECTOR = {
|
||||
walletIndex: 0,
|
||||
importedPubkey: undefined,
|
||||
ledger: false,
|
||||
};
|
||||
|
||||
export class Wallet {
|
||||
constructor(connection, account) {
|
||||
constructor(connection, type, args) {
|
||||
this.connection = connection;
|
||||
this.account = account;
|
||||
this.type = type;
|
||||
this.provider = WalletProviderFactory.getProvider(type, args);
|
||||
}
|
||||
|
||||
static getAccountFromSeed(seed, walletIndex, accountIndex = 0) {
|
||||
const derivedSeed = bip32
|
||||
.fromSeed(seed)
|
||||
.derivePath(`m/501'/${walletIndex}'/0/${accountIndex}`).privateKey;
|
||||
return new Account(nacl.sign.keyPair.fromSeed(derivedSeed).secretKey);
|
||||
static create = async (connection, type, args) => {
|
||||
const instance = new Wallet(connection, type, args);
|
||||
await instance.provider.init();
|
||||
return instance;
|
||||
}
|
||||
|
||||
get publicKey() {
|
||||
return this.account.publicKey;
|
||||
return this.provider.publicKey;
|
||||
}
|
||||
|
||||
get allowsExport() {
|
||||
return this.type === 'local';
|
||||
}
|
||||
|
||||
getTokenAccountInfo = async () => {
|
||||
let accounts = await getOwnedTokenAccounts(
|
||||
this.connection,
|
||||
this.account.publicKey,
|
||||
this.publicKey,
|
||||
);
|
||||
return accounts.map(({ publicKey, accountInfo }) => {
|
||||
setInitialAccountInfo(this.connection, publicKey, accountInfo);
|
||||
|
@ -66,7 +60,7 @@ export class Wallet {
|
|||
createTokenAccount = async (tokenAddress) => {
|
||||
return await createAndInitializeTokenAccount({
|
||||
connection: this.connection,
|
||||
payer: this.account,
|
||||
payer: this,
|
||||
mintPublicKey: tokenAddress,
|
||||
newAccount: new Account(),
|
||||
});
|
||||
|
@ -87,7 +81,7 @@ export class Wallet {
|
|||
}
|
||||
return await transferTokens({
|
||||
connection: this.connection,
|
||||
owner: this.account,
|
||||
owner: this,
|
||||
sourcePublicKey: source,
|
||||
destinationPublicKey: destination,
|
||||
amount,
|
||||
|
@ -97,25 +91,24 @@ export class Wallet {
|
|||
};
|
||||
|
||||
transferSol = async (destination, amount) => {
|
||||
const tx = new Transaction().add(
|
||||
SystemProgram.transfer({
|
||||
fromPubkey: this.publicKey,
|
||||
toPubkey: destination,
|
||||
lamports: amount,
|
||||
}),
|
||||
);
|
||||
return await this.connection.sendTransaction(tx, [this.account], {
|
||||
preflightCommitment: 'single',
|
||||
});
|
||||
return nativeTransfer(this.connection, this, destination, amount);
|
||||
};
|
||||
|
||||
closeTokenAccount = async (publicKey) => {
|
||||
return await closeTokenAccount({
|
||||
connection: this.connection,
|
||||
owner: this.account,
|
||||
owner: this,
|
||||
sourcePublicKey: publicKey,
|
||||
});
|
||||
};
|
||||
|
||||
signTransaction = async (transaction) => {
|
||||
return this.provider.signTransaction(transaction);
|
||||
}
|
||||
|
||||
createSignature = async (message) => {
|
||||
return this.provider.createSignature(message);
|
||||
}
|
||||
}
|
||||
|
||||
const WalletContext = React.createContext(null);
|
||||
|
@ -124,6 +117,7 @@ export function WalletProvider({ children }) {
|
|||
useListener(walletSeedChanged, 'change');
|
||||
const { mnemonic, seed, importsEncryptionKey } = getUnlockedMnemonicAndSeed();
|
||||
const connection = useConnection();
|
||||
const [wallet, setWallet] = useState();
|
||||
|
||||
// `privateKeyImports` are accounts imported *in addition* to HD wallets
|
||||
const [privateKeyImports, setPrivateKeyImports] = useLocalStorageState(
|
||||
|
@ -135,43 +129,63 @@ export function WalletProvider({ children }) {
|
|||
'walletSelector',
|
||||
DEFAULT_WALLET_SELECTOR,
|
||||
);
|
||||
const [ledgerPubKey, setLedgerPubKey] = useState(walletSelector.ledger ? walletSelector.importedPubkey : undefined);
|
||||
|
||||
// `walletCount` is the number of HD wallets.
|
||||
const [walletCount, setWalletCount] = useLocalStorageState('walletCount', 1);
|
||||
|
||||
const wallet = useMemo(() => {
|
||||
useEffect(() => { (async () => {
|
||||
if (!seed) {
|
||||
return null;
|
||||
}
|
||||
const account =
|
||||
walletSelector.walletIndex !== undefined
|
||||
? Wallet.getAccountFromSeed(
|
||||
Buffer.from(seed, 'hex'),
|
||||
walletSelector.walletIndex,
|
||||
let wallet;
|
||||
if (walletSelector.ledger) {
|
||||
try {
|
||||
const onDisconnect = () => {
|
||||
setWalletSelector(DEFAULT_WALLET_SELECTOR);
|
||||
setLedgerPubKey(undefined);
|
||||
};
|
||||
wallet = await Wallet.create(connection, 'ledger', {onDisconnect});
|
||||
} catch (e) {
|
||||
console.log(`received error using ledger wallet: ${e}`);
|
||||
setWalletSelector(DEFAULT_WALLET_SELECTOR);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!wallet) {
|
||||
const account =
|
||||
walletSelector.walletIndex !== undefined
|
||||
? getAccountFromSeed(
|
||||
Buffer.from(seed, 'hex'),
|
||||
walletSelector.walletIndex,
|
||||
)
|
||||
: new Account(
|
||||
(() => {
|
||||
const { nonce, ciphertext } = privateKeyImports[
|
||||
walletSelector.importedPubkey
|
||||
: new Account(
|
||||
(() => {
|
||||
const { nonce, ciphertext } = privateKeyImports[
|
||||
walletSelector.importedPubkey
|
||||
];
|
||||
return nacl.secretbox.open(
|
||||
bs58.decode(ciphertext),
|
||||
bs58.decode(nonce),
|
||||
importsEncryptionKey,
|
||||
);
|
||||
})(),
|
||||
);
|
||||
return new Wallet(connection, account);
|
||||
}, [
|
||||
return nacl.secretbox.open(
|
||||
bs58.decode(ciphertext),
|
||||
bs58.decode(nonce),
|
||||
importsEncryptionKey,
|
||||
);
|
||||
})());
|
||||
wallet = await Wallet.create(connection, 'local', {account})
|
||||
}
|
||||
setWallet(wallet);
|
||||
})();}, [
|
||||
connection,
|
||||
seed,
|
||||
walletSelector,
|
||||
privateKeyImports,
|
||||
importsEncryptionKey,
|
||||
setWalletSelector,
|
||||
]);
|
||||
|
||||
function addAccount({ name, importedAccount }) {
|
||||
if (importedAccount === undefined) {
|
||||
function addAccount({ name, importedAccount, ledger }) {
|
||||
if (ledger) {
|
||||
setLedgerPubKey(importedAccount);
|
||||
} else if (importedAccount === undefined) {
|
||||
name && localStorage.setItem(`name${walletCount}`, name);
|
||||
setWalletCount(walletCount + 1);
|
||||
} else {
|
||||
|
@ -197,7 +211,7 @@ export function WalletProvider({ children }) {
|
|||
}
|
||||
const [walletNames, setWalletNames] = useState(getWalletNames())
|
||||
function setAccountName(selector, newName) {
|
||||
if (selector.importedPubkey) {
|
||||
if (selector.importedPubkey && !selector.ledger) {
|
||||
let newPrivateKeyImports = { ...privateKeyImports };
|
||||
newPrivateKeyImports[selector.importedPubkey.toString()].name = newName;
|
||||
setPrivateKeyImports(newPrivateKeyImports);
|
||||
|
@ -214,10 +228,10 @@ export function WalletProvider({ children }) {
|
|||
|
||||
const seedBuffer = Buffer.from(seed, 'hex');
|
||||
const derivedAccounts = [...Array(walletCount).keys()].map((idx) => {
|
||||
let address = Wallet.getAccountFromSeed(seedBuffer, idx).publicKey;
|
||||
let address = getAccountFromSeed(seedBuffer, idx).publicKey;
|
||||
let name = localStorage.getItem(`name${idx}`);
|
||||
return {
|
||||
selector: { walletIndex: idx, importedPubkey: undefined },
|
||||
selector: { walletIndex: idx, importedPubkey: undefined, ledger: false },
|
||||
isSelected: walletSelector.walletIndex === idx,
|
||||
address,
|
||||
name: idx === 0 ? 'Main account' : name || `Account ${idx}`,
|
||||
|
@ -227,16 +241,25 @@ export function WalletProvider({ children }) {
|
|||
const importedAccounts = Object.keys(privateKeyImports).map((pubkey) => {
|
||||
const { name } = privateKeyImports[pubkey];
|
||||
return {
|
||||
selector: { walletIndex: undefined, importedPubkey: pubkey },
|
||||
selector: { walletIndex: undefined, importedPubkey: pubkey, ledger: false },
|
||||
address: new PublicKey(bs58.decode(pubkey)),
|
||||
name: `${name} (imported)`, // TODO: do this in the Component with styling.
|
||||
isSelected: walletSelector.importedPubkey === pubkey,
|
||||
};
|
||||
});
|
||||
|
||||
if (ledgerPubKey) {
|
||||
derivedAccounts.push({
|
||||
selector: {walletIndex: undefined, importedPubkey: ledgerPubKey, ledger: true},
|
||||
address: new PublicKey(ledgerPubKey),// todo: get the ledger address
|
||||
name: 'Hardware wallet',
|
||||
isSelected: walletSelector.ledger,
|
||||
})
|
||||
}
|
||||
|
||||
return derivedAccounts.concat(importedAccounts);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [seed, walletCount, walletSelector, privateKeyImports, walletNames]);
|
||||
}, [seed, walletCount, walletSelector, privateKeyImports, walletNames, ledgerPubKey]);
|
||||
|
||||
return (
|
||||
<WalletContext.Provider
|
||||
|
@ -270,7 +293,7 @@ export function useWalletPublicKeys() {
|
|||
wallet.getTokenAccountInfo,
|
||||
);
|
||||
const getPublicKeys = () => [
|
||||
wallet.account.publicKey,
|
||||
wallet.publicKey,
|
||||
...(tokenAccountInfo
|
||||
? tokenAccountInfo.map(({ publicKey }) => publicKey)
|
||||
: []),
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import { LocalStorageWalletProvider } from './localStorage';
|
||||
import { LedgerWalletProvider } from './ledger';
|
||||
|
||||
export class WalletProviderFactory {
|
||||
static getProvider(type, args) {
|
||||
if (type === 'local') {
|
||||
return new LocalStorageWalletProvider(args)
|
||||
}
|
||||
|
||||
if (type === 'ledger') {
|
||||
return new LedgerWalletProvider(args);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
import { PublicKey } from "@solana/web3.js";
|
||||
const bs58 = require("bs58");
|
||||
|
||||
const INS_GET_PUBKEY = 0x05;
|
||||
const INS_SIGN_MESSAGE = 0x06;
|
||||
|
||||
const P1_NON_CONFIRM = 0x00;
|
||||
const P1_CONFIRM = 0x01;
|
||||
|
||||
const P2_EXTEND = 0x01;
|
||||
const P2_MORE = 0x02;
|
||||
|
||||
const MAX_PAYLOAD = 255;
|
||||
|
||||
const LEDGER_CLA = 0xe0;
|
||||
|
||||
/*
|
||||
* Helper for chunked send of large payloads
|
||||
*/
|
||||
async function solana_send(transport, instruction, p1, payload) {
|
||||
var p2 = 0;
|
||||
var payload_offset = 0;
|
||||
|
||||
if (payload.length > MAX_PAYLOAD) {
|
||||
while ((payload.length - payload_offset) > MAX_PAYLOAD) {
|
||||
const buf = payload.slice(payload_offset, payload_offset + MAX_PAYLOAD);
|
||||
payload_offset += MAX_PAYLOAD;
|
||||
console.log("send", (p2 | P2_MORE).toString(16), buf.length.toString(16), buf);
|
||||
const reply = await transport.send(LEDGER_CLA, instruction, p1, (p2 | P2_MORE), buf);
|
||||
if (reply.length !== 2) {
|
||||
throw new Error(
|
||||
"solana_send: Received unexpected reply payload",
|
||||
"UnexpectedReplyPayload"
|
||||
);
|
||||
}
|
||||
p2 |= P2_EXTEND;
|
||||
}
|
||||
}
|
||||
|
||||
const buf = payload.slice(payload_offset);
|
||||
console.log("send", p2.toString(16), buf.length.toString(16), buf);
|
||||
const reply = await transport.send(LEDGER_CLA, instruction, p1, p2, buf);
|
||||
|
||||
return reply.slice(0, reply.length - 2);
|
||||
}
|
||||
|
||||
const BIP32_HARDENED_BIT = ((1 << 31) >>> 0);
|
||||
function _harden(n) {
|
||||
return (n | BIP32_HARDENED_BIT) >>> 0;
|
||||
}
|
||||
|
||||
export function solana_derivation_path(account, change) {
|
||||
var length;
|
||||
if (typeof (account) === 'number') {
|
||||
if (typeof (change) === 'number') {
|
||||
length = 4;
|
||||
} else {
|
||||
length = 3;
|
||||
}
|
||||
} else {
|
||||
length = 2;
|
||||
}
|
||||
|
||||
var derivation_path = Buffer.alloc(1 + (length * 4));
|
||||
// eslint-disable-next-line
|
||||
var offset = 0;
|
||||
offset = derivation_path.writeUInt8(length, offset);
|
||||
offset = derivation_path.writeUInt32BE(_harden(44), offset); // Using BIP44
|
||||
offset = derivation_path.writeUInt32BE(_harden(501), offset); // Solana's BIP44 path
|
||||
|
||||
if (length > 2) {
|
||||
offset = derivation_path.writeUInt32BE(_harden(account), offset);
|
||||
if (length === 4) {
|
||||
offset = derivation_path.writeUInt32BE(_harden(change), offset);
|
||||
}
|
||||
}
|
||||
|
||||
return derivation_path;
|
||||
}
|
||||
|
||||
async function solana_ledger_get_pubkey(transport, derivation_path) {
|
||||
return solana_send(transport, INS_GET_PUBKEY, P1_NON_CONFIRM, derivation_path);
|
||||
}
|
||||
|
||||
export async function solana_ledger_sign_transaction(transport, derivation_path, transaction) {
|
||||
const msg_bytes = transaction.serializeMessage();
|
||||
return solana_ledger_sign_bytes(transport, derivation_path, msg_bytes);
|
||||
}
|
||||
|
||||
export async function solana_ledger_sign_bytes(transport, derivation_path, msg_bytes) {
|
||||
var num_paths = Buffer.alloc(1);
|
||||
num_paths.writeUInt8(1);
|
||||
|
||||
const payload = Buffer.concat([num_paths, derivation_path, msg_bytes]);
|
||||
|
||||
return solana_send(transport, INS_SIGN_MESSAGE, P1_CONFIRM, payload);
|
||||
}
|
||||
|
||||
export async function getPublicKey(transport) {
|
||||
const from_derivation_path = solana_derivation_path();
|
||||
const from_pubkey_bytes = await solana_ledger_get_pubkey(transport, from_derivation_path);
|
||||
const from_pubkey_string = bs58.encode(from_pubkey_bytes);
|
||||
|
||||
return new PublicKey(from_pubkey_string);
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import TransportWebUsb from "@ledgerhq/hw-transport-webusb";
|
||||
import {
|
||||
getPublicKey,
|
||||
solana_derivation_path,
|
||||
solana_ledger_sign_bytes,
|
||||
solana_ledger_sign_transaction
|
||||
} from './ledger-core';
|
||||
import bs58 from "bs58";
|
||||
import nacl from "tweetnacl";
|
||||
|
||||
export class LedgerWalletProvider {
|
||||
constructor(args) {
|
||||
this.onDisconnect = (args && args.onDisconnect) || (() => {});
|
||||
}
|
||||
|
||||
init = async () => {
|
||||
this.transport = await TransportWebUsb.create();
|
||||
this.pubKey = await getPublicKey(this.transport);
|
||||
this.transport.on('disconnect', this.onDisconnect);
|
||||
this.listAddresses = async (walletCount) => {
|
||||
// TODO: read accounts from ledger
|
||||
return [this.pubKey];
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
get publicKey() {
|
||||
return this.pubKey;
|
||||
}
|
||||
|
||||
signTransaction = async (transaction) => {
|
||||
const from_derivation_path = solana_derivation_path();
|
||||
const sig_bytes = await solana_ledger_sign_transaction(this.transport, from_derivation_path, transaction);
|
||||
transaction.addSignature(this.publicKey, sig_bytes);
|
||||
return transaction;
|
||||
}
|
||||
|
||||
createSignature = async (message) => {
|
||||
const from_derivation_path = solana_derivation_path();
|
||||
const sig_bytes = await solana_ledger_sign_bytes(this.transport, from_derivation_path, message);
|
||||
return bs58.encode(sig_bytes)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import { getUnlockedMnemonicAndSeed } from './../wallet-seed';
|
||||
import * as bip32 from 'bip32';
|
||||
import nacl from 'tweetnacl';
|
||||
import { Account } from '@solana/web3.js';
|
||||
import bs58 from 'bs58';
|
||||
|
||||
export function getAccountFromSeed(seed, walletIndex, accountIndex = 0) {
|
||||
const derivedSeed = bip32
|
||||
.fromSeed(seed)
|
||||
.derivePath(`m/501'/${walletIndex}'/0/${accountIndex}`).privateKey;
|
||||
return new Account(nacl.sign.keyPair.fromSeed(derivedSeed).secretKey);
|
||||
}
|
||||
|
||||
|
||||
export class LocalStorageWalletProvider {
|
||||
constructor(args) {
|
||||
const { seed } = getUnlockedMnemonicAndSeed();
|
||||
this.account = args.account
|
||||
this.listAddresses = async (walletCount) => {
|
||||
const seedBuffer = Buffer.from(seed, 'hex');
|
||||
return [...Array(walletCount).keys()].map(
|
||||
(walletIndex) => {
|
||||
let address = getAccountFromSeed(seedBuffer, walletIndex).publicKey;
|
||||
let name = localStorage.getItem(`name${walletIndex}`);
|
||||
return { index: walletIndex, address, name };
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
init = async () => {
|
||||
return this;
|
||||
}
|
||||
|
||||
get publicKey() {
|
||||
return this.account.publicKey;
|
||||
}
|
||||
|
||||
signTransaction = async (transaction) => {
|
||||
transaction.partialSign(this.account);
|
||||
return transaction;
|
||||
}
|
||||
|
||||
createSignature = (message) => {
|
||||
return bs58.encode(nacl.sign.detached(message, this.account.secretKey))
|
||||
}
|
||||
}
|
47
yarn.lock
47
yarn.lock
|
@ -1494,6 +1494,44 @@
|
|||
"@types/yargs" "^15.0.0"
|
||||
chalk "^3.0.0"
|
||||
|
||||
"@ledgerhq/devices@^5.34.0":
|
||||
version "5.34.0"
|
||||
resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-5.34.0.tgz#4cb073a7bc9528f42f539241fedf44c0c6b451dc"
|
||||
integrity sha512-oRoZDDfufXAv9KofQdOXc0QztISa9x/YVdiWs55iOlNRQjWYHSPFzeSP6+J+tN0Au/GS9v+wcnTf+tHCtVz27Q==
|
||||
dependencies:
|
||||
"@ledgerhq/errors" "^5.34.0"
|
||||
"@ledgerhq/logs" "^5.30.0"
|
||||
rxjs "^6.6.3"
|
||||
|
||||
"@ledgerhq/errors@^5.34.0":
|
||||
version "5.34.0"
|
||||
resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-5.34.0.tgz#c003b0929f8ca0c74901a11a88b3e3db4b40e4b7"
|
||||
integrity sha512-8Td60sOP5wzsjmxmj9Q5hjrr1XjCfB3Vdifq+1fiD6rpjyscYLgmoP2y5GYDPceDhalA/GOrKNSf+aIVVmBNOw==
|
||||
|
||||
"@ledgerhq/hw-transport-webusb@^5.34.0":
|
||||
version "5.34.0"
|
||||
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-webusb/-/hw-transport-webusb-5.34.0.tgz#8947502fee7b959d050976eb508984bb32cc9a95"
|
||||
integrity sha512-GXzirgAtOZD7kV1QiJ3e7UoN8br9yt8WDCnYwXHiulJmO21biT186L7X8J3folQJrqohJ6UgUncYamtp1JflSA==
|
||||
dependencies:
|
||||
"@ledgerhq/devices" "^5.34.0"
|
||||
"@ledgerhq/errors" "^5.34.0"
|
||||
"@ledgerhq/hw-transport" "^5.34.0"
|
||||
"@ledgerhq/logs" "^5.30.0"
|
||||
|
||||
"@ledgerhq/hw-transport@^5.34.0":
|
||||
version "5.34.0"
|
||||
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-5.34.0.tgz#b27eb0d9941a2e6220e5af3e3d4f48ce8a993982"
|
||||
integrity sha512-JxhqU9sBn+WH3CPMus9b70SED7LEeW17xw0VL5aRdxFu4YS5rhy5Xf4Sd5jIQfyDkHik1ouzfJVuQEju8+GGBw==
|
||||
dependencies:
|
||||
"@ledgerhq/devices" "^5.34.0"
|
||||
"@ledgerhq/errors" "^5.34.0"
|
||||
events "^3.2.0"
|
||||
|
||||
"@ledgerhq/logs@^5.30.0":
|
||||
version "5.30.0"
|
||||
resolved "https://registry.yarnpkg.com/@ledgerhq/logs/-/logs-5.30.0.tgz#76e8d7e5a06a73c9b99da51fa5befd5cfd5309d4"
|
||||
integrity sha512-wUhg2VTfUrWihjdGqKkH/s7TBzdIM1yyd2LiscYsfTX2I0xYDMnpE+NkMReeGU8PN3QhCPgnlg9/P9V6UWoJBA==
|
||||
|
||||
"@material-ui/core@^4.11.0":
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.11.0.tgz#b69b26e4553c9e53f2bfaf1053e216a0af9be15a"
|
||||
|
@ -5151,7 +5189,7 @@ eventemitter3@^4.0.7:
|
|||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
|
||||
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
|
||||
|
||||
events@^3.0.0:
|
||||
events@^3.0.0, events@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379"
|
||||
integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==
|
||||
|
@ -10766,6 +10804,13 @@ rxjs@^6.5.3, rxjs@^6.6.0:
|
|||
dependencies:
|
||||
tslib "^1.9.0"
|
||||
|
||||
rxjs@^6.6.3:
|
||||
version "6.6.3"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552"
|
||||
integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==
|
||||
dependencies:
|
||||
tslib "^1.9.0"
|
||||
|
||||
safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
|
|
Loading…
Reference in New Issue