From 8271a92ce133630dccffaf4ff9cb3de9a0183da9 Mon Sep 17 00:00:00 2001 From: RyukTheCoder Date: Tue, 24 Dec 2024 09:24:40 +0000 Subject: [PATCH 1/3] feat: add bitcoin signer for phantom on hub --- wallets/core/namespaces/utxo/package.json | 8 ++ wallets/core/package.json | 6 +- wallets/core/src/hub/provider/types.ts | 2 + wallets/core/src/namespaces/common/types.ts | 2 +- wallets/core/src/namespaces/utxo/actions.ts | 3 + wallets/core/src/namespaces/utxo/after.ts | 3 + wallets/core/src/namespaces/utxo/and.ts | 5 ++ wallets/core/src/namespaces/utxo/before.ts | 3 + wallets/core/src/namespaces/utxo/builders.ts | 10 +++ wallets/core/src/namespaces/utxo/constants.ts | 2 + wallets/core/src/namespaces/utxo/mod.ts | 8 ++ wallets/core/src/namespaces/utxo/types.ts | 13 +++ wallets/provider-phantom/package.json | 4 +- wallets/provider-phantom/src/constants.ts | 2 +- wallets/provider-phantom/src/legacy/index.ts | 13 +-- wallets/provider-phantom/src/legacy/signer.ts | 3 + .../provider-phantom/src/legacy/utxoSigner.ts | 79 +++++++++++++++++++ .../provider-phantom/src/namespaces/utxo.ts | 52 ++++++++++++ wallets/provider-phantom/src/provider.ts | 2 + wallets/provider-phantom/src/utils.ts | 36 ++++++++- wallets/react/src/hub/utils.ts | 2 +- wallets/shared/src/rango.ts | 2 +- widget/embedded/src/utils/hub.ts | 2 + yarn.lock | 28 ++++++- 24 files changed, 276 insertions(+), 14 deletions(-) create mode 100644 wallets/core/namespaces/utxo/package.json create mode 100644 wallets/core/src/namespaces/utxo/actions.ts create mode 100644 wallets/core/src/namespaces/utxo/after.ts create mode 100644 wallets/core/src/namespaces/utxo/and.ts create mode 100644 wallets/core/src/namespaces/utxo/before.ts create mode 100644 wallets/core/src/namespaces/utxo/builders.ts create mode 100644 wallets/core/src/namespaces/utxo/constants.ts create mode 100644 wallets/core/src/namespaces/utxo/mod.ts create mode 100644 wallets/core/src/namespaces/utxo/types.ts create mode 100644 wallets/provider-phantom/src/legacy/utxoSigner.ts create mode 100644 wallets/provider-phantom/src/namespaces/utxo.ts diff --git a/wallets/core/namespaces/utxo/package.json b/wallets/core/namespaces/utxo/package.json new file mode 100644 index 0000000000..0144af0d00 --- /dev/null +++ b/wallets/core/namespaces/utxo/package.json @@ -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 +} diff --git a/wallets/core/package.json b/wallets/core/package.json index 5061b775d8..607612ccf6 100644 --- a/wallets/core/package.json +++ b/wallets/core/package.json @@ -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": [ @@ -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}'", diff --git a/wallets/core/src/hub/provider/types.ts b/wallets/core/src/hub/provider/types.ts index cfc3e0e3c6..c606e43d02 100644 --- a/wallets/core/src/hub/provider/types.ts +++ b/wallets/core/src/hub/provider/types.ts @@ -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'; @@ -25,6 +26,7 @@ export interface CommonNamespaces { evm: EvmActions; solana: SolanaActions; cosmos: CosmosActions; + utxo: UtxoActions; } export type CommonNamespaceKeys = Prettify; diff --git a/wallets/core/src/namespaces/common/types.ts b/wallets/core/src/namespaces/common/types.ts index f4a2e7f8f9..66681ad4b8 100644 --- a/wallets/core/src/namespaces/common/types.ts +++ b/wallets/core/src/namespaces/common/types.ts @@ -6,7 +6,7 @@ type RangoNamespace = | 'EVM' | 'Solana' | 'Cosmos' - | 'UTXO' + | 'Utxo' | 'Starknet' | 'Tron' | 'Ton'; diff --git a/wallets/core/src/namespaces/utxo/actions.ts b/wallets/core/src/namespaces/utxo/actions.ts new file mode 100644 index 0000000000..1f5ece7400 --- /dev/null +++ b/wallets/core/src/namespaces/utxo/actions.ts @@ -0,0 +1,3 @@ +import { recommended as commonRecommended } from '../common/actions.js'; + +export const recommended = [...commonRecommended]; diff --git a/wallets/core/src/namespaces/utxo/after.ts b/wallets/core/src/namespaces/utxo/after.ts new file mode 100644 index 0000000000..229710d3bf --- /dev/null +++ b/wallets/core/src/namespaces/utxo/after.ts @@ -0,0 +1,3 @@ +import { recommended as commonRecommended } from '../common/after.js'; + +export const recommended = [...commonRecommended]; diff --git a/wallets/core/src/namespaces/utxo/and.ts b/wallets/core/src/namespaces/utxo/and.ts new file mode 100644 index 0000000000..0eb8ef6141 --- /dev/null +++ b/wallets/core/src/namespaces/utxo/and.ts @@ -0,0 +1,5 @@ +import { connectAndUpdateStateForSingleNetwork } from '../common/mod.js'; + +export const recommended = [ + ['connect', connectAndUpdateStateForSingleNetwork] as const, +]; diff --git a/wallets/core/src/namespaces/utxo/before.ts b/wallets/core/src/namespaces/utxo/before.ts new file mode 100644 index 0000000000..ceeb4ffad1 --- /dev/null +++ b/wallets/core/src/namespaces/utxo/before.ts @@ -0,0 +1,3 @@ +import { recommended as commonRecommended } from '../common/before.js'; + +export const recommended = [...commonRecommended]; diff --git a/wallets/core/src/namespaces/utxo/builders.ts b/wallets/core/src/namespaces/utxo/builders.ts new file mode 100644 index 0000000000..041d9da128 --- /dev/null +++ b/wallets/core/src/namespaces/utxo/builders.ts @@ -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('connect') + .and(connectAndUpdateStateForSingleNetwork) + .after(intoConnectionFinished); diff --git a/wallets/core/src/namespaces/utxo/constants.ts b/wallets/core/src/namespaces/utxo/constants.ts new file mode 100644 index 0000000000..cfce085e21 --- /dev/null +++ b/wallets/core/src/namespaces/utxo/constants.ts @@ -0,0 +1,2 @@ +export const CAIP_NAMESPACE = 'bip122'; +export const CAIP_BITCOIN_CHAIN_ID = '000000000019d6689c085ae165831e93'; diff --git a/wallets/core/src/namespaces/utxo/mod.ts b/wallets/core/src/namespaces/utxo/mod.ts new file mode 100644 index 0000000000..8f697e4c7f --- /dev/null +++ b/wallets/core/src/namespaces/utxo/mod.ts @@ -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'; diff --git a/wallets/core/src/namespaces/utxo/types.ts b/wallets/core/src/namespaces/utxo/types.ts new file mode 100644 index 0000000000..f0cb5007aa --- /dev/null +++ b/wallets/core/src/namespaces/utxo/types.ts @@ -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; +} + +export type ProviderAPI = Record; diff --git a/wallets/provider-phantom/package.json b/wallets/provider-phantom/package.json index f001afbb4e..b5bb563243 100644 --- a/wallets/provider-phantom/package.json +++ b/wallets/provider-phantom/package.json @@ -23,9 +23,11 @@ "dependencies": { "@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" } -} \ No newline at end of file +} diff --git a/wallets/provider-phantom/src/constants.ts b/wallets/provider-phantom/src/constants.ts index 14b8bbf497..a947deb808 100644 --- a/wallets/provider-phantom/src/constants.ts +++ b/wallets/provider-phantom/src/constants.ts @@ -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'], }, ], }; diff --git a/wallets/provider-phantom/src/legacy/index.ts b/wallets/provider-phantom/src/legacy/index.ts index 527f77ed39..b14aafc9ac 100644 --- a/wallets/provider-phantom/src/legacy/index.ts +++ b/wallets/provider-phantom/src/legacy/index.ts @@ -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', @@ -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, }; }; diff --git a/wallets/provider-phantom/src/legacy/signer.ts b/wallets/provider-phantom/src/legacy/signer.ts index c0af13aaae..76e899d45f 100644 --- a/wallets/provider-phantom/src/legacy/signer.ts +++ b/wallets/provider-phantom/src/legacy/signer.ts @@ -9,11 +9,14 @@ export default async function getSigners( ): Promise { 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; } diff --git a/wallets/provider-phantom/src/legacy/utxoSigner.ts b/wallets/provider-phantom/src/legacy/utxoSigner.ts new file mode 100644 index 0000000000..c355338776 --- /dev/null +++ b/wallets/provider-phantom/src/legacy/utxoSigner.ts @@ -0,0 +1,79 @@ +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 fromHexString = (hexString: string) => + Uint8Array.from( + hexString + .match(/.{1,2}/g) + ?.map((byte) => parseInt(byte, 16)) as Iterable + ); + +export class BTCSigner implements GenericSigner { + private provider: TransferExternalProvider; + constructor(provider: TransferExternalProvider) { + this.provider = provider; + } + + async signMessage(): Promise { + throw SignerError.UnimplementedError('signMessage'); + } + + async signAndSendTx(tx: Transfer): Promise<{ hash: string }> { + const { + fromWalletAddress, + asset, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + psbt, // psbt in hex string format + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + signingIndexes, // index of inputs to sign in an array of number + } = tx; + + if (asset.blockchain !== Networks.BTC) { + throw new Error( + `Signing ${asset.blockchain} transaction is not implemented by the signer.` + ); + } + + const signedPSBTBytes = await this.provider.signPSBT(fromHexString(psbt), { + inputsToSign: [ + { + address: fromWalletAddress, + signingIndexes: signingIndexes, + }, + ], + }); + + // 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: '1', + method: 'sendrawtransaction', + params: [finalPsbtBaseHex], + }); + + console.log('hash', hash); + + return { hash }; + } +} diff --git a/wallets/provider-phantom/src/namespaces/utxo.ts b/wallets/provider-phantom/src/namespaces/utxo.ts new file mode 100644 index 0000000000..7df5c24a2a --- /dev/null +++ b/wallets/provider-phantom/src/namespaces/utxo.ts @@ -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().build(); + +const utxo = new NamespaceBuilder('Utxo', WALLET_ID) + .action(connect) + .action(disconnect) + .build(); + +export { utxo }; diff --git a/wallets/provider-phantom/src/provider.ts b/wallets/provider-phantom/src/provider.ts index 8e022815ca..5a8ba335db 100644 --- a/wallets/provider-phantom/src/provider.ts +++ b/wallets/provider-phantom/src/provider.ts @@ -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) @@ -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 }; diff --git a/wallets/provider-phantom/src/utils.ts b/wallets/provider-phantom/src/utils.ts index be8309b6df..070b961643 100644 --- a/wallets/provider-phantom/src/utils.ts +++ b/wallets/provider-phantom/src/utils.ts @@ -2,6 +2,7 @@ import type { ProviderAPI as EvmProviderApi } from '@rango-dev/wallets-core/name import type { ProviderAPI as SolanaProviderApi } from '@rango-dev/wallets-core/namespaces/solana'; import { LegacyNetworks } from '@rango-dev/wallets-core/legacy'; +import { type Connect, Networks } from '@rango-dev/wallets-shared'; type Provider = Map; @@ -12,7 +13,7 @@ export function phantom(): Provider | null { return null; } - const { solana, ethereum } = phantom; + const { solana, ethereum, bitcoin } = phantom; const instances: Provider = new Map(); @@ -24,6 +25,10 @@ export function phantom(): Provider | null { instances.set(LegacyNetworks.SOLANA, solana); } + if (bitcoin && bitcoin.isPhantom) { + instances.set(LegacyNetworks.BTC, bitcoin); + } + return instances; } @@ -53,3 +58,32 @@ export function solanaPhantom(): SolanaProviderApi { return solanaInstance; } + +export function bitcoinPhantom(): SolanaProviderApi { + const instance = phantom(); + const bitcoinInstance = instance?.get(LegacyNetworks.BTC); + + if (!bitcoinInstance) { + throw new Error( + 'Phantom not injected or Bitcoin not enabled. Please check your wallet.' + ); + } + + return bitcoinInstance; +} + +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, + }; +}; diff --git a/wallets/react/src/hub/utils.ts b/wallets/react/src/hub/utils.ts index 75de486cc8..b7e9b24aaa 100644 --- a/wallets/react/src/hub/utils.ts +++ b/wallets/react/src/hub/utils.ts @@ -290,7 +290,7 @@ export function discoverNamespace(network: string): Namespace { case Networks.DOGE: case Networks.LTC: case Networks.TRON: - return 'UTXO'; + return 'Utxo'; case Networks.TON: return 'Ton'; case Networks.POLKADOT: diff --git a/wallets/shared/src/rango.ts b/wallets/shared/src/rango.ts index 2654d9a580..0c45e2af95 100644 --- a/wallets/shared/src/rango.ts +++ b/wallets/shared/src/rango.ts @@ -120,7 +120,7 @@ export const namespaces: Record< mainBlockchain: 'COSMOS', title: 'Cosmos', }, - UTXO: { + Utxo: { mainBlockchain: 'BTC', title: 'Utxo', }, diff --git a/widget/embedded/src/utils/hub.ts b/widget/embedded/src/utils/hub.ts index aa5ba48ab8..0841249734 100644 --- a/widget/embedded/src/utils/hub.ts +++ b/widget/embedded/src/utils/hub.ts @@ -12,6 +12,8 @@ export function convertCommonNamespacesKeysToLegacyNamespace( return 'Solana'; case 'cosmos': return 'Cosmos'; + case 'utxo': + return 'Utxo'; default: throw new Error( 'Can not convert this common namespace key to a proper legacy key.' diff --git a/yarn.lock b/yarn.lock index 478d05a99b..7406eac183 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9400,6 +9400,15 @@ axios@^1.7.4: form-data "^4.0.0" proxy-from-env "^1.1.0" +axios@^1.7.7: + version "1.7.9" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a" + integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-core@^7.0.0-bridge.0: version "7.0.0-bridge.0" resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece" @@ -9588,6 +9597,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-path@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/bip32-path/-/bip32-path-0.4.2.tgz#5db0416ad6822712f077836e2557b8697c0c7c99" @@ -9625,6 +9639,18 @@ bitcoin-ops@^1.3.0, bitcoin-ops@^1.4.1: resolved "https://registry.yarnpkg.com/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz#e45de620398e22fd4ca6023de43974ff42240278" integrity sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow== +bitcoinjs-lib@6.1.5: + 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" @@ -19054,7 +19080,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.18.0: +typeforce@^1.11.3, typeforce@^1.11.5, typeforce@^1.18.0: version "1.18.0" resolved "https://registry.yarnpkg.com/typeforce/-/typeforce-1.18.0.tgz#d7416a2c5845e085034d70fcc5b6cc4a90edbfdc" integrity sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g== From 35f830b6cc4d8324bb664240edf5913d94a6f69a Mon Sep 17 00:00:00 2001 From: Eren Yeager Date: Tue, 24 Dec 2024 15:51:15 +0000 Subject: [PATCH 2/3] fix: using server's psbt --- wallets/provider-phantom/package.json | 1 + .../provider-phantom/src/legacy/utxoSigner.ts | 139 ++++++++++++++---- yarn.lock | 50 +++---- 3 files changed, 131 insertions(+), 59 deletions(-) diff --git a/wallets/provider-phantom/package.json b/wallets/provider-phantom/package.json index b5bb563243..04fe3104ce 100644 --- a/wallets/provider-phantom/package.json +++ b/wallets/provider-phantom/package.json @@ -21,6 +21,7 @@ "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", diff --git a/wallets/provider-phantom/src/legacy/utxoSigner.ts b/wallets/provider-phantom/src/legacy/utxoSigner.ts index c355338776..217cc62953 100644 --- a/wallets/provider-phantom/src/legacy/utxoSigner.ts +++ b/wallets/provider-phantom/src/legacy/utxoSigner.ts @@ -1,13 +1,42 @@ 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 * as bitcoin from 'bitcoinjs-lib'; import { SignerError } from 'rango-types'; type TransferExternalProvider = any; -const BTC_RPC_URL = 'https://rpc.ankr.com/btc'; +const _BTC_RPC_URL = 'https://rpc.ankr.com/btc'; + +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 + inputs: PSBTInput[]; + // ouputs is either PSBTOutputToAddress or PSBTOutputToScript + outputs: PSBTOutput[]; +} +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( @@ -26,54 +55,102 @@ export class BTCSigner implements GenericSigner { throw SignerError.UnimplementedError('signMessage'); } - async signAndSendTx(tx: Transfer): Promise<{ hash: string }> { - const { - fromWalletAddress, - asset, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - psbt, // psbt in hex string format - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - signingIndexes, // index of inputs to sign in an array of number - } = tx; + 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 psbt = new bitcoin.Psbt({ + network: bitcoin.networks.bitcoin, + }); - const signedPSBTBytes = await this.provider.signPSBT(fromHexString(psbt), { + apiObj.inputs.forEach((input) => { + const { hash, index, witnessUtxo, nonWitnessUtxo } = input; + // TODO: use proper type + const payload: any = { + hash, + index, + }; + + if (witnessUtxo) { + payload.witnessUtxo = { + script: Buffer.from(witnessUtxo.script, 'hex'), + value: Number(witnessUtxo.value), + }; + } + + if (nonWitnessUtxo) { + payload.nonWitnessUtxo = Buffer.from(nonWitnessUtxo, 'hex'); + } + + psbt.addInput(payload); + }); + + apiObj.outputs.forEach((output) => { + if (output.script) { + psbt.addOutput({ + value: Number(output.value), + script: Buffer.from(output.script, 'hex'), + }); + } else if (output.address) { + psbt.addOutput({ + address: output.address, + value: Number(output.value), + }); + } else { + throw new Error('Invalid output in your UTXO.'); + } + }); + + const hex = psbt.toHex(); + + const signedPSBTBytes = await this.provider.signPSBT(fromHexString(hex), { inputsToSign: [ { address: fromWalletAddress, - signingIndexes: signingIndexes, + // TODO: Should we sign all the inputs? + signingIndexes: apiObj.inputs.map((_, i) => i), }, ], }); // Finalize PSBT - const finalPsbt = Bitcoin.Psbt.fromBuffer(Buffer.from(signedPSBTBytes)); + 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: '1', - method: 'sendrawtransaction', - params: [finalPsbtBaseHex], + console.log('finalPsbtBaseHex', { + hex: finalPsbt.toHex(), + base64: finalPsbt.toBase64(), }); - console.log('hash', hash); - - return { hash }; + return { hash: finalPsbtBaseHex }; + /* + * // Broadcast PSBT to rpc node + *const hash = await axios.post< + * { id: string; method: string; params: string[] }, + * string + *>(BTC_RPC_URL, { + * id: '1', + * method: 'sendrawtransaction', + * params: [finalPsbtBaseHex], + *}); + * + *console.log('hash', hash); + * + *return { hash }; + * + */ } } diff --git a/yarn.lock b/yarn.lock index 7406eac183..e084606470 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2099,6 +2099,13 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@bitcoinerlab/secp256k1@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@bitcoinerlab/secp256k1/-/secp256k1-1.2.0.tgz#429d043ef4218b9c71915b50172e9aa4a2a8fea4" + integrity sha512-jeujZSzb3JOZfmJYI0ph1PVpCRV5oaexCgy+RvCXV8XlY+XFB/2n3WOcvBsKLsOw78KYgnQrQWb2HrKE4be88Q== + dependencies: + "@noble/curves" "^1.7.0" + "@chromatic-com/storybook@^1.3.3": version "1.3.3" resolved "https://registry.yarnpkg.com/@chromatic-com/storybook/-/storybook-1.3.3.tgz#102d173d7e67cbc7f974648eaa459aa3d3d53f91" @@ -4345,6 +4352,13 @@ dependencies: "@noble/hashes" "1.4.0" +"@noble/curves@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.7.0.tgz#0512360622439256df892f21d25b388f52505e45" + integrity sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw== + dependencies: + "@noble/hashes" "1.6.0" + "@noble/hashes@1.3.2", "@noble/hashes@^1", "@noble/hashes@^1.0.0", "@noble/hashes@^1.2.0", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.2": version "1.3.2" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" @@ -4355,6 +4369,11 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== +"@noble/hashes@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.6.0.tgz#d4bfb516ad6e7b5111c216a5cc7075f4cf19e6c5" + integrity sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ== + "@noble/hashes@~1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.5.0.tgz#abadc5ca20332db2b1b2aa3e496e9af1213570b0" @@ -18319,16 +18338,7 @@ string-argv@0.3.2: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== -"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: +"string-width-cjs@npm:string-width@^4.2.0", 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== @@ -18420,14 +18430,7 @@ stringify-object@^5.0.0: is-obj "^3.0.0" is-regexp "^3.1.0" -"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: +"strip-ansi-cjs@npm:strip-ansi@^6.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== @@ -19796,7 +19799,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.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== @@ -19814,15 +19817,6 @@ 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" From bc8854a04d30e0c7e845dcc965da5918f20cda0e Mon Sep 17 00:00:00 2001 From: RyukTheCoder Date: Sat, 18 Jan 2025 14:05:25 +0000 Subject: [PATCH 3/3] fix: finalize sign and broadcast tx --- .../provider-phantom/src/legacy/utxoSigner.ts | 104 +++++------------- 1 file changed, 28 insertions(+), 76 deletions(-) diff --git a/wallets/provider-phantom/src/legacy/utxoSigner.ts b/wallets/provider-phantom/src/legacy/utxoSigner.ts index 217cc62953..afd69c9b1f 100644 --- a/wallets/provider-phantom/src/legacy/utxoSigner.ts +++ b/wallets/provider-phantom/src/legacy/utxoSigner.ts @@ -2,12 +2,13 @@ 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://rpc.ankr.com/btc'; +const BTC_RPC_URL = 'https://go.getblock.io/f37bad28a991436483c0a3679a3acbee'; interface PSBTInput { hash: string; @@ -29,9 +30,11 @@ interface PSBTOutput { interface PSBT { // inputs is list of PSBTInput, which have witnessUtxo or nonWitnessUtxo - inputs: PSBTInput[]; + psbtInputs: PSBTInput[]; // ouputs is either PSBTOutputToAddress or PSBTOutputToScript - outputs: PSBTOutput[]; + 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. @@ -70,87 +73,36 @@ export class BTCSigner implements GenericSigner { // Initialize ECC library bitcoin.initEccLib(secp256k1); - const psbt = new bitcoin.Psbt({ - network: bitcoin.networks.bitcoin, - }); - - apiObj.inputs.forEach((input) => { - const { hash, index, witnessUtxo, nonWitnessUtxo } = input; - // TODO: use proper type - const payload: any = { - hash, - index, - }; - - if (witnessUtxo) { - payload.witnessUtxo = { - script: Buffer.from(witnessUtxo.script, 'hex'), - value: Number(witnessUtxo.value), - }; - } - - if (nonWitnessUtxo) { - payload.nonWitnessUtxo = Buffer.from(nonWitnessUtxo, 'hex'); - } - - psbt.addInput(payload); - }); - - apiObj.outputs.forEach((output) => { - if (output.script) { - psbt.addOutput({ - value: Number(output.value), - script: Buffer.from(output.script, 'hex'), - }); - } else if (output.address) { - psbt.addOutput({ - address: output.address, - value: Number(output.value), - }); - } else { - throw new Error('Invalid output in your UTXO.'); + 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), + }, + ], } - }); - - const hex = psbt.toHex(); - - const signedPSBTBytes = await this.provider.signPSBT(fromHexString(hex), { - inputsToSign: [ - { - address: fromWalletAddress, - // TODO: Should we sign all the inputs? - signingIndexes: apiObj.inputs.map((_, i) => i), - }, - ], - }); + ); // Finalize PSBT const finalPsbt = bitcoin.Psbt.fromBuffer(Buffer.from(signedPSBTBytes)); finalPsbt.finalizeAllInputs(); - const finalPsbtBaseHex = finalPsbt.toHex(); + const finalPsbtBaseHex = finalPsbt.extractTransaction().toHex(); - console.log('finalPsbtBaseHex', { - hex: finalPsbt.toHex(), - base64: finalPsbt.toBase64(), + // Broadcast PSBT to rpc node + const response = await axios.post(BTC_RPC_URL, { + id: 'test', + method: 'sendrawtransaction', + params: [finalPsbtBaseHex], }); - return { hash: finalPsbtBaseHex }; - /* - * // Broadcast PSBT to rpc node - *const hash = await axios.post< - * { id: string; method: string; params: string[] }, - * string - *>(BTC_RPC_URL, { - * id: '1', - * method: 'sendrawtransaction', - * params: [finalPsbtBaseHex], - *}); - * - *console.log('hash', hash); - * - *return { hash }; - * - */ + if (!response.data.result) { + throw new Error(response.data.error?.message); + } + + return { hash: response.data.result }; } }