@zakyirsyaad/monagent-plugin 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/monad/identity/get.d.ts +19 -0
- package/dist/commands/monad/identity/get.js +113 -0
- package/dist/commands/monad/identity/register.d.ts +16 -0
- package/dist/commands/monad/identity/register.js +122 -0
- package/dist/commands/monad/jobs/complete.d.ts +15 -0
- package/dist/commands/monad/jobs/complete.js +63 -0
- package/dist/commands/{jobs → monad/jobs}/create.d.ts +5 -2
- package/dist/commands/monad/jobs/create.js +88 -0
- package/dist/commands/monad/jobs/refund.d.ts +14 -0
- package/dist/commands/monad/jobs/refund.js +56 -0
- package/dist/commands/{pay.d.ts → monad/pay.d.ts} +5 -2
- package/dist/commands/monad/pay.js +123 -0
- package/dist/commands/monad/reputation/check.d.ts +18 -0
- package/dist/commands/monad/reputation/check.js +103 -0
- package/dist/commands/monad/reputation/give.d.ts +15 -0
- package/dist/commands/monad/reputation/give.js +99 -0
- package/dist/commands/monad/x402/pay.d.ts +21 -0
- package/dist/commands/monad/x402/pay.js +224 -0
- package/dist/index.d.ts +10 -7
- package/dist/index.js +10 -7
- package/dist/monad.d.ts +424 -0
- package/dist/monad.js +186 -0
- package/dist/sdk.d.ts +62 -39
- package/dist/sdk.js +95 -8
- package/oclif.manifest.json +1304 -24
- package/package.json +45 -9
- package/dist/commands/identity/get.d.ts +0 -13
- package/dist/commands/identity/get.js +0 -59
- package/dist/commands/identity/register.d.ts +0 -12
- package/dist/commands/identity/register.js +0 -39
- package/dist/commands/jobs/create.js +0 -38
- package/dist/commands/pay.js +0 -33
- package/dist/commands/reputation/check.d.ts +0 -12
- package/dist/commands/reputation/check.js +0 -46
- package/dist/commands/reputation/give.d.ts +0 -12
- package/dist/commands/reputation/give.js +0 -45
- package/dist/commands/x402/pay.d.ts +0 -13
- package/dist/commands/x402/pay.js +0 -67
- package/dist/commands.test.d.ts +0 -1
- package/dist/commands.test.js +0 -141
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { encodeFunctionData, erc20Abi, parseEther, parseUnits } from "viem";
|
|
2
|
+
import { resolveChain, monadPaymentInputSchema, } from "../../monad.js";
|
|
3
|
+
import { BaseMonadPluginCommand, CommandError, InputFieldType, executeTransaction, } from "../../sdk.js";
|
|
4
|
+
import { PluginCommand } from "@metamask/agent-wallet/plugin";
|
|
5
|
+
export class MonadPayCommand extends BaseMonadPluginCommand {
|
|
6
|
+
static description = "Send direct MON or ERC-20 payment on Monad";
|
|
7
|
+
pluginCommandId = "monad:pay";
|
|
8
|
+
static inputs = {
|
|
9
|
+
to: {
|
|
10
|
+
type: InputFieldType.Text,
|
|
11
|
+
flag: "to",
|
|
12
|
+
message: "Recipient EVM address on Monad",
|
|
13
|
+
required: true,
|
|
14
|
+
},
|
|
15
|
+
amount: {
|
|
16
|
+
type: InputFieldType.Text,
|
|
17
|
+
flag: "amount",
|
|
18
|
+
message: "Amount to transfer (e.g. 0.5)",
|
|
19
|
+
required: true,
|
|
20
|
+
},
|
|
21
|
+
token: {
|
|
22
|
+
type: InputFieldType.Text,
|
|
23
|
+
flag: "token",
|
|
24
|
+
message: "Token symbol (MON, USDC, or 0x<address>)",
|
|
25
|
+
default: "MON",
|
|
26
|
+
required: false,
|
|
27
|
+
},
|
|
28
|
+
memo: {
|
|
29
|
+
type: InputFieldType.Text,
|
|
30
|
+
flag: "memo",
|
|
31
|
+
message: "Payment memo or reason (audit log note, not stored on-chain)",
|
|
32
|
+
required: false,
|
|
33
|
+
},
|
|
34
|
+
chainId: {
|
|
35
|
+
type: InputFieldType.Text,
|
|
36
|
+
flag: "chain-id",
|
|
37
|
+
aliases: ["chainId"],
|
|
38
|
+
message: "Monad chain ID (10143 for testnet, 143 for mainnet)",
|
|
39
|
+
required: false,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
static flags = PluginCommand.flagsWithInputs(this.inputs);
|
|
43
|
+
async execute(io) {
|
|
44
|
+
const rawInputs = await io.resolveInputs(MonadPayCommand.inputs);
|
|
45
|
+
const parsed = monadPaymentInputSchema.safeParse(rawInputs);
|
|
46
|
+
if (!parsed.success) {
|
|
47
|
+
throw new CommandError("INVALID_INPUT", `Invalid payment input: ${parsed.error.issues.map((i) => i.message).join(", ")}`, "Provide a valid recipient address and positive amount.");
|
|
48
|
+
}
|
|
49
|
+
const { to, amount, token, memo } = parsed.data;
|
|
50
|
+
const chain = resolveChain(rawInputs.chainId);
|
|
51
|
+
const tokenUpper = token.trim().toUpperCase();
|
|
52
|
+
let targetAddress;
|
|
53
|
+
let txValue = 0n;
|
|
54
|
+
let txData = undefined;
|
|
55
|
+
if (tokenUpper === "MON") {
|
|
56
|
+
targetAddress = to;
|
|
57
|
+
txValue = parseEther(amount);
|
|
58
|
+
if (txValue <= 0n) {
|
|
59
|
+
throw new CommandError("INVALID_AMOUNT", "Amount must be strictly greater than zero.", "Provide an amount > 0.");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else if (tokenUpper === "USDC") {
|
|
63
|
+
targetAddress = chain.usdc;
|
|
64
|
+
const parsedAmount = parseUnits(amount, 6);
|
|
65
|
+
if (parsedAmount <= 0n) {
|
|
66
|
+
throw new CommandError("INVALID_AMOUNT", "Amount must be strictly greater than zero.", "Provide an amount > 0.");
|
|
67
|
+
}
|
|
68
|
+
txData = encodeFunctionData({
|
|
69
|
+
abi: erc20Abi,
|
|
70
|
+
functionName: "transfer",
|
|
71
|
+
args: [to, parsedAmount],
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
else if (/^0x[a-fA-F0-9]{40}$/.test(token.trim())) {
|
|
75
|
+
const customTokenAddress = token.trim();
|
|
76
|
+
const client = this.getPublicClient(chain.chainId);
|
|
77
|
+
const code = await client.getCode({ address: customTokenAddress });
|
|
78
|
+
if (!code || code === "0x") {
|
|
79
|
+
throw new CommandError("INVALID_TOKEN_CONTRACT", `No contract bytecode found at token address ${customTokenAddress} on chain ${chain.chainId}.`, "Provide a valid deployed ERC-20 token contract address.");
|
|
80
|
+
}
|
|
81
|
+
let decimals = 18;
|
|
82
|
+
try {
|
|
83
|
+
decimals = (await client.readContract({
|
|
84
|
+
address: customTokenAddress,
|
|
85
|
+
abi: erc20Abi,
|
|
86
|
+
functionName: "decimals",
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
throw new CommandError("INVALID_TOKEN_CONTRACT", `Failed to read decimals from token contract ${customTokenAddress}: ${err?.message || String(err)}`, "Ensure the target address implements the ERC-20 standard.");
|
|
91
|
+
}
|
|
92
|
+
const parsedAmount = parseUnits(amount, decimals);
|
|
93
|
+
if (parsedAmount <= 0n) {
|
|
94
|
+
throw new CommandError("INVALID_AMOUNT", "Amount must be strictly greater than zero.", "Provide an amount > 0.");
|
|
95
|
+
}
|
|
96
|
+
targetAddress = customTokenAddress;
|
|
97
|
+
txData = encodeFunctionData({
|
|
98
|
+
abi: erc20Abi,
|
|
99
|
+
functionName: "transfer",
|
|
100
|
+
args: [to, parsedAmount],
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
throw new CommandError("UNSUPPORTED_TOKEN", `Token "${token}" is not supported. Supported tokens are MON, USDC, or a contract address (0x...).`, "Specify --token MON, --token USDC, or --token 0x<address>.");
|
|
105
|
+
}
|
|
106
|
+
const hash = await executeTransaction(this.ctx, io, this.pluginCommandId, {
|
|
107
|
+
chainId: chain.chainId,
|
|
108
|
+
to: targetAddress,
|
|
109
|
+
value: txValue,
|
|
110
|
+
data: txData,
|
|
111
|
+
});
|
|
112
|
+
io.emit(`Paid ${amount} ${tokenUpper === "MON" ? "MON" : token} to ${to} on ${chain.name}. TxHash: ${hash}`);
|
|
113
|
+
io.emit(`Explorer: ${chain.explorerUrl}/tx/${hash}`);
|
|
114
|
+
return {
|
|
115
|
+
transactionHash: hash,
|
|
116
|
+
to,
|
|
117
|
+
amount,
|
|
118
|
+
token,
|
|
119
|
+
memo,
|
|
120
|
+
chainId: chain.chainId,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { BaseMonadPluginCommand, type CommandIO, type InputSchema } from "../../../sdk.js";
|
|
2
|
+
export interface CheckReputationResult {
|
|
3
|
+
agentId: string;
|
|
4
|
+
feedbackCount: number;
|
|
5
|
+
averageScore: number;
|
|
6
|
+
trustTier: "HIGH" | "MEDIUM" | "LOW" | "UNRATED";
|
|
7
|
+
chainId: number;
|
|
8
|
+
}
|
|
9
|
+
export declare class MonadReputationCheckCommand extends BaseMonadPluginCommand<CheckReputationResult> {
|
|
10
|
+
static description: string;
|
|
11
|
+
static requiresAuth: boolean;
|
|
12
|
+
static requiresInit: boolean;
|
|
13
|
+
protected readonly pluginCommandId = "monad:reputation:check";
|
|
14
|
+
static readonly inputs: InputSchema;
|
|
15
|
+
static flags: any;
|
|
16
|
+
static args: any;
|
|
17
|
+
execute(io: CommandIO): Promise<CheckReputationResult>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { resolveChain, erc8004ReputationAbi, } from "../../../monad.js";
|
|
2
|
+
import { BaseMonadPluginCommand, CommandError, InputFieldType, } from "../../../sdk.js";
|
|
3
|
+
import { PluginCommand, schemaToArgs } from "@metamask/agent-wallet/plugin";
|
|
4
|
+
export class MonadReputationCheckCommand extends BaseMonadPluginCommand {
|
|
5
|
+
static description = "Check peer agent reputation on Monad ERC-8004 Reputation Registry";
|
|
6
|
+
static requiresAuth = false;
|
|
7
|
+
static requiresInit = false;
|
|
8
|
+
pluginCommandId = "monad:reputation:check";
|
|
9
|
+
static inputs = {
|
|
10
|
+
agentId: {
|
|
11
|
+
type: InputFieldType.Text,
|
|
12
|
+
flag: "agentId",
|
|
13
|
+
message: "Agent token ID in ERC-8004 registry",
|
|
14
|
+
required: true,
|
|
15
|
+
index: 0,
|
|
16
|
+
},
|
|
17
|
+
tag1: {
|
|
18
|
+
type: InputFieldType.Text,
|
|
19
|
+
flag: "tag1",
|
|
20
|
+
message: "Filter by primary category tag (e.g. speed)",
|
|
21
|
+
required: false,
|
|
22
|
+
},
|
|
23
|
+
tag2: {
|
|
24
|
+
type: InputFieldType.Text,
|
|
25
|
+
flag: "tag2",
|
|
26
|
+
message: "Filter by secondary category tag (e.g. task)",
|
|
27
|
+
required: false,
|
|
28
|
+
},
|
|
29
|
+
chainId: {
|
|
30
|
+
type: InputFieldType.Text,
|
|
31
|
+
flag: "chain-id",
|
|
32
|
+
aliases: ["chainId"],
|
|
33
|
+
message: "Monad chain ID (10143 for testnet, 143 for mainnet)",
|
|
34
|
+
required: false,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
static flags = PluginCommand.flagsWithInputs(this.inputs);
|
|
38
|
+
static args = schemaToArgs(this.inputs);
|
|
39
|
+
async execute(io) {
|
|
40
|
+
const rawInputs = await io.resolveInputs(MonadReputationCheckCommand.inputs);
|
|
41
|
+
const agentId = String(rawInputs.agentId).trim();
|
|
42
|
+
if (!/^\d+$/.test(agentId)) {
|
|
43
|
+
throw new CommandError("INVALID_INPUT", `Invalid agentId "${agentId}". Must be a numeric string.`, "Provide a valid agent ID number (e.g. 1).");
|
|
44
|
+
}
|
|
45
|
+
const tag1 = rawInputs.tag1 || "";
|
|
46
|
+
const tag2 = rawInputs.tag2 || "";
|
|
47
|
+
const chain = resolveChain(rawInputs.chainId);
|
|
48
|
+
const client = this.getPublicClient(chain.chainId);
|
|
49
|
+
const tokenIdBigInt = BigInt(agentId);
|
|
50
|
+
// Official ERC-8004 requires clientAddresses array (reverts if empty without getClients)
|
|
51
|
+
let clientsToQuery = [];
|
|
52
|
+
try {
|
|
53
|
+
const registeredClients = await client.readContract({
|
|
54
|
+
address: chain.reputationRegistry,
|
|
55
|
+
abi: erc8004ReputationAbi,
|
|
56
|
+
functionName: "getClients",
|
|
57
|
+
args: [tokenIdBigInt],
|
|
58
|
+
});
|
|
59
|
+
if (registeredClients && Array.isArray(registeredClients)) {
|
|
60
|
+
clientsToQuery = registeredClients;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Contract might have no clients or call failed
|
|
65
|
+
}
|
|
66
|
+
let feedbackCount = 0;
|
|
67
|
+
let averageScore = 0;
|
|
68
|
+
if (clientsToQuery.length > 0) {
|
|
69
|
+
try {
|
|
70
|
+
const [count, summaryValue, decimals] = await client.readContract({
|
|
71
|
+
address: chain.reputationRegistry,
|
|
72
|
+
abi: erc8004ReputationAbi,
|
|
73
|
+
functionName: "getSummary",
|
|
74
|
+
args: [tokenIdBigInt, clientsToQuery, tag1, tag2],
|
|
75
|
+
});
|
|
76
|
+
feedbackCount = Number(count);
|
|
77
|
+
const scoreDivider = 10 ** decimals;
|
|
78
|
+
averageScore =
|
|
79
|
+
feedbackCount > 0 ? Number(summaryValue) / (feedbackCount * scoreDivider) : 0;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Fallback to unrated if getSummary reverts
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
let trustTier = "UNRATED";
|
|
86
|
+
if (feedbackCount > 0) {
|
|
87
|
+
if (averageScore >= 80)
|
|
88
|
+
trustTier = "HIGH";
|
|
89
|
+
else if (averageScore >= 50)
|
|
90
|
+
trustTier = "MEDIUM";
|
|
91
|
+
else
|
|
92
|
+
trustTier = "LOW";
|
|
93
|
+
}
|
|
94
|
+
io.emit(`Agent #${agentId} Reputation on ${chain.name}: ${feedbackCount} reviews, Average: ${averageScore.toFixed(1)}, Trust Tier: ${trustTier}`);
|
|
95
|
+
return {
|
|
96
|
+
agentId,
|
|
97
|
+
feedbackCount,
|
|
98
|
+
averageScore,
|
|
99
|
+
trustTier,
|
|
100
|
+
chainId: chain.chainId,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type MonadReputationFeedback } from "../../../monad.js";
|
|
2
|
+
import { BaseMonadPluginCommand, type CommandIO, type InputSchema } from "../../../sdk.js";
|
|
3
|
+
export interface GiveReputationResult {
|
|
4
|
+
transactionHash: `0x${string}`;
|
|
5
|
+
feedback: MonadReputationFeedback;
|
|
6
|
+
registryAddress: string;
|
|
7
|
+
chainId: number;
|
|
8
|
+
}
|
|
9
|
+
export declare class MonadReputationGiveCommand extends BaseMonadPluginCommand<GiveReputationResult> {
|
|
10
|
+
static description: string;
|
|
11
|
+
protected readonly pluginCommandId = "monad:reputation:give";
|
|
12
|
+
static readonly inputs: InputSchema;
|
|
13
|
+
static flags: any;
|
|
14
|
+
execute(io: CommandIO): Promise<GiveReputationResult>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { encodeFunctionData, keccak256, toHex } from "viem";
|
|
2
|
+
import { resolveChain, erc8004ReputationAbi, monadReputationFeedbackSchema, } from "../../../monad.js";
|
|
3
|
+
import { BaseMonadPluginCommand, CommandError, InputFieldType, executeTransaction, } from "../../../sdk.js";
|
|
4
|
+
import { PluginCommand } from "@metamask/agent-wallet/plugin";
|
|
5
|
+
export class MonadReputationGiveCommand extends BaseMonadPluginCommand {
|
|
6
|
+
static description = "Submit on-chain peer feedback to Monad ERC-8004 Reputation Registry";
|
|
7
|
+
pluginCommandId = "monad:reputation:give";
|
|
8
|
+
static inputs = {
|
|
9
|
+
agentId: {
|
|
10
|
+
type: InputFieldType.Text,
|
|
11
|
+
flag: "agentId",
|
|
12
|
+
message: "Target agent ID in ERC-8004 registry",
|
|
13
|
+
required: true,
|
|
14
|
+
},
|
|
15
|
+
value: {
|
|
16
|
+
type: InputFieldType.Text,
|
|
17
|
+
flag: "value",
|
|
18
|
+
message: "Feedback score (-100 to 100)",
|
|
19
|
+
required: true,
|
|
20
|
+
},
|
|
21
|
+
decimals: {
|
|
22
|
+
type: InputFieldType.Text,
|
|
23
|
+
flag: "decimals",
|
|
24
|
+
message: "Decimals for score (default 0)",
|
|
25
|
+
required: false,
|
|
26
|
+
},
|
|
27
|
+
tag1: {
|
|
28
|
+
type: InputFieldType.Text,
|
|
29
|
+
flag: "tag1",
|
|
30
|
+
message: "Primary feedback category tag (e.g. speed)",
|
|
31
|
+
required: false,
|
|
32
|
+
},
|
|
33
|
+
tag2: {
|
|
34
|
+
type: InputFieldType.Text,
|
|
35
|
+
flag: "tag2",
|
|
36
|
+
message: "Secondary category tag (e.g. accuracy)",
|
|
37
|
+
required: false,
|
|
38
|
+
},
|
|
39
|
+
endpoint: {
|
|
40
|
+
type: InputFieldType.Text,
|
|
41
|
+
flag: "endpoint",
|
|
42
|
+
message: "Service endpoint that performed the task",
|
|
43
|
+
required: false,
|
|
44
|
+
},
|
|
45
|
+
feedbackURI: {
|
|
46
|
+
type: InputFieldType.Text,
|
|
47
|
+
flag: "feedbackURI",
|
|
48
|
+
message: "URI pointing to detailed review or task proof",
|
|
49
|
+
required: false,
|
|
50
|
+
},
|
|
51
|
+
chainId: {
|
|
52
|
+
type: InputFieldType.Text,
|
|
53
|
+
flag: "chain-id",
|
|
54
|
+
aliases: ["chainId"],
|
|
55
|
+
message: "Monad chain ID (10143 for testnet, 143 for mainnet)",
|
|
56
|
+
required: false,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
static flags = PluginCommand.flagsWithInputs(this.inputs);
|
|
60
|
+
async execute(io) {
|
|
61
|
+
const rawInputs = await io.resolveInputs(MonadReputationGiveCommand.inputs);
|
|
62
|
+
const parsed = monadReputationFeedbackSchema.safeParse(rawInputs);
|
|
63
|
+
if (!parsed.success) {
|
|
64
|
+
throw new CommandError("INVALID_INPUT", `Invalid feedback input: ${parsed.error.issues.map((i) => i.message).join(", ")}`, "Provide a numeric agentId and score between -100 and 100.");
|
|
65
|
+
}
|
|
66
|
+
const feedback = parsed.data;
|
|
67
|
+
const chain = resolveChain(rawInputs.chainId);
|
|
68
|
+
const feedbackHash = feedback.feedbackURI
|
|
69
|
+
? keccak256(toHex(feedback.feedbackURI))
|
|
70
|
+
: keccak256(toHex("feedback"));
|
|
71
|
+
const data = encodeFunctionData({
|
|
72
|
+
abi: erc8004ReputationAbi,
|
|
73
|
+
functionName: "giveFeedback",
|
|
74
|
+
args: [
|
|
75
|
+
BigInt(feedback.agentId),
|
|
76
|
+
BigInt(feedback.value),
|
|
77
|
+
feedback.decimals,
|
|
78
|
+
feedback.tag1,
|
|
79
|
+
feedback.tag2,
|
|
80
|
+
feedback.endpoint,
|
|
81
|
+
feedback.feedbackURI,
|
|
82
|
+
feedbackHash,
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
const hash = await executeTransaction(this.ctx, io, this.pluginCommandId, {
|
|
86
|
+
chainId: chain.chainId,
|
|
87
|
+
to: chain.reputationRegistry,
|
|
88
|
+
data,
|
|
89
|
+
});
|
|
90
|
+
io.emit(`Feedback submitted for Agent #${feedback.agentId} on ${chain.name}! Score: ${feedback.value}. TxHash: ${hash}`);
|
|
91
|
+
io.emit(`Explorer: ${chain.explorerUrl}/tx/${hash}`);
|
|
92
|
+
return {
|
|
93
|
+
transactionHash: hash,
|
|
94
|
+
feedback,
|
|
95
|
+
registryAddress: chain.reputationRegistry,
|
|
96
|
+
chainId: chain.chainId,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { BaseMonadPluginCommand, type CommandIO, type InputSchema } from "../../../sdk.js";
|
|
2
|
+
export interface X402PayResult {
|
|
3
|
+
url: string;
|
|
4
|
+
statusCode: number;
|
|
5
|
+
response: string;
|
|
6
|
+
paymentSettled: boolean;
|
|
7
|
+
paymentDetails?: {
|
|
8
|
+
scheme: string;
|
|
9
|
+
network: string;
|
|
10
|
+
asset: string;
|
|
11
|
+
amount: string;
|
|
12
|
+
payTo: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export declare class MonadX402PayCommand extends BaseMonadPluginCommand<X402PayResult> {
|
|
16
|
+
static description: string;
|
|
17
|
+
protected readonly pluginCommandId = "monad:x402:pay";
|
|
18
|
+
static readonly inputs: InputSchema;
|
|
19
|
+
static flags: any;
|
|
20
|
+
execute(io: CommandIO): Promise<X402PayResult>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { x402Client, x402HTTPClient } from "@x402/core/client";
|
|
2
|
+
import { registerExactEvmScheme } from "@x402/evm/exact/client";
|
|
3
|
+
import { recoverTypedDataAddress } from "viem";
|
|
4
|
+
import { resolveChain, MONAD_TESTNET_USDC, MONAD_MAINNET_USDC, } from "../../../monad.js";
|
|
5
|
+
import { BaseMonadPluginCommand, CommandError, InputFieldType, executeSignTypedData, } from "../../../sdk.js";
|
|
6
|
+
import { PluginCommand } from "@metamask/agent-wallet/plugin";
|
|
7
|
+
export class MonadX402PayCommand extends BaseMonadPluginCommand {
|
|
8
|
+
static description = "Execute paid tool/API call over HTTP 402 with real x402 EVM micropayments";
|
|
9
|
+
pluginCommandId = "monad:x402:pay";
|
|
10
|
+
static inputs = {
|
|
11
|
+
url: {
|
|
12
|
+
type: InputFieldType.Text,
|
|
13
|
+
flag: "url",
|
|
14
|
+
message: "Target HTTP 402 paid endpoint URL",
|
|
15
|
+
required: true,
|
|
16
|
+
},
|
|
17
|
+
method: {
|
|
18
|
+
type: InputFieldType.Text,
|
|
19
|
+
flag: "method",
|
|
20
|
+
message: "HTTP method (GET or POST)",
|
|
21
|
+
default: "GET",
|
|
22
|
+
required: false,
|
|
23
|
+
},
|
|
24
|
+
body: {
|
|
25
|
+
type: InputFieldType.Text,
|
|
26
|
+
flag: "body",
|
|
27
|
+
message: "HTTP request JSON body for POST requests",
|
|
28
|
+
required: false,
|
|
29
|
+
},
|
|
30
|
+
maxSpend: {
|
|
31
|
+
type: InputFieldType.Text,
|
|
32
|
+
flag: "maxSpend",
|
|
33
|
+
aliases: ["max-spend"],
|
|
34
|
+
message: "Maximum spend limit in token base units (e.g. 1000000 for 1 USDC)",
|
|
35
|
+
required: false,
|
|
36
|
+
},
|
|
37
|
+
payer: {
|
|
38
|
+
type: InputFieldType.Text,
|
|
39
|
+
flag: "payer",
|
|
40
|
+
message: "Payer EVM wallet address to sign payment authorization",
|
|
41
|
+
required: true,
|
|
42
|
+
},
|
|
43
|
+
chainId: {
|
|
44
|
+
type: InputFieldType.Text,
|
|
45
|
+
flag: "chain-id",
|
|
46
|
+
aliases: ["chainId"],
|
|
47
|
+
message: "Monad chain ID (10143 for testnet, 143 for mainnet)",
|
|
48
|
+
required: false,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
static flags = PluginCommand.flagsWithInputs(this.inputs);
|
|
52
|
+
async execute(io) {
|
|
53
|
+
const rawInputs = await io.resolveInputs(MonadX402PayCommand.inputs);
|
|
54
|
+
const url = String(rawInputs.url).trim();
|
|
55
|
+
const method = (rawInputs.method || "GET").toUpperCase();
|
|
56
|
+
const body = rawInputs.body;
|
|
57
|
+
const maxSpend = BigInt(rawInputs.maxSpend || "1000000");
|
|
58
|
+
const rawPayer = String(rawInputs.payer || "").trim();
|
|
59
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(rawPayer) || rawPayer.toLowerCase() === "0x0000000000000000000000000000000000000000") {
|
|
60
|
+
throw new CommandError("INVALID_INPUT", `Invalid or missing --payer address "${rawPayer}". A non-zero EVM address is required.`, "Provide your agent wallet address using --payer 0x...");
|
|
61
|
+
}
|
|
62
|
+
const payerAddress = rawPayer;
|
|
63
|
+
io.emit(`Executing request to paid API: ${url}...`);
|
|
64
|
+
let initialRes;
|
|
65
|
+
try {
|
|
66
|
+
initialRes = await fetch(url, {
|
|
67
|
+
method,
|
|
68
|
+
headers: {
|
|
69
|
+
Accept: "application/json",
|
|
70
|
+
...(body ? { "Content-Type": "application/json" } : {}),
|
|
71
|
+
},
|
|
72
|
+
body: method === "POST" && body ? body : undefined,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
throw new CommandError("FETCH_FAILED", `Failed to reach target URL: ${err?.message || String(err)}`, "Verify endpoint URL and network connectivity.");
|
|
77
|
+
}
|
|
78
|
+
if (initialRes.status !== 402) {
|
|
79
|
+
const text = await initialRes.text();
|
|
80
|
+
return {
|
|
81
|
+
url,
|
|
82
|
+
statusCode: initialRes.status,
|
|
83
|
+
response: text,
|
|
84
|
+
paymentSettled: false,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// Server returned 402 Payment Required. Parse x402 headers or JSON body.
|
|
88
|
+
const client = new x402Client();
|
|
89
|
+
const httpClient = new x402HTTPClient(client);
|
|
90
|
+
let paymentRequired;
|
|
91
|
+
try {
|
|
92
|
+
const headerVal = initialRes.headers.get("PAYMENT-REQUIRED") ||
|
|
93
|
+
initialRes.headers.get("payment-required");
|
|
94
|
+
const jsonBody = await initialRes.clone().json().catch(() => null);
|
|
95
|
+
if (headerVal) {
|
|
96
|
+
paymentRequired = httpClient.getPaymentRequiredResponse((name) => initialRes.headers.get(name), jsonBody);
|
|
97
|
+
}
|
|
98
|
+
else if (jsonBody && typeof jsonBody === "object") {
|
|
99
|
+
if (jsonBody.x402Version === 1) {
|
|
100
|
+
paymentRequired = jsonBody;
|
|
101
|
+
}
|
|
102
|
+
else if (jsonBody.x402Version === 2 || jsonBody.accepts) {
|
|
103
|
+
paymentRequired = {
|
|
104
|
+
x402Version: jsonBody.x402Version || 2,
|
|
105
|
+
accepts: jsonBody.accepts || [],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
throw new Error("Missing x402 accepts field in JSON body");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
throw new Error("No PAYMENT-REQUIRED header or JSON body present");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
throw new CommandError("INVALID_402_RESPONSE", `Server returned HTTP 402 but x402 Payment-Required headers/body could not be parsed: ${err?.message || String(err)}`, "Ensure the server implements the x402 protocol specification.");
|
|
118
|
+
}
|
|
119
|
+
// Resolve target chain from user input (defaults to 10143)
|
|
120
|
+
const targetChain = resolveChain(rawInputs.chainId);
|
|
121
|
+
// Inspect accepted payment requirements matching targetChain.caip2
|
|
122
|
+
const accepts = paymentRequired.accepts || [];
|
|
123
|
+
const requirement = accepts.find((req) => req.scheme === "exact" && req.network === targetChain.caip2);
|
|
124
|
+
if (!requirement) {
|
|
125
|
+
throw new CommandError("UNSUPPORTED_PAYMENT_NETWORK", `No compatible exact EVM payment requirement found for ${targetChain.name} (${targetChain.caip2}). Server accepts: ${JSON.stringify(accepts)}`, `Ensure the server supports ${targetChain.name} (${targetChain.caip2}).`);
|
|
126
|
+
}
|
|
127
|
+
const requestedAmount = BigInt(requirement.amount);
|
|
128
|
+
if (requestedAmount > maxSpend) {
|
|
129
|
+
throw new CommandError("MAX_SPEND_EXCEEDED", `Requested payment ${requestedAmount} exceeds maximum configured spend limit ${maxSpend}.`, "Increase --maxSpend if you wish to allow higher payment.");
|
|
130
|
+
}
|
|
131
|
+
const resolvedChain = targetChain;
|
|
132
|
+
// Enrich extra domain info if server didn't include it for USDC
|
|
133
|
+
const isUsdc = requirement.asset?.toLowerCase() === MONAD_TESTNET_USDC.toLowerCase() ||
|
|
134
|
+
requirement.asset?.toLowerCase() === MONAD_MAINNET_USDC.toLowerCase();
|
|
135
|
+
if (isUsdc && !requirement.extra) {
|
|
136
|
+
requirement.extra = {
|
|
137
|
+
name: "USDC",
|
|
138
|
+
version: "2",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (!requirement.maxTimeoutSeconds) {
|
|
142
|
+
requirement.maxTimeoutSeconds = 3600;
|
|
143
|
+
}
|
|
144
|
+
io.emit(`Negotiating x402 payment: ${requirement.amount} (${requirement.asset}) on ${resolvedChain.name} (${requirement.network}) to ${requirement.payTo}...`);
|
|
145
|
+
// createPaymentPayload must sign EXACTLY the inspected requirement
|
|
146
|
+
const sanitizedPaymentRequired = {
|
|
147
|
+
...paymentRequired,
|
|
148
|
+
accepts: [requirement],
|
|
149
|
+
};
|
|
150
|
+
// Build ClientEvmSigner backed by MetaMask walletExecutor
|
|
151
|
+
let lastTypedData = null;
|
|
152
|
+
let lastSignature = null;
|
|
153
|
+
const evmSigner = {
|
|
154
|
+
address: payerAddress,
|
|
155
|
+
signTypedData: async (typedDataMsg) => {
|
|
156
|
+
lastTypedData = typedDataMsg;
|
|
157
|
+
const sig = await executeSignTypedData(this.ctx, io, this.pluginCommandId, {
|
|
158
|
+
chainId: resolvedChain.chainId,
|
|
159
|
+
typedData: {
|
|
160
|
+
domain: typedDataMsg.domain,
|
|
161
|
+
types: typedDataMsg.types,
|
|
162
|
+
primaryType: typedDataMsg.primaryType,
|
|
163
|
+
message: typedDataMsg.message,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
lastSignature = sig;
|
|
167
|
+
return sig;
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
registerExactEvmScheme(client, { signer: evmSigner, networks: [requirement.network] });
|
|
171
|
+
let paymentPayload;
|
|
172
|
+
try {
|
|
173
|
+
paymentPayload = await httpClient.createPaymentPayload(sanitizedPaymentRequired);
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
throw new CommandError("PAYMENT_PAYLOAD_FAILED", `Failed to generate x402 payment authorization: ${err?.message || String(err)}`, "Ensure wallet is unlocked and approved to sign EIP-712 payment authorization.");
|
|
177
|
+
}
|
|
178
|
+
// Verify signer matches payerAddress using recoverTypedDataAddress. Abort on ANY error.
|
|
179
|
+
if (lastTypedData && lastSignature) {
|
|
180
|
+
try {
|
|
181
|
+
const recovered = await recoverTypedDataAddress({
|
|
182
|
+
domain: lastTypedData.domain,
|
|
183
|
+
types: lastTypedData.types,
|
|
184
|
+
primaryType: lastTypedData.primaryType,
|
|
185
|
+
message: lastTypedData.message,
|
|
186
|
+
signature: lastSignature,
|
|
187
|
+
});
|
|
188
|
+
if (recovered.toLowerCase() !== payerAddress.toLowerCase()) {
|
|
189
|
+
throw new CommandError("PAYER_MISMATCH", `Recovered signature address ${recovered} does not match specified --payer ${payerAddress}.`, "Ensure the active wallet in MetaMask matches --payer.");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
if (err instanceof CommandError)
|
|
194
|
+
throw err;
|
|
195
|
+
throw new CommandError("SIGNATURE_VERIFICATION_FAILED", `Failed to verify EIP-712 payment signature: ${err?.message || String(err)}`, "Verify that the wallet produced a valid EIP-712 signature.");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const paymentHeaders = httpClient.encodePaymentSignatureHeader(paymentPayload);
|
|
199
|
+
io.emit("x402 Payment authorization signed. Re-submitting request with payment proof...");
|
|
200
|
+
const paidRes = await fetch(url, {
|
|
201
|
+
method,
|
|
202
|
+
headers: {
|
|
203
|
+
Accept: "application/json",
|
|
204
|
+
...(body ? { "Content-Type": "application/json" } : {}),
|
|
205
|
+
...paymentHeaders,
|
|
206
|
+
},
|
|
207
|
+
body: method === "POST" && body ? body : undefined,
|
|
208
|
+
});
|
|
209
|
+
const responseText = await paidRes.text();
|
|
210
|
+
return {
|
|
211
|
+
url,
|
|
212
|
+
statusCode: paidRes.status,
|
|
213
|
+
response: responseText,
|
|
214
|
+
paymentSettled: paidRes.status >= 200 && paidRes.status < 300,
|
|
215
|
+
paymentDetails: {
|
|
216
|
+
scheme: requirement.scheme,
|
|
217
|
+
network: requirement.network,
|
|
218
|
+
asset: requirement.asset,
|
|
219
|
+
amount: requirement.amount,
|
|
220
|
+
payTo: requirement.payTo,
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
export * from "./sdk.js";
|
|
2
|
-
export * from "./
|
|
3
|
-
export * from "./commands/
|
|
4
|
-
export * from "./commands/identity/
|
|
5
|
-
export * from "./commands/
|
|
6
|
-
export * from "./commands/reputation/
|
|
7
|
-
export * from "./commands/
|
|
8
|
-
export * from "./commands/
|
|
2
|
+
export * from "./monad.js";
|
|
3
|
+
export * from "./commands/monad/pay.js";
|
|
4
|
+
export * from "./commands/monad/identity/register.js";
|
|
5
|
+
export * from "./commands/monad/identity/get.js";
|
|
6
|
+
export * from "./commands/monad/reputation/check.js";
|
|
7
|
+
export * from "./commands/monad/reputation/give.js";
|
|
8
|
+
export * from "./commands/monad/x402/pay.js";
|
|
9
|
+
export * from "./commands/monad/jobs/create.js";
|
|
10
|
+
export * from "./commands/monad/jobs/complete.js";
|
|
11
|
+
export * from "./commands/monad/jobs/refund.js";
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
export * from "./sdk.js";
|
|
2
|
-
export * from "./
|
|
3
|
-
export * from "./commands/
|
|
4
|
-
export * from "./commands/identity/
|
|
5
|
-
export * from "./commands/
|
|
6
|
-
export * from "./commands/reputation/
|
|
7
|
-
export * from "./commands/
|
|
8
|
-
export * from "./commands/
|
|
2
|
+
export * from "./monad.js";
|
|
3
|
+
export * from "./commands/monad/pay.js";
|
|
4
|
+
export * from "./commands/monad/identity/register.js";
|
|
5
|
+
export * from "./commands/monad/identity/get.js";
|
|
6
|
+
export * from "./commands/monad/reputation/check.js";
|
|
7
|
+
export * from "./commands/monad/reputation/give.js";
|
|
8
|
+
export * from "./commands/monad/x402/pay.js";
|
|
9
|
+
export * from "./commands/monad/jobs/create.js";
|
|
10
|
+
export * from "./commands/monad/jobs/complete.js";
|
|
11
|
+
export * from "./commands/monad/jobs/refund.js";
|