From 18f8194d5aa07424103641c8e548fa37a625da69 Mon Sep 17 00:00:00 2001 From: Gary Wang Date: Thu, 30 Jul 2020 20:38:34 -0700 Subject: [PATCH] Add dialog for sending tokens --- src/components/BalancesList.js | 19 ++++++- src/components/SendDialog.js | 91 +++++++++++++++++++++++++++++++++ src/utils/token-instructions.js | 15 ++++++ src/utils/tokens.js | 28 +++++++++- src/utils/wallet.js | 13 ++++- 5 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 src/components/SendDialog.js diff --git a/src/components/BalancesList.js b/src/components/BalancesList.js index d2aec61..64acc98 100644 --- a/src/components/BalancesList.js +++ b/src/components/BalancesList.js @@ -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() { resourceCache(wallet.getAccountBalance).clear()} + style={{ marginRight: -12 }} > @@ -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 - @@ -169,6 +178,12 @@ function BalanceListItem({ index }) { + setSendDialogOpen(false)} + balanceInfo={balanceInfo} + index={index} + /> ); } diff --git a/src/components/SendDialog.js b/src/components/SendDialog.js new file mode 100644 index 0000000..dad2ce5 --- /dev/null +++ b/src/components/SendDialog.js @@ -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 ( + + + Send {tokenName ?? abbreviateAddress(mint)} + {tokenTicker ? ` (${tokenTicker})` : null} + + + setDestinationAddress(e.target.value.trim())} + /> + {tokenTicker} + ) : null, + inputProps: { + step: Math.pow(10, -decimals), + }, + }} + value={transferAmountString} + onChange={(e) => setTransferAmountString(e.target.value.trim())} + helperText={`Max: ${balanceAmount / Math.pow(10, decimals)}`} + /> + + + + + + + ); +} diff --git a/src/utils/token-instructions.js b/src/utils/token-instructions.js index faa0979..c64ffbe 100644 --- a/src/utils/token-instructions.js +++ b/src/utils/token-instructions.js @@ -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, + }); +} diff --git a/src/utils/tokens.js b/src/utils/tokens.js index d3e071c..a96e536 100644 --- a/src/utils/tokens.js +++ b/src/utils/tokens.js @@ -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, + }); +} diff --git a/src/utils/wallet.js b/src/utils/wallet.js index 39366ce..756825a 100644 --- a/src/utils/wallet.js +++ b/src/utils/wallet.js @@ -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);