@shapeshiftoss/hdwallet-gridplus 1.62.10-alpha.3 → 1.62.10

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/src/ethereum.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Common, Hardfork } from "@ethereumjs/common";
2
- import { RLP } from "@ethereumjs/rlp";
3
- import { TransactionFactory, TransactionType, TypedTxData } from "@ethereumjs/tx";
2
+ import { TransactionFactory, TypedTxData } from "@ethereumjs/tx";
4
3
  import * as core from "@shapeshiftoss/hdwallet-core";
5
4
  import { Client, Constants, Utils } from "gridplus-sdk";
5
+ import { encode } from "rlp";
6
6
 
7
7
  export async function ethGetAddress(client: Client, msg: core.ETHGetAddress): Promise<core.Address | null> {
8
8
  const address = (await client.getAddresses({ startPath: msg.addressNList, n: 1 }))[0];
@@ -23,23 +23,37 @@ export async function ethSignTx(client: Client, msg: core.ETHSignTx): Promise<co
23
23
  nonce: msg.nonce,
24
24
  gasLimit: msg.gasLimit,
25
25
  chainId: msg.chainId,
26
- type: isEIP1559 ? TransactionType.FeeMarketEIP1559 : TransactionType.Legacy,
27
- maxFeePerGas: msg.maxFeePerGas,
28
- maxPriorityFeePerGas: msg.maxPriorityFeePerGas,
29
- gasPrice: msg.gasPrice,
26
+ // Add explicit type field for TransactionFactory to correctly detect transaction type
27
+ type: isEIP1559 ? 2 : 0,
28
+ ...(isEIP1559
29
+ ? {
30
+ maxFeePerGas: msg.maxFeePerGas,
31
+ maxPriorityFeePerGas: msg.maxPriorityFeePerGas,
32
+ }
33
+ : {
34
+ gasPrice: msg.gasPrice,
35
+ }),
30
36
  };
31
37
 
32
- const common = Common.custom({ chainId: msg.chainId }, { hardfork: Hardfork.London });
38
+ const common = isEIP1559
39
+ ? Common.custom({ chainId: msg.chainId }, { hardfork: Hardfork.London })
40
+ : Common.custom({ chainId: msg.chainId });
41
+
42
+ // Use TransactionFactory with explicit type field (Kevin's approach)
33
43
  const unsignedTx = TransactionFactory.fromTxData(txData, { common });
34
44
 
35
- const payload = isEIP1559 ? unsignedTx.getMessageToSign() : RLP.encode(unsignedTx.getMessageToSign());
45
+ // Handle payload encoding based on transaction type
46
+ // Legacy transactions return an array that needs RLP encoding
47
+ // EIP-1559 transactions return a pre-encoded buffer
48
+ const rawPayload = unsignedTx.getMessageToSign();
49
+ const payload = Array.isArray(rawPayload) ? encode(rawPayload) : rawPayload;
36
50
 
37
51
  const fwVersion = client.getFwVersion();
38
52
  const supportsDecoderRecursion = fwVersion.major > 0 || fwVersion.minor >= 16;
39
53
 
40
54
  const decoderResult = await (() => {
41
55
  if (!msg.data || (msg.data.startsWith("0x") && Buffer.from(msg.data.slice(2), "hex").length < 4)) {
42
- return { def: undefined };
56
+ return { def: null };
43
57
  }
44
58
  return Utils.fetchCalldataDecoder(msg.data, msg.to, msg.chainId, supportsDecoderRecursion);
45
59
  })();
@@ -53,7 +67,7 @@ export async function ethSignTx(client: Client, msg: core.ETHSignTx): Promise<co
53
67
  hashType: Constants.SIGNING.HASHES.KECCAK256,
54
68
  encodingType: Constants.SIGNING.ENCODINGS.EVM,
55
69
  signerPath: msg.addressNList,
56
- decoder: def ? Buffer.from(def) : undefined,
70
+ decoder: def,
57
71
  },
