Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement Ethereum, Polygon and Bitcoin with phantom #677

Merged
merged 55 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
507e342
feat: implement phantom
NeOMakinG Sep 6, 2024
f516a17
feat: update supports
NeOMakinG Sep 6, 2024
617ea6c
feat: add btc supprot
NeOMakinG Sep 9, 2024
07b2584
Merge branch 'master' into phantom
NeOMakinG Sep 9, 2024
014913d
feat: lint
NeOMakinG Sep 9, 2024
50f63cf
Merge remote-tracking branch 'origin/master' into phantom
gomesalexandre Sep 17, 2024
69d8892
feat: cleanup
gomesalexandre Sep 17, 2024
b55e957
feat: revert me
gomesalexandre Sep 17, 2024
d622b47
feat: wip window.solana
gomesalexandre Sep 18, 2024
af83e76
feat: fromHexString outside of fn scope
gomesalexandre Sep 18, 2024
0ff0419
feat: rm testnet support
gomesalexandre Sep 18, 2024
14fa180
feat: rm useless fn
gomesalexandre Sep 18, 2024
bde4f7a
feat: cleanup
gomesalexandre Sep 18, 2024
65c70ff
fix: isUnlocked
gomesalexandre Sep 18, 2024
2e6c32c
feat: more cleanunp
gomesalexandre Sep 18, 2024
9357bdc
fix: describePath
gomesalexandre Sep 18, 2024
29f33c8
fix: btcSupportsCoin
gomesalexandre Sep 18, 2024
23fa837
feat: make it compile
gomesalexandre Sep 18, 2024
a5b9296
feat: btcSignMessage
gomesalexandre Sep 18, 2024
e5b646a
feat: bitcoin requestAccounts()
gomesalexandre Sep 18, 2024
2d1645d
feat: safety
gomesalexandre Sep 18, 2024
76a77d6
Revert "feat: bitcoin requestAccounts()"
gomesalexandre Sep 18, 2024
a80e1e1
feat: only p2wpkh effectively supported
gomesalexandre Sep 18, 2024
e863308
feat: getPublicKeys
gomesalexandre Sep 18, 2024
d14e519
feat: Bitcoin coin only
gomesalexandre Sep 18, 2024
d227432
fix: gasLimit over gas
gomesalexandre Sep 18, 2024
2094a1f
feat: add gasPrice
gomesalexandre Sep 18, 2024
df0d141
fix: psbt
gomesalexandre Sep 18, 2024
860373c
feat: cleanup
gomesalexandre Sep 18, 2024
f620a16
feat: more cleanup
gomesalexandre Sep 18, 2024
6193e58
feat: more more cleanup
gomesalexandre Sep 18, 2024
b82d050
feat: more more more cleanup
gomesalexandre Sep 18, 2024
d6e3e96
feat: more more more more cleanup
gomesalexandre Sep 18, 2024
128b407
feat: more more more more more cleanup
gomesalexandre Sep 18, 2024
e810ac0
feat: rm address cache
gomesalexandre Sep 18, 2024
de602be
fix: tests types
gomesalexandre Sep 18, 2024
53a5cd0
fix: test
gomesalexandre Sep 18, 2024
8aee937
Revert "fix: test"
gomesalexandre Sep 18, 2024
5637977
fix: actually fix test
gomesalexandre Sep 18, 2024
a9d3035
feat: add window.ethereum
gomesalexandre Sep 19, 2024
389d7e6
feat: cleanup
gomesalexandre Sep 19, 2024
8e1fff9
feat: more cleanup
gomesalexandre Sep 19, 2024
a5078c5
feat: more more cleaup
gomesalexandre Sep 19, 2024
248372b
feat: more cleanup
gomesalexandre Sep 19, 2024
e149db1
fix: OP_RETURN_DATA :fridaydog:
gomesalexandre Sep 20, 2024
f528f10
feat: more cleanupy
gomesalexandre Sep 20, 2024
66b2348
feat: saner tests
gomesalexandre Sep 20, 2024
0f648fd
feat: up-to-date hdwallet deps
gomesalexandre Sep 20, 2024
0ff50a4
Merge remote-tracking branch 'origin/master' into phantom
gomesalexandre Sep 20, 2024
dd5b0f1
cleanup
kaladinlight Sep 20, 2024
6b89f94
fix test
kaladinlight Sep 20, 2024
7cb54cb
cleanup bitcoin
kaladinlight Sep 20, 2024
1107098
fix test take 2
kaladinlight Sep 20, 2024
e18b090
feat: cleanup tsconfig.json
gomesalexandre Sep 20, 2024
1e04575
chore(release): publish 1.55.6
gomesalexandre Sep 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/sandbox/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ <h4>Select</h4>
<button id="portis">Pair Portis</button>
<button id="native">Pair Native</button>
<button id="metaMask">Pair MetaMask</button>
<button id="phantom">Pair Phantom</button>
<button id="xdefi">Pair XDEFI</button>
<button id="keplr">Pair Keplr</button>
<button id="tallyHo">Pair Tally Ho</button>
Expand Down
22 changes: 22 additions & 0 deletions examples/sandbox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as keplr from "@shapeshiftoss/hdwallet-keplr";
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 phantom from "@shapeshiftoss/hdwallet-phantom";
import * as native from "@shapeshiftoss/hdwallet-native";
import * as portis from "@shapeshiftoss/hdwallet-portis";
import * as tallyHo from "@shapeshiftoss/hdwallet-tallyho";
Expand Down Expand Up @@ -127,6 +128,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);
Expand Down Expand Up @@ -159,6 +161,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");
Expand Down Expand Up @@ -243,6 +246,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();
Expand Down Expand Up @@ -405,6 +421,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) {
Expand Down
1 change: 1 addition & 0 deletions examples/sandbox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@shapeshiftoss/hdwallet-ledger-webhid": "1.55.0",
"@shapeshiftoss/hdwallet-ledger-webusb": "1.55.0",
"@shapeshiftoss/hdwallet-metamask": "1.55.0",
"@shapeshiftoss/hdwallet-phantom": "1.55.0",
"@shapeshiftoss/hdwallet-native": "1.55.0",
"@shapeshiftoss/hdwallet-portis": "1.55.0",
"@shapeshiftoss/hdwallet-tallyho": "1.55.0",
Expand Down
24 changes: 24 additions & 0 deletions packages/hdwallet-phantom/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@shapeshiftoss/hdwallet-phantom",
"version": "1.55.0",
"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/hdwallet-core": "1.54.2",
kaladinlight marked this conversation as resolved.
Show resolved Hide resolved
"eth-rpc-errors": "^4.0.3",
gomesalexandre marked this conversation as resolved.
Show resolved Hide resolved
"lodash": "^4.17.21"
gomesalexandre marked this conversation as resolved.
Show resolved Hide resolved
},
"devDependencies": {
"@types/lodash": "^4.14.168"
}
}
59 changes: 59 additions & 0 deletions packages/hdwallet-phantom/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as core from "@shapeshiftoss/hdwallet-core";
import { providers } from "ethers";

