@shapeshiftoss/hdwallet-gridplus 1.62.4-gridplus.alpha.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE.md +21 -0
  3. package/dist/adapter.d.ts +19 -0
  4. package/dist/adapter.d.ts.map +1 -0
  5. package/dist/adapter.js +129 -0
  6. package/dist/adapter.js.map +1 -0
  7. package/dist/bitcoin.d.ts +7 -0
  8. package/dist/bitcoin.d.ts.map +1 -0
  9. package/dist/bitcoin.js +619 -0
  10. package/dist/bitcoin.js.map +1 -0
  11. package/dist/constants.d.ts +18 -0
  12. package/dist/constants.d.ts.map +1 -0
  13. package/dist/constants.js +51 -0
  14. package/dist/constants.js.map +1 -0
  15. package/dist/cosmos.d.ts +7 -0
  16. package/dist/cosmos.d.ts.map +1 -0
  17. package/dist/cosmos.js +156 -0
  18. package/dist/cosmos.js.map +1 -0
  19. package/dist/ethereum.d.ts +7 -0
  20. package/dist/ethereum.d.ts.map +1 -0
  21. package/dist/ethereum.js +294 -0
  22. package/dist/ethereum.js.map +1 -0
  23. package/dist/gridplus.d.ts +112 -0
  24. package/dist/gridplus.d.ts.map +1 -0
  25. package/dist/gridplus.js +574 -0
  26. package/dist/gridplus.js.map +1 -0
  27. package/dist/index.d.ts +4 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +24 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/mayachain.d.ts +7 -0
  32. package/dist/mayachain.d.ts.map +1 -0
  33. package/dist/mayachain.js +163 -0
  34. package/dist/mayachain.js.map +1 -0
  35. package/dist/solana.d.ts +5 -0
  36. package/dist/solana.d.ts.map +1 -0
  37. package/dist/solana.js +120 -0
  38. package/dist/solana.js.map +1 -0
  39. package/dist/thorchain.d.ts +5 -0
  40. package/dist/thorchain.d.ts.map +1 -0
  41. package/dist/thorchain.js +143 -0
  42. package/dist/thorchain.js.map +1 -0
  43. package/dist/transport.d.ts +28 -0
  44. package/dist/transport.d.ts.map +1 -0
  45. package/dist/transport.js +148 -0
  46. package/dist/transport.js.map +1 -0
  47. package/dist/utils.d.ts +17 -0
  48. package/dist/utils.d.ts.map +1 -0
  49. package/dist/utils.js +117 -0
  50. package/dist/utils.js.map +1 -0
  51. package/package.json +38 -0
  52. package/package.json.bak +38 -0
  53. package/src/adapter.ts +109 -0
  54. package/src/bitcoin.ts +711 -0
  55. package/src/constants.ts +52 -0
  56. package/src/cosmos.ts +132 -0
  57. package/src/ethereum.ts +305 -0
  58. package/src/gridplus.ts +550 -0
  59. package/src/index.ts +3 -0
  60. package/src/mayachain.ts +150 -0
  61. package/src/solana.ts +97 -0
  62. package/src/thorchain.ts +125 -0
  63. package/src/transport.ts +131 -0
  64. package/src/utils.ts +101 -0
  65. package/tsconfig.json +10 -0
  66. package/tsconfig.tsbuildinfo +1 -0
