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/dist/actions.d.ts +81 -0
- package/dist/actions.js +318 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +74 -0
- package/dist/intel.d.ts +61 -0
- package/dist/intel.js +342 -0
- package/dist/mappers.d.ts +31 -0
- package/dist/mappers.js +258 -0
- package/dist/types.d.ts +353 -0
- package/dist/types.js +9 -0
- package/dist/vanity.d.ts +14 -0
- package/dist/vanity.js +115 -0
- package/package.json +25 -0
- package/readme.md +203 -0
- package/src/actions.ts +523 -0
- package/src/index.ts +141 -0
- package/src/intel.ts +424 -0
- package/src/mappers.ts +302 -0
- package/src/types.ts +425 -0
- package/src/vanity.ts +159 -0
- package/tests/test_devnet_e2e.ts +401 -0
- package/tests/test_e2e.ts +213 -0
- package/tests/test_sim.ts +458 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pyre Kit E2E Test
|
|
3
|
+
*
|
|
4
|
+
* Tests the full faction warfare flow against a surfpool fork.
|
|
5
|
+
*
|
|
6
|
+
* Prerequisites:
|
|
7
|
+
* surfpool start --network mainnet --no-tui
|
|
8
|
+
*
|
|
9
|
+
* Run:
|
|
10
|
+
* pnpm test (or: npx tsx tests/test_e2e.ts)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Connection, LAMPORTS_PER_SOL } from '@solana/web3.js';
|
|
14
|
+
import {
|
|
15
|
+
createEphemeralAgent,
|
|
16
|
+
createStronghold,
|
|
17
|
+
fundStronghold,
|
|
18
|
+
recruitAgent,
|
|
19
|
+
launchFaction,
|
|
20
|
+
getFactions,
|
|
21
|
+
getFaction,
|
|
22
|
+
getJoinQuote,
|
|
23
|
+
joinFaction,
|
|
24
|
+
getComms,
|
|
25
|
+
rally,
|
|
26
|
+
defect,
|
|
27
|
+
getMembers,
|
|
28
|
+
getStrongholdForAgent,
|
|
29
|
+
} from '../src/index.js';
|
|
30
|
+
|
|
31
|
+
const RPC_URL = process.env.RPC_URL ?? 'http://localhost:8899';
|
|
32
|
+
|
|
33
|
+
async function sendAndConfirm(connection: Connection, agent: ReturnType<typeof createEphemeralAgent>, result: any) {
|
|
34
|
+
const tx = result.transaction;
|
|
35
|
+
const signed = agent.sign(tx);
|
|
36
|
+
const sig = await connection.sendRawTransaction(signed.serialize());
|
|
37
|
+
await connection.confirmTransaction(sig, 'confirmed');
|
|
38
|
+
console.log(` tx: ${sig}`);
|
|
39
|
+
|
|
40
|
+
// Handle additional transactions
|
|
41
|
+
if (result.additionalTransactions) {
|
|
42
|
+
for (const addlTx of result.additionalTransactions) {
|
|
43
|
+
const addlSigned = agent.sign(addlTx);
|
|
44
|
+
const addlSig = await connection.sendRawTransaction(addlSigned.serialize());
|
|
45
|
+
await connection.confirmTransaction(addlSig, 'confirmed');
|
|
46
|
+
console.log(` additional tx: ${addlSig}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return sig;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function main() {
|
|
54
|
+
const connection = new Connection(RPC_URL, 'confirmed');
|
|
55
|
+
console.log(`Pyre Kit E2E Test — RPC: ${RPC_URL}\n`);
|
|
56
|
+
|
|
57
|
+
// 1. Create ephemeral agents
|
|
58
|
+
console.log('1. Creating ephemeral agents...');
|
|
59
|
+
const agent = createEphemeralAgent();
|
|
60
|
+
const agent2 = createEphemeralAgent();
|
|
61
|
+
console.log(` Agent 1: ${agent.publicKey}`);
|
|
62
|
+
console.log(` Agent 2: ${agent2.publicKey}`);
|
|
63
|
+
|
|
64
|
+
// Airdrop SOL for testing
|
|
65
|
+
console.log(' Requesting airdrops...');
|
|
66
|
+
const [airdropSig, airdropSig2] = await Promise.all([
|
|
67
|
+
connection.requestAirdrop(agent.keypair.publicKey, 10 * LAMPORTS_PER_SOL),
|
|
68
|
+
connection.requestAirdrop(agent2.keypair.publicKey, 1 * LAMPORTS_PER_SOL),
|
|
69
|
+
]);
|
|
70
|
+
await Promise.all([
|
|
71
|
+
connection.confirmTransaction(airdropSig, 'confirmed'),
|
|
72
|
+
connection.confirmTransaction(airdropSig2, 'confirmed'),
|
|
73
|
+
]);
|
|
74
|
+
console.log(' Airdrops confirmed');
|
|
75
|
+
|
|
76
|
+
// 2. Create stronghold (vault)
|
|
77
|
+
console.log('\n2. Creating stronghold...');
|
|
78
|
+
const strongholdResult = await createStronghold(connection, {
|
|
79
|
+
creator: agent.publicKey,
|
|
80
|
+
});
|
|
81
|
+
await sendAndConfirm(connection, agent, strongholdResult);
|
|
82
|
+
console.log(' Stronghold created');
|
|
83
|
+
|
|
84
|
+
// 3. Fund stronghold
|
|
85
|
+
console.log('\n3. Funding stronghold...');
|
|
86
|
+
const fundResult = await fundStronghold(connection, {
|
|
87
|
+
depositor: agent.publicKey,
|
|
88
|
+
stronghold_creator: agent.publicKey,
|
|
89
|
+
amount_sol: 5 * LAMPORTS_PER_SOL,
|
|
90
|
+
});
|
|
91
|
+
await sendAndConfirm(connection, agent, fundResult);
|
|
92
|
+
console.log(' Funded 5 SOL');
|
|
93
|
+
|
|
94
|
+
// 4. Recruit agent (link wallet)
|
|
95
|
+
console.log('\n4. Recruiting agent (self-link)...');
|
|
96
|
+
// Agent is already linked as creator, but let's verify
|
|
97
|
+
const agentLink = await getStrongholdForAgent(connection, agent.publicKey);
|
|
98
|
+
console.log(` Stronghold found: ${agentLink ? 'yes' : 'no'}`);
|
|
99
|
+
if (agentLink) {
|
|
100
|
+
console.log(` SOL balance: ${agentLink.sol_balance / LAMPORTS_PER_SOL} SOL`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 5. Launch faction
|
|
104
|
+
console.log('\n5. Launching faction...');
|
|
105
|
+
const launchResult = await launchFaction(connection, {
|
|
106
|
+
founder: agent.publicKey,
|
|
107
|
+
name: 'Pyre Test Faction',
|
|
108
|
+
symbol: 'PYRE',
|
|
109
|
+
metadata_uri: 'https://torch.market/test-metadata.json',
|
|
110
|
+
community_faction: true,
|
|
111
|
+
});
|
|
112
|
+
await sendAndConfirm(connection, agent, launchResult);
|
|
113
|
+
const factionMint = launchResult.mint.toBase58();
|
|
114
|
+
console.log(` Faction launched: ${factionMint}`);
|
|
115
|
+
|
|
116
|
+
// 6. List factions
|
|
117
|
+
console.log('\n6. Listing factions...');
|
|
118
|
+
const factions = await getFactions(connection, { limit: 10 });
|
|
119
|
+
console.log(` Total factions: ${factions.total}`);
|
|
120
|
+
const ourFaction = factions.factions.find(f => f.mint === factionMint);
|
|
121
|
+
console.log(` Our faction found: ${ourFaction ? 'yes' : 'no'}`);
|
|
122
|
+
if (ourFaction) {
|
|
123
|
+
console.log(` Status: ${ourFaction.status}, Members: ${ourFaction.members}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 7. Get faction detail
|
|
127
|
+
console.log('\n7. Getting faction detail...');
|
|
128
|
+
const detail = await getFaction(connection, factionMint);
|
|
129
|
+
console.log(` Name: ${detail.name}`);
|
|
130
|
+
console.log(` Status: ${detail.status}`);
|
|
131
|
+
console.log(` Tier: ${detail.tier}`);
|
|
132
|
+
console.log(` Founder: ${detail.founder}`);
|
|
133
|
+
console.log(` War chest SOL: ${detail.war_chest_sol}`);
|
|
134
|
+
|
|
135
|
+
// 8. Get join quote
|
|
136
|
+
console.log('\n8. Getting join quote (0.1 SOL)...');
|
|
137
|
+
const quote = await getJoinQuote(connection, factionMint, 0.1 * LAMPORTS_PER_SOL);
|
|
138
|
+
console.log(` Tokens out: ${quote.tokens_to_user}`);
|
|
139
|
+
console.log(` Price impact: ${quote.price_impact_percent}%`);
|
|
140
|
+
|
|
141
|
+
// 9. Join faction with message (comms)
|
|
142
|
+
console.log('\n9. Joining faction...');
|
|
143
|
+
const joinResult = await joinFaction(connection, {
|
|
144
|
+
mint: factionMint,
|
|
145
|
+
agent: agent.publicKey,
|
|
146
|
+
amount_sol: 0.1 * LAMPORTS_PER_SOL,
|
|
147
|
+
strategy: 'fortify',
|
|
148
|
+
message: 'For the faction! First to join.',
|
|
149
|
+
stronghold: agent.publicKey,
|
|
150
|
+
});
|
|
151
|
+
await sendAndConfirm(connection, agent, joinResult);
|
|
152
|
+
console.log(' Joined faction');
|
|
153
|
+
|
|
154
|
+
// 10. Read comms
|
|
155
|
+
console.log('\n10. Reading faction comms...');
|
|
156
|
+
const comms = await getComms(connection, factionMint);
|
|
157
|
+
console.log(` Total comms: ${comms.total}`);
|
|
158
|
+
for (const c of comms.comms) {
|
|
159
|
+
console.log(` [${new Date(c.timestamp * 1000).toISOString()}] ${c.sender.slice(0, 8)}...: ${c.memo}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 11. Rally support (different agent — can't rally your own faction)
|
|
163
|
+
console.log('\n11. Rallying faction (agent 2)...');
|
|
164
|
+
const rallyResult = await rally(connection, {
|
|
165
|
+
mint: factionMint,
|
|
166
|
+
agent: agent2.publicKey,
|
|
167
|
+
});
|
|
168
|
+
await sendAndConfirm(connection, agent2, rallyResult);
|
|
169
|
+
console.log(' Rally sent');
|
|
170
|
+
|
|
171
|
+
// Verify rally
|
|
172
|
+
const detailAfterRally = await getFaction(connection, factionMint);
|
|
173
|
+
console.log(` Rallies: ${detailAfterRally.rallies}`);
|
|
174
|
+
|
|
175
|
+
// 12. Defect with message
|
|
176
|
+
console.log('\n12. Defecting from faction...');
|
|
177
|
+
// Sell half of what we bought
|
|
178
|
+
const sellAmount = Math.floor(quote.tokens_to_user / 2);
|
|
179
|
+
const defectResult = await defect(connection, {
|
|
180
|
+
mint: factionMint,
|
|
181
|
+
agent: agent.publicKey,
|
|
182
|
+
amount_tokens: sellAmount,
|
|
183
|
+
message: 'Strategic withdrawal. Will return.',
|
|
184
|
+
stronghold: agent.publicKey,
|
|
185
|
+
});
|
|
186
|
+
await sendAndConfirm(connection, agent, defectResult);
|
|
187
|
+
console.log(` Defected ${sellAmount} tokens`);
|
|
188
|
+
|
|
189
|
+
// 13. Check members
|
|
190
|
+
console.log('\n13. Checking members...');
|
|
191
|
+
const members = await getMembers(connection, factionMint);
|
|
192
|
+
console.log(` Total members: ${members.total_members}`);
|
|
193
|
+
for (const m of members.members.slice(0, 5)) {
|
|
194
|
+
console.log(` ${m.address.slice(0, 8)}... — ${m.balance} (${m.percentage.toFixed(2)}%)`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 14. Verify stronghold for agent
|
|
198
|
+
console.log('\n14. Verifying stronghold link...');
|
|
199
|
+
const stronghold = await getStrongholdForAgent(connection, agent.publicKey);
|
|
200
|
+
if (stronghold) {
|
|
201
|
+
console.log(` Stronghold: ${stronghold.address}`);
|
|
202
|
+
console.log(` Authority: ${stronghold.authority}`);
|
|
203
|
+
console.log(` SOL balance: ${stronghold.sol_balance / LAMPORTS_PER_SOL} SOL`);
|
|
204
|
+
console.log(` Linked agents: ${stronghold.linked_agents}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
console.log('\n✓ All steps completed successfully');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
main().catch((err) => {
|
|
211
|
+
console.error('\n✗ Test failed:', err);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
});
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pyre Kit Faction Warfare Simulation
|
|
3
|
+
*
|
|
4
|
+
* Spins up 500 agent wallets and runs a random walk simulation
|
|
5
|
+
* of faction warfare: launching, joining, defecting, rallying.
|
|
6
|
+
*
|
|
7
|
+
* Prerequisites:
|
|
8
|
+
* surfpool start --network mainnet --no-tui
|
|
9
|
+
*
|
|
10
|
+
* Run:
|
|
11
|
+
* npx tsx tests/test_sim.ts
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Connection, LAMPORTS_PER_SOL } from '@solana/web3.js';
|
|
15
|
+
import {
|
|
16
|
+
createEphemeralAgent,
|
|
17
|
+
launchFaction,
|
|
18
|
+
directJoinFaction,
|
|
19
|
+
defect,
|
|
20
|
+
rally,
|
|
21
|
+
getFactions,
|
|
22
|
+
getFaction,
|
|
23
|
+
getMembers,
|
|
24
|
+
getComms,
|
|
25
|
+
getFactionLeaderboard,
|
|
26
|
+
getWorldStats,
|
|
27
|
+
detectAlliances,
|
|
28
|
+
} from '../src/index.js';
|
|
29
|
+
|
|
30
|
+
const RPC_URL = process.env.RPC_URL ?? 'http://localhost:8899';
|
|
31
|
+
|
|
32
|
+
const AGENT_COUNT = 500;
|
|
33
|
+
const FACTION_COUNT = 15;
|
|
34
|
+
const ROUNDS = 20;
|
|
35
|
+
const AGENTS_PER_ROUND = 80;
|
|
36
|
+
const JOIN_SOL = 0.05 * LAMPORTS_PER_SOL;
|
|
37
|
+
const AIRDROP_SOL = 2 * LAMPORTS_PER_SOL;
|
|
38
|
+
|
|
39
|
+
type Agent = ReturnType<typeof createEphemeralAgent>;
|
|
40
|
+
|
|
41
|
+
interface AgentState {
|
|
42
|
+
agent: Agent;
|
|
43
|
+
holdings: Map<string, number>; // mint -> token balance
|
|
44
|
+
founded: string | null; // mint they founded
|
|
45
|
+
voted: Set<string>; // mints already voted on
|
|
46
|
+
rallied: Set<string>; // mints already rallied
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface FactionState {
|
|
50
|
+
mint: string;
|
|
51
|
+
name: string;
|
|
52
|
+
founder: string; // pubkey of founding agent
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Helpers ──────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
async function sendAndConfirm(connection: Connection, agent: Agent, result: any): Promise<string> {
|
|
58
|
+
const tx = result.transaction;
|
|
59
|
+
const signed = agent.sign(tx);
|
|
60
|
+
const sig = await connection.sendRawTransaction(signed.serialize());
|
|
61
|
+
await connection.confirmTransaction(sig, 'confirmed');
|
|
62
|
+
|
|
63
|
+
if (result.additionalTransactions) {
|
|
64
|
+
for (const addlTx of result.additionalTransactions) {
|
|
65
|
+
const addlSigned = agent.sign(addlTx);
|
|
66
|
+
const addlSig = await connection.sendRawTransaction(addlSigned.serialize());
|
|
67
|
+
await connection.confirmTransaction(addlSig, 'confirmed');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return sig;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function pick<T>(arr: T[]): T {
|
|
75
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function pickN<T>(arr: T[], n: number): T[] {
|
|
79
|
+
const shuffled = [...arr].sort(() => Math.random() - 0.5);
|
|
80
|
+
return shuffled.slice(0, n);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const FACTION_NAMES = [
|
|
84
|
+
'Iron Vanguard', 'Obsidian Order', 'Crimson Dawn', 'Shadow Covenant',
|
|
85
|
+
'Ember Collective', 'Void Walkers', 'Solar Reign', 'Frost Legion',
|
|
86
|
+
'Thunder Pact', 'Ash Republic', 'Neon Syndicate', 'Storm Brigade',
|
|
87
|
+
'Lunar Assembly', 'Flame Sentinels', 'Dark Meridian', 'Phoenix Accord',
|
|
88
|
+
'Steel Dominion', 'Crystal Enclave', 'Rogue Alliance', 'Titan Front',
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const FACTION_SYMBOLS = [
|
|
92
|
+
'IRON', 'OBSD', 'CRIM', 'SHAD', 'EMBR', 'VOID', 'SOLR', 'FRST',
|
|
93
|
+
'THDR', 'ASHR', 'NEON', 'STRM', 'LUNR', 'FLMS', 'DARK', 'PHNX',
|
|
94
|
+
'STEL', 'CRYS', 'ROGU', 'TITN',
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
const JOIN_MESSAGES = [
|
|
98
|
+
'Pledging allegiance.', 'Reporting for duty.', 'This faction will rise.',
|
|
99
|
+
'Strategic position acquired.', 'In for the long haul.', 'Joining the cause.',
|
|
100
|
+
'Scouting this faction.', 'Alliance confirmed.', 'Deploying capital.',
|
|
101
|
+
'Interesting opportunity.', 'Following the signal.', 'Reconnaissance buy.',
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const DEFECT_MESSAGES = [
|
|
105
|
+
'Strategic withdrawal.', 'This pyre burns too dim.', 'Found a stronger faction.',
|
|
106
|
+
'Tactical repositioning.', 'The leadership is weak.', 'Cutting losses.',
|
|
107
|
+
'Better opportunities elsewhere.', 'Betrayal is just strategy.', 'Moving on.',
|
|
108
|
+
'The war chest is empty.', 'This faction peaked.', 'Exit protocol initiated.',
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
// ─── Simulation ───────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
async function main() {
|
|
114
|
+
const connection = new Connection(RPC_URL, 'confirmed');
|
|
115
|
+
console.log(`Pyre Faction Warfare Simulation`);
|
|
116
|
+
console.log(` RPC: ${RPC_URL}`);
|
|
117
|
+
console.log(` Agents: ${AGENT_COUNT}`);
|
|
118
|
+
console.log(` Factions: ${FACTION_COUNT}`);
|
|
119
|
+
console.log(` Rounds: ${ROUNDS}`);
|
|
120
|
+
console.log(` Agents/round: ${AGENTS_PER_ROUND}\n`);
|
|
121
|
+
|
|
122
|
+
// ── Phase 1: Spawn agents ──────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
console.log('Phase 1: Spawning agents...');
|
|
125
|
+
const agents: AgentState[] = [];
|
|
126
|
+
for (let i = 0; i < AGENT_COUNT; i++) {
|
|
127
|
+
agents.push({
|
|
128
|
+
agent: createEphemeralAgent(),
|
|
129
|
+
holdings: new Map(),
|
|
130
|
+
founded: null,
|
|
131
|
+
voted: new Set(),
|
|
132
|
+
rallied: new Set(),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
console.log(` ${AGENT_COUNT} agents created\n`);
|
|
136
|
+
|
|
137
|
+
// ── Phase 2: Airdrop SOL ───────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
console.log('Phase 2: Airdropping SOL...');
|
|
140
|
+
const AIRDROP_BATCH = 25;
|
|
141
|
+
for (let i = 0; i < agents.length; i += AIRDROP_BATCH) {
|
|
142
|
+
const batch = agents.slice(i, i + AIRDROP_BATCH);
|
|
143
|
+
await Promise.all(
|
|
144
|
+
batch.map(async (a) => {
|
|
145
|
+
const sig = await connection.requestAirdrop(a.agent.keypair.publicKey, AIRDROP_SOL);
|
|
146
|
+
await connection.confirmTransaction(sig, 'confirmed');
|
|
147
|
+
})
|
|
148
|
+
);
|
|
149
|
+
if ((i + AIRDROP_BATCH) % 100 === 0 || i + AIRDROP_BATCH >= agents.length) {
|
|
150
|
+
console.log(` ${Math.min(i + AIRDROP_BATCH, agents.length)}/${AGENT_COUNT} funded`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
console.log();
|
|
154
|
+
|
|
155
|
+
// ── Phase 3: Launch factions ───────────────────────────────────
|
|
156
|
+
|
|
157
|
+
console.log('Phase 3: Launching factions...');
|
|
158
|
+
const founders = pickN(agents, FACTION_COUNT);
|
|
159
|
+
const factions: FactionState[] = [];
|
|
160
|
+
|
|
161
|
+
for (let i = 0; i < founders.length; i++) {
|
|
162
|
+
const a = founders[i];
|
|
163
|
+
const name = FACTION_NAMES[i];
|
|
164
|
+
const symbol = FACTION_SYMBOLS[i];
|
|
165
|
+
try {
|
|
166
|
+
const result = await launchFaction(connection, {
|
|
167
|
+
founder: a.agent.publicKey,
|
|
168
|
+
name,
|
|
169
|
+
symbol,
|
|
170
|
+
metadata_uri: `https://pyre.gg/factions/${symbol.toLowerCase()}.json`,
|
|
171
|
+
community_faction: true,
|
|
172
|
+
});
|
|
173
|
+
await sendAndConfirm(connection, a.agent, result);
|
|
174
|
+
const mint = result.mint.toBase58();
|
|
175
|
+
factions.push({ mint, name, founder: a.agent.publicKey });
|
|
176
|
+
a.founded = mint;
|
|
177
|
+
console.log(` [${symbol}] ${name} — founded by ${a.agent.publicKey.slice(0, 8)}...`);
|
|
178
|
+
} catch (err: any) {
|
|
179
|
+
console.log(` FAIL launching ${name}: ${err.message}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
console.log(` ${factions.length} factions live\n`);
|
|
183
|
+
|
|
184
|
+
if (factions.length === 0) {
|
|
185
|
+
console.error('No factions launched. Aborting simulation.');
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Phase 4: Random walk simulation ────────────────────────────
|
|
190
|
+
|
|
191
|
+
console.log('Phase 4: Random walk simulation...\n');
|
|
192
|
+
|
|
193
|
+
let totalJoins = 0;
|
|
194
|
+
let totalDefections = 0;
|
|
195
|
+
let totalRallies = 0;
|
|
196
|
+
let totalErrors = 0;
|
|
197
|
+
|
|
198
|
+
for (let round = 1; round <= ROUNDS; round++) {
|
|
199
|
+
const roundAgents = pickN(agents, AGENTS_PER_ROUND);
|
|
200
|
+
let roundJoins = 0;
|
|
201
|
+
let roundDefections = 0;
|
|
202
|
+
let roundRallies = 0;
|
|
203
|
+
let roundErrors = 0;
|
|
204
|
+
|
|
205
|
+
console.log(`─── Round ${round}/${ROUNDS} ───`);
|
|
206
|
+
|
|
207
|
+
// Process agents in batches to avoid overwhelming the RPC
|
|
208
|
+
const ROUND_BATCH = 10;
|
|
209
|
+
for (let i = 0; i < roundAgents.length; i += ROUND_BATCH) {
|
|
210
|
+
const batch = roundAgents.slice(i, i + ROUND_BATCH);
|
|
211
|
+
await Promise.all(
|
|
212
|
+
batch.map(async (a) => {
|
|
213
|
+
await executeRandomAction(connection, a, factions);
|
|
214
|
+
})
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Tally round results by checking what actions succeeded
|
|
219
|
+
for (const a of roundAgents) {
|
|
220
|
+
// We track joins/defections in executeRandomAction via the stats object
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Get round stats from the counter
|
|
224
|
+
const stats = getRoundStats();
|
|
225
|
+
roundJoins = stats.joins;
|
|
226
|
+
roundDefections = stats.defections;
|
|
227
|
+
roundRallies = stats.rallies;
|
|
228
|
+
roundErrors += stats.errors;
|
|
229
|
+
|
|
230
|
+
totalJoins += roundJoins;
|
|
231
|
+
totalDefections += roundDefections;
|
|
232
|
+
totalRallies += roundRallies;
|
|
233
|
+
totalErrors += roundErrors;
|
|
234
|
+
|
|
235
|
+
console.log(` Joins: ${roundJoins} | Defections: ${roundDefections} | Rallies: ${roundRallies} | Errors: ${roundErrors}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
console.log(`\n─── Simulation Complete ───`);
|
|
239
|
+
console.log(` Total joins: ${totalJoins}`);
|
|
240
|
+
console.log(` Total defections: ${totalDefections}`);
|
|
241
|
+
console.log(` Total rallies: ${totalRallies}`);
|
|
242
|
+
console.log(` Total errors: ${totalErrors}\n`);
|
|
243
|
+
|
|
244
|
+
// ── Phase 5: Intel debrief ─────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
console.log('Phase 5: Intelligence debrief...\n');
|
|
247
|
+
|
|
248
|
+
// Leaderboard
|
|
249
|
+
console.log('Faction Leaderboard:');
|
|
250
|
+
try {
|
|
251
|
+
const leaderboard = await getFactionLeaderboard(connection, { limit: factions.length });
|
|
252
|
+
for (let i = 0; i < leaderboard.length; i++) {
|
|
253
|
+
const f = leaderboard[i];
|
|
254
|
+
console.log(` ${i + 1}. [${f.symbol}] ${f.name} — power: ${f.score.toFixed(2)}, mcap: ${f.market_cap_sol.toFixed(4)} SOL, members: ${f.members}`);
|
|
255
|
+
}
|
|
256
|
+
} catch (err: any) {
|
|
257
|
+
console.log(` Could not fetch leaderboard: ${err.message}`);
|
|
258
|
+
}
|
|
259
|
+
console.log();
|
|
260
|
+
|
|
261
|
+
// Alliance detection
|
|
262
|
+
if (factions.length >= 2) {
|
|
263
|
+
console.log('Alliance Detection:');
|
|
264
|
+
try {
|
|
265
|
+
const alliances = await detectAlliances(
|
|
266
|
+
connection,
|
|
267
|
+
factions.map(f => f.mint),
|
|
268
|
+
20,
|
|
269
|
+
);
|
|
270
|
+
if (alliances.length === 0) {
|
|
271
|
+
console.log(' No alliances detected');
|
|
272
|
+
}
|
|
273
|
+
for (const a of alliances.slice(0, 10)) {
|
|
274
|
+
const names = a.factions.map(m => factions.find(f => f.mint === m)?.name ?? m.slice(0, 8));
|
|
275
|
+
console.log(` ${names.join(' + ')} — ${a.shared_members} shared (${a.overlap_percent.toFixed(1)}% overlap)`);
|
|
276
|
+
}
|
|
277
|
+
} catch (err: any) {
|
|
278
|
+
console.log(` Could not detect alliances: ${err.message}`);
|
|
279
|
+
}
|
|
280
|
+
console.log();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Faction details for top 3
|
|
284
|
+
console.log('Top Faction Details:');
|
|
285
|
+
for (const f of factions.slice(0, 3)) {
|
|
286
|
+
try {
|
|
287
|
+
const detail = await getFaction(connection, f.mint);
|
|
288
|
+
const members = await getMembers(connection, f.mint, 5);
|
|
289
|
+
const comms = await getComms(connection, f.mint, 5);
|
|
290
|
+
|
|
291
|
+
console.log(` [${detail.symbol}] ${detail.name}`);
|
|
292
|
+
console.log(` Status: ${detail.status} | Tier: ${detail.tier}`);
|
|
293
|
+
console.log(` Price: ${detail.price_sol.toFixed(6)} SOL | MCap: ${detail.market_cap_sol.toFixed(4)} SOL`);
|
|
294
|
+
console.log(` Members: ${members.total_members} | Rallies: ${detail.rallies}`);
|
|
295
|
+
console.log(` War Chest: ${detail.war_chest_sol.toFixed(4)} SOL`);
|
|
296
|
+
console.log(` Votes: scorched_earth=${detail.votes_scorched_earth} fortify=${detail.votes_fortify}`);
|
|
297
|
+
if (comms.comms.length > 0) {
|
|
298
|
+
console.log(` Recent comms:`);
|
|
299
|
+
for (const c of comms.comms.slice(0, 3)) {
|
|
300
|
+
console.log(` ${c.sender.slice(0, 8)}...: "${c.memo}"`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (members.members.length > 0) {
|
|
304
|
+
console.log(` Top holders:`);
|
|
305
|
+
for (const m of members.members.slice(0, 3)) {
|
|
306
|
+
console.log(` ${m.address.slice(0, 8)}... — ${m.percentage.toFixed(2)}%`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
} catch (err: any) {
|
|
310
|
+
console.log(` Could not fetch ${f.name}: ${err.message}`);
|
|
311
|
+
}
|
|
312
|
+
console.log();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// World stats
|
|
316
|
+
console.log('World Stats:');
|
|
317
|
+
try {
|
|
318
|
+
const world = await getWorldStats(connection);
|
|
319
|
+
console.log(` Total factions: ${world.total_factions}`);
|
|
320
|
+
console.log(` Rising: ${world.rising_factions} | Ascended: ${world.ascended_factions}`);
|
|
321
|
+
console.log(` Total SOL locked: ${world.total_sol_locked.toFixed(4)} SOL`);
|
|
322
|
+
if (world.most_powerful) {
|
|
323
|
+
console.log(` Most powerful: [${world.most_powerful.symbol}] ${world.most_powerful.name} (score: ${world.most_powerful.score.toFixed(2)})`);
|
|
324
|
+
}
|
|
325
|
+
} catch (err: any) {
|
|
326
|
+
console.log(` Could not fetch world stats: ${err.message}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
console.log('\nSimulation complete.');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ─── Round Stats Tracking ─────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
let _roundJoins = 0;
|
|
335
|
+
let _roundDefections = 0;
|
|
336
|
+
let _roundRallies = 0;
|
|
337
|
+
let _roundErrors = 0;
|
|
338
|
+
|
|
339
|
+
function resetRoundStats() {
|
|
340
|
+
_roundJoins = 0;
|
|
341
|
+
_roundDefections = 0;
|
|
342
|
+
_roundRallies = 0;
|
|
343
|
+
_roundErrors = 0;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function getRoundStats() {
|
|
347
|
+
const stats = { joins: _roundJoins, defections: _roundDefections, rallies: _roundRallies, errors: _roundErrors };
|
|
348
|
+
resetRoundStats();
|
|
349
|
+
return stats;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ─── Action Execution ─────────────────────────────────────────────
|
|
353
|
+
|
|
354
|
+
async function executeRandomAction(
|
|
355
|
+
connection: Connection,
|
|
356
|
+
agentState: AgentState,
|
|
357
|
+
factions: FactionState[],
|
|
358
|
+
) {
|
|
359
|
+
const holdsFactions = [...agentState.holdings.entries()].filter(([, bal]) => bal > 0);
|
|
360
|
+
const canDefect = holdsFactions.length > 0;
|
|
361
|
+
|
|
362
|
+
// Weight actions: 60% join, 20% defect (if possible), 20% rally
|
|
363
|
+
const roll = Math.random();
|
|
364
|
+
let action: 'join' | 'defect' | 'rally';
|
|
365
|
+
|
|
366
|
+
if (roll < 0.6 || (!canDefect)) {
|
|
367
|
+
action = 'join';
|
|
368
|
+
} else if (roll < 0.8 && canDefect) {
|
|
369
|
+
action = 'defect';
|
|
370
|
+
} else {
|
|
371
|
+
action = 'rally';
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
switch (action) {
|
|
375
|
+
case 'join': {
|
|
376
|
+
const faction = pick(factions);
|
|
377
|
+
const alreadyVoted = agentState.voted.has(faction.mint);
|
|
378
|
+
const message = pick(JOIN_MESSAGES);
|
|
379
|
+
const params: any = {
|
|
380
|
+
mint: faction.mint,
|
|
381
|
+
agent: agentState.agent.publicKey,
|
|
382
|
+
amount_sol: JOIN_SOL,
|
|
383
|
+
message,
|
|
384
|
+
};
|
|
385
|
+
if (!alreadyVoted) {
|
|
386
|
+
params.strategy = Math.random() > 0.5 ? 'fortify' : 'scorched_earth';
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
const result = await directJoinFaction(connection, params);
|
|
390
|
+
await sendAndConfirm(connection, agentState.agent, result);
|
|
391
|
+
const prev = agentState.holdings.get(faction.mint) ?? 0;
|
|
392
|
+
agentState.holdings.set(faction.mint, prev + 1);
|
|
393
|
+
agentState.voted.add(faction.mint);
|
|
394
|
+
_roundJoins++;
|
|
395
|
+
} catch (err: any) {
|
|
396
|
+
console.log(` [JOIN ERROR] ${agentState.agent.publicKey.slice(0, 8)}... -> ${faction.name}: ${err.message}`);
|
|
397
|
+
_roundErrors++;
|
|
398
|
+
}
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
case 'defect': {
|
|
403
|
+
const [mint, balance] = pick(holdsFactions);
|
|
404
|
+
const faction = factions.find(f => f.mint === mint);
|
|
405
|
+
if (!faction) { _roundErrors++; break; }
|
|
406
|
+
const sellAmount = Math.max(1, Math.floor(balance * (0.3 + Math.random() * 0.7)));
|
|
407
|
+
const message = pick(DEFECT_MESSAGES);
|
|
408
|
+
try {
|
|
409
|
+
const result = await defect(connection, {
|
|
410
|
+
mint: faction.mint,
|
|
411
|
+
agent: agentState.agent.publicKey,
|
|
412
|
+
amount_tokens: sellAmount,
|
|
413
|
+
message,
|
|
414
|
+
});
|
|
415
|
+
await sendAndConfirm(connection, agentState.agent, result);
|
|
416
|
+
const remaining = (agentState.holdings.get(mint) ?? 0) - sellAmount;
|
|
417
|
+
if (remaining <= 0) {
|
|
418
|
+
agentState.holdings.delete(mint);
|
|
419
|
+
} else {
|
|
420
|
+
agentState.holdings.set(mint, remaining);
|
|
421
|
+
}
|
|
422
|
+
_roundDefections++;
|
|
423
|
+
} catch (err: any) {
|
|
424
|
+
console.log(` [DEFECT ERROR] ${agentState.agent.publicKey.slice(0, 8)}... -> ${faction.name} (${sellAmount} tokens): ${err.message}`);
|
|
425
|
+
_roundErrors++;
|
|
426
|
+
}
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
case 'rally': {
|
|
431
|
+
// Can't rally your own faction or one you already rallied
|
|
432
|
+
const eligible = factions.filter(f =>
|
|
433
|
+
f.founder !== agentState.agent.publicKey &&
|
|
434
|
+
!agentState.rallied.has(f.mint)
|
|
435
|
+
);
|
|
436
|
+
if (eligible.length === 0) { break; } // nothing to rally, not an error
|
|
437
|
+
const faction = pick(eligible);
|
|
438
|
+
try {
|
|
439
|
+
const result = await rally(connection, {
|
|
440
|
+
mint: faction.mint,
|
|
441
|
+
agent: agentState.agent.publicKey,
|
|
442
|
+
});
|
|
443
|
+
await sendAndConfirm(connection, agentState.agent, result);
|
|
444
|
+
agentState.rallied.add(faction.mint);
|
|
445
|
+
_roundRallies++;
|
|
446
|
+
} catch (err: any) {
|
|
447
|
+
console.log(` [RALLY ERROR] ${agentState.agent.publicKey.slice(0, 8)}... -> ${faction.name}: ${err.message}`);
|
|
448
|
+
_roundErrors++;
|
|
449
|
+
}
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
main().catch((err) => {
|
|
456
|
+
console.error('\nSimulation failed:', err);
|
|
457
|
+
process.exit(1);
|
|
458
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"forceConsistentCasingInFileNames": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*.ts"],
|
|
16
|
+
"exclude": ["tests", "dist", "node_modules"]
|
|
17
|
+
}
|