otto-intel-mcp 0.1.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/README.md +102 -0
- package/dist/adapter/cdp-signer.d.ts +133 -0
- package/dist/adapter/cdp-signer.js +356 -0
- package/dist/adapter/cdp-signer.js.map +1 -0
- package/dist/adapter/chain.d.ts +36 -0
- package/dist/adapter/chain.js +65 -0
- package/dist/adapter/chain.js.map +1 -0
- package/dist/adapter/erc20.d.ts +39 -0
- package/dist/adapter/erc20.js +17 -0
- package/dist/adapter/erc20.js.map +1 -0
- package/dist/adapter/lifi-decode.d.ts +52 -0
- package/dist/adapter/lifi-decode.js +149 -0
- package/dist/adapter/lifi-decode.js.map +1 -0
- package/dist/adapter/refusal.d.ts +21 -0
- package/dist/adapter/refusal.js +55 -0
- package/dist/adapter/refusal.js.map +1 -0
- package/dist/adapter/sent-step.d.ts +15 -0
- package/dist/adapter/sent-step.js +6 -0
- package/dist/adapter/sent-step.js.map +1 -0
- package/dist/adapter/verify.d.ts +149 -0
- package/dist/adapter/verify.js +432 -0
- package/dist/adapter/verify.js.map +1 -0
- package/dist/adapter-cdp-index.d.ts +6 -0
- package/dist/adapter-cdp-index.js +7 -0
- package/dist/adapter-index.d.ts +15 -0
- package/dist/adapter-index.js +15 -0
- package/dist/artifact-id.d.ts +16 -0
- package/dist/artifact-id.js +60 -0
- package/dist/boot-redaction.d.ts +16 -0
- package/dist/boot-redaction.js +54 -0
- package/dist/data-source.d.ts +7 -0
- package/dist/data-source.js +2 -0
- package/dist/errors.d.ts +70 -0
- package/dist/errors.js +221 -0
- package/dist/execution-config.d.ts +232 -0
- package/dist/execution-config.js +443 -0
- package/dist/execution-delegated-definition.d.ts +165 -0
- package/dist/execution-delegated-definition.js +116 -0
- package/dist/execution-delegation-admin-definition.d.ts +208 -0
- package/dist/execution-delegation-admin-definition.js +170 -0
- package/dist/execution-delegation-admin.d.ts +74 -0
- package/dist/execution-delegation-admin.js +290 -0
- package/dist/execution-delegation-policy.d.ts +257 -0
- package/dist/execution-delegation-policy.js +279 -0
- package/dist/execution-delegation.d.ts +190 -0
- package/dist/execution-delegation.js +545 -0
- package/dist/execution-errors.d.ts +9 -0
- package/dist/execution-errors.js +128 -0
- package/dist/execution-index.d.ts +17 -0
- package/dist/execution-index.js +16 -0
- package/dist/execution-intent.d.ts +14 -0
- package/dist/execution-intent.js +36 -0
- package/dist/execution-registration.d.ts +113 -0
- package/dist/execution-registration.js +161 -0
- package/dist/execution-tool-definitions.d.ts +1103 -0
- package/dist/execution-tool-definitions.js +1051 -0
- package/dist/execution-tools.d.ts +85 -0
- package/dist/execution-tools.js +690 -0
- package/dist/execution-types.d.ts +274 -0
- package/dist/execution-types.js +157 -0
- package/dist/hyperliquid-info-client.d.ts +31 -0
- package/dist/hyperliquid-info-client.js +121 -0
- package/dist/hyperliquid-order-assertions.d.ts +89 -0
- package/dist/hyperliquid-order-assertions.js +325 -0
- package/dist/hyperliquid-order-builder.d.ts +47 -0
- package/dist/hyperliquid-order-builder.js +55 -0
- package/dist/lifi-bridge-assertions.d.ts +49 -0
- package/dist/lifi-bridge-assertions.js +303 -0
- package/dist/lifi-execution-client.d.ts +74 -0
- package/dist/lifi-execution-client.js +165 -0
- package/dist/lifi-fee-assertions.d.ts +32 -0
- package/dist/lifi-fee-assertions.js +103 -0
- package/dist/lifi-swap-assertions.d.ts +32 -0
- package/dist/lifi-swap-assertions.js +214 -0
- package/dist/local-config.d.ts +9 -0
- package/dist/local-config.js +45 -0
- package/dist/mcp-server.d.ts +4 -0
- package/dist/mcp-server.js +44 -0
- package/dist/polymarket-clob-client.d.ts +73 -0
- package/dist/polymarket-clob-client.js +186 -0
- package/dist/polymarket-order-assertions.d.ts +60 -0
- package/dist/polymarket-order-assertions.js +273 -0
- package/dist/polymarket-order-builder.d.ts +43 -0
- package/dist/polymarket-order-builder.js +74 -0
- package/dist/prepared-artifacts.d.ts +45 -0
- package/dist/prepared-artifacts.js +92 -0
- package/dist/stdio-main.d.ts +2 -0
- package/dist/stdio-main.js +20 -0
- package/dist/tool-definitions.d.ts +20 -0
- package/dist/tool-definitions.js +162 -0
- package/dist/x402-read-source.d.ts +11 -0
- package/dist/x402-read-source.js +77 -0
- package/package.json +162 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { decodeAbiParameters, encodeAbiParameters, getAddress, parseAbiParameters, } from 'viem';
|
|
2
|
+
import { attributedFeeAmount } from './execution-config.js';
|
|
3
|
+
import { isExecutionRefusal, refuse } from './execution-errors.js';
|
|
4
|
+
const FORWARD_FEES_BODY = parseAbiParameters('address token, (address recipient, uint256 amount)[] distributions');
|
|
5
|
+
/** `FeeCollector.collectTokenFees(...)` — the only calldata a reviewed fee-forwarding leg may carry. */
|
|
6
|
+
export const FORWARD_ERC20_FEES_SELECTOR = '0x332d746b';
|
|
7
|
+
function attributionRefuse(message) {
|
|
8
|
+
return refuse('ATTRIBUTION_MISSING', message);
|
|
9
|
+
}
|
|
10
|
+
function argRegion(data) {
|
|
11
|
+
return `0x${data.slice(10)}`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Decode a `collectTokenFees` body. Shared by the same-chain swap and cross-chain bridge
|
|
15
|
+
* constructors so the fund-critical attribution shape has exactly one implementation: two copies
|
|
16
|
+
* of this check would drift, and a drifted copy is an unattributed or over-charged payload.
|
|
17
|
+
*/
|
|
18
|
+
export function decodeFeeDistributions(callData) {
|
|
19
|
+
if (callData.slice(0, 10).toLowerCase() !== FORWARD_ERC20_FEES_SELECTOR) {
|
|
20
|
+
return attributionRefuse('LI.FI fee leg does not call forwardERC20Fees');
|
|
21
|
+
}
|
|
22
|
+
const args = argRegion(callData);
|
|
23
|
+
try {
|
|
24
|
+
const decoded = decodeAbiParameters(FORWARD_FEES_BODY, args);
|
|
25
|
+
if (encodeAbiParameters(FORWARD_FEES_BODY, decoded).toLowerCase() !== args.toLowerCase()) {
|
|
26
|
+
attributionRefuse('LI.FI fee calldata is non-canonical or carries trailing bytes');
|
|
27
|
+
}
|
|
28
|
+
const [token, distributions] = decoded;
|
|
29
|
+
if (distributions.length === 0 || distributions.length > 8) {
|
|
30
|
+
attributionRefuse('LI.FI fee calldata has an unexpected distribution count');
|
|
31
|
+
}
|
|
32
|
+
for (const distribution of distributions) {
|
|
33
|
+
if (getAddress(distribution.recipient) ===
|
|
34
|
+
getAddress('0x0000000000000000000000000000000000000000')) {
|
|
35
|
+
attributionRefuse('LI.FI fee calldata has a zero recipient');
|
|
36
|
+
}
|
|
37
|
+
if (distribution.amount <= 0n) {
|
|
38
|
+
attributionRefuse('LI.FI fee calldata has a non-positive amount');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { token: getAddress(token), distributions };
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (isExecutionRefusal(error))
|
|
45
|
+
throw error;
|
|
46
|
+
return attributionRefuse('LI.FI fee calldata could not be decoded');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Assert the decoded fee body carries exactly the versioned recipient set, the exact configured
|
|
51
|
+
* Otto amount, and an aggregate within the configured cap. Assert-what-you-built: this runs on the
|
|
52
|
+
* serialized calldata that is about to be returned, not on the values the constructor intended.
|
|
53
|
+
*/
|
|
54
|
+
export function assertFeeDistributionBody(input) {
|
|
55
|
+
const feeBody = decodeFeeDistributions(input.callData);
|
|
56
|
+
if (feeBody.token !== input.expectedToken) {
|
|
57
|
+
attributionRefuse('LI.FI fee-forwarding body names a different token');
|
|
58
|
+
}
|
|
59
|
+
const expectedOttoFee = attributedFeeAmount(input.grossAmount, input.config.lifiFee);
|
|
60
|
+
if (expectedOttoFee <= 0n) {
|
|
61
|
+
refuse('UNSUPPORTED_LIFI_ROUTE', 'input amount is too small to encode the configured Otto fee');
|
|
62
|
+
}
|
|
63
|
+
const allowedFeeRecipients = input.config.lifiAllowedFeeRecipients.map((recipient) => getAddress(recipient));
|
|
64
|
+
const allowedFeeRecipientSet = new Set(allowedFeeRecipients);
|
|
65
|
+
if (allowedFeeRecipients.length === 0 ||
|
|
66
|
+
allowedFeeRecipientSet.size !== allowedFeeRecipients.length ||
|
|
67
|
+
!allowedFeeRecipientSet.has(input.config.lifiFeeRecipient)) {
|
|
68
|
+
refuse('UNSUPPORTED_LIFI_ROUTE', 'LI.FI fee-recipient configuration is invalid');
|
|
69
|
+
}
|
|
70
|
+
const recipientCounts = new Map();
|
|
71
|
+
let aggregateFeeAmount = 0n;
|
|
72
|
+
for (const distribution of feeBody.distributions) {
|
|
73
|
+
const recipient = getAddress(distribution.recipient);
|
|
74
|
+
if (!allowedFeeRecipientSet.has(recipient)) {
|
|
75
|
+
attributionRefuse('LI.FI fee-forwarding body contains an unrecognized recipient');
|
|
76
|
+
}
|
|
77
|
+
recipientCounts.set(recipient, (recipientCounts.get(recipient) ?? 0) + 1);
|
|
78
|
+
aggregateFeeAmount += distribution.amount;
|
|
79
|
+
}
|
|
80
|
+
if (feeBody.distributions.length !== allowedFeeRecipients.length ||
|
|
81
|
+
allowedFeeRecipients.some((recipient) => recipientCounts.get(recipient) !== 1)) {
|
|
82
|
+
attributionRefuse('LI.FI fee-forwarding body does not match the versioned recipient set');
|
|
83
|
+
}
|
|
84
|
+
const ottoDistribution = feeBody.distributions.find((distribution) => getAddress(distribution.recipient) === input.config.lifiFeeRecipient);
|
|
85
|
+
if (ottoDistribution?.amount !== expectedOttoFee) {
|
|
86
|
+
attributionRefuse('LI.FI fee-forwarding body does not contain the exact configured Otto fee');
|
|
87
|
+
}
|
|
88
|
+
const aggregateFeeCap = attributedFeeAmount(input.grossAmount, input.config.lifiAggregateFeeCap);
|
|
89
|
+
if (aggregateFeeCap <= 0n) {
|
|
90
|
+
refuse('UNSUPPORTED_LIFI_ROUTE', 'input amount is too small to encode the configured aggregate fee cap');
|
|
91
|
+
}
|
|
92
|
+
if (aggregateFeeAmount > aggregateFeeCap) {
|
|
93
|
+
// Everything above has already passed: every recipient is in the versioned set, the set is
|
|
94
|
+
// complete and duplicate-free, and Otto's own distribution carries the exact configured
|
|
95
|
+
// amount. So an overage HERE is upstream take above the reviewed rate, never a missing or
|
|
96
|
+
// misdirected Otto fee — and it gets its own code for that reason. The cap has zero headroom
|
|
97
|
+
// over the live aggregate by design (see LIFI_FEE_CAP_EXCEEDED), so this is the branch a
|
|
98
|
+
// LI.FI repricing lands on, and it must degrade loudly under its own name rather than
|
|
99
|
+
// masquerading as a fee-integrity failure on whatever watches these refusals.
|
|
100
|
+
refuse('LIFI_FEE_CAP_EXCEEDED', `LI.FI fee body distributes ${aggregateFeeAmount} against the configured cap of ${aggregateFeeCap} on a gross of ${input.grossAmount}; the Otto attribution is exact, so the overage is upstream take`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=lifi-fee-assertions.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type Address, type Hex } from 'viem';
|
|
2
|
+
import { type ExecutionServiceConfig } from './execution-config.js';
|
|
3
|
+
import type { AccountProfile, ExecutionAssertion } from './execution-types.js';
|
|
4
|
+
import { FORWARD_ERC20_FEES_SELECTOR } from './lifi-fee-assertions.js';
|
|
5
|
+
import type { LiFiQuote } from './lifi-execution-client.js';
|
|
6
|
+
export declare const LIFI_SWAP_SELECTORS: Readonly<{
|
|
7
|
+
singleErc20ToErc20: Hex;
|
|
8
|
+
multipleErc20ToErc20: Hex;
|
|
9
|
+
}>;
|
|
10
|
+
/** Re-exported so existing importers keep one selector constant across both LI.FI constructors. */
|
|
11
|
+
export { FORWARD_ERC20_FEES_SELECTOR };
|
|
12
|
+
export interface LiFiSwapAssertionInput {
|
|
13
|
+
readonly quote: LiFiQuote;
|
|
14
|
+
readonly profile: AccountProfile;
|
|
15
|
+
readonly chainId: number;
|
|
16
|
+
readonly fromToken: Address;
|
|
17
|
+
readonly toToken: Address;
|
|
18
|
+
readonly fromAmount: bigint;
|
|
19
|
+
readonly maxSlippageBps: number;
|
|
20
|
+
readonly minAmountOut?: bigint;
|
|
21
|
+
readonly config: ExecutionServiceConfig;
|
|
22
|
+
}
|
|
23
|
+
export interface AssertedLiFiSwap {
|
|
24
|
+
readonly approvalSpender: Address;
|
|
25
|
+
readonly transaction: {
|
|
26
|
+
readonly to: Address;
|
|
27
|
+
readonly value: '0';
|
|
28
|
+
readonly data: Hex;
|
|
29
|
+
};
|
|
30
|
+
readonly assertions: readonly ExecutionAssertion[];
|
|
31
|
+
}
|
|
32
|
+
export declare function decodeAndAssertLiFiSwap(input: LiFiSwapAssertionInput): AssertedLiFiSwap;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { decodeAbiParameters, encodeAbiParameters, getAddress, parseAbiParameters, } from 'viem';
|
|
2
|
+
import { LIFI_DIAMOND_ADDRESS, isReviewedLiFiFeeForwarder, reviewedLiFiFeeForwarders, unreviewedFeeForwarderMessage, LIFI_INTEGRATOR, } from './execution-config.js';
|
|
3
|
+
import { isExecutionRefusal, refuse } from './execution-errors.js';
|
|
4
|
+
import { assertFeeDistributionBody, FORWARD_ERC20_FEES_SELECTOR, } from './lifi-fee-assertions.js';
|
|
5
|
+
const SWAP_DATA_TUPLE = '(address callTo, address approveTo, address sendingAssetId, address receivingAssetId, uint256 fromAmount, bytes callData, bool requiresDeposit)';
|
|
6
|
+
const LIFI_SINGLE_PARAMS = parseAbiParameters(`bytes32 _transactionId, string _integrator, string _referrer, address _receiver, uint256 _minAmount, ${SWAP_DATA_TUPLE} _swapData`);
|
|
7
|
+
const LIFI_MULTIPLE_PARAMS = parseAbiParameters(`bytes32 _transactionId, string _integrator, string _referrer, address _receiver, uint256 _minAmount, ${SWAP_DATA_TUPLE}[] _swapData`);
|
|
8
|
+
export const LIFI_SWAP_SELECTORS = Object.freeze({
|
|
9
|
+
singleErc20ToErc20: '0x4666fc80',
|
|
10
|
+
multipleErc20ToErc20: '0x5fd9ae2e',
|
|
11
|
+
});
|
|
12
|
+
/** Re-exported so existing importers keep one selector constant across both LI.FI constructors. */
|
|
13
|
+
export { FORWARD_ERC20_FEES_SELECTOR };
|
|
14
|
+
function quoteRefuse(message) {
|
|
15
|
+
return refuse('LIFI_QUOTE_INVALID', message);
|
|
16
|
+
}
|
|
17
|
+
function attributionRefuse(message) {
|
|
18
|
+
return refuse('ATTRIBUTION_MISSING', message);
|
|
19
|
+
}
|
|
20
|
+
function asAddress(value, field) {
|
|
21
|
+
if (typeof value !== 'string')
|
|
22
|
+
quoteRefuse(`${field} is missing`);
|
|
23
|
+
try {
|
|
24
|
+
return getAddress(value);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return quoteRefuse(`${field} is not a valid EVM address`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function positiveBigInt(value, field) {
|
|
31
|
+
if (typeof value !== 'string' || !/^[1-9]\d*$/.test(value)) {
|
|
32
|
+
return quoteRefuse(`${field} must be a positive decimal integer`);
|
|
33
|
+
}
|
|
34
|
+
return BigInt(value);
|
|
35
|
+
}
|
|
36
|
+
function transactionData(value) {
|
|
37
|
+
if (typeof value !== 'string' || !/^0x(?:[0-9a-fA-F]{2}){4,}$/.test(value)) {
|
|
38
|
+
return quoteRefuse('transaction calldata is missing or malformed');
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
function argRegion(data) {
|
|
43
|
+
return `0x${data.slice(10)}`;
|
|
44
|
+
}
|
|
45
|
+
function decodeSwap(data) {
|
|
46
|
+
const selector = data.slice(0, 10).toLowerCase();
|
|
47
|
+
const args = argRegion(data);
|
|
48
|
+
try {
|
|
49
|
+
if (selector === LIFI_SWAP_SELECTORS.singleErc20ToErc20) {
|
|
50
|
+
const decoded = decodeAbiParameters(LIFI_SINGLE_PARAMS, args);
|
|
51
|
+
if (encodeAbiParameters(LIFI_SINGLE_PARAMS, decoded).toLowerCase() !== args.toLowerCase()) {
|
|
52
|
+
quoteRefuse('single-swap calldata is non-canonical or carries trailing bytes');
|
|
53
|
+
}
|
|
54
|
+
const [, integrator, , receiver, minAmount, leg] = decoded;
|
|
55
|
+
return { integrator, receiver: getAddress(receiver), minAmount, legs: [leg] };
|
|
56
|
+
}
|
|
57
|
+
if (selector === LIFI_SWAP_SELECTORS.multipleErc20ToErc20) {
|
|
58
|
+
const decoded = decodeAbiParameters(LIFI_MULTIPLE_PARAMS, args);
|
|
59
|
+
if (encodeAbiParameters(LIFI_MULTIPLE_PARAMS, decoded).toLowerCase() !== args.toLowerCase()) {
|
|
60
|
+
quoteRefuse('multiple-swap calldata is non-canonical or carries trailing bytes');
|
|
61
|
+
}
|
|
62
|
+
const [, integrator, , receiver, minAmount, legs] = decoded;
|
|
63
|
+
return { integrator, receiver: getAddress(receiver), minAmount, legs };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (isExecutionRefusal(error))
|
|
68
|
+
throw error;
|
|
69
|
+
return quoteRefuse('transaction calldata could not be decoded as GenericSwapV3 ERC20-to-ERC20');
|
|
70
|
+
}
|
|
71
|
+
return refuse('UNSUPPORTED_LIFI_ROUTE', `LI.FI selector ${selector} is not supported by the v1 ERC20-to-ERC20 route set`);
|
|
72
|
+
}
|
|
73
|
+
function assertQuoteBinding(input) {
|
|
74
|
+
const { action, estimate, transactionRequest } = input.quote;
|
|
75
|
+
if (action.fromChainId !== input.chainId || action.toChainId !== input.chainId) {
|
|
76
|
+
quoteRefuse('LI.FI quote is not bound to the requested same chain');
|
|
77
|
+
}
|
|
78
|
+
if (action.fromAmount !== input.fromAmount.toString()) {
|
|
79
|
+
quoteRefuse('LI.FI quote input amount does not match the intent');
|
|
80
|
+
}
|
|
81
|
+
if (asAddress(action.fromToken.address, 'quote input token') !== input.fromToken ||
|
|
82
|
+
asAddress(action.toToken.address, 'quote output token') !== input.toToken) {
|
|
83
|
+
quoteRefuse('LI.FI quote token pair does not match the intent');
|
|
84
|
+
}
|
|
85
|
+
const account = getAddress(input.profile.address);
|
|
86
|
+
if (asAddress(action.fromAddress, 'quote sender') !== account ||
|
|
87
|
+
asAddress(action.toAddress, 'quote receiver') !== account) {
|
|
88
|
+
return refuse('ACCOUNT_BINDING_MISMATCH', 'LI.FI quote sender or receiver is not the bound account');
|
|
89
|
+
}
|
|
90
|
+
if (Math.round(action.slippage * 10_000) !== input.maxSlippageBps) {
|
|
91
|
+
quoteRefuse('LI.FI quote slippage does not match the requested bound');
|
|
92
|
+
}
|
|
93
|
+
const approvalSpender = asAddress(estimate.approvalAddress, 'LI.FI approval target');
|
|
94
|
+
const transactionTarget = asAddress(transactionRequest.to, 'LI.FI transaction target');
|
|
95
|
+
if (approvalSpender !== LIFI_DIAMOND_ADDRESS || transactionTarget !== LIFI_DIAMOND_ADDRESS) {
|
|
96
|
+
quoteRefuse('LI.FI approval or transaction target is not the reviewed Diamond');
|
|
97
|
+
}
|
|
98
|
+
if (transactionRequest.from !== undefined &&
|
|
99
|
+
asAddress(transactionRequest.from, 'LI.FI transaction sender') !== account) {
|
|
100
|
+
return refuse('ACCOUNT_BINDING_MISMATCH', 'LI.FI transaction sender is not the bound account');
|
|
101
|
+
}
|
|
102
|
+
let transactionValue;
|
|
103
|
+
try {
|
|
104
|
+
transactionValue = BigInt(transactionRequest.value);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return quoteRefuse('LI.FI transaction value is invalid');
|
|
108
|
+
}
|
|
109
|
+
if (transactionValue !== 0n) {
|
|
110
|
+
quoteRefuse('ERC20-input LI.FI swap unexpectedly transfers native value');
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
approvalSpender,
|
|
114
|
+
transactionTarget,
|
|
115
|
+
transactionValue,
|
|
116
|
+
data: transactionData(transactionRequest.data),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
export function decodeAndAssertLiFiSwap(input) {
|
|
120
|
+
if (input.profile.kind !== 'eoa' && input.profile.kind !== 'safe') {
|
|
121
|
+
return refuse('UNSUPPORTED_ACCOUNT_PROFILE', 'only EOA and Safe account profiles are supported');
|
|
122
|
+
}
|
|
123
|
+
if (input.profile.chainId !== input.chainId) {
|
|
124
|
+
return refuse('ACCOUNT_BINDING_MISMATCH', 'account profile chain does not match swap chain');
|
|
125
|
+
}
|
|
126
|
+
const feeForwarders = reviewedLiFiFeeForwarders(input.chainId);
|
|
127
|
+
if (!feeForwarders) {
|
|
128
|
+
return refuse('UNSUPPORTED_LIFI_ROUTE', 'chain is outside the reviewed LI.FI v1 route set');
|
|
129
|
+
}
|
|
130
|
+
const boundQuote = assertQuoteBinding(input);
|
|
131
|
+
const decoded = decodeSwap(boundQuote.data);
|
|
132
|
+
if (decoded.legs.length === 0)
|
|
133
|
+
quoteRefuse('LI.FI calldata has no swap legs');
|
|
134
|
+
if (decoded.integrator !== LIFI_INTEGRATOR) {
|
|
135
|
+
attributionRefuse('LI.FI calldata does not carry the exact Otto integrator');
|
|
136
|
+
}
|
|
137
|
+
if (getAddress(decoded.receiver) !== getAddress(input.profile.address)) {
|
|
138
|
+
return refuse('ACCOUNT_BINDING_MISMATCH', 'LI.FI calldata receiver is not the bound account');
|
|
139
|
+
}
|
|
140
|
+
let deposited = 0n;
|
|
141
|
+
for (const leg of decoded.legs) {
|
|
142
|
+
if (leg.requiresDeposit) {
|
|
143
|
+
if (getAddress(leg.sendingAssetId) !== input.fromToken) {
|
|
144
|
+
quoteRefuse('LI.FI calldata contains a foreign-token deposit leg');
|
|
145
|
+
}
|
|
146
|
+
deposited += leg.fromAmount;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (deposited !== input.fromAmount) {
|
|
150
|
+
quoteRefuse('LI.FI calldata deposit amount does not equal the exact intent amount');
|
|
151
|
+
}
|
|
152
|
+
const lastLeg = decoded.legs[decoded.legs.length - 1];
|
|
153
|
+
if (!lastLeg || getAddress(lastLeg.receivingAssetId) !== input.toToken) {
|
|
154
|
+
quoteRefuse('LI.FI calldata output token does not match the intent');
|
|
155
|
+
}
|
|
156
|
+
// Classify fee-capable legs before checking their exact shape. Otherwise a second executable
|
|
157
|
+
// forwardERC20Fees call can evade the complete recipient-set and aggregate-cap checks merely by
|
|
158
|
+
// naming a different approveTo target.
|
|
159
|
+
const feeLegs = decoded.legs.filter((leg) => isReviewedLiFiFeeForwarder(input.chainId, leg.callTo) ||
|
|
160
|
+
leg.callData.slice(0, 10).toLowerCase() === FORWARD_ERC20_FEES_SELECTOR);
|
|
161
|
+
if (feeLegs.length !== 1) {
|
|
162
|
+
attributionRefuse('LI.FI calldata does not contain exactly one reviewed fee-forwarding leg');
|
|
163
|
+
}
|
|
164
|
+
const feeLeg = feeLegs[0];
|
|
165
|
+
// The drift alarm: an unreviewed collector is refused BY ADDRESS, so an upstream rollout reads as
|
|
166
|
+
// a loud "review and add" instead of a dark rail.
|
|
167
|
+
if (!isReviewedLiFiFeeForwarder(input.chainId, feeLeg.callTo) ||
|
|
168
|
+
!isReviewedLiFiFeeForwarder(input.chainId, feeLeg.approveTo) ||
|
|
169
|
+
getAddress(feeLeg.callTo) !== getAddress(feeLeg.approveTo)) {
|
|
170
|
+
attributionRefuse(unreviewedFeeForwarderMessage(input.chainId, feeLeg.callTo, feeLeg.approveTo));
|
|
171
|
+
}
|
|
172
|
+
if (feeLeg.callData.slice(0, 10).toLowerCase() !== FORWARD_ERC20_FEES_SELECTOR ||
|
|
173
|
+
!feeLeg.requiresDeposit ||
|
|
174
|
+
getAddress(feeLeg.sendingAssetId) !== input.fromToken ||
|
|
175
|
+
getAddress(feeLeg.receivingAssetId) !== input.fromToken ||
|
|
176
|
+
feeLeg.fromAmount !== input.fromAmount) {
|
|
177
|
+
attributionRefuse('LI.FI fee-forwarding leg is not bound to the exact input token and amount');
|
|
178
|
+
}
|
|
179
|
+
assertFeeDistributionBody({
|
|
180
|
+
callData: feeLeg.callData,
|
|
181
|
+
expectedToken: input.fromToken,
|
|
182
|
+
grossAmount: input.fromAmount,
|
|
183
|
+
config: input.config,
|
|
184
|
+
});
|
|
185
|
+
const quotedOutput = positiveBigInt(input.quote.estimate.toAmount, 'LI.FI quoted output');
|
|
186
|
+
const quotedMinimum = positiveBigInt(input.quote.estimate.toAmountMin, 'LI.FI quoted minimum output');
|
|
187
|
+
const slippageFloor = (quotedOutput * BigInt(10_000 - input.maxSlippageBps)) / 10000n;
|
|
188
|
+
if (decoded.minAmount !== quotedMinimum || decoded.minAmount < slippageFloor) {
|
|
189
|
+
quoteRefuse('LI.FI calldata minimum output is inconsistent with the slippage bound');
|
|
190
|
+
}
|
|
191
|
+
if (input.minAmountOut !== undefined && decoded.minAmount < input.minAmountOut) {
|
|
192
|
+
quoteRefuse('LI.FI calldata minimum output is below the caller constraint');
|
|
193
|
+
}
|
|
194
|
+
return Object.freeze({
|
|
195
|
+
approvalSpender: boundQuote.approvalSpender,
|
|
196
|
+
transaction: Object.freeze({
|
|
197
|
+
to: boundQuote.transactionTarget,
|
|
198
|
+
value: '0',
|
|
199
|
+
data: boundQuote.data,
|
|
200
|
+
}),
|
|
201
|
+
assertions: Object.freeze([
|
|
202
|
+
Object.freeze({ id: 'lifi_integrator_exact', result: 'passed' }),
|
|
203
|
+
Object.freeze({ id: 'lifi_fee_recipient_exact', result: 'passed' }),
|
|
204
|
+
Object.freeze({ id: 'lifi_fee_amount_exact', result: 'passed' }),
|
|
205
|
+
Object.freeze({ id: 'lifi_fee_recipient_set_versioned', result: 'passed' }),
|
|
206
|
+
Object.freeze({ id: 'lifi_fee_aggregate_cap', result: 'passed' }),
|
|
207
|
+
Object.freeze({ id: 'lifi_receiver_account_bound', result: 'passed' }),
|
|
208
|
+
Object.freeze({ id: 'lifi_min_out_slippage_bound', result: 'passed' }),
|
|
209
|
+
Object.freeze({ id: 'lifi_token_amount_bound', result: 'passed' }),
|
|
210
|
+
Object.freeze({ id: 'lifi_targets_value_bound', result: 'passed' }),
|
|
211
|
+
]),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=lifi-swap-assertions.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Hex } from 'viem';
|
|
2
|
+
export interface LocalX402Config {
|
|
3
|
+
readonly privateKey: Hex;
|
|
4
|
+
readonly baseUrl: string;
|
|
5
|
+
readonly rpcUrl?: string;
|
|
6
|
+
readonly timeoutMs: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function assertAllowedX402BaseUrl(raw: string): string;
|
|
9
|
+
export declare function loadLocalX402Config(env?: NodeJS.ProcessEnv): LocalX402Config;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
function requireNonBlank(env, name) {
|
|
2
|
+
const value = env[name]?.trim();
|
|
3
|
+
if (!value)
|
|
4
|
+
throw new Error(`${name} is required`);
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
function positiveInteger(raw, fallback, name) {
|
|
8
|
+
if (raw === undefined || raw.trim() === '')
|
|
9
|
+
return fallback;
|
|
10
|
+
const value = Number(raw);
|
|
11
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
12
|
+
throw new Error(`${name} must be a positive integer`);
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
function isLoopback(hostname) {
|
|
16
|
+
return (hostname === 'localhost' ||
|
|
17
|
+
hostname === '127.0.0.1' ||
|
|
18
|
+
hostname === '::1' ||
|
|
19
|
+
hostname === '[::1]');
|
|
20
|
+
}
|
|
21
|
+
export function assertAllowedX402BaseUrl(raw) {
|
|
22
|
+
const url = new URL(raw);
|
|
23
|
+
if (url.username || url.password)
|
|
24
|
+
throw new Error('X402_BASE_URL must not contain credentials');
|
|
25
|
+
const isOfficial = url.protocol === 'https:' && url.origin === 'https://x402.ottoai.services';
|
|
26
|
+
const isLocal = ['http:', 'https:'].includes(url.protocol) && isLoopback(url.hostname);
|
|
27
|
+
if (!isOfficial && !isLocal) {
|
|
28
|
+
throw new Error('X402_BASE_URL must be https://x402.ottoai.services or a loopback test server');
|
|
29
|
+
}
|
|
30
|
+
return url.toString().replace(/\/$/, '');
|
|
31
|
+
}
|
|
32
|
+
export function loadLocalX402Config(env = process.env) {
|
|
33
|
+
const rawKey = requireNonBlank(env, 'X402_PRIVATE_KEY');
|
|
34
|
+
const normalizedKey = rawKey.startsWith('0x') ? rawKey : `0x${rawKey}`;
|
|
35
|
+
if (!/^0x[a-fA-F0-9]{64}$/.test(normalizedKey)) {
|
|
36
|
+
throw new Error('X402_PRIVATE_KEY must be a 32-byte hexadecimal private key');
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
privateKey: normalizedKey,
|
|
40
|
+
baseUrl: assertAllowedX402BaseUrl(env.X402_BASE_URL?.trim() || 'https://x402.ottoai.services'),
|
|
41
|
+
rpcUrl: env.X402_RPC_URL?.trim() || undefined,
|
|
42
|
+
timeoutMs: positiveInteger(env.X402_TIMEOUT_MS, 30_000, 'X402_TIMEOUT_MS'),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=local-config.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { IntelDataSource } from './data-source.js';
|
|
3
|
+
import type { ExecutionToolRuntime } from './execution-tools.js';
|
|
4
|
+
export declare function createIntelMcpServer(dataSource: IntelDataSource, executionRuntime?: ExecutionToolRuntime): McpServer;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { mapToolError } from './errors.js';
|
|
3
|
+
import { registerExecutionTools } from './execution-registration.js';
|
|
4
|
+
import { TOOL_DEFINITIONS } from './tool-definitions.js';
|
|
5
|
+
const READ_ONLY_ANNOTATIONS = Object.freeze({
|
|
6
|
+
readOnlyHint: true,
|
|
7
|
+
destructiveHint: false,
|
|
8
|
+
idempotentHint: true,
|
|
9
|
+
openWorldHint: true,
|
|
10
|
+
});
|
|
11
|
+
function stringify(value) {
|
|
12
|
+
return JSON.stringify(value, (_key, item) => typeof item === 'bigint' ? item.toString() : item);
|
|
13
|
+
}
|
|
14
|
+
async function callDefinition(dataSource, definition, rawInput) {
|
|
15
|
+
const input = definition.inputSchema.parse(rawInput);
|
|
16
|
+
try {
|
|
17
|
+
const result = await dataSource.read(definition, input);
|
|
18
|
+
return {
|
|
19
|
+
content: [{ type: 'text', text: stringify(result) }],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return mapToolError(error);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function createIntelMcpServer(dataSource, executionRuntime) {
|
|
27
|
+
const server = new McpServer({
|
|
28
|
+
name: 'otto-intel',
|
|
29
|
+
title: 'Otto Intel',
|
|
30
|
+
version: '0.1.0',
|
|
31
|
+
});
|
|
32
|
+
for (const definition of TOOL_DEFINITIONS) {
|
|
33
|
+
server.registerTool(definition.name, {
|
|
34
|
+
title: definition.title,
|
|
35
|
+
description: definition.description,
|
|
36
|
+
inputSchema: definition.inputSchema,
|
|
37
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
38
|
+
}, (input) => callDefinition(dataSource, definition, input));
|
|
39
|
+
}
|
|
40
|
+
if (executionRuntime)
|
|
41
|
+
registerExecutionTools(server, executionRuntime);
|
|
42
|
+
return server;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=mcp-server.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { Hex } from 'viem';
|
|
3
|
+
import { type ExecutionServiceConfig, type PolymarketTickSize } from './execution-config.js';
|
|
4
|
+
/** The live `/markets/{condition_id}` body, as captured in `test/fixtures/polymarket-market-*.json`. */
|
|
5
|
+
export declare const POLYMARKET_MARKET_RESPONSE_SCHEMA: z.ZodObject<{
|
|
6
|
+
condition_id: z.ZodString;
|
|
7
|
+
minimum_tick_size: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
|
|
8
|
+
minimum_order_size: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
|
|
9
|
+
neg_risk: z.ZodOptional<z.ZodBoolean>;
|
|
10
|
+
accepting_orders: z.ZodBoolean;
|
|
11
|
+
active: z.ZodBoolean;
|
|
12
|
+
closed: z.ZodBoolean;
|
|
13
|
+
archived: z.ZodOptional<z.ZodBoolean>;
|
|
14
|
+
enable_order_book: z.ZodBoolean;
|
|
15
|
+
tokens: z.ZodArray<z.ZodObject<{
|
|
16
|
+
token_id: z.ZodString;
|
|
17
|
+
outcome: z.ZodOptional<z.ZodString>;
|
|
18
|
+
}, z.core.$loose>>;
|
|
19
|
+
}, z.core.$loose>;
|
|
20
|
+
export declare const POLYMARKET_TOKEN_MARKET_RESPONSE_SCHEMA: z.ZodObject<{
|
|
21
|
+
condition_id: z.ZodString;
|
|
22
|
+
}, z.core.$loose>;
|
|
23
|
+
/** The live `/fees/builder-fees/{code}` body: the value source for the ruled builder rates (§8). */
|
|
24
|
+
export declare const POLYMARKET_BUILDER_FEES_RESPONSE_SCHEMA: z.ZodObject<{
|
|
25
|
+
code: z.ZodString;
|
|
26
|
+
builder_maker_fee_rate_bps: z.ZodNumber;
|
|
27
|
+
builder_taker_fee_rate_bps: z.ZodNumber;
|
|
28
|
+
enabled: z.ZodBoolean;
|
|
29
|
+
}, z.core.$loose>;
|
|
30
|
+
export interface PolymarketMarketMetadata {
|
|
31
|
+
readonly tokenId: string;
|
|
32
|
+
readonly conditionId: string;
|
|
33
|
+
readonly tickSize: PolymarketTickSize;
|
|
34
|
+
/** Minimum order size in shares, as the venue publishes it; never a value this repo invents. */
|
|
35
|
+
readonly minimumOrderSize: number;
|
|
36
|
+
readonly negRisk: boolean;
|
|
37
|
+
readonly acceptingOrders: boolean;
|
|
38
|
+
readonly active: boolean;
|
|
39
|
+
readonly closed: boolean;
|
|
40
|
+
readonly orderBookEnabled: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface PolymarketBuilderFeeRates {
|
|
43
|
+
readonly code: string;
|
|
44
|
+
readonly makerBps: number;
|
|
45
|
+
readonly takerBps: number;
|
|
46
|
+
readonly enabled: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface PolymarketClobClient {
|
|
49
|
+
getMarketMetadata(tokenId: string, signal?: AbortSignal): Promise<PolymarketMarketMetadata>;
|
|
50
|
+
getBuilderFeeRates(builderCode: Hex, signal?: AbortSignal): Promise<PolymarketBuilderFeeRates>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Map a live market body onto the metadata the constructor needs, refusing every shape the
|
|
54
|
+
* constructor cannot act on. Shared by the REST client and the recorded-fixture loader, so the
|
|
55
|
+
* assertions in tests run over the same parse the network path uses.
|
|
56
|
+
*/
|
|
57
|
+
export declare function polymarketMarketMetadata(raw: unknown, tokenId: string): PolymarketMarketMetadata;
|
|
58
|
+
/**
|
|
59
|
+
* The live builder-registry row for exactly the expected code. A row for a different code is
|
|
60
|
+
* refused rather than trusted: the whole point of the read is to bind Otto's own attribution.
|
|
61
|
+
*/
|
|
62
|
+
export declare function polymarketBuilderFeeRates(raw: unknown, expectedCode: Hex): PolymarketBuilderFeeRates;
|
|
63
|
+
export declare function buildPolymarketMarketByTokenUrl(tokenId: string, config: Pick<ExecutionServiceConfig, 'polymarketClobBaseUrl'>): URL;
|
|
64
|
+
export declare function buildPolymarketMarketUrl(conditionId: string, config: Pick<ExecutionServiceConfig, 'polymarketClobBaseUrl'>): URL;
|
|
65
|
+
export declare function buildPolymarketBuilderFeesUrl(builderCode: Hex, config: Pick<ExecutionServiceConfig, 'polymarketClobBaseUrl'>): URL;
|
|
66
|
+
export declare class RestPolymarketClobClient implements PolymarketClobClient {
|
|
67
|
+
private readonly config;
|
|
68
|
+
private readonly fetchImpl;
|
|
69
|
+
constructor(config: ExecutionServiceConfig, fetchImpl?: typeof fetch);
|
|
70
|
+
getMarketMetadata(tokenId: string, signal?: AbortSignal): Promise<PolymarketMarketMetadata>;
|
|
71
|
+
getBuilderFeeRates(builderCode: Hex, signal?: AbortSignal): Promise<PolymarketBuilderFeeRates>;
|
|
72
|
+
private getJson;
|
|
73
|
+
}
|