@moltium/world-core 0.1.22 → 0.1.24

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/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  createLogger,
4
4
  logger,
5
5
  normalizeAgentUrl
6
- } from "./chunk-DWXNX5V3.js";
6
+ } from "./chunk-Y563WZWH.js";
7
7
 
8
8
  // src/config/types.ts
9
9
  function profileFromCard(card, walletAddress) {
@@ -766,7 +766,7 @@ var BlockchainClient = class {
766
766
  return this.wallet.address;
767
767
  }
768
768
  /**
769
- * Get entry fee in MON
769
+ * Get entry fee in MON (formatted)
770
770
  */
771
771
  async getEntryFee() {
772
772
  if (!this.membershipContract) {
@@ -780,6 +780,78 @@ var BlockchainClient = class {
780
780
  return "0";
781
781
  }
782
782
  }
783
+ /**
784
+ * Get entry fee in wei (raw BigInt string)
785
+ */
786
+ async getEntryFeeWei() {
787
+ if (!this.membershipContract) {
788
+ return "0";
789
+ }
790
+ try {
791
+ const fee = await this.membershipContract.entryFee();
792
+ return fee.toString();
793
+ } catch (error) {
794
+ logger3.error("Failed to get entry fee (wei)", { error: error.message });
795
+ return "0";
796
+ }
797
+ }
798
+ // Track used payment tx hashes to prevent replay attacks
799
+ usedPayments = /* @__PURE__ */ new Set();
800
+ /**
801
+ * Verify a payment transaction from an agent.
802
+ * Checks: tx confirmed, correct sender, correct recipient (world wallet), sufficient amount.
803
+ * Prevents replay attacks by tracking used tx hashes.
804
+ */
805
+ async verifyPayment(txHash, expectedSender, expectedAmountWei) {
806
+ const normalizedHash = txHash.toLowerCase();
807
+ if (this.usedPayments.has(normalizedHash)) {
808
+ logger3.warn("Payment verification: transaction already used", { txHash });
809
+ return false;
810
+ }
811
+ try {
812
+ const receipt = await this.provider.getTransactionReceipt(txHash);
813
+ if (!receipt || receipt.status !== 1) {
814
+ logger3.warn("Payment verification: transaction not confirmed or failed", { txHash });
815
+ return false;
816
+ }
817
+ const tx = await this.provider.getTransaction(txHash);
818
+ if (!tx) {
819
+ logger3.warn("Payment verification: transaction not found", { txHash });
820
+ return false;
821
+ }
822
+ if (tx.from.toLowerCase() !== expectedSender.toLowerCase()) {
823
+ logger3.warn("Payment verification: wrong sender", {
824
+ expected: expectedSender,
825
+ actual: tx.from
826
+ });
827
+ return false;
828
+ }
829
+ if (tx.to?.toLowerCase() !== this.wallet.address.toLowerCase()) {
830
+ logger3.warn("Payment verification: wrong recipient", {
831
+ expected: this.wallet.address,
832
+ actual: tx.to
833
+ });
834
+ return false;
835
+ }
836
+ if (tx.value < BigInt(expectedAmountWei)) {
837
+ logger3.warn("Payment verification: insufficient amount", {
838
+ expected: expectedAmountWei,
839
+ actual: tx.value.toString()
840
+ });
841
+ return false;
842
+ }
843
+ this.usedPayments.add(normalizedHash);
844
+ logger3.info("Payment verified successfully", {
845
+ txHash,
846
+ sender: tx.from,
847
+ amount: ethers.formatEther(tx.value)
848
+ });
849
+ return true;
850
+ } catch (error) {
851
+ logger3.error("Payment verification error", { txHash, error: error.message });
852
+ return false;
853
+ }
854
+ }
783
855
  };
784
856
 
785
857
  // src/a2a/WorldA2AClient.ts
@@ -1246,30 +1318,71 @@ var TickOrchestrator = class {
1246
1318
 
1247
1319
  // src/a2a/MessageRouter.ts
1248
1320
  var logger8 = createLogger("MessageRouter");
1321
+ var c = {
1322
+ reset: "\x1B[0m",
1323
+ bold: "\x1B[1m",
1324
+ dim: "\x1B[2m",
1325
+ blue: "\x1B[34m",
1326
+ green: "\x1B[32m",
1327
+ yellow: "\x1B[33m",
1328
+ magenta: "\x1B[35m",
1329
+ cyan: "\x1B[36m",
1330
+ red: "\x1B[31m",
1331
+ gray: "\x1B[90m"
1332
+ };
1249
1333
  var MessageRouter = class {
1250
1334
  constructor(a2aClient, getAgents) {
1251
1335
  this.a2aClient = a2aClient;
1252
1336
  this.getAgents = getAgents;
1253
1337
  }
1338
+ /**
1339
+ * Extract base URL (protocol://host:port) from a full URL for comparison.
1340
+ * Agent cards use full URLs (e.g. http://localhost:3001/a2a/jsonrpc)
1341
+ * while agents send base URLs (e.g. http://localhost:3001).
1342
+ */
1343
+ extractBaseUrl(url) {
1344
+ try {
1345
+ const parsed = new URL(url);
1346
+ return `${parsed.protocol}//${parsed.host}`;
1347
+ } catch {
1348
+ return url;
1349
+ }
1350
+ }
1351
+ /**
1352
+ * Find an agent by URL, normalizing both sides to base URL for comparison
1353
+ */
1354
+ findAgent(agents, url) {
1355
+ const baseUrl = this.extractBaseUrl(url);
1356
+ return agents.find((a) => this.extractBaseUrl(a.url) === baseUrl);
1357
+ }
1254
1358
  /**
1255
1359
  * Route a message from one agent to another
1256
1360
  */
1257
1361
  async route(fromAgentUrl, toAgentUrl, message) {
1258
1362
  const agents = this.getAgents();
1259
- const sender = agents.find((a) => a.url === fromAgentUrl);
1363
+ const sender = this.findAgent(agents, fromAgentUrl);
1260
1364
  if (!sender) {
1365
+ logger8.warn(`Message rejected \u2014 sender not in world: ${fromAgentUrl}`);
1261
1366
  return {
1262
1367
  success: false,
1263
1368
  error: `Sender not in world: ${fromAgentUrl}`
1264
1369
  };
1265
1370
  }
1266
- const recipient = agents.find((a) => a.url === toAgentUrl);
1371
+ const recipient = this.findAgent(agents, toAgentUrl);
1267
1372
  if (!recipient) {
1373
+ logger8.warn(`Message rejected \u2014 recipient not in world: ${toAgentUrl}`);
1268
1374
  return {
1269
1375
  success: false,
1270
1376
  error: `Recipient not in world: ${toAgentUrl}`
1271
1377
  };
1272
1378
  }
1379
+ console.log(
1380
+ `
1381
+ ${c.cyan}\u2501\u2501\u2501 MESSAGE \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501${c.reset}
1382
+ ${c.bold}${c.blue}${sender.name}${c.reset} ${c.dim}\u2192${c.reset} ${c.bold}${c.green}${recipient.name}${c.reset}
1383
+ ${c.dim}\u2502${c.reset} ${this.truncate(message, 300)}
1384
+ ${c.cyan}\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501${c.reset}`
1385
+ );
1273
1386
  const wrappedMessage = JSON.stringify({
1274
1387
  type: "world_message",
1275
1388
  from: {
@@ -1279,19 +1392,33 @@ var MessageRouter = class {
1279
1392
  },
1280
1393
  message
1281
1394
  });
1282
- logger8.info(`Routing message: ${sender.name} \u2192 ${recipient.name}`);
1283
- return this.a2aClient.sendToAgent(toAgentUrl, wrappedMessage, {
1395
+ const response = await this.a2aClient.sendToAgent(toAgentUrl, wrappedMessage, {
1284
1396
  type: "world_message",
1285
1397
  fromAgentUrl
1286
1398
  });
1399
+ if (response.success && response.reply) {
1400
+ console.log(
1401
+ `
1402
+ ${c.green}\u2501\u2501\u2501 REPLY \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501${c.reset}
1403
+ ${c.bold}${c.green}${recipient.name}${c.reset} ${c.dim}\u2192${c.reset} ${c.bold}${c.blue}${sender.name}${c.reset}
1404
+ ${c.dim}\u2502${c.reset} ${this.truncate(response.reply, 300)}
1405
+ ${c.green}\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501${c.reset}`
1406
+ );
1407
+ } else if (!response.success) {
1408
+ console.log(
1409
+ `${c.red} \u2717 ${recipient.name} failed to respond: ${response.error || "no response"}${c.reset}`
1410
+ );
1411
+ }
1412
+ return response;
1287
1413
  }
1288
1414
  /**
1289
1415
  * Broadcast a message from one agent to all other agents in the world
1290
1416
  */
1291
1417
  async routeToAll(fromAgentUrl, message) {
1292
1418
  const agents = this.getAgents();
1293
- const sender = agents.find((a) => a.url === fromAgentUrl);
1419
+ const sender = this.findAgent(agents, fromAgentUrl);
1294
1420
  if (!sender) {
1421
+ logger8.warn(`Broadcast rejected \u2014 sender not in world: ${fromAgentUrl}`);
1295
1422
  const result = /* @__PURE__ */ new Map();
1296
1423
  result.set(fromAgentUrl, {
1297
1424
  success: false,
@@ -1299,6 +1426,15 @@ var MessageRouter = class {
1299
1426
  });
1300
1427
  return result;
1301
1428
  }
1429
+ const senderBaseUrl = this.extractBaseUrl(fromAgentUrl);
1430
+ const recipients = agents.filter((a) => this.extractBaseUrl(a.url) !== senderBaseUrl);
1431
+ console.log(
1432
+ `
1433
+ ${c.yellow}\u2501\u2501\u2501 BROADCAST \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501${c.reset}
1434
+ ${c.bold}${c.blue}${sender.name}${c.reset} ${c.dim}\u2192${c.reset} ${c.bold}${c.yellow}ALL (${recipients.length} agents)${c.reset}
1435
+ ${c.dim}\u2502${c.reset} ${this.truncate(message, 300)}
1436
+ ${c.yellow}\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501${c.reset}`
1437
+ );
1302
1438
  const wrappedMessage = JSON.stringify({
1303
1439
  type: "world_broadcast",
1304
1440
  from: {
@@ -1309,7 +1445,6 @@ var MessageRouter = class {
1309
1445
  message
1310
1446
  });
1311
1447
  const results = /* @__PURE__ */ new Map();
1312
- const recipients = agents.filter((a) => a.url !== fromAgentUrl);
1313
1448
  const promises = recipients.map(async (recipient) => {
1314
1449
  const response = await this.a2aClient.sendToAgent(
1315
1450
  recipient.url,
@@ -1317,11 +1452,32 @@ var MessageRouter = class {
1317
1452
  { type: "world_broadcast", fromAgentUrl }
1318
1453
  );
1319
1454
  results.set(recipient.url, response);
1455
+ if (response.success && response.reply) {
1456
+ console.log(
1457
+ `${c.green} \u21A9 ${c.bold}${recipient.name}${c.reset}${c.green} replied:${c.reset} ${this.truncate(response.reply, 200)}`
1458
+ );
1459
+ } else if (!response.success) {
1460
+ console.log(
1461
+ `${c.red} \u2717 ${recipient.name} failed: ${response.error || "no response"}${c.reset}`
1462
+ );
1463
+ }
1320
1464
  });
1321
1465
  await Promise.allSettled(promises);
1322
- logger8.info(`Broadcast from ${sender.name} to ${recipients.length} agents`);
1466
+ const succeeded = Array.from(results.values()).filter((r) => r.success).length;
1467
+ const failed = Array.from(results.values()).filter((r) => !r.success).length;
1468
+ console.log(
1469
+ `${c.dim} Broadcast complete: ${c.green}${succeeded} replied${c.reset}${c.dim}${failed > 0 ? `, ${c.red}${failed} failed${c.reset}` : ""}${c.reset}
1470
+ `
1471
+ );
1323
1472
  return results;
1324
1473
  }
1474
+ /**
1475
+ * Truncate long messages for logging
1476
+ */
1477
+ truncate(text, maxLen) {
1478
+ if (text.length <= maxLen) return text;
1479
+ return text.slice(0, maxLen) + "...";
1480
+ }
1325
1481
  };
1326
1482
 
1327
1483
  // src/engine/World.ts
@@ -1476,10 +1632,20 @@ var World = class {
1476
1632
  });
1477
1633
  logger9.info("World stopped");
1478
1634
  }
1635
+ /**
1636
+ * Get the blockchain client (for server endpoints)
1637
+ */
1638
+ getBlockchainClient() {
1639
+ return this.blockchainClient;
1640
+ }
1479
1641
  /**
1480
1642
  * Admit an agent to the world
1481
1643
  */
1482
- async admitAgent(card, walletAddress) {
1644
+ async admitAgent(card, walletAddress, paymentTxHash) {
1645
+ if (this.agents.has(card.url)) {
1646
+ logger9.info(`Agent already in world, skipping re-admission: ${card.name}`);
1647
+ return;
1648
+ }
1483
1649
  const decision = await this.evaluator.evaluate(card, this.agents.size, walletAddress);
1484
1650
  if (!decision.admitted) {
1485
1651
  throw new Error(`Agent admission denied: ${decision.reason}`);
@@ -1491,9 +1657,36 @@ var World = class {
1491
1657
  }
1492
1658
  logger9.info(`Agent validated on-chain: ${card.name}`);
1493
1659
  }
1494
- if (this.blockchainClient && this.config.blockchain?.requireMembership && walletAddress) {
1660
+ if (this.blockchainClient && this.config.blockchain?.requireMembership) {
1661
+ if (!walletAddress) {
1662
+ throw new Error(
1663
+ "This world requires blockchain membership. Provide a walletAddress when joining. Set AGENT_WALLET_PRIVATE_KEY in your .env file to auto-derive a wallet address."
1664
+ );
1665
+ }
1495
1666
  const hasMembership = await this.blockchainClient.hasMembership(walletAddress);
1496
1667
  if (!hasMembership) {
1668
+ const entryFeeWei = await this.blockchainClient.getEntryFeeWei();
1669
+ if (entryFeeWei !== "0" && BigInt(entryFeeWei) > 0n) {
1670
+ if (!paymentTxHash) {
1671
+ throw new Error(
1672
+ `Entry fee required: ${entryFeeWei} wei. Query GET /world/join-info for payment details, pay the fee, then include paymentTxHash in your join request.`
1673
+ );
1674
+ }
1675
+ const paymentValid = await this.blockchainClient.verifyPayment(
1676
+ paymentTxHash,
1677
+ walletAddress,
1678
+ entryFeeWei
1679
+ );
1680
+ if (!paymentValid) {
1681
+ throw new Error(
1682
+ `Payment verification failed for tx ${paymentTxHash}. Ensure the transaction is confirmed, sent from ${walletAddress}, to ${this.blockchainClient.getWalletAddress()}, for at least ${entryFeeWei} wei.`
1683
+ );
1684
+ }
1685
+ logger9.info(`Entry fee payment verified for ${card.name}`, {
1686
+ txHash: paymentTxHash,
1687
+ amount: entryFeeWei
1688
+ });
1689
+ }
1497
1690
  logger9.info(`Minting membership NFT for agent: ${card.name}`);
1498
1691
  await this.blockchainClient.mintMembership(walletAddress);
1499
1692
  } else {
@@ -1677,9 +1870,40 @@ function createWorldApp(world) {
1677
1870
  }
1678
1871
  });
1679
1872
  });
1873
+ app.get("/world/join-info", async (req, res) => {
1874
+ try {
1875
+ const blockchain = world.config.blockchain;
1876
+ const client = world.getBlockchainClient();
1877
+ if (!blockchain || !client) {
1878
+ return res.json({
1879
+ worldName: world.config.name,
1880
+ requiresPayment: false,
1881
+ entryFee: "0",
1882
+ paymentAddress: null,
1883
+ chainId: null,
1884
+ rpcUrl: null
1885
+ });
1886
+ }
1887
+ const entryFeeWei = await client.getEntryFeeWei();
1888
+ res.json({
1889
+ worldName: world.config.name,
1890
+ requiresPayment: entryFeeWei !== "0" && BigInt(entryFeeWei) > 0n,
1891
+ entryFee: entryFeeWei,
1892
+ paymentAddress: client.getWalletAddress(),
1893
+ chainId: blockchain.chainId || null,
1894
+ rpcUrl: blockchain.rpcUrl
1895
+ });
1896
+ } catch (error) {
1897
+ logger10.error("Failed to get join info:", error);
1898
+ res.status(500).json({
1899
+ success: false,
1900
+ error: error.message || "Failed to get join info"
1901
+ });
1902
+ }
1903
+ });
1680
1904
  app.post("/world/join", async (req, res) => {
1681
1905
  try {
1682
- const { agentUrl, walletAddress } = req.body;
1906
+ const { agentUrl, walletAddress, paymentTxHash } = req.body;
1683
1907
  if (!agentUrl) {
1684
1908
  return res.status(400).json({
1685
1909
  success: false,
@@ -1687,9 +1911,10 @@ function createWorldApp(world) {
1687
1911
  });
1688
1912
  }
1689
1913
  logger10.info(`Agent join request from: ${agentUrl}`, {
1690
- wallet: walletAddress || "none"
1914
+ wallet: walletAddress || "none",
1915
+ paymentTx: paymentTxHash ? `${paymentTxHash.slice(0, 10)}...` : "none"
1691
1916
  });
1692
- const cardFetcher = new (await import("./CardFetcher-4ENWKI6E.js")).CardFetcher();
1917
+ const cardFetcher = new (await import("./CardFetcher-IMUPYNEQ.js")).CardFetcher();
1693
1918
  const result = await cardFetcher.fetchCard(agentUrl);
1694
1919
  if (!result.success || !result.card) {
1695
1920
  return res.status(400).json({
@@ -1697,7 +1922,7 @@ function createWorldApp(world) {
1697
1922
  error: `Failed to fetch agent card: ${result.error}`
1698
1923
  });
1699
1924
  }
1700
- await world.admitAgent(result.card, walletAddress);
1925
+ await world.admitAgent(result.card, walletAddress, paymentTxHash);
1701
1926
  res.json({
1702
1927
  success: true,
1703
1928
  message: "Agent admitted to world",
@@ -1855,6 +2080,7 @@ async function startWorldServer(world) {
1855
2080
  logger10.info("");
1856
2081
  logger10.info("Endpoints:");
1857
2082
  logger10.info(` GET / - World info`);
2083
+ logger10.info(` GET /world/join-info - Join requirements & entry fee`);
1858
2084
  logger10.info(` POST /world/join - Agent join request`);
1859
2085
  logger10.info(` GET /world/agents - List agents`);
1860
2086
  logger10.info(` GET /world/state - World state`);