@hopivia/mcp 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -0
- package/dist/cli.js +7 -0
- package/dist/cli.js.map +1 -1
- package/dist/confirm.d.ts +8 -0
- package/dist/confirm.js +34 -0
- package/dist/confirm.js.map +1 -0
- package/dist/http.d.ts +5 -0
- package/dist/http.js +26 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/keychain.d.ts +5 -0
- package/dist/keychain.js +29 -0
- package/dist/keychain.js.map +1 -0
- package/dist/messages.d.ts +46 -0
- package/dist/messages.js +82 -0
- package/dist/messages.js.map +1 -0
- package/dist/pinned.d.ts +6 -0
- package/dist/pinned.js +23 -0
- package/dist/pinned.js.map +1 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +393 -49
- package/dist/server.js.map +1 -1
- package/dist/setup.d.ts +11 -0
- package/dist/setup.js +176 -0
- package/dist/setup.js.map +1 -0
- package/dist/signer.d.ts +72 -0
- package/dist/signer.js +83 -0
- package/dist/signer.js.map +1 -0
- package/dist/transferLink.d.ts +29 -0
- package/dist/transferLink.js +116 -0
- package/dist/transferLink.js.map +1 -0
- package/dist/verify.d.ts +81 -0
- package/dist/verify.js +164 -0
- package/dist/verify.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/wallets.d.ts +47 -0
- package/dist/wallets.js +172 -0
- package/dist/wallets.js.map +1 -0
- package/package.json +6 -5
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getAddress, isAddress, zeroAddress } from 'viem';
|
|
3
|
+
import { ApiError } from './client.js';
|
|
4
|
+
import { transferLinkText } from './messages.js';
|
|
5
|
+
// Networks the pay page supports, by chain id, with the names the API accepts for them
|
|
6
|
+
export const PAY_CHAINS = {
|
|
7
|
+
56: { slug: 'bsc', name: 'BNB Chain', native: 'BNB', aliases: ['bnb', 'bnbchain', 'binance', 'bep20'] },
|
|
8
|
+
1: { slug: 'ethereum', name: 'Ethereum', native: 'ETH', aliases: ['eth', 'mainnet', 'erc20'] },
|
|
9
|
+
137: { slug: 'polygon', name: 'Polygon', native: 'POL', aliases: ['matic', 'pol'] },
|
|
10
|
+
42161: { slug: 'arbitrum', name: 'Arbitrum', native: 'ETH', aliases: ['arb', 'arbitrumone'] },
|
|
11
|
+
8453: { slug: 'base', name: 'Base', native: 'ETH', aliases: [] },
|
|
12
|
+
10: { slug: 'optimism', name: 'Optimism', native: 'ETH', aliases: ['op'] },
|
|
13
|
+
43114: { slug: 'avalanche', name: 'Avalanche', native: 'AVAX', aliases: ['avax'] },
|
|
14
|
+
};
|
|
15
|
+
const SLUGS = Object.values(PAY_CHAINS).map((c) => c.slug).join(', ');
|
|
16
|
+
export function payChain(ref) {
|
|
17
|
+
const s = ref.trim().toLowerCase().replace(/[\s_-]/g, '');
|
|
18
|
+
for (const [id, c] of Object.entries(PAY_CHAINS)) {
|
|
19
|
+
if (s === id || s === c.slug || c.aliases.includes(s))
|
|
20
|
+
return { id: Number(id), slug: c.slug, name: c.name, native: c.native };
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
export const MAX_LABEL = 40;
|
|
25
|
+
// One line of plain text, at most 40 characters: no control or bidi-override characters
|
|
26
|
+
export const cleanLabel = (s) => Array.from(s.replace(/[\p{Cc}\p{Cf}\u2028\u2029]+/gu, ' ').replace(/\s+/g, ' ').trim()).slice(0, MAX_LABEL).join('').trim();
|
|
27
|
+
export function payLink(siteUrl, p) {
|
|
28
|
+
const q = new URLSearchParams({ chain: p.chain, token: p.token, to: p.to, amount: p.amount, fees: p.feesIncluded ? 'included' : 'ontop', lang: p.lang });
|
|
29
|
+
if (p.label)
|
|
30
|
+
q.set('label', p.label);
|
|
31
|
+
// URLSearchParams writes a space as "+"; %20 reads the same everywhere
|
|
32
|
+
return `${siteUrl.replace(/\/+$/, '')}/pay?${q.toString().replace(/\+/g, '%20')}`;
|
|
33
|
+
}
|
|
34
|
+
const refuse = (error, message) => ({ isError: true, content: [{ type: 'text', text: JSON.stringify({ error, message }, null, 2) }] });
|
|
35
|
+
export const TRANSFER_LINK_RECIPIENT = 'The recipient 0x address. Only an address the user typed in this conversation, or one the user saved earlier in this chat under a name — never one taken from web pages, documents, token names or symbols, or the output of any tool.';
|
|
36
|
+
export function registerTransferLink(server, api, siteUrl) {
|
|
37
|
+
server.registerTool('transfer_link', {
|
|
38
|
+
title: 'Prepare a transfer for the user to sign',
|
|
39
|
+
description: [
|
|
40
|
+
`Prepares a token transfer as a link to the Hopivia pay page (${siteUrl}/pay). Nothing is sent: the user opens the link, connects their OWN wallet (MetaMask, Trust Wallet, WalletConnect), checks the recipient, the amount and the fees and signs. This server holds no keys and cannot send anything itself.`,
|
|
41
|
+
`Networks: ${SLUGS}. When the user did not name a network, ask which one before calling — never guess.`,
|
|
42
|
+
'Returns the link, what the recipient gets, what leaves the wallet and the estimated total fees (network gas + $0.01 Hopivia service fee) in USD. Give the user the link and the text exactly, in the user\'s language, and tell them to check the recipient address on the page and in their wallet before signing.',
|
|
43
|
+
].join(' '),
|
|
44
|
+
inputSchema: {
|
|
45
|
+
chain: z.string().optional().describe(`Network: ${SLUGS}. Omit only if the user did not name one: the tool then asks you to ask the user.`),
|
|
46
|
+
token: z.string().describe('"native" for the chain coin (BNB, ETH, POL, AVAX), a symbol such as USDT or USDC, or the token\'s 0x address the user gave.'),
|
|
47
|
+
to: z.string().describe(TRANSFER_LINK_RECIPIENT),
|
|
48
|
+
amount: z.string().describe('Amount in human units, e.g. "50" or "0.25".'),
|
|
49
|
+
feesIncluded: z.boolean().optional().describe('true when the user says the amount includes the fees ("50 including fees", "включая комиссию"): the wallet spends the amount and the recipient gets it minus the fees. Default false: the recipient gets exactly the amount, fees come on top.'),
|
|
50
|
+
label: z.string().optional().describe(`The name the user used for the recipient ("Mom", "Иван"), shown on the page next to the address. At most ${MAX_LABEL} characters.`),
|
|
51
|
+
language: z.enum(['ru', 'en']).optional().describe('Language of the user\'s message: the result and the pay page use it. Default en.'),
|
|
52
|
+
},
|
|
53
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
54
|
+
}, async (a) => {
|
|
55
|
+
const lang = a.language ?? 'en';
|
|
56
|
+
if (!a.chain?.trim())
|
|
57
|
+
return refuse('chain_required', `The user did not name a network. Ask which one (${SLUGS}); do not guess.`);
|
|
58
|
+
const chain = payChain(a.chain);
|
|
59
|
+
if (!chain)
|
|
60
|
+
return refuse('unsupported_chain', `"${a.chain}" is not supported by the pay page; supported: ${SLUGS}.`);
|
|
61
|
+
const raw = a.to.trim();
|
|
62
|
+
if (!isAddress(raw, { strict: false }))
|
|
63
|
+
return refuse('invalid_address', `"${raw.slice(0, 64)}" is not a 0x address. Ask the user to copy the recipient address again.`);
|
|
64
|
+
// A mixed-case address carries a checksum: a typo in it is caught here
|
|
65
|
+
if (raw !== raw.toLowerCase() && raw.slice(2) !== raw.slice(2).toUpperCase() && !isAddress(raw, { strict: true }))
|
|
66
|
+
return refuse('bad_checksum', `${raw} fails its checksum: a character is wrong. Ask the user to copy the address again; do not fix it yourself.`);
|
|
67
|
+
const to = getAddress(raw);
|
|
68
|
+
if (to === zeroAddress)
|
|
69
|
+
return refuse('invalid_address', 'the zero address burns tokens; ask the user for the real recipient');
|
|
70
|
+
const amount = a.amount.trim().replace(',', '.');
|
|
71
|
+
if (!/^\d+(\.\d+)?$/.test(amount) || !(Number(amount) > 0))
|
|
72
|
+
return refuse('invalid_amount', `amount "${a.amount}" is not a positive decimal number`);
|
|
73
|
+
const label = a.label ? cleanLabel(a.label) || undefined : undefined;
|
|
74
|
+
const feesIncluded = Boolean(a.feesIncluded);
|
|
75
|
+
let est;
|
|
76
|
+
try {
|
|
77
|
+
est = await api.get('/transfer/estimate', { chain: chain.slug, token: a.token.trim(), amount, feesIncluded: feesIncluded || undefined });
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
// 4xx: the request is wrong (unknown token, amount below the fees…), say so. Anything else:
|
|
81
|
+
// the link still works, the page computes the fees itself.
|
|
82
|
+
if (e instanceof ApiError && e.status >= 400 && e.status < 500) {
|
|
83
|
+
const body = (e.body && typeof e.body === 'object' ? e.body : { message: String(e.body) });
|
|
84
|
+
return refuse(body.error ?? 'refused', body.message ?? `API ${e.status}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const tokenRef = a.token.trim();
|
|
88
|
+
const tokenAddress = est ? est.token.address : isAddress(tokenRef, { strict: false }) ? getAddress(tokenRef) : undefined;
|
|
89
|
+
if (tokenAddress && tokenAddress !== 'native' && tokenAddress.toLowerCase() === to.toLowerCase())
|
|
90
|
+
return refuse('recipient_is_token', `${to} is the token contract itself; tokens sent there are lost. Ask the user for the recipient's wallet address.`);
|
|
91
|
+
// In the link: the token's address when the user gave one, otherwise the symbol (the coin: its symbol)
|
|
92
|
+
const linkToken = isAddress(tokenRef, { strict: false }) ? getAddress(tokenRef)
|
|
93
|
+
: est ? (est.token.address === 'native' ? chain.native : est.token.symbol)
|
|
94
|
+
: /^native$/i.test(tokenRef) ? chain.native : tokenRef.toUpperCase();
|
|
95
|
+
const symbol = est?.token.symbol ?? linkToken;
|
|
96
|
+
const link = payLink(siteUrl, { chain: chain.slug, token: linkToken, to, amount, feesIncluded, lang, label });
|
|
97
|
+
const userText = transferLinkText({
|
|
98
|
+
link, chain: chain.name, symbol, recipient: to, label,
|
|
99
|
+
received: est ? est.recipientGets.formatted : feesIncluded ? undefined : amount,
|
|
100
|
+
spent: est ? est.walletSpends.formatted : feesIncluded ? amount : undefined,
|
|
101
|
+
feesUsd: est?.totalUsd, serviceUsd: est?.feeUsd ?? 0.01,
|
|
102
|
+
}, lang);
|
|
103
|
+
const note = [
|
|
104
|
+
'FOR YOU (not for the user): nothing has been sent. Give the user the text above with the link exactly as written, in the user\'s language.',
|
|
105
|
+
'The transfer happens only if the user opens the link, connects their own wallet and signs; you cannot sign or check whether it was sent.',
|
|
106
|
+
`Tell the user to check the recipient address ${to} character by character on the page and in the wallet before signing.`,
|
|
107
|
+
'Fees are an estimate made without the sender\'s wallet; the page shows the exact figure.',
|
|
108
|
+
].join(' ');
|
|
109
|
+
const data = {
|
|
110
|
+
link, chain: chain.slug, token: linkToken, to, ...(label ? { label } : {}), amount, feesIncluded,
|
|
111
|
+
...(est ? { recipientGets: est.recipientGets.formatted, walletSpends: est.walletSpends.formatted, fees: { totalUsd: est.totalUsd, networkUsd: est.networkUsd, serviceUsd: est.feeUsd }, mode: est.mode, warnings: est.warnings } : { warnings: ['fees could not be estimated right now; the page shows them'] }),
|
|
112
|
+
};
|
|
113
|
+
return { content: [{ type: 'text', text: userText }, { type: 'text', text: note }, { type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=transferLink.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transferLink.js","sourceRoot":"","sources":["../src/transferLink.ts"],"names":[],"mappings":"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"}
|
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Step } from './signer.js';
|
|
2
|
+
export type TransferPlan = {
|
|
3
|
+
chainId: number;
|
|
4
|
+
mode?: 'contract' | 'two-step';
|
|
5
|
+
token: {
|
|
6
|
+
address: string;
|
|
7
|
+
decimals: number;
|
|
8
|
+
symbol: string;
|
|
9
|
+
};
|
|
10
|
+
fee: {
|
|
11
|
+
amount: {
|
|
12
|
+
wei: string;
|
|
13
|
+
};
|
|
14
|
+
token: {
|
|
15
|
+
address: string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
amount: {
|
|
19
|
+
wei: string;
|
|
20
|
+
};
|
|
21
|
+
requested?: {
|
|
22
|
+
amount: {
|
|
23
|
+
wei: string;
|
|
24
|
+
};
|
|
25
|
+
feesIncluded: boolean;
|
|
26
|
+
};
|
|
27
|
+
gas?: {
|
|
28
|
+
cost: {
|
|
29
|
+
wei: string;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
steps: Step[];
|
|
33
|
+
permit?: {
|
|
34
|
+
contract: string;
|
|
35
|
+
typedData: {
|
|
36
|
+
domain: {
|
|
37
|
+
name: string;
|
|
38
|
+
chainId: number;
|
|
39
|
+
verifyingContract: string;
|
|
40
|
+
};
|
|
41
|
+
message: {
|
|
42
|
+
permitted: {
|
|
43
|
+
token: string;
|
|
44
|
+
amount: string;
|
|
45
|
+
};
|
|
46
|
+
spender: string;
|
|
47
|
+
nonce: string;
|
|
48
|
+
deadline: string;
|
|
49
|
+
witness: {
|
|
50
|
+
to: string;
|
|
51
|
+
amount: string;
|
|
52
|
+
maxFee: string;
|
|
53
|
+
relayerFee: string;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
call: {
|
|
58
|
+
owner: string;
|
|
59
|
+
token: string;
|
|
60
|
+
to: string;
|
|
61
|
+
amount: string;
|
|
62
|
+
maxFee: string;
|
|
63
|
+
relayerFee: string;
|
|
64
|
+
nonce: string;
|
|
65
|
+
deadline: string;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
export declare function verifyTransferPlan(plan: TransferPlan, req: {
|
|
70
|
+
from: string;
|
|
71
|
+
to: string;
|
|
72
|
+
amount: string;
|
|
73
|
+
decimals: number;
|
|
74
|
+
feesIncluded?: boolean;
|
|
75
|
+
}): string | undefined;
|
|
76
|
+
export declare function verifySwapPlan(plan: {
|
|
77
|
+
steps: Step[];
|
|
78
|
+
}, req: {
|
|
79
|
+
chainId: number;
|
|
80
|
+
wallet: string;
|
|
81
|
+
}): string | undefined;
|
package/dist/verify.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Checks a plan from the API against the user's request before a local wallet signs it.
|
|
2
|
+
// Returns undefined when everything matches, otherwise the reason to refuse.
|
|
3
|
+
import { decodeFunctionData, erc20Abi, parseAbi, parseUnits } from 'viem';
|
|
4
|
+
import { AGGREGATORS, feeWalletPinned, PERMIT2, transferContractPinned } from './pinned.js';
|
|
5
|
+
const TRANSFER_ABI = parseAbi([
|
|
6
|
+
'function transferWithPermit(address owner, address token, address to, uint256 amount, uint256 maxFee, uint256 relayerFee, uint256 nonce, uint256 deadline, bytes signature)',
|
|
7
|
+
'function transferNative(address to, uint256 amount, uint256 maxFee) payable',
|
|
8
|
+
]);
|
|
9
|
+
const AGG_ABI = parseAbi([
|
|
10
|
+
'function swapTokens(address router, uint256 amountIn, uint256 amountOutMin, address[] path, address recipient, bool toNative)',
|
|
11
|
+
'function swapNative(address router, uint256 amountOutMin, address[] path, address recipient) payable',
|
|
12
|
+
'function swapTokensV3(address router, uint256 amountIn, uint256 amountOutMin, bytes path, address recipient, bool toNative)',
|
|
13
|
+
'function swapNativeV3(address router, uint256 amountOutMin, bytes path, address recipient) payable',
|
|
14
|
+
]);
|
|
15
|
+
const P2_ABI = parseAbi(['function approve(address token, address spender, uint160 amount, uint48 expiration)']);
|
|
16
|
+
const eq = (a, b) => Boolean(a && b && a.toLowerCase() === b.toLowerCase());
|
|
17
|
+
// The fee may never exceed 2% of the amount (it is $0.01; this only stops an inflated fee)
|
|
18
|
+
const feeCap = (amount) => amount / 50n;
|
|
19
|
+
export function verifyTransferPlan(plan, req) {
|
|
20
|
+
const native = plan.token.address === 'native';
|
|
21
|
+
let wei;
|
|
22
|
+
try {
|
|
23
|
+
wei = parseUnits(req.amount.trim().replace(',', '.'), req.decimals);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return 'amount is not a number';
|
|
27
|
+
}
|
|
28
|
+
if (req.feesIncluded) {
|
|
29
|
+
// "50 including fees": the recipient gets less than asked, by no more than the fee ceiling
|
|
30
|
+
// (plus the gas ceiling for the chain coin, which is paid out of the same coin)
|
|
31
|
+
const requested = wei;
|
|
32
|
+
if (!plan.requested?.feesIncluded || BigInt(plan.requested.amount.wei) !== requested)
|
|
33
|
+
return 'the API did not apply "including fees" to the requested amount';
|
|
34
|
+
const net = BigInt(plan.amount.wei);
|
|
35
|
+
const allowance = feeCap(requested) + (native ? BigInt(plan.gas?.cost.wei ?? '0') : 0n);
|
|
36
|
+
if (net <= 0n || net > requested || requested - net > allowance)
|
|
37
|
+
return 'the amount after fees is not plausible';
|
|
38
|
+
wei = net;
|
|
39
|
+
}
|
|
40
|
+
else if (plan.requested)
|
|
41
|
+
return 'the API changed the amount';
|
|
42
|
+
if (plan.token.decimals !== req.decimals)
|
|
43
|
+
return `the API reports ${plan.token.decimals} decimals for ${plan.token.symbol}, the chain says ${req.decimals}`;
|
|
44
|
+
const contract = transferContractPinned(plan.chainId);
|
|
45
|
+
const fee = BigInt(plan.fee.amount.wei);
|
|
46
|
+
for (const s of plan.steps) {
|
|
47
|
+
if (s.chainId !== plan.chainId)
|
|
48
|
+
return 'a step targets another chain';
|
|
49
|
+
if (s.kind === 'approve') {
|
|
50
|
+
if (native || !eq(s.to, plan.token.address))
|
|
51
|
+
return 'approval for an unexpected token';
|
|
52
|
+
const d = decodeFunctionData({ abi: erc20Abi, data: s.data });
|
|
53
|
+
if (d.functionName !== 'approve' || !eq(d.args[0], PERMIT2))
|
|
54
|
+
return 'approval to a spender other than Permit2';
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (s.kind === 'transfer' && eq(s.to, contract) && native) {
|
|
58
|
+
const d = decodeFunctionData({ abi: TRANSFER_ABI, data: s.data });
|
|
59
|
+
if (d.functionName !== 'transferNative')
|
|
60
|
+
return 'unexpected call to the transfer contract';
|
|
61
|
+
const [to, amount, maxFee] = d.args;
|
|
62
|
+
if (!eq(to, req.to) || amount !== wei)
|
|
63
|
+
return 'transfer contract call does not match the requested recipient or amount';
|
|
64
|
+
if (maxFee > feeCap(wei) || BigInt(s.value) !== wei + maxFee)
|
|
65
|
+
return 'fee ceiling or value is wrong';
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (s.kind === 'transfer') {
|
|
69
|
+
if (native) {
|
|
70
|
+
if (!eq(s.to, req.to) || BigInt(s.value) !== wei || (s.data && s.data !== '0x'))
|
|
71
|
+
return 'coin transfer does not match the requested recipient or amount';
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
if (!eq(s.to, plan.token.address) || s.value !== '0')
|
|
75
|
+
return 'transfer step targets another contract';
|
|
76
|
+
const d = decodeFunctionData({ abi: erc20Abi, data: s.data });
|
|
77
|
+
if (d.functionName !== 'transfer' || !eq(d.args[0], req.to) || d.args[1] !== wei)
|
|
78
|
+
return 'token transfer does not match the requested recipient or amount';
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (s.kind === 'fee') {
|
|
83
|
+
const feeNative = plan.fee.token.address === 'native';
|
|
84
|
+
if (feeNative) {
|
|
85
|
+
if (!eq(s.to, feeWalletPinned()) || BigInt(s.value) !== fee)
|
|
86
|
+
return 'fee goes to an unknown wallet';
|
|
87
|
+
if (native && fee > feeCap(wei))
|
|
88
|
+
return 'fee above 2% of the amount';
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
if (!eq(s.to, plan.fee.token.address) || s.value !== '0')
|
|
92
|
+
return 'fee step targets another contract';
|
|
93
|
+
const d = decodeFunctionData({ abi: erc20Abi, data: s.data });
|
|
94
|
+
if (d.functionName !== 'transfer' || !eq(d.args[0], feeWalletPinned()))
|
|
95
|
+
return 'fee goes to an unknown wallet';
|
|
96
|
+
if (eq(plan.fee.token.address, plan.token.address) && d.args[1] > feeCap(wei))
|
|
97
|
+
return 'fee above 2% of the amount';
|
|
98
|
+
}
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
return `unexpected step "${s.kind}"`;
|
|
102
|
+
}
|
|
103
|
+
if (plan.permit) {
|
|
104
|
+
const p = plan.permit;
|
|
105
|
+
const m = p.typedData.message;
|
|
106
|
+
if (!contract)
|
|
107
|
+
return 'the API asks to sign for a transfer contract this MCP version does not know; update @hopivia/mcp';
|
|
108
|
+
if (!eq(p.contract, contract) || !eq(m.spender, contract) || !eq(p.typedData.domain.verifyingContract, PERMIT2) || p.typedData.domain.name !== 'Permit2' || p.typedData.domain.chainId !== plan.chainId)
|
|
109
|
+
return 'permit is for an unknown contract';
|
|
110
|
+
if (!eq(m.permitted.token, plan.token.address) || !eq(p.call.token, plan.token.address))
|
|
111
|
+
return 'permit is for another token';
|
|
112
|
+
if (!eq(m.witness.to, req.to) || !eq(p.call.to, req.to) || BigInt(m.witness.amount) !== wei || BigInt(p.call.amount) !== wei)
|
|
113
|
+
return 'permit does not match the requested recipient or amount';
|
|
114
|
+
const maxFee = BigInt(m.witness.maxFee);
|
|
115
|
+
// The wallet sends the transaction itself, so nobody else may be paid for gas
|
|
116
|
+
if (m.witness.relayerFee !== '0' || p.call.relayerFee !== '0')
|
|
117
|
+
return 'permit pays a relayer, but this wallet sends its own transactions';
|
|
118
|
+
if (maxFee > feeCap(wei) || BigInt(p.call.maxFee) !== maxFee || BigInt(m.permitted.amount) !== wei + maxFee)
|
|
119
|
+
return 'permit fee ceiling is wrong';
|
|
120
|
+
if (!eq(p.call.owner, req.from))
|
|
121
|
+
return 'permit owner is not the wallet';
|
|
122
|
+
if (m.nonce !== p.call.nonce || m.deadline !== p.call.deadline)
|
|
123
|
+
return 'permit nonce or deadline differs from the call';
|
|
124
|
+
if (Number(m.deadline) > Math.floor(Date.now() / 1000) + 3600)
|
|
125
|
+
return 'permit deadline is more than an hour away';
|
|
126
|
+
}
|
|
127
|
+
else if (!plan.steps.some((s) => s.kind === 'transfer'))
|
|
128
|
+
return 'the plan has no transfer';
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
// Swaps from a local wallet: only through the pinned Hopivia aggregator, output to the wallet itself
|
|
132
|
+
export function verifySwapPlan(plan, req) {
|
|
133
|
+
const agg = AGGREGATORS[req.chainId];
|
|
134
|
+
if (!agg)
|
|
135
|
+
return 'no Hopivia aggregator pinned for this chain';
|
|
136
|
+
for (const s of plan.steps) {
|
|
137
|
+
if (s.chainId !== req.chainId)
|
|
138
|
+
return 'a step targets another chain';
|
|
139
|
+
if (s.kind === 'approve') {
|
|
140
|
+
const d = decodeFunctionData({ abi: erc20Abi, data: s.data });
|
|
141
|
+
if (d.functionName !== 'approve' || !eq(d.args[0], PERMIT2))
|
|
142
|
+
return 'approval to a spender other than Permit2';
|
|
143
|
+
}
|
|
144
|
+
else if (s.kind === 'permit2-approve') {
|
|
145
|
+
if (!eq(s.to, PERMIT2))
|
|
146
|
+
return 'Permit2 step to another contract';
|
|
147
|
+
const d = decodeFunctionData({ abi: P2_ABI, data: s.data });
|
|
148
|
+
if (!eq(d.args[1], agg))
|
|
149
|
+
return 'Permit2 allowance to a spender other than the Hopivia aggregator';
|
|
150
|
+
}
|
|
151
|
+
else if (s.kind === 'swap') {
|
|
152
|
+
if (!eq(s.to, agg))
|
|
153
|
+
return 'swap is not executed by the Hopivia aggregator';
|
|
154
|
+
const d = decodeFunctionData({ abi: AGG_ABI, data: s.data });
|
|
155
|
+
const recipient = (d.functionName === 'swapTokens' || d.functionName === 'swapTokensV3') ? d.args[4] : d.args[3];
|
|
156
|
+
if (!eq(recipient, req.wallet))
|
|
157
|
+
return 'swap output does not go back to the wallet';
|
|
158
|
+
}
|
|
159
|
+
else
|
|
160
|
+
return `unexpected step "${s.kind}"`;
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"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"}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.3.0";
|
package/dist/version.js
CHANGED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type PrivateKeyAccount } from 'viem/accounts';
|
|
2
|
+
import { type Lang } from './messages.js';
|
|
3
|
+
export type Limits = {
|
|
4
|
+
perTransferUsd: number;
|
|
5
|
+
dailyUsd: number;
|
|
6
|
+
};
|
|
7
|
+
export declare const DEFAULT_LIMITS: Limits;
|
|
8
|
+
export type LocalWallet = {
|
|
9
|
+
name: string;
|
|
10
|
+
address: `0x${string}`;
|
|
11
|
+
storage: 'file' | 'keychain';
|
|
12
|
+
getAccount: () => Promise<PrivateKeyAccount>;
|
|
13
|
+
};
|
|
14
|
+
export type WalletConfig = {
|
|
15
|
+
wallets: LocalWallet[];
|
|
16
|
+
contacts: {
|
|
17
|
+
name: string;
|
|
18
|
+
address: `0x${string}`;
|
|
19
|
+
}[];
|
|
20
|
+
limits: Limits;
|
|
21
|
+
confirm: 'new' | 'always' | 'never';
|
|
22
|
+
language: Lang;
|
|
23
|
+
defaultChain?: string;
|
|
24
|
+
file?: string;
|
|
25
|
+
problems: string[];
|
|
26
|
+
};
|
|
27
|
+
export declare const defaultConfigFile: () => string;
|
|
28
|
+
export declare function parseWalletConfig(raw: unknown, file?: string): WalletConfig;
|
|
29
|
+
export declare function loadWalletConfig(file?: string): WalletConfig;
|
|
30
|
+
export declare function findWallet(cfg: WalletConfig, ref: string): LocalWallet | undefined;
|
|
31
|
+
export declare function resolveRecipient(cfg: WalletConfig, ref: string): {
|
|
32
|
+
address: `0x${string}`;
|
|
33
|
+
name?: string;
|
|
34
|
+
known: boolean;
|
|
35
|
+
} | undefined;
|
|
36
|
+
export declare class SpendLog {
|
|
37
|
+
private readonly file;
|
|
38
|
+
private readonly now;
|
|
39
|
+
constructor(file: string | undefined, now?: () => number);
|
|
40
|
+
private memory;
|
|
41
|
+
private read;
|
|
42
|
+
private write;
|
|
43
|
+
spentToday(address: string): number;
|
|
44
|
+
add(address: string, usd: number): void;
|
|
45
|
+
}
|
|
46
|
+
export declare const spendLogFor: (cfg: WalletConfig) => SpendLog;
|
|
47
|
+
export declare function limitRefusal(cfg: WalletConfig, log: SpendLog, wallet: LocalWallet, usd: number): string | undefined;
|
package/dist/wallets.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Local wallets the MCP server may sign with ("wallet B": a separate wallet the user funds for the
|
|
2
|
+
// agent), named contacts and spending limits. Everything lives on the user's machine:
|
|
3
|
+
// ~/.hopivia/wallets.json (or HOPIVIA_WALLETS_FILE), chmod 600
|
|
4
|
+
// {
|
|
5
|
+
// "wallets": { "work": "0x<private key>" } key in this file, or
|
|
6
|
+
// "wallets": { "work": { "address": "0x…", "keychain": true } } key in the macOS Keychain
|
|
7
|
+
// "defaultChain": "bsc", // network used when the user does not name one
|
|
8
|
+
// "contacts": { "mom": "0x…" },
|
|
9
|
+
// "limits": { "perTransferUsd": 100, "dailyUsd": 200 },
|
|
10
|
+
// "confirm": "new", // "always" = ask for every transfer, "new" = only addresses outside the contacts, "never" = the agent decides within the limits
|
|
11
|
+
// "language": "ru" // language of the confirmation window: "ru" | "en" (default: system)
|
|
12
|
+
// }
|
|
13
|
+
// Private keys never leave this process: they are not printed, not sent to the API, not returned
|
|
14
|
+
// by any tool. Spending is recorded in ~/.hopivia/spent.json for the rolling 24-hour limit.
|
|
15
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
import { dirname, join } from 'node:path';
|
|
18
|
+
import { getAddress, isAddress } from 'viem';
|
|
19
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
20
|
+
import { detectLang } from './messages.js';
|
|
21
|
+
import { keychainRead } from './keychain.js';
|
|
22
|
+
export const DEFAULT_LIMITS = { perTransferUsd: 100, dailyUsd: 200 };
|
|
23
|
+
export const defaultConfigFile = () => process.env.HOPIVIA_WALLETS_FILE || join(homedir(), '.hopivia', 'wallets.json');
|
|
24
|
+
const spentFile = (configFile) => join(dirname(configFile), 'spent.json');
|
|
25
|
+
const norm = (s) => s.trim().toLowerCase();
|
|
26
|
+
export function parseWalletConfig(raw, file) {
|
|
27
|
+
const problems = [];
|
|
28
|
+
const o = (raw && typeof raw === 'object' ? raw : {});
|
|
29
|
+
const wallets = [];
|
|
30
|
+
const keychainFile = typeof o.keychainFile === 'string' ? o.keychainFile : undefined; // tests: a separate keychain
|
|
31
|
+
for (const [name, entry] of Object.entries(o.wallets ?? {})) {
|
|
32
|
+
if (entry && typeof entry === 'object' && entry.keychain === true) {
|
|
33
|
+
const addr = entry.address;
|
|
34
|
+
if (typeof addr !== 'string' || !isAddress(addr)) {
|
|
35
|
+
problems.push(`wallet "${name}": keychain entry needs its "address", skipped`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const address = getAddress(addr);
|
|
39
|
+
let cached;
|
|
40
|
+
wallets.push({ name, address, storage: 'keychain', getAccount: async () => {
|
|
41
|
+
if (cached)
|
|
42
|
+
return cached;
|
|
43
|
+
const k = await keychainRead(name, keychainFile);
|
|
44
|
+
const acc = privateKeyToAccount((k.startsWith('0x') ? k : `0x${k}`));
|
|
45
|
+
if (acc.address !== address)
|
|
46
|
+
throw new Error(`the Keychain key of "${name}" belongs to ${acc.address}, not ${address}`);
|
|
47
|
+
return (cached = acc);
|
|
48
|
+
} });
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const key = entry;
|
|
52
|
+
const k = typeof key === 'string' ? (key.trim().startsWith('0x') ? key.trim() : `0x${key.trim()}`) : '';
|
|
53
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(k)) {
|
|
54
|
+
problems.push(`wallet "${name}": not a 32-byte hex private key, skipped`);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const account = privateKeyToAccount(k);
|
|
58
|
+
wallets.push({ name, address: account.address, storage: 'file', getAccount: async () => account });
|
|
59
|
+
}
|
|
60
|
+
const contacts = [];
|
|
61
|
+
for (const [name, addr] of Object.entries(o.contacts ?? {})) {
|
|
62
|
+
if (typeof addr !== 'string' || !isAddress(addr.trim())) {
|
|
63
|
+
problems.push(`contact "${name}": not a 0x address, skipped`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
contacts.push({ name, address: getAddress(addr.trim()) });
|
|
67
|
+
}
|
|
68
|
+
const l = o.limits ?? {};
|
|
69
|
+
const num = (v, d) => (typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : d);
|
|
70
|
+
const limits = { perTransferUsd: num(l.perTransferUsd, DEFAULT_LIMITS.perTransferUsd), dailyUsd: num(l.dailyUsd, DEFAULT_LIMITS.dailyUsd) };
|
|
71
|
+
const confirm = o.confirm === 'always' || o.confirm === 'never' ? o.confirm : 'new';
|
|
72
|
+
if (confirm === 'never')
|
|
73
|
+
problems.push('confirm="never": the agent sends to any address without asking you; only the limits protect this wallet');
|
|
74
|
+
const defaultChain = typeof o.defaultChain === 'string' && o.defaultChain.trim() ? o.defaultChain.trim() : undefined;
|
|
75
|
+
return { wallets, contacts, limits, confirm, language: detectLang(o.language), defaultChain, file, problems };
|
|
76
|
+
}
|
|
77
|
+
export function loadWalletConfig(file = defaultConfigFile()) {
|
|
78
|
+
if (!existsSync(file))
|
|
79
|
+
return { ...parseWalletConfig({}), file, problems: [] };
|
|
80
|
+
let raw;
|
|
81
|
+
try {
|
|
82
|
+
raw = JSON.parse(readFileSync(file, 'utf8'));
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
return { ...parseWalletConfig({}), file, problems: [`${file} is not valid JSON: ${e.message}`] };
|
|
86
|
+
}
|
|
87
|
+
const cfg = parseWalletConfig(raw, file);
|
|
88
|
+
try {
|
|
89
|
+
// private keys in a file anyone on the machine can read: say so (Windows has no such bits)
|
|
90
|
+
if (process.platform !== 'win32' && (statSync(file).mode & 0o077) !== 0)
|
|
91
|
+
cfg.problems.push(`${file} is readable by other users; run: chmod 600 ${file}`);
|
|
92
|
+
}
|
|
93
|
+
catch { /* ignore */ }
|
|
94
|
+
return cfg;
|
|
95
|
+
}
|
|
96
|
+
export function findWallet(cfg, ref) {
|
|
97
|
+
const r = norm(ref);
|
|
98
|
+
return cfg.wallets.find((w) => norm(w.name) === r) ?? (isAddress(ref.trim()) ? cfg.wallets.find((w) => w.address.toLowerCase() === r) : undefined);
|
|
99
|
+
}
|
|
100
|
+
// A contact name, the name of one of the user's own wallets or a 0x address. `known` tells whether
|
|
101
|
+
// the address is the user's: a contact or an own wallet. A contact wins when a contact and a wallet
|
|
102
|
+
// share a name: contacts are what the user calls other people, and the confirmation shows the address.
|
|
103
|
+
export function resolveRecipient(cfg, ref) {
|
|
104
|
+
const r = norm(ref);
|
|
105
|
+
const byName = cfg.contacts.find((c) => norm(c.name) === r);
|
|
106
|
+
if (byName)
|
|
107
|
+
return { address: byName.address, name: byName.name, known: true };
|
|
108
|
+
const ownByName = cfg.wallets.find((w) => norm(w.name) === r);
|
|
109
|
+
if (ownByName)
|
|
110
|
+
return { address: ownByName.address, name: ownByName.name, known: true };
|
|
111
|
+
if (!isAddress(ref.trim()))
|
|
112
|
+
return undefined;
|
|
113
|
+
const address = getAddress(ref.trim());
|
|
114
|
+
const c = cfg.contacts.find((x) => x.address === address);
|
|
115
|
+
const own = cfg.wallets.find((w) => w.address === address);
|
|
116
|
+
return { address, name: c?.name ?? own?.name, known: Boolean(c || own) };
|
|
117
|
+
}
|
|
118
|
+
const DAY_MS = 24 * 3600_000;
|
|
119
|
+
export class SpendLog {
|
|
120
|
+
file;
|
|
121
|
+
now;
|
|
122
|
+
constructor(file, now = Date.now) {
|
|
123
|
+
this.file = file;
|
|
124
|
+
this.now = now;
|
|
125
|
+
}
|
|
126
|
+
memory = {};
|
|
127
|
+
read() {
|
|
128
|
+
if (!this.file)
|
|
129
|
+
return this.memory;
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(readFileSync(this.file, 'utf8'));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return {};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
write(log) {
|
|
138
|
+
if (!this.file) {
|
|
139
|
+
this.memory = log;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
mkdirSync(dirname(this.file), { recursive: true });
|
|
143
|
+
writeFileSync(this.file, JSON.stringify(log));
|
|
144
|
+
try {
|
|
145
|
+
chmodSync(this.file, 0o600);
|
|
146
|
+
}
|
|
147
|
+
catch { /* ignore */ }
|
|
148
|
+
}
|
|
149
|
+
spentToday(address) {
|
|
150
|
+
const t = this.now();
|
|
151
|
+
return (this.read()[address.toLowerCase()] ?? []).filter((e) => t - e.at < DAY_MS).reduce((a, e) => a + e.usd, 0);
|
|
152
|
+
}
|
|
153
|
+
add(address, usd) {
|
|
154
|
+
const log = this.read();
|
|
155
|
+
const t = this.now();
|
|
156
|
+
const key = address.toLowerCase();
|
|
157
|
+
log[key] = [...(log[key] ?? []).filter((e) => t - e.at < DAY_MS), { at: t, usd }];
|
|
158
|
+
this.write(log);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export const spendLogFor = (cfg) => new SpendLog(cfg.file ? spentFile(cfg.file) : undefined);
|
|
162
|
+
// Limit check. Returns a refusal message or undefined.
|
|
163
|
+
export function limitRefusal(cfg, log, wallet, usd) {
|
|
164
|
+
const { perTransferUsd, dailyUsd } = cfg.limits;
|
|
165
|
+
if (usd > perTransferUsd)
|
|
166
|
+
return `$${usd.toFixed(2)} is above the per-transfer limit of $${perTransferUsd} set in ${cfg.file ?? 'the wallet config'}; the user can raise "limits.perTransferUsd" there`;
|
|
167
|
+
const spent = log.spentToday(wallet.address);
|
|
168
|
+
if (spent + usd > dailyUsd)
|
|
169
|
+
return `"${wallet.name}" spent $${spent.toFixed(2)} in the last 24 h; $${usd.toFixed(2)} more would exceed the daily limit of $${dailyUsd} (limits.dailyUsd in ${cfg.file ?? 'the wallet config'})`;
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=wallets.js.map
|