@portkey/ca-agent-skills 1.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.
@@ -0,0 +1,281 @@
1
+ import AElf from 'aelf-sdk';
2
+ import type { WalletInfo, TransactionResult } from './types.js';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Types (aelf-sdk does not ship clean TS types, see ./aelf-sdk.d.ts)
6
+ // ---------------------------------------------------------------------------
7
+
8
+ export interface AElfWallet {
9
+ address: string;
10
+ privateKey: string;
11
+ mnemonic?: string;
12
+ BIP44Path?: string;
13
+ childWallet?: unknown;
14
+ keyPair?: unknown;
15
+ }
16
+
17
+ /* eslint-disable @typescript-eslint/no-explicit-any */
18
+ export type AElfInstance = InstanceType<typeof AElf>;
19
+ export type AElfContract = Record<string, any>;
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Instance & contract caches
23
+ // ---------------------------------------------------------------------------
24
+
25
+ const aelfInstanceCache: Record<string, AElfInstance> = {};
26
+ const contractCache: Record<string, AElfContract> = {};
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Wallet helpers
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /** Create a brand-new wallet (manager keypair). */
33
+ export function createWallet(): WalletInfo {
34
+ const w = AElf.wallet.createNewWallet();
35
+ return {
36
+ address: w.address,
37
+ privateKey: w.privateKey,
38
+ mnemonic: w.mnemonic,
39
+ };
40
+ }
41
+
42
+ /** Restore a wallet from a private key string. */
43
+ export function getWalletByPrivateKey(privateKey: string): AElfWallet {
44
+ return AElf.wallet.getWalletByPrivateKey(privateKey) as AElfWallet;
45
+ }
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // AElf instance
49
+ // ---------------------------------------------------------------------------
50
+
51
+ /** Get (or create) an AElf SDK instance for the given RPC URL. */
52
+ export function getAelfInstance(rpcUrl: string): AElfInstance {
53
+ if (!aelfInstanceCache[rpcUrl]) {
54
+ aelfInstanceCache[rpcUrl] = new AElf(new AElf.providers.HttpProvider(rpcUrl, 20_000));
55
+ }
56
+ return aelfInstanceCache[rpcUrl];
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Contract instance
61
+ // ---------------------------------------------------------------------------
62
+
63
+ /**
64
+ * Get (or create) a contract instance.
65
+ * - For view-only calls, `wallet` can be any wallet (a default one is fine).
66
+ * - For send calls, `wallet` must be the manager wallet that owns the CA.
67
+ */
68
+ export async function getContractInstance(
69
+ rpcUrl: string,
70
+ contractAddress: string,
71
+ wallet: AElfWallet,
72
+ ): Promise<AElfContract> {
73
+ const key = `${rpcUrl}|${contractAddress}|${wallet.address}`;
74
+ if (!contractCache[key]) {
75
+ const instance = getAelfInstance(rpcUrl);
76
+ contractCache[key] = await instance.chain.contractAt(contractAddress, wallet);
77
+ }
78
+ return contractCache[key];
79
+ }
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Contract call helpers
83
+ // ---------------------------------------------------------------------------
84
+
85
+ /** Call a read-only (view) method on a contract. */
86
+ export async function callViewMethod<T = unknown>(
87
+ rpcUrl: string,
88
+ contractAddress: string,
89
+ methodName: string,
90
+ params?: Record<string, unknown>,
91
+ ): Promise<T> {
92
+ // View methods don't require a real identity — use a random ephemeral wallet.
93
+ // NEVER hard-code a real private key here.
94
+ const defaultWallet = createWallet();
95
+ const contract = await getContractInstance(rpcUrl, contractAddress, defaultWallet);
96
+
97
+ const method = contract[methodName];
98
+ if (!method || typeof method?.call !== 'function') {
99
+ throw new Error(`Contract method "${methodName}" not found at ${contractAddress}`);
100
+ }
101
+
102
+ const result = await method.call(params ?? {});
103
+ if (result && typeof result === 'object' && 'error' in result && result.error) {
104
+ throw new Error(`View call ${methodName} failed: ${JSON.stringify(result.error)}`);
105
+ }
106
+ // aelf-sdk wraps result in { result } or returns directly
107
+ return ((result && typeof result === 'object' && 'result' in result ? result.result : result) ?? result) as T;
108
+ }
109
+
110
+ /** Call a state-changing (send) method on a contract. Returns the TX result. */
111
+ export async function callSendMethod(
112
+ rpcUrl: string,
113
+ contractAddress: string,
114
+ wallet: AElfWallet,
115
+ methodName: string,
116
+ params: Record<string, unknown>,
117
+ ): Promise<{ transactionId: string; data: TransactionResult }> {
118
+ const contract = await getContractInstance(rpcUrl, contractAddress, wallet);
119
+
120
+ const method = contract[methodName];
121
+ if (!method || typeof method !== 'function') {
122
+ throw new Error(`Contract method "${methodName}" not found at ${contractAddress}`);
123
+ }
124
+
125
+ const sendResult = await method(params);
126
+ const txId = sendResult?.result?.TransactionId || sendResult?.TransactionId;
127
+
128
+ if (sendResult && typeof sendResult === 'object' && 'error' in sendResult && sendResult.error) {
129
+ throw new Error(
130
+ `Send call ${methodName} failed: ${JSON.stringify(sendResult.error)}`,
131
+ );
132
+ }
133
+
134
+ if (!txId) {
135
+ throw new Error(`Send call ${methodName} did not return a TransactionId`);
136
+ }
137
+
138
+ // Wait for TX to be mined, then fetch result
139
+ await sleep(1000);
140
+ const txResult = await getTxResult(rpcUrl, txId);
141
+ return { transactionId: txId, data: txResult };
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // ManagerForwardCall parameter encoding
146
+ // ---------------------------------------------------------------------------
147
+
148
+ /**
149
+ * Encode parameters for ManagerForwardCall.
150
+ *
151
+ * ManagerForwardCall expects `args` as protobuf-encoded bytes of the target
152
+ * method's input type. This function:
153
+ * 1. Fetches the target contract's FileDescriptorSet
154
+ * 2. Finds the target method's input protobuf type
155
+ * 3. Encodes the args using that type
156
+ *
157
+ * Returns the full ManagerForwardCall params ready to be sent.
158
+ */
159
+ export async function encodeManagerForwardCallParams(
160
+ rpcUrl: string,
161
+ params: {
162
+ caHash: string;
163
+ contractAddress: string;
164
+ methodName: string;
165
+ args: Record<string, unknown>;
166
+ },
167
+ ): Promise<Record<string, unknown>> {
168
+ const instance = getAelfInstance(rpcUrl);
169
+
170
+ // Get target contract's protobuf descriptors
171
+ const fds = await instance.chain.getContractFileDescriptorSet(params.contractAddress);
172
+ const root = AElf.pbjs.Root.fromDescriptor(fds, 'proto3');
173
+
174
+ // Find the method's input type across all services
175
+ let inputType: unknown = null;
176
+ const cleanMethodName = params.methodName.replace('.', '');
177
+
178
+ for (const svc of root.nestedArray) {
179
+ if ((svc as { methods?: Record<string, unknown> }).methods) {
180
+ const service = svc as { methods: Record<string, { resolvedRequestType?: unknown }> };
181
+ const method = service.methods[cleanMethodName];
182
+ if (method?.resolvedRequestType) {
183
+ inputType = method.resolvedRequestType;
184
+ break;
185
+ }
186
+ }
187
+ // Also check nested namespaces
188
+ if ((svc as { nestedArray?: unknown[] }).nestedArray) {
189
+ for (const nested of (svc as { nestedArray: { methods?: Record<string, { resolvedRequestType?: unknown }> }[] }).nestedArray) {
190
+ if (nested.methods?.[cleanMethodName]?.resolvedRequestType) {
191
+ inputType = nested.methods[cleanMethodName].resolvedRequestType;
192
+ break;
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ if (!inputType) {
199
+ throw new Error(
200
+ `Method "${params.methodName}" not found in contract ${params.contractAddress}`,
201
+ );
202
+ }
203
+
204
+ // Encode args using protobuf
205
+ const type = inputType as {
206
+ fromObject: (obj: unknown) => unknown;
207
+ encode: (msg: unknown) => { finish: () => Uint8Array };
208
+ };
209
+
210
+ let input = params.args;
211
+ // Apply aelf-sdk transforms if available
212
+ if (AElf.utils?.transform?.transformMapToArray) {
213
+ input = AElf.utils.transform.transformMapToArray(inputType, input);
214
+ }
215
+ if (AElf.utils?.transform?.transform && AElf.utils?.transform?.INPUT_TRANSFORMERS) {
216
+ input = AElf.utils.transform.transform(
217
+ inputType,
218
+ input,
219
+ AElf.utils.transform.INPUT_TRANSFORMERS,
220
+ );
221
+ }
222
+
223
+ const message = type.fromObject(input);
224
+ const encodedArgs = type.encode(message).finish();
225
+
226
+ return {
227
+ caHash: params.caHash,
228
+ contractAddress: params.contractAddress,
229
+ methodName: params.methodName,
230
+ args: encodedArgs,
231
+ };
232
+ }
233
+
234
+ // ---------------------------------------------------------------------------
235
+ // Transaction result polling
236
+ // ---------------------------------------------------------------------------
237
+
238
+ const TX_RESULT_MAX_RETRIES = 20;
239
+ const TX_RESULT_RETRY_DELAY = 1500;
240
+
241
+ /** Poll for a transaction result until it is mined or fails. */
242
+ export async function getTxResult(
243
+ rpcUrl: string,
244
+ txId: string,
245
+ maxRetries = TX_RESULT_MAX_RETRIES,
246
+ ): Promise<TransactionResult> {
247
+ const instance = getAelfInstance(rpcUrl);
248
+
249
+ for (let i = 0; i < maxRetries; i++) {
250
+ try {
251
+ const result = await instance.chain.getTxResult(txId);
252
+ if (result.Status === 'MINED' || result.Status === 'FAILED') {
253
+ if (result.Status === 'FAILED') {
254
+ throw new Error(`Transaction ${txId} FAILED: ${result.Error || 'Unknown error'}`);
255
+ }
256
+ return result as TransactionResult;
257
+ }
258
+ } catch (err: unknown) {
259
+ // If the error is our own FAILED error, re-throw
260
+ if (err instanceof Error && err.message.includes('FAILED')) throw err;
261
+ // Otherwise it might be "not found yet", keep retrying
262
+ }
263
+ await sleep(TX_RESULT_RETRY_DELAY);
264
+ }
265
+
266
+ throw new Error(`Transaction ${txId} not confirmed after ${maxRetries} retries`);
267
+ }
268
+
269
+ // ---------------------------------------------------------------------------
270
+ // Helpers
271
+ // ---------------------------------------------------------------------------
272
+
273
+ function sleep(ms: number): Promise<void> {
274
+ return new Promise((resolve) => setTimeout(resolve, ms));
275
+ }
276
+
277
+ /** Clear all caches (useful for testing). */
278
+ export function clearCaches(): void {
279
+ for (const key of Object.keys(aelfInstanceCache)) delete aelfInstanceCache[key];
280
+ for (const key of Object.keys(contractCache)) delete contractCache[key];
281
+ }
@@ -0,0 +1,103 @@
1
+ declare module 'aelf-sdk' {
2
+ interface HttpProvider {
3
+ new (host: string, timeout?: number): HttpProvider;
4
+ }
5
+
6
+ interface WalletModule {
7
+ createNewWallet(): {
8
+ address: string;
9
+ privateKey: string;
10
+ mnemonic: string;
11
+ BIP44Path: string;
12
+ childWallet: unknown;
13
+ keyPair: unknown;
14
+ };
15
+ getWalletByPrivateKey(privateKey: string): {
16
+ address: string;
17
+ privateKey: string;
18
+ keyPair: unknown;
19
+ };
20
+ }
21
+
22
+ interface ChainModule {
23
+ contractAt(
24
+ contractAddress: string,
25
+ wallet: { address: string; privateKey?: string },
26
+ ): Promise<Record<string, any>>;
27
+ getTxResult(txId: string): Promise<Record<string, any>>;
28
+ getContractFileDescriptorSet(address: string): Promise<any>;
29
+ }
30
+
31
+ interface PbjsModule {
32
+ Root: {
33
+ fromDescriptor(descriptor: any, syntax?: string): any;
34
+ };
35
+ }
36
+
37
+ interface UtilsModule {
38
+ transform?: {
39
+ transformMapToArray(type: any, params: any): any;
40
+ transform(type: any, params: any, transformers: any): any;
41
+ INPUT_TRANSFORMERS: any;
42
+ };
43
+ }
44
+
45
+ class AElf {
46
+ constructor(provider: HttpProvider);
47
+ chain: ChainModule;
48
+ currentProvider: { host: string };
49
+ static providers: { HttpProvider: new (host: string, timeout?: number) => HttpProvider };
50
+ static wallet: WalletModule;
51
+ static pbjs: PbjsModule;
52
+ static utils: UtilsModule;
53
+ }
54
+
55
+ export default AElf;
56
+ }
57
+
58
+ declare module 'aelf-sdk/src/util/keyStore.js' {
59
+ interface KeystoreWalletInfo {
60
+ privateKey: string;
61
+ mnemonic: string;
62
+ address?: string;
63
+ nickName?: string;
64
+ }
65
+
66
+ interface KeystoreObject {
67
+ version: number;
68
+ type: string;
69
+ nickName?: string;
70
+ address: string;
71
+ crypto: {
72
+ cipher: string;
73
+ ciphertext: string;
74
+ cipherparams: { iv: string };
75
+ mnemonicEncrypted: string;
76
+ kdf: string;
77
+ kdfparams: {
78
+ r: number;
79
+ n: number;
80
+ p: number;
81
+ dklen: number;
82
+ salt: string;
83
+ };
84
+ mac: string;
85
+ };
86
+ }
87
+
88
+ export function getKeystore(
89
+ walletInfo: KeystoreWalletInfo,
90
+ password: string,
91
+ option?: Record<string, unknown>,
92
+ ): KeystoreObject;
93
+
94
+ export function unlockKeystore(
95
+ keyStore: KeystoreObject | Record<string, unknown>,
96
+ password: string,
97
+ ): KeystoreWalletInfo;
98
+
99
+ export function checkPassword(
100
+ keyStore: KeystoreObject | Record<string, unknown>,
101
+ password: string,
102
+ ): boolean;
103
+ }
package/lib/config.ts ADDED
@@ -0,0 +1,46 @@
1
+ import type { NetworkType, PortkeyConfig, NetworkDefaults } from './types.js';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Network defaults
5
+ // ---------------------------------------------------------------------------
6
+
7
+ const NETWORK_DEFAULTS: Record<NetworkType, NetworkDefaults> = {
8
+ mainnet: {
9
+ apiUrl: 'https://aa-portkey.portkey.finance',
10
+ graphqlUrl: 'https://indexer-api.aefinder.io/api/app/graphql/portkey',
11
+ },
12
+ testnet: {
13
+ apiUrl: 'https://aa-portkey-test.portkey.finance',
14
+ graphqlUrl: 'https://test-indexer-api.aefinder.io/api/app/graphql/portkey',
15
+ },
16
+ };
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Config builder
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /**
23
+ * Build a PortkeyConfig with the following priority (high -> low):
24
+ * 1. Function parameter `override`
25
+ * 2. CLI arguments (handled by the caller)
26
+ * 3. MCP env block (handled by the caller)
27
+ * 4. Environment variables: PORTKEY_NETWORK, PORTKEY_API_URL, PORTKEY_GRAPHQL_URL
28
+ * 5. Code defaults (mainnet)
29
+ */
30
+ export function getConfig(override?: Partial<PortkeyConfig> & { network?: NetworkType }): PortkeyConfig {
31
+ const network: NetworkType =
32
+ override?.network || (process.env.PORTKEY_NETWORK as NetworkType) || 'mainnet';
33
+
34
+ const defaults = NETWORK_DEFAULTS[network];
35
+ if (!defaults) {
36
+ throw new Error(`Unknown network: ${network}. Expected "mainnet" or "testnet".`);
37
+ }
38
+
39
+ return {
40
+ network,
41
+ apiUrl: override?.apiUrl || process.env.PORTKEY_API_URL || defaults.apiUrl,
42
+ graphqlUrl: override?.graphqlUrl || process.env.PORTKEY_GRAPHQL_URL || defaults.graphqlUrl,
43
+ };
44
+ }
45
+
46
+ export { NETWORK_DEFAULTS };
package/lib/http.ts ADDED
@@ -0,0 +1,197 @@
1
+ import type { PortkeyConfig } from './types.js';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Types
5
+ // ---------------------------------------------------------------------------
6
+
7
+ export interface HttpRequestOptions {
8
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
9
+ params?: Record<string, string | number | boolean | undefined>;
10
+ data?: unknown;
11
+ headers?: Record<string, string>;
12
+ timeout?: number;
13
+ }
14
+
15
+ interface ApiResponse<T = unknown> {
16
+ code: string;
17
+ message: string;
18
+ data: T;
19
+ }
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // HttpError — structured error for precise matching
23
+ // ---------------------------------------------------------------------------
24
+
25
+ export class HttpError extends Error {
26
+ /** HTTP status code (e.g. 400, 404, 500) */
27
+ statusCode: number;
28
+ /** Portkey API error code (e.g. '3002'), if present */
29
+ errorCode: string | null;
30
+ /** Raw response body text */
31
+ responseBody: string;
32
+
33
+ constructor(statusCode: number, statusText: string, body: string) {
34
+ // Try to extract API error code and message from JSON body
35
+ let errorCode: string | null = null;
36
+ let apiMessage = '';
37
+ try {
38
+ const parsed = JSON.parse(body);
39
+ errorCode = parsed?.code ?? parsed?.Code ?? null;
40
+ apiMessage = parsed?.message ?? parsed?.Message ?? '';
41
+ } catch {
42
+ apiMessage = body;
43
+ }
44
+
45
+ super(`HTTP ${statusCode} ${statusText}: ${apiMessage || body}`);
46
+ this.name = 'HttpError';
47
+ this.statusCode = statusCode;
48
+ this.errorCode = errorCode;
49
+ this.responseBody = body;
50
+ }
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // HTTP client
55
+ // ---------------------------------------------------------------------------
56
+
57
+ const DEFAULT_TIMEOUT = 20_000;
58
+
59
+ /**
60
+ * Lightweight HTTP client for Portkey Backend API.
61
+ *
62
+ * Usage:
63
+ * const client = createHttpClient(config);
64
+ * const result = await client.get('/api/app/wallet/getRegisterInfo', { params: { ... } });
65
+ * const result = await client.post('/api/app/account/sendVerificationRequest', { data: { ... } });
66
+ */
67
+ export function createHttpClient(config: PortkeyConfig) {
68
+ const baseUrl = config.apiUrl.replace(/\/$/, '');
69
+
70
+ async function request<T = unknown>(endpoint: string, options: HttpRequestOptions = {}): Promise<T> {
71
+ const { method = 'GET', params, data, headers = {}, timeout = DEFAULT_TIMEOUT } = options;
72
+
73
+ // Build URL with query params
74
+ let url = `${baseUrl}${endpoint}`;
75
+ if (params) {
76
+ const searchParams = new URLSearchParams();
77
+ for (const [key, value] of Object.entries(params)) {
78
+ if (value !== undefined && value !== null) {
79
+ searchParams.set(key, String(value));
80
+ }
81
+ }
82
+ const qs = searchParams.toString();
83
+ if (qs) url += `?${qs}`;
84
+ }
85
+
86
+ // Build fetch options
87
+ const fetchOptions: RequestInit = {
88
+ method,
89
+ headers: {
90
+ 'Content-Type': 'application/json',
91
+ ...headers,
92
+ },
93
+ };
94
+
95
+ if (data && method !== 'GET') {
96
+ fetchOptions.body = JSON.stringify(data);
97
+ }
98
+
99
+ // Timeout via AbortController
100
+ const controller = new AbortController();
101
+ const timer = setTimeout(() => controller.abort(), timeout);
102
+ fetchOptions.signal = controller.signal;
103
+
104
+ try {
105
+ const response = await fetch(url, fetchOptions);
106
+ clearTimeout(timer);
107
+
108
+ if (!response.ok) {
109
+ const text = await response.text().catch(() => '');
110
+ throw new HttpError(response.status, response.statusText, text);
111
+ }
112
+
113
+ const json = await response.json() as ApiResponse<T> | T;
114
+
115
+ // Portkey API wraps some responses in { code, message, data }
116
+ if (json && typeof json === 'object' && 'code' in json) {
117
+ const wrapped = json as ApiResponse<T>;
118
+ if (wrapped.code !== '20000' && wrapped.code !== '0') {
119
+ throw new Error(`API error [${wrapped.code}]: ${wrapped.message}`);
120
+ }
121
+ return wrapped.data;
122
+ }
123
+
124
+ return json as T;
125
+ } catch (err: unknown) {
126
+ clearTimeout(timer);
127
+ if (err instanceof DOMException && err.name === 'AbortError') {
128
+ throw new Error(`Request timeout after ${timeout}ms: ${method} ${url}`);
129
+ }
130
+ throw err;
131
+ }
132
+ }
133
+
134
+ return {
135
+ get<T = unknown>(endpoint: string, options?: Omit<HttpRequestOptions, 'method' | 'data'>) {
136
+ return request<T>(endpoint, { ...options, method: 'GET' });
137
+ },
138
+ post<T = unknown>(endpoint: string, options?: Omit<HttpRequestOptions, 'method'>) {
139
+ return request<T>(endpoint, { ...options, method: 'POST' });
140
+ },
141
+ put<T = unknown>(endpoint: string, options?: Omit<HttpRequestOptions, 'method'>) {
142
+ return request<T>(endpoint, { ...options, method: 'PUT' });
143
+ },
144
+ del<T = unknown>(endpoint: string, options?: Omit<HttpRequestOptions, 'method'>) {
145
+ return request<T>(endpoint, { ...options, method: 'DELETE' });
146
+ },
147
+ /** Raw request with full control */
148
+ request,
149
+ };
150
+ }
151
+
152
+ export type HttpClient = ReturnType<typeof createHttpClient>;
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // SSRF protection — validate user-supplied RPC URLs
156
+ // ---------------------------------------------------------------------------
157
+
158
+ const PRIVATE_IP_PATTERNS = [
159
+ /^localhost$/i,
160
+ /^127\./,
161
+ /^10\./,
162
+ /^172\.(1[6-9]|2\d|3[01])\./,
163
+ /^192\.168\./,
164
+ /^0\./,
165
+ /^\[::1\]/,
166
+ /^\[fd/i, // IPv6 unique-local
167
+ /^\[fe80:/i, // IPv6 link-local
168
+ ];
169
+
170
+ /**
171
+ * Validate a user-supplied RPC URL to prevent SSRF attacks.
172
+ * - Requires https:// (or http:// only for known aelf domains)
173
+ * - Blocks private/internal IP ranges and localhost
174
+ *
175
+ * Throws if the URL is unsafe.
176
+ */
177
+ export function validateRpcUrl(url: string): void {
178
+ let parsed: URL;
179
+ try {
180
+ parsed = new URL(url);
181
+ } catch {
182
+ throw new Error(`Invalid RPC URL: ${url}`);
183
+ }
184
+
185
+ // Only allow http(s)
186
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
187
+ throw new Error(`RPC URL must use http(s) protocol, got: ${parsed.protocol}`);
188
+ }
189
+
190
+ // Block private/internal addresses
191
+ const hostname = parsed.hostname;
192
+ for (const pattern of PRIVATE_IP_PATTERNS) {
193
+ if (pattern.test(hostname)) {
194
+ throw new Error(`RPC URL must not point to a private/internal address: ${hostname}`);
195
+ }
196
+ }
197
+ }