@tanakayuto/intmax402-client 0.1.2 → 0.2.1

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/dist/client.d.ts CHANGED
@@ -1,22 +1,29 @@
1
+ import { Token } from "intmax2-server-sdk";
1
2
  export interface INTMAX402ClientOptions {
2
3
  privateKey: string;
3
4
  environment?: "mainnet" | "testnet";
5
+ l1RpcUrl?: string;
4
6
  }
5
7
  export declare class INTMAX402Client {
6
8
  private wallet;
7
9
  private environment;
8
10
  private initialized;
11
+ private intmaxClient;
9
12
  constructor(options: INTMAX402ClientOptions);
10
13
  init(): Promise<void>;
14
+ initPayment(l1RpcUrl?: string): Promise<void>;
11
15
  getAddress(): string;
16
+ getIntMaxAddress(): string;
12
17
  getRpcUrl(): string;
13
- /**
14
- * Sign a nonce using Ethereum personal_sign (compatible with ethers.verifyMessage).
15
- */
16
18
  sign(nonce: string): Promise<string>;
19
+ sendPayment(recipientAddress: string, amount: string, token: Token): Promise<{
20
+ txTreeRoot: string;
21
+ transferDigest: string;
22
+ }>;
17
23
  /**
18
24
  * Fetch a resource, automatically handling 401/402 INTMAX402 challenges.
19
- * Flow: GET 401/402 + nonce sign GET + Authorization 200
25
+ * For identity mode: GET -> 401 + nonce -> sign -> GET + Authorization -> 200
26
+ * For payment mode: GET -> 402 + nonce + payment info -> pay + sign -> GET + Authorization (with txHash) -> 200
20
27
  */
21
28
  fetch(url: string, options?: RequestInit): Promise<Response>;
22
29
  }
package/dist/client.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.INTMAX402Client = void 0;
4
4
  const ethers_1 = require("ethers");
5
5
  const intmax402_core_1 = require("@tanakayuto/intmax402-core");
