Add dialog for sending tokens

This commit is contained in:
Gary Wang 2020-07-30 20:38:34 -07:00
parent a3dddb7b77
commit 18f8194d5a
5 changed files with 162 additions and 4 deletions

View File

@ -24,6 +24,7 @@ import IconButton from '@material-ui/core/IconButton';
import Tooltip from '@material-ui/core/Tooltip';
import { preloadResource } from 'use-async-resource/lib';
import AddTokenDialog from './AddTokenDialog';
import SendDialog from './SendDialog';
const balanceFormat = new Intl.NumberFormat(undefined, {
minimumFractionDigits: 4,
@ -56,6 +57,7 @@ export default function BalancesList() {
<Tooltip title="Refresh" arrow>
<IconButton
onClick={() => resourceCache(wallet.getAccountBalance).clear()}
style={{ marginRight: -12 }}
>
<RefreshIcon />
</IconButton>
@ -100,8 +102,10 @@ function BalanceListItem({ index }) {
const [getBalance] = useAsyncResource(wallet.getAccountBalance, index);
const [open, setOpen] = useState(false);
const classes = useStyles();
const [sendDialogOpen, setSendDialogOpen] = useState(false);
const account = wallet.getAccount(index);
const balanceInfo = getBalance();
let {
amount,
decimals,
@ -109,7 +113,7 @@ function BalanceListItem({ index }) {
tokenName,
tokenTicker,
initialized,
} = getBalance();
} = balanceInfo;
if (!initialized && index !== 0) {
return null;
@ -140,7 +144,12 @@ function BalanceListItem({ index }) {
>
Receive
</Button>
<Button variant="outlined" color="primary" startIcon={<SendIcon />}>
<Button
variant="outlined"
color="primary"
startIcon={<SendIcon />}
onClick={() => setSendDialogOpen(true)}
>
Send
</Button>
</div>
@ -169,6 +178,12 @@ function BalanceListItem({ index }) {
</Typography>
</div>
</Collapse>
<SendDialog
open={sendDialogOpen}
onClose={() => setSendDialogOpen(false)}
balanceInfo={balanceInfo}
index={index}
/>
</>
);
}

View File

@ -0,0 +1,91 @@
import React, { 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 TextField from '@material-ui/core/TextField';
import DialogForm from './DialogForm';
import { useWallet } from '../utils/wallet';
import { PublicKey } from '@solana/web3.js';
import { abbreviateAddress } from '../utils/utils';
import InputAdornment from '@material-ui/core/InputAdornment';
export default function SendDialog({ open, onClose, index, balanceInfo }) {
const wallet = useWallet();
const [destinationAddress, setDestinationAddress] = useState('');
const [transferAmountString, setTransferAmountString] = useState('');
const [submitting, setSubmitting] = useState(false);
let {
amount: balanceAmount,
decimals,
mint,
tokenName,
tokenTicker,
} = balanceInfo;
async function onSubmit() {
setSubmitting(true);
try {
let amount = Math.round(
parseFloat(transferAmountString) * Math.pow(10, decimals),
);
if (!amount || amount <= 0) {
throw new Error('Invalid amount');
}
await wallet.transferToken(
index,
new PublicKey(destinationAddress),
amount,
);
onClose();
} catch (e) {
console.warn(e);
} finally {
setSubmitting(false);
}
}
return (
<DialogForm open={open} onClose={onClose} onSubmit={onSubmit}>
<DialogTitle>
Send {tokenName ?? abbreviateAddress(mint)}
{tokenTicker ? ` (${tokenTicker})` : null}
</DialogTitle>
<DialogContent>
<TextField
label="Recipient Address"
fullWidth
variant="outlined"
margin="normal"
value={destinationAddress}
onChange={(e) => setDestinationAddress(e.target.value.trim())}
/>
<TextField
label="Amount"
fullWidth
variant="outlined"
margin="normal"
type="number"
InputProps={{
endAdornment: tokenTicker ? (
<InputAdornment position="end">{tokenTicker}</InputAdornment>
) : null,
inputProps: {
step: Math.pow(10, -decimals),
},
}}
value={transferAmountString}
onChange={(e) => setTransferAmountString(e.target.value.trim())}
helperText={`Max: ${balanceAmount / Math.pow(10, decimals)}`}
/>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Cancel</Button>
<Button type="submit" color="primary" disabled={submitting}>
Send
</Button>
</DialogActions>
</DialogForm>
);
}

View File

@ -82,3 +82,18 @@ export function initializeAccount({ account, mint, owner }) {
programId: TOKEN_PROGRAM_ID,
});
}
export function transfer({ source, destination, amount, owner }) {
let keys = [
{ pubkey: source, isSigner: false, isWritable: true },
{ pubkey: destination, isSigner: false, isWritable: true },
{ pubkey: owner, isSigner: true, isWritable: false },
];
return new TransactionInstruction({
keys,
data: encodeTokenInstructionData({
transfer: { amount },
}),
programId: TOKEN_PROGRAM_ID,
});
}

View File

@ -1,8 +1,13 @@
import { sendAndConfirmTransaction, SystemProgram } from '@solana/web3.js';
import {
sendAndConfirmTransaction,
SystemProgram,
Transaction,
} from '@solana/web3.js';
import {
initializeAccount,
initializeMint,
TOKEN_PROGRAM_ID,
transfer,
} from './token-instructions';
import { ACCOUNT_LAYOUT, MINT_LAYOUT } from './token-state';
@ -87,3 +92,24 @@ export async function createAndInitializeTokenAccount({
confirmations: 1,
});
}
export async function transferTokens({
connection,
owner,
sourcePublicKey,
destinationPublicKey,
amount,
}) {
let transaction = new Transaction().add(
transfer({
source: sourcePublicKey,
destination: destinationPublicKey,
owner: owner.publicKey,
amount,
}),
);
let signers = [owner];
return await sendAndConfirmTransaction(connection, transaction, signers, {
confirmations: 1,
});
}

View File

@ -3,7 +3,7 @@ import * as bip32 from 'bip32';
import { Account } from '@solana/web3.js';
import nacl from 'tweetnacl';
import { useConnection } from './connection';
import { createAndInitializeTokenAccount } from './tokens';
import { createAndInitializeTokenAccount, transferTokens } from './tokens';
import { resourceCache } from 'use-async-resource';
import { TOKEN_PROGRAM_ID } from './token-instructions';
import {
@ -90,6 +90,17 @@ export class Wallet {
ACCOUNT_LAYOUT.span,
);
};
transferToken = async (index, destination, amount) => {
let tokenAccount = this.getAccount(index);
await transferTokens({
connection: this.connection,
owner: this.account,
sourcePublicKey: tokenAccount.publicKey,
destinationPublicKey: destination,
amount,
});
};
}
const WalletContext = React.createContext(null);