Skip to content

Commit

Permalink
gui: fix bugs on holdings page (#366)
Browse files Browse the repository at this point in the history
1. Remove empty wSOL token account
2. Fix the zero balances switch
3. Add a convenient uiAmount to Vault interface so that we don't need to
compute ui amount from lamports on various pages
  • Loading branch information
yurushao authored Jan 14, 2025
1 parent 0c975a6 commit 460721a
Show file tree
Hide file tree
Showing 10 changed files with 184 additions and 174 deletions.
2 changes: 1 addition & 1 deletion anchor/src/client/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export type TxOptions = {

export type TokenAccount = {
owner: PublicKey;
pubkey: PublicKey;
pubkey: PublicKey; // ata
mint: PublicKey;
programId: PublicKey;
decimals: number;
Expand Down
60 changes: 33 additions & 27 deletions anchor/src/client/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ export class StateClient {
}

if (singleTx) {
// @ts-ignore
const initStateIx = await this.base.program.methods
.initializeState(stateModel)
.accountsPartial({
Expand Down Expand Up @@ -93,9 +92,12 @@ export class StateClient {
.rpc();

const addShareClassTxs = await Promise.all(
mints.map(async (shareClass: any, j: number) => {
mints.map(async (shareClass, j: number) => {
const shareClassMint = this.base.getShareClassPda(statePda, j);

// FIXME: setting rawOpenfunds to null is a workarond for
// Access violation in stack frame 5 at address 0x200005ff8 of size 8
shareClass.rawOpenfunds = null;
return await this.base.program.methods
.addShareClass(shareClass)
.accounts({
Expand Down Expand Up @@ -150,60 +152,64 @@ export class StateClient {
* Create a full state model from a partial state model
*/
enrichStateModel(partialStateModel: Partial<StateModel>): StateModel {
const stateModel = { ...partialStateModel };
const owner = this.base.getSigner();
const defaultDate = new Date().toISOString().split("T")[0];

// createdKey = hash fund name and get first 8 bytes
// useful for computing state account PDA in the future
const createdKey = [
...Buffer.from(
anchor.utils.sha256.hash(this.base.getName(stateModel)),
anchor.utils.sha256.hash(this.base.getName(partialStateModel)),
).subarray(0, 8),
];
stateModel.created = {
partialStateModel.created = {
key: createdKey,
owner,
};

stateModel.rawOpenfunds = new FundOpenfundsModel(
stateModel.rawOpenfunds ?? {},
partialStateModel.rawOpenfunds = new FundOpenfundsModel(
partialStateModel.rawOpenfunds ?? {},
);

stateModel.owner = new ManagerModel({
...(stateModel.owner || {}),
partialStateModel.owner = new ManagerModel({
...(partialStateModel.owner || {}),
pubkey: owner,
});

stateModel.company = new CompanyModel(stateModel.company || {});
partialStateModel.company = new CompanyModel(
partialStateModel.company || {},
);

if (stateModel.mints?.length == 1) {
const shareClass = stateModel.mints[0];
stateModel.name = stateModel.name || shareClass.name;
if (partialStateModel.mints?.length == 1) {
const shareClass = partialStateModel.mints[0];
partialStateModel.name = partialStateModel.name || shareClass.name;

stateModel.rawOpenfunds.fundCurrency =
stateModel.rawOpenfunds?.fundCurrency ||
partialStateModel.rawOpenfunds.fundCurrency =
partialStateModel.rawOpenfunds?.fundCurrency ||
shareClass.rawOpenfunds?.shareClassCurrency ||
null;
} else if (stateModel.mints?.length && stateModel.mints.length > 1) {
} else if (
partialStateModel.mints?.length &&
partialStateModel.mints.length > 1
) {
throw new Error("Fund with more than 1 share class is not supported");
}

if (stateModel.isEnabled) {
stateModel.rawOpenfunds.fundLaunchDate =
stateModel.rawOpenfunds?.fundLaunchDate || defaultDate;
if (partialStateModel.isEnabled) {
partialStateModel.rawOpenfunds.fundLaunchDate =
partialStateModel.rawOpenfunds?.fundLaunchDate || defaultDate;
}

// fields containing fund id / pda
const statePda = this.base.getStatePda(stateModel);
stateModel.uri =
stateModel.uri || `https://gui.glam.systems/products/${statePda}`;
stateModel.metadataUri =
stateModel.metadataUri ||
const statePda = this.base.getStatePda(partialStateModel);
partialStateModel.uri =
partialStateModel.uri || `https://gui.glam.systems/products/${statePda}`;
partialStateModel.metadataUri =
partialStateModel.metadataUri ||
`https://api.glam.systems/v0/openfunds?fund=${statePda}&format=csv`;

// build openfunds models for each share classes
(stateModel.mints || []).forEach(
(partialStateModel.mints || []).forEach(
(shareClass: ShareClassModel, i: number) => {
if (shareClass.rawOpenfunds) {
if (shareClass.rawOpenfunds.shareClassLifecycle === "active") {
Expand All @@ -226,12 +232,12 @@ export class StateClient {
);

// convert partial share class models to full share class models
stateModel.mints = (stateModel.mints || []).map(
partialStateModel.mints = (partialStateModel.mints || []).map(
// @ts-ignore
(s) => new ShareClassModel(s),
);

return new StateModel(stateModel);
return new StateModel(partialStateModel, this.base.program.programId);
}

/**
Expand Down
27 changes: 6 additions & 21 deletions anchor/src/react/glam.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,8 @@ import { atomWithStorage } from "jotai/utils";
import type { StateModel } from "../models";
import { GlamClient } from "../client";
import { useAtomValue, useSetAtom } from "jotai/react";
import { PublicKey } from "@solana/web3.js";
import { LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js";
import { WSOL } from "../constants";
import {
getAssociatedTokenAddressSync,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import { DriftMarketConfigs, GlamDriftUser } from "../client/drift";
import { TokenAccount } from "../client/base";
import { useCluster } from "./cluster-provider";
Expand All @@ -41,7 +37,7 @@ interface TokenPrice {
interface GlamProviderContext {
glamClient: GlamClient;
activeGlamState?: GlamStateCache;
vault?: Vault;
vault: Vault;
glamStatesList: GlamStateCache[];
allGlamStates: StateModel[];
userWallet: UserWallet;
Expand All @@ -57,12 +53,14 @@ interface UserWallet {
queryKey: string[];
pubkey?: PublicKey; // if pubkey is null, wallet is not connected
balanceLamports: number;
uiAmount: number;
tokenAccounts: TokenAccount[];
}

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

Expand Down Expand Up @@ -114,25 +112,12 @@ const fetchBalances = async (glamClient: GlamClient, owner: PublicKey) => {
const balanceLamports =
await glamClient.provider.connection.getBalance(owner);
const tokenAccounts = await glamClient.getTokenAccountsByOwner(owner);

// Add wSOL account if it doesn't exist, so that we can properly combine SOL and wSOL balances
// FIXME: this leads to a bug on holdings page that the wSOL ata has balance 0 but cannot be closed
if (!tokenAccounts.find((ta) => ta.mint.equals(WSOL))) {
tokenAccounts.push({
owner,
mint: WSOL,
programId: TOKEN_PROGRAM_ID,
pubkey: getAssociatedTokenAddressSync(WSOL, owner, true),
amount: "0",
uiAmount: 0,
decimals: 9,
frozen: false,
});
}
const uiAmount = balanceLamports / LAMPORTS_PER_SOL;

return {
balanceLamports,
tokenAccounts,
uiAmount,
};
};

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"anchor": "nx run anchor:anchor",
"anchor-build": "nx run anchor:anchor build",
"anchor-localnet": "nx run anchor:anchor localnet",
"anchor-test": "cd anchor && anchor test -- --tools-version v1.43",
"anchor-test-detach": "cd anchor && anchor test --detach -- --tools-version v1.43",
"anchor-test": "cd anchor && anchor test",
"anchor-test-detach": "cd anchor && anchor test --detach",
"build": "nx run playground:build",
"dev": "nx run playground:dev",
"pg-dev": "nx run playground:dev",
Expand Down
80 changes: 40 additions & 40 deletions playground/src/app/(mint)/mint/create/createMintForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,46 +122,46 @@ export default function MultiStepForm() {
openfundsData,
});

try {
const glamState = {
name: basicInfoFormData.name,
isEnabled: true,
rawOpenfunds: {
fundDomicileAlpha2: openfundsData.fund.fundDomicileAlpha2,
} as Partial<FundOpenfundsModel>,
// @ts-ignore
integrationAcls: [{ name: { mint: {} }, features: [] }],
company: {
fundGroupName: openfundsData.company.fundGroupName,
} as Partial<CompanyModel>,
owner: {
pubkey: glamClient.getSigner(),
kind: { wallet: {} },
} as Partial<ManagerModel>,
mints: [
{
uri: "",
fundId: null,
imageUri: "",
allowlist: [],
blocklist: [],
name: basicInfoFormData.name,
symbol: basicInfoFormData.symbol,
asset: new PublicKey(0),
lockUpPeriodInSeconds: policyFormData.lockUp * 3600,
permanentDelegate: policyFormData.permanentDelegate
? new PublicKey(policyFormData.permanentDelegate)
: new PublicKey(0),
defaultAccountStateFrozen: policyFormData.defaultAccountStateFrozen,
isRawOpenfunds: true,
rawOpenfunds: {
isin: openfundsData.shareClass.iSIN,
shareClassCurrency: openfundsData.shareClass.shareClassCurrency,
} as Partial<ShareClassOpenfundsModel>,
},
],
} as Partial<StateModel>;
const glamState = {
name: basicInfoFormData.name,
isEnabled: true,
rawOpenfunds: {
fundDomicileAlpha2: openfundsData.fund.fundDomicileAlpha2,
} as Partial<FundOpenfundsModel>,
// @ts-ignore
integrationAcls: [{ name: { mint: {} }, features: [] }],
company: {
fundGroupName: openfundsData.company.fundGroupName,
} as Partial<CompanyModel>,
owner: {
pubkey: glamClient.getSigner(),
kind: { wallet: {} },
} as Partial<ManagerModel>,
mints: [
{
uri: "",
fundId: null,
imageUri: "",
allowlist: [],
blocklist: [],
name: basicInfoFormData.name,
symbol: basicInfoFormData.symbol,
asset: new PublicKey(0),
lockUpPeriodInSeconds: policyFormData.lockUp * 3600,
permanentDelegate: policyFormData.permanentDelegate
? new PublicKey(policyFormData.permanentDelegate)
: new PublicKey(0),
defaultAccountStateFrozen: policyFormData.defaultAccountStateFrozen,
isRawOpenfunds: true,
rawOpenfunds: {
isin: openfundsData.shareClass.iSIN,
shareClassCurrency: openfundsData.shareClass.shareClassCurrency,
} as Partial<ShareClassOpenfundsModel>,
},
],
} as Partial<StateModel>;

try {
const [txSig, statePda] = await glamClient.state.createState(
glamState,
true,
Expand All @@ -174,7 +174,7 @@ export default function MultiStepForm() {
product: "Mint",
});
toast({
title: "Fund created successfully",
title: "Mint created successfully",
description: <ExplorerLink path={`tx/${txSig}`} label={txSig} />,
});
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
"use client";

import { Cross2Icon, MixerHorizontalIcon, PlusIcon, ReloadIcon } from "@radix-ui/react-icons";
import {
Cross2Icon,
MixerHorizontalIcon,
PlusIcon,
ReloadIcon,
} from "@radix-ui/react-icons";
import { Table } from "@tanstack/react-table";

import { Button } from "@/components/ui/button";
Expand All @@ -9,19 +14,25 @@ import { DataTableRefresh } from "./data-table-refresh";

import { useGlam } from "@glam/anchor/react";
import { Label } from "@/components/ui/label";
import {QrCodeIcon } from "lucide-react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { QrCodeIcon } from "lucide-react";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Switch } from "@/components/ui/switch";
import React from "react";

interface DataTableToolbarProps<TData> {
table: Table<TData>;
showZeroBalances: boolean;
setShowZeroBalances: (showZeroBalances: boolean) => void;
onOpenSheet: () => void;
}

export function DataTableToolbar<TData>({
table,
showZeroBalances,
setShowZeroBalances,
onOpenSheet,
}: DataTableToolbarProps<TData>) {
Expand Down Expand Up @@ -63,29 +74,23 @@ export function DataTableToolbar<TData>({
</div>

<Popover>
<PopoverTrigger>
<Button
variant="outline"
size="icon"
className={"mr-2 h-8 flex"}
>
<MixerHorizontalIcon className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent
className={"mr-2"}
align={"end"}
>
<PopoverTrigger>
<Button variant="outline" size="icon" className={"mr-2 h-8 flex"}>
<MixerHorizontalIcon className="h-4 w-4" />
</Button>
</PopoverTrigger>

<PopoverContent className={"mr-2"} align={"end"}>
<div className="flex items-center space-x-2">
<Switch
defaultChecked={true}
defaultChecked={showZeroBalances}
id="zero-balances"
onCheckedChange={(checked: boolean) => {
setShowZeroBalances(checked);
}}
/>
<Label htmlFor="zero-balances">Show Zero Balances</Label>
</div>
<Label htmlFor="zero-balances">Show Zero Balances</Label>
</div>
</PopoverContent>
</Popover>

Expand Down
Loading

0 comments on commit 460721a

Please sign in to comment.