@turtleclub/opportunities 0.1.0-beta.14 → 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.
@@ -1,31 +1,44 @@
1
1
  "use client";
2
2
 
3
- import { useMemo, useState } from "react";
4
- import { Button } from "@turtleclub/ui";
5
- import { ShieldAlert, Send, ChevronDown, ChevronUp, type LucideProps } from "lucide-react";
3
+ import { ChangeEvent, FormEvent, useMemo, useState } from "react";
4
+ import { Button, Checkbox, Input } from "@turtleclub/ui";
5
+ import { ShieldAlert, Send, ChevronDown, ChevronUp, type LucideProps, Shield } from "lucide-react";
6
6
  import type { ComponentType } from "react";
7
-
7
+ import type { CoverRequestData, CoverRequestFormProps } from "../types";
8
8
  const ShieldAlertIcon = ShieldAlert as ComponentType<LucideProps>;
9
9
  const SendIcon = Send as ComponentType<LucideProps>;
10
10
  const ChevronUpIcon = ChevronUp as ComponentType<LucideProps>;
11
11
  const ChevronDownIcon = ChevronDown as ComponentType<LucideProps>;
12
+ const ShieldIcon = Shield as ComponentType<LucideProps>;
12
13
 
13
- export interface CoverRequestFormProps {
14
- protocolName: string;
15
- baseApyLabel?: string;
16
- onDismiss?: () => void;
17
- onSubmit?: (desiredApySacrifice: number | null) => void;
18
- }
14
+ const PERIOD_OPTIONS = [
15
+ { label: "28d", days: 28 },
16
+ { label: "3m", days: 90 },
17
+ { label: "6m", days: 180 },
18
+ { label: "1y", days: 365 },
19
+ ] as const;
19
20
 
