@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/constants.js
ADDED
|
@@ -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
|
+
}
|
package/src/encoding.js
ADDED
|
@@ -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
|
+
}
|