58
72
  });
59
73
 
@@ -65,7 +79,29 @@ export async function ethSignTx(client: Client, msg: core.ETHSignTx): Promise<co
65
79
  if (!Buffer.isBuffer(s)) throw new Error("Invalid signature (s)");
66
80
  if (!Buffer.isBuffer(v)) throw new Error("Invalid signature (v)");
67
81
 
68
- const signedTx = TransactionFactory.fromTxData({ ...txData, r, s, v }, { common });
82
+ // Reconstruct signed transaction using TransactionFactory with explicit type field
83
+ const signedTxData = {
84
+ to: msg.to,
85
+ value: msg.value,
86
+ data: msg.data,
87
+ nonce: msg.nonce,
88
+ gasLimit: msg.gasLimit,
89
+ chainId: msg.chainId,
90
+ type: isEIP1559 ? 2 : 0,
91
+ r,
92
+ s,
93
+ v,
94
+ ...(isEIP1559
95
+ ? {
96
+ maxFeePerGas: msg.maxFeePerGas,
97
+ maxPriorityFeePerGas: msg.maxPriorityFeePerGas,
98
+ }
99
+ : {
100
+ gasPrice: msg.gasPrice,
101
+ }),
102
+ };
103
+
104
+ const signedTx = TransactionFactory.fromTxData(signedTxData, { common });
69
105
  const serialized = `0x${Buffer.from(signedTx.serialize()).toString("hex")}`;
70
106
 
71
107
  return { r: `0x${r.toString("hex")}`, s: `0x${s.toString("hex")}`, v: v.readUIntBE(0, v.length), serialized };
package/src/gridplus.ts CHANGED
@@ -8,8 +8,8 @@ import * as eth from "./ethereum";
8
8
  import * as mayachain from "./mayachain";
9
9
  import * as solana from "./solana";
10
10
  import * as thorchain from "./thorchain";
11
-
12
- const ZERO_BUFFER = Buffer.alloc(32);
11
+ import { GridPlusTransport } from "./transport";
12
+ import { convertXpubVersion, scriptTypeToAccountType } from "./utils";
13
13
 
