@stelis/say-ur-intent 0.0.0 → 0.0.2
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 +4 -39
- package/dist/adapters/adapterLifecycleValidators.js +7 -0
- package/dist/adapters/adapterPromptSurfaces.js +71 -0
- package/dist/adapters/deepbook/deepbookHumanReviewProducer.js +175 -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 +269 -0
- package/dist/adapters/flowx/flowxSwapHumanReviewProducer.js +176 -0
- package/dist/adapters/flowx/flowxSwapIntent.js +79 -0
- package/dist/adapters/flowx/flowxSwapQuotePolicy.js +104 -0
- package/dist/adapters/flowx/flowxSwapReviewEvidence.js +468 -0
- package/dist/adapters/flowx/flowxSwapReviewLifecycle.js +85 -0
- package/dist/adapters/flowx/flowxSwapTransactionMaterialProducer.js +362 -0
- package/dist/adapters/intentPlanFactories.js +59 -0
- package/dist/adapters/reviewAdapters.js +81 -0
- package/dist/core/action/adapterLifecycleValidation.js +12 -0
- package/dist/core/action/forbiddenFields.js +43 -0
- package/dist/core/action/humanReadableReviewEvidence.js +203 -0
- package/dist/core/action/humanReadableReviewProjectionVerifier.js +29 -0
- package/dist/core/action/ptbVisualizationProducer.js +66 -0
- package/dist/core/action/reviewCheckResults.js +6 -0
- package/dist/core/action/reviewStateValidation.js +11 -0
- package/dist/core/action/reviewTimeSimulationEvidence.js +471 -0
- package/dist/core/action/schemas.js +529 -0
- package/dist/core/action/signableAdapterContract.js +993 -0
- package/dist/core/action/swapHumanReadableReviewProjection.js +124 -0
- package/dist/core/action/swapQuotePolicyEvidence.js +278 -0
- package/dist/core/action/transactionObjectOwnershipEvidence.js +247 -0
- package/dist/core/action/transactionObjectOwnershipProducer.js +329 -0
- package/dist/core/action/types.js +35 -0
- package/dist/core/action/walletReviewContractAssembler.js +282 -0
- package/dist/core/activity/activityStore.js +15 -0
- package/dist/core/activity/localDataService.js +258 -0
- package/dist/core/activity/localDataTypes.js +11 -0
- package/dist/core/activity/localDataValidation.js +396 -0
- package/dist/core/activity/schemaVersion.js +1 -0
- package/dist/core/activity/sqliteActivityStore.js +820 -0
- package/dist/core/activity/sqliteActivityStoreRows.js +430 -0
- package/dist/core/activity/sqliteActivityStoreSchema.js +258 -0
- package/dist/core/activity/sqliteActivityStoreTypes.js +5 -0
- package/dist/core/activity/suiFunctionTarget.js +43 -0
- package/dist/core/activity/transactionActivityAccountEffects.js +189 -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 +87 -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/deepbookRawQuoteClient.js +86 -0
- package/dist/core/read/deepbookReadHelpers.js +265 -0
- package/dist/core/read/deepbookRegistry.js +133 -0
- package/dist/core/read/flowxQuoteClient.js +117 -0
- package/dist/core/read/flowxReadHelpers.js +145 -0
- package/dist/core/read/flowxRegistry.js +174 -0
- package/dist/core/read/intentEvidenceResponseFormatting.js +228 -0
- package/dist/core/read/readResponseGuidance.js +451 -0
- package/dist/core/read/readService.js +1164 -0
- package/dist/core/read/readServiceTypes.js +59 -0
- package/dist/core/read/settlementParityFormatting.js +82 -0
- package/dist/core/read/walletReadHelpers.js +99 -0
- package/dist/core/review/reviewChecks.js +54 -0
- package/dist/core/review/reviewComputation.js +38 -0
- package/dist/core/review/reviewComputationResult.js +87 -0
- package/dist/core/session/localSession.js +31 -0
- package/dist/core/session/privateReviewArtifacts.js +73 -0
- package/dist/core/session/sessionErrors.js +9 -0
- package/dist/core/session/sessionStore.js +821 -0
- package/dist/core/session/settingsSession.js +1 -0
- package/dist/core/session/settingsSessions.js +43 -0
- package/dist/core/session/status.js +86 -0
- package/dist/core/session/transactionMaterialStore.js +205 -0
- package/dist/core/session/wait.js +102 -0
- package/dist/core/session/walletIdentity.js +103 -0
- package/dist/core/session/walletIdentitySessions.js +189 -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 +381 -0
- package/dist/mcp/result.js +17 -0
- package/dist/mcp/schemas.js +8 -0
- package/dist/mcp/server.js +30 -0
- package/dist/mcp/serverInfo.js +123 -0
- package/dist/mcp/toolErrors.js +105 -0
- package/dist/mcp/toolNames.js +50 -0
- package/dist/mcp/tools/account/index.js +44 -0
- package/dist/mcp/tools/action/prepareSuiActionReview.js +120 -0
- package/dist/mcp/tools/read/commonSchemas.js +43 -0
- package/dist/mcp/tools/read/deepbookReadTools.js +453 -0
- package/dist/mcp/tools/read/flowxReadTools.js +135 -0
- package/dist/mcp/tools/read/index.js +16 -0
- package/dist/mcp/tools/read/readToolHelpers.js +68 -0
- package/dist/mcp/tools/read/reviewActivityTools.js +176 -0
- package/dist/mcp/tools/read/serverStatusTools.js +103 -0
- package/dist/mcp/tools/read/transactionActivityOutput.js +300 -0
- package/dist/mcp/tools/read/transactionActivityTools.js +544 -0
- package/dist/mcp/tools/read/walletReadTools.js +733 -0
- package/dist/mcp/tools/session/executionResultTools.js +92 -0
- package/dist/mcp/tools/session/index.js +8 -0
- package/dist/mcp/tools/session/shared.js +79 -0
- package/dist/mcp/tools/session/statusTools.js +134 -0
- package/dist/mcp/tools/session/walletIdentityTools.js +119 -0
- package/dist/mcp/tools/settings/index.js +64 -0
- package/dist/review-app/analysis.css +1 -0
- package/dist/review-app/analysis.js +1 -0
- package/dist/review-app/arc-BjIacwQm.js +1 -0
- package/dist/review-app/architecture-U656AL7Q-aSB9x1OK.js +1 -0
- package/dist/review-app/architectureDiagram-VXUJARFQ-C5W6re2I.js +36 -0
- package/dist/review-app/array-BmXUUrU6.js +1 -0
- package/dist/review-app/blockDiagram-VD42YOAC-20MLNcUm.js +122 -0
- package/dist/review-app/c4Diagram-YG6GDRKO-BZXRrcck.js +10 -0
- package/dist/review-app/channel-lk2p_CUu.js +1 -0
- package/dist/review-app/chunk-4BX2VUAB-BPITOdjX.js +1 -0
- package/dist/review-app/chunk-55IACEB6-Dz-pyw5k.js +1 -0
- package/dist/review-app/chunk-76Q3JFCE-cK_X1P_l.js +1 -0
- package/dist/review-app/chunk-ABZYJK2D-Dt4W53JI.js +81 -0
- package/dist/review-app/chunk-ATLVNIR6-fZHLXURb.js +1 -0
- package/dist/review-app/chunk-B4BG7PRW-BbgcjusC.js +165 -0
- package/dist/review-app/chunk-BJD4TVEz.js +1 -0
- package/dist/review-app/chunk-CVBHYZKI-CViawAKX.js +1 -0
- package/dist/review-app/chunk-DI55MBZ5-C5aoul-d.js +220 -0
- package/dist/review-app/chunk-FMBD7UC4-Chxmw62A.js +15 -0
- package/dist/review-app/chunk-FPAJGGOC-DDHjQ09H.js +80 -0
- package/dist/review-app/chunk-FWNWRKHM-CVVQUptk.js +1 -0
- package/dist/review-app/chunk-HN2XXSSU-yzNpjaSZ.js +1 -0
- package/dist/review-app/chunk-JA3XYJ7Z-C5ZJdU01.js +70 -0
- package/dist/review-app/chunk-JZLCHNYA-BBST4Cnk.js +54 -0
- package/dist/review-app/chunk-LBM3YZW2-CdwAPuHr.js +1 -0
- package/dist/review-app/chunk-LHMN2FUI-BtB5uDcp.js +1 -0
- package/dist/review-app/chunk-O7ZBX7Z2-pxdK4Sa3.js +1 -0
- package/dist/review-app/chunk-QN33PNHL-CbVv3uGK.js +1 -0
- package/dist/review-app/chunk-QXUST7PY-DKM2-t2c.js +7 -0
- package/dist/review-app/chunk-QZHKN3VN-C5ni2pN_.js +1 -0
- package/dist/review-app/chunk-S3R3BYOJ-BWvOhDs0.js +2 -0
- package/dist/review-app/chunk-S6J4BHB3-D9Fk0YeD.js +1 -0
- package/dist/review-app/chunk-T53DSG4Q-C1qEyzyV.js +1 -0
- package/dist/review-app/chunk-TZMSLE5B-B--7eU69.js +1 -0
- package/dist/review-app/classDiagram-2ON5EDUG-DlL1m2bp.js +1 -0
- package/dist/review-app/classDiagram-v2-WZHVMYZB-FXRskT1j.js +1 -0
- package/dist/review-app/clone-BZZb7gpZ.js +1 -0
- package/dist/review-app/cose-bilkent-S5V4N54A-CRIb8XEO.js +1 -0
- package/dist/review-app/cytoscape.esm-C7jYqDP5.js +321 -0
- package/dist/review-app/dagre-6UL2VRFP-FNCAXbdE.js +4 -0
- package/dist/review-app/dagre-Be46QtUd.js +1 -0
- package/dist/review-app/defaultLocale-BaWNtAUL.js +1 -0
- package/dist/review-app/diagram-PSM6KHXK-ylLWjiNM.js +24 -0
- package/dist/review-app/diagram-QEK2KX5R-BCDcESxs.js +43 -0
- package/dist/review-app/diagram-S2PKOQOG-Vdrc-vrO.js +24 -0
- package/dist/review-app/dist-WPc74x_f.js +1 -0
- package/dist/review-app/erDiagram-Q2GNP2WA-E5ZsUbDF.js +60 -0
- package/dist/review-app/flatten-DHf9IeNI.js +1 -0
- package/dist/review-app/flowDiagram-NV44I4VS-DBSQuj6x.js +162 -0
- package/dist/review-app/ganttDiagram-LVOFAZNH-CKUOsqwl.js +267 -0
- package/dist/review-app/gitGraph-F6HP7TQM-DsAD6qK1.js +1 -0
- package/dist/review-app/gitGraphDiagram-NY62KEGX-BCeIMWdl.js +65 -0
- package/dist/review-app/graphlib-CiX5CXxR.js +1 -0
- package/dist/review-app/http-DMvwuuFk.js +1 -0
- package/dist/review-app/identity-DY8PXc6t.js +1 -0
- package/dist/review-app/info-NVLQJR56-Dlx1nZic.js +1 -0
- package/dist/review-app/infoDiagram-F6ZHWCRC-CAuANIrz.js +2 -0
- package/dist/review-app/init-BvqephKz.js +1 -0
- package/dist/review-app/journeyDiagram-XKPGCS4Q-C-Z9phnx.js +139 -0
- package/dist/review-app/kanban-definition-3W4ZIXB7-DufgZABq.js +89 -0
- package/dist/review-app/katex-B-Z-NXXN.js +257 -0
- package/dist/review-app/line-DiIv3Jgw.js +1 -0
- package/dist/review-app/linear-Cv-UPvo1.js +1 -0
- package/dist/review-app/math-kmyYrkHL.js +1 -0
- package/dist/review-app/mermaid-parser.core-DkwUYTPl.js +4 -0
- package/dist/review-app/mindmap-definition-VGOIOE7T-TM_CqdmV.js +68 -0
- package/dist/review-app/ordinal-BliTlkoG.js +1 -0
- package/dist/review-app/packet-BFZMPI3H-DqbnU92v.js +1 -0
- package/dist/review-app/path-AEo9W6mQ.js +1 -0
- package/dist/review-app/pie-7BOR55EZ-LJzaLkgr.js +1 -0
- package/dist/review-app/pieDiagram-ADFJNKIX-BAs8OfRS.js +30 -0
- package/dist/review-app/quadrantDiagram-AYHSOK5B-CyUDZP5S.js +7 -0
- package/dist/review-app/radar-NHE76QYJ-DBpHc8_Y.js +1 -0
- package/dist/review-app/reduce-B-HuPpdd.js +1 -0
- package/dist/review-app/requirementDiagram-UZGBJVZJ-BEHix78P.js +64 -0
- package/dist/review-app/review.css +1 -0
- package/dist/review-app/review.js +43 -0
- package/dist/review-app/sankeyDiagram-TZEHDZUN-B2bKbmsm.js +10 -0
- package/dist/review-app/sequenceDiagram-WL72ISMW-DVLOORFJ.js +145 -0
- package/dist/review-app/settings.css +1 -0
- package/dist/review-app/settings.js +1 -0
- package/dist/review-app/src-Buml7cM5.js +1 -0
- package/dist/review-app/stateDiagram-FKZM4ZOC-sFGGp2kV.js +1 -0
- package/dist/review-app/stateDiagram-v2-4FDKWEC3-BHfCF4dX.js +1 -0
- package/dist/review-app/timeline-definition-IT6M3QCI-BESnBijC.js +61 -0
- package/dist/review-app/treemap-KMMF4GRG-wnVLBDeQ.js +1 -0
- package/dist/review-app/walletStatus-CcojOdGy.js +7 -0
- package/dist/review-app/xychartDiagram-PRI3JC2R-BGWVfCx4.js +7 -0
- package/dist/review-server/assets.js +48 -0
- package/dist/review-server/html.js +66 -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 +10 -0
- package/dist/review-server/server.js +568 -0
- package/dist/review-server/settingsApi.js +182 -0
- package/dist/review-server/walletIdentityResponse.js +13 -0
- package/dist/runtime/config.js +103 -0
- package/dist/runtime/localSettingsService.js +198 -0
- package/dist/runtime/logger.js +50 -0
- package/dist/runtime/reviewServerAcquire.js +128 -0
- package/dist/runtime/smokeMainnetRead.js +529 -0
- package/dist/runtime/smokeMainnetReadAssertions.js +308 -0
- package/dist/runtime/start.js +295 -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/docs/AGENT_BEHAVIOR.md +1 -1
- package/docs/AGENT_DEVELOPMENT_POLICY.md +20 -0
- package/docs/FRONTEND_POLICY.md +4 -3
- package/docs/MCP_SETUP.md +59 -7
- package/docs/MCP_TOOLS.md +1 -1
- package/docs/SDK_API.md +5 -1
- package/package.json +3 -2
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { DeepBookClient, mainnetCoins } from "@mysten/deepbook-v3";
|
|
2
|
+
import { Transaction } from "@mysten/sui/transactions";
|
|
3
|
+
import { parseDeepbookRawU64 } from "../../core/read/deepbookReadHelpers.js";
|
|
4
|
+
import { LocalTransactionMaterialStoreError } from "../../core/session/transactionMaterialStore.js";
|
|
5
|
+
import { suiTransactionDigestSchema } from "../../core/suiAddress.js";
|
|
6
|
+
import { failReviewCheck, passReviewCheck } from "../../core/review/reviewComputationResult.js";
|
|
7
|
+
const DEEPBOOK_SWAP_GAS_BUDGET_MIST = 50000000n;
|
|
8
|
+
const SUI_COIN_TYPE = "0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI";
|
|
9
|
+
export function deepbookSwapBalanceRequirements(input) {
|
|
10
|
+
if (input.sourceCoinType === SUI_COIN_TYPE) {
|
|
11
|
+
return [
|
|
12
|
+
{
|
|
13
|
+
symbol: input.sourceSymbol,
|
|
14
|
+
coinType: SUI_COIN_TYPE,
|
|
15
|
+
decimals: input.sourceDecimals,
|
|
16
|
+
requiredRaw: input.sourceAmountRaw + DEEPBOOK_SWAP_GAS_BUDGET_MIST
|
|
17
|
+
}
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
return [
|
|
21
|
+
{
|
|
22
|
+
symbol: input.sourceSymbol,
|
|
23
|
+
coinType: input.sourceCoinType,
|
|
24
|
+
decimals: input.sourceDecimals,
|
|
25
|
+
requiredRaw: input.sourceAmountRaw
|
|
26
|
+
},
|
|
27
|
+
{ symbol: "SUI", coinType: SUI_COIN_TYPE, decimals: 9, requiredRaw: DEEPBOOK_SWAP_GAS_BUDGET_MIST }
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
function displayRaw(raw, decimals) {
|
|
31
|
+
const base = 10n ** BigInt(decimals);
|
|
32
|
+
const whole = raw / base;
|
|
33
|
+
const fraction = (raw % base).toString().padStart(decimals, "0").replace(/0+$/, "");
|
|
34
|
+
return fraction ? `${whole}.${fraction}` : whole.toString();
|
|
35
|
+
}
|
|
36
|
+
export function createDeepbookSwapTransactionMaterialProducer(options) {
|
|
37
|
+
return async (input) => {
|
|
38
|
+
if (options.network !== "mainnet" || options.chainIdentifier !== options.expectedChainIdentifier) {
|
|
39
|
+
return {
|
|
40
|
+
status: "blocked",
|
|
41
|
+
blockedReason: "network_mismatch",
|
|
42
|
+
checks: [
|
|
43
|
+
failReviewCheck("deepbook_transaction_material_network_mismatch", "Transaction material network", "DeepBook transaction material build requires a verified Sui mainnet gRPC endpoint and matching mainnet chain identifier.", "network")
|
|
44
|
+
]
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (input.poolResolution.direction !== input.quotePolicy.direction || input.quote.direction !== input.quotePolicy.direction) {
|
|
48
|
+
return {
|
|
49
|
+
status: "blocked",
|
|
50
|
+
blockedReason: "amount_mismatch",
|
|
51
|
+
checks: [
|
|
52
|
+
failReviewCheck("deepbook_transaction_material_quote_direction_mismatch", "Transaction material quote policy", "DeepBook transaction material build requires pool direction, quote evidence, and quote policy to describe the same swap direction.", "adapter")
|
|
53
|
+
]
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const quoteExpiresAt = quotePolicyExpiresAt(input.quotePolicy);
|
|
57
|
+
if (quoteExpiresAt.getTime() <= input.now.getTime()) {
|
|
58
|
+
return {
|
|
59
|
+
status: "refresh_required",
|
|
60
|
+
refreshReason: "quote_stale",
|
|
61
|
+
checks: [
|
|
62
|
+
failReviewCheck("deepbook_transaction_material_quote_expired", "Transaction material quote policy", "DeepBook transaction material was not built because the quote policy expired before build.", "quote")
|
|
63
|
+
]
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
let sourceAmountRaw;
|
|
67
|
+
let minOutRaw;
|
|
68
|
+
let deepAmountRaw;
|
|
69
|
+
try {
|
|
70
|
+
sourceAmountRaw = parseDeepbookRawU64(input.quotePolicy.sourceAmountRaw, "sourceAmountRaw", { positive: true });
|
|
71
|
+
minOutRaw = parseDeepbookRawU64(input.quotePolicy.minOutRaw, "minOutRaw", { positive: true });
|
|
72
|
+
deepAmountRaw = parseDeepbookRawU64(input.quotePolicy.deepAmountRaw, "deepAmountRaw");
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
return {
|
|
76
|
+
status: "blocked",
|
|
77
|
+
blockedReason: "amount_mismatch",
|
|
78
|
+
checks: [
|
|
79
|
+
failReviewCheck("deepbook_transaction_material_raw_amount_invalid", "Transaction material quote policy", error instanceof Error ? error.message : "DeepBook transaction material requires valid raw u64 quote policy amounts.", "quote")
|
|
80
|
+
]
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const sourceQuoteAmount = input.quote.rawQuote.inputAmount;
|
|
85
|
+
const requirements = deepbookSwapBalanceRequirements({
|
|
86
|
+
sourceSymbol: sourceQuoteAmount.symbol,
|
|
87
|
+
sourceCoinType: sourceQuoteAmount.coinType,
|
|
88
|
+
sourceDecimals: sourceQuoteAmount.decimals,
|
|
89
|
+
sourceAmountRaw: sourceAmountRaw
|
|
90
|
+
});
|
|
91
|
+
for (const requirement of requirements) {
|
|
92
|
+
const balance = await options.client.core.getBalance({
|
|
93
|
+
owner: input.account,
|
|
94
|
+
coinType: requirement.coinType
|
|
95
|
+
});
|
|
96
|
+
const heldRaw = BigInt(balance.balance.balance);
|
|
97
|
+
if (heldRaw < requirement.requiredRaw) {
|
|
98
|
+
const isGasOnly = requirement.coinType === SUI_COIN_TYPE && sourceQuoteAmount.coinType !== SUI_COIN_TYPE;
|
|
99
|
+
return {
|
|
100
|
+
status: "blocked",
|
|
101
|
+
blockedReason: "insufficient_balance",
|
|
102
|
+
checks: [
|
|
103
|
+
failReviewCheck("deepbook_transaction_material_build_failed", "Transaction material build", `Insufficient balance: this swap needs ${displayRaw(requirement.requiredRaw, requirement.decimals)} ${requirement.symbol}${isGasOnly ? " for gas" : requirement.coinType === SUI_COIN_TYPE ? " (amount + gas)" : ""}, but the account holds ${displayRaw(heldRaw, requirement.decimals)} ${requirement.symbol}. Nothing was signed and no funds moved.`, "adapter")
|
|
104
|
+
]
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const deepbook = new DeepBookClient({
|
|
109
|
+
client: options.client,
|
|
110
|
+
address: input.account,
|
|
111
|
+
network: options.network
|
|
112
|
+
});
|
|
113
|
+
const transaction = new Transaction();
|
|
114
|
+
transaction.setSender(input.account);
|
|
115
|
+
// The DeepBook SDK reserves a 0.25 SUI gas budget by default, which
|
|
116
|
+
// blocks small wallets whose balance is close to the swap amount. A
|
|
117
|
+
// mainnet DeepBook swap costs well under this explicit 0.05 SUI budget.
|
|
118
|
+
transaction.setGasBudget(DEEPBOOK_SWAP_GAS_BUDGET_MIST);
|
|
119
|
+
// input_coin fee mode swaps with an explicit zero DEEP coin; the Move
|
|
120
|
+
// entry point then charges the taker fee in the source coin at the
|
|
121
|
+
// protocol penalty. The explicit zero coin avoids SDK coin selection,
|
|
122
|
+
// which cannot resolve a zero balance for accounts holding no DEEP.
|
|
123
|
+
const inputFeeMode = input.quotePolicy.feeMode === "input_coin";
|
|
124
|
+
const zeroDeepCoin = inputFeeMode
|
|
125
|
+
? transaction.moveCall({
|
|
126
|
+
target: "0x2::coin::zero",
|
|
127
|
+
typeArguments: [mainnetCoins.DEEP.type]
|
|
128
|
+
})
|
|
129
|
+
: undefined;
|
|
130
|
+
const swapDeepAmountRaw = inputFeeMode ? 0n : deepAmountRaw;
|
|
131
|
+
const [baseCoinResult, quoteCoinResult, deepCoinResult] = input.quotePolicy.direction === "base_to_quote"
|
|
132
|
+
? deepbook.deepBook.swapExactBaseForQuote({
|
|
133
|
+
poolKey: input.poolResolution.poolKey,
|
|
134
|
+
amount: sourceAmountRaw,
|
|
135
|
+
minOut: minOutRaw,
|
|
136
|
+
deepAmount: swapDeepAmountRaw,
|
|
137
|
+
...(zeroDeepCoin ? { deepCoin: zeroDeepCoin } : {})
|
|
138
|
+
})(transaction)
|
|
139
|
+
: deepbook.deepBook.swapExactQuoteForBase({
|
|
140
|
+
poolKey: input.poolResolution.poolKey,
|
|
141
|
+
amount: sourceAmountRaw,
|
|
142
|
+
minOut: minOutRaw,
|
|
143
|
+
deepAmount: swapDeepAmountRaw,
|
|
144
|
+
...(zeroDeepCoin ? { deepCoin: zeroDeepCoin } : {})
|
|
145
|
+
})(transaction);
|
|
146
|
+
transaction.transferObjects([baseCoinResult, quoteCoinResult, deepCoinResult], input.account);
|
|
147
|
+
const transactionBytes = await transaction.build({ client: options.client });
|
|
148
|
+
options.materialStore.deleteReviewSessionTransactionMaterials(input.reviewSessionId);
|
|
149
|
+
const handle = options.materialStore.recordTransactionMaterial({
|
|
150
|
+
reviewSessionId: input.reviewSessionId,
|
|
151
|
+
planId: input.plan.id,
|
|
152
|
+
account: input.account,
|
|
153
|
+
kind: "deepbook_swap_transaction_data",
|
|
154
|
+
source: "say_ur_intent_built",
|
|
155
|
+
transactionBytes,
|
|
156
|
+
expiresAt: quoteExpiresAt,
|
|
157
|
+
redactedDiagnostics: {
|
|
158
|
+
protocol: input.plan.protocol,
|
|
159
|
+
adapterId: input.plan.adapterId,
|
|
160
|
+
actionKind: input.plan.actionKind,
|
|
161
|
+
poolKey: input.poolResolution.poolKey,
|
|
162
|
+
direction: input.quotePolicy.direction,
|
|
163
|
+
quoteFetchedAt: input.quotePolicy.fetchedAt,
|
|
164
|
+
quoteExpiresAt: quoteExpiresAt.toISOString(),
|
|
165
|
+
sourceAmountRaw: input.quotePolicy.sourceAmountRaw,
|
|
166
|
+
minOutRaw: input.quotePolicy.minOutRaw,
|
|
167
|
+
deepAmountRaw: input.quotePolicy.deepAmountRaw
|
|
168
|
+
}
|
|
169
|
+
}, input.now);
|
|
170
|
+
return {
|
|
171
|
+
status: "completed",
|
|
172
|
+
evidence: handle,
|
|
173
|
+
checks: [
|
|
174
|
+
passReviewCheck("deepbook_transaction_material_built", "Transaction material build", "Built account-bound DeepBook swap transaction material from Say Ur Intent quote policy and stored the unsigned transaction bytes only in the local material store until quote expiry. This is not wallet handoff, signing data, signing readiness, or execution readiness.", "adapter")
|
|
175
|
+
]
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
const blockedReason = blockedReasonForBuildError(error);
|
|
180
|
+
return {
|
|
181
|
+
status: "blocked",
|
|
182
|
+
blockedReason,
|
|
183
|
+
checks: [
|
|
184
|
+
failReviewCheck("deepbook_transaction_material_build_failed", "Transaction material build", buildFailureMessage(blockedReason), "adapter")
|
|
185
|
+
]
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
export function createDeepbookSwapTransactionMaterialDigestProducer(options) {
|
|
191
|
+
return async (input) => {
|
|
192
|
+
const material = options.materialStore.getTransactionMaterial(input.materialHandle, input.now);
|
|
193
|
+
if (!material) {
|
|
194
|
+
return {
|
|
195
|
+
status: "refresh_required",
|
|
196
|
+
refreshReason: "quote_stale",
|
|
197
|
+
checks: [
|
|
198
|
+
failReviewCheck("deepbook_transaction_material_digest_unavailable", "Transaction material digest", "DeepBook transaction material digest was not computed because the stored local material was unavailable or expired; refresh the review evidence before continuing.", "adapter")
|
|
199
|
+
]
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
let transactionDigest;
|
|
203
|
+
try {
|
|
204
|
+
transactionDigest = await Transaction.from(material.transactionBytes).getDigest();
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return {
|
|
208
|
+
status: "blocked",
|
|
209
|
+
blockedReason: "object_resolution_failed",
|
|
210
|
+
checks: [
|
|
211
|
+
failReviewCheck("deepbook_transaction_material_digest_failed", "Transaction material digest", "DeepBook transaction material digest could not be derived from the stored local transaction bytes.", "adapter")
|
|
212
|
+
]
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const parsedDigest = suiTransactionDigestSchema.safeParse(transactionDigest);
|
|
216
|
+
if (!parsedDigest.success) {
|
|
217
|
+
return {
|
|
218
|
+
status: "blocked",
|
|
219
|
+
blockedReason: "object_resolution_failed",
|
|
220
|
+
checks: [
|
|
221
|
+
failReviewCheck("deepbook_transaction_material_digest_invalid", "Transaction material digest", "DeepBook transaction material digest did not match the pinned Sui SDK transaction digest format.", "adapter")
|
|
222
|
+
]
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
status: "completed",
|
|
227
|
+
evidence: {
|
|
228
|
+
materialId: material.materialId,
|
|
229
|
+
reviewSessionId: material.reviewSessionId,
|
|
230
|
+
planId: material.planId,
|
|
231
|
+
account: material.account,
|
|
232
|
+
kind: material.kind,
|
|
233
|
+
source: material.source,
|
|
234
|
+
digestKind: "sui_transaction_digest",
|
|
235
|
+
transactionDigest: parsedDigest.data,
|
|
236
|
+
computedAt: input.now.toISOString(),
|
|
237
|
+
expiresAt: material.expiresAt
|
|
238
|
+
},
|
|
239
|
+
checks: [
|
|
240
|
+
passReviewCheck("deepbook_transaction_material_digest_commitment", "Transaction material digest", "Derived a Sui transaction digest from the stored local unsigned transaction material. The digest and bytes remain internal until later review stages bind object ownership, human-readable review, and simulation evidence.", "adapter")
|
|
241
|
+
]
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function quotePolicyExpiresAt(policy) {
|
|
246
|
+
return new Date(Date.parse(policy.fetchedAt) + policy.staleAfterMs);
|
|
247
|
+
}
|
|
248
|
+
function blockedReasonForBuildError(error) {
|
|
249
|
+
if (error instanceof LocalTransactionMaterialStoreError) {
|
|
250
|
+
return "object_resolution_failed";
|
|
251
|
+
}
|
|
252
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
253
|
+
if (/insufficient ?coin ?balance|insufficient balance/i.test(message)) {
|
|
254
|
+
return "insufficient_balance";
|
|
255
|
+
}
|
|
256
|
+
if (/gas/i.test(message) && /insufficient|no valid|payment|budget/i.test(message)) {
|
|
257
|
+
return "insufficient_gas";
|
|
258
|
+
}
|
|
259
|
+
return "object_resolution_failed";
|
|
260
|
+
}
|
|
261
|
+
function buildFailureMessage(blockedReason) {
|
|
262
|
+
if (blockedReason === "insufficient_balance") {
|
|
263
|
+
return "DeepBook transaction material build failed before wallet handoff because the account does not have enough source or fee assets.";
|
|
264
|
+
}
|
|
265
|
+
if (blockedReason === "insufficient_gas") {
|
|
266
|
+
return "DeepBook transaction material build failed before wallet handoff because a usable gas payment could not be resolved.";
|
|
267
|
+
}
|
|
268
|
+
return "DeepBook transaction material build failed before wallet handoff because required account-bound objects could not be resolved.";
|
|
269
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { createSwapHumanReadableReviewEvidence } from "../../core/action/swapHumanReadableReviewProjection.js";
|
|
2
|
+
import { failReviewCheck, passReviewCheck } from "../../core/review/reviewComputationResult.js";
|
|
3
|
+
export function createFlowxSwapHumanReadableReviewProducer() {
|
|
4
|
+
return (input) => {
|
|
5
|
+
try {
|
|
6
|
+
assertFlowxHumanReviewSources(input);
|
|
7
|
+
const review = buildFlowxHumanReadableReview(input);
|
|
8
|
+
const evidence = createSwapHumanReadableReviewEvidence({
|
|
9
|
+
transactionMaterial: input.transactionMaterial,
|
|
10
|
+
transactionMaterialDigest: input.transactionMaterialDigest,
|
|
11
|
+
swapQuotePolicy: input.swapQuotePolicy,
|
|
12
|
+
transactionObjectOwnership: input.transactionObjectOwnership,
|
|
13
|
+
adapterId: input.plan.adapterId,
|
|
14
|
+
protocol: input.plan.protocol,
|
|
15
|
+
actionKind: input.plan.actionKind,
|
|
16
|
+
review,
|
|
17
|
+
derivedAt: input.now
|
|
18
|
+
});
|
|
19
|
+
return {
|
|
20
|
+
status: "completed",
|
|
21
|
+
evidence,
|
|
22
|
+
checks: [
|
|
23
|
+
passReviewCheck("flowx_human_readable_review_evidence", "Human-readable review", "Prepared a human-readable account-bound swap review from material-bound quote policy and object ownership evidence. This is not wallet handoff, signing data, signing readiness, or execution readiness.", "adapter")
|
|
24
|
+
]
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return {
|
|
29
|
+
status: "blocked",
|
|
30
|
+
blockedReason: classifyHumanReviewFailure(error),
|
|
31
|
+
checks: [
|
|
32
|
+
failReviewCheck("flowx_human_readable_review_failed", "Human-readable review", error instanceof Error ? error.message : "FlowX human-readable review evidence could not be produced.", "adapter")
|
|
33
|
+
]
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function buildFlowxHumanReadableReview(input) {
|
|
39
|
+
const sourceAmount = amountFromQuotePolicy("input", input.swapQuotePolicy.sourceAmount, {
|
|
40
|
+
displayAmount: input.requestedIntent.from.amountDisplay,
|
|
41
|
+
displayAmountSource: "user_display_intent_not_signing_input"
|
|
42
|
+
});
|
|
43
|
+
const expectedOutput = amountFromQuotePolicy("expected_output", input.swapQuotePolicy.expectedOutput);
|
|
44
|
+
const minimumOutput = amountFromQuotePolicy("minimum_output", input.swapQuotePolicy.minimumOutput);
|
|
45
|
+
const protocolFee = amountFromQuotePolicy("fee", input.swapQuotePolicy.protocolFee);
|
|
46
|
+
return {
|
|
47
|
+
kind: "swap_human_readable_review",
|
|
48
|
+
proposedAction: {
|
|
49
|
+
title: input.plan.title,
|
|
50
|
+
summary: input.plan.summary,
|
|
51
|
+
actionKind: input.plan.actionKind,
|
|
52
|
+
adapterId: input.plan.adapterId,
|
|
53
|
+
protocol: input.plan.protocol,
|
|
54
|
+
network: "sui:mainnet"
|
|
55
|
+
},
|
|
56
|
+
assetFlow: {
|
|
57
|
+
outgoing: [sourceAmount],
|
|
58
|
+
expectedIncoming: [expectedOutput],
|
|
59
|
+
minimumIncoming: [minimumOutput],
|
|
60
|
+
fees: [protocolFee]
|
|
61
|
+
},
|
|
62
|
+
recipients: [
|
|
63
|
+
{ role: "connected_account", address: input.account },
|
|
64
|
+
{ role: "output_recipient", address: input.account }
|
|
65
|
+
],
|
|
66
|
+
targets: [
|
|
67
|
+
{
|
|
68
|
+
kind: "swap_output_asset",
|
|
69
|
+
symbol: input.swapQuotePolicy.expectedOutput.asset.symbol,
|
|
70
|
+
coinType: input.swapQuotePolicy.expectedOutput.asset.coinType,
|
|
71
|
+
protocol: input.plan.protocol,
|
|
72
|
+
poolKey: input.swapQuotePolicy.quoteSource.poolKey,
|
|
73
|
+
direction: input.swapQuotePolicy.quoteSource.direction
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
evidenceUsed: [
|
|
77
|
+
{
|
|
78
|
+
id: "flowx_quote_policy",
|
|
79
|
+
label: "Quote policy",
|
|
80
|
+
source: "quote",
|
|
81
|
+
summary: "Quote policy evidence supplies the input, expected output, minimum output, and slippage policy shown in asset flow. The route was chosen by the FlowX router, not by this review; the review verified the routed pool against the pinned registry and the FlowX CLMM swap fee is charged inside the pool, already reflected in the expected output."
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "transaction_material_digest",
|
|
85
|
+
label: "Transaction material digest",
|
|
86
|
+
source: "digest_commitment",
|
|
87
|
+
summary: "The review is bound internally to the stored local unsigned transaction material digest; the digest and transaction bytes are not public review output."
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "transaction_object_ownership",
|
|
91
|
+
label: "Object ownership",
|
|
92
|
+
source: "wallet",
|
|
93
|
+
summary: "Object ownership evidence is derived from stored transaction data and Sui mainnet object reads."
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
missingEvidence: [
|
|
97
|
+
{
|
|
98
|
+
id: "review_time_simulation",
|
|
99
|
+
label: "Review-time simulation",
|
|
100
|
+
reason: "The review has not simulated the stored transaction material with required effects, balance changes, object types, and transaction fields."
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
requiredUserChoices: [
|
|
104
|
+
{
|
|
105
|
+
id: "wallet_authorization_later",
|
|
106
|
+
label: "Wallet authorization",
|
|
107
|
+
reason: "The wallet signature request happens on this review page after the digest-gated handoff; nothing is signed without your approval in the wallet."
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
unsupportedClaims: [
|
|
111
|
+
{
|
|
112
|
+
id: "no_signing_readiness",
|
|
113
|
+
label: "No signing readiness",
|
|
114
|
+
reason: "Human-readable review evidence does not prove the action is ready to sign."
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "no_execution_readiness",
|
|
118
|
+
label: "No execution readiness",
|
|
119
|
+
reason: "Review-time simulation, wallet handoff, signing, and execution receipt evidence are not complete."
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "no_route_recommendation",
|
|
123
|
+
label: "No route recommendation",
|
|
124
|
+
reason: "The routed pool was selected by the FlowX router; this review verifies the resulting path and on-chain minimum output and does not rank venues or recommend routes."
|
|
125
|
+
}
|
|
126
|
+
],
|
|
127
|
+
freshness: {
|
|
128
|
+
status: "current",
|
|
129
|
+
evaluatedAt: input.now.toISOString(),
|
|
130
|
+
expiresAt: input.transactionMaterial.expiresAt,
|
|
131
|
+
reason: "Human-readable review evidence expires with the stored local transaction material and quote policy."
|
|
132
|
+
},
|
|
133
|
+
blockingChecks: [
|
|
134
|
+
failReviewCheck("flowx_review_time_simulation_missing", "Review-time simulation", "Review-time simulation evidence is still required before any wallet handoff, signing, or execution.", "simulation")
|
|
135
|
+
]
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function assertFlowxHumanReviewSources(input) {
|
|
139
|
+
if (input.swapQuotePolicy.adapterId !== input.plan.adapterId ||
|
|
140
|
+
input.swapQuotePolicy.protocol !== input.plan.protocol ||
|
|
141
|
+
input.swapQuotePolicy.actionKind !== input.plan.actionKind) {
|
|
142
|
+
throw new Error("human-readable review quote policy identity must match the action plan");
|
|
143
|
+
}
|
|
144
|
+
if (input.swapQuotePolicy.quoteSource.poolKey !== input.routedPool.poolKey) {
|
|
145
|
+
throw new Error("human-readable review pool target must match swap quote policy poolKey");
|
|
146
|
+
}
|
|
147
|
+
const expectedDirection = input.quotePolicy.swapXToY ? "base_to_quote" : "quote_to_base";
|
|
148
|
+
if (input.swapQuotePolicy.quoteSource.direction !== expectedDirection) {
|
|
149
|
+
throw new Error("human-readable review target direction must match swap quote policy direction");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function classifyHumanReviewFailure(error) {
|
|
153
|
+
const message = error instanceof Error ? error.message : "";
|
|
154
|
+
if (/object ownership/i.test(message)) {
|
|
155
|
+
return "object_resolution_failed";
|
|
156
|
+
}
|
|
157
|
+
if (/asset|target|pool|direction|protocol|adapter/i.test(message)) {
|
|
158
|
+
return "asset_mismatch";
|
|
159
|
+
}
|
|
160
|
+
if (/amount|quote|slippage|min/i.test(message)) {
|
|
161
|
+
return "amount_mismatch";
|
|
162
|
+
}
|
|
163
|
+
return "unsupported_action";
|
|
164
|
+
}
|
|
165
|
+
function amountFromQuotePolicy(role, amount, display) {
|
|
166
|
+
return {
|
|
167
|
+
role,
|
|
168
|
+
symbol: amount.asset.symbol,
|
|
169
|
+
coinType: amount.asset.coinType,
|
|
170
|
+
decimals: amount.asset.decimals,
|
|
171
|
+
rawAmount: amount.raw,
|
|
172
|
+
rawAmountSource: "quote_policy_evidence",
|
|
173
|
+
...(display?.displayAmount ? { displayAmount: display.displayAmount } : {}),
|
|
174
|
+
...(display?.displayAmountSource ? { displayAmountSource: display.displayAmountSource } : {})
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { accountBoundReviewRequiredCheck, signingViaLocalReviewOnlyCheck } from "../../core/review/reviewChecks.js";
|
|
4
|
+
import { canonicalFlowxSymbol } from "../../core/read/flowxRegistry.js";
|
|
5
|
+
export const FLOWX_SWAP_ACTION_KIND = "swap";
|
|
6
|
+
export const FLOWX_SWAP_ADAPTER_ID = "flowx-swap";
|
|
7
|
+
export const FLOWX_SWAP_PROTOCOL = "FlowXCLMM";
|
|
8
|
+
export const flowxSwapIntentInputSchema = z.object({
|
|
9
|
+
type: z.literal(FLOWX_SWAP_ACTION_KIND),
|
|
10
|
+
from: z.object({
|
|
11
|
+
symbol: z.string().min(1),
|
|
12
|
+
amount: z.string().min(1)
|
|
13
|
+
}),
|
|
14
|
+
to: z.object({
|
|
15
|
+
symbol: z.string().min(1)
|
|
16
|
+
}),
|
|
17
|
+
maxSlippageBps: z.number().int().min(1).max(1000)
|
|
18
|
+
});
|
|
19
|
+
export const flowxSwapRequestedIntentSchema = z.object({
|
|
20
|
+
type: z.literal(FLOWX_SWAP_ACTION_KIND),
|
|
21
|
+
from: z.object({
|
|
22
|
+
symbol: z.string().min(1),
|
|
23
|
+
amountDisplay: z.string().min(1)
|
|
24
|
+
}),
|
|
25
|
+
to: z.object({
|
|
26
|
+
symbol: z.string().min(1)
|
|
27
|
+
}),
|
|
28
|
+
maxSlippageBps: z.number().int().min(1).max(1000)
|
|
29
|
+
});
|
|
30
|
+
export function createFlowxSwapActionPlan(intent, now) {
|
|
31
|
+
const requestedIntent = normalizeFlowxSwapRequestedIntent(intent);
|
|
32
|
+
return {
|
|
33
|
+
id: `plan_${randomUUID()}`,
|
|
34
|
+
actionKind: FLOWX_SWAP_ACTION_KIND,
|
|
35
|
+
adapterId: FLOWX_SWAP_ADAPTER_ID,
|
|
36
|
+
protocol: FLOWX_SWAP_PROTOCOL,
|
|
37
|
+
title: `Review ${requestedIntent.from.amountDisplay} ${requestedIntent.from.symbol} to ${requestedIntent.to.symbol}`,
|
|
38
|
+
summary: "Account-bound FlowX review evidence is computed after a wallet account is connected. The review URL displays the proposal and local review evidence; when every evidence stage completes, the review page offers user-controlled wallet signing through a digest-gated handoff. This MCP response contains no sign action, signing data, transaction bytes, or signing readiness.",
|
|
39
|
+
assetFlowPreview: {
|
|
40
|
+
outgoing: [
|
|
41
|
+
{ symbol: requestedIntent.from.symbol, amount: requestedIntent.from.amountDisplay, amountKind: "display_intent" }
|
|
42
|
+
],
|
|
43
|
+
expectedIncoming: [
|
|
44
|
+
{ symbol: requestedIntent.to.symbol, amount: "unknown", amountKind: "display_intent", approx: true }
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
adapterData: {
|
|
48
|
+
requestedIntent
|
|
49
|
+
},
|
|
50
|
+
createdAt: now.toISOString(),
|
|
51
|
+
preliminaryChecks: [accountBoundReviewRequiredCheck(), signingViaLocalReviewOnlyCheck()]
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export const flowxSwapActionPlanDataSchema = z.object({
|
|
55
|
+
requestedIntent: flowxSwapRequestedIntentSchema
|
|
56
|
+
});
|
|
57
|
+
export const flowxSwapActionPlanIdentitySchema = z.object({
|
|
58
|
+
actionKind: z.literal(FLOWX_SWAP_ACTION_KIND),
|
|
59
|
+
adapterId: z.literal(FLOWX_SWAP_ADAPTER_ID),
|
|
60
|
+
protocol: z.literal(FLOWX_SWAP_PROTOCOL)
|
|
61
|
+
}).passthrough();
|
|
62
|
+
export function isFlowxSwapActionPlanIdentity(plan) {
|
|
63
|
+
return flowxSwapActionPlanIdentitySchema.safeParse(plan).success;
|
|
64
|
+
}
|
|
65
|
+
export function normalizeFlowxSwapRequestedIntent(input) {
|
|
66
|
+
const fromSymbol = canonicalFlowxSymbol(input.from.symbol)?.symbol ?? input.from.symbol.trim();
|
|
67
|
+
const toSymbol = canonicalFlowxSymbol(input.to.symbol)?.symbol ?? input.to.symbol.trim();
|
|
68
|
+
return {
|
|
69
|
+
type: FLOWX_SWAP_ACTION_KIND,
|
|
70
|
+
from: {
|
|
71
|
+
symbol: fromSymbol,
|
|
72
|
+
amountDisplay: input.from.amount
|
|
73
|
+
},
|
|
74
|
+
to: {
|
|
75
|
+
symbol: toSymbol
|
|
76
|
+
},
|
|
77
|
+
maxSlippageBps: input.maxSlippageBps
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { parseDeepbookRawU64 } from "../../core/read/deepbookReadHelpers.js";
|
|
2
|
+
export const FLOWX_REVIEW_QUOTE_STALE_AFTER_MS = 30_000;
|
|
3
|
+
// Same-machine pipelines capture `now` before the network quote fetch, so the
|
|
4
|
+
// fetch timestamp can land slightly "in the future". Treat skew within this
|
|
5
|
+
// tolerance as a fresh quote; beyond it is a clock-integrity violation.
|
|
6
|
+
export const FLOWX_QUOTE_FUTURE_SKEW_TOLERANCE_MS = 5_000;
|
|
7
|
+
export const FLOWX_MAX_SLIPPAGE_BPS = 1000;
|
|
8
|
+
export const FLOWX_MIN_SLIPPAGE_BPS = 1;
|
|
9
|
+
const BPS_DENOMINATOR = 10000n;
|
|
10
|
+
// The FlowX universal router expresses slippage on a 1e6 denominator; one
|
|
11
|
+
// basis point is 100 router units.
|
|
12
|
+
export const FLOWX_ROUTER_SLIPPAGE_UNITS_PER_BPS = 100;
|
|
13
|
+
export function deriveFlowxSwapQuotePolicy(input) {
|
|
14
|
+
assertSlippageBps(input.maxSlippageBps);
|
|
15
|
+
const staleAfterMs = input.staleAfterMs ?? FLOWX_REVIEW_QUOTE_STALE_AFTER_MS;
|
|
16
|
+
if (!Number.isInteger(staleAfterMs) || staleAfterMs < 1) {
|
|
17
|
+
throw new Error("staleAfterMs must be a positive integer");
|
|
18
|
+
}
|
|
19
|
+
const fetchedAtMs = parseIsoUtcTimestampMs(input.fetchedAt, "fetchedAt");
|
|
20
|
+
const nowMs = parseValidDateMs(input.now, "now");
|
|
21
|
+
const rawQuoteAgeMs = nowMs - fetchedAtMs;
|
|
22
|
+
if (rawQuoteAgeMs < -FLOWX_QUOTE_FUTURE_SKEW_TOLERANCE_MS) {
|
|
23
|
+
return {
|
|
24
|
+
status: "refresh_required",
|
|
25
|
+
refreshReason: "quote_stale",
|
|
26
|
+
reason: "quote_timestamp_in_future",
|
|
27
|
+
swapXToY: input.swapXToY,
|
|
28
|
+
fetchedAt: input.fetchedAt,
|
|
29
|
+
quoteAgeMs: 0,
|
|
30
|
+
staleAfterMs,
|
|
31
|
+
clockSkewMs: -rawQuoteAgeMs
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const clockSkewMs = rawQuoteAgeMs < 0 ? -rawQuoteAgeMs : undefined;
|
|
35
|
+
const quoteAgeMs = rawQuoteAgeMs < 0 ? 0 : rawQuoteAgeMs;
|
|
36
|
+
if (quoteAgeMs > staleAfterMs) {
|
|
37
|
+
return {
|
|
38
|
+
status: "refresh_required",
|
|
39
|
+
refreshReason: "quote_stale",
|
|
40
|
+
reason: "quote_stale",
|
|
41
|
+
swapXToY: input.swapXToY,
|
|
42
|
+
fetchedAt: input.fetchedAt,
|
|
43
|
+
quoteAgeMs,
|
|
44
|
+
staleAfterMs
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const expectedOut = parseDeepbookRawU64(input.amountOutRaw, "amountOutRaw");
|
|
48
|
+
if (expectedOut === 0n) {
|
|
49
|
+
return unavailableQuote(input, quoteAgeMs, staleAfterMs, "zero_expected_output");
|
|
50
|
+
}
|
|
51
|
+
const minOut = (expectedOut * (BPS_DENOMINATOR - BigInt(input.maxSlippageBps))) / BPS_DENOMINATOR;
|
|
52
|
+
if (minOut === 0n) {
|
|
53
|
+
return unavailableQuote(input, quoteAgeMs, staleAfterMs, "zero_min_out");
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
status: "ok",
|
|
57
|
+
swapXToY: input.swapXToY,
|
|
58
|
+
quoteFresh: true,
|
|
59
|
+
fetchedAt: input.fetchedAt,
|
|
60
|
+
quoteAgeMs,
|
|
61
|
+
staleAfterMs,
|
|
62
|
+
...(clockSkewMs !== undefined ? { clockSkewMs } : {}),
|
|
63
|
+
maxSlippageBps: input.maxSlippageBps,
|
|
64
|
+
routerSlippageUnits: input.maxSlippageBps * FLOWX_ROUTER_SLIPPAGE_UNITS_PER_BPS,
|
|
65
|
+
deadlineMsEpoch: fetchedAtMs + staleAfterMs,
|
|
66
|
+
sourceAmountRaw: parseDeepbookRawU64(input.amountInRaw, "amountInRaw", { positive: true }).toString(),
|
|
67
|
+
expectedOutRaw: expectedOut.toString(),
|
|
68
|
+
minOutRaw: minOut.toString()
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function assertSlippageBps(value) {
|
|
72
|
+
if (!Number.isInteger(value) || value < FLOWX_MIN_SLIPPAGE_BPS || value > FLOWX_MAX_SLIPPAGE_BPS) {
|
|
73
|
+
throw new Error(`maxSlippageBps must be an integer from ${FLOWX_MIN_SLIPPAGE_BPS} to ${FLOWX_MAX_SLIPPAGE_BPS}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function parseIsoUtcTimestampMs(value, field) {
|
|
77
|
+
const parsed = new Date(value);
|
|
78
|
+
const timestampMs = parsed.getTime();
|
|
79
|
+
if (!Number.isFinite(timestampMs) || parsed.toISOString() !== value) {
|
|
80
|
+
throw new Error(`${field} must be an ISO 8601 UTC timestamp`);
|
|
81
|
+
}
|
|
82
|
+
return timestampMs;
|
|
83
|
+
}
|
|
84
|
+
function parseValidDateMs(value, field) {
|
|
85
|
+
if (!(value instanceof Date)) {
|
|
86
|
+
throw new Error(`${field} must be a valid Date`);
|
|
87
|
+
}
|
|
88
|
+
const timestampMs = value.getTime();
|
|
89
|
+
if (!Number.isFinite(timestampMs)) {
|
|
90
|
+
throw new Error(`${field} must be a valid Date`);
|
|
91
|
+
}
|
|
92
|
+
return timestampMs;
|
|
93
|
+
}
|
|
94
|
+
function unavailableQuote(input, quoteAgeMs, staleAfterMs, reason) {
|
|
95
|
+
return {
|
|
96
|
+
status: "refresh_required",
|
|
97
|
+
refreshReason: "quote_unavailable",
|
|
98
|
+
reason,
|
|
99
|
+
swapXToY: input.swapXToY,
|
|
100
|
+
fetchedAt: input.fetchedAt,
|
|
101
|
+
quoteAgeMs,
|
|
102
|
+
staleAfterMs
|
|
103
|
+
};
|
|
104
|
+
}
|