-
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 09fcedd
Showing
12 changed files
with
858 additions
and
82 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,92 @@ | ||
import { Payment, Transaction } from 'bitcoinjs-lib'; | ||
import { computeScantweak } from '@/indexer/indexer.service'; | ||
|
||
export function generateScantweak( | ||
transaction: Transaction, | ||
outputs: Payment[], | ||
): string { | ||
const txid = transaction.getId(); | ||
|
||
const txin = transaction.ins.map((input, index) => { | ||
const isWitness = transaction.hasWitnesses(); | ||
|
||
return { | ||
txid: Buffer.from(input.hash).reverse().toString('hex'), | ||
vout: input.index, | ||
scriptSig: isWitness | ||
? '' | ||
: Buffer.from(input.script).toString('hex'), | ||
witness: isWitness | ||
? input.witness.map((v) => Buffer.from(v).toString('hex')) | ||
: undefined, | ||
prevOutScript: Buffer.from(outputs[index].output).toString('hex'), | ||
}; | ||
}); | ||
|
||
const txout = transaction.outs.map((output) => ({ | ||
scriptPubKey: Buffer.from(output.script).toString('hex'), | ||
value: Number(output.value), | ||
})); | ||
|
||
const scantweak = computeScantweak(txid, txin, txout)[0]; | ||
|
||
return scantweak.toString('hex'); | ||
} | ||
|
||
export const varIntSize = (value: number): number => { | ||
if (value < 0xfd) return 1; | ||
else if (value <= 0xffff) return 3; | ||
else if (value <= 0xffffffff) return 5; | ||
else return 9; | ||
}; | ||
|
||
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 const parseSilentBlock = (data: Buffer) => { | ||
const type = data.readUInt8(0); | ||
const transactions = []; | ||
|
||
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 }; | ||
}; |
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,139 @@ | ||
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.url = `${protocol}://${host}:${port}/`; | ||
this.axiosConfig = { | ||
method: 'POST', | ||
auth: { | ||
username: user, | ||
password: password, | ||
}, | ||
}; | ||
} | ||
|
||
public async request(config: AxiosRequestConfig): Promise<any> { | ||
try { | ||
const response = await axios.request({ | ||
...this.axiosConfig, | ||
...config, | ||
url: this.url, | ||
}); | ||
return response.data?.result; | ||
} catch (error) { | ||
throw new Error( | ||
`Request failed with status code ${error.response.status}`, | ||
); | ||
} | ||
} | ||
|
||
async getBlockHeight(): Promise<number> { | ||
return await this.request({ | ||
data: { | ||
jsonrpc: '1.0', | ||
id: 'silent_payment_indexer', | ||
method: 'getblockcount', | ||
params: [], | ||
}, | ||
}); | ||
} | ||
|
||
async createWallet(walletName: string): Promise<any> { | ||
return await this.request({ | ||
data: { | ||
method: 'createwallet', | ||
params: [walletName], | ||
jsonrpc: '1.0', | ||
id: 'silent_payment_indexer', | ||
}, | ||
}); | ||
} | ||
|
||
async loadWallet(walletName: string): Promise<any> { | ||
return await this.request({ | ||
data: { | ||
method: 'loadwallet', | ||
params: [walletName], | ||
jsonrpc: '1.0', | ||
id: 'silent_payment_indexer', | ||
}, | ||
}); | ||
} | ||
|
||
async getNewAddress(): Promise<string> { | ||
return await this.request({ | ||
data: { | ||
method: 'getnewaddress', | ||
params: [], | ||
jsonrpc: '1.0', | ||
id: 'silent_payment_indexer', | ||
}, | ||
}); | ||
} | ||
|
||
async mineToAddress(numBlocks: number, address: string): Promise<any> { | ||
return await this.request({ | ||
data: { | ||
method: 'generatetoaddress', | ||
params: [numBlocks, address], | ||
}, | ||
}); | ||
} | ||
|
||
async sendToAddress(address: string, amount: number): Promise<string> { | ||
return await this.request({ | ||
data: { | ||
method: 'sendtoaddress', | ||
params: [address, amount], | ||
}, | ||
}); | ||
} | ||
|
||
async sendRawTransaction(rawTx: string): Promise<any> { | ||
return await this.request({ | ||
data: { | ||
method: 'sendrawtransaction', | ||
params: [rawTx], | ||
}, | ||
}); | ||
} | ||
|
||
async getTxOut(txid: string, vout: number): Promise<PartialUtxo> { | ||
return await this.request({ | ||
data: { | ||
method: 'gettxout', | ||
params: [txid, vout], | ||
}, | ||
}); | ||
} | ||
|
||
async getRawTransaction(txid: string): Promise<any> { | ||
return await 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,131 @@ | ||
import { mnemonicToSeedSync } from 'bip39'; | ||
import { BIP32Factory } from 'bip32'; | ||
import * as ecc from 'tiny-secp256k1'; | ||
import { | ||
initEccLib, | ||
payments, | ||
Psbt, | ||
networks, | ||
Payment, | ||
Transaction, | ||
} from 'bitcoinjs-lib'; | ||
|
||
initEccLib(ecc); | ||
const bip32 = BIP32Factory(ecc); | ||
|
||
export class WalletHelper { | ||
private mnemonic: string; | ||
private seed: Buffer; | ||
private root: any; | ||
|
||
constructor( | ||
mnemonic = 'select approve zebra athlete happy whisper parrot will yellow fortune demand father', | ||
) { | ||
this.mnemonic = mnemonic; | ||
this.seed = mnemonicToSeedSync(this.mnemonic); | ||
this.root = bip32.fromSeed(this.seed, networks.regtest); | ||
} | ||
|
||
getMnemonic(): string { | ||
return this.mnemonic; | ||
} | ||
|
||
generateAddresses( | ||
count: number, | ||
type: 'p2wpkh' | 'p2wsh' | '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: 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; | ||
} | ||
|
||
createWallet(): { mnemonic: string; addresses: Payment[] } { | ||
const addresses = this.generateAddresses(10, 'p2wpkh'); | ||
return { mnemonic: this.mnemonic, addresses }; | ||
} | ||
|
||
/** | ||
* Craft and sign a transaction sending 6 BTC to the provided Taproot address. | ||
* | ||
* @param utxos - Array of UTXOs to spend from. | ||
* @param taprootAddress - The Taproot address to send to. | ||
* @param fee - The fee to apply in satoshis. | ||
* @returns {string} The raw signed transaction hex. | ||
*/ | ||
craftTransaction( | ||
utxos: Array<{ | ||
txid: string; | ||
vout: number; | ||
value: number; | ||
rawTx: string; | ||
}>, | ||
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 BTC_TO_SATOSHI = 1e8; | ||
const outputValue = 5.999 * BTC_TO_SATOSHI; | ||
const fee = 0.001 * BTC_TO_SATOSHI; | ||
|
||
if (totalInputValue < outputValue + fee) { | ||
throw new Error('Insufficient funds'); | ||
} | ||
|
||
psbt.addOutput({ | ||
address: taprootOutput.address, | ||
tapInternalKey: taprootOutput.internalPubkey, | ||
value: BigInt(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) => | ||
pubKey.length === 32 ? pubKey : pubKey.slice(1, 33); |
Oops, something went wrong.