14
14
  export function isGridPlus(wallet: core.HDWallet): wallet is GridPlusHDWallet {
15
15
  return isObject(wallet) && (wallet as any)._isGridPlus;
@@ -273,12 +273,12 @@ export class GridPlusHDWallet
273
273
  extends GridPlusWalletInfo
274
274
  implements
275
275
  core.HDWallet,
276
- core.BTCWallet,
277
- core.CosmosWallet,
278
276
  core.ETHWallet,
279
- core.MayachainWallet,
280
277
  core.SolanaWallet,
281
- core.ThorchainWallet
278
+ core.BTCWallet,
279
+ core.CosmosWallet,
280
+ core.ThorchainWallet,
281
+ core.MayachainWallet
282
282
  {
283
283
  readonly _supportsArbitrum = true;
284
284
  readonly _supportsArbitrumNova = false;
@@ -298,80 +298,117 @@ export class GridPlusHDWallet
298
298
 
299
299
  readonly _isGridPlus = true;
300
300
 
301
- client: Client | undefined;
301
+ private activeWalletId?: string;
302
+
303
+ transport: GridPlusTransport;
304
+ client?: Client;
302
305
 
303
- constructor(client: Client) {
306
+ constructor(transport: GridPlusTransport) {
304
307
  super();
305
- this.client = client;
306
- }
307
-
308
- async cancel(): Promise<void> {}
309
- async clearSession(): Promise<void> {}
310
- async initialize(): Promise<void> {}
311
- async loadDevice(): Promise<void> {}
312
- async recover(): Promise<void> {}
313
- async reset(): Promise<void> {}
314
- async sendCharacter(): Promise<void> {}
315
- async sendPassphrase(): Promise<void> {}
316
- async sendPin(): Promise<void> {}
317
- async sendWord(): Promise<void> {}
318
- async wipe(): Promise<void> {}
319
-
320
- async getDeviceID(): Promise<string> {
321
- if (!this.client) throw new Error("Device not connected");
322
- return this.client.getDeviceId();
308
+ this.transport = transport;
323
309
  }
324
310
 
325
- async getFeatures(): Promise<Record<string, any>> {
326
- if (!this.client) throw new Error("Device not connected");
311
+ public setActiveWalletId(walletId: string): void {
312
+ this.activeWalletId = walletId;
313
+ }
327
314
 
315
+ async getFeatures(): Promise<Record<string, any>> {
328
316
  return {
329
317
  vendor: "GridPlus",
330
- deviceId: this.client.getDeviceId(),
318
+ deviceId: this.transport.deviceId,
331
319
  model: "Lattice1",
332
320
  };
333
321
  }
334
322
 
335
- async getFirmwareVersion(): Promise<string> {
336
- if (!this.client) throw new Error("Device not connected");
337
- const { major, minor, fix } = this.client.getFwVersion();
338
- return `${major}.${minor}.${fix}`;
323
+ public async isLocked(): Promise<boolean> {
324
+ return !this.transport.isConnected();
339
325
  }
340
326
 
341
- async getModel(): Promise<string> {
342
- return "Lattice1";
327
+ public async clearSession(): Promise<void> {
328
+ if (!this.client) return;
329
+ await this.transport.disconnect();
330
+ this.client = undefined;
343
331
  }
344
332
 
345
- async getLabel(): Promise<string> {
346
- return "GridPlus Lattice1";
333
+ public async isInitialized(): Promise<boolean> {
334
+ return !!this.client;
347
335
  }
348
336
 
349
- async isInitialized(): Promise<boolean> {
350
- return Boolean(this.client);
351
- }
337
+ public async initialize(): Promise<void> {
338
+ // Get the GridPlus client from transport after successful pairing
339
+ this.client = this.transport.getClient();
352
340
 
353
- async isLocked(): Promise<boolean> {
354
- return false;
341
+ if (!this.client) {
342
+ throw new Error("GridPlus client not available - device may not be paired");
343
+ }
344
+
345
+ // Validate that the client has the expected methods
346
+ if (typeof this.client.getAddresses !== "function") {
347
+ throw new Error("GridPlus client missing required getAddresses method");
348
+ }
355
349
  }
356
350
 
357
- async ping(msg: core.Ping): Promise<core.Pong> {
351
+ public async ping(msg: core.Ping): Promise<core.Pong> {
358
352
  return { msg: msg.msg };
359
353
  }
360
354
 
361
- async disconnect(): Promise<void> {
362
- this.client = undefined;
355
+ public async sendPin(): Promise<void> {}
356
+
357
+ public async sendPassphrase(): Promise<void> {}
358
+
359
+ public async sendCharacter(): Promise<void> {}
360
+
361
+ public async sendWord(): Promise<void> {}
362
+
363
+ public async cancel(): Promise<void> {
364
+ // GridPlus has no pending device interactions to cancel
365
+ // Wallet persists in keyring - do not disconnect
363
366
  }
364
367
 
365
- async getActiveWalletId(): Promise<string | undefined> {
366
- if (!this.client) throw new Error("Device not connected");
368
+ public async wipe(): Promise<void> {
369
+ throw new Error("GridPlus does not support wiping");
370
+ }
367
371
 
368
- const { external, internal } = await this.client.fetchActiveWallet();
372
+ public async reset(): Promise<void> {
373
+ await this.clearSession();
374
+ await this.initialize();
375
+ }
376
+
377
+ public async recover(): Promise<void> {
378
+ throw new Error("GridPlus does not support recovery mode");
379
+ }
380
+
381
+ public async loadDevice(): Promise<void> {
382
+ throw new Error("GridPlus does not support device loading");
383
+ }
384
+
385
+ public describePath(): core.PathDescription {
386
+ return {
387
+ verbose: "GridPlus does not support path descriptions yet",
388
+ coin: "Unknown",
389
+ isKnown: false,
390
+ };
391
+ }
392
+
393
+ public async getModel(): Promise<string> {
394
+ return "Lattice1";
395
+ }
369
396
 
370
- if (!external.uid.equals(ZERO_BUFFER)) return external.uid.toString("hex");
371
- if (!internal.uid.equals(ZERO_BUFFER)) return internal.uid.toString("hex");
397
+ public async getLabel(): Promise<string> {
398
+ return "GridPlus Lattice1";
399
+ }
400
+
401
+ public async getFirmwareVersion(): Promise<string> {
402
+ if (!this.client) throw new Error("Device not connected");
403
+ const { major, minor, fix } = this.client.getFwVersion();
404
+ return `${major}.${minor}.${fix}`;
405
+ }
406
+
407
+ public async getDeviceID(): Promise<string> {
408
+ return this.activeWalletId || (await this.transport.getDeviceID());
372
409
  }
373
410
 
374
- async getPublicKeys(msg: Array<core.GetPublicKey>): Promise<Array<core.PublicKey | null>> {
411
+ public async getPublicKeys(msg: Array<core.GetPublicKey>): Promise<Array<core.PublicKey | null>> {
375
412
  if (!this.client) throw new Error("Device not connected");
376
413
 
377
414
  const publicKeys: Array<core.PublicKey | null> = [];
@@ -380,18 +417,18 @@ export class GridPlusHDWallet
380
417
  const { addressNList, curve, coin, scriptType } = getPublicKey;
381
418
 
382
419
  try {
383
- const flag = (() => {
384
- switch (curve) {
385
- // For UTXO chains (Bitcoin, Dogecoin), we need the xpub
386
- case "secp256k1":
387
- return Constants.GET_ADDR_FLAGS.SECP256K1_XPUB;
388
- // For Solana/ed25519 chains, we need the public key
389
- case "ed25519":
390
- return Constants.GET_ADDR_FLAGS.ED25519_PUB;
391
- default:
392
- throw new Error(`Unsupported curve: ${curve}`);
393
- }
394
- })();
420
+ let flag: number;
421
+
422
+ // Determine the appropriate flag based on curve type
423
+ if (curve === "secp256k1") {
424
+ // For UTXO chains (Bitcoin, Dogecoin), we need the xpub
425
+ flag = Constants.GET_ADDR_FLAGS.SECP256K1_XPUB;
426
+ } else if (curve === "ed25519") {
427
+ // For Solana/ed25519 chains, we need the public key
428
+ flag = Constants.GET_ADDR_FLAGS.ED25519_PUB;
429
+ } else {
430
+ throw new Error(`Unsupported curve: ${curve}`);
431
+ }
395
432
 
396
433
  const addresses = await this.client!.getAddresses({
397
434
  startPath: addressNList,
@@ -399,15 +436,17 @@ export class GridPlusHDWallet
399
436
  flag,
400
437
  });
401
438
 
402
- if (!addresses.length) throw new Error("No public key returned from device");
439
+ if (!addresses.length) {
440
+ throw new Error("No public key returned from device");
441
+ }
403
442
 
404
443
  // addresses[0] contains either xpub string (for SECP256K1_XPUB) or pubkey hex (for ED25519_PUB)
405
444
  let xpub = typeof addresses[0] === "string" ? addresses[0] : Buffer.from(addresses[0]).toString("hex");
406
445
 
407
446
  // Convert xpub format for Dogecoin/Litecoin (GridPlus returns Bitcoin xpub format)
408
447
  if (coin && curve === "secp256k1") {
409
- const accountType = scriptType ? core.scriptTypeToAccountType[scriptType] : undefined;
410
- xpub = core.convertXpubVersion(xpub, accountType, coin);
448
+ const accountType = scriptTypeToAccountType(scriptType);
449
+ xpub = convertXpubVersion(xpub, accountType, coin);
411
450
  }
412
451
 
413
452
  publicKeys.push({ xpub });
@@ -419,45 +458,53 @@ export class GridPlusHDWallet
419
458
  return publicKeys;
420
459
  }
421
460
 
422
- async btcGetAddress(msg: core.BTCGetAddress): Promise<string | null> {
461
+ public getSessionId(): string | undefined {
462
+ return this.transport.getSessionId();
463
+ }
464
+
465
+ public async disconnect(): Promise<void> {
466
+ await this.clearSession();
467
+ }
468
+
469
+ public async btcGetAddress(msg: core.BTCGetAddress): Promise<string | null> {
423
470
  if (!this.client) throw new Error("Device not connected");
424
471
  return btc.btcGetAddress(this.client!, msg);
425
472
  }
426
473
 
427
- async btcSignTx(msg: core.BTCSignTx): Promise<core.BTCSignedTx | null> {
474
+ public async btcSignTx(msg: core.BTCSignTx): Promise<core.BTCSignedTx | null> {
428
475
  if (!this.client) throw new Error("Device not connected");
429
476
  return btc.btcSignTx(this.client, msg);
430
477
  }
431
478
 
432
- async btcSignMessage(): Promise<core.BTCSignedMessage | null> {
479
+ public async btcSignMessage(): Promise<core.BTCSignedMessage | null> {
433
480
  throw new Error("GridPlus BTC message signing not yet implemented");
434
481
  }
435
482
 
436
- async btcVerifyMessage(): Promise<boolean | null> {
483
+ public async btcVerifyMessage(): Promise<boolean | null> {
437
484
  throw new Error("GridPlus BTC message verification not yet implemented");
438
485
  }
439
486
 
440
- async ethGetAddress(msg: core.ETHGetAddress): Promise<core.Address | null> {
487
+ public async ethGetAddress(msg: core.ETHGetAddress): Promise<core.Address | null> {
441
488
  if (!this.client) throw new Error("Device not connected");
442
489
  return eth.ethGetAddress(this.client, msg);
443
490
  }
444
491
 
445
- async ethSignTx(msg: core.ETHSignTx): Promise<core.ETHSignedTx> {
492
+ public async ethSignTx(msg: core.ETHSignTx): Promise<core.ETHSignedTx> {
446
493
  if (!this.client) throw new Error("Device not connected");
447
494
  return eth.ethSignTx(this.client, msg);
448
495
  }
449
496
 
450
- async ethSignTypedData(msg: core.ETHSignTypedData): Promise<core.ETHSignedTypedData> {
497
+ public async ethSignTypedData(msg: core.ETHSignTypedData): Promise<core.ETHSignedTypedData> {
451
498
  if (!this.client) throw new Error("Device not connected");
452
499
  return eth.ethSignTypedData(this.client, msg);
453
500
  }
454
501
 
455
- async ethSignMessage(msg: core.ETHSignMessage): Promise<core.ETHSignedMessage> {
502
+ public async ethSignMessage(msg: core.ETHSignMessage): Promise<core.ETHSignedMessage> {
456
503
  if (!this.client) throw new Error("Device not connected");
457
504
  return eth.ethSignMessage(this.client, msg);
458
505
  }
459
506
 
460
- async ethVerifyMessage(): Promise<boolean> {
507
+ public async ethVerifyMessage(): Promise<boolean> {
461
508
  throw new Error("GridPlus ETH message verification not implemented yet");
462
509
  }
463
510
 
@@ -473,42 +520,42 @@ export class GridPlusHDWallet
473
520
  }
474
521
  }
475
522
 
476
- async solanaGetAddress(msg: core.SolanaGetAddress): Promise<string | null> {
523
+ public async solanaGetAddress(msg: core.SolanaGetAddress): Promise<string | null> {
477
524
  this.assertSolanaFwSupport();
478
525
  return solana.solanaGetAddress(this.client, msg);
479
526
  }
480
527
 
481
- async solanaSignTx(msg: core.SolanaSignTx): Promise<core.SolanaSignedTx | null> {
528
+ public async solanaSignTx(msg: core.SolanaSignTx): Promise<core.SolanaSignedTx | null> {
482
529
  this.assertSolanaFwSupport();
483
530
  return solana.solanaSignTx(this.client, msg);
484
531
  }
485
532
 
486
- async cosmosGetAddress(msg: core.CosmosGetAddress): Promise<string | null> {
533
+ public async cosmosGetAddress(msg: core.CosmosGetAddress): Promise<string | null> {
487
534
  if (!this.client) throw new Error("Device not connected");
488
535
  return cosmos.cosmosGetAddress(this.client, msg);
489
536
  }
490
537
 
491
- async cosmosSignTx(msg: core.CosmosSignTx): Promise<core.CosmosSignedTx | null> {
538
+ public async cosmosSignTx(msg: core.CosmosSignTx): Promise<core.CosmosSignedTx | null> {
492
539
  if (!this.client) throw new Error("Device not connected");
493
540
  return cosmos.cosmosSignTx(this.client, msg);
494
541
  }
495
542
 
496
- async thorchainGetAddress(msg: core.ThorchainGetAddress): Promise<string | null> {
543
+ public async thorchainGetAddress(msg: core.ThorchainGetAddress): Promise<string | null> {
497
544
  if (!this.client) throw new Error("Device not connected");
498
545
  return thorchain.thorchainGetAddress(this.client, msg);
499
546
  }
500
547
 
501
- async thorchainSignTx(msg: core.ThorchainSignTx): Promise<core.ThorchainSignedTx | null> {
548
+ public async thorchainSignTx(msg: core.ThorchainSignTx): Promise<core.ThorchainSignedTx | null> {
502
549
  if (!this.client) throw new Error("Device not connected");
503
550
  return thorchain.thorchainSignTx(this.client, msg);
504
551
  }
505
552
 
506
- async mayachainGetAddress(msg: core.MayachainGetAddress): Promise<string | null> {
553
+ public async mayachainGetAddress(msg: core.MayachainGetAddress): Promise<string | null> {
507
554
  if (!this.client) throw new Error("Device not connected");
508
555
  return mayachain.mayachainGetAddress(this.client, msg);
509
556
  }
510
557
 
511
- async mayachainSignTx(msg: core.MayachainSignTx): Promise<core.MayachainSignedTx | null> {
558
+ public async mayachainSignTx(msg: core.MayachainSignTx): Promise<core.MayachainSignedTx | null> {
512
559
  if (!this.client) throw new Error("Device not connected");
513
560
  return mayachain.mayachainSignTx(this.client, msg);
514
561
  }
package/src/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from "./adapter";
2
2
  export { GridPlusHDWallet, GridPlusWalletInfo, isGridPlus } from "./gridplus";
3
+ export * from "./transport";
@@ -0,0 +1,131 @@
1
+ import * as core from "@shapeshiftoss/hdwallet-core";
2
+ import { randomBytes } from "crypto";
3
+ import { Client } from "gridplus-sdk";
4
+
5
+ export type GridPlusTransportConfig = {
6
+ deviceId: string;
7
+ password?: string;
8
+ };
9
+
10
+ export class GridPlusTransport extends core.Transport {
11
+ public deviceId?: string;
12
+ public password?: string;
13
+ public connected: boolean = false;
14
+ private client?: Client;
15
+ // Session identifier used to track reconnections. When present, we can skip
16
+ // passing deviceId to SDK setup() which avoids triggering the pairing screen
17
+ // on the device and enables faster reconnection from localStorage.
18
+ private sessionId?: string;
19
+
20
+ constructor(config: GridPlusTransportConfig) {
21
+ super(new core.Keyring());
22
+ this.deviceId = config.deviceId;
23
+ this.password = config.password;
24
+ }
25
+
26
+ public getDeviceID(): Promise<string> {
27
+ return Promise.resolve(this.deviceId || "");
28
+ }
29
+
30
+ public async connect(): Promise<void> {
31
+ if (!this.deviceId) {
32
+ throw new Error("Device ID is required to connect to GridPlus");
33
+ }
34
+
35
+ const { isPaired } = await this.setup(this.deviceId, this.password);
36
+
37
+ if (!isPaired) {
38
+ throw new Error("Device is not paired");
39
+ }
40
+ }
41
+
42
+ public async connectGridPlus(deviceId: string, password?: string): Promise<void> {
43
+ this.deviceId = deviceId;
44
+ this.password = password || "shapeshift-default";
45
+ await this.connect();
46
+ }
47
+
48
+ public async disconnect(): Promise<void> {
49
+ this.connected = false;
50
+ this.deviceId = undefined;
51
+ this.password = undefined;
52
+ }
53
+
54
+ public isConnected(): boolean {
55
+ return this.connected;
56
+ }
57
+
58
+ public async setup(
59
+ deviceId: string,
60
+ password?: string,
61
+ existingSessionId?: string
62
+ ): Promise<{ isPaired: boolean; sessionId: string }> {
63
+ this.deviceId = deviceId;
64
+ this.password = password || "shapeshift-default";
65
+
66
+ // Use existing sessionId if provided, otherwise generate new one
67
+ if (existingSessionId) {
68
+ this.sessionId = existingSessionId;
69
+ } else if (!this.sessionId) {
70
+ this.sessionId = randomBytes(32).toString("hex");
71
+ }
72
+
73
+ // Create Client instance directly (Frame pattern) - no localStorage!
74
+ // This ensures we always get fresh activeWallets from device
75
+ if (!this.client) {
76
+ this.client = new Client({
77
+ name: "ShapeShift",
78
+ baseUrl: "https://signing.gridpl.us",
79
+ privKey: Buffer.from(this.sessionId, "hex"),
80
+ retryCount: 3,
81
+ timeout: 60000,
82
+ skipRetryOnWrongWallet: true,
83
+ });
84
+
85
+ try {
86
+ // Connect to device - returns true if paired, false if needs pairing
87
+ const isPaired = await this.client.connect(deviceId);
88
+ this.connected = true;
89
+ return { isPaired, sessionId: this.sessionId };
90
+ } catch (error) {
91
+ // Handle "Device Locked" error - treat as unpaired
92
+ const errorMessage = error instanceof Error ? error.message : String(error);
93
+ if (errorMessage.toLowerCase().includes("device locked")) {
94
+ this.connected = true;
95
+ return { isPaired: false, sessionId: this.sessionId };
96
+ }
97
+
98
+ throw error;
99
+ }
100
+ } else {
101
+ // Client already exists, reset active wallets to clear stale state before reconnecting
102
+ // This is critical when switching between SafeCards - ensures fresh wallet state from device
103
+ this.client.resetActiveWallets();
104
+ const isPaired = await this.client.connect(deviceId);
105
+ this.connected = true;
106
+ return { isPaired, sessionId: this.sessionId };
107
+ }
108
+ }
109
+
110
+ public async pair(pairingCode: string): Promise<boolean> {
111
+ if (!this.client) {
112
+ throw new Error("Client not initialized. Call setup() first.");
113
+ }
114
+
115
+ const result = await this.client.pair(pairingCode);
116
+ this.connected = !!result;
117
+ return !!result;
118
+ }
119
+
120
+ public getClient(): Client | undefined {
121
+ return this.client;
122
+ }
123
+
124
+ public getSessionId(): string | undefined {
125
+ return this.sessionId;
126
+ }
127
+
128
+ public async call(): Promise<any> {
129
+ throw new Error("GridPlus transport call not implemented");
130
+ }
131
+ }