Create associated token accounts and wrap SOL (#59)

This commit is contained in:
Armani Ferrante 2021-06-24 15:32:34 -07:00 committed by GitHub
parent c64caec9d5
commit 671225aec8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 219 additions and 51 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@project-serum/swap-ui",
"version": "0.1.8",
"version": "0.1.9",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"homepage": "https://github.com/project-serum/swap-ui",
@ -8,7 +8,7 @@
"dependencies": {
"@fontsource/roboto": "^4.3.0",
"@project-serum/serum": "^0.13.34",
"@project-serum/swap": "^0.1.0-alpha.28",
"@project-serum/swap": "^0.1.0-alpha.31",
"@solana/spl-token": "^0.1.4"
},
"peerDependencies": {
@ -42,7 +42,7 @@
"@project-serum/anchor": "^0.7.0",
"@project-serum/serum": "^0.13.34",
"@project-serum/sol-wallet-adapter": "^0.2.0",
"@project-serum/swap": "^0.1.0-alpha.28",
"@project-serum/swap": "^0.1.0-alpha.31",
"@solana/spl-token": "^0.1.4",
"@solana/spl-token-registry": "^0.2.86",
"@solana/web3.js": "^1.17.0",

View File

@ -1,6 +1,13 @@
import { useState } from "react";
import { PublicKey } from "@solana/web3.js";
import { BN } from "@project-serum/anchor";
import {
PublicKey,
Keypair,
Transaction,
SystemProgram,
Signer,
} from "@solana/web3.js";
import { Token, TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { BN, Provider } from "@project-serum/anchor";
import {
makeStyles,
Card,
@ -24,6 +31,7 @@ import { useCanSwap, useReferral } from "../context/Swap";
import TokenDialog from "./TokenDialog";
import { SettingsButton } from "./Settings";
import { InfoLabel } from "./Info";
import { WRAPPED_SOL_MINT } from "../utils/pubkeys";
const useStyles = makeStyles((theme) => ({
card: {
@ -334,9 +342,11 @@ export function SwapButton() {
const canSwap = useCanSwap();
const referral = useReferral(fromMarket);
const fair = useSwapFair();
const fromWallet = useOwnedTokenAccount(fromMint);
const toWallet = useOwnedTokenAccount(toMint);
const quoteMint = useMint(fromMarket && fromMarket.quoteMintAddress);
let fromWallet = useOwnedTokenAccount(fromMint);
let toWallet = useOwnedTokenAccount(toMint);
const quoteMint = fromMarket && fromMarket.quoteMintAddress;
const quoteMintInfo = useMint(quoteMint);
const quoteWallet = useOwnedTokenAccount(quoteMint);
// Click handler.
const sendSwapTransaction = async () => {
@ -346,41 +356,93 @@ export function SwapButton() {
if (!fair) {
throw new Error("Invalid fair");
}
if (!quoteMint) {
if (!quoteMint || !quoteMintInfo) {
throw new Error("Quote mint not found");
}
// All transactions to send for the swap.
let txs: { tx: Transaction; signers: Array<Signer | undefined> }[] = [];
const amount = new BN(fromAmount * 10 ** fromMintInfo.decimals);
const minExchangeRate = {
rate: new BN((10 ** toMintInfo.decimals * FEE_MULTIPLIER) / fair)
.muln(100 - slippage)
.divn(100),
fromDecimals: fromMintInfo.decimals,
quoteDecimals: quoteMint.decimals,
strict: isStrict,
};
const fromOpenOrders = fromMarket
? openOrders.get(fromMarket?.address.toString())
: undefined;
const toOpenOrders = toMarket
? openOrders.get(toMarket?.address.toString())
: undefined;
await swapClient.swap({
fromMint,
toMint,
fromWallet: fromWallet ? fromWallet.publicKey : undefined,
toWallet: toWallet ? toWallet.publicKey : undefined,
amount,
minExchangeRate,
referral,
// Pass in the below parameters so that the client doesn't perform
// wasteful network requests when we already have the data.
fromMarket,
toMarket,
fromOpenOrders: fromOpenOrders ? fromOpenOrders[0].address : undefined,
toOpenOrders: toOpenOrders ? toOpenOrders[0].address : undefined,
// Auto close newly created open orders accounts.
close: isClosingNewAccounts,
});
const isSol =
fromMint.equals(WRAPPED_SOL_MINT) || toMint.equals(WRAPPED_SOL_MINT);
const wrappedSolAccount = isSol ? Keypair.generate() : undefined;
// Wrap the SOL into an SPL token.
if (isSol) {
txs.push(
await wrapSol(
swapClient.program.provider,
wrappedSolAccount as Keypair,
fromMint,
amount
)
);
}
// Build the swap.
txs.push(
...(await (async () => {
if (!fromMarket) {
throw new Error("Market undefined");
}
const minExchangeRate = {
rate: new BN((10 ** toMintInfo.decimals * FEE_MULTIPLIER) / fair)
.muln(100 - slippage)
.divn(100),
fromDecimals: fromMintInfo.decimals,
quoteDecimals: quoteMintInfo.decimals,
strict: isStrict,
};
const fromOpenOrders = fromMarket
? openOrders.get(fromMarket?.address.toString())
: undefined;
const toOpenOrders = toMarket
? openOrders.get(toMarket?.address.toString())
: undefined;
const fromWalletAddr = fromMint.equals(WRAPPED_SOL_MINT)
? wrappedSolAccount!.publicKey
: fromWallet
? fromWallet.publicKey
: undefined;
const toWalletAddr = toMint.equals(WRAPPED_SOL_MINT)
? wrappedSolAccount!.publicKey
: toWallet
? toWallet.publicKey
: undefined;
return await swapClient.swapTxs({
fromMint,
toMint,
quoteMint,
amount,
minExchangeRate,
referral,
fromMarket,
toMarket,
// Automatically created if undefined.
fromOpenOrders: fromOpenOrders
? fromOpenOrders[0].address
: undefined,
toOpenOrders: toOpenOrders ? toOpenOrders[0].address : undefined,
fromWallet: fromWalletAddr,
toWallet: toWalletAddr,
quoteWallet: quoteWallet ? quoteWallet.publicKey : undefined,
// Auto close newly created open orders accounts.
close: isClosingNewAccounts,
});
})())
);
// Unwrap the SOL.
if (isSol) {
txs.push(
unwrapSol(swapClient.program.provider, wrappedSolAccount as Keypair)
);
}
await swapClient.program.provider.sendAll(txs);
};
return (
<Button
@ -393,3 +455,63 @@ export function SwapButton() {
</Button>
);
}
async function wrapSol(
provider: Provider,
wrappedSolAccount: Keypair,
fromMint: PublicKey,
amount: BN
): Promise<{ tx: Transaction; signers: Array<Signer | undefined> }> {
const tx = new Transaction();
const signers = [wrappedSolAccount];
// Create new, rent exempt account.
tx.add(
SystemProgram.createAccount({
fromPubkey: provider.wallet.publicKey,
newAccountPubkey: wrappedSolAccount.publicKey,
lamports: await Token.getMinBalanceRentForExemptAccount(
provider.connection
),
space: 165,
programId: TOKEN_PROGRAM_ID,
})
);
// Transfer lamports. These will be converted to an SPL balance by the
// token program.
if (fromMint.equals(WRAPPED_SOL_MINT)) {
tx.add(
SystemProgram.transfer({
fromPubkey: provider.wallet.publicKey,
toPubkey: wrappedSolAccount.publicKey,
lamports: amount.toNumber(),
})
);
}
// Initialize the account.
tx.add(
Token.createInitAccountInstruction(
TOKEN_PROGRAM_ID,
WRAPPED_SOL_MINT,
wrappedSolAccount.publicKey,
provider.wallet.publicKey
)
);
return { tx, signers };
}
function unwrapSol(
provider: Provider,
wrappedSolAccount: Keypair
): { tx: Transaction; signers: Array<Signer | undefined> } {
const tx = new Transaction();
tx.add(
Token.createCloseAccountInstruction(
TOKEN_PROGRAM_ID,
wrappedSolAccount.publicKey,
provider.wallet.publicKey,
provider.wallet.publicKey,
[]
)
);
return { tx, signers: [] };
}

View File

@ -139,7 +139,7 @@ export function DexContextProvider(props: any) {
mint.publicKey.equals(m.account.baseMintAddress)
)[0];
const quoteMintInfo = mintInfos.filter((mint) =>
mint.publicKey.equals(m.account.baseMintAddress)
mint.publicKey.equals(m.account.quoteMintAddress)
)[0];
assert.ok(baseMintInfo && quoteMintInfo);
// @ts-ignore

View File

@ -20,6 +20,7 @@ import {
SPL_REGISTRY_SOLLET_TAG,
SPL_REGISTRY_WORM_TAG,
} from "./TokenList";
import { useOwnedTokenAccount } from "../context/Token";
const DEFAULT_SLIPPAGE_PERCENT = 0.5;
@ -180,6 +181,7 @@ export function useCanSwap(): boolean {
const { fromMint, toMint, fromAmount, toAmount } = useSwapContext();
const { swapClient } = useDexContext();
const { wormholeMap, solletMap } = useTokenListContext();
const fromWallet = useOwnedTokenAccount(fromMint);
const fair = useSwapFair();
const route = useRouteVerbose(fromMint, toMint);
if (route === null) {
@ -187,6 +189,9 @@ export function useCanSwap(): boolean {
}
return (
// From wallet exists.
fromWallet !== undefined &&
fromWallet !== null &&
// Fair price is defined.
fair !== undefined &&
fair > 0 &&

View File

@ -1,7 +1,7 @@
import React, { useContext, useState, useEffect } from "react";
import * as assert from "assert";
import { useAsync } from "react-async-hook";
import { Provider } from "@project-serum/anchor";
import { Provider, BN } from "@project-serum/anchor";
import { PublicKey, Account } from "@solana/web3.js";
import {
MintInfo,
@ -10,6 +10,7 @@ import {
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import { getOwnedTokenAccounts, parseTokenAccountData } from "../utils/tokens";
import { WRAPPED_SOL_MINT } from "../utils/pubkeys";
export type TokenContext = {
provider: Provider;
@ -27,6 +28,7 @@ export function TokenContextProvider(props: any) {
setRefresh((r) => r + 1);
return;
}
// Fetch SPL tokens.
getOwnedTokenAccounts(provider.connection, provider.wallet.publicKey).then(
(accs) => {
if (accs) {
@ -35,6 +37,22 @@ export function TokenContextProvider(props: any) {
}
}
);
// Fetch SOL balance.
provider.connection
.getAccountInfo(provider.wallet.publicKey)
.then((acc: { lamports: number }) => {
if (acc) {
_OWNED_TOKEN_ACCOUNTS_CACHE.push({
publicKey: provider.wallet.publicKey,
// @ts-ignore
account: {
amount: new BN(acc.lamports),
mint: WRAPPED_SOL_MINT,
},
});
setRefresh((r) => r + 1);
}
});
}, [provider.wallet.publicKey, provider.connection]);
return (
@ -76,12 +94,32 @@ export function useOwnedTokenAccount(
: 0
);
const tokenAccount = tokenAccounts[0];
let tokenAccount = tokenAccounts[0];
const isSol = mint?.equals(WRAPPED_SOL_MINT);
// Stream updates when the balance changes.
useEffect(() => {
let listener: number;
if (tokenAccount) {
// SOL is special cased since it's not an SPL token.
if (tokenAccount && isSol) {
listener = provider.connection.onAccountChange(
provider.wallet.publicKey,
(info: { lamports: number }) => {
const token = {
amount: new BN(info.lamports),
mint: WRAPPED_SOL_MINT,
} as TokenAccount;
if (token.amount !== tokenAccount.account.amount) {
const index = _OWNED_TOKEN_ACCOUNTS_CACHE.indexOf(tokenAccount);
assert.ok(index >= 0);
_OWNED_TOKEN_ACCOUNTS_CACHE[index].account = token;
setRefresh((r) => r + 1);
}
}
);
}
// SPL tokens.
else if (tokenAccount) {
listener = provider.connection.onAccountChange(
tokenAccount.publicKey,
(info) => {
@ -106,7 +144,7 @@ export function useOwnedTokenAccount(
return undefined;
}
if (tokenAccounts.length === 0) {
if (!isSol && tokenAccounts.length === 0) {
return null;
}

View File

@ -38,8 +38,7 @@ export function TokenListContextProvider(props: any) {
const tokens = tokenList.filter((t: TokenInfo) => {
const isUsdxQuoted =
t.extensions?.serumV3Usdt || t.extensions?.serumV3Usdc;
const isSol = t.address === "So11111111111111111111111111111111111111112";
return isUsdxQuoted && !isSol;
return isUsdxQuoted;
});
tokens.sort((a: TokenInfo, b: TokenInfo) =>
a.symbol < b.symbol ? -1 : a.symbol > b.symbol ? 1 : 0

View File

@ -16,6 +16,10 @@ export const USDT_MINT = new PublicKey(
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
);
export const WRAPPED_SOL_MINT = new PublicKey(
"So11111111111111111111111111111111111111112"
);
export const WORM_MARKET_BASE = new PublicKey(
"6a9wpsZpZGxGhFVSQBpcTNjNjytdbSA1iUw1A5KNDxPw"
);

View File

@ -1654,10 +1654,10 @@
bs58 "^4.0.1"
eventemitter3 "^4.0.4"
"@project-serum/swap@^0.1.0-alpha.28":
version "0.1.0-alpha.28"
resolved "https://registry.yarnpkg.com/@project-serum/swap/-/swap-0.1.0-alpha.28.tgz#523bb161c2d0011f4df0317ed31b77107f72ed06"
integrity sha512-LJ6roKiK43JyAjjDNejXxyFurngXI+YVNdliTnedKp8MgJm6QFj2S8606xiD0Col3aXI6jq7Y6xw6CczHc1j4g==
"@project-serum/swap@^0.1.0-alpha.31":
version "0.1.0-alpha.31"
resolved "https://registry.yarnpkg.com/@project-serum/swap/-/swap-0.1.0-alpha.31.tgz#0656c9959f7be18248731c6ec8025d7619d58b74"
integrity sha512-LiepwTqC9+1PYF+Oce2VQJmiia8Li9ysRwbvbaW5+0B4ZmcXS1rc3fqrn8Xyi36XL4NGRbsT7xZa/XoyP5S7fg==
dependencies:
"@project-serum/serum" "^0.13.34"
"@solana/spl-token" "^0.1.3"