Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Holdings #357

Merged
merged 2 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion anchor/src/client/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export type TokenAccount = {
mint: PublicKey;
programId: PublicKey;
decimals: number;
amount: number;
amount: string;
uiAmount: number;
frozen: boolean;
};
Expand Down
58 changes: 56 additions & 2 deletions anchor/src/client/fund.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
PublicKey,
VersionedTransaction,
Transaction,
TransactionInstruction,
TransactionSignature,
ComputeBudgetProgram,
} from "@solana/web3.js";
Expand Down Expand Up @@ -131,14 +132,17 @@ export class FundClient {
): Promise<TransactionSignature> {
const openfunds = this.base.getOpenfundsPDA(fundPDA);

return await this.base.program.methods
const tx = await this.base.program.methods
.closeFund()
.accounts({
fund: fundPDA,
openfunds,
})
.preInstructions(txOptions.preInstructions || [])
.rpc();
.transaction();

const vTx = await this.base.intoVersionedTransaction({ tx, ...txOptions });
return await this.base.sendAndConfirm(vTx);
}

/**
Expand Down Expand Up @@ -291,6 +295,26 @@ export class FundClient {
* @param txOptions
* @returns
*/
public async closeTokenAccountsIx(
fund: PublicKey,
tokenAccounts: PublicKey[],
): Promise<TransactionInstruction> {
// @ts-ignore
return await this.base.program.methods
.closeTokenAccounts()
.accounts({
fund,
})
.remainingAccounts(
tokenAccounts.map((account) => ({
pubkey: account,
isSigner: false,
isWritable: true,
})),
)
.instruction();
}

