@microslop/ping-directory-sdk 0.1.5

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.

Potentially problematic release.


This version of @microslop/ping-directory-sdk might be problematic. Click here for more details.

@@ -0,0 +1,103 @@
1
+ // Constants mirroring `programs/ping-directory/src/{lib,utils/message_tags,state/*}.rs`.
2
+ // Bumping any value here = on-chain breaking change.
3
+
4
+ import { PublicKey } from '@solana/web3.js';
5
+
6
+ export const PROGRAM_ID = new PublicKey('638WoGuZRYWBasJeeX4kkd4kdUB4MQ68K9P73GhBXduE');
7
+ export const ED25519_PROGRAM = new PublicKey('Ed25519SigVerify111111111111111111111111111');
8
+ export const SYSTEM_PROGRAM = new PublicKey('11111111111111111111111111111111');
9
+ export const SYSVAR_INSTRUCTIONS = new PublicKey('Sysvar1nstructions1111111111111111111111111');
10
+
11
+ // Clusters this program is deployed to. (Callers can still pass their own
12
+ // Connection/rpcUrl; these are convenience defaults — see PingDirectory's
13
+ // `cluster` option.)
14
+ // • LOCALNET — shared dev localnet (solana-test-validator behind a reverse proxy).
15
+ // • MAINNET — not live yet (not deployed to mainnet-beta as of writing).
16
+ export const Cluster = Object.freeze({
17
+ LOCALNET: 'https://sol-net.local.microslop.software',
18
+ MAINNET: 'https://api.mainnet-beta.solana.com',
19
+ });
20
+
21
+ // PubSub WebSocket endpoints for clusters whose WS is NOT where web3.js derives
22
+ // it (wss://<rpc-host>/). The localnet's PubSub is reverse-proxied at `/ws`;
23
+ // mainnet derives correctly so it has no entry. PingDirectory applies these
24
+ // automatically when you pass `cluster` (or a matching `rpcUrl`).
25
+ export const ClusterWs = Object.freeze({
26
+ LOCALNET: 'wss://sol-net.local.microslop.software/ws',
27
+ });
28
+
29
+ // Default cluster when a caller specifies neither `cluster` nor `rpcUrl`.
30
+ // Localnet during dev — flip to 'MAINNET' at launch.
31
+ export const DEFAULT_CLUSTER = 'LOCALNET';
32
+ export const DEFAULT_RPC = Cluster[DEFAULT_CLUSTER];
33
+ export const DEFAULT_WS = ClusterWs[DEFAULT_CLUSTER];
34
+
35
+ // Re-exported for callers that need the seed string directly.
36
+ export { Seeds as SeedNames };
37
+
38
+ // ── Ed25519 message-tag discriminators (mirror utils/message_tags.rs) ──
39
+ export const MessageTags = Object.freeze({
40
+ REGISTER: 0x01,
41
+ UNREGISTER: 0x02,
42
+ UPDATE_PUBKEY: 0x03,
43
+ TRANSFER_USERNAME: 0x04,
44
+ CANCEL_SALE: 0x05,
45
+ REQUEST_UNLOCK: 0x06,
46
+ LOCK: 0x07,
47
+ SET_ACCOUNT_TYPE: 0x08,
48
+ // 0x09 is the on-chain tag for finalize_profile_photo. Two aliases for
49
+ // client clarity (both resolve to byte 0x09).
50
+ SET_PROFILE_PHOTO: 0x09,
51
+ FINALIZE_PROFILE_PHOTO: 0x09,
52
+ CLEAR_PROFILE_PHOTO: 0x0A,
53
+ EQUIP_ITEM: 0x0B,
54
+ UNEQUIP_SLOT: 0x0C,
55
+ EQUIP_PRO_DEFAULT: 0x13,
56
+ ATTACH_PUBKEY: 0x0D,
57
+ LIST_ACTIVE: 0x0E,
58
+ BUY_ACTIVE: 0x0F,
59
+ INIT_PROFILE_PHOTO: 0x10,
60
+ DISCARD_ITEM: 0x11,
61
+ MARK_COMPROMISED: 0x12,
62
+ });
63
+
64
+ // ── State discriminators ───────────────────────────────────────────────
65
+ export const UsernameState = Object.freeze({ RESERVED: 0, ACTIVE: 1, UNREGISTERED: 2 });
66
+ export const ListingKind = Object.freeze({ RESERVED: 0, ACTIVE: 1 });
67
+ export const GraceKind = Object.freeze({ WALLET: 0, ED25519: 1 });
68
+ export const PhotoMime = Object.freeze({ JPEG: 0, PNG: 1, WEBP: 2, GIF: 3 });
69
+ export const ItemKind = Object.freeze({ NAMEPLATE: 0, MESSAGE_BUBBLE: 1, NAME_FONT: 2, BADGE: 3 });
70
+ export const AccountType = Object.freeze({ USER: 0, GROUP: 1 });
71
+ export const UserIndexStatus = Object.freeze({ ACTIVE: 0, UNREGISTERED: 1 });
72
+
73
+ // ── PDA seed strings (mirror state/*.rs SEED constants) ────────────────
74
+ export const Seeds = Object.freeze({
75
+ CONFIG: 'config',
76
+ USERNAME: 'username',
77
+ REVERSE: 'reverse',
78
+ NONCE: 'nonce',
79
+ GRACE: 'grace',
80
+ SALE: 'sale',
81
+ REFERRAL: 'referral',
82
+ USER_INDEX: 'user_idx',
83
+ UID_MAP: 'uid_map',
84
+ ADMIN: 'admin',
85
+ TREASURY_WALLET: 'treasury_wallet',
86
+ MODERATION: 'moderation',
87
+ BLOCKED: 'blocked',
88
+ SHOP_ITEM: 'shop_item',
89
+ INVENTORY: 'inventory',
90
+ PROFILE_PHOTO: 'profile_photo',
91
+ ED25519: 'ed25519',
92
+ });
93
+
94
+ // ── Hard caps and policy constants ─────────────────────────────────────
95
+ export const MAX_USERNAME_LEN = 20;
96
+ export const MAX_INVENTORY = 64; // Inventory::MAX_OWNED
97
+ export const MAX_PHOTO_BYTES = 10_181; // ProfilePhoto::MAX_DATA_SIZE
98
+ export const MAX_PHOTO_DIM = 8192; // ProfilePhoto::MAX_DIMENSION
99
+ export const BPS_DENOMINATOR = 10_000;
100
+ export const REFERRAL_BPS = 1_000; // 10%
101
+
102
+ // ── Tx-fee + compute budget defaults ──────────────────────────────────
103
+ export const COMPUTE_BUDGET_PROGRAM = new PublicKey('ComputeBudget111111111111111111111111111111');
@@ -0,0 +1,322 @@
1
+ // Borsh deserializers for state accounts. Each strips the 8-byte
2
+ // Anchor discriminator and parses the remaining bytes per the layout
3
+ // in `programs/ping-directory/src/state/*.rs`.
4
+
5
+ import { PublicKey } from '@solana/web3.js';
6
+
7
+ class Reader {
8
+ constructor(bytes) {
9
+ this.b = bytes;
10
+ this.i = 0;
11
+ this.dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
12
+ }
13
+ /** True iff `n` more bytes remain to be read. Used by deserializers
14
+ * that want to gracefully accept accounts written under an older
15
+ * (shorter) schema — see `deserializeUsernameAccount` which treats
16
+ * the trailing `equippedBadge` field as optional for back-compat
17
+ * with pre-BADGE-slot accounts. */
18
+ has(n) { return this.i + n <= this.b.length; }
19
+ u8() { return this.b[this.i++]; }
20
+ bool() { return this.u8() !== 0; }
21
+ u16() { const v = this.dv.getUint16(this.i, true); this.i += 2; return v; }
22
+ u32() { const v = this.dv.getUint32(this.i, true); this.i += 4; return v; }
23
+ u64() { const v = this.dv.getBigUint64(this.i, true); this.i += 8; return v; }
24
+ i64() { const v = this.dv.getBigInt64(this.i, true); this.i += 8; return v; }
25
+ bytes(n) { const out = this.b.slice(this.i, this.i + n); this.i += n; return out; }
26
+ pubkey() { return new PublicKey(this.bytes(32)); }
27
+ pubkeyBytes() { return new Uint8Array(this.bytes(32)); }
28
+ string() { const n = this.u32(); return new TextDecoder().decode(this.bytes(n)); }
29
+ optionPubkey() {
30
+ const tag = this.u8();
31
+ if (tag === 0) return null;
32
+ if (tag === 1) return this.pubkey();
33
+ throw new Error(`Bad option tag ${tag}`);
34
+ }
35
+ optionU32() {
36
+ const tag = this.u8();
37
+ if (tag === 0) return null;
38
+ if (tag === 1) return this.u32();
39
+ throw new Error(`Bad option tag ${tag}`);
40
+ }
41
+ vecU32() {
42
+ const n = this.u32();
43
+ const out = new Array(n);
44
+ for (let i = 0; i < n; i++) out[i] = this.u32();
45
+ return out;
46
+ }
47
+ vecU8() {
48
+ const n = this.u32();
49
+ return this.bytes(n);
50
+ }
51
+ }
52
+
53
+ const stripDisc = (data) => new Uint8Array(data.slice(8));
54
+
55
+ // ── Config ──────────────────────────────────────────────────────────
56
+ export function deserializeConfig(data) {
57
+ const r = new Reader(stripDisc(data));
58
+ return {
59
+ owner: r.pubkey(),
60
+ registrationFees: Array.from({ length: 7 }, () => r.u64()),
61
+ proMonthlyPrice: r.u64(),
62
+ proLifetimePrice: r.u64(),
63
+ saleFeeBps: r.u16(),
64
+ minSalePrice: r.u64(),
65
+ gracePeriod: r.i64(),
66
+ unlockDelay: r.i64(),
67
+ unlockWindow: r.i64(),
68
+ autoUnfreezeDelay: r.i64(),
69
+ // Per-MIME init_profile_photo fee (lamports). Indexed by ProfilePhoto::MIME_*
70
+ // (0=jpeg, 1=png, 2=webp, 3=gif). Owner-tunable via set_profile_photo_fee.
71
+ profilePhotoFees: Array.from({ length: 4 }, () => r.u64()),
72
+ registrationPaused: r.bool(),
73
+ proPaused: r.bool(),
74
+ totalRegistered: r.u64(),
75
+ totalReferralPending: r.u64(),
76
+ nextUserId: r.u64(),
77
+ pendingOwner: r.pubkey(),
78
+ bump: r.u8(),
79
+ };
80
+ }
81
+
82
+ // ── UsernameAccount ─────────────────────────────────────────────────
83
+ //
84
+ // Source-of-truth for personal data: premium, equipped cosmetics,
85
+ // referrer, account_type live here (they survive key rotation, since
86
+ // state is keyed by username).
87
+ //
88
+ // `state` ∈ {0 = RESERVED, 1 = ACTIVE, 2 = UNREGISTERED}. UNREGISTERED
89
+ // is a tombstone: the PDA persists forever (paid once per name) and
90
+ // is reused by re-registration via `init_if_needed`.
91
+ export function deserializeUsernameAccount(data) {
92
+ const r = new Reader(stripDisc(data));
93
+ // Account schema version is encoded in the total allocation size
94
+ // (Anchor reserves the full struct SIZE at PDA init, so the byte
95
+ // length is fixed per-version regardless of which Option fields
96
+ // are populated). 172 bytes (stripped) = pre-BADGE-slot schema;
97
+ // 177 bytes = current schema with `equipped_badge`. Reading past
98
+ // the old layout's bump would consume the bump byte as the next
99
+ // Option tag and crash on "Bad option tag <bump value>".
100
+ const hasBadgeSlot = r.b.length >= 177;
101
+ return {
102
+ username: r.string(),
103
+ state: r.u8(),
104
+ wallet: r.pubkey(),
105
+ ed25519Pubkey: r.pubkeyBytes(),
106
+ referrer: r.optionPubkey(),
107
+ proUntil: r.i64(),
108
+ proLifetime: r.bool(),
109
+ registeredAt: r.i64(),
110
+ attachedAt: r.i64(),
111
+ accountType: r.u8(),
112
+ unlockRequestedAt: r.i64(),
113
+ equippedNameplate: r.optionU32(),
114
+ equippedBubble: r.optionU32(),
115
+ equippedNameFont: r.optionU32(),
116
+ // Custom badge equipped from the shop. When null AND the username is
117
+ // premium, clients should render the implicit "default premium badge".
118
+ // See `isPro(usernameAccount, nowSec)` for the activation check.
119
+ //
120
+ // Back-compat: pre-BADGE-slot accounts end immediately after
121
+ // `equippedNameFont` + `bump`. Use the schema-version flag computed
122
+ // from `r.b.length` above (177+ bytes = new layout includes the
123
+ // badge slot). A buffer-remaining-bytes heuristic doesn't work
124
+ // because Anchor zero-pads to the full reserved size, so trailing
125
+ // padding looks identical to a populated badge slot until the
126
+ // option tag is checked.
127
+ equippedBadge: hasBadgeSlot ? r.optionU32() : null,
128
+ bump: r.u8(),
129
+ };
130
+ }
131
+
132
+ // True iff the username currently has premium — lifetime-granted (admin/owner
133
+ // flag, see `set_pro`) or within paid time (`subscribe_pro` extended
134
+ // `pro_until`). Mirrors the on-chain `UsernameAccount::is_pro` helper.
135
+ //
136
+ // `usernameAccount` is a result of `deserializeUsernameAccount` (or
137
+ // `sdk.fetchUsername`). `nowSec` is a unix-ts in seconds; defaults to wall
138
+ // clock. Passes through `BigInt` comparisons so the caller can stay BigInt
139
+ // throughout.
140
+ //
141
+ // UX note: clients use this to decide whether to render the implicit
142
+ // "default premium" cosmetic in any empty equipped slot
143
+ // (`equippedNameplate / Bubble / NameFont / Badge`).
144
+ export function isPro(usernameAccount, nowSec = Math.floor(Date.now() / 1000)) {
145
+ if (!usernameAccount) return false;
146
+ if (usernameAccount.proLifetime) return true;
147
+ const now = typeof nowSec === 'bigint' ? nowSec : BigInt(nowSec);
148
+ const until = typeof usernameAccount.proUntil === 'bigint'
149
+ ? usernameAccount.proUntil
150
+ : BigInt(usernameAccount.proUntil ?? 0);
151
+ return until > now;
152
+ }
153
+
154
+ // ── Ed25519Account (bound username + compromised flag) ──────────────
155
+ //
156
+ // `boundUsername` is `string | null` (the username currently controlled
157
+ // by this key, or null when not bound).
158
+ // `compromisedAt` is a BigInt unix-ts. `0n` = not marked. One-way.
159
+ export function deserializeEd25519Account(data) {
160
+ const r = new Reader(stripDisc(data));
161
+ // bound_username: Option<String>
162
+ const tag = r.u8();
163
+ let boundUsername = null;
164
+ if (tag === 1) {
165
+ boundUsername = r.string();
166
+ } else if (tag !== 0) {
167
+ throw new Error(`Bad option tag ${tag}`);
168
+ }
169
+ return {
170
+ boundUsername,
171
+ compromisedAt: r.i64(),
172
+ bump: r.u8(),
173
+ };
174
+ }
175
+
176
+ // ── SaleListing ─────────────────────────────────────────────────────
177
+ export function deserializeSaleListing(data) {
178
+ const r = new Reader(stripDisc(data));
179
+ return {
180
+ price: r.u64(),
181
+ receiverWallet: r.pubkey(),
182
+ sellerKind: r.u8(),
183
+ listNonce: new Uint8Array(r.bytes(16)),
184
+ bump: r.u8(),
185
+ };
186
+ }
187
+
188
+ // ── GracePeriod ─────────────────────────────────────────────────────
189
+ export function deserializeGracePeriod(data) {
190
+ const r = new Reader(stripDisc(data));
191
+ return {
192
+ originalKind: r.u8(),
193
+ originalPubkey: r.pubkeyBytes(),
194
+ until: r.i64(),
195
+ bump: r.u8(),
196
+ };
197
+ }
198
+
199
+ // ── ReferralBalance ─────────────────────────────────────────────────
200
+ export function deserializeReferralBalance(data) {
201
+ const r = new Reader(stripDisc(data));
202
+ return { balance: r.u64(), bump: r.u8() };
203
+ }
204
+
205
+ // ── ProfilePhoto ────────────────────────────────────────────────────
206
+ export function deserializeProfilePhoto(data) {
207
+ const r = new Reader(stripDisc(data));
208
+ return {
209
+ finalized: r.bool(),
210
+ mime: r.u8(),
211
+ width: r.u16(),
212
+ height: r.u16(),
213
+ setAt: r.i64(),
214
+ initPayer: r.pubkey(),
215
+ data: r.vecU8(),
216
+ bump: r.u8(),
217
+ };
218
+ }
219
+
220
+ // Header-only decode — everything before the variable `data` blob. Pair
221
+ // with a `dataSlice` fetch so callers can read mime/dims/setAt without
222
+ // pulling the up-to-10KB image. `setAt` is the cheap change signal: it
223
+ // bumps on every finalize, so a client can decide whether to refetch the
224
+ // full photo. (The cryptographic hash is NOT on the account — it lives
225
+ // only in the ProfilePhotoSet event / the indexer.)
226
+ export const PROFILE_PHOTO_META_LEN = 54; // 8 disc + 1+1+2+2+8 + 32 init_payer
227
+ export function deserializeProfilePhotoMeta(data) {
228
+ const r = new Reader(stripDisc(data));
229
+ return {
230
+ finalized: r.bool(),
231
+ mime: r.u8(),
232
+ width: r.u16(),
233
+ height: r.u16(),
234
+ setAt: r.i64(),
235
+ initPayer: r.pubkey(),
236
+ };
237
+ }
238
+
239
+ // ── Inventory ───────────────────────────────────────────────────────
240
+ export function deserializeInventory(data) {
241
+ const r = new Reader(stripDisc(data));
242
+ return { ownedItems: r.vecU32(), bump: r.u8() };
243
+ }
244
+
245
+ // ── ShopItem ────────────────────────────────────────────────────────
246
+ export function deserializeShopItem(data) {
247
+ const r = new Reader(stripDisc(data));
248
+ const out = {
249
+ id: r.u32(),
250
+ kind: r.u8(),
251
+ price: r.u64(),
252
+ active: r.bool(),
253
+ metadata: r.string(),
254
+ bump: r.u8(),
255
+ };
256
+ // `pro_only` was appended after launch. ShopItem accounts written
257
+ // before the Pro-purchase gate lack the trailing byte — decode them
258
+ // as `false` (freely purchasable) rather than throwing.
259
+ out.proOnly = r.has(1) ? r.bool() : false;
260
+ return out;
261
+ }
262
+
263
+ // ── Admin ───────────────────────────────────────────────────────────
264
+ export function deserializeAdmin(data) {
265
+ const r = new Reader(stripDisc(data));
266
+ return { bump: r.u8() };
267
+ }
268
+
269
+ // ── TreasuryWallet ──────────────────────────────────────────────────
270
+ // Existence of the PDA at [b"treasury_wallet", pubkey] = the address may
271
+ // withdraw the treasury. Empty struct (just a bump), mirrors Admin.
272
+ export function deserializeTreasuryWallet(data) {
273
+ const r = new Reader(stripDisc(data));
274
+ return { bump: r.u8() };
275
+ }
276
+
277
+ // ── Blocklist ───────────────────────────────────────────────────────
278
+ export function deserializeBlocklist(data) {
279
+ const r = new Reader(stripDisc(data));
280
+ return { bump: r.u8() };
281
+ }
282
+
283
+ // ── Moderation (companion visibility PDA) ───────────────────────────
284
+ // Lives at [b"moderation", username]. ABSENCE of the PDA = fully
285
+ // visible (the deserializer is only called when the PDA exists). The
286
+ // stored flags are `*_hidden`; callers usually want the inverse
287
+ // (`visible`) — `PingDirectory.fetchVisibility` does that mapping.
288
+ export function deserializeModeration(data) {
289
+ const r = new Reader(stripDisc(data));
290
+ return {
291
+ usernameHidden: r.bool(),
292
+ pfpHidden: r.bool(),
293
+ updatedAt: r.i64(),
294
+ updatedBy: r.pubkey(),
295
+ bump: r.u8(),
296
+ };
297
+ }
298
+
299
+ // ── Nonce ───────────────────────────────────────────────────────────
300
+ export function deserializeNonce(data) {
301
+ const r = new Reader(stripDisc(data));
302
+ return { bump: r.u8() };
303
+ }
304
+
305
+ // ── UserIndex ───────────────────────────────────────────────────────
306
+ export function deserializeUserIndex(data) {
307
+ const r = new Reader(stripDisc(data));
308
+ return {
309
+ username: r.string(),
310
+ originalEd25519Pubkey: r.pubkeyBytes(),
311
+ registeredAt: r.i64(),
312
+ status: r.u8(),
313
+ bump: r.u8(),
314
+ };
315
+ }
316
+
317
+ // ── UsernameUserIdMap ───────────────────────────────────────────────
318
+ export function deserializeUsernameUserIdMap(data) {
319
+ const r = new Reader(stripDisc(data));
320
+ return { userId: r.u64(), bump: r.u8() };
321
+ }
322
+
package/src/disc.js ADDED
@@ -0,0 +1,76 @@
1
+ // Anchor discriminator helpers.
2
+ //
3
+ // Instruction disc = sha256("global:<snake_case_name>")[0..8]
4
+ // Account disc = sha256("account:<PascalCaseStructName>")[0..8]
5
+ //
6
+ // Inline SHA-256 (pure JS, sync) so the SDK works in browser + Node
7
+ // without `node:crypto`. Implementation follows FIPS 180-4.
8
+
9
+ const K = new Uint32Array([
10
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
11
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
12
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
13
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
14
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
15
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
16
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
17
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
18
+ ]);
19
+
20
+ /** Sync SHA-256 of a string or `Uint8Array`. Returns 32-byte `Uint8Array`.
21
+ * Pure-JS so it works in any runtime (browser without secure-context,
22
+ * Node, Tauri WebView, etc.) — no `crypto.subtle` / `node:crypto` dep. */
23
+ export function sha256(input) {
24
+ const bytes = typeof input === 'string' ? new TextEncoder().encode(input) : input;
25
+ const len = bytes.length;
26
+ const padLen = ((len + 9 + 63) & ~63) - len;
27
+ const buf = new Uint8Array(len + padLen);
28
+ buf.set(bytes);
29
+ buf[len] = 0x80;
30
+ const bitLen = BigInt(len) * 8n;
31
+ const dv = new DataView(buf.buffer);
32
+ dv.setUint32(buf.length - 8, Number(bitLen >> 32n) >>> 0, false);
33
+ dv.setUint32(buf.length - 4, Number(bitLen & 0xffffffffn) >>> 0, false);
34
+
35
+ let h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, h3 = 0xa54ff53a;
36
+ let h4 = 0x510e527f, h5 = 0x9b05688c, h6 = 0x1f83d9ab, h7 = 0x5be0cd19;
37
+
38
+ const w = new Uint32Array(64);
39
+ for (let i = 0; i < buf.length; i += 64) {
40
+ for (let t = 0; t < 16; t++) w[t] = dv.getUint32(i + t * 4, false);
41
+ for (let t = 16; t < 64; t++) {
42
+ const s0 = ((w[t-15] >>> 7) | (w[t-15] << 25)) ^ ((w[t-15] >>> 18) | (w[t-15] << 14)) ^ (w[t-15] >>> 3);
43
+ const s1 = ((w[t-2] >>> 17) | (w[t-2] << 15)) ^ ((w[t-2] >>> 19) | (w[t-2] << 13)) ^ (w[t-2] >>> 10);
44
+ w[t] = (w[t-16] + s0 + w[t-7] + s1) >>> 0;
45
+ }
46
+ let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7;
47
+ for (let t = 0; t < 64; t++) {
48
+ const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
49
+ const ch = (e & f) ^ (~e & g);
50
+ const t1 = (h + S1 + ch + K[t] + w[t]) >>> 0;
51
+ const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
52
+ const mj = (a & b) ^ (a & c) ^ (b & c);
53
+ const t2 = (S0 + mj) >>> 0;
54
+ h = g; g = f; f = e; e = (d + t1) >>> 0;
55
+ d = c; c = b; b = a; a = (t1 + t2) >>> 0;
56
+ }
57
+ h0 = (h0 + a) >>> 0; h1 = (h1 + b) >>> 0; h2 = (h2 + c) >>> 0; h3 = (h3 + d) >>> 0;
58
+ h4 = (h4 + e) >>> 0; h5 = (h5 + f) >>> 0; h6 = (h6 + g) >>> 0; h7 = (h7 + h) >>> 0;
59
+ }
60
+
61
+ const out = new Uint8Array(32);
62
+ const odv = new DataView(out.buffer);
63
+ odv.setUint32(0, h0, false); odv.setUint32(4, h1, false);
64
+ odv.setUint32(8, h2, false); odv.setUint32(12, h3, false);
65
+ odv.setUint32(16, h4, false); odv.setUint32(20, h5, false);
66
+ odv.setUint32(24, h6, false); odv.setUint32(28, h7, false);
67
+ return out;
68
+ }
69
+
70
+ export function ixDisc(name) {
71
+ return sha256(`global:${name}`).slice(0, 8);
72
+ }
73
+
74
+ export function accDisc(name) {
75
+ return sha256(`account:${name}`).slice(0, 8);
76
+ }
@@ -0,0 +1,130 @@
1
+ // Pure-JS encoding helpers for Borsh + ed25519 message construction.
2
+ // We don't depend on the `borsh` package — the program's structs are
3
+ // simple enough that hand-rolled serialization is clearer and avoids the
4
+ // "what version of borsh" hazard.
5
+
6
+ import nacl from 'tweetnacl';
7
+ import { Ed25519Program, PublicKey } from '@solana/web3.js';
8
+
9
+ // ── Endian-aware integer encoders ───────────────────────────────────
10
+ export function u8(n) {
11
+ const b = new Uint8Array(1); b[0] = n & 0xff; return b;
12
+ }
13
+ export function u16LE(n) {
14
+ const b = new Uint8Array(2);
15
+ new DataView(b.buffer).setUint16(0, n, true);
16
+ return b;
17
+ }
18
+ export function u32LE(n) {
19
+ const b = new Uint8Array(4);
20
+ new DataView(b.buffer).setUint32(0, n, true);
21
+ return b;
22
+ }
23
+ export function u64LE(n) {
24
+ const b = new Uint8Array(8);
25
+ const v = BigInt(n);
26
+ new DataView(b.buffer).setBigUint64(0, v, true);
27
+ return b;
28
+ }
29
+ export function i64LE(n) {
30
+ const b = new Uint8Array(8);
31
+ new DataView(b.buffer).setBigInt64(0, BigInt(n), true);
32
+ return b;
33
+ }
34
+
35
+ // ── Borsh helpers ──────────────────────────────────────────────────
36
+ // Borsh string = u32 length prefix (little-endian) + UTF-8 bytes.
37
+ export function borshString(s) {
38
+ const utf = new TextEncoder().encode(s);
39
+ return concat(u32LE(utf.length), utf);
40
+ }
41
+ // Borsh Option<T> = 0 (None) or 1 + T (Some).
42
+ export function borshOptionPubkey(pk) {
43
+ if (pk == null) return new Uint8Array([0]);
44
+ return concat(new Uint8Array([1]), pkBytes(pk));
45
+ }
46
+ // Borsh Vec<u8> = u32 length prefix + bytes.
47
+ export function borshBytes(bytes) {
48
+ const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
49
+ return concat(u32LE(arr.length), arr);
50
+ }
51
+
52
+ // ── Pubkey helpers ─────────────────────────────────────────────────
53
+ //
54
+ // Duck-typed instead of `instanceof PublicKey` so we correctly handle
55
+ // `PublicKey` instances that came from a *different copy* of
56
+ // `@solana/web3.js` than the one bundled inside the SDK's own
57
+ // `node_modules`. That dual-instance hazard fires whenever a host app
58
+ // (or a script) hoists web3.js separately from the SDK — `instanceof`
59
+ // returns `false` for objects functionally identical to ours, the
60
+ // fallback path silently returns an empty `Uint8Array`, and the on-chain
61
+ // program rejects the malformed instruction with `InstructionDidNotDeserialize`.
62
+ //
63
+ // Accepted shapes:
64
+ // - any PublicKey-like with a `.toBytes()` method (from any web3.js copy)
65
+ // - a 32-byte `Uint8Array` (already-raw bytes)
66
+ // - `null` / `undefined` → 32 zero bytes (matches `Pubkey::default()`)
67
+ export function pkBytes(p) {
68
+ if (p == null) return new Uint8Array(32);
69
+ // Length-validate Uint8Array inputs so a non-32-byte buffer doesn't
70
+ // silently pass through and produce malformed ix bytes on chain. The
71
+ // old early-return `if (p instanceof Uint8Array) return p;` accepted
72
+ // any length; on-chain failure was `InstructionDidNotDeserialize`
73
+ // instead of a clear client-side error.
74
+ if (p instanceof Uint8Array) {
75
+ if (p.length !== 32) {
76
+ throw new Error(`pkBytes: Uint8Array must be 32 bytes, got ${p.length}`);
77
+ }
78
+ return p;
79
+ }
80
+ if (typeof p.toBytes === 'function') {
81
+ const b = p.toBytes();
82
+ if (b instanceof Uint8Array && b.length === 32) return b;
83
+ }
84
+ // ArrayBuffer / Array-like with numeric indices (be permissive but safe).
85
+ if (Array.isArray(p) || (p && typeof p.length === 'number')) {
86
+ const arr = new Uint8Array(p);
87
+ if (arr.length === 32) return arr;
88
+ }
89
+ throw new Error('pkBytes: expected a PublicKey-like or 32-byte Uint8Array');
90
+ }
91
+
92
+ // ── Concatenation ──────────────────────────────────────────────────
93
+ export function concat(...parts) {
94
+ let total = 0;
95
+ for (const p of parts) total += p.length;
96
+ const out = new Uint8Array(total);
97
+ let off = 0;
98
+ for (const p of parts) { out.set(p, off); off += p.length; }
99
+ return out;
100
+ }
101
+
102
+ // ── Random bytes (for nonces) ──────────────────────────────────────
103
+ export function randomBytes(n) {
104
+ return nacl.randomBytes(n);
105
+ }
106
+
107
+ // 16-byte nonce — match the program's Nonce PDA seed shape.
108
+ export function randomNonce() {
109
+ return nacl.randomBytes(16);
110
+ }
111
+
112
+ // ── Ed25519 signing + precompile ix ────────────────────────────────
113
+ //
114
+ // The program's `verify_ed25519_signature` scans the instructions sysvar
115
+ // for any preceding ed25519 precompile whose pubkey AND message match.
116
+ // We build a single precompile per signed handler call.
117
+ export function buildEd25519Ix(keyPair, message) {
118
+ const sig = nacl.sign.detached(message, keyPair.secretKey);
119
+ return Ed25519Program.createInstructionWithPublicKey({
120
+ publicKey: keyPair.publicKey,
121
+ message,
122
+ signature: sig,
123
+ });
124
+ }
125
+
126
+ // Tagged signed-message convenience: prepend the 1-byte instruction tag
127
+ // and concatenate the body parts (already Uint8Arrays).
128
+ export function signedMsg(tag, ...parts) {
129
+ return concat(new Uint8Array([tag]), ...parts);
130
+ }