import { PhantomHDWallet } from "./phantom";

declare global {
interface Window {
phantom?: {
ethereum?: providers.ExternalProvider;
bitcoin?: providers.ExternalProvider;
gomesalexandre marked this conversation as resolved.
Show resolved Hide resolved
};
}
}

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<number> {
return Object.keys(this.keyring.wallets).length;
}

public async pairDevice(): Promise<PhantomHDWallet | undefined> {
const evmProvider = window.phantom?.ethereum;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: checkIsMetaMaskImpersonator will fail in web, which we may or may not have to accommodate: https://github.com/MetaMask/detect-provider/blob/4b85b3445c746b40d4a52a5679dfe357f29238c4/src/detect-ethereum-provider.ts#L31 (notably, handleSend and useStakingAction use it for MM with snaps detection

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(() =>
gomesalexandre marked this conversation as resolved.
Show resolved Hide resolved
kaladinlight marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
180 changes: 180 additions & 0 deletions packages/hdwallet-phantom/src/bitcoin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import * as bitcoin from "@shapeshiftoss/bitcoinjs-lib";
import * as core from "@shapeshiftoss/hdwallet-core";

type BtcAccount = {
address: string;
addressType: "p2tr" | "p2wpkh" | "p2sh" | "p2pkh";
gomesalexandre marked this conversation as resolved.
Show resolved Hide resolved
publicKey: string;
purpose: "payment" | "ordinals";
};

function getNetwork(coin: string): bitcoin.networks.Network {
switch (coin.toLowerCase()) {
case "bitcoin":
return bitcoin.networks.bitcoin;
case "testnet":
return bitcoin.networks.testnet;
gomesalexandre marked this conversation as resolved.
Show resolved Hide resolved
default:
throw new Error(`Unsupported coin: ${coin}`);
}
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function bitcoinNextAccountPath(msg: core.BTCAccountPath): core.BTCAccountPath | undefined {
// Only support one account for now (like portis).
return undefined;
}
gomesalexandre marked this conversation as resolved.
Show resolved Hide resolved

export async function bitcoinGetPublicKeys(msg: core.BTCGetAddress, provider: any): Promise<string[]> {
const accounts = await provider.requestAccounts();
const paymentPublicKey = accounts.find((account: BtcAccount) => account.purpose === "payment")?.publicKey;

return [paymentPublicKey];
}
gomesalexandre marked this conversation as resolved.
Show resolved Hide resolved

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function bitcoinGetAddress(msg: core.BTCGetAddress, provider: any): Promise<string> {
const accounts = await provider.requestAccounts();
const paymentAddress = accounts.find((account: BtcAccount) => account.purpose === "payment")?.address;
gomesalexandre marked this conversation as resolved.
Show resolved Hide resolved

return paymentAddress;
}

async function addInput(
wallet: core.BTCWallet,
psbt: bitcoin.Psbt,
input: core.BTCSignTxInput,
coin: string,
network: bitcoin.networks.Network
): Promise<void> {
const inputData: bitcoin.PsbtTxInput & {
nonWitnessUtxo?: Buffer;
witnessUtxo?: { script: Buffer; value: number };
} = {
hash: Buffer.from(input.txid, "hex"),
index: input.vout,
};

if (input.sequence !== undefined) {
inputData.sequence = input.sequence;
}

if (input.scriptType) {
switch (input.scriptType) {
case "p2pkh":
inputData.nonWitnessUtxo = Buffer.from(input.hex, "hex");
break;
case "p2sh-p2wpkh":
case "p2wpkh": {
const inputAddress = await wallet.btcGetAddress({ addressNList: input.addressNList, coin, showDisplay: false });
gomesalexandre marked this conversation as resolved.
Show resolved Hide resolved

if (!inputAddress) throw new Error("Could not get address from wallet");

inputData.witnessUtxo = {
script: bitcoin.address.toOutputScript(inputAddress, network),
value: parseInt(input.amount),
};
break;
}
default:
gomesalexandre marked this conversation as resolved.
Show resolved Hide resolved
throw new Error(`Unsupported script type: ${input.scriptType}`);
}
}

psbt.addInput(inputData);
}

async function addOutput(
wallet: core.BTCWallet,
psbt: bitcoin.Psbt,
output: core.BTCSignTxOutput,
coin: string
): Promise<void> {
if ("address" in output && output.address) {
psbt.addOutput({
address: output.address,
value: parseInt(output.amount),
});
} else if ("addressNList" in output && output.addressNList) {
const outputAddress = await wallet.btcGetAddress({ addressNList: output.addressNList, coin, showDisplay: false });

if (!outputAddress) throw new Error("Could not get address from wallet");

psbt.addOutput({
address: outputAddress,
value: parseInt(output.amount),
});
} else if ("opReturnData" in output && output.opReturnData) {
const data = Buffer.from(output.opReturnData.toString(), "hex");
const embed = bitcoin.payments.embed({ data: [data] });
psbt.addOutput({
script: embed.output!,
value: 0,
});
}
}

export async function bitcoinSignTx(
wallet: core.BTCWallet,
msg: core.BTCSignTx,
provider: any
): Promise<core.BTCSignedTx | null> {
if (!msg.inputs.length || !msg.outputs.length) {
throw new Error("Invalid input: Empty inputs or outputs");
}

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(wallet, psbt, input, msg.coin, network);
}

for (const output of msg.outputs) {
await addOutput(wallet, psbt, output, msg.coin);
}

const inputsToSign = await Promise.all(
msg.inputs.map(async (input, index) => {
const address = await wallet.btcGetAddress({
addressNList: input.addressNList,
coin: msg.coin,
showDisplay: false,
});

return {
address,
signingIndexes: [index],
sigHash: bitcoin.Transaction.SIGHASH_ALL,
kaladinlight marked this conversation as resolved.
Show resolved Hide resolved
};
})
);

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)));
};
gomesalexandre marked this conversation as resolved.
Show resolved Hide resolved
const signedPsbtHex = await provider.signPSBT(fromHexString(psbt.toHex()), { inputsToSign });

const signedPsbt = bitcoin.Psbt.fromHex(signedPsbtHex, { network });

signedPsbt.finalizeAllInputs();
const tx = signedPsbt.extractTransaction();

const signatures = signedPsbt.data.inputs.map((input) =>
input.partialSig ? input.partialSig[0].signature.toString("hex") : ""
);

return {
signatures,
serializedTx: tx.toHex(),
};
}
Loading