// src/tools/dex_swap.mo
import McpTypes "mo:mcp-motoko-sdk/mcp/Types";
import AuthTypes "mo:mcp-motoko-sdk/auth/Types";
import Result "mo:base/Result";
import Principal "mo:base/Principal";
module {
// Define the tool schema
public func config() : McpTypes.Tool = {
name = "dex_swap";
title = ?"Execute DEX Swap";
description = ?"Swap tokens using the DEX";
payment = null;
inputSchema = Json.obj([
("type", Json.str("object")),
("properties", Json.obj([
("fromToken", Json.obj([
("type", Json.str("string")),
("description", Json.str("Token to swap from (e.g., 'ICP')"))
])),
("amount", Json.obj([
("type", Json.str("string")),
("description", Json.str("Amount to swap"))
])),
("toToken", Json.obj([
("type", Json.str("string")),
("description", Json.str("Token to swap to (e.g., 'ckBTC')"))
])),
("userWallet", Json.obj([
("type", Json.str("string")),
("description", Json.str("User's wallet principal"))
]))
])),
("required", Json.arr([
Json.str("fromToken"),
Json.str("amount"),
Json.str("toToken"),
Json.str("userWallet")
]))
]);
outputSchema = ?Json.obj([
("type", Json.str("object")),
("properties", Json.obj([
("txId", Json.obj([("type", Json.str("string"))])),
("amountOut", Json.obj([("type", Json.str("string"))])),
("status", Json.obj([("type", Json.str("string"))]))
]))
]);
};
// Implement the tool handler
public func handle(
context: ToolContext.ToolContext
) : (
McpTypes.JsonValue,
?AuthTypes.AuthInfo,
(Result.Result<McpTypes.CallToolResult, McpTypes.HandlerError>) -> ()
) -> async () {
func(args, auth, cb) : async () {
// Helper functions
func makeError(message: Text) {
cb(#ok({
content = [#text({ text = message })];
isError = true;
structuredContent = null
}));
};
func ok(structured: Json.Json) {
cb(#ok({
content = [#text({ text = Json.stringify(structured, null) })];
isError = false;
structuredContent = ?structured
}));
};
// 1. Check authorization
let ownerPrincipal = switch (auth) {
case (?authInfo) authInfo.principal;
case (null) { return makeError("Authentication required") };
};
if (ownerPrincipal != context.owner) {
return makeError("Unauthorized: Only owner can execute swaps");
};
// 2. Parse inputs
let amount = switch (Json.getAsText(args, "amount")) {
case (#ok a) {
switch (Nat.fromText(a)) {
case (?n) n;
case (null) { return makeError("Invalid amount") };
}
};
case _ { return makeError("Missing amount") };
};
let userWallet = switch (Json.getAsText(args, "userWallet")) {
case (#ok w) {
switch (Principal.fromText(w)) {
case (p) p;
case (_) { return makeError("Invalid wallet principal") };
}
};
case _ { return makeError("Missing userWallet") };
};
// 3. Check wrapper's balance
let icpLedger = actor(Principal.toText(context.icpLedgerId)) : ICRC1.Self;
let wrapperBalance = await icpLedger.icrc1_balance_of({
owner = context.canisterPrincipal;
subaccount = null;
});
let fee = 10_000; // ICP fee
let totalNeeded = amount + fee;
// 4. Pull funds if needed
if (wrapperBalance < totalNeeded) {
let amountToPull = totalNeeded - wrapperBalance;
let transferResult = await icpLedger.icrc2_transfer_from({
from = { owner = userWallet; subaccount = null };
to = { owner = context.canisterPrincipal; subaccount = null };
amount = amountToPull;
fee = ?fee;
memo = null;
created_at_time = null;
});
switch (transferResult) {
case (#Ok(_)) {};
case (#Err(e)) {
return makeError("Failed to pull funds: " # debug_show(e));
};
};
};
// 5. Approve DEX
let approveResult = await icpLedger.icrc2_approve({
spender = { owner = context.dexCanisterId; subaccount = null };
amount = amount;
expires_at = ?(Time.now() + 300_000_000_000); // 5 min expiry
fee = ?fee;
memo = null;
created_at_time = null;
});
switch (approveResult) {
case (#Ok(_)) {};
case (#Err(e)) {
return makeError("Failed to approve DEX: " # debug_show(e));
};
};
// 6. Execute swap
let dex = actor(Principal.toText(context.dexCanisterId)) : DEX.Self;
let swapResult = await dex.swap({
fromToken = "ICP";
toToken = "ckBTC";
amount = amount;
slippage = 0.5; // 0.5%
});
switch (swapResult) {
case (#Ok(result)) {
let output = Json.obj([
("txId", Json.str(Nat.toText(result.txId))),
("amountOut", Json.str(Nat.toText(result.amountOut))),
("status", Json.str("success"))
]);
ok(output);
};
case (#Err(e)) {
makeError("Swap failed: " # e);
};
};
};
};
};