@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
|
@@ -1,122 +1,126 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
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";
|
|
5
6
|
|
|
6
7
|
interface UseCoverQuoteOptions {
|
|
7
8
|
productId: number;
|
|
8
9
|
buyerAddress: string;
|
|
9
10
|
amountToCover: string;
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
coverPeriod: number;
|
|
12
|
+
tokenSymbol: string;
|
|
13
|
+
decimals: number | undefined;
|
|
14
|
+
opportunity: Opportunity;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
|
-
interface UseCoverQuoteReturn
|
|
15
|
-
|
|
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;
|
|
16
25
|
refetch: () => void;
|
|
17
26
|
}
|
|
18
27
|
|
|
19
28
|
let nexusSdkInstance: import("@nexusmutual/sdk").NexusSDK | null = null;
|
|
20
|
-
let coverAssetEnum: typeof import("@nexusmutual/sdk").CoverAsset | null = null;
|
|
21
29
|
|
|
22
30
|
const TERMS_IPFS_CID = "QmXUzXDMbeKSCewUie34vPD7mCAGnshi4ULRy4h7DLmoRS";
|
|
23
31
|
|
|
24
32
|
async function getNexusSdk() {
|
|
25
|
-
if (!nexusSdkInstance
|
|
26
|
-
const { NexusSDK
|
|
33
|
+
if (!nexusSdkInstance) {
|
|
34
|
+
const { NexusSDK } = await import("@nexusmutual/sdk");
|
|
27
35
|
nexusSdkInstance = new NexusSDK();
|
|
28
|
-
coverAssetEnum = CoverAsset;
|
|
29
36
|
}
|
|
30
|
-
|
|
31
|
-
return { nexusSdk: nexusSdkInstance, CoverAsset: coverAssetEnum };
|
|
37
|
+
return nexusSdkInstance;
|
|
32
38
|
}
|
|
33
39
|
|
|
34
|
-
|
|
35
40
|
async function fetchNexusQuote(
|
|
36
41
|
productId: number,
|
|
37
42
|
buyerAddress: string,
|
|
38
|
-
|
|
39
|
-
period: number
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
const amountWei = parseEther(amount).toString();
|
|
43
|
+
amountToCover: string,
|
|
44
|
+
period: number,
|
|
45
|
+
coverAsset: CoverAsset
|
|
46
|
+
): Promise<GetQuoteResponse> {
|
|
47
|
+
const nexusSdk = await getNexusSdk();
|
|
44
48
|
|
|
45
49
|
const { result, error } = await nexusSdk.quote.getQuoteAndBuyCoverInputs({
|
|
46
50
|
productId,
|
|
47
|
-
amount:
|
|
51
|
+
amount: amountToCover,
|
|
48
52
|
period,
|
|
49
|
-
coverAsset
|
|
50
|
-
paymentAsset:
|
|
53
|
+
coverAsset,
|
|
54
|
+
paymentAsset: coverAsset,
|
|
51
55
|
buyerAddress,
|
|
52
56
|
ipfsCidOrContent: TERMS_IPFS_CID,
|
|
53
57
|
});
|
|
54
58
|
|
|
59
|
+
|
|
55
60
|
if (error) {
|
|
56
61
|
throw new Error(error.message || "Failed to fetch Nexus quote");
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
return result as
|
|
64
|
+
return result as GetQuoteResponse;
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
export function useCoverQuote({
|
|
63
68
|
productId,
|
|
64
69
|
buyerAddress,
|
|
65
70
|
amountToCover,
|
|
66
|
-
|
|
71
|
+
coverPeriod,
|
|
72
|
+
tokenSymbol,
|
|
73
|
+
decimals,
|
|
74
|
+
opportunity,
|
|
67
75
|
}: UseCoverQuoteOptions): UseCoverQuoteReturn {
|
|
68
|
-
const [
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return () => clearTimeout(timeoutId);
|
|
109
|
-
}, [fetchQuote, debounceMs]);
|
|
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;
|
|
110
115
|
|
|
111
116
|
return {
|
|
112
|
-
coverPeriod,
|
|
113
|
-
setCoverPeriod,
|
|
114
117
|
isLoading,
|
|
115
|
-
error,
|
|
116
|
-
quoteResult,
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
error: queryError?.message ?? null,
|
|
119
|
+
quoteResult: data?.quoteResult ?? null,
|
|
120
|
+
premium: data?.premium ?? null,
|
|
121
|
+
originalAPY,
|
|
122
|
+
adjustedAPY,
|
|
123
|
+
coverCostPerc,
|
|
124
|
+
refetch,
|
|
120
125
|
};
|
|
121
126
|
}
|
|
122
|
-
|
|
@@ -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
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { useCallback, useMemo } from "react";
|
|
2
|
+
import { getAddress } from "viem";
|
|
3
|
+
import { useReadContract } from "wagmi";
|
|
4
4
|
import nexusSdk from "@nexusmutual/sdk";
|
|
5
|
-
import { useUserCoverNfts
|
|
6
|
-
|
|
5
|
+
import { useUserCoverNfts } from "./useUserCoverNfts";
|
|
6
|
+
import type { Abi } from "viem";
|
|
7
7
|
// Raw cover data from contract (all numbers are BigInt)
|
|
8
8
|
interface RawCoverData {
|
|
9
9
|
coverId: bigint;
|
|
@@ -54,56 +54,31 @@ interface UseExistingCoversReturn {
|
|
|
54
54
|
refetch: () => void;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
const publicClient = createPublicClient({
|
|
58
|
-
chain: mainnet,
|
|
59
|
-
transport: http(),
|
|
60
|
-
});
|
|
61
|
-
|
|
62
57
|
export function useExistingCovers(userAddress: string | undefined): UseExistingCoversReturn {
|
|
63
58
|
const { coverNfts, isLoading: isNftsLoading, refetch: refetchNfts } = useUserCoverNfts(userAddress);
|
|
64
|
-
const [covers, setCovers] = useState<CoverData[]>([]);
|
|
65
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
66
|
-
const [error, setError] = useState<string | null>(null);
|
|
67
|
-
|
|
68
|
-
const fetchCovers = useCallback(async () => {
|
|
69
|
-
if (isNftsLoading || coverNfts.length === 0) {
|
|
70
|
-
setCovers([]);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
setIsLoading(true);
|
|
75
|
-
setError(null);
|
|
76
59
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
const result = await publicClient.readContract({
|
|
85
|
-
address: getAddress(coverViewerAddress),
|
|
86
|
-
abi: coverViewerAbi as any,
|
|
87
|
-
functionName: "getCovers",
|
|
88
|
-
args: [coverIds],
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const rawCovers = result as RawCoverData[];
|
|
92
|
-
const parsedCovers = rawCovers.map(parseCoverData);
|
|
93
|
-
setCovers(parsedCovers);
|
|
94
|
-
} catch (err: unknown) {
|
|
95
|
-
const errorMessage = err instanceof Error ? err.message : "Failed to fetch cover details";
|
|
96
|
-
console.error("Cover details fetch error:", err);
|
|
97
|
-
setError(errorMessage);
|
|
98
|
-
setCovers([]);
|
|
99
|
-
} finally {
|
|
100
|
-
setIsLoading(false);
|
|
101
|
-
}
|
|
102
|
-
}, [coverNfts, isNftsLoading]);
|
|
60
|
+
const coverIds = useMemo(
|
|
61
|
+
() => coverNfts.map((nft) => BigInt(nft.token_id)),
|
|
62
|
+
[coverNfts]
|
|
63
|
+
);
|
|
64
|
+
const coverViewerAddress = nexusSdk.addresses?.CoverViewer;
|
|
65
|
+
const coverViewerAbi = nexusSdk.abis?.CoverViewer;
|
|
103
66
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
67
|
+
const {
|
|
68
|
+
data: covers = [],
|
|
69
|
+
isLoading: isContractLoading,
|
|
70
|
+
error: contractError,
|
|
71
|
+
refetch: refetchCovers,
|
|
72
|
+
} = useReadContract({
|
|
73
|
+
address: coverViewerAddress ? getAddress(coverViewerAddress) : undefined,
|
|
74
|
+
abi: coverViewerAbi as Abi,
|
|
75
|
+
functionName: "getCovers",
|
|
76
|
+
args: [coverIds],
|
|
77
|
+
query: {
|
|
78
|
+
enabled: !isNftsLoading && coverNfts.length > 0 && !!coverViewerAddress && !!coverViewerAbi,
|
|
79
|
+
select: (data) => (data as RawCoverData[]).map(parseCoverData),
|
|
80
|
+
},
|
|
81
|
+
});
|
|
107
82
|
|
|
108
83
|
const hasCoverForProduct = useCallback(
|
|
109
84
|
(productId: number): CoverData | undefined => {
|
|
@@ -114,12 +89,12 @@ export function useExistingCovers(userAddress: string | undefined): UseExistingC
|
|
|
114
89
|
|
|
115
90
|
return {
|
|
116
91
|
covers,
|
|
117
|
-
isLoading:
|
|
118
|
-
error,
|
|
92
|
+
isLoading: isNftsLoading || isContractLoading,
|
|
93
|
+
error: contractError?.message ?? null,
|
|
119
94
|
hasCoverForProduct,
|
|
120
95
|
refetch: () => {
|
|
121
96
|
refetchNfts();
|
|
122
|
-
|
|
97
|
+
refetchCovers();
|
|
123
98
|
},
|
|
124
99
|
};
|
|
125
100
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
|
-
import { products, ProductTypes } from "@nexusmutual/sdk";
|
|
2
|
+
import { CoverAsset, products, ProductTypes } from "@nexusmutual/sdk";
|
|
3
3
|
import type { Opportunity } from "@turtleclub/hooks";
|
|
4
4
|
import { NexusProduct } from "../types";
|
|
5
5
|
|
|
@@ -53,11 +53,15 @@ export const getNexusProductLookup = (opportunity: Opportunity | null | undefine
|
|
|
53
53
|
const protocolName = getProtocolForInsurance(opportunity);
|
|
54
54
|
const nexusProduct = protocolName ? NEXUS_PRODUCT_MAP[protocolName.toLowerCase()] : null;
|
|
55
55
|
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
56
59
|
return {
|
|
57
60
|
protocolName: protocolName ?? "Unknown",
|
|
58
61
|
productId: nexusProduct?.id ?? null,
|
|
59
62
|
coverProductName: nexusProduct?.name ?? null,
|
|
60
63
|
isCoverable: !!nexusProduct,
|
|
64
|
+
coverAssets: nexusProduct?.coverAssets ?? [],
|
|
61
65
|
};
|
|
62
66
|
};
|
|
63
67
|
|
|
@@ -66,6 +70,7 @@ export interface NexusProductLookup {
|
|
|
66
70
|
productId: number | null;
|
|
67
71
|
coverProductName: string | null;
|
|
68
72
|
isCoverable: boolean;
|
|
73
|
+
coverAssets: string[];
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
export function useNexusProduct(opportunity: Opportunity | null | undefined): NexusProductLookup {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { useCallback
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
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";
|
|
4
5
|
import { useWriteContract, useConfig } from "wagmi";
|
|
5
6
|
import { waitForTransactionReceipt } from "wagmi/actions";
|
|
6
7
|
import { formatPurchaseError } from "../utils";
|
|
7
|
-
import type { NexusQuoteResult } from "../types";
|
|
8
8
|
|
|
9
9
|
type UseNexusPurchaseArgs = {
|
|
10
10
|
onSuccess?: (message: string) => void;
|
|
@@ -12,53 +12,55 @@ type UseNexusPurchaseArgs = {
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
type UseNexusPurchaseReturn = {
|
|
15
|
-
purchase: (quoteResult:
|
|
15
|
+
purchase: (quoteResult: GetQuoteResponse) => Promise<void>;
|
|
16
16
|
isPurchasing: boolean;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
export function useNexusPurchase(
|
|
20
|
-
|
|
19
|
+
export function useNexusPurchase({
|
|
20
|
+
onSuccess,
|
|
21
|
+
onError,
|
|
22
|
+
}: UseNexusPurchaseArgs): UseNexusPurchaseReturn {
|
|
21
23
|
const config = useConfig();
|
|
22
24
|
const { writeContractAsync } = useWriteContract();
|
|
23
|
-
const [isPurchasing, setIsPurchasing] = useState(false);
|
|
24
|
-
|
|
25
|
-
const purchase = useCallback(
|
|
26
|
-
async (quoteResult: NexusQuoteResult): Promise<string> => {
|
|
27
|
-
try {
|
|
28
|
-
setIsPurchasing(true);
|
|
29
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;
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
throw new Error('Nexus SDK not configured correctly for Cover.');
|
|
36
|
-
}
|
|
32
|
+
if (!coverAddress || !coverAbi) {
|
|
33
|
+
throw new Error("Nexus SDK not configured correctly for Cover.");
|
|
34
|
+
}
|
|
35
|
+
const isNativePayment = buyCoverParams.paymentAsset === 0;
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
+
});
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
+
});
|
|
51
58
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
onError?.(formatPurchaseError(err));
|
|
56
|
-
throw err;
|
|
57
|
-
} finally {
|
|
58
|
-
setIsPurchasing(false);
|
|
59
|
-
}
|
|
59
|
+
const purchase = useCallback(
|
|
60
|
+
async (quoteResult: GetQuoteResponse) => {
|
|
61
|
+
await mutateAsync(quoteResult);
|
|
60
62
|
},
|
|
61
|
-
[
|
|
63
|
+
[mutateAsync]
|
|
62
64
|
);
|
|
63
65
|
|
|
64
66
|
return { purchase, isPurchasing };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { useConfig, useWriteContract } from "wagmi";
|
|
4
|
+
import { readContract, waitForTransactionReceipt } from "wagmi/actions";
|
|
5
|
+
import { erc20Abi } from "viem";
|
|
6
|
+
|
|
7
|
+
interface UseTokenApprovalParams {
|
|
8
|
+
userAddress: `0x${string}` | undefined;
|
|
9
|
+
tokenAddress: `0x${string}`;
|
|
10
|
+
spenderAddress: `0x${string}`;
|
|
11
|
+
chainId: number;
|
|
12
|
+
amount: bigint | null | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface UseTokenApprovalReturn {
|
|
16
|
+
needsApproval: boolean;
|
|
17
|
+
isCheckingApproval: boolean;
|
|
18
|
+
approve: () => Promise<void>;
|
|
19
|
+
isApproving: boolean;
|
|
20
|
+
refetchApprovalStatus: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const NATIVE_TOKEN_ADDRESS_ZERO =
|
|
24
|
+
"0x0000000000000000000000000000000000000000" as const;
|
|
25
|
+
export const NATIVE_TOKEN_ADDRESS_EEEE =
|
|
26
|
+
"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" as const;
|
|
27
|
+
|
|
28
|
+
function isNativeTokenAddress(address: string): boolean {
|
|
29
|
+
const lower = address.toLowerCase();
|
|
30
|
+
return lower === NATIVE_TOKEN_ADDRESS_ZERO || lower === NATIVE_TOKEN_ADDRESS_EEEE;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function useTokenApproval({
|
|
34
|
+
userAddress,
|
|
35
|
+
tokenAddress,
|
|
36
|
+
spenderAddress,
|
|
37
|
+
chainId,
|
|
38
|
+
amount,
|
|
39
|
+
}: UseTokenApprovalParams): UseTokenApprovalReturn {
|
|
40
|
+
const config = useConfig();
|
|
41
|
+
const queryClient = useQueryClient();
|
|
42
|
+
const { writeContractAsync } = useWriteContract();
|
|
43
|
+
|
|
44
|
+
const isNativeToken = isNativeTokenAddress(tokenAddress);
|
|
45
|
+
const hasValidAmount = !!amount && amount > 0n;
|
|
46
|
+
const isEnabled = !!userAddress && hasValidAmount && !isNativeToken;
|
|
47
|
+
|
|
48
|
+
const queryKey = [
|
|
49
|
+
"tokenApproval",
|
|
50
|
+
userAddress,
|
|
51
|
+
tokenAddress,
|
|
52
|
+
spenderAddress,
|
|
53
|
+
amount?.toString(),
|
|
54
|
+
chainId,
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const {
|
|
58
|
+
data: needsApproval = false,
|
|
59
|
+
isLoading: isCheckingApproval,
|
|
60
|
+
refetch,
|
|
61
|
+
} = useQuery({
|
|
62
|
+
queryKey,
|
|
63
|
+
queryFn: async () => {
|
|
64
|
+
if (!userAddress || !amount) return false;
|
|
65
|
+
|
|
66
|
+
const currentAllowance = await readContract(config, {
|
|
67
|
+
address: tokenAddress,
|
|
68
|
+
abi: erc20Abi,
|
|
69
|
+
functionName: "allowance",
|
|
70
|
+
args: [userAddress, spenderAddress],
|
|
71
|
+
chainId,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return currentAllowance < amount;
|
|
75
|
+
},
|
|
76
|
+
enabled: isEnabled,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const { mutateAsync: approveAsync, isPending: isApproving } = useMutation({
|
|
80
|
+
mutationFn: async () => {
|
|
81
|
+
if (!userAddress || !amount) {
|
|
82
|
+
throw new Error("User address and amount are required");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const approvalHash = await writeContractAsync({
|
|
86
|
+
address: tokenAddress,
|
|
87
|
+
abi: erc20Abi,
|
|
88
|
+
functionName: "approve",
|
|
89
|
+
args: [spenderAddress, amount],
|
|
90
|
+
chainId,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await waitForTransactionReceipt(config, {
|
|
94
|
+
hash: approvalHash,
|
|
95
|
+
chainId,
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
onSuccess: () => {
|
|
99
|
+
queryClient.invalidateQueries({ queryKey });
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const approve = useCallback(async () => {
|
|
104
|
+
await approveAsync();
|
|
105
|
+
}, [approveAsync]);
|
|
106
|
+
|
|
107
|
+
const refetchApprovalStatus = useCallback(() => {
|
|
108
|
+
refetch();
|
|
109
|
+
}, [refetch]);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
needsApproval,
|
|
113
|
+
isCheckingApproval,
|
|
114
|
+
approve,
|
|
115
|
+
isApproving,
|
|
116
|
+
refetchApprovalStatus,
|
|
117
|
+
};
|
|
118
|
+
}
|