From 278c92408793ee3c8112cec94ae34b759e92dd3f Mon Sep 17 00:00:00 2001 From: Philippe Maes Date: Fri, 25 Sep 2020 15:33:27 +0200 Subject: [PATCH 1/4] Transfer token function --- src/utils/send.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/utils/send.js b/src/utils/send.js index 2c427f782..be03a0abe 100644 --- a/src/utils/send.js +++ b/src/utils/send.js @@ -8,6 +8,49 @@ import { } from '@solana/web3.js'; import { TOKEN_MINTS, TokenInstructions } from '@project-serum/serum'; +export async function transferToken({ source, destination, amount, owner }) { + let transaction = source.equals(owner) + ? SystemProgram.transfer({ + fromPubkey: owner, + toPubkey: destination, + lamports: amount, + }) + : new Transaction().add( + TokenInstructions.transfer({ + source, + destination, + owner, + amount, + }), + ); + const onConfirm = (result) => { + if (result.timeout) { + notify({ + message: 'Timed out', + type: 'error', + description: 'Timed out awaiting confirmation on transaction', + }); + } else if (result.err) { + console.log(result.err); + notify({ message: 'Error transferring tokens', type: 'error' }); + } else { + notify({ message: 'Token transfer confirmed', type: 'success' }); + onSuccess && onSuccess(); + } + }; + const onBeforeSend = () => notify({ message: 'Transferring tokens...' }); + const onAfterSend = () => + notify({ message: 'Tokens transferred', type: 'success' }); + return await sendTransaction({ + transaction, + wallet, + connection, + onBeforeSend, + onAfterSend, + onConfirm, + }); +} + export async function createTokenAccountTransaction({ connection, wallet, From 9aeef807f539c76e47c5da6b98b6ad2936b25e4d Mon Sep 17 00:00:00 2001 From: Philippe Maes Date: Fri, 25 Sep 2020 16:38:08 +0200 Subject: [PATCH 2/4] Transfer tokens UI (WIP) --- src/components/StandaloneBalancesDisplay.jsx | 41 ++++++++-- src/components/TransferDialog.jsx | 81 ++++++++++++++++++++ src/utils/send.js | 31 ++++++-- src/utils/tokens.js | 15 ++++ 4 files changed, 157 insertions(+), 11 deletions(-) create mode 100644 src/components/TransferDialog.jsx diff --git a/src/components/StandaloneBalancesDisplay.jsx b/src/components/StandaloneBalancesDisplay.jsx index 5d53ce3df..4cf7d7792 100644 --- a/src/components/StandaloneBalancesDisplay.jsx +++ b/src/components/StandaloneBalancesDisplay.jsx @@ -10,6 +10,7 @@ import { useSelectedQuoteCurrencyAccount, } from '../utils/markets'; import DepositDialog from './DepositDialog'; +import TransferDialog from './TransferDialog'; import { useWallet } from '../utils/wallet'; import Link from './Link'; import { settleFunds } from '../utils/send'; @@ -37,6 +38,7 @@ export default function StandaloneBalancesDisplay() { const connection = useSendConnection(); const { providerUrl, providerName, wallet } = useWallet(); const [depositCoin, setDepositCoin] = useState(''); + const [transferCoin, setTransferCoin] = useState(null); const baseCurrencyAccount = useSelectedBaseCurrencyAccount(); const quoteCurrencyAccount = useSelectedQuoteCurrencyAccount(); const baseCurrencyBalances = @@ -58,9 +60,19 @@ export default function StandaloneBalancesDisplay() { return ( {[ - [baseCurrency, baseCurrencyBalances], - [quoteCurrency, quoteCurrencyBalances], - ].map(([currency, balances], index) => ( + [ + market?.baseMintAddress, + baseCurrency, + baseCurrencyAccount, + baseCurrencyBalances, + ], + [ + market?.quoteMintAddress, + quoteCurrency, + quoteCurrencyAccount, + quoteCurrencyBalances, + ], + ].map(([mint, currency, account, balances], index) => ( {currency} {balances && balances.unsettled} - + - + + + setTransferCoin({ + coin: currency, + source: account?.pubkey, + mint, + }) + } + > + Transfer + + + Settle @@ -108,6 +135,10 @@ export default function StandaloneBalancesDisplay() { depositCoin={depositCoin} onClose={() => setDepositCoin('')} /> + setTransferCoin(null)} + /> ); } diff --git a/src/components/TransferDialog.jsx b/src/components/TransferDialog.jsx new file mode 100644 index 000000000..13d75a0d0 --- /dev/null +++ b/src/components/TransferDialog.jsx @@ -0,0 +1,81 @@ +import React, { useState } from 'react'; +import { Modal, Input } from 'antd'; +import { useWallet } from '../utils/wallet'; +import { useConnection } from '../utils/connection'; +import { isValidPublicKey } from '../utils/utils'; +import { transferToken } from '../utils/send'; +import { notify } from '../utils/notifications'; +import { PublicKey } from '@solana/web3.js'; +import { getMintDecimals } from '../utils/tokens'; + +export default function TransferDialog({ onClose, transferCoin }) { + const { connected, wallet } = useWallet(); + const connection = useConnection(); + + const [destination, setDestination] = useState(''); + const [amount, setAmount] = useState(0); + const [transferInProgress, setTransferInProgress] = useState(false); + + const onDoClose = () => { + setDestination(''); + setAmount(0); + onClose(); + }; + + const onTransfer = async () => { + try { + const decimals = await getMintDecimals(connection, transferCoin?.mint); + if (!decimals) { + notify({ message: 'Could not get source data', type: 'error' }); + return; + } + + const parsedAmount = Math.round(amount * 10 ** decimals); + await transferToken({ + wallet, + connection, + source: transferCoin?.source, + destination: new PublicKey(destination), + amount: parsedAmount, + onBeforeSendCallBack: () => setTransferInProgress(true), + onConfirmCallBack: (err) => { + setTransferInProgress(false); + if (!err) onDoClose(); + }, + }); + } catch (e) { + notify({ + message: 'Error transferring tokens: ' + e.message, + type: 'error', + }); + setTransferInProgress(false); + } + }; + + const canSubmit = connected && isValidPublicKey(destination) && amount > 0; + + return ( + +
+ setDestination(e.target.value)} + /> + setAmount(e.target.value)} + type="number" + /> +
+
+ ); +} diff --git a/src/utils/send.js b/src/utils/send.js index be03a0abe..b54c624b1 100644 --- a/src/utils/send.js +++ b/src/utils/send.js @@ -8,10 +8,26 @@ import { } from '@solana/web3.js'; import { TOKEN_MINTS, TokenInstructions } from '@project-serum/serum'; -export async function transferToken({ source, destination, amount, owner }) { - let transaction = source.equals(owner) +export async function transferToken({ + wallet, + connection, + source, + destination, + amount, + onBeforeSendCallBack, + onConfirmCallBack, +}) { + console.log( + JSON.stringify({ + wallet: wallet.publicKey, + source, + destination, + amount, + }), + ); + let transaction = source.equals(wallet.publicKey) ? SystemProgram.transfer({ - fromPubkey: owner, + fromPubkey: wallet.publicKey, toPubkey: destination, lamports: amount, }) @@ -19,7 +35,7 @@ export async function transferToken({ source, destination, amount, owner }) { TokenInstructions.transfer({ source, destination, - owner, + owner: wallet.publicKey, amount, }), ); @@ -35,10 +51,13 @@ export async function transferToken({ source, destination, amount, owner }) { notify({ message: 'Error transferring tokens', type: 'error' }); } else { notify({ message: 'Token transfer confirmed', type: 'success' }); - onSuccess && onSuccess(); } + onConfirmCallBack && onConfirmCallBack(result.err || result.timeout); + }; + const onBeforeSend = () => { + notify({ message: 'Transferring tokens...' }); + onBeforeSendCallBack && onBeforeSendCallBack(); }; - const onBeforeSend = () => notify({ message: 'Transferring tokens...' }); const onAfterSend = () => notify({ message: 'Tokens transferred', type: 'success' }); return await sendTransaction({ diff --git a/src/utils/tokens.js b/src/utils/tokens.js index 63ad1d4ff..0b859ca6d 100644 --- a/src/utils/tokens.js +++ b/src/utils/tokens.js @@ -10,6 +10,12 @@ export const ACCOUNT_LAYOUT = BufferLayout.struct([ BufferLayout.blob(93), ]); +export const MINT_LAYOUT = BufferLayout.struct([ + BufferLayout.blob(44), + BufferLayout.u8('decimals'), + BufferLayout.blob(37), +]); + export function parseTokenAccountData(data) { let { mint, owner, amount } = ACCOUNT_LAYOUT.decode(data); return { @@ -19,6 +25,15 @@ export function parseTokenAccountData(data) { }; } +export async function getMintDecimals(connection, publicKey) { + const mintInfo = await connection.getAccountInfo(publicKey); + if (!mintInfo?.data) { + return; + } + let { decimals } = MINT_LAYOUT.decode(mintInfo.data); + return decimals; +} + export function getOwnedAccountsFilters(publicKey) { return [ { From c8e41823e057abf1b1c65a15c07f859d8882883f Mon Sep 17 00:00:00 2001 From: Philippe Maes Date: Sat, 26 Sep 2020 13:15:39 +0200 Subject: [PATCH 3/4] Cleanup up transfer UI --- src/components/StandaloneBalancesDisplay.jsx | 66 ++++++++++++++------ src/components/TopBar.js | 11 +--- src/components/TransferDialog.jsx | 57 ++++++++++++++--- src/components/WalletConnect.jsx | 20 ++++++ 4 files changed, 118 insertions(+), 36 deletions(-) create mode 100644 src/components/WalletConnect.jsx diff --git a/src/components/StandaloneBalancesDisplay.jsx b/src/components/StandaloneBalancesDisplay.jsx index 4cf7d7792..f7106e685 100644 --- a/src/components/StandaloneBalancesDisplay.jsx +++ b/src/components/StandaloneBalancesDisplay.jsx @@ -15,6 +15,7 @@ import { useWallet } from '../utils/wallet'; import Link from './Link'; import { settleFunds } from '../utils/send'; import { useSendConnection } from '../utils/connection'; +import WalletConnect from './WalletConnect'; const RowBox = styled(Row)` padding-bottom: 20px; @@ -25,6 +26,10 @@ const Tip = styled.p` padding-top: 6px; `; +const Label = styled.span` + color: rgba(255, 255, 255, 0.5); +`; + const ActionButton = styled(Button)` color: #2abdd2; background-color: #212734; @@ -36,7 +41,7 @@ export default function StandaloneBalancesDisplay() { const balances = useBalances(); const openOrdersAccount = useSelectedOpenOrdersAccount(true); const connection = useSendConnection(); - const { providerUrl, providerName, wallet } = useWallet(); + const { providerUrl, providerName, wallet, connected } = useWallet(); const [depositCoin, setDepositCoin] = useState(''); const [transferCoin, setTransferCoin] = useState(null); const baseCurrencyAccount = useSelectedBaseCurrencyAccount(); @@ -75,27 +80,46 @@ export default function StandaloneBalancesDisplay() { ].map(([mint, currency, account, balances], index) => ( {currency} - - Wallet balance: - {balances && balances.wallet} - - - Unsettled balance: - {balances && balances.unsettled} - + {connected ? ( + <> + + + + + {balances && balances.wallet} + + + + + + {balances && balances.unsettled} + + + ) : ( +
+ +
+ )} setDepositCoin(currency)} > Deposit @@ -105,6 +129,7 @@ export default function StandaloneBalancesDisplay() { setTransferCoin({ coin: currency, @@ -117,7 +142,12 @@ export default function StandaloneBalancesDisplay() { - + Settle diff --git a/src/components/TopBar.js b/src/components/TopBar.js index a8d2d4431..9b1121d94 100644 --- a/src/components/TopBar.js +++ b/src/components/TopBar.js @@ -7,6 +7,7 @@ import styled from 'styled-components'; import { useWallet, WALLET_PROVIDERS } from '../utils/wallet'; import { ENDPOINTS, useConnectionConfig } from '../utils/connection'; import LinkAddress from './LinkAddress'; +import WalletConnect from './WalletConnect'; const Wrapper = styled.div` background-color: #0d1017; @@ -95,15 +96,7 @@ export default function TopBar() {
- + {connected && ( } diff --git a/src/components/TransferDialog.jsx b/src/components/TransferDialog.jsx index 13d75a0d0..6d48ad191 100644 --- a/src/components/TransferDialog.jsx +++ b/src/components/TransferDialog.jsx @@ -1,5 +1,7 @@ import React, { useState } from 'react'; -import { Modal, Input } from 'antd'; +import styled from 'styled-components'; +import { Modal, Input, Button, Row, Col, Typography } from 'antd'; +import { useBalances } from '../utils/markets'; import { useWallet } from '../utils/wallet'; import { useConnection } from '../utils/connection'; import { isValidPublicKey } from '../utils/utils'; @@ -8,17 +10,33 @@ import { notify } from '../utils/notifications'; import { PublicKey } from '@solana/web3.js'; import { getMintDecimals } from '../utils/tokens'; +const { Text } = Typography; + +const InputField = styled(Input)` + margin-bottom: 8px; +`; + +const ActionButton = styled(Button)` + color: #2abdd2; + background-color: #212734; + border-width: 0px; +`; + export default function TransferDialog({ onClose, transferCoin }) { const { connected, wallet } = useWallet(); const connection = useConnection(); + const balances = useBalances(); - const [destination, setDestination] = useState(''); - const [amount, setAmount] = useState(0); + const [destination, setDestination] = useState(null); + const [amount, setAmount] = useState(null); const [transferInProgress, setTransferInProgress] = useState(false); + const balance = + balances && balances.find((b) => b.coin === transferCoin?.coin)?.wallet; + const onDoClose = () => { - setDestination(''); - setAmount(0); + setDestination(null); + setAmount(null); onClose(); }; @@ -52,7 +70,11 @@ export default function TransferDialog({ onClose, transferCoin }) { } }; - const canSubmit = connected && isValidPublicKey(destination) && amount > 0; + const canSubmit = + connected && + isValidPublicKey(destination) && + amount > 0 && + amount <= balance; return (
- + + Invalid address + + + )} + setDestination(e.target.value)} /> - balance && ( + + + Not enough balances + + + )} + setAmount(e.target.value)} type="number" /> + setAmount(balance)}> + Max: {balance} +
); diff --git a/src/components/WalletConnect.jsx b/src/components/WalletConnect.jsx new file mode 100644 index 000000000..6faf0f19a --- /dev/null +++ b/src/components/WalletConnect.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Button } from 'antd'; +import { UserOutlined } from '@ant-design/icons'; +import { useWallet } from '../utils/wallet'; + +export default function WalletConnect() { + const { connected, wallet } = useWallet(); + + return ( + + ); +} From b1ed18b9646dd390391d4e189e7edccf6045e76b Mon Sep 17 00:00:00 2001 From: Philippe Maes Date: Sat, 26 Sep 2020 15:32:15 +0200 Subject: [PATCH 4/4] QR code + generate deposit account --- src/components/DepositDialog.jsx | 83 +++++++++++++++++++++++--------- src/utils/send.js | 45 +++++++++++++++++ 2 files changed, 105 insertions(+), 23 deletions(-) diff --git a/src/components/DepositDialog.jsx b/src/components/DepositDialog.jsx index 326119bc8..18c173c40 100644 --- a/src/components/DepositDialog.jsx +++ b/src/components/DepositDialog.jsx @@ -1,5 +1,6 @@ -import React from 'react'; -import { Modal } from 'antd'; +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { Modal, Button } from 'antd'; import { useSelectedBaseCurrencyAccount, useMarket, @@ -7,16 +8,48 @@ import { } from '../utils/markets'; import { TOKEN_MINTS } from '@project-serum/serum'; import { useWallet } from '../utils/wallet'; -import Link from './Link'; +import { useConnection } from '../utils/connection'; +import { createTokenAccount } from '../utils/send'; +import QRCode from 'qrcode.react'; +import WalletConnect from './WalletConnect'; +import { notify } from '../utils/notifications'; + +const ActionButton = styled(Button)` + color: #2abdd2; + background-color: #212734; + border-width: 0px; +`; export default function DepositDialog({ onClose, depositCoin }) { let coinMint = depositCoin && TOKEN_MINTS.find(({ name }) => name === depositCoin)?.address; const { market } = useMarket(); - const { providerName, providerUrl } = useWallet(); + const { connected, wallet } = useWallet(); + const connection = useConnection(); const baseCurrencyAccount = useSelectedBaseCurrencyAccount(); const quoteCurrencyAccount = useSelectedQuoteCurrencyAccount(); + + const [isCreatingTokenAccount, setIsCreatingTokenAccount] = useState(false); + + const doCreateTokenAccount = async () => { + try { + await createTokenAccount({ + wallet, + connection, + mintPublicKey: coinMint, + onBeforeSendCallBack: () => setIsCreatingTokenAccount(true), + onConfirmCallBack: () => setIsCreatingTokenAccount(false), + }); + } catch (e) { + notify({ + message: 'Error creating token account: ' + e.message, + type: 'error', + }); + setIsCreatingTokenAccount(false); + } + }; + if (!coinMint) { return null; } @@ -37,25 +70,29 @@ export default function DepositDialog({ onClose, depositCoin }) { onOk={onClose} onCancel={onClose} > -
-

Mint address:

-

{coinMint.toBase58()}

-
-

Deposit address:

-

- {account ? ( - account.pubkey.toBase58() - ) : ( - <> - Visit{' '} - - {providerName} - {' '} - to create an account for this mint - - )} -

-
+
+

Deposit address:

+

+ {account ? ( + account.pubkey.toBase58() + ) : ( + <> + {connected ? ( + + Create token account + + ) : ( + + )} + + )} +

+ {account && }
); diff --git a/src/utils/send.js b/src/utils/send.js index b54c624b1..832f14d39 100644 --- a/src/utils/send.js +++ b/src/utils/send.js @@ -97,6 +97,51 @@ export async function createTokenAccountTransaction({ }; } +export async function createTokenAccount({ + connection, + wallet, + mintPublicKey, + onBeforeSendCallBack, + onConfirmCallBack, +}) { + const { transaction, signer } = await createTokenAccountTransaction({ + connection, + wallet, + mintPublicKey, + }); + + const onConfirm = (result) => { + if (result.timeout) { + notify({ + message: 'Timed out', + type: 'error', + description: 'Timed out awaiting confirmation on transaction', + }); + } else if (result.err) { + console.log(result.err); + notify({ message: 'Error creating token account', type: 'error' }); + } else { + notify({ message: 'Token account creation confirmed', type: 'success' }); + } + onConfirmCallBack && onConfirmCallBack(); + }; + const onBeforeSend = () => { + notify({ message: 'Creating token account...' }); + onBeforeSendCallBack && onBeforeSendCallBack(); + }; + const onAfterSend = () => + notify({ message: 'Token account created', type: 'success' }); + return await sendTransaction({ + transaction, + signers: [signer, wallet.publicKey], + wallet, + connection, + onBeforeSend, + onAfterSend, + onConfirm, + }); +} + export async function settleFunds({ market, openOrders,