@t2000/engine 0.54.1 → 0.55.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/README.md +29 -4
- package/dist/index.d.ts +238 -16
- package/dist/index.js +464 -130
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -532,12 +532,219 @@ function resetDefiCacheStore() {
|
|
|
532
532
|
activeStore = new InMemoryDefiCacheStore();
|
|
533
533
|
}
|
|
534
534
|
|
|
535
|
+
// src/wallet-cache.ts
|
|
536
|
+
var InMemoryWalletCacheStore = class {
|
|
537
|
+
store = /* @__PURE__ */ new Map();
|
|
538
|
+
async get(address) {
|
|
539
|
+
const slot = this.store.get(address.toLowerCase());
|
|
540
|
+
if (!slot) return null;
|
|
541
|
+
if (Date.now() >= slot.expiresAt) {
|
|
542
|
+
this.store.delete(address.toLowerCase());
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
return slot.entry;
|
|
546
|
+
}
|
|
547
|
+
async set(address, entry, ttlSec) {
|
|
548
|
+
this.store.set(address.toLowerCase(), {
|
|
549
|
+
entry,
|
|
550
|
+
expiresAt: Date.now() + ttlSec * 1e3
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
async delete(address) {
|
|
554
|
+
this.store.delete(address.toLowerCase());
|
|
555
|
+
}
|
|
556
|
+
async clear() {
|
|
557
|
+
this.store.clear();
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
var activeStore2 = new InMemoryWalletCacheStore();
|
|
561
|
+
function setWalletCacheStore(store) {
|
|
562
|
+
activeStore2 = store;
|
|
563
|
+
}
|
|
564
|
+
function getWalletCacheStore() {
|
|
565
|
+
return activeStore2;
|
|
566
|
+
}
|
|
567
|
+
function resetWalletCacheStore() {
|
|
568
|
+
activeStore2 = new InMemoryWalletCacheStore();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// src/cross-instance-lock.ts
|
|
572
|
+
var InMemoryFetchLock = class {
|
|
573
|
+
held = /* @__PURE__ */ new Map();
|
|
574
|
+
async acquire(key, leaseSec) {
|
|
575
|
+
const now = Date.now();
|
|
576
|
+
const expiry = this.held.get(key);
|
|
577
|
+
if (expiry !== void 0 && expiry > now) return false;
|
|
578
|
+
this.held.set(key, now + leaseSec * 1e3);
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
async release(key) {
|
|
582
|
+
this.held.delete(key);
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
var activeLock = new InMemoryFetchLock();
|
|
586
|
+
function setFetchLock(lock) {
|
|
587
|
+
activeLock = lock;
|
|
588
|
+
}
|
|
589
|
+
function getFetchLock() {
|
|
590
|
+
return activeLock;
|
|
591
|
+
}
|
|
592
|
+
function resetFetchLock() {
|
|
593
|
+
activeLock = new InMemoryFetchLock();
|
|
594
|
+
}
|
|
595
|
+
var DEFAULT_LEASE_SEC = 15;
|
|
596
|
+
var DEFAULT_POLL_BUDGET_MS = 4500;
|
|
597
|
+
var DEFAULT_POLL_INTERVAL_MS = 100;
|
|
598
|
+
async function awaitOrFetch(key, fetcher, opts = {}) {
|
|
599
|
+
const lock = opts.lock ?? getFetchLock();
|
|
600
|
+
const leaseSec = opts.leaseSec ?? DEFAULT_LEASE_SEC;
|
|
601
|
+
const pollBudgetMs = opts.pollBudgetMs ?? DEFAULT_POLL_BUDGET_MS;
|
|
602
|
+
const pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
603
|
+
const rng = opts.rng ?? Math.random;
|
|
604
|
+
const now = opts.now ?? Date.now;
|
|
605
|
+
const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve, reject) => {
|
|
606
|
+
const timer = setTimeout(resolve, ms);
|
|
607
|
+
if (opts.signal) {
|
|
608
|
+
const onAbort = () => {
|
|
609
|
+
clearTimeout(timer);
|
|
610
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
611
|
+
};
|
|
612
|
+
if (opts.signal.aborted) onAbort();
|
|
613
|
+
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
614
|
+
}
|
|
615
|
+
}));
|
|
616
|
+
let acquired = false;
|
|
617
|
+
try {
|
|
618
|
+
acquired = await lock.acquire(key, leaseSec);
|
|
619
|
+
} catch (err) {
|
|
620
|
+
console.warn(`[fetch-lock] acquire(${key}) threw; falling through to direct fetch:`, err);
|
|
621
|
+
return fetcher();
|
|
622
|
+
}
|
|
623
|
+
if (acquired) {
|
|
624
|
+
try {
|
|
625
|
+
return await fetcher();
|
|
626
|
+
} finally {
|
|
627
|
+
try {
|
|
628
|
+
await lock.release(key);
|
|
629
|
+
} catch (err) {
|
|
630
|
+
console.warn(`[fetch-lock] release(${key}) failed (non-fatal):`, err);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (!opts.pollCache) {
|
|
635
|
+
return fetcher();
|
|
636
|
+
}
|
|
637
|
+
const deadline = now() + pollBudgetMs;
|
|
638
|
+
while (now() < deadline) {
|
|
639
|
+
const jitterPx = (rng() * 0.4 - 0.2) * pollIntervalMs;
|
|
640
|
+
const wait = Math.max(0, pollIntervalMs + jitterPx);
|
|
641
|
+
try {
|
|
642
|
+
await sleep2(wait);
|
|
643
|
+
} catch (err) {
|
|
644
|
+
if (err?.name === "AbortError") throw err;
|
|
645
|
+
return fetcher();
|
|
646
|
+
}
|
|
647
|
+
let cached = null;
|
|
648
|
+
try {
|
|
649
|
+
cached = await opts.pollCache();
|
|
650
|
+
} catch (err) {
|
|
651
|
+
console.warn(`[fetch-lock] pollCache(${key}) threw; continuing to poll:`, err);
|
|
652
|
+
}
|
|
653
|
+
if (cached !== null) return cached;
|
|
654
|
+
}
|
|
655
|
+
return fetcher();
|
|
656
|
+
}
|
|
657
|
+
|
|
535
658
|
// src/blockvision-prices.ts
|
|
536
659
|
var BLOCKVISION_BASE = "https://api.blockvision.org/v2/sui";
|
|
537
660
|
var PORTFOLIO_TIMEOUT_MS = 4e3;
|
|
538
661
|
var PRICES_TIMEOUT_MS = 3e3;
|
|
539
662
|
var CACHE_TTL_MS = 6e4;
|
|
540
|
-
var
|
|
663
|
+
var WALLET_FRESH_TTL_MS_BLOCKVISION = 6e4;
|
|
664
|
+
var WALLET_FRESH_TTL_MS_DEGRADED = 15e3;
|
|
665
|
+
var WALLET_STICKY_TTL_SEC = 30 * 60;
|
|
666
|
+
var WALLET_LOCK_KEY = (address) => `bv-lock:wallet:${address.toLowerCase()}`;
|
|
667
|
+
var DEFI_LOCK_KEY = (address) => `bv-lock:defi:${address.toLowerCase()}`;
|
|
668
|
+
var BV_RETRY_MAX_ATTEMPTS = 3;
|
|
669
|
+
var BV_RETRY_BASE_DELAY_MS = 250;
|
|
670
|
+
var BV_RETRY_BACKOFF_FACTOR = 3;
|
|
671
|
+
var BV_RETRY_JITTER = 0.25;
|
|
672
|
+
var BV_RETRY_AFTER_CAP_MS = 5e3;
|
|
673
|
+
var CB_WINDOW_MS = 5e3;
|
|
674
|
+
var CB_THRESHOLD = 10;
|
|
675
|
+
var CB_COOLDOWN_MS = 3e4;
|
|
676
|
+
var cb429Timestamps = [];
|
|
677
|
+
var cbOpenUntil = 0;
|
|
678
|
+
function cbIsOpen(now) {
|
|
679
|
+
return now < cbOpenUntil;
|
|
680
|
+
}
|
|
681
|
+
function cbRecord429(now) {
|
|
682
|
+
cb429Timestamps.push(now);
|
|
683
|
+
cb429Timestamps = cb429Timestamps.filter((t) => now - t < CB_WINDOW_MS);
|
|
684
|
+
if (cb429Timestamps.length >= CB_THRESHOLD && !cbIsOpen(now)) {
|
|
685
|
+
cbOpenUntil = now + CB_COOLDOWN_MS;
|
|
686
|
+
console.warn(
|
|
687
|
+
`[blockvision] circuit breaker OPEN \u2014 ${CB_THRESHOLD} 429s in ${CB_WINDOW_MS}ms, retries disabled for ${CB_COOLDOWN_MS / 1e3}s`
|
|
688
|
+
);
|
|
689
|
+
cb429Timestamps = [];
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
async function fetchBlockVisionWithRetry(url, init, opts = {}) {
|
|
693
|
+
const rng = opts.rng ?? Math.random;
|
|
694
|
+
const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve, reject) => {
|
|
695
|
+
const timer = setTimeout(resolve, ms);
|
|
696
|
+
if (opts.signal) {
|
|
697
|
+
const onAbort = () => {
|
|
698
|
+
clearTimeout(timer);
|
|
699
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
700
|
+
};
|
|
701
|
+
if (opts.signal.aborted) onAbort();
|
|
702
|
+
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
703
|
+
}
|
|
704
|
+
}));
|
|
705
|
+
let lastError = null;
|
|
706
|
+
let lastResponse = null;
|
|
707
|
+
for (let attempt = 0; attempt < BV_RETRY_MAX_ATTEMPTS; attempt++) {
|
|
708
|
+
if (attempt > 0) {
|
|
709
|
+
let waitMs = BV_RETRY_BASE_DELAY_MS * Math.pow(BV_RETRY_BACKOFF_FACTOR, attempt - 1);
|
|
710
|
+
const retryAfter = lastResponse?.headers.get("retry-after");
|
|
711
|
+
if (retryAfter) {
|
|
712
|
+
const secs = Number(retryAfter);
|
|
713
|
+
if (Number.isFinite(secs) && secs > 0) {
|
|
714
|
+
waitMs = Math.min(secs * 1e3, BV_RETRY_AFTER_CAP_MS);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
const jitterPx = (rng() * 2 - 1) * BV_RETRY_JITTER * waitMs;
|
|
718
|
+
const delay = Math.max(0, waitMs + jitterPx);
|
|
719
|
+
try {
|
|
720
|
+
await sleep2(delay);
|
|
721
|
+
} catch (err) {
|
|
722
|
+
if (lastResponse) return lastResponse;
|
|
723
|
+
throw err;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
try {
|
|
727
|
+
lastResponse = await fetch(url, init);
|
|
728
|
+
} catch (err) {
|
|
729
|
+
lastError = err;
|
|
730
|
+
if (err?.name === "AbortError") throw err;
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
if (lastResponse.ok) return lastResponse;
|
|
734
|
+
if (lastResponse.status !== 429 && lastResponse.status < 500) {
|
|
735
|
+
return lastResponse;
|
|
736
|
+
}
|
|
737
|
+
if (lastResponse.status === 429) {
|
|
738
|
+
const now = (opts.now ?? Date.now)();
|
|
739
|
+
cbRecord429(now);
|
|
740
|
+
if (cbIsOpen(now)) {
|
|
741
|
+
return lastResponse;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (lastResponse) return lastResponse;
|
|
746
|
+
throw lastError ?? new Error("fetch failed after retries");
|
|
747
|
+
}
|
|
541
748
|
var PRICE_LIST_CHUNK = 10;
|
|
542
749
|
var STABLE_USD_PRICES = {
|
|
543
750
|
"0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC": 1,
|
|
@@ -547,47 +754,117 @@ var STABLE_USD_PRICES = {
|
|
|
547
754
|
"0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf::coin::COIN": 1,
|
|
548
755
|
"0xc060006111016b8a020ad5b33834984a437aaa7d3c74c18e09a95d48aceab08c::coin::COIN": 1
|
|
549
756
|
};
|
|
550
|
-
var portfolioCache = /* @__PURE__ */ new Map();
|
|
551
757
|
var portfolioInflight = /* @__PURE__ */ new Map();
|
|
552
758
|
var priceMapCache = null;
|
|
759
|
+
function walletFreshTtlMs(source) {
|
|
760
|
+
switch (source) {
|
|
761
|
+
case "blockvision":
|
|
762
|
+
return WALLET_FRESH_TTL_MS_BLOCKVISION;
|
|
763
|
+
case "sui-rpc-degraded":
|
|
764
|
+
return WALLET_FRESH_TTL_MS_DEGRADED;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
async function safeWalletStoreSet(store, address, entry, ttlSec) {
|
|
768
|
+
try {
|
|
769
|
+
await store.set(address, entry, ttlSec);
|
|
770
|
+
} catch (err) {
|
|
771
|
+
console.warn("[wallet] cache set failed (non-fatal):", err);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
async function safeWalletStoreGet(store, address) {
|
|
775
|
+
try {
|
|
776
|
+
return await store.get(address);
|
|
777
|
+
} catch (err) {
|
|
778
|
+
console.warn("[wallet] cache get failed (continuing as cache miss):", err);
|
|
779
|
+
return null;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
553
782
|
async function fetchAddressPortfolio(address, apiKey, fallbackRpcUrl) {
|
|
554
|
-
const
|
|
555
|
-
const
|
|
556
|
-
if (
|
|
557
|
-
|
|
783
|
+
const store = getWalletCacheStore();
|
|
784
|
+
const cachedEntry = await safeWalletStoreGet(store, address);
|
|
785
|
+
if (cachedEntry) {
|
|
786
|
+
const ageMs = Date.now() - cachedEntry.pricedAt;
|
|
787
|
+
if (ageMs < walletFreshTtlMs(cachedEntry.data.source)) {
|
|
788
|
+
return cachedEntry.data;
|
|
789
|
+
}
|
|
558
790
|
}
|
|
559
|
-
|
|
560
|
-
if (
|
|
561
|
-
|
|
791
|
+
const existing = portfolioInflight.get(address);
|
|
792
|
+
if (existing) return existing;
|
|
793
|
+
const promise = (async () => {
|
|
562
794
|
try {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
795
|
+
return await awaitOrFetch(
|
|
796
|
+
WALLET_LOCK_KEY(address),
|
|
797
|
+
// ----------------------------------------------------------
|
|
798
|
+
// Leader path — runs after we've won the cross-instance lock.
|
|
799
|
+
// Re-checks the cache (small window where another leader on a
|
|
800
|
+
// different process just wrote) before paying for the BV call.
|
|
801
|
+
// ----------------------------------------------------------
|
|
802
|
+
async () => {
|
|
803
|
+
const recheck = await safeWalletStoreGet(store, address);
|
|
804
|
+
if (recheck) {
|
|
805
|
+
const ageMs = Date.now() - recheck.pricedAt;
|
|
806
|
+
if (ageMs < walletFreshTtlMs(recheck.data.source)) {
|
|
807
|
+
return recheck.data;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
if (apiKey && apiKey.trim().length > 0) {
|
|
811
|
+
const blockvision = await fetchPortfolioFromBlockVision(address, apiKey);
|
|
812
|
+
if (blockvision) {
|
|
813
|
+
await safeWalletStoreSet(
|
|
814
|
+
store,
|
|
815
|
+
address,
|
|
816
|
+
{ data: blockvision, pricedAt: Date.now() },
|
|
817
|
+
WALLET_STICKY_TTL_SEC
|
|
818
|
+
);
|
|
819
|
+
return blockvision;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
const degraded = await fetchPortfolioFromSuiRpc(address, apiKey, fallbackRpcUrl);
|
|
823
|
+
const stickyCandidate = recheck && recheck.data.source === "blockvision" && recheck.data.totalUsd > 0 ? recheck : cachedEntry && cachedEntry.data.source === "blockvision" && cachedEntry.data.totalUsd > 0 ? cachedEntry : null;
|
|
824
|
+
const stickyFresh = stickyCandidate && Date.now() - stickyCandidate.pricedAt < WALLET_STICKY_TTL_SEC * 1e3;
|
|
825
|
+
if (stickyFresh) {
|
|
826
|
+
return stickyCandidate.data;
|
|
827
|
+
}
|
|
828
|
+
await safeWalletStoreSet(
|
|
829
|
+
store,
|
|
830
|
+
address,
|
|
831
|
+
{ data: degraded, pricedAt: Date.now() },
|
|
832
|
+
Math.ceil(WALLET_FRESH_TTL_MS_DEGRADED / 1e3)
|
|
833
|
+
);
|
|
834
|
+
return degraded;
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
// Followers poll the wallet cache while the leader fetches.
|
|
838
|
+
// Returns non-null only when the leader has written a
|
|
839
|
+
// fresh-for-source entry — stale entries keep the poll going.
|
|
840
|
+
pollCache: async () => {
|
|
841
|
+
const e = await safeWalletStoreGet(store, address);
|
|
842
|
+
if (!e) return null;
|
|
843
|
+
const ageMs = Date.now() - e.pricedAt;
|
|
844
|
+
return ageMs < walletFreshTtlMs(e.data.source) ? e.data : null;
|
|
845
|
+
}
|
|
568
846
|
}
|
|
569
|
-
|
|
570
|
-
const degraded = await fetchPortfolioFromSuiRpc(address, apiKey, fallbackRpcUrl);
|
|
571
|
-
portfolioCache.set(address, {
|
|
572
|
-
data: degraded,
|
|
573
|
-
ts: Date.now() - (CACHE_TTL_MS - DEGRADED_CACHE_TTL_MS)
|
|
574
|
-
});
|
|
575
|
-
return degraded;
|
|
847
|
+
);
|
|
576
848
|
} finally {
|
|
577
849
|
portfolioInflight.delete(address);
|
|
578
850
|
}
|
|
579
851
|
})();
|
|
580
|
-
portfolioInflight.set(address,
|
|
581
|
-
return
|
|
852
|
+
portfolioInflight.set(address, promise);
|
|
853
|
+
return promise;
|
|
582
854
|
}
|
|
583
855
|
async function fetchPortfolioFromBlockVision(address, apiKey) {
|
|
584
856
|
const url = `${BLOCKVISION_BASE}/account/coins?account=${encodeURIComponent(address)}`;
|
|
857
|
+
const signal = AbortSignal.timeout(PORTFOLIO_TIMEOUT_MS);
|
|
585
858
|
let res;
|
|
586
859
|
try {
|
|
587
|
-
res = await
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
860
|
+
res = await fetchBlockVisionWithRetry(
|
|
861
|
+
url,
|
|
862
|
+
{
|
|
863
|
+
headers: { "x-api-key": apiKey, accept: "application/json" },
|
|
864
|
+
signal
|
|
865
|
+
},
|
|
866
|
+
{ signal }
|
|
867
|
+
);
|
|
591
868
|
} catch (err) {
|
|
592
869
|
console.warn("[blockvision-prices] portfolio fetch threw, degrading:", err);
|
|
593
870
|
return null;
|
|
@@ -718,12 +995,17 @@ async function fetchPricesFromBlockVision(coinTypes, apiKey) {
|
|
|
718
995
|
const chunk = longForms.slice(i, i + PRICE_LIST_CHUNK);
|
|
719
996
|
const tokenIds = encodeURIComponent(chunk.join(","));
|
|
720
997
|
const url = `${BLOCKVISION_BASE}/coin/price/list?tokenIds=${tokenIds}&show24hChange=true`;
|
|
998
|
+
const signal = AbortSignal.timeout(PRICES_TIMEOUT_MS);
|
|
721
999
|
let res;
|
|
722
1000
|
try {
|
|
723
|
-
res = await
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
1001
|
+
res = await fetchBlockVisionWithRetry(
|
|
1002
|
+
url,
|
|
1003
|
+
{
|
|
1004
|
+
headers: { "x-api-key": apiKey, accept: "application/json" },
|
|
1005
|
+
signal
|
|
1006
|
+
},
|
|
1007
|
+
{ signal }
|
|
1008
|
+
);
|
|
727
1009
|
} catch (err) {
|
|
728
1010
|
console.warn("[blockvision-prices] price chunk threw, skipping:", err);
|
|
729
1011
|
continue;
|
|
@@ -799,6 +1081,14 @@ async function safeStoreSet(store, address, entry) {
|
|
|
799
1081
|
console.warn("[defi] cache set failed (non-fatal):", err);
|
|
800
1082
|
}
|
|
801
1083
|
}
|
|
1084
|
+
async function safeDefiStoreGet(store, address) {
|
|
1085
|
+
try {
|
|
1086
|
+
return await store.get(address);
|
|
1087
|
+
} catch (err) {
|
|
1088
|
+
console.warn("[defi] cache get failed (continuing as cache miss):", err);
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
802
1092
|
var warnedMissingApiKey = false;
|
|
803
1093
|
async function fetchAddressDefiPortfolio(address, apiKey, priceHints = {}) {
|
|
804
1094
|
if (!apiKey || apiKey.trim().length === 0) {
|
|
@@ -811,15 +1101,9 @@ async function fetchAddressDefiPortfolio(address, apiKey, priceHints = {}) {
|
|
|
811
1101
|
return { totalUsd: 0, perProtocol: {}, pricedAt: Date.now(), source: "degraded" };
|
|
812
1102
|
}
|
|
813
1103
|
const store = getDefiCacheStore();
|
|
814
|
-
const
|
|
815
|
-
let cachedEntry = null;
|
|
816
|
-
try {
|
|
817
|
-
cachedEntry = await store.get(address);
|
|
818
|
-
} catch (err) {
|
|
819
|
-
console.warn("[defi] cache get failed (continuing as cache miss):", err);
|
|
820
|
-
}
|
|
1104
|
+
const cachedEntry = await safeDefiStoreGet(store, address);
|
|
821
1105
|
if (cachedEntry) {
|
|
822
|
-
const ageMs = now - cachedEntry.pricedAt;
|
|
1106
|
+
const ageMs = Date.now() - cachedEntry.pricedAt;
|
|
823
1107
|
const freshTtlMs = freshTtlForSource(cachedEntry.data.source);
|
|
824
1108
|
if (ageMs < freshTtlMs) {
|
|
825
1109
|
return cachedEntry.data;
|
|
@@ -829,82 +1113,108 @@ async function fetchAddressDefiPortfolio(address, apiKey, priceHints = {}) {
|
|
|
829
1113
|
if (inflight) return inflight;
|
|
830
1114
|
inflight = (async () => {
|
|
831
1115
|
try {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1116
|
+
return await awaitOrFetch(
|
|
1117
|
+
DEFI_LOCK_KEY(address),
|
|
1118
|
+
// Leader path — runs after acquiring the lock.
|
|
1119
|
+
async () => {
|
|
1120
|
+
const recheck = await safeDefiStoreGet(store, address);
|
|
1121
|
+
if (recheck) {
|
|
1122
|
+
const ageMs = Date.now() - recheck.pricedAt;
|
|
1123
|
+
if (ageMs < freshTtlForSource(recheck.data.source)) {
|
|
1124
|
+
return recheck.data;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
const stickyBasis = recheck ?? cachedEntry;
|
|
1128
|
+
const fanoutAt = Date.now();
|
|
1129
|
+
const settled = await Promise.allSettled(
|
|
1130
|
+
DEFI_PROTOCOLS.map((p) => fetchOneDefiProtocol(address, p, apiKey))
|
|
1131
|
+
);
|
|
1132
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1133
|
+
for (const s of settled) {
|
|
1134
|
+
if (s.status === "fulfilled" && s.value) collectCoinTypes(s.value, seen);
|
|
1135
|
+
}
|
|
1136
|
+
const normalizedHints = {};
|
|
1137
|
+
for (const [k, v] of Object.entries(priceHints)) {
|
|
1138
|
+
normalizedHints[normalizeCoinType(k)] = v;
|
|
1139
|
+
}
|
|
1140
|
+
const missing = Array.from(seen).filter((ct) => {
|
|
1141
|
+
const norm = normalizeCoinType(ct);
|
|
1142
|
+
return !normalizedHints[norm] && !STABLE_USD_PRICES[norm];
|
|
1143
|
+
});
|
|
1144
|
+
let fetchedPrices = {};
|
|
1145
|
+
if (missing.length > 0) {
|
|
1146
|
+
try {
|
|
1147
|
+
fetchedPrices = await fetchTokenPrices(missing, apiKey);
|
|
1148
|
+
} catch (err) {
|
|
1149
|
+
console.warn("[defi] fill-missing-prices failed:", err);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
const prices = { ...normalizedHints };
|
|
1153
|
+
for (const [ct, v] of Object.entries(fetchedPrices)) {
|
|
1154
|
+
prices[normalizeCoinType(ct)] ??= v.price;
|
|
1155
|
+
}
|
|
1156
|
+
for (const [ct, p] of Object.entries(STABLE_USD_PRICES)) {
|
|
1157
|
+
prices[normalizeCoinType(ct)] ??= p;
|
|
1158
|
+
}
|
|
1159
|
+
let totalUsd = 0;
|
|
1160
|
+
let failures = 0;
|
|
1161
|
+
const perProtocol = {};
|
|
1162
|
+
for (let i = 0; i < DEFI_PROTOCOLS.length; i++) {
|
|
1163
|
+
const proto = DEFI_PROTOCOLS[i];
|
|
1164
|
+
const s = settled[i];
|
|
1165
|
+
if (s.status !== "fulfilled" || !s.value) {
|
|
1166
|
+
failures++;
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
try {
|
|
1170
|
+
const usd = normalizeProtocol(proto, s.value, prices);
|
|
1171
|
+
if (Number.isFinite(usd) && usd !== 0) {
|
|
1172
|
+
perProtocol[proto] = usd;
|
|
1173
|
+
totalUsd += usd;
|
|
1174
|
+
}
|
|
1175
|
+
} catch (err) {
|
|
1176
|
+
console.warn(`[defi] ${proto} normaliser threw:`, err);
|
|
1177
|
+
failures++;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
if (totalUsd < 0) totalUsd = 0;
|
|
1181
|
+
const fetchedAt = Date.now();
|
|
1182
|
+
const summary = {
|
|
1183
|
+
totalUsd,
|
|
1184
|
+
perProtocol,
|
|
1185
|
+
pricedAt: fetchedAt,
|
|
1186
|
+
source: failures === DEFI_PROTOCOLS.length ? "degraded" : failures > 0 ? "partial" : "blockvision"
|
|
1187
|
+
};
|
|
1188
|
+
const cachedPositive = stickyBasis && stickyBasis.data.totalUsd > 0 && fanoutAt - stickyBasis.pricedAt < DEFI_STICKY_TTL_SEC * 1e3;
|
|
1189
|
+
if (summary.source === "blockvision") {
|
|
1190
|
+
await safeStoreSet(store, address, { data: summary, pricedAt: fetchedAt });
|
|
1191
|
+
return summary;
|
|
1192
|
+
}
|
|
1193
|
+
if (summary.source === "partial" && summary.totalUsd > 0) {
|
|
1194
|
+
await safeStoreSet(store, address, { data: summary, pricedAt: fetchedAt });
|
|
1195
|
+
return summary;
|
|
1196
|
+
}
|
|
1197
|
+
if (cachedPositive) {
|
|
1198
|
+
const stale = {
|
|
1199
|
+
...stickyBasis.data,
|
|
1200
|
+
source: "partial-stale"
|
|
1201
|
+
};
|
|
1202
|
+
return stale;
|
|
1203
|
+
}
|
|
1204
|
+
return summary;
|
|
1205
|
+
},
|
|
1206
|
+
{
|
|
1207
|
+
// Followers poll the DeFi cache while the leader fans out.
|
|
1208
|
+
// Returns non-null only when the leader has written a
|
|
1209
|
+
// fresh-for-source entry — stale entries keep the poll going.
|
|
1210
|
+
pollCache: async () => {
|
|
1211
|
+
const e = await safeDefiStoreGet(store, address);
|
|
1212
|
+
if (!e) return null;
|
|
1213
|
+
const ageMs = Date.now() - e.pricedAt;
|
|
1214
|
+
return ageMs < freshTtlForSource(e.data.source) ? e.data : null;
|
|
877
1215
|
}
|
|
878
|
-
} catch (err) {
|
|
879
|
-
console.warn(`[defi] ${proto} normaliser threw:`, err);
|
|
880
|
-
failures++;
|
|
881
1216
|
}
|
|
882
|
-
|
|
883
|
-
if (totalUsd < 0) totalUsd = 0;
|
|
884
|
-
const fetchedAt = Date.now();
|
|
885
|
-
const summary = {
|
|
886
|
-
totalUsd,
|
|
887
|
-
perProtocol,
|
|
888
|
-
pricedAt: fetchedAt,
|
|
889
|
-
source: failures === DEFI_PROTOCOLS.length ? "degraded" : failures > 0 ? "partial" : "blockvision"
|
|
890
|
-
};
|
|
891
|
-
const cachedPositive = cachedEntry && cachedEntry.data.totalUsd > 0 && now - cachedEntry.pricedAt < DEFI_STICKY_TTL_SEC * 1e3;
|
|
892
|
-
if (summary.source === "blockvision") {
|
|
893
|
-
await safeStoreSet(store, address, { data: summary, pricedAt: fetchedAt });
|
|
894
|
-
return summary;
|
|
895
|
-
}
|
|
896
|
-
if (summary.source === "partial" && summary.totalUsd > 0) {
|
|
897
|
-
await safeStoreSet(store, address, { data: summary, pricedAt: fetchedAt });
|
|
898
|
-
return summary;
|
|
899
|
-
}
|
|
900
|
-
if (cachedPositive) {
|
|
901
|
-
const stale = {
|
|
902
|
-
...cachedEntry.data,
|
|
903
|
-
source: "partial-stale"
|
|
904
|
-
};
|
|
905
|
-
return stale;
|
|
906
|
-
}
|
|
907
|
-
return summary;
|
|
1217
|
+
);
|
|
908
1218
|
} finally {
|
|
909
1219
|
defiInflight.delete(address);
|
|
910
1220
|
}
|
|
@@ -914,12 +1224,17 @@ async function fetchAddressDefiPortfolio(address, apiKey, priceHints = {}) {
|
|
|
914
1224
|
}
|
|
915
1225
|
async function fetchOneDefiProtocol(address, protocol, apiKey) {
|
|
916
1226
|
const url = `${BLOCKVISION_BASE}/account/defiPortfolio?address=${encodeURIComponent(address)}&protocol=${protocol}`;
|
|
1227
|
+
const signal = AbortSignal.timeout(DEFI_PORTFOLIO_TIMEOUT_MS);
|
|
917
1228
|
let res;
|
|
918
1229
|
try {
|
|
919
|
-
res = await
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1230
|
+
res = await fetchBlockVisionWithRetry(
|
|
1231
|
+
url,
|
|
1232
|
+
{
|
|
1233
|
+
headers: { "x-api-key": apiKey, accept: "application/json" },
|
|
1234
|
+
signal
|
|
1235
|
+
},
|
|
1236
|
+
{ signal }
|
|
1237
|
+
);
|
|
923
1238
|
} catch (err) {
|
|
924
1239
|
console.warn(`[defi] ${protocol} fetch threw:`, err);
|
|
925
1240
|
return null;
|
|
@@ -1185,12 +1500,12 @@ function normalizeProtocol(protocol, result, prices) {
|
|
|
1185
1500
|
if (bespoke) return bespoke(result, prices);
|
|
1186
1501
|
return walkProtocolResponse(result, prices);
|
|
1187
1502
|
}
|
|
1188
|
-
function clearPortfolioCache() {
|
|
1189
|
-
|
|
1503
|
+
async function clearPortfolioCache() {
|
|
1504
|
+
await getWalletCacheStore().clear();
|
|
1190
1505
|
portfolioInflight.clear();
|
|
1191
1506
|
}
|
|
1192
|
-
function clearPortfolioCacheFor(address) {
|
|
1193
|
-
|
|
1507
|
+
async function clearPortfolioCacheFor(address) {
|
|
1508
|
+
await getWalletCacheStore().delete(address);
|
|
1194
1509
|
portfolioInflight.delete(address);
|
|
1195
1510
|
}
|
|
1196
1511
|
function clearPriceMapCache() {
|
|
@@ -3329,12 +3644,31 @@ var portfolioAnalysisTool = buildTool({
|
|
|
3329
3644
|
`${apiUrl}/api/analytics/portfolio-history?days=7`,
|
|
3330
3645
|
{ headers: { "x-sui-address": address }, signal: context.signal }
|
|
3331
3646
|
).then((res) => res.ok ? res.json() : null).catch(() => null) : Promise.resolve(null),
|
|
3332
|
-
// DeFi fetch — prefer the audric snapshot's already-computed
|
|
3333
|
-
//
|
|
3334
|
-
//
|
|
3335
|
-
//
|
|
3336
|
-
//
|
|
3337
|
-
|
|
3647
|
+
// DeFi fetch — prefer the audric snapshot's already-computed
|
|
3648
|
+
// value, but only when we can trust it. Two trust signals:
|
|
3649
|
+
// 1. `source === 'blockvision'` — fully successful fresh read
|
|
3650
|
+
// (even if value is 0, that's a confirmed empty position).
|
|
3651
|
+
// 2. `defiValueUsd > 0` — any positive value, regardless
|
|
3652
|
+
// of source. `partial-stale` with a positive total is fine,
|
|
3653
|
+
// `partial` with a positive total is the live equivalent.
|
|
3654
|
+
//
|
|
3655
|
+
// [Bug — 2026-04-28 round 2] Pre-fix the trust gate was
|
|
3656
|
+
// `defiSource !== 'degraded'`, which let `partial + 0` through
|
|
3657
|
+
// as authoritative. During a BlockVision 429 burst the audric
|
|
3658
|
+
// host's `/api/portfolio` returns `partial + 0` (some protocols
|
|
3659
|
+
// failed, the rest reported $0, no sticky-positive available
|
|
3660
|
+
// *in that process*) — but the engine's direct fetcher in the
|
|
3661
|
+
// chat route may have a sticky-positive in *this* Vercel
|
|
3662
|
+
// instance's cache. Trusting audric's $0 silently dropped the
|
|
3663
|
+
// DeFi line that `balance_check` (which always calls direct)
|
|
3664
|
+
// showed correctly on the same turn — same SSOT-divergence bug
|
|
3665
|
+
// class, manifested in a different layer.
|
|
3666
|
+
//
|
|
3667
|
+
// The new condition routes around audric's $0 in exactly that
|
|
3668
|
+
// case. When the direct fetch ALSO returns $0 the answer is
|
|
3669
|
+
// consistent across tools (both report degraded), which is the
|
|
3670
|
+
// honest UX during a real outage.
|
|
3671
|
+
audricSnapshot && (audricSnapshot.defiSource === "blockvision" || audricSnapshot.defiValueUsd > 0) ? Promise.resolve({
|
|
3338
3672
|
totalUsd: audricSnapshot.defiValueUsd,
|
|
3339
3673
|
perProtocol: {},
|
|
3340
3674
|
pricedAt: Date.now(),
|
|
@@ -4418,7 +4752,7 @@ function getModifiableFields(toolName) {
|
|
|
4418
4752
|
}
|
|
4419
4753
|
|
|
4420
4754
|
// src/prompt.ts
|
|
4421
|
-
var DEFAULT_SYSTEM_PROMPT = `You are Audric \u2014 a financial agent on Sui. Audric is exactly five products: Audric Passport (the trust layer \u2014 Google sign-in, non-custodial wallet, tap-to-confirm consent, sponsored gas \u2014 wraps every other product), Audric Intelligence (you \u2014 the 5-system brain: Agent Harness with 34 tools, Reasoning Engine with
|
|
4755
|
+
var DEFAULT_SYSTEM_PROMPT = `You are Audric \u2014 a financial agent on Sui. Audric is exactly five products: Audric Passport (the trust layer \u2014 Google sign-in, non-custodial wallet, tap-to-confirm consent, sponsored gas \u2014 wraps every other product), Audric Intelligence (you \u2014 the 5-system brain: Agent Harness with 34 tools, Reasoning Engine with 14 guards and 6 skill recipes, Silent Profile, Chain Memory, AdviceLog), Audric Finance (manage money on Sui \u2014 Save via NAVI lending at 3-8% APY USDC, Credit via NAVI borrowing with health factor, Swap via Cetus aggregator across 20+ DEXs at 0.1% fee, Charts for yield/health/portfolio viz), Audric Pay (move money \u2014 send USDC, receive via payment links / invoices / QR; free, global, instant on Sui), and Audric Store (creator marketplace, ships Phase 5 \u2014 say "coming soon" if asked). Save, swap, borrow, repay, withdraw, charts \u2192 Audric Finance. Send, receive, payment-link, invoice, QR \u2192 Audric Pay. Your silent context (profile, memory, chain facts, advice log) shapes your replies but never surfaces as a notification \u2014 you act only when the user asks, and every write waits on their tap-to-confirm via Passport. You can also call 41 paid APIs (music, image, research, translation, weather, fulfilment) via MPP micropayments using the pay_api tool \u2014 this is an internal capability, not a promoted product, so only mention it when the user asks for something that needs it.
|
|
4422
4756
|
|
|
4423
4757
|
## Response rules
|
|
4424
4758
|
- 1-2 sentences max. No bullet lists unless asked. No preambles.
|
|
@@ -5852,7 +6186,7 @@ var QueryEngine = class {
|
|
|
5852
6186
|
};
|
|
5853
6187
|
if (this.walletAddress) {
|
|
5854
6188
|
this.portfolioCache?.delete(this.walletAddress);
|
|
5855
|
-
clearPortfolioCacheFor(this.walletAddress);
|
|
6189
|
+
await clearPortfolioCacheFor(this.walletAddress);
|
|
5856
6190
|
}
|
|
5857
6191
|
if (!signal.aborted) {
|
|
5858
6192
|
await new Promise((resolve) => {
|
|
@@ -7752,6 +8086,6 @@ function sanitizeAnthropicMessages(messages) {
|
|
|
7752
8086
|
return merged;
|
|
7753
8087
|
}
|
|
7754
8088
|
|
|
7755
|
-
export { AnthropicProvider, BalanceTracker, CANVAS_TEMPLATES, ContextBudget, CostTracker, DEFAULT_GUARD_CONFIG, DEFAULT_PERMISSION_CONFIG, DEFAULT_SYSTEM_PROMPT, EarlyToolDispatcher, InMemoryDefiCacheStore, McpClientManager, McpResponseCache, MemorySessionStore, NAVI_MCP_CONFIG, NAVI_MCP_URL, NAVI_SERVER_NAME, NaviTools, PERMISSION_PRESETS, QueryEngine, READ_TOOLS, RecipeRegistry, RetryTracker, TOOL_FLAGS, TOOL_MODIFIABLE_FIELDS, TxMutex, WRITE_TOOLS, activitySummaryTool, adaptAllMcpTools, adaptAllServerTools, adaptMcpTool, applyToolFlags, balanceCheckTool, borrowTool, budgetToolResult, buildCachedSystemPrompt, buildMcpTools, buildProactivenessInstructions, buildProfileContext, buildSelfEvaluationInstruction, buildStateContext, buildTool, claimRewardsTool, classifyEffort, clearPortfolioCache, clearPortfolioCacheFor, clearPriceMapCache, compactMessages, createGuardRunnerState, engineToSSE, estimateTokens, explainTxTool, extractConversationText, extractMcpText, fetchAddressDefiPortfolio, fetchAddressPortfolio, fetchAudricHistory, fetchAudricPortfolio, fetchAvailableRewards, fetchBalance, fetchHealthFactor, fetchPositions, fetchProtocolStats, fetchRates, fetchSavings, fetchTokenPrices, fetchWalletCoins, findTool, getAudricApiBase, getDefaultTools, getDefiCacheStore, getMcpManager, getModifiableFields, getToolFlags, getWalletAddress, guardArtifactPreview, guardStaleData, hasNaviMcp, healthCheckTool, loadRecipes, microcompact, mppServicesTool, parseMcpJson, parseRecipe, parseSSE, payApiTool, portfolioAnalysisTool, protocolDeepDiveTool, ratesInfoTool, registerEngineTools, renderCanvasTool, repayDebtTool, requireAgent, resetDefiCacheStore, resolvePermissionTier, resolveUsdValue, runGuards, runTools, saveContactTool, saveDepositTool, savingsInfoTool, sendTransferTool, serializeSSE, setDefiCacheStore, spendingAnalyticsTool, swapExecuteTool, swapQuoteTool, tokenPricesTool, toolNameToOperation, toolsToDefinitions, transactionHistoryTool, transformBalance, transformHealthFactor, transformPositions, transformRates, transformRewards, transformSavings, updateGuardStateAfterToolResult, validateHistory, voloStakeTool, voloStatsTool, voloUnstakeTool, webSearchTool, withdrawTool, yieldSummaryTool };
|
|
8089
|
+
export { AnthropicProvider, BalanceTracker, CANVAS_TEMPLATES, ContextBudget, CostTracker, DEFAULT_GUARD_CONFIG, DEFAULT_LEASE_SEC, DEFAULT_PERMISSION_CONFIG, DEFAULT_POLL_BUDGET_MS, DEFAULT_POLL_INTERVAL_MS, DEFAULT_SYSTEM_PROMPT, EarlyToolDispatcher, InMemoryDefiCacheStore, InMemoryFetchLock, InMemoryWalletCacheStore, McpClientManager, McpResponseCache, MemorySessionStore, NAVI_MCP_CONFIG, NAVI_MCP_URL, NAVI_SERVER_NAME, NaviTools, PERMISSION_PRESETS, QueryEngine, READ_TOOLS, RecipeRegistry, RetryTracker, TOOL_FLAGS, TOOL_MODIFIABLE_FIELDS, TxMutex, WRITE_TOOLS, activitySummaryTool, adaptAllMcpTools, adaptAllServerTools, adaptMcpTool, applyToolFlags, awaitOrFetch, balanceCheckTool, borrowTool, budgetToolResult, buildCachedSystemPrompt, buildMcpTools, buildProactivenessInstructions, buildProfileContext, buildSelfEvaluationInstruction, buildStateContext, buildTool, claimRewardsTool, classifyEffort, clearPortfolioCache, clearPortfolioCacheFor, clearPriceMapCache, compactMessages, createGuardRunnerState, engineToSSE, estimateTokens, explainTxTool, extractConversationText, extractMcpText, fetchAddressDefiPortfolio, fetchAddressPortfolio, fetchAudricHistory, fetchAudricPortfolio, fetchAvailableRewards, fetchBalance, fetchHealthFactor, fetchPositions, fetchProtocolStats, fetchRates, fetchSavings, fetchTokenPrices, fetchWalletCoins, findTool, getAudricApiBase, getDefaultTools, getDefiCacheStore, getFetchLock, getMcpManager, getModifiableFields, getToolFlags, getWalletAddress, getWalletCacheStore, guardArtifactPreview, guardStaleData, hasNaviMcp, healthCheckTool, loadRecipes, microcompact, mppServicesTool, parseMcpJson, parseRecipe, parseSSE, payApiTool, portfolioAnalysisTool, protocolDeepDiveTool, ratesInfoTool, registerEngineTools, renderCanvasTool, repayDebtTool, requireAgent, resetDefiCacheStore, resetFetchLock, resetWalletCacheStore, resolvePermissionTier, resolveUsdValue, runGuards, runTools, saveContactTool, saveDepositTool, savingsInfoTool, sendTransferTool, serializeSSE, setDefiCacheStore, setFetchLock, setWalletCacheStore, spendingAnalyticsTool, swapExecuteTool, swapQuoteTool, tokenPricesTool, toolNameToOperation, toolsToDefinitions, transactionHistoryTool, transformBalance, transformHealthFactor, transformPositions, transformRates, transformRewards, transformSavings, updateGuardStateAfterToolResult, validateHistory, voloStakeTool, voloStatsTool, voloUnstakeTool, webSearchTool, withdrawTool, yieldSummaryTool };
|
|
7756
8090
|
//# sourceMappingURL=index.js.map
|
|
7757
8091
|
//# sourceMappingURL=index.js.map
|