@zktx.io/sui-mcp 0.5.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/LICENSE +21 -0
- package/LICENSES/@mysten-bcs-2.0.5-Apache-2.0.txt +205 -0
- package/LICENSES/@mysten-sui-2.17.0-Apache-2.0.txt +201 -0
- package/LICENSES/@mysten-utils-0.3.3-Apache-2.0.txt +205 -0
- package/LICENSES/fancy-canvas-2.1.0-MIT.txt +7 -0
- package/LICENSES/lightweight-charts-5.2.0-NOTICE.txt +2 -0
- package/README.md +288 -0
- package/dist/adapters/adapterLifecycleValidators.js +5 -0
- package/dist/adapters/adapterPromptSurfaces.js +60 -0
- package/dist/adapters/deepbook/deepbookHumanReviewProducer.js +178 -0
- package/dist/adapters/deepbook/deepbookQuotePolicy.js +112 -0
- package/dist/adapters/deepbook/deepbookReviewEvidence.js +507 -0
- package/dist/adapters/deepbook/deepbookReviewLifecycle.js +85 -0
- package/dist/adapters/deepbook/deepbookSwapIntent.js +79 -0
- package/dist/adapters/deepbook/deepbookTransactionMaterialProducer.js +254 -0
- package/dist/adapters/intentPlanFactories.js +51 -0
- package/dist/adapters/reviewAdapters.js +47 -0
- package/dist/core/action/adapterLifecycleValidation.js +12 -0
- package/dist/core/action/contractNameRegistry.js +151 -0
- package/dist/core/action/forbiddenFields.js +43 -0
- package/dist/core/action/humanReadableReviewEvidence.js +193 -0
- package/dist/core/action/humanReadableReviewProjectionVerifier.js +29 -0
- package/dist/core/action/ptbVisualizationProducer.js +71 -0
- package/dist/core/action/receiptPtbGraph.js +29 -0
- package/dist/core/action/reviewCheckResults.js +6 -0
- package/dist/core/action/reviewStateValidation.js +11 -0
- package/dist/core/action/reviewTimeSimulationEvidence.js +470 -0
- package/dist/core/action/schemas.js +491 -0
- package/dist/core/action/signableAdapterContract.js +998 -0
- package/dist/core/action/suiChainReceiptEvidence.js +106 -0
- package/dist/core/action/suiChainReceiptMapping.js +107 -0
- package/dist/core/action/suiChainReceiptReader.js +211 -0
- package/dist/core/action/suiChainReceiptVerifier.js +178 -0
- package/dist/core/action/swapHumanReadableReviewProjection.js +130 -0
- package/dist/core/action/swapQuotePolicyEvidence.js +271 -0
- package/dist/core/action/transactionFunding.js +244 -0
- package/dist/core/action/transactionObjectOwnershipEvidence.js +256 -0
- package/dist/core/action/transactionObjectOwnershipProducer.js +323 -0
- package/dist/core/action/types.js +33 -0
- package/dist/core/action/walletReviewContractAssembler.js +286 -0
- package/dist/core/activity/accountAssetTimeline.js +157 -0
- package/dist/core/activity/accountAssetTimelineUsdcReferences.js +120 -0
- package/dist/core/activity/activityStore.js +16 -0
- package/dist/core/activity/externalActivityCoverage.js +76 -0
- package/dist/core/activity/externalActivityTransactionStream.js +82 -0
- package/dist/core/activity/localDataService.js +296 -0
- package/dist/core/activity/localDataTypes.js +11 -0
- package/dist/core/activity/localDataValidation.js +439 -0
- package/dist/core/activity/schemaVersion.js +2 -0
- package/dist/core/activity/sqliteActivityStore.js +1068 -0
- package/dist/core/activity/sqliteActivityStoreRows.js +389 -0
- package/dist/core/activity/sqliteActivityStoreSchema.js +367 -0
- package/dist/core/activity/sqliteActivityStoreTypes.js +5 -0
- package/dist/core/activity/suiFunctionTarget.js +43 -0
- package/dist/core/activity/transactionActivityAccountEffects.js +186 -0
- package/dist/core/activity/transactionActivityAnalysis.js +295 -0
- package/dist/core/activity/transactionActivityClassifier.js +306 -0
- package/dist/core/activity/transactionActivityDetails.js +229 -0
- package/dist/core/activity/transactionActivityProtocolRules.js +218 -0
- package/dist/core/activity/transactionActivityScanPolicy.js +170 -0
- package/dist/core/activity/transactionActivityService.js +379 -0
- package/dist/core/activity/transactionActivityTypes.js +18 -0
- package/dist/core/eventlog/sink.js +35 -0
- package/dist/core/evidence/settlementFamilies.js +85 -0
- package/dist/core/evidence/userAnswerUse.js +1 -0
- package/dist/core/numeric/rawU64.js +63 -0
- package/dist/core/preferences/preferencesStore.js +26 -0
- package/dist/core/preferences/sqlitePreferencesRepository.js +136 -0
- package/dist/core/proposal/externalProposalReview.js +347 -0
- package/dist/core/proposal/schemas.js +208 -0
- package/dist/core/proposal/types.js +35 -0
- package/dist/core/read/amounts.js +14 -0
- package/dist/core/read/coinMetadata.js +60 -0
- package/dist/core/read/deepbookOfficialIndexerSource.js +373 -0
- package/dist/core/read/deepbookRawQuoteClient.js +86 -0
- package/dist/core/read/deepbookReadHelpers.js +321 -0
- package/dist/core/read/deepbookRegistry.js +157 -0
- package/dist/core/read/deepbookSourceOwners.js +159 -0
- package/dist/core/read/deepbookUsdcChartService.js +342 -0
- package/dist/core/read/intentEvidenceResponseFormatting.js +228 -0
- package/dist/core/read/readCardInputs.js +19 -0
- package/dist/core/read/readCardResult.js +40 -0
- package/dist/core/read/readResponseGuidance.js +402 -0
- package/dist/core/read/readService.js +1639 -0
- package/dist/core/read/readServiceTypes.js +88 -0
- package/dist/core/read/settlementParityFormatting.js +82 -0
- package/dist/core/read/walletReadHelpers.js +99 -0
- package/dist/core/review/reviewChecks.js +45 -0
- package/dist/core/review/reviewComputation.js +38 -0
- package/dist/core/review/reviewComputationResult.js +87 -0
- package/dist/core/session/cardSession.js +31 -0
- package/dist/core/session/cardSessionStore.js +204 -0
- package/dist/core/session/keyedRecordStore.js +18 -0
- package/dist/core/session/liveReviewSessionContract.js +3 -0
- package/dist/core/session/localSession.js +31 -0
- package/dist/core/session/privateReviewArtifacts.js +75 -0
- package/dist/core/session/reviewValidity.js +49 -0
- package/dist/core/session/sessionErrors.js +9 -0
- package/dist/core/session/sessionRecordStore.js +40 -0
- package/dist/core/session/sessionStore.js +655 -0
- package/dist/core/session/settingsSession.js +1 -0
- package/dist/core/session/settingsSessions.js +45 -0
- package/dist/core/session/sqliteCardStore.js +88 -0
- package/dist/core/session/sqliteSessionStore.js +213 -0
- package/dist/core/session/sqliteTransactionMaterialStore.js +64 -0
- package/dist/core/session/sqliteWalletWorkflowStore.js +821 -0
- package/dist/core/session/status.js +60 -0
- package/dist/core/session/transactionMaterialStore.js +221 -0
- package/dist/core/session/transactionRequest.js +72 -0
- package/dist/core/session/wait.js +79 -0
- package/dist/core/session/walletConnection.js +92 -0
- package/dist/core/session/walletRuntime.js +38 -0
- package/dist/core/session/walletWorkflow.js +608 -0
- package/dist/core/session/workflowState.js +80 -0
- package/dist/core/session/workflowView.js +150 -0
- package/dist/core/suiAddress.js +18 -0
- package/dist/core/suiEndpoint.js +72 -0
- package/dist/mcp/activeAccountResponse.js +24 -0
- package/dist/mcp/prompts.js +146 -0
- package/dist/mcp/registerTool.js +19 -0
- package/dist/mcp/resources.js +72 -0
- package/dist/mcp/responseGuidance.js +326 -0
- package/dist/mcp/result.js +17 -0
- package/dist/mcp/schemas.js +11 -0
- package/dist/mcp/server.js +32 -0
- package/dist/mcp/serverInfo.js +131 -0
- package/dist/mcp/toolErrors.js +118 -0
- package/dist/mcp/toolNames.js +58 -0
- package/dist/mcp/tools/account/index.js +44 -0
- package/dist/mcp/tools/action/prepareSuiActionReview.js +126 -0
- package/dist/mcp/tools/read/commonSchemas.js +43 -0
- package/dist/mcp/tools/read/deepbookReadTools.js +686 -0
- package/dist/mcp/tools/read/index.js +14 -0
- package/dist/mcp/tools/read/readToolHelpers.js +97 -0
- package/dist/mcp/tools/read/reviewActivityTools.js +179 -0
- package/dist/mcp/tools/read/serverStatusTools.js +100 -0
- package/dist/mcp/tools/read/transactionActivityOutput.js +371 -0
- package/dist/mcp/tools/read/transactionActivityTools.js +858 -0
- package/dist/mcp/tools/read/walletReadTools.js +734 -0
- package/dist/mcp/tools/session/executionResultTools.js +44 -0
- package/dist/mcp/tools/session/index.js +8 -0
- package/dist/mcp/tools/session/shared.js +36 -0
- package/dist/mcp/tools/session/statusTools.js +69 -0
- package/dist/mcp/tools/session/walletConnectionTools.js +69 -0
- package/dist/mcp/tools/settings/index.js +66 -0
- package/dist/mcp-app/account.html +3133 -0
- package/dist/mcp-app/account.notices.txt +973 -0
- package/dist/mcp-app/chart.html +3354 -0
- package/dist/mcp-app/chart.notices.txt +1194 -0
- package/dist/mcp-app/connect.html +3179 -0
- package/dist/mcp-app/connect.notices.txt +1012 -0
- package/dist/mcp-app/receipt.html +8430 -0
- package/dist/mcp-app/receipt.notices.txt +3549 -0
- package/dist/mcp-app/review.html +8430 -0
- package/dist/mcp-app/review.notices.txt +3549 -0
- package/dist/mcp-ui/contracts.js +31 -0
- package/dist/mcp-ui/readCards.js +28 -0
- package/dist/mcp-ui/tools.js +134 -0
- package/dist/review-app/brand-dark.svg +1 -0
- package/dist/review-app/brand-light.svg +1 -0
- package/dist/review-app/favicon.svg +1 -0
- package/dist/review-app/settings.css +1 -0
- package/dist/review-app/settings.js +1 -0
- package/dist/review-app/ui.css +809 -0
- package/dist/review-server/assets.js +48 -0
- package/dist/review-server/html.js +45 -0
- package/dist/review-server/http.js +47 -0
- package/dist/review-server/middleware/hostOrigin.js +48 -0
- package/dist/review-server/middleware/reviewToken.js +7 -0
- package/dist/review-server/reviewServerPolicy.js +1 -0
- package/dist/review-server/server.js +72 -0
- package/dist/review-server/settingsApi.js +158 -0
- package/dist/runtime/application.js +203 -0
- package/dist/runtime/config.js +97 -0
- package/dist/runtime/localSettingsService.js +198 -0
- package/dist/runtime/logger.js +66 -0
- package/dist/runtime/reviewDependencies.js +58 -0
- package/dist/runtime/reviewServerAcquire.js +93 -0
- package/dist/runtime/runtimeTermination.js +60 -0
- package/dist/runtime/shared/authenticatedFetch.js +127 -0
- package/dist/runtime/shared/control.js +93 -0
- package/dist/runtime/shared/dataAccess.js +66 -0
- package/dist/runtime/shared/mcpHttp.js +84 -0
- package/dist/runtime/shared/ownerLease.js +50 -0
- package/dist/runtime/shared/server.js +98 -0
- package/dist/runtime/shared/stdio.js +113 -0
- package/dist/runtime/smokeMainnetRead.js +391 -0
- package/dist/runtime/smokeMainnetReadAssertions.js +306 -0
- package/dist/runtime/smokeReview.js +40 -0
- package/dist/runtime/start.js +90 -0
- package/dist/runtime/suiEndpoint.js +97 -0
- package/dist/runtime/suiTransactionGraphqlMapping.js +200 -0
- package/dist/runtime/suiTransactionGraphqlQueries.js +231 -0
- package/dist/runtime/suiTransactionGraphqlSource.js +148 -0
- package/dist/runtime/walletConnectConfig.js +3 -0
- package/dist/runtime/walletConnectStorage.js +99 -0
- package/dist/runtime/walletConnectTransport.js +128 -0
- package/dist/runtime/walletSdkChild.js +3 -0
- package/dist/runtime/walletSdkChildRuntime.js +160 -0
- package/dist/runtime/walletSdkIpc.js +26 -0
- package/dist/runtime/walletSdkProcess.js +358 -0
- package/docs/AGENT_BEHAVIOR.md +524 -0
- package/docs/AGENT_DEVELOPMENT_POLICY.md +815 -0
- package/docs/FRONTEND_POLICY.md +534 -0
- package/docs/LOCAL_DB_ARCHITECTURE.md +381 -0
- package/docs/MCP_SETUP.md +742 -0
- package/docs/MCP_TOOLS.md +1318 -0
- package/docs/SDK_API.md +206 -0
- package/docs/SIGNABLE_ADAPTER_CONTRACT.md +396 -0
- package/docs/TRANSACTION_ACTIVITY_LOG.md +194 -0
- package/docs/UTILITY_INDEX.md +275 -0
- package/docs/WALLET_CONNECTION.md +238 -0
- package/docs/golden-scenarios/BEHAVIOR_MATRIX.md +166 -0
- package/docs/golden-scenarios/INTENT_EVIDENCE_GOLDEN_ANSWERS.md +161 -0
- package/docs/golden-scenarios/INTENT_EVIDENCE_MATRIX.md +259 -0
- package/package.json +79 -0
- package/protocols/deepbook-margin.md +15 -0
- package/protocols/deepbook-v3.md +24 -0
|
@@ -0,0 +1,1639 @@
|
|
|
1
|
+
import { COIN_METADATA_CACHE_TTL_MS, DEEPBOOK_SCALAR_UNIT_SOURCE, assertValidDecimals, decimalsFromScalar, parseDisplayAmountToRaw, normalizeCoinType } from "./coinMetadata.js";
|
|
2
|
+
import { createDeepBookReadClient } from "./deepbookRawQuoteClient.js";
|
|
3
|
+
import { DEFAULT_DEEPBOOK_OFFICIAL_INDEXER_INTERVAL, DeepbookOfficialIndexerSourceError, deepbookOfficialIndexerIntervalDurationMs, parseDeepbookOfficialIndexerInterval, selectDeepbookOfficialIndexerCanonicalUsdcPools } from "./deepbookOfficialIndexerSource.js";
|
|
4
|
+
import { DEEPBOOK_OFFICIAL_INDEXER_SOURCE_BASE, DEEPBOOK_SDK_SIMULATION_SOURCE_BASE, DEEPBOOK_SOURCE_FIELD_VALUES } from "./deepbookSourceOwners.js";
|
|
5
|
+
import { deepbookUnitForCoinType, canonicalDeepbookSymbol, getDeepbookCoinEntryBySymbol, getKnownPool, invalidDeepbookScalar, listDeepbookTokenRegistry, PINNED_DEEPBOOK_COINS, resolveDeepbookPoolForSymbols } from "./deepbookRegistry.js";
|
|
6
|
+
import { sumRawAmounts } from "./amounts.js";
|
|
7
|
+
import { buildUsdSettlementAssetGroup, commonAssetGroupDecimals, formatSettlementAssetRawAmount, normalizeSettlementDenomination } from "../evidence/settlementFamilies.js";
|
|
8
|
+
import { assertDeepbookDisplayBalances, assertPositiveInteger, assertValidDeepbookMidPrice, assertValidDeepbookQuote, deepbookAccountInventorySource, deepbookDisplayQuantitySemantics, deepbookMidPriceSemantics, deepbookUsdcPriceHistoryPairFromOfficialPool, deepbookUsdcPriceHistoryQuantitySemantics, deepbookUsdcPriceHistoryResponseSummary, deepbookQuoteQuantitySemantics, normalizeManagerAddresses, normalizeOptionalManagerAddress, parseQuoteDisplayAmount, parseRawAmount, toDeepbookAccountSummary, toDeepbookDisplayQuoteFromRaw } from "./deepbookReadHelpers.js";
|
|
9
|
+
import { classifyWalletBalance, unavailableUnit, unitFromDeepbook, unitFromMetadataRecord, walletBalanceQuantitySemantics, withResolvedUnit, withUnavailableUnit } from "./walletReadHelpers.js";
|
|
10
|
+
import { deepbookAccountInventoryUserAnswerUse, deepbookMidPriceUserAnswerUse, deepbookOrderbookUserAnswerUse, deepbookUsdcPriceAtTimeUserAnswerUse, deepbookUsdcPriceHistoryUserAnswerUse, deepbookQuoteUserAnswerUse, intentEvidenceUserAnswerUse, settlementAssetGroupParityUserAnswerUse, walletBalanceUserAnswerUse, walletClassificationUserAnswerUse } from "./readResponseGuidance.js";
|
|
11
|
+
import { intentEvidenceQuantitySemantics, intentEvidenceResponseEvidence, intentEvidenceResponseSummary, intentEvidenceSettlementAssetCoverageBoundary, intentEvidenceSupportedClaims, isIntentEvidenceTargetAssetSelectionSource, isSupportedIntentEvidenceKind } from "./intentEvidenceResponseFormatting.js";
|
|
12
|
+
import { roundPriceInReferenceAsset, settlementAssetGroupParityQuantitySemantics, settlementAssetGroupParityResponseSummary, settlementAssetGroupParityStatistics } from "./settlementParityFormatting.js";
|
|
13
|
+
import { DEEPBOOK_MID_PRICE_DIRECTION, DEEPBOOK_MID_PRICE_PRECISION, DEEPBOOK_MID_PRICE_TYPE, DEEPBOOK_RAW_QUOTE_QUANTITY_KIND, DEEPBOOK_USDC_PRICE_HISTORY_UNSUPPORTED_CLAIMS, DEFAULT_DEEPBOOK_SIMULATION_SENDER, INTENT_EVIDENCE_TARGET_ASSET_SELECTION_SOURCES, MAX_DEEPBOOK_ACCOUNT_OPEN_ORDER_IDS, MAX_DEEPBOOK_USDC_PRICE_HISTORY_BARS, MAX_DEEPBOOK_ORDERBOOK_TICKS, MAX_WALLET_BALANCE_SCAN_PAGES, NOT_INSPECTED_ASSET_CLASSES, ReadServiceCacheError, ReadServiceInputError } from "./readServiceTypes.js";
|
|
14
|
+
export * from "./readServiceTypes.js";
|
|
15
|
+
export { listDeepbookTokenRegistry } from "./deepbookRegistry.js";
|
|
16
|
+
const DEFAULT_DEEPBOOK_USDC_PRICE_AT_TIME_MAX_DISTANCE_MINUTES = 360;
|
|
17
|
+
// Account inventory object scan: pages of listOwnedObjects to walk (50 objects per
|
|
18
|
+
// page) and the max NFTs to surface. A coin-heavy account may truncate before all
|
|
19
|
+
// objects are seen; the snapshot flags that rather than paging unbounded.
|
|
20
|
+
const ACCOUNT_OBJECT_SCAN_PAGES = 6;
|
|
21
|
+
const ACCOUNT_NFT_LIMIT = 60;
|
|
22
|
+
const DEEPBOOK_USDC_PRICE_HISTORY_RESPONSE_SUMMARY = {
|
|
23
|
+
...deepbookUsdcPriceHistoryResponseSummary()
|
|
24
|
+
};
|
|
25
|
+
function deepbookUsdcPriceHistorySelector(input) {
|
|
26
|
+
const selectors = [
|
|
27
|
+
input.poolName === undefined ? undefined : { kind: "pool_name", value: input.poolName.trim().toUpperCase() },
|
|
28
|
+
input.assetSymbol === undefined
|
|
29
|
+
? undefined
|
|
30
|
+
: { kind: "asset_symbol", value: input.assetSymbol.trim().toUpperCase() },
|
|
31
|
+
input.coinType === undefined
|
|
32
|
+
? undefined
|
|
33
|
+
: { kind: "coin_type", value: normalizedHistoryCoinType(input.coinType) }
|
|
34
|
+
].filter((selector) => selector !== undefined && selector.value.length > 0);
|
|
35
|
+
if (selectors.length !== 1) {
|
|
36
|
+
throw new ReadServiceInputError("input_invalid", "Provide exactly one DeepBook USDC history selector: poolName, assetSymbol, or coinType", {
|
|
37
|
+
fields: ["poolName", "assetSymbol", "coinType"],
|
|
38
|
+
providedSelectorCount: selectors.length
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return selectors[0];
|
|
42
|
+
}
|
|
43
|
+
function normalizedHistoryCoinType(value) {
|
|
44
|
+
try {
|
|
45
|
+
return normalizeCoinType(value);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new ReadServiceInputError("input_invalid", "coinType must be a valid Sui coin type", {
|
|
49
|
+
field: "coinType",
|
|
50
|
+
value
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function parseDeepbookUsdcPriceHistoryInterval(value) {
|
|
55
|
+
if (value === undefined) {
|
|
56
|
+
return DEFAULT_DEEPBOOK_OFFICIAL_INDEXER_INTERVAL;
|
|
57
|
+
}
|
|
58
|
+
if (typeof value !== "string") {
|
|
59
|
+
throw new ReadServiceInputError("input_invalid", "interval must be a supported DeepBook official Indexer interval", {
|
|
60
|
+
field: "interval",
|
|
61
|
+
value
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
return parseDeepbookOfficialIndexerInterval(value);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error instanceof DeepbookOfficialIndexerSourceError && error.reason === "invalid_payload") {
|
|
69
|
+
throw new ReadServiceInputError("input_invalid", "interval must be a supported DeepBook official Indexer interval", {
|
|
70
|
+
field: "interval",
|
|
71
|
+
value
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function deepbookUsdcPriceHistoryRange(start, end, interval) {
|
|
78
|
+
const startDate = parseUtcIsoHistoryDate(start, "start");
|
|
79
|
+
const endDate = parseUtcIsoHistoryDate(end, "end");
|
|
80
|
+
const durationMs = endDate.getTime() - startDate.getTime();
|
|
81
|
+
if (durationMs <= 0) {
|
|
82
|
+
throw new ReadServiceInputError("input_invalid", "end must be after start for DeepBook USDC history", {
|
|
83
|
+
start,
|
|
84
|
+
end
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const selectedInterval = parseDeepbookUsdcPriceHistoryInterval(interval);
|
|
88
|
+
const intervalDurationMs = deepbookOfficialIndexerIntervalDurationMs(selectedInterval);
|
|
89
|
+
return {
|
|
90
|
+
start: startDate.toISOString(),
|
|
91
|
+
end: endDate.toISOString(),
|
|
92
|
+
timeZone: "UTC",
|
|
93
|
+
interval: selectedInterval,
|
|
94
|
+
intervalDurationMs,
|
|
95
|
+
maxBars: MAX_DEEPBOOK_USDC_PRICE_HISTORY_BARS,
|
|
96
|
+
requestedCandleSlots: Math.ceil(durationMs / intervalDurationMs)
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function deepbookUsdcPriceAtTimeTarget(input) {
|
|
100
|
+
const target = parseUtcIsoHistoryDate(input.targetTime, "targetTime");
|
|
101
|
+
const maxDistanceMinutes = input.maxDistanceMinutes ?? DEFAULT_DEEPBOOK_USDC_PRICE_AT_TIME_MAX_DISTANCE_MINUTES;
|
|
102
|
+
if (!Number.isInteger(maxDistanceMinutes) || maxDistanceMinutes <= 0) {
|
|
103
|
+
throw new ReadServiceInputError("input_invalid", "maxDistanceMinutes must be a positive integer", {
|
|
104
|
+
field: "maxDistanceMinutes",
|
|
105
|
+
value: input.maxDistanceMinutes
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const interval = parseDeepbookUsdcPriceHistoryInterval(input.interval);
|
|
109
|
+
const intervalDurationMs = deepbookOfficialIndexerIntervalDurationMs(interval);
|
|
110
|
+
const intervalMinutes = intervalDurationMs / 60_000;
|
|
111
|
+
const maxSearchMinutes = Math.floor((MAX_DEEPBOOK_USDC_PRICE_HISTORY_BARS * intervalMinutes -
|
|
112
|
+
intervalMinutes) /
|
|
113
|
+
2);
|
|
114
|
+
if (maxDistanceMinutes > maxSearchMinutes) {
|
|
115
|
+
throw new ReadServiceInputError("input_invalid", "maxDistanceMinutes exceeds the supported DeepBook USDC search window", {
|
|
116
|
+
field: "maxDistanceMinutes",
|
|
117
|
+
maxDistanceMinutes,
|
|
118
|
+
maxSupportedMinutes: maxSearchMinutes
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const targetMs = target.getTime();
|
|
122
|
+
const start = new Date(targetMs - maxDistanceMinutes * 60 * 1000);
|
|
123
|
+
const end = new Date(targetMs + maxDistanceMinutes * 60 * 1000 + intervalDurationMs);
|
|
124
|
+
return {
|
|
125
|
+
targetTime: target.toISOString(),
|
|
126
|
+
searchWindow: {
|
|
127
|
+
start: start.toISOString(),
|
|
128
|
+
end: end.toISOString(),
|
|
129
|
+
maxDistanceMinutes
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function parseUtcIsoHistoryDate(value, field) {
|
|
134
|
+
const timestamp = Date.parse(value);
|
|
135
|
+
if (!Number.isFinite(timestamp)) {
|
|
136
|
+
throw new ReadServiceInputError("input_invalid", `${field} must be an ISO UTC timestamp`, { field, value });
|
|
137
|
+
}
|
|
138
|
+
const date = new Date(timestamp);
|
|
139
|
+
if (date.toISOString() !== value) {
|
|
140
|
+
throw new ReadServiceInputError("input_invalid", `${field} must be a canonical ISO UTC timestamp`, {
|
|
141
|
+
field,
|
|
142
|
+
value
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return date;
|
|
146
|
+
}
|
|
147
|
+
function deepbookUsdcPriceHistoryMatchingPairs(usdcPools, selector) {
|
|
148
|
+
switch (selector.kind) {
|
|
149
|
+
case "pool_name":
|
|
150
|
+
return usdcPools.filter((pool) => pool.pool_name.toUpperCase() === selector.value);
|
|
151
|
+
case "asset_symbol":
|
|
152
|
+
return usdcPools.filter((pool) => pool.base_asset_symbol.toUpperCase() === selector.value);
|
|
153
|
+
case "coin_type":
|
|
154
|
+
return usdcPools.filter((pool) => normalizeCoinType(pool.base_asset_id) === selector.value);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function deepbookUsdcPriceHistorySource(input) {
|
|
158
|
+
return {
|
|
159
|
+
kind: DEEPBOOK_OFFICIAL_INDEXER_SOURCE_BASE.kind,
|
|
160
|
+
baseUrl: input.poolSource.baseUrl,
|
|
161
|
+
sourceStatement: input.candleSource.sourceStatement,
|
|
162
|
+
poolList: {
|
|
163
|
+
url: input.poolSource.url,
|
|
164
|
+
fetchedAt: input.poolSource.fetchedAt
|
|
165
|
+
},
|
|
166
|
+
candles: {
|
|
167
|
+
url: input.candleSource.url,
|
|
168
|
+
fetchedAt: input.candleSource.fetchedAt,
|
|
169
|
+
poolName: input.candleSource.poolName ?? "",
|
|
170
|
+
interval: input.candleSource.interval ?? DEFAULT_DEEPBOOK_OFFICIAL_INDEXER_INTERVAL,
|
|
171
|
+
startTimeMs: input.candleSource.startTimeMs ?? 0,
|
|
172
|
+
endTimeMs: input.candleSource.endTimeMs ?? 0,
|
|
173
|
+
limit: input.candleSource.limit ?? MAX_DEEPBOOK_USDC_PRICE_HISTORY_BARS
|
|
174
|
+
},
|
|
175
|
+
chainRecomputedBySuiMcp: DEEPBOOK_OFFICIAL_INDEXER_SOURCE_BASE.chainRecomputedBySuiMcp
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function deepbookOfficialSourceUnavailableReason(error, phase) {
|
|
179
|
+
if (error instanceof DeepbookOfficialIndexerSourceError && error.reason === "invalid_payload") {
|
|
180
|
+
return "official_indexer_invalid_payload";
|
|
181
|
+
}
|
|
182
|
+
return phase === "pools" ? "pool_list_unavailable" : "candle_fetch_failed";
|
|
183
|
+
}
|
|
184
|
+
function deepbookUsdcPriceAtTimeMatch(input) {
|
|
185
|
+
const targetMs = Date.parse(input.target.targetTime);
|
|
186
|
+
const exact = input.bars.find((candle) => {
|
|
187
|
+
const startMs = Date.parse(candle.start);
|
|
188
|
+
const endMs = Date.parse(candle.end);
|
|
189
|
+
return targetMs >= startMs && targetMs < endMs;
|
|
190
|
+
});
|
|
191
|
+
if (exact !== undefined) {
|
|
192
|
+
return {
|
|
193
|
+
candle: exact,
|
|
194
|
+
match: deepbookUsdcPriceAtTimeMatchFromBar({
|
|
195
|
+
kind: "exact_bucket",
|
|
196
|
+
distanceMinutes: 0,
|
|
197
|
+
pair: input.pair,
|
|
198
|
+
candle: exact
|
|
199
|
+
})
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const candidates = input.bars
|
|
203
|
+
.flatMap((candle) => {
|
|
204
|
+
const startMs = Date.parse(candle.start);
|
|
205
|
+
const endMs = Date.parse(candle.end);
|
|
206
|
+
if (endMs <= targetMs) {
|
|
207
|
+
return [
|
|
208
|
+
{
|
|
209
|
+
candle,
|
|
210
|
+
kind: "nearest_before",
|
|
211
|
+
distanceMinutes: (targetMs - endMs) / 60_000
|
|
212
|
+
}
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
if (startMs > targetMs) {
|
|
216
|
+
return [
|
|
217
|
+
{
|
|
218
|
+
candle,
|
|
219
|
+
kind: "nearest_after",
|
|
220
|
+
distanceMinutes: (startMs - targetMs) / 60_000
|
|
221
|
+
}
|
|
222
|
+
];
|
|
223
|
+
}
|
|
224
|
+
return [];
|
|
225
|
+
})
|
|
226
|
+
.filter((candidate) => candidate.distanceMinutes <= input.target.searchWindow.maxDistanceMinutes)
|
|
227
|
+
.sort((left, right) => {
|
|
228
|
+
const byDistance = left.distanceMinutes - right.distanceMinutes;
|
|
229
|
+
if (byDistance !== 0) {
|
|
230
|
+
return byDistance;
|
|
231
|
+
}
|
|
232
|
+
if (left.kind !== right.kind) {
|
|
233
|
+
return left.kind === "nearest_before" ? -1 : 1;
|
|
234
|
+
}
|
|
235
|
+
return Date.parse(left.candle.start) - Date.parse(right.candle.start);
|
|
236
|
+
});
|
|
237
|
+
const best = candidates[0];
|
|
238
|
+
if (best === undefined) {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
candle: best.candle,
|
|
243
|
+
match: deepbookUsdcPriceAtTimeMatchFromBar({
|
|
244
|
+
kind: best.kind,
|
|
245
|
+
distanceMinutes: best.distanceMinutes,
|
|
246
|
+
pair: input.pair,
|
|
247
|
+
candle: best.candle
|
|
248
|
+
})
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function deepbookUsdcPriceAtTimeMatchFromBar(input) {
|
|
252
|
+
return {
|
|
253
|
+
kind: input.kind,
|
|
254
|
+
distanceMinutes: Number(input.distanceMinutes.toFixed(6)),
|
|
255
|
+
representativePrice: {
|
|
256
|
+
field: "matchedCandle.close",
|
|
257
|
+
value: input.candle.close,
|
|
258
|
+
quoteAsset: "USDC",
|
|
259
|
+
baseAssetSymbol: input.pair.baseAsset.symbol,
|
|
260
|
+
priceConvention: "USDC_PER_BASE"
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
export class SuiReadService {
|
|
265
|
+
#client;
|
|
266
|
+
#network;
|
|
267
|
+
#chainIdentifier;
|
|
268
|
+
#coinMetadataCache;
|
|
269
|
+
#now;
|
|
270
|
+
#deepbookFactory;
|
|
271
|
+
#coinMetadataTtlMs;
|
|
272
|
+
#deepbookCoins;
|
|
273
|
+
#deepbookOfficialIndexerSource;
|
|
274
|
+
constructor(options) {
|
|
275
|
+
this.#client = options.client;
|
|
276
|
+
this.#network = options.network;
|
|
277
|
+
this.#chainIdentifier = options.chainIdentifier;
|
|
278
|
+
this.#coinMetadataCache = options.coinMetadataCache;
|
|
279
|
+
this.#now = options.now ?? (() => new Date());
|
|
280
|
+
this.#coinMetadataTtlMs = options.coinMetadataTtlMs ?? COIN_METADATA_CACHE_TTL_MS;
|
|
281
|
+
this.#deepbookCoins = options.deepbookCoins ?? PINNED_DEEPBOOK_COINS;
|
|
282
|
+
this.#deepbookFactory =
|
|
283
|
+
options.deepbookFactory ??
|
|
284
|
+
((simulationSender, factoryOptions) => createDeepBookReadClient({
|
|
285
|
+
client: options.client,
|
|
286
|
+
simulationSender,
|
|
287
|
+
network: this.#network,
|
|
288
|
+
...(factoryOptions?.balanceManagers === undefined
|
|
289
|
+
? {}
|
|
290
|
+
: { balanceManagers: factoryOptions.balanceManagers })
|
|
291
|
+
}));
|
|
292
|
+
this.#deepbookOfficialIndexerSource = options.deepbookOfficialIndexerSource;
|
|
293
|
+
}
|
|
294
|
+
async summarizeWalletAssets(input) {
|
|
295
|
+
const options = { owner: input.account };
|
|
296
|
+
if (input.cursor !== undefined) {
|
|
297
|
+
options.cursor = input.cursor;
|
|
298
|
+
}
|
|
299
|
+
const result = await this.#client.core.listBalances(options);
|
|
300
|
+
const balances = [];
|
|
301
|
+
for (const balance of result.balances) {
|
|
302
|
+
balances.push(await this.#withUnit(balance));
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
status: "ok",
|
|
306
|
+
account: input.account,
|
|
307
|
+
fetchedAt: this.#fetchedAt(),
|
|
308
|
+
userAnswerUse: walletBalanceUserAnswerUse(),
|
|
309
|
+
quantitySemantics: walletBalanceQuantitySemantics(),
|
|
310
|
+
source: {
|
|
311
|
+
sdk: "@mysten/sui",
|
|
312
|
+
transport: "grpc",
|
|
313
|
+
method: "client.core.listBalances"
|
|
314
|
+
},
|
|
315
|
+
balances,
|
|
316
|
+
hasNextPage: result.hasNextPage,
|
|
317
|
+
cursor: result.cursor
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
async classifyWalletAssets(input) {
|
|
321
|
+
const summary = await this.summarizeWalletAssets(input);
|
|
322
|
+
return {
|
|
323
|
+
status: "ok",
|
|
324
|
+
account: summary.account,
|
|
325
|
+
fetchedAt: summary.fetchedAt,
|
|
326
|
+
userAnswerUse: walletClassificationUserAnswerUse(),
|
|
327
|
+
quantitySemantics: summary.quantitySemantics,
|
|
328
|
+
source: summary.source,
|
|
329
|
+
classifiedAssets: summary.balances.map((balance) => classifyWalletBalance(balance, this.#deepbookCoins)),
|
|
330
|
+
uninspectedAssetClasses: NOT_INSPECTED_ASSET_CLASSES.map((assetClass) => ({ ...assetClass })),
|
|
331
|
+
hasNextPage: summary.hasNextPage,
|
|
332
|
+
cursor: summary.cursor
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
// The public account snapshot for the account page: the SuiNS name, coin
|
|
336
|
+
// balances (with the held-as split), owned NFTs, and other owned objects grouped
|
|
337
|
+
// by type. Each part is read independently so a missing name or object scan never
|
|
338
|
+
// blocks the balances.
|
|
339
|
+
async summarizeAccountInventory(input) {
|
|
340
|
+
const [walletSummary, name, owned] = await Promise.all([
|
|
341
|
+
this.summarizeWalletAssets(input),
|
|
342
|
+
this.#resolveAccountName(input.account),
|
|
343
|
+
this.#scanOwnedObjects(input.account)
|
|
344
|
+
]);
|
|
345
|
+
return {
|
|
346
|
+
status: "ok",
|
|
347
|
+
account: input.account,
|
|
348
|
+
fetchedAt: this.#fetchedAt(),
|
|
349
|
+
name,
|
|
350
|
+
// Drop coins whose total balance is zero — an emptied coin object or
|
|
351
|
+
// accumulator residue is not a holding worth listing on the account page.
|
|
352
|
+
balances: walletSummary.balances.filter((balance) => {
|
|
353
|
+
try {
|
|
354
|
+
return BigInt(balance.balance) !== 0n;
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
}),
|
|
360
|
+
nfts: owned.nfts,
|
|
361
|
+
objectGroups: owned.objectGroups,
|
|
362
|
+
scannedObjects: owned.scanned,
|
|
363
|
+
objectsTruncated: owned.truncated
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
// The account's primary SuiNS name, or null when it has none (the lookup throws
|
|
367
|
+
// NOT_FOUND) or the call fails — a name is convenience, never gating.
|
|
368
|
+
async #resolveAccountName(account) {
|
|
369
|
+
const resolve = this.#client.core.defaultNameServiceName;
|
|
370
|
+
if (!resolve) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
const response = await resolve.call(this.#client.core, { address: account });
|
|
375
|
+
return response.data.name ?? null;
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// Enumerate owned objects (capped) and split them: Coin objects belong to the
|
|
382
|
+
// balance view, objects carrying a Display name/image are NFTs, and the rest are
|
|
383
|
+
// grouped by Move type. listOwnedObjects returns all three intermixed, so a
|
|
384
|
+
// coin-heavy account may truncate before reaching every NFT.
|
|
385
|
+
async #scanOwnedObjects(account) {
|
|
386
|
+
const nfts = [];
|
|
387
|
+
const groups = new Map();
|
|
388
|
+
const core = this.#client.core;
|
|
389
|
+
const listOwnedObjects = core.listOwnedObjects;
|
|
390
|
+
if (!listOwnedObjects) {
|
|
391
|
+
return { nfts, objectGroups: [], scanned: 0, truncated: false };
|
|
392
|
+
}
|
|
393
|
+
let cursor;
|
|
394
|
+
let scanned = 0;
|
|
395
|
+
let truncated = false;
|
|
396
|
+
for (let page = 0; page < ACCOUNT_OBJECT_SCAN_PAGES; page += 1) {
|
|
397
|
+
const response = await listOwnedObjects.call(core, {
|
|
398
|
+
owner: account,
|
|
399
|
+
include: { display: true },
|
|
400
|
+
...(cursor === undefined ? {} : { cursor })
|
|
401
|
+
});
|
|
402
|
+
for (const object of response.objects ?? []) {
|
|
403
|
+
scanned += 1;
|
|
404
|
+
const type = object.type ?? "";
|
|
405
|
+
if (type.includes("::coin::Coin<")) {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
const output = object.display?.output;
|
|
409
|
+
const name = typeof output?.name === "string" ? output.name : undefined;
|
|
410
|
+
const imageUrl = typeof output?.image_url === "string" ? output.image_url : undefined;
|
|
411
|
+
if (name !== undefined || imageUrl !== undefined) {
|
|
412
|
+
if (nfts.length < ACCOUNT_NFT_LIMIT) {
|
|
413
|
+
nfts.push({ objectId: object.objectId, type, name, imageUrl });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
groups.set(type, (groups.get(type) ?? 0) + 1);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (!response.hasNextPage) {
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
cursor = response.cursor ?? undefined;
|
|
424
|
+
if (page === ACCOUNT_OBJECT_SCAN_PAGES - 1) {
|
|
425
|
+
truncated = true;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const objectGroups = [...groups.entries()]
|
|
429
|
+
.map(([type, count]) => ({ type, count }))
|
|
430
|
+
.sort((left, right) => right.count - left.count || left.type.localeCompare(right.type));
|
|
431
|
+
return { nfts, objectGroups, scanned, truncated };
|
|
432
|
+
}
|
|
433
|
+
listSettlementAssetGroups() {
|
|
434
|
+
return {
|
|
435
|
+
status: "ok",
|
|
436
|
+
fetchedAt: this.#fetchedAt(),
|
|
437
|
+
assetGroups: [buildUsdSettlementAssetGroup(this.#deepbookCoins)]
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
async summarizeSettlementAssetGroupParity(input) {
|
|
441
|
+
let denomination;
|
|
442
|
+
try {
|
|
443
|
+
denomination = normalizeSettlementDenomination(input.denomination);
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
throw new ReadServiceInputError("input_invalid", "Unsupported settlement denomination", {
|
|
447
|
+
field: "denomination",
|
|
448
|
+
value: input.denomination,
|
|
449
|
+
supportedAliases: buildUsdSettlementAssetGroup(this.#deepbookCoins).aliases
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
const assetGroup = buildUsdSettlementAssetGroup(this.#deepbookCoins);
|
|
453
|
+
if (assetGroup.includedAssets.length === 0) {
|
|
454
|
+
throw new ReadServiceInputError("registry_miss", "No pinned USD-denominated settlement assets are available", {
|
|
455
|
+
assetGroupId: assetGroup.id
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
const referenceAsset = this.#resolveSettlementAssetGroupSymbol(input.referenceAssetSymbol ?? "USDC", assetGroup, "referenceAssetSymbol");
|
|
459
|
+
const deepbook = this.#deepbookFactory(input.simulationSender);
|
|
460
|
+
const assets = await Promise.all(assetGroup.includedAssets.map((asset) => this.#settlementAssetGroupParityAsset(asset, referenceAsset, deepbook)));
|
|
461
|
+
const samples = assets.filter((asset) => asset.status === "reference_asset" || asset.status === "measured");
|
|
462
|
+
if (samples.length === 0) {
|
|
463
|
+
throw new ReadServiceInputError("registry_miss", "No parity samples are available for the settlement asset group", {
|
|
464
|
+
assetGroupId: assetGroup.id,
|
|
465
|
+
referenceAssetSymbol: referenceAsset.symbol
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
const statistics = settlementAssetGroupParityStatistics(samples, assets.length - samples.length);
|
|
469
|
+
return {
|
|
470
|
+
status: "ok",
|
|
471
|
+
fetchedAt: this.#fetchedAt(),
|
|
472
|
+
denomination,
|
|
473
|
+
assetGroupId: assetGroup.id,
|
|
474
|
+
userAnswerUse: settlementAssetGroupParityUserAnswerUse(),
|
|
475
|
+
referenceAsset: {
|
|
476
|
+
...referenceAsset,
|
|
477
|
+
role: "measurement_reference_not_settlement_choice"
|
|
478
|
+
},
|
|
479
|
+
quantitySemantics: settlementAssetGroupParityQuantitySemantics(),
|
|
480
|
+
evidenceSources: {
|
|
481
|
+
settlementAssetGroup: assetGroup.evidenceSources,
|
|
482
|
+
midPrice: {
|
|
483
|
+
...DEEPBOOK_SDK_SIMULATION_SOURCE_BASE,
|
|
484
|
+
method: "midPrice",
|
|
485
|
+
precision: DEEPBOOK_MID_PRICE_PRECISION
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
assets,
|
|
489
|
+
statistics,
|
|
490
|
+
responseSummary: settlementAssetGroupParityResponseSummary({
|
|
491
|
+
assetGroupId: assetGroup.id,
|
|
492
|
+
referenceAssetSymbol: referenceAsset.symbol,
|
|
493
|
+
statistics
|
|
494
|
+
}),
|
|
495
|
+
unsupportedClaims: [
|
|
496
|
+
"settlement_token_selection",
|
|
497
|
+
"fiat_usd_cash_out",
|
|
498
|
+
"payment_execution_readiness",
|
|
499
|
+
"route_recommendation",
|
|
500
|
+
"best_route",
|
|
501
|
+
"transaction_building",
|
|
502
|
+
"signing_readiness",
|
|
503
|
+
"profit_or_pnl",
|
|
504
|
+
"cost_basis"
|
|
505
|
+
]
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
async previewIntentEvidence(input) {
|
|
509
|
+
if (!isSupportedIntentEvidenceKind(input.intentKind)) {
|
|
510
|
+
throw new ReadServiceInputError("input_invalid", "Unsupported intentKind", {
|
|
511
|
+
field: "intentKind",
|
|
512
|
+
value: input.intentKind
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
if (input.intentKind === "cover_payment_like_amount" && input.requiredDisplayAmount === undefined) {
|
|
516
|
+
throw new ReadServiceInputError("input_invalid", "requiredDisplayAmount is required for cover_payment_like_amount", {
|
|
517
|
+
field: "requiredDisplayAmount",
|
|
518
|
+
intentKind: input.intentKind
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
if (input.targetAssetSelectionSource !== undefined &&
|
|
522
|
+
!isIntentEvidenceTargetAssetSelectionSource(input.targetAssetSelectionSource)) {
|
|
523
|
+
throw new ReadServiceInputError("input_invalid", "Unsupported targetAssetSelectionSource", {
|
|
524
|
+
field: "targetAssetSelectionSource",
|
|
525
|
+
value: input.targetAssetSelectionSource,
|
|
526
|
+
supportedValues: [...INTENT_EVIDENCE_TARGET_ASSET_SELECTION_SOURCES]
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
if (input.intentKind === "summarize_settlement_asset_group_balance") {
|
|
530
|
+
if (input.requiredDisplayAmount !== undefined) {
|
|
531
|
+
throw new ReadServiceInputError("input_invalid", "requiredDisplayAmount is only supported for cover_payment_like_amount", {
|
|
532
|
+
field: "requiredDisplayAmount",
|
|
533
|
+
intentKind: input.intentKind
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
if (input.targetAssetSymbol !== undefined) {
|
|
537
|
+
throw new ReadServiceInputError("input_invalid", "targetAssetSymbol is only supported for cover_payment_like_amount", {
|
|
538
|
+
field: "targetAssetSymbol",
|
|
539
|
+
intentKind: input.intentKind
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
if (input.targetAssetSelectionSource !== undefined) {
|
|
543
|
+
throw new ReadServiceInputError("input_invalid", "targetAssetSelectionSource is only supported for cover_payment_like_amount", {
|
|
544
|
+
field: "targetAssetSelectionSource",
|
|
545
|
+
intentKind: input.intentKind
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
if (input.acceptedSourceAssetSymbols !== undefined) {
|
|
549
|
+
throw new ReadServiceInputError("input_invalid", "acceptedSourceAssetSymbols is only supported for cover_payment_like_amount", {
|
|
550
|
+
field: "acceptedSourceAssetSymbols",
|
|
551
|
+
intentKind: input.intentKind
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (input.intentKind === "cover_payment_like_amount") {
|
|
556
|
+
if (input.targetAssetSymbol !== undefined && input.targetAssetSelectionSource === undefined) {
|
|
557
|
+
throw new ReadServiceInputError("input_invalid", "targetAssetSelectionSource is required when targetAssetSymbol is supplied", {
|
|
558
|
+
field: "targetAssetSelectionSource",
|
|
559
|
+
requiredWith: "targetAssetSymbol",
|
|
560
|
+
supportedValues: [...INTENT_EVIDENCE_TARGET_ASSET_SELECTION_SOURCES]
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
if (input.targetAssetSymbol === undefined && input.targetAssetSelectionSource !== undefined) {
|
|
564
|
+
throw new ReadServiceInputError("input_invalid", "targetAssetSymbol is required when targetAssetSelectionSource is supplied", {
|
|
565
|
+
field: "targetAssetSymbol",
|
|
566
|
+
requiredWith: "targetAssetSelectionSource"
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
let denomination;
|
|
571
|
+
try {
|
|
572
|
+
denomination = normalizeSettlementDenomination(input.denomination);
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
throw new ReadServiceInputError("input_invalid", "Unsupported settlement denomination", {
|
|
576
|
+
field: "denomination",
|
|
577
|
+
value: input.denomination,
|
|
578
|
+
supportedAliases: buildUsdSettlementAssetGroup(this.#deepbookCoins).aliases
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
const assetGroup = buildUsdSettlementAssetGroup(this.#deepbookCoins);
|
|
582
|
+
if (assetGroup.includedAssets.length === 0) {
|
|
583
|
+
throw new ReadServiceInputError("registry_miss", "No pinned USD-denominated settlement assets are available", {
|
|
584
|
+
assetGroupId: assetGroup.id
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
const targetAsset = input.intentKind !== "cover_payment_like_amount" || input.targetAssetSymbol === undefined
|
|
588
|
+
? undefined
|
|
589
|
+
: this.#resolveSettlementAssetGroupSymbol(input.targetAssetSymbol, assetGroup, "targetAssetSymbol");
|
|
590
|
+
const targetAssetSelectionSource = targetAsset === undefined ? undefined : input.targetAssetSelectionSource;
|
|
591
|
+
const acceptedSourceSymbols = input.intentKind !== "cover_payment_like_amount" || input.acceptedSourceAssetSymbols === undefined
|
|
592
|
+
? undefined
|
|
593
|
+
: input.acceptedSourceAssetSymbols.map((symbol, index) => this.#resolveSettlementAssetGroupSymbol(symbol, assetGroup, `acceptedSourceAssetSymbols[${index}]`).symbol);
|
|
594
|
+
const acceptedSourceSet = acceptedSourceSymbols === undefined ? undefined : new Set(acceptedSourceSymbols);
|
|
595
|
+
const scan = await this.#scanWalletAssetClassificationPages(input.account);
|
|
596
|
+
const balances = this.#intentEvidenceAssetGroupBalances(assetGroup, scan.classifiedAssets);
|
|
597
|
+
const commonDecimals = commonAssetGroupDecimals(assetGroup.includedAssets);
|
|
598
|
+
const aggregate = this.#intentEvidenceAggregate({
|
|
599
|
+
requiredDisplayAmount: input.requiredDisplayAmount,
|
|
600
|
+
balances,
|
|
601
|
+
commonDecimals,
|
|
602
|
+
blockedReason: scan.blockedReason
|
|
603
|
+
});
|
|
604
|
+
const settlementAssetCoverage = this.#intentEvidenceSettlementAssetCoverage(aggregate);
|
|
605
|
+
const requiredDisplayAmount = input.requiredDisplayAmount;
|
|
606
|
+
let selectedTarget;
|
|
607
|
+
if (targetAsset !== undefined) {
|
|
608
|
+
if (requiredDisplayAmount === undefined) {
|
|
609
|
+
throw new Error("target settlement evidence requires requiredDisplayAmount");
|
|
610
|
+
}
|
|
611
|
+
if (targetAssetSelectionSource === undefined) {
|
|
612
|
+
throw new Error("target settlement evidence requires user selection provenance");
|
|
613
|
+
}
|
|
614
|
+
if (scan.blockedReason === undefined) {
|
|
615
|
+
selectedTarget = this.#intentEvidenceSelectedTarget({
|
|
616
|
+
targetAsset,
|
|
617
|
+
selectionSource: targetAssetSelectionSource,
|
|
618
|
+
requiredDisplayAmount,
|
|
619
|
+
balances
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const candidateConversions = input.intentKind === "cover_payment_like_amount" && scan.blockedReason === undefined
|
|
624
|
+
? await this.#intentEvidenceCandidateConversions({
|
|
625
|
+
targetAsset,
|
|
626
|
+
balances,
|
|
627
|
+
acceptedSourceSet
|
|
628
|
+
})
|
|
629
|
+
: [];
|
|
630
|
+
const requiredUserChoices = this.#intentEvidenceRequiredUserChoices(input.intentKind, targetAsset, candidateConversions);
|
|
631
|
+
const responseEvidence = intentEvidenceResponseEvidence(targetAsset, settlementAssetCoverage, candidateConversions);
|
|
632
|
+
const responseSummary = intentEvidenceResponseSummary({
|
|
633
|
+
intentKind: input.intentKind,
|
|
634
|
+
assetGroupId: assetGroup.id,
|
|
635
|
+
settlementAssetCoverage,
|
|
636
|
+
responseEvidenceMode: responseEvidence.mode,
|
|
637
|
+
requiredUserChoices
|
|
638
|
+
});
|
|
639
|
+
const { excludedAssets: _excludedAssets, ...settlementAssetGroup } = assetGroup;
|
|
640
|
+
return {
|
|
641
|
+
status: "ok",
|
|
642
|
+
account: input.account,
|
|
643
|
+
fetchedAt: this.#fetchedAt(),
|
|
644
|
+
userAnswerUse: intentEvidenceUserAnswerUse(settlementAssetCoverage.status, responseEvidence),
|
|
645
|
+
intent: {
|
|
646
|
+
intentKind: input.intentKind,
|
|
647
|
+
denomination,
|
|
648
|
+
...(input.requiredDisplayAmount === undefined ? {} : { requiredDisplayAmount: input.requiredDisplayAmount }),
|
|
649
|
+
...(targetAsset === undefined
|
|
650
|
+
? {}
|
|
651
|
+
: { targetAssetSymbol: targetAsset.symbol, targetAssetSelectionSource }),
|
|
652
|
+
...(acceptedSourceSymbols === undefined ? {} : { acceptedSourceAssetSymbols: acceptedSourceSymbols })
|
|
653
|
+
},
|
|
654
|
+
quantitySemantics: intentEvidenceQuantitySemantics(),
|
|
655
|
+
evidenceSources: {
|
|
656
|
+
walletBalances: {
|
|
657
|
+
sdk: "@mysten/sui",
|
|
658
|
+
transport: "grpc",
|
|
659
|
+
method: "client.core.listBalances"
|
|
660
|
+
},
|
|
661
|
+
settlementAssetGroup: assetGroup.evidenceSources,
|
|
662
|
+
quoteEvidence: DEEPBOOK_SOURCE_FIELD_VALUES.pinnedSdkWhenTargetAssetSelected
|
|
663
|
+
},
|
|
664
|
+
settlementAssetGroup,
|
|
665
|
+
balances,
|
|
666
|
+
aggregate,
|
|
667
|
+
settlementAssetCoverage,
|
|
668
|
+
...(selectedTarget === undefined ? {} : { selectedTarget }),
|
|
669
|
+
candidateConversions,
|
|
670
|
+
blockedReasons: scan.blockedReason === undefined ? [] : [scan.blockedReason],
|
|
671
|
+
responseEvidence,
|
|
672
|
+
responseSummary,
|
|
673
|
+
requiredUserChoices,
|
|
674
|
+
supportedClaims: intentEvidenceSupportedClaims(settlementAssetCoverage, selectedTarget, candidateConversions),
|
|
675
|
+
unsupportedClaims: [
|
|
676
|
+
"settlement_token_selection",
|
|
677
|
+
"fiat_usd_cash_out",
|
|
678
|
+
"gas_reserve_or_fee_readiness",
|
|
679
|
+
"best_route_or_venue_comparison",
|
|
680
|
+
"route_dependent_payment_support",
|
|
681
|
+
"payment_execution_readiness",
|
|
682
|
+
"transaction_building",
|
|
683
|
+
"signing_readiness",
|
|
684
|
+
"profit_or_pnl",
|
|
685
|
+
"cost_basis"
|
|
686
|
+
],
|
|
687
|
+
uninspectedAssetClasses: scan.uninspectedAssetClasses,
|
|
688
|
+
inspectedBalancePages: scan.inspectedBalancePages,
|
|
689
|
+
inspectedCoinBalanceCount: scan.inspectedCoinBalanceCount
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
async inspectDeepbookOrderbook(input) {
|
|
693
|
+
getKnownPool(input.poolKey);
|
|
694
|
+
assertPositiveInteger(input.ticks, "ticks", MAX_DEEPBOOK_ORDERBOOK_TICKS);
|
|
695
|
+
const deepbook = this.#deepbookFactory(input.simulationSender);
|
|
696
|
+
const [midPrice, poolBookParams, level2TicksFromMid] = await Promise.all([
|
|
697
|
+
deepbook.midPrice(input.poolKey),
|
|
698
|
+
deepbook.poolBookParams(input.poolKey),
|
|
699
|
+
deepbook.getLevel2TicksFromMid(input.poolKey, input.ticks)
|
|
700
|
+
]);
|
|
701
|
+
const checkedMidPrice = assertValidDeepbookMidPrice(input.poolKey, midPrice);
|
|
702
|
+
return {
|
|
703
|
+
status: "ok",
|
|
704
|
+
poolKey: input.poolKey,
|
|
705
|
+
ticks: input.ticks,
|
|
706
|
+
fetchedAt: this.#fetchedAt(),
|
|
707
|
+
userAnswerUse: deepbookOrderbookUserAnswerUse(),
|
|
708
|
+
source: {
|
|
709
|
+
...DEEPBOOK_SDK_SIMULATION_SOURCE_BASE,
|
|
710
|
+
methods: ["midPrice", "poolBookParams", "getLevel2TicksFromMid"]
|
|
711
|
+
},
|
|
712
|
+
midPrice: checkedMidPrice,
|
|
713
|
+
poolBookParams,
|
|
714
|
+
level2TicksFromMid
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
async getDeepbookMidPrice(input) {
|
|
718
|
+
const pool = getKnownPool(input.poolKey);
|
|
719
|
+
const deepbook = this.#deepbookFactory(input.simulationSender);
|
|
720
|
+
const midPrice = assertValidDeepbookMidPrice(input.poolKey, await deepbook.midPrice(input.poolKey));
|
|
721
|
+
return {
|
|
722
|
+
status: "ok",
|
|
723
|
+
poolKey: input.poolKey,
|
|
724
|
+
base: pool.baseCoin,
|
|
725
|
+
quote: pool.quoteCoin,
|
|
726
|
+
userAnswerUse: deepbookMidPriceUserAnswerUse(),
|
|
727
|
+
priceSemantics: deepbookMidPriceSemantics(),
|
|
728
|
+
price: midPrice,
|
|
729
|
+
priceDirection: DEEPBOOK_MID_PRICE_DIRECTION,
|
|
730
|
+
priceType: DEEPBOOK_MID_PRICE_TYPE,
|
|
731
|
+
fetchedAt: this.#fetchedAt(),
|
|
732
|
+
source: {
|
|
733
|
+
...DEEPBOOK_SDK_SIMULATION_SOURCE_BASE,
|
|
734
|
+
method: "midPrice",
|
|
735
|
+
precision: DEEPBOOK_MID_PRICE_PRECISION
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
async getDeepbookUsdcPriceHistory(input) {
|
|
740
|
+
const fetchedAt = this.#fetchedAt();
|
|
741
|
+
const selector = deepbookUsdcPriceHistorySelector(input);
|
|
742
|
+
const range = deepbookUsdcPriceHistoryRange(input.start, input.end, input.interval);
|
|
743
|
+
const common = {
|
|
744
|
+
fetchedAt,
|
|
745
|
+
requested: { selector, range },
|
|
746
|
+
userAnswerUse: deepbookUsdcPriceHistoryUserAnswerUse(),
|
|
747
|
+
quantitySemantics: deepbookUsdcPriceHistoryQuantitySemantics(),
|
|
748
|
+
responseSummary: DEEPBOOK_USDC_PRICE_HISTORY_RESPONSE_SUMMARY,
|
|
749
|
+
unsupportedClaims: [...DEEPBOOK_USDC_PRICE_HISTORY_UNSUPPORTED_CLAIMS]
|
|
750
|
+
};
|
|
751
|
+
if (range.requestedCandleSlots > MAX_DEEPBOOK_USDC_PRICE_HISTORY_BARS) {
|
|
752
|
+
return {
|
|
753
|
+
status: "unsupported_range",
|
|
754
|
+
...common,
|
|
755
|
+
reason: "requested_range_exceeds_max_bars"
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
if (this.#deepbookOfficialIndexerSource === undefined) {
|
|
759
|
+
return {
|
|
760
|
+
status: "source_unavailable",
|
|
761
|
+
...common,
|
|
762
|
+
reason: "official_indexer_not_configured"
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
let poolResult;
|
|
766
|
+
try {
|
|
767
|
+
poolResult = await this.#deepbookOfficialIndexerSource.fetchPools();
|
|
768
|
+
}
|
|
769
|
+
catch (error) {
|
|
770
|
+
return {
|
|
771
|
+
status: "source_unavailable",
|
|
772
|
+
...common,
|
|
773
|
+
reason: deepbookOfficialSourceUnavailableReason(error, "pools")
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
const usdcPools = selectDeepbookOfficialIndexerCanonicalUsdcPools(poolResult.pools);
|
|
777
|
+
const matchingPools = deepbookUsdcPriceHistoryMatchingPairs(usdcPools, selector);
|
|
778
|
+
if (matchingPools.length !== 1) {
|
|
779
|
+
return {
|
|
780
|
+
status: "unsupported_pair",
|
|
781
|
+
...common,
|
|
782
|
+
reason: matchingPools.length === 0
|
|
783
|
+
? "selector_not_in_official_indexer"
|
|
784
|
+
: "selector_resolves_to_multiple_usdc_pools",
|
|
785
|
+
matchingPoolNames: matchingPools.map((pool) => pool.pool_name),
|
|
786
|
+
availablePoolNames: usdcPools.map((pool) => pool.pool_name)
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
const pool = matchingPools[0];
|
|
790
|
+
const outputPair = deepbookUsdcPriceHistoryPairFromOfficialPool(pool);
|
|
791
|
+
let candleResult;
|
|
792
|
+
try {
|
|
793
|
+
candleResult = await this.#deepbookOfficialIndexerSource.fetchCandles({
|
|
794
|
+
poolName: pool.pool_name,
|
|
795
|
+
interval: range.interval,
|
|
796
|
+
startTimeMs: Date.parse(range.start),
|
|
797
|
+
endTimeMs: Date.parse(range.end),
|
|
798
|
+
limit: MAX_DEEPBOOK_USDC_PRICE_HISTORY_BARS
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
catch (error) {
|
|
802
|
+
return {
|
|
803
|
+
status: "source_unavailable",
|
|
804
|
+
...common,
|
|
805
|
+
reason: deepbookOfficialSourceUnavailableReason(error, "candles"),
|
|
806
|
+
pair: outputPair,
|
|
807
|
+
source: {
|
|
808
|
+
kind: DEEPBOOK_OFFICIAL_INDEXER_SOURCE_BASE.kind,
|
|
809
|
+
baseUrl: poolResult.source.baseUrl,
|
|
810
|
+
sourceStatement: poolResult.source.sourceStatement,
|
|
811
|
+
poolList: {
|
|
812
|
+
url: poolResult.source.url,
|
|
813
|
+
fetchedAt: poolResult.source.fetchedAt
|
|
814
|
+
},
|
|
815
|
+
chainRecomputedBySuiMcp: DEEPBOOK_OFFICIAL_INDEXER_SOURCE_BASE.chainRecomputedBySuiMcp
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
const source = deepbookUsdcPriceHistorySource({
|
|
820
|
+
poolSource: poolResult.source,
|
|
821
|
+
candleSource: candleResult.source
|
|
822
|
+
});
|
|
823
|
+
const startMs = Date.parse(range.start);
|
|
824
|
+
const endMs = Date.parse(range.end);
|
|
825
|
+
const bars = candleResult.candles
|
|
826
|
+
.filter((candle) => {
|
|
827
|
+
const candleStart = Date.parse(candle.start);
|
|
828
|
+
return candleStart >= startMs && candleStart < endMs;
|
|
829
|
+
})
|
|
830
|
+
.sort((left, right) => Date.parse(left.start) - Date.parse(right.start));
|
|
831
|
+
const candleAvailability = bars.length === 0 ? "no_candles_in_range" : "available";
|
|
832
|
+
return {
|
|
833
|
+
status: "ok",
|
|
834
|
+
...common,
|
|
835
|
+
pair: outputPair,
|
|
836
|
+
candleAvailability,
|
|
837
|
+
barCount: bars.length,
|
|
838
|
+
bars,
|
|
839
|
+
source
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
async getDeepbookUsdcPriceAtTime(input) {
|
|
843
|
+
const target = deepbookUsdcPriceAtTimeTarget(input);
|
|
844
|
+
const history = await this.getDeepbookUsdcPriceHistory({
|
|
845
|
+
poolName: input.poolName,
|
|
846
|
+
assetSymbol: input.assetSymbol,
|
|
847
|
+
coinType: input.coinType,
|
|
848
|
+
interval: input.interval,
|
|
849
|
+
start: target.searchWindow.start,
|
|
850
|
+
end: target.searchWindow.end
|
|
851
|
+
});
|
|
852
|
+
const common = {
|
|
853
|
+
fetchedAt: history.fetchedAt,
|
|
854
|
+
target,
|
|
855
|
+
requested: history.requested,
|
|
856
|
+
quantitySemantics: history.quantitySemantics,
|
|
857
|
+
responseSummary: history.responseSummary,
|
|
858
|
+
unsupportedClaims: history.unsupportedClaims
|
|
859
|
+
};
|
|
860
|
+
if (history.status === "unsupported_pair") {
|
|
861
|
+
return {
|
|
862
|
+
status: "unsupported_pair",
|
|
863
|
+
...common,
|
|
864
|
+
reason: history.reason,
|
|
865
|
+
matchingPoolNames: history.matchingPoolNames,
|
|
866
|
+
availablePoolNames: history.availablePoolNames,
|
|
867
|
+
userAnswerUse: deepbookUsdcPriceAtTimeUserAnswerUse(false)
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
if (history.status === "unsupported_range") {
|
|
871
|
+
return {
|
|
872
|
+
status: "unsupported_range",
|
|
873
|
+
...common,
|
|
874
|
+
reason: history.reason,
|
|
875
|
+
userAnswerUse: deepbookUsdcPriceAtTimeUserAnswerUse(false)
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
if (history.status === "source_unavailable") {
|
|
879
|
+
return {
|
|
880
|
+
status: "source_unavailable",
|
|
881
|
+
...common,
|
|
882
|
+
reason: history.reason,
|
|
883
|
+
pair: history.pair,
|
|
884
|
+
source: history.source,
|
|
885
|
+
userAnswerUse: deepbookUsdcPriceAtTimeUserAnswerUse(false)
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
const matched = deepbookUsdcPriceAtTimeMatch({
|
|
889
|
+
target,
|
|
890
|
+
pair: history.pair,
|
|
891
|
+
bars: history.bars
|
|
892
|
+
});
|
|
893
|
+
if (matched === undefined) {
|
|
894
|
+
return {
|
|
895
|
+
status: "no_price_in_search_window",
|
|
896
|
+
...common,
|
|
897
|
+
pair: history.pair,
|
|
898
|
+
candleAvailability: history.candleAvailability,
|
|
899
|
+
source: history.source,
|
|
900
|
+
userAnswerUse: deepbookUsdcPriceAtTimeUserAnswerUse(false)
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
return {
|
|
904
|
+
status: "ok",
|
|
905
|
+
...common,
|
|
906
|
+
pair: history.pair,
|
|
907
|
+
match: matched.match,
|
|
908
|
+
matchedCandle: matched.candle,
|
|
909
|
+
candleAvailability: history.candleAvailability,
|
|
910
|
+
source: history.source,
|
|
911
|
+
userAnswerUse: deepbookUsdcPriceAtTimeUserAnswerUse(true)
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
async quoteDeepbookAction(input) {
|
|
915
|
+
const pool = getKnownPool(input.poolKey);
|
|
916
|
+
const amount = parseRawAmount(input.amountRaw);
|
|
917
|
+
const feeMode = input.feeMode ?? "deep";
|
|
918
|
+
const deepbook = this.#deepbookFactory(input.simulationSender);
|
|
919
|
+
if (feeMode === "input_coin" && (!deepbook.getQuoteQuantityOutInputFeeRaw || !deepbook.getBaseQuantityOutInputFeeRaw)) {
|
|
920
|
+
throw new ReadServiceInputError("quote_unavailable", "Input-fee DeepBook quoting is not supported by this read client", {
|
|
921
|
+
poolKey: input.poolKey
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
const rawReturnValues = input.direction === "base_to_quote"
|
|
925
|
+
? feeMode === "input_coin"
|
|
926
|
+
? await deepbook.getQuoteQuantityOutInputFeeRaw(input.poolKey, amount)
|
|
927
|
+
: await deepbook.getQuoteQuantityOutRaw(input.poolKey, amount)
|
|
928
|
+
: feeMode === "input_coin"
|
|
929
|
+
? await deepbook.getBaseQuantityOutInputFeeRaw(input.poolKey, amount)
|
|
930
|
+
: await deepbook.getBaseQuantityOutRaw(input.poolKey, amount);
|
|
931
|
+
const rawQuote = this.#toDeepbookRawQuoteEvidence({
|
|
932
|
+
poolKey: input.poolKey,
|
|
933
|
+
direction: input.direction,
|
|
934
|
+
amountRaw: input.amountRaw,
|
|
935
|
+
rawReturnValues,
|
|
936
|
+
feeMode
|
|
937
|
+
});
|
|
938
|
+
const quote = assertValidDeepbookQuote(input.poolKey, input.direction, toDeepbookDisplayQuoteFromRaw(rawReturnValues, this.#deepbookQuoteUnits(pool.baseCoin, pool.quoteCoin)));
|
|
939
|
+
return {
|
|
940
|
+
status: "ok",
|
|
941
|
+
poolKey: input.poolKey,
|
|
942
|
+
direction: input.direction,
|
|
943
|
+
amountRaw: input.amountRaw,
|
|
944
|
+
fetchedAt: this.#fetchedAt(),
|
|
945
|
+
userAnswerUse: deepbookQuoteUserAnswerUse("raw"),
|
|
946
|
+
quantitySemantics: deepbookQuoteQuantitySemantics("raw_u64"),
|
|
947
|
+
source: {
|
|
948
|
+
...DEEPBOOK_SDK_SIMULATION_SOURCE_BASE,
|
|
949
|
+
method: input.direction === "base_to_quote"
|
|
950
|
+
? feeMode === "input_coin"
|
|
951
|
+
? "getQuoteQuantityOutInputFee"
|
|
952
|
+
: "getQuoteQuantityOut"
|
|
953
|
+
: feeMode === "input_coin"
|
|
954
|
+
? "getBaseQuantityOutInputFee"
|
|
955
|
+
: "getBaseQuantityOut",
|
|
956
|
+
returnValueEncoding: "bcs.u64"
|
|
957
|
+
},
|
|
958
|
+
quote,
|
|
959
|
+
rawQuote
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
async quoteDeepbookDisplayAmount(input) {
|
|
963
|
+
const pool = getKnownPool(input.poolKey);
|
|
964
|
+
const sourceSymbol = input.direction === "base_to_quote" ? pool.baseCoin : pool.quoteCoin;
|
|
965
|
+
const sourceCoin = getDeepbookCoinEntryBySymbol(sourceSymbol, this.#deepbookCoins);
|
|
966
|
+
const decimals = decimalsFromScalar(sourceCoin.coin.scalar);
|
|
967
|
+
if (decimals === undefined) {
|
|
968
|
+
throw invalidDeepbookScalar(sourceCoin.symbol, sourceCoin.coin.scalar);
|
|
969
|
+
}
|
|
970
|
+
const amountRaw = parseQuoteDisplayAmount(input.amountDisplay, decimals);
|
|
971
|
+
const rawQuote = await this.quoteDeepbookAction({
|
|
972
|
+
poolKey: input.poolKey,
|
|
973
|
+
direction: input.direction,
|
|
974
|
+
amountRaw,
|
|
975
|
+
simulationSender: input.simulationSender,
|
|
976
|
+
feeMode: input.feeMode
|
|
977
|
+
});
|
|
978
|
+
return {
|
|
979
|
+
status: "ok",
|
|
980
|
+
pool: {
|
|
981
|
+
poolKey: input.poolKey,
|
|
982
|
+
base: pool.baseCoin,
|
|
983
|
+
quote: pool.quoteCoin
|
|
984
|
+
},
|
|
985
|
+
direction: input.direction,
|
|
986
|
+
inputAmount: {
|
|
987
|
+
display: input.amountDisplay,
|
|
988
|
+
raw: amountRaw,
|
|
989
|
+
asset: {
|
|
990
|
+
symbol: sourceCoin.symbol,
|
|
991
|
+
coinType: normalizeCoinType(sourceCoin.coin.type),
|
|
992
|
+
decimals,
|
|
993
|
+
unitSource: DEEPBOOK_SCALAR_UNIT_SOURCE
|
|
994
|
+
}
|
|
995
|
+
},
|
|
996
|
+
fetchedAt: rawQuote.fetchedAt,
|
|
997
|
+
userAnswerUse: deepbookQuoteUserAnswerUse("display"),
|
|
998
|
+
quantitySemantics: deepbookQuoteQuantitySemantics("display_source_amount_converted_to_raw_u64"),
|
|
999
|
+
source: rawQuote.source,
|
|
1000
|
+
quote: rawQuote.quote,
|
|
1001
|
+
rawQuote: rawQuote.rawQuote
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
#toDeepbookRawQuoteEvidence(input) {
|
|
1005
|
+
const pool = getKnownPool(input.poolKey);
|
|
1006
|
+
const inputSymbol = input.direction === "base_to_quote" ? pool.baseCoin : pool.quoteCoin;
|
|
1007
|
+
const outputSymbol = input.direction === "base_to_quote" ? pool.quoteCoin : pool.baseCoin;
|
|
1008
|
+
const baseOut = this.#deepbookRawQuoteAmount(pool.baseCoin, input.rawReturnValues.baseOutRaw);
|
|
1009
|
+
const quoteOut = this.#deepbookRawQuoteAmount(pool.quoteCoin, input.rawReturnValues.quoteOutRaw);
|
|
1010
|
+
return {
|
|
1011
|
+
kind: DEEPBOOK_RAW_QUOTE_QUANTITY_KIND,
|
|
1012
|
+
sourceMoveFunction: input.direction === "base_to_quote"
|
|
1013
|
+
? input.feeMode === "input_coin"
|
|
1014
|
+
? "pool::get_quote_quantity_out_input_fee"
|
|
1015
|
+
: "pool::get_quote_quantity_out"
|
|
1016
|
+
: input.feeMode === "input_coin"
|
|
1017
|
+
? "pool::get_base_quantity_out_input_fee"
|
|
1018
|
+
: "pool::get_base_quantity_out",
|
|
1019
|
+
returnValueSourceMoveFunction: input.feeMode === "input_coin" ? "pool::get_quantity_out_input_fee" : "pool::get_quantity_out",
|
|
1020
|
+
returnValueOrder: ["base_quantity_out", "quote_quantity_out", "deep_quantity_required"],
|
|
1021
|
+
inputAmount: this.#deepbookRawQuoteAmount(inputSymbol, input.amountRaw),
|
|
1022
|
+
baseOut,
|
|
1023
|
+
quoteOut,
|
|
1024
|
+
deepRequired: this.#deepbookRawQuoteAmount("DEEP", input.rawReturnValues.deepRequiredRaw),
|
|
1025
|
+
directionalOutput: outputSymbol === pool.baseCoin ? baseOut : quoteOut,
|
|
1026
|
+
boundary: {
|
|
1027
|
+
outputBeforeSlippagePolicy: true,
|
|
1028
|
+
notFor: [
|
|
1029
|
+
"final_min_out",
|
|
1030
|
+
"transaction_building",
|
|
1031
|
+
"signing_data",
|
|
1032
|
+
"signing_readiness",
|
|
1033
|
+
"price_impact",
|
|
1034
|
+
"mid_price_slippage",
|
|
1035
|
+
"quote_vs_mid_slippage",
|
|
1036
|
+
"effective_price",
|
|
1037
|
+
"venue_comparison",
|
|
1038
|
+
"best_route",
|
|
1039
|
+
"route_recommendation",
|
|
1040
|
+
"fiat_usd_cash_out",
|
|
1041
|
+
"external_market_price_conversion",
|
|
1042
|
+
"external_market_lookup",
|
|
1043
|
+
"usd_peg_assumption",
|
|
1044
|
+
"bank_cash_out_estimate",
|
|
1045
|
+
"profit_or_pnl",
|
|
1046
|
+
"cost_basis"
|
|
1047
|
+
]
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
#deepbookRawQuoteAmount(symbol, raw) {
|
|
1052
|
+
const coin = getDeepbookCoinEntryBySymbol(symbol, this.#deepbookCoins);
|
|
1053
|
+
const decimals = decimalsFromScalar(coin.coin.scalar);
|
|
1054
|
+
if (decimals === undefined) {
|
|
1055
|
+
throw invalidDeepbookScalar(coin.symbol, coin.coin.scalar);
|
|
1056
|
+
}
|
|
1057
|
+
return {
|
|
1058
|
+
raw,
|
|
1059
|
+
symbol: coin.symbol,
|
|
1060
|
+
coinType: normalizeCoinType(coin.coin.type),
|
|
1061
|
+
decimals,
|
|
1062
|
+
unitSource: DEEPBOOK_SCALAR_UNIT_SOURCE
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
#deepbookQuoteUnits(baseSymbol, quoteSymbol) {
|
|
1066
|
+
const baseCoin = getDeepbookCoinEntryBySymbol(baseSymbol, this.#deepbookCoins);
|
|
1067
|
+
const quoteCoin = getDeepbookCoinEntryBySymbol(quoteSymbol, this.#deepbookCoins);
|
|
1068
|
+
const deepCoin = getDeepbookCoinEntryBySymbol("DEEP", this.#deepbookCoins);
|
|
1069
|
+
const baseDecimals = decimalsFromScalar(baseCoin.coin.scalar);
|
|
1070
|
+
const quoteDecimals = decimalsFromScalar(quoteCoin.coin.scalar);
|
|
1071
|
+
const deepDecimals = decimalsFromScalar(deepCoin.coin.scalar);
|
|
1072
|
+
if (baseDecimals === undefined) {
|
|
1073
|
+
throw invalidDeepbookScalar(baseCoin.symbol, baseCoin.coin.scalar);
|
|
1074
|
+
}
|
|
1075
|
+
if (quoteDecimals === undefined) {
|
|
1076
|
+
throw invalidDeepbookScalar(quoteCoin.symbol, quoteCoin.coin.scalar);
|
|
1077
|
+
}
|
|
1078
|
+
if (deepDecimals === undefined) {
|
|
1079
|
+
throw invalidDeepbookScalar(deepCoin.symbol, deepCoin.coin.scalar);
|
|
1080
|
+
}
|
|
1081
|
+
return {
|
|
1082
|
+
baseDecimals,
|
|
1083
|
+
quoteDecimals,
|
|
1084
|
+
deepDecimals
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
async summarizeDeepbookAccountInventory(input) {
|
|
1088
|
+
const normalizedManagerAddress = normalizeOptionalManagerAddress(input.managerAddress);
|
|
1089
|
+
const pool = input.poolKey === undefined ? undefined : getKnownPool(input.poolKey);
|
|
1090
|
+
const discoveryClient = this.#deepbookFactory(input.account);
|
|
1091
|
+
const managerAddresses = normalizeManagerAddresses(await discoveryClient.getBalanceManagerIds(input.account));
|
|
1092
|
+
const requested = {
|
|
1093
|
+
...(input.poolKey === undefined ? {} : { poolKey: input.poolKey }),
|
|
1094
|
+
...(normalizedManagerAddress === undefined ? {} : { managerAddress: normalizedManagerAddress })
|
|
1095
|
+
};
|
|
1096
|
+
const base = {
|
|
1097
|
+
status: "ok",
|
|
1098
|
+
account: input.account,
|
|
1099
|
+
fetchedAt: this.#fetchedAt(),
|
|
1100
|
+
requested,
|
|
1101
|
+
managerAddresses,
|
|
1102
|
+
quantitySemantics: deepbookDisplayQuantitySemantics(),
|
|
1103
|
+
...(input.poolKey === undefined || pool === undefined
|
|
1104
|
+
? {}
|
|
1105
|
+
: {
|
|
1106
|
+
pool: {
|
|
1107
|
+
poolKey: input.poolKey,
|
|
1108
|
+
base: pool.baseCoin,
|
|
1109
|
+
quote: pool.quoteCoin
|
|
1110
|
+
}
|
|
1111
|
+
})
|
|
1112
|
+
};
|
|
1113
|
+
if (input.poolKey === undefined && normalizedManagerAddress === undefined) {
|
|
1114
|
+
return {
|
|
1115
|
+
...base,
|
|
1116
|
+
userAnswerUse: deepbookAccountInventoryUserAnswerUse("manager_discovery_only"),
|
|
1117
|
+
detailStatus: "manager_discovery_only",
|
|
1118
|
+
source: deepbookAccountInventorySource(["getBalanceManagerIds"])
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
if (input.poolKey === undefined) {
|
|
1122
|
+
return {
|
|
1123
|
+
...base,
|
|
1124
|
+
userAnswerUse: deepbookAccountInventoryUserAnswerUse("pool_key_required"),
|
|
1125
|
+
detailStatus: "pool_key_required",
|
|
1126
|
+
source: deepbookAccountInventorySource(["getBalanceManagerIds"])
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
if (normalizedManagerAddress === undefined) {
|
|
1130
|
+
return {
|
|
1131
|
+
...base,
|
|
1132
|
+
userAnswerUse: deepbookAccountInventoryUserAnswerUse("manager_address_required"),
|
|
1133
|
+
detailStatus: "manager_address_required",
|
|
1134
|
+
source: deepbookAccountInventorySource(["getBalanceManagerIds"])
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
if (!managerAddresses.includes(normalizedManagerAddress)) {
|
|
1138
|
+
return {
|
|
1139
|
+
...base,
|
|
1140
|
+
userAnswerUse: deepbookAccountInventoryUserAnswerUse("manager_address_not_discovered_for_active_account"),
|
|
1141
|
+
detailStatus: "manager_address_not_discovered_for_active_account",
|
|
1142
|
+
source: deepbookAccountInventorySource(["getBalanceManagerIds"])
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
const detailClient = this.#deepbookFactory(input.account, {
|
|
1146
|
+
balanceManagers: {
|
|
1147
|
+
[normalizedManagerAddress]: { address: normalizedManagerAddress }
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
const accountExists = await detailClient.accountExists(input.poolKey, normalizedManagerAddress);
|
|
1151
|
+
if (!accountExists) {
|
|
1152
|
+
return {
|
|
1153
|
+
...base,
|
|
1154
|
+
userAnswerUse: deepbookAccountInventoryUserAnswerUse("account_not_found"),
|
|
1155
|
+
detailStatus: "account_not_found",
|
|
1156
|
+
source: deepbookAccountInventorySource(["getBalanceManagerIds", "accountExists"]),
|
|
1157
|
+
accountExists
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
const [accountSummary, lockedBalances, openOrderIds] = await Promise.all([
|
|
1161
|
+
detailClient.account(input.poolKey, normalizedManagerAddress),
|
|
1162
|
+
detailClient.lockedBalance(input.poolKey, normalizedManagerAddress),
|
|
1163
|
+
detailClient.accountOpenOrders(input.poolKey, normalizedManagerAddress)
|
|
1164
|
+
]);
|
|
1165
|
+
const cappedOpenOrderIds = openOrderIds.slice(0, MAX_DEEPBOOK_ACCOUNT_OPEN_ORDER_IDS);
|
|
1166
|
+
return {
|
|
1167
|
+
...base,
|
|
1168
|
+
userAnswerUse: deepbookAccountInventoryUserAnswerUse("available"),
|
|
1169
|
+
detailStatus: "available",
|
|
1170
|
+
source: deepbookAccountInventorySource([
|
|
1171
|
+
"getBalanceManagerIds",
|
|
1172
|
+
"accountExists",
|
|
1173
|
+
"account",
|
|
1174
|
+
"lockedBalance",
|
|
1175
|
+
"accountOpenOrders"
|
|
1176
|
+
]),
|
|
1177
|
+
accountExists,
|
|
1178
|
+
accountSummary: toDeepbookAccountSummary(accountSummary),
|
|
1179
|
+
lockedBalances: assertDeepbookDisplayBalances(lockedBalances, "lockedBalances"),
|
|
1180
|
+
openOrderIds: cappedOpenOrderIds,
|
|
1181
|
+
openOrderCount: openOrderIds.length,
|
|
1182
|
+
openOrderIdsTruncated: openOrderIds.length > cappedOpenOrderIds.length
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
#resolveSettlementAssetGroupSymbol(symbol, assetGroup, field) {
|
|
1186
|
+
const canonical = canonicalDeepbookSymbol(symbol, this.#deepbookCoins);
|
|
1187
|
+
if (canonical === undefined) {
|
|
1188
|
+
throw new ReadServiceInputError("input_invalid", "Settlement asset symbol is not in the pinned DeepBook registry", {
|
|
1189
|
+
field,
|
|
1190
|
+
value: symbol
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
const asset = assetGroup.includedAssets.find((candidate) => candidate.symbol === canonical);
|
|
1194
|
+
if (asset === undefined) {
|
|
1195
|
+
throw new ReadServiceInputError("input_invalid", "Settlement asset symbol is not in the supported assetGroup", {
|
|
1196
|
+
field,
|
|
1197
|
+
value: symbol,
|
|
1198
|
+
canonicalSymbol: canonical,
|
|
1199
|
+
assetGroupId: assetGroup.id
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
return asset;
|
|
1203
|
+
}
|
|
1204
|
+
#intentEvidenceAssetGroupBalances(assetGroup, classifiedAssets) {
|
|
1205
|
+
return assetGroup.includedAssets.map((asset) => {
|
|
1206
|
+
const matchingBalances = classifiedAssets
|
|
1207
|
+
.filter((classified) => {
|
|
1208
|
+
try {
|
|
1209
|
+
return normalizeCoinType(classified.balance.coinType) === asset.coinType;
|
|
1210
|
+
}
|
|
1211
|
+
catch {
|
|
1212
|
+
return false;
|
|
1213
|
+
}
|
|
1214
|
+
})
|
|
1215
|
+
.map((classified) => classified.balance.balance);
|
|
1216
|
+
const currentRawAmount = sumRawAmounts(matchingBalances);
|
|
1217
|
+
return {
|
|
1218
|
+
...asset,
|
|
1219
|
+
currentRawAmount,
|
|
1220
|
+
currentDisplayAmount: formatSettlementAssetRawAmount(currentRawAmount, asset.decimals),
|
|
1221
|
+
walletBalanceEvidence: "current_wallet_coin_balance_snapshot"
|
|
1222
|
+
};
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
#intentEvidenceAggregate(input) {
|
|
1226
|
+
if (input.blockedReason !== undefined) {
|
|
1227
|
+
return {
|
|
1228
|
+
status: "unavailable_wallet_balance_scan_incomplete",
|
|
1229
|
+
...(input.requiredDisplayAmount === undefined ? {} : { requiredDisplayAmount: input.requiredDisplayAmount }),
|
|
1230
|
+
reason: input.blockedReason
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
if (input.commonDecimals === undefined) {
|
|
1234
|
+
return {
|
|
1235
|
+
status: "unavailable_mixed_decimals",
|
|
1236
|
+
...(input.requiredDisplayAmount === undefined ? {} : { requiredDisplayAmount: input.requiredDisplayAmount })
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
const currentRawAmount = sumRawAmounts(input.balances.map((balance) => balance.currentRawAmount));
|
|
1240
|
+
if (input.requiredDisplayAmount === undefined) {
|
|
1241
|
+
return {
|
|
1242
|
+
status: "available",
|
|
1243
|
+
currentRawAmount,
|
|
1244
|
+
currentDisplayAmount: formatSettlementAssetRawAmount(currentRawAmount, input.commonDecimals),
|
|
1245
|
+
decimals: input.commonDecimals,
|
|
1246
|
+
unitSource: DEEPBOOK_SCALAR_UNIT_SOURCE
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
const requiredRawAmount = this.#parseIntentDisplayAmount(input.requiredDisplayAmount, input.commonDecimals, "requiredDisplayAmount");
|
|
1250
|
+
const shortfallRawAmount = BigInt(currentRawAmount) >= BigInt(requiredRawAmount)
|
|
1251
|
+
? "0"
|
|
1252
|
+
: (BigInt(requiredRawAmount) - BigInt(currentRawAmount)).toString();
|
|
1253
|
+
return {
|
|
1254
|
+
status: "available",
|
|
1255
|
+
requiredDisplayAmount: input.requiredDisplayAmount,
|
|
1256
|
+
requiredRawAmount,
|
|
1257
|
+
currentRawAmount,
|
|
1258
|
+
currentDisplayAmount: formatSettlementAssetRawAmount(currentRawAmount, input.commonDecimals),
|
|
1259
|
+
shortfallRawAmount,
|
|
1260
|
+
shortfallDisplayAmount: formatSettlementAssetRawAmount(shortfallRawAmount, input.commonDecimals),
|
|
1261
|
+
decimals: input.commonDecimals,
|
|
1262
|
+
unitSource: DEEPBOOK_SCALAR_UNIT_SOURCE
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
#intentEvidenceSettlementAssetCoverage(aggregate) {
|
|
1266
|
+
const boundary = intentEvidenceSettlementAssetCoverageBoundary();
|
|
1267
|
+
if (aggregate.status === "unavailable_mixed_decimals") {
|
|
1268
|
+
return {
|
|
1269
|
+
status: "unavailable_mixed_decimals",
|
|
1270
|
+
...(aggregate.requiredDisplayAmount === undefined
|
|
1271
|
+
? {}
|
|
1272
|
+
: { requiredDisplayAmount: aggregate.requiredDisplayAmount }),
|
|
1273
|
+
reason: "asset_group_assets_do_not_share_verified_decimals",
|
|
1274
|
+
boundary
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
if (aggregate.status === "unavailable_wallet_balance_scan_incomplete") {
|
|
1278
|
+
return {
|
|
1279
|
+
status: "unavailable_wallet_balance_scan_incomplete",
|
|
1280
|
+
...(aggregate.requiredDisplayAmount === undefined
|
|
1281
|
+
? {}
|
|
1282
|
+
: { requiredDisplayAmount: aggregate.requiredDisplayAmount }),
|
|
1283
|
+
reason: aggregate.reason,
|
|
1284
|
+
boundary
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
if (aggregate.requiredDisplayAmount === undefined) {
|
|
1288
|
+
return {
|
|
1289
|
+
status: "balance_total_only",
|
|
1290
|
+
currentRawAmount: aggregate.currentRawAmount,
|
|
1291
|
+
currentDisplayAmount: aggregate.currentDisplayAmount,
|
|
1292
|
+
decimals: aggregate.decimals,
|
|
1293
|
+
unitSource: aggregate.unitSource,
|
|
1294
|
+
boundary
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
if (aggregate.requiredRawAmount === undefined ||
|
|
1298
|
+
aggregate.shortfallRawAmount === undefined ||
|
|
1299
|
+
aggregate.shortfallDisplayAmount === undefined) {
|
|
1300
|
+
throw new Error("settlement-asset coverage requires complete target amount evidence");
|
|
1301
|
+
}
|
|
1302
|
+
const shortfallRawAmount = aggregate.shortfallRawAmount;
|
|
1303
|
+
return {
|
|
1304
|
+
status: BigInt(shortfallRawAmount) === 0n ? "covered_by_settlement_asset_balance" : "shortfall_in_settlement_asset_balance",
|
|
1305
|
+
requiredDisplayAmount: aggregate.requiredDisplayAmount,
|
|
1306
|
+
requiredRawAmount: aggregate.requiredRawAmount,
|
|
1307
|
+
currentRawAmount: aggregate.currentRawAmount,
|
|
1308
|
+
currentDisplayAmount: aggregate.currentDisplayAmount,
|
|
1309
|
+
shortfallRawAmount,
|
|
1310
|
+
shortfallDisplayAmount: aggregate.shortfallDisplayAmount,
|
|
1311
|
+
decimals: aggregate.decimals,
|
|
1312
|
+
unitSource: aggregate.unitSource,
|
|
1313
|
+
boundary
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
#intentEvidenceSelectedTarget(input) {
|
|
1317
|
+
const targetBalance = input.balances.find((balance) => balance.symbol === input.targetAsset.symbol);
|
|
1318
|
+
const currentRawAmount = targetBalance?.currentRawAmount ?? "0";
|
|
1319
|
+
const requiredRawAmount = this.#parseIntentDisplayAmount(input.requiredDisplayAmount, input.targetAsset.decimals, "requiredDisplayAmount");
|
|
1320
|
+
const shortfallRawAmount = BigInt(currentRawAmount) >= BigInt(requiredRawAmount)
|
|
1321
|
+
? "0"
|
|
1322
|
+
: (BigInt(requiredRawAmount) - BigInt(currentRawAmount)).toString();
|
|
1323
|
+
return {
|
|
1324
|
+
...input.targetAsset,
|
|
1325
|
+
selectionSource: input.selectionSource,
|
|
1326
|
+
requiredRawAmount,
|
|
1327
|
+
currentRawAmount,
|
|
1328
|
+
currentDisplayAmount: formatSettlementAssetRawAmount(currentRawAmount, input.targetAsset.decimals),
|
|
1329
|
+
shortfallRawAmount,
|
|
1330
|
+
shortfallDisplayAmount: formatSettlementAssetRawAmount(shortfallRawAmount, input.targetAsset.decimals)
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
async #settlementAssetGroupParityAsset(asset, referenceAsset, deepbook) {
|
|
1334
|
+
if (asset.symbol === referenceAsset.symbol) {
|
|
1335
|
+
return {
|
|
1336
|
+
...asset,
|
|
1337
|
+
status: "reference_asset",
|
|
1338
|
+
priceInReferenceAsset: 1,
|
|
1339
|
+
parityDirection: "reference_asset_per_group_asset",
|
|
1340
|
+
reason: "reference_asset_is_measurement_baseline_not_settlement_choice"
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
let directPool;
|
|
1344
|
+
try {
|
|
1345
|
+
const resolved = resolveDeepbookPoolForSymbols({
|
|
1346
|
+
sourceSymbol: asset.symbol,
|
|
1347
|
+
targetSymbol: referenceAsset.symbol
|
|
1348
|
+
});
|
|
1349
|
+
directPool = { poolKey: resolved.poolKey, direction: resolved.direction };
|
|
1350
|
+
}
|
|
1351
|
+
catch {
|
|
1352
|
+
return {
|
|
1353
|
+
...asset,
|
|
1354
|
+
status: "no_direct_deepbook_pool",
|
|
1355
|
+
reason: "No direct DeepBook mainnet pool exists between this group asset and the measurement reference asset."
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
let poolMidPrice;
|
|
1359
|
+
try {
|
|
1360
|
+
poolMidPrice = assertValidDeepbookMidPrice(directPool.poolKey, await deepbook.midPrice(directPool.poolKey));
|
|
1361
|
+
}
|
|
1362
|
+
catch (error) {
|
|
1363
|
+
return {
|
|
1364
|
+
...asset,
|
|
1365
|
+
status: "mid_price_unavailable",
|
|
1366
|
+
poolKey: directPool.poolKey,
|
|
1367
|
+
direction: directPool.direction,
|
|
1368
|
+
reason: error instanceof Error ? error.message : "DeepBook mid-price lookup failed."
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
return {
|
|
1372
|
+
...asset,
|
|
1373
|
+
status: "measured",
|
|
1374
|
+
priceInReferenceAsset: roundPriceInReferenceAsset(directPool.direction === "base_to_quote" ? poolMidPrice : 1 / poolMidPrice),
|
|
1375
|
+
parityDirection: "reference_asset_per_group_asset",
|
|
1376
|
+
poolKey: directPool.poolKey,
|
|
1377
|
+
direction: directPool.direction,
|
|
1378
|
+
poolMidPrice,
|
|
1379
|
+
poolMidPriceDirection: DEEPBOOK_MID_PRICE_DIRECTION
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
async #intentEvidenceCandidateConversions(input) {
|
|
1383
|
+
const candidates = [];
|
|
1384
|
+
for (const balance of input.balances) {
|
|
1385
|
+
if (balance.currentRawAmount === "0" || balance.symbol === input.targetAsset?.symbol) {
|
|
1386
|
+
continue;
|
|
1387
|
+
}
|
|
1388
|
+
if (input.acceptedSourceSet !== undefined && !input.acceptedSourceSet.has(balance.symbol)) {
|
|
1389
|
+
candidates.push({
|
|
1390
|
+
sourceSymbol: balance.symbol,
|
|
1391
|
+
...(input.targetAsset === undefined ? {} : { targetSymbol: input.targetAsset.symbol }),
|
|
1392
|
+
sourceRawAmount: balance.currentRawAmount,
|
|
1393
|
+
sourceDisplayAmount: balance.currentDisplayAmount,
|
|
1394
|
+
status: "filtered_by_accepted_source_assets",
|
|
1395
|
+
reason: "The source asset was not included in acceptedSourceAssetSymbols."
|
|
1396
|
+
});
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
if (input.targetAsset === undefined) {
|
|
1400
|
+
candidates.push({
|
|
1401
|
+
sourceSymbol: balance.symbol,
|
|
1402
|
+
sourceRawAmount: balance.currentRawAmount,
|
|
1403
|
+
sourceDisplayAmount: balance.currentDisplayAmount,
|
|
1404
|
+
status: "target_asset_not_selected",
|
|
1405
|
+
reason: "No target settlement asset was selected, so conversion quotes are not requested."
|
|
1406
|
+
});
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
let directPool;
|
|
1410
|
+
try {
|
|
1411
|
+
const resolved = resolveDeepbookPoolForSymbols({
|
|
1412
|
+
sourceSymbol: balance.symbol,
|
|
1413
|
+
targetSymbol: input.targetAsset.symbol
|
|
1414
|
+
});
|
|
1415
|
+
directPool = { poolKey: resolved.poolKey, direction: resolved.direction };
|
|
1416
|
+
}
|
|
1417
|
+
catch {
|
|
1418
|
+
candidates.push({
|
|
1419
|
+
sourceSymbol: balance.symbol,
|
|
1420
|
+
targetSymbol: input.targetAsset.symbol,
|
|
1421
|
+
sourceRawAmount: balance.currentRawAmount,
|
|
1422
|
+
sourceDisplayAmount: balance.currentDisplayAmount,
|
|
1423
|
+
status: "no_direct_deepbook_pool",
|
|
1424
|
+
reason: "No direct DeepBook mainnet pool exists for this source and target pair."
|
|
1425
|
+
});
|
|
1426
|
+
continue;
|
|
1427
|
+
}
|
|
1428
|
+
try {
|
|
1429
|
+
const quote = await this.quoteDeepbookAction({
|
|
1430
|
+
poolKey: directPool.poolKey,
|
|
1431
|
+
direction: directPool.direction,
|
|
1432
|
+
amountRaw: balance.currentRawAmount,
|
|
1433
|
+
simulationSender: DEFAULT_DEEPBOOK_SIMULATION_SENDER
|
|
1434
|
+
});
|
|
1435
|
+
candidates.push({
|
|
1436
|
+
sourceSymbol: balance.symbol,
|
|
1437
|
+
targetSymbol: input.targetAsset.symbol,
|
|
1438
|
+
sourceRawAmount: balance.currentRawAmount,
|
|
1439
|
+
sourceDisplayAmount: balance.currentDisplayAmount,
|
|
1440
|
+
status: "quoted",
|
|
1441
|
+
directPool,
|
|
1442
|
+
quote,
|
|
1443
|
+
boundary: [
|
|
1444
|
+
"quote_snapshot_only",
|
|
1445
|
+
"not_final_min_out",
|
|
1446
|
+
"not_route_recommendation",
|
|
1447
|
+
"not_route_dependent_payment_support",
|
|
1448
|
+
"not_payment_readiness",
|
|
1449
|
+
"not_signing_readiness"
|
|
1450
|
+
]
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
catch (error) {
|
|
1454
|
+
candidates.push({
|
|
1455
|
+
sourceSymbol: balance.symbol,
|
|
1456
|
+
targetSymbol: input.targetAsset.symbol,
|
|
1457
|
+
sourceRawAmount: balance.currentRawAmount,
|
|
1458
|
+
sourceDisplayAmount: balance.currentDisplayAmount,
|
|
1459
|
+
status: "quote_unavailable",
|
|
1460
|
+
reason: error instanceof Error ? error.message : "DeepBook quote failed for this candidate.",
|
|
1461
|
+
directPool
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
return candidates;
|
|
1466
|
+
}
|
|
1467
|
+
#intentEvidenceRequiredUserChoices(intentKind, targetAsset, candidateConversions) {
|
|
1468
|
+
if (intentKind === "summarize_settlement_asset_group_balance") {
|
|
1469
|
+
return [];
|
|
1470
|
+
}
|
|
1471
|
+
const choices = [];
|
|
1472
|
+
if (targetAsset === undefined) {
|
|
1473
|
+
choices.push("Choose the onchain settlement asset or merchant-accepted USD-denominated asset set before target-specific settlement evidence can be completed.");
|
|
1474
|
+
}
|
|
1475
|
+
if (candidateConversions.some((candidate) => candidate.status === "quoted")) {
|
|
1476
|
+
choices.push("Choose which quoted candidate assets, if any, the user wants to convert.");
|
|
1477
|
+
}
|
|
1478
|
+
return choices;
|
|
1479
|
+
}
|
|
1480
|
+
#parseIntentDisplayAmount(displayAmount, decimals, field) {
|
|
1481
|
+
try {
|
|
1482
|
+
return parseDisplayAmountToRaw(displayAmount, decimals);
|
|
1483
|
+
}
|
|
1484
|
+
catch (error) {
|
|
1485
|
+
throw new ReadServiceInputError("input_invalid", "requiredDisplayAmount must be an unsigned decimal string within verified decimals", {
|
|
1486
|
+
field,
|
|
1487
|
+
value: displayAmount,
|
|
1488
|
+
decimals,
|
|
1489
|
+
reason: error instanceof Error ? error.message : "unknown"
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
#fetchedAt() {
|
|
1494
|
+
return this.#now().toISOString();
|
|
1495
|
+
}
|
|
1496
|
+
// Resolve one coin type's verified unit (decimals/symbol) through the shared
|
|
1497
|
+
// coin-metadata cache and DeepBook fallback. Public so other readers (e.g. the
|
|
1498
|
+
// public chain-receipt reader) can render decimal amounts without duplicating
|
|
1499
|
+
// the cache logic. Fails soft to an unavailable unit on an unresolvable type.
|
|
1500
|
+
async resolveCoinUnit(coinType) {
|
|
1501
|
+
let normalizedCoinType;
|
|
1502
|
+
try {
|
|
1503
|
+
normalizedCoinType = normalizeCoinType(coinType);
|
|
1504
|
+
}
|
|
1505
|
+
catch {
|
|
1506
|
+
return unavailableUnit("coin_type_unresolved");
|
|
1507
|
+
}
|
|
1508
|
+
return this.#resolveCoinUnitForNormalizedCoinType(normalizedCoinType);
|
|
1509
|
+
}
|
|
1510
|
+
async #withUnit(balance) {
|
|
1511
|
+
let normalizedCoinType;
|
|
1512
|
+
try {
|
|
1513
|
+
normalizedCoinType = normalizeCoinType(balance.coinType);
|
|
1514
|
+
}
|
|
1515
|
+
catch {
|
|
1516
|
+
return withUnavailableUnit(balance, "coin_type_unresolved");
|
|
1517
|
+
}
|
|
1518
|
+
const unit = await this.#resolveCoinUnitForNormalizedCoinType(normalizedCoinType);
|
|
1519
|
+
return withResolvedUnit(balance, unit);
|
|
1520
|
+
}
|
|
1521
|
+
async #resolveCoinUnitForNormalizedCoinType(normalizedCoinType) {
|
|
1522
|
+
const now = this.#now();
|
|
1523
|
+
let cached;
|
|
1524
|
+
try {
|
|
1525
|
+
cached = await this.#coinMetadataCache.getCoinMetadata({
|
|
1526
|
+
coinType: normalizedCoinType,
|
|
1527
|
+
chainIdentifier: this.#chainIdentifier,
|
|
1528
|
+
now
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
catch (error) {
|
|
1532
|
+
throw new ReadServiceCacheError("read", error);
|
|
1533
|
+
}
|
|
1534
|
+
if (cached.status === "hit") {
|
|
1535
|
+
return unitFromMetadataRecord(cached.record, "hit");
|
|
1536
|
+
}
|
|
1537
|
+
let metadata;
|
|
1538
|
+
try {
|
|
1539
|
+
metadata = await this.#client.core.getCoinMetadata({ coinType: normalizedCoinType });
|
|
1540
|
+
}
|
|
1541
|
+
catch {
|
|
1542
|
+
return unavailableUnit("metadata_lookup_failed");
|
|
1543
|
+
}
|
|
1544
|
+
if (metadata.coinMetadata !== null) {
|
|
1545
|
+
const record = this.#cacheRecordFromMetadata(normalizedCoinType, metadata.coinMetadata, now);
|
|
1546
|
+
try {
|
|
1547
|
+
await this.#coinMetadataCache.setCoinMetadata(record);
|
|
1548
|
+
}
|
|
1549
|
+
catch (error) {
|
|
1550
|
+
throw new ReadServiceCacheError("write", error);
|
|
1551
|
+
}
|
|
1552
|
+
return unitFromMetadataRecord(record, cached.status === "expired" ? "expired_refetched" : "miss");
|
|
1553
|
+
}
|
|
1554
|
+
try {
|
|
1555
|
+
const fallback = deepbookUnitForCoinType(normalizedCoinType, this.#deepbookCoins);
|
|
1556
|
+
if (fallback) {
|
|
1557
|
+
return unitFromDeepbook(fallback);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
catch (error) {
|
|
1561
|
+
if (error instanceof ReadServiceInputError) {
|
|
1562
|
+
return unavailableUnit("no_verified_decimals");
|
|
1563
|
+
}
|
|
1564
|
+
throw error;
|
|
1565
|
+
}
|
|
1566
|
+
return unavailableUnit("metadata_not_found");
|
|
1567
|
+
}
|
|
1568
|
+
async #scanWalletAssetClassificationPages(account) {
|
|
1569
|
+
const classifiedAssets = [];
|
|
1570
|
+
let uninspectedAssetClasses = NOT_INSPECTED_ASSET_CLASSES.map((assetClass) => ({
|
|
1571
|
+
...assetClass
|
|
1572
|
+
}));
|
|
1573
|
+
let cursor;
|
|
1574
|
+
const requestedCursors = new Set();
|
|
1575
|
+
for (let pageIndex = 0; pageIndex < MAX_WALLET_BALANCE_SCAN_PAGES; pageIndex += 1) {
|
|
1576
|
+
if (cursor !== undefined && cursor !== null) {
|
|
1577
|
+
if (requestedCursors.has(cursor)) {
|
|
1578
|
+
return {
|
|
1579
|
+
classifiedAssets,
|
|
1580
|
+
uninspectedAssetClasses,
|
|
1581
|
+
inspectedBalancePages: pageIndex,
|
|
1582
|
+
inspectedCoinBalanceCount: classifiedAssets.length,
|
|
1583
|
+
blockedReason: "wallet_balance_pagination_did_not_advance"
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
requestedCursors.add(cursor);
|
|
1587
|
+
}
|
|
1588
|
+
const page = await this.classifyWalletAssets({ account, ...(cursor === undefined ? {} : { cursor }) });
|
|
1589
|
+
classifiedAssets.push(...page.classifiedAssets);
|
|
1590
|
+
uninspectedAssetClasses = page.uninspectedAssetClasses;
|
|
1591
|
+
if (!page.hasNextPage) {
|
|
1592
|
+
return {
|
|
1593
|
+
classifiedAssets,
|
|
1594
|
+
uninspectedAssetClasses,
|
|
1595
|
+
inspectedBalancePages: pageIndex + 1,
|
|
1596
|
+
inspectedCoinBalanceCount: classifiedAssets.length
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
if (typeof page.cursor !== "string" ||
|
|
1600
|
+
page.cursor.length === 0 ||
|
|
1601
|
+
page.cursor === cursor ||
|
|
1602
|
+
requestedCursors.has(page.cursor)) {
|
|
1603
|
+
return {
|
|
1604
|
+
classifiedAssets,
|
|
1605
|
+
uninspectedAssetClasses,
|
|
1606
|
+
inspectedBalancePages: pageIndex + 1,
|
|
1607
|
+
inspectedCoinBalanceCount: classifiedAssets.length,
|
|
1608
|
+
blockedReason: "wallet_balance_pagination_did_not_advance"
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
cursor = page.cursor;
|
|
1612
|
+
}
|
|
1613
|
+
return {
|
|
1614
|
+
classifiedAssets,
|
|
1615
|
+
uninspectedAssetClasses,
|
|
1616
|
+
inspectedBalancePages: MAX_WALLET_BALANCE_SCAN_PAGES,
|
|
1617
|
+
inspectedCoinBalanceCount: classifiedAssets.length,
|
|
1618
|
+
blockedReason: "wallet_balance_page_limit_exceeded"
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
#cacheRecordFromMetadata(coinType, metadata, now) {
|
|
1622
|
+
const decimals = assertValidDecimals(metadata.decimals);
|
|
1623
|
+
return {
|
|
1624
|
+
coinType,
|
|
1625
|
+
chainIdentifier: this.#chainIdentifier,
|
|
1626
|
+
decimals,
|
|
1627
|
+
symbol: metadata.symbol,
|
|
1628
|
+
name: metadata.name,
|
|
1629
|
+
fetchedAt: now.toISOString(),
|
|
1630
|
+
expiresAt: new Date(now.getTime() + this.#coinMetadataTtlMs).toISOString()
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
listDeepbookTokenRegistry() {
|
|
1634
|
+
return listDeepbookTokenRegistry(this.#deepbookCoins);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
export function createSuiReadService(options) {
|
|
1638
|
+
return new SuiReadService(options);
|
|
1639
|
+
}
|