Skip to content

Commit

Permalink
feat: add bitcoin signer for phantom on hub
Browse files Browse the repository at this point in the history
  • Loading branch information
RyukTheCoder committed Feb 4, 2025
1 parent 6fd292b commit 6ed6089
Show file tree
Hide file tree
Showing 24 changed files with 307 additions and 42 deletions.
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;
}
87 changes: 87 additions & 0 deletions wallets/provider-phantom/src/legacy/utxoSigner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
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 PSBT {
// inputs is list of PSBTInput, which have witnessUtxo or nonWitnessUtxo
inputsToSign: { address: string; signingIndexes: number[] }[];
// psbt in Base64
unsignedPsbtBase64: 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;
}

function base64ToUint8Array(base64String: string) {
const binaryString = atob(base64String);
const length = binaryString.length;
const uint8Array = new Uint8Array(length);

for (let i = 0; i < length; i++) {
uint8Array[i] = binaryString.charCodeAt(i);
}

return uint8Array;
}

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 { 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(
base64ToUint8Array(apiObj.unsignedPsbtBase64),
{
inputsToSign: apiObj.inputsToSign,
}
);

// 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

0 comments on commit 6ed6089

Please sign in to comment.