@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,173 @@
1
+ // Profile photo: init / write_chunk / finalize / clear.
2
+ //
3
+ // ProfilePhoto is keyed by username (`[b"profile_photo", username]`).
4
+ // All four handlers operate on this same PDA. The first three thread
5
+ // the bound key's Ed25519Account for the on-chain auto-freeze check;
6
+ // `clear_profile_photo` intentionally does NOT — the user must always
7
+ // be able to clear their photo (required to satisfy the
8
+ // "photo cleared first" preconditions on transfer / sale / unregister).
9
+
10
+ import { TransactionInstruction, SystemProgram } from '@solana/web3.js';
11
+ import { PROGRAM_ID, SYSVAR_INSTRUCTIONS, MessageTags } from '../constants.js';
12
+ import {
13
+ borshString, borshBytes, u8, u16LE, u32LE, randomNonce,
14
+ buildEd25519Ix, signedMsg, concat,
15
+ } from '../encoding.js';
16
+ import { ixDisc } from '../disc.js';
17
+ import {
18
+ findUsernamePDA, findProfilePhotoPDA, findNoncePDA, findSaleListingPDA,
19
+ findEd25519AccountPDA, findConfigPDA,
20
+ } from '../pda.js';
21
+
22
+ const k = (pubkey, isSigner, isWritable) => ({ pubkey, isSigner, isWritable });
23
+
24
+ export function initProfilePhoto({
25
+ username, totalSize, mime, width, height, ed25519Keypair, payer, nonce = randomNonce(),
26
+ }) {
27
+ const msg = signedMsg(
28
+ MessageTags.INIT_PROFILE_PHOTO,
29
+ payer.publicKey.toBytes(),
30
+ u32LE(totalSize),
31
+ u8(mime),
32
+ u16LE(width),
33
+ u16LE(height),
34
+ nonce,
35
+ new TextEncoder().encode(username),
36
+ );
37
+ const ed = buildEd25519Ix(ed25519Keypair, msg);
38
+
39
+ const data = concat(
40
+ ixDisc('init_profile_photo'),
41
+ borshString(username),
42
+ u32LE(totalSize),
43
+ u8(mime),
44
+ u16LE(width),
45
+ u16LE(height),
46
+ nonce,
47
+ );
48
+ const [configPda] = findConfigPDA();
49
+ const [usernamePda] = findUsernamePDA(username);
50
+ const [edPda] = findEd25519AccountPDA(ed25519Keypair.publicKey);
51
+ const [photoPda] = findProfilePhotoPDA(username);
52
+ const [noncePda] = findNoncePDA(nonce);
53
+ const [salePda] = findSaleListingPDA(username);
54
+ const ix = new TransactionInstruction({
55
+ programId: PROGRAM_ID,
56
+ keys: [
57
+ // `config` is mut: per-MIME fee is transferred from `payer` into the
58
+ // Config PDA's lamports (sweepable later via `withdraw_treasury`).
59
+ k(configPda, false, true),
60
+ k(usernamePda, false, false),
61
+ k(edPda, false, false),
62
+ k(photoPda, false, true),
63
+ k(noncePda, false, true),
64
+ k(salePda, false, false),
65
+ k(payer.publicKey, true, true),
66
+ k(SYSVAR_INSTRUCTIONS, false, false),
67
+ k(SystemProgram.programId, false, false),
68
+ ],
69
+ data,
70
+ });
71
+ return { instructions: [ed, ix], signers: [payer] };
72
+ }
73
+
74
+ // write_photo_chunk: wallet-auth (`payer == profile_photo.init_payer`).
75
+ // Caller must supply `boundEd25519Pubkey` (read from UsernameAccount)
76
+ // so we can include the bound key's Ed25519Account for the freeze check.
77
+ export function writePhotoChunk({ username, offset, chunk, payer, boundEd25519Pubkey }) {
78
+ if (boundEd25519Pubkey == null) {
79
+ throw new Error('writePhotoChunk: boundEd25519Pubkey required');
80
+ }
81
+ const data = concat(
82
+ ixDisc('write_photo_chunk'),
83
+ borshString(username),
84
+ u32LE(offset),
85
+ borshBytes(chunk),
86
+ );
87
+ const [usernamePda] = findUsernamePDA(username);
88
+ const [edPda] = findEd25519AccountPDA(boundEd25519Pubkey);
89
+ const [photoPda] = findProfilePhotoPDA(username);
90
+ const ix = new TransactionInstruction({
91
+ programId: PROGRAM_ID,
92
+ keys: [
93
+ k(usernamePda, false, false),
94
+ k(edPda, false, false),
95
+ k(photoPda, false, true),
96
+ k(payer.publicKey, true, false),
97
+ ],
98
+ data,
99
+ });
100
+ return { instructions: [ix], signers: [payer] };
101
+ }
102
+
103
+ export function finalizeProfilePhoto({
104
+ username, imageHash, mime, width, height, ed25519Keypair, payer, nonce = randomNonce(),
105
+ }) {
106
+ const msg = signedMsg(
107
+ MessageTags.FINALIZE_PROFILE_PHOTO,
108
+ imageHash,
109
+ u8(mime),
110
+ u16LE(width),
111
+ u16LE(height),
112
+ nonce,
113
+ new TextEncoder().encode(username),
114
+ );
115
+ const ed = buildEd25519Ix(ed25519Keypair, msg);
116
+
117
+ const data = concat(
118
+ ixDisc('finalize_profile_photo'),
119
+ borshString(username),
120
+ imageHash,
121
+ nonce,
122
+ );
123
+ const [usernamePda] = findUsernamePDA(username);
124
+ const [edPda] = findEd25519AccountPDA(ed25519Keypair.publicKey);
125
+ const [photoPda] = findProfilePhotoPDA(username);
126
+ const [noncePda] = findNoncePDA(nonce);
127
+ const ix = new TransactionInstruction({
128
+ programId: PROGRAM_ID,
129
+ keys: [
130
+ k(usernamePda, false, false),
131
+ k(edPda, false, false),
132
+ k(photoPda, false, true),
133
+ k(noncePda, false, true),
134
+ k(payer.publicKey, true, true),
135
+ k(SYSVAR_INSTRUCTIONS, false, false),
136
+ k(SystemProgram.programId, false, false),
137
+ ],
138
+ data,
139
+ });
140
+ return { instructions: [ed, ix], signers: [payer] };
141
+ }
142
+
143
+ // clear_profile_photo is now freeze-gated (MEDIUM-6, 2026-05): the
144
+ // handler reads `ed25519_account` to check `compromised_at`. SDK must
145
+ // thread the Ed25519Account PDA in the same slot order as the on-chain
146
+ // `ClearProfilePhoto` struct.
147
+ export function clearProfilePhoto({
148
+ username, ed25519Keypair, initPayer, payer, nonce = randomNonce(),
149
+ }) {
150
+ const msg = signedMsg(MessageTags.CLEAR_PROFILE_PHOTO, nonce, new TextEncoder().encode(username));
151
+ const ed = buildEd25519Ix(ed25519Keypair, msg);
152
+
153
+ const data = concat(ixDisc('clear_profile_photo'), borshString(username), nonce);
154
+ const [usernamePda] = findUsernamePDA(username);
155
+ const [edPda] = findEd25519AccountPDA(ed25519Keypair.publicKey);
156
+ const [photoPda] = findProfilePhotoPDA(username);
157
+ const [noncePda] = findNoncePDA(nonce);
158
+ const ix = new TransactionInstruction({
159
+ programId: PROGRAM_ID,
160
+ keys: [
161
+ k(usernamePda, false, false),
162
+ k(edPda, false, false),
163
+ k(photoPda, false, true),
164
+ k(initPayer, false, true),
165
+ k(noncePda, false, true),
166
+ k(payer.publicKey, true, true),
167
+ k(SYSVAR_INSTRUCTIONS, false, false),
168
+ k(SystemProgram.programId, false, false),
169
+ ],
170
+ data,
171
+ });
172
+ return { instructions: [ed, ix], signers: [payer] };
173
+ }
package/src/ix/pro.js ADDED
@@ -0,0 +1,91 @@
1
+ // Premium: subscribe_pro (open-payer), set_pro (admin/owner toggle).
2
+
3
+ import { TransactionInstruction, SystemProgram, PublicKey } from '@solana/web3.js';
4
+ import { PROGRAM_ID } from '../constants.js';
5
+ import { borshString, u8, concat } from '../encoding.js';
6
+ import { ixDisc } from '../disc.js';
7
+ import {
8
+ findConfigPDA, findUsernamePDA, findReferralBalancePDA, findAdminPDA,
9
+ findEd25519AccountPDA,
10
+ } from '../pda.js';
11
+
12
+ const k = (pubkey, isSigner, isWritable) => ({ pubkey, isSigner, isWritable });
13
+
14
+ // subscribe_pro: writes pro_until / pro_lifetime onto
15
+ // UsernameAccount. The username's stored referrer (also on UsernameAccount)
16
+ // gets credited 10%.
17
+ //
18
+ // Caller must supply:
19
+ // - `boundEd25519Pubkey`: the username's bound ed25519 key (read from
20
+ // `UsernameAccount.ed25519Pubkey`). Required for the on-chain
21
+ // auto-freeze gate.
22
+ // - `referrerForUsername`: the referrer recorded on UsernameAccount.referrer
23
+ // (Pubkey | null). Used to derive the referral PDA.
24
+ export function subscribePro({
25
+ username, months, payer, boundEd25519Pubkey, referrerForUsername = null,
26
+ }) {
27
+ if (boundEd25519Pubkey == null) {
28
+ throw new Error('subscribePro: boundEd25519Pubkey required');
29
+ }
30
+ const data = concat(ixDisc('subscribe_pro'), borshString(username), u8(months));
31
+ const [configPda] = findConfigPDA();
32
+ const [usernamePda] = findUsernamePDA(username);
33
+ const [edPda] = findEd25519AccountPDA(boundEd25519Pubkey);
34
+ const refKey = referrerForUsername ?? PublicKey.default;
35
+ const [referralPda] = findReferralBalancePDA(refKey);
36
+ const ix = new TransactionInstruction({
37
+ programId: PROGRAM_ID,
38
+ keys: [
39
+ k(configPda, false, true),
40
+ k(usernamePda, false, true),
41
+ k(edPda, false, false),
42
+ k(referralPda, false, true),
43
+ k(payer.publicKey, true, true),
44
+ k(SystemProgram.programId, false, false),
45
+ ],
46
+ data,
47
+ });
48
+ return { instructions: [ix], signers: [payer] };
49
+ }
50
+
51
+ // set_pro: forever=true → owner-only; forever=false → owner OR admin.
52
+ // Caller passes admin Pubkey (or null) — required field but only consulted
53
+ // if signer != owner. Writes the lifetime flag onto UsernameAccount.
54
+ //
55
+ // On-chain handler does NOT include ed25519_account (no auto-freeze gate
56
+ // for owner/admin admin-paths).
57
+ export function setPro({ username, forever, signer, admin = null }) {
58
+ const data = concat(ixDisc('set_pro'), borshString(username), u8(forever ? 1 : 0));
59
+ const [configPda] = findConfigPDA();
60
+ const [usernamePda] = findUsernamePDA(username);
61
+ const adminPda = admin ? findAdminPDA(admin)[0] : null;
62
+ const ix = new TransactionInstruction({
63
+ programId: PROGRAM_ID,
64
+ keys: [
65
+ k(configPda, false, false),
66
+ k(usernamePda, false, true),
67
+ k(signer.publicKey, true, false),
68
+ // Anchor's `Option<Account>` accepts the program id when None.
69
+ k(adminPda ?? PROGRAM_ID, false, false),
70
+ ],
71
+ data,
72
+ });
73
+ return { instructions: [ix], signers: [signer] };
74
+ }
75
+
76
+ export function withdrawReferral({ recipient }) {
77
+ const data = ixDisc('withdraw_referral');
78
+ const [configPda] = findConfigPDA();
79
+ const [referralPda] = findReferralBalancePDA(recipient.publicKey);
80
+ const ix = new TransactionInstruction({
81
+ programId: PROGRAM_ID,
82
+ keys: [
83
+ k(configPda, false, true),
84
+ k(referralPda, false, true),
85
+ k(recipient.publicKey, true, true),
86
+ k(SystemProgram.programId, false, false),
87
+ ],
88
+ data,
89
+ });
90
+ return { instructions: [ix], signers: [recipient] };
91
+ }
@@ -0,0 +1,41 @@
1
+ // Key revocation: mark_compromised.
2
+
3
+ import { TransactionInstruction, SystemProgram } from '@solana/web3.js';
4
+ import { PROGRAM_ID, SYSVAR_INSTRUCTIONS, MessageTags } from '../constants.js';
5
+ import { randomNonce, buildEd25519Ix, signedMsg, concat } from '../encoding.js';
6
+ import { ixDisc } from '../disc.js';
7
+ import { findEd25519AccountPDA, findNoncePDA } from '../pda.js';
8
+
9
+ const k = (pubkey, isSigner, isWritable) => ({ pubkey, isSigner, isWritable });
10
+
11
+ // Signed message body (after 2026-05 update — LOW-15):
12
+ // TAG_MARK_COMPROMISED(1) || pubkey(32) || payer(32) || nonce(16)
13
+ // Binding `payer` is consistency with other economic-side-effect ixes
14
+ // (not exploitable today — outcome is identical regardless of payer).
15
+ export function markCompromised({ ed25519Keypair, payer, nonce = randomNonce() }) {
16
+ const msg = signedMsg(
17
+ MessageTags.MARK_COMPROMISED,
18
+ ed25519Keypair.publicKey,
19
+ payer.publicKey.toBytes(),
20
+ nonce,
21
+ );
22
+ const ed = buildEd25519Ix(ed25519Keypair, msg);
23
+
24
+ const data = concat(ixDisc('mark_compromised'), ed25519Keypair.publicKey, nonce);
25
+
26
+ const [edPda] = findEd25519AccountPDA(ed25519Keypair.publicKey);
27
+ const [noncePda] = findNoncePDA(nonce);
28
+
29
+ const ix = new TransactionInstruction({
30
+ programId: PROGRAM_ID,
31
+ keys: [
32
+ k(edPda, false, true),
33
+ k(noncePda, false, true),
34
+ k(payer.publicKey, true, true),
35
+ k(SYSVAR_INSTRUCTIONS, false, false),
36
+ k(SystemProgram.programId, false, false),
37
+ ],
38
+ data,
39
+ });
40
+ return { instructions: [ed, ix], signers: [payer] };
41
+ }
package/src/ix/shop.js ADDED
@@ -0,0 +1,322 @@
1
+ // Shop / cosmetics: shop_add_item, shop_update_item, purchase_item,
2
+ // equip_item, unequip_slot, discard_item.
3
+ //
4
+ // Inventory PDA is keyed by username (cosmetics travel with the name).
5
+ // Equipped slots live on UsernameAccount. The bound key's Ed25519Account
6
+ // is threaded for the on-chain auto-freeze check.
7
+
8
+ import { TransactionInstruction, SystemProgram, PublicKey } from '@solana/web3.js';
9
+ import { PROGRAM_ID, SYSVAR_INSTRUCTIONS, MessageTags } from '../constants.js';
10
+ import {
11
+ borshString, u8, u32LE, u64LE, randomNonce,
12
+ buildEd25519Ix, signedMsg, concat,
13
+ } from '../encoding.js';
14
+ import { ixDisc } from '../disc.js';
15
+ import {
16
+ findConfigPDA, findUsernamePDA, findShopItemPDA, findInventoryPDA,
17
+ findReferralBalancePDA, findNoncePDA, findSaleListingPDA,
18
+ findEd25519AccountPDA, findAdminPDA,
19
+ } from '../pda.js';
20
+
21
+ const k = (pubkey, isSigner, isWritable) => ({ pubkey, isSigner, isWritable });
22
+
23
+ // ── shop_add_item (owner OR admin) ──────────────────────────────────
24
+ export function shopAddItem({ id, kind, price, metadata, signer, admin = null }) {
25
+ const data = concat(
26
+ ixDisc('shop_add_item'),
27
+ u32LE(id),
28
+ u8(kind),
29
+ u64LE(price),
30
+ borshString(metadata),
31
+ );
32
+ const [configPda] = findConfigPDA();
33
+ const [shopPda] = findShopItemPDA(id);
34
+ const adminPda = admin ? findAdminPDA(admin)[0] : null;
35
+ const ix = new TransactionInstruction({
36
+ programId: PROGRAM_ID,
37
+ keys: [
38
+ k(configPda, false, false),
39
+ k(shopPda, false, true),
40
+ k(signer.publicKey, true, true), // signer (init payer)
41
+ k(adminPda ?? PROGRAM_ID, false, false), // admin Option (seed-bound to signer)
42
+ k(SystemProgram.programId, false, false),
43
+ ],
44
+ data,
45
+ });
46
+ return { instructions: [ix], signers: [signer] };
47
+ }
48
+
49
+ // ── shop_update_item (owner OR admin; metadata immutable post-add) ──
50
+ export function shopUpdateItem({ id, price, active, signer, admin = null }) {
51
+ const data = concat(ixDisc('shop_update_item'), u32LE(id), u64LE(price), u8(active ? 1 : 0));
52
+ const [configPda] = findConfigPDA();
53
+ const [shopPda] = findShopItemPDA(id);
54
+ const adminPda = admin ? findAdminPDA(admin)[0] : null;
55
+ const ix = new TransactionInstruction({
56
+ programId: PROGRAM_ID,
57
+ keys: [
58
+ k(configPda, false, false),
59
+ k(shopPda, false, true),
60
+ k(signer.publicKey, true, false),
61
+ k(adminPda ?? PROGRAM_ID, false, false), // admin Option (seed-bound to signer)
62
+ ],
63
+ data,
64
+ });
65
+ return { instructions: [ix], signers: [signer] };
66
+ }
67
+
68
+ // ── shop_set_pro_only (owner-only) ──────────────────────────────────
69
+ //
70
+ // Toggles the Pro-purchase gate on an existing item. Additive to
71
+ // `shopAddItem` (its arg list is unchanged); call this afterwards to
72
+ // mark an item Pro-only. Owners only — same auth shape as shopUpdateItem.
73
+ export function shopSetProOnly({ id, proOnly, signer, admin = null }) {
74
+ const data = concat(ixDisc('shop_set_pro_only'), u32LE(id), u8(proOnly ? 1 : 0));
75
+ const [configPda] = findConfigPDA();
76
+ const [shopPda] = findShopItemPDA(id);
77
+ const adminPda = admin ? findAdminPDA(admin)[0] : null;
78
+ const ix = new TransactionInstruction({
79
+ programId: PROGRAM_ID,
80
+ keys: [
81
+ k(configPda, false, false),
82
+ k(shopPda, false, true),
83
+ k(signer.publicKey, true, false),
84
+ k(adminPda ?? PROGRAM_ID, false, false), // admin Option (seed-bound to signer)
85
+ ],
86
+ data,
87
+ });
88
+ return { instructions: [ix], signers: [signer] };
89
+ }
90
+
91
+ // ── shop_set_active (owner-only) ────────────────────────────────────
92
+ //
93
+ // Delist (active=false) / relist (active=true) without touching price.
94
+ // The ShopItem account stays on chain when delisted, so the catalog
95
+ // remembers it was once listed (client renders a "Retired" state).
96
+ export function shopSetActive({ id, active, signer, admin = null }) {
97
+ const data = concat(ixDisc('shop_set_active'), u32LE(id), u8(active ? 1 : 0));
98
+ const [configPda] = findConfigPDA();
99
+ const [shopPda] = findShopItemPDA(id);
100
+ const adminPda = admin ? findAdminPDA(admin)[0] : null;
101
+ const ix = new TransactionInstruction({
102
+ programId: PROGRAM_ID,
103
+ keys: [
104
+ k(configPda, false, false),
105
+ k(shopPda, false, true),
106
+ k(signer.publicKey, true, false),
107
+ k(adminPda ?? PROGRAM_ID, false, false), // admin Option (seed-bound to signer)
108
+ ],
109
+ data,
110
+ });
111
+ return { instructions: [ix], signers: [signer] };
112
+ }
113
+
114
+ // ── purchase_item (open-payer; auto-freeze gated) ───────────────────
115
+ //
116
+ // Caller must supply:
117
+ // - `boundEd25519Pubkey`: bound key from UsernameAccount.ed25519Pubkey
118
+ // - `referrerForUsername`: UsernameAccount.referrer (Pubkey | null) —
119
+ // used to derive the referral PDA seed.
120
+ export function purchaseItem({
121
+ username, itemId, expectedPrice, payer, boundEd25519Pubkey, referrerForUsername = null,
122
+ }) {
123
+ if (boundEd25519Pubkey == null) {
124
+ throw new Error('purchaseItem: boundEd25519Pubkey required');
125
+ }
126
+ const data = concat(
127
+ ixDisc('purchase_item'),
128
+ borshString(username),
129
+ u32LE(itemId),
130
+ u64LE(expectedPrice),
131
+ );
132
+ const [configPda] = findConfigPDA();
133
+ const [usernamePda] = findUsernamePDA(username);
134
+ const [edPda] = findEd25519AccountPDA(boundEd25519Pubkey);
135
+ const [shopPda] = findShopItemPDA(itemId);
136
+ const [inventoryPda] = findInventoryPDA(username);
137
+ const refKey = referrerForUsername ?? PublicKey.default;
138
+ const [referralPda] = findReferralBalancePDA(refKey);
139
+ const ix = new TransactionInstruction({
140
+ programId: PROGRAM_ID,
141
+ keys: [
142
+ k(configPda, false, true),
143
+ k(usernamePda, false, false),
144
+ k(edPda, false, false),
145
+ k(shopPda, false, false),
146
+ k(inventoryPda, false, true),
147
+ k(referralPda, false, true),
148
+ k(payer.publicKey, true, true),
149
+ k(SystemProgram.programId, false, false),
150
+ ],
151
+ data,
152
+ });
153
+ return { instructions: [ix], signers: [payer] };
154
+ }
155
+
156
+ // ── gift_item (owner OR admin; drops an item into a username's inventory) ──
157
+ //
158
+ // Two signers: `signer` is the owner/admin authority; `payer` funds the
159
+ // inventory rent (defaults to `signer`). admin Option is seed-bound to
160
+ // signer.key() and sits BETWEEN signer and payer (position 6).
161
+ export function giftItem({ username, itemId, boundEd25519Pubkey, signer, payer = signer, admin = null }) {
162
+ if (boundEd25519Pubkey == null) {
163
+ throw new Error('giftItem: boundEd25519Pubkey required');
164
+ }
165
+ const data = concat(ixDisc('gift_item'), borshString(username), u32LE(itemId));
166
+ const [configPda] = findConfigPDA();
167
+ const [usernamePda] = findUsernamePDA(username);
168
+ const [edPda] = findEd25519AccountPDA(boundEd25519Pubkey);
169
+ const [shopPda] = findShopItemPDA(itemId);
170
+ const [inventoryPda] = findInventoryPDA(username);
171
+ const adminPda = admin ? findAdminPDA(admin)[0] : null;
172
+ const ix = new TransactionInstruction({
173
+ programId: PROGRAM_ID,
174
+ keys: [
175
+ k(configPda, false, false), // 0 config
176
+ k(usernamePda, false, false), // 1 username_account
177
+ k(edPda, false, false), // 2 ed25519_account
178
+ k(shopPda, false, false), // 3 shop_item
179
+ k(inventoryPda, false, true), // 4 inventory (init_if_needed, payer=payer)
180
+ k(signer.publicKey, true, false), // 5 signer (authority, NOT mut)
181
+ k(adminPda ?? PROGRAM_ID, false, false), // 6 admin Option (seed-bound to signer)
182
+ k(payer.publicKey, true, true), // 7 payer (mut, rent funder)
183
+ k(SystemProgram.programId, false, false), // 8 system_program
184
+ ],
185
+ data,
186
+ });
187
+ const signers = signer.publicKey.equals(payer.publicKey) ? [signer] : [signer, payer];
188
+ return { instructions: [ix], signers };
189
+ }
190
+
191
+ // ── equip_item (ed25519; auto-freeze gated) ─────────────────────────
192
+ //
193
+ // Equipped slots live on UsernameAccount. Inventory PDA is keyed by
194
+ // username.
195
+ export function equipItem({ username, itemId, kind, ed25519Keypair, payer, nonce = randomNonce() }) {
196
+ const msg = signedMsg(
197
+ MessageTags.EQUIP_ITEM,
198
+ u32LE(itemId),
199
+ u8(kind),
200
+ nonce,
201
+ new TextEncoder().encode(username),
202
+ );
203
+ const ed = buildEd25519Ix(ed25519Keypair, msg);
204
+
205
+ const data = concat(ixDisc('equip_item'), borshString(username), u32LE(itemId), nonce);
206
+ const [usernamePda] = findUsernamePDA(username);
207
+ const [edPda] = findEd25519AccountPDA(ed25519Keypair.publicKey);
208
+ const [shopPda] = findShopItemPDA(itemId);
209
+ const [inventoryPda] = findInventoryPDA(username);
210
+ const [noncePda] = findNoncePDA(nonce);
211
+ const ix = new TransactionInstruction({
212
+ programId: PROGRAM_ID,
213
+ keys: [
214
+ k(usernamePda, false, true),
215
+ k(edPda, false, false),
216
+ k(shopPda, false, false),
217
+ k(inventoryPda, false, false),
218
+ k(noncePda, false, true),
219
+ k(payer.publicKey, true, true),
220
+ k(SYSVAR_INSTRUCTIONS, false, false),
221
+ k(SystemProgram.programId, false, false),
222
+ ],
223
+ data,
224
+ });
225
+ return { instructions: [ed, ix], signers: [payer] };
226
+ }
227
+
228
+ // ── unequip_slot (ed25519; auto-freeze gated) ───────────────────────
229
+ //
230
+ // Unequips a slot on UsernameAccount.
231
+ export function unequipSlot({ username, kind, ed25519Keypair, payer, nonce = randomNonce() }) {
232
+ const msg = signedMsg(MessageTags.UNEQUIP_SLOT, u8(kind), nonce, new TextEncoder().encode(username));
233
+ const ed = buildEd25519Ix(ed25519Keypair, msg);
234
+
235
+ const data = concat(ixDisc('unequip_slot'), borshString(username), u8(kind), nonce);
236
+ const [usernamePda] = findUsernamePDA(username);
237
+ const [edPda] = findEd25519AccountPDA(ed25519Keypair.publicKey);
238
+ const [noncePda] = findNoncePDA(nonce);
239
+ const ix = new TransactionInstruction({
240
+ programId: PROGRAM_ID,
241
+ keys: [
242
+ k(usernamePda, false, true),
243
+ k(edPda, false, false),
244
+ k(noncePda, false, true),
245
+ k(payer.publicKey, true, true),
246
+ k(SYSVAR_INSTRUCTIONS, false, false),
247
+ k(SystemProgram.programId, false, false),
248
+ ],
249
+ data,
250
+ });
251
+ return { instructions: [ed, ix], signers: [payer] };
252
+ }
253
+
254
+ // ── equip_pro_default (ed25519; premium-gated, auto-freeze gated)
255
+ //
256
+ // Sets `equipped_X = Some(0)` for the given kind — the per-kind
257
+ // premium-default sentinel. No item_id arg (always 0), no inventory
258
+ // check (ID 0 is never inventoried). On-chain handler reverts with
259
+ // `RequiresPro` if the username isn't currently premium.
260
+ //
261
+ // `subscribe_pro` already auto-equips Some(0) into empty slots, so
262
+ // the most common path to use this builder is "premium user with a
263
+ // custom equip wants to switch back to the default chip" — typically
264
+ // after an explicit unequip.
265
+ export function equipProDefault({ username, kind, ed25519Keypair, payer, nonce = randomNonce() }) {
266
+ const msg = signedMsg(
267
+ MessageTags.EQUIP_PRO_DEFAULT,
268
+ u8(kind),
269
+ nonce,
270
+ new TextEncoder().encode(username),
271
+ );
272
+ const ed = buildEd25519Ix(ed25519Keypair, msg);
273
+
274
+ const data = concat(ixDisc('equip_pro_default'), borshString(username), u8(kind), nonce);
275
+ const [usernamePda] = findUsernamePDA(username);
276
+ const [edPda] = findEd25519AccountPDA(ed25519Keypair.publicKey);
277
+ const [noncePda] = findNoncePDA(nonce);
278
+ const ix = new TransactionInstruction({
279
+ programId: PROGRAM_ID,
280
+ keys: [
281
+ k(usernamePda, false, true),
282
+ k(edPda, false, false),
283
+ k(noncePda, false, true),
284
+ k(payer.publicKey, true, true),
285
+ k(SYSVAR_INSTRUCTIONS, false, false),
286
+ k(SystemProgram.programId, false, false),
287
+ ],
288
+ data,
289
+ });
290
+ return { instructions: [ed, ix], signers: [payer] };
291
+ }
292
+
293
+ // ── discard_item (ed25519; auto-freeze gated; requires no listing) ──
294
+ //
295
+ // Removes an item from Inventory; auto-clears matching equipped slots
296
+ // on UsernameAccount.
297
+ export function discardItem({ username, itemId, ed25519Keypair, payer, nonce = randomNonce() }) {
298
+ const msg = signedMsg(MessageTags.DISCARD_ITEM, u32LE(itemId), nonce, new TextEncoder().encode(username));
299
+ const ed = buildEd25519Ix(ed25519Keypair, msg);
300
+
301
+ const data = concat(ixDisc('discard_item'), borshString(username), u32LE(itemId), nonce);
302
+ const [usernamePda] = findUsernamePDA(username);
303
+ const [edPda] = findEd25519AccountPDA(ed25519Keypair.publicKey);
304
+ const [inventoryPda] = findInventoryPDA(username);
305
+ const [salePda] = findSaleListingPDA(username);
306
+ const [noncePda] = findNoncePDA(nonce);
307
+ const ix = new TransactionInstruction({
308
+ programId: PROGRAM_ID,
309
+ keys: [
310
+ k(usernamePda, false, true),
311
+ k(edPda, false, false),
312
+ k(inventoryPda, false, true),
313
+ k(salePda, false, false),
314
+ k(noncePda, false, true),
315
+ k(payer.publicKey, true, true),
316
+ k(SYSVAR_INSTRUCTIONS, false, false),
317
+ k(SystemProgram.programId, false, false),
318
+ ],
319
+ data,
320
+ });
321
+ return { instructions: [ed, ix], signers: [payer] };
322
+ }