@signals-protocol/v1-sdk 1.4.0 → 1.5.0

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/index.d.ts CHANGED
@@ -10,4 +10,4 @@ export * as MathUtils from "./utils/math";
10
10
  export { toWAD, toMicroUSDC } from "./clmsr-sdk";
11
11
  export { createCLMSRSDK, createSignalsSDK } from "./clmsr-sdk";
12
12
  export * from "./share";
13
- export declare const VERSION = "1.4.0";
13
+ export declare const VERSION = "1.5.0";
package/dist/index.js CHANGED
@@ -67,4 +67,4 @@ Object.defineProperty(exports, "createSignalsSDK", { enumerable: true, get: func
67
67
  // Share image VNode templates
68
68
  __exportStar(require("./share"), exports);
69
69
  // Version (keep in sync with package.json)
70
- exports.VERSION = "1.4.0";
70
+ exports.VERSION = "1.5.0";
@@ -0,0 +1,9 @@
1
+ export type AvatarTheme = "light" | "dark";
2
+ export interface AvatarData {
3
+ pattern: boolean[][];
4
+ primary: string;
5
+ secondary: string;
6
+ background: string;
7
+ }
8
+ export declare const AVATAR_COLOR_PALETTE: readonly ["#1444c2", "#2d88ff", "#fdd979", "#8bcaff", "#00BF40", "#F7931A", "#FF4343", "#6366f1", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#fb7185", "#fc97f3", "#5eead4", "#c4b5fd"];
9
+ export declare function getAvatarData(address: string, theme: AvatarTheme): AvatarData;
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AVATAR_COLOR_PALETTE = void 0;
4
+ exports.getAvatarData = getAvatarData;
5
+ exports.AVATAR_COLOR_PALETTE = [
6
+ "#1444c2",
7
+ "#2d88ff",
8
+ "#fdd979",
9
+ "#8bcaff",
10
+ "#00BF40",
11
+ "#F7931A",
12
+ "#FF4343",
13
+ "#6366f1",
14
+ "#8b5cf6",
15
+ "#ec4899",
16
+ "#f59e0b",
17
+ "#10b981",
18
+ "#fb7185",
19
+ "#fc97f3",
20
+ "#5eead4",
21
+ "#c4b5fd",
22
+ ];
23
+ const AVATAR_BACKGROUNDS = {
24
+ light: "#f8fafc",
25
+ dark: "#1e1e22",
26
+ };
27
+ function simpleHash(str) {
28
+ let hash = 5381;
29
+ for (let i = 0; i < str.length; i += 1) {
30
+ const char = str.charCodeAt(i);
31
+ hash = (hash << 5) + hash + char;
32
+ }
33
+ return Math.abs(hash);
34
+ }
35
+ function getSecondaryHash(address) {
36
+ let hash = 0;
37
+ const suffix = address.slice(-8);
38
+ for (let i = 0; i < suffix.length; i += 1) {
39
+ hash = ((hash << 3) + hash + suffix.charCodeAt(i)) & 0xffffffff;
40
+ }
41
+ return Math.abs(hash);
42
+ }
43
+ function generateSeedValues(address) {
44
+ const hash1 = simpleHash(address);
45
+ const hash2 = getSecondaryHash(address);
46
+ const values = [];
47
+ let seed = hash1;
48
+ for (let i = 0; i < 10; i += 1) {
49
+ seed = (seed * 1103015245 + 12345 + hash2) & 0x7fffffff;
50
+ values.push(seed);
51
+ }
52
+ return values;
53
+ }
54
+ function generatePixelPattern(address) {
55
+ const seeds = generateSeedValues(address);
56
+ const pattern = Array(5)
57
+ .fill(null)
58
+ .map(() => Array(5).fill(false));
59
+ for (let y = 0; y < 5; y += 1) {
60
+ for (let x = 0; x < 2; x += 1) {
61
+ const index = y * 2 + x;
62
+ const seedIndex = index % seeds.length;
63
+ const shouldFill = seeds[seedIndex] % 100 > 40;
64
+ pattern[y][x] = shouldFill;
65
+ pattern[y][4 - x] = shouldFill;
66
+ }
67
+ const centerIndex = y + 10;
68
+ const centerSeedIndex = centerIndex % seeds.length;
69
+ pattern[y][2] = seeds[centerSeedIndex] % 100 > 40;
70
+ }
71
+ const countFilled = () => pattern.flat().filter(Boolean).length;
72
+ const ensureMinPixels = (minPixels) => {
73
+ let filledCount = countFilled();
74
+ if (filledCount >= minPixels)
75
+ return;
76
+ const candidates = [];
77
+ for (let y = 0; y < 5; y += 1) {
78
+ for (let x = 0; x < 3; x += 1) {
79
+ if (!pattern[y][x]) {
80
+ candidates.push([y, x]);
81
+ }
82
+ }
83
+ }
84
+ let i = 0;
85
+ while (filledCount < minPixels && candidates.length > 0) {
86
+ const idx = (seeds[i % seeds.length] + i) % candidates.length;
87
+ const [y, x] = candidates.splice(idx, 1)[0];
88
+ if (!pattern[y][x]) {
89
+ pattern[y][x] = true;
90
+ if (x !== 2) {
91
+ if (!pattern[y][4 - x]) {
92
+ pattern[y][4 - x] = true;
93
+ filledCount += 2;
94
+ }
95
+ else {
96
+ filledCount += 1;
97
+ }
98
+ }
99
+ else {
100
+ filledCount += 1;
101
+ }
102
+ }
103
+ i += 1;
104
+ }
105
+ };
106
+ ensureMinPixels(12);
107
+ const parityCounts = { even: 0, odd: 0 };
108
+ for (let y = 0; y < 5; y += 1) {
109
+ for (let x = 0; x < 5; x += 1) {
110
+ if (pattern[y][x]) {
111
+ if ((y + x) % 2 === 0) {
112
+ parityCounts.even += 1;
113
+ }
114
+ else {
115
+ parityCounts.odd += 1;
116
+ }
117
+ }
118
+ }
119
+ }
120
+ if (parityCounts.even === 0 || parityCounts.odd === 0) {
121
+ const missingParity = parityCounts.even === 0 ? 0 : 1;
122
+ const candidates = [];
123
+ for (let y = 0; y < 5; y += 1) {
124
+ for (let x = 0; x < 3; x += 1) {
125
+ if (!pattern[y][x] && (y + x) % 2 === missingParity) {
126
+ candidates.push([y, x]);
127
+ }
128
+ }
129
+ }
130
+ if (candidates.length > 0) {
131
+ const idx = (seeds[(seeds.length - 1 + missingParity) % seeds.length] +
132
+ parityCounts.even +
133
+ parityCounts.odd) %
134
+ candidates.length;
135
+ const [y, x] = candidates[idx];
136
+ pattern[y][x] = true;
137
+ if (x !== 2) {
138
+ pattern[y][4 - x] = true;
139
+ }
140
+ }
141
+ }
142
+ return pattern;
143
+ }
144
+ function generateColors(address) {
145
+ const seeds = generateSeedValues(address);
146
+ const hash1 = simpleHash(address);
147
+ const hash2 = getSecondaryHash(address);
148
+ const primaryColorIndex = Math.abs(hash1 ^ seeds[3]) % exports.AVATAR_COLOR_PALETTE.length;
149
+ let secondaryColorIndex = Math.abs(hash2 ^ seeds[7]) % exports.AVATAR_COLOR_PALETTE.length;
150
+ if (secondaryColorIndex === primaryColorIndex) {
151
+ secondaryColorIndex =
152
+ (secondaryColorIndex + 1) % exports.AVATAR_COLOR_PALETTE.length;
153
+ }
154
+ return {
155
+ primary: exports.AVATAR_COLOR_PALETTE[primaryColorIndex],
156
+ secondary: exports.AVATAR_COLOR_PALETTE[secondaryColorIndex],
157
+ };
158
+ }
159
+ function getAvatarData(address, theme) {
160
+ const colors = generateColors(address);
161
+ return {
162
+ pattern: generatePixelPattern(address),
163
+ primary: colors.primary,
164
+ secondary: colors.secondary,
165
+ background: AVATAR_BACKGROUNDS[theme],
166
+ };
167
+ }
@@ -3,6 +3,8 @@ export { buildPositionVNode } from "./position-image";
3
3
  export { buildProfileVNode } from "./profile-image";
4
4
  export { buildDistributionVNode } from "./distribution-image";
5
5
  export { buildBrandVNode } from "./brand-image";
6
+ export { AVATAR_COLOR_PALETTE, getAvatarData } from "./avatar";
7
+ export type { AvatarData, AvatarTheme } from "./avatar";
6
8
  export { h } from "./h";
7
9
  export { SIGNALS_LOGO_URI, buildLogoStrokeSVG } from "./assets";
8
10
  export { formatUSDC, formatPrice, formatPriceRange, percForm, formatUsdPnl, getPnlColor, } from "./format";
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.formatDistributionPriceRange = exports.getPeakRange = exports.formatXTick = exports.generateXAxisTicks = exports.getSqrtHeight = exports.getBarCategory = exports.getPnlColor = exports.formatUsdPnl = exports.percForm = exports.formatPriceRange = exports.formatPrice = exports.formatUSDC = exports.buildLogoStrokeSVG = exports.SIGNALS_LOGO_URI = exports.h = exports.buildBrandVNode = exports.buildDistributionVNode = exports.buildProfileVNode = exports.buildPositionVNode = void 0;
3
+ exports.formatDistributionPriceRange = exports.getPeakRange = exports.formatXTick = exports.generateXAxisTicks = exports.getSqrtHeight = exports.getBarCategory = exports.getPnlColor = exports.formatUsdPnl = exports.percForm = exports.formatPriceRange = exports.formatPrice = exports.formatUSDC = exports.buildLogoStrokeSVG = exports.SIGNALS_LOGO_URI = exports.h = exports.getAvatarData = exports.AVATAR_COLOR_PALETTE = exports.buildBrandVNode = exports.buildDistributionVNode = exports.buildProfileVNode = exports.buildPositionVNode = void 0;
4
4
  // VNode builders
5
5
  var position_image_1 = require("./position-image");
6
6
  Object.defineProperty(exports, "buildPositionVNode", { enumerable: true, get: function () { return position_image_1.buildPositionVNode; } });
@@ -10,6 +10,9 @@ var distribution_image_1 = require("./distribution-image");
10
10
  Object.defineProperty(exports, "buildDistributionVNode", { enumerable: true, get: function () { return distribution_image_1.buildDistributionVNode; } });
11
11
  var brand_image_1 = require("./brand-image");
12
12
  Object.defineProperty(exports, "buildBrandVNode", { enumerable: true, get: function () { return brand_image_1.buildBrandVNode; } });
13
+ var avatar_1 = require("./avatar");
14
+ Object.defineProperty(exports, "AVATAR_COLOR_PALETTE", { enumerable: true, get: function () { return avatar_1.AVATAR_COLOR_PALETTE; } });
15
+ Object.defineProperty(exports, "getAvatarData", { enumerable: true, get: function () { return avatar_1.getAvatarData; } });
13
16
  // Helpers (used by v1-server for its own distribution rendering)
14
17
  var h_1 = require("./h");
15
18
  Object.defineProperty(exports, "h", { enumerable: true, get: function () { return h_1.h; } });
@@ -184,7 +184,7 @@ function buildPositionVNode(input) {
184
184
  marginBottom: 2,
185
185
  display: "flex",
186
186
  },
187
- }, "PnL"),
187
+ }, "P&L"),
188
188
  ];
