chainlesschain 0.51.0 → 0.66.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/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AppLayout-Rvi759IS.js → AppLayout-6SPt_8Y_.js} +1 -1
- package/src/assets/web-panel/assets/{Dashboard-DBhFxXYQ.js → Dashboard-Br7kCwKJ.js} +2 -2
- package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +1 -0
- package/src/assets/web-panel/assets/{index-uL0cZ8N_.js → index-tN-8TosE.js} +2 -2
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/agent-network.js +785 -0
- package/src/commands/automation.js +654 -0
- package/src/commands/dao.js +565 -0
- package/src/commands/did-v2.js +620 -0
- package/src/commands/economy.js +578 -0
- package/src/commands/evolution.js +391 -0
- package/src/commands/hmemory.js +442 -0
- package/src/commands/perf.js +433 -0
- package/src/commands/pipeline.js +449 -0
- package/src/commands/plugin-ecosystem.js +517 -0
- package/src/commands/sandbox.js +401 -0
- package/src/commands/social.js +311 -0
- package/src/commands/sso.js +798 -0
- package/src/commands/workflow.js +320 -0
- package/src/commands/zkp.js +227 -1
- package/src/index.js +21 -0
- package/src/lib/agent-economy.js +479 -0
- package/src/lib/agent-network.js +1121 -0
- package/src/lib/automation-engine.js +948 -0
- package/src/lib/dao-governance.js +569 -0
- package/src/lib/did-v2-manager.js +1127 -0
- package/src/lib/evolution-system.js +453 -0
- package/src/lib/hierarchical-memory.js +481 -0
- package/src/lib/perf-tuning.js +734 -0
- package/src/lib/pipeline-orchestrator.js +928 -0
- package/src/lib/plugin-ecosystem.js +1109 -0
- package/src/lib/sandbox-v2.js +306 -0
- package/src/lib/social-graph-analytics.js +707 -0
- package/src/lib/sso-manager.js +841 -0
- package/src/lib/workflow-engine.js +454 -1
- package/src/lib/zkp-engine.js +249 -20
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +0 -1
package/src/lib/agent-economy.js
CHANGED
|
@@ -367,3 +367,482 @@ export function _resetState() {
|
|
|
367
367
|
export function _setBalance(agentId, balance, locked) {
|
|
368
368
|
_balances.set(agentId, { balance, locked: locked || 0 });
|
|
369
369
|
}
|
|
370
|
+
|
|
371
|
+
// ═════════════════════════════════════════════════════════════════
|
|
372
|
+
// Phase 85 — Agent Economy 2.0 additions (strictly-additive)
|
|
373
|
+
// Frozen canonical enums (PAYMENT_TYPE / CHANNEL_STATUS / RESOURCE_TYPE /
|
|
374
|
+
// NFT_STATUS) + payment-model-aware pricing + channel lifecycle (open →
|
|
375
|
+
// active → settling → closed, + disputed) + NFT status state machine +
|
|
376
|
+
// contribution-share based revenue distribution + extended stats.
|
|
377
|
+
// ═════════════════════════════════════════════════════════════════
|
|
378
|
+
|
|
379
|
+
export const PAYMENT_TYPE = Object.freeze({
|
|
380
|
+
PER_CALL: "per_call",
|
|
381
|
+
PER_TOKEN: "per_token",
|
|
382
|
+
PER_MINUTE: "per_minute",
|
|
383
|
+
FLAT_RATE: "flat_rate",
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
export const CHANNEL_STATUS = Object.freeze({
|
|
387
|
+
OPEN: "open",
|
|
388
|
+
ACTIVE: "active",
|
|
389
|
+
SETTLING: "settling",
|
|
390
|
+
CLOSED: "closed",
|
|
391
|
+
DISPUTED: "disputed",
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
export const RESOURCE_TYPE = Object.freeze({
|
|
395
|
+
COMPUTE: "compute",
|
|
396
|
+
STORAGE: "storage",
|
|
397
|
+
MODEL: "model",
|
|
398
|
+
DATA: "data",
|
|
399
|
+
SKILL: "skill",
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
export const NFT_STATUS = Object.freeze({
|
|
403
|
+
MINTED: "minted",
|
|
404
|
+
LISTED: "listed",
|
|
405
|
+
SOLD: "sold",
|
|
406
|
+
BURNED: "burned",
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
const _PAYMENT_TYPE_VALUES = new Set(Object.values(PAYMENT_TYPE));
|
|
410
|
+
const _CHANNEL_STATUS_VALUES = new Set(Object.values(CHANNEL_STATUS));
|
|
411
|
+
const _RESOURCE_TYPE_VALUES = new Set(Object.values(RESOURCE_TYPE));
|
|
412
|
+
const _NFT_STATUS_VALUES = new Set(Object.values(NFT_STATUS));
|
|
413
|
+
|
|
414
|
+
// V2 stores
|
|
415
|
+
const _v2PriceModels = new Map(); // serviceId → { paymentType, rate, metadata }
|
|
416
|
+
const _v2NftStatus = new Map(); // nftId → { status, listedPrice?, soldTo?, royaltyPercent }
|
|
417
|
+
const _v2TaskContributions = new Map(); // taskId → [{ agentId, weight }]
|
|
418
|
+
const _v2Distributions = []; // { id, taskId, total, shares[], distributedAt }
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Set a price model for a service keyed by PAYMENT_TYPE.
|
|
422
|
+
* rate semantics:
|
|
423
|
+
* per_call — cost per invocation
|
|
424
|
+
* per_token — cost per token (LLM usage)
|
|
425
|
+
* per_minute — cost per minute (time-based)
|
|
426
|
+
* flat_rate — one-shot cost (amount is ignored on pay)
|
|
427
|
+
*/
|
|
428
|
+
export function priceServiceV2(
|
|
429
|
+
db,
|
|
430
|
+
{ serviceId, paymentType, rate, metadata } = {},
|
|
431
|
+
) {
|
|
432
|
+
if (!serviceId) throw new Error("serviceId required");
|
|
433
|
+
if (!_PAYMENT_TYPE_VALUES.has(paymentType))
|
|
434
|
+
throw new Error(`Invalid paymentType: ${paymentType}`);
|
|
435
|
+
if (!Number.isFinite(rate) || rate < 0)
|
|
436
|
+
throw new Error(`Invalid rate: ${rate}`);
|
|
437
|
+
const entry = {
|
|
438
|
+
serviceId,
|
|
439
|
+
paymentType,
|
|
440
|
+
rate,
|
|
441
|
+
metadata: metadata || {},
|
|
442
|
+
updatedAt: new Date().toISOString(),
|
|
443
|
+
};
|
|
444
|
+
_v2PriceModels.set(serviceId, entry);
|
|
445
|
+
return entry;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function getPriceModel(serviceId) {
|
|
449
|
+
return _v2PriceModels.get(serviceId) || null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Pay for a service using its registered price model. Computes amount based on
|
|
454
|
+
* paymentType and usage (tokens / minutes / calls). Falls through to existing
|
|
455
|
+
* pay() for actual balance transfer + DB write.
|
|
456
|
+
*/
|
|
457
|
+
export function payV2(
|
|
458
|
+
db,
|
|
459
|
+
{
|
|
460
|
+
fromAgentId,
|
|
461
|
+
toAgentId,
|
|
462
|
+
serviceId,
|
|
463
|
+
tokens,
|
|
464
|
+
minutes,
|
|
465
|
+
calls = 1,
|
|
466
|
+
metadata,
|
|
467
|
+
} = {},
|
|
468
|
+
) {
|
|
469
|
+
if (!fromAgentId || !toAgentId)
|
|
470
|
+
throw new Error("fromAgentId & toAgentId required");
|
|
471
|
+
if (!serviceId) throw new Error("serviceId required");
|
|
472
|
+
const model = _v2PriceModels.get(serviceId);
|
|
473
|
+
if (!model) throw new Error(`No price model for service: ${serviceId}`);
|
|
474
|
+
let amount;
|
|
475
|
+
switch (model.paymentType) {
|
|
476
|
+
case PAYMENT_TYPE.PER_CALL:
|
|
477
|
+
amount = model.rate * (calls || 1);
|
|
478
|
+
break;
|
|
479
|
+
case PAYMENT_TYPE.PER_TOKEN:
|
|
480
|
+
if (!Number.isFinite(tokens) || tokens < 0)
|
|
481
|
+
throw new Error("tokens required for per_token pricing");
|
|
482
|
+
amount = model.rate * tokens;
|
|
483
|
+
break;
|
|
484
|
+
case PAYMENT_TYPE.PER_MINUTE:
|
|
485
|
+
if (!Number.isFinite(minutes) || minutes < 0)
|
|
486
|
+
throw new Error("minutes required for per_minute pricing");
|
|
487
|
+
amount = model.rate * minutes;
|
|
488
|
+
break;
|
|
489
|
+
case PAYMENT_TYPE.FLAT_RATE:
|
|
490
|
+
amount = model.rate;
|
|
491
|
+
break;
|
|
492
|
+
default:
|
|
493
|
+
throw new Error(`Unknown paymentType: ${model.paymentType}`);
|
|
494
|
+
}
|
|
495
|
+
if (amount <= 0) {
|
|
496
|
+
// No-op transfer: still record the intent
|
|
497
|
+
return {
|
|
498
|
+
txId: null,
|
|
499
|
+
from: fromAgentId,
|
|
500
|
+
to: toAgentId,
|
|
501
|
+
amount: 0,
|
|
502
|
+
paymentType: model.paymentType,
|
|
503
|
+
serviceId,
|
|
504
|
+
skipped: true,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
const result = pay(
|
|
508
|
+
db,
|
|
509
|
+
fromAgentId,
|
|
510
|
+
toAgentId,
|
|
511
|
+
amount,
|
|
512
|
+
JSON.stringify({
|
|
513
|
+
serviceId,
|
|
514
|
+
paymentType: model.paymentType,
|
|
515
|
+
metadata: metadata || {},
|
|
516
|
+
}),
|
|
517
|
+
);
|
|
518
|
+
return { ...result, paymentType: model.paymentType, serviceId };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Open a two-sided state channel with both parties depositing.
|
|
523
|
+
* Transitions: OPEN → (activateChannel) → ACTIVE → (initiateSettlement) →
|
|
524
|
+
* SETTLING → (closeChannelV2) → CLOSED. DISPUTED can be set from ACTIVE.
|
|
525
|
+
*/
|
|
526
|
+
export function openChannelV2(
|
|
527
|
+
db,
|
|
528
|
+
{ partyA, partyB, depositA = 0, depositB = 0 } = {},
|
|
529
|
+
) {
|
|
530
|
+
if (!partyA || !partyB) throw new Error("partyA & partyB required");
|
|
531
|
+
if (partyA === partyB) throw new Error("partyA cannot equal partyB");
|
|
532
|
+
if (depositA < 0 || depositB < 0)
|
|
533
|
+
throw new Error("deposits must be non-negative");
|
|
534
|
+
// Use existing openChannel for partyA deposit + DB; then attach partyB deposit
|
|
535
|
+
const ch = openChannel(db, partyA, partyB, depositA);
|
|
536
|
+
if (depositB > 0) {
|
|
537
|
+
const balB = _getBalance(partyB);
|
|
538
|
+
if (balB.balance < depositB)
|
|
539
|
+
throw new Error(
|
|
540
|
+
`Insufficient balance for partyB: ${balB.balance} < ${depositB}`,
|
|
541
|
+
);
|
|
542
|
+
balB.balance -= depositB;
|
|
543
|
+
balB.locked += depositB;
|
|
544
|
+
const stored = _channels.get(ch.id);
|
|
545
|
+
stored.balanceB = depositB;
|
|
546
|
+
ch.balanceB = depositB;
|
|
547
|
+
db.prepare(`UPDATE economy_channels SET balance_b = ? WHERE id = ?`).run(
|
|
548
|
+
depositB,
|
|
549
|
+
ch.id,
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
// Upgrade channel.status to use canonical enum value
|
|
553
|
+
const stored = _channels.get(ch.id);
|
|
554
|
+
stored.status = CHANNEL_STATUS.OPEN;
|
|
555
|
+
return { ...stored };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export function activateChannel(db, channelId) {
|
|
559
|
+
const ch = _channels.get(channelId);
|
|
560
|
+
if (!ch) throw new Error(`Channel not found: ${channelId}`);
|
|
561
|
+
if (ch.status !== CHANNEL_STATUS.OPEN)
|
|
562
|
+
throw new Error(`Cannot activate: channel is ${ch.status}, not open`);
|
|
563
|
+
ch.status = CHANNEL_STATUS.ACTIVE;
|
|
564
|
+
db.prepare(`UPDATE economy_channels SET status = ? WHERE id = ?`).run(
|
|
565
|
+
CHANNEL_STATUS.ACTIVE,
|
|
566
|
+
channelId,
|
|
567
|
+
);
|
|
568
|
+
return { ...ch };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function initiateSettlement(
|
|
572
|
+
db,
|
|
573
|
+
channelId,
|
|
574
|
+
{ finalBalanceA, finalBalanceB } = {},
|
|
575
|
+
) {
|
|
576
|
+
const ch = _channels.get(channelId);
|
|
577
|
+
if (!ch) throw new Error(`Channel not found: ${channelId}`);
|
|
578
|
+
if (ch.status !== CHANNEL_STATUS.ACTIVE)
|
|
579
|
+
throw new Error(`Cannot settle: channel is ${ch.status}, not active`);
|
|
580
|
+
const total = (ch.balanceA || 0) + (ch.balanceB || 0);
|
|
581
|
+
const a = Number.isFinite(finalBalanceA) ? finalBalanceA : ch.balanceA;
|
|
582
|
+
const b = Number.isFinite(finalBalanceB) ? finalBalanceB : ch.balanceB;
|
|
583
|
+
if (a < 0 || b < 0) throw new Error("Final balances must be non-negative");
|
|
584
|
+
if (Math.abs(a + b - total) > 1e-9)
|
|
585
|
+
throw new Error(`Settlement must preserve total: ${a}+${b} ≠ ${total}`);
|
|
586
|
+
ch.balanceA = a;
|
|
587
|
+
ch.balanceB = b;
|
|
588
|
+
ch.status = CHANNEL_STATUS.SETTLING;
|
|
589
|
+
db.prepare(
|
|
590
|
+
`UPDATE economy_channels SET status = ?, balance_a = ?, balance_b = ? WHERE id = ?`,
|
|
591
|
+
).run(CHANNEL_STATUS.SETTLING, a, b, channelId);
|
|
592
|
+
return { ...ch };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export function closeChannelV2(db, channelId) {
|
|
596
|
+
const ch = _channels.get(channelId);
|
|
597
|
+
if (!ch) throw new Error(`Channel not found: ${channelId}`);
|
|
598
|
+
if (
|
|
599
|
+
ch.status !== CHANNEL_STATUS.SETTLING &&
|
|
600
|
+
ch.status !== CHANNEL_STATUS.OPEN &&
|
|
601
|
+
ch.status !== CHANNEL_STATUS.ACTIVE
|
|
602
|
+
)
|
|
603
|
+
throw new Error(`Cannot close: channel is ${ch.status}`);
|
|
604
|
+
// Release locked funds back as balance to each party
|
|
605
|
+
const balA = _getBalance(ch.partyA);
|
|
606
|
+
const balB = _getBalance(ch.partyB);
|
|
607
|
+
balA.balance += ch.balanceA;
|
|
608
|
+
balA.locked = Math.max(0, balA.locked - ch.balanceA);
|
|
609
|
+
balB.balance += ch.balanceB;
|
|
610
|
+
balB.locked = Math.max(0, balB.locked - ch.balanceB);
|
|
611
|
+
ch.status = CHANNEL_STATUS.CLOSED;
|
|
612
|
+
db.prepare(`UPDATE economy_channels SET status = ? WHERE id = ?`).run(
|
|
613
|
+
CHANNEL_STATUS.CLOSED,
|
|
614
|
+
channelId,
|
|
615
|
+
);
|
|
616
|
+
return { ...ch };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
export function disputeChannel(db, channelId, reason) {
|
|
620
|
+
const ch = _channels.get(channelId);
|
|
621
|
+
if (!ch) throw new Error(`Channel not found: ${channelId}`);
|
|
622
|
+
if (
|
|
623
|
+
ch.status === CHANNEL_STATUS.CLOSED ||
|
|
624
|
+
ch.status === CHANNEL_STATUS.DISPUTED
|
|
625
|
+
)
|
|
626
|
+
throw new Error(`Cannot dispute: channel is ${ch.status}`);
|
|
627
|
+
ch.status = CHANNEL_STATUS.DISPUTED;
|
|
628
|
+
ch.disputeReason = reason || null;
|
|
629
|
+
db.prepare(`UPDATE economy_channels SET status = ? WHERE id = ?`).run(
|
|
630
|
+
CHANNEL_STATUS.DISPUTED,
|
|
631
|
+
channelId,
|
|
632
|
+
);
|
|
633
|
+
return { ...ch };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
export function listChannelsV2(options = {}) {
|
|
637
|
+
let result = [..._channels.values()];
|
|
638
|
+
if (options.status) {
|
|
639
|
+
if (!_CHANNEL_STATUS_VALUES.has(options.status))
|
|
640
|
+
throw new Error(`Invalid status: ${options.status}`);
|
|
641
|
+
result = result.filter((c) => c.status === options.status);
|
|
642
|
+
}
|
|
643
|
+
if (options.party)
|
|
644
|
+
result = result.filter(
|
|
645
|
+
(c) => c.partyA === options.party || c.partyB === options.party,
|
|
646
|
+
);
|
|
647
|
+
return result;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* List a resource with validated RESOURCE_TYPE enum.
|
|
652
|
+
*/
|
|
653
|
+
export function listResourceV2(
|
|
654
|
+
db,
|
|
655
|
+
{ sellerId, resourceType, name, price, available = 1, metadata } = {},
|
|
656
|
+
) {
|
|
657
|
+
if (!_RESOURCE_TYPE_VALUES.has(resourceType))
|
|
658
|
+
throw new Error(`Invalid resourceType: ${resourceType}`);
|
|
659
|
+
if (!Number.isFinite(price) || price < 0)
|
|
660
|
+
throw new Error(`Invalid price: ${price}`);
|
|
661
|
+
const listing = listResource(db, resourceType, sellerId, price, available);
|
|
662
|
+
listing.name = name || null;
|
|
663
|
+
listing.metadata = metadata || {};
|
|
664
|
+
return listing;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Mint an NFT tracked against NFT_STATUS state machine with royalty metadata.
|
|
669
|
+
*/
|
|
670
|
+
export function mintNFTV2(
|
|
671
|
+
db,
|
|
672
|
+
{ owner, assetType, metadata, royaltyPercent = 0 } = {},
|
|
673
|
+
) {
|
|
674
|
+
if (!owner) throw new Error("owner required");
|
|
675
|
+
if (!assetType) throw new Error("assetType required");
|
|
676
|
+
if (royaltyPercent < 0 || royaltyPercent > 50)
|
|
677
|
+
throw new Error("royaltyPercent must be 0..50");
|
|
678
|
+
const nft = mintNFT(db, owner, assetType, metadata);
|
|
679
|
+
_v2NftStatus.set(nft.id, {
|
|
680
|
+
status: NFT_STATUS.MINTED,
|
|
681
|
+
royaltyPercent,
|
|
682
|
+
listedPrice: null,
|
|
683
|
+
soldTo: null,
|
|
684
|
+
});
|
|
685
|
+
return { ...nft, status: NFT_STATUS.MINTED, royaltyPercent };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export function listNFT(db, nftId, price) {
|
|
689
|
+
const status = _v2NftStatus.get(nftId);
|
|
690
|
+
if (!status) throw new Error(`NFT not found: ${nftId}`);
|
|
691
|
+
if (status.status !== NFT_STATUS.MINTED)
|
|
692
|
+
throw new Error(`Cannot list NFT in ${status.status} state`);
|
|
693
|
+
if (!Number.isFinite(price) || price <= 0)
|
|
694
|
+
throw new Error(`Invalid price: ${price}`);
|
|
695
|
+
status.status = NFT_STATUS.LISTED;
|
|
696
|
+
status.listedPrice = price;
|
|
697
|
+
return { nftId, status: NFT_STATUS.LISTED, price };
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export function buyNFT(db, nftId, buyer) {
|
|
701
|
+
const nft = _nfts.get(nftId);
|
|
702
|
+
if (!nft) throw new Error(`NFT not found: ${nftId}`);
|
|
703
|
+
const status = _v2NftStatus.get(nftId);
|
|
704
|
+
if (!status || status.status !== NFT_STATUS.LISTED)
|
|
705
|
+
throw new Error(`NFT not listed`);
|
|
706
|
+
const price = status.listedPrice;
|
|
707
|
+
const buyerBal = _getBalance(buyer);
|
|
708
|
+
if (buyerBal.balance < price)
|
|
709
|
+
throw new Error(`Insufficient balance: ${buyerBal.balance} < ${price}`);
|
|
710
|
+
// Split: royalty to original minter, remainder to current owner
|
|
711
|
+
const royalty = (price * status.royaltyPercent) / 100;
|
|
712
|
+
const sellerTake = price - royalty;
|
|
713
|
+
buyerBal.balance -= price;
|
|
714
|
+
const sellerBal = _getBalance(nft.owner);
|
|
715
|
+
sellerBal.balance += sellerTake;
|
|
716
|
+
// Royalty always goes to original owner (first minter) — in this v2, nft.owner
|
|
717
|
+
// is the same as original owner since we don't track chain of custody.
|
|
718
|
+
if (royalty > 0) sellerBal.balance += royalty;
|
|
719
|
+
nft.owner = buyer;
|
|
720
|
+
status.status = NFT_STATUS.SOLD;
|
|
721
|
+
status.soldTo = buyer;
|
|
722
|
+
return { nftId, buyer, price, royalty, status: NFT_STATUS.SOLD };
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
export function burnNFT(db, nftId) {
|
|
726
|
+
const nft = _nfts.get(nftId);
|
|
727
|
+
if (!nft) throw new Error(`NFT not found: ${nftId}`);
|
|
728
|
+
const status = _v2NftStatus.get(nftId);
|
|
729
|
+
if (!status) throw new Error(`NFT status missing: ${nftId}`);
|
|
730
|
+
if (status.status === NFT_STATUS.BURNED)
|
|
731
|
+
throw new Error("NFT already burned");
|
|
732
|
+
status.status = NFT_STATUS.BURNED;
|
|
733
|
+
return { nftId, status: NFT_STATUS.BURNED };
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
export function getNFTStatus(nftId) {
|
|
737
|
+
return _v2NftStatus.get(nftId) || null;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Record a task-scoped contribution with a weight (used for share calculation).
|
|
742
|
+
*/
|
|
743
|
+
export function recordTaskContribution(
|
|
744
|
+
db,
|
|
745
|
+
{ taskId, agentId, weight = 1 } = {},
|
|
746
|
+
) {
|
|
747
|
+
if (!taskId) throw new Error("taskId required");
|
|
748
|
+
if (!agentId) throw new Error("agentId required");
|
|
749
|
+
if (!Number.isFinite(weight) || weight <= 0)
|
|
750
|
+
throw new Error("weight must be positive");
|
|
751
|
+
if (!_v2TaskContributions.has(taskId)) _v2TaskContributions.set(taskId, []);
|
|
752
|
+
const entry = { agentId, weight, recordedAt: new Date().toISOString() };
|
|
753
|
+
_v2TaskContributions.get(taskId).push(entry);
|
|
754
|
+
return { taskId, ...entry };
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
export function getTaskContributions(taskId) {
|
|
758
|
+
return [...(_v2TaskContributions.get(taskId) || [])];
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Distribute revenue across contributors proportional to their weights.
|
|
763
|
+
* Returns an array of { agentId, share, newBalance }.
|
|
764
|
+
*/
|
|
765
|
+
export function distributeRevenueV2(db, { taskId, total } = {}) {
|
|
766
|
+
if (!taskId) throw new Error("taskId required");
|
|
767
|
+
if (!Number.isFinite(total) || total <= 0)
|
|
768
|
+
throw new Error("total must be positive");
|
|
769
|
+
const contribs = _v2TaskContributions.get(taskId) || [];
|
|
770
|
+
if (contribs.length === 0)
|
|
771
|
+
throw new Error(`No contributions for task: ${taskId}`);
|
|
772
|
+
// Aggregate by agent
|
|
773
|
+
const weightByAgent = new Map();
|
|
774
|
+
for (const c of contribs)
|
|
775
|
+
weightByAgent.set(
|
|
776
|
+
c.agentId,
|
|
777
|
+
(weightByAgent.get(c.agentId) || 0) + c.weight,
|
|
778
|
+
);
|
|
779
|
+
const totalWeight = [...weightByAgent.values()].reduce((a, b) => a + b, 0);
|
|
780
|
+
const shares = [];
|
|
781
|
+
const now = new Date().toISOString();
|
|
782
|
+
for (const [agentId, w] of weightByAgent) {
|
|
783
|
+
const share = (total * w) / totalWeight;
|
|
784
|
+
const bal = _getBalance(agentId);
|
|
785
|
+
bal.balance += share;
|
|
786
|
+
db.prepare(
|
|
787
|
+
`INSERT OR REPLACE INTO economy_balances (agent_id, balance, locked, updated_at)
|
|
788
|
+
VALUES (?, ?, ?, ?)`,
|
|
789
|
+
).run(agentId, bal.balance, bal.locked, now);
|
|
790
|
+
shares.push({ agentId, share, weight: w, newBalance: bal.balance });
|
|
791
|
+
}
|
|
792
|
+
const record = {
|
|
793
|
+
id: crypto.randomUUID(),
|
|
794
|
+
taskId,
|
|
795
|
+
total,
|
|
796
|
+
shares,
|
|
797
|
+
distributedAt: now,
|
|
798
|
+
};
|
|
799
|
+
_v2Distributions.push(record);
|
|
800
|
+
return record;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export function listDistributions({ taskId } = {}) {
|
|
804
|
+
if (taskId) return _v2Distributions.filter((d) => d.taskId === taskId);
|
|
805
|
+
return [..._v2Distributions];
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Extended V2 stats.
|
|
810
|
+
*/
|
|
811
|
+
export function getEconomyStatsV2() {
|
|
812
|
+
const channelsByStatus = {};
|
|
813
|
+
for (const c of _channels.values()) {
|
|
814
|
+
const st = c.status || "unknown";
|
|
815
|
+
channelsByStatus[st] = (channelsByStatus[st] || 0) + 1;
|
|
816
|
+
}
|
|
817
|
+
const nftByStatus = {};
|
|
818
|
+
for (const n of _v2NftStatus.values()) {
|
|
819
|
+
const st = n.status || "unknown";
|
|
820
|
+
nftByStatus[st] = (nftByStatus[st] || 0) + 1;
|
|
821
|
+
}
|
|
822
|
+
const resourcesByType = {};
|
|
823
|
+
for (const l of _market.values()) {
|
|
824
|
+
resourcesByType[l.resourceType] =
|
|
825
|
+
(resourcesByType[l.resourceType] || 0) + 1;
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
totalAccounts: _balances.size,
|
|
829
|
+
totalChannels: _channels.size,
|
|
830
|
+
channelsByStatus,
|
|
831
|
+
totalListings: _market.size,
|
|
832
|
+
resourcesByType,
|
|
833
|
+
totalNFTs: _nfts.size,
|
|
834
|
+
nftByStatus,
|
|
835
|
+
priceModels: _v2PriceModels.size,
|
|
836
|
+
distributions: _v2Distributions.length,
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Reset hook augmentation — called by _resetState indirectly (callers should
|
|
841
|
+
// explicitly call _resetV2State in tests, since _resetState is unchanged for
|
|
842
|
+
// strictly-additive preservation).
|
|
843
|
+
export function _resetV2State() {
|
|
844
|
+
_v2PriceModels.clear();
|
|
845
|
+
_v2NftStatus.clear();
|
|
846
|
+
_v2TaskContributions.clear();
|
|
847
|
+
_v2Distributions.length = 0;
|
|
848
|
+
}
|