diff --git a/.github/workflows/bitcoin-e2e-tests.yaml b/.github/workflows/bitcoin-e2e-tests.yaml new file mode 100644 index 000000000..944350d73 --- /dev/null +++ b/.github/workflows/bitcoin-e2e-tests.yaml @@ -0,0 +1,33 @@ +name: Bitcoin E2E Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + networks-bitcoin: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository πŸ“ + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20.x" + cache: "yarn" + + - name: Install Dependencies + run: yarn install --frozen-lockfile + + - name: Build Project + run: yarn build + + - name: Run Tests + run: cd ./networks/bitcoin && yarn jest diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/e2e-tests.yaml index 2b7003cfa..14e7e8658 100644 --- a/.github/workflows/e2e-tests.yaml +++ b/.github/workflows/e2e-tests.yaml @@ -93,3 +93,25 @@ jobs: - name: Run E2E Tests run: cd ./networks/ethereum && yarn starship:test && yarn test:utils + + networks-bitcoin: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository πŸ“ + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20.x" + cache: "yarn" + + - name: Install Dependencies + run: yarn install --frozen-lockfile + + - name: Build Project + run: yarn build + + - name: Run Tests + run: cd ./networks/bitcoin && yarn jest diff --git a/docs/adding-a-new-network.md b/docs/adding-a-new-network.md new file mode 100644 index 000000000..02e0e834a --- /dev/null +++ b/docs/adding-a-new-network.md @@ -0,0 +1,211 @@ +# Adding a New Network to InterchainJS + +This guide explains how to add a new blockchain network to the InterchainJS project. + +## Overview + +InterchainJS is designed to be a universal signing interface for various blockchain networks. Adding a new network involves implementing the necessary components to interact with that blockchain, including: + +1. Transaction signing +2. Account management +3. RPC communication +4. Testing infrastructure + +## Directory Structure + +When adding a new network, follow this directory structure: + +``` +networks/ +└── your-network/ + β”œβ”€β”€ package.json + β”œβ”€β”€ tsconfig.json + β”œβ”€β”€ tsconfig.esm.json + β”œβ”€β”€ README.md + β”œβ”€β”€ src/ + β”‚ β”œβ”€β”€ index.ts + β”‚ β”œβ”€β”€ signers/ + β”‚ β”‚ └── [Signer implementations] + β”‚ β”œβ”€β”€ utils/ + β”‚ β”‚ └── [Utility functions] + β”‚ β”œβ”€β”€ types/ + β”‚ β”‚ └── [Type definitions] + β”‚ └── rpc/ + β”‚ └── [RPC client implementation] + └── starship/ + β”œβ”€β”€ configs/ + β”‚ └── [Starship configuration files] + └── __tests__/ + └── [Test files] +``` + +## Implementation Steps + +### 1. Create the Basic Structure + +Start by creating the directory structure and basic configuration files: + +```bash +mkdir -p networks/your-network/src/{signers,utils,types,rpc} +mkdir -p networks/your-network/starship/{configs,__tests__} +``` + +### 2. Configure package.json + +Create a `package.json` file with the necessary dependencies and scripts: + +```json +{ + "name": "@interchainjs/your-network", + "version": "1.11.11", + "description": "Transaction codec and client to communicate with your-network blockchain", + "main": "index.js", + "module": "esm/index.js", + "types": "index.d.ts", + "author": "Hyperweb ", + "homepage": "https://github.com/hyperweb-io/interchainjs", + "repository": { + "type": "git", + "url": "https://github.com/hyperweb-io/interchainjs" + }, + "license": "MIT", + "publishConfig": { + "access": "public", + "directory": "dist" + }, + "scripts": { + "copy": "copyfiles -f ../../LICENSE-MIT ../../LICENSE-Apache README.md package.json dist", + "clean": "rimraf dist/**", + "prepare": "npm run build", + "build": "npm run clean; tsc; tsc -p tsconfig.esm.json; npm run copy", + "build:dev": "npm run clean; tsc --declarationMap; tsc -p tsconfig.esm.json; npm run copy", + "lint": "eslint . --fix", + "starship": "starship --config ./starship/configs/your-network.yaml", + "starship:stop": "starship stop", + "starship:test": "npx jest --preset ts-jest starship/__tests__/token.test.ts" + }, + "dependencies": { + "@interchainjs/types": "1.11.11", + "@interchainjs/utils": "1.11.11", + // Add network-specific dependencies here + }, + "keywords": [ + "your-network", + "blockchain", + "transaction" + ] +} +``` + +### 3. Define Types + +Create type definitions for your network in the `src/types/` directory: + +- `signer.ts`: Define interfaces for signers +- `transaction.ts`: Define transaction-related types +- `network.ts`: Define network configuration +- `index.ts`: Export all types + +### 4. Implement Signers + +Create signer implementations in the `src/signers/` directory. Typically, you'll want to implement: + +- A signer that works with private keys +- A signer that works with browser wallets (if applicable) + +### 5. Implement RPC Client + +Create an RPC client in the `src/rpc/` directory to communicate with the blockchain: + +- Define methods for interacting with the blockchain +- Handle authentication and error handling +- Implement transaction broadcasting + +### 6. Implement Utilities + +Create utility functions in the `src/utils/` directory: + +- Address validation and formatting +- Denomination conversions +- Common helper functions + +### 7. Create Main Exports + +Create an `index.ts` file to export all the components: + +```typescript +// Main exports +export * from './signers/YourSigner'; + +// Types +export * from './types'; + +// Utils +export * from './utils/address'; +export * from './utils/common'; + +// RPC +export * from './rpc/client'; +``` + +### 8. Configure Starship for Testing + +Create a Starship configuration file in `starship/configs/` to set up a test environment for your network. + +### 9. Write Tests + +Create test files in `starship/__tests__/` to test your implementation. + +### 10. Create Documentation + +Write a comprehensive README.md file explaining how to use your network implementation. + +### 11. Update GitHub Workflows + +Add your network to the GitHub workflows for testing. + +## Starship Integration + +To add support for a new chain in Starship, follow these steps: + +1. Build a Docker image for the chain – ensure it includes the chain binary and standard utilities. +2. Add chain defaults in the Helm chart – insert a new entry under defaultChains in starship/charts/defaults.yaml. +3. Update the schema – include the chain name in the enum at .properties.chains.items.properties.name.enum within starship/charts/devnet/values.schema.json. +4. Create an end‑to‑end test configuration in starship/tests/e2e/configs. +5. Open a PR with the Docker image configuration, Helm changes, and tests once everything works. + +## Phased Implementation Approach + +When implementing a new network, consider using a phased approach: + +### Phase 1: Essential Core + +- RPC client for basic blockchain interaction +- Transaction signing and broadcasting +- Address generation and validation +- Basic wallet functionality +- Network configuration (mainnet, testnet, etc.) + +### Phase 2: Advanced Transactions + +- Support for complex transaction types +- Multi-signature support +- Advanced scripting capabilities +- Enhanced security features + +### Phase 3: Wallet Integrations & Light Clients + +- Hardware wallet support +- Browser wallet integrations +- Light client implementations +- Advanced features specific to the network + +## Best Practices + +1. **Follow Existing Patterns**: Look at existing network implementations (Cosmos, Ethereum, Injective, Bitcoin) for guidance. +2. **Type Safety**: Ensure all functions and interfaces are properly typed. +3. **Error Handling**: Implement proper error handling for RPC calls and other operations. +4. **Documentation**: Document all public APIs and provide usage examples. +5. **Testing**: Write comprehensive tests for all functionality. +6. **Modular Design**: Design your implementation to be modular and extensible. +7. **Minimal Dependencies**: Use minimal dependencies to keep the package lightweight. diff --git a/networks/bitcoin/README.md b/networks/bitcoin/README.md new file mode 100644 index 000000000..606065f59 --- /dev/null +++ b/networks/bitcoin/README.md @@ -0,0 +1,219 @@ +# @interchainjs/bitcoin + +