189
189
  if (hideAmounts) {
190
190
  pnlChildren.push((0, h_1.h)("div", {
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildProfileVNode = buildProfileVNode;
4
4
  const h_1 = require("./h");
5
5
  const assets_1 = require("./assets");
6
+ const avatar_1 = require("./avatar");
6
7
  const CARD_WIDTH = 1200;
7
8
  const CARD_HEIGHT = 630;
8
9
  const OUTER_PADDING = 24;
@@ -15,6 +16,9 @@ const PLOT_RIGHT = 18;
15
16
  const PLOT_TOP = 14;
16
17
  const PLOT_BOTTOM = 24;
17
18
  const BAND_WIDTH = 18;
19
+ const DAY_MS = 86400000;
20
+ const AVATAR_SIZE = 96;
21
+ const MAX_RENDERED_POSITIONS = 1000;
18
22
  const OG_THEMES = {
19
23
  light: {
20
24
  pageBg: "#eef7ff", // bg-surface-soft
@@ -41,13 +45,83 @@ const OG_THEMES = {
41
45
  neutralBg: "#27272A", // surface-muted
42
46
  },
43
47
  };
44
- const TIER_ACCENT_COLORS = {
45
- BRONZE: "#C7742A",
46
- SILVER: "#BFC6CE",
47
- GOLD: "#F3BC3C",
48
- PLATINUM: "#63CBB2",
49
- DIAMOND: "#5EC0F2",
48
+ const TIER_CHIP_THEMES = {
49
+ BRONZE: {
50
+ light: {
51
+ from: "#FFE6CC",
52
+ to: "#FFC999",
53
+ border: "#C97E3D",
54
+ icon: "#8B4513",
55
+ },
56
+ dark: {
57
+ from: "#3D2B1A",
58
+ to: "#5C3A1E",
59
+ border: "#8B5E2B",
60
+ icon: "#F5E5D4",
61
+ },
62
+ },
63
+ SILVER: {
64
+ light: {
65
+ from: "#F2F5F8",
66
+ to: "#E2E7EE",
67
+ border: "#BFC6CE",
68
+ icon: "#6B7280",
69
+ },
70
+ dark: {
71
+ from: "#2A2D31",
72
+ to: "#363A40",
73
+ border: "#6B7280",
74
+ icon: "#F3F4F6",
75
+ },
76
+ },
77
+ GOLD: {
78
+ light: {
79
+ from: "#FFF0B8",
80
+ to: "#FFD875",
81
+ border: "#D9A441",
82
+ icon: "#DA9100",
83
+ },
84
+ dark: {
85
+ from: "#3D3520",
86
+ to: "#4A3D1A",
87
+ border: "#B8892E",
88
+ icon: "#FFE9B3",
89
+ },
90
+ },
91
+ PLATINUM: {
92
+ light: {
93
+ from: "#E4FFF7",
94
+ to: "#CDEFE5",
95
+ border: "#8FD7C6",
96
+ icon: "#059669",
97
+ },
98
+ dark: {
99
+ from: "#1A3D33",
100
+ to: "#1F3B31",
101
+ border: "#4DAE96",
102
+ icon: "#BAEBD9",
103
+ },
104
+ },
105
+ DIAMOND: {
106
+ light: {
107
+ from: "#E2F4FF",
108
+ to: "#C9E9FF",
109
+ border: "#89D0FF",
110
+ icon: "#0284C7",
111
+ },
112
+ dark: {
113
+ from: "#1A2F3D",
114
+ to: "#1E3545",
115
+ border: "#5AAFE0",
116
+ icon: "#5EC0F2",
117
+ },
118
+ },
50
119
  };
120
+ const PRICE_TICK_STEPS = [
121
+ 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000,
122
+ 250000, 500000, 1000000,
123
+ ];
124
+ const MAX_MATERIALIZED_PRICE_TICKS = 20;
51
125
  function shortenAddress(address) {
52
126
  return `${address.slice(0, 6)}…${address.slice(-4)}`;
53
127
  }
@@ -99,65 +173,130 @@ function normalizePositions(positions) {
99
173
  });
100
174
  return normalized;
101
175
  }
176
+ function capRenderablePositions(positions) {
177
+ if (positions.length <= MAX_RENDERED_POSITIONS) {
178
+ return positions;
179
+ }
180
+ return positions
181
+ .map((position, index) => ({ position, index }))
182
+ .sort((a, b) => b.position.marketEndTimestampMs - a.position.marketEndTimestampMs ||
183
+ a.index - b.index)
184
+ .slice(0, MAX_RENDERED_POSITIONS)
185
+ .map(({ position }) => position);
186
+ }
102
187
  function buildMainText(input) {
188
+ const handle = resolveDisplayName(input.realHandle ?? null);
189
+ if (handle)
190
+ return handle;
103
191
  if (input.hideWallet)
104
192
  return "A Signals trader";
105
193
  return resolveDisplayName(input.displayName) ?? shortenAddress(input.address);
106
194
  }
195
+ function formatCallCopy(totalCalls) {
196
+ return `${totalCalls} BTC range ${totalCalls === 1 ? "call" : "calls"}`;
197
+ }
107
198
  function buildSubline(input) {
108
- const callCopy = `${input.totalCalls} BTC range calls`;
199
+ const callCopy = formatCallCopy(input.totalCalls);
109
200
  if (input.hideWallet)
110
- return callCopy;
111
- const parts = [];
112
- if (resolveDisplayName(input.displayName) !== null) {
113
- parts.push(shortenAddress(input.address));
201
+ return `${callCopy} on Signals`;
202
+ if (resolveDisplayName(input.realHandle ?? null) !== null) {
203
+ return `${shortenAddress(input.address)} with ${callCopy}`;
114
204
  }
115
205
  if (input.twitter) {
116
- parts.push(`@${input.twitter}`);
206
+ return `@${input.twitter} with ${callCopy}`;
207
+ }
208
+ return `${callCopy} on Signals`;
209
+ }
210
+ function buildPriceTickCandidate(min, max, step) {
211
+ const alignedMin = Math.floor(min / step) * step;
212
+ const alignedMax = Math.ceil(max / step) * step;
213
+ const count = Math.floor((alignedMax - alignedMin) / step) + 1;
214
+ const ticks = [];
215
+ if (count > MAX_MATERIALIZED_PRICE_TICKS) {
216
+ return {
217
+ min: alignedMin,
218
+ max: alignedMax,
219
+ step,
220
+ count,
221
+ ticks,
222
+ };
117
223
  }
118
- parts.push(callCopy);
119
- return parts.join(" · ");
224
+ for (let tick = alignedMin; tick <= alignedMax + step / 2; tick += step) {
225
+ ticks.push(tick);
226
+ }
227
+ return {
228
+ min: alignedMin,
229
+ max: alignedMax,
230
+ step,
231
+ count,
232
+ ticks,
233
+ };
120
234
  }
121
235
  function getPriceTickStep(span) {
122
236
  const target = span / 4;
123
- const candidates = [
124
- 500,
125
- 1000,
126
- 2500,
127
- 5000,
128
- 10000,
129
- 25000,
130
- 50000,
131
- 100000,
132
- 250000,
133
- 500000,
134
- 1000000,
135
- ];
136
- return candidates.find((step) => step >= target) ?? 1000000;
237
+ return (PRICE_TICK_STEPS.find((step) => step >= target) ??
238
+ PRICE_TICK_STEPS[PRICE_TICK_STEPS.length - 1]);
137
239
  }
138
240
  function getExpandedPriceBounds(min, max) {
139
241
  const span = max - min;
140
- const step = getPriceTickStep(span);
242
+ const desiredStep = getPriceTickStep(span);
243
+ const candidates = PRICE_TICK_STEPS.map((step) => buildPriceTickCandidate(min, max, step));
244
+ const validCandidates = candidates.filter((candidate) => candidate.ticks.length > 0 &&
245
+ candidate.count >= 4 &&
246
+ candidate.count <= 5);
247
+ const fallbackCandidates = candidates.filter((candidate) => candidate.ticks.length > 0);
248
+ const [selected] = (validCandidates.length > 0
249
+ ? validCandidates
250
+ : fallbackCandidates.length > 0
251
+ ? fallbackCandidates
252
+ : candidates)
253
+ .slice()
254
+ .sort((a, b) => {
255
+ const aCountPenalty = a.count >= 4 && a.count <= 5
256
+ ? 0
257
+ : Math.abs(a.count - 5) * 1000000;
258
+ const bCountPenalty = b.count >= 4 && b.count <= 5
259
+ ? 0
260
+ : Math.abs(b.count - 5) * 1000000;
261
+ return (aCountPenalty +
262
+ Math.abs(a.step - desiredStep) -
263
+ (bCountPenalty + Math.abs(b.step - desiredStep)));
264
+ });
141
265
  return {
142
- min: Math.floor(min / step) * step,
143
- max: Math.ceil(max / step) * step,
144
- step,
266
+ min: selected.min,
267
+ max: selected.max,
268
+ step: selected.step,
145
269
  };
146
270
  }
147
271
  function getPriceTicks(min, max, step) {
272
+ const count = Math.floor((max - min) / step) + 1;
148
273
  const ticks = [];
149
- for (let tick = min; tick <= max + step / 2; tick += step) {
150
- ticks.push(tick);
151
- if (ticks.length >= 6)
152
- break;
274
+ if (count <= MAX_MATERIALIZED_PRICE_TICKS) {
275
+ for (let tick = min; tick <= max + step / 2; tick += step) {
276
+ ticks.push(tick);
277
+ }
278
+ }
279
+ if (count >= 4 && count <= 5 && ticks.length > 0) {
280
+ return { ticks, step };
153
281
  }
154
- return ticks.length > 0 ? ticks : [min, max];
282
+ const fallbackCount = count < 4 ? 4 : 5;
283
+ const fallbackStep = (max - min) / (fallbackCount - 1);
284
+ return {
285
+ ticks: Array.from({ length: fallbackCount }, (_, index) => Math.round(min + fallbackStep * index)),
286
+ step: fallbackStep,
287
+ };
155
288
  }
156
- function formatAxisLabel(value) {
289
+ function formatAxisLabel(value, step) {
290
+ if (step < 100) {
291
+ return `$${Math.round(value).toLocaleString("en-US")}`;
292
+ }
157
293
  if (Math.abs(value) >= 1000) {
294
+ if (step >= 100 && step < 1000) {
295
+ return `$${(value / 1000).toFixed(1)}k`;
296
+ }
158
297
  return `$${Math.round(value / 1000)}k`;
159
298
  }
160
- return `$${Math.round(value)}`;
299
+ return `$${Math.round(value).toLocaleString("en-US")}`;
161
300
  }
162
301
  function computePriceBounds(btcHistory, positions) {
163
302
  let rawMin = Infinity;
@@ -229,9 +368,239 @@ function buildHistoryPath(btcHistory, scaleX, scaleY) {
229
368
  })
230
369
  .join(" ");
231
370
  }
371
+ function clamp(value, min, max) {
372
+ return Math.min(Math.max(value, min), max);
373
+ }
374
+ function formatTierName(tier) {
375
+ return `${tier[0]}${tier.slice(1).toLowerCase()}`;
376
+ }
377
+ function buildTierChip(tier, themeName, theme) {
378
+ const tierTheme = TIER_CHIP_THEMES[tier][themeName];
379
+ return (0, h_1.h)("div", {
380
+ style: {
381
+ display: "flex",
382
+ alignItems: "center",
383
+ gap: 8,
384
+ padding: "8px 14px",
385
+ borderRadius: 999,
386
+ border: `1px solid ${tierTheme.border}`,
387
+ background: `linear-gradient(135deg, ${tierTheme.from}, ${tierTheme.to})`,
388
+ color: theme.ink,
389
+ fontSize: 18,
390
+ fontWeight: 700,
391
+ lineHeight: 1,
392
+ flexShrink: 0,
393
+ },
394
+ }, (0, h_1.h)("svg", {
395
+ width: 18,
396
+ height: 18,
397
+ viewBox: "0 0 16 16",
398
+ style: { display: "flex", flexShrink: 0 },
399
+ }, (0, h_1.h)("polygon", {
400
+ points: "8 1 15 8 8 15 1 8",
401
+ fill: tierTheme.icon,
402
+ })), (0, h_1.h)("div", { style: { display: "flex" } }, formatTierName(tier)));
403
+ }
404
+ function buildAddressAvatar(address, themeName, theme) {
405
+ const avatar = (0, avatar_1.getAvatarData)(address, themeName);
406
+ return (0, h_1.h)("div", {
407
+ style: {
408
+ width: AVATAR_SIZE,
409
+ height: AVATAR_SIZE,
410
+ display: "flex",
411
+ flexDirection: "column",
412
+ overflow: "hidden",
413
+ borderRadius: 22,
414
+ border: `1px solid ${theme.border}`,
415
+ background: avatar.background,
416
+ flexShrink: 0,
417
+ },
418
+ }, ...avatar.pattern.map((row, y) => (0, h_1.h)("div", {
419
+ style: {
420
+ display: "flex",
421
+ width: "100%",
422
+ height: "20%",
423
+ },
424
+ }, ...row.map((isFilled, x) => (0, h_1.h)("div", {
425
+ style: {
426
+ width: "20%",
427
+ height: "100%",
428
+ backgroundColor: isFilled
429
+ ? (y + x) % 2 === 0
430
+ ? avatar.primary
431
+ : avatar.secondary
432
+ : "transparent",
433
+ },
434
+ })))));
435
+ }
436
+ function buildGenericAvatar(theme) {
437
+ return (0, h_1.h)("div", {
438
+ style: {
439
+ width: AVATAR_SIZE,
440
+ height: AVATAR_SIZE,
441
+ display: "flex",
442
+ alignItems: "center",
443
+ justifyContent: "center",
444
+ overflow: "hidden",
445
+ borderRadius: 22,
446
+ border: `1px solid ${theme.border}`,
447
+ background: theme.primary,
448
+ flexShrink: 0,
449
+ },
450
+ }, (0, h_1.h)("div", {
451
+ style: {
452
+ width: 58,
453
+ height: 58,
454
+ display: "flex",
455
+ alignItems: "center",
456
+ justifyContent: "center",
457
+ borderRadius: 999,
458
+ background: theme.card,
459
+ },
460
+ }, (0, h_1.h)("img", {
461
+ src: assets_1.SIGNALS_LOGO_URI,
462
+ width: 34,
463
+ height: 32,
464
+ style: { display: "flex" },
465
+ })));
466
+ }
467
+ function buildAvatar(input, theme) {
468
+ if (input.hideWallet) {
469
+ return buildGenericAvatar(theme);
470
+ }
471
+ return buildAddressAvatar(input.address, input.theme, theme);
472
+ }
473
+ function buildBrandWordmark(theme) {
474
+ return (0, h_1.h)("div", {
475
+ style: {
476
+ display: "flex",
477
+ alignItems: "center",
478
+ gap: 10,
479
+ flexShrink: 0,
480
+ },
481
+ }, (0, h_1.h)("img", {
482
+ src: assets_1.SIGNALS_LOGO_URI,
483
+ width: 34,
484
+ height: 32,
485
+ style: { display: "flex" },
486
+ }), (0, h_1.h)("div", {
487
+ style: {
488
+ display: "flex",
489
+ fontSize: 28,
490
+ fontWeight: 700,
491
+ color: theme.primary,
492
+ },
493
+ }, "Signals"));
494
+ }
495
+ function buildHeader(input, theme, mainText, subline) {
496
+ return (0, h_1.h)("div", {
497
+ style: {
498
+ display: "flex",
499
+ alignItems: "center",
500
+ justifyContent: "space-between",
501
+ gap: 24,
502
+ minHeight: AVATAR_SIZE,
503
+ },
504
+ }, (0, h_1.h)("div", {
505
+ style: {
506
+ flex: 1,
507
+ minWidth: 0,
508
+ display: "flex",
509
+ alignItems: "center",
510
+ gap: 18,
511
+ },
512
+ }, buildAvatar(input, theme), (0, h_1.h)("div", {
513
+ style: {
514
+ flex: 1,
515
+ minWidth: 0,
516
+ display: "flex",
517
+ flexDirection: "column",
518
+ },
519
+ }, (0, h_1.h)("div", {
520
+ style: {
521
+ display: "flex",
522
+ alignItems: "center",
523
+ gap: 14,
524
+ minWidth: 0,
525
+ },
526
+ }, (0, h_1.h)("div", {
527
+ style: {
528
+ display: "flex",
529
+ fontSize: 52,
530
+ lineHeight: 1.05,
531
+ fontWeight: 700,
532
+ color: theme.ink,
533
+ maxWidth: 620,
534
+ overflow: "hidden",
535
+ textOverflow: "ellipsis",
536
+ whiteSpace: "nowrap",
537
+ },
538
+ }, mainText), buildTierChip(input.tier, input.theme, theme)), (0, h_1.h)("div", {
539
+ style: {
540
+ display: "flex",
541
+ fontSize: 20,
542
+ fontWeight: 500,
543
+ color: theme.inkMuted,
544
+ marginTop: 8,
545
+ overflow: "hidden",
546
+ textOverflow: "ellipsis",
547
+ whiteSpace: "nowrap",
548
+ },
549
+ }, subline))), buildBrandWordmark(theme));
550
+ }
551
+ function buildStatItem(label, value, valueColor, theme) {
552
+ return (0, h_1.h)("div", {
553
+ style: {
554
+ flex: 1,
555
+ minWidth: 0,
556
+ display: "flex",
557
+ flexDirection: "column",
558
+ },
559
+ }, (0, h_1.h)("div", {
560
+ style: {
561
+ display: "flex",
562
+ fontSize: 14,
563
+ fontWeight: 500,
564
+ color: theme.inkMuted,
565
+ marginBottom: 8,
566
+ },
567
+ }, label), (0, h_1.h)("div", {
568
+ style: {
569
+ display: "flex",
570
+ fontSize: 30,
571
+ lineHeight: 1.1,
572
+ fontWeight: 700,
573
+ color: valueColor,
574
+ },
575
+ }, value));
576
+ }
577
+ function buildStatSeparator(theme) {
578
+ return (0, h_1.h)("div", {
579
+ style: {
580
+ width: 1,
581
+ height: 28,
582
+ display: "flex",
583
+ background: theme.border,
584
+ margin: "0 18px",
585
+ flexShrink: 0,
586
+ },
587
+ });
588
+ }
589
+ function buildStatRow(input, theme, pnlValue, roiValue, bestMultValue) {
590
+ return (0, h_1.h)("div", {
591
+ style: {
592
+ display: "flex",
593
+ alignItems: "center",
594
+ marginTop: 20,
595
+ padding: "0 4px",
596
+ },
597
+ }, buildStatItem("P&L", pnlValue, input.hideAmounts
598
+ ? theme.inkMuted
599
+ : getSignedValueColor(input.totalPnl, theme), theme), buildStatSeparator(theme), buildStatItem("ROI", roiValue, getSignedValueColor(input.roi, theme), theme), buildStatSeparator(theme), buildStatItem("Best call", bestMultValue, theme.primary, theme));
600
+ }
232
601
  function buildChartArea(theme, btcHistory, positions) {
233
602
  const normalizedPositions = normalizePositions(positions);
234
- const renderablePositions = normalizedPositions.filter((position) => position.status !== "CLOSED");
603
+ const renderablePositions = capRenderablePositions(normalizedPositions);
235
604
  const sortedHistory = [...btcHistory].sort((a, b) => a.t - b.t);
236
605
  if (btcHistory.length === 0 && renderablePositions.length === 0) {
237
606
  return (0, h_1.h)("div", {
@@ -281,12 +650,40 @@ function buildChartArea(theme, btcHistory, positions) {
281
650
  return PLOT_TOP + plotHeight / 2;
282
651
  }
283
652
  return (PLOT_TOP +
284
- (1 -
285
- (value - priceBounds.min) / (priceBounds.max - priceBounds.min)) *
653
+ (1 - (value - priceBounds.min) / (priceBounds.max - priceBounds.min)) *
286
654
  plotHeight);
287
655
  };
288
656
  const priceTicks = getPriceTicks(priceBounds.min, priceBounds.max, priceBounds.step);
289
657
  const hasOpenPositions = renderablePositions.some((position) => position.status === "OPEN");
658
+ const latestSettlement = renderablePositions
659
+ .filter((position) => (position.status === "WIN" || position.status === "LOSS") &&
660
+ position.settlementPrice != null)
661
+ .sort((a, b) => b.marketEndTimestampMs - a.marketEndTimestampMs)[0];
662
+ const groupSizes = new Map();
663
+ const groupOrdinals = new Map();
664
+ renderablePositions.forEach((position) => {
665
+ const timestamp = position.marketEndTimestampMs;
666
+ const ordinal = groupSizes.get(timestamp) ?? 0;
667
+ groupOrdinals.set(position, ordinal);
668
+ groupSizes.set(timestamp, ordinal + 1);
669
+ });
670
+ const dayWidth = timeMin === timeMax ? 0 : (DAY_MS / (timeMax - timeMin)) * plotWidth;
671
+ const getBandX = (position) => {
672
+ const groupSize = groupSizes.get(position.marketEndTimestampMs) ?? 1;
673
+ const ordinal = groupOrdinals.get(position) ?? 0;
674
+ const center = scaleX(position.marketEndTimestampMs);
675
+ const spreadWidth = groupSize <= 1
676
+ ? 0
677
+ : timeMin === timeMax
678
+ ? plotWidth / 4
679
+ : Math.max(dayWidth, BAND_WIDTH * (groupSize + 1));
680
+ const offset = groupSize <= 1
681
+ ? 0
682
+ : (ordinal - (groupSize - 1) / 2) * (spreadWidth / groupSize);
683
+ const rawX = center + offset - BAND_WIDTH / 2;
684
+ return clamp(rawX, PLOT_LEFT, PLOT_LEFT + plotWidth - BAND_WIDTH);
685
+ };
686
+ const getBandCenterX = (position) => getBandX(position) + BAND_WIDTH / 2;
290
687
  const svgChildren = [];
291
688
  if (hasOpenPositions) {
292
689
  svgChildren.push((0, h_1.h)("defs", null, (0, h_1.h)("pattern", {
@@ -300,7 +697,7 @@ function buildChartArea(theme, btcHistory, positions) {
300
697
  strokeWidth: 2,
301
698
  }))));
302
699
  }
303
- priceTicks.forEach((tick) => {
700
+ priceTicks.ticks.forEach((tick) => {
304
701
  const y = scaleY(tick);
305
702
  svgChildren.push((0, h_1.h)("line", {
306
703
  x1: PLOT_LEFT,
@@ -312,10 +709,23 @@ function buildChartArea(theme, btcHistory, positions) {
312
709
  strokeWidth: 1,
313
710
  }));
314
711
  });
712
+ if (latestSettlement) {
713
+ const y = scaleY(latestSettlement.settlementPrice);
714
+ svgChildren.push((0, h_1.h)("line", {
715
+ x1: PLOT_LEFT,
716
+ x2: PLOT_LEFT + plotWidth,
717
+ y1: y,
718
+ y2: y,
719
+ stroke: theme.bitcoin,
720
+ strokeDasharray: "4 5",
721
+ strokeWidth: 1.5,
722
+ strokeOpacity: 0.75,
723
+ }));
724
+ }
315
725
  renderablePositions.forEach((position) => {
316
726
  const topPrice = Math.max(position.lowerPrice, position.upperPrice);
317
727
  const bottomPrice = Math.min(position.lowerPrice, position.upperPrice);
318
- const x = scaleX(position.marketEndTimestampMs) - BAND_WIDTH / 2;
728
+ const x = getBandX(position);
319
729
  const top = scaleY(topPrice);
320
730
  const bottom = scaleY(bottomPrice);
321
731
  const height = Math.max(bottom - top, 6);
@@ -327,13 +737,22 @@ function buildChartArea(theme, btcHistory, positions) {
327
737
  height,
328
738
  rx: 6,
329
739
  fill: "url(#profile-open-band)",
740
+ opacity: 0.4,
330
741
  stroke: theme.bitcoin,
331
742
  strokeWidth: 2,
332
743
  }));
333
744
  return;
334
745
  }
335
- const fill = position.status === "WIN" ? theme.primary : theme.negative;
336
- const fillOpacity = position.status === "WIN" ? 0.55 : 0.28;
746
+ const fill = position.status === "WIN"
747
+ ? theme.primary
748
+ : position.status === "LOSS"
749
+ ? theme.negative
750
+ : theme.inkMuted;
751
+ const fillOpacity = position.status === "WIN"
752
+ ? 0.45
753
+ : position.status === "LOSS"
754
+ ? 0.28
755
+ : 0.3;
337
756
  svgChildren.push((0, h_1.h)("rect", {
338
757
  x,
339
758
  y: top,
@@ -343,7 +762,7 @@ function buildChartArea(theme, btcHistory, positions) {
343
762
  fill,
344
763
  fillOpacity,
345
764
  stroke: fill,
346
- strokeOpacity: 0.9,
765
+ strokeOpacity: position.status === "CLOSED" ? 0.55 : 0.9,
347
766
  }));
348
767
  });
349
768
  if (sortedHistory.length === 1) {
@@ -368,14 +787,14 @@ function buildChartArea(theme, btcHistory, positions) {
368
787
  if ((position.status === "WIN" || position.status === "LOSS") &&
369
788
  position.settlementPrice != null) {
370
789
  svgChildren.push((0, h_1.h)("circle", {
371
- cx: scaleX(position.marketEndTimestampMs),
790
+ cx: getBandCenterX(position),
372
791
  cy: scaleY(position.settlementPrice),
373
792
  r: 4.5,
374
793
  fill: position.status === "WIN" ? theme.primary : theme.negative,
375
794
  }));
376
795
  }
377
796
  });
378
- const labelNodes = priceTicks.map((tick) => (0, h_1.h)("div", {
797
+ const labelNodes = priceTicks.ticks.map((tick) => (0, h_1.h)("div", {
379
798
  style: {
380
799
  position: "absolute",
381
800
  left: 12,
@@ -387,7 +806,26 @@ function buildChartArea(theme, btcHistory, positions) {
387
806
  background: theme.card,
388
807
  padding: "0 4px",
389
808
  },
390
- }, formatAxisLabel(tick)));
809
+ }, formatAxisLabel(tick, priceTicks.step)));
810
+ const settlementLabel = latestSettlement == null
811
+ ? []
812
+ : [
813
+ (0, h_1.h)("div", {
814
+ style: {
815
+ position: "absolute",
816
+ left: PLOT_LEFT + plotWidth - 64,
817
+ top: scaleY(latestSettlement.settlementPrice) - 14,
818
+ display: "flex",
819
+ fontSize: 14,
820
+ fontWeight: 700,
821
+ color: theme.bitcoin,
822
+ background: theme.card,
823
+ border: `1px solid ${theme.bitcoin}`,
824
+ borderRadius: 999,
825
+ padding: "4px 10px",
826
+ },
827
+ }, formatAxisLabel(latestSettlement.settlementPrice, priceTicks.step)),
828
+ ];
391
829
  return (0, h_1.h)("div", {
392
830
  style: {
393
831
  width: CHART_WIDTH,
@@ -395,9 +833,6 @@ function buildChartArea(theme, btcHistory, positions) {
395
833
  display: "flex",
396
834
  position: "relative",
397
835
  overflow: "hidden",
398
- background: theme.neutralBg,
399
- border: `1px solid ${theme.border}`,
400
- borderRadius: 20,
401
836
  },
402
837
  }, (0, h_1.h)("svg", {
403
838
  width: CHART_WIDTH,
@@ -409,43 +844,15 @@ function buildChartArea(theme, btcHistory, positions) {
409
844
  display: "flex",
410
845
  pointerEvents: "none",
411
846
  },
412
- }, ...svgChildren), ...labelNodes);
413
- }
414
- function buildStatCard(label, value, valueColor, theme) {
415
- return (0, h_1.h)("div", {
416
- style: {
417
- flex: 1,
418
- display: "flex",
419
- flexDirection: "column",
420
- padding: "16px 18px",
421
- borderRadius: 18,
422
- border: `1px solid ${theme.border}`,
423
- background: theme.neutralBg,
424
- },
425
- }, (0, h_1.h)("div", {
426
- style: {
427
- display: "flex",
428
- fontSize: 14,
429
- fontWeight: 500,
430
- color: theme.inkMuted,
431
- marginBottom: 10,
432
- },
433
- }, label), (0, h_1.h)("div", {
434
- style: {
435
- display: "flex",
436
- fontSize: 28,
437
- lineHeight: 1.1,
438
- fontWeight: 700,
439
- color: valueColor,
440
- },
441
- }, value));
847
+ }, ...svgChildren), ...labelNodes, ...settlementLabel);
442
848
  }
443
849
  function buildProfileVNode(input) {
444
850
  const theme = OG_THEMES[input.theme];
445
- const accent = TIER_ACCENT_COLORS[input.tier];
446
851
  const mainText = buildMainText(input);
447
852
  const subline = buildSubline(input);
448
- const pnlValue = input.hideAmounts ? "••••" : formatProfilePnl(input.totalPnl);
853
+ const pnlValue = input.hideAmounts
854
+ ? "••••"
855
+ : formatProfilePnl(input.totalPnl);
449
856
  const roiValue = formatProfileRoi(input.roi);
450
857
  const bestMultValue = formatBestMult(input.bestMult);
451
858
  return (0, h_1.h)("div", {
@@ -467,79 +874,18 @@ function buildProfileVNode(input) {
467
874
  border: `1px solid ${theme.border}`,
468
875
  borderRadius: 24,
469
876
  },
470
- }, (0, h_1.h)("div", {
471
- style: {
472
- display: "flex",
473
- alignItems: "center",
474
- justifyContent: "space-between",
475
- },
476
- }, (0, h_1.h)("div", {
477
- style: {
478
- display: "flex",
479
- alignItems: "center",
480
- gap: 10,
481
- },
482
- }, (0, h_1.h)("img", {
483
- src: assets_1.SIGNALS_LOGO_URI,
484
- width: 34,
485
- height: 32,
486
- style: { display: "flex" },
487
- }), (0, h_1.h)("div", {
488
- style: {
489
- display: "flex",
490
- fontSize: 28,
491
- fontWeight: 700,
492
- color: theme.primary,
493
- },
494
- }, "Signals")), (0, h_1.h)("div", {
495
- style: {
496
- display: "flex",
497
- fontSize: 18,
498
- fontWeight: 700,
499
- letterSpacing: "0.04em",
500
- textTransform: "uppercase",
501
- color: accent,
502
- },
503
- }, input.tier)), (0, h_1.h)("div", {
877
+ }, buildHeader(input, theme, mainText, subline), (0, h_1.h)("div", {
504
878
  style: {
505
879
  flex: 1,
506
880
  display: "flex",
507
881
  flexDirection: "column",
508
- marginTop: 20,
509
- },
510
- }, (0, h_1.h)("div", {
511
- style: {
512
- display: "flex",
513
- flexDirection: "column",
882
+ marginTop: 22,
514
883
  },
515
884
  }, (0, h_1.h)("div", {
516
885
  style: {
517
886
  display: "flex",
518
- fontSize: 52,
519
- lineHeight: 1.05,
520
- fontWeight: 700,
521
- color: theme.ink,
522
- marginBottom: 10,
523
- },
524
- }, mainText), (0, h_1.h)("div", {
525
- style: {
526
- display: "flex",
527
- fontSize: 20,
528
- fontWeight: 500,
529
- color: theme.inkMuted,
530
- },
531
- }, subline)), (0, h_1.h)("div", {
532
- style: {
533
- display: "flex",
534
- marginTop: 24,
535
- },
536
- }, buildChartArea(theme, input.btcHistory, input.positions)), (0, h_1.h)("div", {
537
- style: {
538
- display: "flex",
539
- gap: 16,
540
- marginTop: 20,
541
887
  },
542
- }, buildStatCard("P&L", pnlValue, getSignedValueColor(input.totalPnl, theme), theme), buildStatCard("ROI", roiValue, getSignedValueColor(input.roi, theme), theme), buildStatCard("Best call", bestMultValue, theme.primary, theme)), (0, h_1.h)("div", {
888
+ }, buildChartArea(theme, input.btcHistory, input.positions)), buildStatRow(input, theme, pnlValue, roiValue, bestMultValue), (0, h_1.h)("div", {
543
889
  style: {
544
890
  display: "flex",
545
891
  justifyContent: "flex-end",
@@ -30,7 +30,22 @@ export interface ProfileChartPosition {
30
30
  marketEndTimestampMs: number;
31
31
  }
32
32
  export interface ProfileImageInput {
33
+ /**
34
+ * User-facing display fallback used when `realHandle` is empty and
35
+ * `hideWallet` is false. This may include an address fallback supplied by
36
+ * legacy callers; it is never shown while `hideWallet` is true.
37
+ */
33
38
  displayName: string | null;
39
+ /**
40
+ * Real, human-friendly handle (e.g. Namoshi name) that is safe to show
41
+ * even when `hideWallet` is true.
42
+ *
43
+ * Pass null when no real handle exists. Do not pass a truncated address
44
+ * fallback here; that belongs in `displayName`. Optional for backward
45
+ * compatibility, so callers that omit it get the existing hideWallet
46
+ * fallback semantics.
47
+ */
48
+ realHandle?: string | null;
34
49
  address: string;
35
50
  twitter: string | null;
36
51
  tier: "BRONZE" | "SILVER" | "GOLD" | "PLATINUM" | "DIAMOND";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signals-protocol/v1-sdk",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Signals v1 SDK for CLMSR market calculations and utilities",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -11,8 +11,6 @@
11
11
  "test": "jest",
12
12
  "test:cov": "jest --coverage",
13
13
  "test:watch": "jest --watch",
14
- "npm:auth": "node scripts/write-npmrc.js",
15
- "publish:package": "NPM_CONFIG_USERCONFIG=.npmrc.auth npm publish",
16
14
  "prepack": "yarn rebuild",
17
15
  "lint": "eslint src/**/*.ts",
18
16
  "format": "prettier --write src/**/*.ts",
@@ -38,6 +36,7 @@
38
36
  "eslint": "^8.0.0",
39
37
  "jest": "^29.5.0",
40
38
  "prettier": "^3.0.0",
39
+ "semver": "^7.7.3",
41
40
  "ts-jest": "^29.1.0",
42
41
  "typedoc": "^0.28.10",
43
42
  "typedoc-docusaurus-theme": "^1.4.2",
@@ -52,7 +51,7 @@
52
51
  ],
53
52
  "repository": {
54
53
  "type": "git",
55
- "url": "git+https://github.com/signals-protocol/signals.git"
54
+ "url": "git+https://github.com/signals-protocol/v1-sdk.git"
56
55
  },
57
56
  "exports": {
58
57
  ".": {