@basedone/core 0.2.8 → 0.3.1

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.
@@ -286,6 +286,10 @@ export interface ValidateDiscountResponse {
286
286
  /** Discount amount */
287
287
  discountAmount: number;
288
288
  };
289
+ /** Merchant ID the discount belongs to */
290
+ merchantId?: string;
291
+ /** Merchant name the discount belongs to */
292
+ merchantName?: string | null;
289
293
  /** Subtotal */
290
294
  subtotal?: number;
291
295
  /** Total */
@@ -1325,6 +1329,30 @@ export interface CashAccountBalanceResponse {
1325
1329
  currency: string;
1326
1330
  }
1327
1331
 
1332
+ // ============================================================================
1333
+ // Customer Notification Responses
1334
+ // ============================================================================
1335
+
1336
+ export interface CustomerNotification {
1337
+ id: string;
1338
+ type: string;
1339
+ title: string;
1340
+ message: string;
1341
+ metadata: Record<string, any> | null;
1342
+ isRead: boolean;
1343
+ createdAt: string;
1344
+ }
1345
+
1346
+ export interface CustomerNotificationsResponse {
1347
+ notifications: CustomerNotification[];
1348
+ stats: { unread: number };
1349
+ pagination: { total: number; limit: number; offset: number; hasMore: boolean };
1350
+ }
1351
+
1352
+ export interface MarkNotificationsReadResponse {
1353
+ updated: number;
1354
+ }
1355
+
1328
1356
  export interface DeliveryAddressResponse {
1329
1357
  name: string;
1330
1358
  phoneNumber: string;
@@ -0,0 +1,197 @@
1
+ import { OrderStatus } from "../types/enums";
2
+
3
+ /** Detect pickup / on-site-collection shipping methods (normalised matching) */
4
+ export function isPickupOrder(
5
+ shippingMethod?: string | null,
6
+ shippingRateId?: string | null
7
+ ): boolean {
8
+ // Primary check: shippingRateId === "PICKUP" (from mobile app)
9
+ if (shippingRateId && shippingRateId.trim().toUpperCase() === "PICKUP") {
10
+ return true;
11
+ }
12
+
13
+ if (!shippingMethod) return false;
14
+ const normalized = shippingMethod
15
+ .trim()
16
+ .toLowerCase()
17
+ .replace(/[\s-]+/g, " ");
18
+ return (
19
+ normalized === "pickup" ||
20
+ normalized === "on site collection" ||
21
+ normalized === "onsite collection"
22
+ );
23
+ }
24
+
25
+ /**
26
+ * Canonical order status transitions.
27
+ *
28
+ * Buyer-protected escrow flow:
29
+ * CREATED → PAYMENT_RESERVED → MERCHANT_ACCEPTED → SHIPPED → DELIVERED → SETTLED
30
+ *
31
+ * Cancellation is allowed from any non-terminal state except DELIVERED (already in settlement).
32
+ */
33
+ export const ORDER_STATUS_TRANSITIONS: Partial<Record<OrderStatus, OrderStatus[]>> = {
34
+ [OrderStatus.CREATED]: [
35
+ OrderStatus.PAYMENT_RESERVED,
36
+ OrderStatus.MERCHANT_ACCEPTED,
37
+ OrderStatus.CANCELLED,
38
+ ],
39
+ [OrderStatus.PAYMENT_RESERVED]: [
40
+ OrderStatus.MERCHANT_ACCEPTED,
41
+ OrderStatus.CANCELLED,
42
+ ],
43
+ [OrderStatus.MERCHANT_ACCEPTED]: [
44
+ OrderStatus.SHIPPED,
45
+ OrderStatus.CANCELLED,
46
+ ],
47
+ // Backward compat for existing SETTLED orders created before escrow change
48
+ // Note: CANCELLED removed — settled orders have funds paid out, no clawback mechanism
49
+ [OrderStatus.SETTLED]: [OrderStatus.SHIPPED],
50
+ [OrderStatus.SHIPPED]: [OrderStatus.DELIVERED, OrderStatus.CANCELLED],
51
+ // Settlement triggered on delivery (buyer-protected escrow)
52
+ [OrderStatus.DELIVERED]: [OrderStatus.SETTLED],
53
+ // Terminal states
54
+ [OrderStatus.CANCELLED]: [],
55
+ };
56
+
57
+ /**
58
+ * Validate whether transitioning from `currentStatus` to `newStatus` is allowed.
59
+ *
60
+ * For pickup / on-site-collection orders, MERCHANT_ACCEPTED → DELIVERED and
61
+ * SETTLED → DELIVERED are permitted (skipping SHIPPED).
62
+ */
63
+ export function validateStatusTransition(
64
+ currentStatus: string,
65
+ newStatus: string,
66
+ options?: {
67
+ shippingMethod?: string | null;
68
+ shippingRateId?: string | null;
69
+ }
70
+ ): { valid: boolean; error?: string } {
71
+ // Pickup orders can skip SHIPPED and go directly to DELIVERED
72
+ if (
73
+ isPickupOrder(options?.shippingMethod, options?.shippingRateId) &&
74
+ (currentStatus === OrderStatus.MERCHANT_ACCEPTED ||
75
+ currentStatus === OrderStatus.SETTLED) &&
76
+ newStatus === OrderStatus.DELIVERED
77
+ ) {
78
+ return { valid: true };
79
+ }
80
+
81
+ const allowed =
82
+ ORDER_STATUS_TRANSITIONS[currentStatus as OrderStatus] ?? [];
83
+ if (!allowed.includes(newStatus as OrderStatus)) {
84
+ if (allowed.length === 0) {
85
+ return {
86
+ valid: false,
87
+ error: `Cannot change status from ${currentStatus} — this is a final state`,
88
+ };
89
+ }
90
+ return {
91
+ valid: false,
92
+ error: `Cannot transition from ${currentStatus} to ${newStatus}. Allowed: ${allowed.join(", ")}`,
93
+ };
94
+ }
95
+
96
+ return { valid: true };
97
+ }
98
+
99
+ /**
100
+ * Return the list of statuses reachable from `currentStatus`.
101
+ * For pickup orders, DELIVERED is added when on MERCHANT_ACCEPTED or SETTLED.
102
+ */
103
+ export function getNextStatuses(
104
+ currentStatus: string,
105
+ options?: {
106
+ shippingMethod?: string | null;
107
+ shippingRateId?: string | null;
108
+ }
109
+ ): string[] {
110
+ const base = [
111
+ ...(ORDER_STATUS_TRANSITIONS[currentStatus as OrderStatus] ?? []),
112
+ ];
113
+
114
+ if (
115
+ isPickupOrder(options?.shippingMethod, options?.shippingRateId) &&
116
+ (currentStatus === OrderStatus.MERCHANT_ACCEPTED ||
117
+ currentStatus === OrderStatus.SETTLED) &&
118
+ !base.includes(OrderStatus.DELIVERED)
119
+ ) {
120
+ base.push(OrderStatus.DELIVERED);
121
+ }
122
+
123
+ return base;
124
+ }
125
+
126
+ /** Human-readable label for a status. */
127
+ export function getStatusLabel(status: string): string {
128
+ const labels: Record<string, string> = {
129
+ CREATED: "Created",
130
+ PAYMENT_RESERVED: "Payment Reserved",
131
+ MERCHANT_ACCEPTED: "Accepted",
132
+ SETTLED: "Completed (Paid)",
133
+ SHIPPED: "Shipped",
134
+ DELIVERED: "Delivered",
135
+ CANCELLED: "Cancelled",
136
+ };
137
+ return labels[status] || status;
138
+ }
139
+
140
+ /** UI colour key for a status. */
141
+ export function getStatusColor(status: string): string {
142
+ const colors: Record<string, string> = {
143
+ CREATED: "gray",
144
+ PAYMENT_RESERVED: "blue",
145
+ MERCHANT_ACCEPTED: "purple",
146
+ SETTLED: "indigo",
147
+ SHIPPED: "yellow",
148
+ DELIVERED: "green",
149
+ CANCELLED: "red",
150
+ };
151
+ return colors[status] || "gray";
152
+ }
153
+
154
+ /** Whether the order can be cancelled from its current status. */
155
+ export function canCancelOrder(currentStatus: string): boolean {
156
+ return (
157
+ ORDER_STATUS_TRANSITIONS[currentStatus as OrderStatus]?.includes(
158
+ OrderStatus.CANCELLED
159
+ ) ?? false
160
+ );
161
+ }
162
+
163
+ /** Whether tracking info is required for a status change. */
164
+ export function requiresTrackingInfo(
165
+ newStatus: string,
166
+ options?: {
167
+ shippingMethod?: string | null;
168
+ shippingRateId?: string | null;
169
+ }
170
+ ): boolean {
171
+ if (newStatus !== OrderStatus.SHIPPED) return false;
172
+ return !isPickupOrder(options?.shippingMethod, options?.shippingRateId);
173
+ }
174
+
175
+ /** Whether a status change should trigger customer notification. */
176
+ export function shouldNotifyCustomer(newStatus: string): boolean {
177
+ return [
178
+ OrderStatus.MERCHANT_ACCEPTED,
179
+ OrderStatus.SHIPPED,
180
+ OrderStatus.DELIVERED,
181
+ OrderStatus.CANCELLED,
182
+ ].includes(newStatus as OrderStatus);
183
+ }
184
+
185
+ /** Status progression percentage (for progress bars). */
186
+ export function getStatusProgress(status: string): number {
187
+ const progressMap: Record<string, number> = {
188
+ CREATED: 10,
189
+ PAYMENT_RESERVED: 25,
190
+ MERCHANT_ACCEPTED: 40,
191
+ SETTLED: 50,
192
+ SHIPPED: 75,
193
+ DELIVERED: 100,
194
+ CANCELLED: 0,
195
+ };
196
+ return progressMap[status] || 0;
197
+ }
@@ -1,5 +1,14 @@
1
1
  import { InfoClient, PerpsAssetCtx, MarginTables, PerpDex as LegacyPerpDex } from "@nktkas/hyperliquid";
2
2
 
3
+ /** Payload entries from Hyperliquid `info` → `{ type: "perpConciseAnnotations" }`. */
4
+ export interface PerpConciseAnnotationMeta {
5
+ category?: string;
6
+ displayName?: string;
7
+ keywords?: string[];
8
+ }
9
+
10
+ export type PerpConciseAnnotations = [string, PerpConciseAnnotationMeta][];
11
+
3
12
  export interface PerpsMeta {
4
13
  collateralToken: number;
5
14
  /** Trading universes available for perpetual trading. */
@@ -32,6 +41,10 @@ export interface PerpsUniverse {
32
41
  growthMode?: "enabled";
33
42
  /** Margin mode for the universe. */
34
43
  marginMode?: "strictIsolated" | "noCross";
44
+ /** From `perpConciseAnnotations` when present (search/display). */
45
+ category?: string;
46
+ displayName?: string;
47
+ keywords?: string[];
35
48
  }
36
49
 
37
50
  export interface PerpDex extends LegacyPerpDex {
@@ -40,10 +53,60 @@ export interface PerpDex extends LegacyPerpDex {
40
53
 
41
54
  export type AllPerpsMeta = PerpsMeta[];
42
55
 
56
+ export async function getPerpConciseAnnotations(
57
+ infoClient: InfoClient,
58
+ ): Promise<PerpConciseAnnotations> {
59
+ return infoClient.transport.request<PerpConciseAnnotations>("info", {
60
+ type: "perpConciseAnnotations",
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Merges `perpConciseAnnotations` into each universe row by matching `name`.
66
+ */
67
+ export function enrichAllPerpsMetaWithAnnotations(
68
+ metas: AllPerpsMeta,
69
+ annotations: PerpConciseAnnotations,
70
+ ): AllPerpsMeta {
71
+ const map = new Map<string, PerpConciseAnnotationMeta>(annotations);
72
+
73
+ return metas.map((meta) => ({
74
+ ...meta,
75
+ universe: meta.universe.map((u) => {
76
+ const ann = map.get(u.name);
77
+ if (!ann) return u;
78
+
79
+ return {
80
+ ...u,
81
+ ...(ann.category != null && ann.category !== ""
82
+ ? { category: ann.category }
83
+ : {}),
84
+ ...(ann.displayName != null && ann.displayName !== ""
85
+ ? { displayName: ann.displayName }
86
+ : {}),
87
+ ...(ann.keywords != null && ann.keywords.length > 0
88
+ ? { keywords: ann.keywords }
89
+ : {}),
90
+ };
91
+ }),
92
+ }));
93
+ }
94
+
43
95
  export async function getAllPerpsMeta(
44
96
  infoClient: InfoClient,
45
97
  ): Promise<AllPerpsMeta> {
46
- return infoClient.transport.request<AllPerpsMeta>("info", {
47
- type: "allPerpMetas",
48
- });
98
+ const [metas, annotations] = await Promise.all([
99
+ infoClient.transport.request<AllPerpsMeta>("info", {
100
+ type: "allPerpMetas",
101
+ }),
102
+ getPerpConciseAnnotations(infoClient).catch((): PerpConciseAnnotations => {
103
+ return [];
104
+ }),
105
+ ]);
106
+
107
+ if (!annotations.length) {
108
+ return metas;
109
+ }
110
+
111
+ return enrichAllPerpsMetaWithAnnotations(metas, annotations);
49
112
  }
@@ -17,6 +17,10 @@ export interface PerpsInstrument extends BaseInstrument {
17
17
  collateralTokenSymbol: string;
18
18
  dex?: string; // HIP3 dex name (e.g., "xyz" for "xyz:MSTR")
19
19
  collateralTokenIndex?: number;
20
+ /** From merged `perpConciseAnnotations` (display/search). */
21
+ category?: string;
22
+ displayName?: string;
23
+ keywords?: string[];
20
24
  }
21
25
 
22
26
  // Spot-specific types (metadata only - no asset context data)
@@ -280,6 +284,9 @@ export class InstrumentClient {
280
284
  isDelisted: info.isDelisted,
281
285
  dex: AssetIdUtils.extractDexName(info.name),
282
286
  collateralTokenIndex: perpMeta.collateralToken,
287
+ category: info.category,
288
+ displayName: info.displayName,
289
+ keywords: info.keywords,
283
290
  };
284
291
 
285
292
  this.addInstrument(instrument);
package/lib/meta/types.ts CHANGED
@@ -33,4 +33,8 @@ export interface PerpsUniverse {
33
33
  growthMode?: "enabled";
34
34
  /** Margin mode for the universe. */
35
35
  marginMode?: "strictIsolated" | "noCross";
36
+ /** From Hyperliquid `perpConciseAnnotations` when merged into meta. */
37
+ category?: string;
38
+ displayName?: string;
39
+ keywords?: string[];
36
40
  }
package/lib/types.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { UserAbstractionMode } from "./abstraction";
2
+
3
+ export interface PerpDexState {
4
+ totalVaultEquity: number;
5
+ perpsAtOpenInterestCap?: Array<string>;
6
+ leadingVaults?: Array<LeadingVault>;
7
+ }
8
+
9
+
10
+ // Additional undocumented fields in WebData3 will be removed on a future update
11
+ export interface WsWebData3 {
12
+ userState: {
13
+ abstraction: UserAbstractionMode;
14
+ agentAddress: string | null;
15
+ agentValidUntil: number | null;
16
+ serverTime: number;
17
+ cumLedger: number;
18
+ isVault: boolean;
19
+ user: string;
20
+ optOutOfSpotDusting?: boolean;
21
+ dexAbstractionEnabled?: boolean;
22
+ };
23
+ perpDexStates: Array<PerpDexState>;
24
+ }
25
+
26
+ export interface LeadingVault {
27
+ address: string;
28
+ name: string;
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basedone/core",
3
- "version": "0.2.8",
3
+ "version": "0.3.1",
4
4
  "description": "Core utilities for Based One",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -63,4 +63,4 @@
63
63
  "optional": true
64
64
  }
65
65
  }
66
- }
66
+ }