diff --git a/examples/sandbox/index.html b/examples/sandbox/index.html index 0cdbe1c51..bf6675cf0 100644 --- a/examples/sandbox/index.html +++ b/examples/sandbox/index.html @@ -116,6 +116,7 @@

Select

+ diff --git a/examples/sandbox/index.ts b/examples/sandbox/index.ts index 8c181b212..5920c8983 100644 --- a/examples/sandbox/index.ts +++ b/examples/sandbox/index.ts @@ -11,6 +11,7 @@ import * as ledgerWebHID from "@shapeshiftoss/hdwallet-ledger-webhid"; import * as ledgerWebUSB from "@shapeshiftoss/hdwallet-ledger-webusb"; import * as metaMask from "@shapeshiftoss/hdwallet-metamask"; import * as native from "@shapeshiftoss/hdwallet-native"; +import * as phantom from "@shapeshiftoss/hdwallet-phantom"; import * as portis from "@shapeshiftoss/hdwallet-portis"; import * as tallyHo from "@shapeshiftoss/hdwallet-tallyho"; import * as trezorConnect from "@shapeshiftoss/hdwallet-trezor-connect"; @@ -125,6 +126,7 @@ const kkbridgeAdapter = keepkeyTcp.TCPKeepKeyAdapter.useKeyring(keyring); const kkemuAdapter = keepkeyTcp.TCPKeepKeyAdapter.useKeyring(keyring); const portisAdapter = portis.PortisAdapter.useKeyring(keyring, { portisAppId }); const metaMaskAdapter = metaMask.MetaMaskAdapter.useKeyring(keyring); +const phantomAdapter = phantom.PhantomAdapter.useKeyring(keyring); const tallyHoAdapter = tallyHo.TallyHoAdapter.useKeyring(keyring); const walletConnectAdapter = walletConnect.WalletConnectAdapter.useKeyring(keyring, walletConnectOptions); const walletConnectV2Adapter = walletConnectv2.WalletConnectV2Adapter.useKeyring(keyring, walletConnectV2Options); @@ -157,6 +159,7 @@ const $ledgerwebhid = $("#ledgerwebhid"); const $portis = $("#portis"); const $native = $("#native"); const $metaMask = $("#metaMask"); +const $phantom = $("#phantom"); const $coinbase = $("#coinbase"); const $tallyHo = $("#tallyHo"); const $walletConnect = $("#walletConnect"); @@ -241,6 +244,19 @@ $metaMask.on("click", async (e) => { } }); +$phantom.on("click", async (e) => { + e.preventDefault(); + wallet = await phantomAdapter.pairDevice(); + window["wallet"] = wallet; + let deviceID = "nothing"; + try { + deviceID = await wallet.getDeviceID(); + $("#keyring select").val(deviceID); + } catch (err) { + console.error(err); + } +}); + $coinbase.on("click", async (e) => { e.preventDefault(); wallet = await coinbaseAdapter.pairDevice(); @@ -403,6 +419,12 @@ async function deviceConnected(deviceId) { console.error("Could not initialize MetaMaskAdapter", e); } + try { + await phantomAdapter.initialize(); + } catch (e) { + console.error("Could not initialize PhantomAdapter", e); + } + try { await tallyHoAdapter.initialize(); } catch (e) { diff --git a/examples/sandbox/json/ethereum/OpenSea-ethSignTypedDataV4.json b/examples/sandbox/json/ethereum/OpenSea-ethSignTypedDataV4.json index 44b3a7400..bd20f0def 100644 --- a/examples/sandbox/json/ethereum/OpenSea-ethSignTypedDataV4.json +++ b/examples/sandbox/json/ethereum/OpenSea-ethSignTypedDataV4.json @@ -118,7 +118,7 @@ "domain": { "name": "Seaport", "version": "1.5", - "chainId": 137, + "chainId": 1, "verifyingContract": "0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC" }, "message": { diff --git a/examples/sandbox/json/ethereum/ethTx.json b/examples/sandbox/json/ethereum/ethTx.json index c43243e07..7420ccdd6 100644 --- a/examples/sandbox/json/ethereum/ethTx.json +++ b/examples/sandbox/json/ethereum/ethTx.json @@ -156,7 +156,8 @@ "verifyingContract": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "chainId": 1 }, - "primaryType": "EIP712Domain" + "primaryType": "EIP712Domain", + "message": {} } } }, diff --git a/examples/sandbox/package.json b/examples/sandbox/package.json index 70e96bf9d..ba5d2143e 100644 --- a/examples/sandbox/package.json +++ b/examples/sandbox/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-sandbox", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "private": true, "browserslist": "> 0.5%, last 2 versions, not dead", @@ -12,24 +12,25 @@ "dependencies": { "@esm2cjs/p-queue": "^7.3.0", "@metamask/eth-sig-util": "^7.0.0", - "@shapeshiftoss/hdwallet-coinbase": "1.55.5", - "@shapeshiftoss/hdwallet-core": "1.55.5", - "@shapeshiftoss/hdwallet-keepkey": "1.55.5", - "@shapeshiftoss/hdwallet-keepkey-tcp": "1.55.5", - "@shapeshiftoss/hdwallet-keepkey-webusb": "1.55.5", - "@shapeshiftoss/hdwallet-keplr": "1.55.5", - "@shapeshiftoss/hdwallet-ledger": "1.55.5", - "@shapeshiftoss/hdwallet-ledger-webhid": "1.55.5", - "@shapeshiftoss/hdwallet-ledger-webusb": "1.55.5", - "@shapeshiftoss/hdwallet-metamask": "1.55.5", - "@shapeshiftoss/hdwallet-native": "1.55.5", - "@shapeshiftoss/hdwallet-portis": "1.55.5", - "@shapeshiftoss/hdwallet-tallyho": "1.55.5", - "@shapeshiftoss/hdwallet-trezor": "1.55.5", - "@shapeshiftoss/hdwallet-trezor-connect": "1.55.5", - "@shapeshiftoss/hdwallet-walletconnect": "1.55.5", - "@shapeshiftoss/hdwallet-walletconnectv2": "1.55.5", - "@shapeshiftoss/hdwallet-xdefi": "1.55.5", + "@shapeshiftoss/hdwallet-coinbase": "1.55.6", + "@shapeshiftoss/hdwallet-core": "1.55.6", + "@shapeshiftoss/hdwallet-keepkey": "1.55.6", + "@shapeshiftoss/hdwallet-keepkey-tcp": "1.55.6", + "@shapeshiftoss/hdwallet-keepkey-webusb": "1.55.6", + "@shapeshiftoss/hdwallet-keplr": "1.55.6", + "@shapeshiftoss/hdwallet-ledger": "1.55.6", + "@shapeshiftoss/hdwallet-ledger-webhid": "1.55.6", + "@shapeshiftoss/hdwallet-ledger-webusb": "1.55.6", + "@shapeshiftoss/hdwallet-metamask": "1.55.6", + "@shapeshiftoss/hdwallet-native": "1.55.6", + "@shapeshiftoss/hdwallet-phantom": "1.55.6", + "@shapeshiftoss/hdwallet-portis": "1.55.6", + "@shapeshiftoss/hdwallet-tallyho": "1.55.6", + "@shapeshiftoss/hdwallet-trezor": "1.55.6", + "@shapeshiftoss/hdwallet-trezor-connect": "1.55.6", + "@shapeshiftoss/hdwallet-walletconnect": "1.55.6", + "@shapeshiftoss/hdwallet-walletconnectv2": "1.55.6", + "@shapeshiftoss/hdwallet-xdefi": "1.55.6", "bip32": "^2.0.4", "eip-712": "^1.0.0", "jquery": "^3.7.1", diff --git a/integration/package.json b/integration/package.json index 8a964fe63..5e1b8b031 100644 --- a/integration/package.json +++ b/integration/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/integration", - "version": "1.55.5", + "version": "1.55.6", "main": "index.js", "license": "MIT", "private": true, @@ -10,15 +10,15 @@ "dev": "lerna run test --scope integration --parallel --include-filtered-dependencies" }, "dependencies": { - "@shapeshiftoss/hdwallet-core": "1.55.5", - "@shapeshiftoss/hdwallet-keepkey": "1.55.5", - "@shapeshiftoss/hdwallet-keepkey-nodewebusb": "1.55.5", - "@shapeshiftoss/hdwallet-keepkey-tcp": "1.55.5", - "@shapeshiftoss/hdwallet-ledger": "1.55.5", - "@shapeshiftoss/hdwallet-native": "1.55.5", - "@shapeshiftoss/hdwallet-portis": "1.55.5", - "@shapeshiftoss/hdwallet-trezor": "1.55.5", - "@shapeshiftoss/hdwallet-xdefi": "1.55.5", + "@shapeshiftoss/hdwallet-core": "1.55.6", + "@shapeshiftoss/hdwallet-keepkey": "1.55.6", + "@shapeshiftoss/hdwallet-keepkey-nodewebusb": "1.55.6", + "@shapeshiftoss/hdwallet-keepkey-tcp": "1.55.6", + "@shapeshiftoss/hdwallet-ledger": "1.55.6", + "@shapeshiftoss/hdwallet-native": "1.55.6", + "@shapeshiftoss/hdwallet-portis": "1.55.6", + "@shapeshiftoss/hdwallet-trezor": "1.55.6", + "@shapeshiftoss/hdwallet-xdefi": "1.55.6", "fast-json-stable-stringify": "^2.1.0", "msw": "^0.27.1", "whatwg-fetch": "^3.6.2" diff --git a/lerna.json b/lerna.json index d474d95b4..6b41bc3e9 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "lerna": "5.2.0", - "version": "1.55.5", + "version": "1.55.6", "npmClient": "yarn", "useWorkspaces": true, "command": { diff --git a/packages/hdwallet-coinbase/package.json b/packages/hdwallet-coinbase/package.json index f93eab7bd..baa8a2acb 100644 --- a/packages/hdwallet-coinbase/package.json +++ b/packages/hdwallet-coinbase/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-coinbase", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "publishConfig": { "access": "public" @@ -15,7 +15,7 @@ }, "dependencies": { "@coinbase/wallet-sdk": "^3.6.6", - "@shapeshiftoss/hdwallet-core": "1.55.5", + "@shapeshiftoss/hdwallet-core": "1.55.6", "eth-rpc-errors": "^4.0.3", "lodash": "^4.17.21" }, diff --git a/packages/hdwallet-coinbase/src/coinbase.ts b/packages/hdwallet-coinbase/src/coinbase.ts index 2e148c75c..aca7b9431 100644 --- a/packages/hdwallet-coinbase/src/coinbase.ts +++ b/packages/hdwallet-coinbase/src/coinbase.ts @@ -344,7 +344,6 @@ export class CoinbaseHDWallet implements core.HDWallet, core.ETHWallet { return this.info.ethNextAccountPath(msg); } - // TODO: Respect msg.addressNList! // eslint-disable-next-line @typescript-eslint/no-unused-vars public async ethGetAddress(msg: core.ETHGetAddress): Promise { if (this.ethAddress) { diff --git a/packages/hdwallet-core/package.json b/packages/hdwallet-core/package.json index 0655e3ae6..53c5a67e5 100644 --- a/packages/hdwallet-core/package.json +++ b/packages/hdwallet-core/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-core", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "publishConfig": { "access": "public" @@ -14,8 +14,10 @@ "prepublishOnly": "yarn clean && yarn build" }, "dependencies": { + "@shapeshiftoss/bitcoinjs-lib": "5.2.0-shapeshift.2", "@shapeshiftoss/proto-tx-builder": "^0.8.0", "eip-712": "^1.0.0", + "ethers": "5.7.2", "eventemitter2": "^5.0.1", "lodash": "^4.17.21", "rxjs": "^6.4.0", diff --git a/packages/hdwallet-core/src/bitcoin.ts b/packages/hdwallet-core/src/bitcoin.ts index 49c9971fb..5725d42a5 100644 --- a/packages/hdwallet-core/src/bitcoin.ts +++ b/packages/hdwallet-core/src/bitcoin.ts @@ -1,3 +1,4 @@ +import * as bitcoin from "@shapeshiftoss/bitcoinjs-lib"; import * as ta from "type-assertions"; import { addressNListToBIP32, slip44ByCoin } from "./utils"; @@ -488,3 +489,50 @@ export function segwitNativeAccount(coin: Coin, slip44: number, accountIdx: numb addressNList: [0x80000000 + 84, 0x80000000 + slip44, 0x80000000 + accountIdx], }; } + +export function validateVoutOrdering(msg: BTCSignTxNative, tx: bitcoin.Transaction): boolean { + // From THORChain specification: + /* ignoreTx checks if we can already ignore a tx according to preset rules + + we expect array of "vout" for a BTC to have this format + OP_RETURN is mandatory only on inbound tx + vout:0 is our vault + vout:1 is any any change back to themselves + vout:2 is OP_RETURN (first 80 bytes) + vout:3 is OP_RETURN (next 80 bytes) + + Rules to ignore a tx are: + - vout:0 doesn't have coins (value) + - vout:0 doesn't have address + - count vouts > 4 + - count vouts with coins (value) > 2 + */ + + // Check that vout:0 contains the vault address + if (bitcoin.address.fromOutputScript(tx.outs[0].script) != msg.vaultAddress) { + console.error("Vout:0 does not contain vault address."); + return false; + } + + // TODO: Can we check and make sure vout:1 is our address? + + // Check and make sure vout:2 exists + if (tx.outs.length < 3) { + console.error("Not enough outputs found in transaction.", msg); + return false; + } + // Check and make sure vout:2 has OP_RETURN data + const opcode = bitcoin.script.decompile(tx.outs[2].script)?.[0]; + if (Object.keys(bitcoin.script.OPS).find((k) => bitcoin.script.OPS[k] === opcode) != "OP_RETURN") { + console.error("OP_RETURN output not found for transaction."); + return false; + } + + // Make sure vout:3 does not exist + if (tx.outs[3]) { + console.error("Illegal second op_return output found."); + return false; + } + + return true; +} diff --git a/packages/hdwallet-core/src/ethereum.ts b/packages/hdwallet-core/src/ethereum.ts index 16d15a465..22bf307f2 100644 --- a/packages/hdwallet-core/src/ethereum.ts +++ b/packages/hdwallet-core/src/ethereum.ts @@ -1,5 +1,6 @@ import { Bytes } from "@ethersproject/bytes"; import { TypedData } from "eip-712"; +import { ethers } from "ethers"; import { addressNListToBIP32, slip44ByCoin } from "./utils"; import { BIP32Path, HDWallet, HDWalletInfo, PathDescription } from "./wallet"; @@ -240,3 +241,16 @@ export function describeETHPath(path: BIP32Path): PathDescription { isPrefork: false, }; } + +export function buildMessage(message: ethers.utils.BytesLike): Uint8Array { + const messageBytes = + typeof message === "string" && !ethers.utils.isHexString(message) + ? ethers.utils.toUtf8Bytes(message) + : ethers.utils.arrayify(message); + + return ethers.utils.concat([ + ethers.utils.toUtf8Bytes("\x19Ethereum Signed Message:\n"), + ethers.utils.toUtf8Bytes(String(messageBytes.length)), + messageBytes, + ]); +} diff --git a/packages/hdwallet-keepkey-chromeusb/package.json b/packages/hdwallet-keepkey-chromeusb/package.json index d7391cf82..52e79af52 100644 --- a/packages/hdwallet-keepkey-chromeusb/package.json +++ b/packages/hdwallet-keepkey-chromeusb/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-keepkey-chromeusb", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "publishConfig": { "access": "public" @@ -14,7 +14,7 @@ "prepublishOnly": "yarn clean && yarn build" }, "dependencies": { - "@shapeshiftoss/hdwallet-core": "1.55.5", - "@shapeshiftoss/hdwallet-keepkey": "1.55.5" + "@shapeshiftoss/hdwallet-core": "1.55.6", + "@shapeshiftoss/hdwallet-keepkey": "1.55.6" } } diff --git a/packages/hdwallet-keepkey-electron/package.json b/packages/hdwallet-keepkey-electron/package.json index ac024a86a..27849cefe 100644 --- a/packages/hdwallet-keepkey-electron/package.json +++ b/packages/hdwallet-keepkey-electron/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-keepkey-electron", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "publishConfig": { "access": "public" @@ -14,7 +14,7 @@ "prepublishOnly": "yarn clean && yarn build" }, "dependencies": { - "@shapeshiftoss/hdwallet-keepkey": "1.55.5", + "@shapeshiftoss/hdwallet-keepkey": "1.55.6", "uuid": "^8.3.2" }, "peerDependencies": { diff --git a/packages/hdwallet-keepkey-nodehid/package.json b/packages/hdwallet-keepkey-nodehid/package.json index 3d3b638e4..9ed6ce3e4 100644 --- a/packages/hdwallet-keepkey-nodehid/package.json +++ b/packages/hdwallet-keepkey-nodehid/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-keepkey-nodehid", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "publishConfig": { "access": "public" @@ -14,7 +14,7 @@ "prepublishOnly": "yarn clean && yarn build" }, "dependencies": { - "@shapeshiftoss/hdwallet-keepkey": "1.55.5" + "@shapeshiftoss/hdwallet-keepkey": "1.55.6" }, "peerDependencies": { "node-hid": "^2.1.1" diff --git a/packages/hdwallet-keepkey-nodewebusb/package.json b/packages/hdwallet-keepkey-nodewebusb/package.json index 5b662d7bd..25822cb38 100644 --- a/packages/hdwallet-keepkey-nodewebusb/package.json +++ b/packages/hdwallet-keepkey-nodewebusb/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-keepkey-nodewebusb", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "publishConfig": { "access": "public" @@ -14,8 +14,8 @@ "prepublishOnly": "yarn clean && yarn build" }, "dependencies": { - "@shapeshiftoss/hdwallet-core": "1.55.5", - "@shapeshiftoss/hdwallet-keepkey": "1.55.5" + "@shapeshiftoss/hdwallet-core": "1.55.6", + "@shapeshiftoss/hdwallet-keepkey": "1.55.6" }, "peerDependencies": { "usb": "^2.3.1" diff --git a/packages/hdwallet-keepkey-tcp/package.json b/packages/hdwallet-keepkey-tcp/package.json index af259f49a..6e4d800f5 100644 --- a/packages/hdwallet-keepkey-tcp/package.json +++ b/packages/hdwallet-keepkey-tcp/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-keepkey-tcp", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "publishConfig": { "access": "public" @@ -14,8 +14,8 @@ "prepublishOnly": "yarn clean && yarn build" }, "dependencies": { - "@shapeshiftoss/hdwallet-core": "1.55.5", - "@shapeshiftoss/hdwallet-keepkey": "1.55.5", + "@shapeshiftoss/hdwallet-core": "1.55.6", + "@shapeshiftoss/hdwallet-keepkey": "1.55.6", "axios": "^0.21.1" } } diff --git a/packages/hdwallet-keepkey-webusb/package.json b/packages/hdwallet-keepkey-webusb/package.json index 3fc429e7d..7c08eccc4 100644 --- a/packages/hdwallet-keepkey-webusb/package.json +++ b/packages/hdwallet-keepkey-webusb/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-keepkey-webusb", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "publishConfig": { "access": "public" @@ -14,8 +14,8 @@ "prepublishOnly": "yarn clean && yarn build" }, "dependencies": { - "@shapeshiftoss/hdwallet-core": "1.55.5", - "@shapeshiftoss/hdwallet-keepkey": "1.55.5" + "@shapeshiftoss/hdwallet-core": "1.55.6", + "@shapeshiftoss/hdwallet-keepkey": "1.55.6" }, "devDependencies": { "@types/w3c-web-usb": "^1.0.4" diff --git a/packages/hdwallet-keepkey/package.json b/packages/hdwallet-keepkey/package.json index f9fbb8722..ea0ca354f 100644 --- a/packages/hdwallet-keepkey/package.json +++ b/packages/hdwallet-keepkey/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-keepkey", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "publishConfig": { "access": "public" @@ -20,7 +20,7 @@ "@keepkey/device-protocol": "^7.12.2", "@metamask/eth-sig-util": "^7.0.0", "@shapeshiftoss/bitcoinjs-lib": "5.2.0-shapeshift.2", - "@shapeshiftoss/hdwallet-core": "1.55.5", + "@shapeshiftoss/hdwallet-core": "1.55.6", "@shapeshiftoss/proto-tx-builder": "^0.8.0", "bignumber.js": "^9.0.1", "bnb-javascript-sdk-nobroadcast": "^2.16.14", diff --git a/packages/hdwallet-keplr/package.json b/packages/hdwallet-keplr/package.json index 5867ef595..88b4a7434 100644 --- a/packages/hdwallet-keplr/package.json +++ b/packages/hdwallet-keplr/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-keplr", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "publishConfig": { "access": "public" @@ -15,7 +15,7 @@ }, "dependencies": { "@shapeshiftoss/caip": "8.15.0", - "@shapeshiftoss/hdwallet-core": "1.55.5", + "@shapeshiftoss/hdwallet-core": "1.55.6", "@shapeshiftoss/proto-tx-builder": "^0.8.0", "@shapeshiftoss/types": "3.1.3", "base64-js": "^1.5.1", diff --git a/packages/hdwallet-ledger-webhid/package.json b/packages/hdwallet-ledger-webhid/package.json index 9302ca9c3..b71ecd952 100644 --- a/packages/hdwallet-ledger-webhid/package.json +++ b/packages/hdwallet-ledger-webhid/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-ledger-webhid", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "publishConfig": { "access": "public" @@ -19,8 +19,8 @@ "@ledgerhq/hw-transport": "^6.31.2", "@ledgerhq/hw-transport-webhid": "^6.29.2", "@ledgerhq/live-common": "^21.8.2", - "@shapeshiftoss/hdwallet-core": "1.55.5", - "@shapeshiftoss/hdwallet-ledger": "1.55.5", + "@shapeshiftoss/hdwallet-core": "1.55.6", + "@shapeshiftoss/hdwallet-ledger": "1.55.6", "@types/w3c-web-hid": "^1.0.2" }, "devDependencies": { diff --git a/packages/hdwallet-ledger-webusb/package.json b/packages/hdwallet-ledger-webusb/package.json index 1bb60f3b0..cb39bdbba 100644 --- a/packages/hdwallet-ledger-webusb/package.json +++ b/packages/hdwallet-ledger-webusb/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-ledger-webusb", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "publishConfig": { "access": "public" @@ -20,8 +20,8 @@ "@ledgerhq/hw-transport-webusb": "^6.29.2", "@ledgerhq/live-common": "^21.8.2", "@ledgerhq/logs": "^6.10.1", - "@shapeshiftoss/hdwallet-core": "1.55.5", - "@shapeshiftoss/hdwallet-ledger": "1.55.5", + "@shapeshiftoss/hdwallet-core": "1.55.6", + "@shapeshiftoss/hdwallet-ledger": "1.55.6", "@types/w3c-web-usb": "^1.0.4", "p-queue": "^7.4.1" }, diff --git a/packages/hdwallet-ledger/package.json b/packages/hdwallet-ledger/package.json index bd838a6b7..fd547d79c 100644 --- a/packages/hdwallet-ledger/package.json +++ b/packages/hdwallet-ledger/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-ledger", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "publishConfig": { "access": "public" @@ -19,7 +19,7 @@ "@ethereumjs/tx": "^3.3.0", "@ledgerhq/hw-app-cosmos": "^6.29.1", "@shapeshiftoss/bitcoinjs-lib": "5.2.0-shapeshift.2", - "@shapeshiftoss/hdwallet-core": "1.55.5", + "@shapeshiftoss/hdwallet-core": "1.55.6", "base64-js": "^1.5.1", "bchaddrjs": "^0.4.4", "bitcoinjs-message": "^2.0.0", diff --git a/packages/hdwallet-metamask-shapeshift-multichain/package.json b/packages/hdwallet-metamask-shapeshift-multichain/package.json index f16c724c3..66734796b 100644 --- a/packages/hdwallet-metamask-shapeshift-multichain/package.json +++ b/packages/hdwallet-metamask-shapeshift-multichain/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-shapeshift-multichain", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "publishConfig": { "access": "public" @@ -17,7 +17,7 @@ "@metamask/detect-provider": "^1.2.0", "@metamask/onboarding": "^1.0.1", "@shapeshiftoss/common-api": "^9.3.0", - "@shapeshiftoss/hdwallet-core": "1.55.5", + "@shapeshiftoss/hdwallet-core": "1.55.6", "@shapeshiftoss/metamask-snaps-adapter": "^1.0.10", "@shapeshiftoss/metamask-snaps-types": "^1.0.10", "eth-rpc-errors": "^4.0.3", diff --git a/packages/hdwallet-metamask/package.json b/packages/hdwallet-metamask/package.json index 5927fc4e3..528a3d837 100644 --- a/packages/hdwallet-metamask/package.json +++ b/packages/hdwallet-metamask/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-metamask", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "publishConfig": { "access": "public" @@ -16,7 +16,7 @@ "dependencies": { "@metamask/detect-provider": "^1.2.0", "@metamask/onboarding": "^1.0.1", - "@shapeshiftoss/hdwallet-core": "1.55.5", + "@shapeshiftoss/hdwallet-core": "1.55.6", "eth-rpc-errors": "^4.0.3", "lodash": "^4.17.21" }, diff --git a/packages/hdwallet-metamask/src/metamask.ts b/packages/hdwallet-metamask/src/metamask.ts index d305e7285..3592388e2 100644 --- a/packages/hdwallet-metamask/src/metamask.ts +++ b/packages/hdwallet-metamask/src/metamask.ts @@ -345,9 +345,8 @@ export class MetaMaskHDWallet implements core.HDWallet, core.ETHWallet { return this.info.ethNextAccountPath(msg); } - // TODO: Respect msg.addressNList! // eslint-disable-next-line @typescript-eslint/no-unused-vars - public async ethGetAddress(msg: core.ETHGetAddress): Promise { + public async ethGetAddress(_msg: core.ETHGetAddress): Promise { if (this.ethAddress) { return this.ethAddress; } diff --git a/packages/hdwallet-native-vault/package.json b/packages/hdwallet-native-vault/package.json index 1bec662a3..67fe8c531 100644 --- a/packages/hdwallet-native-vault/package.json +++ b/packages/hdwallet-native-vault/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-native-vault", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "publishConfig": { "access": "public" @@ -14,7 +14,7 @@ "prepublishOnly": "yarn clean && yarn build" }, "dependencies": { - "@shapeshiftoss/hdwallet-native": "1.55.5", + "@shapeshiftoss/hdwallet-native": "1.55.6", "bip39": "^3.0.4", "hash-wasm": "^4.11.0", "idb-keyval": "^6.0.3", diff --git a/packages/hdwallet-native/package.json b/packages/hdwallet-native/package.json index 7043d1b29..72d8c5677 100644 --- a/packages/hdwallet-native/package.json +++ b/packages/hdwallet-native/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-native", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "publishConfig": { "access": "public" @@ -16,7 +16,7 @@ "dependencies": { "@shapeshiftoss/bitcoinjs-lib": "5.2.0-shapeshift.2", "@shapeshiftoss/fiosdk": "1.2.1-shapeshift.6", - "@shapeshiftoss/hdwallet-core": "1.55.5", + "@shapeshiftoss/hdwallet-core": "1.55.6", "@shapeshiftoss/proto-tx-builder": "^0.8.0", "@zxing/text-encoding": "^0.9.0", "bchaddrjs": "^0.4.9", diff --git a/packages/hdwallet-native/src/bitcoin.ts b/packages/hdwallet-native/src/bitcoin.ts index 182ea6bbd..7275d9af4 100644 --- a/packages/hdwallet-native/src/bitcoin.ts +++ b/packages/hdwallet-native/src/bitcoin.ts @@ -171,53 +171,6 @@ export function MixinNativeBTCWallet 4 - - count vouts with coins (value) > 2 - */ - - // Check that vout:0 contains the vault address - if (bitcoin.address.fromOutputScript(tx.outs[0].script) != msg.vaultAddress) { - console.error("Vout:0 does not contain vault address."); - return false; - } - - // TODO: Can we check and make sure vout:1 is our address? - - // Check and make sure vout:2 exists - if (tx.outs.length < 3) { - console.error("Not enough outputs found in transaction.", msg); - return false; - } - // Check and make sure vout:2 has OP_RETURN data - const opcode = bitcoin.script.decompile(tx.outs[2].script)?.[0]; - if (Object.keys(bitcoin.script.OPS).find((k) => bitcoin.script.OPS[k] === opcode) != "OP_RETURN") { - console.error("OP_RETURN output not found for transaction."); - return false; - } - - // Make sure vout:3 does not exist - if (tx.outs[3]) { - console.error("Illegal second op_return output found."); - return false; - } - - return true; - } - async buildInput(coin: core.Coin, input: core.BTCSignTxInputNative): Promise { return this.needsMnemonic(!!this.#masterKey, async () => { const { addressNList, amount, hex, scriptType } = input; @@ -355,7 +308,7 @@ export function MixinNativeBTCWallet { - const messageBuf = buildMessage(messageData); + const messageBuf = core.buildMessage(messageData); const nodeAdapter = await this.nodeAdapter.derivePath(core.addressNListToBIP32(addressNList)); const rawSig = await SecP256K1.RecoverableSignature.signCanonically(nodeAdapter.node, "keccak256", messageBuf); return joinSignature(ethSigFromRecoverableSig(rawSig)); diff --git a/packages/hdwallet-native/src/ethereum.ts b/packages/hdwallet-native/src/ethereum.ts index ff35ea5ff..f70a5b18b 100644 --- a/packages/hdwallet-native/src/ethereum.ts +++ b/packages/hdwallet-native/src/ethereum.ts @@ -4,7 +4,6 @@ import { keccak256, parseTransaction, recoverAddress } from "ethers/lib/utils.js import * as Isolation from "./crypto/isolation"; import SignerAdapter from "./crypto/isolation/adapters/ethereum"; import { NativeHDWalletBase } from "./native"; -import { buildMessage } from "./util"; export function MixinNativeETHWalletInfo>(Base: TBase) { // eslint-disable-next-line @typescript-eslint/no-shadow @@ -142,7 +141,7 @@ export function MixinNativeETHWallet { if (!signature.startsWith("0x")) signature = `0x${signature}`; - const digest = keccak256(buildMessage(message)); + const digest = keccak256(core.buildMessage(message)); return recoverAddress(digest, signature) === address; } }; diff --git a/packages/hdwallet-native/src/util.ts b/packages/hdwallet-native/src/util.ts index 1a7c7862f..a6a0c4e46 100644 --- a/packages/hdwallet-native/src/util.ts +++ b/packages/hdwallet-native/src/util.ts @@ -1,5 +1,4 @@ import * as core from "@shapeshiftoss/hdwallet-core"; -import { ethers } from "ethers"; import { BTCScriptType } from "./bitcoin"; import * as Isolation from "./crypto/isolation"; @@ -16,16 +15,3 @@ export async function getKeyPair( const path = core.addressNListToBIP32(addressNList); return await wallet.derivePath(path); } - -export function buildMessage(message: ethers.utils.BytesLike): Uint8Array { - const messageBytes = - typeof message === "string" && !ethers.utils.isHexString(message) - ? ethers.utils.toUtf8Bytes(message) - : ethers.utils.arrayify(message); - - return ethers.utils.concat([ - ethers.utils.toUtf8Bytes("\x19Ethereum Signed Message:\n"), - ethers.utils.toUtf8Bytes(String(messageBytes.length)), - messageBytes, - ]); -} diff --git a/packages/hdwallet-phantom/package.json b/packages/hdwallet-phantom/package.json new file mode 100644 index 000000000..238194a75 --- /dev/null +++ b/packages/hdwallet-phantom/package.json @@ -0,0 +1,26 @@ +{ + "name": "@shapeshiftoss/hdwallet-phantom", + "version": "1.55.6", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "main": "dist/index.js", + "source": "src/index.ts", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc --build", + "clean": "rm -rf dist node_modules tsconfig.tsbuildinfo", + "prepublishOnly": "yarn clean && yarn build" + }, + "dependencies": { + "@shapeshiftoss/bitcoinjs-lib": "5.2.0-shapeshift.2", + "@shapeshiftoss/hdwallet-core": "1.55.6", + "base64-js": "^1.5.1", + "bitcoinjs-message": "^2.0.0", + "lodash": "^4.17.21" + }, + "devDependencies": { + "@types/lodash": "^4.14.168" + } +} diff --git a/packages/hdwallet-phantom/src/adapter.ts b/packages/hdwallet-phantom/src/adapter.ts new file mode 100644 index 000000000..1a65a5480 --- /dev/null +++ b/packages/hdwallet-phantom/src/adapter.ts @@ -0,0 +1,62 @@ +import * as core from "@shapeshiftoss/hdwallet-core"; + +import { PhantomHDWallet } from "./phantom"; +import { PhantomEvmProvider, PhantomUtxoProvider } from "./types"; + +declare global { + interface Window { + phantom?: { + ethereum?: PhantomEvmProvider; + bitcoin?: PhantomUtxoProvider; + // TODO: update with proper types once implemented + // https://github.com/anza-xyz/wallet-adapter/blob/3761cd8cc867da39da7c0b070bbf8779402cff36/packages/wallets/phantom/src/adapter.ts#L36 + solana?: any; + }; + } +} + +export class PhantomAdapter { + keyring: core.Keyring; + + private constructor(keyring: core.Keyring) { + this.keyring = keyring; + } + + public static useKeyring(keyring: core.Keyring) { + return new PhantomAdapter(keyring); + } + + public async initialize(): Promise { + return Object.keys(this.keyring.wallets).length; + } + + public async pairDevice(): Promise { + const evmProvider = window.phantom?.ethereum; + const bitcoinProvider = window.phantom?.bitcoin; + + if (!evmProvider || !bitcoinProvider) { + window.open("https://phantom.app/", "_blank"); + console.error("Please install Phantom!"); + throw new Error("Phantom provider not found"); + } + + try { + await evmProvider.request?.({ method: "eth_requestAccounts" }).catch(() => + evmProvider.request?.({ + method: "wallet_requestPermissions", + params: [{ eth_accounts: {} }], + }) + ); + } catch (error) { + console.error("Could not get Phantom accounts. "); + throw error; + } + const wallet = new PhantomHDWallet(evmProvider, bitcoinProvider); + await wallet.initialize(); + const deviceID = await wallet.getDeviceID(); + this.keyring.add(wallet, deviceID); + this.keyring.emit(["Phantom", deviceID, core.Events.CONNECT], deviceID); + + return wallet; + } +} diff --git a/packages/hdwallet-phantom/src/bitcoin.ts b/packages/hdwallet-phantom/src/bitcoin.ts new file mode 100644 index 000000000..7dd00781e --- /dev/null +++ b/packages/hdwallet-phantom/src/bitcoin.ts @@ -0,0 +1,169 @@ +import * as bitcoin from "@shapeshiftoss/bitcoinjs-lib"; +import * as core from "@shapeshiftoss/hdwallet-core"; +import { BTCInputScriptType } from "@shapeshiftoss/hdwallet-core"; + +import { PhantomUtxoProvider } from "./types"; + +export type BtcAccount = { + address: string; + // Phantom supposedly supports more scriptTypes but in effect, doesn't (currently) + // https://github.com/orgs/phantom/discussions/173 + addressType: BTCInputScriptType.SpendWitness; + publicKey: string; + purpose: "payment" | "ordinals"; +}; + +const fromHexString = (hexString: string) => { + const bytes = hexString.match(/.{1,2}/g); + if (!bytes) throw new Error("Invalid hex string"); + + return Uint8Array.from(bytes.map((byte) => parseInt(byte, 16))); +}; + +const getNetwork = (coin: string): bitcoin.networks.Network => { + switch (coin.toLowerCase()) { + case "bitcoin": + return bitcoin.networks.bitcoin; + default: + throw new Error(`Unsupported coin: ${coin}`); + } +}; + +export const btcGetAccountPaths = (msg: core.BTCGetAccountPaths): Array => { + const slip44 = core.slip44ByCoin(msg.coin); + if (slip44 === undefined) return []; + + const bip84 = core.segwitNativeAccount(msg.coin, slip44, msg.accountIdx); + + const coinPaths = { + bitcoin: [bip84], + } as Partial>>; + + let paths: Array = coinPaths[msg.coin.toLowerCase()] || []; + + if (msg.scriptType !== undefined) { + paths = paths.filter((path) => { + return path.scriptType === msg.scriptType; + }); + } + + return paths; +}; + +export async function bitcoinGetAddress(_msg: core.BTCGetAddress, provider: any): Promise { + const accounts = await provider.requestAccounts(); + const paymentAddress = accounts.find((account: BtcAccount) => account.purpose === "payment")?.address; + + return paymentAddress; +} + +async function addInput(psbt: bitcoin.Psbt, input: core.BTCSignTxInput): Promise { + switch (input.scriptType) { + // Phantom supposedly supports more scriptTypes but in effect, doesn't (currently) + // https://github.com/orgs/phantom/discussions/173 + case BTCInputScriptType.SpendWitness: { + psbt.addInput({ + hash: input.txid, + index: input.vout, + nonWitnessUtxo: Buffer.from(input.hex, "hex"), + }); + + break; + } + default: + throw new Error(`Unsupported script type: ${input.scriptType}`); + } +} + +async function addOutput( + wallet: core.BTCWallet, + psbt: bitcoin.Psbt, + output: core.BTCSignTxOutput, + coin: string +): Promise { + if (!output.amount) throw new Error("Invalid output - missing amount."); + + const address = await (async () => { + if (output.address) return output.address; + + if (output.addressNList) { + const outputAddress = await wallet.btcGetAddress({ addressNList: output.addressNList, coin, showDisplay: false }); + if (!outputAddress) throw new Error("Could not get address from wallet"); + return outputAddress; + } + })(); + + if (!address) throw new Error("Invalid output - no address"); + + psbt.addOutput({ address, value: parseInt(output.amount) }); +} + +export async function bitcoinSignTx( + wallet: core.BTCWallet, + msg: core.BTCSignTx, + provider: PhantomUtxoProvider +): Promise { + const network = getNetwork(msg.coin); + + const psbt = new bitcoin.Psbt({ network }); + + psbt.setVersion(msg.version ?? 2); + if (msg.locktime) { + psbt.setLocktime(msg.locktime); + } + + for (const input of msg.inputs) { + await addInput(psbt, input); + } + + for (const output of msg.outputs) { + await addOutput(wallet, psbt, output, msg.coin); + } + + if (msg.opReturnData) { + const data = Buffer.from(msg.opReturnData, "utf-8"); + const embed = bitcoin.payments.embed({ data: [data] }); + const script = embed.output; + if (!script) throw new Error("unable to build OP_RETURN script"); + psbt.addOutput({ script, value: 0 }); + } + + const inputsToSign = await Promise.all( + msg.inputs.map(async (input, index) => { + const address = await wallet.btcGetAddress({ + addressNList: input.addressNList, + coin: msg.coin, + showDisplay: false, + }); + + if (!address) throw new Error("Could not get address from wallet"); + + return { + address, + signingIndexes: [index], + sigHash: bitcoin.Transaction.SIGHASH_ALL, + }; + }) + ); + + const signedPsbtHex = await provider.signPSBT(fromHexString(psbt.toHex()), { inputsToSign }); + const signedPsbt = bitcoin.Psbt.fromBuffer(Buffer.from(signedPsbtHex), { network }); + + signedPsbt.finalizeAllInputs(); + + const tx = signedPsbt.extractTransaction(); + + // If this is a THORChain transaction, validate the vout ordering + if (msg.vaultAddress && !core.validateVoutOrdering(msg, tx)) { + throw new Error("Improper vout ordering for BTC Thorchain transaction"); + } + + const signatures = signedPsbt.data.inputs.map((input) => + input.partialSig ? input.partialSig[0].signature.toString("hex") : "" + ); + + return { + signatures, + serializedTx: tx.toHex(), + }; +} diff --git a/packages/hdwallet-phantom/src/ethereum.ts b/packages/hdwallet-phantom/src/ethereum.ts new file mode 100644 index 000000000..ee483f872 --- /dev/null +++ b/packages/hdwallet-phantom/src/ethereum.ts @@ -0,0 +1,111 @@ +import * as core from "@shapeshiftoss/hdwallet-core"; +import { ETHSignedMessage } from "@shapeshiftoss/hdwallet-core"; +import { isHexString } from "ethers/lib/utils"; + +import { PhantomEvmProvider } from "./types"; + +export function ethGetAccountPaths(msg: core.ETHGetAccountPath): Array { + const slip44 = core.slip44ByCoin(msg.coin); + if (slip44 === undefined) return []; + return [ + { + addressNList: [0x80000000 + 44, 0x80000000 + slip44, 0x80000000 + msg.accountIdx, 0, 0], + hardenedPath: [0x80000000 + 44, 0x80000000 + slip44, 0x80000000 + msg.accountIdx], + relPath: [0, 0], + description: "Phantom", + }, + ]; +} + +export async function ethSendTx( + msg: core.ETHSignTx, + phantom: PhantomEvmProvider, + from: string +): Promise { + try { + const utxBase = { + from: from, + to: msg.to, + value: msg.value, + chainId: msg.chainId, + data: msg.data, + gasLimit: msg.gasLimit, + }; + + const utx = msg.maxFeePerGas + ? { + ...utxBase, + maxFeePerGas: msg.maxFeePerGas, + maxPriorityFeePerGas: msg.maxPriorityFeePerGas, + } + : { ...utxBase, gasPrice: msg.gasPrice }; + + const signedTx = await phantom.request?.({ + method: "eth_sendTransaction", + params: [utx], + }); + + return { hash: signedTx } as core.ETHTxHash; + } catch (error) { + console.error(error); + return null; + } +} + +export async function ethSignMessage( + msg: core.ETHSignMessage, + phantom: PhantomEvmProvider, + address: string +): Promise { + try { + if (!isHexString(msg.message)) throw new Error("data is not an hex string"); + const signedMsg = await phantom.request?.({ + method: "personal_sign", + params: [msg.message, address], + }); + + return { + address: address, + signature: signedMsg, + } as ETHSignedMessage; + } catch (error) { + console.error(error); + return null; + } +} + +export async function ethSignTypedData( + msg: core.ETHSignTypedData, + phantom: PhantomEvmProvider, + address: string +): Promise { + try { + const signedMsg = await phantom.request?.({ + method: "eth_signTypedData_v4", + params: [address, JSON.stringify(msg.typedData)], + }); + + return { + address: address, + signature: signedMsg, + } as ETHSignedMessage; + } catch (error) { + console.error(error); + return null; + } +} + +export async function ethGetAddress(phantom: PhantomEvmProvider): Promise { + if (!(phantom && phantom.request)) { + return null; + } + try { + const ethAccounts = await phantom.request({ + method: "eth_accounts", + }); + return ethAccounts[0]; + } catch (error) { + console.error(error); + return null; + } +} diff --git a/packages/hdwallet-phantom/src/index.ts b/packages/hdwallet-phantom/src/index.ts new file mode 100644 index 000000000..ee1fe48d8 --- /dev/null +++ b/packages/hdwallet-phantom/src/index.ts @@ -0,0 +1,2 @@ +export * from "./adapter"; +export * from "./phantom"; diff --git a/packages/hdwallet-phantom/src/phantom.test.ts b/packages/hdwallet-phantom/src/phantom.test.ts new file mode 100644 index 000000000..202df1f20 --- /dev/null +++ b/packages/hdwallet-phantom/src/phantom.test.ts @@ -0,0 +1,179 @@ +import * as core from "@shapeshiftoss/hdwallet-core"; + +import { PhantomHDWallet } from "."; +import { PhantomUtxoProvider } from "./types"; + +describe("PhantomHDWallet", () => { + let wallet: PhantomHDWallet; + + beforeEach(() => { + wallet = new PhantomHDWallet( + core.untouchable("PhantomHDWallet:provider"), + core.untouchable("PhantomHDWallet:provider") + ); + }); + + it("should match the metadata", async () => { + expect(wallet.getVendor()).toBe("Phantom"); + expect(wallet.hasOnDevicePinEntry()).toBe(false); + expect(wallet.hasOnDevicePassphrase()).toBe(true); + expect(wallet.hasOnDeviceDisplay()).toBe(true); + expect(wallet.hasOnDeviceRecovery()).toBe(true); + expect(await wallet.ethSupportsNetwork(1)).toBe(true); + expect(await wallet.ethSupportsSecureTransfer()).toBe(false); + expect(wallet.ethSupportsNativeShapeShift()).toBe(false); + expect(await wallet.ethSupportsEIP1559()).toBe(true); + expect(wallet.supportsOfflineSigning()).toBe(false); + expect(wallet.supportsBip44Accounts()).toBe(false); + expect(wallet.supportsBroadcast()).toBe(true); + }); + + it("should test ethSignMessage", async () => { + wallet.evmProvider = { + _metamask: { + isUnlocked: () => true, + }, + request: jest.fn().mockReturnValue( + `Object { + "address": "0x73d0385F4d8E00C5e6504C6030F47BF6212736A8", + "signature": "0x05f51140905ffa33ffdc57f46b0b8d8fbb1d2a99f8cd843ca27893c01c31351c08b76d83dce412731c846e3b50649724415deb522d00950fbf4f2c1459c2b70b1b", + }` + ), + }; + const msg = "0x737570657220736563726574206d657373616765"; // super secret message + expect( + await wallet.ethSignMessage({ + addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"), + message: msg, + }) + ).toMatchInlineSnapshot(` + Object { + "address": "O", + "signature": "Object { + \\"address\\": \\"0x73d0385F4d8E00C5e6504C6030F47BF6212736A8\\", + \\"signature\\": \\"0x05f51140905ffa33ffdc57f46b0b8d8fbb1d2a99f8cd843ca27893c01c31351c08b76d83dce412731c846e3b50649724415deb522d00950fbf4f2c1459c2b70b1b\\", + }", + } + `); + }); + + it("ethSignMessage returns null on error", async () => { + wallet.evmProvider = { + _metamask: { + isUnlocked: () => true, + }, + request: jest.fn().mockRejectedValue(new Error("An Error has occurred")), + }; + + const msg = "0x737570657220736563726574206d657373616765"; // super secret message + const sig = await wallet.ethSignMessage({ + addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"), + message: msg, + }); + + expect(sig).toBe(null); + }); + + it("ethGetAddress returns a valid address", async () => { + wallet.evmProvider = { + _metamask: { + isUnlocked: () => true, + }, + request: jest.fn().mockReturnValue(["0x73d0385F4d8E00C5e6504C6030F47BF6212736A8"]), + }; + + const address = await wallet.ethGetAddress(); + + expect(address).toEqual("0x73d0385F4d8E00C5e6504C6030F47BF6212736A8"); + }); + it("btcGetAddress returns a valid address", async () => { + wallet.bitcoinProvider = { + requestAccounts: jest.fn().mockReturnValue([ + { + purpose: "payment", + address: "bc1q9sjm947kn2hz84syykmem7dshvevm8xm5dkrpg", + }, + ]), + } as unknown as PhantomUtxoProvider; + + const address = await wallet.btcGetAddress({ + coin: "Bitcoin", + } as core.BTCGetAddress); + + expect(address).toEqual("bc1q9sjm947kn2hz84syykmem7dshvevm8xm5dkrpg"); + }); + + it("ethSendTx returns a valid hash", async () => { + wallet.evmProvider = { + _metamask: { + isUnlocked: () => true, + }, + request: jest.fn().mockReturnValue("0x123"), + }; + + const hash = await wallet.ethSendTx({ + addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"), + nonce: "0xDEADBEEF", + gasPrice: "0xDEADBEEF", + gasLimit: "0xDEADBEEF", + to: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF", + value: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF", + data: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF", + chainId: 1, + }); + expect(wallet.evmProvider.request).toHaveBeenCalled(); + expect(hash).toMatchObject({ hash: "0x123" }); + }); + it("ethSendTx returns a valid hash if maxFeePerGas is present in msg", async () => { + wallet.evmProvider = { + _metamask: { + isUnlocked: () => true, + }, + request: jest.fn().mockReturnValue("0x123"), + }; + + const hash = await wallet.ethSendTx({ + addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"), + nonce: "0xDEADBEEF", + gasLimit: "0xDEADBEEF", + maxFeePerGas: "0xDEADBEEF", + to: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF", + value: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF", + data: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF", + chainId: 1, + }); + expect(wallet.evmProvider.request).toHaveBeenCalled(); + expect(hash).toMatchObject({ hash: "0x123" }); + }); + it("ethSendTx returns null on error", async () => { + wallet.evmProvider = { + _metamask: { + isUnlocked: () => true, + }, + request: jest.fn().mockRejectedValue(new Error("An Error has occurred")), + }; + + const hash = await wallet.ethSendTx({ + addressNList: core.bip32ToAddressNList("m/44'/60'/0'/0/0"), + nonce: "0xDEADBEEF", + gasPrice: "0xDEADBEEF", + gasLimit: "0xDEADBEEF", + to: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF", + value: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF", + data: "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEF", + chainId: 1, + }); + expect(wallet.evmProvider.request).toHaveBeenCalled(); + expect(hash).toBe(null); + }); + it("ethVerifyMessage returns true for a valid signature", async () => { + expect( + await wallet.ethVerifyMessage({ + address: "0x2068dD92B6690255553141Dfcf00dF308281f763", + message: "Hello World", + signature: + "0x61f1dda82e9c3800e960894396c9ce8164fd1526fccb136c71b88442405f7d09721725629915d10bc7cecfca2818fe76bc5816ed96a1b0cebee9b03b052980131b", + }) + ).toEqual(true); + }); +}); diff --git a/packages/hdwallet-phantom/src/phantom.ts b/packages/hdwallet-phantom/src/phantom.ts new file mode 100644 index 000000000..30c001106 --- /dev/null +++ b/packages/hdwallet-phantom/src/phantom.ts @@ -0,0 +1,347 @@ +import * as core from "@shapeshiftoss/hdwallet-core"; +import { BTCInputScriptType } from "@shapeshiftoss/hdwallet-core"; +import Base64 from "base64-js"; +import * as bitcoinMsg from "bitcoinjs-message"; +import { keccak256, recoverAddress } from "ethers/lib/utils.js"; +import _ from "lodash"; + +import * as btc from "./bitcoin"; +import * as eth from "./ethereum"; +import { PhantomEvmProvider, PhantomUtxoProvider } from "./types"; + +export function isPhantom(wallet: core.HDWallet): wallet is PhantomHDWallet { + return _.isObject(wallet) && (wallet as any)._isPhantom; +} + +export class PhantomHDWalletInfo implements core.HDWalletInfo, core.BTCWalletInfo, core.ETHWalletInfo { + readonly _supportsBTCInfo = true; + readonly _supportsETHInfo = true; + + evmProvider: PhantomEvmProvider; + + constructor(evmProvider: PhantomEvmProvider) { + this.evmProvider = evmProvider; + } + + public getVendor(): string { + return "Phantom"; + } + + public hasOnDevicePinEntry(): boolean { + return false; + } + + public hasOnDevicePassphrase(): boolean { + return true; + } + + public hasOnDeviceDisplay(): boolean { + return true; + } + + public hasOnDeviceRecovery(): boolean { + return true; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public hasNativeShapeShift(srcCoin: core.Coin, dstCoin: core.Coin): boolean { + return false; + } + + public supportsBip44Accounts(): boolean { + return false; + } + + public supportsOfflineSigning(): boolean { + return false; + } + + public supportsBroadcast(): boolean { + return true; + } + + public describePath(msg: core.DescribePath): core.PathDescription { + switch (msg.coin.toLowerCase()) { + case "bitcoin": { + const unknown = core.unknownUTXOPath(msg.path, msg.coin, msg.scriptType); + + if (!msg.scriptType) return unknown; + if (!this.btcSupportsCoin(msg.coin)) return unknown; + if (!this.btcSupportsScriptType(msg.coin, msg.scriptType)) return unknown; + + return core.describeUTXOPath(msg.path, msg.coin, msg.scriptType); + } + case "ethereum": + return core.describeETHPath(msg.path); + default: + throw new Error("Unsupported path"); + } + } + + /** Ethereum */ + + public async ethSupportsNetwork(chainId: number): Promise { + return chainId === 1; + } + + public async ethGetChainId(): Promise { + try { + if (!this.evmProvider.request) throw new Error("Provider does not support ethereum.request"); + // chainId as hex string + const chainId: string = await this.evmProvider.request({ method: "eth_chainId" }); + return parseInt(chainId, 16); + } catch (e) { + console.error(e); + return null; + } + } + + public async ethSupportsSecureTransfer(): Promise { + return false; + } + + public ethSupportsNativeShapeShift(): boolean { + return false; + } + + public async ethSupportsEIP1559(): Promise { + return true; + } + + public ethGetAccountPaths(msg: core.ETHGetAccountPath): Array { + return eth.ethGetAccountPaths(msg); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public ethNextAccountPath(msg: core.ETHAccountPath): core.ETHAccountPath | undefined { + throw new Error("Method not implemented"); + } + + /** Bitcoin */ + + public async btcSupportsCoin(coin: core.Coin): Promise { + return coin === "bitcoin"; + } + + public async btcSupportsScriptType(coin: string, scriptType?: core.BTCInputScriptType | undefined): Promise { + if (!this.btcSupportsCoin(coin)) return false; + + switch (scriptType) { + case core.BTCInputScriptType.SpendWitness: + return true; + default: + return false; + } + } + + public async btcSupportsSecureTransfer(): Promise { + return false; + } + + public btcSupportsNativeShapeShift(): boolean { + return false; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public btcGetAccountPaths(msg: core.BTCGetAccountPaths): Array { + return btc.btcGetAccountPaths(msg); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public btcIsSameAccount(msg: core.BTCAccountPath[]): boolean { + throw new Error("Method not implemented."); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public btcNextAccountPath(msg: core.BTCAccountPath): core.BTCAccountPath | undefined { + throw new Error("Method not implemented"); + } +} + +export class PhantomHDWallet extends PhantomHDWalletInfo implements core.HDWallet, core.BTCWallet, core.ETHWallet { + readonly _supportsBTC = true; + readonly _supportsETH = true; + readonly _supportsEthSwitchChain = false; + readonly _supportsAvalanche = false; + readonly _supportsOptimism = false; + readonly _supportsPolygon = true; + readonly _supportsGnosis = false; + readonly _supportsArbitrum = false; + readonly _supportsArbitrumNova = false; + readonly _supportsBase = false; + readonly _supportsBSC = false; + readonly _isPhantom = true; + + evmProvider: PhantomEvmProvider; + bitcoinProvider: PhantomUtxoProvider; + + constructor(evmProvider: PhantomEvmProvider, bitcoinProvider: PhantomUtxoProvider) { + super(evmProvider); + this.evmProvider = evmProvider; + this.bitcoinProvider = bitcoinProvider; + } + + public async getDeviceID(): Promise { + return "phantom:" + (await this.ethGetAddress()); + } + + async getFeatures(): Promise> { + return {}; + } + + public async getFirmwareVersion(): Promise { + return "phantom"; + } + + public async getModel(): Promise { + return "Phantom"; + } + + public async getLabel(): Promise { + return "Phantom"; + } + + public async isInitialized(): Promise { + return true; + } + + public async isLocked(): Promise { + return !this.evmProvider._metamask.isUnlocked(); + } + + public async clearSession(): Promise {} + + public async initialize(): Promise {} + + public async ping(msg: core.Ping): Promise { + return { msg: msg.msg }; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async sendPin(pin: string): Promise {} + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async sendPassphrase(passphrase: string): Promise {} + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async sendCharacter(charater: string): Promise {} + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async sendWord(word: string): Promise {} + + public async cancel(): Promise {} + + public async wipe(): Promise {} + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async reset(msg: core.ResetDevice): Promise {} + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async recover(msg: core.RecoverDevice): Promise {} + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async loadDevice(msg: core.LoadDevice): Promise {} + + public async disconnect(): Promise {} + + public async getPublicKeys(msg: Array): Promise> { + return await Promise.all( + msg.map(async (getPublicKey) => { + const { coin, scriptType } = getPublicKey; + + // Only p2wpkh effectively supported for now + if (coin === "Bitcoin" && scriptType === BTCInputScriptType.SpendWitness) { + // Note this is a pubKey, not an xpub, however phantom does not support utxo derivation, + // so this functions as an account (xpub) for all intents and purposes + const pubKey = await this.btcGetAddress({ coin: "Bitcoin" } as core.BTCGetAddress); + return { xpub: pubKey } as core.PublicKey; + } + + return null; + }) + ); + } + + /** Ethereum */ + + public async ethGetAddress(): Promise { + return eth.ethGetAddress(this.evmProvider); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async ethSignTx(msg: core.ETHSignTx): Promise { + throw new Error("Method not implemented"); + } + + public async ethSendTx(msg: core.ETHSignTx): Promise { + const address = await this.ethGetAddress(); + return address ? eth.ethSendTx(msg, this.evmProvider, address) : null; + } + + public async ethSignMessage(msg: core.ETHSignMessage): Promise { + const address = await this.ethGetAddress(); + return address ? eth.ethSignMessage(msg, this.evmProvider, address) : null; + } + + async ethSignTypedData(msg: core.ETHSignTypedData): Promise { + const address = await this.ethGetAddress(); + return address ? eth.ethSignTypedData(msg, this.evmProvider, address) : null; + } + + public async ethVerifyMessage(msg: core.ETHVerifyMessage): Promise { + if (!msg.signature.startsWith("0x")) msg.signature = `0x${msg.signature}`; + const digest = keccak256(core.buildMessage(msg.message)); + return recoverAddress(digest, msg.signature) === msg.address; + } + + /** Bitcoin */ + + public async btcGetAddress(msg: core.BTCGetAddress): Promise { + const value = await (async () => { + switch (msg.coin) { + case "Bitcoin": { + const accounts = await this.bitcoinProvider.requestAccounts(); + const paymentAddress = accounts.find((account) => account.purpose === "payment")?.address; + + return paymentAddress; + } + default: + return null; + } + })(); + if (!value || typeof value !== "string") return null; + + return value; + } + + public async btcSignTx(msg: core.BTCSignTx): Promise { + const { coin } = msg; + switch (coin) { + case "Bitcoin": + return btc.bitcoinSignTx(this, msg, this.bitcoinProvider); + default: + return null; + } + } + + public async btcSignMessage(msg: core.BTCSignMessage): Promise { + const { coin } = msg; + switch (coin) { + case "Bitcoin": { + const address = await this.btcGetAddress({ coin } as core.BTCGetAddress); + if (!address) throw new Error(`Could not get ${coin} address`); + const message = new TextEncoder().encode(msg.message); + + const { signature } = await this.bitcoinProvider.signMessage(address, message); + return { signature: core.toHexString(signature), address }; + } + default: + return null; + } + } + + public async btcVerifyMessage(msg: core.BTCVerifyMessage): Promise { + const signature = Base64.fromByteArray(core.fromHexString(msg.signature)); + return bitcoinMsg.verify(msg.message, msg.address, signature); + } +} diff --git a/packages/hdwallet-phantom/src/types.ts b/packages/hdwallet-phantom/src/types.ts new file mode 100644 index 000000000..ceaa936f9 --- /dev/null +++ b/packages/hdwallet-phantom/src/types.ts @@ -0,0 +1,23 @@ +import { providers } from "ethers"; + +import { BtcAccount } from "./bitcoin"; + +export type PhantomEvmProvider = providers.ExternalProvider & { + _metamask: { + isUnlocked: () => boolean; + }; +}; + +export type PhantomUtxoProvider = providers.ExternalProvider & { + requestAccounts: () => Promise; + signMessage: ( + address: string, + message: Uint8Array + ) => Promise<{ + signature: Uint8Array; + }>; + signPSBT( + psbt: Uint8Array, + options: { inputsToSign: { sigHash?: number | undefined; address: string; signingIndexes: number[] }[] } + ): Promise; +}; diff --git a/packages/hdwallet-phantom/tsconfig.json b/packages/hdwallet-phantom/tsconfig.json new file mode 100644 index 000000000..0c82f8910 --- /dev/null +++ b/packages/hdwallet-phantom/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"], + "references": [{ "path": "../hdwallet-core" }] +} \ No newline at end of file diff --git a/packages/hdwallet-portis/package.json b/packages/hdwallet-portis/package.json index 05429184e..08b422d30 100644 --- a/packages/hdwallet-portis/package.json +++ b/packages/hdwallet-portis/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-portis", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "publishConfig": { "access": "public" @@ -15,7 +15,7 @@ }, "dependencies": { "@portis/web3": "3.0.10", - "@shapeshiftoss/hdwallet-core": "1.55.5", + "@shapeshiftoss/hdwallet-core": "1.55.6", "base64-js": "^1.5.1", "bip32": "^2.0.4", "bitcoinjs-lib": "^5.1.6", diff --git a/packages/hdwallet-tallyho/package.json b/packages/hdwallet-tallyho/package.json index 4218e5c5c..6f2383f1b 100644 --- a/packages/hdwallet-tallyho/package.json +++ b/packages/hdwallet-tallyho/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-tallyho", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "publishConfig": { "access": "public" @@ -14,7 +14,7 @@ "prepublishOnly": "yarn clean && yarn build" }, "dependencies": { - "@shapeshiftoss/hdwallet-core": "1.55.5", + "@shapeshiftoss/hdwallet-core": "1.55.6", "lodash": "^4.17.21", "tallyho-onboarding": "^1.0.2" }, diff --git a/packages/hdwallet-trezor-connect/package.json b/packages/hdwallet-trezor-connect/package.json index 8ce6ca263..bc7d772a6 100644 --- a/packages/hdwallet-trezor-connect/package.json +++ b/packages/hdwallet-trezor-connect/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-trezor-connect", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "publishConfig": { "access": "public" @@ -14,8 +14,8 @@ "prepublishOnly": "yarn clean && yarn build" }, "dependencies": { - "@shapeshiftoss/hdwallet-core": "1.55.5", - "@shapeshiftoss/hdwallet-trezor": "1.55.5", + "@shapeshiftoss/hdwallet-core": "1.55.6", + "@shapeshiftoss/hdwallet-trezor": "1.55.6", "@trezor/rollout": "^1.2.0", "trezor-connect": "^8.2.1" } diff --git a/packages/hdwallet-trezor/package.json b/packages/hdwallet-trezor/package.json index 868635e0c..004e3575e 100644 --- a/packages/hdwallet-trezor/package.json +++ b/packages/hdwallet-trezor/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-trezor", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "publishConfig": { "access": "public" @@ -17,7 +17,7 @@ "dependencies": { "@ethereumjs/common": "^2.4.0", "@ethereumjs/tx": "^3.3.0", - "@shapeshiftoss/hdwallet-core": "1.55.5", + "@shapeshiftoss/hdwallet-core": "1.55.6", "base64-js": "^1.5.1", "lodash": "^4.17.21" }, diff --git a/packages/hdwallet-walletconnect/package.json b/packages/hdwallet-walletconnect/package.json index b8ce276e2..7e62e6dcf 100644 --- a/packages/hdwallet-walletconnect/package.json +++ b/packages/hdwallet-walletconnect/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-walletconnect", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "publishConfig": { "access": "public" @@ -15,7 +15,7 @@ "prepublishOnly": "yarn clean && yarn build" }, "dependencies": { - "@shapeshiftoss/hdwallet-core": "1.55.5", + "@shapeshiftoss/hdwallet-core": "1.55.6", "@walletconnect/qrcode-modal": "^1.7.8", "@walletconnect/web3-provider": "^1.7.8", "ethers": "^5.6.5" diff --git a/packages/hdwallet-walletconnectV2/package.json b/packages/hdwallet-walletconnectV2/package.json index 20622130f..ccc929665 100644 --- a/packages/hdwallet-walletconnectV2/package.json +++ b/packages/hdwallet-walletconnectV2/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-walletconnectv2", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "publishConfig": { "access": "public" @@ -15,7 +15,7 @@ "prepublishOnly": "yarn clean && yarn build" }, "dependencies": { - "@shapeshiftoss/hdwallet-core": "1.55.5", + "@shapeshiftoss/hdwallet-core": "1.55.6", "@walletconnect/ethereum-provider": "^2.10.1", "@walletconnect/modal": "^2.6.2", "ethers": "^5.6.5" diff --git a/packages/hdwallet-xdefi/package.json b/packages/hdwallet-xdefi/package.json index 57f24e1fe..c1e9b06d8 100644 --- a/packages/hdwallet-xdefi/package.json +++ b/packages/hdwallet-xdefi/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/hdwallet-xdefi", - "version": "1.55.5", + "version": "1.55.6", "license": "MIT", "publishConfig": { "access": "public" @@ -14,7 +14,7 @@ "prepublishOnly": "yarn clean && yarn build" }, "dependencies": { - "@shapeshiftoss/hdwallet-core": "1.55.5", + "@shapeshiftoss/hdwallet-core": "1.55.6", "lodash": "^4.17.21" }, "devDependencies": {