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.
- package/.github/workflows/npm-publish.yml +3 -0
- package/CHANGELOG.md +12 -0
- package/CLAUDE.md +9 -3
- package/README.md +8 -4
- package/dist/sdk.d.ts +15 -1
- package/dist/sdk.js +20 -4
- package/package.json +2 -1
- package/src/sdk.ts +39 -4
- package/test/hash-args.test.mjs +37 -0
- package/test/http-timeout.test.mjs +23 -0
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
|
-
|
|
23
|
-
`npm run build`
|
|
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?)
|
|
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
|
-
|
|
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:
|
|
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({
|
|
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.
|
|
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(
|
|
417
|
-
|
|
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
|
+
});
|