@turtleclub/opportunities 0.1.0-beta.20 → 0.1.0-beta.22
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.
- package/CHANGELOG.md +10 -0
- package/package.json +7 -4
- package/src/cover-offer/README.md +46 -0
- package/src/cover-offer/components/CoverOfferCard.tsx +312 -286
- package/src/cover-offer/components/CoverRequestForm.tsx +342 -267
- package/src/cover-offer/components/CoveredEventsInfo.tsx +120 -0
- package/src/cover-offer/components/ExistingCoverInfo.tsx +74 -0
- package/src/cover-offer/components/NexusCoverSection.tsx +17 -55
- package/src/cover-offer/components/PurchaseButtonSection.tsx +106 -0
- package/src/cover-offer/constants.ts +25 -1
- package/src/cover-offer/hooks/useCheckNexusMembership.ts +26 -48
- package/src/cover-offer/hooks/useCoverQuote.ts +80 -76
- package/src/cover-offer/hooks/useCoverTokenSelection.ts +84 -0
- package/src/cover-offer/hooks/useDebouncedValue.ts +12 -0
- package/src/cover-offer/hooks/useExistingCovers.ts +29 -54
- package/src/cover-offer/hooks/useNexusProduct.ts +6 -1
- package/src/cover-offer/hooks/useNexusPurchase.ts +42 -40
- package/src/cover-offer/hooks/useTokenApproval.ts +118 -0
- package/src/cover-offer/hooks/useUserCoverNfts.ts +10 -67
- package/src/cover-offer/index.ts +1 -1
- package/src/cover-offer/types/index.ts +6 -30
- package/src/cover-offer/utils/index.ts +9 -1
- package/src/cover-offer/components/MembershipRequestCard.tsx +0 -77
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { ChevronDown, ChevronUp, type LucideProps } from "lucide-react";
|
|
5
|
+
import type { ComponentType } from "react";
|
|
6
|
+
import { NEXUS_PROTOCOL_COVER_TERMS_URL } from "../constants";
|
|
7
|
+
|
|
8
|
+
const ChevronDownIcon = ChevronDown as ComponentType<LucideProps>;
|
|
9
|
+
const ChevronUpIcon = ChevronUp as ComponentType<LucideProps>;
|
|
10
|
+
|
|
11
|
+
const COVERED_EVENTS = [
|
|
12
|
+
"Smart contract exploits/hacks",
|
|
13
|
+
"Oracle failure or manipulation",
|
|
14
|
+
"Liquidation failure",
|
|
15
|
+
"Governance takeovers",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const EXCLUSIONS = [
|
|
19
|
+
"Loss of value of any asset (i.e., depeg event)",
|
|
20
|
+
"Losses due to phishing, private key breaches, malware",
|
|
21
|
+
"Losses due to frontend attacks where the protocol is unaffected",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export function CoveredEventsInfo() {
|
|
25
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="p-3 rounded-lg bg-background/50 border border-border space-y-2">
|
|
29
|
+
{/* Summary - Always visible */}
|
|
30
|
+
<p className="text-xs text-muted-foreground leading-relaxed">
|
|
31
|
+
<span className="text-primary font-medium">Single Protocol Cover</span> protects against
|
|
32
|
+
loss of funds on all EVM-compatible chains.
|
|
33
|
+
</p>
|
|
34
|
+
|
|
35
|
+
{/* Covered Events - Compact */}
|
|
36
|
+
<div className="flex flex-wrap gap-1.5">
|
|
37
|
+
{COVERED_EVENTS.map((event) => (
|
|
38
|
+
<span
|
|
39
|
+
key={event}
|
|
40
|
+
className="text-xs px-2 py-0.5 rounded-full bg-primary/10 text-primary border border-primary/20"
|
|
41
|
+
>
|
|
42
|
+
{event}
|
|
43
|
+
</span>
|
|
44
|
+
))}
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
{/* Expand/Collapse Button */}
|
|
48
|
+
<button
|
|
49
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
50
|
+
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
51
|
+
>
|
|
52
|
+
{isExpanded ? (
|
|
53
|
+
<>
|
|
54
|
+
<ChevronUpIcon className="w-3 h-3" />
|
|
55
|
+
<span>Show less</span>
|
|
56
|
+
</>
|
|
57
|
+
) : (
|
|
58
|
+
<>
|
|
59
|
+
<ChevronDownIcon className="w-3 h-3" />
|
|
60
|
+
<span>View exclusions & claim details</span>
|
|
61
|
+
</>
|
|
62
|
+
)}
|
|
63
|
+
</button>
|
|
64
|
+
|
|
65
|
+
{/* Expanded Details */}
|
|
66
|
+
{isExpanded && (
|
|
67
|
+
<div className="space-y-3 pt-2 border-t border-border">
|
|
68
|
+
{/* Exclusions */}
|
|
69
|
+
<div className="space-y-1.5">
|
|
70
|
+
<p className="text-xs uppercase tracking-wide text-muted-foreground font-medium">
|
|
71
|
+
Not Covered
|
|
72
|
+
</p>
|
|
73
|
+
<ul className="text-xs text-muted-foreground space-y-1">
|
|
74
|
+
{EXCLUSIONS.map((exclusion) => (
|
|
75
|
+
<li key={exclusion} className="flex items-start gap-1.5">
|
|
76
|
+
<span className="text-destructive mt-0.5">✕</span>
|
|
77
|
+
<span>{exclusion}</span>
|
|
78
|
+
</li>
|
|
79
|
+
))}
|
|
80
|
+
</ul>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Deductible & Claims */}
|
|
84
|
+
<div className="space-y-1.5">
|
|
85
|
+
<p className="text-xs uppercase tracking-wide text-muted-foreground font-medium">
|
|
86
|
+
Claim Process
|
|
87
|
+
</p>
|
|
88
|
+
<ul className="text-xs text-muted-foreground space-y-1">
|
|
89
|
+
<li className="flex items-start gap-1.5">
|
|
90
|
+
<span className="text-primary mt-0.5">•</span>
|
|
91
|
+
<span>5% deductible of the Cover Amount</span>
|
|
92
|
+
</li>
|
|
93
|
+
<li className="flex items-start gap-1.5">
|
|
94
|
+
<span className="text-primary mt-0.5">•</span>
|
|
95
|
+
<span>14-day waiting period after loss event</span>
|
|
96
|
+
</li>
|
|
97
|
+
<li className="flex items-start gap-1.5">
|
|
98
|
+
<span className="text-primary mt-0.5">•</span>
|
|
99
|
+
<span>Claims accepted up to 35 days after cover ends</span>
|
|
100
|
+
</li>
|
|
101
|
+
</ul>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Disclaimer */}
|
|
105
|
+
<p className="text-xs text-muted-foreground/70 leading-relaxed">
|
|
106
|
+
This is not a contract of insurance. Cover is provided on a discretionary basis.{" "}
|
|
107
|
+
<a
|
|
108
|
+
href={NEXUS_PROTOCOL_COVER_TERMS_URL}
|
|
109
|
+
target="_blank"
|
|
110
|
+
rel="noopener noreferrer"
|
|
111
|
+
className="text-primary hover:text-primary/80 underline underline-offset-2"
|
|
112
|
+
>
|
|
113
|
+
Read complete terms
|
|
114
|
+
</a>
|
|
115
|
+
</p>
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ComponentType } from "react";
|
|
4
|
+
import { CheckCircle, type LucideProps } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
import { parseCoverTiming } from "../utils";
|
|
7
|
+
import { NEXUS_AVAILABLE_TOKENS, NEXUS_PROTOCOL_COVER_TERMS_URL } from "../constants";
|
|
8
|
+
import type { CoverData } from "../hooks/useExistingCovers";
|
|
9
|
+
import { CoverAsset } from "@nexusmutual/sdk";
|
|
10
|
+
import { formatToken } from "@turtleclub/utils";
|
|
11
|
+
import { formatUnits } from "viem";
|
|
12
|
+
|
|
13
|
+
const CheckCircleIcon = CheckCircle as ComponentType<LucideProps>;
|
|
14
|
+
|
|
15
|
+
export interface ExistingCoverInfoProps {
|
|
16
|
+
cover: CoverData;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ExistingCoverInfo({ cover }: ExistingCoverInfoProps) {
|
|
20
|
+
const asset = CoverAsset[cover.coverAsset];
|
|
21
|
+
const token = NEXUS_AVAILABLE_TOKENS.find((token) => token.symbol === asset);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
<div className="p-3 rounded-lg bg-green-500/10 border border-green-500/20">
|
|
26
|
+
<div className="flex items-center gap-2 mb-2">
|
|
27
|
+
<CheckCircleIcon className="w-4 h-4 text-green-500" />
|
|
28
|
+
<span className="text-sm font-medium text-green-500">
|
|
29
|
+
You already have cover for this protocol
|
|
30
|
+
</span>
|
|
31
|
+
</div>
|
|
32
|
+
<p className="text-xs text-muted-foreground leading-relaxed">
|
|
33
|
+
Your deposit is protected against smart contract bugs, oracle failures, and governance
|
|
34
|
+
attacks on this protocol. See{" "}
|
|
35
|
+
<a
|
|
36
|
+
href={NEXUS_PROTOCOL_COVER_TERMS_URL}
|
|
37
|
+
target="_blank"
|
|
38
|
+
rel="noopener noreferrer"
|
|
39
|
+
className="text-primary hover:text-primary/80 underline underline-offset-2"
|
|
40
|
+
>
|
|
41
|
+
terms
|
|
42
|
+
</a>{" "}
|
|
43
|
+
for details.
|
|
44
|
+
</p>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div className="grid grid-cols-2 gap-3">
|
|
48
|
+
<div className="p-3 rounded-lg bg-background border border-border text-center">
|
|
49
|
+
<p className="text-[10px] uppercase tracking-wide text-muted-foreground mb-1">
|
|
50
|
+
Cover Amount
|
|
51
|
+
</p>
|
|
52
|
+
{token?.decimals && (
|
|
53
|
+
<p className="text-lg font-bold text-foreground">
|
|
54
|
+
{Number(formatUnits(cover.amount, token?.decimals)).toLocaleString('en-US', { maximumFractionDigits: 6 })} {token?.symbol}
|
|
55
|
+
</p>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
<div className="p-3 rounded-lg bg-background border border-border text-center">
|
|
59
|
+
<p className="text-[10px] uppercase tracking-wide text-muted-foreground mb-1">Period</p>
|
|
60
|
+
<p className="text-lg font-bold text-foreground">
|
|
61
|
+
{parseCoverTiming(cover.start, cover.period, cover.gracePeriod).periodDays} days
|
|
62
|
+
</p>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div className="p-3 rounded-lg bg-background border border-border">
|
|
67
|
+
<div className="flex items-center justify-between">
|
|
68
|
+
<span className="text-xs text-muted-foreground">Cover ID</span>
|
|
69
|
+
<span className="text-sm font-medium text-foreground">#{cover.coverId.toString()}</span>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -1,87 +1,49 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
import { Shield, ExternalLink, ChevronDown, ChevronUp, type LucideProps } from "lucide-react";
|
|
5
|
-
import type { ComponentType } from "react";
|
|
6
|
-
import { Button } from "@turtleclub/ui";
|
|
7
3
|
import type { NexusCoverSectionProps } from "../types";
|
|
8
4
|
import { getOpportunityAPY, formatAPY } from "../utils";
|
|
9
5
|
import { useNexusProduct } from "../hooks/useNexusProduct";
|
|
10
|
-
import { useCheckNexusMembership } from "../hooks/useCheckNexusMembership";
|
|
11
6
|
import { CoverOfferCard } from "./CoverOfferCard";
|
|
12
7
|
import { CoverRequestForm } from "./CoverRequestForm";
|
|
13
|
-
import {
|
|
14
|
-
import { MembershipRequestCard } from "./MembershipRequestCard";
|
|
15
|
-
|
|
16
|
-
const ShieldIcon = Shield as ComponentType<LucideProps>;
|
|
17
|
-
const ExternalLinkIcon = ExternalLink as ComponentType<LucideProps>;
|
|
18
|
-
const ChevronDownIcon = ChevronDown as ComponentType<LucideProps>;
|
|
19
|
-
const ChevronUpIcon = ChevronUp as ComponentType<LucideProps>;
|
|
8
|
+
import { TurtleHooksProvider } from "@turtleclub/hooks";
|
|
20
9
|
|
|
21
10
|
export function NexusCoverSection({
|
|
22
11
|
opportunity,
|
|
23
|
-
amountToCover,
|
|
24
12
|
buyerAddress,
|
|
25
13
|
onSuccess,
|
|
26
14
|
onError,
|
|
27
15
|
startExpanded = false,
|
|
28
16
|
className,
|
|
29
17
|
}: NexusCoverSectionProps) {
|
|
30
|
-
const
|
|
31
|
-
|
|
18
|
+
const { isCoverable, productId, protocolName, coverProductName, coverAssets } =
|
|
19
|
+
useNexusProduct(opportunity);
|
|
32
20
|
const baseApy = getOpportunityAPY(opportunity);
|
|
33
|
-
const { isMember, isLoading: isMembershipLoading } = useCheckNexusMembership(buyerAddress);
|
|
34
|
-
|
|
35
|
-
if (isCoverable && isMembershipLoading) {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
21
|
|
|
39
|
-
|
|
40
|
-
// if the user is not a member and the opportunity is not coverable, show nothing
|
|
41
|
-
if (protocolName === "Unknown" || !buyerAddress || (!isCoverable && !isMember)) return null;
|
|
22
|
+
if (protocolName === "Unknown" || !buyerAddress) return null;
|
|
42
23
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
isExpanded={isExpanded}
|
|
47
|
-
onToggle={() => setIsExpanded(!isExpanded)}
|
|
48
|
-
membershipUrl={NEXUS_MEMBERSHIP_URL}
|
|
49
|
-
ShieldIcon={ShieldIcon}
|
|
50
|
-
ExternalLinkIcon={ExternalLinkIcon}
|
|
51
|
-
ChevronUpIcon={ChevronUpIcon}
|
|
52
|
-
ChevronDownIcon={ChevronDownIcon}
|
|
53
|
-
/>
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (isCoverable && productId !== null && isMember) {
|
|
58
|
-
return (
|
|
59
|
-
<div className={className}>
|
|
24
|
+
return (
|
|
25
|
+
<div className={className}>
|
|
26
|
+
{isCoverable && productId !== null && coverAssets && coverAssets.length > 0 ? (
|
|
60
27
|
<CoverOfferCard
|
|
61
28
|
productId={productId}
|
|
62
29
|
opportunity={opportunity}
|
|
63
|
-
amountToCover={amountToCover}
|
|
64
30
|
buyerAddress={buyerAddress}
|
|
65
31
|
protocolName={protocolName}
|
|
66
32
|
coverProductName={coverProductName ?? "Protocol Cover"}
|
|
67
33
|
onSuccess={onSuccess}
|
|
68
34
|
onError={onError}
|
|
69
35
|
startExpanded={startExpanded}
|
|
36
|
+
coverAvailableAssets={coverAssets || []}
|
|
70
37
|
/>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
depositedAmount={amountToCover}
|
|
81
|
-
onSuccess={onSuccess}
|
|
82
|
-
onError={onError}
|
|
83
|
-
startExpanded={startExpanded}
|
|
84
|
-
/>
|
|
38
|
+
) : (
|
|
39
|
+
<CoverRequestForm
|
|
40
|
+
protocolName={protocolName}
|
|
41
|
+
baseApyLabel={formatAPY(baseApy)}
|
|
42
|
+
onSuccess={onSuccess}
|
|
43
|
+
onError={onError}
|
|
44
|
+
startExpanded={startExpanded}
|
|
45
|
+
/>
|
|
46
|
+
)}
|
|
85
47
|
</div>
|
|
86
48
|
);
|
|
87
49
|
}
|
|
@@ -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
|
+
}
|
|
@@ -4,5 +4,29 @@ export const NEXUS_KYC_REQUIREMENTS_URL =
|
|
|
4
4
|
"https://docs.nexusmutual.io/overview/membership/#kyc-requirements";
|
|
5
5
|
export const NEXUS_MEMBERSHIP_URL = "https://app.nexusmutual.io/become-member";
|
|
6
6
|
export const NEXUS_COVER_NFT_ADDRESS = "0xcafeaca76be547f14d0220482667b42d8e7bc3eb";
|
|
7
|
-
export const
|
|
7
|
+
export const NEXUS_COVER_ROUTER = "0xcafeac0fF5dA0A2777d915531bfA6B29d282Ee62";
|
|
8
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;
|
|
@@ -1,54 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { getAddress } from "viem";
|
|
2
|
+
import type { Abi } from "viem";
|
|
3
|
+
import { useReadContract } from "wagmi";
|
|
4
4
|
import nexusSdk from "@nexusmutual/sdk";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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;
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
const masterAddress = nexusSdk.addresses?.NXMaster;
|
|
28
|
-
const masterAbi = nexusSdk.abis?.NXMaster;
|
|
29
|
-
|
|
30
|
-
if (!masterAddress || !masterAbi) {
|
|
31
|
-
throw new Error('Nexus SDK not configured correctly for NXMaster.');
|
|
32
|
-
}
|
|
33
|
-
const checksummedAddress = getAddress(userAddress);
|
|
34
|
-
const result = await publicClient.readContract({
|
|
35
|
-
address: getAddress(masterAddress),
|
|
36
|
-
abi: masterAbi,
|
|
37
|
-
functionName: 'isMember',
|
|
38
|
-
args: [checksummedAddress],
|
|
39
|
-
});
|
|
40
|
-
setIsMember(result as boolean);
|
|
41
|
-
} catch (err: any) {
|
|
42
|
-
console.error("Error checking membership:", err.message);
|
|
43
|
-
setError(err);
|
|
44
|
-
setIsMember(false);
|
|
45
|
-
} finally {
|
|
46
|
-
setIsLoading(false);
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
checkMembership();
|
|
51
|
-
}, [userAddress]);
|
|
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
|
+
});
|
|
52
30
|
|
|
53
31
|
return { isMember, isLoading, error };
|
|
54
32
|
}
|