@ecadlabs/tezosx-mcp 1.0.6 → 1.0.8

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/dist/index.js CHANGED
@@ -31,27 +31,26 @@ function isValidPrivateKey(key) {
31
31
  const log = (msg) => console.error(`[tezosx-mcp] ${msg}`);
32
32
  const init = async () => {
33
33
  log('Starting server...');
34
- // Determine network
35
- const networkName = (process.env.TEZOS_NETWORK || 'mainnet');
34
+ // Try to load config: env vars first, then persistent store
35
+ const stored = loadConfig();
36
+ const privateKey = process.env.SPENDING_PRIVATE_KEY?.trim() || stored.spendingPrivateKey?.trim();
37
+ const spendingContract = process.env.SPENDING_CONTRACT?.trim() || stored.spendingContract?.trim();
38
+ const storedNetwork = stored.network && stored.network in NETWORKS ? stored.network : undefined;
39
+ // Determine network: env var > stored config > mainnet
40
+ const networkName = (process.env.TEZOS_NETWORK || storedNetwork || 'mainnet');
36
41
  if (!(networkName in NETWORKS)) {
37
42
  throw new ReferenceError(`Invalid network: ${networkName}. Valid options: ${Object.keys(NETWORKS).join(', ')}`);
38
43
  }
39
44
  log(`Network: ${networkName}`);
40
45
  // Create shared mutable config
41
46
  const liveConfig = createLiveConfig(networkName);
42
- // Try to load config: env vars first, then persistent store
43
- const stored = loadConfig();
44
- const privateKey = process.env.SPENDING_PRIVATE_KEY?.trim() || stored.spendingPrivateKey?.trim();
45
- const spendingContract = process.env.SPENDING_CONTRACT?.trim() || stored.spendingContract?.trim();
46
- const storedNetwork = stored.network && stored.network in NETWORKS ? stored.network : undefined;
47
- const configNetwork = networkName || storedNetwork;
48
47
  if (privateKey && spendingContract) {
49
48
  if (!isValidPrivateKey(privateKey)) {
50
49
  log('Warning: Invalid private key format. Must start with edsk, spsk, or p2sk. Wallet not configured.');
51
50
  }
52
51
  else {
53
52
  try {
54
- const spendingAddress = await configureLiveConfig(liveConfig, privateKey, spendingContract, configNetwork);
53
+ const spendingAddress = await configureLiveConfig(liveConfig, privateKey, spendingContract, networkName);
55
54
  log(`Wallet configured: ${spendingAddress}`);
56
55
  if (stored.spendingPrivateKey) {
57
56
  log('Config loaded from persistent store');
@@ -0,0 +1,27 @@
1
+ import z from "zod";
2
+ import type { LiveConfig } from "../live-config.js";
3
+ export declare const createBridgeExecuteWithdrawalTool: (config: LiveConfig) => {
4
+ name: string;
5
+ config: {
6
+ title: string;
7
+ description: string;
8
+ inputSchema: z.ZodObject<{
9
+ outboxLevel: z.ZodOptional<z.ZodNumber>;
10
+ messageIndex: z.ZodOptional<z.ZodNumber>;
11
+ cementedCommitment: z.ZodOptional<z.ZodString>;
12
+ outputProof: z.ZodOptional<z.ZodString>;
13
+ }, z.z.core.$strip>;
14
+ annotations: {
15
+ readOnlyHint: boolean;
16
+ destructiveHint: boolean;
17
+ idempotentHint: boolean;
18
+ openWorldHint: boolean;
19
+ };
20
+ };
21
+ handler: (params: any) => Promise<{
22
+ content: {
23
+ type: "text";
24
+ text: string;
25
+ }[];
26
+ }>;
27
+ };
@@ -0,0 +1,125 @@
1
+ import axios from "axios";
2
+ import z from "zod";
3
+ import { ensureRevealed } from "./reveal_account.js";
4
+ const CONFIRMATIONS_TO_WAIT = 3;
5
+ const BRIDGE_CONFIG = {
6
+ mainnet: {
7
+ smartRollupAddress: "sr1Ghq66tYK9y3r8CC1Tf8i8m5nxh8nTvZEf",
8
+ },
9
+ shadownet: {
10
+ smartRollupAddress: "sr19fMYrr5C4qqvQqQrDSjtP31GcrWjodzvg",
11
+ },
12
+ };
13
+ // Default rollup node URLs (can be overridden via env)
14
+ const ROLLUP_NODE_URLS = {
15
+ mainnet: process.env.ETHERLINK_ROLLUP_NODE_MAINNET,
16
+ shadownet: process.env.ETHERLINK_ROLLUP_NODE_SHADOWNET,
17
+ };
18
+ const inputSchema = z.object({
19
+ // Option 1: provide outbox level + message index (requires rollup node access)
20
+ outboxLevel: z.number().optional().describe("The Tezos L1 block level of the outbox containing the withdrawal message. " +
21
+ "Used with messageIndex to auto-fetch the proof from the rollup node."),
22
+ messageIndex: z.number().optional().describe("The index of the message within the outbox level. Defaults to 0."),
23
+ // Option 2: provide pre-computed proof directly
24
+ cementedCommitment: z.string().optional().describe("The cemented commitment hash (src1...). Required if outputProof is provided directly."),
25
+ outputProof: z.string().optional().describe("The hex-encoded output proof. If provided along with cementedCommitment, skips rollup node query."),
26
+ });
27
+ async function fetchProofFromRollupNode(rollupNodeUrl, outboxLevel, messageIndex) {
28
+ const url = `${rollupNodeUrl}/global/block/head/helpers/proofs/outbox/${outboxLevel}/messages?index=${messageIndex}`;
29
+ try {
30
+ const { data } = await axios.get(url, { timeout: 30_000 });
31
+ return {
32
+ commitment: data.commitment,
33
+ proof: data.proof,
34
+ };
35
+ }
36
+ catch (error) {
37
+ const msg = error instanceof Error ? error.message : String(error);
38
+ throw new Error(`Failed to fetch outbox proof from rollup node at ${rollupNodeUrl}: ${msg}\n` +
39
+ `Ensure ETHERLINK_ROLLUP_NODE_MAINNET or ETHERLINK_ROLLUP_NODE_SHADOWNET env var is set to a valid rollup node URL.`);
40
+ }
41
+ }
42
+ async function getCementedCommitment(tezosRpcUrl, rollupAddress) {
43
+ const url = `${tezosRpcUrl}/chains/main/blocks/head/context/smart_rollups/smart_rollup/${rollupAddress}/last_cemented_commitment_hash_with_level`;
44
+ try {
45
+ const { data } = await axios.get(url, { timeout: 15_000 });
46
+ return { hash: data.hash, level: data.level };
47
+ }
48
+ catch (error) {
49
+ const msg = error instanceof Error ? error.message : String(error);
50
+ throw new Error(`Failed to get cemented commitment: ${msg}`);
51
+ }
52
+ }
53
+ export const createBridgeExecuteWithdrawalTool = (config) => ({
54
+ name: "tezos_bridge_execute_withdrawal",
55
+ config: {
56
+ title: "Execute Bridge Withdrawal",
57
+ description: "Executes a cemented outbox message on Tezos L1 to finalize a standard bridge withdrawal (Etherlink → Tezos). " +
58
+ "This can only be done after the commitment containing the withdrawal has been cemented (~15 days after initiation). " +
59
+ "Provide either outboxLevel + messageIndex (auto-fetches proof from rollup node) or cementedCommitment + outputProof directly. " +
60
+ "Requires ETHERLINK_ROLLUP_NODE_MAINNET or ETHERLINK_ROLLUP_NODE_SHADOWNET env var for auto-fetch mode.",
61
+ inputSchema,
62
+ annotations: {
63
+ readOnlyHint: false,
64
+ destructiveHint: false,
65
+ idempotentHint: true,
66
+ openWorldHint: true,
67
+ },
68
+ },
69
+ handler: async (params) => {
70
+ params = params;
71
+ const { Tezos, networkName, spendingAddress } = config;
72
+ const bridgeConfig = BRIDGE_CONFIG[networkName];
73
+ if (!bridgeConfig) {
74
+ throw new Error(`Bridge not supported on network: ${networkName}`);
75
+ }
76
+ let commitment;
77
+ let proof;
78
+ if (params.outputProof && params.cementedCommitment) {
79
+ // Direct mode: use provided proof
80
+ commitment = params.cementedCommitment;
81
+ proof = params.outputProof;
82
+ }
83
+ else if (params.outboxLevel !== undefined) {
84
+ // Auto-fetch mode: query rollup node for proof
85
+ const rollupNodeUrl = ROLLUP_NODE_URLS[networkName];
86
+ if (!rollupNodeUrl) {
87
+ throw new Error(`No rollup node URL configured for ${networkName}. ` +
88
+ `Set ETHERLINK_ROLLUP_NODE_${networkName.toUpperCase()} environment variable, ` +
89
+ `or provide cementedCommitment and outputProof directly.`);
90
+ }
91
+ const messageIndex = params.messageIndex ?? 0;
92
+ const proofResult = await fetchProofFromRollupNode(rollupNodeUrl, params.outboxLevel, messageIndex);
93
+ commitment = proofResult.commitment;
94
+ proof = proofResult.proof;
95
+ }
96
+ else {
97
+ throw new Error("Provide either: (1) outboxLevel + messageIndex to auto-fetch proof from rollup node, " +
98
+ "or (2) cementedCommitment + outputProof directly.");
99
+ }
100
+ // Check spender has enough for fees
101
+ const spenderBalance = await Tezos.tz.getBalance(spendingAddress);
102
+ if (spenderBalance.isZero()) {
103
+ throw new Error("Spending account balance is 0. Please fund the spending address to cover transaction fees.");
104
+ }
105
+ await ensureRevealed(Tezos);
106
+ // Execute the outbox message
107
+ const op = await Tezos.contract.smartRollupExecuteOutboxMessage({
108
+ rollup: bridgeConfig.smartRollupAddress,
109
+ cementedCommitment: commitment,
110
+ outputProof: proof,
111
+ });
112
+ await op.confirmation(CONFIRMATIONS_TO_WAIT);
113
+ const tzktBase = config.tzktApi.replace('api.', '').replace('/v1', '');
114
+ const tzktUrl = `${tzktBase}/${op.hash}`;
115
+ return {
116
+ content: [{
117
+ type: "text",
118
+ text: `Outbox message executed successfully.\n` +
119
+ `Commitment: ${commitment}\n` +
120
+ `Operation: ${tzktUrl}\n` +
121
+ `The withdrawal funds have been released to the recipient on Tezos L1.`,
122
+ }],
123
+ };
124
+ },
125
+ });
@@ -0,0 +1,25 @@
1
+ import z from "zod";
2
+ import type { LiveConfig } from "../live-config.js";
3
+ export declare const createBridgeStatusTool: (config: LiveConfig) => {
4
+ name: string;
5
+ config: {
6
+ title: string;
7
+ description: string;
8
+ inputSchema: z.ZodObject<{
9
+ address: z.ZodOptional<z.ZodString>;
10
+ limit: z.ZodOptional<z.ZodNumber>;
11
+ }, z.z.core.$strip>;
12
+ annotations: {
13
+ readOnlyHint: boolean;
14
+ destructiveHint: boolean;
15
+ idempotentHint: boolean;
16
+ openWorldHint: boolean;
17
+ };
18
+ };
19
+ handler: (params: any) => Promise<{
20
+ content: {
21
+ type: "text";
22
+ text: string;
23
+ }[];
24
+ }>;
25
+ };
@@ -0,0 +1,155 @@
1
+ import axios from "axios";
2
+ import z from "zod";
3
+ const BRIDGE_CONFIG = {
4
+ mainnet: {
5
+ bridgeContract: "KT1Wj8SUGmnEPFqyahHAcjcNQwe6YGhEXJb5",
6
+ exchangerContract: "KT1CeFqjJRJPNVvhvznQrWfHad2jCiDZ6Lyj",
7
+ fastWithdrawalContract: "KT1BGwyCrnJ6HuEYP7X8Q2UooTdxmEYHiK6j",
8
+ smartRollupAddress: "sr1Ghq66tYK9y3r8CC1Tf8i8m5nxh8nTvZEf",
9
+ },
10
+ shadownet: {
11
+ bridgeContract: "KT19aBsSWvWtvEkbiqReJnD8UzQMWcD8SHUD",
12
+ exchangerContract: "KT1JYZsawXmeArts18nn4uT79tUJc4AGTYgc",
13
+ fastWithdrawalContract: "KT1E1f7ze8FcjbFu4UwnBqNo4kbbS6Zo5Y9W",
14
+ smartRollupAddress: "sr19fMYrr5C4qqvQqQrDSjtP31GcrWjodzvg",
15
+ },
16
+ };
17
+ const MUTEZ_PER_TEZ = 1_000_000;
18
+ const inputSchema = z.object({
19
+ address: z.string().optional().describe("Tezos (tz1/tz2/KT1) or Etherlink (0x) address to check bridge transfers for. Defaults to the spending contract address."),
20
+ limit: z.number().optional().describe("Max results per category. Default: 5"),
21
+ });
22
+ function formatMutez(mutez) {
23
+ return `${(mutez / MUTEZ_PER_TEZ).toFixed(6)} XTZ`;
24
+ }
25
+ function formatEtherlinkAddress(hex) {
26
+ return `0x${hex}`;
27
+ }
28
+ export const createBridgeStatusTool = (config) => ({
29
+ name: "tezos_bridge_status",
30
+ config: {
31
+ title: "Bridge Transfer Status",
32
+ description: "Checks the status of Tezos <-> Etherlink bridge transfers for a given address. " +
33
+ "Shows deposits (Tezos -> Etherlink), standard withdrawals (Etherlink -> Tezos), " +
34
+ "and fast withdrawals. Accepts either a Tezos or Etherlink address.",
35
+ inputSchema,
36
+ annotations: {
37
+ readOnlyHint: true,
38
+ destructiveHint: false,
39
+ idempotentHint: true,
40
+ openWorldHint: true,
41
+ },
42
+ },
43
+ handler: async (params) => {
44
+ params = params;
45
+ const { networkName, spendingContract, tzktApi } = config;
46
+ const bridgeConfig = BRIDGE_CONFIG[networkName];
47
+ if (!bridgeConfig) {
48
+ throw new Error(`Bridge not supported on network: ${networkName}`);
49
+ }
50
+ const limit = params.limit ?? 5;
51
+ const address = params.address ?? spendingContract;
52
+ const isTezosAddress = /^(tz[123]|KT1)[a-zA-Z0-9]{33}$/.test(address);
53
+ const isEtherlinkAddress = /^0x[0-9a-fA-F]{40}$/.test(address);
54
+ if (!isTezosAddress && !isEtherlinkAddress) {
55
+ throw new Error("Invalid address format. Provide a Tezos (tz1/tz2/KT1) or Etherlink (0x) address.");
56
+ }
57
+ const results = [];
58
+ const tzktBase = tzktApi;
59
+ // Query deposits
60
+ try {
61
+ let depositUrl;
62
+ if (isTezosAddress) {
63
+ depositUrl = `${tzktBase}/v1/operations/transactions?target=${bridgeConfig.bridgeContract}&entrypoint=deposit&sender=${address}&limit=${limit}&sort.desc=id`;
64
+ }
65
+ else {
66
+ const l2Hex = address.slice(2).toLowerCase();
67
+ depositUrl = `${tzktBase}/v1/operations/transactions?target=${bridgeConfig.bridgeContract}&entrypoint=deposit&parameter.l2_address=${l2Hex}&limit=${limit}&sort.desc=id`;
68
+ }
69
+ const { data: deposits } = await axios.get(depositUrl);
70
+ if (deposits.length > 0) {
71
+ results.push(`## Deposits (Tezos → Etherlink): ${deposits.length} found\n`);
72
+ for (const dep of deposits) {
73
+ const amount = formatMutez(dep.amount);
74
+ const l2 = dep.parameter?.value?.l2_address
75
+ ? formatEtherlinkAddress(dep.parameter.value.l2_address)
76
+ : "unknown";
77
+ const from = dep.sender?.address ?? "unknown";
78
+ results.push(`- **${amount}** → ${l2}\n` +
79
+ ` From: ${from} | ${dep.status} | ${dep.timestamp}\n` +
80
+ ` Hash: ${dep.hash}`);
81
+ }
82
+ }
83
+ else {
84
+ results.push("## Deposits (Tezos → Etherlink): none found");
85
+ }
86
+ }
87
+ catch {
88
+ results.push("## Deposits: query failed");
89
+ }
90
+ // Query standard withdrawals (exchanger burn) — only for Tezos addresses
91
+ if (isTezosAddress) {
92
+ try {
93
+ const burnUrl = `${tzktBase}/v1/operations/transactions?target=${bridgeConfig.exchangerContract}&entrypoint=burn&parameter.address=${address}&limit=${limit}&sort.desc=id`;
94
+ const { data: burns } = await axios.get(burnUrl);
95
+ if (burns.length > 0) {
96
+ results.push(`\n## Standard Withdrawals (Etherlink → Tezos): ${burns.length} found\n`);
97
+ for (const burn of burns) {
98
+ const ticketAmount = burn.parameter?.value?.ticket?.amount ?? "?";
99
+ const amount = formatMutez(Number(ticketAmount));
100
+ results.push(`- **${amount}** → ${address}\n` +
101
+ ` ${burn.status} | ${burn.timestamp}\n` +
102
+ ` Hash: ${burn.hash}`);
103
+ }
104
+ }
105
+ else {
106
+ results.push("\n## Standard Withdrawals (Etherlink → Tezos): none found");
107
+ }
108
+ }
109
+ catch {
110
+ results.push("\n## Standard Withdrawals: query failed");
111
+ }
112
+ }
113
+ // Query fast withdrawals
114
+ try {
115
+ let fastUrl;
116
+ if (isTezosAddress) {
117
+ fastUrl = `${tzktBase}/v1/operations/transactions?target=${bridgeConfig.fastWithdrawalContract}&entrypoint=payout_withdrawal&parameter.withdrawal.base_withdrawer=${address}&limit=${limit}&sort.desc=id`;
118
+ }
119
+ else {
120
+ const l2Hex = address.slice(2).toLowerCase();
121
+ fastUrl = `${tzktBase}/v1/operations/transactions?target=${bridgeConfig.fastWithdrawalContract}&entrypoint=payout_withdrawal&parameter.withdrawal.l2_caller=${l2Hex}&limit=${limit}&sort.desc=id`;
122
+ }
123
+ const { data: fastOps } = await axios.get(fastUrl);
124
+ if (fastOps.length > 0) {
125
+ results.push(`\n## Fast Withdrawals: ${fastOps.length} found\n`);
126
+ for (const fw of fastOps) {
127
+ const w = fw.parameter?.value?.withdrawal;
128
+ const fullAmount = w?.full_amount ? formatMutez(Number(w.full_amount)) : "?";
129
+ const paidAmount = formatMutez(fw.amount);
130
+ const recipient = w?.base_withdrawer ?? "unknown";
131
+ const l2Caller = w?.l2_caller ? formatEtherlinkAddress(w.l2_caller) : "unknown";
132
+ const fee = w?.full_amount
133
+ ? formatMutez(Number(w.full_amount) - fw.amount)
134
+ : "?";
135
+ results.push(`- **${fullAmount}** (paid: ${paidAmount}, fee: ${fee})\n` +
136
+ ` To: ${recipient} | From L2: ${l2Caller}\n` +
137
+ ` ID: ${w?.withdrawal_id ?? "?"} | ${fw.status} | ${fw.timestamp}\n` +
138
+ ` Hash: ${fw.hash}`);
139
+ }
140
+ }
141
+ else {
142
+ results.push("\n## Fast Withdrawals: none found");
143
+ }
144
+ }
145
+ catch {
146
+ results.push("\n## Fast Withdrawals: query failed");
147
+ }
148
+ return {
149
+ content: [{
150
+ type: "text",
151
+ text: `# Bridge Status for ${address}\nNetwork: ${networkName}\n\n${results.join("\n")}`,
152
+ }],
153
+ };
154
+ },
155
+ });
@@ -0,0 +1,25 @@
1
+ import z from "zod";
2
+ import { type LiveConfig } from "../live-config.js";
3
+ export declare const createBridgeToEtherlinkTool: (config: LiveConfig) => {
4
+ name: string;
5
+ config: {
6
+ title: string;
7
+ description: string;
8
+ inputSchema: z.ZodObject<{
9
+ amount: z.ZodNumber;
10
+ etherlinkAddress: z.ZodString;
11
+ }, z.z.core.$strip>;
12
+ annotations: {
13
+ readOnlyHint: boolean;
14
+ destructiveHint: boolean;
15
+ idempotentHint: boolean;
16
+ openWorldHint: boolean;
17
+ };
18
+ };
19
+ handler: (params: any) => Promise<{
20
+ content: {
21
+ type: "text";
22
+ text: string;
23
+ }[];
24
+ }>;
25
+ };
@@ -0,0 +1,116 @@
1
+ import z from "zod";
2
+ import { ensureRevealed } from "./reveal_account.js";
3
+ import { calculateFeeRebate } from "../live-config.js";
4
+ // Constants
5
+ const MUTEZ_PER_TEZ = 1_000_000;
6
+ const CONFIRMATIONS_TO_WAIT = 3;
7
+ // Bridge configuration per network
8
+ // bridge contract = the evm_bridge.mligo contract that handles deposits
9
+ // exchanger = the exchanger.mligo contract (not called directly by users)
10
+ // smartRollupAddress = passed as evm_address param to the bridge deposit entrypoint
11
+ const BRIDGE_CONFIG = {
12
+ mainnet: {
13
+ smartRollupAddress: "sr1Ghq66tYK9y3r8CC1Tf8i8m5nxh8nTvZEf",
14
+ bridgeContractAddress: "KT1Wj8SUGmnEPFqyahHAcjcNQwe6YGhEXJb5",
15
+ },
16
+ shadownet: {
17
+ smartRollupAddress: "sr19fMYrr5C4qqvQqQrDSjtP31GcrWjodzvg",
18
+ bridgeContractAddress: "KT19aBsSWvWtvEkbiqReJnD8UzQMWcD8SHUD",
19
+ },
20
+ };
21
+ const inputSchema = z.object({
22
+ amount: z.number().describe("The amount of XTZ to bridge to Etherlink."),
23
+ etherlinkAddress: z.string().describe("The Etherlink (0x) address to receive the bridged XTZ."),
24
+ });
25
+ // Cache the last-verified spender so we only hit the chain when config changes
26
+ let verifiedSpender = null;
27
+ export const createBridgeToEtherlinkTool = (config) => ({
28
+ name: "tezos_bridge_to_etherlink",
29
+ config: {
30
+ title: "Bridge XTZ to Etherlink",
31
+ description: "Bridges XTZ from Tezos to Etherlink via the native bridge. " +
32
+ "Sends XTZ from the spending contract through the Tezos-Etherlink bridge to the specified Etherlink address. " +
33
+ "The bridged XTZ will appear as native tokens on Etherlink after the rollup processes the deposit.",
34
+ inputSchema,
35
+ annotations: {
36
+ readOnlyHint: false,
37
+ destructiveHint: true,
38
+ idempotentHint: false,
39
+ openWorldHint: true,
40
+ },
41
+ },
42
+ handler: async (params) => {
43
+ params = params;
44
+ const { Tezos, spendingContract, spendingAddress, networkName } = config;
45
+ const bridgeConfig = BRIDGE_CONFIG[networkName];
46
+ if (!bridgeConfig) {
47
+ throw new Error(`Bridge not supported on network: ${networkName}`);
48
+ }
49
+ // Validate Etherlink address format
50
+ if (!/^0x[0-9a-fA-F]{40}$/.test(params.etherlinkAddress)) {
51
+ throw new Error("Invalid Etherlink address. Must be a 0x-prefixed, 40 hex character address.");
52
+ }
53
+ const amountMutez = Math.floor(params.amount * MUTEZ_PER_TEZ);
54
+ if (amountMutez <= 0) {
55
+ throw new Error("Amount must be greater than 0.");
56
+ }
57
+ // Verify the server's signer matches the on-chain spender (only when config changes)
58
+ const needsVerify = !verifiedSpender
59
+ || verifiedSpender.address !== spendingAddress
60
+ || verifiedSpender.contract !== spendingContract;
61
+ if (needsVerify) {
62
+ const c = await Tezos.contract.at(spendingContract);
63
+ const storage = await c.storage();
64
+ if (storage.spender !== spendingAddress) {
65
+ throw new Error(`Spender mismatch: the server's signing key (${spendingAddress}) does not match ` +
66
+ `the contract's spender (${storage.spender}). ` +
67
+ `Please regenerate the spender key from the dashboard.`);
68
+ }
69
+ verifiedSpender = { address: spendingAddress, contract: spendingContract };
70
+ }
71
+ // Check spending contract has enough for the bridge amount
72
+ const contractBalance = await Tezos.tz.getBalance(spendingContract);
73
+ if (contractBalance.toNumber() < amountMutez) {
74
+ throw new Error(`Insufficient spending contract balance. ` +
75
+ `Requested: ${amountMutez} mutez, Available: ${contractBalance.toNumber()} mutez`);
76
+ }
77
+ // Ensure account is revealed before estimating
78
+ await ensureRevealed(Tezos);
79
+ // Step 1: Fund the spender from the spending contract
80
+ // The spender needs the bridge amount + fees to execute the bridge deposit
81
+ const spenderBalance = await Tezos.tz.getBalance(spendingAddress);
82
+ const spenderMutez = spenderBalance.toNumber();
83
+ const feeRebate = calculateFeeRebate(spenderMutez);
84
+ const spendingContractInstance = await Tezos.contract.at(spendingContract);
85
+ const fundCall = spendingContractInstance.methodsObject.spend({
86
+ recipient: spendingAddress,
87
+ amount: amountMutez,
88
+ fee_rebate: feeRebate,
89
+ });
90
+ const fundOp = await fundCall.send();
91
+ await fundOp.confirmation(CONFIRMATIONS_TO_WAIT);
92
+ // Step 2: Call the bridge contract's deposit entrypoint directly
93
+ // The deposit entrypoint takes:
94
+ // evm_address: the smart rollup address (sr1...)
95
+ // l2_address: the Etherlink receiver as hex bytes (without 0x prefix)
96
+ // XTZ is sent along with the call as the amount to bridge
97
+ const bridgeContract = await Tezos.contract.at(bridgeConfig.bridgeContractAddress);
98
+ const l2AddressBytes = params.etherlinkAddress.slice(2).toLowerCase();
99
+ const depositCall = bridgeContract.methodsObject.deposit({
100
+ evm_address: bridgeConfig.smartRollupAddress,
101
+ l2_address: l2AddressBytes,
102
+ });
103
+ const depositOp = await depositCall.send({ amount: amountMutez, mutez: true });
104
+ await depositOp.confirmation(CONFIRMATIONS_TO_WAIT);
105
+ const tzktBase = config.tzktApi.replace('api.', '').replace('/v1', '');
106
+ const tzktUrl = `${tzktBase}/${depositOp.hash}`;
107
+ return {
108
+ content: [{
109
+ type: "text",
110
+ text: `Bridged ${params.amount} XTZ to Etherlink address ${params.etherlinkAddress}\n` +
111
+ `Tezos operation: ${tzktUrl}\n` +
112
+ `The deposit will appear on Etherlink after the rollup processes it (typically a few minutes).`,
113
+ }],
114
+ };
115
+ },
116
+ });
@@ -131,4 +131,72 @@ export declare const createTools: (liveConfig: LiveConfig, http: boolean) => ({
131
131
  text: string;
132
132
  }[];
133
133
  }>;
134
+ } | {
135
+ name: string;
136
+ config: {
137
+ title: string;
138
+ description: string;
139
+ inputSchema: import("zod").ZodObject<{
140
+ amount: import("zod").ZodNumber;
141
+ etherlinkAddress: import("zod").ZodString;
142
+ }, import("zod/v4/core").$strip>;
143
+ annotations: {
144
+ readOnlyHint: boolean;
145
+ destructiveHint: boolean;
146
+ idempotentHint: boolean;
147
+ openWorldHint: boolean;
148
+ };
149
+ };
150
+ handler: (params: any) => Promise<{
151
+ content: {
152
+ type: "text";
153
+ text: string;
154
+ }[];
155
+ }>;
156
+ } | {
157
+ name: string;
158
+ config: {
159
+ title: string;
160
+ description: string;
161
+ inputSchema: import("zod").ZodObject<{
162
+ address: import("zod").ZodOptional<import("zod").ZodString>;
163
+ limit: import("zod").ZodOptional<import("zod").ZodNumber>;
164
+ }, import("zod/v4/core").$strip>;
165
+ annotations: {
166
+ readOnlyHint: boolean;
167
+ destructiveHint: boolean;
168
+ idempotentHint: boolean;
169
+ openWorldHint: boolean;
170
+ };
171
+ };
172
+ handler: (params: any) => Promise<{
173
+ content: {
174
+ type: "text";
175
+ text: string;
176
+ }[];
177
+ }>;
178
+ } | {
179
+ name: string;
180
+ config: {
181
+ title: string;
182
+ description: string;
183
+ inputSchema: import("zod").ZodObject<{
184
+ outboxLevel: import("zod").ZodOptional<import("zod").ZodNumber>;
185
+ messageIndex: import("zod").ZodOptional<import("zod").ZodNumber>;
186
+ cementedCommitment: import("zod").ZodOptional<import("zod").ZodString>;
187
+ outputProof: import("zod").ZodOptional<import("zod").ZodString>;
188
+ }, import("zod/v4/core").$strip>;
189
+ annotations: {
190
+ readOnlyHint: boolean;
191
+ destructiveHint: boolean;
192
+ idempotentHint: boolean;
193
+ openWorldHint: boolean;
194
+ };
195
+ };
196
+ handler: (params: any) => Promise<{
197
+ content: {
198
+ type: "text";
199
+ text: string;
200
+ }[];
201
+ }>;
134
202
  })[];
@@ -8,6 +8,9 @@ import { createParseX402RequirementsTool } from "./parse_x402_requirements.js";
8
8
  import { createRevealAccountTool } from "./reveal_account.js";
9
9
  import { createRecoverSpenderFundsTool } from "./recover_spender_funds.js";
10
10
  import { createSendXtzTool } from "./send_xtz.js";
11
+ import { createBridgeToEtherlinkTool } from "./bridge_to_etherlink.js";
12
+ import { createBridgeStatusTool } from "./bridge_status.js";
13
+ import { createBridgeExecuteWithdrawalTool } from "./bridge_execute_withdrawal.js";
11
14
  import { createGetDashboardTool } from "./get_dashboard.js";
12
15
  const getNotConfiguredMessage = () => `Wallet not configured.
13
16
 
@@ -43,6 +46,9 @@ export const createTools = (liveConfig, http) => {
43
46
  createRecoverSpenderFundsTool(liveConfig),
44
47
  createRevealAccountTool(liveConfig),
45
48
  createSendXtzTool(liveConfig),
49
+ createBridgeToEtherlinkTool(liveConfig),
50
+ createBridgeStatusTool(liveConfig),
51
+ createBridgeExecuteWithdrawalTool(liveConfig),
46
52
  ];
47
53
  if (!http) {
48
54
  tools.push(createGetDashboardTool(liveConfig));
@@ -51,7 +51,7 @@ export const createRecoverSpenderFundsTool = (config) => ({
51
51
  const isContract = destination.startsWith("KT1");
52
52
  if (isContract) {
53
53
  const contract = await Tezos.contract.at(destination);
54
- const depositCall = contract.methodsObject.default_(null);
54
+ const depositCall = contract.methodsObject["default"](null);
55
55
  const estimate = await Tezos.estimate.contractCall(depositCall);
56
56
  const maxAmount = balanceMutez - estimate.suggestedFeeMutez;
57
57
  if (maxAmount <= 0) {
@@ -62,7 +62,7 @@ export const createRecoverSpenderFundsTool = (config) => ({
62
62
  }],
63
63
  };
64
64
  }
65
- const operation = await contract.methodsObject.default_(null).send({
65
+ const operation = await contract.methodsObject["default"](null).send({
66
66
  amount: maxAmount,
67
67
  mutez: true,
68
68
  fee: estimate.suggestedFeeMutez,