Add button for adding tokens
This commit is contained in:
parent
f85d37c8cb
commit
a3dddb7b77
|
@ -28,7 +28,7 @@
|
|||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
|
||||
/>
|
||||
<title>Solana SPL Wallet</title>
|
||||
<title>Solana SPL Token Wallet</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"short_name": "Solana SPL Wallet",
|
||||
"name": "Solana SPL Wallet",
|
||||
"short_name": "Solana Wallet",
|
||||
"name": "Solana SPL Token Wallet",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
import React, { useState } from 'react';
|
||||
import DialogContentText from '@material-ui/core/DialogContentText';
|
||||
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 TextField from '@material-ui/core/TextField';
|
||||
import DialogForm from './DialogForm';
|
||||
import { useAsyncResource } from 'use-async-resource/lib';
|
||||
import { useWallet } from '../utils/wallet';
|
||||
import { LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js';
|
||||
|
||||
const feeFormat = new Intl.NumberFormat(undefined, {
|
||||
minimumFractionDigits: 6,
|
||||
maximumFractionDigits: 6,
|
||||
});
|
||||
|
||||
export default function AddTokenDialog({ open, onClose }) {
|
||||
let wallet = useWallet();
|
||||
let [getCost] = useAsyncResource(wallet.tokenAccountCost, []);
|
||||
|
||||
let [mintAddress, setMintAddress] = useState('');
|
||||
let [tokenName, setTokenName] = useState('');
|
||||
let [tokenSymbol, setTokenSymbol] = useState('');
|
||||
let [submitting, setSubmitting] = useState(false);
|
||||
|
||||
async function onSubmit() {
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await wallet.createTokenAccount(new PublicKey(mintAddress));
|
||||
onClose();
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogForm open={open} onClose={onClose} onSubmit={onSubmit}>
|
||||
<DialogTitle>Add Token</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Add a token to your wallet. This will cost{' '}
|
||||
{feeFormat.format(getCost() / LAMPORTS_PER_SOL)} Solana.
|
||||
</DialogContentText>
|
||||
<TextField
|
||||
label="Token Mint Address"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
value={mintAddress}
|
||||
onChange={(e) => setMintAddress(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
label="Token Name"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
value={tokenName}
|
||||
onChange={(e) => setTokenName(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
label="Token Symbol"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
value={tokenSymbol}
|
||||
onChange={(e) => setTokenSymbol(e.target.value)}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<Button type="submit" color="primary" disabled={submitting}>
|
||||
Add
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogForm>
|
||||
);
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
import React, { Suspense, useState } from 'react';
|
||||
import React, { Suspense, useEffect, useState } from 'react';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import { useWallet } from '../utils/wallet';
|
||||
import { useAsyncResource } from 'use-async-resource';
|
||||
import { useWallet, useWalletAccountCount } from '../utils/wallet';
|
||||
import { resourceCache, useAsyncResource } from 'use-async-resource';
|
||||
import LoadingIndicator from './LoadingIndicator';
|
||||
import Collapse from '@material-ui/core/Collapse';
|
||||
import { Typography } from '@material-ui/core';
|
||||
|
@ -21,8 +21,9 @@ import Toolbar from '@material-ui/core/Toolbar';
|
|||
import AddIcon from '@material-ui/icons/Add';
|
||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import { resourceCache } from 'use-async-resource';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import { preloadResource } from 'use-async-resource/lib';
|
||||
import AddTokenDialog from './AddTokenDialog';
|
||||
|
||||
const balanceFormat = new Intl.NumberFormat(undefined, {
|
||||
minimumFractionDigits: 4,
|
||||
|
@ -32,6 +33,13 @@ const balanceFormat = new Intl.NumberFormat(undefined, {
|
|||
|
||||
export default function BalancesList() {
|
||||
const wallet = useWallet();
|
||||
const accountCount = useWalletAccountCount();
|
||||
useEffect(() => {
|
||||
for (let i = 0; i < accountCount + 5; ++i) {
|
||||
preloadResource(wallet.getAccountBalance, i);
|
||||
}
|
||||
}, [wallet, accountCount]);
|
||||
const [showAddTokenDialog, setShowAddTokenDialog] = useState(false);
|
||||
|
||||
return (
|
||||
<Paper>
|
||||
|
@ -41,7 +49,7 @@ export default function BalancesList() {
|
|||
Balances
|
||||
</Typography>
|
||||
<Tooltip title="Add Token" arrow>
|
||||
<IconButton>
|
||||
<IconButton onClick={() => setShowAddTokenDialog(true)}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
@ -55,12 +63,16 @@ export default function BalancesList() {
|
|||
</Toolbar>
|
||||
</AppBar>
|
||||
<List disablePadding>
|
||||
{[...Array(wallet.accountCount + 1).keys()].map((i) => (
|
||||
{[...Array(accountCount + 1).keys()].map((i) => (
|
||||
<Suspense key={i} fallback={<LoadingIndicator />}>
|
||||
<BalanceListItem index={i} />
|
||||
</Suspense>
|
||||
))}
|
||||
</List>
|
||||
<AddTokenDialog
|
||||
open={showAddTokenDialog}
|
||||
onClose={() => setShowAddTokenDialog(false)}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import React from 'react';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
|
||||
export default function DialogForm({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
children,
|
||||
...rest
|
||||
}) {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
PaperProps={{
|
||||
component: 'form',
|
||||
onSubmit: (e) => {
|
||||
e.preventDefault();
|
||||
if (onSubmit) {
|
||||
onSubmit();
|
||||
}
|
||||
},
|
||||
}}
|
||||
onClose={onClose}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
|
@ -17,7 +17,7 @@ export default function NavigationFrame({ children }) {
|
|||
<>
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<Typography variant="h6">Solana SPL Wallet</Typography>
|
||||
<Typography variant="h6">Solana SPL Token Wallet</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<main className={classes.content}>{children}</main>
|
||||
|
|
|
@ -39,6 +39,15 @@ export function useEffectAfterTimeout(effect, timeout) {
|
|||
});
|
||||
}
|
||||
|
||||
export function useListener(emitter, eventName) {
|
||||
let [, forceUpdate] = useState(0);
|
||||
useEffect(() => {
|
||||
let listener = () => forceUpdate((i) => i + 1);
|
||||
emitter.on(eventName, listener);
|
||||
return () => emitter.removeListener(eventName, listener);
|
||||
}, [emitter, eventName]);
|
||||
}
|
||||
|
||||
export function abbreviateAddress(address) {
|
||||
let base58 = address.toBase58();
|
||||
return base58.slice(0, 4) + '…' + base58.slice(base58.length - 4);
|
||||
|
|
|
@ -6,7 +6,13 @@ import { useConnection } from './connection';
|
|||
import { createAndInitializeTokenAccount } from './tokens';
|
||||
import { resourceCache } from 'use-async-resource';
|
||||
import { TOKEN_PROGRAM_ID } from './token-instructions';
|
||||
import { parseMintData, parseTokenAccountData } from './token-state';
|
||||
import {
|
||||
ACCOUNT_LAYOUT,
|
||||
parseMintData,
|
||||
parseTokenAccountData,
|
||||
} from './token-state';
|
||||
import EventEmitter from 'events';
|
||||
import { useListener } from './utils';
|
||||
|
||||
export class Wallet {
|
||||
constructor(connection, seed, walletIndex = 0) {
|
||||
|
@ -15,6 +21,8 @@ export class Wallet {
|
|||
this.walletIndex = walletIndex;
|
||||
this.accountCount = 1;
|
||||
this.account = this.getAccount(0);
|
||||
|
||||
this.emitter = new EventEmitter();
|
||||
}
|
||||
|
||||
getAccount = (index) => {
|
||||
|
@ -30,6 +38,7 @@ export class Wallet {
|
|||
|
||||
if (info && this.accountCount < index + 1) {
|
||||
this.accountCount = index + 1;
|
||||
this.emitter.emit('accountCountChange');
|
||||
}
|
||||
|
||||
if (info?.owner.equals(TOKEN_PROGRAM_ID)) {
|
||||
|
@ -73,6 +82,13 @@ export class Wallet {
|
|||
});
|
||||
++this.accountCount;
|
||||
resourceCache(this.getAccountBalance).delete(index);
|
||||
this.emitter.emit('accountCountChange');
|
||||
};
|
||||
|
||||
tokenAccountCost = async () => {
|
||||
return this.connection.getMinimumBalanceForRentExemption(
|
||||
ACCOUNT_LAYOUT.span,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -104,6 +120,12 @@ export function useWallet() {
|
|||
return useContext(WalletContext).wallet;
|
||||
}
|
||||
|
||||
export function useWalletAccountCount() {
|
||||
let wallet = useWallet();
|
||||
useListener(wallet.emitter, 'accountCountChange');
|
||||
return wallet.accountCount;
|
||||
}
|
||||
|
||||
export async function mnemonicToSecretKey(mnemonic) {
|
||||
const { mnemonicToSeed } = await import('bip39');
|
||||
const rootSeed = Buffer.from(await mnemonicToSeed(mnemonic), 'hex');
|
||||
|
|
Loading…
Reference in New Issue