apinow-sdk 0.12.3 → 0.12.4
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 +140 -132
- package/dist/index.d.ts +28 -1
- package/dist/index.js +400 -19
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# ApiNow SDK
|
|
2
2
|
|
|
3
|
-
A TypeScript SDK for interacting with ApiNow endpoints, supporting Ethereum and Base chains.
|
|
3
|
+
A TypeScript SDK for interacting with ApiNow endpoints, supporting Ethereum and Base chains. This SDK simplifies payments by automatically handling `402 Payment Required` responses, including on-the-fly token swaps.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
7
|
+
- **Automatic x402 Payments**: Intercepts `402` responses to handle payment flows automatically.
|
|
8
|
+
- **On-the-fly Token Swaps**: If you don't have the required payment token, the SDK can swap a common asset (like ETH, WETH, or USDC) to make the payment, powered by 0x.
|
|
9
|
+
- **Flexible Pricing**: Supports endpoints that require a fixed token amount or a USD equivalent.
|
|
10
|
+
- **Configurable Payment**: Prioritize which tokens you prefer to pay with.
|
|
11
|
+
- **Multi-chain support**: Works with Ethereum and Base.
|
|
12
|
+
- **Node.js Environment**: Designed to work in a Node.js environment.
|
|
12
13
|
|
|
13
14
|
## Installation
|
|
14
15
|
|
|
@@ -20,147 +21,132 @@ yarn add apinow-sdk
|
|
|
20
21
|
|
|
21
22
|
## Usage
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
The primary way to use the SDK is with the `execute` method. It's a single call that handles all the complexity of API payments for you.
|
|
24
25
|
|
|
25
26
|
```typescript
|
|
26
27
|
import apiNow from 'apinow-sdk';
|
|
27
28
|
|
|
29
|
+
// The API endpoint you want to interact with.
|
|
28
30
|
const ENDPOINT_URL = 'https://apinow.fun/api/endpoints/your-endpoint';
|
|
31
|
+
|
|
32
|
+
// Your private key, securely stored (e.g., in an environment variable).
|
|
29
33
|
const YOUR_WALLET_PRIVATE_KEY = '0x...';
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
35
|
+
async function main() {
|
|
36
|
+
try {
|
|
37
|
+
// The `execute` method handles everything automatically.
|
|
38
|
+
// If the API requires a payment (402), the SDK will:
|
|
39
|
+
// 1. Find the best token you hold to pay with.
|
|
40
|
+
// 2. If needed, swap a common asset (like ETH or USDC) to the required token.
|
|
41
|
+
// 3. Send the payment transaction.
|
|
42
|
+
// 4. Retry the original request with proof of payment.
|
|
43
|
+
const response = await apiNow.execute(
|
|
44
|
+
ENDPOINT_URL,
|
|
45
|
+
YOUR_WALLET_PRIVATE_KEY,
|
|
46
|
+
{ // Optional: request options
|
|
47
|
+
method: 'POST',
|
|
48
|
+
data: { query: 'your-data' }
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
console.log('API Response:', response);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error('Operation failed:', error);
|
|
55
|
+
}
|
|
48
56
|
}
|
|
57
|
+
|
|
58
|
+
main();
|
|
49
59
|
```
|
|
50
60
|
|
|
51
|
-
|
|
61
|
+
## How It Works: Automatic Payments
|
|
52
62
|
|
|
53
|
-
|
|
63
|
+
When you call `execute`, the SDK makes a request to the endpoint. If the server responds with a `402 Payment Required` status, the SDK automatically performs the following steps:
|
|
54
64
|
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
1. **Parses Payment Options**: The `402` response contains a list of accepted payment options. This can be a single token (fixed price) or multiple tokens (USD equivalent price, e.g., "$5 of USDC" or "$5 of ETH").
|
|
66
|
+
2. **Checks Balances**: It checks your wallet balance for each of the accepted payment tokens.
|
|
67
|
+
3. **Prioritizes Payment**: It attempts to pay using your tokens in a preferred order (default: `['USDC', 'WETH', 'ETH']`).
|
|
68
|
+
4. **Swaps if Needed**: If you don't have any of the *required* tokens, the SDK will try to swap one of your preferred assets for the required one. For example, it can swap your USDC to pay with a different required token.
|
|
69
|
+
5. **Pays and Retries**: Once the payment transaction is sent, the SDK automatically retries the original API request, now with proof of payment.
|
|
57
70
|
|
|
58
|
-
|
|
59
|
-
ENDPOINT_URL,
|
|
60
|
-
YOUR_WALLET_PRIVATE_KEY,
|
|
61
|
-
CUSTOM_RPC_URL // Provide the RPC URL here
|
|
62
|
-
);
|
|
63
|
-
```
|
|
71
|
+
## Configuration
|
|
64
72
|
|
|
65
|
-
|
|
73
|
+
You can customize the behavior of the `execute` method with the `opts` and `paymentConfig` parameters.
|
|
66
74
|
|
|
67
|
-
|
|
75
|
+
### Request Options (`opts`)
|
|
68
76
|
|
|
69
|
-
|
|
70
|
-
const response = await apiNow.infoBuyResponse(
|
|
71
|
-
ENDPOINT_URL,
|
|
72
|
-
YOUR_WALLET_PRIVATE_KEY,
|
|
73
|
-
undefined, // Use default RPC
|
|
74
|
-
{ fastMode: true } // Option included, but no effect yet
|
|
75
|
-
);
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
### Manual Payment (Separate Steps)
|
|
77
|
+
Passed as the third argument to `execute`. This corresponds to `TxResponseOptions`.
|
|
79
78
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
import { ethers } from 'ethers';
|
|
79
|
+
- `method`: The HTTP method for your request (e.g., `'GET'`, `'POST'`). Defaults to `'GET'`.
|
|
80
|
+
- `data`: The payload for your request. For `POST` requests, this is the JSON body. For `GET`, it's converted to query parameters.
|
|
83
81
|
|
|
84
|
-
|
|
85
|
-
const YOUR_WALLET_PRIVATE_KEY = '0x...';
|
|
86
|
-
const YOUR_CUSTOM_RPC_URL = 'https://your-node.com'; // Optional
|
|
82
|
+
### Payment Configuration (`paymentConfig`)
|
|
87
83
|
|
|
88
|
-
|
|
89
|
-
const info = await apiNow.info(ENDPOINT_URL);
|
|
90
|
-
const { requiredAmount, walletAddress, chain, tokenAddress } = info;
|
|
84
|
+
Passed as the fourth argument to `execute`. This corresponds to `X402PaymentConfig`.
|
|
91
85
|
|
|
92
|
-
|
|
93
|
-
const amountBigInt = BigInt(requiredAmount);
|
|
86
|
+
- `preferredTokens`: An array of token symbols (e.g., `['USDC', 'WETH']`) that you prefer to pay with. The SDK will check your balance of these tokens first.
|
|
94
87
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
amountBigInt,
|
|
88
|
+
```typescript
|
|
89
|
+
await apiNow.execute(
|
|
90
|
+
ENDPOINT_URL,
|
|
99
91
|
YOUR_WALLET_PRIVATE_KEY,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
tokenAddress // Optional: specify ERC20 token if required
|
|
103
|
-
// fastMode param exists but is unused by handler
|
|
92
|
+
{ method: 'POST', data: { /* ... */ } }, // opts
|
|
93
|
+
{ preferredTokens: ['DAI', 'USDC'] } // paymentConfig
|
|
104
94
|
);
|
|
105
|
-
|
|
95
|
+
```
|
|
106
96
|
|
|
107
|
-
|
|
108
|
-
await new Promise(resolve => setTimeout(resolve, 5000)); // Example 5s delay
|
|
97
|
+
## Legacy Flow (Backward Compatibility)
|
|
109
98
|
|
|
110
|
-
|
|
99
|
+
For backward compatibility, the `infoBuyResponse` method is still available. It performs a less sophisticated multi-step payment process.
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
const response = await apiNow.infoBuyResponse(
|
|
111
103
|
ENDPOINT_URL,
|
|
112
|
-
|
|
104
|
+
YOUR_WALLET_PRIVATE_KEY
|
|
113
105
|
);
|
|
114
|
-
console.log('API Response:', apiResponse);
|
|
115
106
|
```
|
|
116
107
|
|
|
117
108
|
## API Reference
|
|
118
109
|
|
|
119
|
-
### `
|
|
120
|
-
|
|
121
|
-
Gets payment requirement information about an ApiNow endpoint.
|
|
122
|
-
|
|
123
|
-
### `buy(walletAddress: string, amount: bigint, userWalletPrivateKey: string, chain: 'eth' | 'base', rpcUrl?: string, tokenAddress?: string, fastMode?: boolean): Promise<string>`
|
|
124
|
-
|
|
125
|
-
Sends the required payment transaction to the specified address.
|
|
126
|
-
- `amount`: The required amount in the smallest unit (wei).
|
|
127
|
-
- `userWalletPrivateKey`: The private key of the wallet sending the funds.
|
|
128
|
-
- `chain`: The blockchain target ('eth' or 'base').
|
|
129
|
-
- `rpcUrl` (Optional): Overrides the default public RPC URL.
|
|
130
|
-
- `tokenAddress` (Optional): The contract address if paying with an ERC20 token.
|
|
131
|
-
- `fastMode` (Optional): Parameter exists but currently has no effect.
|
|
132
|
-
|
|
133
|
-
Returns the transaction hash.
|
|
134
|
-
|
|
135
|
-
### `txResponse(endpoint: string, txHash: string, opts?: TxResponseOptions): Promise<any>`
|
|
110
|
+
### `execute(endpoint, privateKey, opts?, paymentConfig?)`
|
|
111
|
+
Handles a request and its potential payment in a single, automatic call. This is the recommended method.
|
|
136
112
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
- `opts` (Optional): Options like `{ method: 'POST', data: {...} }`.
|
|
113
|
+
### `infoBuyResponse(endpoint, privateKey, rpcUrl?, opts?)`
|
|
114
|
+
(Legacy) Combines `info`, `buy`, and `txResponse` into a single call.
|
|
140
115
|
|
|
141
|
-
|
|
116
|
+
### `info(endpoint)`
|
|
117
|
+
(Legacy) Gets payment requirement information from an endpoint.
|
|
142
118
|
|
|
143
|
-
### `
|
|
119
|
+
### `buy(walletAddress, amount, privateKey, chain, ...)`
|
|
120
|
+
(Legacy) Sends a payment transaction.
|
|
144
121
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
- `rpcUrl` (Optional): Overrides the default public RPC URL for the payment.
|
|
148
|
-
- `opts` (Optional): Contains `fastMode` boolean (currently no effect) and any `TxResponseOptions`.
|
|
149
|
-
|
|
150
|
-
Returns the final API response.
|
|
122
|
+
### `txResponse(endpoint, txHash, opts?)`
|
|
123
|
+
(Legacy) Fetches the API response after a payment has been made manually.
|
|
151
124
|
|
|
152
125
|
## Types
|
|
153
126
|
|
|
154
127
|
```typescript
|
|
155
|
-
//
|
|
156
|
-
interface
|
|
157
|
-
|
|
158
|
-
walletAddress: string;
|
|
159
|
-
httpMethod: string;
|
|
160
|
-
tokenAddress?: string; // ERC20 address
|
|
128
|
+
// Response from a 402 error
|
|
129
|
+
interface X402PaymentInfo {
|
|
130
|
+
challenge: string;
|
|
161
131
|
chain: 'eth' | 'base';
|
|
132
|
+
recipientAddress: string;
|
|
133
|
+
options: X402PaymentOption[];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// A single way to pay
|
|
137
|
+
interface X402PaymentOption {
|
|
138
|
+
tokenAddress: string;
|
|
139
|
+
symbol: string;
|
|
140
|
+
amount: string;
|
|
141
|
+
decimals: number;
|
|
162
142
|
}
|
|
163
143
|
|
|
144
|
+
// Configuration for payments
|
|
145
|
+
interface X402PaymentConfig {
|
|
146
|
+
preferredTokens?: string[];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Options for the API request itself
|
|
164
150
|
interface TxResponseOptions {
|
|
165
151
|
method?: string;
|
|
166
152
|
data?: any;
|
|
@@ -172,46 +158,68 @@ interface TxResponseOptions {
|
|
|
172
158
|
- **Ethereum:** `https://rpc.ankr.com/eth`
|
|
173
159
|
- **Base:** `https://mainnet.base.org`
|
|
174
160
|
|
|
175
|
-
You can override these by providing the `rpcUrl` parameter to `buy` or `infoBuyResponse`.
|
|
176
|
-
|
|
177
161
|
## Error Handling
|
|
178
162
|
|
|
179
163
|
The SDK throws descriptive errors for:
|
|
180
164
|
- Invalid endpoint URLs or configurations.
|
|
181
165
|
- RPC communication errors.
|
|
182
166
|
- Transaction signing or sending failures.
|
|
183
|
-
- Insufficient funds or
|
|
184
|
-
- Failures during API response fetching
|
|
167
|
+
- Insufficient funds or failure to find a valid swap.
|
|
168
|
+
- Failures during API response fetching.
|
|
185
169
|
|
|
186
170
|
Wrap calls in `try...catch` blocks for robust error handling.
|
|
187
171
|
|
|
188
172
|
## Compatibility
|
|
189
173
|
|
|
190
|
-
This SDK uses
|
|
191
|
-
- Node.js (v18+ recommended
|
|
192
|
-
|
|
193
|
-
|
|
174
|
+
This SDK uses `node-fetch`, making it compatible with:
|
|
175
|
+
- Node.js (v18+ recommended)
|
|
176
|
+
|
|
177
|
+
It is NOT directly compatible with browsers or edge environments that do not provide a Node.js-compatible `fetch` API.
|
|
194
178
|
|
|
195
179
|
## License
|
|
196
180
|
|
|
197
181
|
MIT
|
|
198
182
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
in the
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
183
|
+
## Examples
|
|
184
|
+
|
|
185
|
+
This project includes a test server and a test runner to demonstrate various payment scenarios.
|
|
186
|
+
|
|
187
|
+
1. **Create a `.env` file** in the root of the project and add your wallet's private key:
|
|
188
|
+
```
|
|
189
|
+
PRIVATE_KEY=your_private_key_here
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
2. **Install dependencies:**
|
|
193
|
+
```bash
|
|
194
|
+
npm install
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
3. **Start the test server:**
|
|
198
|
+
The test server simulates an API that requires different types of payments.
|
|
199
|
+
```bash
|
|
200
|
+
node test/test-server.js
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
4. **Run the test runner:**
|
|
204
|
+
In a separate terminal, run the test runner to execute a series of transactions against the test server.
|
|
205
|
+
```bash
|
|
206
|
+
node test/test-runner.js
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
This will demonstrate:
|
|
210
|
+
- Paying with USDC
|
|
211
|
+
- Paying with a custom ERC20 token
|
|
212
|
+
- Paying with a token priced in USDC
|
|
213
|
+
- Fallback token payments
|
|
214
|
+
- Handling various error conditions
|
|
215
|
+
|
|
216
|
+
## Local Development
|
|
217
|
+
|
|
218
|
+
1. **Build the project:**
|
|
219
|
+
```bash
|
|
220
|
+
npm run build
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
This will compile the TypeScript source files into JavaScript in the `dist` directory.
|
|
224
|
+
|
|
225
|
+
## Contributing
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { Wallet } from 'ethers';
|
|
1
2
|
interface TxResponseOptions {
|
|
2
3
|
method?: string;
|
|
3
4
|
data?: any;
|
|
5
|
+
signature?: string;
|
|
4
6
|
}
|
|
5
7
|
interface InfoResponse {
|
|
6
8
|
requiredAmount: string;
|
|
@@ -10,10 +12,34 @@ interface InfoResponse {
|
|
|
10
12
|
chain: 'eth' | 'base';
|
|
11
13
|
decimals?: number;
|
|
12
14
|
}
|
|
15
|
+
interface X402PaymentOption {
|
|
16
|
+
tokenAddress: string;
|
|
17
|
+
symbol: string;
|
|
18
|
+
amount?: string;
|
|
19
|
+
usdAmount?: string;
|
|
20
|
+
decimals: number;
|
|
21
|
+
}
|
|
22
|
+
interface X402PaymentInfo {
|
|
23
|
+
challenge: string;
|
|
24
|
+
chain: 'eth' | 'base';
|
|
25
|
+
recipientAddress: string;
|
|
26
|
+
options: X402PaymentOption[];
|
|
27
|
+
}
|
|
28
|
+
interface X402PaymentConfig {
|
|
29
|
+
preferredTokens?: string[];
|
|
30
|
+
swapFromAssets?: {
|
|
31
|
+
symbol: string;
|
|
32
|
+
address: string;
|
|
33
|
+
}[];
|
|
34
|
+
slippagePercentage?: string;
|
|
35
|
+
}
|
|
36
|
+
declare function get0xSwapQuote(buyToken: string, sellToken: string, buyAmount: bigint, buyTokenDecimals: number, takerAddress: string, chain: 'eth' | 'base', slippagePercentage?: string): Promise<any>;
|
|
13
37
|
declare class ApiNow {
|
|
14
38
|
private handlers;
|
|
15
39
|
info(endpoint: string): Promise<InfoResponse>;
|
|
16
40
|
buy(walletAddress: string, amount: bigint, userWalletPrivateKey: string, chain: 'eth' | 'base', rpcUrl?: string, tokenAddress?: string, fastMode?: boolean): Promise<string>;
|
|
41
|
+
execute(endpoint: string, userWalletPrivateKey: string, opts?: TxResponseOptions, paymentConfig?: X402PaymentConfig): Promise<any>;
|
|
42
|
+
approve(wallet: Wallet, tokenAddress: string, spenderAddress: string, amount: bigint): Promise<string>;
|
|
17
43
|
txResponse(endpoint: string, txHash: string, opts?: TxResponseOptions): Promise<any>;
|
|
18
44
|
infoBuyResponse(endpoint: string, userWalletPrivateKey: string, rpcUrl?: string, opts?: TxResponseOptions & {
|
|
19
45
|
fastMode?: boolean;
|
|
@@ -21,4 +47,5 @@ declare class ApiNow {
|
|
|
21
47
|
}
|
|
22
48
|
declare const apiNow: ApiNow;
|
|
23
49
|
export default apiNow;
|
|
24
|
-
export
|
|
50
|
+
export { get0xSwapQuote };
|
|
51
|
+
export type { InfoResponse, TxResponseOptions, X402PaymentInfo, X402PaymentOption, X402PaymentConfig };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
1
|
+
console.log('[sdk] SDK v1.1 loaded. This version includes header parsing and extensive logging.');
|
|
2
|
+
import { ethers, Contract, Wallet, JsonRpcProvider, parseUnits, isAddress } from 'ethers';
|
|
3
|
+
import fetch from 'node-fetch'; // Import node-fetch and its RequestInit
|
|
3
4
|
// Default RPC URLs
|
|
4
5
|
const DEFAULT_ETH_RPC = 'https://rpc.ankr.com/eth';
|
|
5
6
|
const DEFAULT_BASE_RPC = 'https://mainnet.base.org';
|
|
7
|
+
const NATIVE_TOKEN_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';
|
|
8
|
+
const CHAIN_CONFIG = {
|
|
9
|
+
'1': {
|
|
10
|
+
USDC: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
|
11
|
+
WETH: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
|
|
12
|
+
ETH: NATIVE_TOKEN_ADDRESS
|
|
13
|
+
},
|
|
14
|
+
'8453': {
|
|
15
|
+
USDC: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
|
|
16
|
+
WETH: '0x4200000000000000000000000000000000000006',
|
|
17
|
+
ETH: NATIVE_TOKEN_ADDRESS
|
|
18
|
+
}
|
|
19
|
+
};
|
|
6
20
|
// --- Helper function for fetch (Keep this) ---
|
|
7
21
|
async function fetchJson(url, options) {
|
|
8
22
|
console.error(`fetchJson (using node-fetch): Called with URL: ${url}`);
|
|
@@ -96,6 +110,343 @@ async function fetchJson(url, options) {
|
|
|
96
110
|
console.error(`fetchJson: Successfully fetched and parsed JSON (first 500 chars): ${JSON.stringify(responseData).substring(0, 500)}`);
|
|
97
111
|
return responseData;
|
|
98
112
|
}
|
|
113
|
+
// Helper to check ERC20 token balance
|
|
114
|
+
async function getTokenBalance(provider, ownerAddress, tokenAddress) {
|
|
115
|
+
const abi = ["function balanceOf(address owner) view returns (uint256)"];
|
|
116
|
+
const contract = new Contract(tokenAddress, abi, provider);
|
|
117
|
+
try {
|
|
118
|
+
const balance = await contract.balanceOf(ownerAddress);
|
|
119
|
+
return balance;
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
console.error(`[sdk] Could not get balance for token ${tokenAddress} (this is expected for invalid tokens in fallback tests). Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
123
|
+
return 0n; // Return 0 if the token address is invalid or another error occurs
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Helper to check ERC20 token allowance
|
|
127
|
+
async function checkAllowance(provider, ownerAddress, tokenAddress, spenderAddress) {
|
|
128
|
+
const abi = ["function allowance(address owner, address spender) view returns (uint256)"];
|
|
129
|
+
const contract = new Contract(tokenAddress, abi, provider);
|
|
130
|
+
const allowance = await contract.allowance(ownerAddress, spenderAddress);
|
|
131
|
+
return allowance;
|
|
132
|
+
}
|
|
133
|
+
// New helper for x402 flow
|
|
134
|
+
async function fetchWithX402(url, options, api, // Pass in the ApiNow instance
|
|
135
|
+
userWalletPrivateKey, paymentConfig = {}) {
|
|
136
|
+
const originalResponse = await fetch(url, options);
|
|
137
|
+
if (originalResponse.status !== 402) {
|
|
138
|
+
if (!originalResponse.ok) {
|
|
139
|
+
const errorBody = await originalResponse.text();
|
|
140
|
+
throw new Error(`HTTP Error ${originalResponse.status}: ${errorBody}`);
|
|
141
|
+
}
|
|
142
|
+
return originalResponse.json();
|
|
143
|
+
}
|
|
144
|
+
console.error("402 Payment Required. Handling payment...");
|
|
145
|
+
// --- FIX START: Parse the www-authenticate header instead of the body ---
|
|
146
|
+
const wwwAuthHeader = originalResponse.headers.get('www-authenticate');
|
|
147
|
+
console.log('[sdk] Raw www-authenticate header:', wwwAuthHeader);
|
|
148
|
+
if (!wwwAuthHeader) {
|
|
149
|
+
throw new Error('402 response is missing the www-authenticate header.');
|
|
150
|
+
}
|
|
151
|
+
const l402Match = wwwAuthHeader.match(/L402="([^"]+)"/);
|
|
152
|
+
console.log('[sdk] Regex match for L402 token:', l402Match);
|
|
153
|
+
if (!l402Match || !l402Match[1]) {
|
|
154
|
+
throw new Error('Could not parse L402 token from www-authenticate header.');
|
|
155
|
+
}
|
|
156
|
+
const l402Token = l402Match[1];
|
|
157
|
+
console.log('[sdk] Extracted L402 Base64 token:', l402Token);
|
|
158
|
+
let paymentInfo;
|
|
159
|
+
try {
|
|
160
|
+
const decodedToken = Buffer.from(l402Token, 'base64').toString('utf8');
|
|
161
|
+
console.log('[sdk] Decoded token (JSON string):', decodedToken);
|
|
162
|
+
const parsedToken = JSON.parse(decodedToken);
|
|
163
|
+
console.log('[sdk] Parsed token (JavaScript object):', JSON.stringify(parsedToken, null, 2));
|
|
164
|
+
// Ensure the parsed token matches the X402PaymentInfo interface
|
|
165
|
+
if (typeof parsedToken === 'object' && parsedToken !== null && 'challenge' in parsedToken && 'chain' in parsedToken && 'recipientAddress' in parsedToken && 'options' in parsedToken && Array.isArray(parsedToken.options)) {
|
|
166
|
+
paymentInfo = parsedToken;
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
console.error('[sdk] Parsed token failed validation. Keys:', Object.keys(parsedToken));
|
|
170
|
+
throw new Error('Parsed L402 token is not in the expected X402PaymentInfo format (missing challenge, chain, recipientAddress, or options).');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
console.error('[sdk] Error during token decoding/parsing:', e);
|
|
175
|
+
throw new Error(`Failed to decode or parse L402 token: ${e instanceof Error ? e.message : String(e)}`);
|
|
176
|
+
}
|
|
177
|
+
// --- FIX END ---
|
|
178
|
+
// --- NEW FIX START: Select RPC URL based on the parsed chain ---
|
|
179
|
+
const rpcUrl = paymentInfo.chain === 'base' ? DEFAULT_BASE_RPC : DEFAULT_ETH_RPC;
|
|
180
|
+
const provider = new JsonRpcProvider(rpcUrl);
|
|
181
|
+
const wallet = new Wallet(userWalletPrivateKey, provider);
|
|
182
|
+
console.log(`[sdk] Using RPC URL for chain "${paymentInfo.chain}": ${rpcUrl}`);
|
|
183
|
+
// --- NEW FIX END ---
|
|
184
|
+
const { challenge, chain, recipientAddress, options: paymentOptions } = paymentInfo;
|
|
185
|
+
let txHash;
|
|
186
|
+
// Define a preference order for payment tokens.
|
|
187
|
+
const preferredTokens = paymentConfig.preferredTokens || ['USDC', 'WETH']; // Default preference
|
|
188
|
+
const sortedOptions = [...paymentOptions].sort((a, b) => {
|
|
189
|
+
const aIndex = preferredTokens.indexOf(a.symbol);
|
|
190
|
+
const bIndex = preferredTokens.indexOf(b.symbol);
|
|
191
|
+
if (aIndex === -1 && bIndex === -1)
|
|
192
|
+
return 0;
|
|
193
|
+
if (aIndex === -1)
|
|
194
|
+
return 1;
|
|
195
|
+
if (bIndex === -1)
|
|
196
|
+
return -1;
|
|
197
|
+
return aIndex - bIndex;
|
|
198
|
+
});
|
|
199
|
+
const nativeSymbol = chain === 'base' ? 'ETH' : 'ETH'; // Could be more specific
|
|
200
|
+
const nativeOption = sortedOptions.find(o => o.symbol === nativeSymbol);
|
|
201
|
+
if (nativeOption) {
|
|
202
|
+
const nativeIndex = sortedOptions.indexOf(nativeOption);
|
|
203
|
+
sortedOptions.splice(nativeIndex, 1);
|
|
204
|
+
sortedOptions.push(nativeOption); // Always try native token last unless specified
|
|
205
|
+
}
|
|
206
|
+
for (const option of sortedOptions) {
|
|
207
|
+
const { tokenAddress, amount, decimals, symbol, usdAmount } = option;
|
|
208
|
+
let requiredAmount;
|
|
209
|
+
if (usdAmount) {
|
|
210
|
+
console.log(`[sdk] Option requires a USD value of ${usdAmount}. Fetching price for ${symbol}...`);
|
|
211
|
+
const tokenPriceInUsd = await getUsdcPriceForToken(tokenAddress, chain, decimals);
|
|
212
|
+
const requiredTokens = parseFloat(usdAmount) / tokenPriceInUsd;
|
|
213
|
+
console.log(`[sdk] Current price of ${symbol} is ~$${tokenPriceInUsd.toFixed(4)}. Required tokens: ${requiredTokens}`);
|
|
214
|
+
requiredAmount = parseUnits(requiredTokens.toString(), decimals);
|
|
215
|
+
}
|
|
216
|
+
else if (amount) {
|
|
217
|
+
requiredAmount = parseUnits(amount, decimals);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
console.warn(`[sdk] Skipping payment option for ${symbol} because it has no 'amount' or 'usdAmount'.`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
console.log(`[sdk] Checking balance for ${symbol} (${tokenAddress}). Required: ${requiredAmount.toString()}`);
|
|
224
|
+
let balance;
|
|
225
|
+
if (tokenAddress.toLowerCase() === '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee') {
|
|
226
|
+
balance = await provider.getBalance(wallet.address);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
balance = await getTokenBalance(provider, wallet.address, tokenAddress);
|
|
230
|
+
}
|
|
231
|
+
console.log(`[sdk] Found balance for ${symbol}: ${balance.toString()}`);
|
|
232
|
+
if (balance >= requiredAmount) {
|
|
233
|
+
console.log(`[sdk] Sufficient balance found for ${symbol}. Proceeding with payment.`);
|
|
234
|
+
txHash = await api.buy(recipientAddress, requiredAmount, userWalletPrivateKey, chain, rpcUrl, tokenAddress);
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (!txHash) {
|
|
239
|
+
// --- Try to swap ---
|
|
240
|
+
console.log("No direct payment option possible. Attempting to find a swap.");
|
|
241
|
+
const chainId = chain === 'base' ? '8453' : '1';
|
|
242
|
+
const defaultSwapAssets = [
|
|
243
|
+
{ symbol: 'USDC', address: CHAIN_CONFIG[chainId].USDC },
|
|
244
|
+
{ symbol: 'WETH', address: CHAIN_CONFIG[chainId].WETH },
|
|
245
|
+
{ symbol: 'ETH', address: CHAIN_CONFIG[chainId].ETH }
|
|
246
|
+
];
|
|
247
|
+
const swapHierarchy = paymentConfig.swapFromAssets || defaultSwapAssets;
|
|
248
|
+
for (const targetOption of sortedOptions) {
|
|
249
|
+
for (const sourceAsset of swapHierarchy) {
|
|
250
|
+
try {
|
|
251
|
+
const quote = await get0xSwapQuote(targetOption.tokenAddress, sourceAsset.address,
|
|
252
|
+
// --- FIX for USD amounts in swaps ---
|
|
253
|
+
targetOption.usdAmount ?
|
|
254
|
+
(await (async () => {
|
|
255
|
+
console.log(`[sdk] Swap target requires a USD value of ${targetOption.usdAmount}. Fetching price for ${targetOption.symbol}...`);
|
|
256
|
+
const tokenPriceInUsd = await getUsdcPriceForToken(targetOption.tokenAddress, chain, targetOption.decimals);
|
|
257
|
+
const requiredTokens = parseFloat(targetOption.usdAmount) / tokenPriceInUsd;
|
|
258
|
+
console.log(`[sdk] Swap target ${targetOption.symbol} price is ~$${tokenPriceInUsd.toFixed(4)}. Required tokens for swap: ${requiredTokens}`);
|
|
259
|
+
return parseUnits(requiredTokens.toFixed(18), targetOption.decimals); // Use high precision for swap amount
|
|
260
|
+
})()) :
|
|
261
|
+
parseUnits(targetOption.amount, targetOption.decimals), // Use non-null assertion for amount
|
|
262
|
+
// --- End FIX ---
|
|
263
|
+
targetOption.decimals, wallet.address, chain, paymentConfig.slippagePercentage);
|
|
264
|
+
const buyAmount = BigInt(quote.buyAmount);
|
|
265
|
+
let cost;
|
|
266
|
+
let sourceBalance;
|
|
267
|
+
if (sourceAsset.address === NATIVE_TOKEN_ADDRESS) {
|
|
268
|
+
if (!quote.transaction || !quote.transaction.gasPrice || !quote.transaction.gas) {
|
|
269
|
+
throw new Error('0x quote response for native asset swap is missing transaction details (gas/gasPrice).');
|
|
270
|
+
}
|
|
271
|
+
cost = BigInt(quote.sellAmount) + (BigInt(quote.transaction.gasPrice) * BigInt(quote.transaction.gas)); // Correctly reference gasPrice and gas
|
|
272
|
+
sourceBalance = await provider.getBalance(wallet.address);
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
cost = BigInt(quote.sellAmount);
|
|
276
|
+
sourceBalance = await getTokenBalance(provider, wallet.address, sourceAsset.address);
|
|
277
|
+
// Check allowance for the 0x spender contract and approve if necessary
|
|
278
|
+
const allowance = await checkAllowance(provider, wallet.address, sourceAsset.address, quote.allowanceTarget);
|
|
279
|
+
if (allowance < cost) {
|
|
280
|
+
console.log(`Insufficient allowance for ${sourceAsset.symbol}. Approving ${quote.allowanceTarget} now...`);
|
|
281
|
+
const approveTxHash = await api.approve(wallet, sourceAsset.address, quote.allowanceTarget, cost);
|
|
282
|
+
console.log(`Approval transaction sent: ${approveTxHash}. Waiting for confirmation...`);
|
|
283
|
+
const approveReceipt = await provider.waitForTransaction(approveTxHash);
|
|
284
|
+
if (!approveReceipt || approveReceipt.status === 0) {
|
|
285
|
+
throw new Error(`Approval transaction for ${sourceAsset.symbol} failed.`);
|
|
286
|
+
}
|
|
287
|
+
console.log('✅ Approval confirmed.');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (sourceBalance >= cost) {
|
|
291
|
+
console.log(`Found affordable swap: ${sourceAsset.symbol} -> ${targetOption.symbol}.`);
|
|
292
|
+
console.log('--- Full 0x Quote Response ---');
|
|
293
|
+
console.log(JSON.stringify(quote, null, 2));
|
|
294
|
+
console.log('-----------------------------');
|
|
295
|
+
// Execute Swap
|
|
296
|
+
const swapTx = {
|
|
297
|
+
to: quote.transaction.to,
|
|
298
|
+
data: quote.transaction.data,
|
|
299
|
+
value: quote.transaction.value,
|
|
300
|
+
gasPrice: quote.transaction.gasPrice,
|
|
301
|
+
gasLimit: quote.transaction.gas, // Add gasLimit from quote
|
|
302
|
+
nonce: await provider.getTransactionCount(wallet.address, 'latest'),
|
|
303
|
+
chainId: parseInt(chainId),
|
|
304
|
+
};
|
|
305
|
+
console.log("Sending swap transaction...");
|
|
306
|
+
const signedSwapTx = await wallet.signTransaction(swapTx);
|
|
307
|
+
const swapTxHash = await provider.send('eth_sendRawTransaction', [signedSwapTx]);
|
|
308
|
+
console.log(`Swap transaction sent: ${swapTxHash}. Waiting for confirmation...`);
|
|
309
|
+
const swapReceipt = await provider.waitForTransaction(swapTxHash);
|
|
310
|
+
if (!swapReceipt || swapReceipt.status === 0) {
|
|
311
|
+
throw new Error(`Swap transaction failed: ${swapTxHash}`);
|
|
312
|
+
}
|
|
313
|
+
console.log('Swap transaction confirmed.');
|
|
314
|
+
// Execute Payment
|
|
315
|
+
console.log(`Swap successful. Proceeding with final payment.`);
|
|
316
|
+
txHash = await api.buy(recipientAddress, buyAmount, // Use the amount from the swap quote
|
|
317
|
+
userWalletPrivateKey, chain, rpcUrl, targetOption.tokenAddress);
|
|
318
|
+
break; // Exit the source asset loop
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
console.error(`Could not get swap quote for ${sourceAsset.symbol} -> ${targetOption.symbol}:`, error instanceof Error ? error.message : String(error));
|
|
323
|
+
continue; // Try next source asset
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (txHash)
|
|
327
|
+
break; // Exit the target option loop
|
|
328
|
+
}
|
|
329
|
+
if (!txHash) {
|
|
330
|
+
throw new Error("Could not find a valid payment or swap option.");
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
console.error(`Payment transaction sent: ${txHash}. Waiting for confirmation...`);
|
|
334
|
+
const paymentReceipt = await provider.waitForTransaction(txHash);
|
|
335
|
+
if (!paymentReceipt || paymentReceipt.status === 0) {
|
|
336
|
+
throw new Error(`Final payment transaction failed: ${txHash}`);
|
|
337
|
+
}
|
|
338
|
+
console.log('Final payment transaction confirmed.');
|
|
339
|
+
const signature = await wallet.signMessage(challenge);
|
|
340
|
+
const retryOptions = { ...options };
|
|
341
|
+
retryOptions.headers['Authorization'] = `X402 ${txHash}:${signature}`;
|
|
342
|
+
console.error(`Retrying request to ${url} with payment proof.`);
|
|
343
|
+
const finalResponse = await fetch(url, retryOptions);
|
|
344
|
+
if (!finalResponse.ok) {
|
|
345
|
+
const errorBody = await finalResponse.text();
|
|
346
|
+
throw new Error(`API request failed after payment: ${errorBody}`);
|
|
347
|
+
}
|
|
348
|
+
return finalResponse.json();
|
|
349
|
+
}
|
|
350
|
+
async function get0xSwapQuote(buyToken, sellToken, buyAmount, buyTokenDecimals, takerAddress, chain, slippagePercentage = '0.01' // Default 1% slippage
|
|
351
|
+
) {
|
|
352
|
+
const apiUrl = `https://api.0x.org`;
|
|
353
|
+
const chainId = chain === 'base' ? '8453' : '1';
|
|
354
|
+
const apiKey = process.env.ZERO_X_API_KEY;
|
|
355
|
+
if (!apiKey) {
|
|
356
|
+
throw new Error('ZERO_X_API_KEY is not set in the .env file. Please get a free key from https://dashboard.0x.org/apps');
|
|
357
|
+
}
|
|
358
|
+
const headers = {
|
|
359
|
+
'0x-api-key': apiKey,
|
|
360
|
+
'0x-version': 'v2'
|
|
361
|
+
};
|
|
362
|
+
// Step 1: Get a spot price by making a "reverse" lookup.
|
|
363
|
+
// We sell a nominal amount of the buyToken to get a price in terms of the sellToken.
|
|
364
|
+
const nominalSellAmount = parseUnits('1', buyTokenDecimals);
|
|
365
|
+
const priceParams = new URLSearchParams({
|
|
366
|
+
chainId: chainId,
|
|
367
|
+
buyToken: sellToken, // Swapped for reverse lookup
|
|
368
|
+
sellToken: buyToken, // Swapped for reverse lookup
|
|
369
|
+
sellAmount: nominalSellAmount.toString(),
|
|
370
|
+
taker: takerAddress,
|
|
371
|
+
}).toString();
|
|
372
|
+
const priceUrl = `${apiUrl}/swap/permit2/price?${priceParams}`;
|
|
373
|
+
console.log(`[sdk] Fetching 0x spot price with reverse lookup: ${priceUrl}`);
|
|
374
|
+
const priceResponse = await fetch(priceUrl, { headers });
|
|
375
|
+
if (!priceResponse.ok) {
|
|
376
|
+
const errorBody = await priceResponse.text();
|
|
377
|
+
console.error(`[sdk] Full 0x API spot price error response:`, errorBody);
|
|
378
|
+
throw new Error(`Failed to get 0x spot price: ${errorBody}`);
|
|
379
|
+
}
|
|
380
|
+
const priceData = (await priceResponse.json());
|
|
381
|
+
if (!priceData.buyAmount || !priceData.sellAmount || BigInt(priceData.sellAmount) === 0n) {
|
|
382
|
+
throw new Error('Could not determine a valid price for the swap. This might be due to low liquidity.');
|
|
383
|
+
}
|
|
384
|
+
// Step 2: Calculate the required sellAmount based on the reverse price quote.
|
|
385
|
+
// The reverse price gives us a ratio of sellToken per buyToken.
|
|
386
|
+
// sellAmount = buyAmount * (priceData.buyAmount / priceData.sellAmount)
|
|
387
|
+
const estimatedSellAmount = (buyAmount * BigInt(priceData.buyAmount)) / BigInt(priceData.sellAmount);
|
|
388
|
+
// Add a slippage buffer to the sell amount to ensure the trade goes through.
|
|
389
|
+
const slippageFactor = 1 + parseFloat(slippagePercentage);
|
|
390
|
+
const finalSellAmount = BigInt(Math.ceil(Number(estimatedSellAmount) * slippageFactor));
|
|
391
|
+
// Step 3: Get the firm quote with the estimated sellAmount.
|
|
392
|
+
const quoteParams = new URLSearchParams({
|
|
393
|
+
chainId: chainId,
|
|
394
|
+
buyToken: buyToken,
|
|
395
|
+
sellToken: sellToken,
|
|
396
|
+
sellAmount: finalSellAmount.toString(),
|
|
397
|
+
taker: takerAddress,
|
|
398
|
+
}).toString();
|
|
399
|
+
const quoteUrl = `${apiUrl}/swap/permit2/quote?${quoteParams}`;
|
|
400
|
+
console.error(`Fetching 0x firm quote: ${quoteUrl}`);
|
|
401
|
+
const response = await fetch(quoteUrl, { headers });
|
|
402
|
+
if (!response.ok) {
|
|
403
|
+
const errorBody = await response.text();
|
|
404
|
+
console.error(`[sdk] Full 0x API quote error response for ${quoteUrl}:`, errorBody);
|
|
405
|
+
throw new Error(`Failed to get 0x quote: ${errorBody}`);
|
|
406
|
+
}
|
|
407
|
+
return response.json();
|
|
408
|
+
}
|
|
409
|
+
// --- New helper to get the USDC price of a token ---
|
|
410
|
+
async function getUsdcPriceForToken(tokenAddress, chain, tokenDecimals) {
|
|
411
|
+
if (tokenAddress.toLowerCase() === CHAIN_CONFIG[chain === 'base' ? '8453' : '1'].USDC.toLowerCase()) {
|
|
412
|
+
return 1.0; // USDC is always 1.0 USD
|
|
413
|
+
}
|
|
414
|
+
const apiUrl = `https://api.0x.org`;
|
|
415
|
+
const chainId = chain === 'base' ? '8453' : '1';
|
|
416
|
+
const apiKey = process.env.ZERO_X_API_KEY;
|
|
417
|
+
if (!apiKey) {
|
|
418
|
+
throw new Error('ZERO_X_API_KEY is not set in the .env file for price lookup.');
|
|
419
|
+
}
|
|
420
|
+
const headers = {
|
|
421
|
+
'0x-api-key': apiKey,
|
|
422
|
+
'0x-version': 'v2'
|
|
423
|
+
};
|
|
424
|
+
const nominalSellAmount = parseUnits('1', tokenDecimals).toString();
|
|
425
|
+
// We're buying USDC by selling one full unit of the token to find its price.
|
|
426
|
+
const priceParams = new URLSearchParams({
|
|
427
|
+
chainId: chainId,
|
|
428
|
+
buyToken: CHAIN_CONFIG[chainId].USDC,
|
|
429
|
+
sellToken: tokenAddress,
|
|
430
|
+
sellAmount: nominalSellAmount,
|
|
431
|
+
}).toString();
|
|
432
|
+
const priceUrl = `${apiUrl}/swap/permit2/price?${priceParams}`;
|
|
433
|
+
console.log(`[sdk] Fetching USDC price for ${tokenAddress} via 0x: ${priceUrl}`);
|
|
434
|
+
const priceResponse = await fetch(priceUrl, { headers });
|
|
435
|
+
if (!priceResponse.ok) {
|
|
436
|
+
const errorBody = await priceResponse.text();
|
|
437
|
+
throw new Error(`Failed to get 0x price for ${tokenAddress}: ${errorBody}`);
|
|
438
|
+
}
|
|
439
|
+
const priceData = (await priceResponse.json());
|
|
440
|
+
if (!priceData.buyAmount) {
|
|
441
|
+
throw new Error(`Invalid price response from 0x API: ${JSON.stringify(priceData)}`);
|
|
442
|
+
}
|
|
443
|
+
// The buyAmount is the amount of USDC (6 decimals) we get for 1 full unit of the sellToken.
|
|
444
|
+
const price = parseFloat(ethers.formatUnits(priceData.buyAmount, 6));
|
|
445
|
+
if (isNaN(price) || price <= 0) {
|
|
446
|
+
throw new Error(`Could not determine a valid price from 0x API response.`);
|
|
447
|
+
}
|
|
448
|
+
return price;
|
|
449
|
+
}
|
|
99
450
|
// --- Helper function for RPC calls (Keep this) ---
|
|
100
451
|
async function sendJsonRpc(rpcUrl, method, params) {
|
|
101
452
|
const response = await fetch(rpcUrl, {
|
|
@@ -123,14 +474,12 @@ async function sendJsonRpc(rpcUrl, method, params) {
|
|
|
123
474
|
class EthereumHandler {
|
|
124
475
|
async buy(walletAddress, amount, userWalletPrivateKey, chain, rpcUrl, tokenAddress) {
|
|
125
476
|
const rpc = rpcUrl || (chain === 'base' ? DEFAULT_BASE_RPC : DEFAULT_ETH_RPC);
|
|
477
|
+
const provider = new JsonRpcProvider(rpc);
|
|
478
|
+
const wallet = new Wallet(userWalletPrivateKey, provider);
|
|
126
479
|
if (!walletAddress || !isAddress(walletAddress)) {
|
|
127
480
|
throw new Error('Invalid recipient wallet address');
|
|
128
481
|
}
|
|
129
|
-
const wallet = new Wallet(userWalletPrivateKey);
|
|
130
|
-
const senderAddress = wallet.address;
|
|
131
482
|
try {
|
|
132
|
-
const nonce = await sendJsonRpc(rpc, 'eth_getTransactionCount', [senderAddress, 'latest']);
|
|
133
|
-
const feeData = await sendJsonRpc(rpc, 'eth_gasPrice', []);
|
|
134
483
|
let txRequest;
|
|
135
484
|
if (tokenAddress) {
|
|
136
485
|
if (!isAddress(tokenAddress)) {
|
|
@@ -141,27 +490,21 @@ class EthereumHandler {
|
|
|
141
490
|
const data = iface.encodeFunctionData("transfer", [walletAddress, amount]);
|
|
142
491
|
txRequest = {
|
|
143
492
|
to: tokenAddress,
|
|
144
|
-
nonce: parseInt(nonce, 16),
|
|
145
|
-
gasPrice: feeData,
|
|
146
|
-
gasLimit: 100000,
|
|
147
493
|
data: data,
|
|
148
|
-
|
|
494
|
+
value: 0
|
|
149
495
|
};
|
|
150
496
|
}
|
|
151
497
|
else {
|
|
152
498
|
txRequest = {
|
|
153
499
|
to: walletAddress,
|
|
154
500
|
value: amount,
|
|
155
|
-
nonce: parseInt(nonce, 16),
|
|
156
|
-
gasPrice: feeData,
|
|
157
|
-
gasLimit: 21000,
|
|
158
|
-
chainId: (await sendJsonRpc(rpc, 'eth_chainId', [])),
|
|
159
501
|
};
|
|
160
502
|
}
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
503
|
+
const txResponse = await wallet.sendTransaction(txRequest);
|
|
504
|
+
console.error(`Transaction sent: ${txResponse.hash}. Waiting for confirmation...`);
|
|
505
|
+
await txResponse.wait();
|
|
506
|
+
console.error(`Transaction confirmed: ${txResponse.hash}`);
|
|
507
|
+
return txResponse.hash;
|
|
165
508
|
}
|
|
166
509
|
catch (error) {
|
|
167
510
|
console.error('Detailed ETH error:', error);
|
|
@@ -198,6 +541,35 @@ class ApiNow {
|
|
|
198
541
|
}
|
|
199
542
|
return handler.buy(walletAddress, amount, userWalletPrivateKey, chain, rpcUrl, tokenAddress);
|
|
200
543
|
}
|
|
544
|
+
async execute(endpoint, userWalletPrivateKey, opts = {}, paymentConfig = {}) {
|
|
545
|
+
console.error(`Executing request for endpoint: ${endpoint}`);
|
|
546
|
+
const url = new URL(endpoint);
|
|
547
|
+
const method = (opts.method || 'GET').toUpperCase();
|
|
548
|
+
const fetchOptions = {
|
|
549
|
+
method: method,
|
|
550
|
+
headers: {
|
|
551
|
+
'Content-Type': 'application/json',
|
|
552
|
+
'Accept': '*/*',
|
|
553
|
+
},
|
|
554
|
+
};
|
|
555
|
+
if (opts.data) {
|
|
556
|
+
if (method === 'GET' || method === 'HEAD') {
|
|
557
|
+
const params = new URLSearchParams(opts.data);
|
|
558
|
+
params.forEach((value, key) => url.searchParams.append(key, value));
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
fetchOptions.body = JSON.stringify(opts.data);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return fetchWithX402(url.toString(), fetchOptions, this, userWalletPrivateKey, paymentConfig);
|
|
565
|
+
}
|
|
566
|
+
async approve(wallet, tokenAddress, spenderAddress, amount) {
|
|
567
|
+
const abi = ["function approve(address spender, uint256 amount)"];
|
|
568
|
+
const contract = new Contract(tokenAddress, abi, wallet);
|
|
569
|
+
const tx = await contract.approve(spenderAddress, amount);
|
|
570
|
+
await tx.wait();
|
|
571
|
+
return tx.hash;
|
|
572
|
+
}
|
|
201
573
|
async txResponse(endpoint, txHash, opts = {}) {
|
|
202
574
|
console.error(`txResponse: Called with endpoint: ${endpoint}, txHash: ${txHash}, opts:`, JSON.stringify(opts, null, 2));
|
|
203
575
|
if (!endpoint || typeof endpoint !== 'string') {
|
|
@@ -225,6 +597,10 @@ class ApiNow {
|
|
|
225
597
|
},
|
|
226
598
|
// body is set conditionally below
|
|
227
599
|
};
|
|
600
|
+
if (opts.signature) {
|
|
601
|
+
fetchOptions.headers['X-Signature'] = opts.signature;
|
|
602
|
+
console.error(`txResponse: Included signature in X-Signature header.`);
|
|
603
|
+
}
|
|
228
604
|
if (method === 'GET' || method === 'HEAD') {
|
|
229
605
|
// --- GET/HEAD: Append data as query params ---
|
|
230
606
|
if (opts.data && typeof opts.data === 'object' && Object.keys(opts.data).length > 0) {
|
|
@@ -290,6 +666,9 @@ class ApiNow {
|
|
|
290
666
|
console.error(`Attempting payment: Chain=${chain}, To=${walletAddress}, Amount=${amountBigInt.toString()}, Token=${tokenAddress || 'Native'}`);
|
|
291
667
|
const txHash = await this.buy(walletAddress, amountBigInt, userWalletPrivateKey, chain, rpcUrl, tokenAddress, opts.fastMode);
|
|
292
668
|
console.error(`Transaction sent: ${txHash}`);
|
|
669
|
+
const wallet = new Wallet(userWalletPrivateKey);
|
|
670
|
+
const signature = await wallet.signMessage(txHash);
|
|
671
|
+
console.error(`Generated signature for txHash ${txHash}: ${signature}`);
|
|
293
672
|
if (!opts.fastMode) {
|
|
294
673
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
295
674
|
}
|
|
@@ -300,7 +679,8 @@ class ApiNow {
|
|
|
300
679
|
// Create specific options for txResponse
|
|
301
680
|
const txResponseOpts = {
|
|
302
681
|
method: info.httpMethod || 'POST', // Use the method from info, default to POST
|
|
303
|
-
data: opts.data // Pass the original data payload intended for the API
|
|
682
|
+
data: opts.data, // Pass the original data payload intended for the API
|
|
683
|
+
signature: signature
|
|
304
684
|
};
|
|
305
685
|
// Call txResponse with the tailored options
|
|
306
686
|
return this.txResponse(endpoint, txHash, txResponseOpts);
|
|
@@ -308,3 +688,4 @@ class ApiNow {
|
|
|
308
688
|
}
|
|
309
689
|
const apiNow = new ApiNow();
|
|
310
690
|
export default apiNow;
|
|
691
|
+
export { get0xSwapQuote }; // Export for testing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apinow-sdk",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.4",
|
|
4
4
|
"description": "ApiNow SDK · The endpoint vending machine",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
"apinow-sdk": "^0.11.19",
|
|
25
25
|
"body-parser": "^2.2.0",
|
|
26
26
|
"ethers": "^6.13.5",
|
|
27
|
-
"express": "^5.1.0",
|
|
28
27
|
"node-fetch": "^3.3.2"
|
|
29
28
|
},
|
|
30
29
|
"devDependencies": {
|