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 bitcoin signer for phantom on hub #972

Open
wants to merge 3 commits into
base: next
Choose a base branch
from
Open
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
8 changes: 8 additions & 0 deletions wallets/core/namespaces/utxo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@rango-dev/wallets-core/namespaces/utxo",
"type": "module",
"main": "../../dist/namespaces/utxo/mod.js",
"module": "../../dist/namespaces/utxo/mod.js",
"types": "../../dist/namespaces/utxo/mod.d.ts",
"sideEffects": false
}
6 changes: 5 additions & 1 deletion wallets/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
"./namespaces/solana": {
"types": "./dist/namespaces/solana/mod.d.ts",
"default": "./dist/namespaces/solana/mod.js"
},
"./namespaces/utxo": {
"types": "./dist/namespaces/utxo/mod.d.ts",
"default": "./dist/namespaces/utxo/mod.js"
}
},
"files": [
Expand All @@ -38,7 +42,7 @@
"legacy"
],
"scripts": {
"build": "node ../../scripts/build/command.mjs --path wallets/core --inputs src/mod.ts,src/utils/mod.ts,src/legacy/mod.ts,src/namespaces/evm/mod.ts,src/namespaces/solana/mod.ts,src/namespaces/common/mod.ts",
"build": "node ../../scripts/build/command.mjs --path wallets/core --inputs src/mod.ts,src/utils/mod.ts,src/legacy/mod.ts,src/namespaces/evm/mod.ts,src/namespaces/solana/mod.ts,src/namespaces/utxo/mod.ts,src/namespaces/common/mod.ts",
"ts-check": "tsc --declaration --emitDeclarationOnly -p ./tsconfig.json",
"clean": "rimraf dist",
"format": "prettier --write '{.,src}/**/*.{ts,tsx}'",
Expand Down
2 changes: 2 additions & 0 deletions wallets/core/src/hub/provider/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { LegacyState } from '../../legacy/mod.js';
import type { CosmosActions } from '../../namespaces/cosmos/mod.js';
import type { EvmActions } from '../../namespaces/evm/mod.js';
import type { SolanaActions } from '../../namespaces/solana/mod.js';
import type { UtxoActions } from '../../namespaces/utxo/mod.js';
import type { AnyFunction, FunctionWithContext } from '../../types/actions.js';
import type { Prettify } from '../../types/utils.js';

Expand All @@ -25,6 +26,7 @@ export interface CommonNamespaces {
evm: EvmActions;
solana: SolanaActions;
cosmos: CosmosActions;
utxo: UtxoActions;
}

export type CommonNamespaceKeys = Prettify<keyof CommonNamespaces>;
Expand Down
2 changes: 1 addition & 1 deletion wallets/core/src/namespaces/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ type RangoNamespace =
| 'EVM'
| 'Solana'
| 'Cosmos'
| 'UTXO'
| 'Utxo'
| 'Starknet'
| 'Tron'
| 'Ton';
Expand Down
3 changes: 3 additions & 0 deletions wallets/core/src/namespaces/utxo/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { recommended as commonRecommended } from '../common/actions.js';

export const recommended = [...commonRecommended];
3 changes: 3 additions & 0 deletions wallets/core/src/namespaces/utxo/after.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { recommended as commonRecommended } from '../common/after.js';

export const recommended = [...commonRecommended];
5 changes: 5 additions & 0 deletions wallets/core/src/namespaces/utxo/and.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { connectAndUpdateStateForSingleNetwork } from '../common/mod.js';

export const recommended = [
['connect', connectAndUpdateStateForSingleNetwork] as const,
];
3 changes: 3 additions & 0 deletions wallets/core/src/namespaces/utxo/before.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { recommended as commonRecommended } from '../common/before.js';

export const recommended = [...commonRecommended];
10 changes: 10 additions & 0 deletions wallets/core/src/namespaces/utxo/builders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { UtxoActions } from './types.js';

import { ActionBuilder } from '../../mod.js';
import { intoConnectionFinished } from '../common/after.js';
import { connectAndUpdateStateForSingleNetwork } from '../common/and.js';

export const connect = () =>
new ActionBuilder<UtxoActions, 'connect'>('connect')
.and(connectAndUpdateStateForSingleNetwork)
.after(intoConnectionFinished);
2 changes: 2 additions & 0 deletions wallets/core/src/namespaces/utxo/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const CAIP_NAMESPACE = 'bip122';
export const CAIP_BITCOIN_CHAIN_ID = '000000000019d6689c085ae165831e93';
8 changes: 8 additions & 0 deletions wallets/core/src/namespaces/utxo/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export * as actions from './actions.js';
export * as after from './after.js';
export * as and from './and.js';
export * as before from './before.js';
export * as builders from './builders.js';

export type { ProviderAPI, UtxoActions } from './types.js';
export { CAIP_NAMESPACE, CAIP_BITCOIN_CHAIN_ID } from './constants.js';
13 changes: 13 additions & 0 deletions wallets/core/src/namespaces/utxo/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Accounts } from '../../types/accounts.js';
import type {
AutoImplementedActionsByRecommended,
CommonActions,
} from '../common/types.js';

