Auto create token accounts for destination wallet
This commit is contained in:
parent
eac4ae3c8d
commit
81a6464665
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue