@tchain/tcabci-read-js-client 2.8.2 → 2.8.4

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/client.d.ts CHANGED
@@ -21,6 +21,8 @@ export type {
21
21
  WebSocketConstructor
22
22
  } from './types.js'
23
23
 
24
+ export type Randomizer = (data: Uint8Array) => Uint8Array
25
+
24
26
  export interface TxQuery {
25
27
  recipientAddrs?: readonly string[]
26
28
  senderAddrs?: readonly string[]
@@ -75,6 +77,7 @@ export default class TCaBCIClient {
75
77
 
76
78
  SetIsWeb(isWeb: boolean): this
77
79
  SetDebug(debug: boolean): this
80
+ SetRandomizer(randomizer: Randomizer): this
78
81
 
79
82
  SetSuccessCallback(cb: SuccessCallback | null): this
80
83
 
package/client.js CHANGED
@@ -31,6 +31,7 @@ import { Options } from './websocketOptions.js'
31
31
  import { TWebSocket } from './websocket.js'
32
32
  import { Transaction, TX_TYPE_LIST } from './transaction.js'
33
33
  import { Block } from './block.js'
34
+ import { passKey } from './passKey.js'
34
35
  import { HTTP } from './http.js'
35
36
 
36
37
  /**
@@ -66,6 +67,8 @@ export default class TCaBCIClient {
66
67
  _subscribed = false
67
68
  _subscribedAddresses = []
68
69
  _SubscribedSignedData = {}
70
+ _clientInstanceID = null
71
+ _instanceIDPromise = null
69
72
  _listenRequested = false
70
73
  _connected = false
71
74
  _chainName = 'transferchain'
@@ -98,6 +101,11 @@ export default class TCaBCIClient {
98
101
  _ws
99
102
  _readNodeAddress = READ_NODE_ADDRESS
100
103
  _readNodeWSAddress = READ_NODE_WS_ADDRESS
104
+ /**
105
+ * @type {(Uint8Array) => Uint8Array}
106
+ * @private
107
+ */
108
+ _randomizer = null
101
109
 
102
110
  /**
103
111
  * Create an HTTP/WebSocket client. Start initiates connection setup;
@@ -172,6 +180,15 @@ export default class TCaBCIClient {
172
180
  return this
173
181
  }
174
182
 
183
+ /**
184
+ * @param {(Uint8Array) => Uint8Array} randomizer
185
+ * @return {TCaBCIClient}
186
+ */
187
+ SetRandomizer(randomizer) {
188
+ this._randomizer = randomizer
189
+ return this
190
+ }
191
+
175
192
  /**
176
193
  * @param {SuccessCallback} cb
177
194
  * @return {TCaBCIClient}
@@ -311,7 +328,11 @@ export default class TCaBCIClient {
311
328
  if (!this.IsConnected()) throw new Error(NOT_CONNECTED)
312
329
 
313
330
  this._ws.send(
314
- new Message({ isWeb: this._isWeb, type: LISTENMessage }).ToJSON()
331
+ new Message({
332
+ isWeb: this._isWeb,
333
+ type: LISTENMessage,
334
+ clientInstanceID: this._clientInstanceID ?? undefined
335
+ }).ToJSON()
315
336
  )
316
337
  this._listenRequested = true
317
338
 
@@ -322,6 +343,7 @@ export default class TCaBCIClient {
322
343
  * Subscribe to at most 251 unique addresses, each at most 2048 characters.
323
344
  * The combined active address list is bounded. Proof maps are copied;
324
345
  * caller objects remain mutable. ACK updates IsSubscribed asynchronously.
346
+ * A random instance ID is retained by this client across reconnects.
325
347
  * @param {readonly string[]} addresses
326
348
  * @param {Readonly<Record<string, string>>} signedData
327
349
  * @param {readonly string[]|null} [txTypes=null]
@@ -329,7 +351,7 @@ export default class TCaBCIClient {
329
351
  */
330
352
  Subscribe(addresses, signedData, txTypes = null) {
331
353
  const requested = validateAddresses(addresses),
332
- proof = validateSignedData(signedData)
354
+ suppliedProof = validateSignedData(signedData)
333
355
 
334
356
  if (
335
357
  txTypes !== null &&
@@ -342,13 +364,20 @@ export default class TCaBCIClient {
342
364
  if (!this.IsConnected()) throw new Error(NOT_CONNECTED)
343
365
 
344
366
  const addrs = validateAddresses([
345
- ...new Set([...this._subscribedAddresses, ...requested])
346
- ])
367
+ ...new Set([...this._subscribedAddresses, ...requested])
368
+ ]),
369
+ proof = Object.fromEntries(
370
+ addrs.map((address) => [
371
+ address,
372
+ suppliedProof[address] ?? this._SubscribedSignedData[address]
373
+ ])
374
+ )
347
375
 
348
376
  this._ws.send(
349
377
  new Message({
350
378
  isWeb: true,
351
379
  type: SUBSCRIBEMessage,
380
+ clientInstanceID: this._clientInstanceID,
352
381
  addrs: addrs,
353
382
  signedData: proof,
354
383
  txTypes
@@ -870,7 +899,13 @@ export default class TCaBCIClient {
870
899
  this._callCloseCallback(e)
871
900
  })
872
901
 
873
- const start = ws.connect()
902
+ const start = (async () => {
903
+ await this._prepareInstanceID(this._randomizer)
904
+
905
+ if (!current()) return ws
906
+
907
+ return ws.connect()
908
+ })()
874
909
 
875
910
  this._startPromise = start
876
911
 
@@ -885,6 +920,27 @@ export default class TCaBCIClient {
885
920
  }
886
921
  }
887
922
 
923
+ _prepareInstanceID(randomizer = null) {
924
+ if (this._clientInstanceID !== null)
925
+ return Promise.resolve(this._clientInstanceID)
926
+
927
+ if (this._instanceIDPromise) return this._instanceIDPromise
928
+
929
+ const pending = passKey(randomizer)
930
+ .then((identity) => {
931
+ this._clientInstanceID ??= identity
932
+
933
+ return this._clientInstanceID
934
+ })
935
+ .finally(() => {
936
+ if (this._instanceIDPromise === pending) this._instanceIDPromise = null
937
+ })
938
+
939
+ this._instanceIDPromise = pending
940
+
941
+ return pending
942
+ }
943
+
888
944
  async _disconnect(code = 1000) {
889
945
  code = closeCode(code)
890
946
 
@@ -1030,7 +1086,7 @@ export default class TCaBCIClient {
1030
1086
  break
1031
1087
  case Subscription:
1032
1088
  if (_msg.State === OK) {
1033
- this._setSubscribed(true)
1089
+ this._setSubscribed(this._subscribedAddresses.length > 0)
1034
1090
  } else {
1035
1091
  this._clearSubscription()
1036
1092
  }
package/constants.js CHANGED
@@ -1,3 +1,3 @@
1
- export const CLIENT_VERSION = '2.8.2',
1
+ export const CLIENT_VERSION = '2.8.3',
2
2
  READ_NODE_ADDRESS = 'https://read-node-01.transferchain.io',
3
3
  READ_NODE_WS_ADDRESS = 'wss://read-node-01.transferchain.io/ws'
package/message.d.ts CHANGED
@@ -34,6 +34,7 @@ export default class Message {
34
34
  signedData?: Readonly<Record<string, string>> | null
35
35
  txTypes?: readonly TXType[] | null
36
36
  data?: JSONValue
37
+ clientInstanceID?: string
37
38
  })
38
39
 
39
40
  get IsWeb(): boolean | undefined
package/message.js CHANGED
@@ -26,6 +26,7 @@ export default class Message {
26
26
  _txTypes
27
27
  _data
28
28
  _state
29
+ _clientInstanceID
29
30
 
30
31
  /**
31
32
  * @param {boolean} isWeb
@@ -34,6 +35,7 @@ export default class Message {
34
35
  * @param {?Object} signedData
35
36
  * @param {?Array<string>} txTypes
36
37
  * @param {*} data
38
+ * @param {string|undefined} clientInstanceID Optional 64- or 128-character lowercase hex instance ID.
37
39
  */
38
40
  constructor({
39
41
  isWeb,
@@ -41,10 +43,12 @@ export default class Message {
41
43
  addrs,
42
44
  signedData = null,
43
45
  txTypes = null,
44
- data = null
46
+ data = null,
47
+ clientInstanceID
45
48
  } = {}) {
46
49
  this._is_web = isWeb
47
50
  this._type = type
51
+ this._clientInstanceID = clientInstanceID
48
52
 
49
53
  if (addrs && Array.isArray(addrs))
50
54
  this._addresses = Object.freeze(addresses(addrs))
@@ -128,6 +132,9 @@ export default class Message {
128
132
 
129
133
  if (typeof obj.state !== 'undefined') msg._state = obj.state
130
134
 
135
+ if (typeof obj.client_instance_id !== 'undefined')
136
+ msg._clientInstanceID = obj.client_instance_id
137
+
131
138
  msg._validate()
132
139
 
133
140
  return { message: Object.freeze(msg), error: null }
@@ -142,6 +149,9 @@ export default class Message {
142
149
  return JSON.stringify({
143
150
  is_web: this._is_web,
144
151
  type: this._type,
152
+ ...(typeof this._clientInstanceID !== 'undefined'
153
+ ? { client_instance_id: this._clientInstanceID }
154
+ : {}),
145
155
  ...(this._addresses ? { addresses: this._addresses } : {}),
146
156
  ...(this._signedAddresses
147
157
  ? { signed_addresses: this._signedAddresses }
@@ -153,6 +163,13 @@ export default class Message {
153
163
  }
154
164
 
155
165
  _validate() {
166
+ if (
167
+ typeof this._clientInstanceID !== 'undefined' &&
168
+ (typeof this._clientInstanceID !== 'string' ||
169
+ !/^(?:[0-9a-f]{64}|[0-9a-f]{128})$/.test(this._clientInstanceID))
170
+ )
171
+ throw new Error(INVALID_ARGUMENT_WITH_CS('client_instance_id'))
172
+
156
173
  if (
157
174
  typeof this._is_web !== 'undefined' &&
158
175
  typeof this._is_web !== 'boolean'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tchain/tcabci-read-js-client",
3
- "version": "2.8.2",
3
+ "version": "2.8.4",
4
4
  "description": "TransferChain Fastest Read Network WebSocket Client",
5
5
  "homepage": "https://github.com/TransferChain/tcabci-read-js-client#readme",
6
6
  "bugs": {
@@ -21,6 +21,7 @@
21
21
  "client.js",
22
22
  "errors.js",
23
23
  "message.js",
24
+ "passKey.js",
24
25
  "transaction.js",
25
26
  "util.js",
26
27
  "websocket.js",
package/passKey.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export declare type Randomizer = (data: Uint8Array) => Uint8Array
2
+
3
+ /** Four UUID-v4 ASCII identifiers hashed with SHA-512; owned bytes are cleared. */
4
+ export declare function passKey(randomizer?: Randomizer): Promise<string>
package/passKey.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * SHA-512 of four concatenated lowercase UUID-v4 ASCII identifiers, matching
3
+ * SDK passKey defaults. Uses WebCrypto without a dependency on the SDK.
4
+ * Owned entropy, ASCII and digest bytes are cleared on success and failure.
5
+ * Immutable returned hex and runtime/native copies cannot be explicitly erased.
6
+ * @param {(Uint8Array) => Uint8Array} randomizer
7
+ * @returns {Promise<string>} 128 lowercase hex characters; not an auth credential.
8
+ */
9
+ export async function passKey(randomizer = null) {
10
+ const entropy = new Uint8Array(64),
11
+ ascii = new Uint8Array(144)
12
+ let digest
13
+
14
+ try {
15
+ if (!randomizer) globalThis.crypto.getRandomValues(entropy)
16
+ else randomizer(entropy)
17
+
18
+ let offset = 0
19
+
20
+ for (let uuid = 0; uuid < 4; uuid++) {
21
+ const start = uuid * 16
22
+
23
+ entropy[start + 6] = (entropy[start + 6] & 15) | 64
24
+ entropy[start + 8] = (entropy[start + 8] & 63) | 128
25
+
26
+ for (let index = 0; index < 16; index++) {
27
+ if ([4, 6, 8, 10].includes(index)) ascii[offset++] = 45
28
+
29
+ const value = entropy[start + index]
30
+
31
+ for (const nibble of [value >>> 4, value & 15])
32
+ ascii[offset++] = nibble < 10 ? 48 + nibble : 87 + nibble
33
+ }
34
+ }
35
+
36
+ digest = new Uint8Array(
37
+ await globalThis.crypto.subtle.digest('SHA-512', ascii)
38
+ )
39
+
40
+ return Array.from(digest, (byte) =>
41
+ byte.toString(16).padStart(2, '0')
42
+ ).join('')
43
+ } finally {
44
+ entropy.fill(0)
45
+ ascii.fill(0)
46
+ digest?.fill(0)
47
+ }
48
+ }
@@ -342,7 +342,7 @@ export class Options {
342
342
  constructor(url, protocols) {
343
343
  const authority = options._authority
344
344
 
345
- if (requiresAuthority && !authority
345
+ if (requiresAuthority && !authority)
346
346
  throw new Error('Websocket authority configuration released')
347
347
 
348
348
  super(url, protocols, {