aipou-mcp-server 0.3.1 → 0.5.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/NOTICE +8 -0
- package/README.md +25 -0
- package/dist/collector.js +1 -20
- package/dist/doctor.d.ts +32 -0
- package/dist/doctor.js +112 -0
- package/dist/doctor.test.d.ts +1 -0
- package/dist/doctor.test.js +162 -0
- package/dist/identity.js +16 -2
- package/dist/index.js +13807 -78
- package/dist/init.d.ts +32 -0
- package/dist/init.js +69 -0
- package/dist/init.test.d.ts +1 -0
- package/dist/init.test.js +113 -0
- package/dist/receipt-metadata.d.ts +5 -0
- package/dist/receipt-metadata.js +16 -0
- package/dist/receipt-metadata.test.d.ts +1 -0
- package/dist/receipt-metadata.test.js +22 -0
- package/dist/secure-file.d.ts +1 -0
- package/dist/secure-file.js +21 -0
- package/package.json +7 -5
- package/server.json +11 -4
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const AIPOU_TOKEN_ADDRESS = "0x55f0Cc5e51A1284D20337d6cbb18938C8A1ABCbB";
|
|
2
|
+
export declare const AIPOU_CLAIMS_ADDRESS = "0x4ca4C98fB784D20EdC8E2A7F531dAab4c6e53058";
|
|
3
|
+
export interface InitializationResult {
|
|
4
|
+
mode: "persistent_identity_initialized" | "persistent_identity_already_initialized";
|
|
5
|
+
wallet: string;
|
|
6
|
+
dataDir: string;
|
|
7
|
+
keyFile: string;
|
|
8
|
+
platformAccessRestricted: true;
|
|
9
|
+
mcpConfig: {
|
|
10
|
+
mcpServers: {
|
|
11
|
+
aipou: {
|
|
12
|
+
command: "npx";
|
|
13
|
+
args: ["-y", "aipou-mcp-server"];
|
|
14
|
+
env: {
|
|
15
|
+
AIPOU_AGENT_KEY_FILE: string;
|
|
16
|
+
AIPOU_DATA_DIR: string;
|
|
17
|
+
AIPOU_CONTRACT_ADDRESS: string;
|
|
18
|
+
AIPOU_CLAIMS_ADDRESS: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
safety: {
|
|
24
|
+
dedicatedWalletCreated: boolean;
|
|
25
|
+
privateKeyPrinted: false;
|
|
26
|
+
existingKeyOverwritten: false;
|
|
27
|
+
fundsMoved: false;
|
|
28
|
+
claimSubmitted: false;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export declare function initializePersistentIdentity(dataDirInput?: string): Promise<InitializationResult>;
|
|
32
|
+
export declare function describeExistingIdentity(dataDirInput?: string): Promise<InitializationResult>;
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Wallet } from "ethers";
|
|
5
|
+
import { restrictToCurrentUser } from "./secure-file.js";
|
|
6
|
+
export const AIPOU_TOKEN_ADDRESS = "0x55f0Cc5e51A1284D20337d6cbb18938C8A1ABCbB";
|
|
7
|
+
export const AIPOU_CLAIMS_ADDRESS = "0x4ca4C98fB784D20EdC8E2A7F531dAab4c6e53058";
|
|
8
|
+
function identityPaths(dataDirInput) {
|
|
9
|
+
const dataDir = path.resolve(dataDirInput || path.join(os.homedir(), ".aipou"));
|
|
10
|
+
return { dataDir, keyFile: path.join(dataDir, "agent-wallet.key") };
|
|
11
|
+
}
|
|
12
|
+
function buildResult(mode, walletAddress, dataDir, keyFile) {
|
|
13
|
+
return {
|
|
14
|
+
mode,
|
|
15
|
+
wallet: walletAddress,
|
|
16
|
+
dataDir,
|
|
17
|
+
keyFile,
|
|
18
|
+
platformAccessRestricted: true,
|
|
19
|
+
mcpConfig: {
|
|
20
|
+
mcpServers: {
|
|
21
|
+
aipou: {
|
|
22
|
+
command: "npx",
|
|
23
|
+
args: ["-y", "aipou-mcp-server"],
|
|
24
|
+
env: {
|
|
25
|
+
AIPOU_AGENT_KEY_FILE: keyFile,
|
|
26
|
+
AIPOU_DATA_DIR: dataDir,
|
|
27
|
+
AIPOU_CONTRACT_ADDRESS: AIPOU_TOKEN_ADDRESS,
|
|
28
|
+
AIPOU_CLAIMS_ADDRESS
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
safety: {
|
|
34
|
+
dedicatedWalletCreated: mode === "persistent_identity_initialized",
|
|
35
|
+
privateKeyPrinted: false,
|
|
36
|
+
existingKeyOverwritten: false,
|
|
37
|
+
fundsMoved: false,
|
|
38
|
+
claimSubmitted: false
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export async function initializePersistentIdentity(dataDirInput) {
|
|
43
|
+
const { dataDir, keyFile } = identityPaths(dataDirInput);
|
|
44
|
+
const wallet = Wallet.createRandom();
|
|
45
|
+
await mkdir(dataDir, { recursive: true, mode: 0o700 });
|
|
46
|
+
await writeFile(keyFile, `${wallet.privateKey}\n`, {
|
|
47
|
+
encoding: "utf8",
|
|
48
|
+
mode: 0o600,
|
|
49
|
+
flag: "wx"
|
|
50
|
+
});
|
|
51
|
+
const platformAccessRestricted = await restrictToCurrentUser(keyFile);
|
|
52
|
+
if (!platformAccessRestricted) {
|
|
53
|
+
await rm(keyFile, { force: true });
|
|
54
|
+
throw new Error("Could not restrict dedicated-wallet key access to the current user");
|
|
55
|
+
}
|
|
56
|
+
return buildResult("persistent_identity_initialized", wallet.address, dataDir, keyFile);
|
|
57
|
+
}
|
|
58
|
+
// Second-run UX: an existing key is reported, never regenerated or printed.
|
|
59
|
+
// Re-applying the ACL heals a broken restriction; the existing key is never removed.
|
|
60
|
+
export async function describeExistingIdentity(dataDirInput) {
|
|
61
|
+
const { dataDir, keyFile } = identityPaths(dataDirInput);
|
|
62
|
+
const privateKey = (await readFile(keyFile, "utf8")).trim();
|
|
63
|
+
const wallet = new Wallet(privateKey);
|
|
64
|
+
const platformAccessRestricted = await restrictToCurrentUser(keyFile);
|
|
65
|
+
if (!platformAccessRestricted) {
|
|
66
|
+
throw new Error("Existing dedicated-wallet key found, but its access could not be restricted to the current user");
|
|
67
|
+
}
|
|
68
|
+
return buildResult("persistent_identity_already_initialized", wallet.address, dataDir, keyFile);
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
10
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
11
|
+
import { agentWallet } from "./identity.js";
|
|
12
|
+
import { describeExistingIdentity, initializePersistentIdentity } from "./init.js";
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
14
|
+
function parseTextResult(result) {
|
|
15
|
+
const content = result.content;
|
|
16
|
+
const text = content?.find((part) => part.type === "text")?.text;
|
|
17
|
+
if (!text)
|
|
18
|
+
throw new Error("Tool result did not include a text JSON payload");
|
|
19
|
+
return JSON.parse(text);
|
|
20
|
+
}
|
|
21
|
+
test("persistent init creates a dedicated key file without printing the secret", async () => {
|
|
22
|
+
const dataDir = await mkdtemp(path.join(os.tmpdir(), "aipou-init-test-"));
|
|
23
|
+
const previousInlineKey = process.env.AIPOU_AGENT_PRIVATE_KEY;
|
|
24
|
+
const previousKeyFile = process.env.AIPOU_AGENT_KEY_FILE;
|
|
25
|
+
try {
|
|
26
|
+
const result = await initializePersistentIdentity(dataDir);
|
|
27
|
+
const privateKey = (await readFile(result.keyFile, "utf8")).trim();
|
|
28
|
+
assert.equal(result.mode, "persistent_identity_initialized");
|
|
29
|
+
assert.equal(result.platformAccessRestricted, true);
|
|
30
|
+
assert.match(result.wallet, /^0x[0-9a-fA-F]{40}$/);
|
|
31
|
+
assert.match(privateKey, /^0x[0-9a-fA-F]{64}$/);
|
|
32
|
+
assert.equal(JSON.stringify(result).includes(privateKey), false);
|
|
33
|
+
assert.equal(result.mcpConfig.mcpServers.aipou.env.AIPOU_AGENT_KEY_FILE, result.keyFile);
|
|
34
|
+
assert.deepEqual(result.safety, {
|
|
35
|
+
dedicatedWalletCreated: true,
|
|
36
|
+
privateKeyPrinted: false,
|
|
37
|
+
existingKeyOverwritten: false,
|
|
38
|
+
fundsMoved: false,
|
|
39
|
+
claimSubmitted: false
|
|
40
|
+
});
|
|
41
|
+
delete process.env.AIPOU_AGENT_PRIVATE_KEY;
|
|
42
|
+
process.env.AIPOU_AGENT_KEY_FILE = result.keyFile;
|
|
43
|
+
assert.equal(agentWallet().address, result.wallet);
|
|
44
|
+
await assert.rejects(initializePersistentIdentity(dataDir), /EEXIST|file already exists/i);
|
|
45
|
+
assert.equal((await readFile(result.keyFile, "utf8")).trim(), privateKey);
|
|
46
|
+
const existing = await describeExistingIdentity(dataDir);
|
|
47
|
+
assert.equal(existing.mode, "persistent_identity_already_initialized");
|
|
48
|
+
assert.equal(existing.wallet, result.wallet);
|
|
49
|
+
assert.equal(existing.safety.dedicatedWalletCreated, false);
|
|
50
|
+
assert.equal(JSON.stringify(existing).includes(privateKey), false);
|
|
51
|
+
assert.equal((await readFile(result.keyFile, "utf8")).trim(), privateKey);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
if (previousInlineKey === undefined)
|
|
55
|
+
delete process.env.AIPOU_AGENT_PRIVATE_KEY;
|
|
56
|
+
else
|
|
57
|
+
process.env.AIPOU_AGENT_PRIVATE_KEY = previousInlineKey;
|
|
58
|
+
if (previousKeyFile === undefined)
|
|
59
|
+
delete process.env.AIPOU_AGENT_KEY_FILE;
|
|
60
|
+
else
|
|
61
|
+
process.env.AIPOU_AGENT_KEY_FILE = previousKeyFile;
|
|
62
|
+
await rm(dataDir, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
test("re-running CLI init reports the existing identity cleanly", async () => {
|
|
66
|
+
const dataDir = await mkdtemp(path.join(os.tmpdir(), "aipou-init-rerun-test-"));
|
|
67
|
+
const indexPath = fileURLToPath(new URL("./index.js", import.meta.url));
|
|
68
|
+
try {
|
|
69
|
+
const first = JSON.parse((await execFileAsync(process.execPath, [indexPath, "--init", "--data-dir", dataDir])).stdout);
|
|
70
|
+
const { stdout, stderr } = await execFileAsync(process.execPath, [indexPath, "--init", "--data-dir", dataDir]);
|
|
71
|
+
const second = JSON.parse(stdout);
|
|
72
|
+
const privateKey = (await readFile(second.keyFile, "utf8")).trim();
|
|
73
|
+
assert.equal(second.mode, "persistent_identity_already_initialized");
|
|
74
|
+
assert.equal(second.wallet, first.wallet);
|
|
75
|
+
assert.equal(second.safety.dedicatedWalletCreated, false);
|
|
76
|
+
assert.equal(second.safety.existingKeyOverwritten, false);
|
|
77
|
+
assert.equal(stdout.includes(privateKey), false);
|
|
78
|
+
assert.equal(stderr.includes("EEXIST"), false);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
await rm(dataDir, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
test("CLI init output starts an MCP server with the generated key file", async () => {
|
|
85
|
+
const dataDir = await mkdtemp(path.join(os.tmpdir(), "aipou-init-cli-test-"));
|
|
86
|
+
const indexPath = fileURLToPath(new URL("./index.js", import.meta.url));
|
|
87
|
+
let transport;
|
|
88
|
+
try {
|
|
89
|
+
const { stdout } = await execFileAsync(process.execPath, [indexPath, "--init", "--data-dir", dataDir]);
|
|
90
|
+
const result = JSON.parse(stdout);
|
|
91
|
+
const privateKey = (await readFile(result.keyFile, "utf8")).trim();
|
|
92
|
+
assert.equal(stdout.includes(privateKey), false);
|
|
93
|
+
const config = result.mcpConfig.mcpServers.aipou;
|
|
94
|
+
transport = new StdioClientTransport({
|
|
95
|
+
command: process.execPath,
|
|
96
|
+
args: [indexPath],
|
|
97
|
+
stderr: "pipe",
|
|
98
|
+
env: { ...process.env, ...config.env }
|
|
99
|
+
});
|
|
100
|
+
const client = new Client({ name: "aipou-init-cli-test", version: "1.0.0" });
|
|
101
|
+
await client.connect(transport);
|
|
102
|
+
const identity = parseTextResult(await client.callTool({
|
|
103
|
+
name: "get_aipou_identity",
|
|
104
|
+
arguments: {}
|
|
105
|
+
}));
|
|
106
|
+
assert.equal(identity.wallet, result.wallet);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
if (transport)
|
|
110
|
+
await transport.close();
|
|
111
|
+
await rm(dataDir, { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { collectorFingerprint } from "./collector.js";
|
|
2
|
+
export const AIPOU_RECEIPT_META_KEY = "io.github.0xddneto/aipou-receipt";
|
|
3
|
+
export function buildReceiptResultMeta(receipt) {
|
|
4
|
+
const digest = receipt.receiptId.replace(/^0x/, "").toLowerCase();
|
|
5
|
+
return {
|
|
6
|
+
[AIPOU_RECEIPT_META_KEY]: {
|
|
7
|
+
id: receipt.receiptId,
|
|
8
|
+
uri: `aipou://receipts/${receipt.receiptId}`,
|
|
9
|
+
issuer: collectorFingerprint(receipt.collectorPublicKey),
|
|
10
|
+
digest: `sha256:${digest}`,
|
|
11
|
+
evidenceClass: "issuer_asserted",
|
|
12
|
+
scheme: "aipou-receipt-v1",
|
|
13
|
+
status: "local"
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { generateKeyPairSync } from "node:crypto";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { AIPOU_RECEIPT_META_KEY, buildReceiptResultMeta } from "./receipt-metadata.js";
|
|
5
|
+
test("receipt result metadata is compact, namespaced, and contains no claim authority", () => {
|
|
6
|
+
const pair = generateKeyPairSync("ed25519");
|
|
7
|
+
const collectorPublicKey = pair.publicKey.export({ format: "pem", type: "spki" }).toString();
|
|
8
|
+
const receiptId = `0x${"ab".repeat(32)}`;
|
|
9
|
+
const metadata = buildReceiptResultMeta({ receiptId, collectorPublicKey });
|
|
10
|
+
const projection = metadata[AIPOU_RECEIPT_META_KEY];
|
|
11
|
+
assert.deepEqual(Object.keys(metadata), [AIPOU_RECEIPT_META_KEY]);
|
|
12
|
+
assert.equal(projection.id, receiptId);
|
|
13
|
+
assert.equal(projection.uri, `aipou://receipts/${receiptId}`);
|
|
14
|
+
assert.match(String(projection.issuer), /^sha256:[0-9a-f]{64}$/);
|
|
15
|
+
assert.equal(projection.digest, `sha256:${"ab".repeat(32)}`);
|
|
16
|
+
assert.equal(projection.evidenceClass, "issuer_asserted");
|
|
17
|
+
assert.equal(projection.scheme, "aipou-receipt-v1");
|
|
18
|
+
assert.equal(projection.status, "local");
|
|
19
|
+
assert.equal("wallet" in projection, false);
|
|
20
|
+
assert.equal("reward" in projection, false);
|
|
21
|
+
assert.equal("claimStatus" in projection, false);
|
|
22
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function restrictToCurrentUser(filePath: string): Promise<boolean>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
// mode 0o600 is a no-op on NTFS, so Windows also needs an explicit ACL.
|
|
5
|
+
// A false result is surfaced by onboarding; existing receipt collection keeps
|
|
6
|
+
// its historical best-effort behavior.
|
|
7
|
+
export async function restrictToCurrentUser(filePath) {
|
|
8
|
+
if (process.platform !== "win32")
|
|
9
|
+
return true;
|
|
10
|
+
const user = process.env.USERNAME;
|
|
11
|
+
if (!user)
|
|
12
|
+
return false;
|
|
13
|
+
const principal = process.env.USERDOMAIN ? `${process.env.USERDOMAIN}\\${user}` : user;
|
|
14
|
+
try {
|
|
15
|
+
await execFileAsync("icacls", [filePath, "/inheritance:r", "/grant:r", `${principal}:F`]);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aipou-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"mcpName": "io.github.0xddneto/ai-proof-of-us",
|
|
5
5
|
"description": "MCP server for private, signed AI task receipts and optional validated AIPOU claims.",
|
|
6
6
|
"author": "0xddneto",
|
|
@@ -29,14 +29,15 @@
|
|
|
29
29
|
"files": [
|
|
30
30
|
"dist",
|
|
31
31
|
"README.md",
|
|
32
|
+
"NOTICE",
|
|
32
33
|
"server.json"
|
|
33
34
|
],
|
|
34
35
|
"bin": {
|
|
35
36
|
"aipou-mcp": "dist/index.js"
|
|
36
37
|
},
|
|
37
38
|
"scripts": {
|
|
38
|
-
"build": "tsc -p tsconfig.json",
|
|
39
|
-
"test": "npm run build && node --test dist/protocol.test.js dist/rewards.test.js dist/canonical.test.js dist/demo.test.js dist/version.test.js",
|
|
39
|
+
"build": "tsc -p tsconfig.json && esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/index.js --external:dotenv --external:dotenv/* --external:ethers --external:merkletreejs --external:zod --external:zod/*",
|
|
40
|
+
"test": "npm run build && node --test dist/protocol.test.js dist/rewards.test.js dist/canonical.test.js dist/demo.test.js dist/doctor.test.js dist/init.test.js dist/receipt-metadata.test.js dist/version.test.js",
|
|
40
41
|
"pack:check": "npm test && npm pack --dry-run",
|
|
41
42
|
"demo": "npm run build && node dist/index.js --demo",
|
|
42
43
|
"dev": "tsx src/index.ts",
|
|
@@ -50,14 +51,15 @@
|
|
|
50
51
|
"node": ">=20"
|
|
51
52
|
},
|
|
52
53
|
"dependencies": {
|
|
53
|
-
"@modelcontextprotocol/sdk": "^1.13.0",
|
|
54
54
|
"dotenv": "^16.6.1",
|
|
55
55
|
"ethers": "^6.17.0",
|
|
56
56
|
"merkletreejs": "^0.6.0",
|
|
57
|
-
"zod": "^3.
|
|
57
|
+
"zod": "^3.25.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
60
61
|
"@types/node": "^22.5.4",
|
|
62
|
+
"esbuild": "0.28.1",
|
|
61
63
|
"tsx": "^4.19.0",
|
|
62
64
|
"typescript": "^5.5.4"
|
|
63
65
|
}
|
package/server.json
CHANGED
|
@@ -9,23 +9,30 @@
|
|
|
9
9
|
"source": "github",
|
|
10
10
|
"subfolder": "mcp-server"
|
|
11
11
|
},
|
|
12
|
-
"version": "0.
|
|
12
|
+
"version": "0.5.0",
|
|
13
13
|
"packages": [
|
|
14
14
|
{
|
|
15
15
|
"registryType": "npm",
|
|
16
16
|
"identifier": "aipou-mcp-server",
|
|
17
|
-
"version": "0.
|
|
17
|
+
"version": "0.5.0",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
|
20
20
|
},
|
|
21
21
|
"environmentVariables": [
|
|
22
22
|
{
|
|
23
|
-
"description": "
|
|
24
|
-
"isRequired":
|
|
23
|
+
"description": "Optional inline private key for a dedicated farming wallet. Prefer AIPOU_AGENT_KEY_FILE and never use a primary wallet.",
|
|
24
|
+
"isRequired": false,
|
|
25
25
|
"format": "string",
|
|
26
26
|
"isSecret": true,
|
|
27
27
|
"name": "AIPOU_AGENT_PRIVATE_KEY"
|
|
28
28
|
},
|
|
29
|
+
{
|
|
30
|
+
"description": "Path to the protected dedicated-wallet key created by aipou-mcp --init.",
|
|
31
|
+
"isRequired": false,
|
|
32
|
+
"format": "string",
|
|
33
|
+
"isSecret": false,
|
|
34
|
+
"name": "AIPOU_AGENT_KEY_FILE"
|
|
35
|
+
},
|
|
29
36
|
{
|
|
30
37
|
"description": "AIPOU token contract address on the configured network.",
|
|
31
38
|
"isRequired": true,
|