@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
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# ā” paynode-402-cli
|
|
2
|
+
|
|
3
|
+
[](https://base.org)
|
|
4
|
+
[](https://paynode.dev)
|
|
5
|
+
|
|
6
|
+
The official command-line interface for the [PayNode](https://github.com/PayNodeLabs) protocol. Designed specifically for the **Agentic Era**, allowing AI Agents to execute zero-install, stateless micro-payments via the HTTP 402 standard.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## š ļø Configuration & Credentials
|
|
11
|
+
|
|
12
|
+
To enable automated blockchain signing, this tool requires a private key provided via environment variables.
|
|
13
|
+
|
|
14
|
+
> [!IMPORTANT]
|
|
15
|
+
> **Mandatory Variable**: `CLIENT_PRIVATE_KEY` (0x...)
|
|
16
|
+
> All signature and payment operations rely on this key. Ensure it is set in your environment before execution.
|
|
17
|
+
|
|
18
|
+
> [!CAUTION]
|
|
19
|
+
> **BURNER WALLET ONLY**: NEVER use a primary or "cold storage" wallet. This tool is designed for autonomous agents; always use a dedicated burner wallet with minimal funds (< 10 USDC).
|
|
20
|
+
|
|
21
|
+
### Setup Example (Unix)
|
|
22
|
+
```bash
|
|
23
|
+
export CLIENT_PRIVATE_KEY="0x..."
|
|
24
|
+
bunx @paynodelabs/paynode-402-cli check
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## š¤ For AI Agents (Zero-Install)
|
|
30
|
+
|
|
31
|
+
Your agent can use this tool dynamically via `bunx` without any manual installation:
|
|
32
|
+
|
|
33
|
+
### 1. Check Readiness (Gas & USDC)
|
|
34
|
+
```bash
|
|
35
|
+
bunx @paynodelabs/paynode-402-cli check --network testnet --json
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Access a 402-Protected API
|
|
39
|
+
The CLI automatically detects the 402 challenge, performs the handshake, signs the payment (on-chain or EIP-3009), and returns the final resource.
|
|
40
|
+
```bash
|
|
41
|
+
bunx @paynodelabs/paynode-402-cli request "https://api.example.com/data" --network testnet --json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 3. Mint Test USDC (Base Sepolia)
|
|
45
|
+
```bash
|
|
46
|
+
bunx @paynodelabs/paynode-402-cli mint --amount 100 --network testnet
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## š Command Summary
|
|
52
|
+
|
|
53
|
+
| Command | Description |
|
|
54
|
+
| :--- | :--- |
|
|
55
|
+
| `check` | Check ETH/USDC balances and readiness on Base L2 |
|
|
56
|
+
| `mint` | Mint Mock USDC on Base Sepolia for testing |
|
|
57
|
+
| `request <URL>` | Access a protected resource by handling the 402 challenge |
|
|
58
|
+
| `list-paid-apis` | Discover payable APIs from the PayNode Marketplace |
|
|
59
|
+
| `get-api-detail <id>` | Inspect one marketplace API |
|
|
60
|
+
| `invoke-paid-api <id>` | Invoke a marketplace API using the 402 flow |
|
|
61
|
+
|
|
62
|
+
### Global Flags
|
|
63
|
+
- `--network <name>`: `mainnet` or `testnet` (default: `testnet`).
|
|
64
|
+
- `--json`: Format output as machine-readable JSON (preferred for Agents).
|
|
65
|
+
- `--confirm-mainnet`: Explicit flag required for real USDC transactions on mainnet.
|
|
66
|
+
- `--background`: Execute in background and return a `task_id` for long-running handshakes.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## š References
|
|
71
|
+
- **Marketplace**: [https://mk.paynode.dev](https://mk.paynode.dev)
|
|
72
|
+
- **Protocol SPEC**: [PayNode Docs](https://docs.paynode.dev)
|
|
73
|
+
- **GitHub**: [PayNodeLabs/paynode-402-cli](https://github.com/PayNodeLabs/paynode-402-cli)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { ethers } from '@paynodelabs/sdk-js';
|
|
2
|
+
import {
|
|
3
|
+
getPrivateKey,
|
|
4
|
+
resolveNetwork,
|
|
5
|
+
requireMainnetConfirmation,
|
|
6
|
+
reportError,
|
|
7
|
+
jsonEnvelope,
|
|
8
|
+
withRetry,
|
|
9
|
+
EXIT_CODES,
|
|
10
|
+
BaseCliOptions
|
|
11
|
+
} from '../utils.ts';
|
|
12
|
+
|
|
13
|
+
interface CheckOptions extends BaseCliOptions {
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Minimum gas threshold: 0.001 ETH (in wei)
|
|
17
|
+
const MIN_GAS_WEI = ethers.parseEther('0.001');
|
|
18
|
+
|
|
19
|
+
export async function checkAction(options: CheckOptions) {
|
|
20
|
+
const isJson = !!options.json;
|
|
21
|
+
const pk = getPrivateKey(isJson);
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const { provider, usdcAddress, chainId, networkName, isSandbox } = await resolveNetwork(
|
|
25
|
+
options.rpc,
|
|
26
|
+
options.network,
|
|
27
|
+
options.rpcTimeout
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Mainnet safety gate
|
|
31
|
+
requireMainnetConfirmation(isSandbox, !!options.confirmMainnet, isJson);
|
|
32
|
+
|
|
33
|
+
const wallet = new ethers.Wallet(pk, provider);
|
|
34
|
+
const address = wallet.address;
|
|
35
|
+
|
|
36
|
+
// Fetch balances with retry
|
|
37
|
+
const [ethBalance, usdcBalance] = await withRetry(
|
|
38
|
+
() => Promise.all([
|
|
39
|
+
provider.getBalance(address),
|
|
40
|
+
new ethers.Contract(
|
|
41
|
+
usdcAddress,
|
|
42
|
+
['function balanceOf(address) view returns (uint256)'],
|
|
43
|
+
provider
|
|
44
|
+
).balanceOf(address)
|
|
45
|
+
]),
|
|
46
|
+
'balanceCheck'
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// BigInt comparisons are used for logic (no precision loss).
|
|
50
|
+
// parseFloat is only for human-readable display and non-critical JSON values.
|
|
51
|
+
const ethValue = parseFloat(ethers.formatEther(ethBalance));
|
|
52
|
+
const usdcValue = parseFloat(ethers.formatUnits(usdcBalance, 6));
|
|
53
|
+
const isGasReady = ethBalance >= MIN_GAS_WEI;
|
|
54
|
+
const isTokensReady = usdcBalance > 0n;
|
|
55
|
+
|
|
56
|
+
if (isJson) {
|
|
57
|
+
console.log(
|
|
58
|
+
jsonEnvelope({
|
|
59
|
+
status: 'success',
|
|
60
|
+
address,
|
|
61
|
+
eth: ethValue,
|
|
62
|
+
usdc: usdcValue,
|
|
63
|
+
network: networkName,
|
|
64
|
+
chainId,
|
|
65
|
+
is_sandbox: isSandbox,
|
|
66
|
+
checks: {
|
|
67
|
+
gas_ready: isGasReady,
|
|
68
|
+
tokens_ready: isTokensReady,
|
|
69
|
+
can_pay: isGasReady && isTokensReady
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
} else {
|
|
74
|
+
console.log(`\nš **PayNode Wallet Status**`);
|
|
75
|
+
console.log(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
76
|
+
console.log(`š¤ **Address**: \`${address}\``);
|
|
77
|
+
console.log(`š **Network**: ${networkName}`);
|
|
78
|
+
console.log(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
79
|
+
console.log(`ā½ **ETH (Gas)**: ${ethValue.toFixed(6)} ETH ${isGasReady ? 'ā
Ready' : 'ā ļø Low balance'}`);
|
|
80
|
+
console.log(`šµ **USDC**: ${usdcValue.toFixed(2)} USDC ${isTokensReady ? 'ā
Ready' : 'ā Empty'}`);
|
|
81
|
+
console.log(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
82
|
+
|
|
83
|
+
if (isGasReady && isTokensReady) {
|
|
84
|
+
console.log(`š **Status**: Ready to handle x402 autonomous payments.`);
|
|
85
|
+
} else {
|
|
86
|
+
console.log(`ā **Status**: Action required. See tips below.`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Safety Warning for Burner Wallets (Mainnet Only)
|
|
90
|
+
if (!isSandbox && usdcValue > 10) {
|
|
91
|
+
console.warn(
|
|
92
|
+
`\n> [!CAUTION]\n> High balance detected ($${usdcValue.toFixed(2)} USDC). This is a burner wallet. \n> Consider sweeping excess funds to cold storage to minimize risk.`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!isGasReady) {
|
|
97
|
+
if (isSandbox) {
|
|
98
|
+
console.warn(
|
|
99
|
+
`\n> [!WARNING]\n> Gas balance is critically low (< 0.001 ETH). Please deposit ETH to \`${address}\` on ${networkName}.`
|
|
100
|
+
);
|
|
101
|
+
console.warn(
|
|
102
|
+
`> **Faucet**: [console.optimism.io/faucet](https://console.optimism.io/faucet) (Recommended)`
|
|
103
|
+
);
|
|
104
|
+
} else {
|
|
105
|
+
console.warn(
|
|
106
|
+
`\n> [!WARNING]\n> Gas balance is critically low (< 0.001 ETH). Please deposit ETH to \`${address}\` on ${networkName}.`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!isTokensReady) {
|
|
111
|
+
if (isSandbox) {
|
|
112
|
+
console.warn(
|
|
113
|
+
`\n> [!TIP]\n> You're on Testnet! Run \`bun run paynode-402 mint --network testnet\` to get 1,000 free Test USDC.`
|
|
114
|
+
);
|
|
115
|
+
} else {
|
|
116
|
+
console.warn(
|
|
117
|
+
`\n> [!NOTE]\n> Mainnet USDC balance is 0. This wallet is currently restricted to free trials.`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
console.log(``);
|
|
122
|
+
}
|
|
123
|
+
} catch (error: any) {
|
|
124
|
+
reportError(error, isJson, EXIT_CODES.NETWORK_ERROR);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { MarketplaceClient } from '../marketplace/client.ts';
|
|
2
|
+
import { jsonEnvelope, reportError, EXIT_CODES, BaseCliOptions } from '../utils.ts';
|
|
3
|
+
|
|
4
|
+
interface GetApiDetailOptions extends BaseCliOptions {
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function getApiDetailAction(apiId: string, options: GetApiDetailOptions) {
|
|
8
|
+
const isJson = !!options.json;
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const client = new MarketplaceClient({
|
|
12
|
+
baseUrl: options.marketUrl,
|
|
13
|
+
json: isJson
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const detail = await client.getApiDetail(apiId, options.network);
|
|
17
|
+
|
|
18
|
+
if (isJson) {
|
|
19
|
+
console.log(jsonEnvelope({
|
|
20
|
+
status: 'success',
|
|
21
|
+
api: detail
|
|
22
|
+
}));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log(JSON.stringify(detail, null, 2));
|
|
27
|
+
} catch (error: any) {
|
|
28
|
+
reportError(error, isJson, EXIT_CODES.NETWORK_ERROR);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { requestAction } from './request.ts';
|
|
2
|
+
import { MarketplaceClient } from '../marketplace/client.ts';
|
|
3
|
+
import { reportError, EXIT_CODES, BaseCliOptions } from '../utils.ts';
|
|
4
|
+
|
|
5
|
+
interface InvokePaidApiOptions extends BaseCliOptions {
|
|
6
|
+
method?: string;
|
|
7
|
+
data?: string;
|
|
8
|
+
header?: string | string[];
|
|
9
|
+
background?: boolean;
|
|
10
|
+
output?: string;
|
|
11
|
+
maxAge?: number;
|
|
12
|
+
taskDir?: string;
|
|
13
|
+
taskId?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function mergeHeaders(
|
|
17
|
+
marketplaceHeaders: Record<string, string> | undefined,
|
|
18
|
+
cliHeader: string | string[] | undefined
|
|
19
|
+
): string[] {
|
|
20
|
+
const merged: string[] = [];
|
|
21
|
+
|
|
22
|
+
for (const [key, value] of Object.entries(marketplaceHeaders || {})) {
|
|
23
|
+
merged.push(`${key}: ${value}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (Array.isArray(cliHeader)) {
|
|
27
|
+
merged.push(...cliHeader);
|
|
28
|
+
} else if (cliHeader) {
|
|
29
|
+
merged.push(cliHeader);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return merged;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parsePayload(data?: string): any {
|
|
36
|
+
if (!data) return undefined;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(data);
|
|
40
|
+
} catch (err: any) {
|
|
41
|
+
const isJsonLike = data.trim().startsWith('{') || data.trim().startsWith('[');
|
|
42
|
+
if (isJsonLike) {
|
|
43
|
+
console.warn(`ā ļø [Warning] Invocation data looks like JSON but failed to parse: ${err.message}`);
|
|
44
|
+
console.warn(`Sending as raw string instead. Please verify your JSON syntax.`);
|
|
45
|
+
} else {
|
|
46
|
+
console.warn(`ā ļø [Warning] Invocation data is not valid JSON. Sending as raw string.`);
|
|
47
|
+
}
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function invokePaidApiAction(apiId: string, options: InvokePaidApiOptions) {
|
|
53
|
+
const isJson = !!options.json;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const client = new MarketplaceClient({
|
|
57
|
+
baseUrl: options.marketUrl,
|
|
58
|
+
json: isJson
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const invoke = await client.prepareInvoke(apiId, {
|
|
62
|
+
network: options.network,
|
|
63
|
+
payload: parsePayload(options.data)
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const requestHeaders = mergeHeaders(invoke.headers, options.header);
|
|
67
|
+
const hasPreparedBody = !!(invoke.body && typeof invoke.body === 'object' && Object.keys(invoke.body).length > 0);
|
|
68
|
+
const requestBody = hasPreparedBody
|
|
69
|
+
? JSON.stringify(invoke.body)
|
|
70
|
+
: (options.data || undefined); // Use undefined if no data to trigger smart promotion if needed
|
|
71
|
+
|
|
72
|
+
await requestAction(invoke.invoke_url, [], {
|
|
73
|
+
json: options.json,
|
|
74
|
+
network: options.network || invoke.network, // Delegate fallback to resolveNetwork
|
|
75
|
+
rpc: options.rpc,
|
|
76
|
+
rpcTimeout: options.rpcTimeout,
|
|
77
|
+
confirmMainnet: options.confirmMainnet,
|
|
78
|
+
method: options.method || invoke.method || 'POST',
|
|
79
|
+
data: requestBody,
|
|
80
|
+
header: requestHeaders,
|
|
81
|
+
background: options.background,
|
|
82
|
+
dryRun: options.dryRun,
|
|
83
|
+
output: options.output,
|
|
84
|
+
maxAge: options.maxAge,
|
|
85
|
+
taskDir: options.taskDir,
|
|
86
|
+
taskId: options.taskId
|
|
87
|
+
});
|
|
88
|
+
} catch (error: any) {
|
|
89
|
+
reportError(error, isJson, EXIT_CODES.NETWORK_ERROR);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { MarketplaceClient } from '../marketplace/client.ts';
|
|
2
|
+
import { jsonEnvelope, reportError, EXIT_CODES, BaseCliOptions } from '../utils.ts';
|
|
3
|
+
|
|
4
|
+
interface ListPaidApisOptions extends BaseCliOptions {
|
|
5
|
+
limit?: string | number;
|
|
6
|
+
tag?: string | string[];
|
|
7
|
+
seller?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function listPaidApisAction(options: ListPaidApisOptions) {
|
|
11
|
+
const isJson = !!options.json;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const client = new MarketplaceClient({
|
|
15
|
+
baseUrl: options.marketUrl,
|
|
16
|
+
json: isJson
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const tags = Array.isArray(options.tag)
|
|
20
|
+
? options.tag
|
|
21
|
+
: options.tag
|
|
22
|
+
? [options.tag]
|
|
23
|
+
: [];
|
|
24
|
+
|
|
25
|
+
const result = await client.listCatalog({
|
|
26
|
+
network: options.network,
|
|
27
|
+
limit: options.limit ? Number(options.limit) : undefined,
|
|
28
|
+
tag: tags,
|
|
29
|
+
seller: options.seller
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (isJson) {
|
|
33
|
+
console.log(jsonEnvelope({
|
|
34
|
+
status: 'success',
|
|
35
|
+
total: result.total || result.items.length,
|
|
36
|
+
items: result.items
|
|
37
|
+
}));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (result.items.length === 0) {
|
|
42
|
+
console.log('No paid APIs found.');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const item of result.items) {
|
|
47
|
+
const price = item.price_per_call ? `${item.price_per_call} ${item.currency || 'USDC'}` : 'unpriced';
|
|
48
|
+
const network = item.network || 'unspecified';
|
|
49
|
+
const tagsLine = item.tags && item.tags.length > 0 ? ` [${item.tags.join(', ')}]` : '';
|
|
50
|
+
console.log(`- ${item.id}: ${item.name} | ${price} | ${network}${tagsLine}`);
|
|
51
|
+
if (item.description) {
|
|
52
|
+
console.log(` ${item.description}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch (error: any) {
|
|
56
|
+
reportError(error, isJson, EXIT_CODES.NETWORK_ERROR);
|
|
57
|
+
}
|
|
58
|
+
}
|
package/commands/mint.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { ethers } from '@paynodelabs/sdk-js';
|
|
2
|
+
import {
|
|
3
|
+
getPrivateKey,
|
|
4
|
+
resolveNetwork,
|
|
5
|
+
reportError,
|
|
6
|
+
jsonEnvelope,
|
|
7
|
+
withRetry,
|
|
8
|
+
EXIT_CODES,
|
|
9
|
+
BaseCliOptions
|
|
10
|
+
} from '../utils.ts';
|
|
11
|
+
|
|
12
|
+
interface MintOptions extends BaseCliOptions {
|
|
13
|
+
amount?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function mintAction(options: MintOptions) {
|
|
17
|
+
const isJson = !!options.json;
|
|
18
|
+
const pk = getPrivateKey(isJson);
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const { provider, usdcAddress, chainId, networkName, isSandbox } = await resolveNetwork(
|
|
22
|
+
options.rpc,
|
|
23
|
+
options.network || 'testnet',
|
|
24
|
+
options.rpcTimeout
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (!isSandbox) {
|
|
28
|
+
throw new Error(`Minting is only supported on Sepolia. Current ChainID: ${chainId}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const wallet = new ethers.Wallet(pk, provider);
|
|
32
|
+
const mintAmountStr = String(options.amount || '1000');
|
|
33
|
+
|
|
34
|
+
// Gas check
|
|
35
|
+
const balance = await provider.getBalance(wallet.address);
|
|
36
|
+
if (balance === 0n) {
|
|
37
|
+
reportError(
|
|
38
|
+
`Gas balance is 0 ETH on ${networkName}. Please fund \`${wallet.address}\` to continue.\n` +
|
|
39
|
+
`š” **Faucet**: [console.optimism.io/faucet](https://console.optimism.io/faucet) ā 0.01 ETH daily (Recommended)`,
|
|
40
|
+
isJson,
|
|
41
|
+
EXIT_CODES.INSUFFICIENT_FUNDS
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Progress messages are sent to stderr to avoid polluting stdout
|
|
46
|
+
// when valid JSON output is expected by the caller (e.g. via --json)
|
|
47
|
+
if (!isJson) {
|
|
48
|
+
console.error(`š° Connecting to ${networkName}...`);
|
|
49
|
+
console.error(`š Minting ${mintAmountStr} USDC for address: ${wallet.address}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const abi = ['function mint(address to, uint256 amount) external'];
|
|
53
|
+
const usdc = new ethers.Contract(usdcAddress, abi, wallet);
|
|
54
|
+
|
|
55
|
+
const amount = ethers.parseUnits(mintAmountStr, 6);
|
|
56
|
+
|
|
57
|
+
if (!isJson) console.error('ā³ Sending mint transaction...');
|
|
58
|
+
const tx = await withRetry(
|
|
59
|
+
() => usdc.mint(wallet.address, amount),
|
|
60
|
+
'mint'
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (!isJson) console.error('ā³ Waiting for confirmation...');
|
|
64
|
+
const receipt: any = await withRetry(
|
|
65
|
+
() => tx.wait(),
|
|
66
|
+
'mintConfirm'
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (!receipt || receipt.status !== 1) {
|
|
70
|
+
throw new Error('Transaction reverted or failed.');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (isJson) {
|
|
74
|
+
console.log(
|
|
75
|
+
jsonEnvelope({
|
|
76
|
+
status: 'success',
|
|
77
|
+
txHash: tx.hash,
|
|
78
|
+
address: wallet.address,
|
|
79
|
+
amount: mintAmountStr,
|
|
80
|
+
token: 'MockUSDC',
|
|
81
|
+
network: networkName
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
} else {
|
|
85
|
+
console.log(`\nā
**Success!**`);
|
|
86
|
+
console.log(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
87
|
+
console.log(`š° **Minted**: ${mintAmountStr} Test USDC`);
|
|
88
|
+
console.log(`š **Network**: ${networkName}`);
|
|
89
|
+
console.log(`š **Tx Hash**: \`${tx.hash}\``);
|
|
90
|
+
console.log(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
91
|
+
console.log(`š **Your wallet (\`${wallet.address}\`) is now funded and ready for testing.**`);
|
|
92
|
+
console.log(``);
|
|
93
|
+
}
|
|
94
|
+
} catch (error: any) {
|
|
95
|
+
reportError(error, isJson, EXIT_CODES.NETWORK_ERROR);
|
|
96
|
+
}
|
|
97
|
+
}
|