@turtleclub/opportunities 0.1.0-beta.13 → 0.1.0-beta.15

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
+
@@ -1,94 +1,65 @@
1
- import { useCallback, useState } from "react";
2
- import nexusSdk from "@nexusmutual/sdk";
3
- import * as ethers from "ethers";
4
- import type { NexusQuoteResult } from "../types";
5
-
6
- type UseNexusPurchaseArgs = {
7
- buyerAddress: string;
8
- productId: number;
9
- amountToCover: string;
10
- onSuccess?: (result: NexusPurchaseResult) => void;
11
- onError?: (error: unknown) => void;
12
- };
13
-
14
- type NexusPurchaseTxRequest = {
15
- to: `0x${string}`;
16
- data: `0x${string}`;
17
- value: bigint;
18
- account?: `0x${string}`;
19
- };
20
-
21
- type NexusPurchaseResult = {
22
- txRequest: NexusPurchaseTxRequest;
23
- quoteResult: NexusQuoteResult;
24
- meta: {
25
- buyerAddress: string;
26
- productId: number;
27
- amountToCover: string;
28
- coverPeriod: number;
29
- };
30
- };
31
-
32
- type UseNexusPurchaseReturn = {
33
- purchase: (quoteResult: NexusQuoteResult, coverPeriod: number) => Promise<NexusPurchaseResult>;
34
- isPurchasing: boolean;
35
- error: unknown | null;
36
- result: NexusPurchaseResult | null;
37
- };
38
-
39
- /** Encapsulate Nexus buyCover prep to mirror executeNexusTransaction flow */
40
- export function useNexusPurchase(args: UseNexusPurchaseArgs): UseNexusPurchaseReturn {
41
- const { buyerAddress, productId, amountToCover, onSuccess, onError } = args;
42
- const [isPurchasing, setIsPurchasing] = useState(false);
43
- const [error, setError] = useState<unknown | null>(null);
44
- const [result, setResult] = useState<NexusPurchaseResult | null>(null);
45
-
46
- const purchase = useCallback(
47
- async (quoteResult: NexusQuoteResult, coverPeriod: number) => {
48
- try {
49
- setIsPurchasing(true);
50
- setError(null);
51
-
52
- const { buyCoverParams, poolAllocationRequests } = quoteResult.buyCoverInput;
53
- const coverBrokerInterface = new ethers.Interface(nexusSdk.abis?.CoverBroker);
54
- const data = coverBrokerInterface.encodeFunctionData("buyCover", [
55
- buyCoverParams,
56
- poolAllocationRequests,
57
- ]);
58
-
59
- const txRequest: NexusPurchaseTxRequest = {
60
- to: nexusSdk.addresses?.CoverBroker as `0x${string}`,
61
- data: data as `0x${string}`,
62
- value: BigInt(quoteResult.displayInfo.premiumInAsset),
63
- account: buyerAddress as `0x${string}`,
64
- };
65
-
66
- const purchaseResult: NexusPurchaseResult = {
67
- txRequest,
68
- quoteResult,
69
- meta: {
70
- buyerAddress,
71
- productId,
72
- amountToCover,
73
- coverPeriod,
74
- },
75
- };
76
-
77
- setResult(purchaseResult);
78
- onSuccess?.(purchaseResult);
79
- return purchaseResult;
80
- } catch (err) {
81
- setError(err);
82
-
83
- onError?.(err);
84
- throw err;
85
- } finally {
86
- setIsPurchasing(false);
87
- }
88
- },
89
- [amountToCover, buyerAddress, onError, onSuccess, productId]
90
- );
91
-
92
- return { purchase, isPurchasing, error, result };
93
- }
94
-
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
+
@@ -1,6 +1,4 @@
1
1
  export { NexusCoverSection } from "./components/NexusCoverSection";
2
2
  export type { NexusCoverSectionProps, NexusQuoteResult } from "./types";
3
3
 
4
- export { useCoverQuote } from "./hooks/useCoverQuote";
5
- export { useNexusPurchase } from "./hooks/useNexusPurchase";
6
4
  export { useNexusProduct, getNexusProductLookup, getProtocolForInsurance } from "./hooks/useNexusProduct";
@@ -22,12 +22,11 @@ export interface CoverOfferCardProps {
22
22
  opportunity: Opportunity;
23
23
  amountToCover: string;
24
24
  buyerAddress: string;
25
-
26
- onPurchase: (quoteResult: NexusQuoteResult, coverPeriod: number) => void;
27
- isPurchasing?: boolean;
28
- purchaseError?: unknown;
29
-
30
- onDismiss?: () => void;
25
+ protocolName: string;
26
+ coverProductName: string;
27
+ onSuccess?: (message: string) => void;
28
+ onError?: (message: string) => void;
29
+ startExpanded?: boolean;
31
30
  }
32
31
 
33
32
  export interface CoverQuoteState {
@@ -42,8 +41,25 @@ export interface NexusCoverSectionProps {
42
41
  opportunity: Opportunity;
43
42
  amountToCover: string;
44
43
  buyerAddress: string;
45
- onSuccess: (res: any) => void;
46
- onError: (err: any) => void;
47
- onDismiss?: () => void;
44
+ onSuccess?: (message: string) => void;
45
+ onError?: (message: string) => void;
48
46
  className?: string;
47
+ startExpanded?: boolean;
49
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
+ }
@@ -1,8 +1,6 @@
1
1
  import type { Opportunity } from "@turtleclub/hooks";
2
+ import { BaseError, UserRejectedRequestError } from "viem";
2
3
 
3
- /**
4
- * Calculate the total APY from an opportunity's incentives
5
- */
6
4
  export function getOpportunityAPY(opportunity: Opportunity): number {
7
5
  if (!opportunity.incentives || opportunity.incentives.length === 0) {
8
6
  return 0;
@@ -27,19 +25,58 @@ export function calculateAdjustedAPY(
27
25
  return Math.max(0, opportunityAPY - coverCostPercentage);
28
26
  }
29
27
 
30
- /**
31
- * Format APY for display
32
- */
28
+
33
29
  export function formatAPY(apy: number): string {
34
30
  return `${apy.toFixed(2)}%`;
35
31
  }
36
32
 
37
- /**
38
- * Format ETH amount for display
39
- */
40
33
  export function formatEthAmount(amount: string, decimals: number = 6): string {
41
34
  const num = parseFloat(amount);
42
35
  if (isNaN(num)) return "0";
43
36
  return num.toFixed(decimals);
44
37
  }
45
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
+ }