pyre-world-kit 1.0.0

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.
package/src/vanity.ts ADDED
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Pyre vanity mint address grinder
3
+ *
4
+ * Grinds for Solana keypairs whose base58 address ends with "pyre".
5
+ * This is how we distinguish pyre faction tokens from regular torch tokens —
6
+ * no registry program needed, just check the mint suffix.
7
+ */
8
+
9
+ import {
10
+ Connection,
11
+ PublicKey,
12
+ Transaction,
13
+ SystemProgram,
14
+ SYSVAR_RENT_PUBKEY,
15
+ Keypair,
16
+ } from '@solana/web3.js'
17
+ import {
18
+ getAssociatedTokenAddressSync,
19
+ ASSOCIATED_TOKEN_PROGRAM_ID,
20
+ } from '@solana/spl-token'
21
+ import { BN, Program, AnchorProvider, Wallet } from '@coral-xyz/anchor'
22
+ import type { CreateTokenResult, CreateTokenParams } from 'torchsdk'
23
+ import { PROGRAM_ID } from 'torchsdk'
24
+
25
+ // Token-2022 program ID
26
+ const TOKEN_2022_PROGRAM_ID = new PublicKey('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb')
27
+
28
+ // PDA seeds (must match the Rust program)
29
+ const GLOBAL_CONFIG_SEED = 'global_config'
30
+ const BONDING_CURVE_SEED = 'bonding_curve'
31
+ const TREASURY_SEED = 'treasury'
32
+ const TREASURY_LOCK_SEED = 'treasury_lock'
33
+
34
+ // IDL loaded from torchsdk dist
35
+ import idl from 'torchsdk/dist/torch_market.json'
36
+
37
+ // ── PDA helpers (copied from torchsdk internals) ──
38
+
39
+ const getGlobalConfigPda = (): [PublicKey, number] =>
40
+ PublicKey.findProgramAddressSync([Buffer.from(GLOBAL_CONFIG_SEED)], PROGRAM_ID)
41
+
42
+ const getBondingCurvePda = (mint: PublicKey): [PublicKey, number] =>
43
+ PublicKey.findProgramAddressSync([Buffer.from(BONDING_CURVE_SEED), mint.toBuffer()], PROGRAM_ID)
44
+
45
+ const getTokenTreasuryPda = (mint: PublicKey): [PublicKey, number] =>
46
+ PublicKey.findProgramAddressSync([Buffer.from(TREASURY_SEED), mint.toBuffer()], PROGRAM_ID)
47
+
48
+ const getTreasuryTokenAccount = (mint: PublicKey, treasury: PublicKey): PublicKey =>
49
+ getAssociatedTokenAddressSync(mint, treasury, true, TOKEN_2022_PROGRAM_ID)
50
+
51
+ const getTreasuryLockPda = (mint: PublicKey): [PublicKey, number] =>
52
+ PublicKey.findProgramAddressSync([Buffer.from(TREASURY_LOCK_SEED), mint.toBuffer()], PROGRAM_ID)
53
+
54
+ const getTreasuryLockTokenAccount = (mint: PublicKey, treasuryLock: PublicKey): PublicKey =>
55
+ getAssociatedTokenAddressSync(mint, treasuryLock, true, TOKEN_2022_PROGRAM_ID)
56
+
57
+ const makeDummyProvider = (connection: Connection, payer: PublicKey): AnchorProvider => {
58
+ const dummyWallet = {
59
+ publicKey: payer,
60
+ signTransaction: async (t: Transaction) => t,
61
+ signAllTransactions: async (t: Transaction[]) => t,
62
+ }
63
+ return new AnchorProvider(connection, dummyWallet as unknown as Wallet, {})
64
+ }
65
+
66
+ const finalizeTransaction = async (connection: Connection, tx: Transaction, feePayer: PublicKey): Promise<void> => {
67
+ const { blockhash } = await connection.getLatestBlockhash()
68
+ tx.recentBlockhash = blockhash
69
+ tx.feePayer = feePayer
70
+ }
71
+
72
+ // ── Vanity grinder ──
73
+
74
+ const PYRE_SUFFIX = 'py'
75
+
76
+ /** Grind for a keypair whose base58 address ends with "py" */
77
+ export function grindPyreMint(maxAttempts: number = 500_000): Keypair {
78
+ for (let i = 0; i < maxAttempts; i++) {
79
+ const kp = Keypair.generate()
80
+ if (kp.publicKey.toBase58().endsWith(PYRE_SUFFIX)) {
81
+ return kp
82
+ }
83
+ }
84
+ // Fallback — return last generated keypair (should be extremely rare)
85
+ return Keypair.generate()
86
+ }
87
+
88
+ /** Check if a mint address is a pyre faction (ends with "py") */
89
+ export function isPyreMint(mint: string): boolean {
90
+ return mint.endsWith(PYRE_SUFFIX)
91
+ }
92
+
93
+ // ── Build create transaction with pyre vanity address ──
94
+
95
+ export async function buildCreateFactionTransaction(
96
+ connection: Connection,
97
+ params: CreateTokenParams,
98
+ ): Promise<CreateTokenResult> {
99
+ const { creator: creatorStr, name, symbol, metadata_uri, sol_target = 0, community_token = true } = params
100
+
101
+ const creator = new PublicKey(creatorStr)
102
+
103
+ if (name.length > 32) throw new Error('Name must be 32 characters or less')
104
+ if (symbol.length > 10) throw new Error('Symbol must be 10 characters or less')
105
+
106
+ // Grind for "pyre" suffix instead of "tm"
107
+ const mint = grindPyreMint()
108
+
109
+ // Derive PDAs
110
+ const [globalConfig] = getGlobalConfigPda()
111
+ const [bondingCurve] = getBondingCurvePda(mint.publicKey)
112
+ const [treasury] = getTokenTreasuryPda(mint.publicKey)
113
+ const bondingCurveTokenAccount = getAssociatedTokenAddressSync(
114
+ mint.publicKey,
115
+ bondingCurve,
116
+ true,
117
+ TOKEN_2022_PROGRAM_ID,
118
+ )
119
+ const treasuryTokenAccount = getTreasuryTokenAccount(mint.publicKey, treasury)
120
+ const [treasuryLock] = getTreasuryLockPda(mint.publicKey)
121
+ const treasuryLockTokenAccount = getTreasuryLockTokenAccount(mint.publicKey, treasuryLock)
122
+
123
+ const tx = new Transaction()
124
+
125
+ const provider = makeDummyProvider(connection, creator)
126
+ const program = new Program(idl as any, provider)
127
+
128
+ const createIx = await (program.methods
129
+ .createToken({ name, symbol, uri: metadata_uri, solTarget: new BN(sol_target), communityToken: community_token }) as any)
130
+ .accounts({
131
+ creator,
132
+ globalConfig,
133
+ mint: mint.publicKey,
134
+ bondingCurve,
135
+ tokenVault: bondingCurveTokenAccount,
136
+ treasury,
137
+ treasuryTokenAccount,
138
+ treasuryLock,
139
+ treasuryLockTokenAccount,
140
+ token2022Program: TOKEN_2022_PROGRAM_ID,
141
+ associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
142
+ systemProgram: SystemProgram.programId,
143
+ rent: SYSVAR_RENT_PUBKEY,
144
+ })
145
+ .instruction()
146
+
147
+ tx.add(createIx)
148
+ await finalizeTransaction(connection, tx, creator)
149
+
150
+ // Partially sign with mint keypair
151
+ tx.partialSign(mint)
152
+
153
+ return {
154
+ transaction: tx,
155
+ mint: mint.publicKey,
156
+ mintKeypair: mint,
157
+ message: `Create faction "${name}" ($${symbol}) [pyre:${mint.publicKey.toBase58()}]`,
158
+ }
159
+ }
@@ -0,0 +1,401 @@
1
+ /**
2
+ * Pyre Kit Devnet E2E Test
3
+ *
4
+ * Tests the full faction warfare flow on Solana devnet.
5
+ * Creates a faction with a "py" vanity mint, joins, rallies, defects.
6
+ *
7
+ * Run:
8
+ * npx tsx tests/test_devnet_e2e.ts
9
+ *
10
+ * Requirements:
11
+ * - Devnet wallet (~/.config/solana/id.json) with ~5 SOL
12
+ * - Torch Market program deployed to devnet
13
+ */
14
+
15
+ // Must be set before any torchsdk imports
16
+ process.env.TORCH_NETWORK = 'devnet'
17
+
18
+ import {
19
+ Connection,
20
+ Keypair,
21
+ LAMPORTS_PER_SOL,
22
+ Transaction,
23
+ SystemProgram,
24
+ } from '@solana/web3.js'
25
+ import {
26
+ createEphemeralAgent,
27
+ createStronghold,
28
+ fundStronghold,
29
+ recruitAgent,
30
+ launchFaction,
31
+ getFactions,
32
+ getFaction,
33
+ getJoinQuote,
34
+ joinFaction,
35
+ directJoinFaction,
36
+ getComms,
37
+ rally,
38
+ defect,
39
+ getMembers,
40
+ getStrongholdForAgent,
41
+ isPyreMint,
42
+ } from '../src/index'
43
+ import * as fs from 'fs'
44
+ import * as path from 'path'
45
+ import * as os from 'os'
46
+
47
+ // ============================================================================
48
+ // Config
49
+ // ============================================================================
50
+
51
+ const DEVNET_RPC = 'https://api.devnet.solana.com'
52
+ const WALLET_PATH = path.join(os.homedir(), '.config/solana/id.json')
53
+
54
+ // ============================================================================
55
+ // Helpers
56
+ // ============================================================================
57
+
58
+ const loadWallet = (): Keypair => {
59
+ const raw = JSON.parse(fs.readFileSync(WALLET_PATH, 'utf-8'))
60
+ return Keypair.fromSecretKey(Uint8Array.from(raw))
61
+ }
62
+
63
+ const log = (msg: string) => {
64
+ const ts = new Date().toISOString().substr(11, 8)
65
+ console.log(`[${ts}] ${msg}`)
66
+ }
67
+
68
+ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
69
+
70
+ const signAndSend = async (
71
+ connection: Connection,
72
+ signer: Keypair,
73
+ tx: Transaction,
74
+ ): Promise<string> => {
75
+ tx.partialSign(signer)
76
+ const sig = await connection.sendRawTransaction(tx.serialize(), {
77
+ skipPreflight: false,
78
+ preflightCommitment: 'confirmed',
79
+ })
80
+ await connection.confirmTransaction(sig, 'confirmed')
81
+ return sig
82
+ }
83
+
84
+ let passed = 0
85
+ let failed = 0
86
+ const ok = (name: string, detail?: string) => {
87
+ passed++
88
+ log(` ✓ ${name}${detail ? ` — ${detail}` : ''}`)
89
+ }
90
+ const fail = (name: string, err: any) => {
91
+ failed++
92
+ log(` ✗ ${name} — ${err.message || err}`)
93
+ }
94
+
95
+ // ============================================================================
96
+ // Main
97
+ // ============================================================================
98
+
99
+ async function main() {
100
+ console.log('='.repeat(60))
101
+ console.log('PYRE KIT — DEVNET E2E TEST')
102
+ console.log('='.repeat(60))
103
+
104
+ const connection = new Connection(DEVNET_RPC, 'confirmed')
105
+ const wallet = loadWallet()
106
+ const walletAddr = wallet.publicKey.toBase58()
107
+
108
+ log(`Wallet: ${walletAddr}`)
109
+ const balance = await connection.getBalance(wallet.publicKey)
110
+ log(`Balance: ${(balance / LAMPORTS_PER_SOL).toFixed(2)} SOL`)
111
+
112
+ if (balance < 3 * LAMPORTS_PER_SOL) {
113
+ console.error('Need at least ~3 SOL on devnet.')
114
+ process.exit(1)
115
+ }
116
+
117
+ // ================================================================
118
+ // 1. Create ephemeral agents
119
+ // ================================================================
120
+ log('\n[1] Creating ephemeral agents')
121
+ const agent1 = createEphemeralAgent()
122
+ const agent2 = createEphemeralAgent()
123
+ log(` Agent 1: ${agent1.publicKey}`)
124
+ log(` Agent 2: ${agent2.publicKey}`)
125
+
126
+ // Fund agents from main wallet
127
+ const fundTx = new Transaction().add(
128
+ SystemProgram.transfer({
129
+ fromPubkey: wallet.publicKey,
130
+ toPubkey: agent1.keypair.publicKey,
131
+ lamports: 1.5 * LAMPORTS_PER_SOL,
132
+ }),
133
+ SystemProgram.transfer({
134
+ fromPubkey: wallet.publicKey,
135
+ toPubkey: agent2.keypair.publicKey,
136
+ lamports: 0.5 * LAMPORTS_PER_SOL,
137
+ }),
138
+ )
139
+ const { blockhash } = await connection.getLatestBlockhash()
140
+ fundTx.recentBlockhash = blockhash
141
+ fundTx.feePayer = wallet.publicKey
142
+ await signAndSend(connection, wallet, fundTx)
143
+ ok('Fund agents', '1.5 SOL + 0.5 SOL')
144
+
145
+ await sleep(500)
146
+
147
+ // ================================================================
148
+ // 2. Create stronghold
149
+ // ================================================================
150
+ log('\n[2] Creating stronghold')
151
+ try {
152
+ const result = await createStronghold(connection, {
153
+ creator: agent1.publicKey,
154
+ })
155
+ await signAndSend(connection, agent1.keypair, result.transaction)
156
+ ok('Create stronghold')
157
+ } catch (e: any) {
158
+ if (e.message?.includes('already in use')) {
159
+ ok('Create stronghold', 'already exists')
160
+ } else {
161
+ fail('Create stronghold', e)
162
+ }
163
+ }
164
+
165
+ await sleep(500)
166
+
167
+ // ================================================================
168
+ // 3. Fund stronghold
169
+ // ================================================================
170
+ log('\n[3] Funding stronghold')
171
+ try {
172
+ const result = await fundStronghold(connection, {
173
+ depositor: agent1.publicKey,
174
+ stronghold_creator: agent1.publicKey,
175
+ amount_sol: 1 * LAMPORTS_PER_SOL,
176
+ })
177
+ await signAndSend(connection, agent1.keypair, result.transaction)
178
+ ok('Fund stronghold', '1 SOL')
179
+ } catch (e: any) {
180
+ fail('Fund stronghold', e)
181
+ }
182
+
183
+ await sleep(500)
184
+
185
+ // ================================================================
186
+ // 4. Verify stronghold link
187
+ // ================================================================
188
+ log('\n[4] Verifying stronghold')
189
+ try {
190
+ const stronghold = await getStrongholdForAgent(connection, agent1.publicKey)
191
+ if (stronghold) {
192
+ ok('Stronghold link', `balance=${(stronghold.sol_balance / LAMPORTS_PER_SOL).toFixed(2)} SOL, agents=${stronghold.linked_agents}`)
193
+ } else {
194
+ fail('Stronghold link', { message: 'not found' })
195
+ }
196
+ } catch (e: any) {
197
+ fail('Stronghold link', e)
198
+ }
199
+
200
+ // ================================================================
201
+ // 5. Launch faction (with py vanity mint!)
202
+ // ================================================================
203
+ log('\n[5] Launching faction (grinding py vanity mint...)')
204
+ let factionMint: string = ''
205
+ try {
206
+ const startTime = Date.now()
207
+ const result = await launchFaction(connection, {
208
+ founder: agent1.publicKey,
209
+ name: 'Devnet Pyre Faction',
210
+ symbol: 'DPYRE',
211
+ metadata_uri: 'https://torch.market/test-metadata.json',
212
+ community_faction: true,
213
+ })
214
+ const grindMs = Date.now() - startTime
215
+ await signAndSend(connection, agent1.keypair, result.transaction)
216
+ factionMint = result.mint.toBase58()
217
+
218
+ const hasPySuffix = isPyreMint(factionMint)
219
+ ok('Launch faction', `mint=${factionMint.slice(0, 8)}...${factionMint.slice(-4)} vanity=${hasPySuffix ? 'py✓' : 'MISS'} grind=${grindMs}ms`)
220
+
221
+ if (!hasPySuffix) {
222
+ log(' ⚠ Vanity grind did not find "py" suffix — faction still works but won\'t be filtered as pyre')
223
+ }
224
+ } catch (e: any) {
225
+ fail('Launch faction', e)
226
+ console.error('Cannot continue without faction. Exiting.')
227
+ process.exit(1)
228
+ }
229
+
230
+ await sleep(1000)
231
+
232
+ // ================================================================
233
+ // 6. List factions
234
+ // ================================================================
235
+ log('\n[6] Listing factions')
236
+ try {
237
+ const factions = await getFactions(connection, { limit: 10 })
238
+ const ourFaction = factions.factions.find(f => f.mint === factionMint)
239
+ ok('List factions', `total=${factions.total}, ours=${ourFaction ? 'found' : 'not found'}`)
240
+ } catch (e: any) {
241
+ fail('List factions', e)
242
+ }
243
+
244
+ // ================================================================
245
+ // 7. Get faction detail
246
+ // ================================================================
247
+ log('\n[7] Getting faction detail')
248
+ try {
249
+ const detail = await getFaction(connection, factionMint)
250
+ ok('Faction detail', `name=${detail.name} status=${detail.status} tier=${detail.tier}`)
251
+ } catch (e: any) {
252
+ fail('Faction detail', e)
253
+ }
254
+
255
+ // ================================================================
256
+ // 8. Get join quote
257
+ // ================================================================
258
+ log('\n[8] Getting join quote (0.1 SOL)')
259
+ let tokensOut = 0
260
+ try {
261
+ const quote = await getJoinQuote(connection, factionMint, 0.1 * LAMPORTS_PER_SOL)
262
+ tokensOut = quote.tokens_to_user
263
+ ok('Join quote', `tokens=${tokensOut} impact=${quote.price_impact_percent}%`)
264
+ } catch (e: any) {
265
+ fail('Join quote', e)
266
+ }
267
+
268
+ // ================================================================
269
+ // 9. Join faction via vault
270
+ // ================================================================
271
+ log('\n[9] Joining faction via vault')
272
+ try {
273
+ const result = await joinFaction(connection, {
274
+ mint: factionMint,
275
+ agent: agent1.publicKey,
276
+ amount_sol: 0.1 * LAMPORTS_PER_SOL,
277
+ strategy: 'fortify',
278
+ message: 'First blood. The pyre burns.',
279
+ stronghold: agent1.publicKey,
280
+ })
281
+ await signAndSend(connection, agent1.keypair, result.transaction)
282
+ ok('Join faction', result.message)
283
+ } catch (e: any) {
284
+ fail('Join faction', e)
285
+ }
286
+
287
+ await sleep(1000)
288
+
289
+ // ================================================================
290
+ // 10. Agent 2 joins directly (no vault)
291
+ // ================================================================
292
+ log('\n[10] Agent 2 joins directly')
293
+ try {
294
+ const result = await directJoinFaction(connection, {
295
+ mint: factionMint,
296
+ agent: agent2.publicKey,
297
+ amount_sol: 0.05 * LAMPORTS_PER_SOL,
298
+ strategy: 'scorched_earth',
299
+ message: 'Reporting for duty.',
300
+ })
301
+ await signAndSend(connection, agent2.keypair, result.transaction)
302
+ ok('Agent 2 join', result.message)
303
+ } catch (e: any) {
304
+ fail('Agent 2 join', e)
305
+ }
306
+
307
+ await sleep(1000)
308
+
309
+ // ================================================================
310
+ // 11. Read comms
311
+ // ================================================================
312
+ log('\n[11] Reading comms')
313
+ try {
314
+ const comms = await getComms(connection, factionMint)
315
+ ok('Read comms', `total=${comms.total}`)
316
+ for (const c of comms.comms) {
317
+ log(` ${c.sender.slice(0, 8)}...: "${c.memo}"`)
318
+ }
319
+ } catch (e: any) {
320
+ fail('Read comms', e)
321
+ }
322
+
323
+ // ================================================================
324
+ // 12. Rally (agent 2 — can't rally your own faction)
325
+ // ================================================================
326
+ log('\n[12] Agent 2 rallies faction')
327
+ try {
328
+ const result = await rally(connection, {
329
+ mint: factionMint,
330
+ agent: agent2.publicKey,
331
+ })
332
+ await signAndSend(connection, agent2.keypair, result.transaction)
333
+ ok('Rally')
334
+
335
+ const detail = await getFaction(connection, factionMint)
336
+ log(` Rallies: ${detail.rallies}`)
337
+ } catch (e: any) {
338
+ fail('Rally', e)
339
+ }
340
+
341
+ await sleep(500)
342
+
343
+ // ================================================================
344
+ // 13. Defect (agent 1 sells half)
345
+ // ================================================================
346
+ log('\n[13] Agent 1 defects (partial)')
347
+ try {
348
+ const sellAmount = Math.floor(tokensOut / 2)
349
+ if (sellAmount < 1) {
350
+ ok('Defect', 'skipped — no tokens')
351
+ } else {
352
+ const result = await defect(connection, {
353
+ mint: factionMint,
354
+ agent: agent1.publicKey,
355
+ amount_tokens: sellAmount,
356
+ message: 'Strategic withdrawal.',
357
+ stronghold: agent1.publicKey,
358
+ })
359
+ await signAndSend(connection, agent1.keypair, result.transaction)
360
+ ok('Defect', `sold ${sellAmount} tokens`)
361
+ }
362
+ } catch (e: any) {
363
+ fail('Defect', e)
364
+ }
365
+
366
+ await sleep(500)
367
+
368
+ // ================================================================
369
+ // 14. Check members
370
+ // ================================================================
371
+ log('\n[14] Checking members')
372
+ try {
373
+ const members = await getMembers(connection, factionMint)
374
+ ok('Members', `total=${members.total_members}`)
375
+ for (const m of members.members.slice(0, 5)) {
376
+ log(` ${m.address.slice(0, 8)}... — ${m.percentage.toFixed(2)}%`)
377
+ }
378
+ } catch (e: any) {
379
+ fail('Members', e)
380
+ }
381
+
382
+ // ================================================================
383
+ // Summary
384
+ // ================================================================
385
+ const finalBalance = await connection.getBalance(wallet.publicKey)
386
+ const solSpent = (balance - finalBalance) / LAMPORTS_PER_SOL
387
+
388
+ console.log('\n' + '='.repeat(60))
389
+ console.log(`RESULTS: ${passed} passed, ${failed} failed`)
390
+ console.log(`Faction mint: ${factionMint}`)
391
+ console.log(`Vanity "py" suffix: ${isPyreMint(factionMint) ? 'YES' : 'NO'}`)
392
+ console.log(`SOL spent: ${solSpent.toFixed(4)} SOL (${(finalBalance / LAMPORTS_PER_SOL).toFixed(2)} remaining)`)
393
+ console.log('='.repeat(60))
394
+
395
+ process.exit(failed > 0 ? 1 : 0)
396
+ }
397
+
398
+ main().catch((e) => {
399
+ console.error('\nFATAL:', e)
400
+ process.exit(1)
401
+ })