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

!WIP feat: add btc signer for phantom #891

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion wallets/provider-phantom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@
"dependencies": {
"@rango-dev/signer-solana": "^0.32.1-next.1",
"@rango-dev/wallets-shared": "^0.37.1-next.3",
"axios": "^1.7.7",
"bitcoinjs-lib": "6.1.5",
"coinselect": "^3.1.12",
"rango-types": "^0.1.69"
},
"publishConfig": {
"access": "public"
}
}
}
30 changes: 27 additions & 3 deletions wallets/provider-phantom/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
import { type Connect, Networks } from '@rango-dev/wallets-shared';

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

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

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

return instances;
}

return null;
}

export const getBTCAccounts: 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,
};
};
67 changes: 62 additions & 5 deletions wallets/provider-phantom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@ import type {
CanEagerConnect,
CanSwitchNetwork,
Connect,
ProviderConnectResult,
Subscribe,
WalletInfo,
} from '@rango-dev/wallets-shared';
import type { BlockchainMeta, SignerFactory } from 'rango-types';

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

import { phantom as phantom_instance } from './helpers.js';
import { getBTCAccounts, phantom as getPhantomInstance } from './helpers.js';
import signer from './signer.js';

const WALLET = WalletTypes.PHANTOM;
Expand All @@ -23,8 +26,55 @@ export const config = {
type: WALLET,
};

export const getInstance = phantom_instance;
export const connect: Connect = getSolanaAccounts;
export const getInstance = getPhantomInstance;

export const connect: Connect = async ({ instance, meta, namespaces }) => {
const solanaInstance = chooseInstance(instance, meta, Networks.SOLANA);
const btcInstance = chooseInstance(instance, meta, Networks.BTC);

const results = [];

const solanaNamespace = namespaces?.find(
(namespaceItem) => namespaceItem.namespace === Namespace.Solana
);
const utxoNamespace = namespaces?.find(
(namespaceItem) => namespaceItem.namespace === Namespace.Utxo
);

if (!solanaNamespace && !utxoNamespace) {
throw new Error('You should select one of these namespaces: Solana, BTC');
}

if (solanaNamespace) {
if (!solanaInstance) {
throw new Error(
'Could not connect Solana account. Consider adding Solana to your wallet.'
);
}

const accounts = (await getSolanaAccounts({
instance: solanaInstance,
meta,
})) as ProviderConnectResult;
results.push(accounts);
}

if (utxoNamespace) {
if (!btcInstance) {
throw new Error(
'Could not connect BTC account. Consider adding BTC to your wallet.'
);
}

const accounts = (await getBTCAccounts({
instance: btcInstance,
meta,
})) as ProviderConnectResult;
results.push(accounts);
}

return results;
};

