@turtleclub/opportunities 0.1.0-beta.2 → 0.1.0-beta.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,126 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { createPublicClient, http, getAddress } from "viem";
3
+ import { mainnet } from "viem/chains";
4
+ import nexusSdk from "@nexusmutual/sdk";
5
+ import { useUserCoverNfts, type CoverNft } from "./useUserCoverNfts";
6
+
7
+ // Raw cover data from contract (all numbers are BigInt)
8
+ interface RawCoverData {
9
+ coverId: bigint;
10
+ productId: bigint;
11
+ coverAsset: bigint;
12
+ amountPaidOut: bigint;
13
+ amount: bigint;
14
+ start: bigint;
15
+ period: bigint;
16
+ gracePeriod: bigint;
17
+ segmentId: bigint;
18
+ expired: boolean;
19
+ }
20
+
21
+ export interface CoverData {
22
+ coverId: bigint;
23
+ productId: number;
24
+ coverAsset: number;
25
+ amountPaidOut: bigint;
26
+ amount: bigint;
27
+ start: number;
28
+ period: number;
29
+ gracePeriod: number;
30
+ segmentId: number;
31
+ expired: boolean;
32
+ }
33
+
34
+ function parseCoverData(raw: RawCoverData): CoverData {
35
+ return {
36
+ coverId: raw.coverId,
37
+ productId: Number(raw.productId),
38
+ coverAsset: Number(raw.coverAsset),
39
+ amountPaidOut: raw.amountPaidOut,
40
+ amount: raw.amount,
41
+ start: Number(raw.start),
42
+ period: Number(raw.period),
43
+ gracePeriod: Number(raw.gracePeriod),
44
+ segmentId: Number(raw.segmentId),
45
+ expired: raw.expired,
46
+ };
47
+ }
48
+
49
+ interface UseExistingCoversReturn {
50
+ covers: CoverData[];
51
+ isLoading: boolean;
52
+ error: string | null;
53
+ hasCoverForProduct: (productId: number) => CoverData | undefined;
54
+ refetch: () => void;
55
+ }
56
+
57
+ const publicClient = createPublicClient({
58
+ chain: mainnet,
59
+ transport: http(),
60
+ });
61
+
62
+ export function useExistingCovers(userAddress: string | undefined): UseExistingCoversReturn {
63
+ const { coverNfts, isLoading: isNftsLoading, refetch: refetchNfts } = useUserCoverNfts(userAddress);
64
+ const [covers, setCovers] = useState<CoverData[]>([]);
65
+ const [isLoading, setIsLoading] = useState(false);
66
+ const [error, setError] = useState<string | null>(null);
67
+
68
+ const fetchCovers = useCallback(async () => {
69
+ if (isNftsLoading || coverNfts.length === 0) {
70
+ setCovers([]);
71
+ return;
72
+ }
73
+
74
+ setIsLoading(true);
75
+ setError(null);
76
+
77
+ try {
78
+ const coverIds = coverNfts.map((nft) => BigInt(nft.token_id));
79
+ const coverViewerAddress = nexusSdk.addresses?.CoverViewer;
80
+ const coverViewerAbi = nexusSdk.abis?.CoverViewer;
81
+ if (!coverViewerAddress || !coverViewerAbi) {
82
+ throw new Error('Nexus SDK not configured correctly for CoverViewer.');
83
+ }
84
+ const result = await publicClient.readContract({
85
+ address: getAddress(coverViewerAddress),
86
+ abi: coverViewerAbi as any,
87
+ functionName: "getCovers",
88
+ args: [coverIds],
89
+ });
90
+
91
+ const rawCovers = result as RawCoverData[];
92
+ const parsedCovers = rawCovers.map(parseCoverData);
93
+ setCovers(parsedCovers);
94
+ } catch (err: unknown) {
95
+ const errorMessage = err instanceof Error ? err.message : "Failed to fetch cover details";
96
+ console.error("Cover details fetch error:", err);
97
+ setError(errorMessage);
98
+ setCovers([]);
99
+ } finally {
100
+ setIsLoading(false);
101
+ }
102
+ }, [coverNfts, isNftsLoading]);
103
+
104
+ useEffect(() => {
105
+ fetchCovers();
106
+ }, [fetchCovers]);
107
+
108
+ const hasCoverForProduct = useCallback(
109
+ (productId: number): CoverData | undefined => {
110
+ return covers.find((cover) => cover.productId === productId && !cover.expired);
111
+ },
112
+ [covers]
113
+ );
114
+
115
+ return {
116
+ covers,
117
+ isLoading: isLoading || isNftsLoading,
118
+ error,
119
+ hasCoverForProduct,
120
+ refetch: () => {
121
+ refetchNfts();
122
+ fetchCovers();
123
+ },
124
+ };
125
+ }
126
+
@@ -0,0 +1,74 @@
1
+ import { useMemo } from "react";
2
+ import { products, ProductTypes } from "@nexusmutual/sdk";
3
+ import type { Opportunity } from "@turtleclub/hooks";
4
+ import { NexusProduct } from "../types";
5
+
6
+ const getHighestPriorityProduct = (
7
+ currentBest: NexusProduct | null,
8
+ newProduct: NexusProduct,
9
+ ): boolean => {
10
+ // Prefer v3 products, and for v3 prefer the highest id
11
+ if (newProduct.name.includes("v3") && currentBest && !currentBest.name.includes("v3")) {
12
+ return true;
13
+ }
14
+
15
+ if (newProduct.name.includes("v3") && currentBest?.name.includes("v3") && newProduct.id > currentBest.id) {
16
+ return true;
17
+ }
18
+
19
+ if (!currentBest) {
20
+ return true;
21
+ }
22
+
23
+ return false;
24
+ };
25
+
26
+ const NEXUS_PRODUCT_MAP: Record<string, NexusProduct> = products
27
+ .filter((x) => !x.isDeprecated && !x.isPrivate && x.productType === ProductTypes.singleProtocol)
28
+ .reduce((acc: Record<string, NexusProduct>, product: any) => {
29
+ const protocolNameMatch = product.name.match(/^(\w+)/);
30
+ const protocolName = protocolNameMatch ? protocolNameMatch[1].toLowerCase() : null;
31
+
32
+ if (!protocolName) {
33
+ return acc;
34
+ }
35
+
36
+ const currentBest = acc[protocolName] || null;
37
+
38
+ if (getHighestPriorityProduct(currentBest, product)) {
39
+ acc[protocolName] = {
40
+ id: product.id,
41
+ name: product.name,
42
+ coverAssets: product.coverAssets,
43
+ };
44
+ }
45
+ return acc;
46
+ }, {});
47
+
48
+ export const getProtocolForInsurance = (opportunity: Opportunity | null | undefined) => {
49
+ return opportunity?.vaultConfig?.infraProvider?.name ?? null;
50
+ };
51
+
52
+ export const getNexusProductLookup = (opportunity: Opportunity | null | undefined) => {
53
+ const protocolName = getProtocolForInsurance(opportunity);
54
+ const nexusProduct = protocolName ? NEXUS_PRODUCT_MAP[protocolName.toLowerCase()] : null;
55
+
56
+ return {
57
+ protocolName: protocolName ?? "Unknown",
58
+ productId: nexusProduct?.id ?? null,
59
+ coverProductName: nexusProduct?.name ?? null,
60
+ isCoverable: !!nexusProduct,
61
+ };
62
+ };
63
+
64
+ export interface NexusProductLookup {
65
+ protocolName: string;
66
+ productId: number | null;
67
+ coverProductName: string | null;
68
+ isCoverable: boolean;
69
+ }
70
+
71
+ export function useNexusProduct(opportunity: Opportunity | null | undefined): NexusProductLookup {
72
+ return useMemo(() => getNexusProductLookup(opportunity), [opportunity]);
73
+ }
74
+
@@ -0,0 +1,65 @@
1
+ import { useCallback, useState } from "react";
2
+ import nexusSdk from "@nexusmutual/sdk";
3
+ import { getAddress } from "viem";
4
+ import { useWriteContract, useConfig } from "wagmi";
5
+ import { waitForTransactionReceipt } from "wagmi/actions";
6
+ import { formatPurchaseError } from "../utils";
7
+ import type { NexusQuoteResult } from "../types";
8
+
9
+ type UseNexusPurchaseArgs = {
10
+ onSuccess?: (message: string) => void;
11
+ onError?: (message: string) => void;
12
+ };
13
+
14
+ type UseNexusPurchaseReturn = {
15
+ purchase: (quoteResult: NexusQuoteResult) => Promise<string>;
16
+ isPurchasing: boolean;
17
+ };
18
+
19
+ export function useNexusPurchase(args: UseNexusPurchaseArgs): UseNexusPurchaseReturn {
20
+ const { onSuccess, onError } = args;
21
+ const config = useConfig();
22
+ const { writeContractAsync } = useWriteContract();
23
+ const [isPurchasing, setIsPurchasing] = useState(false);
24
+
25
+ const purchase = useCallback(
26
+ async (quoteResult: NexusQuoteResult): Promise<string> => {
27
+ try {
28
+ setIsPurchasing(true);
29
+
30
+
31
+ const { buyCoverParams, poolAllocationRequests } = quoteResult.buyCoverInput;
32
+ const coverAddress = nexusSdk.addresses?.Cover;
33
+ const coverAbi = nexusSdk.abis?.Cover;
34
+ if (!coverAddress || !coverAbi) {
35
+ throw new Error('Nexus SDK not configured correctly for Cover.');
36
+ }
37
+
38
+ const txHash = await writeContractAsync({
39
+ address: getAddress(coverAddress),
40
+ abi: coverAbi as any,
41
+ functionName: "buyCover",
42
+ args: [buyCoverParams, poolAllocationRequests],
43
+ value: BigInt(quoteResult.displayInfo.premiumInAsset),
44
+ chainId: 1,
45
+ });
46
+
47
+ await waitForTransactionReceipt(config, {
48
+ hash: txHash,
49
+ confirmations: 1,
50
+ });
51
+
52
+ onSuccess?.("Cover purchased successfully");
53
+ return txHash;
54
+ } catch (err) {
55
+ onError?.(formatPurchaseError(err));
56
+ throw err;
57
+ } finally {
58
+ setIsPurchasing(false);
59
+ }
60
+ },
61
+ [config, writeContractAsync, onError, onSuccess]
62
+ );
63
+
64
+ return { purchase, isPurchasing };
65
+ }
@@ -0,0 +1,83 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { NEXUS_COVER_NFT_ADDRESS, USER_NFTS_API_URL } from "../constants";
3
+
4
+ interface UserNft {
5
+ user: string;
6
+ chain: number;
7
+ token: string;
8
+ token_id: string;
9
+ last_transfer: string;
10
+ }
11
+
12
+ export type CoverNft = UserNft;
13
+
14
+ interface UseUserCoverNftsReturn {
15
+ coverNfts: CoverNft[];
16
+ isLoading: boolean;
17
+ error: string | null;
18
+ refetch: () => void;
19
+ }
20
+
21
+ async function fetchUserNfts(userAddress: string): Promise<UserNft[]> {
22
+ const response = await fetch(USER_NFTS_API_URL, {
23
+ method: "POST",
24
+ headers: {
25
+ "Content-Type": "application/json",
26
+ },
27
+ body: JSON.stringify({
28
+ user: userAddress,
29
+ chain: 1,
30
+ }),
31
+ });
32
+
33
+
34
+
35
+ if (!response.ok) {
36
+ throw new Error("Failed to fetch user NFTs");
37
+ }
38
+
39
+ return response.json();
40
+ }
41
+
42
+ export function useUserCoverNfts(userAddress: string | undefined): UseUserCoverNftsReturn {
43
+ const [coverNfts, setCoverNfts] = useState<CoverNft[]>([]);
44
+ const [isLoading, setIsLoading] = useState(false);
45
+ const [error, setError] = useState<string | null>(null);
46
+
47
+ const fetchCoverNfts = useCallback(async () => {
48
+ if (!userAddress) {
49
+ setCoverNfts([]);
50
+ return;
51
+ }
52
+
53
+ setIsLoading(true);
54
+ setError(null);
55
+
56
+ try {
57
+ const nfts = await fetchUserNfts(userAddress);
58
+ const filtered = nfts.filter(
59
+ (nft) => nft.token.toLowerCase() === NEXUS_COVER_NFT_ADDRESS.toLowerCase()
60
+ );
61
+ setCoverNfts(filtered);
62
+ } catch (err: unknown) {
63
+ const errorMessage = err instanceof Error ? err.message : "Failed to fetch cover NFTs";
64
+ console.error("Cover NFTs fetch error:", err);
65
+ setError(errorMessage);
66
+ setCoverNfts([]);
67
+ } finally {
68
+ setIsLoading(false);
69
+ }
70
+ }, [userAddress]);
71
+
72
+ useEffect(() => {
73
+ fetchCoverNfts();
74
+ }, [fetchCoverNfts]);
75
+
76
+ return {
77
+ coverNfts,
78
+ isLoading,
79
+ error,
80
+ refetch: fetchCoverNfts,
81
+ };
82
+ }
83
+
@@ -0,0 +1,4 @@
1
+ export { NexusCoverSection } from "./components/NexusCoverSection";
2
+ export type { NexusCoverSectionProps, NexusQuoteResult } from "./types";
3
+
4
+ export { useNexusProduct, getNexusProductLookup, getProtocolForInsurance } from "./hooks/useNexusProduct";
@@ -0,0 +1,65 @@
1
+ import type { Opportunity } from "@turtleclub/hooks";
2
+
3
+ /** Result from Nexus SDK quote API */
4
+ export interface NexusQuoteResult {
5
+ buyCoverInput: {
6
+ buyCoverParams: unknown;
7
+ poolAllocationRequests: unknown;
8
+ };
9
+ displayInfo: {
10
+ premiumInAsset: string;
11
+ yearlyCostPerc: number;
12
+ };
13
+ }
14
+ export type NexusProduct = {
15
+ id: number;
16
+ name: string;
17
+ coverAssets: number[];
18
+ };
19
+
20
+ export interface CoverOfferCardProps {
21
+ productId: number;
22
+ opportunity: Opportunity;
23
+ amountToCover: string;
24
+ buyerAddress: string;
25
+ protocolName: string;
26
+ coverProductName: string;
27
+ onSuccess?: (message: string) => void;
28
+ onError?: (message: string) => void;
29
+ startExpanded?: boolean;
30
+ }
31
+
32
+ export interface CoverQuoteState {
33
+ isLoading: boolean;
34
+ error: string | null;
35
+ quoteResult: NexusQuoteResult | null;
36
+ coverPeriod: number;
37
+ premiumEth: string | null;
38
+ yearlyCostPerc: number | null;
39
+ }
40
+ export interface NexusCoverSectionProps {
41
+ opportunity: Opportunity;
42
+ amountToCover: string;
43
+ buyerAddress: string;
44
+ onSuccess?: (message: string) => void;
45
+ onError?: (message: string) => void;
46
+ className?: string;
47
+ startExpanded?: boolean;
48
+ }
49
+
50
+ export interface CoverRequestData {
51
+ protocolName: string;
52
+ coverageAmount: number;
53
+ periodDays: number;
54
+ desiredApySacrifice: number;
55
+ estimatedPremium: number;
56
+ }
57
+ export interface CoverRequestFormProps {
58
+ protocolName: string;
59
+ depositedAmount?: string;
60
+ baseApyLabel?: string;
61
+ onDismiss?: () => void;
62
+ onSuccess?: (message: string) => void;
63
+ onError?: (message: string) => void;
64
+ startExpanded?: boolean;
65
+ }
@@ -0,0 +1,82 @@
1
+ import type { Opportunity } from "@turtleclub/hooks";
2
+ import { BaseError, UserRejectedRequestError } from "viem";
3
+
4
+ export function getOpportunityAPY(opportunity: Opportunity): number {
5
+ if (!opportunity.incentives || opportunity.incentives.length === 0) {
6
+ return 0;
7
+ }
8
+
9
+ return opportunity.incentives.reduce((total, incentive) => {
10
+ return total + (incentive.yield ?? 0);
11
+ }, 0);
12
+ }
13
+
14
+ /**
15
+ * Calculate the adjusted APY after subtracting the cover cost
16
+ * @param opportunityAPY - The original APY as a percentage (e.g., 5 for 5%)
17
+ * @param yearlyCostPerc - The yearly cost as a decimal (e.g., 0.025 for 2.5%)
18
+ * @returns The adjusted APY as a percentage
19
+ */
20
+ export function calculateAdjustedAPY(
21
+ opportunityAPY: number,
22
+ yearlyCostPerc: number
23
+ ): number {
24
+ const coverCostPercentage = yearlyCostPerc * 100;
25
+ return Math.max(0, opportunityAPY - coverCostPercentage);
26
+ }
27
+
28
+
29
+ export function formatAPY(apy: number): string {
30
+ return `${apy.toFixed(2)}%`;
31
+ }
32
+
33
+ export function formatEthAmount(amount: string, decimals: number = 6): string {
34
+ const num = parseFloat(amount);
35
+ if (isNaN(num)) return "0";
36
+ return num.toFixed(decimals);
37
+ }
38
+
39
+
40
+ export const formatPurchaseError = (err: unknown): string => {
41
+ console.error("Purchase error:", err, typeof err);
42
+ if (err instanceof UserRejectedRequestError) {
43
+ return "Transaction cancelled";
44
+ }
45
+
46
+ if (err instanceof BaseError) {
47
+ const userRejected =
48
+ typeof err.walk === "function"
49
+ ? err.walk((error) => error instanceof UserRejectedRequestError)
50
+ : false;
51
+ if (userRejected) {
52
+ return "Transaction cancelled";
53
+ }
54
+
55
+ return err.shortMessage || err.message;
56
+ }
57
+
58
+ if (err instanceof Error) {
59
+ return err.message;
60
+ }
61
+
62
+ return "Failed to purchase cover";
63
+ };
64
+
65
+
66
+
67
+ const SECS_IN_DAY = 24 * 60 * 60;
68
+
69
+ export function parseCoverTiming(startSec: number, periodSec: number, gracePeriodSec: number = 0) {
70
+ const expiresSec = startSec + periodSec;
71
+ const graceEndsSec = expiresSec + gracePeriodSec;
72
+
73
+ return {
74
+ startDate: new Date(startSec * 1000),
75
+ expiresDate: new Date(expiresSec * 1000),
76
+ graceEndsDate: new Date(graceEndsSec * 1000),
77
+ periodDays: periodSec / SECS_IN_DAY,
78
+ graceDays: gracePeriodSec / SECS_IN_DAY,
79
+ expiresSec,
80
+ graceEndsSec,
81
+ };
82
+ }
package/src/index.ts CHANGED
@@ -7,6 +7,9 @@ export * from "./deposit";
7
7
  // Transaction Status
8
8
  export * from "./transaction-status";
9
9
 
10
+ // Cover Offer
11
+ export * from "./cover-offer";
12
+
10
13
  // Opportunity Table
11
14
  export * from "./opportunity-table/components";
12
15