clutch-hub-sdk-js 4.0.0 → 4.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.
@@ -46,6 +46,9 @@ jobs:
46
46
  - name: Build package
47
47
  run: npm run build
48
48
 
49
+ - name: Test (against dist/)
50
+ run: npm test
51
+
49
52
  - name: Run semantic-release
50
53
  id: semantic-release
51
54
  run: |
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [4.1.0](https://github.com/clutchprotocol/clutch-hub-sdk-js/compare/v4.0.0...v4.1.0) (2026-09-10)
2
+
3
+
4
+ ### Features
5
+
6
+ * bound every hub HTTP request with a timeout ([bdd1b3d](https://github.com/clutchprotocol/clutch-hub-sdk-js/commit/bdd1b3dae5f9335c7acd6b0cc2c9bfa3df836809))
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * normalize hash arguments before querying the hub for offers ([1cd0dfd](https://github.com/clutchprotocol/clutch-hub-sdk-js/commit/1cd0dfd63616b3ee7554770ec644cce1c6a711fb))
12
+
1
13
  ## [4.0.0](https://github.com/clutchprotocol/clutch-hub-sdk-js/compare/v3.0.0...v4.0.0) (2026-09-04)
2
14
 
3
15
 
package/CLAUDE.md CHANGED
@@ -19,8 +19,10 @@ Only four source files — the SDK is deliberately small:
19
19
  `Signature`, …).
20
20
  - `src/index.ts` — barrel re-exports. New public symbols must be reachable from here.
21
21
 
22
- There is no test suite. `test_rlp_fix.{js,mjs}` are ad-hoc manual scripts run against `dist/` after
23
- `npm run build` — not wired into CI.
22
+ Tests live in `test/*.test.mjs` and use Node's built-in runner (`npm test` → `node --test test/`),
23
+ no framework. They import from `dist/`, so `npm run build` first; the release workflow runs them
24
+ after the build and before semantic-release. `test_rlp_fix.{js,mjs}` and `test_wire_v3.mjs` at the
25
+ repo root are older ad-hoc manual scripts, not part of `npm test`.
24
26
 
25
27
  ## Transaction Lifecycle (client side)
26
28
 
@@ -44,7 +46,11 @@ legacy JSON-string quoting); empty referrer encodes as `''`.
44
46
 
45
47
  ## Public API Surface (`ClutchHubSdk`)
46
48
 
47
- - **Constructor / identity**: `new ClutchHubSdk(apiUrl, publicKey, privateKey?)`, `getPublicKey()`,
49
+ - **Constructor / identity**: `new ClutchHubSdk(apiUrl, publicKey, privateKey?, chainId?, options?)`
50
+ where `options.timeoutMs` bounds every hub HTTP request (default `DEFAULT_HTTP_TIMEOUT_MS`,
51
+ 30 s; `0` disables). Hash arguments to `listRideOffers`/`subscribeRideOffers` go through
52
+ `normalizeTxHashForQuery` (strip `0x`, lowercase) because the hub matches them as exact
53
+ strings. `getPublicKey()`,
48
54
  `setPrivateKey(privateKey)`, `isAuthenticated()`. The private key (constructor arg or
49
55
  `setPrivateKey`) is required for token issuance — `generateToken` demands a signed
50
56
  proof-of-key-ownership challenge. It is kept in a module-global map keyed by publicKey
package/README.md CHANGED
@@ -25,18 +25,22 @@ import { ClutchHubSdk } from 'clutch-hub-sdk-js';
25
25
 
26
26
  // privateKey is needed for authenticated calls: generateToken requires a signed
27
27
  // proof-of-key-ownership challenge (the key stays local, it is never sent).
28
- const sdk = new ClutchHubSdk('http://localhost:3000', publicKey, privateKey);
28
+ // chainId comes from your own config, never from the hub. The optional fifth argument
29
+ // sets the HTTP timeout (default 30 s; 0 disables it).
30
+ const sdk = new ClutchHubSdk('http://localhost:3000', publicKey, privateKey, 2077, { timeoutMs: 30_000 });
29
31
 
30
- // Create, sign, and submit a ride request
32
+ // Create, sign, and submit a ride request. Amounts are bigint: 1 USD = 1,000,000 CLT.
31
33
  const unsigned = await sdk.createUnsignedRideRequest({
32
34
  pickup: { latitude: 35.7, longitude: 51.4 },
33
35
  dropoff: { latitude: 35.8, longitude: 51.5 },
34
- fare: 1000,
36
+ fare: 5_000_000n,
35
37
  });
36
- const signed = await sdk.signTransaction(unsigned, privateKey);
38
+ const signed = await sdk.signTransaction(unsigned, privateKey, { type: 'RideRequest', fare: 5_000_000n });
37
39
  await sdk.submitTransaction(signed.rawTransaction);
38
40
  ```
39
41
 
42
+ Hash arguments (`listRideOffers`, `subscribeRideOffers`) accept the `0x`-prefixed form that `signTransaction` returns; the SDK normalizes them to the form the hub matches on.
43
+
40
44
  ## Features
41
45
 
42
46
  - Client-side signing (private keys never sent to server)
package/dist/sdk.d.ts CHANGED
@@ -8,6 +8,18 @@ export declare function stripHexPrefix(hex: string): string;
8
8
  * and remove the `0x` prefix.
9
9
  */
10
10
  export declare function normalizeTxHashForRlp(hex: string): string;
11
+ /** Default timeout for every HTTP request the SDK makes to the hub, in milliseconds. */
12
+ export declare const DEFAULT_HTTP_TIMEOUT_MS = 30000;
13
+ /** Optional settings for {@link ClutchHubSdk}. */
14
+ export interface ClutchHubSdkOptions {
15
+ /**
16
+ * HTTP timeout for every hub request (queries, mutations, `generateToken`), in milliseconds.
17
+ * Defaults to {@link DEFAULT_HTTP_TIMEOUT_MS}. `0` disables it, which is what the SDK did
18
+ * before this option existed: a request the hub never answered hung the caller forever.
19
+ * Subscriptions ride on graphql-ws and are unaffected.
20
+ */
21
+ timeoutMs?: number;
22
+ }
11
23
  declare global {
12
24
  interface Window {
13
25
  Buffer: typeof Buffer;
@@ -118,8 +130,10 @@ export declare class ClutchHubSdk {
118
130
  * it is defeats the check chain_id exists to provide. If omitted, `signTransaction` still
119
131
  * verifies every other `expected` field but skips the chain_id pin (nothing was pinned to
120
132
  * check against) rather than failing every real transaction against a phantom "chain 0".
133
+ * @param options Optional settings — see {@link ClutchHubSdkOptions}. Today that is the HTTP
134
+ * timeout (`timeoutMs`, default {@link DEFAULT_HTTP_TIMEOUT_MS}).
121
135
  */
122
- constructor(apiUrl: string, publicKey: string, privateKey?: string, chainId?: number);
136
+ constructor(apiUrl: string, publicKey: string, privateKey?: string, chainId?: number, options?: ClutchHubSdkOptions);
123
137
  /**
124
138
  * Get the current public key associated with this SDK instance.
125
139
  * @returns The public key string
package/dist/sdk.js CHANGED
@@ -30,6 +30,17 @@ export function normalizeTxHashForRlp(hex) {
30
30
  }
31
31
  return stripHexPrefix(s);
32
32
  }
33
+ /**
34
+ * The hub stores transaction hashes as 64 lowercase hex characters with no `0x`, and its
35
+ * hash-taking query arguments (`listRideOffers`, `rideOffersUpdated`) match as exact strings.
36
+ * A hash as returned by `signTransaction` (`0x…`) therefore has to be normalized before it
37
+ * can be queried with, or the hub answers with an empty list.
38
+ */
39
+ function normalizeTxHashForQuery(hex) {
40
+ return normalizeTxHashForRlp(hex).toLowerCase();
41
+ }
42
+ /** Default timeout for every HTTP request the SDK makes to the hub, in milliseconds. */
43
+ export const DEFAULT_HTTP_TIMEOUT_MS = 30000;
33
44
  if (typeof window !== 'undefined' && !window.Buffer) {
34
45
  window.Buffer = Buffer;
35
46
  }
@@ -273,11 +284,16 @@ export class ClutchHubSdk {
273
284
  * it is defeats the check chain_id exists to provide. If omitted, `signTransaction` still
274
285
  * verifies every other `expected` field but skips the chain_id pin (nothing was pinned to
275
286
  * check against) rather than failing every real transaction against a phantom "chain 0".
287
+ * @param options Optional settings — see {@link ClutchHubSdkOptions}. Today that is the HTTP
288
+ * timeout (`timeoutMs`, default {@link DEFAULT_HTTP_TIMEOUT_MS}).
276
289
  */
277
- constructor(apiUrl, publicKey, privateKey, chainId) {
290
+ constructor(apiUrl, publicKey, privateKey, chainId, options = {}) {
278
291
  this.token = null;
279
292
  this.tokenExpireTime = 0;
280
- this.apiClient = axios.create({ baseURL: apiUrl });
293
+ this.apiClient = axios.create({
294
+ baseURL: apiUrl,
295
+ timeout: options.timeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS,
296
+ });
281
297
  this.publicKey = publicKey;
282
298
  this.chainId = chainId ?? 0;
283
299
  this.chainIdConfigured = chainId !== undefined;
@@ -664,7 +680,7 @@ export class ClutchHubSdk {
664
680
  }
665
681
  }
666
682
  `;
667
- return this.subscribeGraphqlListField(query, { rideRequestTxHash }, 'rideOffersUpdated', handlers);
683
+ return this.subscribeGraphqlListField(query, { rideRequestTxHash: normalizeTxHashForQuery(rideRequestTxHash) }, 'rideOffersUpdated', handlers);
668
684
  }
669
685
  /**
670
686
  * Subscribe to active trips, optionally filtered by driver or passenger address.
@@ -745,7 +761,7 @@ export class ClutchHubSdk {
745
761
  }
746
762
  }
747
763
  `;
748
- const result = await this.executeGraphQL(query, { rideRequestTxHash });
764
+ const result = await this.executeGraphQL(query, { rideRequestTxHash: normalizeTxHashForQuery(rideRequestTxHash) });
749
765
  return result.listRideOffers.map((r) => ({ ...r, fare: BigInt(r.fare) }));
750
766
  }
751
767
  /**
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "clutch-hub-sdk-js",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "JavaScript SDK for interacting with the clutch-hub-api",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "scripts": {
9
9
  "build": "tsc",
10
+ "test": "node --test test/",
10
11
  "prepare": "npm run build"
11
12
  },
12
13
  "keywords": [
package/src/sdk.ts CHANGED
@@ -56,6 +56,30 @@ export function normalizeTxHashForRlp(hex: string): string {
56
56
  return stripHexPrefix(s);
57
57
  }
58
58
 
59
+ /**
60
+ * The hub stores transaction hashes as 64 lowercase hex characters with no `0x`, and its
61
+ * hash-taking query arguments (`listRideOffers`, `rideOffersUpdated`) match as exact strings.
62
+ * A hash as returned by `signTransaction` (`0x…`) therefore has to be normalized before it
63
+ * can be queried with, or the hub answers with an empty list.
64
+ */
65
+ function normalizeTxHashForQuery(hex: string): string {
66
+ return normalizeTxHashForRlp(hex).toLowerCase();
67
+ }
68
+
69
+ /** Default timeout for every HTTP request the SDK makes to the hub, in milliseconds. */
70
+ export const DEFAULT_HTTP_TIMEOUT_MS = 30_000;
71
+
72
+ /** Optional settings for {@link ClutchHubSdk}. */
73
+ export interface ClutchHubSdkOptions {
74
+ /**
75
+ * HTTP timeout for every hub request (queries, mutations, `generateToken`), in milliseconds.
76
+ * Defaults to {@link DEFAULT_HTTP_TIMEOUT_MS}. `0` disables it, which is what the SDK did
77
+ * before this option existed: a request the hub never answered hung the caller forever.
78
+ * Subscriptions ride on graphql-ws and are unaffected.
79
+ */
80
+ timeoutMs?: number;
81
+ }
82
+
59
83
  // Expose Buffer to browser contexts
60
84
  declare global {
61
85
  interface Window { Buffer: typeof Buffer }
@@ -412,9 +436,20 @@ export class ClutchHubSdk {
412
436
  * it is defeats the check chain_id exists to provide. If omitted, `signTransaction` still
413
437
  * verifies every other `expected` field but skips the chain_id pin (nothing was pinned to
414
438
  * check against) rather than failing every real transaction against a phantom "chain 0".
439
+ * @param options Optional settings — see {@link ClutchHubSdkOptions}. Today that is the HTTP
440
+ * timeout (`timeoutMs`, default {@link DEFAULT_HTTP_TIMEOUT_MS}).
415
441
  */
416
- constructor(apiUrl: string, publicKey: string, privateKey?: string, chainId?: number) {
417
- this.apiClient = axios.create({ baseURL: apiUrl });
442
+ constructor(
443
+ apiUrl: string,
444
+ publicKey: string,
445
+ privateKey?: string,
446
+ chainId?: number,
447
+ options: ClutchHubSdkOptions = {}
448
+ ) {
449
+ this.apiClient = axios.create({
450
+ baseURL: apiUrl,
451
+ timeout: options.timeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS,
452
+ });
418
453
  this.publicKey = publicKey;
419
454
  this.chainId = chainId ?? 0;
420
455
  this.chainIdConfigured = chainId !== undefined;
@@ -883,7 +918,7 @@ export class ClutchHubSdk {
883
918
  `;
884
919
  return this.subscribeGraphqlListField<AvailableRideOffer>(
885
920
  query,
886
- { rideRequestTxHash },
921
+ { rideRequestTxHash: normalizeTxHashForQuery(rideRequestTxHash) },
887
922
  'rideOffersUpdated',
888
923
  handlers
889
924
  );
@@ -1000,7 +1035,7 @@ export class ClutchHubSdk {
1000
1035
  `;
1001
1036
  const result = await this.executeGraphQL<{
1002
1037
  listRideOffers: (Omit<AvailableRideOffer, 'fare'> & { fare: string })[];
1003
- }>(query, { rideRequestTxHash });
1038
+ }>(query, { rideRequestTxHash: normalizeTxHashForQuery(rideRequestTxHash) });
1004
1039
  return result.listRideOffers.map((r) => ({ ...r, fare: BigInt(r.fare) }));
1005
1040
  }
1006
1041
 
@@ -0,0 +1,37 @@
1
+ // Runs against dist/ — `npm run build` first. `node --test test/` (Node >= 20, no framework).
2
+ import test from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+ import { ClutchHubSdk } from '../dist/index.js';
5
+
6
+ const HASH = 'ae174ce0588b221ed24d685518d486513187ab5ee649347062e75066a2aa9a37';
7
+
8
+ // The hub matches hash arguments as exact strings, without 0x and in lowercase. The SDK must
9
+ // send that form whatever the caller passed, or the hub answers with an empty list.
10
+ function sdkWithRecordingPost() {
11
+ const sdk = new ClutchHubSdk('http://hub.test', '0x4196c526e2bb5dd02c2e2613b43291b0736afc47');
12
+ const sent = [];
13
+ sdk.apiClient.post = async (_url, body) => {
14
+ sent.push(body);
15
+ return { data: { data: { listRideOffers: [{ txHash: 'ff', rideRequestTxHash: HASH, fare: '5000000', driverAddress: '0x1' }] } } };
16
+ };
17
+ return { sdk, sent };
18
+ }
19
+
20
+ test('listRideOffers strips a 0x prefix before querying the hub', async () => {
21
+ const { sdk, sent } = sdkWithRecordingPost();
22
+ const offers = await sdk.listRideOffers('0x' + HASH);
23
+ assert.equal(sent[0].variables.rideRequestTxHash, HASH);
24
+ assert.equal(offers[0].fare, 5000000n);
25
+ });
26
+
27
+ test('listRideOffers lowercases and unquotes a legacy JSON-wrapped hash', async () => {
28
+ const { sdk, sent } = sdkWithRecordingPost();
29
+ await sdk.listRideOffers(JSON.stringify('0X' + HASH.toUpperCase()));
30
+ assert.equal(sent[0].variables.rideRequestTxHash, HASH);
31
+ });
32
+
33
+ test('listRideOffers passes an already-normalized hash through unchanged', async () => {
34
+ const { sdk, sent } = sdkWithRecordingPost();
35
+ await sdk.listRideOffers(HASH);
36
+ assert.equal(sent[0].variables.rideRequestTxHash, HASH);
37
+ });
@@ -0,0 +1,23 @@
1
+ // Runs against dist/ — `npm run build` first. `node --test test/` (Node >= 20, no framework).
2
+ import test from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+ import { ClutchHubSdk, DEFAULT_HTTP_TIMEOUT_MS } from '../dist/index.js';
5
+
6
+ const PK = '0x4196c526e2bb5dd02c2e2613b43291b0736afc47';
7
+
8
+ test('the hub HTTP client has a timeout by default', () => {
9
+ const sdk = new ClutchHubSdk('http://hub.test', PK);
10
+ assert.equal(DEFAULT_HTTP_TIMEOUT_MS, 30_000);
11
+ assert.equal(sdk.apiClient.defaults.timeout, DEFAULT_HTTP_TIMEOUT_MS);
12
+ });
13
+
14
+ test('timeoutMs overrides the default, and 0 disables it', () => {
15
+ assert.equal(new ClutchHubSdk('http://hub.test', PK, undefined, 2077, { timeoutMs: 5_000 }).apiClient.defaults.timeout, 5_000);
16
+ assert.equal(new ClutchHubSdk('http://hub.test', PK, undefined, 2077, { timeoutMs: 0 }).apiClient.defaults.timeout, 0);
17
+ });
18
+
19
+ test('the four-argument constructor still works unchanged', () => {
20
+ const sdk = new ClutchHubSdk('http://hub.test', PK, undefined, 2077);
21
+ assert.equal(sdk.getPublicKey(), PK);
22
+ assert.equal(sdk.apiClient.defaults.timeout, DEFAULT_HTTP_TIMEOUT_MS);
23
+ });