@turtleclub/covers 0.1.0-beta.1

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.
Files changed (27) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +51 -0
  3. package/package.json +38 -0
  4. package/src/index.ts +1 -0
  5. package/src/nexus-mutual-cover/components/CoverOfferCard.tsx +495 -0
  6. package/src/nexus-mutual-cover/components/CoverRequestForm.tsx +362 -0
  7. package/src/nexus-mutual-cover/components/CoveredEventsInfo.tsx +120 -0
  8. package/src/nexus-mutual-cover/components/ExistingCoverInfo.tsx +74 -0
  9. package/src/nexus-mutual-cover/components/ExistingCoverRequest.tsx +65 -0
  10. package/src/nexus-mutual-cover/components/NexusCoverSection.tsx +49 -0
  11. package/src/nexus-mutual-cover/components/PurchaseButtonSection.tsx +106 -0
  12. package/src/nexus-mutual-cover/components/index.ts +7 -0
  13. package/src/nexus-mutual-cover/constants.ts +39 -0
  14. package/src/nexus-mutual-cover/hooks/index.ts +9 -0
  15. package/src/nexus-mutual-cover/hooks/useCheckNexusMembership.ts +32 -0
  16. package/src/nexus-mutual-cover/hooks/useCoverQuote.ts +126 -0
  17. package/src/nexus-mutual-cover/hooks/useCoverTokenSelection.ts +84 -0
  18. package/src/nexus-mutual-cover/hooks/useDebouncedValue.ts +12 -0
  19. package/src/nexus-mutual-cover/hooks/useExistingCovers.ts +110 -0
  20. package/src/nexus-mutual-cover/hooks/useNexusProduct.ts +79 -0
  21. package/src/nexus-mutual-cover/hooks/useNexusPurchase.ts +67 -0
  22. package/src/nexus-mutual-cover/hooks/useTokenApproval.ts +118 -0
  23. package/src/nexus-mutual-cover/hooks/useUserCoverNfts.ts +31 -0
  24. package/src/nexus-mutual-cover/index.ts +4 -0
  25. package/src/nexus-mutual-cover/types/index.ts +41 -0
  26. package/src/nexus-mutual-cover/utils/index.ts +95 -0
  27. package/tsconfig.json +22 -0
