@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,418 @@
1
+ // Identity instructions: reserve, register (atomic), attach_pubkey,
2
+ // transfer_reserved, unregister_reserved, update_pubkey, transfer_username,
3
+ // unregister_active, set_account_type.
4
+
5
+ import { TransactionInstruction, SystemProgram } from '@solana/web3.js';
6
+ import { PROGRAM_ID, SYSVAR_INSTRUCTIONS, MessageTags } from '../constants.js';
7
+ import {
8
+ borshString, borshOptionPubkey, u8, u64LE, randomNonce,
9
+ buildEd25519Ix, signedMsg, pkBytes, concat,
10
+ } from '../encoding.js';
11
+ import { ixDisc } from '../disc.js';
12
+ import {
13
+ findConfigPDA, findUsernamePDA, findEd25519AccountPDA, findNoncePDA,
14
+ findGracePeriodPDA, findReferralBalancePDA, findUserIndexPDA, findUidMapPDA,
15
+ findBlocklistPDA, findSaleListingPDA, findProfilePhotoPDA, findAdminPDA,
16
+ } from '../pda.js';
17
+
18
+ const k = (pubkey, isSigner, isWritable) => ({ pubkey, isSigner, isWritable });
19
+
20
+ // ── reserve_username ─────────────────────────────────────────────────
21
+ export function reserveUsername({ username, accountType = 0, referrer = null, expectedUserId, payer }) {
22
+ const data = concat(
23
+ ixDisc('reserve_username'),
24
+ borshString(username),
25
+ u8(accountType),
26
+ borshOptionPubkey(referrer),
27
+ u64LE(expectedUserId),
28
+ );
29
+ const [configPda] = findConfigPDA();
30
+ const [usernamePda] = findUsernamePDA(username);
31
+ const [userIdxPda] = findUserIndexPDA(expectedUserId);
32
+ const [uidMapPda] = findUidMapPDA(username);
33
+ const [referralPda] = findReferralBalancePDA(referrer);
34
+ const [blocklistPda] = findBlocklistPDA(username);
35
+ const [graceCheck] = findGracePeriodPDA(username);
36
+
37
+ const ix = new TransactionInstruction({
38
+ programId: PROGRAM_ID,
39
+ keys: [
40
+ k(configPda, false, true),
41
+ k(usernamePda, false, true),
42
+ k(userIdxPda, false, true),
43
+ k(uidMapPda, false, true),
44
+ k(referralPda, false, true),
45
+ k(blocklistPda, false, false),
46
+ k(graceCheck, false, true),
47
+ k(payer.publicKey, true, true),
48
+ k(SystemProgram.programId, false, false),
49
+ ],
50
+ data,
51
+ });
52
+ return { instructions: [ix], signers: [payer] };
53
+ }
54
+
55
+ // ── register (atomic reserve+attach) ─────────────────────────────────
56
+ //
57
+ // Signed message body (after 2026-05 update — MEDIUM-10):
58
+ // TAG_REGISTER(1) || pubkey(32) || nonce(16) || username
59
+ // || referrer_present(1) || referrer_bytes(32) || account_type(1)
60
+ //
61
+ // The 1-byte presence prefix disambiguates `None` (0x00 || zeros) from
62
+ // `Some(Pubkey::default())` (0x01 || zeros) on the wire. The on-chain
63
+ // handler also rejects `Some(Pubkey::default())` explicitly.
64
+ export function register({
65
+ username, ed25519Keypair, accountType = 0, referrer = null,
66
+ expectedUserId, payer, nonce = randomNonce(), admin = null,
67
+ }) {
68
+ const referrerPresent = referrer ? 1 : 0;
69
+ const refBytes = referrer ? pkBytes(referrer) : new Uint8Array(32);
70
+ const msg = signedMsg(
71
+ MessageTags.REGISTER,
72
+ ed25519Keypair.publicKey,
73
+ nonce,
74
+ new TextEncoder().encode(username),
75
+ u8(referrerPresent),
76
+ refBytes,
77
+ u8(accountType),
78
+ );
79
+ const ed = buildEd25519Ix(ed25519Keypair, msg);
80
+
81
+ const data = concat(
82
+ ixDisc('register'),
83
+ borshString(username),
84
+ ed25519Keypair.publicKey,
85
+ nonce,
86
+ borshOptionPubkey(referrer),
87
+ u8(accountType),
88
+ u64LE(expectedUserId),
89
+ );
90
+
91
+ const [configPda] = findConfigPDA();
92
+ const [usernamePda] = findUsernamePDA(username);
93
+ const [ed25519Pda] = findEd25519AccountPDA(ed25519Keypair.publicKey);
94
+ const [noncePda] = findNoncePDA(nonce);
95
+ const [referralPda] = findReferralBalancePDA(referrer);
96
+ const [userIdxPda] = findUserIndexPDA(expectedUserId);
97
+ const [uidMapPda] = findUidMapPDA(username);
98
+ const [blocklistPda] = findBlocklistPDA(username);
99
+ const [graceCheck] = findGracePeriodPDA(username);
100
+ const adminPda = admin ? findAdminPDA(admin)[0] : null;
101
+
102
+ const ix = new TransactionInstruction({
103
+ programId: PROGRAM_ID,
104
+ keys: [
105
+ k(configPda, false, true),
106
+ k(usernamePda, false, true),
107
+ k(ed25519Pda, false, true),
108
+ k(noncePda, false, true),
109
+ k(referralPda, false, true),
110
+ k(userIdxPda, false, true),
111
+ k(uidMapPda, false, true),
112
+ k(blocklistPda, false, false),
113
+ k(graceCheck, false, true),
114
+ k(payer.publicKey, true, true),
115
+ k(adminPda ?? PROGRAM_ID, false, false), // admin Option (seed-bound to payer)
116
+ k(SYSVAR_INSTRUCTIONS, false, false),
117
+ k(SystemProgram.programId, false, false),
118
+ ],
119
+ data,
120
+ });
121
+ return { instructions: [ed, ix], signers: [payer] };
122
+ }
123
+
124
+ // ── attach_pubkey (Reserved → Active) ────────────────────────────────
125
+ //
126
+ // Reserved-side fields (referrer + account_type) were already persisted
127
+ // on UsernameAccount at reserve time, so attach takes only the username
128
+ // + new pubkey + nonce.
129
+ //
130
+ // Signed message body: TAG_ATTACH_PUBKEY(1) || pubkey(32) || nonce(16) || username
131
+ export function attachPubkey({
132
+ username, ed25519Keypair, currentWallet, userId, nonce = randomNonce(),
133
+ }) {
134
+ if (userId == null) throw new Error('attachPubkey: userId required');
135
+
136
+ const msg = signedMsg(
137
+ MessageTags.ATTACH_PUBKEY,
138
+ ed25519Keypair.publicKey,
139
+ nonce,
140
+ new TextEncoder().encode(username),
141
+ );
142
+ const ed = buildEd25519Ix(ed25519Keypair, msg);
143
+
144
+ const data = concat(
145
+ ixDisc('attach_pubkey'),
146
+ borshString(username),
147
+ ed25519Keypair.publicKey,
148
+ nonce,
149
+ );
150
+
151
+ const [configPda] = findConfigPDA();
152
+ const [usernamePda] = findUsernamePDA(username);
153
+ const [ed25519Pda] = findEd25519AccountPDA(ed25519Keypair.publicKey);
154
+ const [noncePda] = findNoncePDA(nonce);
155
+ const [salePda] = findSaleListingPDA(username);
156
+ const [uidMapPda] = findUidMapPDA(username);
157
+ const [userIdxPda] = findUserIndexPDA(userId);
158
+
159
+ // `config` was added at the head of `AttachPubkey` so the handler
160
+ // can honor `registration_paused` (MEDIUM-9). Order must match the
161
+ // on-chain accounts struct exactly.
162
+ const ix = new TransactionInstruction({
163
+ programId: PROGRAM_ID,
164
+ keys: [
165
+ k(configPda, false, false),
166
+ k(usernamePda, false, true),
167
+ k(ed25519Pda, false, true),
168
+ k(salePda, false, false),
169
+ k(userIdxPda, false, true),
170
+ k(uidMapPda, false, false),
171
+ k(noncePda, false, true),
172
+ k(currentWallet.publicKey, true, true),
173
+ k(SYSVAR_INSTRUCTIONS, false, false),
174
+ k(SystemProgram.programId, false, false),
175
+ ],
176
+ data,
177
+ });
178
+ return { instructions: [ed, ix], signers: [currentWallet] };
179
+ }
180
+
181
+ // ── transfer_reserved (no nonce) ─────────────────────────────────────
182
+ export function transferReserved({ username, newWallet, currentWallet }) {
183
+ const data = concat(
184
+ ixDisc('transfer_reserved'),
185
+ borshString(username),
186
+ pkBytes(newWallet),
187
+ );
188
+ const [usernamePda] = findUsernamePDA(username);
189
+ const [salePda] = findSaleListingPDA(username);
190
+ const ix = new TransactionInstruction({
191
+ programId: PROGRAM_ID,
192
+ keys: [
193
+ k(usernamePda, false, true),
194
+ k(salePda, false, false),
195
+ k(currentWallet.publicKey, true, false),
196
+ ],
197
+ data,
198
+ });
199
+ return { instructions: [ix], signers: [currentWallet] };
200
+ }
201
+
202
+ export function unregisterReserved({ username, currentWallet, userId }) {
203
+ if (userId == null) throw new Error('unregisterReserved: userId required');
204
+ const data = concat(ixDisc('unregister_reserved'), borshString(username));
205
+ const [configPda] = findConfigPDA();
206
+ const [usernamePda] = findUsernamePDA(username);
207
+ const [salePda] = findSaleListingPDA(username);
208
+ const [gracePda] = findGracePeriodPDA(username);
209
+ const [uidMapPda] = findUidMapPDA(username);
210
+ const [userIdxPda] = findUserIndexPDA(userId);
211
+ const ix = new TransactionInstruction({
212
+ programId: PROGRAM_ID,
213
+ keys: [
214
+ k(configPda, false, true),
215
+ k(usernamePda, false, true),
216
+ k(salePda, false, false),
217
+ k(gracePda, false, true),
218
+ k(uidMapPda, false, true),
219
+ k(userIdxPda, false, true),
220
+ k(currentWallet.publicKey, true, true),
221
+ k(SystemProgram.programId, false, false),
222
+ ],
223
+ data,
224
+ });
225
+ return { instructions: [ix], signers: [currentWallet] };
226
+ }
227
+
228
+ // ── update_pubkey (key rotation; auto-freeze gated on old key) ───────
229
+ //
230
+ // Account list mirrors `programs/.../update_pubkey.rs`.
231
+ // No Inventory or ProfilePhoto in the account list — those are seeded by
232
+ // username, so they need no migration.
233
+ export function updatePubkey({
234
+ username, currentEd25519Keypair, newEd25519Pubkey, currentEd25519Pubkey,
235
+ payer, nonce = randomNonce(),
236
+ }) {
237
+ const msg = signedMsg(
238
+ MessageTags.UPDATE_PUBKEY,
239
+ pkBytes(newEd25519Pubkey),
240
+ nonce,
241
+ new TextEncoder().encode(username),
242
+ );
243
+ const ed = buildEd25519Ix(currentEd25519Keypair, msg);
244
+
245
+ const data = concat(
246
+ ixDisc('update_pubkey'),
247
+ borshString(username),
248
+ pkBytes(newEd25519Pubkey),
249
+ nonce,
250
+ );
251
+ const [configPda] = findConfigPDA();
252
+ const [usernamePda] = findUsernamePDA(username);
253
+ const [oldEdPda] = findEd25519AccountPDA(pkBytes(currentEd25519Pubkey));
254
+ const [newEdPda] = findEd25519AccountPDA(pkBytes(newEd25519Pubkey));
255
+ const [noncePda] = findNoncePDA(nonce);
256
+ const [salePda] = findSaleListingPDA(username);
257
+ const ix = new TransactionInstruction({
258
+ programId: PROGRAM_ID,
259
+ keys: [
260
+ k(configPda, false, false),
261
+ k(usernamePda, false, true),
262
+ k(oldEdPda, false, true),
263
+ k(newEdPda, false, true),
264
+ k(noncePda, false, true),
265
+ k(salePda, false, false),
266
+ k(payer.publicKey, true, true),
267
+ k(SYSVAR_INSTRUCTIONS, false, false),
268
+ k(SystemProgram.programId, false, false),
269
+ ],
270
+ data,
271
+ });
272
+ return { instructions: [ed, ix], signers: [payer] };
273
+ }
274
+
275
+ // ── transfer_username (auto-freeze gated) ────────────────────────────
276
+ //
277
+ // Account list mirrors `programs/.../transfer_username.rs`. Includes
278
+ // `profile_photo_check` (seeded by username) — must be empty.
279
+ export function transferUsername({
280
+ username, currentEd25519Keypair, newEd25519Pubkey, currentEd25519Pubkey,
281
+ payer, nonce = randomNonce(),
282
+ }) {
283
+ const msg = signedMsg(
284
+ MessageTags.TRANSFER_USERNAME,
285
+ pkBytes(newEd25519Pubkey),
286
+ nonce,
287
+ new TextEncoder().encode(username),
288
+ );
289
+ const ed = buildEd25519Ix(currentEd25519Keypair, msg);
290
+
291
+ const data = concat(
292
+ ixDisc('transfer_username'),
293
+ borshString(username),
294
+ pkBytes(newEd25519Pubkey),
295
+ nonce,
296
+ );
297
+ const [configPda] = findConfigPDA();
298
+ const [usernamePda] = findUsernamePDA(username);
299
+ const [oldEdPda] = findEd25519AccountPDA(pkBytes(currentEd25519Pubkey));
300
+ const [newEdPda] = findEd25519AccountPDA(pkBytes(newEd25519Pubkey));
301
+ const [noncePda] = findNoncePDA(nonce);
302
+ const [salePda] = findSaleListingPDA(username);
303
+ const [photoCheck] = findProfilePhotoPDA(username);
304
+ const ix = new TransactionInstruction({
305
+ programId: PROGRAM_ID,
306
+ keys: [
307
+ k(configPda, false, false),
308
+ k(usernamePda, false, true),
309
+ k(oldEdPda, false, true),
310
+ k(newEdPda, false, true),
311
+ k(noncePda, false, true),
312
+ k(salePda, false, false),
313
+ k(photoCheck, false, false),
314
+ k(payer.publicKey, true, true),
315
+ k(SYSVAR_INSTRUCTIONS, false, false),
316
+ k(SystemProgram.programId, false, false),
317
+ ],
318
+ data,
319
+ });
320
+ return { instructions: [ed, ix], signers: [payer] };
321
+ }
322
+
323
+ // ── unregister_active (auto-freeze gated; UsernameAccount permanent) ──
324
+ //
325
+ // Note: the on-chain handler now flips UsernameAccount.state →
326
+ // UNREGISTERED rather than closing it (the PDA stays open forever).
327
+ // `username_user_id_map` is the only account that gets closed — its
328
+ // rent goes to `refund_to`.
329
+ export function unregisterActive({
330
+ username, currentEd25519Keypair, currentEd25519Pubkey,
331
+ refundTo, payer, userId, nonce = randomNonce(),
332
+ }) {
333
+ const msg = signedMsg(
334
+ MessageTags.UNREGISTER,
335
+ pkBytes(currentEd25519Pubkey),
336
+ pkBytes(refundTo),
337
+ nonce,
338
+ new TextEncoder().encode(username),
339
+ );
340
+ const ed = buildEd25519Ix(currentEd25519Keypair, msg);
341
+
342
+ const data = concat(
343
+ ixDisc('unregister_active'),
344
+ borshString(username),
345
+ pkBytes(refundTo),
346
+ nonce,
347
+ );
348
+ const [configPda] = findConfigPDA();
349
+ const [usernamePda] = findUsernamePDA(username);
350
+ const [edPda] = findEd25519AccountPDA(pkBytes(currentEd25519Pubkey));
351
+ const [gracePda] = findGracePeriodPDA(username);
352
+ const [salePda] = findSaleListingPDA(username);
353
+ const [photoCheck] = findProfilePhotoPDA(username);
354
+ const [uidMapPda] = findUidMapPDA(username);
355
+ const [userIdxPda] = findUserIndexPDA(userId);
356
+ const [noncePda] = findNoncePDA(nonce);
357
+ const ix = new TransactionInstruction({
358
+ programId: PROGRAM_ID,
359
+ keys: [
360
+ k(configPda, false, true),
361
+ k(refundTo, false, true),
362
+ k(usernamePda, false, true),
363
+ k(edPda, false, true),
364
+ k(gracePda, false, true),
365
+ k(salePda, false, false),
366
+ k(photoCheck, false, false),
367
+ k(uidMapPda, false, true),
368
+ k(userIdxPda, false, true),
369
+ k(noncePda, false, true),
370
+ k(payer.publicKey, true, true),
371
+ k(SYSVAR_INSTRUCTIONS, false, false),
372
+ k(SystemProgram.programId, false, false),
373
+ ],
374
+ data,
375
+ });
376
+ return { instructions: [ed, ix], signers: [payer] };
377
+ }
378
+
379
+ // ── set_account_type (lock-gated; auto-freeze gated) ─────────────────
380
+ //
381
+ // account_type lives on UsernameAccount; the bound key's Ed25519Account
382
+ // PDA is included for the on-chain auto-freeze check.
383
+ export function setAccountType({
384
+ username, ed25519Keypair, accountType, payer, nonce = randomNonce(),
385
+ }) {
386
+ const msg = signedMsg(
387
+ MessageTags.SET_ACCOUNT_TYPE,
388
+ u8(accountType),
389
+ nonce,
390
+ new TextEncoder().encode(username),
391
+ );
392
+ const ed = buildEd25519Ix(ed25519Keypair, msg);
393
+
394
+ const data = concat(
395
+ ixDisc('set_account_type'),
396
+ borshString(username),
397
+ u8(accountType),
398
+ nonce,
399
+ );
400
+ const [configPda] = findConfigPDA();
401
+ const [usernamePda] = findUsernamePDA(username);
402
+ const [edPda] = findEd25519AccountPDA(ed25519Keypair.publicKey);
403
+ const [noncePda] = findNoncePDA(nonce);
404
+ const ix = new TransactionInstruction({
405
+ programId: PROGRAM_ID,
406
+ keys: [
407
+ k(configPda, false, false),
408
+ k(usernamePda, false, true),
409
+ k(edPda, false, false),
410
+ k(noncePda, false, true),
411
+ k(payer.publicKey, true, true),
412
+ k(SYSVAR_INSTRUCTIONS, false, false),
413
+ k(SystemProgram.programId, false, false),
414
+ ],
415
+ data,
416
+ });
417
+ return { instructions: [ed, ix], signers: [payer] };
418
+ }
@@ -0,0 +1,10 @@
1
+ // Aggregate re-export of every instruction builder.
2
+
3
+ export * from './identity.js';
4
+ export * from './marketplace.js';
5
+ export * from './lock.js';
6
+ export * from './photo.js';
7
+ export * from './shop.js';
8
+ export * from './pro.js';
9
+ export * from './admin.js';
10
+ export * from './revoke.js';
package/src/ix/lock.js ADDED
@@ -0,0 +1,63 @@
1
+ // Lock / unlock: request_unlock + lock.
2
+
3
+ import { TransactionInstruction, SystemProgram } from '@solana/web3.js';
4
+ import { PROGRAM_ID, SYSVAR_INSTRUCTIONS, MessageTags } from '../constants.js';
5
+ import { borshString, randomNonce, buildEd25519Ix, signedMsg, concat } from '../encoding.js';
6
+ import { ixDisc } from '../disc.js';
7
+ import {
8
+ findConfigPDA, findUsernamePDA, findNoncePDA, findSaleListingPDA,
9
+ findEd25519AccountPDA,
10
+ } from '../pda.js';
11
+
12
+ const k = (pubkey, isSigner, isWritable) => ({ pubkey, isSigner, isWritable });
13
+
14
+ // request_unlock now also threads the bound key's Ed25519Account so the
15
+ // program can run `require_not_frozen` on the auto-freeze gate.
16
+ export function requestUnlock({ username, ed25519Keypair, payer, nonce = randomNonce() }) {
17
+ const msg = signedMsg(MessageTags.REQUEST_UNLOCK, nonce, new TextEncoder().encode(username));
18
+ const ed = buildEd25519Ix(ed25519Keypair, msg);
19
+ const data = concat(ixDisc('request_unlock'), borshString(username), nonce);
20
+ const [configPda] = findConfigPDA();
21
+ const [usernamePda] = findUsernamePDA(username);
22
+ const [edPda] = findEd25519AccountPDA(ed25519Keypair.publicKey);
23
+ const [noncePda] = findNoncePDA(nonce);
24
+ const ix = new TransactionInstruction({
25
+ programId: PROGRAM_ID,
26
+ keys: [
27
+ k(configPda, false, false),
28
+ k(usernamePda, false, true),
29
+ k(edPda, false, false),
30
+ k(noncePda, false, true),
31
+ k(payer.publicKey, true, true),
32
+ k(SYSVAR_INSTRUCTIONS, false, false),
33
+ k(SystemProgram.programId, false, false),
34
+ ],
35
+ data,
36
+ });
37
+ return { instructions: [ed, ix], signers: [payer] };
38
+ }
39
+
40
+ // lock now also threads the bound key's Ed25519Account (auto-freeze gate).
41
+ export function lock({ username, ed25519Keypair, payer, nonce = randomNonce() }) {
42
+ const msg = signedMsg(MessageTags.LOCK, nonce, new TextEncoder().encode(username));
43
+ const ed = buildEd25519Ix(ed25519Keypair, msg);
44
+ const data = concat(ixDisc('lock'), borshString(username), nonce);
45
+ const [usernamePda] = findUsernamePDA(username);
46
+ const [edPda] = findEd25519AccountPDA(ed25519Keypair.publicKey);
47
+ const [salePda] = findSaleListingPDA(username);
48
+ const [noncePda] = findNoncePDA(nonce);
49
+ const ix = new TransactionInstruction({
50
+ programId: PROGRAM_ID,
51
+ keys: [
52
+ k(usernamePda, false, true),
53
+ k(edPda, false, false),
54
+ k(salePda, false, false),
55
+ k(noncePda, false, true),
56
+ k(payer.publicKey, true, true),
57
+ k(SYSVAR_INSTRUCTIONS, false, false),
58
+ k(SystemProgram.programId, false, false),
59
+ ],
60
+ data,
61
+ });
62
+ return { instructions: [ed, ix], signers: [payer] };
63
+ }
@@ -0,0 +1,198 @@
1
+ // Marketplace: list_reserved, list_active, buy_reserved, cancel_sale.
2
+
3
+ import { TransactionInstruction, SystemProgram } from '@solana/web3.js';
4
+ import { PROGRAM_ID, SYSVAR_INSTRUCTIONS, MessageTags } from '../constants.js';
5
+ import {
6
+ borshString, u64LE, randomNonce,
7
+ buildEd25519Ix, signedMsg, pkBytes, concat,
8
+ } from '../encoding.js';
9
+ import { ixDisc } from '../disc.js';
10
+ import {
11
+ findConfigPDA, findUsernamePDA, findSaleListingPDA,
12
+ findEd25519AccountPDA, findNoncePDA, findProfilePhotoPDA,
13
+ findReferralBalancePDA,
14
+ } from '../pda.js';
15
+ import { PublicKey } from '@solana/web3.js';
16
+
17
+ const k = (pubkey, isSigner, isWritable) => ({ pubkey, isSigner, isWritable });
18
+
19
+ // ── list_reserved (wallet-auth, no nonce) ────────────────────────────
20
+ //
21
+ // On-chain `ListReserved` has 5 accounts: config, username_account,
22
+ // sale_listing(init, payer=current_wallet), current_wallet(mut Signer),
23
+ // system_program. The signer also funds rent — there's no separate
24
+ // payer. The on-chain handler no longer threads a Nonce PDA: wallet-auth
25
+ // has no signed-message body, so Solana's outer-tx-signature uniqueness
26
+ // already prevents replay. `nonce` and `payer` args kept for API
27
+ // back-compat (ignored — payer rejected if different from currentWallet).
28
+ export function listReserved({ username, price, receiverWallet, currentWallet, payer, nonce: _nonce }) {
29
+ if (payer && payer !== currentWallet && payer.publicKey?.toBase58?.() !== currentWallet.publicKey.toBase58()) {
30
+ throw new Error('listReserved: `payer` must equal `currentWallet` (on-chain handler funds rent from the signing wallet)');
31
+ }
32
+ const data = concat(
33
+ ixDisc('list_reserved'),
34
+ borshString(username),
35
+ u64LE(price),
36
+ pkBytes(receiverWallet),
37
+ );
38
+ const [configPda] = findConfigPDA();
39
+ const [usernamePda] = findUsernamePDA(username);
40
+ const [salePda] = findSaleListingPDA(username);
41
+ const ix = new TransactionInstruction({
42
+ programId: PROGRAM_ID,
43
+ keys: [
44
+ k(configPda, false, false),
45
+ k(usernamePda, false, false),
46
+ k(salePda, false, true),
47
+ k(currentWallet.publicKey, true, true),
48
+ k(SystemProgram.programId, false, false),
49
+ ],
50
+ data,
51
+ });
52
+ return { instructions: [ix], signers: [currentWallet] };
53
+ }
54
+
55
+ // ── list_active (demote-and-list) ────────────────────────────────────
56
+ //
57
+ // Demotes the username from Active to Reserved as part of listing:
58
+ // - clears `bound_username` on Ed25519Account (PDA stays open so
59
+ // `compromised_at` / `pfp_banned_until` survive — closing here would
60
+ // let a banned key wipe its ban via re-register, MEDIUM-5)
61
+ // - closes ProfilePhoto PDA if one exists (rent → receiver_wallet)
62
+ // - mutates UsernameAccount: state=Reserved, wallet=receiver_wallet,
63
+ // ed25519_pubkey=zero, attached_at=0, unlock_requested_at=0,
64
+ // equipped_* = None. (premium / inventory / account_type / referrer
65
+ // stay — slot-bound.)
66
+ //
67
+ // Requires the seller's ed25519 sig (LIST_ACTIVE precompile message)
68
+ // + the unlock window to be open (same gate as transfer/unregister).
69
+ export function listActive({
70
+ username, price, receiverWallet, ed25519Keypair,
71
+ payer, nonce = randomNonce(),
72
+ }) {
73
+ const msg = signedMsg(
74
+ MessageTags.LIST_ACTIVE,
75
+ u64LE(price),
76
+ pkBytes(receiverWallet),
77
+ nonce,
78
+ new TextEncoder().encode(username),
79
+ );
80
+ const ed = buildEd25519Ix(ed25519Keypair, msg);
81
+
82
+ const data = concat(
83
+ ixDisc('list_active'),
84
+ borshString(username),
85
+ u64LE(price),
86
+ pkBytes(receiverWallet),
87
+ nonce,
88
+ );
89
+ const [configPda] = findConfigPDA();
90
+ const [usernamePda] = findUsernamePDA(username);
91
+ const [edPda] = findEd25519AccountPDA(ed25519Keypair.publicKey);
92
+ const [photoPda] = findProfilePhotoPDA(username);
93
+ const [salePda] = findSaleListingPDA(username);
94
+ const [noncePda] = findNoncePDA(nonce);
95
+ // `receiver_wallet` appears twice: once as an instruction arg (above)
96
+ // and once as a mutable account for rent destination. Both share the
97
+ // same pubkey thanks to the program's `address = receiver_wallet`
98
+ // constraint.
99
+ const receiverPk = receiverWallet instanceof PublicKey
100
+ ? receiverWallet
101
+ : new PublicKey(pkBytes(receiverWallet));
102
+ const ix = new TransactionInstruction({
103
+ programId: PROGRAM_ID,
104
+ keys: [
105
+ k(configPda, false, false),
106
+ k(usernamePda, false, true),
107
+ k(edPda, false, true),
108
+ k(photoPda, false, true),
109
+ k(receiverPk, false, true),
110
+ k(salePda, false, true),
111
+ k(noncePda, false, true),
112
+ k(payer.publicKey, true, true),
113
+ k(SYSVAR_INSTRUCTIONS, false, false),
114
+ k(SystemProgram.programId, false, false),
115
+ ],
116
+ data,
117
+ });
118
+ return { instructions: [ed, ix], signers: [payer] };
119
+ }
120
+
121
+ // ── buy_reserved (with expected_price + expected_receiver per #3) ────
122
+ //
123
+ // Pass `referrerForUsername` so the SDK can derive the `ReferralBalance`
124
+ // PDA the program needs as an account (lazy-init, seeded by the
125
+ // referrer pubkey or `Pubkey::default()` when no referrer is set). The
126
+ // `PingDirectory.buy()` wrapper fetches this off the UsernameAccount;
127
+ // raw callers must supply it (`null` when the slot has no referrer).
128
+ export function buyReserved({
129
+ username, expectedPrice, expectedReceiver, receiverWallet, buyer,
130
+ referrerForUsername = null,
131
+ nonce: _nonce,
132
+ }) {
133
+ // Nonce PDA removed (wallet-auth, no signed-message body to replay).
134
+ const data = concat(
135
+ ixDisc('buy_reserved'),
136
+ borshString(username),
137
+ u64LE(expectedPrice),
138
+ pkBytes(expectedReceiver),
139
+ );
140
+ const [configPda] = findConfigPDA();
141
+ const [usernamePda] = findUsernamePDA(username);
142
+ const [salePda] = findSaleListingPDA(username);
143
+ // ReferralBalance PDA seed: referrer pubkey, or `Pubkey::default()`
144
+ // (32 zero bytes) when the slot has no referrer.
145
+ const referrerKey = referrerForUsername
146
+ ? (referrerForUsername instanceof PublicKey
147
+ ? referrerForUsername
148
+ : new PublicKey(pkBytes(referrerForUsername)))
149
+ : PublicKey.default;
150
+ const [referralPda] = findReferralBalancePDA(referrerKey);
151
+ const ix = new TransactionInstruction({
152
+ programId: PROGRAM_ID,
153
+ keys: [
154
+ k(configPda, false, true),
155
+ k(usernamePda, false, true),
156
+ k(salePda, false, true),
157
+ k(receiverWallet, false, true),
158
+ k(referralPda, false, true),
159
+ k(buyer.publicKey, true, true),
160
+ k(SystemProgram.programId, false, false),
161
+ ],
162
+ data,
163
+ });
164
+ return { instructions: [ix], signers: [buyer] };
165
+ }
166
+
167
+ // buy_active was removed in the demote-on-list rewrite. After
168
+ // `list_active` demotes the username to Reserved, every listing buys
169
+ // through the wallet-auth `buyReserved` path below. Re-exported as
170
+ // `buy` (alias) for clarity at callsites that don't care about
171
+ // Reserved-vs-Active provenance.
172
+
173
+ // ── cancel_sale (wallet-auth, no nonce) ──────────────────────────────
174
+ // Single cancel path after the demote-on-list rewrite. The `_reserved`
175
+ // suffix was retired in 2026-05 (LOW-25) — every listing settles via
176
+ // the wallet-auth path because `list_active` demotes the username.
177
+ export function cancelSale({ username, receiverWallet, currentWallet }) {
178
+ const data = concat(ixDisc('cancel_sale'), borshString(username));
179
+ const [usernamePda] = findUsernamePDA(username);
180
+ const [salePda] = findSaleListingPDA(username);
181
+ const ix = new TransactionInstruction({
182
+ programId: PROGRAM_ID,
183
+ keys: [
184
+ k(usernamePda, false, false),
185
+ k(salePda, false, true),
186
+ k(receiverWallet, false, true),
187
+ k(currentWallet.publicKey, true, false),
188
+ ],
189
+ data,
190
+ });
191
+ return { instructions: [ix], signers: [currentWallet] };
192
+ }
193
+
194
+ // Buy alias for state-agnostic call sites. Program has only `buy_reserved`
195
+ // + `cancel_sale` after the demote-on-list rewrite.
196
+ export const buy = buyReserved;
197
+ // Back-compat alias for SDK callers still importing the old name.
198
+ export const cancelSaleReserved = cancelSale;