@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 +12 -0
- package/package.json +10 -10
- package/src/nexus-mutual-cover/components/CoverOfferCard.tsx +11 -9
- package/src/nexus-mutual-cover/components/CoverRequestForm.tsx +1 -1
- package/src/nexus-mutual-cover/components/NexusCoverSection.tsx +1 -1
- package/src/nexus-mutual-cover/constants.ts +4 -0
- package/src/nexus-mutual-cover/hooks/useCheckNexusMembership.ts +9 -0
- package/src/nexus-mutual-cover/hooks/useCoverQuote.ts +3 -0
- package/src/nexus-mutual-cover/hooks/useNexusProduct.ts +4 -3
- package/src/nexus-mutual-cover/hooks/useNexusPurchase.ts +22 -2
- package/src/nexus-mutual-cover/utils/index.ts +35 -1
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.
|
|
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
|
-
"@
|
|
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
|
-
"@
|
|
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": "
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
{
|
|
428
|
+
{formatBalance(premium)} {selectedTokenSymbol}
|
|
427
429
|
</span>
|
|
428
430
|
) : error ? (
|
|
429
|
-
<span className="text-sm text-destructive">
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|