@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,76 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('node:fs');
|
|
4
|
+
vi.mock('node:os');
|
|
5
|
+
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as os from 'node:os';
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.restoreAllMocks();
|
|
11
|
+
vi.mocked(os.homedir).mockReturnValue('/home/test');
|
|
12
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
13
|
+
vi.mocked(fs.mkdirSync).mockReturnValue(undefined);
|
|
14
|
+
vi.mocked(fs.writeFileSync).mockReturnValue(undefined);
|
|
15
|
+
vi.mocked(fs.chmodSync).mockReturnValue(undefined);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('mandate login', () => {
|
|
19
|
+
it('registers agent and returns credentials', async () => {
|
|
20
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
21
|
+
ok: true,
|
|
22
|
+
status: 201,
|
|
23
|
+
json: () => Promise.resolve({
|
|
24
|
+
agentId: 'uuid-1',
|
|
25
|
+
runtimeKey: 'mndt_test_abc123xyz',
|
|
26
|
+
claimUrl: 'https://app.mandate.md/claim/uuid-1',
|
|
27
|
+
evmAddress: '0x1234567890abcdef1234567890abcdef12345678',
|
|
28
|
+
chainId: 84532,
|
|
29
|
+
}),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const { default: cli } = await import('../index.js');
|
|
33
|
+
|
|
34
|
+
let output = '';
|
|
35
|
+
await cli.serve(['login', '--name', 'TestAgent', '--address', '0x1234567890abcdef1234567890abcdef12345678'], {
|
|
36
|
+
stdout(s: string) { output += s; },
|
|
37
|
+
exit() {},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(output).toContain('uuid-1');
|
|
41
|
+
expect(output).toContain('mndt_test_abc');
|
|
42
|
+
expect(output).not.toContain('mndt_test_abc123xyz'); // masked
|
|
43
|
+
expect(fs.writeFileSync).toHaveBeenCalled();
|
|
44
|
+
expect(fs.chmodSync).toHaveBeenCalledWith(expect.stringContaining('credentials.json'), 0o600);
|
|
45
|
+
|
|
46
|
+
vi.unstubAllGlobals();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('passes defaultPolicy when limits provided', async () => {
|
|
50
|
+
const fetchSpy = vi.fn().mockResolvedValue({
|
|
51
|
+
ok: true,
|
|
52
|
+
status: 201,
|
|
53
|
+
json: () => Promise.resolve({
|
|
54
|
+
agentId: 'uuid-2',
|
|
55
|
+
runtimeKey: 'mndt_test_xyz789abc',
|
|
56
|
+
claimUrl: 'https://app.mandate.md/claim/uuid-2',
|
|
57
|
+
evmAddress: '0x0000000000000000000000000000000000000000',
|
|
58
|
+
chainId: 84532,
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
vi.stubGlobal('fetch', fetchSpy);
|
|
62
|
+
|
|
63
|
+
const { default: cli } = await import('../index.js');
|
|
64
|
+
|
|
65
|
+
await cli.serve(['login', '--name', 'LimitAgent', '--per-tx-limit', '100', '--daily-limit', '500'], {
|
|
66
|
+
stdout() {},
|
|
67
|
+
exit() {},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const body = JSON.parse(fetchSpy.mock.calls[0][1].body);
|
|
71
|
+
expect(body.defaultPolicy.spendLimitPerTxUsd).toBe(100);
|
|
72
|
+
expect(body.defaultPolicy.spendLimitPerDayUsd).toBe(500);
|
|
73
|
+
|
|
74
|
+
vi.unstubAllGlobals();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('node:fs');
|
|
4
|
+
vi.mock('node:os');
|
|
5
|
+
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as os from 'node:os';
|
|
8
|
+
|
|
9
|
+
const CREDS = {
|
|
10
|
+
runtimeKey: 'mndt_test_abc123',
|
|
11
|
+
agentId: 'uuid-1',
|
|
12
|
+
claimUrl: 'http://x',
|
|
13
|
+
chainId: 84532,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.restoreAllMocks();
|
|
18
|
+
vi.mocked(os.homedir).mockReturnValue('/home/test');
|
|
19
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
20
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(CREDS));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('mandate status', () => {
|
|
24
|
+
it('returns intent status with CTA for reserved', async () => {
|
|
25
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
26
|
+
ok: true,
|
|
27
|
+
status: 200,
|
|
28
|
+
json: () => Promise.resolve({
|
|
29
|
+
intentId: 'intent-1',
|
|
30
|
+
status: 'reserved',
|
|
31
|
+
txHash: null,
|
|
32
|
+
blockNumber: null,
|
|
33
|
+
gasUsed: null,
|
|
34
|
+
amountUsd: null,
|
|
35
|
+
decodedAction: null,
|
|
36
|
+
summary: null,
|
|
37
|
+
blockReason: null,
|
|
38
|
+
requiresApproval: false,
|
|
39
|
+
approvalId: null,
|
|
40
|
+
expiresAt: null,
|
|
41
|
+
}),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
const { default: cli } = await import('../index.js');
|
|
45
|
+
|
|
46
|
+
let output = '';
|
|
47
|
+
await cli.serve(['status', 'intent-1'], {
|
|
48
|
+
stdout(s: string) { output += s; },
|
|
49
|
+
exit() {},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(output).toContain('reserved');
|
|
53
|
+
expect(output).toContain('mandate event');
|
|
54
|
+
|
|
55
|
+
vi.unstubAllGlobals();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns confirmed status with no CTA', async () => {
|
|
59
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
60
|
+
ok: true,
|
|
61
|
+
status: 200,
|
|
62
|
+
json: () => Promise.resolve({
|
|
63
|
+
intentId: 'intent-1',
|
|
64
|
+
status: 'confirmed',
|
|
65
|
+
txHash: '0xabc',
|
|
66
|
+
blockNumber: '100',
|
|
67
|
+
gasUsed: '50000',
|
|
68
|
+
amountUsd: '10.00',
|
|
69
|
+
decodedAction: 'transfer',
|
|
70
|
+
summary: null,
|
|
71
|
+
blockReason: null,
|
|
72
|
+
requiresApproval: false,
|
|
73
|
+
approvalId: null,
|
|
74
|
+
expiresAt: null,
|
|
75
|
+
}),
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
const { default: cli } = await import('../index.js');
|
|
79
|
+
|
|
80
|
+
let output = '';
|
|
81
|
+
await cli.serve(['status', 'intent-1'], {
|
|
82
|
+
stdout(s: string) { output += s; },
|
|
83
|
+
exit() {},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(output).toContain('confirmed');
|
|
87
|
+
expect(output).toContain('0xabc');
|
|
88
|
+
// No next step for terminal state
|
|
89
|
+
expect(output).not.toContain('mandate event');
|
|
90
|
+
expect(output).not.toContain('mandate approve');
|
|
91
|
+
|
|
92
|
+
vi.unstubAllGlobals();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('shows approval CTA for approval_pending', async () => {
|
|
96
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
97
|
+
ok: true,
|
|
98
|
+
status: 200,
|
|
99
|
+
json: () => Promise.resolve({
|
|
100
|
+
intentId: 'intent-2',
|
|
101
|
+
status: 'approval_pending',
|
|
102
|
+
txHash: null,
|
|
103
|
+
blockNumber: null,
|
|
104
|
+
gasUsed: null,
|
|
105
|
+
amountUsd: null,
|
|
106
|
+
decodedAction: null,
|
|
107
|
+
summary: null,
|
|
108
|
+
blockReason: null,
|
|
109
|
+
requiresApproval: true,
|
|
110
|
+
approvalId: 'appr-1',
|
|
111
|
+
expiresAt: null,
|
|
112
|
+
}),
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
const { default: cli } = await import('../index.js');
|
|
116
|
+
|
|
117
|
+
let output = '';
|
|
118
|
+
await cli.serve(['status', 'intent-2'], {
|
|
119
|
+
stdout(s: string) { output += s; },
|
|
120
|
+
exit() {},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(output).toContain('approval_pending');
|
|
124
|
+
expect(output).toContain('mandate approve');
|
|
125
|
+
|
|
126
|
+
vi.unstubAllGlobals();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { encodeFunctionData, parseAbi } from 'viem';
|
|
3
|
+
|
|
4
|
+
vi.mock('node:fs');
|
|
5
|
+
vi.mock('node:os');
|
|
6
|
+
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as os from 'node:os';
|
|
9
|
+
|
|
10
|
+
const CREDS = {
|
|
11
|
+
runtimeKey: 'mndt_test_abc123',
|
|
12
|
+
agentId: 'uuid-1',
|
|
13
|
+
claimUrl: 'http://x',
|
|
14
|
+
chainId: 84532,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.restoreAllMocks();
|
|
19
|
+
vi.mocked(os.homedir).mockReturnValue('/home/test');
|
|
20
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
21
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(CREDS));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('mandate transfer', () => {
|
|
25
|
+
it('encodes ERC20 calldata and validates', async () => {
|
|
26
|
+
const fetchSpy = vi.fn().mockResolvedValue({
|
|
27
|
+
ok: true,
|
|
28
|
+
status: 200,
|
|
29
|
+
json: () => Promise.resolve({
|
|
30
|
+
allowed: true,
|
|
31
|
+
intentId: 'intent-t1',
|
|
32
|
+
requiresApproval: false,
|
|
33
|
+
approvalId: null,
|
|
34
|
+
blockReason: null,
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
vi.stubGlobal('fetch', fetchSpy);
|
|
38
|
+
|
|
39
|
+
const { default: cli } = await import('../index.js');
|
|
40
|
+
|
|
41
|
+
let output = '';
|
|
42
|
+
await cli.serve([
|
|
43
|
+
'transfer',
|
|
44
|
+
'--to', '0x1234567890abcdef1234567890abcdef12345678',
|
|
45
|
+
'--amount', '10000000',
|
|
46
|
+
'--token', '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
|
|
47
|
+
'--reason', 'Invoice #127',
|
|
48
|
+
'--nonce', '42',
|
|
49
|
+
'--max-fee-per-gas', '1000000000',
|
|
50
|
+
'--max-priority-fee-per-gas', '1000000000',
|
|
51
|
+
], {
|
|
52
|
+
stdout(s: string) { output += s; },
|
|
53
|
+
exit() {},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Verify the request body has encoded calldata
|
|
57
|
+
const body = JSON.parse(fetchSpy.mock.calls[0][1].body);
|
|
58
|
+
expect(body.calldata).toContain('0xa9059cbb'); // transfer selector
|
|
59
|
+
expect(body.to).toBe('0x036CbD53842c5426634e7929541eC2318f3dCF7e'); // token contract, not recipient
|
|
60
|
+
|
|
61
|
+
expect(output).toContain('intent-t1');
|
|
62
|
+
expect(output).toContain('unsignedTx');
|
|
63
|
+
expect(output).toContain('calldata');
|
|
64
|
+
|
|
65
|
+
vi.unstubAllGlobals();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('returns blocked output on policy violation', async () => {
|
|
69
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
70
|
+
ok: false,
|
|
71
|
+
status: 422,
|
|
72
|
+
json: () => Promise.resolve({
|
|
73
|
+
blockReason: 'daily_limit_exceeded',
|
|
74
|
+
}),
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
const { default: cli } = await import('../index.js');
|
|
78
|
+
|
|
79
|
+
let output = '';
|
|
80
|
+
await cli.serve([
|
|
81
|
+
'transfer',
|
|
82
|
+
'--to', '0x1234567890abcdef1234567890abcdef12345678',
|
|
83
|
+
'--amount', '99999999999',
|
|
84
|
+
'--token', '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
|
|
85
|
+
'--reason', 'Huge transfer',
|
|
86
|
+
'--nonce', '1',
|
|
87
|
+
'--max-fee-per-gas', '1000000000',
|
|
88
|
+
'--max-priority-fee-per-gas', '1000000000',
|
|
89
|
+
], {
|
|
90
|
+
stdout(s: string) { output += s; },
|
|
91
|
+
exit() {},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(output).toContain('POLICY_BLOCKED');
|
|
95
|
+
expect(output).toContain('daily_limit_exceeded');
|
|
96
|
+
|
|
97
|
+
vi.unstubAllGlobals();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { computeIntentHash } from '@mandate.md/sdk';
|
|
3
|
+
|
|
4
|
+
vi.mock('node:fs');
|
|
5
|
+
vi.mock('node:os');
|
|
6
|
+
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as os from 'node:os';
|
|
9
|
+
|
|
10
|
+
const CREDS = {
|
|
11
|
+
runtimeKey: 'mndt_test_abc123',
|
|
12
|
+
agentId: 'uuid-1',
|
|
13
|
+
claimUrl: 'http://x',
|
|
14
|
+
chainId: 84532,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.restoreAllMocks();
|
|
19
|
+
vi.mocked(os.homedir).mockReturnValue('/home/test');
|
|
20
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
21
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(CREDS));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('mandate validate', () => {
|
|
25
|
+
it('returns ok when policy check passes', async () => {
|
|
26
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
27
|
+
ok: true,
|
|
28
|
+
status: 200,
|
|
29
|
+
json: () => Promise.resolve({
|
|
30
|
+
allowed: true,
|
|
31
|
+
intentId: 'intent-1',
|
|
32
|
+
requiresApproval: false,
|
|
33
|
+
approvalId: null,
|
|
34
|
+
blockReason: null,
|
|
35
|
+
}),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
const { default: cli } = await import('../index.js');
|
|
39
|
+
|
|
40
|
+
let output = '';
|
|
41
|
+
await cli.serve([
|
|
42
|
+
'validate',
|
|
43
|
+
'--to', '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
|
|
44
|
+
'--calldata', '0xa9059cbb',
|
|
45
|
+
'--nonce', '42',
|
|
46
|
+
'--gas-limit', '90000',
|
|
47
|
+
'--max-fee-per-gas', '1000000000',
|
|
48
|
+
'--max-priority-fee-per-gas', '1000000000',
|
|
49
|
+
'--reason', 'Invoice #127',
|
|
50
|
+
], {
|
|
51
|
+
stdout(s: string) { output += s; },
|
|
52
|
+
exit() {},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(output).toContain('intent-1');
|
|
56
|
+
expect(output).toContain('ok');
|
|
57
|
+
|
|
58
|
+
vi.unstubAllGlobals();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('computes intentHash and sends it in request', async () => {
|
|
62
|
+
const fetchSpy = vi.fn().mockResolvedValue({
|
|
63
|
+
ok: true,
|
|
64
|
+
status: 200,
|
|
65
|
+
json: () => Promise.resolve({
|
|
66
|
+
allowed: true,
|
|
67
|
+
intentId: 'intent-1',
|
|
68
|
+
requiresApproval: false,
|
|
69
|
+
approvalId: null,
|
|
70
|
+
blockReason: null,
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
vi.stubGlobal('fetch', fetchSpy);
|
|
74
|
+
|
|
75
|
+
const { default: cli } = await import('../index.js');
|
|
76
|
+
|
|
77
|
+
await cli.serve([
|
|
78
|
+
'validate',
|
|
79
|
+
'--to', '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
|
|
80
|
+
'--nonce', '42',
|
|
81
|
+
'--gas-limit', '90000',
|
|
82
|
+
'--max-fee-per-gas', '1000000000',
|
|
83
|
+
'--max-priority-fee-per-gas', '1000000000',
|
|
84
|
+
'--reason', 'Test',
|
|
85
|
+
], {
|
|
86
|
+
stdout() {},
|
|
87
|
+
exit() {},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const body = JSON.parse(fetchSpy.mock.calls[0][1].body);
|
|
91
|
+
// Verify intentHash is present and is a hex string
|
|
92
|
+
expect(body.intentHash).toMatch(/^0x[a-f0-9]{64}$/);
|
|
93
|
+
|
|
94
|
+
// Verify it matches what computeIntentHash would produce
|
|
95
|
+
const expected = computeIntentHash({
|
|
96
|
+
chainId: 84532,
|
|
97
|
+
nonce: 42,
|
|
98
|
+
to: '0x036CbD53842c5426634e7929541eC2318f3dCF7e' as `0x${string}`,
|
|
99
|
+
calldata: '0x',
|
|
100
|
+
valueWei: '0',
|
|
101
|
+
gasLimit: '90000',
|
|
102
|
+
maxFeePerGas: '1000000000',
|
|
103
|
+
maxPriorityFeePerGas: '1000000000',
|
|
104
|
+
});
|
|
105
|
+
expect(body.intentHash).toBe(expected);
|
|
106
|
+
|
|
107
|
+
vi.unstubAllGlobals();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('returns blocked output on PolicyBlockedError', async () => {
|
|
111
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
112
|
+
ok: false,
|
|
113
|
+
status: 422,
|
|
114
|
+
json: () => Promise.resolve({
|
|
115
|
+
blockReason: 'per_tx_limit_exceeded',
|
|
116
|
+
blockDetail: '$150 exceeds $100/tx limit',
|
|
117
|
+
}),
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
const { default: cli } = await import('../index.js');
|
|
121
|
+
|
|
122
|
+
let output = '';
|
|
123
|
+
await cli.serve([
|
|
124
|
+
'validate',
|
|
125
|
+
'--to', '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
|
|
126
|
+
'--nonce', '1',
|
|
127
|
+
'--gas-limit', '90000',
|
|
128
|
+
'--max-fee-per-gas', '1000000000',
|
|
129
|
+
'--max-priority-fee-per-gas', '1000000000',
|
|
130
|
+
'--reason', 'Big payment',
|
|
131
|
+
], {
|
|
132
|
+
stdout(s: string) { output += s; },
|
|
133
|
+
exit() {},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(output).toContain('POLICY_BLOCKED');
|
|
137
|
+
expect(output).toContain('per_tx_limit_exceeded');
|
|
138
|
+
|
|
139
|
+
vi.unstubAllGlobals();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('returns approval required output', async () => {
|
|
143
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
144
|
+
ok: true,
|
|
145
|
+
status: 200,
|
|
146
|
+
json: () => Promise.resolve({
|
|
147
|
+
allowed: true,
|
|
148
|
+
intentId: 'intent-2',
|
|
149
|
+
requiresApproval: true,
|
|
150
|
+
approvalId: 'approval-1',
|
|
151
|
+
blockReason: null,
|
|
152
|
+
}),
|
|
153
|
+
}));
|
|
154
|
+
|
|
155
|
+
const { default: cli } = await import('../index.js');
|
|
156
|
+
|
|
157
|
+
let output = '';
|
|
158
|
+
await cli.serve([
|
|
159
|
+
'validate',
|
|
160
|
+
'--to', '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
|
|
161
|
+
'--nonce', '1',
|
|
162
|
+
'--gas-limit', '90000',
|
|
163
|
+
'--max-fee-per-gas', '1000000000',
|
|
164
|
+
'--max-priority-fee-per-gas', '1000000000',
|
|
165
|
+
'--reason', 'Big payment needing approval',
|
|
166
|
+
], {
|
|
167
|
+
stdout(s: string) { output += s; },
|
|
168
|
+
exit() {},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(output).toContain('requiresApproval');
|
|
172
|
+
expect(output).toContain('intent-2');
|
|
173
|
+
expect(output).toContain('approve');
|
|
174
|
+
|
|
175
|
+
vi.unstubAllGlobals();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('requires auth middleware (no creds = error)', async () => {
|
|
179
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
180
|
+
|
|
181
|
+
const { default: cli } = await import('../index.js');
|
|
182
|
+
|
|
183
|
+
let output = '';
|
|
184
|
+
let exitCode: number | undefined;
|
|
185
|
+
await cli.serve([
|
|
186
|
+
'validate',
|
|
187
|
+
'--to', '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
|
|
188
|
+
'--nonce', '1',
|
|
189
|
+
'--gas-limit', '90000',
|
|
190
|
+
'--max-fee-per-gas', '1000000000',
|
|
191
|
+
'--max-priority-fee-per-gas', '1000000000',
|
|
192
|
+
'--reason', 'Test',
|
|
193
|
+
], {
|
|
194
|
+
stdout(s: string) { output += s; },
|
|
195
|
+
exit(code: number) { exitCode = code; },
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(output).toContain('NOT_AUTHENTICATED');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from 'incur';
|
|
2
|
+
import { updateCredentials } from '../credentials.js';
|
|
3
|
+
import type { CommandDef } from './types.js';
|
|
4
|
+
|
|
5
|
+
export const activateCommand: CommandDef = {
|
|
6
|
+
description: 'Set EVM address after registration',
|
|
7
|
+
args: z.object({
|
|
8
|
+
address: z.string().describe('EVM address (0x...)'),
|
|
9
|
+
}),
|
|
10
|
+
examples: [
|
|
11
|
+
{ args: { address: '0x1234567890abcdef1234567890abcdef12345678' }, description: 'Set wallet address' },
|
|
12
|
+
],
|
|
13
|
+
async run(c: any) {
|
|
14
|
+
const { address } = c.args;
|
|
15
|
+
const client = c.var.client;
|
|
16
|
+
|
|
17
|
+
const res = await fetch(`${c.var.credentials.baseUrl ?? 'https://app.mandate.md'}/api/activate`, {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: {
|
|
20
|
+
'Content-Type': 'application/json',
|
|
21
|
+
'Authorization': `Bearer ${c.var.credentials.runtimeKey}`,
|
|
22
|
+
},
|
|
23
|
+
body: JSON.stringify({ evmAddress: address }),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
const data = await res.json().catch(() => ({}));
|
|
28
|
+
return c.error({ code: 'ACTIVATE_FAILED', message: data.message ?? 'Activation failed' });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const data = await res.json();
|
|
32
|
+
updateCredentials({ evmAddress: data.evmAddress });
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
activated: true,
|
|
36
|
+
evmAddress: data.evmAddress,
|
|
37
|
+
onboardingUrl: data.onboardingUrl,
|
|
38
|
+
next: 'Run: mandate validate (start validating transactions)',
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from 'incur';
|
|
2
|
+
import type { CommandDef } from './types.js';
|
|
3
|
+
|
|
4
|
+
export const approveCommand: CommandDef = {
|
|
5
|
+
description: 'Wait for owner approval on a pending intent',
|
|
6
|
+
args: z.object({
|
|
7
|
+
intentId: z.string().describe('Intent ID awaiting approval'),
|
|
8
|
+
}),
|
|
9
|
+
options: z.object({
|
|
10
|
+
timeout: z.number().optional().describe('Timeout in seconds (default: 3600)'),
|
|
11
|
+
}),
|
|
12
|
+
examples: [
|
|
13
|
+
{ args: { intentId: 'uuid-1' }, description: 'Wait for approval' },
|
|
14
|
+
],
|
|
15
|
+
async run(c: any) {
|
|
16
|
+
const client = c.var.client;
|
|
17
|
+
const timeoutMs = (c.options.timeout ?? 3600) * 1000;
|
|
18
|
+
|
|
19
|
+
const status = await client.waitForApproval(c.args.intentId, {
|
|
20
|
+
timeoutMs,
|
|
21
|
+
onPoll: () => {},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (status.status === 'approved' || status.status === 'confirmed') {
|
|
25
|
+
return {
|
|
26
|
+
status: 'approved',
|
|
27
|
+
intentId: c.args.intentId,
|
|
28
|
+
feedback: '\u2705 Approved \u2014 ready to broadcast',
|
|
29
|
+
next: `Run: mandate event ${c.args.intentId} --tx-hash 0x...`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
status: status.status,
|
|
35
|
+
intentId: c.args.intentId,
|
|
36
|
+
feedback: `Intent ended with status: ${status.status}`,
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from 'incur';
|
|
2
|
+
import type { CommandDef } from './types.js';
|
|
3
|
+
|
|
4
|
+
export const eventCommand: CommandDef = {
|
|
5
|
+
description: 'Post txHash after signing and broadcasting',
|
|
6
|
+
args: z.object({
|
|
7
|
+
intentId: z.string().describe('Intent ID from validate'),
|
|
8
|
+
}),
|
|
9
|
+
options: z.object({
|
|
10
|
+
txHash: z.string().describe('Transaction hash (0x...)'),
|
|
11
|
+
}),
|
|
12
|
+
examples: [
|
|
13
|
+
{ args: { intentId: 'uuid-1' }, options: { txHash: '0xabc123' }, description: 'Post transaction hash' },
|
|
14
|
+
],
|
|
15
|
+
async run(c: any) {
|
|
16
|
+
const client = c.var.client;
|
|
17
|
+
await client.postEvent(c.args.intentId, c.options.txHash as `0x${string}`);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
posted: true,
|
|
21
|
+
intentId: c.args.intentId,
|
|
22
|
+
next: `Run: mandate status ${c.args.intentId}`,
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from 'incur';
|
|
2
|
+
import { MandateClient } from '@mandate.md/sdk';
|
|
3
|
+
import { saveCredentials } from '../credentials.js';
|
|
4
|
+
import type { CommandDef } from './types.js';
|
|
5
|
+
|
|
6
|
+
export const loginCommand: CommandDef = {
|
|
7
|
+
description: 'Register a new agent and store credentials',
|
|
8
|
+
options: z.object({
|
|
9
|
+
name: z.string().describe('Agent name'),
|
|
10
|
+
address: z.string().optional().describe('EVM address (0x...)'),
|
|
11
|
+
perTxLimit: z.number().optional().describe('Per-transaction USD limit'),
|
|
12
|
+
dailyLimit: z.number().optional().describe('Daily USD limit'),
|
|
13
|
+
baseUrl: z.string().optional().describe('Mandate API base URL'),
|
|
14
|
+
chainId: z.number().optional().describe('Chain ID (default: 84532)'),
|
|
15
|
+
}),
|
|
16
|
+
alias: { perTxLimit: 'p', dailyLimit: 'd' },
|
|
17
|
+
examples: [
|
|
18
|
+
{ options: { name: 'MyAgent', address: '0x1234567890abcdef1234567890abcdef12345678' }, description: 'Register with address' },
|
|
19
|
+
{ options: { name: 'MyAgent' }, description: 'Register without address (set later via activate)' },
|
|
20
|
+
],
|
|
21
|
+
async run(c: any) {
|
|
22
|
+
const { name, address, perTxLimit, dailyLimit, baseUrl, chainId } = c.options;
|
|
23
|
+
|
|
24
|
+
const defaultPolicy: Record<string, number> = {};
|
|
25
|
+
if (perTxLimit !== undefined) defaultPolicy.spendLimitPerTxUsd = perTxLimit;
|
|
26
|
+
if (dailyLimit !== undefined) defaultPolicy.spendLimitPerDayUsd = dailyLimit;
|
|
27
|
+
|
|
28
|
+
const result = await MandateClient.register({
|
|
29
|
+
name,
|
|
30
|
+
evmAddress: (address ?? '0x0000000000000000000000000000000000000000') as `0x${string}`,
|
|
31
|
+
chainId: chainId ?? 84532,
|
|
32
|
+
defaultPolicy: Object.keys(defaultPolicy).length ? defaultPolicy : undefined,
|
|
33
|
+
baseUrl,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
saveCredentials({
|
|
37
|
+
runtimeKey: result.runtimeKey,
|
|
38
|
+
agentId: result.agentId,
|
|
39
|
+
claimUrl: result.claimUrl,
|
|
40
|
+
evmAddress: result.evmAddress,
|
|
41
|
+
chainId: result.chainId,
|
|
42
|
+
baseUrl,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const masked = result.runtimeKey.slice(0, 14) + '...' + result.runtimeKey.slice(-3);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
agentId: result.agentId,
|
|
49
|
+
runtimeKey: masked,
|
|
50
|
+
claimUrl: result.claimUrl,
|
|
51
|
+
evmAddress: result.evmAddress || undefined,
|
|
52
|
+
next: 'Run: mandate whoami (verify) or mandate validate (first tx)',
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'incur';
|
|
2
|
+
import type { CommandDef } from './types.js';
|
|
3
|
+
|
|
4
|
+
const CTA: Record<string, string> = {
|
|
5
|
+
reserved: 'Run: mandate event <intentId> --tx-hash 0x...',
|
|
6
|
+
approval_pending: 'Run: mandate approve <intentId>',
|
|
7
|
+
broadcasted: 'Run: mandate status <intentId> (poll again)',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const statusCommand: CommandDef = {
|
|
11
|
+
description: 'Check intent state',
|
|
12
|
+
args: z.object({
|
|
13
|
+
intentId: z.string().describe('Intent ID'),
|
|
14
|
+
}),
|
|
15
|
+
examples: [
|
|
16
|
+
{ args: { intentId: 'uuid-1' }, description: 'Check status of an intent' },
|
|
17
|
+
],
|
|
18
|
+
async run(c: any) {
|
|
19
|
+
const client = c.var.client;
|
|
20
|
+
const status = await client.getStatus(c.args.intentId);
|
|
21
|
+
|
|
22
|
+
const result: Record<string, unknown> = { ...status };
|
|
23
|
+
const cta = CTA[status.status];
|
|
24
|
+
if (cta) result.next = cta;
|
|
25
|
+
|
|
26
|
+
return result;
|
|
27
|
+
},
|
|
28
|
+
};
|