public async closeTokenAccountsTx(
fund: PublicKey,
tokenAccounts: PublicKey[],
Expand Down Expand Up @@ -372,6 +396,36 @@ export class FundClient {
return await this.base.intoVersionedTransaction({ tx, ...txOptions });
}

public async withdrawIxs(
fund: PublicKey,
asset: PublicKey,
amount: number | BN,
txOptions: TxOptions,
): Promise<TransactionInstruction[]> {
const signer = txOptions.signer || this.base.getSigner();
const { tokenProgram } = await this.base.fetchMintWithOwner(asset);
const signerAta = this.base.getAta(asset, signer, tokenProgram);

return [
createAssociatedTokenAccountIdempotentInstruction(
signer,
signerAta,
signer,
asset,
tokenProgram,
),
// @ts-ignore
await this.base.program.methods
.withdraw(new BN(amount))
.accounts({
fund,
asset,
tokenProgram,
})
.instruction(),
];
}

public async withdrawTx(
fund: PublicKey,
asset: PublicKey,
Expand Down
2 changes: 1 addition & 1 deletion anchor/src/client/shareclass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class ShareClassClient {
mint: tokenAccount.mint,
programId: TOKEN_2022_PROGRAM_ID,
decimals: mint.decimals,
amount: Number(tokenAccount.amount),
amount: tokenAccount.amount.toString(),
uiAmount: Number(tokenAccount.amount) / 10 ** mint.decimals,
frozen: tokenAccount.isFrozen,
} as TokenAccount;
Expand Down
4 changes: 2 additions & 2 deletions anchor/src/react/glam.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ interface UserWallet {

interface Treasury {
pubkey: PublicKey;
balanceLamports: number;
balanceLamports: number; // TODO: this should be a BN or string, it works until ~9M SOL
tokenAccounts: TokenAccount[];
}

Expand Down Expand Up @@ -119,7 +119,7 @@ const fetchBalances = async (glamClient: GlamClient, owner: PublicKey) => {
mint: WSOL,
programId: TOKEN_PROGRAM_ID,
pubkey: getAssociatedTokenAddressSync(WSOL, owner, true),
amount: 0,
amount: "0",
uiAmount: 0,
decimals: 9,
frozen: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export const columns: ColumnDef<Holding>[] = [

return (
<div className="w-[80px]">
{isSkeletonRow(row) || row.original.notional === 0 ? (
{isSkeletonRow(row) || row.original.notional === -1 ? (
<VariableWidthSkeleton minWidth={40} maxWidth={80} height={20} />
) : (
<span>
Expand All @@ -140,8 +140,8 @@ export const columns: ColumnDef<Holding>[] = [
<NumberFormatter
value={holding.notional}
addCommas={true}
minDecimalPlaces={0}
maxDecimalPlaces={9}
minDecimalPlaces={2}
maxDecimalPlaces={2}
/>
</span>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function DataTableToolbar<TData>({
)}

<Checkbox
defaultChecked={true}
id="zero-balances"
onCheckedChange={(checked: boolean) => {
setShowZeroBalances(checked);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export const holdingSchema = z.object({
mint: z.string(),
ata: z.string(),
price: z.number(),
balance: z.number(),
amount: z.string(), // raw
balance: z.number(), // ui
decimals: z.number(),
notional: z.number(),
logoURI: z.string(),
Expand Down
57 changes: 50 additions & 7 deletions playground/src/app/(vault)/vault/holdings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import { DataTable } from "./components/data-table";
import { columns } from "./components/columns";
import React, { useEffect, useMemo, useState } from "react";
import { LAMPORTS_PER_SOL } from "@solana/web3.js";
import { PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
import { BN } from "@coral-xyz/anchor";
import PageContentWrapper from "@/components/PageContentWrapper";
import { useGlam } from "@glam/anchor/react";
import { Holding } from "./data/holdingSchema";
Expand Down Expand Up @@ -45,7 +46,7 @@ export default function Holdings() {
glamClient,
} = useGlam();

const [showZeroBalances, setShowZeroBalances] = useState(false);
const [showZeroBalances, setShowZeroBalances] = useState(true);
const [isLoadingData, setIsLoading] = useState(true);
const [isTxPending, setIsTxPending] = useState(false);

Expand All @@ -59,6 +60,7 @@ export default function Holdings() {
mint: "",
ata: "",
price: 0,
amount: "0",
balance: 0,
decimals: 9,
notional: 0,
Expand Down Expand Up @@ -86,6 +88,7 @@ export default function Holdings() {
mint: "",
ata: "",
price: price,
amount: "" + treasury?.balanceLamports || "0",
balance: solBalance,
decimals: 9,
notional: solBalance * price || 0,
Expand Down Expand Up @@ -115,9 +118,10 @@ export default function Holdings() {
mint: ta.mint.toBase58(),
ata: ta.pubkey.toBase58(),
price,
amount: ta.amount,
balance: ta.uiAmount,
decimals: ta.decimals,
notional: ta.uiAmount * price,
notional: ta.uiAmount * price || 0,
logoURI,
location: "vault",
lst: tags.indexOf("lst") >= 0,
Expand All @@ -135,14 +139,17 @@ export default function Holdings() {
const price = prices?.find((p) => p.mint === market?.mint)?.price || 0;
// @ts-ignore: balance is UI amount added by glam api, it doesn't existing in the drift sdk types
const balance = Number(p.balance);
const decimals = market?.decimals || 9;
const amount = new BN(balance).mul(new BN(10 ** decimals));
return {
name: `${p.marketIndex}`,
symbol: market?.symbol || "",
mint: "NA",
ata: "NA",
price,
amount: amount.toString(),
balance,
decimals: market?.decimals || 9,
decimals,
notional: balance * price || 0,
logoURI: "https://avatars.githubusercontent.com/u/83389928?s=48&v=4",
location: "drift",
Expand Down Expand Up @@ -176,10 +183,39 @@ export default function Holdings() {
if (!activeFund?.pubkey) {
return;
}
const fund = activeFund.pubkey;

console.log(tableData);
const tokenAccounts = (tableData || [])
.filter((d) => d.ata)
.map((d) => new PublicKey(d.ata));

let preInstructions = (
await Promise.all(
(tableData || [])
.filter((d) => d.balance > 0 && d.mint)
.map(async (d) => {
console.log("withdraw", d.name);
return await glamClient.fund.withdrawIxs(
fund,
new PublicKey(d.mint),
new BN(d.amount),
{},
);
}),
)
).flat();

console.log("closing ATAs:", tokenAccounts);
preInstructions.push(
await glamClient.fund.closeTokenAccountsIx(fund, tokenAccounts),
);

setIsTxPending(true);
try {
const txSig = await glamClient.fund.closeFund(activeFund.pubkey);
const txSig = await glamClient.fund.closeFund(fund, {
preInstructions,
});
toast({
title: "Vault closed",
description: <ExplorerLink path={`tx/${txSig}`} label={txSig} />,
Expand Down Expand Up @@ -226,7 +262,7 @@ export default function Holdings() {

<div className="grid grid-cols-[200px_1fr] gap-6 py-6">
<div className="flex flex-col items-center justify-center">
<QRCodeSVG value={vaultAddress} level="M" size={200} />
<QRCodeSVG value={`solana:vaultAddress`} level="M" size={200} />
<p className="mt-2 text-sm text-muted-foreground text-left">
This is the address of your Vault. Deposit funds by scanning the
QR code or copying the address.
Expand Down Expand Up @@ -282,7 +318,14 @@ export default function Holdings() {
</AccordionTrigger>
<AccordionContent>
<div className="space-y-4">
<DangerCard message="Before closing your vault, transfer all assets and close all token accounts, noting that any remaining SOL will automatically return to your wallet." />
<DangerCard
message={`Only the owner can close this vault.
All assets are transferred to the owner as part of the closing transaction.
If there are too many assets in the vault, the closing transaction might be too big and therefore fail -- in this case please manually transfer assets and/or close empty token accounts.`}
/>
<DangerCard
message={`Do NOT send any asset to this vault while closing, or you risk to permanently loose them.`}
/>
<Button
onClick={closeVault}
variant="destructive"
Expand Down
12 changes: 10 additions & 2 deletions playground/src/components/DangerCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,18 @@ interface Props {

export const DangerCard: React.FC<Props> = ({ message, className }) => (
<Card
className={cn("border border-destructive/20 bg-destructive/5 p-4", className)}
className={cn(
"border border-destructive/20 bg-destructive/5 p-4",
className,
)}
>
<div className="flex">
<p className="text-sm text-destructive">{message}</p>
<p
style={{ whiteSpace: "pre-line" }}
className="text-sm text-destructive"
>
{message}
</p>
</div>
</Card>
);
Loading