20
21
  export function CoverRequestForm({
21
22
  protocolName,
23
+ depositedAmount,
22
24
  baseApyLabel,
23
- onDismiss,
24
- onSubmit,
25
+ onSuccess,
26
+ onError,
27
+ startExpanded,
25
28
  }: CoverRequestFormProps) {
29
+ const [coverageAmount, setCoverageAmount] = useState("");
30
+ const [useDepositedAmount, setUseDepositedAmount] = useState(false);
26
31
  const [desiredApySacrifice, setDesiredApySacrifice] = useState("");
32
+ const [selectedPeriodDays, setSelectedPeriodDays] = useState(90);
27
33
  const [inputError, setInputError] = useState<string | null>(null);
28
- const [isExpanded, setIsExpanded] = useState(true);
34
+ const [isExpanded, setIsExpanded] = useState(startExpanded ?? false);
35
+
36
+ const parsedCoverageAmount = useMemo(() => {
37
+ const amountStr = useDepositedAmount ? (depositedAmount ?? "") : coverageAmount;
38
+ if (!amountStr.trim()) return null;
39
+ const value = Number(amountStr);
40
+ return Number.isFinite(value) && value > 0 ? value : null;
41
+ }, [coverageAmount, useDepositedAmount, depositedAmount]);
29
42
 
30
43
  const parsedSacrifice = useMemo(() => {
31
44
  if (!desiredApySacrifice.trim()) return null;
@@ -33,32 +46,74 @@ export function CoverRequestForm({
33
46
  return Number.isFinite(value) ? value : null;
34
47
  }, [desiredApySacrifice]);
35
48
 
36
- const handleSubmit = () => {
37
- if (desiredApySacrifice.trim() && parsedSacrifice === null) {
38
- setInputError("Enter a valid non-negative number");
49
+ const parsedBaseApy = useMemo(() => {
50
+ if (!baseApyLabel) return null;
51
+ const value = Number(baseApyLabel.replace(/[^0-9.]/g, ""));
52
+ return Number.isFinite(value) ? value : null;
53
+ }, [baseApyLabel]);
54
+
55
+ const netApy = useMemo(() => {
56
+ if (parsedBaseApy === null || parsedSacrifice === null) return null;
57
+ return parsedBaseApy - parsedSacrifice;
58
+ }, [parsedBaseApy, parsedSacrifice]);
59
+
60
+ const estimatedPremium = useMemo(() => {
61
+ if (parsedCoverageAmount === null || parsedSacrifice === null) return null;
62
+ // Premium = Amount × (APY/100) × (Days/365)
63
+ return parsedCoverageAmount * (parsedSacrifice / 100) * (selectedPeriodDays / 365);
64
+ }, [parsedCoverageAmount, parsedSacrifice, selectedPeriodDays]);
65
+
66
+ const resetForm = () => {
67
+ setCoverageAmount("");
68
+ setUseDepositedAmount(false);
69
+ setDesiredApySacrifice("");
70
+ setSelectedPeriodDays(90);
71
+ setInputError(null);
72
+ };
73
+
74
+ const handleSubmit = (): CoverRequestData | undefined => {
75
+ if (parsedCoverageAmount === null) {
76
+ setInputError("Enter a valid coverage amount");
77
+ return;
78
+ }
79
+ if (parsedSacrifice === null) {
80
+ setInputError("Enter a valid APY sacrifice");
39
81
  return;
40
82
  }
41
- if (parsedSacrifice !== null && parsedSacrifice < 0) {
83
+ if (parsedSacrifice < 0) {
42
84
  setInputError("APY sacrifice cannot be negative");
43
85
  return;
44
86
  }
45
87
  setInputError(null);
46
88
 
47
- onSubmit?.(parsedSacrifice);
89
+ const requestData: CoverRequestData = {
90
+ protocolName,
91
+ coverageAmount: parsedCoverageAmount,
92
+ periodDays: selectedPeriodDays,
93
+ desiredApySacrifice: parsedSacrifice,
94
+ estimatedPremium: estimatedPremium ?? 0,
95
+ };
48
96
 
49
- if (!onSubmit) {
50
- // Basic placeholder to surface the interaction during integration.
51
- console.info("Cover request submitted", {
52
- desiredApySacrifice: parsedSacrifice,
53
- protocolName,
54
- });
97
+ onSuccess?.("Cover request submitted");
98
+ console.info("Cover request submitted", requestData);
99
+
100
+ return requestData;
101
+ };
102
+
103
+ const handleFormSubmit = (event: FormEvent<HTMLFormElement>) => {
104
+ event.preventDefault();
105
+ const requestData = handleSubmit();
106
+ if (requestData) {
107
+ console.log("Submitted data successfully", requestData);
108
+ resetForm();
55
109
  }
56
110
  };
57
111
 
58
112
  return (
59
- <div className="rounded-xl bg-gradient-to-br from-primary/5 to-card border border-primary/20">
113
+ <div className="rounded-xl bg-gradient-to-br from-primary/5 to-card overflow-hidden border border-primary/20">
60
114
  <button
61
115
  onClick={() => setIsExpanded(!isExpanded)}
116
+ type="button"
62
117
  className="w-full p-4 flex items-center justify-between hover:bg-primary/5 transition-colors"
63
118
  >
64
119
  <div className="flex items-center gap-3 text-left">
@@ -76,26 +131,6 @@ export function CoverRequestForm({
76
131
  </div>
77
132
  </div>
78
133
  <div className="flex items-center gap-2">
79
- {onDismiss && (
80
- <Button
81
- variant="ghost"
82
- size="icon"
83
- onClick={(e) => {
84
- e.stopPropagation();
85
- onDismiss();
86
- }}
87
- className="h-6 w-6 text-muted-foreground hover:text-foreground"
88
- >
89
- <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
90
- <path
91
- strokeLinecap="round"
92
- strokeLinejoin="round"
93
- strokeWidth={2}
94
- d="M6 18L18 6M6 6l12 12"
95
- />
96
- </svg>
97
- </Button>
98
- )}
99
134
  {isExpanded ? (
100
135
  <ChevronUpIcon className="w-5 h-5 text-muted-foreground" />
101
136
  ) : (
@@ -105,47 +140,127 @@ export function CoverRequestForm({
105
140
  </button>
106
141
 
107
142
  {isExpanded && (
108
- <div className="p-4 space-y-4">
143
+ <form onSubmit={handleFormSubmit} className="p-4 space-y-4">
144
+ <div className="flex items-center justify-between text-sm text-muted-foreground">
145
+ <span className="font-medium text-foreground">{protocolName}</span>
146
+ {baseApyLabel && <span>Current APR: {baseApyLabel}</span>}
147
+ </div>
148
+
149
+ {/* Coverage Amount */}
109
150
  <div className="space-y-2">
110
- <div className="flex items-center justify-between text-sm text-muted-foreground">
111
- <span className="font-medium text-foreground">{protocolName}</span>
112
- {baseApyLabel && <span>Current APR: {baseApyLabel}</span>}
151
+ <label className="text-xs text-foreground">Coverage Amount</label>
152
+ <Input
153
+ variant=""
154
+ value={useDepositedAmount ? (depositedAmount ?? "") : coverageAmount}
155
+ onChange={(e: ChangeEvent<HTMLInputElement>) => {
156
+ const value = e.target.value;
157
+ if (value === "" || /^\d*\.?\d*$/.test(value)) {
158
+ setCoverageAmount(value);
159
+ }
160
+ }}
161
+ placeholder="e.g. 1"
162
+ className="w-full bg-secondary border border-border rounded-sm px-4 py-3 pr-12 text-foreground placeholder-muted-foreground focus:outline-none focus:border-primary/50 transition-colors"
163
+ disabled={useDepositedAmount}
164
+ />
165
+ {depositedAmount && (
166
+ <label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
167
+ <Checkbox
168
+ checked={useDepositedAmount}
169
+ onCheckedChange={(checked) => setUseDepositedAmount(!useDepositedAmount)}
170
+ className="rounded border-border"
171
+ />
172
+ Use my deposited amount ({depositedAmount})
173
+ </label>
174
+ )}
175
+ </div>
176
+
177
+ {/* Period Selector */}
178
+ <div className="space-y-3">
179
+ <label className="text-xs text-foreground text-center block">Coverage Period</label>
180
+ <div className="flex gap-2">
181
+ {PERIOD_OPTIONS.map((option) => (
182
+ <Button
183
+ key={option.days}
184
+ type="button"
185
+ onClick={() => setSelectedPeriodDays(option.days)}
186
+ variant="none"
187
+ className={`flex-1 py-2.5 rounded-lg text-sm font-medium transition-colors ${
188
+ selectedPeriodDays === option.days
189
+ ? "bg-primary text-primary-foreground border-primary hover:border-primary"
190
+ : "bg-secondary text-foreground border-border hover:border-primary/50"
191
+ }`}
192
+ >
193
+ {option.label}
194
+ </Button>
195
+ ))}
113
196
  </div>
114
- <label className="text-xs text-muted-foreground">
115
- APY you&apos;re willing to sacrifice for cover (percentage points)
197
+ </div>
198
+
199
+ {/* APY Sacrifice */}
200
+ <div className="space-y-2">
201
+ <label className="text-xs text-foreground">
202
+ APY you&apos;re willing to sacrifice for cover (%)
116
203
  </label>
117
- <input
204
+ <Input
118
205
  type="text"
206
+ variant="bordered"
119
207
  value={desiredApySacrifice}
120
- onChange={(e) => {
121
- const sanitized = e.target.value.replace(/[^0-9.]/g, "");
122
- setDesiredApySacrifice(sanitized);
208
+ onChange={(e: ChangeEvent<HTMLInputElement>) => {
209
+ const value = e.target.value;
210
+ if (value === "" || /^\d*\.?\d*$/.test(value)) {
211
+ setDesiredApySacrifice(e.target.value);
212
+ }
123
213
  }}
124
- placeholder="e.g. 1.5"
125
- className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/30"
214
+ placeholder="e.g. 2.5"
215
+ className="w-full bg-secondary border border-border rounded-sm px-4 py-3 pr-12 text-foreground placeholder-muted-foreground focus:outline-none focus:border-primary/50 transition-colors"
126
216
  />
127
- {inputError && <p className="text-[11px] text-destructive">{inputError}</p>}
128
- <p className="text-[11px] text-muted-foreground">
129
- We&apos;ll use this to gauge interest and bring cover to the protocols you care about.
217
+
218
+ <div className="flex items-center justify-between text-xs p-2 rounded-md bg-muted/50">
219
+ <span className="text-muted-foreground">
220
+ {parsedBaseApy ?? "—"}% − {parsedSacrifice ?? "—"}% =
221
+ </span>
222
+ <span
223
+ className={`font-medium ${netApy !== null && netApy >= 0 ? "text-green-500" : "text-muted-foreground"}`}
224
+ >
225
+ {netApy !== null ? `${netApy.toFixed(2)}% Net APY` : "— Net APY"}
226
+ </span>
227
+ </div>
228
+ </div>
229
+
230
+ {/* Estimated Premium */}
231
+ <div className="p-3 rounded-sm bg-primary/5 border border-primary/20 space-y-1">
232
+ <p className="text-xs text-muted-foreground">Estimated Premium</p>
233
+ <p className="text-lg font-semibold text-foreground">
234
+ {estimatedPremium !== null
235
+ ? estimatedPremium.toLocaleString(undefined, {
236
+ minimumFractionDigits: 2,
237
+ maximumFractionDigits: 2,
238
+ })
239
+ : "—"}
240
+ </p>
241
+ <p className="text-[10px] text-muted-foreground">
242
+ {parsedCoverageAmount?.toLocaleString() ?? "—"} × {parsedSacrifice ?? "—"}% ×{" "}
243
+ {selectedPeriodDays} days
130
244
  </p>
131
245
  </div>
132
246
 
133
- <div className="flex justify-end gap-2">
134
- {onDismiss && (
135
- <Button
136
- variant="ghost"
137
- onClick={onDismiss}
138
- className="text-muted-foreground hover:text-foreground"
139
- >
140
- Dismiss
141
- </Button>
142
- )}
143
- <Button onClick={handleSubmit} className="flex items-center gap-2" variant="green">
144
- <SendIcon className="w-4 h-4" />
247
+ {inputError && <p className="text-[11px] text-destructive">{inputError}</p>}
248
+
249
+ <p className="text-base text-muted-foreground">
250
+ We&apos;ll use this to gauge interest and bring cover to the protocols you care about.
251
+ </p>
252
+
253
+ <div className="flex justify-center gap-2 w-full ">
254
+ <Button
255
+ className=" flex w-full text-lg font-semibold rounded-sm"
256
+ variant="green"
257
+ type="submit"
258
+ >
259
+ <ShieldIcon className="w-5 h-5 text-primary" />
145
260
  Request Cover
146
261
  </Button>
147
262
  </div>
148
- </div>
263
+ </form>
149
264
  )}
150
265
  </div>
151
266
  );
@@ -0,0 +1,77 @@
1
+ "use client";
2
+
3
+ import type { ComponentType } from "react";
4
+ import { Button } from "@turtleclub/ui";
5
+
6
+ interface MembershipRequestCardProps {
7
+ isExpanded: boolean;
8
+ onToggle: () => void;
9
+ membershipUrl: string;
10
+ ShieldIcon: ComponentType<any>;
11
+ ExternalLinkIcon: ComponentType<any>;
12
+ ChevronUpIcon: ComponentType<any>;
13
+ ChevronDownIcon: ComponentType<any>;
14
+ }
15
+
16
+ export function MembershipRequestCard({
17
+ isExpanded,
18
+ onToggle,
19
+ membershipUrl,
20
+ ShieldIcon,
21
+ ExternalLinkIcon,
22
+ ChevronUpIcon,
23
+ ChevronDownIcon,
24
+ }: MembershipRequestCardProps) {
25
+ return (
26
+ <div className="rounded-xl bg-gradient-to-br from-primary/10 to-card border border-primary/20 overflow-hidden">
27
+ <button
28
+ onClick={onToggle}
29
+ className="w-full p-4 flex items-center justify-between hover:bg-primary/5 transition-colors"
30
+ >
31
+ <div className="flex items-center gap-3">
32
+ <div className="p-2 rounded-lg bg-primary/10 border border-primary/20">
33
+ <ShieldIcon className="w-5 h-5 text-primary" />
34
+ </div>
35
+ <div className="text-left">
36
+ <div className="flex items-center gap-2">
37
+ <span className="text-sm font-semibold text-foreground">Protect Your Deposit</span>
38
+ <span className="px-2 py-0.5 text-[10px] font-medium text-primary bg-primary/10 rounded-full border border-primary/20">
39
+ NEW
40
+ </span>
41
+ </div>
42
+ <p className="text-xs text-muted-foreground">Nexus Mutual Single Protocol Cover</p>
43
+ </div>
44
+ </div>
45
+ {isExpanded ? (
46
+ <ChevronUpIcon className="w-5 h-5 text-muted-foreground" />
47
+ ) : (
48
+ <ChevronDownIcon className="w-5 h-5 text-muted-foreground" />
49
+ )}
50
+ </button>
51
+
52
+ {isExpanded && (
53
+ <div className="px-4 pb-4 space-y-4">
54
+ <div className="p-3 rounded-lg bg-background/50 border border-border">
55
+ <p className="text-xs text-muted-foreground leading-relaxed">
56
+ <span className="text-primary font-medium">Membership required.</span> Join Nexus
57
+ Mutual to protect your deposit against smart contract exploits, oracle failures, and
58
+ protocol-specific risks.
59
+ </p>
60
+ </div>
61
+
62
+ <Button asChild className="w-full" variant="default">
63
+ <a
64
+ href={membershipUrl}
65
+ target="_blank"
66
+ rel="noopener noreferrer"
67
+ className="flex items-center justify-center gap-2"
68
+ >
69
+ Become a Member
70
+ <ExternalLinkIcon className="w-4 h-4" />
71
+ </a>
72
+ </Button>
73
+ </div>
74
+ )}
75
+ </div>
76
+ );
77
+ }
@@ -1,65 +1,87 @@
1
- "use client";
2
-
3
- import type { NexusCoverSectionProps, NexusQuoteResult } from "../types";
4
- import { getOpportunityAPY, formatAPY } from "../utils";
5
- import { useNexusProduct } from "../hooks/useNexusProduct";
6
- import { useNexusPurchase } from "../hooks/useNexusPurchase";
7
- import { CoverOfferCard } from "./CoverOfferCard";
8
- import { CoverRequestForm } from "./CoverRequestForm";
9
-
10
- export function NexusCoverSection({
11
- opportunity,
12
- amountToCover,
13
- buyerAddress,
14
- onSuccess,
15
- onError,
16
- onDismiss,
17
- className,
18
- }: NexusCoverSectionProps) {
19
- const { isCoverable, productId, protocolName } = useNexusProduct(opportunity);
20
- const baseApy = getOpportunityAPY(opportunity);
21
-
22
- const {
23
- purchase,
24
- isPurchasing,
25
- error: purchaseError,
26
- } = useNexusPurchase({
27
- buyerAddress,
28
- productId: productId ?? 0,
29
- amountToCover,
30
- onSuccess: onSuccess,
31
- onError: onError,
32
- });
33
-
34
- const handlePurchase = (quoteResult: NexusQuoteResult, coverPeriod: number) => {
35
- return purchase(quoteResult, coverPeriod);
36
- };
37
-
38
- if (isCoverable && productId !== null) {
39
- return (
40
- <div className={className}>
41
- <CoverOfferCard
42
- productId={productId}
43
- opportunity={opportunity}
44
- amountToCover={amountToCover}
45
- buyerAddress={buyerAddress}
46
- onPurchase={handlePurchase}
47
- isPurchasing={isPurchasing}
48
- purchaseError={purchaseError}
49
- onDismiss={onDismiss}
50
- />
51
- </div>
52
- );
53
- }
54
-
55
- return (
56
- <div className={className}>
57
- <CoverRequestForm
58
- protocolName={protocolName}
59
- baseApyLabel={formatAPY(baseApy)}
60
- onDismiss={onDismiss}
61
- onSubmit={() => {}}
62
- />
63
- </div>
64
- );
65
- }
1
+ "use client";
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
+ import type { NexusCoverSectionProps } from "../types";
8
+ import { getOpportunityAPY, formatAPY } from "../utils";
9
+ import { useNexusProduct } from "../hooks/useNexusProduct";
10
+ import { useCheckNexusMembership } from "../hooks/useCheckNexusMembership";
11
+ import { CoverOfferCard } from "./CoverOfferCard";
12
+ import { CoverRequestForm } from "./CoverRequestForm";
13
+ import { NEXUS_MEMBERSHIP_URL } from "../constants";
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>;
20
+
21
+ export function NexusCoverSection({
22
+ opportunity,
23
+ amountToCover,
24
+ buyerAddress,
25
+ onSuccess,
26
+ onError,
27
+ startExpanded = false,
28
+ className,
29
+ }: NexusCoverSectionProps) {
30
+ const [isExpanded, setIsExpanded] = useState(startExpanded);
31
+ const { isCoverable, productId, protocolName, coverProductName } = useNexusProduct(opportunity);
32
+ const baseApy = getOpportunityAPY(opportunity);
33
+ const { isMember, isLoading: isMembershipLoading } = useCheckNexusMembership(buyerAddress);
34
+
35
+ if (isCoverable && isMembershipLoading) {
36
+ return null;
37
+ }
38
+
39
+ //If the user is not a member but the opportunity is coverable, show the membership url.
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;
42
+
43
+ if (isCoverable && !isMember) {
44
+ return (
45
+ <MembershipRequestCard
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}>
60
+ <CoverOfferCard
61
+ productId={productId}
62
+ opportunity={opportunity}
63
+ amountToCover={amountToCover}
64
+ buyerAddress={buyerAddress}
65
+ protocolName={protocolName}
66
+ coverProductName={coverProductName ?? "Protocol Cover"}
67
+ onSuccess={onSuccess}
68
+ onError={onError}
69
+ startExpanded={startExpanded}
70
+ />
71
+ </div>
72
+ );
73
+ }
74
+
75
+ return (
76
+ <div className={className}>
77
+ <CoverRequestForm
78
+ protocolName={protocolName}
79
+ baseApyLabel={formatAPY(baseApy)}
80
+ depositedAmount={amountToCover}
81
+ onSuccess={onSuccess}
82
+ onError={onError}
83
+ startExpanded={startExpanded}
84
+ />
85
+ </div>
86
+ );
87
+ }
@@ -1,6 +1,8 @@
1
- export const NEXUS_PROTOCOL_COVER_TERMS_URL =
2
- "https://nexusmutual.io/pages/ProtocolCoverv1.0.pdf";
3
-
4
- export const NEXUS_KYC_REQUIREMENTS_URL =
5
- "https://docs.nexusmutual.io/overview/membership/#kyc-requirements";
6
-
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 USER_NFTS_API_URL = "https://lumon.turtle.xyz/query/token/erc721_portfolio";
8
+
@@ -0,0 +1,54 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { createPublicClient, http, getAddress } from 'viem';
3
+ import { mainnet } from 'viem/chains';
4
+ import nexusSdk from "@nexusmutual/sdk";
5
+
6
+ const publicClient = createPublicClient({
7
+ chain: mainnet,
8
+ transport: http(),
9
+ });
10
+
11
+ export function useCheckNexusMembership(userAddress: string | undefined) {
12
+ const [isMember, setIsMember] = useState<boolean>(false);
13
+ const [isLoading, setIsLoading] = useState<boolean>(true);
14
+ const [error, setError] = useState<Error | null>(null);
15
+
16
+ useEffect(() => {
17
+ if (!userAddress) {
18
+ setIsMember(false);
19
+ setIsLoading(false);
20
+ return;
21
+ }
22
+
23
+ const checkMembership = async () => {
24
+ setIsLoading(true);
25
+ setError(null);
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]);
52
+
53
+ return { isMember, isLoading, error };
54
+ }
@@ -1,5 +1,5 @@
1
1
  import { useState, useEffect, useCallback } from "react";
2
- import * as ethers from "ethers";
2
+ import { parseEther, formatEther } from "viem";
3
3
  import type { NexusQuoteResult, CoverQuoteState } from "../types";
4
4
  import { CoverAsset, NexusSDK } from "@nexusmutual/sdk";
5
5
 
@@ -39,9 +39,8 @@ async function fetchNexusQuote(
39
39
  period: number
40
40
  ): Promise<NexusQuoteResult> {
41
41
 
42
- //const { nexusSdk, CoverAsset } = await getNexusSdk();
43
- const nexusSdk = new NexusSDK();
44
- const amountWei = ethers.parseEther(amount).toString();
42
+ const { nexusSdk } = await getNexusSdk();
43
+ const amountWei = parseEther(amount).toString();
45
44
 
46
45
  const { result, error } = await nexusSdk.quote.getQuoteAndBuyCoverInputs({
47
46
  productId,
@@ -74,7 +73,6 @@ export function useCoverQuote({
74
73
  const [yearlyCostPerc, setYearlyCostPerc] = useState<number | null>(null);
75
74
 
76
75
  const fetchQuote = useCallback(async () => {
77
- console.log("fetchQuote", productId, buyerAddress, amountToCover, coverPeriod);
78
76
  if (!productId || !buyerAddress || !amountToCover || parseFloat(amountToCover) <= 0) {
79
77
  console.error("Validation failed", productId, buyerAddress, amountToCover, coverPeriod);
80
78
  return;
@@ -87,7 +85,7 @@ export function useCoverQuote({
87
85
  const result = await fetchNexusQuote(productId, buyerAddress, amountToCover, coverPeriod);
88
86
 
89
87
  setQuoteResult(result);
90
- setPremiumEth(ethers.formatEther(result.displayInfo.premiumInAsset));
88
+ setPremiumEth(formatEther(BigInt(result.displayInfo.premiumInAsset)));
91
89
  setYearlyCostPerc(result.displayInfo.yearlyCostPerc);
92
90
  } catch (err: unknown) {
93
91
  const errorMessage = err instanceof Error ? err.message : "Failed to fetch quote";