@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.
- package/CHANGELOG.md +11 -0
- package/LICENSE.md +21 -0
- package/dist/adapter.d.ts +19 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +129 -0
- package/dist/adapter.js.map +1 -0
- package/dist/bitcoin.d.ts +7 -0
- package/dist/bitcoin.d.ts.map +1 -0
- package/dist/bitcoin.js +619 -0
- package/dist/bitcoin.js.map +1 -0
- package/dist/constants.d.ts +18 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +51 -0
- package/dist/constants.js.map +1 -0
- package/dist/cosmos.d.ts +7 -0
- package/dist/cosmos.d.ts.map +1 -0
- package/dist/cosmos.js +156 -0
- package/dist/cosmos.js.map +1 -0
- package/dist/ethereum.d.ts +7 -0
- package/dist/ethereum.d.ts.map +1 -0
- package/dist/ethereum.js +294 -0
- package/dist/ethereum.js.map +1 -0
- package/dist/gridplus.d.ts +112 -0
- package/dist/gridplus.d.ts.map +1 -0
- package/dist/gridplus.js +574 -0
- package/dist/gridplus.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/mayachain.d.ts +7 -0
- package/dist/mayachain.d.ts.map +1 -0
- package/dist/mayachain.js +163 -0
- package/dist/mayachain.js.map +1 -0
- package/dist/solana.d.ts +5 -0
- package/dist/solana.d.ts.map +1 -0
- package/dist/solana.js +120 -0
- package/dist/solana.js.map +1 -0
- package/dist/thorchain.d.ts +5 -0
- package/dist/thorchain.d.ts.map +1 -0
- package/dist/thorchain.js +143 -0
- package/dist/thorchain.js.map +1 -0
- 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 +17 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +117 -0
- package/dist/utils.js.map +1 -0
- package/package.json +38 -0
- package/package.json.bak +38 -0
- package/src/adapter.ts +109 -0
- package/src/bitcoin.ts +711 -0
- package/src/constants.ts +52 -0
- package/src/cosmos.ts +132 -0
- package/src/ethereum.ts +305 -0
- package/src/gridplus.ts +550 -0
- package/src/index.ts +3 -0
- package/src/mayachain.ts +150 -0
- package/src/solana.ts +97 -0
- package/src/thorchain.ts +125 -0
- package/src/transport.ts +131 -0
- package/src/utils.ts +101 -0
- package/tsconfig.json +10 -0
- 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
|
+
}
|
package/src/thorchain.ts
ADDED
|
@@ -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
|
+
}
|
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
|
+
}
|
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
|
+
}
|