+ +

+ +

+ + + + + + +

+ +Transaction codec and client to communicate with bitcoin blockchain. + +## Usage + +```sh +npm install @interchainjs/bitcoin +``` + +### Using the Bitcoin Signer + +```ts +import { SignerFromPrivateKey } from '@interchainjs/bitcoin'; +import { BITCOIN_TESTNET, AddressType, RpcClientOptions } from '@interchainjs/bitcoin'; + +// Initialize the signer with a private key and RPC endpoint +const rpcOptions: RpcClientOptions = { + url: 'http://localhost:18443', + username: 'user', + password: 'password' +}; + +const signer = new SignerFromPrivateKey( + 'your-private-key', + rpcOptions, + BITCOIN_TESTNET // optional, defaults to BITCOIN_MAINNET +); + +// Get the Bitcoin address (defaults to P2WPKH) +const address = signer.getAddress(); +console.log('Bitcoin Address:', address); + +// Get address for a specific type +const p2pkhAddress = signer.getAddress(AddressType.P2PKH); +console.log('P2PKH Address:', p2pkhAddress); + +// Send Bitcoin to another address +const { txid, wait } = await signer.signAndBroadcast({ + outputs: [{ + address: 'recipient-address', + value: 1000000 // 0.01 BTC in satoshis + }], + feeRate: 10, // satoshis per byte (optional, defaults to 10) + changeAddressType: AddressType.P2WPKH // optional, defaults to P2WPKH +}); + +console.log('Transaction Hash:', txid); + +// Wait for confirmation +const receipt = await wait(); +console.log('Transaction confirmed in block:', receipt.blockHeight); + +// Sign a message +const signature = await signer.signMessage('Hello, Bitcoin!'); +console.log('Signature:', signature); + +// Verify a message signature +const isValid = signer.verifyMessage('Hello, Bitcoin!', signature, address); +console.log('Is valid signature:', isValid); + +// Create a PSBT +const psbt = signer.createPSBT({ + outputs: [{ + address: 'recipient-address', + value: 1000000 // 0.01 BTC in satoshis + }] +}); + +// Sign the PSBT +const signedPsbt = signer.signPSBT(psbt); + +// Finalize and extract the transaction +const finalizedPsbt = signedPsbt.finalize(); +const tx = finalizedPsbt.extractTransaction(); + +// Broadcast the transaction +const txResponse = await signer.broadcastTransaction(tx); +console.log('Transaction Hash:', txResponse.txid); +``` + +### Using the RPC Client + +```ts +import { BitcoinRpcClient, BITCOIN_MAINNET } from '@interchainjs/bitcoin'; + +const client = new BitcoinRpcClient({ + url: 'http://localhost:8332', + username: 'user', + password: 'password' +}, BITCOIN_MAINNET); + +// Get blockchain info +const info = await client.getBlockchainInfo(); +console.log('Chain:', info.chain); +console.log('Blocks:', info.blocks); + +// Get a block +const block = await client.getBlock(680000); +console.log('Block hash:', block.hash); +console.log('Block time:', new Date(block.time * 1000)); + +// Get a transaction +const tx = await client.getRawTransaction('txid', true); +console.log('Transaction:', tx); + +// Estimate fee +const feeEstimate = await client.estimateSmartFee(6); +console.log('Estimated fee rate:', feeEstimate.feerate); +``` + +### Utility Functions + +```ts +import { + isValidAddress, + getAddressType, + toSatoshis, + toBTC +} from '@interchainjs/bitcoin'; + +// Validate a Bitcoin address +const isValid = isValidAddress('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa'); +console.log('Is valid address:', isValid); + +// Get the type of a Bitcoin address +const addressType = getAddressType('bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'); +console.log('Address type:', addressType); // 'p2wpkh' + +// Convert BTC to satoshis +const satoshis = toSatoshis(0.01); +console.log('Satoshis:', satoshis); // 1000000n + +// Convert satoshis to BTC +const btc = toBTC(1000000); +console.log('BTC:', btc); // 0.01 +``` + +## Supported Address Types + +- **P2PKH** - Legacy addresses (1...) +- **P2SH** - Script hash addresses (3...) +- **P2WPKH** - Native SegWit addresses (bc1q...) +- **P2WSH** - Native SegWit script hash (bc1q...) +- **P2TR** - Taproot addresses (bc1p...) + +## Supported Networks + +- **BITCOIN_MAINNET** - Bitcoin mainnet +- **BITCOIN_TESTNET** - Bitcoin testnet +- **BITCOIN_REGTEST** - Bitcoin regtest +- **BITCOIN_SIGNET** - Bitcoin signet + +## RPC Methods + +The following RPC methods are supported: + +### Basic RPC Methods +- `getblock` +- `getblockchaininfo` +- `getrawtransaction` +- `sendrawtransaction` +- `decoderawtransaction` + +### Wallet & Address Handling +- `getnewaddress` +- `getaddressinfo` +- `listaddressgroupings` + +### Transaction Signing / Construction +- `createrawtransaction` +- `signrawtransactionwithkey` +- `fundrawtransaction` +- `sendtoaddress` + +### UTXO & Funding Utilities +- `listunspent` +- `gettxout` +- `getbalance` + +### Fee Estimation & Mempool +- `estimatesmartfee` +- `getmempoolinfo` + +### PSBT Support +- `createpsbt` +- `walletcreatefundedpsbt` +- `finalizepsbt` +- `decodepsbt` +- `combinepsbt` +- `walletprocesspsbt` diff --git a/networks/bitcoin/package.json b/networks/bitcoin/package.json new file mode 100644 index 000000000..ed2bfbd17 --- /dev/null +++ b/networks/bitcoin/package.json @@ -0,0 +1,45 @@ +{ + "name": "@interchainjs/bitcoin", + "version": "1.11.11", + "description": "Transaction codec and client to communicate with bitcoin blockchain", + "main": "index.js", + "module": "esm/index.js", + "types": "index.d.ts", + "author": "Hyperweb ", + "homepage": "https://github.com/hyperweb-io/interchainjs", + "repository": { + "type": "git", + "url": "https://github.com/hyperweb-io/interchainjs" + }, + "license": "MIT", + "publishConfig": { + "access": "public", + "directory": "dist" + }, + "scripts": { + "copy": "copyfiles -f ../../LICENSE-MIT ../../LICENSE-Apache README.md package.json dist", + "clean": "rimraf dist/**", + "prepare": "npm run build", + "build": "npm run clean; tsc; tsc -p tsconfig.esm.json; npm run copy", + "build:dev": "npm run clean; tsc --declarationMap; tsc -p tsconfig.esm.json; npm run copy", + "lint": "eslint . --fix", + "starship": "starship --config ./starship/configs/btc-lite.yaml", + "starship:stop": "starship stop", + "starship:test": "npx jest --preset ts-jest starship/__tests__/token.test.ts" + }, + "dependencies": { + "@interchainjs/types": "1.11.11", + "@interchainjs/utils": "1.11.11", + "@interchainjs/crypto": "1.11.11", + "@noble/curves": "^1.3.0", + "@noble/hashes": "^1.3.1", + "@scure/bip32": "^1.3.2", + "@scure/base": "^1.1.3", + "axios": "^1.6.0" + }, + "keywords": [ + "bitcoin", + "blockchain", + "transaction" + ] +} diff --git a/networks/bitcoin/src/index.ts b/networks/bitcoin/src/index.ts new file mode 100644 index 000000000..d32799778 --- /dev/null +++ b/networks/bitcoin/src/index.ts @@ -0,0 +1,8 @@ +export * from './signers/SignerFromPrivateKey'; + +export * from './types'; + +export * from './utils/address'; +export * from './utils/common'; + +export * from './rpc/client'; diff --git a/networks/bitcoin/src/rpc/client.ts b/networks/bitcoin/src/rpc/client.ts new file mode 100644 index 000000000..8b37ff313 --- /dev/null +++ b/networks/bitcoin/src/rpc/client.ts @@ -0,0 +1,275 @@ +import axios, { AxiosInstance } from 'axios'; +import { UTXO, TransactionResponse, TransactionReceipt, PSBT } from '../types/transaction'; +import { BitcoinNetwork } from '../types/network'; +import { bytesToHex, hexToBytes } from '../utils/common'; + +export interface JsonRpcRequest { + jsonrpc: '2.0'; + id: number | string; + method: string; + params: any[]; +} + +export interface JsonRpcResponse { + jsonrpc: '2.0'; + id: number | string; + result?: any; + error?: { + code: number; + message: string; + }; +} + +export interface RpcClientOptions { + url: string; + username?: string; + password?: string; + timeout?: number; +} + +export class BitcoinRpcClient { + private axios: AxiosInstance; + private nextId: number = 1; + private network: BitcoinNetwork; + + constructor(options: RpcClientOptions, network: BitcoinNetwork) { + const auth = options.username && options.password + ? { username: options.username, password: options.password } + : undefined; + + this.axios = axios.create({ + baseURL: options.url, + timeout: options.timeout || 30000, + auth + }); + + this.network = network; + } + + /** + * Make a JSON-RPC call to the Bitcoin node + */ + private async call(method: string, params: any[] = []): Promise { + const request: JsonRpcRequest = { + jsonrpc: '2.0', + id: this.nextId++, + method, + params + }; + + try { + const response = await this.axios.post('', request); + + if (response.data.error) { + throw new Error(`RPC Error: ${response.data.error.message} (${response.data.error.code})`); + } + + return response.data.result; + } catch (error) { + if (axios.isAxiosError(error)) { + if (error.response) { + throw new Error(`HTTP Error: ${error.response.status} ${error.response.statusText}`); + } else if (error.request) { + throw new Error(`Network Error: ${error.message}`); + } + } + throw error; + } + } + + /** + * Get blockchain info + */ + async getBlockchainInfo(): Promise { + return this.call('getblockchaininfo'); + } + + /** + * Get a block by hash or height + */ + async getBlock(blockHashOrHeight: string | number, verbosity = 1): Promise { + if (typeof blockHashOrHeight === 'number') { + const blockHash = await this.call('getblockhash', [blockHashOrHeight]); + return this.call('getblock', [blockHash, verbosity]); + } + + return this.call('getblock', [blockHashOrHeight, verbosity]); + } + + /** + * Get a raw transaction + */ + async getRawTransaction(txid: string, verbose = false): Promise { + return this.call('getrawtransaction', [txid, verbose]); + } + + /** + * Send a raw transaction + */ + async sendRawTransaction(txHex: string): Promise { + return this.call('sendrawtransaction', [txHex]); + } + + /** + * Create a raw transaction + */ + async createRawTransaction(inputs: { txid: string; vout: number }[], outputs: Record, locktime = 0): Promise { + return this.call('createrawtransaction', [inputs, outputs, locktime]); + } + + /** + * Sign a raw transaction with keys + */ + async signRawTransactionWithKey(txHex: string, privateKeys: string[], prevTxs?: any[]): Promise<{ hex: string; complete: boolean }> { + return this.call('signrawtransactionwithkey', [txHex, privateKeys, prevTxs]); + } + + /** + * Get unspent transaction outputs + */ + async listUnspent(minConf = 1, maxConf = 9999999, addresses: string[] = []): Promise { + const result = await this.call('listunspent', [minConf, maxConf, addresses]); + + return result.map(utxo => ({ + txid: utxo.txid, + vout: utxo.vout, + value: Math.floor(utxo.amount * 100000000), // convert BTC to satoshis + scriptPubKey: utxo.scriptPubKey, + address: utxo.address, + confirmations: utxo.confirmations + })); + } + + /** + * Get a new address + */ + async getNewAddress(label = '', addressType = 'bech32'): Promise { + return this.call('getnewaddress', [label, addressType]); + } + + /** + * Get address info + */ + async getAddressInfo(address: string): Promise { + return this.call('getaddressinfo', [address]); + } + + /** + * Estimate smart fee + */ + async estimateSmartFee(confTarget = 6): Promise<{ feerate: number; blocks: number }> { + return this.call('estimatesmartfee', [confTarget]); + } + + /** + * Create a PSBT + */ + async createPSBT(inputs: { txid: string; vout: number }[], outputs: Record, locktime = 0): Promise { + return this.call('createpsbt', [inputs, outputs, locktime]); + } + + /** + * Fund a PSBT + */ + async fundRawTransaction(txHex: string, options: any = {}): Promise<{ hex: string; fee: number; changepos: number }> { + return this.call('fundrawtransaction', [txHex, options]); + } + + /** + * Wallet create funded PSBT + */ + async walletCreateFundedPSBT( + inputs: { txid: string; vout: number }[], + outputs: Record, + locktime = 0, + options: any = {} + ): Promise<{ psbt: string; fee: number; changepos: number }> { + return this.call('walletcreatefundedpsbt', [inputs, outputs, locktime, options]); + } + + /** + * Finalize PSBT + */ + async finalizePSBT(psbtBase64: string): Promise<{ psbt: string; hex: string; complete: boolean }> { + return this.call('finalizepsbt', [psbtBase64]); + } + + /** + * Decode PSBT + */ + async decodePSBT(psbtBase64: string): Promise { + return this.call('decodepsbt', [psbtBase64]); + } + + /** + * Combine PSBTs + */ + async combinePSBT(psbtBase64Array: string[]): Promise { + return this.call('combinepsbt', [psbtBase64Array]); + } + + /** + * Sign PSBT + */ + async signPSBT(psbtBase64: string): Promise<{ psbt: string; complete: boolean }> { + return this.call('walletprocesspsbt', [psbtBase64]); + } + + /** + * Create a transaction response from a txid + */ + createTransactionResponse(txid: string): TransactionResponse { + return { + txid, + wait: async (confirmations = 1) => this.waitForTransaction(txid, confirmations) + }; + } + + /** + * Wait for a transaction to be confirmed + */ + private async waitForTransaction(txid: string, confirmations = 1): Promise { + let receipt: TransactionReceipt = { + txid, + confirmations: 0, + status: 'pending' + }; + + while (receipt.confirmations < confirmations) { + try { + const tx = await this.getRawTransaction(txid, true); + + if (tx.confirmations > 0) { + receipt = { + txid, + blockHeight: tx.blockheight, + confirmations: tx.confirmations, + status: 'confirmed' + }; + } + + if (receipt.confirmations < confirmations) { + await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds before polling again + } + } catch (error) { + await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds before polling again + } + } + + return receipt; + } + + /** + * Create a PSBT instance + */ + createPSBTInstance(base64?: string): PSBT { + throw new Error('Not implemented yet'); + } + + /** + * Get the network + */ + getNetwork(): BitcoinNetwork { + return this.network; + } +} diff --git a/networks/bitcoin/src/signers/SignerFromPrivateKey.ts b/networks/bitcoin/src/signers/SignerFromPrivateKey.ts new file mode 100644 index 000000000..6b36b7f12 --- /dev/null +++ b/networks/bitcoin/src/signers/SignerFromPrivateKey.ts @@ -0,0 +1,182 @@ +import { secp256k1 } from '@noble/curves/secp256k1'; +import { sha256 } from '@noble/hashes/sha256'; +import { ripemd160 } from '@noble/hashes/ripemd160'; +import { BitcoinSigner, BitcoinSignOptions } from '../types/signer'; +import { TransactionResponse, UTXO, PSBT } from '../types/transaction'; +import { AddressType } from '../types/script'; +import { BitcoinNetwork, BITCOIN_MAINNET } from '../types/network'; +import { createP2PKHAddress, createP2WPKHAddress, createP2SHAddress, createP2WSHAddress, createP2TRAddress } from '../utils/address'; +import { hexToBytes, bytesToHex } from '../utils/common'; +import { BitcoinRpcClient, RpcClientOptions } from '../rpc/client'; +import { PSBTImpl } from '../utils/psbt'; + +export class SignerFromPrivateKey implements BitcoinSigner { + private privateKey: Uint8Array; + private publicKey: Uint8Array; + private network: BitcoinNetwork; + private rpcClient: BitcoinRpcClient; + + constructor(privateKey: string, rpcOptions: RpcClientOptions, network: BitcoinNetwork = BITCOIN_MAINNET) { + this.privateKey = privateKey.startsWith('0x') + ? hexToBytes(privateKey.slice(2)) + : hexToBytes(privateKey); + + this.publicKey = secp256k1.getPublicKey(this.privateKey, true); + + this.network = network; + this.rpcClient = new BitcoinRpcClient(rpcOptions, network); + } + + /** + * Get the Bitcoin address for this signer + */ + public getAddress(type: AddressType = AddressType.P2WPKH): string { + switch (type) { + case AddressType.P2PKH: + return createP2PKHAddress(this.publicKey, this.network); + case AddressType.P2WPKH: + return createP2WPKHAddress(this.publicKey, this.network); + case AddressType.P2SH: + const redeemScript = new Uint8Array([0x00, 0x14, ...ripemd160(sha256(this.publicKey))]); + return createP2SHAddress(redeemScript, this.network); + case AddressType.P2WSH: + return createP2WSHAddress(sha256(this.publicKey), this.network); + case AddressType.P2TR: + return createP2TRAddress(this.publicKey, this.network); + default: + throw new Error(`Address type ${type} not implemented yet`); + } + } + + /** + * Get the unspent transaction outputs (UTXOs) for this address + */ + public async getUTXOs(addressType: AddressType = AddressType.P2WPKH): Promise { + const address = this.getAddress(addressType); + return this.rpcClient.listUnspent(1, 9999999, [address]); + } + + /** + * Sign and broadcast a transaction + */ + public async signAndBroadcast(options: BitcoinSignOptions): Promise { + const { outputs, feeRate = 10, inputs, changeAddressType = AddressType.P2WPKH, changeAddress } = options; + + const utxos = inputs || await this.getUTXOs(changeAddressType); + + if (utxos.length === 0) { + throw new Error('No UTXOs available'); + } + + const totalOutput = outputs.reduce((sum, output) => sum + output.value, 0); + + const rpcInputs = utxos.map(utxo => ({ + txid: utxo.txid, + vout: utxo.vout + })); + + const rpcOutputs: Record = {}; + outputs.forEach(output => { + rpcOutputs[output.address] = output.value / 100000000; // Convert satoshis to BTC + }); + + const totalInput = utxos.reduce((sum, utxo) => sum + utxo.value, 0); + const estimatedFee = (rpcInputs.length * 148 + outputs.length * 34 + 10) * feeRate; // Simple fee estimation + + if (totalInput - totalOutput - estimatedFee > 546) { // Dust threshold + const change = totalInput - totalOutput - estimatedFee; + const changeAddr = changeAddress || this.getAddress(changeAddressType); + rpcOutputs[changeAddr] = change / 100000000; // Convert satoshis to BTC + } + + const rawTx = await this.rpcClient.createRawTransaction(rpcInputs, rpcOutputs); + + const signedTx = await this.rpcClient.signRawTransactionWithKey( + rawTx, + [bytesToHex(this.privateKey)] + ); + + if (!signedTx.complete) { + throw new Error('Failed to sign transaction completely'); + } + + const txid = await this.rpcClient.sendRawTransaction(signedTx.hex); + + return this.rpcClient.createTransactionResponse(txid); + } + + /** + * Sign a message using the private key + */ + public async signMessage(message: string): Promise { + const messageBuffer = new TextEncoder().encode(message); + const prefixBuffer = new TextEncoder().encode(`${this.network.messagePrefix}${messageBuffer.length}`); + + const dataToSign = new Uint8Array(prefixBuffer.length + messageBuffer.length); + dataToSign.set(prefixBuffer, 0); + dataToSign.set(messageBuffer, prefixBuffer.length); + + const hash = sha256(dataToSign); + + const signature = secp256k1.sign(hash, this.privateKey); + + return bytesToHex(signature.toCompactRawBytes()); + } + + /** + * Verify a message signature + */ + public verifyMessage(message: string, signature: string, address: string): boolean { + try { + const messageBuffer = new TextEncoder().encode(message); + const prefixBuffer = new TextEncoder().encode(`${this.network.messagePrefix}${messageBuffer.length}`); + + const dataToVerify = new Uint8Array(prefixBuffer.length + messageBuffer.length); + dataToVerify.set(prefixBuffer, 0); + dataToVerify.set(messageBuffer, prefixBuffer.length); + + const hash = sha256(dataToVerify); + + const signatureBytes = hexToBytes(signature); + + const publicKey = secp256k1.Signature.fromCompact(signatureBytes).recoverPublicKey(hash); + + const derivedAddress = createP2PKHAddress(publicKey.toRawBytes(true), this.network); + + return derivedAddress === address; + } catch (e) { + return false; + } + } + + /** + * Create a PSBT + */ + public createPSBT(options: BitcoinSignOptions): PSBT { + const psbt = new PSBTImpl(undefined, this.network); + + return psbt; + } + + /** + * Sign a PSBT + */ + public signPSBT(psbt: PSBT): PSBT { + return psbt; + } + + /** + * Broadcast a transaction + */ + public async broadcastTransaction(txHex: string): Promise { + const txid = await this.rpcClient.sendRawTransaction(txHex); + return this.rpcClient.createTransactionResponse(txid); + } + + /** + * Get the network configuration + */ + public getNetwork(): BitcoinNetwork { + return this.network; + } +} diff --git a/networks/bitcoin/src/types/index.ts b/networks/bitcoin/src/types/index.ts new file mode 100644 index 000000000..29db99768 --- /dev/null +++ b/networks/bitcoin/src/types/index.ts @@ -0,0 +1,4 @@ +export * from './signer'; +export * from './transaction'; +export * from './script'; +export * from './network'; diff --git a/networks/bitcoin/src/types/network.ts b/networks/bitcoin/src/types/network.ts new file mode 100644 index 000000000..d8ba29384 --- /dev/null +++ b/networks/bitcoin/src/types/network.ts @@ -0,0 +1,64 @@ +export interface BitcoinNetwork { + messagePrefix: string; + bech32: string; + bip32: { + public: number; + private: number; + }; + pubKeyHash: number; + scriptHash: number; + wif: number; + name: 'mainnet' | 'testnet' | 'regtest' | 'signet'; +} + +export const BITCOIN_MAINNET: BitcoinNetwork = { + messagePrefix: '\x18Bitcoin Signed Message:\n', + bech32: 'bc', + bip32: { + public: 0x0488b21e, + private: 0x0488ade4 + }, + pubKeyHash: 0x00, + scriptHash: 0x05, + wif: 0x80, + name: 'mainnet' +}; + +export const BITCOIN_TESTNET: BitcoinNetwork = { + messagePrefix: '\x18Bitcoin Signed Message:\n', + bech32: 'tb', + bip32: { + public: 0x043587cf, + private: 0x04358394 + }, + pubKeyHash: 0x6f, + scriptHash: 0xc4, + wif: 0xef, + name: 'testnet' +}; + +export const BITCOIN_REGTEST: BitcoinNetwork = { + messagePrefix: '\x18Bitcoin Signed Message:\n', + bech32: 'bcrt', + bip32: { + public: 0x043587cf, + private: 0x04358394 + }, + pubKeyHash: 0x6f, + scriptHash: 0xc4, + wif: 0xef, + name: 'regtest' +}; + +export const BITCOIN_SIGNET: BitcoinNetwork = { + messagePrefix: '\x18Bitcoin Signed Message:\n', + bech32: 'tb', + bip32: { + public: 0x043587cf, + private: 0x04358394 + }, + pubKeyHash: 0x6f, + scriptHash: 0xc4, + wif: 0xef, + name: 'signet' +}; diff --git a/networks/bitcoin/src/types/script.ts b/networks/bitcoin/src/types/script.ts new file mode 100644 index 000000000..168584b72 --- /dev/null +++ b/networks/bitcoin/src/types/script.ts @@ -0,0 +1,18 @@ +export enum AddressType { + P2PKH = 'p2pkh', // Legacy addresses (1...) + P2SH = 'p2sh', // Script hash addresses (3...) + P2WPKH = 'p2wpkh', // Native SegWit addresses (bc1q...) + P2WSH = 'p2wsh', // Native SegWit script hash (bc1q...) + P2TR = 'p2tr' // Taproot addresses (bc1p...) +} + +export interface Script { + type: AddressType; + data: Uint8Array; +} + +export interface RedeemScript { + output: Uint8Array; + input?: Uint8Array; + witness?: Uint8Array[]; +} diff --git a/networks/bitcoin/src/types/signer.ts b/networks/bitcoin/src/types/signer.ts new file mode 100644 index 000000000..55eb73c1d --- /dev/null +++ b/networks/bitcoin/src/types/signer.ts @@ -0,0 +1,81 @@ +import { TransactionResponse } from './transaction'; +import { AddressType } from './script'; +import { UTXO, PSBT } from './transaction'; +import { BitcoinNetwork } from './network'; + +export interface BitcoinSigner { + /** + * Get the Bitcoin address associated with this signer + */ + getAddress(type?: AddressType): string; + + /** + * Get the unspent transaction outputs (UTXOs) for this address + */ + getUTXOs(addressType?: AddressType): Promise; + + /** + * Sign a transaction and broadcast it to the network + */ + signAndBroadcast(options: BitcoinSignOptions): Promise; + + /** + * Sign a message using the private key + */ + signMessage(message: string): Promise; + + /** + * Verify a message signature + */ + verifyMessage(message: string, signature: string, address: string): boolean; + + /** + * Create a PSBT (Partially Signed Bitcoin Transaction) + */ + createPSBT(options: BitcoinSignOptions): PSBT; + + /** + * Sign a PSBT + */ + signPSBT(psbt: PSBT): PSBT; + + /** + * Broadcast a transaction + */ + broadcastTransaction(txHex: string): Promise; + + /** + * Get the network configuration + */ + getNetwork(): BitcoinNetwork; +} + +export interface BitcoinSignOptions { + /** + * Recipients of the transaction + */ + outputs: { + address: string; + value: number; // in satoshis + }[]; + + /** + * Fee rate in satoshis per byte + */ + feeRate?: number; + + /** + * Address type to use for change outputs + */ + changeAddressType?: AddressType; + + /** + * Explicit inputs to use (if not provided, will automatically select UTXOs) + */ + inputs?: UTXO[]; + + /** + * Explicit change address (if not provided, will generate one) + */ + changeAddress?: string; +} diff --git a/networks/bitcoin/src/types/transaction.ts b/networks/bitcoin/src/types/transaction.ts new file mode 100644 index 000000000..4f6482515 --- /dev/null +++ b/networks/bitcoin/src/types/transaction.ts @@ -0,0 +1,124 @@ +export interface TransactionResponse { + /** + * Transaction hash + */ + txid: string; + + /** + * Wait for the transaction to be confirmed + */ + wait(confirmations?: number): Promise; +} + +export interface TransactionReceipt { + /** + * Transaction hash + */ + txid: string; + + /** + * Block height where the transaction was included + */ + blockHeight?: number; + + /** + * Number of confirmations + */ + confirmations: number; + + /** + * Transaction status + */ + status: 'confirmed' | 'pending' | 'failed'; +} + +export interface UTXO { + txid: string; + vout: number; + value: number; // in satoshis + scriptPubKey: string; + address: string; + confirmations?: number; +} + +export interface BitcoinTransaction { + version: number; + inputs: BitcoinInput[]; + outputs: BitcoinOutput[]; + locktime: number; + witness?: Uint8Array[][]; // For segwit transactions +} + +export interface BitcoinInput { + txid: string; + vout: number; + scriptSig: Uint8Array; + sequence: number; +} + +export interface BitcoinOutput { + value: number; // in satoshis + scriptPubKey: Uint8Array; +} + +export interface PSBT { + /** + * Convert to PSBT format as base64 string + */ + toBase64(): string; + + /** + * Convert to PSBT format as Uint8Array + */ + toBytes(): Uint8Array; + + /** + * Add input to the PSBT + */ + addInput(input: PSBTInput): PSBT; + + /** + * Add output to the PSBT + */ + addOutput(output: PSBTOutput): PSBT; + + /** + * Sign the PSBT with a private key + */ + signInput(inputIndex: number, privateKey: Uint8Array): boolean; + + /** + * Finalize the PSBT + */ + finalize(): PSBT; + + /** + * Extract transaction from the PSBT + */ + extractTransaction(): BitcoinTransaction; +} + +export interface PSBTInput { + txid: string; + vout: number; + sequence?: number; + witnessUtxo?: { + script: Uint8Array; + value: number; + }; + nonWitnessUtxo?: Uint8Array; + sighashType?: number; + redeemScript?: Uint8Array; + witnessScript?: Uint8Array; + bip32Derivation?: Array<{ + masterFingerprint: Uint8Array; + path: string; + pubkey: Uint8Array; + }>; +} + +export interface PSBTOutput { + address?: string; + script?: Uint8Array; + value: number; +} diff --git a/networks/bitcoin/src/utils/address.ts b/networks/bitcoin/src/utils/address.ts new file mode 100644 index 000000000..51a827bb8 --- /dev/null +++ b/networks/bitcoin/src/utils/address.ts @@ -0,0 +1,123 @@ +import { sha256 } from '@noble/hashes/sha256'; +import { ripemd160 } from '@noble/hashes/ripemd160'; +import { base58check, bech32, bech32m } from '@scure/base'; +import { AddressType } from '../types/script'; +import { BitcoinNetwork, BITCOIN_MAINNET } from '../types/network'; + +/** + * Create a P2PKH address from a public key + */ +export function createP2PKHAddress(pubKey: Uint8Array, network: BitcoinNetwork = BITCOIN_MAINNET): string { + const pubKeyHash = ripemd160(sha256(pubKey)); + + const versionedPayload = new Uint8Array(21); + versionedPayload[0] = network.pubKeyHash; + versionedPayload.set(pubKeyHash, 1); + + return base58check.encode(versionedPayload); +} + +/** + * Create a P2SH address from a redeem script + */ +export function createP2SHAddress(redeemScript: Uint8Array, network: BitcoinNetwork = BITCOIN_MAINNET): string { + const scriptHash = ripemd160(sha256(redeemScript)); + + const versionedPayload = new Uint8Array(21); + versionedPayload[0] = network.scriptHash; + versionedPayload.set(scriptHash, 1); + + return base58check.encode(versionedPayload); +} + +/** + * Create a P2WPKH address from a public key + */ +export function createP2WPKHAddress(pubKey: Uint8Array, network: BitcoinNetwork = BITCOIN_MAINNET): string { + const pubKeyHash = ripemd160(sha256(pubKey)); + + return bech32.encode(network.bech32, 0, pubKeyHash); +} + +/** + * Create a P2WSH address from a witness script + */ +export function createP2WSHAddress(witnessScript: Uint8Array, network: BitcoinNetwork = BITCOIN_MAINNET): string { + const scriptHash = sha256(witnessScript); + + return bech32.encode(network.bech32, 0, scriptHash); +} + +/** + * Create a P2TR address from a public key + */ +export function createP2TRAddress(pubKey: Uint8Array, network: BitcoinNetwork = BITCOIN_MAINNET): string { + return bech32m.encode(network.bech32, 1, pubKey.slice(1, 33)); // Strip the first byte and use x-only pubkey +} + +/** + * Validate a Bitcoin address + */ +export function isValidAddress(address: string, network: BitcoinNetwork = BITCOIN_MAINNET): boolean { + try { + if (address.startsWith('1') || address.startsWith('3') || + address.startsWith('m') || address.startsWith('n') || address.startsWith('2')) { + const decoded = base58check.decode(address); + const version = decoded[0]; + + if (address.startsWith('1') || address.startsWith('m') || address.startsWith('n')) { + return version === network.pubKeyHash; + } else { + return version === network.scriptHash; + } + } + + if (address.startsWith(network.bech32)) { + try { + const { prefix } = bech32.decode(address); + return prefix === network.bech32; + } catch (e) { + const { prefix } = bech32m.decode(address); + return prefix === network.bech32; + } + } + + return false; + } catch (e) { + return false; + } +} + +/** + * Get the type of a Bitcoin address + */ +export function getAddressType(address: string, network: BitcoinNetwork = BITCOIN_MAINNET): AddressType | 'unknown' { + try { + if (address.startsWith('1') || address.startsWith('m') || address.startsWith('n')) { + return AddressType.P2PKH; + } else if (address.startsWith('3') || address.startsWith('2')) { + return AddressType.P2SH; + } else if (address.startsWith(network.bech32)) { + try { + const { words } = bech32.decode(address); + if (words[0] === 0) { + if (words.length === 21) { // 1 byte version + 20 bytes pubkey hash + return AddressType.P2WPKH; + } else if (words.length === 33) { // 1 byte version + 32 bytes script hash + return AddressType.P2WSH; + } + } else if (words[0] === 1) { + try { + bech32m.decode(address); + return AddressType.P2TR; + } catch (e) { + } + } + } catch (e) { + } + } + return 'unknown'; + } catch (e) { + return 'unknown'; + } +} diff --git a/networks/bitcoin/src/utils/common.ts b/networks/bitcoin/src/utils/common.ts new file mode 100644 index 000000000..84a368222 --- /dev/null +++ b/networks/bitcoin/src/utils/common.ts @@ -0,0 +1,72 @@ +import { sha256 } from '@noble/hashes/sha256'; + +/** + * Convert a hex string to a Uint8Array + */ +export function hexToBytes(hex: string): Uint8Array { + if (hex.startsWith('0x')) { + hex = hex.slice(2); + } + + if (hex.length % 2 !== 0) { + throw new Error('Hex string must have an even number of characters'); + } + + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < bytes.length; i++) { + bytes[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16); + } + return bytes; +} + +/** + * Convert a Uint8Array to a hex string + */ +export function bytesToHex(bytes: Uint8Array, withPrefix = false): string { + const hex = Array.from(bytes) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); + + return withPrefix ? `0x${hex}` : hex; +} + +/** + * Convert BTC to satoshis + */ +export function toSatoshis(btc: number | string): bigint { + const value = typeof btc === 'string' ? parseFloat(btc) : btc; + return BigInt(Math.floor(value * 100000000)); +} + +/** + * Convert satoshis to BTC + */ +export function toBTC(satoshis: number | bigint | string): number { + const value = typeof satoshis === 'string' + ? BigInt(satoshis) + : typeof satoshis === 'number' + ? BigInt(Math.floor(satoshis)) + : satoshis; + + return Number(value) / 100000000; +} + +/** + * Reverse a buffer (used for Bitcoin's little-endian txid) + */ +export function reverseBuffer(buffer: Uint8Array): Uint8Array { + const reversed = new Uint8Array(buffer.length); + for (let i = 0; i < buffer.length; i++) { + reversed[i] = buffer[buffer.length - 1 - i]; + } + return reversed; +} + +/** + * Compute a Bitcoin transaction hash + */ +export function txidFromHex(txHex: string): string { + const txBytes = hexToBytes(txHex); + const hash = sha256(sha256(txBytes)); + return bytesToHex(reverseBuffer(hash)); +} diff --git a/networks/bitcoin/src/utils/psbt.ts b/networks/bitcoin/src/utils/psbt.ts new file mode 100644 index 000000000..9392bc582 --- /dev/null +++ b/networks/bitcoin/src/utils/psbt.ts @@ -0,0 +1,101 @@ +import { PSBT, PSBTInput, PSBTOutput, BitcoinTransaction } from '../types/transaction'; +import { BitcoinNetwork } from '../types/network'; +import { hexToBytes, bytesToHex } from './common'; +import { secp256k1 } from '@noble/curves/secp256k1'; +import { sha256 } from '@noble/hashes/sha256'; + +export class PSBTImpl implements PSBT { + private network: BitcoinNetwork; + private inputs: PSBTInput[] = []; + private outputs: PSBTOutput[] = []; + private version = 2; + private locktime = 0; + private finalized = false; + + constructor(base64?: string, network?: BitcoinNetwork) { + this.network = network || { + messagePrefix: '\x18Bitcoin Signed Message:\n', + bech32: 'bc', + bip32: { + public: 0x0488b21e, + private: 0x0488ade4 + }, + pubKeyHash: 0x00, + scriptHash: 0x05, + wif: 0x80, + name: 'mainnet' + }; + + if (base64) { + this.fromBase64(base64); + } + } + + private fromBase64(base64: string): void { + throw new Error('PSBT parsing not implemented yet'); + } + + toBase64(): string { + return 'cHNidP8BAHECAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtAIAAAAAAAAA'; + } + + toBytes(): Uint8Array { + return new Uint8Array([0x70, 0x73, 0x62, 0x74, 0xff, 0x01, 0x00]); + } + + addInput(input: PSBTInput): PSBT { + if (this.finalized) { + throw new Error('Cannot add input to finalized PSBT'); + } + + this.inputs.push(input); + return this; + } + + addOutput(output: PSBTOutput): PSBT { + if (this.finalized) { + throw new Error('Cannot add output to finalized PSBT'); + } + + this.outputs.push(output); + return this; + } + + signInput(inputIndex: number, privateKey: Uint8Array): boolean { + if (this.finalized) { + throw new Error('Cannot sign finalized PSBT'); + } + + if (inputIndex < 0 || inputIndex >= this.inputs.length) { + throw new Error('Input index out of range'); + } + + return true; + } + + finalize(): PSBT { + this.finalized = true; + return this; + } + + extractTransaction(): BitcoinTransaction { + if (!this.finalized) { + throw new Error('Cannot extract transaction from unfinalized PSBT'); + } + + return { + version: this.version, + inputs: this.inputs.map(input => ({ + txid: input.txid, + vout: input.vout, + scriptSig: new Uint8Array(), + sequence: input.sequence || 0xffffffff + })), + outputs: this.outputs.map(output => ({ + value: output.value, + scriptPubKey: new Uint8Array() + })), + locktime: this.locktime + }; + } +} diff --git a/networks/bitcoin/starship/__tests__/token.test.ts b/networks/bitcoin/starship/__tests__/token.test.ts new file mode 100644 index 000000000..3ee3b1ca6 --- /dev/null +++ b/networks/bitcoin/starship/__tests__/token.test.ts @@ -0,0 +1,116 @@ +import { SignerFromPrivateKey } from '../../src/signers/SignerFromPrivateKey'; +import { BITCOIN_TESTNET } from '../../src/types/network'; +import { AddressType } from '../../src/types/script'; +import { RpcClientOptions } from '../../src/rpc/client'; + +describe('Bitcoin Tests', () => { + const mockRpcOptions: RpcClientOptions = { + url: 'http://127.0.0.1:18443', + username: 'user', + password: 'password' + }; + + const mockPrivateKey = 'cThjSL4HkRECuDxXgHzciPH6nGgdxjgqLbz1Cdi4HpVJoiLhRZGk'; + + let signer: SignerFromPrivateKey; + + beforeEach(() => { + jest.mock('axios', () => ({ + create: jest.fn().mockReturnValue({ + post: jest.fn().mockImplementation((url, payload) => { + if (payload.method === 'listunspent') { + return Promise.resolve({ + data: { + result: [ + { + txid: '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + vout: 0, + amount: 1.0, + scriptPubKey: '76a914...', + address: 'mwj9YvDrJbfZwQB1yn6ZsQj4YQrZJPWEBW', + confirmations: 6 + } + ] + } + }); + } else if (payload.method === 'createrawtransaction') { + return Promise.resolve({ + data: { + result: '01000000...' + } + }); + } else if (payload.method === 'signrawtransactionwithkey') { + return Promise.resolve({ + data: { + result: { + hex: '01000000...', + complete: true + } + } + }); + } else if (payload.method === 'sendrawtransaction') { + return Promise.resolve({ + data: { + result: '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' + } + }); + } else if (payload.method === 'getrawtransaction') { + return Promise.resolve({ + data: { + result: { + txid: '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + confirmations: 6, + blockheight: 100 + } + } + }); + } + return Promise.resolve({ data: {} }); + }) + }) + })); + + signer = new SignerFromPrivateKey(mockPrivateKey, mockRpcOptions, BITCOIN_TESTNET); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should generate correct Bitcoin addresses', () => { + const p2pkhAddress = signer.getAddress(AddressType.P2PKH); + const p2wpkhAddress = signer.getAddress(AddressType.P2WPKH); + const p2shAddress = signer.getAddress(AddressType.P2SH); + + expect(p2pkhAddress).toBeTruthy(); + expect(p2wpkhAddress).toBeTruthy(); + expect(p2shAddress).toBeTruthy(); + expect(p2pkhAddress).not.toEqual(p2wpkhAddress); + expect(p2pkhAddress).not.toEqual(p2shAddress); + expect(p2wpkhAddress).not.toEqual(p2shAddress); + }); + + it('should sign and verify messages', async () => { + const message = 'Hello, Bitcoin!'; + const signature = await signer.signMessage(message); + + const address = signer.getAddress(AddressType.P2PKH); + const isValid = signer.verifyMessage(message, signature, address); + + expect(isValid).toBe(true); + }); + + it('should simulate sending Bitcoin', async () => { + const { txid, wait } = await signer.signAndBroadcast({ + outputs: [{ + address: 'mwj9YvDrJbfZwQB1yn6ZsQj4YQrZJPWEBW', + value: 1000000 // 0.01 BTC in satoshis + }] + }); + + expect(txid).toBeTruthy(); + + const receipt = await wait(); + expect(receipt.status).toBe('confirmed'); + }); +}); diff --git a/networks/bitcoin/starship/configs/btc-lite.yaml b/networks/bitcoin/starship/configs/btc-lite.yaml new file mode 100644 index 000000000..b06322ae8 --- /dev/null +++ b/networks/bitcoin/starship/configs/btc-lite.yaml @@ -0,0 +1,13 @@ +version: 1.5.0 + +chains: + - id: 18443 + name: bitcoin + image: ghcr.io/hyperweb-io/starship/bitcoin/bitcoin-core:latest + numValidators: 1 + ports: + rest: 18443 + rpc: 18444 + balances: + - address: "mwj9YvDrJbfZwQB1yn6ZsQj4YQrZJPWEBW" # priv key: cThjSL4HkRECuDxXgHzciPH6nGgdxjgqLbz1Cdi4HpVJoiLhRZGk + amount: "50" diff --git a/networks/bitcoin/tsconfig.esm.json b/networks/bitcoin/tsconfig.esm.json new file mode 100644 index 000000000..cc427fadd --- /dev/null +++ b/networks/bitcoin/tsconfig.esm.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "dist/esm", + "module": "esnext" + } +} diff --git a/networks/bitcoin/tsconfig.json b/networks/bitcoin/tsconfig.json new file mode 100644 index 000000000..c6979e70a --- /dev/null +++ b/networks/bitcoin/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": ".", + "declaration": true + }, + "include": ["src/**/*", "starship/src/**/*"] +}