@@ -0,0 +1,106 @@
1
+ "use client";
2
+
3
+ import type { ComponentType } from "react";
4
+ import { ExternalLink, type LucideProps } from "lucide-react";
5
+ import { Button, Skeleton, Tooltip, TooltipTrigger, TooltipContent, Card } from "@turtleclub/ui";
6
+ import { NEXUS_MEMBERSHIP_URL } from "../constants";
7
+
8
+ const ExternalLinkIcon = ExternalLink as ComponentType<LucideProps>;
9
+
10
+ export interface PurchaseButtonSectionProps {
11
+ isWrongNetwork: boolean;
12
+ isMembershipLoading: boolean;
13
+ isMember: boolean;
14
+ disabledReason: string | null;
15
+ purchaseButton: React.ReactNode;
16
+ onSwitchNetwork: () => void;
17
+ needsApproval?: boolean;
18
+ isCheckingApproval?: boolean;
19
+ onApprove?: () => Promise<void>;
20
+ isApproving?: boolean;
21
+ tokenSymbol?: string;
22
+ }
23
+
24
+ export function PurchaseButtonSection({
25
+ isWrongNetwork,
26
+ isMembershipLoading,
27
+ isMember,
28
+ disabledReason,
29
+ purchaseButton,
30
+ onSwitchNetwork,
31
+ needsApproval = false,
32
+ isCheckingApproval = false,
33
+ onApprove,
34
+ isApproving = false,
35
+ tokenSymbol,
36
+ }: PurchaseButtonSectionProps) {
37
+ if (isWrongNetwork) {
38
+ return (
39
+ <Card variant="border" className="h-fit">
40
+ <Button
41
+ onClick={onSwitchNetwork}
42
+ className="text-primary hover:text-primary/80 w-full cursor-pointer font-medium underline transition-colors"
43
+ >
44
+ Switch to Ethereum network to Buy Cover
45
+ </Button>
46
+ </Card>
47
+ );
48
+ }
49
+
50
+ if (isMembershipLoading) {
51
+ return <Skeleton className="h-10 w-full bg-neutral-alpha-10" />;
52
+ }
53
+
54
+ if (!isMember) {
55
+ return (
56
+ <div className="space-y-3">
57
+ <div className="p-3 rounded-lg bg-background/50 border border-border">
58
+ <p className="text-xs text-muted-foreground leading-relaxed">
59
+ <span className="text-primary font-medium">Membership required.</span> Join Nexus Mutual
60
+ to protect your deposit against smart contract exploits, oracle failures, and
61
+ protocol-specific risks.
62
+ </p>
63
+ </div>
64
+ <Button asChild className="w-full" variant="green">
65
+ <a
66
+ href={NEXUS_MEMBERSHIP_URL}
67
+ target="_blank"
68
+ rel="noopener noreferrer"
69
+ className="flex items-center justify-center gap-2"
70
+ >
71
+ Become a Member
72
+ <ExternalLinkIcon className="w-4 h-4" />
73
+ </a>
74
+ </Button>
75
+ </div>
76
+ );
77
+ }
78
+
79
+ if (needsApproval && tokenSymbol && onApprove) {
80
+ return (
81
+ <div className="space-y-3">
82
+ <Button
83
+ onClick={onApprove}
84
+ disabled={isApproving || isCheckingApproval}
85
+ className="w-full"
86
+ variant="green"
87
+ >
88
+ {isApproving || isCheckingApproval ? "Approving..." : `Approve ${tokenSymbol} spending`}
89
+ </Button>
90
+ </div>
91
+ );
92
+ }
93
+
94
+ if (disabledReason) {
95
+ return (
96
+ <Tooltip>
97
+ <TooltipTrigger asChild>
98
+ <div>{purchaseButton}</div>
99
+ </TooltipTrigger>
100
+ <TooltipContent side="top">{disabledReason}</TooltipContent>
101
+ </Tooltip>
102
+ );
103
+ }
104
+
105
+ return <>{purchaseButton}</>;
106
+ }
@@ -0,0 +1,7 @@
1
+ export { NexusCoverSection } from "./NexusCoverSection";
2
+ export { CoverOfferCard } from "./CoverOfferCard";
3
+ export { CoverRequestForm } from "./CoverRequestForm";
4
+ export { PurchaseButtonSection } from "./PurchaseButtonSection";
5
+ export { ExistingCoverInfo } from "./ExistingCoverInfo";
6
+ export { ExistingCoverRequest } from "./ExistingCoverRequest";
7
+ export { CoveredEventsInfo } from "./CoveredEventsInfo";
@@ -0,0 +1,39 @@
1
+ export const NEXUS_PROTOCOL_COVER_TERMS_URL =
2
+ "https://api.nexusmutual.io/ipfs/QmQz38DSo6DyrHkRj8uvtGFyx842izVvnx8a3qqF99dctG";
3
+ export const NEXUS_KYC_REQUIREMENTS_URL =
4
+ "https://docs.nexusmutual.io/overview/membership/#kyc-requirements";
5
+ export const NEXUS_MEMBERSHIP_URL = "https://app.nexusmutual.io/become-member";
6
+ export const NEXUS_COVER_NFT_ADDRESS = "0xcafeaca76be547f14d0220482667b42d8e7bc3eb";
7
+ export const NEXUS_COVER_ROUTER = "0xcafeac0fF5dA0A2777d915531bfA6B29d282Ee62";
8
+
9
+ // These are the tokens that Nexus Mutual supports.
10
+ export const NEXUS_AVAILABLE_TOKENS = [
11
+ {
12
+ symbol: "ETH",
13
+ name: "Ethereum",
14
+ address: "0x0000000000000000000000000000000000000000",
15
+ logoUrl: "https://storage.googleapis.com/turtle-assets/tokens/eth.png",
16
+ decimals: 18,
17
+ },
18
+ {
19
+ symbol: "USDC",
20
+ name: "USD Coin",
21
+ address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
22
+ logoUrl: "https://storage.googleapis.com/turtle-assets/tokens/usdc.png",
23
+ decimals: 6,
24
+ },
25
+ {
26
+ symbol: "cbBTC",
27
+ name: "Coinbase Wrapped BTC",
28
+ address: "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf",
29
+ logoUrl: "https://storage.googleapis.com/turtle-assets/tokens/cbbtc.png",
30
+ decimals: 8,
31
+ },
32
+ ] as const;
33
+
34
+ export const PERIOD_OPTIONS = [
35
+ { label: "28d", days: 28 },
36
+ { label: "3m", days: 90 },
37
+ { label: "6m", days: 180 },
38
+ { label: "1y", days: 365 },
39
+ ] as const;
@@ -0,0 +1,9 @@
1
+ export { useNexusProduct, getNexusProductLookup, getProtocolForInsurance } from "./useNexusProduct";
2
+ export { useCheckNexusMembership } from "./useCheckNexusMembership";
3
+ export { useCoverQuote } from "./useCoverQuote";
4
+ export { useNexusPurchase } from "./useNexusPurchase";
5
+ export { useExistingCovers } from "./useExistingCovers";
6
+ export { useUserCoverNfts } from "./useUserCoverNfts";
7
+ export { useCoverTokenSelection } from "./useCoverTokenSelection";
8
+ export { useTokenApproval } from "./useTokenApproval";
9
+ export { useDebouncedValue } from "./useDebouncedValue";
@@ -0,0 +1,32 @@
1
+ import { getAddress } from "viem";
2
+ import type { Abi } from "viem";
3
+ import { useReadContract } from "wagmi";
4
+ import nexusSdk from "@nexusmutual/sdk";
5
+
6
+ export function useCheckNexusMembership(
7
+ userAddress: string | undefined,
8
+ chainId: string | number | undefined
9
+ ) {
10
+ const masterAddress = nexusSdk.addresses?.NXMaster;
11
+ const masterAbi = nexusSdk.abis?.NXMaster;
12
+ const numericChainId = typeof chainId === "string" ? parseInt(chainId, 10) : chainId;
13
+ const isOnEthereum = numericChainId === 1;
14
+
15
+ const {
16
+ data: isMember = false,
17
+ isLoading,
18
+ error,
19
+ } = useReadContract({
20
+ address: masterAddress ? getAddress(masterAddress) : undefined,
21
+ abi: masterAbi as Abi,
22
+ functionName: "isMember",
23
+ chainId: 1,
24
+ args: userAddress ? [getAddress(userAddress)] : undefined,
25
+ query: {
26
+ enabled: isOnEthereum && !!userAddress && !!masterAddress && !!masterAbi,
27
+ select: (data) => data as boolean,
28
+ },
29
+ });
30
+
31
+ return { isMember, isLoading, error };
32
+ }
@@ -0,0 +1,126 @@
1
+ import { parseUnits, formatUnits } from "viem";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { CoverAsset, GetQuoteResponse } from "@nexusmutual/sdk";
4
+ import { TOKEN_SYMBOL_TO_COVER_ASSET, getOpportunityAPY, calculateAdjustedAPY } from "../utils";
5
+ import type { Opportunity } from "@turtleclub/hooks";
6
+
7
+ interface UseCoverQuoteOptions {
8
+ productId: number;
9
+ buyerAddress: string;
10
+ amountToCover: string;
11
+ coverPeriod: number;
12
+ tokenSymbol: string;
13
+ decimals: number | undefined;
14
+ opportunity: Opportunity;
15
+ }
16
+
17
+ interface UseCoverQuoteReturn {
18
+ isLoading: boolean;
19
+ error: string | null;
20
+ quoteResult: GetQuoteResponse | null;
21
+ premium: string | null;
22
+ originalAPY: number;
23
+ adjustedAPY: number | null;
24
+ coverCostPerc: number | null;
25
+ refetch: () => void;
26
+ }
27
+
28
+ let nexusSdkInstance: import("@nexusmutual/sdk").NexusSDK | null = null;
29
+
30
+ const TERMS_IPFS_CID = "QmXUzXDMbeKSCewUie34vPD7mCAGnshi4ULRy4h7DLmoRS";
31
+
32
+ async function getNexusSdk() {
33
+ if (!nexusSdkInstance) {
34
+ const { NexusSDK } = await import("@nexusmutual/sdk");
35
+ nexusSdkInstance = new NexusSDK();
36
+ }
37
+ return nexusSdkInstance;
38
+ }
39
+
40
+ async function fetchNexusQuote(
41
+ productId: number,
42
+ buyerAddress: string,
43
+ amountToCover: string,
44
+ period: number,
45
+ coverAsset: CoverAsset
46
+ ): Promise<GetQuoteResponse> {
47
+ const nexusSdk = await getNexusSdk();
48
+
49
+ const { result, error } = await nexusSdk.quote.getQuoteAndBuyCoverInputs({
50
+ productId,
51
+ amount: amountToCover,
52
+ period,
53
+ coverAsset,
54
+ paymentAsset: coverAsset,
55
+ buyerAddress,
56
+ ipfsCidOrContent: TERMS_IPFS_CID,
57
+ });
58
+
59
+
60
+ if (error) {
61
+ throw new Error(error.message || "Failed to fetch Nexus quote");
62
+ }
63
+
64
+ return result as GetQuoteResponse;
65
+ }
66
+
67
+ export function useCoverQuote({
68
+ productId,
69
+ buyerAddress,
70
+ amountToCover,
71
+ coverPeriod,
72
+ tokenSymbol,
73
+ decimals,
74
+ opportunity,
75
+ }: UseCoverQuoteOptions): UseCoverQuoteReturn {
76
+ const coverAsset = TOKEN_SYMBOL_TO_COVER_ASSET[tokenSymbol] ?? CoverAsset.ETH;
77
+ const originalAPY = getOpportunityAPY(opportunity);
78
+
79
+ if (!decimals) {
80
+ return {
81
+ isLoading: false,
82
+ error: "Decimals are required",
83
+ quoteResult: null,
84
+ premium: null,
85
+ originalAPY,
86
+ adjustedAPY: null,
87
+ coverCostPerc: null,
88
+ refetch: () => { },
89
+ };
90
+ }
91
+ const isValidInput =
92
+ !!productId && !!buyerAddress && !!amountToCover && parseFloat(amountToCover) > 0 && decimals !== undefined;
93
+
94
+ const parsedAmount = parseUnits(amountToCover, decimals);
95
+
96
+ const {
97
+ data,
98
+ isLoading,
99
+ error: queryError,
100
+ refetch,
101
+ } = useQuery({
102
+ queryKey: ["coverQuote", productId, buyerAddress, parsedAmount.toString(), coverPeriod, tokenSymbol],
103
+ queryFn: () => fetchNexusQuote(productId, buyerAddress, parsedAmount.toString(), coverPeriod, coverAsset),
104
+ enabled: isValidInput,
105
+ select: (result: GetQuoteResponse) => ({
106
+ quoteResult: result,
107
+ premium: formatUnits(BigInt(result.displayInfo.premiumInAsset), decimals),
108
+ yearlyCostPerc: result.displayInfo.yearlyCostPerc,
109
+ }),
110
+ });
111
+
112
+ const yearlyCostPerc = data?.yearlyCostPerc ?? null;
113
+ const adjustedAPY = yearlyCostPerc !== null ? calculateAdjustedAPY(originalAPY, yearlyCostPerc) : null;
114
+ const coverCostPerc = yearlyCostPerc !== null ? yearlyCostPerc * 100 : null;
115
+
116
+ return {
117
+ isLoading,
118
+ error: queryError?.message ?? null,
119
+ quoteResult: data?.quoteResult ?? null,
120
+ premium: data?.premium ?? null,
121
+ originalAPY,
122
+ adjustedAPY,
123
+ coverCostPerc,
124
+ refetch,
125
+ };
126
+ }
@@ -0,0 +1,84 @@
1
+ import { useState, useMemo } from "react";
2
+ import { usePortfolioBalance, useTokenBalance } from "@turtleclub/hooks";
3
+ import { NEXUS_AVAILABLE_TOKENS } from "../constants";
4
+
5
+ type NexusToken = (typeof NEXUS_AVAILABLE_TOKENS)[number];
6
+
7
+ interface UseCoverTokenSelectionParams {
8
+ buyerAddress: string;
9
+ coverAvailableAssets: string[] | undefined;
10
+ amount: string;
11
+ setAmount: (amount: string) => void;
12
+ }
13
+
14
+ interface UseCoverTokenSelectionReturn {
15
+ filteredTokens: NexusToken[];
16
+ selectedTokenAddress: string;
17
+ setSelectedTokenAddress: (address: string) => void;
18
+ selectedTokenSymbol: string;
19
+ selectedTokenDecimals: number;
20
+ selectedTokenBalance: ReturnType<typeof usePortfolioBalance>["balances"][number] | null;
21
+ usdValue: string | undefined;
22
+ handleMaxClick: () => void;
23
+ }
24
+
25
+ export function useCoverTokenSelection({
26
+ buyerAddress,
27
+ coverAvailableAssets,
28
+ amount,
29
+ setAmount,
30
+ }: UseCoverTokenSelectionParams): UseCoverTokenSelectionReturn {
31
+ const filteredTokens = useMemo(
32
+ () =>
33
+ NEXUS_AVAILABLE_TOKENS.filter((token) =>
34
+ coverAvailableAssets?.includes(token.symbol)
35
+ ),
36
+ [coverAvailableAssets]
37
+ );
38
+
39
+ const [selectedTokenAddress, setSelectedTokenAddress] = useState<string>(
40
+ filteredTokens[0]?.address || ""
41
+ );
42
+
43
+ const { balances } = usePortfolioBalance({
44
+ address: buyerAddress,
45
+ });
46
+
47
+ const userEthereumBalances = useMemo(
48
+ () => balances.filter((balance) => balance.token.chain.chainId === "1"),
49
+ [balances]
50
+ );
51
+
52
+ const selectedTokenBalance = useMemo(
53
+ () =>
54
+ userEthereumBalances.find(
55
+ (balance) => balance.token.address === selectedTokenAddress
56
+ ) ?? null,
57
+ [userEthereumBalances, selectedTokenAddress]
58
+ );
59
+
60
+ const { usdValue, handleMaxClick } = useTokenBalance({
61
+ tokenBalance: selectedTokenBalance,
62
+ amount: amount || undefined,
63
+ setAmount: (newAmount) => setAmount(newAmount ?? ""),
64
+ });
65
+
66
+ const selectedToken = useMemo(
67
+ () => filteredTokens.find((t) => t.address === selectedTokenAddress),
68
+ [filteredTokens, selectedTokenAddress]
69
+ );
70
+
71
+ const selectedTokenSymbol = selectedToken?.symbol ?? "ETH";
72
+ const selectedTokenDecimals = selectedToken?.decimals ?? 18;
73
+
74
+ return {
75
+ filteredTokens,
76
+ selectedTokenAddress,
77
+ setSelectedTokenAddress,
78
+ selectedTokenSymbol,
79
+ selectedTokenDecimals,
80
+ selectedTokenBalance,
81
+ usdValue,
82
+ handleMaxClick,
83
+ };
84
+ }
@@ -0,0 +1,12 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ export function useDebouncedValue<T>(value: T, delay: number): T {
4
+ const [debouncedValue, setDebouncedValue] = useState(value);
5
+
6
+ useEffect(() => {
7
+ const timer = setTimeout(() => setDebouncedValue(value), delay);
8
+ return () => clearTimeout(timer);
9
+ }, [value, delay]);
10
+
11
+ return debouncedValue;
12
+ }
@@ -0,0 +1,110 @@
1
+ import { useCallback, useMemo } from "react";
2
+ import { getAddress } from "viem";
3
+ import { useReadContract } from "wagmi";
4
+ import nexusSdk from "@nexusmutual/sdk";
5
+ import { useUserCoverNfts } from "./useUserCoverNfts";
6
+ import type { Abi } from "viem";
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
+ const start = Number(raw.start);
36
+ const period = Number(raw.period);
37
+ const nowSec = Math.floor(Date.now() / 1000);
38
+ const expiresSec = start + period;
39
+
40
+ return {
41
+ coverId: raw.coverId,
42
+ productId: Number(raw.productId),
43
+ coverAsset: Number(raw.coverAsset),
44
+ amountPaidOut: raw.amountPaidOut,
45
+ amount: raw.amount,
46
+ start,
47
+ period,
48
+ gracePeriod: Number(raw.gracePeriod),
49
+ segmentId: Number(raw.segmentId),
50
+ expired: nowSec > expiresSec,
51
+ };
52
+ }
53
+
54
+ interface UseExistingCoversOptions {
55
+ pollingInterval?: number | false;
56
+ }
57
+
58
+ interface UseExistingCoversReturn {
59
+ covers: CoverData[];
60
+ isLoading: boolean;
61
+ error: string | null;
62
+ hasCoverForProduct: (productId: number) => CoverData | undefined;
63
+ refetch: () => void;
64
+ }
65
+
66
+ export function useExistingCovers(userAddress: string | undefined, options?: UseExistingCoversOptions): UseExistingCoversReturn {
67
+ const { coverNfts, isLoading: isNftsLoading, refetch: refetchNfts } = useUserCoverNfts(userAddress, { pollingInterval: options?.pollingInterval });
68
+
69
+ const coverIds = useMemo(
70
+ () => coverNfts.map((nft) => BigInt(nft.token_id)),
71
+ [coverNfts]
72
+ );
73
+ const coverViewerAddress = nexusSdk.addresses?.CoverViewer;
74
+ const coverViewerAbi = nexusSdk.abis?.CoverViewer;
75
+
76
+ const {
77
+ data: covers = [],
78
+ isLoading: isContractLoading,
79
+ error: contractError,
80
+ refetch: refetchCovers,
81
+ } = useReadContract({
82
+ address: coverViewerAddress ? getAddress(coverViewerAddress) : undefined,
83
+ abi: coverViewerAbi as Abi,
84
+ functionName: "getCovers",
85
+ args: [coverIds],
86
+ query: {
87
+ enabled: !isNftsLoading && coverNfts.length > 0 && !!coverViewerAddress && !!coverViewerAbi,
88
+ select: (data) => (data as RawCoverData[]).map(parseCoverData),
89
+ },
90
+ });
91
+
92
+ const hasCoverForProduct = useCallback(
93
+ (productId: number): CoverData | undefined => {
94
+ return covers.find((cover) => cover.productId === productId && !cover.expired);
95
+ },
96
+ [covers]
97
+ );
98
+
99
+ return {
100
+ covers,
101
+ isLoading: isNftsLoading || isContractLoading,
102
+ error: contractError?.message ?? null,
103
+ hasCoverForProduct,
104
+ refetch: () => {
105
+ refetchNfts();
106
+ refetchCovers();
107
+ },
108
+ };
109
+ }
110
+
@@ -0,0 +1,79 @@
1
+ import { useMemo } from "react";
2
+ import { CoverAsset, 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
+
57
+
58
+
59
+ return {
60
+ protocolName: protocolName ?? "Unknown",
61
+ productId: nexusProduct?.id ?? null,
62
+ coverProductName: nexusProduct?.name ?? null,
63
+ isCoverable: !!nexusProduct,
64
+ coverAssets: nexusProduct?.coverAssets ?? [],
65
+ };
66
+ };
67
+
68
+ export interface NexusProductLookup {
69
+ protocolName: string;
70
+ productId: number | null;
71
+ coverProductName: string | null;
72
+ isCoverable: boolean;
73
+ coverAssets: string[];
74
+ }
75
+
76
+ export function useNexusProduct(opportunity: Opportunity | null | undefined): NexusProductLookup {
77
+ return useMemo(() => getNexusProductLookup(opportunity), [opportunity]);
78
+ }
79
+
@@ -0,0 +1,67 @@
1
+ import { useCallback } from "react";
2
+ import { useMutation } from "@tanstack/react-query";
3
+ import nexusSdk, { GetQuoteResponse } from "@nexusmutual/sdk";
4
+ import { getAddress, type Abi } from "viem";
5
+ import { useWriteContract, useConfig } from "wagmi";
6
+ import { waitForTransactionReceipt } from "wagmi/actions";
7
+ import { formatPurchaseError } from "../utils";
8
+
9
+ type UseNexusPurchaseArgs = {
10
+ onSuccess?: (message: string) => void;
11
+ onError?: (message: string) => void;
12
+ };
13
+
14
+ type UseNexusPurchaseReturn = {
15
+ purchase: (quoteResult: GetQuoteResponse) => Promise<void>;
16
+ isPurchasing: boolean;
17
+ };
18
+
19
+ export function useNexusPurchase({
20
+ onSuccess,
21
+ onError,
22
+ }: UseNexusPurchaseArgs): UseNexusPurchaseReturn {
23
+ const config = useConfig();
24
+ const { writeContractAsync } = useWriteContract();
25
+
26
+ const { mutateAsync, isPending: isPurchasing } = useMutation({
27
+ mutationFn: async (quoteResult: GetQuoteResponse) => {
28
+ const { buyCoverParams, poolAllocationRequests } = quoteResult.buyCoverInput;
29
+ const coverAddress = nexusSdk.addresses?.Cover;
30
+ const coverAbi = nexusSdk.abis?.Cover;
31
+
32
+ if (!coverAddress || !coverAbi) {
33
+ throw new Error("Nexus SDK not configured correctly for Cover.");
34
+ }
35
+ const isNativePayment = buyCoverParams.paymentAsset === 0;
36
+
37
+ const txHash = await writeContractAsync({
38
+ address: getAddress(coverAddress),
39
+ abi: coverAbi as Abi,
40
+ functionName: "buyCover",
41
+ args: [buyCoverParams, poolAllocationRequests],
42
+ value: isNativePayment ? BigInt(quoteResult.displayInfo.premiumInAsset) : undefined,
43
+ chainId: 1,
44
+ });
45
+
46
+ await waitForTransactionReceipt(config, {
47
+ hash: txHash,
48
+ confirmations: 1,
49
+ });
50
+ },
51
+ onSuccess: () => {
52
+ onSuccess?.("Cover purchased successfully");
53
+ },
54
+ onError: (err) => {
55
+ onError?.(formatPurchaseError(err));
56
+ },
57
+ });
58
+
59
+ const purchase = useCallback(
60
+ async (quoteResult: GetQuoteResponse) => {
61
+ await mutateAsync(quoteResult);
62
+ },
63
+ [mutateAsync]
64
+ );
65
+
66
+ return { purchase, isPurchasing };
67
+ }