@shapeshiftoss/hdwallet-gridplus 1.62.10-alpha.2 → 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/dist/adapter.d.ts +11 -3
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +98 -26
- package/dist/adapter.js.map +1 -1
- package/dist/bitcoin.d.ts +2 -1
- package/dist/bitcoin.d.ts.map +1 -1
- package/dist/bitcoin.js +555 -131
- package/dist/bitcoin.js.map +1 -1
- package/dist/ethereum.d.ts.map +1 -1
- package/dist/ethereum.js +34 -18
- package/dist/ethereum.js.map +1 -1
- package/dist/gridplus.d.ts +22 -17
- package/dist/gridplus.d.ts.map +1 -1
- package/dist/gridplus.js +101 -74
- package/dist/gridplus.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/transport.d.ts +28 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +148 -0
- package/dist/transport.js.map +1 -0
- package/dist/utils.d.ts +16 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +87 -2
- package/dist/utils.js.map +1 -1
- package/package.json +5 -6
- package/src/adapter.ts +84 -26
- package/src/bitcoin.ts +650 -134
- package/src/ethereum.ts +47 -11
- package/src/gridplus.ts +130 -83
- package/src/index.ts +1 -0
- package/src/transport.ts +131 -0
- package/src/utils.ts +100 -2
- package/tsconfig.json +1 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
package/src/ethereum.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Common, Hardfork } from "@ethereumjs/common";
|
|
2
|
-
import {
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
301
|
+
private activeWalletId?: string;
|
|
302
|
+
|
|
303
|
+
transport: GridPlusTransport;
|
|
304
|
+
client?: Client;
|
|
302
305
|
|
|
303
|
-
constructor(
|
|
306
|
+
constructor(transport: GridPlusTransport) {
|
|
304
307
|
super();
|
|
305
|
-
this.
|
|
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
|
-
|
|
326
|
-
|
|
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.
|
|
318
|
+
deviceId: this.transport.deviceId,
|
|
331
319
|
model: "Lattice1",
|
|
332
320
|
};
|
|
333
321
|
}
|
|
334
322
|
|
|
335
|
-
async
|
|
336
|
-
|
|
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
|
|
342
|
-
return
|
|
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
|
|
346
|
-
return
|
|
333
|
+
public async isInitialized(): Promise<boolean> {
|
|
334
|
+
return !!this.client;
|
|
347
335
|
}
|
|
348
336
|
|
|
349
|
-
async
|
|
350
|
-
|
|
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
|
-
|
|
354
|
-
|
|
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
|
|
362
|
-
|
|
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
|
|
366
|
-
|
|
368
|
+
public async wipe(): Promise<void> {
|
|
369
|
+
throw new Error("GridPlus does not support wiping");
|
|
370
|
+
}
|
|
367
371
|
|
|
368
|
-
|
|
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
|
-
|
|
371
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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)
|
|
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 =
|
|
410
|
-
xpub =
|
|
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
|
-
|
|
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
package/src/transport.ts
ADDED
|
@@ -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
|
+
}
|