npm.io
0.3.0 • Published 3d ago

@microslop/ping-directory-sdk

Licence
Apache-2.0
Version
0.3.0
Deps
0
Size
213 kB
Vulns
0
Weekly
0

@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 IdlAccount wrappers.

Install

npm install @microslop/ping-directory-sdk @solana/web3.js tweetnacl

Requires 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 a dataSlice/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/503 with exponential backoff + jitter. getProgramAccounts (rate-limited on many RPCs) routes through it too.
  • The beforeRequest auth hook still runs per HTTP request — batching just means it runs once per coalesced getMultipleAccountsInfo.
  • Disable any piece with scheduler: false / batch: false; the raw web3.js Connection remains reachable as sdk._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 deployed

The 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.

License

Apache 2.0

Keywords