package/src/solana.ts ADDED
@@ -0,0 +1,97 @@
1
+ import * as core from "@shapeshiftoss/hdwallet-core";
2
+ import { PublicKey } from "@solana/web3.js";
3
+ import bs58 from "bs58";
4
+ import { Client, Constants } from "gridplus-sdk";
5
+
6
+ export async function solanaGetAddress(client: Client, msg: core.SolanaGetAddress): Promise<string | null> {
7
+ // Solana requires all path indices to be hardened (BIP32 hardened derivation)
8
+ // Hardening is indicated by setting the highest bit (0x80000000)
9
+ // If an index is already >= 0x80000000, it's already hardened; otherwise add 0x80000000
10
+ const correctedPath = msg.addressNList.map((idx) => {
11
+ if (idx >= 0x80000000) {
12
+ return idx;
13
+ } else {
14
+ return idx + 0x80000000;
15
+ }
16
+ });
17
+
18
+ const allHardened = correctedPath.every((idx) => idx >= 0x80000000);
19
+
20
+ if (!allHardened) {
21
+ throw new Error("Failed to harden all Solana path indices for ED25519");
22
+ }
23
+
24
+ const fwVersion = client.getFwVersion();
25
+
26
+ if (fwVersion.major === 0 && fwVersion.minor < 14) {
27
+ throw new Error(
28
+ `Solana requires firmware >= 0.14.0, current: ${fwVersion.major}.${fwVersion.minor}.${fwVersion.fix}`
29
+ );
30
+ }
31
+
32
+ const addresses = await client.getAddresses({
33
+ startPath: correctedPath,
34
+ n: 1,
35
+ flag: Constants.GET_ADDR_FLAGS.ED25519_PUB,
36
+ });
37
+
38
+ if (!addresses.length) {
39
+ throw new Error("No address returned from device");
40
+ }
41
+
42
+ const pubkeyBuffer = Buffer.isBuffer(addresses[0]) ? addresses[0] : Buffer.from(addresses[0], "hex");
43
+
44
+ const address = bs58.encode(pubkeyBuffer);
45
+
46
+ return address;
47
+ }
48
+
49
+ export async function solanaSignTx(client: Client, msg: core.SolanaSignTx): Promise<core.SolanaSignedTx | null> {
50
+ // Ensure all path indices are hardened for Solana (see solanaGetAddress for explanation)
51
+ const correctedPath = msg.addressNList.map((idx) => {
52
+ if (idx >= 0x80000000) return idx;
53
+ return idx + 0x80000000;
54
+ });
55
+
56
+ const allHardened = correctedPath.every((idx) => idx >= 0x80000000);
57
+ if (!allHardened) {
58
+ throw new Error("Failed to harden all Solana path indices - this should never happen");
59
+ }
60
+
61
+ const address = await solanaGetAddress(client, {
62
+ addressNList: correctedPath,
63
+ showDisplay: false,
64
+ });
65
+
66
+ if (!address) throw new Error("Failed to get Solana address");
67
+
68
+ const transaction = core.solanaBuildTransaction(msg, address);
69
+ const messageBytes = transaction.message.serialize();
70
+
71
+ const signingRequest = {
72
+ data: {
73
+ signerPath: correctedPath,
74
+ curveType: Constants.SIGNING.CURVES.ED25519,
75
+ hashType: Constants.SIGNING.HASHES.NONE,
76
+ encodingType: Constants.SIGNING.ENCODINGS.SOLANA,
77
+ payload: Buffer.from(messageBytes),
78
+ },
79
+ };
80
+
81
+ const signData = await client.sign(signingRequest);
82
+
83
+ if (!signData || !signData.sig) {
84
+ throw new Error("No signature returned from device");
85
+ }
86
+
87
+ const signature = Buffer.concat([signData.sig.r, signData.sig.s]);
88
+
89
+ transaction.addSignature(new PublicKey(address), signature);
90
+
91
+ const serializedTx = transaction.serialize();
92
+
93
+ return {
94
+ serialized: Buffer.from(serializedTx).toString("base64"),
95
+ signatures: transaction.signatures.map((sig) => Buffer.from(sig).toString("base64")),
96
+ };
97
+ }
@@ -0,0 +1,125 @@
1
+ import { pointCompress } from "@bitcoinerlab/secp256k1";
2
+ import type { StdTx } from "@cosmjs/amino";
3
+ import type { DirectSignResponse, OfflineDirectSigner } from "@cosmjs/proto-signing";
4
+ import type { SignerData } from "@cosmjs/stargate";
5
+ import * as core from "@shapeshiftoss/hdwallet-core";
6
+ import type { SignDoc } from "cosmjs-types/cosmos/tx/v1beta1/tx";
7
+ import { Client, Constants } from "gridplus-sdk";
8
+ import PLazy from "p-lazy";
9
+
10
+ import { createCosmosAddress } from "./cosmos";
11
+
12
+ const protoTxBuilder = PLazy.from(() => import("@shapeshiftoss/proto-tx-builder"));
13
+ const cosmJsProtoSigning = PLazy.from(() => import("@cosmjs/proto-signing"));
14
+
15
+ export async function thorchainGetAddress(client: Client, msg: core.ThorchainGetAddress): Promise<string | null> {
16
+ const prefix = msg.testnet ? "tthor" : "thor";
17
+
18
+ // Get secp256k1 pubkey using GridPlus client instance
19
+ // Use FULL path - THORChain uses standard BIP44: m/44'/931'/0'/0/0 (5 levels)
20
+ const addresses = await client.getAddresses({
21
+ startPath: msg.addressNList,
22
+ n: 1,
23
+ flag: Constants.GET_ADDR_FLAGS.SECP256K1_PUB,
24
+ });
25
+
26
+ if (!addresses.length) {
27
+ throw new Error("No address returned from device");
28
+ }
29
+
30
+ // GridPlus SDK returns uncompressed 65-byte pubkeys, but THORChain needs compressed 33-byte pubkeys
31
+ const pubkeyBuffer = Buffer.isBuffer(addresses[0]) ? addresses[0] : Buffer.from(addresses[0], "hex");
32
+ const compressedPubkey = pointCompress(pubkeyBuffer, true);
33
+ const compressedHex = Buffer.from(compressedPubkey).toString("hex");
34
+ const thorAddress = createCosmosAddress(compressedHex, prefix);
35
+
36
+ return thorAddress;
37
+ }
38
+
39
+ export async function thorchainSignTx(
40
+ client: Client,
41
+ msg: core.ThorchainSignTx
42
+ ): Promise<core.ThorchainSignedTx | null> {
43
+ // Get the address for this path
44
+ const address = await thorchainGetAddress(client, { addressNList: msg.addressNList, testnet: msg.testnet });
45
+ if (!address) throw new Error("Failed to get THORChain address");
46
+
47
+ // Get the public key using client instance
48
+ const pubkeys = await client.getAddresses({
49
+ startPath: msg.addressNList,
50
+ n: 1,
51
+ flag: Constants.GET_ADDR_FLAGS.SECP256K1_PUB,
52
+ });
53
+
54
+ if (!pubkeys.length) {
55
+ throw new Error("No public key returned from device");
56
+ }
57
+
58
+ // GridPlus SDK returns uncompressed 65-byte pubkeys, but THORChain needs compressed 33-byte pubkeys
59
+ const pubkeyBuffer = Buffer.isBuffer(pubkeys[0]) ? pubkeys[0] : Buffer.from(pubkeys[0], "hex");
60
+ const compressedPubkey = pointCompress(pubkeyBuffer, true);
61
+ const pubkey = Buffer.from(compressedPubkey);
62
+
63
+ // Create a signer adapter for GridPlus with Direct signing (Proto)
64
+ const signer: OfflineDirectSigner = {
65
+ getAccounts: async () => [
66
+ {
67
+ address,
68
+ pubkey,
69
+ algo: "secp256k1" as const,
70
+ },
71
+ ],
72
+ signDirect: async (signerAddress: string, signDoc: SignDoc): Promise<DirectSignResponse> => {
73
+ if (signerAddress !== address) {
74
+ throw new Error("Signer address mismatch");
75
+ }
76
+
77
+ // Use CosmJS to create the sign bytes from the SignDoc
78
+ const signBytes = (await cosmJsProtoSigning).makeSignBytes(signDoc);
79
+
80
+ // Sign using GridPlus SDK general signing
81
+ // Pass unhashed signBytes and let device hash with SHA256
82
+ const signData = {
83
+ data: {
84
+ payload: signBytes,
85
+ curveType: Constants.SIGNING.CURVES.SECP256K1,
86
+ hashType: Constants.SIGNING.HASHES.SHA256,
87
+ encodingType: Constants.SIGNING.ENCODINGS.NONE,
88
+ signerPath: msg.addressNList,
89
+ },
90
+ };
91
+
92
+ const signedResult = await client.sign(signData);
93
+
94
+ if (!signedResult?.sig) {
95
+ throw new Error("No signature returned from device");
96
+ }
97
+
98
+ const { r, s } = signedResult.sig;
99
+ const rHex = Buffer.isBuffer(r) ? r : Buffer.from(r);
100
+ const sHex = Buffer.isBuffer(s) ? s : Buffer.from(s);
101
+
102
+ // Combine r and s for signature
103
+ const signature = Buffer.concat([rHex, sHex]);
104
+
105
+ return {
106
+ signed: signDoc,
107
+ signature: {
108
+ pub_key: {
109
+ type: "tendermint/PubKeySecp256k1",
110
+ value: pubkey.toString("base64"),
111
+ },
112
+ signature: signature.toString("base64"),
113
+ },
114
+ };
115
+ },
116
+ };
117
+
118
+ const signerData: SignerData = {
119
+ sequence: Number(msg.sequence),
120
+ accountNumber: Number(msg.account_number),
121
+ chainId: msg.chain_id,
122
+ };
123
+
124
+ return (await protoTxBuilder).sign(address, msg.tx as StdTx, signer, signerData, "thorchain");
125
+ }
@@ -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
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,101 @@
1
+ import * as core from "@shapeshiftoss/hdwallet-core";
2
+ import * as bech32 from "bech32";
3
+ import { decode as bs58Decode, encode as bs58Encode } from "bs58check";
4
+ import CryptoJS from "crypto-js";
5
+
6
+ import { accountTypeToVersion, convertVersions, UTXO_NETWORK_PARAMS, UtxoAccountType } from "./constants";
7
+
8
+ /**
9
+ * Convert xpub version bytes for different coins (e.g., xpub → dgub for Dogecoin)
10
+ * GridPlus returns Bitcoin-format xpubs, but some coins like Dogecoin need different prefixes
11
+ */
12
+ export function convertXpubVersion(xpub: string, accountType: UtxoAccountType | undefined, coin: string): string {
13
+ if (!accountType) return xpub;
14
+ if (!convertVersions.includes(xpub.substring(0, 4))) {
15
+ return xpub;
16
+ }
17
+
18
+ const payload = bs58Decode(xpub);
19
+ const version = payload.slice(0, 4);
20
+ const desiredVersion = accountTypeToVersion(coin, accountType);
21
+ if (version.compare(desiredVersion) !== 0) {
22
+ const key = payload.slice(4);
23
+ return bs58Encode(Buffer.concat([desiredVersion, key]));
24
+ }
25
+ return xpub;
26
+ }
27
+
28
+ export function scriptTypeToAccountType(scriptType: core.BTCInputScriptType | undefined): UtxoAccountType | undefined {
29
+ switch (scriptType) {
30
+ case core.BTCInputScriptType.SpendAddress:
31
+ return UtxoAccountType.P2pkh;
32
+ case core.BTCInputScriptType.SpendWitness:
33
+ return UtxoAccountType.SegwitNative;
34
+ case core.BTCInputScriptType.SpendP2SHWitness:
35
+ return UtxoAccountType.SegwitP2sh;
36
+ default:
37
+ return undefined;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Derive a UTXO address from a compressed public key
43
+ * @param pubkeyHex - Compressed public key as hex string (33 bytes, starting with 02 or 03)
44
+ * @param coin - Coin name (Bitcoin, Dogecoin, Litecoin, etc.)
45
+ * @param scriptType - Script type (p2pkh, p2wpkh, p2sh-p2wpkh)
46
+ * @returns The derived address
47
+ */
48
+ export function deriveAddressFromPubkey(
49
+ pubkeyHex: string,
50
+ coin: string,
51
+ scriptType: core.BTCInputScriptType = core.BTCInputScriptType.SpendAddress
52
+ ): string {
53
+ const network = UTXO_NETWORK_PARAMS[coin] || UTXO_NETWORK_PARAMS.Bitcoin;
54
+ const pubkeyBuffer = Buffer.from(pubkeyHex, "hex");
55
+
56
+ if (pubkeyBuffer.length !== 33) {
57
+ throw new Error(`Invalid compressed public key length: ${pubkeyBuffer.length} bytes`);
58
+ }
59
+
60
+ // Hash160 = RIPEMD160(SHA256(pubkey))
61
+ const sha256Hash = CryptoJS.SHA256(CryptoJS.enc.Hex.parse(pubkeyHex));
62
+ const hash160 = CryptoJS.RIPEMD160(sha256Hash).toString();
63
+ const hash160Buffer = Buffer.from(hash160, "hex");
64
+
65
+ switch (scriptType) {
66
+ case core.BTCInputScriptType.SpendAddress: {
67
+ // P2PKH: <pubKeyHash version byte> + hash160 + checksum
68
+ const payload = Buffer.concat([Buffer.from([network.pubKeyHash]), hash160Buffer]);
69
+ return bs58Encode(payload);
70
+ }
71
+
72
+ case core.BTCInputScriptType.SpendWitness: {
73
+ // P2WPKH (bech32): witness version 0 + hash160
74
+ if (!network.bech32) {
75
+ throw new Error(`Bech32 not supported for ${coin}`);
76
+ }
77
+ const words = bech32.toWords(hash160Buffer);
78
+ words.unshift(0); // witness version 0
79
+ return bech32.encode(network.bech32, words);
80
+ }
81
+
82
+ case core.BTCInputScriptType.SpendP2SHWitness: {
83
+ // P2SH-P2WPKH: scriptHash of witness program
84
+ // Witness program: OP_0 (0x00) + length (0x14) + hash160
85
+ const witnessProgram = Buffer.concat([Buffer.from([0x00, 0x14]), hash160Buffer]);
86
+
87
+ // Hash160 of witness program
88
+ const wpHex = witnessProgram.toString("hex");
89
+ const wpSha256 = CryptoJS.SHA256(CryptoJS.enc.Hex.parse(wpHex));
90
+ const wpHash160 = CryptoJS.RIPEMD160(wpSha256).toString();
91
+ const wpHash160Buffer = Buffer.from(wpHash160, "hex");
92
+
93
+ // Encode with scriptHash version byte
94
+ const payload = Buffer.concat([Buffer.from([network.scriptHash]), wpHash160Buffer]);
95
+ return bs58Encode(payload);
96
+ }
97
+
98
+ default:
99
+ throw new Error(`Unsupported script type: ${scriptType}`);
100
+ }
101
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["node_modules", "dist"],
9
+ "references": [{ "path": "../hdwallet-core" }]
10
+ }