diff --git a/packages/huma-shared/src/solana/utils/tokenAssetsSharesUtils.ts b/packages/huma-shared/src/solana/utils/tokenAssetsSharesUtils.ts index 29b60a3d..5e642f4f 100644 --- a/packages/huma-shared/src/solana/utils/tokenAssetsSharesUtils.ts +++ b/packages/huma-shared/src/solana/utils/tokenAssetsSharesUtils.ts @@ -1,16 +1,14 @@ -import { BN } from '@coral-xyz/anchor' - export function convertToShares( - totalAssets: BN, - totalSupply: BN, - assets: BN, -): BN { - if (!totalSupply.isZero() && totalAssets.isZero()) { - return new BN(0) + totalAssets: bigint, + totalSupply: bigint, + assets: bigint, +): bigint { + if (totalSupply !== BigInt(0) && totalAssets === BigInt(0)) { + return BigInt(0) } - if (totalSupply.isZero()) { + if (totalSupply === BigInt(0)) { return assets } - return assets.mul(totalSupply).div(totalAssets) + return (assets * totalSupply) / totalAssets } diff --git a/packages/huma-shared/src/stellar/metadata/testnet.ts b/packages/huma-shared/src/stellar/metadata/testnet.ts index 75340ff5..ebf6b651 100644 --- a/packages/huma-shared/src/stellar/metadata/testnet.ts +++ b/packages/huma-shared/src/stellar/metadata/testnet.ts @@ -17,6 +17,7 @@ export const STELLAR_TESTNET_METADATA: StellarPoolsInfo = { creditManager: 'CBEH5SKVKC6GXP5FQLAUFX43GAFRXZDOHDUQW3CRFD5BQVH7L6YSBP4V', creditStorage: 'CADDOLDFYN6Y2DXNYMX2ILVLPLU5W7MAQ7GBOOYWJ6JCH4DGHLUH2FB3', juniorTranche: 'CB6K4IUC3CJHIWVHHLBDTGXVS6CT64EKGD5CGBIDNMSAKVZHCWQ3LM2D', + trancheDecimals: 6, underlyingToken: { address: 'CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA', symbol: 'USDC', diff --git a/packages/huma-shared/src/stellar/types/metadata.ts b/packages/huma-shared/src/stellar/types/metadata.ts index b3ef3117..8dfe3507 100644 --- a/packages/huma-shared/src/stellar/types/metadata.ts +++ b/packages/huma-shared/src/stellar/types/metadata.ts @@ -18,6 +18,7 @@ export type StellarPoolInfo = { creditStorage: string juniorTranche: string seniorTranche?: string + trancheDecimals: number underlyingToken: { address: string symbol: string diff --git a/packages/huma-web-shared/src/stellar/StellarWeb3Provider.tsx b/packages/huma-web-shared/src/stellar/StellarWeb3Provider.tsx index 25650d87..b131a2f4 100644 --- a/packages/huma-web-shared/src/stellar/StellarWeb3Provider.tsx +++ b/packages/huma-web-shared/src/stellar/StellarWeb3Provider.tsx @@ -2,13 +2,13 @@ import { requestAccess, WatchWalletChanges } from '@stellar/freighter-api' import React, { useState, createContext, useMemo, useEffect } from 'react' export interface StellarConnectionContextType { - address: string + address: string | null requestAccessFn: () => void } export const StellarConnectionContext = createContext({ - address: '', + address: null, requestAccessFn: () => {}, }) @@ -19,7 +19,7 @@ type WCProps

= P & { } export function StellarWeb3Provider({ children }: WCProps) { - const [address, setAddress] = useState('') + const [address, setAddress] = useState(null) // Watch for changes in the Freighter wallet address on a 3 second interval by default. // Need this to catch users switching wallet addresses in the extension and for auto-connecting @@ -42,7 +42,7 @@ export function StellarWeb3Provider({ children }: WCProps) { const accessObj = await requestAccess() if (accessObj.error) { - setAddress('') + setAddress(null) } else { setAddress(accessObj.address) } diff --git a/packages/huma-web-shared/src/stellar/hooks/useStellarLender.ts b/packages/huma-web-shared/src/stellar/hooks/useStellarLender.ts index b2600a81..33dd01ed 100644 --- a/packages/huma-web-shared/src/stellar/hooks/useStellarLender.ts +++ b/packages/huma-web-shared/src/stellar/hooks/useStellarLender.ts @@ -4,27 +4,55 @@ import { LenderRedemptionRecord, } from '@huma-finance/soroban-tranche-vault' import { useEffect, useState } from 'react' +import { useForceRefresh } from '../../hooks' +import { DepositRecord, fetchStellarDepositRecord } from '../utils' export const useStellarLender = ( - stellarAddress: string, + stellarAddress: string | null, poolInfo: StellarPoolInfo, ): { isLoadingStellarLender: boolean isJuniorApprovedLender: boolean juniorRedemptionRecord: LenderRedemptionRecord | null + juniorDepositRecord: DepositRecord | null + juniorTrancheWithdrawable: bigint + juniorTrancheShares: bigint isSeniorApprovedLender: boolean seniorRedemptionRecord: LenderRedemptionRecord | null + seniorDepositRecord: DepositRecord | null + seniorTrancheWithdrawable: bigint + seniorTrancheShares: bigint + refresh: () => void } => { const [isLoadingStellarLender, setIsLoadingStellarLender] = useState(true) const [isJuniorApprovedLender, setIsJuniorApprovedLender] = useState(false) const [juniorRedemptionRecord, setJuniorRedemptionRecord] = useState(null) + const [juniorDepositRecord, setJuniorDepositRecord] = + useState(null) + const [juniorTrancheWithdrawable, setJuniorTrancheWithdrawable] = + useState(BigInt(0)) + const [juniorTrancheShares, setJuniorTrancheShares] = useState( + BigInt(0), + ) const [isSeniorApprovedLender, setIsSeniorApprovedLender] = useState(false) const [seniorRedemptionRecord, setSeniorRedemptionRecord] = useState(null) + const [seniorDepositRecord, setSeniorDepositRecord] = + useState(null) + const [seniorTrancheWithdrawable, setSeniorTrancheWithdrawable] = + useState(BigInt(0)) + const [seniorTrancheShares, setSeniorTrancheShares] = useState( + BigInt(0), + ) + const [refreshCount, refresh] = useForceRefresh() useEffect(() => { const fetchData = async () => { + if (!stellarAddress) { + return + } + setIsLoadingStellarLender(true) try { const chainMetadata = STELLAR_CHAINS_INFO[poolInfo.chainId] @@ -34,17 +62,33 @@ export const useStellarLender = ( contractId: poolInfo.juniorTranche, rpcUrl: chainMetadata.rpc, }) - const [isJuniorApproveLenderRes, juniorRedemptionRecordRes] = - await Promise.all([ - juniorTrancheVaultClient.is_approved_lender({ - account: stellarAddress, - }), - juniorTrancheVaultClient.get_latest_redemption_record({ - lender: stellarAddress, - }), - ]) + const [ + isJuniorApproveLenderRes, + juniorRedemptionRecordRes, + juniorDepositRecordRes, + juniorTrancheSharesRes, + ] = await Promise.all([ + juniorTrancheVaultClient.is_approved_lender({ + account: stellarAddress, + }), + juniorTrancheVaultClient.get_latest_redemption_record({ + lender: stellarAddress, + }), + fetchStellarDepositRecord(poolInfo, 'junior', stellarAddress), + juniorTrancheVaultClient.balance({ id: stellarAddress }), + ]) const isJuniorApprovedLenderVal = isJuniorApproveLenderRes.result const juniorRedemptionRecordVal = juniorRedemptionRecordRes.result + setIsJuniorApprovedLender(isJuniorApprovedLenderVal) + setJuniorRedemptionRecord(juniorRedemptionRecordVal) + setJuniorDepositRecord(juniorDepositRecordRes) + setJuniorTrancheWithdrawable( + BigInt( + juniorRedemptionRecordVal.total_amount_processed - + juniorRedemptionRecordVal.total_amount_withdrawn, + ), + ) + setJuniorTrancheShares(juniorTrancheSharesRes.result) let isSeniorApprovedLenderVal = false let seniorRedemptionRecordVal = null @@ -55,23 +99,35 @@ export const useStellarLender = ( contractId: poolInfo.seniorTranche, rpcUrl: chainMetadata.rpc, }) - const [isSeniorApproveLenderRes, seniorRedemptionRecordRes] = - await Promise.all([ - seniorTrancheVaultClient.is_approved_lender({ - account: stellarAddress, - }), - seniorTrancheVaultClient.get_latest_redemption_record({ - lender: stellarAddress, - }), - ]) + const [ + isSeniorApproveLenderRes, + seniorRedemptionRecordRes, + seniorDepositRecordRes, + seniorTrancheSharesRes, + ] = await Promise.all([ + seniorTrancheVaultClient.is_approved_lender({ + account: stellarAddress, + }), + seniorTrancheVaultClient.get_latest_redemption_record({ + lender: stellarAddress, + }), + fetchStellarDepositRecord(poolInfo, 'senior', stellarAddress), + seniorTrancheVaultClient.balance({ id: stellarAddress }), + ]) isSeniorApprovedLenderVal = isSeniorApproveLenderRes.result seniorRedemptionRecordVal = seniorRedemptionRecordRes.result - } - setIsJuniorApprovedLender(isJuniorApprovedLenderVal) - setJuniorRedemptionRecord(juniorRedemptionRecordVal) - setIsSeniorApprovedLender(isSeniorApprovedLenderVal) - setSeniorRedemptionRecord(seniorRedemptionRecordVal) + setSeniorDepositRecord(seniorDepositRecordRes) + setIsSeniorApprovedLender(isSeniorApprovedLenderVal) + setSeniorRedemptionRecord(seniorRedemptionRecordVal) + setSeniorTrancheWithdrawable( + BigInt( + seniorRedemptionRecordVal.total_amount_processed - + seniorRedemptionRecordVal.total_amount_withdrawn, + ), + ) + setSeniorTrancheShares(seniorTrancheSharesRes.result) + } } catch (error) { console.error('Error fetching Stellar lender data:', error) } finally { @@ -80,13 +136,20 @@ export const useStellarLender = ( } fetchData() - }, [stellarAddress, poolInfo]) + }, [stellarAddress, poolInfo, refreshCount]) return { isLoadingStellarLender, isJuniorApprovedLender, juniorRedemptionRecord, + juniorDepositRecord, + juniorTrancheWithdrawable, + juniorTrancheShares, isSeniorApprovedLender, seniorRedemptionRecord, + seniorDepositRecord, + seniorTrancheWithdrawable, + seniorTrancheShares, + refresh, } } diff --git a/packages/huma-web-shared/src/stellar/hooks/useStellarTokenBalance.ts b/packages/huma-web-shared/src/stellar/hooks/useStellarTokenBalance.ts index 260cdbb5..9a6892d7 100644 --- a/packages/huma-web-shared/src/stellar/hooks/useStellarTokenBalance.ts +++ b/packages/huma-web-shared/src/stellar/hooks/useStellarTokenBalance.ts @@ -5,7 +5,7 @@ import { fetchStellarTokenBalance } from '../utils' export const useStellarTokenBalance = ( chainMetadata: StellarChainInfo, tokenAddress: string, - accountAddress: string, + accountAddress: string | null, sourceAddress?: string, ): { tokenBalance: number | null @@ -17,6 +17,10 @@ export const useStellarTokenBalance = ( useEffect(() => { const getTokenBalance = async () => { + if (!accountAddress) { + return + } + setIsLoadingTokenBalance(true) try { const fetchedBalance = await fetchStellarTokenBalance( diff --git a/packages/huma-web-shared/src/stellar/types/stellarPoolState.ts b/packages/huma-web-shared/src/stellar/types/stellarPoolState.ts index b1ed9b2d..8ce1da0e 100644 --- a/packages/huma-web-shared/src/stellar/types/stellarPoolState.ts +++ b/packages/huma-web-shared/src/stellar/types/stellarPoolState.ts @@ -24,6 +24,8 @@ export type StellarPoolState = { amountOriginated?: number amountRepaid?: number disbursementReserve?: number + juniorTrancheTokenSupply?: string + seniorTrancheTokenSupply?: string accruedIncomes?: { eaIncome: string protocolIncome: string diff --git a/packages/huma-web-shared/src/stellar/utils/fetchStellarDepositRecord.ts b/packages/huma-web-shared/src/stellar/utils/fetchStellarDepositRecord.ts new file mode 100644 index 00000000..b0081bbd --- /dev/null +++ b/packages/huma-web-shared/src/stellar/utils/fetchStellarDepositRecord.ts @@ -0,0 +1,66 @@ +import { + STELLAR_CHAINS_INFO, + StellarPoolInfo, + TrancheType, +} from '@huma-finance/shared' +import { SorobanRpc, Address, xdr, scValToNative } from '@stellar/stellar-sdk' + +const getDepositRecordKey = (contractId: string, address: string) => { + const addressScVal = new Address(address).toScVal() + return xdr.LedgerKey.contractData( + new xdr.LedgerKeyContractData({ + contract: new Address(contractId).toScAddress(), + key: xdr.ScVal.scvVec([ + xdr.ScVal.scvSymbol('DepositRecord'), + addressScVal, + ]), + durability: xdr.ContractDataDurability.persistent(), + }), + ) +} + +export type DepositRecord = { + lastDepositTime: number + principal: bigint +} + +export async function fetchStellarDepositRecord( + poolInfo: StellarPoolInfo, + tranche: TrancheType, + account: string, +): Promise { + try { + const chainMetadata = STELLAR_CHAINS_INFO[poolInfo.chainId] + const server = new SorobanRpc.Server(chainMetadata.rpc) + + const key = getDepositRecordKey( + tranche === 'senior' ? poolInfo.seniorTranche! : poolInfo.juniorTranche, + account, + ) + // Get the contract data with proper durability + const response = await server.getLedgerEntries(key) + + const contractData = response.entries[0].val + + if (contractData.switch() === xdr.LedgerEntryType.contractData()) { + const data = scValToNative(contractData.contractData().val()) + + if ( + data.last_deposit_time === undefined || + data.principal === undefined + ) { + throw new Error('Failed to fetch deposit record') + } + + return { + lastDepositTime: Number(data.last_deposit_time), + principal: BigInt(data.principal), + } + } + + throw new Error('Failed to fetch deposit record') + } catch (error) { + console.error('Error fetching deposit record:', error) + throw error + } +} diff --git a/packages/huma-web-shared/src/stellar/utils/index.ts b/packages/huma-web-shared/src/stellar/utils/index.ts index cb31395b..a0553c40 100644 --- a/packages/huma-web-shared/src/stellar/utils/index.ts +++ b/packages/huma-web-shared/src/stellar/utils/index.ts @@ -1,3 +1,4 @@ export * from './fetchStellarTokenBalance' export * from './stellarTryFn' export * from './getClientCommonParams' +export * from './fetchStellarDepositRecord' diff --git a/packages/huma-widget/src/components/Lend/solanaSupply/4-Transfer.tsx b/packages/huma-widget/src/components/Lend/solanaSupply/4-Transfer.tsx index 5bd3e192..5f6c775e 100644 --- a/packages/huma-widget/src/components/Lend/solanaSupply/4-Transfer.tsx +++ b/packages/huma-widget/src/components/Lend/solanaSupply/4-Transfer.tsx @@ -22,7 +22,6 @@ import { } from '@solana/spl-token' import { useWallet } from '@solana/wallet-adapter-react' import { PublicKey, Transaction } from '@solana/web3.js' -import { BN } from '@coral-xyz/anchor' import { useAppDispatch, useAppSelector } from '../../../hooks/useRedux' import { setPointsAccumulated, setStep } from '../../../store/widgets.reducers' import { selectWidgetState } from '../../../store/widgets.selectors' @@ -167,23 +166,23 @@ export function Transfer({ // Approve automatic redemptions const sharesAmount = convertToShares( selectedTranche === 'senior' - ? new BN(poolState.seniorTrancheAssets ?? 0) - : new BN(poolState.juniorTrancheAssets ?? 0), + ? BigInt(poolState.seniorTrancheAssets ?? 0) + : BigInt(poolState.juniorTrancheAssets ?? 0), selectedTranche === 'senior' - ? seniorTrancheMintSupply ?? new BN(0) - : juniorTrancheMintSupply ?? new BN(0), - supplyBigNumber, + ? BigInt(seniorTrancheMintSupply?.toString() ?? 0) + : BigInt(juniorTrancheMintSupply?.toString() ?? 0), + BigInt(supplyBigNumber.toString()), ) const existingShares = convertToShares( selectedTranche === 'senior' - ? new BN(poolState.seniorTrancheAssets ?? 0) - : new BN(poolState.juniorTrancheAssets ?? 0), + ? BigInt(poolState.seniorTrancheAssets ?? 0) + : BigInt(poolState.juniorTrancheAssets ?? 0), selectedTranche === 'senior' - ? seniorTrancheMintSupply ?? new BN(0) - : juniorTrancheMintSupply ?? new BN(0), + ? BigInt(seniorTrancheMintSupply?.toString() ?? 0) + : BigInt(juniorTrancheMintSupply?.toString() ?? 0), selectedTranche === 'senior' - ? new BN(seniorTokenAccount?.amount.toString() ?? '0') - : new BN(juniorTokenAccount?.amount.toString() ?? '0'), + ? BigInt(seniorTokenAccount?.amount.toString() ?? '0') + : BigInt(juniorTokenAccount?.amount.toString() ?? '0'), ) tx.add( createApproveCheckedInstruction( @@ -195,7 +194,7 @@ export function Transfer({ ), new PublicKey(poolInfo.poolAuthority), // delegate publicKey, // owner of the wallet - BigInt(sharesAmount.muln(1.1).add(existingShares).toString()), // amount + (sharesAmount * BigInt(11)) / BigInt(10) + existingShares, // amount poolInfo.trancheDecimals, undefined, // multiSigners TOKEN_2022_PROGRAM_ID,