@0xmonaco/react 0.7.7 → 0.7.9
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/dist/hooks/index.d.ts +15 -0
- package/dist/hooks/index.js +15 -0
- package/dist/hooks/useAuth/index.d.ts +2 -0
- package/dist/hooks/useAuth/index.js +1 -0
- package/dist/hooks/useAuth/types.d.ts +30 -0
- package/dist/hooks/useAuth/types.js +0 -0
- package/dist/hooks/useAuth/useAuth.d.ts +2 -0
- package/dist/hooks/useAuth/useAuth.js +145 -0
- package/dist/hooks/useFees/index.d.ts +2 -0
- package/dist/hooks/useFees/index.js +1 -0
- package/dist/hooks/useFees/types.d.ts +5 -0
- package/dist/hooks/useFees/types.js +0 -0
- package/dist/hooks/useFees/useFees.d.ts +2 -0
- package/dist/hooks/useFees/useFees.js +14 -0
- package/dist/hooks/useMarket/index.d.ts +2 -0
- package/dist/hooks/useMarket/index.js +1 -0
- package/dist/hooks/useMarket/types.d.ts +30 -0
- package/dist/hooks/useMarket/types.js +0 -0
- package/dist/hooks/useMarket/useMarket.d.ts +2 -0
- package/dist/hooks/useMarket/useMarket.js +92 -0
- package/dist/hooks/useMonaco/index.d.ts +2 -0
- package/dist/hooks/useMonaco/index.js +1 -0
- package/dist/hooks/useMonaco/types.d.ts +13 -0
- package/dist/hooks/useMonaco/types.js +0 -0
- package/dist/hooks/useMonaco/useMonaco.d.ts +2 -0
- package/dist/hooks/useMonaco/useMonaco.js +13 -0
- package/dist/hooks/useOHLCV/index.d.ts +2 -0
- package/dist/hooks/useOHLCV/index.js +2 -0
- package/dist/hooks/useOHLCV/types.d.ts +29 -0
- package/dist/hooks/useOHLCV/types.js +0 -0
- package/dist/hooks/useOHLCV/useOHLCV.d.ts +11 -0
- package/dist/hooks/useOHLCV/useOHLCV.js +76 -0
- package/dist/hooks/useOrderbook/index.d.ts +2 -0
- package/dist/hooks/useOrderbook/index.js +1 -0
- package/dist/hooks/useOrderbook/types.d.ts +27 -0
- package/dist/hooks/useOrderbook/types.js +0 -0
- package/dist/hooks/useOrderbook/useOrderbook.d.ts +3 -0
- package/dist/hooks/useOrderbook/useOrderbook.js +31 -0
- package/dist/hooks/usePositions/index.d.ts +2 -0
- package/dist/hooks/usePositions/index.js +1 -0
- package/dist/hooks/usePositions/types.d.ts +11 -0
- package/dist/hooks/usePositions/types.js +0 -0
- package/dist/hooks/usePositions/usePositions.d.ts +2 -0
- package/dist/hooks/usePositions/usePositions.js +65 -0
- package/dist/hooks/useProfile/index.d.ts +2 -0
- package/dist/hooks/useProfile/index.js +1 -0
- package/dist/hooks/useProfile/types.d.ts +23 -0
- package/dist/hooks/useProfile/types.js +0 -0
- package/dist/hooks/useProfile/useProfile.d.ts +2 -0
- package/dist/hooks/useProfile/useProfile.js +128 -0
- package/dist/hooks/useTokenLifecycle/index.d.ts +2 -0
- package/dist/hooks/useTokenLifecycle/index.js +1 -0
- package/dist/hooks/useTokenLifecycle/types.d.ts +23 -0
- package/dist/hooks/useTokenLifecycle/types.js +0 -0
- package/dist/hooks/useTokenLifecycle/useTokenLifecycle.d.ts +20 -0
- package/dist/hooks/useTokenLifecycle/useTokenLifecycle.js +125 -0
- package/dist/hooks/useTokenLifecycle/utils.d.ts +7 -0
- package/dist/hooks/useTokenLifecycle/utils.js +15 -0
- package/dist/hooks/useTrade/index.d.ts +2 -0
- package/dist/hooks/useTrade/index.js +1 -0
- package/dist/hooks/useTrade/types.d.ts +53 -0
- package/dist/hooks/useTrade/types.js +0 -0
- package/dist/hooks/useTrade/useTrade.d.ts +2 -0
- package/dist/hooks/useTrade/useTrade.js +132 -0
- package/dist/hooks/useTradeFeed/index.d.ts +2 -0
- package/dist/hooks/useTradeFeed/index.js +2 -0
- package/dist/hooks/useTradeFeed/types.d.ts +14 -0
- package/dist/hooks/useTradeFeed/types.js +0 -0
- package/dist/hooks/useTradeFeed/useTradeFeed.d.ts +12 -0
- package/dist/hooks/useTradeFeed/useTradeFeed.js +32 -0
- package/dist/hooks/useUserBalances/index.d.ts +2 -0
- package/dist/hooks/useUserBalances/index.js +2 -0
- package/dist/hooks/useUserBalances/types.d.ts +18 -0
- package/dist/hooks/useUserBalances/types.js +0 -0
- package/dist/hooks/useUserBalances/useUserBalances.d.ts +9 -0
- package/dist/hooks/useUserBalances/useUserBalances.js +171 -0
- package/dist/hooks/useUserMovements/index.d.ts +2 -0
- package/dist/hooks/useUserMovements/index.js +2 -0
- package/dist/hooks/useUserMovements/types.d.ts +23 -0
- package/dist/hooks/useUserMovements/types.js +0 -0
- package/dist/hooks/useUserMovements/useUserMovements.d.ts +18 -0
- package/dist/hooks/useUserMovements/useUserMovements.js +122 -0
- package/dist/hooks/useUserOrders/index.d.ts +2 -0
- package/dist/hooks/useUserOrders/index.js +2 -0
- package/dist/hooks/useUserOrders/types.d.ts +18 -0
- package/dist/hooks/useUserOrders/types.js +0 -0
- package/dist/hooks/useUserOrders/useUserOrders.d.ts +11 -0
- package/dist/hooks/useUserOrders/useUserOrders.js +191 -0
- package/dist/hooks/useVault/index.d.ts +2 -0
- package/dist/hooks/useVault/index.js +1 -0
- package/dist/hooks/useVault/types.d.ts +15 -0
- package/dist/hooks/useVault/types.js +0 -0
- package/dist/hooks/useVault/useVault.d.ts +2 -0
- package/dist/hooks/useVault/useVault.js +66 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/provider/MonacoProvider.d.ts +3 -0
- package/dist/provider/MonacoProvider.js +142 -0
- package/dist/provider/TradeFeedProvider.d.ts +22 -0
- package/dist/provider/TradeFeedProvider.js +143 -0
- package/dist/provider/index.d.ts +3 -0
- package/dist/provider/index.js +3 -0
- package/dist/provider/types.d.ts +49 -0
- package/dist/provider/types.js +9 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/tokenStorage.d.ts +38 -0
- package/dist/utils/tokenStorage.js +102 -0
- package/package.json +3 -3
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { useMonacoSDK } from "../useMonaco";
|
|
3
|
+
/**
|
|
4
|
+
* Hook for subscribing to real-time user order events via WebSocket (authenticated)
|
|
5
|
+
*
|
|
6
|
+
* Fetches initial orders from the REST API, then subscribes to real-time updates.
|
|
7
|
+
* Requires authentication - the user must be logged in with a valid JWT token.
|
|
8
|
+
* User is identified on the backend via the JWT token.
|
|
9
|
+
*
|
|
10
|
+
* @param maxOrders - Maximum number of orders to keep in state (default: 50)
|
|
11
|
+
*/
|
|
12
|
+
export function useUserOrders(maxOrders = 50) {
|
|
13
|
+
const { sdk } = useMonacoSDK();
|
|
14
|
+
const [orders, setOrders] = useState([]);
|
|
15
|
+
const [loading, setLoading] = useState(false);
|
|
16
|
+
const [error, setError] = useState(null);
|
|
17
|
+
const [subscribed, setSubscribed] = useState(false);
|
|
18
|
+
const clearError = useCallback(() => setError(null), []);
|
|
19
|
+
const fetchOrders = useCallback(async () => {
|
|
20
|
+
if (!sdk?.trading)
|
|
21
|
+
return;
|
|
22
|
+
setLoading(true);
|
|
23
|
+
try {
|
|
24
|
+
const response = await sdk.trading.getPaginatedOrders({
|
|
25
|
+
page: 1,
|
|
26
|
+
page_size: maxOrders,
|
|
27
|
+
});
|
|
28
|
+
// Combine latest_orders (from Redis) with orders (from PostgreSQL)
|
|
29
|
+
// latest_orders contains real-time data that may not yet be in PostgreSQL
|
|
30
|
+
// Deduplicate by id, preferring latest_orders (newer data)
|
|
31
|
+
const latestOrders = response.latest_orders || [];
|
|
32
|
+
const historicalOrders = response.orders;
|
|
33
|
+
// Merge: latest_orders first, then historical, deduplicated by id
|
|
34
|
+
const seenIds = new Set();
|
|
35
|
+
const mergedOrders = [];
|
|
36
|
+
for (const order of [...latestOrders, ...historicalOrders]) {
|
|
37
|
+
if (!seenIds.has(order.id)) {
|
|
38
|
+
seenIds.add(order.id);
|
|
39
|
+
mergedOrders.push(order);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
setOrders(mergedOrders.slice(0, maxOrders));
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
setLoading(false);
|
|
49
|
+
}
|
|
50
|
+
}, [sdk?.trading, maxOrders]);
|
|
51
|
+
// Manual refresh function
|
|
52
|
+
const refresh = useCallback(async () => {
|
|
53
|
+
await fetchOrders();
|
|
54
|
+
}, [fetchOrders]);
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!sdk?.ws || !sdk?.trading) {
|
|
57
|
+
setSubscribed(false);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
setOrders([]);
|
|
61
|
+
setError(null);
|
|
62
|
+
setLoading(true);
|
|
63
|
+
const limit = Number.isFinite(maxOrders) ? maxOrders : 50;
|
|
64
|
+
// Fetch initial orders via REST API, then subscribe to WebSocket updates
|
|
65
|
+
let unsubscribe;
|
|
66
|
+
fetchOrders()
|
|
67
|
+
.then(() => {
|
|
68
|
+
// Subscribe to WebSocket order updates after initial data is loaded
|
|
69
|
+
// This prevents race conditions where WS events could be overwritten by REST response
|
|
70
|
+
try {
|
|
71
|
+
unsubscribe = sdk.ws.userOrders((event) => {
|
|
72
|
+
setOrders((prev) => {
|
|
73
|
+
const orderId = event.orderId;
|
|
74
|
+
// Check if this order already exists
|
|
75
|
+
const existingIndex = prev.findIndex((o) => o.id === orderId);
|
|
76
|
+
const existingOrder = prev[existingIndex];
|
|
77
|
+
if (existingIndex >= 0 && existingOrder) {
|
|
78
|
+
// Update existing order with new data from event
|
|
79
|
+
const updatedOrder = updateOrderFromEvent(existingOrder, event);
|
|
80
|
+
// If order is terminal (filled, canceled, rejected, expired), keep it but update
|
|
81
|
+
const newOrders = [...prev];
|
|
82
|
+
newOrders[existingIndex] = updatedOrder;
|
|
83
|
+
return newOrders;
|
|
84
|
+
}
|
|
85
|
+
// New order - add to the beginning if it's an OrderPlaced event
|
|
86
|
+
if (event.eventType === "OrderPlaced") {
|
|
87
|
+
const newOrder = orderFromEvent(event);
|
|
88
|
+
if (newOrder) {
|
|
89
|
+
return [newOrder, ...prev].slice(0, limit);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return prev;
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
setSubscribed(true);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
99
|
+
setSubscribed(false);
|
|
100
|
+
setLoading(false);
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
.catch((err) => {
|
|
104
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
105
|
+
setLoading(false);
|
|
106
|
+
});
|
|
107
|
+
return () => {
|
|
108
|
+
unsubscribe?.();
|
|
109
|
+
setSubscribed(false);
|
|
110
|
+
};
|
|
111
|
+
}, [sdk?.ws, sdk?.trading, maxOrders, fetchOrders]);
|
|
112
|
+
return { orders, loading, subscribed, error, clearError, refresh };
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Create an Order object from an OrderPlaced event
|
|
116
|
+
*/
|
|
117
|
+
function orderFromEvent(event) {
|
|
118
|
+
if (event.eventType !== "OrderPlaced")
|
|
119
|
+
return null;
|
|
120
|
+
const { data } = event;
|
|
121
|
+
const tradingPairId = data.tradingPairId || data.symbol;
|
|
122
|
+
if (!tradingPairId)
|
|
123
|
+
return null;
|
|
124
|
+
return {
|
|
125
|
+
id: event.orderId,
|
|
126
|
+
trading_pair_id: tradingPairId,
|
|
127
|
+
order_type: (data.orderType || "LIMIT"),
|
|
128
|
+
side: (data.side || "BUY"),
|
|
129
|
+
price: data.price,
|
|
130
|
+
quantity: data.quantity || "0",
|
|
131
|
+
filled_quantity: "0",
|
|
132
|
+
average_fill_price: undefined,
|
|
133
|
+
status: data.status,
|
|
134
|
+
trading_mode: (data.tradingMode || "SPOT"),
|
|
135
|
+
time_in_force: data.timeInForce,
|
|
136
|
+
created_at: event.timestamp,
|
|
137
|
+
updated_at: event.timestamp,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Update an existing Order with data from a WebSocket event
|
|
142
|
+
*/
|
|
143
|
+
function updateOrderFromEvent(order, event) {
|
|
144
|
+
const data = event.data;
|
|
145
|
+
// Prefer status from event data when available, fall back to event-type-based mapping
|
|
146
|
+
let newStatus = order.status;
|
|
147
|
+
if ("status" in data && data.status) {
|
|
148
|
+
newStatus = data.status;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
switch (event.eventType) {
|
|
152
|
+
case "OrderPlaced":
|
|
153
|
+
newStatus = "SUBMITTED";
|
|
154
|
+
break;
|
|
155
|
+
case "OrderPartiallyFilled":
|
|
156
|
+
newStatus = "PARTIALLY_FILLED";
|
|
157
|
+
break;
|
|
158
|
+
case "OrderFilled":
|
|
159
|
+
newStatus = "FILLED";
|
|
160
|
+
break;
|
|
161
|
+
case "OrderCancelled":
|
|
162
|
+
newStatus = "CANCELLED";
|
|
163
|
+
break;
|
|
164
|
+
case "OrderRejected":
|
|
165
|
+
newStatus = "REJECTED";
|
|
166
|
+
break;
|
|
167
|
+
case "OrderExpired":
|
|
168
|
+
newStatus = "EXPIRED";
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Get filled quantity from different event types
|
|
173
|
+
let filledQuantity = order.filled_quantity;
|
|
174
|
+
let avgFillPrice = order.average_fill_price;
|
|
175
|
+
if ("totalFilled" in data && data.totalFilled) {
|
|
176
|
+
filledQuantity = data.totalFilled;
|
|
177
|
+
}
|
|
178
|
+
if ("filledQuantity" in data && data.filledQuantity) {
|
|
179
|
+
filledQuantity = data.filledQuantity;
|
|
180
|
+
}
|
|
181
|
+
if ("averageFillPrice" in data && data.averageFillPrice) {
|
|
182
|
+
avgFillPrice = data.averageFillPrice;
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
...order,
|
|
186
|
+
status: newStatus,
|
|
187
|
+
filled_quantity: filledQuantity,
|
|
188
|
+
average_fill_price: avgFillPrice,
|
|
189
|
+
updated_at: event.timestamp,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useVault } from "./useVault";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { TransactionResult, WithdrawResult } from "@0xmonaco/types";
|
|
2
|
+
export interface UseVaultReturn {
|
|
3
|
+
/** Approve the vault to spend tokens */
|
|
4
|
+
approve: (assetId: string, amount: bigint, autoWait?: boolean) => Promise<TransactionResult>;
|
|
5
|
+
/** Deposit tokens into the vault */
|
|
6
|
+
deposit: (assetId: string, amount: bigint, autoWait?: boolean) => Promise<TransactionResult>;
|
|
7
|
+
/** Initiate a withdrawal and submit the pre-signed calldata on-chain */
|
|
8
|
+
withdraw: (assetId: string, amount: bigint, autoWait?: boolean) => Promise<WithdrawResult>;
|
|
9
|
+
/** Retry a withdrawal whose on-chain submission never landed — does NOT create a new one */
|
|
10
|
+
retryWithdrawal: (withdrawalIndex: number, autoWait?: boolean) => Promise<WithdrawResult>;
|
|
11
|
+
/** Get the allowance for a token */
|
|
12
|
+
getAllowance: (assetId: string) => Promise<bigint>;
|
|
13
|
+
/** Check if a token needs approval for an amount */
|
|
14
|
+
needsApproval: (assetId: string, amount: bigint) => Promise<boolean>;
|
|
15
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { useMonacoSDK } from "../useMonaco";
|
|
3
|
+
export const useVault = () => {
|
|
4
|
+
const { sdk } = useMonacoSDK();
|
|
5
|
+
const approve = useCallback(async (assetId, amount, autoWait) => {
|
|
6
|
+
if (!sdk)
|
|
7
|
+
throw new Error("SDK not available");
|
|
8
|
+
if (!assetId?.trim())
|
|
9
|
+
throw new Error("Asset ID is required and cannot be empty");
|
|
10
|
+
if (amount <= 0n)
|
|
11
|
+
throw new Error("Amount must be greater than 0");
|
|
12
|
+
return await sdk.vault.approve(assetId, amount, autoWait);
|
|
13
|
+
}, [sdk]);
|
|
14
|
+
const deposit = useCallback(async (assetId, amount, autoWait) => {
|
|
15
|
+
if (!sdk)
|
|
16
|
+
throw new Error("SDK not available");
|
|
17
|
+
if (!assetId?.trim())
|
|
18
|
+
throw new Error("Asset ID is required and cannot be empty");
|
|
19
|
+
if (amount <= 0n)
|
|
20
|
+
throw new Error("Amount must be greater than 0");
|
|
21
|
+
return await sdk.vault.deposit(assetId, amount, autoWait);
|
|
22
|
+
}, [sdk]);
|
|
23
|
+
const withdraw = useCallback(async (assetId, amount, autoWait) => {
|
|
24
|
+
if (!sdk)
|
|
25
|
+
throw new Error("SDK not available");
|
|
26
|
+
if (!assetId?.trim())
|
|
27
|
+
throw new Error("Asset ID is required and cannot be empty");
|
|
28
|
+
if (amount <= 0n)
|
|
29
|
+
throw new Error("Amount must be greater than 0");
|
|
30
|
+
return await sdk.vault.withdraw(assetId, amount, autoWait);
|
|
31
|
+
}, [sdk]);
|
|
32
|
+
const retryWithdrawal = useCallback(async (withdrawalIndex, autoWait) => {
|
|
33
|
+
if (!sdk)
|
|
34
|
+
throw new Error("SDK not available");
|
|
35
|
+
if (!Number.isInteger(withdrawalIndex) || withdrawalIndex < 0) {
|
|
36
|
+
throw new Error("withdrawalIndex must be a non-negative integer");
|
|
37
|
+
}
|
|
38
|
+
return await sdk.vault.retryWithdrawal(withdrawalIndex, autoWait);
|
|
39
|
+
}, [sdk]);
|
|
40
|
+
const getAllowance = useCallback(async (assetId) => {
|
|
41
|
+
if (!sdk)
|
|
42
|
+
throw new Error("SDK not available");
|
|
43
|
+
if (!assetId?.trim())
|
|
44
|
+
throw new Error("Asset ID is required and cannot be empty");
|
|
45
|
+
return await sdk.vault.getAllowance(assetId);
|
|
46
|
+
}, [sdk]);
|
|
47
|
+
const needsApproval = useCallback(async (assetId, amount) => {
|
|
48
|
+
if (!sdk)
|
|
49
|
+
throw new Error("SDK not available");
|
|
50
|
+
if (!assetId?.trim())
|
|
51
|
+
throw new Error("Asset ID is required and cannot be empty");
|
|
52
|
+
if (amount <= 0n)
|
|
53
|
+
throw new Error("Amount must be greater than 0");
|
|
54
|
+
return await sdk.vault.needsApproval(assetId, amount);
|
|
55
|
+
}, [sdk]);
|
|
56
|
+
return {
|
|
57
|
+
// Token operations
|
|
58
|
+
approve,
|
|
59
|
+
deposit,
|
|
60
|
+
withdraw,
|
|
61
|
+
retryWithdrawal,
|
|
62
|
+
// Approval and allowance queries
|
|
63
|
+
getAllowance,
|
|
64
|
+
needsApproval,
|
|
65
|
+
};
|
|
66
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type MonacoContextValue, type MonacoProviderProps } from "./types";
|
|
2
|
+
export declare const MonacoProvider: ({ children, clientId, network, seiRpcUrl, walletClient, tokenLifecycle: tokenLifecycleConfig }: MonacoProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
3
|
+
export declare const useMonacoContext: () => MonacoContextValue;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { MonacoSDK } from "@0xmonaco/core";
|
|
3
|
+
import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { useTokenLifecycle } from "../hooks";
|
|
5
|
+
import { clearAuthState, loadAuthState } from "../utils";
|
|
6
|
+
import { TradeFeedProvider } from "./TradeFeedProvider";
|
|
7
|
+
import { AuthenticationStatus } from "./types";
|
|
8
|
+
const MonacoContext = createContext(null);
|
|
9
|
+
const normalizeUrl = (url) => {
|
|
10
|
+
try {
|
|
11
|
+
const parsed = new URL(url);
|
|
12
|
+
return parsed.href.replace(/\/$/, '');
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return url;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
export const MonacoProvider = ({ children, clientId, network, seiRpcUrl, walletClient, tokenLifecycle: tokenLifecycleConfig }) => {
|
|
19
|
+
const [sdk, setSdk] = useState(null);
|
|
20
|
+
const [error, setError] = useState(null);
|
|
21
|
+
// Track previous network config to detect actual changes vs initial mount
|
|
22
|
+
const prevNetworkRef = useRef(null);
|
|
23
|
+
// Global authentication state (shared across all components)
|
|
24
|
+
const [authenticationStatus, setAuthenticationStatus] = useState(AuthenticationStatus.UNAUTHENTICATED);
|
|
25
|
+
// Token lifecycle management
|
|
26
|
+
const tokenLifecycle = useTokenLifecycle(sdk, tokenLifecycleConfig);
|
|
27
|
+
// Ref to access tokenLifecycle.clearTokens without adding it to effect dependencies
|
|
28
|
+
// (tokenLifecycle depends on sdk, which would cause a circular dependency)
|
|
29
|
+
const tokenLifecycleRef = useRef(tokenLifecycle);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
tokenLifecycleRef.current = tokenLifecycle;
|
|
32
|
+
}, [tokenLifecycle]);
|
|
33
|
+
// Initialize SDK without wallet (for public APIs)
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const prevNetwork = prevNetworkRef.current;
|
|
36
|
+
const networkChanged = prevNetwork !== null && (prevNetwork.network !== network ||
|
|
37
|
+
normalizeUrl(prevNetwork.seiRpcUrl) !== normalizeUrl(seiRpcUrl));
|
|
38
|
+
// Only reset auth state when network actually changes, not on initial mount
|
|
39
|
+
if (networkChanged) {
|
|
40
|
+
setAuthenticationStatus(AuthenticationStatus.UNAUTHENTICATED);
|
|
41
|
+
tokenLifecycleRef.current.clearTokens(); // stop auto-refresh and drop in-memory secrets
|
|
42
|
+
clearAuthState();
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
setError(null);
|
|
46
|
+
const sdkConfig = {
|
|
47
|
+
network,
|
|
48
|
+
seiRpcUrl,
|
|
49
|
+
};
|
|
50
|
+
const newSdk = new MonacoSDK(sdkConfig);
|
|
51
|
+
setSdk(newSdk);
|
|
52
|
+
// Update ref only after successful SDK creation
|
|
53
|
+
prevNetworkRef.current = { network, seiRpcUrl };
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
const normalizedError = err instanceof Error ? err : new Error(String(err));
|
|
57
|
+
setSdk(null);
|
|
58
|
+
setError(normalizedError);
|
|
59
|
+
}
|
|
60
|
+
}, [network, seiRpcUrl]);
|
|
61
|
+
// Cleanup: disconnect WebSocket when SDK changes or unmounts
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
return () => {
|
|
64
|
+
sdk?.ws.disconnect();
|
|
65
|
+
};
|
|
66
|
+
}, [sdk]);
|
|
67
|
+
// Update wallet client when it changes or when SDK is recreated (e.g., network change)
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!sdk || !walletClient)
|
|
70
|
+
return;
|
|
71
|
+
try {
|
|
72
|
+
sdk.setWalletClient(walletClient);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
const normalizedError = err instanceof Error ? err : new Error(String(err));
|
|
76
|
+
setError(normalizedError);
|
|
77
|
+
}
|
|
78
|
+
}, [sdk, walletClient]);
|
|
79
|
+
// Restore cached tokens when SDK changes (including on mount)
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!sdk)
|
|
82
|
+
return;
|
|
83
|
+
const persistTokens = tokenLifecycleConfig?.persistTokens ?? true;
|
|
84
|
+
if (!persistTokens)
|
|
85
|
+
return;
|
|
86
|
+
const cachedAuthState = loadAuthState();
|
|
87
|
+
if (!cachedAuthState)
|
|
88
|
+
return;
|
|
89
|
+
// Check if the token is expired
|
|
90
|
+
if (tokenLifecycle.isTokenExpired(cachedAuthState)) {
|
|
91
|
+
// Set the auth state first so refreshAuth has a refresh token to use
|
|
92
|
+
sdk.setAuthState(cachedAuthState);
|
|
93
|
+
tokenLifecycle.initializeTokens(cachedAuthState);
|
|
94
|
+
// Attempt to refresh
|
|
95
|
+
tokenLifecycle.refreshTokens().then((newAuthState) => {
|
|
96
|
+
if (newAuthState) {
|
|
97
|
+
// Refresh succeeded, SDK auth state is already updated by refreshAuth
|
|
98
|
+
setAuthenticationStatus(AuthenticationStatus.AUTHENTICATED);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// Refresh failed, clear tokens and SDK state
|
|
102
|
+
tokenLifecycle.clearTokens();
|
|
103
|
+
sdk.logout().catch(() => { }); // Ignore errors, we're already in cleanup mode
|
|
104
|
+
setAuthenticationStatus(AuthenticationStatus.UNAUTHENTICATED);
|
|
105
|
+
}
|
|
106
|
+
}).catch((err) => {
|
|
107
|
+
const normalizedError = err instanceof Error ? err : new Error(String(err));
|
|
108
|
+
setError(normalizedError);
|
|
109
|
+
tokenLifecycle.clearTokens();
|
|
110
|
+
sdk.logout().catch(() => { }); // Ignore errors, we're already in cleanup mode
|
|
111
|
+
setAuthenticationStatus(AuthenticationStatus.UNAUTHENTICATED);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// Token is still valid, restore it
|
|
116
|
+
sdk.setAuthState(cachedAuthState);
|
|
117
|
+
tokenLifecycle.initializeTokens(cachedAuthState);
|
|
118
|
+
setAuthenticationStatus(AuthenticationStatus.AUTHENTICATED);
|
|
119
|
+
}
|
|
120
|
+
}, [sdk, tokenLifecycle, tokenLifecycleConfig?.persistTokens]);
|
|
121
|
+
const contextValue = useMemo(() => ({
|
|
122
|
+
sdk,
|
|
123
|
+
clientId,
|
|
124
|
+
isWalletConnected: !!walletClient,
|
|
125
|
+
error,
|
|
126
|
+
// Global authentication state
|
|
127
|
+
authenticationStatus,
|
|
128
|
+
// Token lifecycle management
|
|
129
|
+
tokenLifecycle,
|
|
130
|
+
// Global authentication state setters
|
|
131
|
+
setAuthenticationStatus,
|
|
132
|
+
setError,
|
|
133
|
+
}), [sdk, clientId, walletClient, error, authenticationStatus, tokenLifecycle]);
|
|
134
|
+
return (_jsx(MonacoContext.Provider, { value: contextValue, children: _jsx(TradeFeedProvider, { children: children }) }));
|
|
135
|
+
};
|
|
136
|
+
export const useMonacoContext = () => {
|
|
137
|
+
const context = useContext(MonacoContext);
|
|
138
|
+
if (!context) {
|
|
139
|
+
throw new Error("useMonacoContext must be used within a MonacoProvider. " + "Make sure to wrap your app with <MonacoProvider>.");
|
|
140
|
+
}
|
|
141
|
+
return context;
|
|
142
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { TradeEvent } from "@0xmonaco/types";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
export declare const MAX_TRADES = 50;
|
|
4
|
+
export interface TradeFeedSubscription {
|
|
5
|
+
trades: TradeEvent[];
|
|
6
|
+
error: Error | null;
|
|
7
|
+
subscribed: boolean;
|
|
8
|
+
subscriberCount: number;
|
|
9
|
+
fetchingInitialState: boolean;
|
|
10
|
+
}
|
|
11
|
+
interface TradeFeedContextValue {
|
|
12
|
+
/** Subscribe to a trading pair - returns unsubscribe function */
|
|
13
|
+
subscribe: (tradingPairId: string, fetchInitialTrades: () => Promise<TradeEvent[]>, subscribeToWs: (handler: (event: TradeEvent) => void) => () => void) => () => void;
|
|
14
|
+
/** Get subscription state with reactivity */
|
|
15
|
+
subscriptions: Map<string, TradeFeedSubscription>;
|
|
16
|
+
}
|
|
17
|
+
interface TradeFeedProviderProps {
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}
|
|
20
|
+
export declare const TradeFeedProvider: ({ children }: TradeFeedProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
export declare const useTradeFeedContext: () => TradeFeedContextValue;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
export const MAX_TRADES = 50;
|
|
4
|
+
const TradeFeedContext = createContext(null);
|
|
5
|
+
export const TradeFeedProvider = ({ children }) => {
|
|
6
|
+
const [subscriptions, setSubscriptions] = useState(new Map());
|
|
7
|
+
// Track active WebSocket unsubscribe functions
|
|
8
|
+
const wsUnsubscribes = useRef(new Map());
|
|
9
|
+
// Cleanup all WebSocket subscriptions when provider unmounts
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
return () => {
|
|
12
|
+
for (const unsubscribe of wsUnsubscribes.current.values()) {
|
|
13
|
+
unsubscribe();
|
|
14
|
+
}
|
|
15
|
+
wsUnsubscribes.current.clear();
|
|
16
|
+
};
|
|
17
|
+
}, []);
|
|
18
|
+
const handleTrade = useCallback((tradingPairId, event) => {
|
|
19
|
+
setSubscriptions((prev) => {
|
|
20
|
+
const current = prev.get(tradingPairId);
|
|
21
|
+
if (!current)
|
|
22
|
+
return prev;
|
|
23
|
+
const exists = current.trades.some((t) => t.data.tradeId === event.data.tradeId);
|
|
24
|
+
if (exists)
|
|
25
|
+
return prev;
|
|
26
|
+
const newSubscriptions = new Map(prev);
|
|
27
|
+
newSubscriptions.set(tradingPairId, {
|
|
28
|
+
...current,
|
|
29
|
+
trades: [event, ...current.trades].slice(0, MAX_TRADES),
|
|
30
|
+
error: null, // Clear error on successful WebSocket event
|
|
31
|
+
});
|
|
32
|
+
return newSubscriptions;
|
|
33
|
+
});
|
|
34
|
+
}, []);
|
|
35
|
+
// Use ref to maintain stable reference to handleTrade for the subscribe callback
|
|
36
|
+
const handleTradeRef = useRef(handleTrade);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
handleTradeRef.current = handleTrade;
|
|
39
|
+
});
|
|
40
|
+
const subscribe = useCallback((tradingPairId, fetchInitialTrades, subscribeToWs) => {
|
|
41
|
+
setSubscriptions((prev) => {
|
|
42
|
+
const current = prev.get(tradingPairId);
|
|
43
|
+
if (current) {
|
|
44
|
+
// Already subscribed, just increment count
|
|
45
|
+
const newSubscriptions = new Map(prev);
|
|
46
|
+
newSubscriptions.set(tradingPairId, {
|
|
47
|
+
...current,
|
|
48
|
+
subscriberCount: current.subscriberCount + 1,
|
|
49
|
+
});
|
|
50
|
+
return newSubscriptions;
|
|
51
|
+
}
|
|
52
|
+
// First subscriber - create new subscription
|
|
53
|
+
const newSubscriptions = new Map(prev);
|
|
54
|
+
newSubscriptions.set(tradingPairId, {
|
|
55
|
+
trades: [],
|
|
56
|
+
error: null,
|
|
57
|
+
subscribed: true,
|
|
58
|
+
subscriberCount: 1,
|
|
59
|
+
fetchingInitialState: true,
|
|
60
|
+
});
|
|
61
|
+
return newSubscriptions;
|
|
62
|
+
});
|
|
63
|
+
// Check if this is the first subscriber (no existing WS subscription)
|
|
64
|
+
if (!wsUnsubscribes.current.has(tradingPairId)) {
|
|
65
|
+
// Set placeholder immediately to prevent race condition with concurrent subscribers
|
|
66
|
+
wsUnsubscribes.current.set(tradingPairId, () => { });
|
|
67
|
+
// Fetch initial trades
|
|
68
|
+
fetchInitialTrades()
|
|
69
|
+
.then((initialTrades) => {
|
|
70
|
+
setSubscriptions((prev) => {
|
|
71
|
+
const current = prev.get(tradingPairId);
|
|
72
|
+
if (!current)
|
|
73
|
+
return prev;
|
|
74
|
+
const newSubscriptions = new Map(prev);
|
|
75
|
+
newSubscriptions.set(tradingPairId, {
|
|
76
|
+
...current,
|
|
77
|
+
trades: initialTrades.slice(0, MAX_TRADES),
|
|
78
|
+
error: null,
|
|
79
|
+
fetchingInitialState: false,
|
|
80
|
+
});
|
|
81
|
+
return newSubscriptions;
|
|
82
|
+
});
|
|
83
|
+
})
|
|
84
|
+
.catch((err) => {
|
|
85
|
+
setSubscriptions((prev) => {
|
|
86
|
+
const current = prev.get(tradingPairId);
|
|
87
|
+
if (!current)
|
|
88
|
+
return prev;
|
|
89
|
+
const newSubscriptions = new Map(prev);
|
|
90
|
+
newSubscriptions.set(tradingPairId, {
|
|
91
|
+
...current,
|
|
92
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
93
|
+
fetchingInitialState: false,
|
|
94
|
+
});
|
|
95
|
+
return newSubscriptions;
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
// Subscribe to WebSocket
|
|
99
|
+
const unsubscribe = subscribeToWs((event) => handleTradeRef.current(tradingPairId, event));
|
|
100
|
+
wsUnsubscribes.current.set(tradingPairId, unsubscribe);
|
|
101
|
+
}
|
|
102
|
+
// Return unsubscribe function
|
|
103
|
+
return () => {
|
|
104
|
+
setSubscriptions((prev) => {
|
|
105
|
+
const current = prev.get(tradingPairId);
|
|
106
|
+
if (!current)
|
|
107
|
+
return prev;
|
|
108
|
+
const newCount = current.subscriberCount - 1;
|
|
109
|
+
if (newCount <= 0) {
|
|
110
|
+
// Last subscriber - clean up
|
|
111
|
+
const wsUnsubscribe = wsUnsubscribes.current.get(tradingPairId);
|
|
112
|
+
if (wsUnsubscribe) {
|
|
113
|
+
wsUnsubscribe();
|
|
114
|
+
wsUnsubscribes.current.delete(tradingPairId);
|
|
115
|
+
}
|
|
116
|
+
const newSubscriptions = new Map(prev);
|
|
117
|
+
newSubscriptions.delete(tradingPairId);
|
|
118
|
+
return newSubscriptions;
|
|
119
|
+
}
|
|
120
|
+
// Still have subscribers, just decrement count
|
|
121
|
+
const newSubscriptions = new Map(prev);
|
|
122
|
+
newSubscriptions.set(tradingPairId, {
|
|
123
|
+
...current,
|
|
124
|
+
subscriberCount: newCount,
|
|
125
|
+
});
|
|
126
|
+
return newSubscriptions;
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
}, []);
|
|
130
|
+
const contextValue = useMemo(() => ({
|
|
131
|
+
subscribe,
|
|
132
|
+
subscriptions,
|
|
133
|
+
}), [subscribe, subscriptions]);
|
|
134
|
+
return _jsx(TradeFeedContext.Provider, { value: contextValue, children: children });
|
|
135
|
+
};
|
|
136
|
+
export const useTradeFeedContext = () => {
|
|
137
|
+
const context = useContext(TradeFeedContext);
|
|
138
|
+
if (!context) {
|
|
139
|
+
throw new Error("useTradeFeedContext must be used within a TradeFeedProvider. " +
|
|
140
|
+
"Make sure to wrap your app with <MonacoProvider>.");
|
|
141
|
+
}
|
|
142
|
+
return context;
|
|
143
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { MonacoSDK, Network } from "@0xmonaco/types";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { WalletClient } from "viem";
|
|
4
|
+
import type { TokenLifecycleConfig, UseTokenLifecycleReturn } from "../hooks";
|
|
5
|
+
export declare enum AuthenticationStatus {
|
|
6
|
+
/** User is not authenticated */
|
|
7
|
+
UNAUTHENTICATED = "unauthenticated",
|
|
8
|
+
/** Authentication is in progress */
|
|
9
|
+
AUTHENTICATING = "authenticating",
|
|
10
|
+
/** User is successfully authenticated */
|
|
11
|
+
AUTHENTICATED = "authenticated"
|
|
12
|
+
}
|
|
13
|
+
export interface MonacoContextValue {
|
|
14
|
+
/** Monaco SDK instance */
|
|
15
|
+
sdk: MonacoSDK | null;
|
|
16
|
+
/** Client ID for authentication */
|
|
17
|
+
clientId: string;
|
|
18
|
+
/** Whether a wallet client is connected */
|
|
19
|
+
isWalletConnected: boolean;
|
|
20
|
+
/** Any error that occurred (SDK initialization, authentication, etc.) */
|
|
21
|
+
error: Error | null;
|
|
22
|
+
/** Current authentication status */
|
|
23
|
+
authenticationStatus: AuthenticationStatus;
|
|
24
|
+
/** Token lifecycle manager */
|
|
25
|
+
tokenLifecycle: UseTokenLifecycleReturn;
|
|
26
|
+
/** Function to update authentication status */
|
|
27
|
+
setAuthenticationStatus: (status: AuthenticationStatus) => void;
|
|
28
|
+
/** Function to update error */
|
|
29
|
+
setError: (error: Error | null) => void;
|
|
30
|
+
}
|
|
31
|
+
export interface MonacoProviderProps {
|
|
32
|
+
/** Child components */
|
|
33
|
+
children: ReactNode;
|
|
34
|
+
/** Client ID for authentication */
|
|
35
|
+
clientId: string;
|
|
36
|
+
/**
|
|
37
|
+
* Network to use. Must be one of: "local", "development", "staging", "mainnet".
|
|
38
|
+
*/
|
|
39
|
+
network: Network;
|
|
40
|
+
/** RPC URL for Sei blockchain interactions */
|
|
41
|
+
seiRpcUrl: string;
|
|
42
|
+
/**
|
|
43
|
+
* Optional wallet client for authenticated operations.
|
|
44
|
+
* Can be obtained from wagmi, RainbowKit, ConnectKit, or any viem-compatible wallet provider.
|
|
45
|
+
*/
|
|
46
|
+
walletClient?: WalletClient;
|
|
47
|
+
/** Token lifecycle configuration options */
|
|
48
|
+
tokenLifecycle?: TokenLifecycleConfig;
|
|
49
|
+
}
|