@turtleclub/covers 0.1.0-beta.1 → 0.1.0-beta.3

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 CHANGED
@@ -3,6 +3,18 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [0.1.0-beta.3](https://github.com/turtle-dao/turtle-tools/compare/@turtleclub/covers@0.1.0-beta.2...@turtleclub/covers@0.1.0-beta.3) (2026-02-17)
7
+
8
+ ### Features
9
+
10
+ - nexus purhcase tracking. Adding Nick's suggestions ([#263](https://github.com/turtle-dao/turtle-tools/issues/263)) ([cf9b7d6](https://github.com/turtle-dao/turtle-tools/commit/cf9b7d6e8f7d9ff5d35bef94d657b1b0ccd027db))
11
+
12
+ # [0.1.0-beta.2](https://github.com/turtle-dao/turtle-tools/compare/@turtleclub/covers@0.1.0-beta.1...@turtleclub/covers@0.1.0-beta.2) (2026-02-10)
13
+
14
+ ### Bug Fixes
15
+
16
+ - setting turtle-hooks, TanstackQuery, viem and wagmi as peer dependency ([#257](https://github.com/turtle-dao/turtle-tools/issues/257)) ([9b3c4a2](https://github.com/turtle-dao/turtle-tools/commit/9b3c4a2f8a1cae7dbc4602bcbe4540eccaf109d0))
17
+
6
18
  # 0.1.0-beta.1 (2026-02-09)
7
19
 
8
20
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turtleclub/covers",
3
- "version": "0.1.0-beta.1",
3
+ "version": "0.1.0-beta.3",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
@@ -11,18 +11,18 @@
11
11
  "dependencies": {
12
12
  "@nexusmutual/sdk": "^1.26.0",
13
13
  "@tanstack/react-form": "^1.27.6",
14
- "@tanstack/react-query": "^5.62.3",
15
- "@turtleclub/hooks": "0.5.0-beta.60",
16
- "@turtleclub/ui": "0.7.0-beta.26",
14
+ "@turtleclub/ui": "0.7.0-beta.27",
17
15
  "@turtleclub/utils": "0.4.0-beta.0",
18
- "lucide-react": "^0.542.0",
19
- "viem": "^2.21.54",
20
- "wagmi": "^2.15.6"
16
+ "lucide-react": "^0.542.0"
21
17
  },
22
18
  "peerDependencies": {
23
- "@turtleclub/multichain": "^0.1.0",
19
+ "@tanstack/react-query": "^5.62.3",
20
+ "@turtleclub/hooks": "^0.5.0",
21
+ "@turtleclub/multichain": "^0.5.0",
24
22
  "react": "^18.3.1",
25
- "react-dom": "^18.3.1"
23
+ "react-dom": "^18.3.1",
24
+ "viem": "^2.21.54",
25
+ "wagmi": "^2.14.0"
26
26
  },
27
27
  "peerDependenciesMeta": {
28
28
  "@turtleclub/multichain": {
@@ -34,5 +34,5 @@
34
34
  "@types/react-dom": "^18.3.5",
35
35
  "typescript": "^5.7.2"
36
36
  },
37
- "gitHead": "626e418a3f37222475da12fac2820f9bda4d9cca"
37
+ "gitHead": "117f7b3cc5ea1bcf5c2096ede86145890e8f75ab"
38
38
  }
@@ -22,7 +22,7 @@ import { useCoverQuote } from "../hooks/useCoverQuote";
22
22
  import { useNexusPurchase } from "../hooks/useNexusPurchase";
23
23
  import { useExistingCovers } from "../hooks/useExistingCovers";
24
24
  import { useCheckNexusMembership } from "../hooks/useCheckNexusMembership";
25
- import { formatAPY } from "../utils";
25
+ import { formatAPY, COVER_QUOTE_ERROR_MESSAGE } from "../utils";
26
26
  import { useDebouncedValue } from "../hooks/useDebouncedValue";
27
27
  import {
28
28
  NEXUS_KYC_REQUIREMENTS_URL,
@@ -35,7 +35,7 @@ import { ExistingCoverInfo } from "./ExistingCoverInfo";
35
35
  import { CoveredEventsInfo } from "./CoveredEventsInfo";
36
36
 
37
37
  import { useMultichainAccount } from "@turtleclub/multichain";
38
- import { formatToken } from "@turtleclub/utils";
38
+ import { formatToken, formatBalance } from "@turtleclub/utils";
39
39
  import { useTokenApproval } from "../hooks/useTokenApproval";
40
40
  import { useCoverTokenSelection } from "../hooks/useCoverTokenSelection";
41
41
 
@@ -174,7 +174,7 @@ export function CoverOfferCard({
174
174
  if (!hasAgreedToTerms) return "Checkbox not marked";
175
175
  if (!quoteResult) return "Quote not ready yet";
176
176
  if (isLoading) return "Fetching quote...";
177
- if (error) return error;
177
+ if (error) return COVER_QUOTE_ERROR_MESSAGE;
178
178
  if (hasInsufficientBalance) return "Insufficient balance for premium";
179
179
  if (isPurchasing) return "Purchasing cover...";
180
180
  return null;
@@ -206,7 +206,7 @@ export function CoverOfferCard({
206
206
  <Collapsible
207
207
  open={isExpanded}
208
208
  onOpenChange={setIsExpanded}
209
- className="rounded-xl border overflow-hidden turtle-widget-root"
209
+ className="rounded-xl border border-border overflow-hidden turtle-widget-root"
210
210
  >
211
211
  <CollapsibleTrigger className="w-full p-4 flex items-center cursor-pointer justify-between hover:bg-primary/5 transition-colors">
212
212
  <div className="flex items-center gap-3">
@@ -299,6 +299,7 @@ export function CoverOfferCard({
299
299
  )}
300
300
  showTokenSelectorBalance={false}
301
301
  showInputBalance={false}
302
+ triggerTokenSelectorWidth={135}
302
303
  />
303
304
  </div>
304
305
 
@@ -321,7 +322,7 @@ export function CoverOfferCard({
321
322
  <p className="text-lg font-bold text-foreground">{formatAPY(originalAPY)}</p>
322
323
  </div>
323
324
  <div className="p-3 rounded-lg bg-background/80 border border-border text-center">
324
- <p className="text-xs uppercase tracking-wide text-muted-foreground mb-1"></p>
325
+
325
326
  {isLoading ? (
326
327
  <>
327
328
  <Skeleton className="h-6 w-16 mx-auto bg-neutral-alpha-10" />
@@ -329,16 +330,17 @@ export function CoverOfferCard({
329
330
  </>
330
331
  ) : coverCostPerc !== null ? (
331
332
  <>
333
+ <p className="text-xs text-muted-foreground mb-1">Cover cost</p>
332
334
  <p className="text-lg font-bold text-orange-400">
333
335
  -{coverCostPerc.toFixed(2)}%
334
336
  </p>
335
- <p className="text-xs text-muted-foreground">per year</p>
337
+
336
338
  </>
337
339
  ) : (
338
340
  <p className="text-lg font-bold text-muted-foreground">—</p>
339
341
  )}
340
342
  </div>
341
- <div className="p-3 rounded-lg bg-primary/5 border border-primary/20 text-center">
343
+ <div className="p-3 rounded-lg bg-primary/5 border border-border text-center">
342
344
  <div className="flex items-center justify-center gap-1 mb-1">
343
345
  <p className="text-xs uppercase tracking-wide text-primary/70">Net APY</p>
344
346
  <Tooltip>
@@ -423,10 +425,10 @@ export function CoverOfferCard({
423
425
  hasInsufficientBalance ? "text-destructive" : "text-foreground"
424
426
  )}
425
427
  >
426
- {!isNaN(parseFloat(premium)) ? parseFloat(premium).toFixed(4) : premium} {selectedTokenSymbol}
428
+ {formatBalance(premium)} {selectedTokenSymbol}
427
429
  </span>
428
430
  ) : error ? (
429
- <span className="text-sm text-destructive">No cover available</span>
431
+ <span className="text-sm text-destructive">{COVER_QUOTE_ERROR_MESSAGE}</span>
430
432
  ) : (
431
433
  <span className="text-sm text-muted-foreground">—</span>
432
434
  )}
@@ -138,7 +138,7 @@ export function CoverRequestForm({
138
138
  <Collapsible
139
139
  open={isExpanded}
140
140
  onOpenChange={setIsExpanded}
141
- className="rounded-xl overflow-hidden border"
141
+ className="rounded-xl overflow-hidden border border-border"
142
142
  >
143
143
  <CollapsibleTrigger className="w-full p-4 flex items-center cursor-pointer justify-between hover:bg-primary/5 transition-colors">
144
144
  <div className="flex items-center gap-3 text-left">
@@ -5,7 +5,7 @@ import { getOpportunityAPY, formatAPY } from "../utils";
5
5
  import { useNexusProduct } from "../hooks/useNexusProduct";
6
6
  import { CoverOfferCard } from "./CoverOfferCard";
7
7
  import { CoverRequestForm } from "./CoverRequestForm";
8
- import { TurtleHooksProvider } from "@turtleclub/hooks";
8
+
9
9
 
10
10
  export function NexusCoverSection({
11
11
  opportunity,
@@ -37,3 +37,7 @@ export const PERIOD_OPTIONS = [
37
37
  { label: "6m", days: 180 },
38
38
  { label: "1y", days: 365 },
39
39
  ] as const;
40
+
41
+ // Commission in basis points (10000 = 100%). 1000 = 10%.
42
+ export const TURTLE_COMMISSION_RATIO = 1000;
43
+ export const TURTLE_COMMISSION_DESTINATION = "0x6710217e60008dD16463A0e8D1A3643fBC41566D";
@@ -1,3 +1,4 @@
1
+ import { useRef, useEffect } from "react";
1
2
  import { getAddress } from "viem";
2
3
  import type { Abi } from "viem";
3
4
  import { useReadContract } from "wagmi";
@@ -11,6 +12,7 @@ export function useCheckNexusMembership(
11
12
  const masterAbi = nexusSdk.abis?.NXMaster;
12
13
  const numericChainId = typeof chainId === "string" ? parseInt(chainId, 10) : chainId;
13
14
  const isOnEthereum = numericChainId === 1;
15
+ const confirmedMember = useRef(false);
14
16
 
15
17
  const {
16
18
  data: isMember = false,
@@ -25,8 +27,15 @@ export function useCheckNexusMembership(
25
27
  query: {
26
28
  enabled: isOnEthereum && !!userAddress && !!masterAddress && !!masterAbi,
27
29
  select: (data) => data as boolean,
30
+ refetchOnWindowFocus: !confirmedMember.current,
28
31
  },
29
32
  });
30
33
 
34
+ useEffect(() => {
35
+ if (isMember) {
36
+ confirmedMember.current = true;
37
+ }
38
+ }, [isMember]);
39
+
31
40
  return { isMember, isLoading, error };
32
41
  }
@@ -2,6 +2,7 @@ import { parseUnits, formatUnits } from "viem";
2
2
  import { useQuery } from "@tanstack/react-query";
3
3
  import { CoverAsset, GetQuoteResponse } from "@nexusmutual/sdk";
4
4
  import { TOKEN_SYMBOL_TO_COVER_ASSET, getOpportunityAPY, calculateAdjustedAPY } from "../utils";
5
+ import { TURTLE_COMMISSION_RATIO, TURTLE_COMMISSION_DESTINATION } from "../constants";
5
6
  import type { Opportunity } from "@turtleclub/hooks";
6
7
 
7
8
  interface UseCoverQuoteOptions {
@@ -54,6 +55,8 @@ async function fetchNexusQuote(
54
55
  paymentAsset: coverAsset,
55
56
  buyerAddress,
56
57
  ipfsCidOrContent: TERMS_IPFS_CID,
58
+ commissionRatio: TURTLE_COMMISSION_RATIO,
59
+ commissionDestination: TURTLE_COMMISSION_DESTINATION,
57
60
  });
58
61
 
59
62
 
@@ -2,6 +2,7 @@ import { useMemo } from "react";
2
2
  import { CoverAsset, products, ProductTypes } from "@nexusmutual/sdk";
3
3
  import type { Opportunity } from "@turtleclub/hooks";
4
4
  import { NexusProduct } from "../types";
5
+ import { getMatchedCoverAssets } from "../utils";
5
6
 
6
7
  const getHighestPriorityProduct = (
7
8
  currentBest: NexusProduct | null,
@@ -53,15 +54,15 @@ export const getNexusProductLookup = (opportunity: Opportunity | null | undefine
53
54
  const protocolName = getProtocolForInsurance(opportunity);
54
55
  const nexusProduct = protocolName ? NEXUS_PRODUCT_MAP[protocolName.toLowerCase()] : null;
55
56
 
56
-
57
-
57
+ const allCoverAssets = nexusProduct?.coverAssets ?? [];
58
+ const coverAssets = getMatchedCoverAssets(opportunity?.depositTokens, allCoverAssets);
58
59
 
59
60
  return {
60
61
  protocolName: protocolName ?? "Unknown",
61
62
  productId: nexusProduct?.id ?? null,
62
63
  coverProductName: nexusProduct?.name ?? null,
63
64
  isCoverable: !!nexusProduct,
64
- coverAssets: nexusProduct?.coverAssets ?? [],
65
+ coverAssets,
65
66
  };
66
67
  };
67
68
 
@@ -5,6 +5,9 @@ import { getAddress, type Abi } from "viem";
5
5
  import { useWriteContract, useConfig } from "wagmi";
6
6
  import { waitForTransactionReceipt } from "wagmi/actions";
7
7
  import { formatPurchaseError } from "../utils";
8
+ import { submitCoverPurchase } from "@turtleclub/hooks";
9
+
10
+ const TRACKING_MAX_RETRIES = 4;
8
11
 
9
12
  type UseNexusPurchaseArgs = {
10
13
  onSuccess?: (message: string) => void;
@@ -23,6 +26,15 @@ export function useNexusPurchase({
23
26
  const config = useConfig();
24
27
  const { writeContractAsync } = useWriteContract();
25
28
 
29
+ const { mutate: trackPurchase } = useMutation({
30
+ mutationFn: submitCoverPurchase,
31
+ retry: TRACKING_MAX_RETRIES,
32
+ retryDelay: (attempt) => Math.min(20_000 * 2 ** attempt, 120_000),
33
+ onError: (err) => {
34
+ console.warn("[Cover Tracking] Failed to track purchase after retries:", err);
35
+ },
36
+ });
37
+
26
38
  const { mutateAsync, isPending: isPurchasing } = useMutation({
27
39
  mutationFn: async (quoteResult: GetQuoteResponse) => {
28
40
  const { buyCoverParams, poolAllocationRequests } = quoteResult.buyCoverInput;
@@ -34,6 +46,7 @@ export function useNexusPurchase({
34
46
  }
35
47
  const isNativePayment = buyCoverParams.paymentAsset === 0;
36
48
 
49
+
37
50
  const txHash = await writeContractAsync({
38
51
  address: getAddress(coverAddress),
39
52
  abi: coverAbi as Abi,
@@ -47,9 +60,16 @@ export function useNexusPurchase({
47
60
  hash: txHash,
48
61
  confirmations: 1,
49
62
  });
63
+
64
+ return { txHash, walletAddress: buyCoverParams.owner };
50
65
  },
51
- onSuccess: () => {
66
+ onSuccess: ({ txHash, walletAddress }) => {
52
67
  onSuccess?.("Cover purchased successfully");
68
+
69
+ trackPurchase({
70
+ walletAddress,
71
+ txHash,
72
+ });
53
73
  },
54
74
  onError: (err) => {
55
75
  onError?.(formatPurchaseError(err));
@@ -64,4 +84,4 @@ export function useNexusPurchase({
64
84
  );
65
85
 
66
86
  return { purchase, isPurchasing };
67
- }
87
+ }
@@ -2,6 +2,8 @@ import { CoverAsset } from "@nexusmutual/sdk";
2
2
  import type { Opportunity } from "@turtleclub/hooks";
3
3
  import { BaseError, UserRejectedRequestError } from "viem";
4
4
 
5
+ export const COVER_QUOTE_ERROR_MESSAGE = "Try a smaller value";
6
+
5
7
  export function getOpportunityAPY(opportunity: Opportunity): number {
6
8
  if (!opportunity.incentives || opportunity.incentives.length === 0) {
7
9
  return 0;
@@ -92,4 +94,36 @@ export const TOKEN_SYMBOL_TO_COVER_ASSET: Record<string, CoverAsset> = {
92
94
  DAI: CoverAsset.DAI,
93
95
  USDC: CoverAsset.USDC,
94
96
  cbBTC: CoverAsset.cbBTC,
95
- };
97
+ };
98
+
99
+ const DEPOSIT_TOKEN_NAME_TO_COVER_SYMBOL: [pattern: string, symbol: string][] = [
100
+ ["USD", "USDC"],
101
+ ["ETH", "ETH"],
102
+ ["BTC", "cbBTC"],
103
+ ];
104
+
105
+ /**
106
+ * Filter cover-available assets to only those that match the opportunity's deposit tokens.
107
+ * Matching is based on the deposit token **name** (case-insensitive):
108
+ * name contains "USD" → USDC, "ETH" → ETH, "BTC" → cbBTC
109
+ */
110
+ export function getMatchedCoverAssets(
111
+ depositTokens: { name: string }[] | undefined,
112
+ coverAvailableAssets: string[] | undefined,
113
+ ): string[] {
114
+ if (!coverAvailableAssets?.length) return [];
115
+ if (!depositTokens?.length) return coverAvailableAssets;
116
+
117
+ const matched = new Set<string>();
118
+ for (const token of depositTokens) {
119
+ const upper = token.name.toUpperCase();
120
+ for (const [pattern, symbol] of DEPOSIT_TOKEN_NAME_TO_COVER_SYMBOL) {
121
+ if (upper.includes(pattern)) {
122
+ matched.add(symbol);
123
+ }
124
+ }
125
+ }
126
+
127
+ if (matched.size === 0) return coverAvailableAssets;
128
+ return coverAvailableAssets.filter((asset) => matched.has(asset));
129
+ }