@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.
- package/LICENSE +201 -0
- package/README.md +152 -0
- package/package.json +51 -0
- package/src/PingDirectory.js +1110 -0
- package/src/constants.js +103 -0
- package/src/deserialize.js +322 -0
- package/src/disc.js +76 -0
- package/src/encoding.js +130 -0
- package/src/fees.js +102 -0
- package/src/format.js +149 -0
- package/src/identicon.js +364 -0
- package/src/index.js +58 -0
- package/src/ix/admin.js +351 -0
- package/src/ix/identity.js +418 -0
- package/src/ix/index.js +10 -0
- package/src/ix/lock.js +63 -0
- package/src/ix/marketplace.js +198 -0
- package/src/ix/photo.js +173 -0
- package/src/ix/pro.js +91 -0
- package/src/ix/revoke.js +41 -0
- package/src/ix/shop.js +322 -0
- package/src/pda.js +75 -0
- package/src/wallet.js +189 -0
package/src/ix/photo.js
ADDED
|
@@ -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
|
+
}
|
package/src/ix/revoke.js
ADDED
|
@@ -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
|
+
}
|