@mandate.md/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +493 -0
- package/package.json +21 -0
- package/src/__tests__/credentials.test.ts +83 -0
- package/src/__tests__/event.test.ts +73 -0
- package/src/__tests__/login.test.ts +76 -0
- package/src/__tests__/status.test.ts +128 -0
- package/src/__tests__/transfer.test.ts +99 -0
- package/src/__tests__/validate.test.ts +200 -0
- package/src/commands/activate.ts +41 -0
- package/src/commands/approve.ts +39 -0
- package/src/commands/event.ts +25 -0
- package/src/commands/login.ts +55 -0
- package/src/commands/status.ts +28 -0
- package/src/commands/transfer.ts +111 -0
- package/src/commands/types.ts +10 -0
- package/src/commands/validate.ts +101 -0
- package/src/commands/whoami.ts +17 -0
- package/src/credentials.ts +48 -0
- package/src/index.ts +61 -0
- package/src/middleware.ts +14 -0
- package/src/vars.ts +10 -0
- package/tsconfig.json +8 -0
- package/tsup.config.ts +9 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { z } from 'incur';
|
|
2
|
+
import { encodeFunctionData, parseAbi } from 'viem';
|
|
3
|
+
import { computeIntentHash } from '@mandate.md/sdk';
|
|
4
|
+
import { PolicyBlockedError, RiskBlockedError, ApprovalRequiredError } from '@mandate.md/sdk';
|
|
5
|
+
import type { CommandDef } from './types.js';
|
|
6
|
+
|
|
7
|
+
const erc20Abi = parseAbi(['function transfer(address to, uint256 amount) returns (bool)']);
|
|
8
|
+
|
|
9
|
+
export const transferCommand: CommandDef = {
|
|
10
|
+
description: 'ERC20 transfer with automatic calldata encoding and validation',
|
|
11
|
+
options: z.object({
|
|
12
|
+
to: z.string().describe('Recipient address (0x...)'),
|
|
13
|
+
amount: z.string().describe('Amount in raw token units'),
|
|
14
|
+
token: z.string().describe('ERC20 token contract address (0x...)'),
|
|
15
|
+
reason: z.string().describe('Why this transfer is being sent'),
|
|
16
|
+
chainId: z.number().optional().describe('Chain ID (default from credentials)'),
|
|
17
|
+
nonce: z.number().describe('Transaction nonce'),
|
|
18
|
+
gasLimit: z.string().optional().describe('Gas limit (default: 65000)'),
|
|
19
|
+
maxFeePerGas: z.string().describe('Max fee per gas (wei)'),
|
|
20
|
+
maxPriorityFeePerGas: z.string().describe('Max priority fee per gas (wei)'),
|
|
21
|
+
}),
|
|
22
|
+
examples: [
|
|
23
|
+
{
|
|
24
|
+
options: {
|
|
25
|
+
to: '0xAlice',
|
|
26
|
+
amount: '10000000',
|
|
27
|
+
token: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
|
|
28
|
+
reason: 'Invoice #127',
|
|
29
|
+
nonce: 42,
|
|
30
|
+
maxFeePerGas: '1000000000',
|
|
31
|
+
maxPriorityFeePerGas: '1000000000',
|
|
32
|
+
},
|
|
33
|
+
description: 'Transfer 10 USDC',
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
async run(c: any) {
|
|
37
|
+
const client = c.var.client;
|
|
38
|
+
const creds = c.var.credentials;
|
|
39
|
+
const opts = c.options;
|
|
40
|
+
|
|
41
|
+
const chainId = opts.chainId ?? creds.chainId ?? 84532;
|
|
42
|
+
const gasLimit = opts.gasLimit ?? '65000';
|
|
43
|
+
|
|
44
|
+
const calldata = encodeFunctionData({
|
|
45
|
+
abi: erc20Abi,
|
|
46
|
+
functionName: 'transfer',
|
|
47
|
+
args: [opts.to as `0x${string}`, BigInt(opts.amount)],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const intentHash = computeIntentHash({
|
|
51
|
+
chainId,
|
|
52
|
+
nonce: opts.nonce,
|
|
53
|
+
to: opts.token as `0x${string}`,
|
|
54
|
+
calldata,
|
|
55
|
+
valueWei: '0',
|
|
56
|
+
gasLimit,
|
|
57
|
+
maxFeePerGas: opts.maxFeePerGas,
|
|
58
|
+
maxPriorityFeePerGas: opts.maxPriorityFeePerGas,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const result = await client.validate({
|
|
63
|
+
chainId,
|
|
64
|
+
nonce: opts.nonce,
|
|
65
|
+
to: opts.token as `0x${string}`,
|
|
66
|
+
calldata,
|
|
67
|
+
valueWei: '0',
|
|
68
|
+
gasLimit,
|
|
69
|
+
maxFeePerGas: opts.maxFeePerGas,
|
|
70
|
+
maxPriorityFeePerGas: opts.maxPriorityFeePerGas,
|
|
71
|
+
intentHash,
|
|
72
|
+
reason: opts.reason,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
ok: true,
|
|
77
|
+
intentId: result.intentId,
|
|
78
|
+
feedback: '\u2705 Mandate: policy check passed',
|
|
79
|
+
unsignedTx: {
|
|
80
|
+
to: opts.token,
|
|
81
|
+
calldata,
|
|
82
|
+
value: '0',
|
|
83
|
+
gasLimit,
|
|
84
|
+
maxFeePerGas: opts.maxFeePerGas,
|
|
85
|
+
maxPriorityFeePerGas: opts.maxPriorityFeePerGas,
|
|
86
|
+
nonce: opts.nonce,
|
|
87
|
+
chainId,
|
|
88
|
+
},
|
|
89
|
+
next: `Run: mandate event ${result.intentId} --tx-hash 0x...`,
|
|
90
|
+
};
|
|
91
|
+
} catch (err) {
|
|
92
|
+
if (err instanceof PolicyBlockedError || err instanceof RiskBlockedError) {
|
|
93
|
+
return {
|
|
94
|
+
error: 'POLICY_BLOCKED',
|
|
95
|
+
message: `\uD83D\uDEAB Mandate: blocked \u2014 ${err.message}`,
|
|
96
|
+
blockReason: err.blockReason,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (err instanceof ApprovalRequiredError) {
|
|
100
|
+
return {
|
|
101
|
+
ok: true,
|
|
102
|
+
requiresApproval: true,
|
|
103
|
+
intentId: err.intentId,
|
|
104
|
+
feedback: '\u23F3 Mandate: approval required \u2014 waiting for owner decision',
|
|
105
|
+
next: `Run: mandate approve ${err.intentId}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { z } from 'incur';
|
|
2
|
+
import { computeIntentHash } from '@mandate.md/sdk';
|
|
3
|
+
import { PolicyBlockedError, RiskBlockedError, ApprovalRequiredError } from '@mandate.md/sdk';
|
|
4
|
+
import type { CommandDef } from './types.js';
|
|
5
|
+
|
|
6
|
+
export const validateCommand: CommandDef = {
|
|
7
|
+
description: 'Policy-check a transaction (computes intentHash automatically)',
|
|
8
|
+
options: z.object({
|
|
9
|
+
to: z.string().describe('Recipient address (0x...)'),
|
|
10
|
+
calldata: z.string().optional().describe('Transaction calldata (default: 0x)'),
|
|
11
|
+
valueWei: z.string().optional().describe('Value in wei (default: 0)'),
|
|
12
|
+
nonce: z.number().describe('Transaction nonce'),
|
|
13
|
+
gasLimit: z.string().describe('Gas limit'),
|
|
14
|
+
maxFeePerGas: z.string().describe('Max fee per gas (wei)'),
|
|
15
|
+
maxPriorityFeePerGas: z.string().describe('Max priority fee per gas (wei)'),
|
|
16
|
+
chainId: z.number().optional().describe('Chain ID (default from credentials)'),
|
|
17
|
+
txType: z.number().optional().describe('Transaction type (default: 2)'),
|
|
18
|
+
accessList: z.string().optional().describe('Access list JSON string'),
|
|
19
|
+
reason: z.string().describe('Why this transaction is being sent'),
|
|
20
|
+
}),
|
|
21
|
+
examples: [
|
|
22
|
+
{
|
|
23
|
+
options: {
|
|
24
|
+
to: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
|
|
25
|
+
calldata: '0xa9059cbb000000000000000000000000',
|
|
26
|
+
nonce: 42,
|
|
27
|
+
gasLimit: '90000',
|
|
28
|
+
maxFeePerGas: '1000000000',
|
|
29
|
+
maxPriorityFeePerGas: '1000000000',
|
|
30
|
+
reason: 'Invoice #127 from Alice',
|
|
31
|
+
},
|
|
32
|
+
description: 'Validate an ERC20 transfer',
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
async run(c: any) {
|
|
36
|
+
const client = c.var.client;
|
|
37
|
+
const creds = c.var.credentials;
|
|
38
|
+
const opts = c.options;
|
|
39
|
+
|
|
40
|
+
const chainId = opts.chainId ?? creds.chainId ?? 84532;
|
|
41
|
+
const calldata = (opts.calldata ?? '0x') as `0x${string}`;
|
|
42
|
+
const valueWei = opts.valueWei ?? '0';
|
|
43
|
+
const txType = opts.txType ?? 2;
|
|
44
|
+
const accessList = opts.accessList ? JSON.parse(opts.accessList) : [];
|
|
45
|
+
|
|
46
|
+
const intentHash = computeIntentHash({
|
|
47
|
+
chainId,
|
|
48
|
+
nonce: opts.nonce,
|
|
49
|
+
to: opts.to as `0x${string}`,
|
|
50
|
+
calldata,
|
|
51
|
+
valueWei,
|
|
52
|
+
gasLimit: opts.gasLimit,
|
|
53
|
+
maxFeePerGas: opts.maxFeePerGas,
|
|
54
|
+
maxPriorityFeePerGas: opts.maxPriorityFeePerGas,
|
|
55
|
+
txType,
|
|
56
|
+
accessList,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const result = await client.validate({
|
|
61
|
+
chainId,
|
|
62
|
+
nonce: opts.nonce,
|
|
63
|
+
to: opts.to as `0x${string}`,
|
|
64
|
+
calldata,
|
|
65
|
+
valueWei,
|
|
66
|
+
gasLimit: opts.gasLimit,
|
|
67
|
+
maxFeePerGas: opts.maxFeePerGas,
|
|
68
|
+
maxPriorityFeePerGas: opts.maxPriorityFeePerGas,
|
|
69
|
+
txType,
|
|
70
|
+
accessList,
|
|
71
|
+
intentHash,
|
|
72
|
+
reason: opts.reason,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
ok: true,
|
|
77
|
+
intentId: result.intentId,
|
|
78
|
+
feedback: '\u2705 Mandate: policy check passed',
|
|
79
|
+
next: `Run: mandate event ${result.intentId} --tx-hash 0x...`,
|
|
80
|
+
};
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (err instanceof PolicyBlockedError || err instanceof RiskBlockedError) {
|
|
83
|
+
return {
|
|
84
|
+
error: 'POLICY_BLOCKED',
|
|
85
|
+
message: `\uD83D\uDEAB Mandate: blocked \u2014 ${err.message}`,
|
|
86
|
+
blockReason: err.blockReason,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (err instanceof ApprovalRequiredError) {
|
|
90
|
+
return {
|
|
91
|
+
ok: true,
|
|
92
|
+
requiresApproval: true,
|
|
93
|
+
intentId: err.intentId,
|
|
94
|
+
feedback: '\u23F3 Mandate: approval required \u2014 waiting for owner decision',
|
|
95
|
+
next: `Run: mandate approve ${err.intentId}`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CommandDef } from './types.js';
|
|
2
|
+
|
|
3
|
+
export const whoamiCommand: CommandDef = {
|
|
4
|
+
description: 'Verify credentials and show agent info',
|
|
5
|
+
async run(c: any) {
|
|
6
|
+
const creds = c.var.credentials;
|
|
7
|
+
const masked = creds.runtimeKey.slice(0, 14) + '...' + creds.runtimeKey.slice(-3);
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
agentId: creds.agentId,
|
|
11
|
+
evmAddress: creds.evmAddress ?? 'not set',
|
|
12
|
+
chainId: creds.chainId ?? 84532,
|
|
13
|
+
keyPrefix: masked,
|
|
14
|
+
baseUrl: creds.baseUrl ?? 'https://app.mandate.md',
|
|
15
|
+
};
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
|
|
5
|
+
export interface MandateCredentials {
|
|
6
|
+
runtimeKey: string;
|
|
7
|
+
agentId: string;
|
|
8
|
+
claimUrl: string;
|
|
9
|
+
evmAddress?: string;
|
|
10
|
+
chainId?: number;
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function credentialsPath(): string {
|
|
15
|
+
return path.join(os.homedir(), '.mandate', 'credentials.json');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function credentialsDir(): string {
|
|
19
|
+
return path.join(os.homedir(), '.mandate');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function loadCredentials(): MandateCredentials | null {
|
|
23
|
+
const p = credentialsPath();
|
|
24
|
+
if (!fs.existsSync(p)) return null;
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function saveCredentials(creds: MandateCredentials): void {
|
|
33
|
+
const dir = credentialsDir();
|
|
34
|
+
if (!fs.existsSync(dir)) {
|
|
35
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
const p = credentialsPath();
|
|
38
|
+
fs.writeFileSync(p, JSON.stringify(creds, null, 2));
|
|
39
|
+
fs.chmodSync(p, 0o600);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function updateCredentials(partial: Partial<MandateCredentials>): void {
|
|
43
|
+
const existing = loadCredentials();
|
|
44
|
+
if (!existing) {
|
|
45
|
+
throw new Error('No existing credentials. Run: mandate login');
|
|
46
|
+
}
|
|
47
|
+
saveCredentials({ ...existing, ...partial });
|
|
48
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Cli } from 'incur';
|
|
2
|
+
import { cliVars } from './vars.js';
|
|
3
|
+
import { requireAuth } from './middleware.js';
|
|
4
|
+
import { loginCommand } from './commands/login.js';
|
|
5
|
+
import { activateCommand } from './commands/activate.js';
|
|
6
|
+
import { whoamiCommand } from './commands/whoami.js';
|
|
7
|
+
import { validateCommand } from './commands/validate.js';
|
|
8
|
+
import { transferCommand } from './commands/transfer.js';
|
|
9
|
+
import { eventCommand } from './commands/event.js';
|
|
10
|
+
import { statusCommand } from './commands/status.js';
|
|
11
|
+
import { approveCommand } from './commands/approve.js';
|
|
12
|
+
|
|
13
|
+
export type { CliVars } from './vars.js';
|
|
14
|
+
|
|
15
|
+
const cli = Cli.create('mandate', {
|
|
16
|
+
version: '0.1.0',
|
|
17
|
+
description: 'Non-custodial agent wallet policy layer. Validate transactions against spend limits, allowlists, and approval workflows — without ever touching private keys.',
|
|
18
|
+
vars: cliVars,
|
|
19
|
+
sync: {
|
|
20
|
+
suggestions: [
|
|
21
|
+
'register a new agent with mandate login',
|
|
22
|
+
'validate a transaction before signing',
|
|
23
|
+
'check intent status after broadcasting',
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
.command('login', {
|
|
28
|
+
...loginCommand,
|
|
29
|
+
})
|
|
30
|
+
.command('activate', {
|
|
31
|
+
...activateCommand,
|
|
32
|
+
middleware: [requireAuth],
|
|
33
|
+
})
|
|
34
|
+
.command('whoami', {
|
|
35
|
+
...whoamiCommand,
|
|
36
|
+
middleware: [requireAuth],
|
|
37
|
+
})
|
|
38
|
+
.command('validate', {
|
|
39
|
+
...validateCommand,
|
|
40
|
+
middleware: [requireAuth],
|
|
41
|
+
})
|
|
42
|
+
.command('transfer', {
|
|
43
|
+
...transferCommand,
|
|
44
|
+
middleware: [requireAuth],
|
|
45
|
+
})
|
|
46
|
+
.command('event', {
|
|
47
|
+
...eventCommand,
|
|
48
|
+
middleware: [requireAuth],
|
|
49
|
+
})
|
|
50
|
+
.command('status', {
|
|
51
|
+
...statusCommand,
|
|
52
|
+
middleware: [requireAuth],
|
|
53
|
+
})
|
|
54
|
+
.command('approve', {
|
|
55
|
+
...approveCommand,
|
|
56
|
+
middleware: [requireAuth],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
cli.serve();
|
|
60
|
+
|
|
61
|
+
export default cli;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { middleware } from 'incur';
|
|
2
|
+
import { MandateClient } from '@mandate.md/sdk';
|
|
3
|
+
import { loadCredentials } from './credentials.js';
|
|
4
|
+
import type { CliVars } from './vars.js';
|
|
5
|
+
|
|
6
|
+
export const requireAuth = middleware<CliVars>((c, next) => {
|
|
7
|
+
const creds = loadCredentials();
|
|
8
|
+
if (!creds?.runtimeKey) {
|
|
9
|
+
return c.error({ code: 'NOT_AUTHENTICATED', message: 'No credentials. Run: mandate login' });
|
|
10
|
+
}
|
|
11
|
+
c.set('client', new MandateClient({ runtimeKey: creds.runtimeKey, baseUrl: creds.baseUrl }));
|
|
12
|
+
c.set('credentials', creds);
|
|
13
|
+
return next();
|
|
14
|
+
});
|
package/src/vars.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from 'incur';
|
|
2
|
+
import type { MandateClient } from '@mandate.md/sdk';
|
|
3
|
+
import type { MandateCredentials } from './credentials.js';
|
|
4
|
+
|
|
5
|
+
export const cliVars = z.object({
|
|
6
|
+
client: z.custom<MandateClient>(),
|
|
7
|
+
credentials: z.custom<MandateCredentials>(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export type CliVars = typeof cliVars;
|
package/tsconfig.json
ADDED