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 +14 -0
- package/README.md +20 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/sdk.d.ts +41 -10
- package/dist/sdk.js +95 -34
- package/dist/signers.d.ts +151 -0
- package/dist/signers.js +375 -0
- package/package.json +4 -4
- package/src/index.ts +1 -0
- package/src/sdk.ts +104 -36
- package/src/signers.ts +486 -0
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
|
|
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
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
|
|
124
|
-
*
|
|
125
|
-
*
|
|
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`
|
|
144
|
-
* this SDK's public key. Stored in a module-global map keyed by
|
|
145
|
-
* cache — so every SDK instance and shared WebSocket connection for
|
|
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
|
|
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
|
|
60
|
-
*
|
|
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
|
|
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
|
|
124
|
-
* `publicKey` must have been provided (constructor or `
|
|
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
|
|
139
|
-
if (!
|
|
140
|
-
throw new Error(`ClutchHubSdk: generateToken requires proof of key ownership; provide the private key for ${publicKey} via the ClutchHubSdk constructor or
|
|
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(
|
|
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
|
|
278
|
-
*
|
|
279
|
-
*
|
|
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
|
-
|
|
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`
|
|
313
|
-
* this SDK's public key. Stored in a module-global map keyed by
|
|
314
|
-
* cache — so every SDK instance and shared WebSocket connection for
|
|
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
|
-
|
|
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:
|
|
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
|
|
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;
|