@paynodelabs/paynode-402-cli 2.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/README.md +73 -0
- package/commands/check.ts +126 -0
- package/commands/get-api-detail.ts +30 -0
- package/commands/invoke-paid-api.ts +91 -0
- package/commands/list-paid-apis.ts +58 -0
- package/commands/mint.ts +97 -0
- package/commands/request.ts +400 -0
- package/commands/tasks.ts +74 -0
- package/index.ts +114 -0
- package/marketplace/client.ts +189 -0
- package/marketplace/types.ts +37 -0
- package/package.json +40 -0
- package/tests/commands.test.ts +88 -0
- package/tests/network.test.ts +53 -0
- package/tests/utils.test.ts +50 -0
- package/tsconfig.json +19 -0
- package/utils.ts +400 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { jsonEnvelope, reportError, withRetry, EXIT_CODES, GLOBAL_CONFIG } from '../utils.ts';
|
|
2
|
+
import type { CatalogApiItem, CatalogListResponse, InvokePreparation } from './types.ts';
|
|
3
|
+
|
|
4
|
+
export interface MarketplaceClientOptions {
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
json?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class MarketplaceError extends Error {
|
|
10
|
+
constructor(
|
|
11
|
+
message: string,
|
|
12
|
+
public readonly status: number,
|
|
13
|
+
public readonly code: string = 'unknown_error'
|
|
14
|
+
) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'MarketplaceError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ListCatalogOptions {
|
|
21
|
+
network?: string;
|
|
22
|
+
limit?: number;
|
|
23
|
+
tag?: string[];
|
|
24
|
+
seller?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PrepareInvokeOptions {
|
|
28
|
+
network?: string;
|
|
29
|
+
payload?: any;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function joinUrl(baseUrl: string, path: string): string {
|
|
33
|
+
const normalizedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
34
|
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
35
|
+
return `${normalizedBase}${normalizedPath}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RawCatalogApiItem {
|
|
39
|
+
id?: string;
|
|
40
|
+
api_id?: string;
|
|
41
|
+
name?: string;
|
|
42
|
+
title?: string;
|
|
43
|
+
api_name?: string;
|
|
44
|
+
description?: string;
|
|
45
|
+
tags?: string[];
|
|
46
|
+
price_per_call?: string | number;
|
|
47
|
+
price?: string | number;
|
|
48
|
+
amount?: string | number;
|
|
49
|
+
currency?: string;
|
|
50
|
+
network?: string;
|
|
51
|
+
seller?: any;
|
|
52
|
+
seller_name?: string;
|
|
53
|
+
wallet_address?: string;
|
|
54
|
+
method?: string;
|
|
55
|
+
http_method?: string;
|
|
56
|
+
payable_url?: string;
|
|
57
|
+
payment_url?: string;
|
|
58
|
+
invoke_url?: string;
|
|
59
|
+
input_schema?: any;
|
|
60
|
+
sample_response?: any;
|
|
61
|
+
headers_template?: any;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeCatalogItem(raw: RawCatalogApiItem): CatalogApiItem {
|
|
65
|
+
return {
|
|
66
|
+
id: raw.id || raw.api_id || '',
|
|
67
|
+
name: raw.name || raw.title || raw.api_name || raw.api_id || 'unnamed',
|
|
68
|
+
description: raw.description,
|
|
69
|
+
tags: Array.isArray(raw.tags) ? raw.tags : [],
|
|
70
|
+
price_per_call: String(raw.price_per_call || raw.price || raw.amount || '0'),
|
|
71
|
+
currency: raw.currency || 'USDC',
|
|
72
|
+
network: raw.network,
|
|
73
|
+
seller: (raw.seller && typeof raw.seller === 'object' && Object.keys(raw.seller).length > 0) ? {
|
|
74
|
+
name: raw.seller.name || raw.seller.seller_name,
|
|
75
|
+
wallet_address: raw.seller.wallet_address || raw.seller.address
|
|
76
|
+
} : {
|
|
77
|
+
name: raw.seller_name,
|
|
78
|
+
wallet_address: raw.wallet_address
|
|
79
|
+
},
|
|
80
|
+
method: raw.method || raw.http_method,
|
|
81
|
+
payable_url: raw.payable_url || raw.payment_url,
|
|
82
|
+
invoke_url: raw.invoke_url,
|
|
83
|
+
input_schema: raw.input_schema,
|
|
84
|
+
sample_response: raw.sample_response,
|
|
85
|
+
headers_template: raw.headers_template
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class MarketplaceClient {
|
|
90
|
+
private readonly baseUrl: string;
|
|
91
|
+
private readonly isJson: boolean;
|
|
92
|
+
|
|
93
|
+
constructor(options: MarketplaceClientOptions = {}) {
|
|
94
|
+
this.baseUrl = options.baseUrl || GLOBAL_CONFIG.MARKETPLACE_URL;
|
|
95
|
+
this.isJson = !!options.json;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private async request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
99
|
+
const url = joinUrl(this.baseUrl, path);
|
|
100
|
+
const response = await withRetry(
|
|
101
|
+
() => fetch(url, init),
|
|
102
|
+
`marketplace:${path}`
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
const text = await response.text();
|
|
107
|
+
let errorMessage = `Marketplace request failed (${response.status}) at ${path}: ${text || 'empty response'}`;
|
|
108
|
+
let errorCode = 'unknown_error';
|
|
109
|
+
try {
|
|
110
|
+
const json = JSON.parse(text);
|
|
111
|
+
if (json.message) errorMessage = json.message;
|
|
112
|
+
errorCode = json.code || json.error || errorCode;
|
|
113
|
+
} catch { /* use defaults if parse fails */ }
|
|
114
|
+
|
|
115
|
+
throw new MarketplaceError(errorMessage, response.status, errorCode);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return await response.json() as T;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async listCatalog(options: ListCatalogOptions = {}): Promise<CatalogListResponse> {
|
|
122
|
+
const params = new URLSearchParams();
|
|
123
|
+
if (options.network) params.set('network', options.network);
|
|
124
|
+
if (options.limit) params.set('limit', String(options.limit));
|
|
125
|
+
if (options.seller) params.set('seller', options.seller);
|
|
126
|
+
for (const tag of options.tag || []) {
|
|
127
|
+
params.append('tag', tag);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const query = params.toString();
|
|
131
|
+
const path = `/api/v1/paid-apis${query ? `?${query}` : ''}`;
|
|
132
|
+
const raw = await this.request<any>(path);
|
|
133
|
+
const items = Array.isArray(raw.items)
|
|
134
|
+
? raw.items.map(normalizeCatalogItem)
|
|
135
|
+
: Array.isArray(raw)
|
|
136
|
+
? raw.map(normalizeCatalogItem)
|
|
137
|
+
: [];
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
items,
|
|
141
|
+
total: raw.total || items.length
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async getApiDetail(apiId: string, network?: string): Promise<CatalogApiItem> {
|
|
146
|
+
const params = new URLSearchParams();
|
|
147
|
+
if (network) params.set('network', network);
|
|
148
|
+
const query = params.toString();
|
|
149
|
+
const path = `/api/v1/paid-apis/${encodeURIComponent(apiId)}${query ? `?${query}` : ''}`;
|
|
150
|
+
const raw = await this.request<any>(path);
|
|
151
|
+
return normalizeCatalogItem(raw);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async prepareInvoke(apiId: string, options: PrepareInvokeOptions = {}): Promise<InvokePreparation> {
|
|
155
|
+
try {
|
|
156
|
+
const preparation = await this.request<InvokePreparation>(`/api/v1/paid-apis/${encodeURIComponent(apiId)}/invoke`, {
|
|
157
|
+
method: 'POST',
|
|
158
|
+
headers: {
|
|
159
|
+
'Content-Type': 'application/json'
|
|
160
|
+
},
|
|
161
|
+
body: JSON.stringify({
|
|
162
|
+
network: options.network,
|
|
163
|
+
payload: options.payload ?? {}
|
|
164
|
+
})
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (!preparation.invoke_url) {
|
|
168
|
+
throw new Error('Preparation response missing invoke_url');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return preparation;
|
|
172
|
+
} catch (err: any) {
|
|
173
|
+
console.warn(`[Marketplace] /invoke preparation failed for ${apiId}, falling back to direct proxy. Error: ${err.message}`);
|
|
174
|
+
const detail = await this.getApiDetail(apiId, options.network);
|
|
175
|
+
const invokeUrl = detail.payable_url || detail.invoke_url;
|
|
176
|
+
if (!invokeUrl) {
|
|
177
|
+
throw new Error(`API '${apiId}' is missing payable_url/invoke_url and marketplace did not provide an invoke preparation.`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
api_id: detail.id,
|
|
182
|
+
invoke_url: invokeUrl,
|
|
183
|
+
method: detail.method || 'POST',
|
|
184
|
+
headers: detail.headers_template || {},
|
|
185
|
+
body: options.payload ?? {}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface SellerInfo {
|
|
2
|
+
name?: string;
|
|
3
|
+
wallet_address?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface CatalogApiItem {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
tags?: string[];
|
|
11
|
+
price_per_call?: string;
|
|
12
|
+
currency?: string;
|
|
13
|
+
network?: string;
|
|
14
|
+
seller?: SellerInfo;
|
|
15
|
+
method?: string;
|
|
16
|
+
payable_url?: string;
|
|
17
|
+
invoke_url?: string;
|
|
18
|
+
input_schema?: Record<string, any>;
|
|
19
|
+
sample_response?: any;
|
|
20
|
+
headers_template?: Record<string, string>;
|
|
21
|
+
[key: string]: any;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CatalogListResponse {
|
|
25
|
+
items: CatalogApiItem[];
|
|
26
|
+
total?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface InvokePreparation {
|
|
30
|
+
api_id: string;
|
|
31
|
+
invoke_url: string;
|
|
32
|
+
method?: string;
|
|
33
|
+
headers?: Record<string, string>;
|
|
34
|
+
body?: any;
|
|
35
|
+
network?: string;
|
|
36
|
+
[key: string]: any;
|
|
37
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@paynodelabs/paynode-402-cli",
|
|
3
|
+
"version": "2.5.0",
|
|
4
|
+
"description": "The official command-line interface for the PayNode protocol. Designed for AI Agents to execute stateless micro-payments via HTTP 402.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"paynode-402": "./index.ts"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"paynode",
|
|
12
|
+
"x402",
|
|
13
|
+
"payment-required",
|
|
14
|
+
"web3",
|
|
15
|
+
"bun",
|
|
16
|
+
"cli",
|
|
17
|
+
"agent"
|
|
18
|
+
],
|
|
19
|
+
"author": "PayNode Labs",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "bun test",
|
|
23
|
+
"build": "echo 'No build required for Bun' && exit 0"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@paynodelabs/sdk-js": "^2.4.0",
|
|
27
|
+
"cac": "7.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"bun-types": "^1.0.0"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/PayNodeLabs/paynode-402-cli.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/PayNodeLabs/paynode-402-cli/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/PayNodeLabs/paynode-402-cli#readme"
|
|
40
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { mock, describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 🛡️ [TEST SAFETY]
|
|
5
|
+
* Prevent process.exit from killing the test runner.
|
|
6
|
+
*/
|
|
7
|
+
const exitSpy = spyOn(process, "exit").mockImplementation((code) => {
|
|
8
|
+
throw new Error(`PROCESS_EXIT_${code}`);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 🧬 [ENVIRONMENT SETUP]
|
|
13
|
+
* Pre-set environment variables before any module evaluation.
|
|
14
|
+
*/
|
|
15
|
+
process.env.CLIENT_PRIVATE_KEY = "0x" + "1".repeat(64);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 🔒 [MOCK CORE]
|
|
19
|
+
*/
|
|
20
|
+
const mockEthers = {
|
|
21
|
+
parseEther: (v: string) => 1000000000000000n,
|
|
22
|
+
formatEther: (v: bigint) => "0.001",
|
|
23
|
+
formatUnits: (v: bigint, u: number) => (Number(v) / Math.pow(10, u)).toString(),
|
|
24
|
+
Wallet: class {
|
|
25
|
+
address = "0xMockAddress";
|
|
26
|
+
//@ts-ignore
|
|
27
|
+
connect = () => this;
|
|
28
|
+
constructor(pk: string, provider: any) {}
|
|
29
|
+
},
|
|
30
|
+
Contract: class {
|
|
31
|
+
constructor() {
|
|
32
|
+
//@ts-ignore
|
|
33
|
+
this.balanceOf = async () => 1000000n;
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
JsonRpcProvider: class {
|
|
37
|
+
constructor(url: string) {}
|
|
38
|
+
//@ts-ignore
|
|
39
|
+
getNetwork = async () => ({ chainId: 84532n });
|
|
40
|
+
//@ts-ignore
|
|
41
|
+
getBalance = async () => 1000000000000000n;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
mock.module("ethers", () => mockEthers);
|
|
46
|
+
mock.module("@paynodelabs/sdk-js", () => ({
|
|
47
|
+
ethers: mockEthers,
|
|
48
|
+
BASE_RPC_URLS: ["http://mock-rpc"],
|
|
49
|
+
BASE_RPC_URLS_SANDBOX: ["http://mock-rpc-sandbox"],
|
|
50
|
+
BASE_USDC_ADDRESS: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
51
|
+
BASE_USDC_ADDRESS_SANDBOX: "0x65c088EfBDB0E03185Dbe8e258Ad0cf4Ab7946b0",
|
|
52
|
+
PAYNODE_ROUTER_ADDRESS: "0x4A73696ccF76E7381b044cB95127B3784369Ed63",
|
|
53
|
+
PAYNODE_ROUTER_ADDRESS_SANDBOX: "0x24cD8b68aaC209217ff5a6ef1Bf55a59f2c8Ca6F"
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 🚀 [DEFERRED LOADING]
|
|
58
|
+
*/
|
|
59
|
+
const { checkAction } = await import("../commands/check.ts");
|
|
60
|
+
|
|
61
|
+
describe("checkAction() CLI command tests", () => {
|
|
62
|
+
let logSpy: any;
|
|
63
|
+
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
66
|
+
exitSpy.mockClear();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
logSpy.mockRestore();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("✅ Should output JSON status when --json is provided", async () => {
|
|
74
|
+
await checkAction({ json: true });
|
|
75
|
+
|
|
76
|
+
expect(logSpy).toHaveBeenCalled();
|
|
77
|
+
const lastCall = logSpy.mock.calls[logSpy.mock.calls.length - 1][0];
|
|
78
|
+
const output = JSON.parse(lastCall);
|
|
79
|
+
expect(output.status).toBe("success");
|
|
80
|
+
expect(output.address).toBe("0xMockAddress");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("✅ Should output human-readable status by default", async () => {
|
|
84
|
+
await checkAction({ json: false });
|
|
85
|
+
expect(logSpy).toHaveBeenCalled();
|
|
86
|
+
expect(logSpy.mock.calls[0][0]).toContain("PayNode Wallet Status");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { mock, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ⚠️ [CRITICAL]
|
|
5
|
+
* This mock must reside at the very top of the file to intercept
|
|
6
|
+
* dynamic imports within resolveNetwork() before they evaluate.
|
|
7
|
+
*/
|
|
8
|
+
mock.module("@paynodelabs/sdk-js", () => {
|
|
9
|
+
const mockProvider = {
|
|
10
|
+
getNetwork: async () => ({ chainId: 84532n }), // Force testnet for all
|
|
11
|
+
getBalance: async () => 1000000000001n
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
ethers: {
|
|
15
|
+
JsonRpcProvider: class {
|
|
16
|
+
constructor(public url: string) {}
|
|
17
|
+
getNetwork = async () => mockProvider.getNetwork();
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
BASE_RPC_URLS: ["http://mock-rpc"],
|
|
21
|
+
BASE_RPC_URLS_SANDBOX: ["http://mock-rpc-sandbox"],
|
|
22
|
+
BASE_USDC_ADDRESS: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
23
|
+
BASE_USDC_ADDRESS_SANDBOX: "0x65c088EfBDB0E03185Dbe8e258Ad0cf4Ab7946b0",
|
|
24
|
+
PAYNODE_ROUTER_ADDRESS: "0x4A73696ccF76E7381b044cB95127B3784369Ed63",
|
|
25
|
+
PAYNODE_ROUTER_ADDRESS_SANDBOX: "0x24cD8b68aaC209217ff5a6ef1Bf55a59f2c8Ca6F"
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
import { resolveNetwork } from "../utils.ts";
|
|
30
|
+
|
|
31
|
+
describe("resolveNetwork() unit tests with direct mock", () => {
|
|
32
|
+
|
|
33
|
+
test("✅ Should resolve testnet (Sepolia) by alias", async () => {
|
|
34
|
+
const config = await resolveNetwork(undefined, "testnet");
|
|
35
|
+
expect(config.chainId).toBe(84532);
|
|
36
|
+
expect(config.isSandbox).toBe(true);
|
|
37
|
+
expect(config.usdcAddress).toBe("0x65c088EfBDB0E03185Dbe8e258Ad0cf4Ab7946b0");
|
|
38
|
+
expect(config.routerAddress).toBe("0x24cD8b68aaC209217ff5a6ef1Bf55a59f2c8Ca6F");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("✅ Should resolve mainnet (aliased to Sandbox in this mock)", async () => {
|
|
42
|
+
const config = await resolveNetwork(undefined, "mainnet");
|
|
43
|
+
// Due to our mock forcing 84532n, it will be treated as Sandbox.
|
|
44
|
+
expect(config.chainId).toBe(84532);
|
|
45
|
+
expect(config.networkName).toContain("84532");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("✅ Should use provided custom RPC URL without timing out", async () => {
|
|
49
|
+
const customRpc = "http://my-mock-rpc-node.com";
|
|
50
|
+
const config = await resolveNetwork(customRpc);
|
|
51
|
+
expect(config.rpcUrl).toBe(customRpc);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, test, mock, beforeAll, afterAll } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
generateTaskId,
|
|
4
|
+
maskAddress,
|
|
5
|
+
jsonEnvelope,
|
|
6
|
+
EXIT_CODES,
|
|
7
|
+
SKILL_VERSION,
|
|
8
|
+
SDK_VERSION
|
|
9
|
+
} from "../utils.ts";
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { tmpdir } from "os";
|
|
13
|
+
|
|
14
|
+
describe("PayNode CLI Utilities", () => {
|
|
15
|
+
|
|
16
|
+
test("generateTaskId() should return unique alphanumeric strings", () => {
|
|
17
|
+
const id1 = generateTaskId();
|
|
18
|
+
const id2 = generateTaskId();
|
|
19
|
+
expect(id1).not.toBe(id2);
|
|
20
|
+
expect(id1).toMatch(/^[a-z0-9]+-[a-z0-9]+$/);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("maskAddress() should mask long addresses", () => {
|
|
24
|
+
const addr = "0x1234567890abcdef1234567890abcdef12345678";
|
|
25
|
+
const masked = maskAddress(addr);
|
|
26
|
+
expect(masked).toBe("0x1234...5678");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("maskAddress() should return short strings as-is", () => {
|
|
30
|
+
expect(maskAddress("abc")).toBe("abc");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("jsonEnvelope() should include correct version metadata", () => {
|
|
34
|
+
const data = { foo: "bar" };
|
|
35
|
+
const envelope = JSON.parse(jsonEnvelope(data));
|
|
36
|
+
|
|
37
|
+
expect(envelope.foo).toBe("bar");
|
|
38
|
+
expect(envelope.version).toBe(SKILL_VERSION);
|
|
39
|
+
expect(envelope.skill_version).toBe(SKILL_VERSION);
|
|
40
|
+
expect(envelope.sdk_version).toBe(SDK_VERSION);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("EXIT_CODES should be consistent with protocol spec", () => {
|
|
44
|
+
expect(EXIT_CODES.SUCCESS).toBe(0);
|
|
45
|
+
expect(EXIT_CODES.GENERIC_ERROR).toBe(1);
|
|
46
|
+
expect(EXIT_CODES.AUTH_FAILURE).toBe(3);
|
|
47
|
+
expect(EXIT_CODES.INSUFFICIENT_FUNDS).toBe(7);
|
|
48
|
+
expect(EXIT_CODES.DUST_LIMIT).toBe(8);
|
|
49
|
+
});
|
|
50
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"allowImportingTsExtensions": true,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"ignoreDeprecations": "5.0",
|
|
11
|
+
"allowSyntheticDefaultImports": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"paths": {
|
|
14
|
+
"*": ["./node_modules/*"]
|
|
15
|
+
},
|
|
16
|
+
"types": ["bun-types"]
|
|
17
|
+
},
|
|
18
|
+
"include": ["**/*.ts"]
|
|
19
|
+
}
|