@microslop/ping-directory-sdk
Pure-JS client SDK for the Ping Directory v2 Solana program — username registration, marketplace, premium subscriptions, profile photos, and a cosmetics shop.
- No Anchor runtime dep. Just
@solana/web3.js+tweetnacl. - ESM.
import { PingDirectory } from '@microslop/ping-directory-sdk'. - Browser-friendly. No Node-only deps in the runtime path.
- Typed via JSDoc. Returns plain objects with named fields; no opaque
Anchor
IdlAccountwrappers.
Install
npm install @microslop/ping-directory-sdk @solana/web3.js tweetnaclRequires Node ≥ 18 (or any modern browser bundler).
Quick start
import { Connection, Keypair, LAMPORTS_PER_SOL } from '@solana/web3.js';
import nacl from 'tweetnacl';
import { PingDirectory } from '@microslop/ping-directory-sdk';
const conn = new Connection('https://api.mainnet-beta.solana.com', 'confirmed');
const sdk = new PingDirectory(conn);
// Wallet that will pay registration fees.
const payer = Keypair.fromSecretKey(/* your bytes */);
// The ed25519 identity that will control the username.
const identityKp = nacl.sign.keyPair();
const username = 'alice';
// Atomic reserve+attach in one tx.
const sig = await sdk.register({
username, ed25519Keypair: identityKp, payer,
});
// Read state back.
const acc = await sdk.fetchUsername(username);
console.log(acc.state, acc.ed25519Pubkey, acc.premiumUntil);Request dispatch (batching, rate-limiting, retries)
The SDK owns how reads go out, so you can call fetchUsername(x) naively and it
handles dispatch. Three pieces, all on by default and tunable via the
config-bag constructor (existing method signatures are unchanged):
const sdk = new PingDirectory({
rpcUrl, fetch, beforeRequest, // unchanged
// token-bucket scheduler — every RPC call passes through it
scheduler: { maxConcurrent: 8, requestsPerSec: 20, burst: 40,
retry: { retries: 3, baseDelayMs: 250, on: [429, 503] } },
// coalesce per-account reads issued within the window into one
// getMultipleAccountsInfo (chunked at the 100-account RPC cap, dedup'd)
batch: { windowMs: 8, maxBatch: 100 },
// optional short-TTL memoization of raw account reads (OFF unless set)
cache: { ttlMs: 60_000 },
});- Batching applies to plain account reads (
fetchUsername,fetchEd25519,lookupKey, …). Issue N of them in parallel and the SDK sends ≈1 RPC per 100 instead of N. Reads with adataSlice/explicit commitment can't share a batch and go scheduled-direct. Callers see no API change — same return types, fewer RPCs. - Scheduler gates concurrency + rate and retries
429/503with exponential backoff + jitter.getProgramAccounts(rate-limited on many RPCs) routes through it too. - The
beforeRequestauth hook still runs per HTTP request — batching just means it runs once per coalescedgetMultipleAccountsInfo. - Disable any piece with
scheduler: false/batch: false; the raw web3.js Connection remains reachable assdk._rawConnection.
Transaction preview
Decode a built or serialized transaction into a human-verifiable description before anyone signs it — useful for Build-Mode objects, sponsors, and relays that pay gas for a transaction someone else built. Pure + synchronous.
import { describeTransaction, DESTRUCTIVE_INSTRUCTIONS } from '@microslop/ping-directory-sdk';
const d = describeTransaction(base64Object, { expectedFeePayer: myWallet });
// {
// ok: true, instruction: 'list_reserved', label: 'List name for sale',
// name: 'alice', destructive: true, programOk: true, identitySigned: false,
// feePayer: '7xKX…9fQ',
// fields: [ { label: 'Sell price', value: '0.5 SOL' },
// { label: 'Proceeds to', value: '9aQ…', wallet: true } ],
// errors: [],
// }
if (d.ok && d.destructive) requireExtraConfirmation();Accepts a base64 string, raw bytes, or a Transaction. It enforces a program
allow-list (Ping Directory + Ed25519 / System / ComputeBudget), so an object
calling any other program comes back ok: false. The discriminator → name map
is built from the SDK's own ixDisc, so it can't drift from the builders.
Reverse lookup
Given an ed25519 public key, look up its bound username and revocation state in one round trip:
const r = await sdk.lookupKey(identityKp.publicKey);
// { username: 'alice', compromised: false, compromisedAt: null }Batched form auto-chunks at 100 keys per RPC call:
const results = await sdk.lookupKeys([pk1, pk2, /* ... */ pk500]);
// each entry: { pubkey, username, compromised, compromisedAt }For messenger-style flows that need both identity-state and username-state in one shot:
const u = await sdk.fetchUsernameWithKeyState('alice');
// UsernameAccount + { compromised, compromisedAt }Key revocation
If a user suspects their ed25519 key has leaked, they can permanently retire it (one-way; the same key can never bind a username again):
await sdk.markCompromised({ ed25519Keypair: identityKp, payer });API surface
The PingDirectory facade wraps every program instruction with sensible
defaults — for most flows it's the only thing you need:
- Identity:
register,reserveUsername,attachPubkey,transferReserved,unregisterReserved,updatePubkey,transferUsername,unregisterActive,markCompromised - Lock:
requestUnlock,lock - Marketplace:
listReserved,listActive,buyReserved,buyActive,cancelSaleReserved,cancelSaleActive - Shop / cosmetics:
purchaseItem,equipItem,unequipSlot,discardItem - Profile photo:
initProfilePhoto,writePhotoChunk,finalizeProfilePhoto,clearProfilePhoto - Premium:
subscribePremium,setPremium,withdrawReferral - Reads:
fetchConfig,fetchUsername,fetchUsernameWithKeyState,fetchSaleListing,fetchProfilePhoto,fetchInventory,fetchShopItem,fetchReferralBalance,fetchEd25519,lookupKey,lookupKeys,isCompromised - Admin (owner / admin only):
addAdmin,removeAdmin,proposeOwner,acceptOwner,cancelProposeOwner,pauseRegistration,pausePremium,blocklistAdd,blocklistRemove,withdrawTreasury,setRegistrationFee,setPremiumPriceMonthly,setPremiumPriceLifetime,setSaleFee,setMinSalePrice,setGracePeriod,setUnlockDelay,setUnlockWindow
Lower-level entry points
For advanced flows (custom tx assembly, batching multiple ix into one tx, Squads/multisig signing) the SDK also re-exports:
import {
PROGRAM_ID, MessageTags,
findConfigPDA, findUsernamePDA, findEd25519AccountPDA, /* ... */
deserializeUsernameAccount, deserializeEd25519Account, /* ... */
ix, // namespace of all instruction builders
} from '@microslop/ping-directory-sdk';
const built = ix.register({ username, ed25519Keypair,
referrer: null, expectedUserId: 42n, payer: payer.publicKey,
nonce: randomNonce() });
// built = { instructions: TransactionInstruction[], signers: Signer[] }State semantics
Usernames live in one of two states:
- Reserved — a Solana wallet paid the registration fee. The wallet controls transfer / unregister / list-for-sale. No ed25519 attached.
- Active — an ed25519 key controls the username. The wallet field is permanently zeroed; the wallet has no further authority.
Reserved → Active is one-way (attachPubkey, or atomic register).
Active state is the steady state for messenger-style use; ed25519 is
the canonical identity, the wallet is just for one-time fee payment.
Development
npm install
npm test # requires a localnet validator with the program deployedThe test suite runs against http://127.0.0.1:8899 by default. Override
with RPC_URL=https://.... Tests are sequential (--test-concurrency=1)
because they share global on-chain state.