@acta-markets/ts-sdk 0.0.21-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.
@@ -7,6 +7,9 @@
7
7
  * - Client signs UTF-8 bytes of that text and responds:
8
8
  * `AuthChallenge { challenge, signature: base58(ed25519(utf8(challenge))), pubkey }`
9
9
  *
10
+ * The same `signMessage` primitive is reused for any UTF-8-bytes signing
11
+ * (e.g. `acta:redeem:v1:{pubkey}:{code}` for invite redemption).
12
+ *
10
13
  * Source of truth (server):
11
14
  * - rust-backend/rfq-server/src/server/ws.rs
12
15
  * - rust-backend/rfq-server/src/session/handler.rs
@@ -52,9 +55,9 @@ class KeypairAuthProvider {
52
55
  async getPublicKey() {
53
56
  return this.address;
54
57
  }
55
- async signChallenge(challenge) {
56
- const challengeBytes = utf8ToBytes(challenge);
57
- const signatureBytes = await (0, keys_1.signBytes)(this.privateKey, challengeBytes);
58
+ async signMessage(message) {
59
+ const messageBytes = utf8ToBytes(message);
60
+ const signatureBytes = await (0, keys_1.signBytes)(this.privateKey, messageBytes);
58
61
  return bytesToBase58(signatureBytes);
59
62
  }
60
63
  }
@@ -76,10 +79,10 @@ class WalletAuthProvider {
76
79
  throw new Error("Wallet not connected (missing public key)");
77
80
  return pk;
78
81
  }
79
- async signChallenge(challenge) {
80
- const challengeBytes = utf8ToBytes(challenge);
82
+ async signMessage(message) {
83
+ const messageBytes = utf8ToBytes(message);
81
84
  // Some wallet APIs expect a mutable `Uint8Array`; codecs return `ReadonlyUint8Array`.
82
- const sig = await this.wallet.signMessage(new Uint8Array(challengeBytes));
85
+ const sig = await this.wallet.signMessage(new Uint8Array(messageBytes));
83
86
  return bytesToBase58(sig);
84
87
  }
85
88
  }
@@ -89,16 +92,16 @@ exports.WalletAuthProvider = WalletAuthProvider;
89
92
  */
90
93
  class CustomAuthProvider {
91
94
  getPublicKeyFn;
92
- signChallengeFn;
93
- constructor(getPublicKey, signChallenge) {
95
+ signMessageFn;
96
+ constructor(getPublicKey, signMessage) {
94
97
  this.getPublicKeyFn = getPublicKey;
95
- this.signChallengeFn = signChallenge;
98
+ this.signMessageFn = signMessage;
96
99
  }
97
100
  async getPublicKey() {
98
101
  return this.getPublicKeyFn();
99
102
  }
100
- async signChallenge(challenge) {
101
- return this.signChallengeFn(challenge);
103
+ async signMessage(message) {
104
+ return this.signMessageFn(message);
102
105
  }
103
106
  }
104
107
  exports.CustomAuthProvider = CustomAuthProvider;
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.ActaWsClient = void 0;
5
5
  const flows_1 = require("./flows");
6
6
  const wirePolicy_1 = require("./wirePolicy");
7
+ const referral_1 = require("./referral");
7
8
  function toGenericServerError(err) {
8
9
  const message = err instanceof Error ? err.message : String(err);
9
10
  return { type: "generic", data: { code: "client_error", message } };
@@ -406,6 +407,51 @@ class ActaWsClient extends TypedEventEmitter {
406
407
  logout() {
407
408
  this.send({ type: "Logout" });
408
409
  }
410
+ // ========================================================================
411
+ // Referral / invite
412
+ // ========================================================================
413
+ /**
414
+ * Redeem an invite code. Authentication is proven by the session;
415
+ * no additional signature is required. Validation runs client-side —
416
+ * invalid inputs throw `ReferralCodeError` without a round-trip.
417
+ */
418
+ redeemInvite(rawCode) {
419
+ this.ensureAuthenticated();
420
+ const parsed = (0, referral_1.parseReferralCode)(rawCode);
421
+ if (!parsed.ok)
422
+ throw new referral_1.ReferralCodeError(parsed.error);
423
+ const requestId = this.nextRequestId();
424
+ this.send({
425
+ type: "RedeemInvite",
426
+ data: { request_id: requestId, code: parsed.code },
427
+ });
428
+ return requestId;
429
+ }
430
+ /**
431
+ * Claim a vanity referral code (one-shot per taker). Validation
432
+ * runs client-side — invalid inputs throw `ReferralCodeError`.
433
+ */
434
+ async claimReferralCode(rawCode) {
435
+ this.ensureAuthenticated();
436
+ const parsed = (0, referral_1.parseReferralCode)(rawCode);
437
+ if (!parsed.ok)
438
+ throw new referral_1.ReferralCodeError(parsed.error);
439
+ const requestId = this.nextRequestId();
440
+ this.send({
441
+ type: "ClaimReferralCode",
442
+ data: { request_id: requestId, code: parsed.code },
443
+ });
444
+ return requestId;
445
+ }
446
+ getMyReferralInfo() {
447
+ this.ensureAuthenticated();
448
+ const requestId = this.nextRequestId();
449
+ this.send({
450
+ type: "GetMyReferralInfo",
451
+ data: { request_id: requestId },
452
+ });
453
+ return requestId;
454
+ }
409
455
  getOrderStatus(orderIdHex) {
410
456
  this.ensureAuthenticated();
411
457
  const requestId = this.nextRequestId();
@@ -877,6 +923,18 @@ class ActaWsClient extends TypedEventEmitter {
877
923
  this.emit("subscriptionUpdated", d);
878
924
  }
879
925
  break;
926
+ case "RequireInvite":
927
+ this.emit("requireInvite");
928
+ break;
929
+ case "InviteRedeemed":
930
+ this.emit("inviteRedeemed", message.data);
931
+ break;
932
+ case "ReferralCodeClaimed":
933
+ this.emit("referralCodeClaimed", message.data);
934
+ break;
935
+ case "MyReferralInfo":
936
+ this.emit("myReferralInfo", message.data);
937
+ break;
880
938
  }
881
939
  }
882
940
  /** Taker-only: request current indicative prices for a market + position_type. */
@@ -902,7 +960,7 @@ class ActaWsClient extends TypedEventEmitter {
902
960
  try {
903
961
  const [pubkey, signature] = await Promise.all([
904
962
  this.authProvider.getPublicKey(),
905
- this.authProvider.signChallenge(challenge),
963
+ this.authProvider.signMessage(challenge),
906
964
  ]);
907
965
  this.send({
908
966
  type: "AuthChallenge",
@@ -53,7 +53,7 @@ function parseClientMessage(payload) {
53
53
  function makeAuthProvider(pubkey = "pubkey", signature = "signature") {
54
54
  return {
55
55
  getPublicKey: jest.fn().mockResolvedValue(pubkey),
56
- signChallenge: jest.fn().mockResolvedValue(signature),
56
+ signMessage: jest.fn().mockResolvedValue(signature),
57
57
  };
58
58
  }
59
59
  function makeHarness(overrides = {}) {
@@ -22,3 +22,4 @@ __exportStar(require("./sponsoredTx"), exports);
22
22
  __exportStar(require("./apy"), exports);
23
23
  __exportStar(require("./discovery"), exports);
24
24
  __exportStar(require("./sizing"), exports);
25
+ __exportStar(require("./referral"), exports);
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ /**
3
+ * Referral / invite redemption helpers.
4
+ *
5
+ * Mirrors the server-side validation defined in
6
+ * rust-backend/acta-types/src/invite.rs (ReferralCode::parse).
7
+ * Client-side validation must stay bit-identical to the server on
8
+ * the same input. A hardcoded fixture table is kept here and on the
9
+ * rust side; if either drifts, the cross-impl test will catch it.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ReferralCodeError = exports.REFERRAL_CODE_MAX_LEN = exports.REFERRAL_CODE_MIN_LEN = void 0;
13
+ exports.normalizeReferralCode = normalizeReferralCode;
14
+ exports.parseReferralCode = parseReferralCode;
15
+ exports.REFERRAL_CODE_MIN_LEN = 4;
16
+ exports.REFERRAL_CODE_MAX_LEN = 16;
17
+ class ReferralCodeError extends Error {
18
+ detail;
19
+ constructor(detail) {
20
+ super(detail.kind === "length"
21
+ ? `code length must be between ${detail.min} and ${detail.max}`
22
+ : "code must contain only ASCII letters and digits");
23
+ this.detail = detail;
24
+ this.name = "ReferralCodeError";
25
+ }
26
+ }
27
+ exports.ReferralCodeError = ReferralCodeError;
28
+ /**
29
+ * Trim + ASCII uppercase. No length or charset check. Useful for
30
+ * showing a live preview as the user types.
31
+ */
32
+ function normalizeReferralCode(input) {
33
+ return input.trim().toUpperCase();
34
+ }
35
+ /**
36
+ * Parse + validate + normalize a user-supplied referral code.
37
+ * Mirrors `ReferralCode::parse` on the server. A successful result
38
+ * carries the canonical branded `ReferralCode`.
39
+ */
40
+ function parseReferralCode(input) {
41
+ const normalized = normalizeReferralCode(input);
42
+ if (normalized.length < exports.REFERRAL_CODE_MIN_LEN ||
43
+ normalized.length > exports.REFERRAL_CODE_MAX_LEN) {
44
+ return {
45
+ ok: false,
46
+ error: {
47
+ kind: "length",
48
+ min: exports.REFERRAL_CODE_MIN_LEN,
49
+ max: exports.REFERRAL_CODE_MAX_LEN,
50
+ },
51
+ };
52
+ }
53
+ if (!/^[A-Z0-9]+$/.test(normalized)) {
54
+ return { ok: false, error: { kind: "charset" } };
55
+ }
56
+ return { ok: true, code: normalized };
57
+ }
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const referral_1 = require("./referral");
4
+ // =============================================================================
5
+ // Cross-impl fixture.
6
+ //
7
+ // Bit-for-bit identical to the fixture in
8
+ // rust-backend/acta-types/src/invite.rs::tests
9
+ // If either side changes, update both.
10
+ // =============================================================================
11
+ const ACCEPT_FIXTURES = [
12
+ ["NIKITA", "NIKITA"],
13
+ ["nikita", "NIKITA"],
14
+ [" abc1 ", "ABC1"],
15
+ ["ABCD", "ABCD"],
16
+ ["1234567890ABCDEF", "1234567890ABCDEF"],
17
+ ];
18
+ describe("parseReferralCode", () => {
19
+ test.each(ACCEPT_FIXTURES)("accepts %j → %j", (input, expected) => {
20
+ const res = (0, referral_1.parseReferralCode)(input);
21
+ expect(res.ok).toBe(true);
22
+ if (res.ok)
23
+ expect(res.code).toBe(expected);
24
+ });
25
+ test("rejects too short", () => {
26
+ const res = (0, referral_1.parseReferralCode)("abc");
27
+ expect(res.ok).toBe(false);
28
+ if (!res.ok)
29
+ expect(res.error.kind).toBe("length");
30
+ });
31
+ test("rejects too long", () => {
32
+ const res = (0, referral_1.parseReferralCode)("A".repeat(17));
33
+ expect(res.ok).toBe(false);
34
+ if (!res.ok)
35
+ expect(res.error.kind).toBe("length");
36
+ });
37
+ test("rejects non-ascii", () => {
38
+ const res = (0, referral_1.parseReferralCode)("абвгд");
39
+ expect(res.ok).toBe(false);
40
+ // Cyrillic passes length but fails charset.
41
+ if (!res.ok)
42
+ expect(res.error.kind).toBe("charset");
43
+ });
44
+ test("rejects punctuation", () => {
45
+ const res = (0, referral_1.parseReferralCode)("ABC-12");
46
+ expect(res.ok).toBe(false);
47
+ if (!res.ok)
48
+ expect(res.error.kind).toBe("charset");
49
+ });
50
+ });
51
+ describe("normalizeReferralCode", () => {
52
+ test("trims and uppercases", () => {
53
+ expect((0, referral_1.normalizeReferralCode)(" nikita ")).toBe("NIKITA");
54
+ });
55
+ });
package/dist/ws/auth.d.ts 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
@@ -13,8 +16,8 @@
13
16
  export interface AuthProvider {
14
17
  /** Base58-encoded public key (address). */
15
18
  getPublicKey(): Promise<string>;
16
- /** Base58-encoded ed25519 signature over UTF-8 bytes of the challenge text. */
17
- signChallenge(challenge: string): Promise<string>;
19
+ /** Base58-encoded ed25519 signature over UTF-8 bytes of the message. */
20
+ signMessage(message: string): Promise<string>;
18
21
  }
19
22
  /**
20
23
  * Auth provider backed by a `CryptoKeyPair`.
@@ -31,7 +34,7 @@ export declare class KeypairAuthProvider implements AuthProvider {
31
34
  /** Create from Solana CLI keypair JSON (array of numbers). */
32
35
  static fromJson(json: number[] | string): Promise<KeypairAuthProvider>;
33
36
  getPublicKey(): Promise<string>;
34
- signChallenge(challenge: string): Promise<string>;
37
+ signMessage(message: string): Promise<string>;
35
38
  }
36
39
  /**
37
40
  * Wallet-like provider (browser wallets, custom signers, etc.)
@@ -43,7 +46,7 @@ export declare class WalletAuthProvider implements AuthProvider {
43
46
  private readonly wallet;
44
47
  constructor(wallet: WalletLike);
45
48
  getPublicKey(): Promise<string>;
46
- signChallenge(challenge: string): Promise<string>;
49
+ signMessage(message: string): Promise<string>;
47
50
  }
48
51
  export type WalletLike = {
49
52
  /** Preferred: base58 address string. */
@@ -60,8 +63,8 @@ export type WalletLike = {
60
63
  */
61
64
  export declare class CustomAuthProvider implements AuthProvider {
62
65
  private readonly getPublicKeyFn;
63
- private readonly signChallengeFn;
64
- constructor(getPublicKey: () => Promise<string>, signChallenge: (challenge: string) => Promise<string>);
66
+ private readonly signMessageFn;
67
+ constructor(getPublicKey: () => Promise<string>, signMessage: (message: string) => Promise<string>);
65
68
  getPublicKey(): Promise<string>;
66
- signChallenge(challenge: string): Promise<string>;
69
+ signMessage(message: string): Promise<string>;
67
70
  }
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, MakerBalancesMessage, MakerMarketsMessage, MakerPositionsMessage, MyCapsMessage, MyQuotesMessage, MyTradesMessage, 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";
@@ -145,6 +145,10 @@ export type ActaWsClientEvents = {
145
145
  positionSettled: (event: Extract<ChainEventMessage, {
146
146
  event_type: "PositionSettled";
147
147
  }>) => void;
148
+ requireInvite: () => void;
149
+ inviteRedeemed: (data: InviteRedeemedData) => void;
150
+ referralCodeClaimed: (data: ReferralCodeClaimedData) => void;
151
+ myReferralInfo: (data: MyReferralInfoData) => void;
148
152
  };
149
153
  type EventMap = Record<string, (...args: any[]) => void>;
150
154
  declare class TypedEventEmitter<TEvents extends EventMap> {
@@ -277,6 +281,18 @@ export declare class ActaWsClient extends TypedEventEmitter<ActaWsClientEvents>
277
281
  active_only?: boolean;
278
282
  }): RequestId;
279
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;
280
296
  getOrderStatus(orderIdHex: string): RequestId;
281
297
  cancelRfq(rfqId: string): RequestId;
282
298
  submitQuote(quote: QuoteMessage): void;
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 } };
@@ -403,6 +404,51 @@ export class ActaWsClient extends TypedEventEmitter {
403
404
  logout() {
404
405
  this.send({ type: "Logout" });
405
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
+ }
406
452
  getOrderStatus(orderIdHex) {
407
453
  this.ensureAuthenticated();
408
454
  const requestId = this.nextRequestId();
@@ -874,6 +920,18 @@ export class ActaWsClient extends TypedEventEmitter {
874
920
  this.emit("subscriptionUpdated", d);
875
921
  }
876
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;
877
935
  }
878
936
  }
879
937
  /** Taker-only: request current indicative prices for a market + position_type. */
@@ -899,7 +957,7 @@ export class ActaWsClient extends TypedEventEmitter {
899
957
  try {
900
958
  const [pubkey, signature] = await Promise.all([
901
959
  this.authProvider.getPublicKey(),
902
- this.authProvider.signChallenge(challenge),
960
+ this.authProvider.signMessage(challenge),
903
961
  ]);
904
962
  this.send({
905
963
  type: "AuthChallenge",
@@ -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
+ });
@@ -1,4 +1,5 @@
1
1
  import type { Address } from "@solana/addresses";
2
+ import type { ClaimErrorReason, InviteErrorReason, TakerStatus } from "./referral";
2
3
  export type UuidString = string;
3
4
  export type RequestId = string;
4
5
  export type PubkeyBase58 = string;
@@ -208,6 +209,40 @@ export type ClientMessage = {
208
209
  request_id: RequestId;
209
210
  channels: WsChannel[];
210
211
  };
212
+ } | {
213
+ type: "RedeemInvite";
214
+ data: {
215
+ request_id: RequestId;
216
+ code: string;
217
+ };
218
+ } | {
219
+ type: "ClaimReferralCode";
220
+ data: {
221
+ request_id: RequestId;
222
+ code: string;
223
+ };
224
+ } | {
225
+ type: "GetMyReferralInfo";
226
+ data: {
227
+ request_id: RequestId;
228
+ };
229
+ };
230
+ export type InviteRedeemedData = {
231
+ request_id: RequestId;
232
+ referral_code: string;
233
+ };
234
+ export type ReferralCodeClaimedData = {
235
+ request_id: RequestId;
236
+ referral_code: string;
237
+ };
238
+ export type MyReferralInfoData = {
239
+ request_id: RequestId;
240
+ referral_code: string;
241
+ status: TakerStatus;
242
+ total_invited: number;
243
+ invited_this_period: number;
244
+ max_invites_per_period: number;
245
+ next_slot_frees_in_seconds: number;
211
246
  };
212
247
  export type GetMarketDescriptorsMessage = {
213
248
  request_id: RequestId;
@@ -538,6 +573,17 @@ export type ServerMessage = {
538
573
  underlying_mints?: string[];
539
574
  quote_mints?: string[];
540
575
  };
576
+ } | {
577
+ type: "RequireInvite";
578
+ } | {
579
+ type: "InviteRedeemed";
580
+ data: InviteRedeemedData;
581
+ } | {
582
+ type: "ReferralCodeClaimed";
583
+ data: ReferralCodeClaimedData;
584
+ } | {
585
+ type: "MyReferralInfo";
586
+ data: MyReferralInfoData;
541
587
  };
542
588
  export type MarketDescriptorInfo = {
543
589
  market: MarketDescriptor;
@@ -577,128 +623,154 @@ export type QuoteRefreshRequestedMessage = {
577
623
  reason: QuoteRefreshReason;
578
624
  };
579
625
  export type ServerError = {
580
- type: "unauthenticated";
626
+ type: "Unauthenticated";
581
627
  data: {
582
628
  action: AuthRequiredAction;
583
629
  };
584
630
  } | {
585
- type: "unauthorized";
631
+ type: "Unauthorized";
586
632
  data: {
587
633
  role: UserRole;
588
634
  action: AuthRequiredAction;
589
635
  };
590
636
  } | {
591
- type: "rfq_not_found";
637
+ type: "RfqNotFound";
592
638
  } | {
593
- type: "rfq_not_active";
639
+ type: "RfqNotActive";
594
640
  } | {
595
- type: "invalid_state";
641
+ type: "RfqAlreadyLocked";
642
+ } | {
643
+ type: "InvalidState";
596
644
  data: {
597
645
  state: RfqStateError;
598
646
  };
599
647
  } | {
600
- type: "quote_locked";
648
+ type: "QuoteLocked";
601
649
  data: {
602
650
  reason: QuoteLockedReason;
603
651
  };
604
652
  } | {
605
- type: "quote_not_found";
653
+ type: "QuoteNotFound";
606
654
  } | {
607
- type: "quote_expired";
655
+ type: "QuoteExpired";
608
656
  } | {
609
- type: "invalid_strike";
657
+ type: "QuoteExpiryTooShort";
658
+ data: {
659
+ min_seconds: WsU32;
660
+ };
610
661
  } | {
611
- type: "invalid_valid_until";
662
+ type: "InvalidStrike";
612
663
  } | {
613
- type: "order_id_mismatch";
664
+ type: "InvalidValidUntil";
614
665
  } | {
615
- type: "signature_timeout";
666
+ type: "OrderIdMismatch";
616
667
  } | {
617
- type: "oracle_not_ready";
668
+ type: "UnknownOrder";
618
669
  } | {
619
- type: "oracle_price_stale";
670
+ type: "SignatureTimeout";
671
+ } | {
672
+ type: "OracleNotReady";
673
+ } | {
674
+ type: "OraclePriceStale";
620
675
  data: {
621
676
  age_seconds: WsU64;
622
677
  };
623
678
  } | {
624
- type: "oracle_price_not_ready";
679
+ type: "OraclePriceNotReady";
625
680
  } | {
626
- type: "invalid_position_type";
681
+ type: "InvalidPositionType";
627
682
  } | {
628
- type: "invalid_market";
683
+ type: "InvalidMarket";
629
684
  data: {
630
685
  pubkey: string;
631
686
  };
632
687
  } | {
633
- type: "market_metadata_incomplete";
688
+ type: "MarketMetadataIncomplete";
634
689
  data: {
635
690
  details: string;
636
691
  };
637
692
  } | {
638
- type: "token_metadata_incomplete";
693
+ type: "TokenMetadataIncomplete";
639
694
  data: {
640
695
  details: string;
641
696
  };
642
697
  } | {
643
- type: "rate_limit";
698
+ type: "RateLimit";
644
699
  data: RateLimitReason;
645
700
  } | {
646
- type: "token_oi_cap_exceeded";
701
+ type: "Cap";
702
+ data: CapErrorData;
703
+ } | {
704
+ type: "InternalError";
705
+ } | {
706
+ type: "KernelNotAvailable";
707
+ } | {
708
+ type: "DbDisabled";
647
709
  data: {
710
+ feature: DbFeature;
711
+ };
712
+ } | {
713
+ type: "ServerShuttingDown";
714
+ } | {
715
+ type: "Generic";
716
+ data: {
717
+ code: ErrorCode;
718
+ message: ErrorMessage;
719
+ };
720
+ } | {
721
+ type: "InviteRequired";
722
+ } | {
723
+ type: "Invite";
724
+ data: InviteErrorReason;
725
+ } | {
726
+ type: "Claim";
727
+ data: ClaimErrorReason;
728
+ } | {
729
+ type: "generic";
730
+ data: {
731
+ code: ErrorCode;
732
+ message: ErrorMessage;
733
+ };
734
+ };
735
+ /**
736
+ * Externally-tagged union for rust's `CapError`. Only one key is
737
+ * present at a time. Lock-step with
738
+ * `rust-backend/acta-types/src/errors.rs::CapError`.
739
+ */
740
+ export type CapErrorData = {
741
+ token_oi_cap_exceeded: {
648
742
  underlying_mint: string;
649
743
  current: WsU64;
650
744
  limit: WsU64;
651
745
  };
652
746
  } | {
653
- type: "market_oi_cap_exceeded";
654
- data: {
747
+ market_oi_cap_exceeded: {
655
748
  market_id: string;
656
749
  current: WsU64;
657
750
  limit: WsU64;
658
751
  };
659
752
  } | {
660
- type: "maker_position_cap_exceeded";
661
- data: {
753
+ maker_position_cap_exceeded: {
662
754
  current: WsU32;
663
755
  limit: WsU32;
664
756
  };
665
757
  } | {
666
- type: "maker_notional_cap_exceeded";
667
- data: {
758
+ maker_notional_cap_exceeded: {
668
759
  underlying_mint: string;
669
760
  current: WsU64;
670
761
  limit: WsU64;
671
762
  };
672
763
  } | {
673
- type: "maker_insufficient_balance";
674
- data: {
764
+ maker_insufficient_balance: {
675
765
  available: WsU64;
676
766
  required: WsU64;
677
767
  };
678
768
  } | {
679
- type: "quote_notional_cap_exceeded";
680
- data: {
769
+ quote_notional_cap_exceeded: {
681
770
  quote_mint: string;
682
771
  current: WsU64;
683
772
  limit: WsU64;
684
773
  };
685
- } | {
686
- type: "internal_error";
687
- } | {
688
- type: "kernel_not_available";
689
- } | {
690
- type: "db_disabled";
691
- data: {
692
- feature: DbFeature;
693
- };
694
- } | {
695
- type: "server_shutting_down";
696
- } | {
697
- type: "generic";
698
- data: {
699
- code: ErrorCode;
700
- message: ErrorMessage;
701
- };
702
774
  };
703
775
  /** Confirms quote was received and validated. */
704
776
  export type QuoteAcknowledgedMessage = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acta-markets/ts-sdk",
3
- "version": "0.0.21-beta",
3
+ "version": "0.0.22-beta",
4
4
  "description": "TypeScript SDK for Acta Protocol",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",