Skip to content

Commit

Permalink
Holdings (#357)
Browse files Browse the repository at this point in the history
  • Loading branch information
0x0ece authored Jan 6, 2025
1 parent c3c3092 commit c6f2224
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 19 deletions.
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>
);

0 comments on commit c6f2224

Please sign in to comment.