export interface UtxoActions
extends AutoImplementedActionsByRecommended,
CommonActions {
connect: () => Promise<Accounts>;
}

export type ProviderAPI = Record<string, any>;
5 changes: 4 additions & 1 deletion wallets/provider-phantom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@
"lint": "eslint \"**/*.{ts,tsx}\" --ignore-path ../../.eslintignore"
},
"dependencies": {
"@bitcoinerlab/secp256k1": "^1.2.0",
"@rango-dev/signer-solana": "^0.35.0",
"@rango-dev/wallets-shared": "^0.40.1-next.2",
"axios": "^1.7.7",
"bitcoinjs-lib": "6.1.5",
"rango-types": "^0.1.74"
},
"publishConfig": {
"access": "public"
}
}
}
2 changes: 1 addition & 1 deletion wallets/provider-phantom/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const info: ProviderInfo = {
{
name: 'detached',
// if you are adding a new namespace, don't forget to also update `getWalletInfo`
value: ['solana', 'evm'],
value: ['solana', 'evm', 'utxo'],
},
],
};
13 changes: 7 additions & 6 deletions wallets/provider-phantom/src/legacy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,18 @@ const canEagerConnect: CanEagerConnect = async ({ instance, meta }) => {
export const getWalletInfo: (allBlockChains: BlockchainMeta[]) => WalletInfo = (
allBlockChains
) => {
let supportedChains: BlockchainMeta[] = [];
const solana = solanaBlockchain(allBlockChains);
const evms = allBlockChains.filter(
(chain): chain is EvmBlockchainMeta =>
isEvmBlockchain(chain) &&
EVM_SUPPORTED_CHAINS.includes(chain.name as Networks)
);
const btc = allBlockChains.find((chain) => chain.name === Networks.BTC);
supportedChains = supportedChains.concat(solana).concat(evms);
if (btc) {
supportedChains.push(btc);
}

return {
name: 'Phantom',
Expand All @@ -101,12 +107,7 @@ export const getWalletInfo: (allBlockChains: BlockchainMeta[]) => WalletInfo = (
},
color: '#4d40c6',
// if you are adding a new namespace, don't forget to also update `properties`
supportedChains: [
...solana,
...evms.filter((chain) =>
EVM_SUPPORTED_CHAINS.includes(chain.name as Networks)
),
],
supportedChains,
};
};

Expand Down
3 changes: 3 additions & 0 deletions wallets/provider-phantom/src/legacy/signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ export default async function getSigners(
): Promise<SignerFactory> {
const solProvider = getNetworkInstance(provider, Networks.SOLANA);
const evmProvider = getNetworkInstance(provider, Networks.ETHEREUM);
const bitcoinInstance = getNetworkInstance(provider, Networks.BTC);

const { DefaultEvmSigner } = await import('@rango-dev/signer-evm');
const { DefaultSolanaSigner } = await import('@rango-dev/signer-solana');
const { BTCSigner } = await import('./utxoSigner.js');
const signers = new DefaultSignerFactory();
signers.registerSigner(TxType.SOLANA, new DefaultSolanaSigner(solProvider));
signers.registerSigner(TxType.EVM, new DefaultEvmSigner(evmProvider));
signers.registerSigner(TxType.TRANSFER, new BTCSigner(bitcoinInstance));
return signers;
}
108 changes: 108 additions & 0 deletions wallets/provider-phantom/src/legacy/utxoSigner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import type { GenericSigner, Transfer } from 'rango-types';

import * as secp256k1 from '@bitcoinerlab/secp256k1';
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://go.getblock.io/f37bad28a991436483c0a3679a3acbee';

interface PSBTInput {
hash: string;
index: number;
witnessUtxo: {
// BigInteger
value: string;
script: string;
} | null;
nonWitnessUtxo: string | null;
}

interface PSBTOutput {
// BigInteger
value: string;
address?: string;
script?: string;
}

interface PSBT {
// inputs is list of PSBTInput, which have witnessUtxo or nonWitnessUtxo
psbtInputs: PSBTInput[];
// ouputs is either PSBTOutputToAddress or PSBTOutputToScript
psbtOutputs: PSBTOutput[];
// psbt in hex
psbt: string;
}
interface TransferNext extends Transfer {
utxo: any; // TODO: I think this will be removed from server, if not, we can define the type.
psbt: PSBT | null;
}

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

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: TransferNext): Promise<{ hash: string }> {
const { fromWalletAddress, asset, psbt: apiObj } = tx;

if (!apiObj) {
throw new Error('TODO');
}

if (asset.blockchain !== Networks.BTC) {
throw new Error(
`Signing ${asset.blockchain} transaction is not implemented by the signer.`
);
}
// Initialize ECC library
bitcoin.initEccLib(secp256k1);

const signedPSBTBytes = await this.provider.signPSBT(
fromHexString(apiObj.psbt),
{
inputsToSign: [
{
address: fromWalletAddress,
// TODO: Should we sign all the inputs?
signingIndexes: apiObj.psbtInputs.map((_, i) => i),
},
],
}
);

// Finalize PSBT
const finalPsbt = bitcoin.Psbt.fromBuffer(Buffer.from(signedPSBTBytes));
finalPsbt.finalizeAllInputs();

const finalPsbtBaseHex = finalPsbt.extractTransaction().toHex();

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

if (!response.data.result) {
throw new Error(response.data.error?.message);
}

return { hash: response.data.result };
}
}
52 changes: 52 additions & 0 deletions wallets/provider-phantom/src/namespaces/utxo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { CaipAccount } from '@rango-dev/wallets-core/namespaces/common';
import type { UtxoActions } from '@rango-dev/wallets-core/namespaces/utxo';

