-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
cbdb41d
commit 861eef8
Showing
10 changed files
with
737 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import { Payment, Transaction } from 'bitcoinjs-lib'; | ||
import { IndexerService } from '@/indexer/indexer.service'; | ||
import { SATS_PER_BTC } from '@/common/constants'; | ||
import * as currency from 'currency.js'; | ||
import { varIntSize } from '@/common/common'; | ||
|
||
export function generateScanTweak( | ||
transaction: Transaction, | ||
outputs: Payment[], | ||
indexerService: IndexerService, | ||
): string { | ||
const txins = transaction.ins.map((input, index) => { | ||
const isWitness = transaction.hasWitnesses(); | ||
|
||
return { | ||
txid: input.hash.reverse().toString('hex'), | ||
vout: input.index, | ||
scriptSig: isWitness ? '' : input.script.toString('hex'), | ||
witness: isWitness | ||
? input.witness.map((v) => v.toString('hex')) | ||
: undefined, | ||
prevOutScript: outputs[index].output.toString('hex'), | ||
}; | ||
}); | ||
const txouts = transaction.outs.map((output) => ({ | ||
scriptPubKey: output.script.toString('hex'), | ||
value: output.value, | ||
})); | ||
|
||
const [scanTweak] = indexerService.computeScanTweak(txins, txouts); | ||
|
||
return scanTweak.toString('hex'); | ||
} | ||
|
||
export const readVarInt = (data: Buffer, cursor: number): number => { | ||
const firstByte = data.readUInt8(cursor); | ||
if (firstByte < 0xfd) return firstByte; | ||
else if (firstByte === 0xfd) return data.readUInt16LE(cursor + 1); | ||
else if (firstByte === 0xfe) return data.readUInt32LE(cursor + 1); | ||
else return Number(data.readBigUInt64LE(cursor + 1)); | ||
}; | ||
|
||
export interface SilentBlockTransaction { | ||
txid: string; | ||
outputs: { | ||
value: number; | ||
pubkey: string; | ||
vout: number; | ||
}[]; | ||
scanTweak: string; | ||
} | ||
|
||
export interface SilentBlock { | ||
type: number; | ||
transactions: SilentBlockTransaction[]; | ||
} | ||
|
||
export const parseSilentBlock = (data: Buffer): SilentBlock => { | ||
const type = data.readUInt8(0); | ||
const transactions: SilentBlockTransaction[] = []; | ||
|
||
let cursor = 1; | ||
const count = readVarInt(data, cursor); | ||
cursor += varIntSize(count); | ||
|
||
for (let i = 0; i < count; i++) { | ||
const txid = data.subarray(cursor, cursor + 32).toString('hex'); | ||
cursor += 32; | ||
|
||
const outputs = []; | ||
const outputCount = readVarInt(data, cursor); | ||
cursor += varIntSize(outputCount); | ||
|
||
for (let j = 0; j < outputCount; j++) { | ||
const value = Number(data.readBigUInt64BE(cursor)); | ||
cursor += 8; | ||
|
||
const pubkey = data.subarray(cursor, cursor + 32).toString('hex'); | ||
cursor += 32; | ||
|
||
const vout = data.readUint32BE(cursor); | ||
cursor += 4; | ||
|
||
outputs.push({ value, pubkey, vout }); | ||
} | ||
|
||
const scanTweak = data.subarray(cursor, cursor + 33).toString('hex'); | ||
cursor += 33; | ||
|
||
transactions.push({ txid, outputs, scanTweak }); | ||
} | ||
|
||
return { type, transactions }; | ||
}; | ||
|
||
export const btcToSats = (amount: number): number => { | ||
return currency(amount, { precision: 8 }).multiply(SATS_PER_BTC).value; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import { readFileSync } from 'fs'; | ||
import * as yaml from 'js-yaml'; | ||
import axios, { AxiosRequestConfig } from 'axios'; | ||
|
||
type PartialUtxo = { | ||
value: number; | ||
scriptPubKey: { | ||
address: string; | ||
}; | ||
}; | ||
|
||
export class BitcoinRPCUtil { | ||
private readonly axiosConfig: AxiosRequestConfig; | ||
|
||
constructor(configPath = './config/e2e.config.yaml') { | ||
const config = yaml.load(readFileSync(configPath, 'utf8')) as Record< | ||
string, | ||
any | ||
>; | ||
const user = config.bitcoinCore.rpcUser; | ||
const password = config.bitcoinCore.rpcPass; | ||
const host = config.bitcoinCore.rpcHost; | ||
const port = config.bitcoinCore.rpcPort; | ||
const protocol = config.bitcoinCore.protocol; | ||
this.axiosConfig = { | ||
method: 'POST', | ||
auth: { | ||
username: user, | ||
password: password, | ||
}, | ||
url: `${protocol}://${host}:${port}/`, | ||
}; | ||
} | ||
|
||
public async request(config: AxiosRequestConfig): Promise<any> { | ||
try { | ||
const response = await axios.request({ | ||
...this.axiosConfig, | ||
...config, | ||
}); | ||
return response.data?.result; | ||
} catch (error) { | ||
if (axios.isAxiosError(error)) { | ||
return { | ||
body: error.response?.data, | ||
status: error.response?.status, | ||
}; | ||
} else { | ||
throw error; | ||
} | ||
} | ||
} | ||
|
||
createWallet(walletName: string): Promise<any> { | ||
return this.request({ | ||
data: { | ||
method: 'createwallet', | ||
params: [walletName], | ||
}, | ||
}); | ||
} | ||
|
||
getNewAddress(): Promise<string> { | ||
return this.request({ | ||
data: { | ||
method: 'getnewaddress', | ||
params: [], | ||
}, | ||
}); | ||
} | ||
|
||
mineToAddress(numBlocks: number, address: string): Promise<any> { | ||
return this.request({ | ||
data: { | ||
method: 'generatetoaddress', | ||
params: [numBlocks, address], | ||
}, | ||
}); | ||
} | ||
|
||
sendToAddress(address: string, amount: number): Promise<string> { | ||
return this.request({ | ||
data: { | ||
method: 'sendtoaddress', | ||
params: [address, amount], | ||
}, | ||
}); | ||
} | ||
|
||
sendRawTransaction(rawTx: string): Promise<any> { | ||
return this.request({ | ||
data: { | ||
method: 'sendrawtransaction', | ||
params: [rawTx], | ||
}, | ||
}); | ||
} | ||
|
||
getTxOut(txid: string, vout: number): Promise<PartialUtxo> { | ||
return this.request({ | ||
data: { | ||
method: 'gettxout', | ||
params: [txid, vout], | ||
}, | ||
}); | ||
} | ||
|
||
getRawTransaction(txid: string): Promise<any> { | ||
return this.request({ | ||
data: { | ||
method: 'getrawtransaction', | ||
params: [txid], | ||
}, | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import { BIP32Interface, fromSeed } from 'bip32'; | ||
import * as ecc from 'tiny-secp256k1'; | ||
import { | ||
initEccLib, | ||
payments, | ||
Psbt, | ||
networks, | ||
Payment, | ||
Transaction, | ||
} from 'bitcoinjs-lib'; | ||
import { btcToSats } from '@e2e/helpers/common.helper'; | ||
import { randomBytes } from 'crypto'; | ||
import { toXOnly } from 'bitcoinjs-lib/src/psbt/bip371'; | ||
|
||
initEccLib(ecc); | ||
|
||
export type UTXO = { | ||
txid: string; | ||
vout: number; | ||
value: number; | ||
rawTx: string; | ||
}; | ||
|
||
export class WalletHelper { | ||
private root: BIP32Interface; | ||
|
||
constructor() { | ||
this.root = fromSeed(randomBytes(64), networks.regtest); | ||
} | ||
|
||
generateAddresses(count: number, type: 'p2wpkh' | 'p2tr'): Payment[] { | ||
const outputs: Payment[] = []; | ||
for (let i = 0; i < count; i++) { | ||
const path = `m/84'/0'/0'/0/${i}`; | ||
const child = this.root.derivePath(path); | ||
let output: Payment; | ||
|
||
switch (type) { | ||
case 'p2wpkh': | ||
output = payments.p2wpkh({ | ||
pubkey: child.publicKey, | ||
network: networks.regtest, | ||
}); | ||
break; | ||
case 'p2tr': | ||
output = payments.p2tr({ | ||
internalPubkey: toXOnly(child.publicKey), | ||
network: networks.regtest, | ||
}); | ||
break; | ||
default: | ||
throw new Error('Unsupported address type'); | ||
} | ||
|
||
outputs.push(output); | ||
} | ||
return outputs; | ||
} | ||
|
||
/** | ||
* Craft and sign a transaction sending 5.999 BTC to the provided Taproot address. | ||
* | ||
* @param utxos - Array of UTXOs to spend from. | ||
* @param taprootOutput - The Taproot output to send to. | ||
* @returns {Transaction} The raw signed transaction. | ||
*/ | ||
craftTransaction(utxos: UTXO[], taprootOutput: Payment): Transaction { | ||
const psbt = new Psbt({ network: networks.regtest }); | ||
|
||
utxos.forEach((utxo) => { | ||
psbt.addInput({ | ||
hash: utxo.txid, | ||
index: utxo.vout, | ||
nonWitnessUtxo: Buffer.from(utxo.rawTx, 'hex'), | ||
}); | ||
}); | ||
|
||
// Add the output to the Taproot address (6 BTC) | ||
const totalInputValue = utxos.reduce( | ||
(acc, utxo) => acc + utxo.value, | ||
0, | ||
); | ||
|
||
const outputValue = btcToSats(5.999); | ||
const fee = btcToSats(0.001); | ||
|
||
if (totalInputValue < outputValue + fee) { | ||
throw new Error('Insufficient funds'); | ||
} | ||
|
||
psbt.addOutput({ | ||
address: taprootOutput.address, | ||
tapInternalKey: taprootOutput.internalPubkey, | ||
value: outputValue, | ||
}); | ||
|
||
// Sign the inputs with the corresponding private keys | ||
utxos.forEach((utxo, index) => { | ||
const keyPair = this.root.derivePath(`m/84'/0'/0'/0/${index}`); | ||
psbt.signInput(index, keyPair); | ||
}); | ||
|
||
psbt.finalizeAllInputs(); | ||
|
||
return psbt.extractTransaction(true); | ||
} | ||
} |
Oops, something went wrong.