export const subscribe: Subscribe = ({ instance, updateAccounts, connect }) => {
const handleAccountsChanged = async (publicKey: string) => {
Expand Down Expand Up @@ -59,7 +109,13 @@ export const canEagerConnect: CanEagerConnect = async ({ instance }) => {
export const getWalletInfo: (allBlockChains: BlockchainMeta[]) => WalletInfo = (
allBlockChains
) => {
const solana = solanaBlockchain(allBlockChains);
const supportedChains: BlockchainMeta[] = solanaBlockchain(allBlockChains);

const btc = allBlockChains.find((chain) => chain.name === Networks.BTC);
if (btc) {
supportedChains.push(btc);
}

return {
name: 'Phantom',
img: 'https://raw.githubusercontent.com/rango-exchange/assets/main/wallets/phantom/icon.svg',
Expand All @@ -70,6 +126,7 @@ export const getWalletInfo: (allBlockChains: BlockchainMeta[]) => WalletInfo = (
DEFAULT: 'https://phantom.app/',
},
color: '#4d40c6',
supportedChains: solana,
supportedChains,
namespaces: [Namespace.Solana, Namespace.Utxo],
};
};
3 changes: 3 additions & 0 deletions wallets/provider-phantom/src/signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ export default async function getSigners(
provider: any
): Promise<SignerFactory> {
const solProvider = getNetworkInstance(provider, Networks.SOLANA);
const btcProvider = getNetworkInstance(provider, Networks.BTC);
const signers = new DefaultSignerFactory();
const { DefaultSolanaSigner } = await import('@rango-dev/signer-solana');
const { BTCSigner } = await import('./signers/btc.js');
signers.registerSigner(TxType.SOLANA, new DefaultSolanaSigner(solProvider));
signers.registerSigner(TxType.TRANSFER, new BTCSigner(btcProvider));
return signers;
}
147 changes: 147 additions & 0 deletions wallets/provider-phantom/src/signers/btc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import type { GenericSigner, Transfer } from 'rango-types';

import { Networks } from '@rango-dev/wallets-shared';
import axios from 'axios';
import * as Bitcoin from 'bitcoinjs-lib';
import { SignerError } from 'rango-types';

type TransferExternalProvider = any;

const BTC_RPC_URL = 'https://rpc.ankr.com/btc';
const MAX_MEMO_LENGTH = 80;

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

const compileMemo = (memo: string): Buffer => {
const data = Buffer.from(memo, 'utf8'); // converts MEMO to buffer
return Bitcoin.script.compile([Bitcoin.opcodes.OP_RETURN, data]); // Compile OP_RETURN script
};

export class BTCSigner 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 {
memo,
recipientAddress,
amount,
fromWalletAddress,
asset,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
psbt,
} = tx;

if (asset.blockchain !== Networks.BTC) {
throw new Error(
`Signing ${asset.blockchain} transaction is not implemented by the signer.`
);
}

// Check memo length
if (memo && memo.length > MAX_MEMO_LENGTH) {
throw new Error('memo too long, must not be longer than 80 chars.');
}
const compiledMemo = memo ? compileMemo(memo) : null;

// Initialize an array to store the target outputs of the transaction.
const targetOutputs = [];
// 1. Add the recipient address and amount to the target outputs.
targetOutputs.push({
address: recipientAddress,
value: parseInt(amount),
});
// 2. Add the compiled memo to the target outputs if it exists.
if (compiledMemo) {
targetOutputs.push({ script: compiledMemo, value: 0 });
}

// Initialize a new Bitcoin PSBT object.
const generatedPsbt = new Bitcoin.Psbt({
network: Bitcoin.networks.bitcoin,
}); // Network-specific

// Add inputs to the PSBT from the accumulated inputs.
psbt?.inputs.forEach((input: any) => {
const formattedInput: any = {};
formattedInput.hash = input.hash;
formattedInput.index = input.index;

if (input.witnessUtxo) {
formattedInput.witnessUtxo = {
value: parseInt(input.witnessUtxo.value),
script: Buffer.from(input.witnessUtxo.script, 'hex'),
};
}
generatedPsbt.addInput(formattedInput);
});

// Add outputs to the PSBT from the accumulated outputs.
psbt?.outputs.forEach((output: any) => {
output.value = parseInt(output.value);
if (output.script) {
output.script = Buffer.from(output.script, 'hex');
}
if (!output.address) {
//an empty address means this is the source address
output.address = fromWalletAddress;
}
generatedPsbt.addOutput(output);
});

console.log({ generatedPsbt });

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

// Finalize PSBT
const finalPsbt = Bitcoin.Psbt.fromBuffer(Buffer.from(signedPSBTBytes));
finalPsbt.finalizeAllInputs();
console.log('finalPsbt', finalPsbt);

const finalPsbtBaseHex = finalPsbt.toHex();

console.log('finalPsbtBaseHex', finalPsbtBaseHex);

// Broadcast PSBT to rpc node
const hash = await axios.post<
{ id: string; method: string; params: string[] },
string
>(BTC_RPC_URL, {
id: 'test',
method: 'sendrawtransaction',
params: [finalPsbtBaseHex],
});

console.log('hash', hash);

return { hash };
}
}
2 changes: 1 addition & 1 deletion wallets/shared/src/rango.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export const namespaces: Record<
},
[Namespace.Utxo]: {
mainBlockchain: 'BTC',
title: 'Utxo',
title: 'BTC',
},
[Namespace.Starknet]: {
title: 'Starknet',
Expand Down
Loading