@acta-markets/ts-sdk 0.0.20-beta → 0.0.22-beta

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/ws/auth.js CHANGED
@@ -6,6 +6,9 @@
6
6
  * - Client signs UTF-8 bytes of that text and responds:
7
7
  * `AuthChallenge { challenge, signature: base58(ed25519(utf8(challenge))), pubkey }`
8
8
  *
9
+ * The same `signMessage` primitive is reused for any UTF-8-bytes signing
10
+ * (e.g. `acta:redeem:v1:{pubkey}:{code}` for invite redemption).
11
+ *
9
12
  * Source of truth (server):
10
13
  * - rust-backend/rfq-server/src/server/ws.rs
11
14
  * - rust-backend/rfq-server/src/session/handler.rs
@@ -49,9 +52,9 @@ export class KeypairAuthProvider {
49
52
  async getPublicKey() {
50
53
  return this.address;
51
54
  }
52
- async signChallenge(challenge) {
53
- const challengeBytes = utf8ToBytes(challenge);
54
- const signatureBytes = await signBytes(this.privateKey, challengeBytes);
55
+ async signMessage(message) {
56
+ const messageBytes = utf8ToBytes(message);
57
+ const signatureBytes = await signBytes(this.privateKey, messageBytes);
55
58
  return bytesToBase58(signatureBytes);
56
59
  }
57
60
  }
@@ -72,10 +75,10 @@ export class WalletAuthProvider {
72
75
  throw new Error("Wallet not connected (missing public key)");
73
76
  return pk;
74
77
  }
75
- async signChallenge(challenge) {
76
- const challengeBytes = utf8ToBytes(challenge);
78
+ async signMessage(message) {
79
+ const messageBytes = utf8ToBytes(message);
77
80
  // Some wallet APIs expect a mutable `Uint8Array`; codecs return `ReadonlyUint8Array`.
78
- const sig = await this.wallet.signMessage(new Uint8Array(challengeBytes));
81
+ const sig = await this.wallet.signMessage(new Uint8Array(messageBytes));
79
82
  return bytesToBase58(sig);
80
83
  }
81
84
  }
@@ -84,15 +87,15 @@ export class WalletAuthProvider {
84
87
  */
85
88
  export class CustomAuthProvider {
86
89
  getPublicKeyFn;
87
- signChallengeFn;
88
- constructor(getPublicKey, signChallenge) {
90
+ signMessageFn;
91
+ constructor(getPublicKey, signMessage) {
89
92
  this.getPublicKeyFn = getPublicKey;
90
- this.signChallengeFn = signChallenge;
93
+ this.signMessageFn = signMessage;
91
94
  }
92
95
  async getPublicKey() {
93
96
  return this.getPublicKeyFn();
94
97
  }
95
- async signChallenge(challenge) {
96
- return this.signChallengeFn(challenge);
98
+ async signMessage(message) {
99
+ return this.signMessageFn(message);
97
100
  }
98
101
  }
@@ -2,7 +2,7 @@
2
2
  import type { AuthProvider } from "./auth";
3
3
  import type { SignerLike } from "../chain/orders";
4
4
  import type { Address } from "@solana/addresses";
5
- import type { ActiveRfqInfo, ChainEventMessage, EarnSummaryData, TokenMarketsInfoData, GlobalStats, MarketDescriptorInfo, MarketInfo, MyActiveRfqInfo, MyActiveRfqsMessage, OrderStatusMessage, PositionInfo, QuoteAcknowledgedMessage, QuoteBestStatusMessage, QuoteCancelledMessage, QuoteMessage, QuoteRefreshRequestedMessage, QuoteOutbidMessage, QuoteReceivedMessage, QuoteSelectedMessage, QuotesUpdateMessage, RfqBroadcastMessage, RfqClosedMessage, RfqCreatedMessage, RfqSkippedMessage, RfqRequestMessage, RfqAvailableAgainMessage, QuoteExpiredMessage, QuoteFilledMessage, IndicativePricesMessage, IndicativePricesRequestMessage, IndicativePricesResponseMessage, GetIndicativePricesMessage, RequestId, ServerError, ServerMessage, SnapshotMessage, StatsDelta, SubscriptionsMessage, TokenInfo, TradeInfo, UuidString, VersionMismatchMessage, WelcomeMessage, WsChannel } from "./types";
5
+ import type { ActiveRfqInfo, ChainEventMessage, EarnSummaryData, TokenMarketsInfoData, GlobalStats, MarketDescriptorInfo, MarketInfo, MyActiveRfqInfo, MyActiveRfqsMessage, OrderStatusMessage, PositionInfo, QuoteAcknowledgedMessage, QuoteBestStatusMessage, QuoteCancelledMessage, QuoteMessage, QuoteRefreshRequestedMessage, QuoteOutbidMessage, QuoteReceivedMessage, QuoteSelectedMessage, QuotesUpdateMessage, RfqBroadcastMessage, RfqClosedMessage, RfqCreatedMessage, RfqSkippedMessage, RfqRequestMessage, RfqAvailableAgainMessage, QuoteExpiredMessage, QuoteFilledMessage, IndicativePricesMessage, IndicativePricesRequestMessage, IndicativePricesResponseMessage, GetIndicativePricesMessage, MakerBalancesMessage, MakerMarketsMessage, MakerPositionsMessage, MyCapsMessage, MyQuotesMessage, MyTradesMessage, RequestId, ServerError, ServerMessage, SnapshotMessage, StatsDelta, SubscriptionsMessage, TokenInfo, TradeInfo, UuidString, VersionMismatchMessage, WelcomeMessage, WsChannel, InviteRedeemedData, ReferralCodeClaimedData, MyReferralInfoData } from "./types";
6
6
  export type ConnectionState = "disconnected" | "connecting" | "authenticating" | "authenticated" | "error";
7
7
  export type ClientRole = "taker" | "maker";
8
8
  export type PendingMessagesOverflowPolicy = "drop_oldest" | "drop_newest" | "throw";
@@ -88,6 +88,12 @@ export type ActaWsClientEvents = {
88
88
  earnSummary: (data: EarnSummaryData) => void;
89
89
  tokenMarketsInfo: (data: TokenMarketsInfoData) => void;
90
90
  subscriptions: (msg: SubscriptionsMessage) => void;
91
+ subscriptionUpdated: (data: {
92
+ request_id: RequestId;
93
+ channels: WsChannel[];
94
+ underlying_mints?: string[];
95
+ quote_mints?: string[];
96
+ }) => void;
91
97
  activeRfqs: (rfqs: ActiveRfqInfo[]) => void;
92
98
  rfqBroadcast: (rfq: RfqBroadcastMessage) => void;
93
99
  rfqSkipped: (msg: RfqSkippedMessage) => void;
@@ -106,6 +112,12 @@ export type ActaWsClientEvents = {
106
112
  quoteExpired: (msg: QuoteExpiredMessage) => void;
107
113
  indicativePrices: (msg: IndicativePricesMessage) => void;
108
114
  indicativePricesRequest: (msg: IndicativePricesRequestMessage) => void;
115
+ makerBalances: (msg: MakerBalancesMessage) => void;
116
+ makerPositions: (msg: MakerPositionsMessage) => void;
117
+ myTrades: (msg: MyTradesMessage) => void;
118
+ myCaps: (msg: MyCapsMessage) => void;
119
+ makerMarkets: (msg: MakerMarketsMessage) => void;
120
+ myQuotes: (msg: MyQuotesMessage) => void;
109
121
  myActiveRfqs: (msg: MyActiveRfqsMessage) => void;
110
122
  orderStatus: (msg: OrderStatusMessage) => void;
111
123
  orderAccepted: (orderId: string) => void;
@@ -133,6 +145,10 @@ export type ActaWsClientEvents = {
133
145
  positionSettled: (event: Extract<ChainEventMessage, {
134
146
  event_type: "PositionSettled";
135
147
  }>) => void;
148
+ requireInvite: () => void;
149
+ inviteRedeemed: (data: InviteRedeemedData) => void;
150
+ referralCodeClaimed: (data: ReferralCodeClaimedData) => void;
151
+ myReferralInfo: (data: MyReferralInfoData) => void;
136
152
  };
137
153
  type EventMap = Record<string, (...args: any[]) => void>;
138
154
  declare class TypedEventEmitter<TEvents extends EventMap> {
@@ -176,8 +192,8 @@ export declare class ActaWsClient extends TypedEventEmitter<ActaWsClientEvents>
176
192
  private pingTimer;
177
193
  private shouldReconnect;
178
194
  private subscribedChannels;
179
- private subscribedMarkets;
180
- private hasMarketScope;
195
+ private underlyingMintScope;
196
+ private quoteMintScope;
181
197
  private marketDescriptorsByMarket;
182
198
  readonly state: ClientState;
183
199
  constructor(options: ActaWsClientOptions);
@@ -233,7 +249,50 @@ export declare class ActaWsClient extends TypedEventEmitter<ActaWsClientEvents>
233
249
  }): RequestId;
234
250
  getEarnSummary(): RequestId;
235
251
  getTokenMarketsInfo(underlyingMint: string): RequestId;
252
+ /** Maker-only: get balances per deposited token (total, locked, available). */
253
+ getMakerBalances(): RequestId;
254
+ /** Maker-only: get open positions with optional filters. */
255
+ getMakerPositions(args?: {
256
+ market?: string;
257
+ underlying_mint?: string;
258
+ status?: string[];
259
+ min_expiry_ts?: number;
260
+ }): RequestId;
261
+ /** Maker-only: get trade history with keyset pagination. */
262
+ getMyTrades(args?: {
263
+ limit?: number;
264
+ cursor?: number;
265
+ cursor_id?: string;
266
+ market?: string;
267
+ }): RequestId;
268
+ /** Maker-only: get position, notional, and balance caps. */
269
+ getMyCaps(): RequestId;
270
+ /** Maker-only: get markets where maker has deposits, with optional filters and stats. */
271
+ getMarketsForMaker(args?: {
272
+ underlying_mints?: string[];
273
+ quote_mints?: string[];
274
+ min_expiry_ts?: number;
275
+ max_expiry_ts?: number;
276
+ is_put?: boolean;
277
+ include_stats?: boolean;
278
+ }): RequestId;
279
+ /** Maker-only: get submitted quotes with status. */
280
+ getMyQuotes(args?: {
281
+ active_only?: boolean;
282
+ }): RequestId;
236
283
  logout(): void;
284
+ /**
285
+ * Redeem an invite code. Authentication is proven by the session;
286
+ * no additional signature is required. Validation runs client-side —
287
+ * invalid inputs throw `ReferralCodeError` without a round-trip.
288
+ */
289
+ redeemInvite(rawCode: string): RequestId;
290
+ /**
291
+ * Claim a vanity referral code (one-shot per taker). Validation
292
+ * runs client-side — invalid inputs throw `ReferralCodeError`.
293
+ */
294
+ claimReferralCode(rawCode: string): Promise<RequestId>;
295
+ getMyReferralInfo(): RequestId;
237
296
  getOrderStatus(orderIdHex: string): RequestId;
238
297
  cancelRfq(rfqId: string): RequestId;
239
298
  submitQuote(quote: QuoteMessage): void;
@@ -253,8 +312,21 @@ export declare class ActaWsClient extends TypedEventEmitter<ActaWsClientEvents>
253
312
  makerSigner: SignerLike;
254
313
  }): Promise<void>;
255
314
  cancelQuote(rfqId: string): RequestId;
256
- subscribe(channels: WsChannel[], markets?: string[]): RequestId;
315
+ subscribe(channels: WsChannel[], opts?: {
316
+ underlying_mints?: string[];
317
+ quote_mints?: string[];
318
+ }): RequestId;
257
319
  unsubscribe(channels: WsChannel[]): RequestId;
320
+ addMints(opts: {
321
+ underlying_mints?: string[];
322
+ quote_mints?: string[];
323
+ }): RequestId;
324
+ removeMints(opts: {
325
+ underlying_mints?: string[];
326
+ quote_mints?: string[];
327
+ }): RequestId;
328
+ addChannels(channels: WsChannel[]): RequestId;
329
+ removeChannels(channels: WsChannel[]): RequestId;
258
330
  ping(): void;
259
331
  resumeAuth(sessionId: string): void;
260
332
  private doConnect;
package/dist/ws/client.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /** Acta WebSocket client (rfq-server). */
2
2
  import { buildSignedQuoteMessage, buildAcceptQuoteMessage } from "./flows";
3
3
  import { assertWsU64Safe, validateQuantityBySizeRule } from "./wirePolicy";
4
+ import { parseReferralCode, ReferralCodeError } from "./referral";
4
5
  function toGenericServerError(err) {
5
6
  const message = err instanceof Error ? err.message : String(err);
6
7
  return { type: "generic", data: { code: "client_error", message } };
@@ -107,8 +108,8 @@ export class ActaWsClient extends TypedEventEmitter {
107
108
  pingTimer = null;
108
109
  shouldReconnect = true;
109
110
  subscribedChannels = new Set(["rfqs"]);
110
- subscribedMarkets = new Set();
111
- hasMarketScope = false;
111
+ underlyingMintScope = null; // null = all
112
+ quoteMintScope = null; // null = all
112
113
  marketDescriptorsByMarket = new Map();
113
114
  state = {
114
115
  stats: null,
@@ -336,9 +337,118 @@ export class ActaWsClient extends TypedEventEmitter {
336
337
  });
337
338
  return requestId;
338
339
  }
340
+ /** Maker-only: get balances per deposited token (total, locked, available). */
341
+ getMakerBalances() {
342
+ this.ensureAuthenticated();
343
+ const requestId = this.nextRequestId();
344
+ this.send({
345
+ type: "GetMakerBalances",
346
+ data: { request_id: requestId },
347
+ });
348
+ return requestId;
349
+ }
350
+ /** Maker-only: get open positions with optional filters. */
351
+ getMakerPositions(args) {
352
+ this.ensureAuthenticated();
353
+ const requestId = this.nextRequestId();
354
+ const data = {
355
+ request_id: requestId,
356
+ ...args,
357
+ };
358
+ this.send({ type: "GetMakerPositions", data });
359
+ return requestId;
360
+ }
361
+ /** Maker-only: get trade history with keyset pagination. */
362
+ getMyTrades(args) {
363
+ this.ensureAuthenticated();
364
+ const requestId = this.nextRequestId();
365
+ const data = {
366
+ request_id: requestId,
367
+ ...args,
368
+ };
369
+ this.send({ type: "GetMyTrades", data });
370
+ return requestId;
371
+ }
372
+ /** Maker-only: get position, notional, and balance caps. */
373
+ getMyCaps() {
374
+ this.ensureAuthenticated();
375
+ const requestId = this.nextRequestId();
376
+ this.send({
377
+ type: "GetMyCaps",
378
+ data: { request_id: requestId },
379
+ });
380
+ return requestId;
381
+ }
382
+ /** Maker-only: get markets where maker has deposits, with optional filters and stats. */
383
+ getMarketsForMaker(args) {
384
+ this.ensureAuthenticated();
385
+ const requestId = this.nextRequestId();
386
+ const data = {
387
+ request_id: requestId,
388
+ ...args,
389
+ };
390
+ this.send({ type: "GetMarketsForMaker", data });
391
+ return requestId;
392
+ }
393
+ /** Maker-only: get submitted quotes with status. */
394
+ getMyQuotes(args) {
395
+ this.ensureAuthenticated();
396
+ const requestId = this.nextRequestId();
397
+ const data = {
398
+ request_id: requestId,
399
+ active_only: args?.active_only ?? true,
400
+ };
401
+ this.send({ type: "GetMyQuotes", data });
402
+ return requestId;
403
+ }
339
404
  logout() {
340
405
  this.send({ type: "Logout" });
341
406
  }
407
+ // ========================================================================
408
+ // Referral / invite
409
+ // ========================================================================
410
+ /**
411
+ * Redeem an invite code. Authentication is proven by the session;
412
+ * no additional signature is required. Validation runs client-side —
413
+ * invalid inputs throw `ReferralCodeError` without a round-trip.
414
+ */
415
+ redeemInvite(rawCode) {
416
+ this.ensureAuthenticated();
417
+ const parsed = parseReferralCode(rawCode);
418
+ if (!parsed.ok)
419
+ throw new ReferralCodeError(parsed.error);
420
+ const requestId = this.nextRequestId();
421
+ this.send({
422
+ type: "RedeemInvite",
423
+ data: { request_id: requestId, code: parsed.code },
424
+ });
425
+ return requestId;
426
+ }
427
+ /**
428
+ * Claim a vanity referral code (one-shot per taker). Validation
429
+ * runs client-side — invalid inputs throw `ReferralCodeError`.
430
+ */
431
+ async claimReferralCode(rawCode) {
432
+ this.ensureAuthenticated();
433
+ const parsed = parseReferralCode(rawCode);
434
+ if (!parsed.ok)
435
+ throw new ReferralCodeError(parsed.error);
436
+ const requestId = this.nextRequestId();
437
+ this.send({
438
+ type: "ClaimReferralCode",
439
+ data: { request_id: requestId, code: parsed.code },
440
+ });
441
+ return requestId;
442
+ }
443
+ getMyReferralInfo() {
444
+ this.ensureAuthenticated();
445
+ const requestId = this.nextRequestId();
446
+ this.send({
447
+ type: "GetMyReferralInfo",
448
+ data: { request_id: requestId },
449
+ });
450
+ return requestId;
451
+ }
342
452
  getOrderStatus(orderIdHex) {
343
453
  this.ensureAuthenticated();
344
454
  const requestId = this.nextRequestId();
@@ -392,21 +502,25 @@ export class ActaWsClient extends TypedEventEmitter {
392
502
  });
393
503
  return requestId;
394
504
  }
395
- subscribe(channels, markets) {
505
+ subscribe(channels, opts) {
396
506
  this.ensureAuthenticated();
397
507
  const request_id = this.nextRequestId();
398
508
  for (const c of channels)
399
509
  this.subscribedChannels.add(c);
400
- if (markets) {
401
- this.hasMarketScope = true;
402
- this.subscribedMarkets = new Set(markets);
510
+ if (opts?.underlying_mints) {
511
+ this.underlyingMintScope = new Set(opts.underlying_mints);
403
512
  }
404
- this.send({
405
- type: "Subscribe",
406
- data: this.hasMarketScope
407
- ? { request_id, channels, markets: Array.from(this.subscribedMarkets) }
408
- : { request_id, channels },
409
- });
513
+ if (opts?.quote_mints) {
514
+ this.quoteMintScope = new Set(opts.quote_mints);
515
+ }
516
+ const data = { request_id, channels };
517
+ if (this.underlyingMintScope) {
518
+ data.underlying_mints = Array.from(this.underlyingMintScope);
519
+ }
520
+ if (this.quoteMintScope) {
521
+ data.quote_mints = Array.from(this.quoteMintScope);
522
+ }
523
+ this.send({ type: "Subscribe", data });
410
524
  return request_id;
411
525
  }
412
526
  unsubscribe(channels) {
@@ -420,6 +534,58 @@ export class ActaWsClient extends TypedEventEmitter {
420
534
  });
421
535
  return request_id;
422
536
  }
537
+ addMints(opts) {
538
+ this.ensureAuthenticated();
539
+ const request_id = this.nextRequestId();
540
+ if (opts.underlying_mints) {
541
+ if (!this.underlyingMintScope)
542
+ this.underlyingMintScope = new Set();
543
+ for (const m of opts.underlying_mints)
544
+ this.underlyingMintScope.add(m);
545
+ }
546
+ if (opts.quote_mints) {
547
+ if (!this.quoteMintScope)
548
+ this.quoteMintScope = new Set();
549
+ for (const m of opts.quote_mints)
550
+ this.quoteMintScope.add(m);
551
+ }
552
+ this.send({ type: "AddMints", data: { request_id, ...opts } });
553
+ return request_id;
554
+ }
555
+ removeMints(opts) {
556
+ this.ensureAuthenticated();
557
+ const request_id = this.nextRequestId();
558
+ if (opts.underlying_mints && this.underlyingMintScope) {
559
+ for (const m of opts.underlying_mints)
560
+ this.underlyingMintScope.delete(m);
561
+ if (this.underlyingMintScope.size === 0)
562
+ this.underlyingMintScope = null;
563
+ }
564
+ if (opts.quote_mints && this.quoteMintScope) {
565
+ for (const m of opts.quote_mints)
566
+ this.quoteMintScope.delete(m);
567
+ if (this.quoteMintScope.size === 0)
568
+ this.quoteMintScope = null;
569
+ }
570
+ this.send({ type: "RemoveMints", data: { request_id, ...opts } });
571
+ return request_id;
572
+ }
573
+ addChannels(channels) {
574
+ this.ensureAuthenticated();
575
+ const request_id = this.nextRequestId();
576
+ for (const c of channels)
577
+ this.subscribedChannels.add(c);
578
+ this.send({ type: "AddChannels", data: { request_id, channels } });
579
+ return request_id;
580
+ }
581
+ removeChannels(channels) {
582
+ this.ensureAuthenticated();
583
+ const request_id = this.nextRequestId();
584
+ for (const c of channels)
585
+ this.subscribedChannels.delete(c);
586
+ this.send({ type: "RemoveChannels", data: { request_id, channels } });
587
+ return request_id;
588
+ }
423
589
  ping() {
424
590
  if (this.ws?.readyState === WS_OPEN) {
425
591
  this.send({ type: "Ping" });
@@ -570,6 +736,21 @@ export class ActaWsClient extends TypedEventEmitter {
570
736
  case "MyCaps":
571
737
  this.emit("myCaps", message.data);
572
738
  break;
739
+ case "MakerBalances":
740
+ this.emit("makerBalances", message.data);
741
+ break;
742
+ case "MakerPositions":
743
+ this.emit("makerPositions", message.data);
744
+ break;
745
+ case "MyTrades":
746
+ this.emit("myTrades", message.data);
747
+ break;
748
+ case "MakerMarkets":
749
+ this.emit("makerMarkets", message.data);
750
+ break;
751
+ case "MyQuotes":
752
+ this.emit("myQuotes", message.data);
753
+ break;
573
754
  case "MyActiveRfqs":
574
755
  this.handleMyActiveRfqs(message.data);
575
756
  break;
@@ -727,6 +908,30 @@ export class ActaWsClient extends TypedEventEmitter {
727
908
  case "UnsubscribeAck":
728
909
  this.emit("unsubscribeAck", message.data);
729
910
  break;
911
+ case "SubscriptionUpdated":
912
+ {
913
+ const d = message.data;
914
+ // Sync local state from the server's authoritative response.
915
+ this.subscribedChannels = new Set(d.channels);
916
+ this.underlyingMintScope =
917
+ d.underlying_mints != null ? new Set(d.underlying_mints) : null;
918
+ this.quoteMintScope =
919
+ d.quote_mints != null ? new Set(d.quote_mints) : null;
920
+ this.emit("subscriptionUpdated", d);
921
+ }
922
+ break;
923
+ case "RequireInvite":
924
+ this.emit("requireInvite");
925
+ break;
926
+ case "InviteRedeemed":
927
+ this.emit("inviteRedeemed", message.data);
928
+ break;
929
+ case "ReferralCodeClaimed":
930
+ this.emit("referralCodeClaimed", message.data);
931
+ break;
932
+ case "MyReferralInfo":
933
+ this.emit("myReferralInfo", message.data);
934
+ break;
730
935
  }
731
936
  }
732
937
  /** Taker-only: request current indicative prices for a market + position_type. */
@@ -752,7 +957,7 @@ export class ActaWsClient extends TypedEventEmitter {
752
957
  try {
753
958
  const [pubkey, signature] = await Promise.all([
754
959
  this.authProvider.getPublicKey(),
755
- this.authProvider.signChallenge(challenge),
960
+ this.authProvider.signMessage(challenge),
756
961
  ]);
757
962
  this.send({
758
963
  type: "AuthChallenge",
@@ -812,9 +1017,13 @@ export class ActaWsClient extends TypedEventEmitter {
812
1017
  if (this.subscribedChannels.size > 0) {
813
1018
  const channels = Array.from(this.subscribedChannels);
814
1019
  const request_id = this.nextRequestId();
815
- const data = this.hasMarketScope
816
- ? { request_id, channels, markets: Array.from(this.subscribedMarkets) }
817
- : { request_id, channels };
1020
+ const data = { request_id, channels };
1021
+ if (this.underlyingMintScope) {
1022
+ data.underlying_mints = Array.from(this.underlyingMintScope);
1023
+ }
1024
+ if (this.quoteMintScope) {
1025
+ data.quote_mints = Array.from(this.quoteMintScope);
1026
+ }
818
1027
  this.send({ type: "Subscribe", data });
819
1028
  }
820
1029
  }
@@ -51,7 +51,7 @@ function parseClientMessage(payload) {
51
51
  function makeAuthProvider(pubkey = "pubkey", signature = "signature") {
52
52
  return {
53
53
  getPublicKey: jest.fn().mockResolvedValue(pubkey),
54
- signChallenge: jest.fn().mockResolvedValue(signature),
54
+ signMessage: jest.fn().mockResolvedValue(signature),
55
55
  };
56
56
  }
57
57
  function makeHarness(overrides = {}) {
@@ -6,3 +6,4 @@ export * from "./sponsoredTx";
6
6
  export * from "./apy";
7
7
  export * from "./discovery";
8
8
  export * from "./sizing";
9
+ export * from "./referral";
package/dist/ws/index.js CHANGED
@@ -6,3 +6,4 @@ export * from "./sponsoredTx";
6
6
  export * from "./apy";
7
7
  export * from "./discovery";
8
8
  export * from "./sizing";
9
+ export * from "./referral";
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Referral / invite redemption helpers.
3
+ *
4
+ * Mirrors the server-side validation defined in
5
+ * rust-backend/acta-types/src/invite.rs (ReferralCode::parse).
6
+ * Client-side validation must stay bit-identical to the server on
7
+ * the same input. A hardcoded fixture table is kept here and on the
8
+ * rust side; if either drifts, the cross-impl test will catch it.
9
+ */
10
+ export declare const REFERRAL_CODE_MIN_LEN = 4;
11
+ export declare const REFERRAL_CODE_MAX_LEN = 16;
12
+ declare const brand: unique symbol;
13
+ /**
14
+ * A normalized, validated referral code. The only way to obtain one
15
+ * is through `parseReferralCode`, which enforces trim + ASCII
16
+ * uppercase and length/charset bounds identical to the server's
17
+ * `ReferralCode::parse`.
18
+ */
19
+ export type ReferralCode = string & {
20
+ readonly [brand]: "ReferralCode";
21
+ };
22
+ export type ReferralCodeFormatError = {
23
+ kind: "length";
24
+ min: number;
25
+ max: number;
26
+ } | {
27
+ kind: "charset";
28
+ };
29
+ export declare class ReferralCodeError extends Error {
30
+ readonly detail: ReferralCodeFormatError;
31
+ constructor(detail: ReferralCodeFormatError);
32
+ }
33
+ /**
34
+ * Trim + ASCII uppercase. No length or charset check. Useful for
35
+ * showing a live preview as the user types.
36
+ */
37
+ export declare function normalizeReferralCode(input: string): string;
38
+ /**
39
+ * Parse + validate + normalize a user-supplied referral code.
40
+ * Mirrors `ReferralCode::parse` on the server. A successful result
41
+ * carries the canonical branded `ReferralCode`.
42
+ */
43
+ export declare function parseReferralCode(input: string): {
44
+ ok: true;
45
+ code: ReferralCode;
46
+ } | {
47
+ ok: false;
48
+ error: ReferralCodeFormatError;
49
+ };
50
+ export type InviteErrorReason = "invalid_code" | "code_exhausted" | "code_expired" | "code_owner_inactive" | "code_owner_blacklisted" | "already_registered" | "internal_error";
51
+ export type ClaimErrorReason = "not_registered" | "invalid_format" | "code_taken" | "reserved" | "internal_error";
52
+ export type TakerStatus = "pending" | "active";
53
+ export {};
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Referral / invite redemption helpers.
3
+ *
4
+ * Mirrors the server-side validation defined in
5
+ * rust-backend/acta-types/src/invite.rs (ReferralCode::parse).
6
+ * Client-side validation must stay bit-identical to the server on
7
+ * the same input. A hardcoded fixture table is kept here and on the
8
+ * rust side; if either drifts, the cross-impl test will catch it.
9
+ */
10
+ export const REFERRAL_CODE_MIN_LEN = 4;
11
+ export const REFERRAL_CODE_MAX_LEN = 16;
12
+ export class ReferralCodeError extends Error {
13
+ detail;
14
+ constructor(detail) {
15
+ super(detail.kind === "length"
16
+ ? `code length must be between ${detail.min} and ${detail.max}`
17
+ : "code must contain only ASCII letters and digits");
18
+ this.detail = detail;
19
+ this.name = "ReferralCodeError";
20
+ }
21
+ }
22
+ /**
23
+ * Trim + ASCII uppercase. No length or charset check. Useful for
24
+ * showing a live preview as the user types.
25
+ */
26
+ export function normalizeReferralCode(input) {
27
+ return input.trim().toUpperCase();
28
+ }
29
+ /**
30
+ * Parse + validate + normalize a user-supplied referral code.
31
+ * Mirrors `ReferralCode::parse` on the server. A successful result
32
+ * carries the canonical branded `ReferralCode`.
33
+ */
34
+ export function parseReferralCode(input) {
35
+ const normalized = normalizeReferralCode(input);
36
+ if (normalized.length < REFERRAL_CODE_MIN_LEN ||
37
+ normalized.length > REFERRAL_CODE_MAX_LEN) {
38
+ return {
39
+ ok: false,
40
+ error: {
41
+ kind: "length",
42
+ min: REFERRAL_CODE_MIN_LEN,
43
+ max: REFERRAL_CODE_MAX_LEN,
44
+ },
45
+ };
46
+ }
47
+ if (!/^[A-Z0-9]+$/.test(normalized)) {
48
+ return { ok: false, error: { kind: "charset" } };
49
+ }
50
+ return { ok: true, code: normalized };
51
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
1
+ import { normalizeReferralCode, parseReferralCode } from "./referral";
2
+ // =============================================================================
3
+ // Cross-impl fixture.
4
+ //
5
+ // Bit-for-bit identical to the fixture in
6
+ // rust-backend/acta-types/src/invite.rs::tests
7
+ // If either side changes, update both.
8
+ // =============================================================================
9
+ const ACCEPT_FIXTURES = [
10
+ ["NIKITA", "NIKITA"],
11
+ ["nikita", "NIKITA"],
12
+ [" abc1 ", "ABC1"],
13
+ ["ABCD", "ABCD"],
14
+ ["1234567890ABCDEF", "1234567890ABCDEF"],
15
+ ];
16
+ describe("parseReferralCode", () => {
17
+ test.each(ACCEPT_FIXTURES)("accepts %j → %j", (input, expected) => {
18
+ const res = parseReferralCode(input);
19
+ expect(res.ok).toBe(true);
20
+ if (res.ok)
21
+ expect(res.code).toBe(expected);
22
+ });
23
+ test("rejects too short", () => {
24
+ const res = parseReferralCode("abc");
25
+ expect(res.ok).toBe(false);
26
+ if (!res.ok)
27
+ expect(res.error.kind).toBe("length");
28
+ });
29
+ test("rejects too long", () => {
30
+ const res = parseReferralCode("A".repeat(17));
31
+ expect(res.ok).toBe(false);
32
+ if (!res.ok)
33
+ expect(res.error.kind).toBe("length");
34
+ });
35
+ test("rejects non-ascii", () => {
36
+ const res = parseReferralCode("абвгд");
37
+ expect(res.ok).toBe(false);
38
+ // Cyrillic passes length but fails charset.
39
+ if (!res.ok)
40
+ expect(res.error.kind).toBe("charset");
41
+ });
42
+ test("rejects punctuation", () => {
43
+ const res = parseReferralCode("ABC-12");
44
+ expect(res.ok).toBe(false);
45
+ if (!res.ok)
46
+ expect(res.error.kind).toBe("charset");
47
+ });
48
+ });
49
+ describe("normalizeReferralCode", () => {
50
+ test("trims and uppercases", () => {
51
+ expect(normalizeReferralCode(" nikita ")).toBe("NIKITA");
52
+ });
53
+ });