-
Notifications
You must be signed in to change notification settings - Fork 6
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 ba1a280
Showing
10 changed files
with
791 additions
and
25 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,100 @@ | ||
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 txid = transaction.getId(); | ||
|
||
const txin = 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 txout = transaction.outs.map((output) => ({ | ||
scriptPubKey: output.script.toString('hex'), | ||
value: output.value, | ||
})); | ||
|
||
const [scanTweak] = indexerService.computeScanTweak(txid, txin, txout); | ||
|
||
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,135 @@ | ||
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 url: string; | ||
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; | ||
} | ||
} | ||
} | ||
|
||
getBlockHeight(): Promise<number> { | ||
return this.request({ | ||
data: { | ||
method: 'getblockcount', | ||
params: [], | ||
}, | ||
}); | ||
} | ||
|
||
createWallet(walletName: string): Promise<any> { | ||
return this.request({ | ||
data: { | ||
method: 'createwallet', | ||
params: [walletName], | ||
}, | ||
}); | ||
} | ||
|
||
loadWallet(walletName: string): Promise<any> { | ||
return this.request({ | ||
data: { | ||
method: 'loadwallet', | ||
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,126 @@ | ||
import { mnemonicToSeedSync } from 'bip39'; | ||
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'; | ||
|
||
initEccLib(ecc); | ||
|
||
export type UTXO = { | ||
txid: string; | ||
vout: number; | ||
value: number; | ||
rawTx: string; | ||
}; | ||
export class WalletHelper { | ||
private mnemonic: string; | ||
private seed: Buffer; | ||
private root: BIP32Interface; | ||
|
||
constructor( | ||
mnemonic = 'select approve zebra athlete happy whisper parrot will yellow fortune demand father', | ||
) { | ||
this.mnemonic = mnemonic; | ||
this.seed = mnemonicToSeedSync(this.mnemonic); | ||
this.root = fromSeed(this.seed, 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; | ||
|
||
switch (type) { | ||
case 'p2wpkh': | ||
output = payments.p2wpkh({ | ||
pubkey: Buffer.from(child.publicKey), | ||
network: networks.regtest, | ||
}); | ||
break; | ||
case 'p2tr': | ||
output = payments.p2tr({ | ||
internalPubkey: Buffer.from(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 child = this.root.derivePath(`m/84'/0'/0'/0/${index}`); | ||
const keyPair = child; | ||
psbt.signInput(index, keyPair); | ||
}); | ||
|
||
psbt.finalizeAllInputs(); | ||
|
||
return psbt.extractTransaction(true); | ||
} | ||
} | ||
|
||
const toXOnly = (pubKey) => { | ||
if (pubKey.length === 32) { | ||
return pubKey; | ||
} | ||
|
||
if (pubKey.length === 33 && (pubKey[0] === 0x02 || pubKey[0] === 0x03)) { | ||
return pubKey.slice(1, 33); | ||
} | ||
|
||
throw new Error( | ||
'Invalid public key format. Expected compressed public key or x-only key.', | ||
); | ||
}; |
Oops, something went wrong.