6
+ const intmax2_server_sdk_1 = require("intmax2-server-sdk");
6
7
  const RPC_URLS = {
7
8
  mainnet: "https://api.rpc.intmax.io?network=ethereum",
8
9
  testnet: "https://sepolia.gateway.tenderly.co",
@@ -11,29 +12,58 @@ class INTMAX402Client {
11
12
  wallet;
12
13
  environment;
13
14
  initialized = false;
15
+ intmaxClient = null;
14
16
  constructor(options) {
17
+ // Validate private key before creating wallet
18
+ if (!ethers_1.ethers.isHexString(options.privateKey, 32)) {
19
+ throw new Error("Invalid private key: must be a 32-byte hex string (0x-prefixed)");
20
+ }
15
21
  this.wallet = new ethers_1.ethers.Wallet(options.privateKey);
16
22
  this.environment = options.environment || "testnet";
17
23
  }
18
24
  async init() {
19
- // In production: IntMaxNodeClient.login() (~7s one-time)
25
+ this.initialized = true;
26
+ }
27
+ async initPayment(l1RpcUrl) {
28
+ this.intmaxClient = new intmax2_server_sdk_1.IntMaxNodeClient({
29
+ environment: this.environment,
30
+ eth_private_key: this.wallet.privateKey,
31
+ l1_rpc_url: l1RpcUrl || RPC_URLS[this.environment],
32
+ loggerLevel: "warn",
33
+ });
34
+ await this.intmaxClient.login();
20
35
  this.initialized = true;
21
36
  }
22
37
  getAddress() {
23
38
  return this.wallet.address;
24
39
  }
40
+ getIntMaxAddress() {
41
+ if (!this.intmaxClient)
42
+ throw new Error("Call initPayment() first");
43
+ return this.intmaxClient.address;
44
+ }
25
45
  getRpcUrl() {
26
46
  return RPC_URLS[this.environment];
27
47
  }
28
- /**
29
- * Sign a nonce using Ethereum personal_sign (compatible with ethers.verifyMessage).
30
- */
31
48
  async sign(nonce) {
32
49
  return await this.wallet.signMessage(nonce);
33
50
  }
51
+ async sendPayment(recipientAddress, amount, token) {
52
+ if (!this.intmaxClient) {
53
+ throw new Error("Call initPayment() before sending payments");
54
+ }
55
+ const result = await this.intmaxClient.broadcastTransaction([
56
+ { address: recipientAddress, amount, token },
57
+ ]);
58
+ return {
59
+ txTreeRoot: result.txTreeRoot,
60
+ transferDigest: result.transferDigests[0],
61
+ };
62
+ }
34
63
  /**
35
64
  * Fetch a resource, automatically handling 401/402 INTMAX402 challenges.
36
- * Flow: GET 401/402 + nonce sign GET + Authorization 200
65
+ * For identity mode: GET -> 401 + nonce -> sign -> GET + Authorization -> 200
66
+ * For payment mode: GET -> 402 + nonce + payment info -> pay + sign -> GET + Authorization (with txHash) -> 200
37
67
  */
38
68
  async fetch(url, options) {
39
69
  const initialResponse = await globalThis.fetch(url, options);
@@ -46,11 +76,33 @@ class INTMAX402Client {
46
76
  const challenge = (0, intmax402_core_1.parseWWWAuthenticate)(wwwAuth);
47
77
  if (!challenge)
48
78
  return initialResponse;
49
- // Sign the nonce
50
79
  const signature = await this.sign(challenge.nonce);
51
- // Build authorization header
52
- const authHeader = `INTMAX402 address="${this.wallet.address}", nonce="${challenge.nonce}", signature="${signature}"`;
53
- // Retry with credentials
80
+ let authHeader = `INTMAX402 address="${this.wallet.address}", nonce="${challenge.nonce}", signature="${signature}"`;
81
+ // Payment mode: send payment and include txHash
82
+ if (challenge.mode === "payment") {
83
+ if (!this.intmaxClient) {
84
+ throw new Error("Payment required but intmax client not initialized. Call initPayment() first.");
85
+ }
86
+ if (!challenge.serverAddress || !challenge.amount) {
87
+ throw new Error("Server did not provide serverAddress or amount in challenge");
88
+ }
89
+ // Get token list and find matching token
90
+ const tokens = await this.intmaxClient.getTokensList();
91
+ let paymentToken;
92
+ if (challenge.tokenAddress) {
93
+ const found = tokens.find((t) => t.contractAddress.toLowerCase() === challenge.tokenAddress.toLowerCase());
94
+ if (!found) {
95
+ throw new Error(`Token ${challenge.tokenAddress} not found in token list`);
96
+ }
97
+ paymentToken = found;
98
+ }
99
+ else {
100
+ // Default: native token (index 0)
101
+ paymentToken = tokens.find((t) => t.tokenIndex === 0) || tokens[0];
102
+ }
103
+ const { transferDigest } = await this.sendPayment(challenge.serverAddress, challenge.amount, paymentToken);
104
+ authHeader += `, txHash="${transferDigest}"`;
105
+ }
54
106
  return globalThis.fetch(url, {
55
107
  ...options,
56
108
  headers: {
package/package.json CHANGED
@@ -1,16 +1,17 @@
1
1
  {
2
2
  "name": "@tanakayuto/intmax402-client",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
7
- "@tanakayuto/intmax402-core": "0.1.2",
8
- "ethers": "^6.0.0"
7
+ "@tanakayuto/intmax402-core": "0.2.0",
8
+ "ethers": "^6.0.0",
9
+ "intmax2-server-sdk": "^1.5.2"
9
10
  },
10
11
  "devDependencies": {
11
- "typescript": "^5.4.0",
12
12
  "@types/node": "^20.0.0",
13
- "ethers": "^6.0.0"
13
+ "ethers": "^6.0.0",
14
+ "typescript": "^5.4.0"
14
15
  },
15
16
  "files": [
16
17
  "dist",