-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add bitgo api interaction, setup functions to use for transacti…
…on signing
- Loading branch information
1 parent
36b54c1
commit a56220f
Showing
9 changed files
with
4,672 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# @format | ||
|
||
name: Provision | ||
description: Set up Job with Tasks needed to run a Code Check | ||
runs: | ||
using: 'composite' | ||
steps: | ||
- name: Set up node | ||
uses: actions/setup-node@v3 | ||
with: | ||
node-version: 20 | ||
|
||
- uses: actions/cache@v3 | ||
id: cache | ||
with: | ||
path: '**/node_modules' | ||
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**/package.json') }} |
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,56 @@ | ||
# @format | ||
|
||
name: Code Checks | ||
|
||
on: | ||
merge_group: | ||
push: | ||
branches: | ||
- '**' | ||
|
||
jobs: | ||
lint-eslint: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- uses: ./.github/actions/provision | ||
|
||
- name: Lint | ||
run: yarn lint:eslint | ||
|
||
lint-prettier: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- uses: ./.github/actions/provision | ||
|
||
- name: Prettier | ||
run: yarn lint:prettier | ||
|
||
lint-unused-exports: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- uses: ./.github/actions/provision | ||
|
||
- name: Unused Exports | ||
run: yarn lint:unused-exports | ||
|
||
typecheck: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- uses: ./.github/actions/provision | ||
|
||
- name: Typecheck | ||
run: yarn typecheck | ||
|
||
lint-commit: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v4 | ||
with: | ||
fetch-depth: 0 | ||
|
||
- name: Commit Message | ||
uses: wagoid/commitlint-github-action@v4 |
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,6 @@ | ||
// @ts-check | ||
|
||
import eslint from '@eslint/js'; | ||
import tseslint from 'typescript-eslint'; | ||
|
||
export default tseslint.config(eslint.configs.recommended, ...tseslint.configs.recommended); |
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,50 @@ | ||
{ | ||
"type": "module", | ||
"name": "dlc-btc-bridge-app", | ||
"version": "1.0.0", | ||
"description": "", | ||
"main": "index.ts", | ||
"scripts": { | ||
"clean": "rm -rf dist && rm -rf node_modules", | ||
"build": "tsc", | ||
"start": "node dist/index.js", | ||
"test": "ts-node index.ts", | ||
"lint": "concurrently -g 'yarn lint:eslint' 'yarn lint:prettier' 'yarn run lint:unused-exports' 'yarn run lint:typecheck'", | ||
"lint:eslint": "eslint \"src/**/*.{js,ts}\"", | ||
"lint:eslint:fix": "eslint --fix \"src/**/*.{js,ts}\"", | ||
"lint:prettier": "prettier --check \"{src}/**/*.{ts}\" \"*.{js,json}\"", | ||
"lint:prettier:fix": "prettier --write \"{src}/**/*.{ts}\" *.js", | ||
"lint:unused-exports": "ts-unused-exports tsconfig.json --ignoreFiles=tests", | ||
"lint:typecheck": "tsc --noEmit" | ||
}, | ||
"keywords": [], | ||
"author": "", | ||
"license": "ISC", | ||
"devDependencies": { | ||
"concurrently": "^8.2.2", | ||
"eslint-config-prettier": "^9.1.0", | ||
"eslint-plugin-prettier": "^5.1.3", | ||
"ts-node": "^10.9.2", | ||
"typescript": "4.7.4", | ||
"typescript-eslint": "^7.7.0" | ||
}, | ||
"dependencies": { | ||
"@bitgo/sdk-api": "^1.45.1", | ||
"@bitgo/sdk-coin-btc": "^2.0.6", | ||
"@bitgo/sdk-core": "^26.8.0", | ||
"@scure/base": "^1.1.6", | ||
"@scure/btc-signer": "^1.3.1", | ||
"bip32": "^4.0.0", | ||
"bitcoinjs-lib": "^6.1.5", | ||
"decimal.js": "^10.4.3", | ||
"dotenv": "^16.4.5", | ||
"lint": "^0.8.19", | ||
"ls-lint": "^0.1.2", | ||
"noble": "^1.9.1", | ||
"prettier-eslint": "^16.3.0", | ||
"scure": "^1.6.0", | ||
"tiny-secp256k1": "^2.2.3", | ||
"ts-unused-exports": "^10.0.1", | ||
"typecheck": "^0.1.2" | ||
} | ||
} |
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,258 @@ | ||
/** @format */ | ||
|
||
import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; | ||
import { hex } from '@scure/base'; | ||
import { Transaction, selectUTXO } from '@scure/btc-signer'; | ||
import { P2TROut, p2tr, p2tr_ns, p2wpkh } from '@scure/btc-signer/payment'; | ||
import { taprootTweakPubkey } from '@scure/btc-signer/utils'; | ||
|
||
import BIP32Factory from 'bip32'; | ||
import { Network } from 'bitcoinjs-lib'; | ||
import * as ecc from 'tiny-secp256k1'; | ||
import { satsToBitcoin } from './utilities.js'; | ||
|
||
interface TransactionStatus { | ||
confirmed: boolean; | ||
block_height: number; | ||
block_hash: string; | ||
block_time: number; | ||
} | ||
|
||
interface UTXO { | ||
txid: string; | ||
vout: number; | ||
status: TransactionStatus; | ||
value: number; | ||
} | ||
|
||
export function findPublicKeyOfAddress( | ||
bitcoinAddress: string, | ||
extendedPublicKeyString: string, | ||
bitcoinNetwork: Network | ||
) { | ||
const bip32 = BIP32Factory.BIP32Factory(ecc); | ||
const root = bip32.fromBase58(extendedPublicKeyString); | ||
|
||
for (let i = 0; i < 1000; i++) { | ||
const child = root.derive(1).derive(i); | ||
const { address } = p2wpkh(child.publicKey, bitcoinNetwork); | ||
|
||
if (address === bitcoinAddress) { | ||
console.log('Found matching public key:', child.publicKey.toString('hex')); | ||
return child.publicKey; | ||
} | ||
} | ||
|
||
console.log('No matching public key found.'); | ||
return null; | ||
} | ||
|
||
/** | ||
* Gets the UTXOs of the User's Native Segwit Address. | ||
* | ||
* @param bitcoinNativeSegwitAddress - The User's Native Segwit Address. | ||
* @param bitcoinNetwork - The Bitcoin Network to use. | ||
* @returns A Promise that resolves to the UTXOs of the User's Native Segwit Address. | ||
*/ | ||
async function getUTXOs(bitcoinNativeSegwitAddress: string, bitcoinNetwork: Network): Promise<any> { | ||
const bitcoinBlockchainAPIURL = process.env.BITCOIN_BLOCKCHAIN_API_URL; | ||
|
||
try { | ||
const response = await fetch(`${bitcoinBlockchainAPIURL}/address/${bitcoinNativeSegwitAddress}/utxo`); | ||
|
||
if (!response.ok) { | ||
throw new Error(`Error getting UTXOs: ${response.statusText}`); | ||
} | ||
|
||
const allUTXOs = await response.json(); | ||
|
||
const userPublicKey = hexToBytes(bitcoinNativeSegwitAddress); | ||
const spend = p2wpkh(userPublicKey, bitcoinNetwork); | ||
|
||
const utxos = await Promise.all( | ||
allUTXOs.map(async (utxo: UTXO) => { | ||
const txHex = await (await fetch(`${bitcoinBlockchainAPIURL}/tx/${utxo.txid}/hex`)).text(); | ||
return { | ||
...spend, | ||
txid: utxo.txid, | ||
index: utxo.vout, | ||
value: utxo.value, | ||
nonWitnessUtxo: hex.decode(txHex), | ||
}; | ||
}) | ||
); | ||
return utxos; | ||
} catch (error) { | ||
throw new Error(`Error getting UTXOs: ${error}`); | ||
} | ||
} | ||
|
||
/** | ||
* Creates a Multisig Transaction using the Public Key of the User's Taproot Address and the Attestor Group's Public Key. | ||
* The Funding Transaction is sent to the Multisig Address. | ||
* | ||
* @param userPublicKey - The Public Key of the User's Taproot Address. | ||
* @param attestorGroupPublicKey - The Attestor Group's Public Key. | ||
* @param vaultUUID - The UUID of the Vault. | ||
* @param bitcoinNetwork - The Bitcoin Network to use. | ||
* @returns A promise that resolves to the Multisig Transaction. | ||
*/ | ||
function createMultisigTransaction( | ||
userPublicKey: Uint8Array, | ||
attestorGroupPublicKey: Uint8Array, | ||
vaultUUID: string, | ||
bitcoinNetwork: Network | ||
): P2TROut { | ||
const multisig = p2tr_ns(2, [userPublicKey, attestorGroupPublicKey]); | ||
|
||
const TAPROOT_UNSPENDABLE_KEY_STR = '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0'; | ||
const TAPROOT_UNSPENDABLE_KEY = hexToBytes(TAPROOT_UNSPENDABLE_KEY_STR); | ||
|
||
const tweakedUnspendableWithUUID = taprootTweakPubkey(TAPROOT_UNSPENDABLE_KEY, Buffer.from(vaultUUID))[0]; | ||
const multisigTransaction = p2tr(tweakedUnspendableWithUUID, multisig, bitcoinNetwork); | ||
multisigTransaction.tapInternalKey = tweakedUnspendableWithUUID; | ||
|
||
return multisigTransaction; | ||
} | ||
|
||
/** | ||
* Creates a Funding Transaction to fund the Multisig Transaction. | ||
* | ||
* @param bitcoinAmount - The amount of Bitcoin to fund the Transaction with. | ||
* @param bitcoinNetwork - The Bitcoin Network to use. | ||
* @param multisigAddress - The Multisig Address. | ||
* @param utxos - The UTXOs to use for the Transaction. | ||
* @param userChangeAddress - The user's Change Address. | ||
* @param feeRate - The Fee Rate to use for the Transaction. | ||
* @param feePublicKey - The Fee Recipient's Public Key. | ||
* @param feeBasisPoints - The Fee Basis Points. | ||
* @returns The Funding Transaction. | ||
*/ | ||
function createFundingTransaction( | ||
bitcoinAmount: number, | ||
bitcoinNetwork: Network, | ||
multisigAddress: string, | ||
utxos: any[], | ||
userChangeAddress: string, | ||
feeRate: bigint, | ||
feePublicKey: string, | ||
feeBasisPoints: number | ||
): Transaction { | ||
const feePublicKeyBuffer = Buffer.from(feePublicKey, 'hex'); | ||
const { address: feeAddress } = p2wpkh(feePublicKeyBuffer, bitcoinNetwork); | ||
|
||
if (!feeAddress) throw new Error('Could not create Fee Address'); | ||
|
||
const outputs = [ | ||
{ address: multisigAddress, amount: BigInt(satsToBitcoin(bitcoinAmount)) }, | ||
{ | ||
address: feeAddress, | ||
amount: BigInt(satsToBitcoin(bitcoinAmount) * feeBasisPoints), | ||
}, | ||
]; | ||
|
||
const selected = selectUTXO(utxos, outputs, 'default', { | ||
changeAddress: userChangeAddress, | ||
feePerByte: feeRate, | ||
bip69: false, | ||
createTx: true, | ||
network: bitcoinNetwork, | ||
}); | ||
|
||
const fundingTX = selected?.tx; | ||
|
||
if (!fundingTX) throw new Error('Could not create Funding Transaction'); | ||
|
||
return fundingTX; | ||
} | ||
|
||
/** | ||
* Creates the Closing Transaction. | ||
* Uses the Funding Transaction's ID to create the Closing Transaction. | ||
* The Closing Transaction is sent to the User's Native Segwit Address. | ||
* | ||
* @param bitcoinAmount - The Amount of Bitcoin to fund the Transaction with. | ||
* @param bitcoinNetwork - The Bitcoin Network to use. | ||
* @param fundingTransactionID - The ID of the Funding Transaction. | ||
* @param multisigTransaction - The Multisig Transaction. | ||
* @param userNativeSegwitAddress - The User's Native Segwit Address. | ||
* @param feeRate - The Fee Rate to use for the Transaction. | ||
* @param feePublicKey - The Fee Recipient's Public Key. | ||
* @param feeBasisPoints - The Fee Basis Points. | ||
* @returns The Closing Transaction. | ||
*/ | ||
async function createClosingTransaction( | ||
bitcoinAmount: number, | ||
bitcoinNetwork: Network, | ||
fundingTransactionID: string, | ||
multisigTransaction: P2TROut, | ||
userNativeSegwitAddress: string, | ||
feeRate: bigint, | ||
feePublicKey: string, | ||
feeBasisPoints: number | ||
): Promise<Uint8Array> { | ||
const feePublicKeyBuffer = Buffer.from(feePublicKey, 'hex'); | ||
const { address: feeAddress } = p2wpkh(feePublicKeyBuffer, bitcoinNetwork); | ||
|
||
if (!feeAddress) throw new Error('Could not create Fee Address'); | ||
|
||
const inputs = [ | ||
{ | ||
txid: hexToBytes(fundingTransactionID), | ||
index: 0, | ||
witnessUtxo: { | ||
amount: BigInt(satsToBitcoin(bitcoinAmount)), | ||
script: multisigTransaction.script, | ||
}, | ||
...multisigTransaction, | ||
}, | ||
]; | ||
|
||
const outputs = [ | ||
{ | ||
address: feeAddress, | ||
amount: BigInt(satsToBitcoin(bitcoinAmount) * feeBasisPoints), | ||
}, | ||
]; | ||
|
||
const selected = selectUTXO(inputs, outputs, 'default', { | ||
changeAddress: userNativeSegwitAddress, | ||
feePerByte: feeRate, | ||
bip69: false, | ||
createTx: true, | ||
network: bitcoinNetwork, | ||
}); | ||
|
||
if (!selected?.tx) throw new Error('Could not create Closing Transaction'); | ||
|
||
const closingPSBT = selected.tx.toPSBT(); | ||
|
||
return closingPSBT; | ||
} | ||
|
||
/** | ||
* Broadcasts the Transaction to the Bitcoin Network. | ||
* | ||
* @param transaction - The Transaction to broadcast. | ||
* @returns A Promise that resolves to the Response from the Broadcast Request. | ||
*/ | ||
async function broadcastTransaction(transaction: Transaction): Promise<string> { | ||
const bitcoinBlockchainAPIURL = process.env.BITCOIN_BLOCKCHAIN_API_URL; | ||
|
||
try { | ||
const response = await fetch(`${bitcoinBlockchainAPIURL}/tx`, { | ||
method: 'POST', | ||
body: bytesToHex(transaction.extract()), | ||
}); | ||
|
||
if (!response.ok) { | ||
throw new Error(`HTTP Error! Status: ${response.status}`); | ||
} | ||
|
||
const transactionID = await response.text(); | ||
|
||
return transactionID; | ||
} catch (error) { | ||
throw new Error(`Error broadcasting Transaction: ${error}`); | ||
} | ||
} |
Oops, something went wrong.