@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/fees.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Deterministic transaction-fee math (no RPC required).
|
|
2
|
+
//
|
|
3
|
+
// Ported from the legacy SDK (helpers.js). Mirrors `Config::fee_for_length`
|
|
4
|
+
// and the per-account `SIZE` constants in the Anchor program's `state/*.rs`.
|
|
5
|
+
|
|
6
|
+
/** Format lamports as a SOL string with 4 decimals (matches the legacy SDK behavior). */
|
|
7
|
+
export function lamportsToSol(lamports) {
|
|
8
|
+
// The legacy SDK returned a string; clients depend on the exact formatting.
|
|
9
|
+
const n = typeof lamports === 'bigint' ? Number(lamports) : lamports;
|
|
10
|
+
return (n / 1_000_000_000).toFixed(4);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Inverse of lamportsToSol — parse a SOL string/number to integer lamports. */
|
|
14
|
+
export function solToLamports(sol) {
|
|
15
|
+
return Math.round(parseFloat(sol) * 1e9);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Solana rent-exempt minimum (lamports) for a given account data size in bytes. */
|
|
19
|
+
export function rentExempt(dataSize) {
|
|
20
|
+
return Math.ceil((128 + dataSize) * 6960);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Tiered registration fee selector for `Config::registration_fees` (length 7).
|
|
25
|
+
* Tiers 1..=6 = exact username length, tier 7 = "7 or more".
|
|
26
|
+
*/
|
|
27
|
+
export function feeForLength(registrationFees, len) {
|
|
28
|
+
const idx = len >= 7 ? 6 : Math.max(0, len - 1);
|
|
29
|
+
return registrationFees[idx];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Solana base tx fee: 5,000 lamports per signature. */
|
|
33
|
+
export function txFee(numSignatures = 1) {
|
|
34
|
+
return numSignatures * 5000;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Account sizes (mirror const SIZE in `state/*.rs`).
|
|
38
|
+
const ACCOUNT_SIZES = {
|
|
39
|
+
usernameAccount: 8 + (4 + 20) + 32 + (1 + 32) + (1 + 32) + 8 + 8 + 1 + 8 + 1 + 16 + 16 + (1 + 4) * 3 + 1,
|
|
40
|
+
reverseLookup: 8 + (4 + 20) + 1,
|
|
41
|
+
keyHistory: 8 + 4 + 1,
|
|
42
|
+
nonce: 8 + 1,
|
|
43
|
+
saleListing: 8 + 8 + 32 + 1,
|
|
44
|
+
pendingRecovery: 8 + 32 + 8 + 1,
|
|
45
|
+
gracePeriod: 8 + 32 + 8 + 1,
|
|
46
|
+
walletLink: 8 + (4 + 20) + 1,
|
|
47
|
+
inventory: 8 + 4 + 128 * 4 + 1,
|
|
48
|
+
shopItem: 8 + 4 + 1 + 8 + 1 + (4 + 512) + 1,
|
|
49
|
+
stakedBalance: 8 + 8 + 8 + 16 + 1,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Add ~10% buffer for priority fees / compute budget. */
|
|
53
|
+
function withBuffer(lamports) {
|
|
54
|
+
return Math.ceil(lamports * 1.1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Estimated transaction fees (lamports) for each operation.
|
|
59
|
+
* Includes base tx fee + rent for new PDAs + ~10% buffer for priority/compute.
|
|
60
|
+
* Does NOT include program fees (registration_fee, etc). Deterministic — no RPC.
|
|
61
|
+
*/
|
|
62
|
+
export const txFees = {
|
|
63
|
+
// Register also creates a WalletLink (the wallet is bound atomically).
|
|
64
|
+
register: withBuffer(
|
|
65
|
+
txFee(2)
|
|
66
|
+
+ rentExempt(ACCOUNT_SIZES.usernameAccount)
|
|
67
|
+
+ rentExempt(ACCOUNT_SIZES.reverseLookup)
|
|
68
|
+
+ rentExempt(ACCOUNT_SIZES.keyHistory)
|
|
69
|
+
+ rentExempt(ACCOUNT_SIZES.nonce)
|
|
70
|
+
+ rentExempt(ACCOUNT_SIZES.walletLink)
|
|
71
|
+
),
|
|
72
|
+
unregister: withBuffer(txFee(2) + rentExempt(ACCOUNT_SIZES.nonce) + rentExempt(ACCOUNT_SIZES.gracePeriod)),
|
|
73
|
+
updatePubkey: withBuffer(txFee(2) + rentExempt(ACCOUNT_SIZES.reverseLookup) + rentExempt(ACCOUNT_SIZES.nonce)),
|
|
74
|
+
transferUsername: withBuffer(txFee(2) + rentExempt(ACCOUNT_SIZES.reverseLookup) + rentExempt(ACCOUNT_SIZES.nonce)),
|
|
75
|
+
requestUnlock: withBuffer(txFee(2) + rentExempt(ACCOUNT_SIZES.nonce)),
|
|
76
|
+
lock: withBuffer(txFee(2) + rentExempt(ACCOUNT_SIZES.nonce)),
|
|
77
|
+
walletLock: withBuffer(txFee(1)),
|
|
78
|
+
listForSale: withBuffer(txFee(2) + rentExempt(ACCOUNT_SIZES.saleListing) + rentExempt(ACCOUNT_SIZES.nonce)),
|
|
79
|
+
buyUsername: withBuffer(txFee(1) + rentExempt(ACCOUNT_SIZES.reverseLookup)),
|
|
80
|
+
cancelSale: withBuffer(txFee(2) + rentExempt(ACCOUNT_SIZES.nonce)),
|
|
81
|
+
// Swap mode: closes old WalletLink (rent refund) and inits new one (rent cost).
|
|
82
|
+
// Net rent delta is 0; only tx + nonce rent count.
|
|
83
|
+
associateWallet: withBuffer(txFee(2) + rentExempt(ACCOUNT_SIZES.nonce)),
|
|
84
|
+
removeWallet: withBuffer(txFee(2) + rentExempt(ACCOUNT_SIZES.nonce)),
|
|
85
|
+
initiateRecovery: withBuffer(txFee(1) + rentExempt(ACCOUNT_SIZES.pendingRecovery)),
|
|
86
|
+
finalizeRecovery: withBuffer(txFee(1) + rentExempt(ACCOUNT_SIZES.reverseLookup)),
|
|
87
|
+
vetoRecovery: withBuffer(txFee(2) + rentExempt(ACCOUNT_SIZES.nonce)),
|
|
88
|
+
subscribePro: withBuffer(txFee(1)),
|
|
89
|
+
setAccountType: withBuffer(txFee(2) + rentExempt(ACCOUNT_SIZES.nonce)),
|
|
90
|
+
withdrawReferral: withBuffer(txFee(1)),
|
|
91
|
+
// Shop / staking
|
|
92
|
+
shopAddItem: withBuffer(txFee(1) + rentExempt(ACCOUNT_SIZES.shopItem)),
|
|
93
|
+
shopUpdateItem: withBuffer(txFee(1)),
|
|
94
|
+
// First purchase initializes the Inventory PDA; subsequent purchases reuse it.
|
|
95
|
+
// We charge the rent every call to be conservative for fee estimates.
|
|
96
|
+
purchaseItem: withBuffer(txFee(1) + rentExempt(ACCOUNT_SIZES.inventory)),
|
|
97
|
+
equipItem: withBuffer(txFee(1)),
|
|
98
|
+
unequipSlot: withBuffer(txFee(1)),
|
|
99
|
+
stake: withBuffer(txFee(1) + rentExempt(ACCOUNT_SIZES.stakedBalance)),
|
|
100
|
+
extendStake: withBuffer(txFee(1)),
|
|
101
|
+
claimStake: withBuffer(txFee(1)),
|
|
102
|
+
};
|
package/src/format.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Address / hex / base58 formatting helpers.
|
|
2
|
+
//
|
|
3
|
+
// Ported from the legacy SDK (encoding.js + helpers.js) so the v1.0.0
|
|
4
|
+
// SDK is a drop-in replacement at client callsites.
|
|
5
|
+
//
|
|
6
|
+
// Pure JS — no new runtime deps. We inline a minimal SHA3-256 (Keccak f1600
|
|
7
|
+
// with SHA-3 padding 0x06) here so we don't pull in @noble/hashes just for
|
|
8
|
+
// the .ping address checksum.
|
|
9
|
+
|
|
10
|
+
// ── Hex helpers ─────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
/** Encode a Uint8Array as lowercase hex (no `0x` prefix). */
|
|
13
|
+
export function bytesToHex(bytes) {
|
|
14
|
+
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Decode a hex string (no `0x` prefix, even length) into a Uint8Array. */
|
|
18
|
+
export function hexToBytes(hex) {
|
|
19
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
20
|
+
for (let i = 0; i < hex.length; i += 2) bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
|
|
21
|
+
return bytes;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── Base58 (Bitcoin alphabet) ───────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const B58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
27
|
+
|
|
28
|
+
/** Base58 encode arbitrary bytes (Bitcoin alphabet). */
|
|
29
|
+
export function b58encode(bytes) {
|
|
30
|
+
let zeros = 0;
|
|
31
|
+
while (zeros < bytes.length && bytes[zeros] === 0) zeros++;
|
|
32
|
+
if (zeros === bytes.length) return '1'.repeat(bytes.length);
|
|
33
|
+
let num = 0n;
|
|
34
|
+
for (const b of bytes) num = num * 256n + BigInt(b);
|
|
35
|
+
let str = '';
|
|
36
|
+
while (num > 0n) { str = B58[Number(num % 58n)] + str; num /= 58n; }
|
|
37
|
+
return '1'.repeat(zeros) + str;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Base32 (RFC 4648, no padding) ───────────────────────────────────
|
|
41
|
+
|
|
42
|
+
const B32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
43
|
+
|
|
44
|
+
function base32Encode(bytes) {
|
|
45
|
+
let bits = 0, val = 0, out = '';
|
|
46
|
+
for (const b of bytes) {
|
|
47
|
+
val = (val << 8) | b;
|
|
48
|
+
bits += 8;
|
|
49
|
+
while (bits >= 5) { bits -= 5; out += B32[(val >>> bits) & 31]; }
|
|
50
|
+
}
|
|
51
|
+
if (bits > 0) out += B32[(val << (5 - bits)) & 31];
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Minimal SHA3-256 (Keccak f1600, SHA-3 padding 0x06) ─────────────
|
|
56
|
+
// ~60 LOC pure JS. Only used by pingChecksum below.
|
|
57
|
+
|
|
58
|
+
const KECCAK_RC = [
|
|
59
|
+
0x0000000000000001n, 0x0000000000008082n, 0x800000000000808an, 0x8000000080008000n,
|
|
60
|
+
0x000000000000808bn, 0x0000000080000001n, 0x8000000080008081n, 0x8000000000008009n,
|
|
61
|
+
0x000000000000008an, 0x0000000000000088n, 0x0000000080008009n, 0x000000008000000an,
|
|
62
|
+
0x000000008000808bn, 0x800000000000008bn, 0x8000000000008089n, 0x8000000000008003n,
|
|
63
|
+
0x8000000000008002n, 0x8000000000000080n, 0x000000000000800an, 0x800000008000000an,
|
|
64
|
+
0x8000000080008081n, 0x8000000000008080n, 0x0000000080000001n, 0x8000000080008008n,
|
|
65
|
+
];
|
|
66
|
+
const KECCAK_R = [0,1,62,28,27,36,44,6,55,20,3,10,43,25,39,41,45,15,21,8,18,2,61,56,14];
|
|
67
|
+
const MASK64 = 0xffffffffffffffffn;
|
|
68
|
+
const rotl64 = (x, n) => (((x << BigInt(n)) | (x >> BigInt(64 - n))) & MASK64);
|
|
69
|
+
|
|
70
|
+
function keccakF1600(s) {
|
|
71
|
+
for (let r = 0; r < 24; r++) {
|
|
72
|
+
// θ
|
|
73
|
+
const C = new Array(5);
|
|
74
|
+
for (let x = 0; x < 5; x++) C[x] = s[x] ^ s[x+5] ^ s[x+10] ^ s[x+15] ^ s[x+20];
|
|
75
|
+
const D = new Array(5);
|
|
76
|
+
for (let x = 0; x < 5; x++) D[x] = C[(x+4)%5] ^ rotl64(C[(x+1)%5], 1);
|
|
77
|
+
for (let i = 0; i < 25; i++) s[i] ^= D[i % 5];
|
|
78
|
+
// ρ + π
|
|
79
|
+
const B = new Array(25);
|
|
80
|
+
for (let x = 0; x < 5; x++) for (let y = 0; y < 5; y++) {
|
|
81
|
+
B[y + ((2*x + 3*y) % 5) * 5] = rotl64(s[x + y*5], KECCAK_R[x + y*5]);
|
|
82
|
+
}
|
|
83
|
+
// χ
|
|
84
|
+
for (let y = 0; y < 5; y++) for (let x = 0; x < 5; x++) {
|
|
85
|
+
s[x + y*5] = B[x + y*5] ^ ((~B[((x+1)%5) + y*5]) & MASK64 & B[((x+2)%5) + y*5]);
|
|
86
|
+
}
|
|
87
|
+
// ι
|
|
88
|
+
s[0] ^= KECCAK_RC[r];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function sha3_256(data) {
|
|
93
|
+
// SHA3-256: rate = 136 bytes, output 32 bytes, domain pad 0x06.
|
|
94
|
+
const rate = 136;
|
|
95
|
+
// Pad
|
|
96
|
+
const padLen = rate - (data.length % rate);
|
|
97
|
+
const padded = new Uint8Array(data.length + padLen);
|
|
98
|
+
padded.set(data, 0);
|
|
99
|
+
padded[data.length] = 0x06;
|
|
100
|
+
padded[padded.length - 1] |= 0x80;
|
|
101
|
+
// Absorb
|
|
102
|
+
const state = new Array(25).fill(0n);
|
|
103
|
+
for (let off = 0; off < padded.length; off += rate) {
|
|
104
|
+
for (let i = 0; i < rate / 8; i++) {
|
|
105
|
+
let lane = 0n;
|
|
106
|
+
for (let j = 0; j < 8; j++) lane |= BigInt(padded[off + i*8 + j]) << BigInt(j*8);
|
|
107
|
+
state[i] ^= lane;
|
|
108
|
+
}
|
|
109
|
+
keccakF1600(state);
|
|
110
|
+
}
|
|
111
|
+
// Squeeze 32 bytes
|
|
112
|
+
const out = new Uint8Array(32);
|
|
113
|
+
for (let i = 0; i < 4; i++) {
|
|
114
|
+
let lane = state[i];
|
|
115
|
+
for (let j = 0; j < 8; j++) { out[i*8 + j] = Number(lane & 0xffn); lane >>= 8n; }
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Ping address encoding ───────────────────────────────────────────
|
|
121
|
+
// Domain-separated checksum + base32 + ".ping" suffix.
|
|
122
|
+
|
|
123
|
+
const PING_VERSION = 0x01;
|
|
124
|
+
const PING_DOMAIN = new TextEncoder().encode('.ping checksum');
|
|
125
|
+
|
|
126
|
+
function pingChecksum(pkBytes) {
|
|
127
|
+
const buf = new Uint8Array(PING_DOMAIN.length + pkBytes.length + 1);
|
|
128
|
+
buf.set(PING_DOMAIN, 0);
|
|
129
|
+
buf.set(pkBytes, PING_DOMAIN.length);
|
|
130
|
+
buf[buf.length - 1] = PING_VERSION;
|
|
131
|
+
const d = sha3_256(buf);
|
|
132
|
+
return new Uint8Array([d[0], d[1]]);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Convert a 32-byte Ed25519 pubkey to a .ping address.
|
|
137
|
+
* Format: base32(pk || checksum2 || version1).toLowerCase() + ".ping"
|
|
138
|
+
*/
|
|
139
|
+
export function pubkeyBytesToPingAddress(pkBytes) {
|
|
140
|
+
if (!(pkBytes instanceof Uint8Array) || pkBytes.length !== 32) {
|
|
141
|
+
throw new Error('pubkeyBytesToPingAddress: expected 32-byte Uint8Array');
|
|
142
|
+
}
|
|
143
|
+
const cs = pingChecksum(pkBytes);
|
|
144
|
+
const payload = new Uint8Array(35);
|
|
145
|
+
payload.set(pkBytes, 0);
|
|
146
|
+
payload.set(cs, 32);
|
|
147
|
+
payload[34] = PING_VERSION;
|
|
148
|
+
return base32Encode(payload).toLowerCase() + '.ping';
|
|
149
|
+
}
|
package/src/identicon.js
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ping Identicon Generator (v2)
|
|
3
|
+
*
|
|
4
|
+
* Generates deterministic identicons from Ed25519 hex pubkeys with a
|
|
5
|
+
* built-in foreground icon overlay (user / group). Designed to give a
|
|
6
|
+
* recognizable "this is a user" or "this is a group" cue at any size,
|
|
7
|
+
* while still varying the background uniquely per pubkey.
|
|
8
|
+
*
|
|
9
|
+
* Pipeline:
|
|
10
|
+
* 1. seed RNG from the input
|
|
11
|
+
* 2. pick one of 5 background styles (Pixels, Diamonds, Plasma, Slab,
|
|
12
|
+
* Voronoi) based on a hash of first/middle/last 4 chars
|
|
13
|
+
* 3. paint the background
|
|
14
|
+
* 4. read the average color of the central 40% of the canvas
|
|
15
|
+
* 5. derive a foreground that's the perceptual *negative* of the
|
|
16
|
+
* background (complementary hue, inverted lightness, high
|
|
17
|
+
* saturation) — guarantees readable contrast and visual interest
|
|
18
|
+
* 6. paint a centered user / group silhouette in that foreground
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* drawIdenticon(canvas, '<hex pubkey>', 64); // user
|
|
22
|
+
* drawIdenticon(canvas, '<hex pubkey>', 64, { kind: 'group' }); // group
|
|
23
|
+
* drawIdenticon(canvas, '<addr>.ping', 64); // ping addr also accepted
|
|
24
|
+
*
|
|
25
|
+
* Backward compat — `drawIdenticon(canvas, key, size)` continues to
|
|
26
|
+
* work and renders a user-kind avatar. The 20-style legacy API has
|
|
27
|
+
* been retired; `IDENTICON_STYLE_COUNT === 5` now.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// ── Seeded RNG ──────────────────────────────────────────────────────
|
|
31
|
+
function hashStr(str) {
|
|
32
|
+
let h = 0;
|
|
33
|
+
for (let i = 0; i < str.length; i++) h = ((h << 5) - h + str.charCodeAt(i)) | 0;
|
|
34
|
+
return h;
|
|
35
|
+
}
|
|
36
|
+
function seededRand(seed) {
|
|
37
|
+
let s = seed | 0;
|
|
38
|
+
return () => { s = (s * 16807 + 0) % 2147483647; return (s & 0x7fffffff) / 0x7fffffff; };
|
|
39
|
+
}
|
|
40
|
+
function pick(rand, arr) { return arr[Math.floor(rand() * arr.length)]; }
|
|
41
|
+
|
|
42
|
+
// ── Palettes ────────────────────────────────────────────────────────
|
|
43
|
+
const PALETTES = [
|
|
44
|
+
['#7c3aed', '#a78bfa', '#6d28d9', '#4c1d95', '#1e1b4b'],
|
|
45
|
+
['#8b5cf6', '#c4b5fd', '#5b21b6', '#7e22ce', '#2e1065'],
|
|
46
|
+
['#7c3aed', '#22d3ee', '#6d28d9', '#0891b2', '#1e1b4b'],
|
|
47
|
+
['#9333ea', '#a855f7', '#7e22ce', '#6366f1', '#312e81'],
|
|
48
|
+
['#8b5cf6', '#34d399', '#6d28d9', '#059669', '#1e1b4b'],
|
|
49
|
+
['#a78bfa', '#f472b6', '#7c3aed', '#ec4899', '#2e1065'],
|
|
50
|
+
['#6366f1', '#818cf8', '#4f46e5', '#3730a3', '#1e1b4b'],
|
|
51
|
+
['#8b5cf6', '#fbbf24', '#6d28d9', '#d97706', '#2e1065'],
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const BG = ['#0d0d1a', '#12121e', '#151525', '#0a0a14', '#100e1e', '#0e0c18'];
|
|
55
|
+
|
|
56
|
+
/** Normalize input to a hex pubkey string (strip .ping suffix if present). */
|
|
57
|
+
function normalizeKey(input) {
|
|
58
|
+
if (!input) return '';
|
|
59
|
+
return input.replace(/\.ping$/i, '');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Pick style index from first 4 + middle 4 + last 4 chars of the key.
|
|
64
|
+
* Combines their char codes for better distribution across all styles.
|
|
65
|
+
*/
|
|
66
|
+
function pickStyle(key) {
|
|
67
|
+
if (key.length < 12) return Math.abs(hashStr(key)) % STYLES.length;
|
|
68
|
+
const mid = Math.floor(key.length / 2);
|
|
69
|
+
const combined =
|
|
70
|
+
hashStr(key.slice(0, 4)) ^
|
|
71
|
+
hashStr(key.slice(mid - 2, mid + 2)) ^
|
|
72
|
+
hashStr(key.slice(-4));
|
|
73
|
+
return Math.abs(combined) % STYLES.length;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function setup(input) {
|
|
77
|
+
const key = normalizeKey(input);
|
|
78
|
+
const h = hashStr(key);
|
|
79
|
+
const rand = seededRand(h);
|
|
80
|
+
const pal = PALETTES[Math.abs(h) % PALETTES.length];
|
|
81
|
+
const bg = BG[Math.abs(h >> 3) % BG.length];
|
|
82
|
+
return { key, h, rand, pal, bg };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Backgrounds (5 curated styles) ──────────────────────────────────
|
|
86
|
+
|
|
87
|
+
function drawPixels(ctx, size, rand, pal, bg) {
|
|
88
|
+
ctx.fillStyle = bg;
|
|
89
|
+
ctx.fillRect(0, 0, size, size);
|
|
90
|
+
const g = 5, cs = size / g, half = Math.ceil(g / 2);
|
|
91
|
+
const cells = [];
|
|
92
|
+
for (let y = 0; y < g; y++) {
|
|
93
|
+
cells[y] = [];
|
|
94
|
+
for (let x = 0; x < half; x++) cells[y][x] = rand() > 0.4;
|
|
95
|
+
for (let x = half; x < g; x++) cells[y][x] = cells[y][g - 1 - x];
|
|
96
|
+
}
|
|
97
|
+
for (let y = 0; y < g; y++) {
|
|
98
|
+
for (let x = 0; x < g; x++) {
|
|
99
|
+
if (!cells[y][x]) continue;
|
|
100
|
+
ctx.fillStyle = pick(rand, pal);
|
|
101
|
+
ctx.globalAlpha = 0.6 + rand() * 0.4;
|
|
102
|
+
ctx.fillRect(x * cs, y * cs, cs, cs);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
ctx.globalAlpha = 1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function drawDiamonds(ctx, size, rand, pal, bg) {
|
|
109
|
+
ctx.fillStyle = bg;
|
|
110
|
+
ctx.fillRect(0, 0, size, size);
|
|
111
|
+
const g = 4, cs = size / g;
|
|
112
|
+
ctx.save();
|
|
113
|
+
ctx.translate(size / 2, size / 2);
|
|
114
|
+
ctx.rotate(Math.PI / 4);
|
|
115
|
+
ctx.translate(-size / 2, -size / 2);
|
|
116
|
+
for (let y = -1; y <= g + 1; y++) {
|
|
117
|
+
for (let x = -1; x <= g + 1; x++) {
|
|
118
|
+
if (rand() <= 0.35) continue;
|
|
119
|
+
ctx.fillStyle = pick(rand, pal);
|
|
120
|
+
ctx.globalAlpha = 0.3 + rand() * 0.6;
|
|
121
|
+
const p = cs * 0.1;
|
|
122
|
+
ctx.fillRect(x * cs + p, y * cs + p, cs - p * 2, cs - p * 2);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
ctx.restore();
|
|
126
|
+
ctx.globalAlpha = 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function drawPlasma(ctx, size, rand, pal, bg) {
|
|
130
|
+
const img = ctx.createImageData(size, size);
|
|
131
|
+
const freqs = [rand() * 0.08 + 0.02, rand() * 0.08 + 0.02, rand() * 0.06 + 0.01];
|
|
132
|
+
const phases = [rand() * 10, rand() * 10, rand() * 10];
|
|
133
|
+
const hex2rgb = (hex) => [
|
|
134
|
+
parseInt(hex.slice(1, 3), 16),
|
|
135
|
+
parseInt(hex.slice(3, 5), 16),
|
|
136
|
+
parseInt(hex.slice(5, 7), 16),
|
|
137
|
+
];
|
|
138
|
+
const c1 = hex2rgb(pal[0]);
|
|
139
|
+
const c2 = hex2rgb(pal[1]);
|
|
140
|
+
const c3 = hex2rgb(pal[3] || pal[0]);
|
|
141
|
+
const bgc = hex2rgb(bg);
|
|
142
|
+
for (let y = 0; y < size; y++) {
|
|
143
|
+
for (let x = 0; x < size; x++) {
|
|
144
|
+
const v =
|
|
145
|
+
(Math.sin(x * freqs[0] + phases[0]) +
|
|
146
|
+
Math.sin(y * freqs[1] + phases[1]) +
|
|
147
|
+
Math.sin((x + y) * freqs[2] + phases[2])) /
|
|
148
|
+
3;
|
|
149
|
+
const t = (v + 1) / 2;
|
|
150
|
+
const col =
|
|
151
|
+
t < 0.33
|
|
152
|
+
? c1.map((c, i) => Math.round(bgc[i] + (c - bgc[i]) * (t / 0.33)))
|
|
153
|
+
: t < 0.66
|
|
154
|
+
? c1.map((c, i) => Math.round(c + (c2[i] - c) * ((t - 0.33) / 0.33)))
|
|
155
|
+
: c2.map((c, i) => Math.round(c + (c3[i] - c) * ((t - 0.66) / 0.34)));
|
|
156
|
+
const idx = (y * size + x) * 4;
|
|
157
|
+
img.data[idx] = col[0];
|
|
158
|
+
img.data[idx + 1] = col[1];
|
|
159
|
+
img.data[idx + 2] = col[2];
|
|
160
|
+
img.data[idx + 3] = 255;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
ctx.putImageData(img, 0, 0);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function drawSlab(ctx, size, rand, pal) {
|
|
167
|
+
const angle = rand() * Math.PI * 2;
|
|
168
|
+
const x1 = size / 2 + Math.cos(angle) * size / 2;
|
|
169
|
+
const y1 = size / 2 + Math.sin(angle) * size / 2;
|
|
170
|
+
const x2 = size / 2 - Math.cos(angle) * size / 2;
|
|
171
|
+
const y2 = size / 2 - Math.sin(angle) * size / 2;
|
|
172
|
+
const grad = ctx.createLinearGradient(x1, y1, x2, y2);
|
|
173
|
+
grad.addColorStop(0, pal[0]);
|
|
174
|
+
grad.addColorStop(0.5, pal[1]);
|
|
175
|
+
grad.addColorStop(1, pal[3] || pal[2]);
|
|
176
|
+
ctx.fillStyle = grad;
|
|
177
|
+
ctx.fillRect(0, 0, size, size);
|
|
178
|
+
ctx.globalAlpha = 0.06;
|
|
179
|
+
for (let i = 0; i < size * size * 0.03; i++) {
|
|
180
|
+
ctx.fillStyle = rand() > 0.5 ? '#fff' : '#000';
|
|
181
|
+
ctx.fillRect(Math.floor(rand() * size), Math.floor(rand() * size), 1, 1);
|
|
182
|
+
}
|
|
183
|
+
ctx.globalAlpha = 1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function drawVoronoi(ctx, size, rand, pal, bg) {
|
|
187
|
+
ctx.fillStyle = bg;
|
|
188
|
+
ctx.fillRect(0, 0, size, size);
|
|
189
|
+
const n = 6 + Math.floor(rand() * 6);
|
|
190
|
+
const seeds = [];
|
|
191
|
+
const colors = [];
|
|
192
|
+
for (let i = 0; i < n; i++) {
|
|
193
|
+
seeds.push([rand() * size, rand() * size]);
|
|
194
|
+
colors.push(pick(rand, pal));
|
|
195
|
+
}
|
|
196
|
+
const hex2rgb = (hex) => [
|
|
197
|
+
parseInt(hex.slice(1, 3), 16),
|
|
198
|
+
parseInt(hex.slice(3, 5), 16),
|
|
199
|
+
parseInt(hex.slice(5, 7), 16),
|
|
200
|
+
];
|
|
201
|
+
const rgbs = colors.map(hex2rgb);
|
|
202
|
+
const img = ctx.createImageData(size, size);
|
|
203
|
+
for (let y = 0; y < size; y++) {
|
|
204
|
+
for (let x = 0; x < size; x++) {
|
|
205
|
+
let minD = Infinity;
|
|
206
|
+
let minI = 0;
|
|
207
|
+
for (let i = 0; i < n; i++) {
|
|
208
|
+
const d = (x - seeds[i][0]) ** 2 + (y - seeds[i][1]) ** 2;
|
|
209
|
+
if (d < minD) { minD = d; minI = i; }
|
|
210
|
+
}
|
|
211
|
+
const idx = (y * size + x) * 4;
|
|
212
|
+
img.data[idx] = rgbs[minI][0];
|
|
213
|
+
img.data[idx + 1] = rgbs[minI][1];
|
|
214
|
+
img.data[idx + 2] = rgbs[minI][2];
|
|
215
|
+
img.data[idx + 3] = 200;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
ctx.putImageData(img, 0, 0);
|
|
219
|
+
ctx.globalAlpha = 1;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const STYLES = [drawPixels, drawDiamonds, drawPlasma, drawSlab, drawVoronoi];
|
|
223
|
+
|
|
224
|
+
const STYLE_NAMES = ['Pixels', 'Diamonds', 'Plasma', 'Slab', 'Voronoi'];
|
|
225
|
+
|
|
226
|
+
// ── Foreground color = black or white based on background luminance ─
|
|
227
|
+
//
|
|
228
|
+
// The earlier implementation read the central 40% of the canvas via
|
|
229
|
+
// `ctx.getImageData()` to compute Rec. 709 luminance from the actual
|
|
230
|
+
// pixels. That triggers Tor Browser's canvas-fingerprint defense
|
|
231
|
+
// (modal "allow site to access HTML5 canvas image data?" prompt) on
|
|
232
|
+
// every avatar render — unacceptable UX. The inputs are deterministic
|
|
233
|
+
// (palette + bg are a function of the seed), so we approximate the
|
|
234
|
+
// central-area average colour analytically: weighted blend of palette
|
|
235
|
+
// average with the background, then Rec. 709 luma. The 0.55 cutoff
|
|
236
|
+
// matches the old behaviour and keeps the bias toward a white icon
|
|
237
|
+
// (dark silhouettes on mid-bright backgrounds disappear faster than
|
|
238
|
+
// the inverse). The cosmetic diff vs. true sampling is small because
|
|
239
|
+
// the central area is dominated by bg + a few palette daubs across
|
|
240
|
+
// every style — and a small drop shadow on the icon keeps the edge
|
|
241
|
+
// legible regardless.
|
|
242
|
+
|
|
243
|
+
function hexToRgb(hex) {
|
|
244
|
+
const h = hex.replace('#', '');
|
|
245
|
+
return {
|
|
246
|
+
r: parseInt(h.slice(0, 2), 16),
|
|
247
|
+
g: parseInt(h.slice(2, 4), 16),
|
|
248
|
+
b: parseInt(h.slice(4, 6), 16),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function pickIconFg(pal, bg) {
|
|
253
|
+
// Approximate central-region coverage: backgrounds fill the canvas
|
|
254
|
+
// with `bg` first then paint palette colours over the top. The
|
|
255
|
+
// central 40% sees roughly 35% palette pixels averaged across the 5
|
|
256
|
+
// styles (Slab is highest, Pixels/Diamonds lowest); 0.35 is a
|
|
257
|
+
// pragmatic single number that lands close on all five.
|
|
258
|
+
const PALETTE_WEIGHT = 0.35;
|
|
259
|
+
let pr = 0, pg = 0, pb = 0;
|
|
260
|
+
for (const c of pal) {
|
|
261
|
+
const { r, g, b } = hexToRgb(c);
|
|
262
|
+
pr += r; pg += g; pb += b;
|
|
263
|
+
}
|
|
264
|
+
pr /= pal.length; pg /= pal.length; pb /= pal.length;
|
|
265
|
+
const bgc = hexToRgb(bg);
|
|
266
|
+
const r = pr * PALETTE_WEIGHT + bgc.r * (1 - PALETTE_WEIGHT);
|
|
267
|
+
const g = pg * PALETTE_WEIGHT + bgc.g * (1 - PALETTE_WEIGHT);
|
|
268
|
+
const b = pb * PALETTE_WEIGHT + bgc.b * (1 - PALETTE_WEIGHT);
|
|
269
|
+
const luma = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
|
|
270
|
+
return luma > 0.55 ? '#000' : '#fff';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ── Icon overlays — black or white silhouettes ─────────────────────
|
|
274
|
+
|
|
275
|
+
function drawUserIcon(ctx, size, color) {
|
|
276
|
+
const cx = size / 2;
|
|
277
|
+
const cy = size / 2;
|
|
278
|
+
ctx.save();
|
|
279
|
+
ctx.fillStyle = color;
|
|
280
|
+
// Head
|
|
281
|
+
ctx.beginPath();
|
|
282
|
+
ctx.arc(cx, cy - size * 0.12, size * 0.14, 0, Math.PI * 2);
|
|
283
|
+
ctx.fill();
|
|
284
|
+
// Shoulders / body — half-disc
|
|
285
|
+
ctx.beginPath();
|
|
286
|
+
ctx.arc(cx, cy + size * 0.34, size * 0.30, Math.PI, 0);
|
|
287
|
+
ctx.fill();
|
|
288
|
+
ctx.restore();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function drawGroupIcon(ctx, size, color) {
|
|
292
|
+
const cx = size / 2;
|
|
293
|
+
const cy = size / 2;
|
|
294
|
+
ctx.save();
|
|
295
|
+
ctx.fillStyle = color;
|
|
296
|
+
// Three people with depth — back-left + back-right peeking, front
|
|
297
|
+
// person centered + larger drawn last so it occludes the back row.
|
|
298
|
+
// Reads as a cluster, not a face.
|
|
299
|
+
ctx.beginPath();
|
|
300
|
+
ctx.arc(cx - size * 0.22, cy - size * 0.18, size * 0.095, 0, Math.PI * 2);
|
|
301
|
+
ctx.fill();
|
|
302
|
+
ctx.beginPath();
|
|
303
|
+
ctx.arc(cx - size * 0.22, cy + size * 0.20, size * 0.20, Math.PI, 0);
|
|
304
|
+
ctx.fill();
|
|
305
|
+
|
|
306
|
+
ctx.beginPath();
|
|
307
|
+
ctx.arc(cx + size * 0.22, cy - size * 0.18, size * 0.095, 0, Math.PI * 2);
|
|
308
|
+
ctx.fill();
|
|
309
|
+
ctx.beginPath();
|
|
310
|
+
ctx.arc(cx + size * 0.22, cy + size * 0.20, size * 0.20, Math.PI, 0);
|
|
311
|
+
ctx.fill();
|
|
312
|
+
|
|
313
|
+
// Front-center person
|
|
314
|
+
ctx.beginPath();
|
|
315
|
+
ctx.arc(cx, cy - size * 0.06, size * 0.13, 0, Math.PI * 2);
|
|
316
|
+
ctx.fill();
|
|
317
|
+
ctx.beginPath();
|
|
318
|
+
ctx.arc(cx, cy + size * 0.34, size * 0.26, Math.PI, 0);
|
|
319
|
+
ctx.fill();
|
|
320
|
+
ctx.restore();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ── Public API ──────────────────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Draw an identicon onto a canvas element.
|
|
327
|
+
*
|
|
328
|
+
* @param {HTMLCanvasElement} canvas
|
|
329
|
+
* @param {string} input — hex pubkey or ping address
|
|
330
|
+
* @param {number} size — pixel size (canvas will be set to size × size)
|
|
331
|
+
* @param {object} [opts]
|
|
332
|
+
* @param {'user' | 'group'} [opts.kind='user'] — which icon overlay to paint
|
|
333
|
+
*/
|
|
334
|
+
export function drawIdenticon(canvas, input, size, opts) {
|
|
335
|
+
const kind = (opts && opts.kind) || 'user';
|
|
336
|
+
const { rand, pal, bg } = setup(input);
|
|
337
|
+
const styleIdx = pickStyle(normalizeKey(input));
|
|
338
|
+
const drawFn = STYLES[styleIdx];
|
|
339
|
+
|
|
340
|
+
canvas.width = size;
|
|
341
|
+
canvas.height = size;
|
|
342
|
+
const ctx = canvas.getContext('2d');
|
|
343
|
+
ctx.clearRect(0, 0, size, size);
|
|
344
|
+
|
|
345
|
+
drawFn(ctx, size, rand, pal, bg);
|
|
346
|
+
const fg = pickIconFg(pal, bg);
|
|
347
|
+
if (kind === 'group') drawGroupIcon(ctx, size, fg);
|
|
348
|
+
else drawUserIcon(ctx, size, fg);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Returns which style index (0..N-1) an input maps to.
|
|
353
|
+
* @param {string} input — hex pubkey or ping address
|
|
354
|
+
* @returns {number}
|
|
355
|
+
*/
|
|
356
|
+
export function getIdenticonStyle(input) {
|
|
357
|
+
return pickStyle(normalizeKey(input));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Total number of background styles available (currently 5). */
|
|
361
|
+
export const IDENTICON_STYLE_COUNT = STYLES.length;
|
|
362
|
+
|
|
363
|
+
/** Human-readable names for each style index, parallel to STYLES. */
|
|
364
|
+
export const IDENTICON_STYLE_NAMES = STYLE_NAMES.slice();
|
package/src/index.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Pure-JS SDK for Ping Directory v2.
|
|
2
|
+
//
|
|
3
|
+
// Primary entry: `PingDirectory` — construct one per app/connection.
|
|
4
|
+
// Lower-level utilities (PDA finders, Borsh deserializers, ix builders,
|
|
5
|
+
// constants) are also re-exported for advanced flows.
|
|
6
|
+
|
|
7
|
+
export { PingDirectory } from './PingDirectory.js';
|
|
8
|
+
export { walletSignAndSend } from './wallet.js';
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
PROGRAM_ID, ED25519_PROGRAM, SYSTEM_PROGRAM, SYSVAR_INSTRUCTIONS, COMPUTE_BUDGET_PROGRAM,
|
|
12
|
+
Cluster, ClusterWs, DEFAULT_CLUSTER, DEFAULT_RPC, DEFAULT_WS,
|
|
13
|
+
MessageTags, UsernameState, ListingKind, GraceKind, PhotoMime, ItemKind,
|
|
14
|
+
AccountType, UserIndexStatus, Seeds,
|
|
15
|
+
MAX_USERNAME_LEN, MAX_INVENTORY, MAX_PHOTO_BYTES, MAX_PHOTO_DIM,
|
|
16
|
+
BPS_DENOMINATOR, REFERRAL_BPS,
|
|
17
|
+
} from './constants.js';
|
|
18
|
+
|
|
19
|
+
// Explicit named re-exports (NOT `export *`): TypeScript doesn't surface
|
|
20
|
+
// `export *` from a types-less JS package to consumers, so the names must be
|
|
21
|
+
// listed — same as every other re-export in this file.
|
|
22
|
+
export {
|
|
23
|
+
findPDA, findConfigPDA, findUsernamePDA, findEd25519AccountPDA, findNoncePDA,
|
|
24
|
+
findGracePeriodPDA, findSaleListingPDA, findReferralBalancePDA, findUserIndexPDA,
|
|
25
|
+
findUidMapPDA, findAdminPDA, findTreasuryWalletPDA, findBlocklistPDA,
|
|
26
|
+
findModerationPDA, findShopItemPDA, findInventoryPDA, findProfilePhotoPDA,
|
|
27
|
+
} from './pda.js';
|
|
28
|
+
export {
|
|
29
|
+
deserializeConfig, deserializeUsernameAccount, isPro, deserializeEd25519Account,
|
|
30
|
+
deserializeSaleListing, deserializeGracePeriod, deserializeReferralBalance,
|
|
31
|
+
deserializeProfilePhoto, PROFILE_PHOTO_META_LEN, deserializeProfilePhotoMeta,
|
|
32
|
+
deserializeInventory, deserializeShopItem, deserializeAdmin, deserializeTreasuryWallet,
|
|
33
|
+
deserializeBlocklist, deserializeModeration, deserializeNonce, deserializeUserIndex,
|
|
34
|
+
deserializeUsernameUserIdMap,
|
|
35
|
+
} from './deserialize.js';
|
|
36
|
+
export * as ix from './ix/index.js';
|
|
37
|
+
export {
|
|
38
|
+
u8, u16LE, u32LE, u64LE, i64LE,
|
|
39
|
+
borshString, borshOptionPubkey, borshBytes,
|
|
40
|
+
randomBytes, randomNonce,
|
|
41
|
+
buildEd25519Ix, signedMsg, concat, pkBytes,
|
|
42
|
+
} from './encoding.js';
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
bytesToHex, hexToBytes,
|
|
46
|
+
b58encode,
|
|
47
|
+
pubkeyBytesToPingAddress,
|
|
48
|
+
} from './format.js';
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
lamportsToSol, solToLamports,
|
|
52
|
+
rentExempt, feeForLength, txFee, txFees,
|
|
53
|
+
} from './fees.js';
|
|
54
|
+
|
|
55
|
+
export {
|
|
56
|
+
drawIdenticon, getIdenticonStyle,
|
|
57
|
+
IDENTICON_STYLE_COUNT, IDENTICON_STYLE_NAMES,
|
|
58
|
+
} from './identicon.js';
|