Auto create token accounts for destination wallet

This commit is contained in:
Nathaniel Parke 2020-10-26 19:00:46 +08:00
parent eac4ae3c8d
commit 81a6464665
4 changed files with 216 additions and 6 deletions

View File

@ -28,6 +28,8 @@ import Link from '@material-ui/core/Link';
import Typography from '@material-ui/core/Typography';
import { useAsyncData } from '../utils/fetch-loop';
import CircularProgress from '@material-ui/core/CircularProgress';
import { TOKEN_PROGRAM_ID } from '../utils/tokens/instructions';
import { parseTokenAccountData } from '../utils/tokens/data';
const WUSDC_MINT = new PublicKey(
'BXXkv6z8ykpG1yuvUDPgh732wzVHB69RnB9YgSYh3itW',
@ -54,6 +56,7 @@ export default function SendDialog({ open, onClose, publicKey, balanceInfo }) {
open={open}
onClose={onClose}
onSubmit={() => onSubmitRef.current()}
fullWidth
>
<DialogTitle>
Send {tokenName ?? abbreviateAddress(mint)}
@ -131,15 +134,56 @@ export default function SendDialog({ open, onClose, publicKey, balanceInfo }) {
}
function SendSplDialog({ onClose, publicKey, balanceInfo, onSubmitRef }) {
const defaultAddressHelperText = 'Enter SPL token or Solana address';
const wallet = useWallet();
const [sendTransaction, sending] = useSendTransaction();
const [addressHelperText, setAddressHelperText] = useState(
defaultAddressHelperText,
);
const [passValidation, setPassValidation] = useState();
const {
fields,
destinationAddress,
transferAmountString,
validAmount,
} = useForm(balanceInfo);
const { decimals } = balanceInfo;
} = useForm(balanceInfo, addressHelperText, passValidation);
const { decimals, mint } = balanceInfo;
const mintString = mint && mint.toBase58();
useEffect(() => {
(async () => {
if (!destinationAddress) {
setAddressHelperText(defaultAddressHelperText);
setPassValidation(undefined);
return;
}
try {
const destinationAccountInfo = await wallet.connection.getAccountInfo(
new PublicKey(destinationAddress),
);
if (destinationAccountInfo.owner.equals(TOKEN_PROGRAM_ID)) {
const accountInfo = parseTokenAccountData(
destinationAccountInfo.data,
);
if (accountInfo.mint.toBase58() === mintString) {
setPassValidation(true);
setAddressHelperText('Address is a valid SPL token address');
} else {
setPassValidation(false);
setAddressHelperText('Destination address mint does not match');
}
} else {
setPassValidation(true);
setAddressHelperText('Destination is a Solana address');
}
} catch (e) {
console.log(`Received error validating address ${e}`);
setAddressHelperText(defaultAddressHelperText);
setPassValidation(undefined);
}
})();
}, [destinationAddress, wallet, mintString]);
async function makeTransaction() {
let amount = Math.round(parseFloat(transferAmountString) * 10 ** decimals);
@ -150,6 +194,7 @@ function SendSplDialog({ onClose, publicKey, balanceInfo, onSubmitRef }) {
publicKey,
new PublicKey(destinationAddress),
amount,
balanceInfo.mint,
);
}
@ -243,6 +288,7 @@ function SendSwapDialog({
publicKey,
new PublicKey(swapInfo.address),
amount,
balanceInfo.mint,
swapInfo.memo,
);
}
@ -379,7 +425,7 @@ function SendSwapProgress({ publicKey, signature, onClose, blockchain }) {
);
}
function useForm(balanceInfo) {
function useForm(balanceInfo, addressHelperText, passAddressValidation) {
const [destinationAddress, setDestinationAddress] = useState('');
const [transferAmountString, setTransferAmountString] = useState('');
const { amount: balanceAmount, decimals, tokenSymbol } = balanceInfo;
@ -396,6 +442,13 @@ function useForm(balanceInfo) {
margin="normal"
value={destinationAddress}
onChange={(e) => setDestinationAddress(e.target.value.trim())}
helperText={addressHelperText}
id={
!passAddressValidation && passAddressValidation !== undefined
? 'outlined-error-helper-text'
: undefined
}
error={!passAddressValidation && passAddressValidation !== undefined}
/>
<TextField
label="Amount"

View File

@ -1,5 +1,11 @@
import { PublicKey, SystemProgram, Transaction } from '@solana/web3.js';
import {
PublicKey,
SystemProgram,
Transaction,
Account,
} from '@solana/web3.js';
import {
assertOwner,
closeAccount,
initializeAccount,
initializeMint,
@ -8,7 +14,12 @@ import {
TOKEN_PROGRAM_ID,
transfer,
} from './instructions';
import { ACCOUNT_LAYOUT, getOwnedAccountsFilters, MINT_LAYOUT } from './data';
import {
ACCOUNT_LAYOUT,
getOwnedAccountsFilters,
MINT_LAYOUT,
parseTokenAccountData,
} from './data';
import bs58 from 'bs58';
export async function getOwnedTokenAccounts(connection, publicKey) {
@ -157,6 +168,58 @@ export async function transferTokens({
destinationPublicKey,
amount,
memo,
mint,
}) {
const destinationAccountInfo = await connection.getAccountInfo(
destinationPublicKey,
);
if (destinationAccountInfo.owner.equals(TOKEN_PROGRAM_ID)) {
return await transferBetweenSplTokenAccounts({
connection,
owner,
sourcePublicKey,
destinationPublicKey,
amount,
memo,
});
}
const destinationSplTokenAccount = (
await getOwnedTokenAccounts(connection, destinationPublicKey)
)
.map(({ publicKey, accountInfo }) => {
return { publicKey, parsed: parseTokenAccountData(accountInfo.data) };
})
.filter(({ parsed }) => parsed.mint.equals(mint))
.sort((a, b) => {
return b.parsed.amount - a.parsed.amount;
})[0];
if (destinationSplTokenAccount) {
return await transferBetweenSplTokenAccounts({
connection,
owner,
sourcePublicKey,
destinationPublicKey: destinationSplTokenAccount.publicKey,
amount,
memo,
});
}
return await createAndTransferToAccount({
connection,
owner,
sourcePublicKey,
destinationPublicKey,
amount,
memo,
mint,
});
}
function createTransferBetweenSplTokenAccountsInstruction({
owner,
sourcePublicKey,
destinationPublicKey,
amount,
memo,
}) {
let transaction = new Transaction().add(
transfer({
@ -169,12 +232,81 @@ export async function transferTokens({
if (memo) {
transaction.add(memoInstruction(memo));
}
return transaction;
}
async function transferBetweenSplTokenAccounts({
connection,
owner,
sourcePublicKey,
destinationPublicKey,
amount,
memo,
}) {
const transaction = createTransferBetweenSplTokenAccountsInstruction({
owner,
sourcePublicKey,
destinationPublicKey,
amount,
memo,
});
let signers = [owner];
return await connection.sendTransaction(transaction, signers, {
preflightCommitment: 'single',
});
}
async function createAndTransferToAccount({
connection,
owner,
sourcePublicKey,
destinationPublicKey,
amount,
memo,
mint,
}) {
const newAccount = new Account();
let transaction = new Transaction();
transaction.add(
assertOwner({
account: destinationPublicKey,
owner: SystemProgram.programId,
}),
);
transaction.add(
SystemProgram.createAccount({
fromPubkey: owner.publicKey,
newAccountPubkey: newAccount.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(
ACCOUNT_LAYOUT.span,
),
space: ACCOUNT_LAYOUT.span,
programId: TOKEN_PROGRAM_ID,
}),
);
transaction.add(
initializeAccount({
account: newAccount.publicKey,
mint,
owner: destinationPublicKey,
}),
);
const transferBetweenAccountsTxn = createTransferBetweenSplTokenAccountsInstruction(
{
owner,
sourcePublicKey,
destinationPublicKey: newAccount.publicKey,
amount,
memo,
},
);
transaction.add(transferBetweenAccountsTxn);
let signers = [owner, newAccount];
return await connection.sendTransaction(transaction, signers, {
preflightCommitment: 'single',
});
}
export async function closeTokenAccount({
connection,
owner,

View File

@ -4,6 +4,7 @@ import {
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js';
import { publicKeyLayout } from '@project-serum/serum/lib/layout';
export const TOKEN_PROGRAM_ID = new PublicKey(
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
@ -150,3 +151,26 @@ export function memoInstruction(memo) {
programId: MEMO_PROGRAM_ID,
});
}
export const OWNER_VALIDATION_PROGRAM_ID = new PublicKey(
'4MNPdKu9wFMvEeZBMt3Eipfs5ovVWTJb31pEXDJAAxX5',
);
export const OWNER_VALIDATION_LAYOUT = BufferLayout.struct([
publicKeyLayout('account'),
]);
export function encodeOwnerValidationInstruction(instruction) {
const b = Buffer.alloc(OWNER_VALIDATION_LAYOUT.span);
const span = OWNER_VALIDATION_LAYOUT.encode(instruction, b);
return b.slice(0, span);
}
export function assertOwner({ account, owner }) {
const keys = [{ pubkey: account, isSigner: false, isWritable: false }];
return new TransactionInstruction({
keys,
data: encodeOwnerValidationInstruction({ account: owner }),
programId: OWNER_VALIDATION_PROGRAM_ID,
});
}

View File

@ -69,7 +69,7 @@ export class Wallet {
);
};
transferToken = async (source, destination, amount, memo = null) => {
transferToken = async (source, destination, amount, mint, memo = null) => {
if (source.equals(this.publicKey)) {
if (memo) {
throw new Error('Memo not implemented');
@ -83,6 +83,7 @@ export class Wallet {
destinationPublicKey: destination,
amount,
memo,
mint,
});
};