clutch-hub-sdk-js 4.2.1 → 4.4.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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [4.4.0](https://github.com/clutchprotocol/clutch-hub/compare/v4.3.0...v4.4.0) (2026-10-06)
2
+
3
+
4
+ ### Features
5
+
6
+ * **sdk:** sign with TronLink (signMessageV2, TIP-191), and the demo app connects it ([#32](https://github.com/clutchprotocol/clutch-hub/issues/32)) ([57fdd06](https://github.com/clutchprotocol/clutch-hub/commit/57fdd06297ff0b1f434d4b27d4941777772baeec))
7
+
8
+ ## [4.3.0](https://github.com/clutchprotocol/clutch-hub/compare/v4.2.1...v4.3.0) (2026-10-06)
9
+
10
+
11
+ ### Features
12
+
13
+ * **sdk:** sign with a wallet (MetaMask, Trust Wallet); the demo app holds no key ([#30](https://github.com/clutchprotocol/clutch-hub/issues/30)) ([db41df0](https://github.com/clutchprotocol/clutch-hub/commit/db41df077f19340afe72fac314f36e7773486f58))
14
+
1
15
  ## [4.2.1](https://github.com/clutchprotocol/clutch-hub-sdk-js/compare/v4.2.0...v4.2.1) (2026-09-18)
2
16
 
3
17
 
package/README.md CHANGED
@@ -41,9 +41,26 @@ await sdk.submitTransaction(signed.rawTransaction);
41
41
 
42
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
43
 
44
+ ## Wallets: MetaMask, Trust Wallet, TronLink
45
+
46
+ A wallet keeps the key and signs a short text. MetaMask and Trust Wallet sign it with `personal_sign` (EIP-191). TronLink signs it with `signMessageV2` (TIP-191), which is the same with the prefix `\x19TRON Signed Message:\n`. A TronLink account is the same kind of key as a Clutch account, so its `T…` address is the Clutch address `0x…` of the same key. Use a signer where you used a key:
47
+
48
+ ```javascript
49
+ import { ClutchHubSdk, discoverInjectedWallets, connectWallet } from 'clutch-hub-sdk-js';
50
+
51
+ const [wallet] = await discoverInjectedWallets(); // EIP-6963 and TIP-6963 (TronLink), then window.ethereum / window.tron
52
+ const signer = await connectWallet(wallet); // the wallet asks the user to share an account
53
+
54
+ const sdk = new ClutchHubSdk('http://localhost:3000', signer.address, signer, 2077);
55
+ // ... create the unsigned transaction as above ...
56
+ const signed = await sdk.signTransaction(unsigned, signer, { type: 'RideRequest', fare: 5_000_000n });
57
+ ```
58
+
59
+ Each `signTransaction` and each login opens a prompt in the wallet. The text the wallet shows is `clutch-tx:{chainId}:{hash}` for a transaction and `clutch-auth:{chainId}:{address}:{timestamp}` for the login. The node and the Hub API accept this signature next to the signature of a private key. `wallet.kind` is `'evm'` (MetaMask, Trust Wallet) or `'tron'` (TronLink), and `connectWallet`, `createSignerFor(wallet, account)`, `sharedWalletAccount(wallet)` (the account a wallet already shares, with no prompt) and `watchWalletAccounts(wallet, listener)` work for both. `createWalletSigner(provider, address)` and `createTronLinkSigner(provider, address)` build a signer for a provider you already have, and `createLocalSigner(privateKey)` wraps a key. A user who says no in a wallet gives a rejection (MetaMask and Trust Wallet: `code: 4001`; TronLink: an error with the message `user rejected request`).
60
+
44
61
  ## Features
45
62
 
46
- - Client-side signing (private keys never sent to server)
63
+ - Client-side signing (private keys never sent to server), or a wallet that keeps the key (MetaMask, Trust Wallet, TronLink)
47
64
  - Full ride lifecycle: request, offer, accept, pay, cancel
48
65
  - GraphQL queries and WebSocket subscriptions
49
66
  - TypeScript types
@@ -52,7 +69,8 @@ Hash arguments (`listRideOffers`, `subscribeRideOffers`) accept the `0x`-prefixe
52
69
 
53
70
  | Category | Methods |
54
71
  |----------|---------|
55
- | Auth | Auto `generateToken` via `ensureAuth()` (signed challenge; needs the private key), `setPrivateKey`, `signAuthChallenge` |
72
+ | Auth | Auto `generateToken` via `ensureAuth()` (signed challenge; needs a private key or a signer), `setPrivateKey`, `setSigner`, `signAuthChallenge` |
73
+ | Signers | `createLocalSigner`, `createWalletSigner`, `createTronLinkSigner`, `createSignerFor`, `discoverInjectedWallets`, `connectWallet`, `sharedWalletAccount`, `watchWalletAccounts`, `tronAddressToHex`, `addressFromPrivateKey` |
56
74
  | Write | `createUnsignedRide*`, `signTransaction`, `submitTransaction` |
57
75
  | Read | `listRideRequests`, `listRideOffers`, `listActiveTrips`, `getAccountBalance`, … |
58
76
  | Live | `subscribeRideRequests`, `subscribeRideOffers`, `subscribeActiveTrips`, … |
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './types.js';
2
2
  export * from './sdk.js';
3
+ export * from './signers.js';
3
4
  export { hubGraphqlWsUrl, RIDE_REQUEST_GQL_FIELDS, RIDE_OFFER_GQL_FIELDS, ACTIVE_TRIP_GQL_FIELDS, RECENT_TRIP_GQL_FIELDS, createHubSubscriptionClient, } from './subscriptions.js';
4
5
  export type { SubscriptionHandlers } from './subscriptions.js';
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './types.js';
2
2
  export * from './sdk.js';
3
+ export * from './signers.js';
3
4
  export { hubGraphqlWsUrl, RIDE_REQUEST_GQL_FIELDS, RIDE_OFFER_GQL_FIELDS, ACTIVE_TRIP_GQL_FIELDS, RECENT_TRIP_GQL_FIELDS, createHubSubscriptionClient, } from './subscriptions.js';
package/dist/sdk.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Buffer } from 'buffer';
2
2
  import { type SubscriptionHandlers } from './subscriptions.js';
3
3
  import { AvailableRideRequest, AvailableRideOffer, AvailableActiveTrip, AvailableCompletedTrip, AvailableRecentTrip, BurnArgs, MapBounds, RideRequestArgs, RideOfferArgs, RideAcceptanceArgs, RidePayArgs, RideCancelArgs, RideRequestCancelArgs, Signature } from './types.js';
4
+ import type { Signer } from './signers.js';
4
5
  /** Strip 0x/0X prefix - hex parsers (e.g. @noble/secp256k1) do not accept it. Exported for consumers. */
5
6
  export declare function stripHexPrefix(hex: string): string;
6
7
  /**
@@ -46,6 +47,13 @@ export declare function authChallengeHashHex(chainId: number, publicKey: string,
46
47
  * @param timestamp Unix seconds; the Hub API rejects timestamps more than ±120s from server time.
47
48
  */
48
49
  export declare function signAuthChallenge(chainId: number, publicKey: string, timestamp: number, privateKey: string): Promise<Signature>;
50
+ /** The address of a private key: `0x` and 40 lowercase hex characters. */
51
+ export declare function addressFromPrivateKey(privateKey: string): string;
52
+ /**
53
+ * A signer for a private key held in memory. It signs the hash string, as the SDK always has.
54
+ * A wallet cannot hand over its key: use `createWalletSigner` for that.
55
+ */
56
+ export declare function createLocalSigner(privateKey: string): Signer;
49
57
  /**
50
58
  * Represents an unsigned transaction returned by the GraphQL API.
51
59
  */
@@ -120,9 +128,10 @@ export declare class ClutchHubSdk {
120
128
  /**
121
129
  * @param apiUrl Hub API base URL.
122
130
  * @param publicKey Wallet address (0x + 40 hex) or uncompressed public key (130 hex).
123
- * @param privateKey Optional wallet private key, required to obtain JWTs: `generateToken`
124
- * demands a signed proof-of-key-ownership challenge. May also be provided later via
125
- * {@link setPrivateKey}. Never sent to the API — only used for local signing.
131
+ * @param privateKey Optional private key, or a {@link Signer} (for example a wallet's, see
132
+ * `createWalletSigner`), required to obtain JWTs: `generateToken` demands a signed
133
+ * proof-of-key-ownership challenge. May also be provided later via {@link setPrivateKey}
134
+ * or {@link setSigner}. A key is never sent to the API — only used for local signing.
126
135
  * @param chainId This chain's id (e.g. 2077 for the app's own config), used for the
127
136
  * chain-bound auth challenge and as the default `expected.chainId` pin in
128
137
  * {@link signTransaction}'s `verifyUnsignedTransaction` check. Get this from app config,
@@ -133,24 +142,33 @@ export declare class ClutchHubSdk {
133
142
  * @param options Optional settings — see {@link ClutchHubSdkOptions}. Today that is the HTTP
134
143
  * timeout (`timeoutMs`, default {@link DEFAULT_HTTP_TIMEOUT_MS}).
135
144
  */
136
- constructor(apiUrl: string, publicKey: string, privateKey?: string, chainId?: number, options?: ClutchHubSdkOptions);
145
+ constructor(apiUrl: string, publicKey: string, privateKey?: string | Signer, chainId?: number, options?: ClutchHubSdkOptions);
137
146
  /**
138
147
  * Get the current public key associated with this SDK instance.
139
148
  * @returns The public key string
140
149
  */
141
150
  getPublicKey(): string;
142
151
  /**
143
- * Provide (or replace) the private key used to sign `generateToken` auth challenges for
144
- * this SDK's public key. Stored in a module-global map keyed by publicKey — like the JWT
145
- * cache — so every SDK instance and shared WebSocket connection for this wallet can
146
- * authenticate. In-memory only; never sent to the API.
152
+ * Provide (or replace) the private key — or the {@link Signer} — used to sign `generateToken`
153
+ * auth challenges for this SDK's public key. Stored in a module-global map keyed by
154
+ * publicKey — like the JWT cache — so every SDK instance and shared WebSocket connection for
155
+ * this wallet can authenticate. In-memory only; a key is never sent to the API.
147
156
  */
148
- setPrivateKey(privateKey: string): void;
157
+ setPrivateKey(privateKey: string | Signer): void;
158
+ /** Same as {@link setPrivateKey}, for a signer such as a wallet's (`createWalletSigner`). */
159
+ setSigner(signer: Signer): void;
149
160
  /**
150
161
  * Check if the SDK is currently authenticated.
151
162
  * @returns True if authenticated and token is not expired
152
163
  */
153
164
  isAuthenticated(): boolean;
165
+ /**
166
+ * True when a token for this account is cached and still good, so the next authenticated call
167
+ * opens no sign-in prompt. `isAuthenticated` looks at this instance only; this looks at the
168
+ * cache that every instance of the account shares. A poll should check it first: a wallet's
169
+ * sign-in prompt belongs to something the user did, never to a timer.
170
+ */
171
+ hasValidToken(): boolean;
154
172
  private get authHeaders();
155
173
  /**
156
174
  * WebSocket URL for GraphQL subscriptions (same host as REST/GraphQL HTTP).
@@ -161,6 +179,15 @@ export declare class ClutchHubSdk {
161
179
  * Call `release` when unsubscribing; last release disposes the socket.
162
180
  */
163
181
  private acquireGraphqlWsClient;
182
+ /**
183
+ * The `connection_init` payload of the shared socket: a token when there is one to send.
184
+ *
185
+ * The subscriptions are public, so a token is optional here. This runs at every connect and
186
+ * every reconnect (the socket retries for ever), with nobody watching, so a signer that opens a
187
+ * prompt (a wallet) is not asked for a signature: a token from an earlier, explicit action is
188
+ * still sent. A key signs silently and is asked as before.
189
+ */
190
+ private wsConnectionParams;
164
191
  /**
165
192
  * Shared graphql-ws list subscription: one multiplexed client via {@link acquireGraphqlWsClient}.
166
193
  */
@@ -208,8 +235,12 @@ export declare class ClutchHubSdk {
208
235
  createUnsignedBurn(args: BurnArgs): Promise<UnsignedTransaction>;
209
236
  /**
210
237
  * Signs a transaction and returns the signature and raw RLP-encoded payload.
238
+ *
239
+ * `privateKey` is a private key, or a {@link Signer}. A key signs the hash string. A wallet's
240
+ * signer asks the wallet to sign `clutch-tx:{chainId}:{hash}` with `personal_sign`: the user
241
+ * sees a prompt, so call this only where that is expected.
211
242
  */
212
- signTransaction(unsignedTx: UnsignedTransaction, privateKey: string, expected?: ExpectedTx): Promise<Signature & {
243
+ signTransaction(unsignedTx: UnsignedTransaction, privateKey: string | Signer, expected?: ExpectedTx): Promise<Signature & {
213
244
  rawTransaction: string;
214
245
  txHash: string;
215
246
  }>;
package/dist/sdk.js CHANGED
@@ -54,12 +54,13 @@ const globalTokenCache = new Map();
54
54
  */
55
55
  const inFlightTokenRequests = new Map();
56
56
  /**
57
- * Module-global private-key store keyed by `publicKey` (parallel to the JWT cache).
57
+ * Module-global signer store keyed by `publicKey` (parallel to the JWT cache).
58
58
  * `generateToken` requires proof of key ownership (a signed challenge), so token issuance
59
- * needs the wallet's private key. Keys are kept in memory only and are **never** sent to
60
- * the Hub API — only the challenge signature is.
59
+ * needs a signer for the account: a private key held in memory (`createLocalSigner`) or a
60
+ * wallet that signs for it (`createWalletSigner`). A key is kept in memory only and is
61
+ * **never** sent to the Hub API — only the challenge signature is.
61
62
  */
62
- const globalPrivateKeys = new Map();
63
+ const globalSigners = new Map();
63
64
  /** Prefix of the canonical proof-of-key-ownership message signed for `generateToken`. */
64
65
  export const AUTH_CHALLENGE_PREFIX = 'clutch-auth';
65
66
  /**
@@ -106,6 +107,30 @@ async function signHashHex(hashHex, privateKey) {
106
107
  export async function signAuthChallenge(chainId, publicKey, timestamp, privateKey) {
107
108
  return signHashHex(authChallengeHashHex(chainId, publicKey, timestamp), privateKey);
108
109
  }
110
+ /** The address of a private key: `0x` and 40 lowercase hex characters. */
111
+ export function addressFromPrivateKey(privateKey) {
112
+ const publicKey = secp.getPublicKey(stripHexPrefix(privateKey), false); // 0x04 || X || Y
113
+ return '0x' + Buffer.from(keccak_256(publicKey.slice(1)).slice(-20)).toString('hex');
114
+ }
115
+ /**
116
+ * A signer for a private key held in memory. It signs the hash string, as the SDK always has.
117
+ * A wallet cannot hand over its key: use `createWalletSigner` for that.
118
+ */
119
+ export function createLocalSigner(privateKey) {
120
+ return {
121
+ // A getter, so that a malformed key still fails when something is signed, as it always did,
122
+ // and not when the SDK is built.
123
+ get address() {
124
+ return addressFromPrivateKey(privateKey);
125
+ },
126
+ signTransaction: ({ hashHex }) => signHashHex(hashHex, privateKey),
127
+ signAuthChallenge: ({ hashHex }) => signHashHex(hashHex, privateKey),
128
+ };
129
+ }
130
+ /** Everywhere the SDK takes a private key it also takes a signer. */
131
+ function toSigner(keyOrSigner) {
132
+ return typeof keyOrSigner === 'string' ? createLocalSigner(keyOrSigner) : keyOrSigner;
133
+ }
109
134
  /**
110
135
  * One graphql-ws connection per hub URL + wallet; multiplex all subscriptions on it.
111
136
  * Without this, each subscribe* call opened a new socket (`lazy: false`), which explodes
@@ -120,9 +145,9 @@ function sharedGraphqlWsCacheKey(baseURL, publicKey) {
120
145
  * via the `generateToken` mutation when needed. Shared by `ensureAuth` and the WebSocket
121
146
  * `connectionParams` so all SDK instances and subscriptions share tokens.
122
147
  *
123
- * Token issuance signs the proof-of-key-ownership challenge, so a private key for
124
- * `publicKey` must have been provided (constructor or `setPrivateKey`) unless a cached
125
- * token is still valid.
148
+ * Token issuance signs the proof-of-key-ownership challenge, so a signer (or a private key)
149
+ * for `publicKey` must have been provided (constructor, `setPrivateKey` or `setSigner`)
150
+ * unless a cached token is still valid. With a wallet this is the prompt the user sees.
126
151
  */
127
152
  async function ensureTokenInCacheForPublicKey(publicKey, apiClient, chainId) {
128
153
  const now = Date.now();
@@ -135,9 +160,9 @@ async function ensureTokenInCacheForPublicKey(publicKey, apiClient, chainId) {
135
160
  if (existingInFlight) {
136
161
  return existingInFlight;
137
162
  }
138
- const privateKey = globalPrivateKeys.get(publicKey);
139
- if (!privateKey) {
140
- throw new Error(`ClutchHubSdk: generateToken requires proof of key ownership; provide the private key for ${publicKey} via the ClutchHubSdk constructor or setPrivateKey().`);
163
+ const signer = globalSigners.get(publicKey);
164
+ if (!signer) {
165
+ throw new Error(`ClutchHubSdk: generateToken requires proof of key ownership; provide the private key (or a signer) for ${publicKey} via the ClutchHubSdk constructor, setPrivateKey() or setSigner().`);
141
166
  }
142
167
  const query = `
143
168
  mutation GenerateToken($publicKey: String!, $timestamp: Int!, $signature: AuthSignatureInput!) {
@@ -149,7 +174,10 @@ async function ensureTokenInCacheForPublicKey(publicKey, apiClient, chainId) {
149
174
  `;
150
175
  const requestPromise = (async () => {
151
176
  const timestamp = Math.floor(Date.now() / 1000);
152
- const signature = await signAuthChallenge(chainId, publicKey, timestamp, privateKey);
177
+ const signature = await signer.signAuthChallenge({
178
+ message: buildAuthChallengeMessage(chainId, publicKey, timestamp),
179
+ hashHex: authChallengeHashHex(chainId, publicKey, timestamp),
180
+ });
153
181
  const response = await apiClient.post('/graphql', {
154
182
  query,
155
183
  variables: {
@@ -274,9 +302,10 @@ export class ClutchHubSdk {
274
302
  /**
275
303
  * @param apiUrl Hub API base URL.
276
304
  * @param publicKey Wallet address (0x + 40 hex) or uncompressed public key (130 hex).
277
- * @param privateKey Optional wallet private key, required to obtain JWTs: `generateToken`
278
- * demands a signed proof-of-key-ownership challenge. May also be provided later via
279
- * {@link setPrivateKey}. Never sent to the API — only used for local signing.
305
+ * @param privateKey Optional private key, or a {@link Signer} (for example a wallet's, see
306
+ * `createWalletSigner`), required to obtain JWTs: `generateToken` demands a signed
307
+ * proof-of-key-ownership challenge. May also be provided later via {@link setPrivateKey}
308
+ * or {@link setSigner}. A key is never sent to the API — only used for local signing.
280
309
  * @param chainId This chain's id (e.g. 2077 for the app's own config), used for the
281
310
  * chain-bound auth challenge and as the default `expected.chainId` pin in
282
311
  * {@link signTransaction}'s `verifyUnsignedTransaction` check. Get this from app config,
@@ -298,7 +327,7 @@ export class ClutchHubSdk {
298
327
  this.chainId = chainId ?? 0;
299
328
  this.chainIdConfigured = chainId !== undefined;
300
329
  if (privateKey) {
301
- globalPrivateKeys.set(publicKey, privateKey);
330
+ globalSigners.set(publicKey, toSigner(privateKey));
302
331
  }
303
332
  }
304
333
  /**
@@ -309,13 +338,17 @@ export class ClutchHubSdk {
309
338
  return this.publicKey;
310
339
  }
311
340
  /**
312
- * Provide (or replace) the private key used to sign `generateToken` auth challenges for
313
- * this SDK's public key. Stored in a module-global map keyed by publicKey — like the JWT
314
- * cache — so every SDK instance and shared WebSocket connection for this wallet can
315
- * authenticate. In-memory only; never sent to the API.
341
+ * Provide (or replace) the private key — or the {@link Signer} — used to sign `generateToken`
342
+ * auth challenges for this SDK's public key. Stored in a module-global map keyed by
343
+ * publicKey — like the JWT cache — so every SDK instance and shared WebSocket connection for
344
+ * this wallet can authenticate. In-memory only; a key is never sent to the API.
316
345
  */
317
346
  setPrivateKey(privateKey) {
318
- globalPrivateKeys.set(this.publicKey, privateKey);
347
+ globalSigners.set(this.publicKey, toSigner(privateKey));
348
+ }
349
+ /** Same as {@link setPrivateKey}, for a signer such as a wallet's (`createWalletSigner`). */
350
+ setSigner(signer) {
351
+ globalSigners.set(this.publicKey, signer);
319
352
  }
320
353
  /**
321
354
  * Check if the SDK is currently authenticated.
@@ -326,6 +359,16 @@ export class ClutchHubSdk {
326
359
  const bufferTime = 30000; // 30 seconds
327
360
  return !!(this.token && now < (this.tokenExpireTime - bufferTime));
328
361
  }
362
+ /**
363
+ * True when a token for this account is cached and still good, so the next authenticated call
364
+ * opens no sign-in prompt. `isAuthenticated` looks at this instance only; this looks at the
365
+ * cache that every instance of the account shares. A poll should check it first: a wallet's
366
+ * sign-in prompt belongs to something the user did, never to a timer.
367
+ */
368
+ hasValidToken() {
369
+ const cached = globalTokenCache.get(this.publicKey);
370
+ return !!cached && Date.now() < cached.expireTimeMs - 30000;
371
+ }
329
372
  get authHeaders() {
330
373
  return this.token ? { Authorization: `Bearer ${this.token}` } : {};
331
374
  }
@@ -351,21 +394,9 @@ export class ClutchHubSdk {
351
394
  const key = sharedGraphqlWsCacheKey(base, this.publicKey);
352
395
  let entry = sharedGraphqlWsClients.get(key);
353
396
  if (!entry) {
354
- const pk = this.publicKey;
355
- const apiClient = this.apiClient;
356
- const chainId = this.chainId;
357
397
  const client = createHubSubscriptionClient({
358
398
  url: hubGraphqlWsUrl(base),
359
- connectionParams: async () => {
360
- try {
361
- await ensureTokenInCacheForPublicKey(pk, apiClient, chainId);
362
- }
363
- catch {
364
- /* public list subscriptions work without JWT */
365
- }
366
- const c = globalTokenCache.get(pk);
367
- return c?.token ? { Authorization: `Bearer ${c.token}` } : {};
368
- },
399
+ connectionParams: () => this.wsConnectionParams(),
369
400
  });
370
401
  entry = { client, refcount: 0 };
371
402
  sharedGraphqlWsClients.set(key, entry);
@@ -384,6 +415,29 @@ export class ClutchHubSdk {
384
415
  };
385
416
  return { client: entry.client, release };
386
417
  }
418
+ /**
419
+ * The `connection_init` payload of the shared socket: a token when there is one to send.
420
+ *
421
+ * The subscriptions are public, so a token is optional here. This runs at every connect and
422
+ * every reconnect (the socket retries for ever), with nobody watching, so a signer that opens a
423
+ * prompt (a wallet) is not asked for a signature: a token from an earlier, explicit action is
424
+ * still sent. A key signs silently and is asked as before.
425
+ */
426
+ async wsConnectionParams() {
427
+ const pk = this.publicKey;
428
+ if (!globalSigners.get(pk)?.interactive) {
429
+ try {
430
+ await ensureTokenInCacheForPublicKey(pk, this.apiClient, this.chainId);
431
+ }
432
+ catch {
433
+ /* public list subscriptions work without JWT */
434
+ }
435
+ }
436
+ const cached = globalTokenCache.get(pk);
437
+ return cached?.token && Date.now() < cached.expireTimeMs
438
+ ? { Authorization: `Bearer ${cached.token}` }
439
+ : {};
440
+ }
387
441
  /**
388
442
  * Shared graphql-ws list subscription: one multiplexed client via {@link acquireGraphqlWsClient}.
389
443
  */
@@ -579,6 +633,10 @@ export class ClutchHubSdk {
579
633
  }
580
634
  /**
581
635
  * Signs a transaction and returns the signature and raw RLP-encoded payload.
636
+ *
637
+ * `privateKey` is a private key, or a {@link Signer}. A key signs the hash string. A wallet's
638
+ * signer asks the wallet to sign `clutch-tx:{chainId}:{hash}` with `personal_sign`: the user
639
+ * sees a prompt, so call this only where that is expected.
582
640
  */
583
641
  async signTransaction(unsignedTx, privateKey, expected) {
584
642
  if (expected) {
@@ -619,7 +677,10 @@ export class ClutchHubSdk {
619
677
  const hashBytes = keccak_256(unsignedPayload);
620
678
  const rawHashHex = Buffer.from(hashBytes).toString('hex');
621
679
  // Sign the transaction hash
622
- const signature = await signHashHex(rawHashHex, privateKey);
680
+ const signature = await toSigner(privateKey).signTransaction({
681
+ hashHex: rawHashHex,
682
+ chainId: unsignedTx.chain_id,
683
+ });
623
684
  const rNo0x = stripHexPrefix(signature.r);
624
685
  const sNo0x = stripHexPrefix(signature.s);
625
686
  // RLP-encode full signed transaction to match Rust: [from, nonce, chain_id, r, s, v, hash, data]
@@ -0,0 +1,151 @@
1
+ import type { Signature } from './types.js';
2
+ /** What a signer is asked to sign to send a transaction. */
3
+ export interface TransactionSigningRequest {
4
+ /** Keccak-256 of the unsigned transaction: 64 lowercase hex characters, no `0x`. */
5
+ hashHex: string;
6
+ /** The chain the transaction is for. A wallet's prompt names it. */
7
+ chainId: number;
8
+ }
9
+ /** What a signer is asked to sign to prove it owns a key to the Hub API. */
10
+ export interface AuthChallengeSigningRequest {
11
+ /** The readable challenge, `clutch-auth:{chainId}:{publicKey}:{timestamp}`. */
12
+ message: string;
13
+ /** Keccak-256 of `message`: 64 lowercase hex characters, no `0x`. */
14
+ hashHex: string;
15
+ }
16
+ /** Something that can sign for one account. */
17
+ export interface Signer {
18
+ /** The account this signer signs for: `0x` and 40 lowercase hex characters. */
19
+ readonly address: string;
20
+ /**
21
+ * True when signing opens a prompt for a person (a wallet). The SDK never asks such a signer for
22
+ * a signature in the background, for example when a subscription reconnects; only a call the
23
+ * app makes (create or sign a transaction) can open a prompt.
24
+ */
25
+ readonly interactive?: boolean;
26
+ /** Sign a transaction. A key signs the hash string; a wallet signs `walletTransactionText`. */
27
+ signTransaction(request: TransactionSigningRequest): Promise<Signature>;
28
+ /** Sign the Hub API's proof-of-key-ownership challenge. A key signs its hash; a wallet signs the message. */
29
+ signAuthChallenge(request: AuthChallengeSigningRequest): Promise<Signature>;
30
+ }
31
+ /**
32
+ * The part of an EIP-1193 provider (`window.ethereum`, an EIP-6963 provider) that the SDK uses.
33
+ * `on` and `removeListener` are there for apps that watch `accountsChanged`.
34
+ */
35
+ export interface Eip1193Provider {
36
+ request(args: {
37
+ method: string;
38
+ params?: unknown[];
39
+ }): Promise<unknown>;
40
+ on?(event: string, listener: (...args: any[]) => void): void;
41
+ removeListener?(event: string, listener: (...args: any[]) => void): void;
42
+ }
43
+ /** The text a wallet signs for a transaction. The hash is written without `0x`, in lower case. */
44
+ export declare function walletTransactionText(chainId: number, hashHex: string): string;
45
+ /** What `personal_sign` hashes and signs: `Keccak256("\x19Ethereum Signed Message:\n" + length + text)`. */
46
+ export declare function personalSignDigest(text: string): Uint8Array;
47
+ /** What TronLink's `signMessageV2` (TIP-191) hashes and signs: `Keccak256("\x19TRON Signed Message:\n" + length + text)`. */
48
+ export declare function tronSignDigest(text: string): Uint8Array;
49
+ /**
50
+ * A signer that asks a wallet to sign with `personal_sign`. The wallet shows the user the text,
51
+ * and keeps the key. `address` is the account to sign for; it is lowercased, because the hash of a
52
+ * transaction commits to `from` and the node reads `from` in lower case.
53
+ *
54
+ * The signature is checked here before it is returned: a wallet that signs with another account
55
+ * (the user switched accounts) would otherwise be found out later, as a refusal from the node
56
+ * that does not say why.
57
+ */
58
+ export declare function createWalletSigner(provider: Eip1193Provider, address: string): Signer;
59
+ /**
60
+ * A TRON address as the Clutch address of the same key: `0x` and 40 lowercase hex characters.
61
+ *
62
+ * A TRON account is an Ethereum-type key (the same curve, the same Keccak-256, the same 20 address
63
+ * bytes). TRON writes those bytes in base58 (`T…`): `0x41`, the 20 bytes, and 4 bytes of checksum,
64
+ * and the checksum is checked here. A `0x` address and the `41…` hex form are accepted as they are.
65
+ */
66
+ export declare function tronAddressToHex(address: string): string;
67
+ /** The part of the `tronWeb` that TronLink puts on its provider that the SDK uses. */
68
+ export interface TronWebLike {
69
+ ready?: boolean;
70
+ defaultAddress?: {
71
+ base58?: string | false;
72
+ };
73
+ trx?: {
74
+ signMessageV2?(message: string): Promise<unknown>;
75
+ };
76
+ }
77
+ /**
78
+ * TronLink's provider (`window.tron`, or the one it announces with TIP-6963): EIP-1193 plus a
79
+ * `tronWeb`, which is `false` until the person lets this site use TronLink.
80
+ */
81
+ export interface TronLinkProvider extends Eip1193Provider {
82
+ tronWeb?: TronWebLike | false;
83
+ }
84
+ /**
85
+ * A signer that asks TronLink to sign with `signMessageV2` (TIP-191). TronLink shows the person the
86
+ * text and keeps the key. `address` is the account to sign for (a `0x` address, or the base58 one
87
+ * TronLink shows). It is checked the same way `createWalletSigner` checks: the signature is
88
+ * recovered here, and one from another account is refused before it leaves the SDK.
89
+ *
90
+ * TronLink's documentation is not clear on what `signMessageV2` takes: one page says a hex string
91
+ * and another says plain text or hex. So the text goes in plain first. A TronLink that takes only
92
+ * hex answers "Invalid transaction provided" before it opens a prompt, and then the text goes in
93
+ * again as `0x` hex of its UTF-8 bytes. Any other answer ends the call, because a second try would
94
+ * open a second prompt.
95
+ */
96
+ export declare function createTronLinkSigner(provider: TronLinkProvider, address: string): Signer;
97
+ /** A wallet found in the page. */
98
+ export interface InjectedWallet {
99
+ /** The EIP-6963 or TIP-6963 `rdns` (for example `io.metamask`), or `injected-0` for a bare `window.ethereum`. */
100
+ id: string;
101
+ name: string;
102
+ /** A `data:` image address from the wallet, when it announced one. */
103
+ icon?: string;
104
+ /**
105
+ * `evm` (MetaMask, Trust Wallet: signs with `personal_sign`) or `tron` (TronLink: signs with
106
+ * `signMessageV2`). Default `evm`.
107
+ */
108
+ kind?: 'evm' | 'tron';
109
+ /** For a `tron` wallet this is a {@link TronLinkProvider}. */
110
+ provider: Eip1193Provider;
111
+ }
112
+ export interface WalletDiscoveryOptions {
113
+ /** Where to look. Default: `window`. */
114
+ host?: EventTarget & {
115
+ ethereum?: unknown;
116
+ tron?: unknown;
117
+ tronLink?: unknown;
118
+ };
119
+ /** How long to listen for EIP-6963 and TIP-6963 announcements, in milliseconds. Default: 300. */
120
+ timeoutMs?: number;
121
+ }
122
+ /**
123
+ * The wallets in this page. A wallet that follows EIP-6963 (MetaMask, Trust Wallet) or TIP-6963
124
+ * (TronLink, the same idea for TRON) announces itself, so several can be listed side by side. One
125
+ * that only sets a global is added when no announced wallet is that same provider: `window.ethereum`
126
+ * (older wallets, some in-app browsers), or `window.tron` / `window.tronLink` for TronLink. Returns
127
+ * an empty list when there is no wallet, or when there is no page (Node).
128
+ */
129
+ export declare function discoverInjectedWallets(options?: WalletDiscoveryOptions): Promise<InjectedWallet[]>;
130
+ /**
131
+ * Ask a wallet to share its account (the wallet shows its own prompt) and return a signer for it.
132
+ * Rejects when the user says no (the provider's error, code 4001) or when no account is shared.
133
+ */
134
+ export declare function connectWallet(wallet: InjectedWallet): Promise<Signer>;
135
+ /** A signer for `account` on `wallet`: `personal_sign` for MetaMask and Trust Wallet, `signMessageV2` for TronLink. */
136
+ export declare function createSignerFor(wallet: InjectedWallet, account: string): Signer;
137
+ /** The first account in an answer of this wallet (`accountsChanged`), as a Clutch address; `null` when there is none. */
138
+ export declare function walletAccountFrom(wallet: InjectedWallet, accounts: unknown): string | null;
139
+ /**
140
+ * The account a wallet already shares with this page, as a Clutch address. It never opens a
141
+ * prompt, so an app can use it to connect again by itself on the next visit. `null` when the wallet
142
+ * shares none (locked, or the site was never allowed). A TronLink that allowed the site earlier has
143
+ * its `tronWeb` ready; one that did not has `tronWeb` `false`.
144
+ */
145
+ export declare function sharedWalletAccount(wallet: InjectedWallet): Promise<string | null>;
146
+ /**
147
+ * Call `listener` with the new account (a Clutch address) when the person switches account in the
148
+ * wallet, and with `null` when the wallet stops sharing this site (locked, or disconnected).
149
+ * Returns a function that stops listening.
150
+ */
151
+ export declare function watchWalletAccounts(wallet: InjectedWallet, listener: (account: string | null) => void): () => void;