Skip to content

Commit

Permalink
feat: add bitcoin connect and signer to phantom wallet
Browse files Browse the repository at this point in the history
  • Loading branch information
RyukTheCoder committed Apr 13, 2024
1 parent cd0b9a5 commit 002f060
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 22 deletions.
3 changes: 2 additions & 1 deletion wallets/provider-phantom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@
"dependencies": {
"@rango-dev/signer-solana": "^0.26.0",
"@rango-dev/wallets-shared": "^0.30.0",
"bitcoinjs-lib": "6.1.5",
"rango-types": "^0.1.59"
},
"publishConfig": {
"access": "public"
}
}
}
31 changes: 28 additions & 3 deletions wallets/provider-phantom/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,36 @@
import type { Connect } from '@rango-dev/wallets-shared';

import { Networks } from '@rango-dev/wallets-shared';

export function phantom() {
if ('phantom' in window) {
const instance = window.phantom?.solana;
const instances = new Map();

if (window.phantom?.solana?.isPhantom) {
instances.set(Networks.SOLANA, window.phantom.solana);
}

if (instance?.isPhantom) {
return instance;
if (window.phantom?.bitcoin?.isPhantom) {
instances.set(Networks.BTC, window.phantom.bitcoin);
}

return instances;
}

return null;
}

export const getBitcoinAccounts: Connect = async ({ instance }) => {
const accounts = await instance.requestAccounts();
return {
accounts: accounts.map(
(account: {
address: string;
publicKey: string;
addressType: 'p2tr' | 'p2wpkh' | 'p2sh' | 'p2pkh';
purpose: 'payment' | 'ordinals';
}) => account.address
),
chainId: Networks.BTC,
};
};
50 changes: 42 additions & 8 deletions wallets/provider-phantom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import type {
import type { BlockchainMeta, SignerFactory } from 'rango-types';

import {
chooseInstance,
getSolanaAccounts,
Networks,
WalletTypes,
} from '@rango-dev/wallets-shared';
import { solanaBlockchain } from 'rango-types';

import { phantom as phantom_instance } from './helpers';
import { getBitcoinAccounts, phantom as phantom_instance } from './helpers';
import signer from './signer';

const WALLET = WalletTypes.PHANTOM;
Expand All @@ -24,9 +25,36 @@ export const config = {
};

export const getInstance = phantom_instance;
export const connect: Connect = getSolanaAccounts;
export const connect: Connect = async ({ instance, meta }) => {
const solanaInstance = chooseInstance(instance, meta, Networks.SOLANA);
const bitcoinInstance = chooseInstance(instance, meta, Networks.BTC);

const result = [];
if (solanaInstance) {
const solanaAccounts = await getSolanaAccounts({
instance: solanaInstance,
});
result.push(solanaAccounts);
}

if (bitcoinInstance) {
const bitcoinAccounts = await getBitcoinAccounts({
instance: bitcoinInstance,
});
result.push(bitcoinAccounts);
}

return result;
};

export const subscribe: Subscribe = ({
instance,
updateAccounts,
connect,
meta,
}) => {
const solanaInstance = chooseInstance(instance, meta, Networks.SOLANA);

export const subscribe: Subscribe = ({ instance, updateAccounts, connect }) => {
const handleAccountsChanged = async (publicKey: string) => {
const network = Networks.SOLANA;
if (publicKey) {
Expand All @@ -36,20 +64,22 @@ export const subscribe: Subscribe = ({ instance, updateAccounts, connect }) => {
connect(network);
}
};
instance?.on?.('accountChanged', handleAccountsChanged);
solanaInstance?.on('accountChanged', handleAccountsChanged);

return () => {
instance?.off?.('accountChanged', handleAccountsChanged);
solanaInstance?.off('accountChanged', handleAccountsChanged);
};
};

export const canSwitchNetworkTo: CanSwitchNetwork = () => false;

export const getSigners: (provider: any) => SignerFactory = signer;

export const canEagerConnect: CanEagerConnect = async ({ instance }) => {
export const canEagerConnect: CanEagerConnect = async ({ instance, meta }) => {
const solanaInstance = chooseInstance(instance, meta, Networks.SOLANA);

try {
const result = await instance.connect({ onlyIfTrusted: true });
const result = await solanaInstance.connect({ onlyIfTrusted: true });
return !!result;
} catch (error) {
return false;
Expand All @@ -60,6 +90,10 @@ export const getWalletInfo: (allBlockChains: BlockchainMeta[]) => WalletInfo = (
allBlockChains
) => {
const solana = solanaBlockchain(allBlockChains);
const bitcoin = allBlockChains.filter(
(blockchain) => blockchain.name === Networks.BTC
);

return {
name: 'Phantom',
img: 'https://raw.githubusercontent.com/rango-exchange/assets/main/wallets/phantom/icon.svg',
Expand All @@ -70,6 +104,6 @@ export const getWalletInfo: (allBlockChains: BlockchainMeta[]) => WalletInfo = (
DEFAULT: 'https://phantom.app/',
},
color: '#4d40c6',
supportedChains: solana,
supportedChains: [...solana, ...bitcoin],
};
};
17 changes: 11 additions & 6 deletions wallets/provider-phantom/src/signer.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import type { SignerFactory } from 'rango-types';

import { DefaultSolanaSigner } from '@rango-dev/signer-solana';
import { Networks, getNetworkInstance } from '@rango-dev/wallets-shared';
import {
DefaultSignerFactory,
SignerFactory,
TransactionType as TxType,
} from 'rango-types';
import { getNetworkInstance, Networks } from '@rango-dev/wallets-shared';
import { DefaultSignerFactory, TransactionType as TxType } from 'rango-types';

import { PhantomTransferSigner } from './signers/bitcoinSigner';

export default function getSigners(provider: any): SignerFactory {
const solProvider = getNetworkInstance(provider, Networks.SOLANA);
const bitcoinProvider = getNetworkInstance(provider, Networks.BTC);
const signers = new DefaultSignerFactory();
signers.registerSigner(TxType.SOLANA, new DefaultSolanaSigner(solProvider));
signers.registerSigner(
TxType.TRANSFER,
new PhantomTransferSigner(bitcoinProvider)
);
return signers;
}
84 changes: 84 additions & 0 deletions wallets/provider-phantom/src/signers/bitcoinSigner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type { GenericSigner, Transfer } from 'rango-types';

import * as bitcoin from 'bitcoinjs-lib';
import { SignerError } from 'rango-types';

type TransferExternalProvider = any;

const fromHexString = (hexString: string) =>
Uint8Array.from(
hexString
.match(/.{1,2}/g)
?.map((byte) => parseInt(byte, 16)) as Iterable<number>
);

export class PhantomTransferSigner implements GenericSigner<Transfer> {
private provider: TransferExternalProvider;
constructor(provider: TransferExternalProvider) {
this.provider = provider;
}

async signMessage(): Promise<string> {
throw SignerError.UnimplementedError('signMessage');
}

async signAndSendTx(tx: Transfer): Promise<{ hash: string }> {
const psbt = new bitcoin.Psbt({ network: bitcoin.networks.bitcoin });

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
const { utxo, recipientAddress, amount, fromWalletAddress } = tx;

let utxoAmount = 0;

for (let i = 0; i < utxo.length; i++) {
const item = utxo[i];
psbt.addInput({
hash: item.txId,
index: item.vout,
witnessUtxo: {
script: Buffer.from(item.script, 'hex'),
value: parseInt(item.value),
},
});
utxoAmount += parseInt(item.value);
if (utxoAmount > parseInt(amount)) {
break;
}
}

psbt.addOutput({
address: recipientAddress,
value: parseInt(amount),
});
if (utxoAmount - parseInt(amount) > 0) {
psbt.addOutput({
address: fromWalletAddress,
value: utxoAmount - parseInt(amount),
});
}
const serializedPsbt = psbt.toHex();

const signedPSBTBytes = await this.provider.signPSBT(
fromHexString(serializedPsbt),
{
inputsToSign: [
{
address: tx.fromWalletAddress,
signingIndexes: Array.from(
{ length: psbt.inputCount },
(_, index) => index
),
sigHash: 0,
},
],
}
);

const finalPsbt = bitcoin.Psbt.fromBuffer(Buffer.from(signedPSBTBytes));
finalPsbt.finalizeAllInputs();
console.log('finalPsbt', finalPsbt.toBase64());

return { hash: '' };
}
}
65 changes: 61 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8034,6 +8034,11 @@ bindings@^1.3.0:
dependencies:
file-uri-to-path "1.0.0"

bip174@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/bip174/-/bip174-2.1.1.tgz#ef3e968cf76de234a546962bcf572cc150982f9f"
integrity sha512-mdFV5+/v0XyNYXjBS6CQPLo9ekCx4gtKZFnJm5PMto7Fs9hTTDpkkzOB7/FtluRI6JbUUAu+snTYfJRgHLZbZQ==

bip32@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/bip32/-/bip32-2.0.6.tgz#6a81d9f98c4cd57d05150c60d8f9e75121635134"
Expand Down Expand Up @@ -8064,6 +8069,18 @@ bip39@^3.0.2, bip39@^3.0.3:
dependencies:
"@noble/hashes" "^1.2.0"

[email protected]:
version "6.1.5"
resolved "https://registry.yarnpkg.com/bitcoinjs-lib/-/bitcoinjs-lib-6.1.5.tgz#3b03509ae7ddd80a440f10fc38c4a97f0a028d8c"
integrity sha512-yuf6xs9QX/E8LWE2aMJPNd0IxGofwfuVOiYdNUESkc+2bHHVKjhJd8qewqapeoolh9fihzHGoDCB5Vkr57RZCQ==
dependencies:
"@noble/hashes" "^1.2.0"
bech32 "^2.0.0"
bip174 "^2.1.1"
bs58check "^3.0.1"
typeforce "^1.11.3"
varuint-bitcoin "^1.1.2"

bl@^4.0.3, bl@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
Expand Down Expand Up @@ -8268,6 +8285,14 @@ bs58check@<3.0.0, bs58check@^2.1.1, bs58check@^2.1.2:
create-hash "^1.1.0"
safe-buffer "^5.1.2"

bs58check@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-3.0.1.tgz#2094d13720a28593de1cba1d8c4e48602fdd841c"
integrity sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==
dependencies:
"@noble/hashes" "^1.2.0"
bs58 "^5.0.0"

[email protected]:
version "2.1.1"
resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05"
Expand Down Expand Up @@ -16716,7 +16741,16 @@ string-format@^2.0.0:
resolved "https://registry.yarnpkg.com/string-format/-/string-format-2.0.0.tgz#f2df2e7097440d3b65de31b6d40d54c96eaffb9b"
integrity sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==

"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"

string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
Expand Down Expand Up @@ -16808,7 +16842,14 @@ stringify-object@^5.0.0:
is-obj "^3.0.0"
is-regexp "^3.1.0"

"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"

strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
Expand Down Expand Up @@ -17488,7 +17529,7 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==

typeforce@^1.11.5:
typeforce@^1.11.3, typeforce@^1.11.5:
version "1.18.0"
resolved "https://registry.yarnpkg.com/typeforce/-/typeforce-1.18.0.tgz#d7416a2c5845e085034d70fcc5b6cc4a90edbfdc"
integrity sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==
Expand Down Expand Up @@ -17860,6 +17901,13 @@ [email protected]:
parse-css-color "0.2.0"
pure-color "1.3.0"

varuint-bitcoin@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz#e76c138249d06138b480d4c5b40ef53693e24e92"
integrity sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==
dependencies:
safe-buffer "^5.1.1"

vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
Expand Down Expand Up @@ -18179,7 +18227,7 @@ wordwrapjs@^4.0.0:
reduce-flatten "^2.0.0"
typical "^5.2.0"

"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
Expand All @@ -18197,6 +18245,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"

wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"

wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
Expand Down

0 comments on commit 002f060

Please sign in to comment.