@shapeshiftoss/hdwallet-phantom 1.55.6

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/bitcoin.ts ADDED
@@ -0,0 +1,169 @@
1
+ import * as bitcoin from "@shapeshiftoss/bitcoinjs-lib";
2
+ import * as core from "@shapeshiftoss/hdwallet-core";
3
+ import { BTCInputScriptType } from "@shapeshiftoss/hdwallet-core";
4
+
5
+ import { PhantomUtxoProvider } from "./types";
6
+
7
+ export type BtcAccount = {
8
+ address: string;
9
+ // Phantom supposedly supports more scriptTypes but in effect, doesn't (currently)
10
+ // https://github.com/orgs/phantom/discussions/173
11
+ addressType: BTCInputScriptType.SpendWitness;
12
+ publicKey: string;
13
+ purpose: "payment" | "ordinals";
14
+ };
15
+
16
+ const fromHexString = (hexString: string) => {
17
+ const bytes = hexString.match(/.{1,2}/g);
18
+ if (!bytes) throw new Error("Invalid hex string");
19
+
20
+ return Uint8Array.from(bytes.map((byte) => parseInt(byte, 16)));
21
+ };
22
+
23
+ const getNetwork = (coin: string): bitcoin.networks.Network => {
24
+ switch (coin.toLowerCase()) {
25
+ case "bitcoin":
26
+ return bitcoin.networks.bitcoin;
27
+ default:
28
+ throw new Error(`Unsupported coin: ${coin}`);
29
+ }
30
+ };
31
+
32
+ export const btcGetAccountPaths = (msg: core.BTCGetAccountPaths): Array<core.BTCAccountPath> => {
33
+ const slip44 = core.slip44ByCoin(msg.coin);
34
+ if (slip44 === undefined) return [];
35
+
36
+ const bip84 = core.segwitNativeAccount(msg.coin, slip44, msg.accountIdx);
37
+
38
+ const coinPaths = {
39
+ bitcoin: [bip84],
40
+ } as Partial<Record<string, Array<core.BTCAccountPath>>>;
41
+
42
+ let paths: Array<core.BTCAccountPath> = coinPaths[msg.coin.toLowerCase()] || [];
43
+
44
+ if (msg.scriptType !== undefined) {
45
+ paths = paths.filter((path) => {
46
+ return path.scriptType === msg.scriptType;
47
+ });
48
+ }
49
+
50
+ return paths;
51
+ };
52
+
53
+ export async function bitcoinGetAddress(_msg: core.BTCGetAddress, provider: any): Promise<string> {
54
+ const accounts = await provider.requestAccounts();
55
+ const paymentAddress = accounts.find((account: BtcAccount) => account.purpose === "payment")?.address;
56
+
57
+ return paymentAddress;
58
+ }
59
+
60
+ async function addInput(psbt: bitcoin.Psbt, input: core.BTCSignTxInput): Promise<void> {
61
+ switch (input.scriptType) {
62
+ // Phantom supposedly supports more scriptTypes but in effect, doesn't (currently)
63
+ // https://github.com/orgs/phantom/discussions/173
64
+ case BTCInputScriptType.SpendWitness: {
65
+ psbt.addInput({
66
+ hash: input.txid,
67
+ index: input.vout,
68
+ nonWitnessUtxo: Buffer.from(input.hex, "hex"),
69
+ });
70
+
71
+ break;
72
+ }
73
+ default:
74
+ throw new Error(`Unsupported script type: ${input.scriptType}`);
75
+ }
76
+ }
77
+
78
+ async function addOutput(
79
+ wallet: core.BTCWallet,
80
+ psbt: bitcoin.Psbt,
81
+ output: core.BTCSignTxOutput,
82
+ coin: string
83
+ ): Promise<void> {
84
+ if (!output.amount) throw new Error("Invalid output - missing amount.");
85
+
86
+ const address = await (async () => {
87
+ if (output.address) return output.address;
88
+
89
+ if (output.addressNList) {
90
+ const outputAddress = await wallet.btcGetAddress({ addressNList: output.addressNList, coin, showDisplay: false });
91
+ if (!outputAddress) throw new Error("Could not get address from wallet");
92
+ return outputAddress;
93
+ }
94
+ })();
95
+
96
+ if (!address) throw new Error("Invalid output - no address");
97
+
98
+ psbt.addOutput({ address, value: parseInt(output.amount) });
99
+ }
100
+
101
+ export async function bitcoinSignTx(
102
+ wallet: core.BTCWallet,
103
+ msg: core.BTCSignTx,
104
+ provider: PhantomUtxoProvider
105
+ ): Promise<core.BTCSignedTx | null> {
106
+ const network = getNetwork(msg.coin);
107
+
108
+ const psbt = new bitcoin.Psbt({ network });
109
+
110
+ psbt.setVersion(msg.version ?? 2);
111
+ if (msg.locktime) {
112
+ psbt.setLocktime(msg.locktime);
113
+ }
114
+
115
+ for (const input of msg.inputs) {
116
+ await addInput(psbt, input);
117
+ }
118
+
119
+ for (const output of msg.outputs) {
120
+ await addOutput(wallet, psbt, output, msg.coin);
121
+ }
122
+
123
+ if (msg.opReturnData) {
124
+ const data = Buffer.from(msg.opReturnData, "utf-8");
125
+ const embed = bitcoin.payments.embed({ data: [data] });
126
+ const script = embed.output;
127
+ if (!script) throw new Error("unable to build OP_RETURN script");
128
+ psbt.addOutput({ script, value: 0 });
129
+ }
130
+
131
+ const inputsToSign = await Promise.all(
132
+ msg.inputs.map(async (input, index) => {
133
+ const address = await wallet.btcGetAddress({
134
+ addressNList: input.addressNList,
135
+ coin: msg.coin,
136
+ showDisplay: false,
137
+ });
138
+
139
+ if (!address) throw new Error("Could not get address from wallet");
140
+
141
+ return {
142
+ address,
143
+ signingIndexes: [index],
144
+ sigHash: bitcoin.Transaction.SIGHASH_ALL,
145
+ };
146
+ })
147
+ );
148
+
149
+ const signedPsbtHex = await provider.signPSBT(fromHexString(psbt.toHex()), { inputsToSign });
150
+ const signedPsbt = bitcoin.Psbt.fromBuffer(Buffer.from(signedPsbtHex), { network });
151
+
152
+ signedPsbt.finalizeAllInputs();
153
+
154
+ const tx = signedPsbt.extractTransaction();
155
+
156
+ // If this is a THORChain transaction, validate the vout ordering
157
+ if (msg.vaultAddress && !core.validateVoutOrdering(msg, tx)) {
158
+ throw new Error("Improper vout ordering for BTC Thorchain transaction");
159
+ }
160
+
161
+ const signatures = signedPsbt.data.inputs.map((input) =>
162
+ input.partialSig ? input.partialSig[0].signature.toString("hex") : ""
163
+ );
164
+
165
+ return {
166
+ signatures,
167
+ serializedTx: tx.toHex(),
168
+ };
169
+ }
@@ -0,0 +1,111 @@
1
+ import * as core from "@shapeshiftoss/hdwallet-core";
2
+ import { ETHSignedMessage } from "@shapeshiftoss/hdwallet-core";
3
+ import { isHexString } from "ethers/lib/utils";
4
+
5
+ import { PhantomEvmProvider } from "./types";
6
+
7
+ export function ethGetAccountPaths(msg: core.ETHGetAccountPath): Array<core.ETHAccountPath> {
8
+ const slip44 = core.slip44ByCoin(msg.coin);
9
+ if (slip44 === undefined) return [];
10
+ return [
11
+ {
12
+ addressNList: [0x80000000 + 44, 0x80000000 + slip44, 0x80000000 + msg.accountIdx, 0, 0],
13
+ hardenedPath: [0x80000000 + 44, 0x80000000 + slip44, 0x80000000 + msg.accountIdx],
14
+ relPath: [0, 0],
15
+ description: "Phantom",
16
+ },
17
+ ];
18
+ }
19
+
20
+ export async function ethSendTx(
21
+ msg: core.ETHSignTx,
22
+ phantom: PhantomEvmProvider,
23
+ from: string
24
+ ): Promise<core.ETHTxHash | null> {
25
+ try {
26
+ const utxBase = {
27
+ from: from,
28
+ to: msg.to,
29
+ value: msg.value,
30
+ chainId: msg.chainId,
31
+ data: msg.data,
32
+ gasLimit: msg.gasLimit,
33
+ };
34
+
35
+ const utx = msg.maxFeePerGas
36
+ ? {
37
+ ...utxBase,
38
+ maxFeePerGas: msg.maxFeePerGas,
39
+ maxPriorityFeePerGas: msg.maxPriorityFeePerGas,
40
+ }
41
+ : { ...utxBase, gasPrice: msg.gasPrice };
42
+
43
+ const signedTx = await phantom.request?.({
44
+ method: "eth_sendTransaction",
45
+ params: [utx],
46
+ });
47
+
48
+ return { hash: signedTx } as core.ETHTxHash;
49
+ } catch (error) {
50
+ console.error(error);
51
+ return null;
52
+ }
53
+ }
54
+
55
+ export async function ethSignMessage(
56
+ msg: core.ETHSignMessage,
57
+ phantom: PhantomEvmProvider,
58
+ address: string
59
+ ): Promise<core.ETHSignedMessage | null> {
60
+ try {
61
+ if (!isHexString(msg.message)) throw new Error("data is not an hex string");
62
+ const signedMsg = await phantom.request?.({
63
+ method: "personal_sign",
64
+ params: [msg.message, address],
65
+ });
66
+
67
+ return {
68
+ address: address,
69
+ signature: signedMsg,
70
+ } as ETHSignedMessage;
71
+ } catch (error) {
72
+ console.error(error);
73
+ return null;
74
+ }
75
+ }
76
+
77
+ export async function ethSignTypedData(
78
+ msg: core.ETHSignTypedData,
79
+ phantom: PhantomEvmProvider,
80
+ address: string
81
+ ): Promise<core.ETHSignedMessage | null> {
82
+ try {
83
+ const signedMsg = await phantom.request?.({
84
+ method: "eth_signTypedData_v4",
85
+ params: [address, JSON.stringify(msg.typedData)],
86
+ });
87
+
88
+ return {
89
+ address: address,
90
+ signature: signedMsg,
91
+ } as ETHSignedMessage;
92
+ } catch (error) {
93
+ console.error(error);
94
+ return null;
95
+ }
96
+ }
97
+
98
+ export async function ethGetAddress(phantom: PhantomEvmProvider): Promise<string | null> {
99
+ if (!(phantom && phantom.request)) {
100
+ return null;
101
+ }
102
+ try {
103
+ const ethAccounts = await phantom.request({
104
+ method: "eth_accounts",
105
+ });
106
+ return ethAccounts[0];
107
+ } catch (error) {
108
+ console.error(error);
109
+ return null;
110
+ }
111
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./adapter";
2
+ export * from "./phantom";
@@ -0,0 +1,179 @@
1
+ import * as core from "@shapeshiftoss/hdwallet-core";
2
+
3
+ import { PhantomHDWallet } from ".";
4
+ import { PhantomUtxoProvider } from "./types";
5
+
6
+ describe("PhantomHDWallet", () => {
7
+ let wallet: PhantomHDWallet;
8
+
9
+ beforeEach(() => {
10
+ wallet = new PhantomHDWallet(
11
+ core.untouchable("PhantomHDWallet:provider"),
12
+ core.untouchable("PhantomHDWallet:provider")
13
+ );
14
+ });
15
+
16
+ it("should match the metadata", async () => {
17
+ expect(wallet.getVendor()).toBe("Phantom");
18
+ expect(wallet.hasOnDevicePinEntry()).toBe(false);
19
+ expect(wallet.hasOnDevicePassphrase()).toBe(true);
20
+ expect(wallet.hasOnDeviceDisplay()).toBe(true);
21
+ expect(wallet.hasOnDeviceRecovery()).toBe(true);
22
+ expect(await wallet.ethSupportsNetwork(1)).toBe(true);
23
+ expect(await wallet.ethSupportsSecureTransfer()).toBe(false);
24
+ expect(wallet.ethSupportsNativeShapeShift()).toBe(false);
25
+ expect(await wallet.ethSupportsEIP1559()).toBe(true);
26
+ expect(wallet.supportsOfflineSigning()).toBe(false);
27
+ expect(wallet.supportsBip44Accounts()).toBe(false);
28
+ expect(wallet.supportsBroadcast()).toBe(true);
29
+ });
30
+
31
+ it("should test ethSignMessage", async () => {
32
+ wallet.evmProvider = {
33
+ _metamask: {
34
+ isUnlocked: () => true,
35
+ },
36
+ request: jest.fn().mockReturnValue(
37
+ `Object {
38
+ "address": "0x73d0385F4d8E00C5e6504C6030F47BF6212736A8",
39
+ "signature": "0x05f51140905ffa33ffdc57f46b0b8d8fbb1d2a99f8cd843ca27893c01c31351c08b76d83dce412731c846e3b50649724415deb522d00950fbf4f2c1459c2b70b1b",
40
+ }`
41
+ ),
42
+ };
43
+ const msg = "0x737570657220736563726574206d657373616765"; // super secret message
44
+ expect(
45
+ await wallet.ethSignMessage({
46
+ addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"),
47
+ message: msg,
48
+ })
49
+ ).toMatchInlineSnapshot(`
50
+ Object {
51
+ "address": "O",
52
+ "signature": "Object {
53
+ \\"address\\": \\"0x73d0385F4d8E00C5e6504C6030F47BF6212736A8\\",
54
+ \\"signature\\": \\"0x05f51140905ffa33ffdc57f46b0b8d8fbb1d2a99f8cd843ca27893c01c31351c08b76d83dce412731c846e3b50649724415deb522d00950fbf4f2c1459c2b70b1b\\",
55
+ }",
56
+ }
57
+ `);
58
+ });
59
+
60
+ it("ethSignMessage returns null on error", async () => {
61
+ wallet.evmProvider = {
62
+ _metamask: {
63
+ isUnlocked: () => true,
64
+ },
65
+ request: jest.fn().mockRejectedValue(new Error("An Error has occurred")),
66
+ };
67
+
68
+ const msg = "0x737570657220736563726574206d657373616765"; // super secret message
69
+ const sig = await wallet.ethSignMessage({
70
+ addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"),
71
+ message: msg,
72
+ });
73
+
74
+ expect(sig).toBe(null);
75
+ });
76
+
77
+ it("ethGetAddress returns a valid address", async () => {
78
+ wallet.evmProvider = {
79
+ _metamask: {
80
+ isUnlocked: () => true,
81
+ },
82
+ request: jest.fn().mockReturnValue(["0x73d0385F4d8E00C5e6504C6030F47BF6212736A8"]),
83
+ };
84
+
85
+ const address = await wallet.ethGetAddress();
86
+
87
+ expect(address).toEqual("0x73d0385F4d8E00C5e6504C6030F47BF6212736A8");
88
+ });
89
+ it("btcGetAddress returns a valid address", async () => {
90
+ wallet.bitcoinProvider = {
91
+ requestAccounts: jest.fn().mockReturnValue([
92
+ {
93
+ purpose: "payment",
94
+ address: "bc1q9sjm947kn2hz84syykmem7dshvevm8xm5dkrpg",
95
+ },
96
+ ]),
97
+ } as unknown as PhantomUtxoProvider;
98
+
99
+ const address = await wallet.btcGetAddress({
100
+ coin: "Bitcoin",
101
+ } as core.BTCGetAddress);
102
+
103
+ expect(address).toEqual("bc1q9sjm947kn2hz84syykmem7dshvevm8xm5dkrpg");
104
+ });
105
+
106
+ it("ethSendTx returns a valid hash", async () => {
107
+ wallet.evmProvider = {
108
+ _metamask: {
109
+ isUnlocked: () => true,
110
+ },
111
+ request: jest.fn().mockReturnValue("0x123"),
112
+ };
113
+
114
+ const hash = await wallet.ethSendTx({
115
+ addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"),
116
+ nonce: "0xDEADBEEF",
117
+ gasPrice: "0xDEADBEEF",
118
+ gasLimit: "0xDEADBEEF",
119
+ to: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
120
+ value: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
121
+ data: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
122
+ chainId: 1,
123
+ });
124
+ expect(wallet.evmProvider.request).toHaveBeenCalled();
125
+ expect(hash).toMatchObject({ hash: "0x123" });
126
+ });
127
+ it("ethSendTx returns a valid hash if maxFeePerGas is present in msg", async () => {
128
+ wallet.evmProvider = {
129
+ _metamask: {
130
+ isUnlocked: () => true,
131
+ },
132
+ request: jest.fn().mockReturnValue("0x123"),
133
+ };
134
+
135
+ const hash = await wallet.ethSendTx({
136
+ addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"),
137
+ nonce: "0xDEADBEEF",
138
+ gasLimit: "0xDEADBEEF",
139
+ maxFeePerGas: "0xDEADBEEF",
140
+ to: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
141
+ value: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
142
+ data: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
143
+ chainId: 1,
144
+ });
145
+ expect(wallet.evmProvider.request).toHaveBeenCalled();
146
+ expect(hash).toMatchObject({ hash: "0x123" });
147
+ });
148
+ it("ethSendTx returns null on error", async () => {
149
+ wallet.evmProvider = {
150
+ _metamask: {
151
+ isUnlocked: () => true,
152
+ },
153
+ request: jest.fn().mockRejectedValue(new Error("An Error has occurred")),
154
+ };
155
+
156
+ const hash = await wallet.ethSendTx({
157
+ addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"),
158
+ nonce: "0xDEADBEEF",
159
+ gasPrice: "0xDEADBEEF",
160
+ gasLimit: "0xDEADBEEF",
161
+ to: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
162
+ value: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
163
+ data: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
164
+ chainId: 1,
165
+ });
166
+ expect(wallet.evmProvider.request).toHaveBeenCalled();
167
+ expect(hash).toBe(null);
168
+ });
169
+ it("ethVerifyMessage returns true for a valid signature", async () => {
170
+ expect(
171
+ await wallet.ethVerifyMessage({
172
+ address: "0x2068dD92B6690255553141Dfcf00dF308281f763",
173
+ message: "Hello World",
174
+ signature:
175
+ "0x61f1dda82e9c3800e960894396c9ce8164fd1526fccb136c71b88442405f7d09721725629915d10bc7cecfca2818fe76bc5816ed96a1b0cebee9b03b052980131b",
176
+ })
177
+ ).toEqual(true);
178
+ });
179
+ });