import { NamespaceBuilder } from '@rango-dev/wallets-core';
import { builders as commonBuilders } from '@rango-dev/wallets-core/namespaces/common';
import {
builders,
CAIP_NAMESPACE,
} from '@rango-dev/wallets-core/namespaces/utxo';
import { CAIP } from '@rango-dev/wallets-core/utils';
import { Networks } from '@rango-dev/wallets-shared';

import { WALLET_ID } from '../constants.js';
import { bitcoinPhantom, getBitcoinAccounts } from '../utils.js';

const connect = builders
.connect()
.action(async function () {
const bitcoinInstance = bitcoinPhantom();
const result = await getBitcoinAccounts({
instance: bitcoinInstance,
meta: [],
});
if (Array.isArray(result)) {
throw new Error(
'Expecting bitcoin response to be a single value, not an array.'
);
}

const formatAccounts = result.accounts.map(
(account) =>
CAIP.AccountId.format({
address: account,
chainId: {
namespace: CAIP_NAMESPACE,
reference: Networks.BTC,
},
}) as CaipAccount
);

return [formatAccounts[0]];
})
.build();

const disconnect = commonBuilders.disconnect<UtxoActions>().build();

const utxo = new NamespaceBuilder<UtxoActions>('Utxo', WALLET_ID)
.action(connect)
.action(disconnect)
.build();

export { utxo };
2 changes: 2 additions & 0 deletions wallets/provider-phantom/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ProviderBuilder } from '@rango-dev/wallets-core';
import { info, WALLET_ID } from './constants.js';
import { evm } from './namespaces/evm.js';
import { solana } from './namespaces/solana.js';
import { utxo } from './namespaces/utxo.js';
import { phantom as phantomInstance } from './utils.js';

const provider = new ProviderBuilder(WALLET_ID)
Expand All @@ -17,6 +18,7 @@ const provider = new ProviderBuilder(WALLET_ID)
.config('info', info)
.add('solana', solana)
.add('evm', evm)
.add('utxo', utxo)
.build();

export { provider };
Loading