diff --git a/anchor/src/client/base.ts b/anchor/src/client/base.ts index be498ac5..1b507638 100644 --- a/anchor/src/client/base.ts +++ b/anchor/src/client/base.ts @@ -68,7 +68,7 @@ export type TxOptions = { export type TokenAccount = { owner: PublicKey; - pubkey: PublicKey; + pubkey: PublicKey; // ata mint: PublicKey; programId: PublicKey; decimals: number; diff --git a/anchor/src/client/state.ts b/anchor/src/client/state.ts index 02236e03..99ef9337 100644 --- a/anchor/src/client/state.ts +++ b/anchor/src/client/state.ts @@ -59,7 +59,6 @@ export class StateClient { } if (singleTx) { - // @ts-ignore const initStateIx = await this.base.program.methods .initializeState(stateModel) .accountsPartial({ @@ -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({ @@ -150,7 +152,6 @@ export class StateClient { * Create a full state model from a partial state model */ enrichStateModel(partialStateModel: Partial): StateModel { - const stateModel = { ...partialStateModel }; const owner = this.base.getSigner(); const defaultDate = new Date().toISOString().split("T")[0]; @@ -158,52 +159,57 @@ export class StateClient { // 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") { @@ -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); } /** diff --git a/anchor/src/react/glam.tsx b/anchor/src/react/glam.tsx index 0720931e..81b0de62 100644 --- a/anchor/src/react/glam.tsx +++ b/anchor/src/react/glam.tsx @@ -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"; @@ -41,7 +37,7 @@ interface TokenPrice { interface GlamProviderContext { glamClient: GlamClient; activeGlamState?: GlamStateCache; - vault?: Vault; + vault: Vault; glamStatesList: GlamStateCache[]; allGlamStates: StateModel[]; userWallet: UserWallet; @@ -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[]; } @@ -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, }; }; diff --git a/package.json b/package.json index 6b233f10..a8a9deeb 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/playground/src/app/(mint)/mint/create/createMintForm.tsx b/playground/src/app/(mint)/mint/create/createMintForm.tsx index 9d92b267..ed347e85 100644 --- a/playground/src/app/(mint)/mint/create/createMintForm.tsx +++ b/playground/src/app/(mint)/mint/create/createMintForm.tsx @@ -122,46 +122,46 @@ export default function MultiStepForm() { openfundsData, }); - try { - const glamState = { - name: basicInfoFormData.name, - isEnabled: true, - rawOpenfunds: { - fundDomicileAlpha2: openfundsData.fund.fundDomicileAlpha2, - } as Partial, - // @ts-ignore - integrationAcls: [{ name: { mint: {} }, features: [] }], - company: { - fundGroupName: openfundsData.company.fundGroupName, - } as Partial, - owner: { - pubkey: glamClient.getSigner(), - kind: { wallet: {} }, - } as Partial, - 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, - }, - ], - } as Partial; + const glamState = { + name: basicInfoFormData.name, + isEnabled: true, + rawOpenfunds: { + fundDomicileAlpha2: openfundsData.fund.fundDomicileAlpha2, + } as Partial, + // @ts-ignore + integrationAcls: [{ name: { mint: {} }, features: [] }], + company: { + fundGroupName: openfundsData.company.fundGroupName, + } as Partial, + owner: { + pubkey: glamClient.getSigner(), + kind: { wallet: {} }, + } as Partial, + 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, + }, + ], + } as Partial; + try { const [txSig, statePda] = await glamClient.state.createState( glamState, true, @@ -174,7 +174,7 @@ export default function MultiStepForm() { product: "Mint", }); toast({ - title: "Fund created successfully", + title: "Mint created successfully", description: , }); } catch (error) { diff --git a/playground/src/app/(vault)/vault/holdings/components/data-table-toolbar.tsx b/playground/src/app/(vault)/vault/holdings/components/data-table-toolbar.tsx index e03bbcf2..b8619901 100644 --- a/playground/src/app/(vault)/vault/holdings/components/data-table-toolbar.tsx +++ b/playground/src/app/(vault)/vault/holdings/components/data-table-toolbar.tsx @@ -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"; @@ -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 { table: Table; + showZeroBalances: boolean; setShowZeroBalances: (showZeroBalances: boolean) => void; onOpenSheet: () => void; } export function DataTableToolbar({ table, + showZeroBalances, setShowZeroBalances, onOpenSheet, }: DataTableToolbarProps) { @@ -63,29 +74,23 @@ export function DataTableToolbar({ - - - - + + + + +
{ setShowZeroBalances(checked); }} /> - -
+ +
diff --git a/playground/src/app/(vault)/vault/holdings/components/data-table.tsx b/playground/src/app/(vault)/vault/holdings/components/data-table.tsx index 85722de0..a69466e2 100644 --- a/playground/src/app/(vault)/vault/holdings/components/data-table.tsx +++ b/playground/src/app/(vault)/vault/holdings/components/data-table.tsx @@ -29,6 +29,7 @@ import { DataTableToolbar } from "./data-table-toolbar"; interface DataTableProps { columns: ColumnDef[]; data: TData[]; + showZeroBalances: boolean; setShowZeroBalances: (showZeroBalances: boolean) => void; onOpenSheet: () => void; } @@ -36,6 +37,7 @@ interface DataTableProps { export function DataTable({ columns, data, + showZeroBalances, setShowZeroBalances, onOpenSheet, }: DataTableProps) { @@ -72,6 +74,7 @@ export function DataTable({
diff --git a/playground/src/app/(vault)/vault/holdings/page.tsx b/playground/src/app/(vault)/vault/holdings/page.tsx index ac312723..2e91a8a0 100644 --- a/playground/src/app/(vault)/vault/holdings/page.tsx +++ b/playground/src/app/(vault)/vault/holdings/page.tsx @@ -6,7 +6,7 @@ import React, { useEffect, useMemo, useState } from "react"; 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 { useGlam, WSOL } from "@glam/anchor/react"; import { Holding } from "./data/holdingSchema"; import { Sheet, @@ -62,7 +62,7 @@ export default function Holdings() { price: 0, amount: "0", balance: 0, - decimals: 9, + decimals: 0, notional: 0, logoURI: "", location: "", @@ -77,21 +77,19 @@ export default function Holdings() { useEffect(() => { const holdings: Holding[] = []; - - const solBalance = Number(vault?.balanceLamports) / LAMPORTS_PER_SOL; - if (solBalance > 0) { - const mint = "So11111111111111111111111111111111111111112"; + if (vault.uiAmount && vault.balanceLamports > 0) { + const mint = WSOL.toBase58(); const price = prices?.find((p) => p.mint === mint)?.price || 0; holdings.push({ name: "Solana", symbol: "SOL", mint: "", ata: "", - price: price, - amount: "" + vault?.balanceLamports || "0", - balance: solBalance, + price, + amount: vault?.balanceLamports.toString() || "NaN", + balance: vault?.uiAmount || NaN, decimals: 9, - notional: solBalance * price || 0, + notional: vault.uiAmount * price || 0, location: "vault", logoURI: "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/So11111111111111111111111111111111111111112/logo.png", @@ -101,32 +99,34 @@ export default function Holdings() { if (vault?.tokenAccounts) { holdings.push( - ...vault.tokenAccounts.map((ta) => { - const jupToken = jupTokenList?.find( - (t) => t.address === ta.mint.toBase58(), - ); - const logoURI = jupToken?.logoURI || ""; - const name = jupToken?.name || "Unknown"; - const symbol = jupToken?.symbol || ta.mint.toBase58(); - const price = - prices?.find((p) => p.mint === ta.mint.toBase58())?.price || 0; - const tags = jupToken?.tags || []; - - return { - name, - symbol: symbol === "SOL" ? "wSOL" : symbol, - mint: ta.mint.toBase58(), - ata: ta.pubkey.toBase58(), - price, - amount: ta.amount, - balance: ta.uiAmount, - decimals: ta.decimals, - notional: ta.uiAmount * price || 0, - logoURI, - location: "vault", - lst: tags.indexOf("lst") >= 0, - }; - }), + ...vault.tokenAccounts.map( + ({ mint, pubkey, amount, uiAmount, decimals }) => { + const jupToken = jupTokenList?.find( + (t) => t.address === mint.toBase58(), + ); + const logoURI = jupToken?.logoURI || ""; + const name = jupToken?.name || "Unknown"; + const symbol = jupToken?.symbol || mint.toBase58(); + const price = + prices?.find((p) => p.mint === mint.toBase58())?.price || 0; + const tags = jupToken?.tags || []; + + return { + name, + symbol: symbol === "SOL" ? "wSOL" : symbol, + mint: mint.toBase58(), + ata: pubkey.toBase58(), + price, + amount, + balance: uiAmount, + decimals, + notional: uiAmount * price || 0, + logoURI, + location: "vault", + lst: tags.indexOf("lst") >= 0, + }; + }, + ), ); } @@ -137,7 +137,8 @@ export default function Holdings() { const driftHoldings = spotPositions.map((p) => { const market = spotMarkets.find((m) => m.marketIndex === p.marketIndex); 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 + // FIXME: balance is UI amount added by glam api, it doesn't existing in the drift sdk types + // @ts-ignore const balance = Number(p.balance); const decimals = market?.decimals || 9; const amount = new BN(balance).mul(new BN(10 ** decimals)); @@ -185,17 +186,16 @@ export default function Holdings() { } const statePda = activeGlamState.pubkey; - console.log(tableData); const tokenAccounts = (tableData || []) - .filter((d) => d.ata) + .filter((d) => d.ata && d.location === "vault") .map((d) => new PublicKey(d.ata)); let preInstructions = ( await Promise.all( (tableData || []) - .filter((d) => d.balance > 0 && d.mint) + .filter((d) => d.balance > 0 && d.mint && d.location === "vault") .map(async (d) => { - console.log("withdraw", d.name); + console.log(`withdraw ${d.name} from ${d.location}`); return await glamClient.state.withdrawIxs( statePda, new PublicKey(d.mint), @@ -247,6 +247,7 @@ export default function Holdings() { : tableData.filter((d) => d.balance > 0) } columns={columns} + showZeroBalances={showZeroBalances} setShowZeroBalances={setShowZeroBalances} onOpenSheet={openSheet} /> diff --git a/playground/src/app/(vault)/vault/transfer/page.tsx b/playground/src/app/(vault)/vault/transfer/page.tsx index b4ba9d25..11b687ba 100644 --- a/playground/src/app/(vault)/vault/transfer/page.tsx +++ b/playground/src/app/(vault)/vault/transfer/page.tsx @@ -24,7 +24,7 @@ import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { toast } from "@/components/ui/use-toast"; import PageContentWrapper from "@/components/PageContentWrapper"; -import { useGlam } from "@glam/anchor/react"; +import { useGlam, WSOL } from "@glam/anchor/react"; import { LAMPORTS_PER_SOL } from "@solana/web3.js"; import { parseTxError } from "@/lib/error"; import { ExplorerLink } from "@/components/ExplorerLink"; @@ -60,24 +60,35 @@ export default function Transfer() { const [transferButtonDisabled, setTransferButtonDisabled] = useState(false); const vaultAssets = () => { - const assets = (vault?.tokenAccounts || []).map((ta) => { - const jupToken = jupTokenList?.find( - (t) => t.address === ta.mint.toBase58(), - ); - const name = jupToken?.name || "Unknown"; - const symbol = jupToken?.symbol || ta.mint.toBase58(); - return { - name, - symbol, - address: ta.mint.toBase58(), - decimals: ta.decimals, - balance: - /* combine SOL + wSOL balances */ - symbol === "SOL" - ? ta.uiAmount + (vault?.balanceLamports || 0) / LAMPORTS_PER_SOL - : ta.uiAmount, - } as Asset; - }); + const assets = (vault?.tokenAccounts || []).map( + ({ mint, uiAmount, decimals }) => { + const jupToken = jupTokenList?.find( + (t) => t.address === mint.toBase58(), + ); + const name = jupToken?.name || "Unknown"; + const symbol = jupToken?.symbol || mint.toBase58(); + // If vault holds wSOL, combine SOL + wSOL balance + const balance = + symbol === "SOL" ? uiAmount + (vault?.uiAmount || 0) : uiAmount; + return { + name, + symbol, + address: mint.toBase58(), + decimals: decimals, + balance, + } as Asset; + }, + ); + // If vault does not hold wSOL, explicitly add SOL + if (!assets.find((a) => a.symbol === "SOL")) { + assets.push({ + name: "SOL", + symbol: "SOL", + address: WSOL.toBase58(), + decimals: 9, + balance: vault.uiAmount || NaN, + }); + } return assets; }; @@ -126,13 +137,12 @@ export default function Transfer() { ); setTransferButtonDisabled(true); return; - } else { - setWarning(""); } + + setWarning(""); setTransferButtonDisabled(false); if (from === "Drift") { - console.log(driftUser.spotPositions); setFromAssetList(driftAssets()); return; } diff --git a/playground/src/app/(vault)/vault/wrap/page.tsx b/playground/src/app/(vault)/vault/wrap/page.tsx index afe53b84..b4a0ae5f 100644 --- a/playground/src/app/(vault)/vault/wrap/page.tsx +++ b/playground/src/app/(vault)/vault/wrap/page.tsx @@ -46,7 +46,7 @@ export default function Wrap() { useEffect(() => { if (activeGlamState?.pubkey && vault) { - const solBalance = (vault?.balanceLamports || 0) / LAMPORTS_PER_SOL; + const solBalance = vault?.uiAmount || NaN; const wSolBalance = vault?.tokenAccounts?.find((ta) => ta.mint.equals(WSOL))?.uiAmount || 0; setSolBalance(solBalance);