From 9762084c14b3fde00704211e46587137ec5aa208 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Thu, 30 May 2024 15:40:03 +0200 Subject: [PATCH] feat: add aliases, modify example constants and file, export additional functions --- eslint.config.js | 4 +- package.json | 12 +- .../attestor-handler.ts | 3 +- src/constants/ethereum-constants.ts | 3 +- src/constants/example-constants.ts | 38 +++ src/constants/index.ts | 2 - src/constants/ledger-constants.ts | 2 - src/dlc-handlers/ledger-dlc-handler.ts | 7 +- src/dlc-handlers/private-key-dlc-handler.ts | 13 +- .../software-wallet-dlc-handler.ts | 8 +- src/example.ts | 260 ++++++++++++++++++ .../{ => bitcoin}/bitcoin-functions.ts | 81 +++--- .../bitcoin/bitcoin-request-functions.ts | 85 ++++++ src/functions/bitcoin/index.ts | 14 + src/functions/{ => bitcoin}/psbt-functions.ts | 7 +- .../{ => ethereum}/ethereum-functions.ts | 8 +- src/functions/ethereum/index.ts | 3 + .../hardware-wallet}/ledger-functions.ts | 6 +- src/index.ts | 5 +- src/models/bitcoin-models.ts | 76 ++++- src/models/errors.ts | 2 - src/models/ethereum-models.ts | 1 - src/models/index.ts | 7 +- src/network-handlers/ethereum-handler.ts | 3 +- .../read-only-ethereum-handler.ts | 10 +- src/prompt-functions.ts | 252 ----------------- .../proof-of-reserve-handler.ts | 122 ++++++++ src/test.ts | 204 -------------- src/utilities/index.ts | 1 - tsconfig.json | 13 + 30 files changed, 696 insertions(+), 556 deletions(-) rename src/{query-handlers => attestor-handlers}/attestor-handler.ts (95%) create mode 100644 src/constants/example-constants.ts create mode 100644 src/example.ts rename src/functions/{ => bitcoin}/bitcoin-functions.ts (88%) create mode 100644 src/functions/bitcoin/bitcoin-request-functions.ts create mode 100644 src/functions/bitcoin/index.ts rename src/functions/{ => bitcoin}/psbt-functions.ts (98%) rename src/functions/{ => ethereum}/ethereum-functions.ts (97%) create mode 100644 src/functions/ethereum/index.ts rename src/{ => functions/hardware-wallet}/ledger-functions.ts (91%) delete mode 100644 src/prompt-functions.ts create mode 100644 src/proof-of-reserve-handlers/proof-of-reserve-handler.ts delete mode 100644 src/test.ts diff --git a/eslint.config.js b/eslint.config.js index 1231f14..90042a0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,10 +1,8 @@ -/** @format */ -// @ts-check import eslint from '@eslint/js'; import tseslint from 'typescript-eslint'; export default tseslint.config(eslint.configs.recommended, ...tseslint.configs.recommended, { rules: { - '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-explicit-any': 'off', }, }); diff --git a/package.json b/package.json index daf675f..50f59fb 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,25 @@ { "type": "module", "name": "dlc-btc-app", - "version": "1.0.3", + "version": "1.0.5", "description": "", "main": "dist/index.js", "types": "dist/index.d.ts", - "files": ["dist"], + "files": [ + "dist" + ], "exports": { ".": "./dist/index.js", "./utilities": "./dist/utilities/index.js", "./constants": "./dist/constants/index.js", - "./models": "./dist/models/index.js" + "./models": "./dist/models/index.js", + "./bitcoin-functions": "./dist/functions/bitcoin-functions/index.js", + "./ethereum-functions": "./dist/functions/ethereum-functions/index.js" }, "scripts": { "clean": "rm -rf dist && rm -rf node_modules", "build": "tsc", - "start": "node dist/test.js", + "start": "node dist/example.js", "test": "ts-node index.ts", "lint": "concurrently -g 'yarn lint:eslint' 'yarn lint:prettier' 'yarn run lint:typecheck'", "lint:eslint": "eslint \"src/**/*.{js,ts}\"", diff --git a/src/query-handlers/attestor-handler.ts b/src/attestor-handlers/attestor-handler.ts similarity index 95% rename from src/query-handlers/attestor-handler.ts rename to src/attestor-handlers/attestor-handler.ts index d6440a4..273de23 100644 --- a/src/query-handlers/attestor-handler.ts +++ b/src/attestor-handlers/attestor-handler.ts @@ -1,5 +1,4 @@ -/** @format */ -import { AttestorError } from '../models/errors.js'; +import { AttestorError } from '@models/errors.js'; export class AttestorHandler { private attestorRootURLs: string[]; diff --git a/src/constants/ethereum-constants.ts b/src/constants/ethereum-constants.ts index d14634d..41ca2ba 100644 --- a/src/constants/ethereum-constants.ts +++ b/src/constants/ethereum-constants.ts @@ -1,5 +1,4 @@ -/** @format */ -import { EthereumNetwork, EthereumNetworkID } from '../models/ethereum-models.js'; +import { EthereumNetwork, EthereumNetworkID } from '@models/ethereum-models.js'; export const ethereumArbitrumSepolia: EthereumNetwork = { name: 'ArbSepolia', diff --git a/src/constants/example-constants.ts b/src/constants/example-constants.ts new file mode 100644 index 0000000..14aa31e --- /dev/null +++ b/src/constants/example-constants.ts @@ -0,0 +1,38 @@ +import { regtest } from 'bitcoinjs-lib/src/networks.js'; + +// Bitcoin +export const EXAMPLE_BITCOIN_NETWORK = regtest; +export const EXAMPLE_BITCOIN_EXTENDED_PRIVATE_KEY = ''; +export const EXAMPLE_BITCOIN_WALLET_ACCOUNT_INDEX = 0; +export const EXAMPLE_REGTEST_BITCOIN_BLOCKCHAIN_API = 'https://devnet.dlc.link/electrs'; +export const EXAMPLE_TESTNET_BITCOIN_BLOCKCHAIN_API = 'https://testnet.dlc.link/electrs'; +export const EXAMPLE_BITCOIN_BLOCKCHAIN_FEE_RECOMMENDATION_API = + 'https://devnet.dlc.link/electrs/fee-estimates'; +export const EXAMPLE_BITCOIN_AMOUNT = 0.01; + +// Ethereum +export const EXAMPLE_ETHEREUM_PRIVATE_KEY = + 'e84775987ab35c5016195c6bb006847f35a6470dea671e3f06853987f6d5c59b'; +export const EXAMPLE_ETHEREUM_NODE_API = 'https://sepolia-rollup.arbitrum.io/rpc'; +export const EXAMPLE_ETHEREUM_READ_ONLY_NODE_API = 'https://sepolia-rollup.arbitrum.io/rpc'; +export const EXAMPLE_ETHEREUM_GITHUB_DEPLOYMENT_PLAN_ROOT_URL = + 'https://raw.githubusercontent.com/DLC-link/dlc-solidity'; +export const EXAMPLE_ETHEREUM_DEVNET_GITHUB_DEPLOYMENT_PLAN_BRANCH = 'dev'; +export const EXAMPLE_ETHEREUM_TESTNET_GITHUB_DEPLOYMENT_PLAN_BRANCH = 'testnet-rolling'; +export const EXAMPLE_ETHEREUM_ATTESTOR_CHAIN_ID = 'evm-arbsepolia'; + +// Attestor +export const EXAMPLE_REGTEST_ATTESTOR_APIS = [ + 'https://devnet.dlc.link/attestor-1', + 'https://devnet.dlc.link/attestor-2', + 'https://devnet.dlc.link/attestor-3', +]; + +export const EXAMPLE_TESTNET_ATTESTOR_APIS = [ + 'https://testnet.dlc.link/attestor-1', + 'https://testnet.dlc.link/attestor-2', + 'https://testnet.dlc.link/attestor-3', +]; + +export const EXAMPLE_TESTNET_ATTESTOR_GROUP_PUBLIC_KEY_V1 = + '0c0bf55fa1ab72462467b973b13e556b07d2fdd8d7a30cdfc10f337e23c7ac00'; diff --git a/src/constants/index.ts b/src/constants/index.ts index f95bc8c..e8f4e81 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,5 +1,3 @@ -/** @format */ - export * from './ethereum-constants.js'; export * from './ledger-constants.js'; export { bitcoin, testnet, regtest } from 'bitcoinjs-lib/src/networks.js'; diff --git a/src/constants/ledger-constants.ts b/src/constants/ledger-constants.ts index 6fc09d1..923cbeb 100644 --- a/src/constants/ledger-constants.ts +++ b/src/constants/ledger-constants.ts @@ -1,5 +1,3 @@ -/** @format */ - export const LEDGER_APPS_MAP = { BITCOIN_MAINNET: 'Bitcoin', BITCOIN_TESTNET: 'Bitcoin Test', diff --git a/src/dlc-handlers/ledger-dlc-handler.ts b/src/dlc-handlers/ledger-dlc-handler.ts index 4f42f9f..d2bb164 100644 --- a/src/dlc-handlers/ledger-dlc-handler.ts +++ b/src/dlc-handlers/ledger-dlc-handler.ts @@ -1,6 +1,6 @@ -/** @format */ import { Transaction } from '@scure/btc-signer'; import { P2Ret, P2TROut, p2wpkh } from '@scure/btc-signer/payment'; +import { truncateAddress } from '@utilities/index.js'; import { Network, Psbt } from 'bitcoinjs-lib'; import { bitcoin, regtest, testnet } from 'bitcoinjs-lib/src/networks.js'; import { AppClient, DefaultWalletPolicy, WalletPolicy } from 'ledger-bitcoin'; @@ -13,7 +13,7 @@ import { getFeeRate, getInputByPaymentTypeArray, getUnspendableKeyCommittedToUUID, -} from '../functions/bitcoin-functions.js'; +} from '../functions/bitcoin/bitcoin-functions.js'; import { addNativeSegwitSignaturesToPSBT, addTaprootInputSignaturesToPSBT, @@ -23,10 +23,9 @@ import { getTaprootInputsToSign, updateNativeSegwitInputs, updateTaprootInputs, -} from '../functions/psbt-functions.js'; +} from '../functions/bitcoin/psbt-functions.js'; import { PaymentInformation } from '../models/bitcoin-models.js'; import { RawVault } from '../models/ethereum-models.js'; -import { truncateAddress } from '../utilities/index.js'; interface LedgerPolicyInformation { nativeSegwitWalletPolicy: DefaultWalletPolicy; diff --git a/src/dlc-handlers/private-key-dlc-handler.ts b/src/dlc-handlers/private-key-dlc-handler.ts index bf4edaa..6fdcecd 100644 --- a/src/dlc-handlers/private-key-dlc-handler.ts +++ b/src/dlc-handlers/private-key-dlc-handler.ts @@ -1,4 +1,3 @@ -/** @format */ import { Transaction, p2wpkh } from '@scure/btc-signer'; import { P2Ret, P2TROut } from '@scure/btc-signer/payment'; import { Signer } from '@scure/btc-signer/transaction'; @@ -13,8 +12,11 @@ import { getBalance, getFeeRate, getUnspendableKeyCommittedToUUID, -} from '../functions/bitcoin-functions.js'; -import { createClosingTransaction, createFundingTransaction } from '../functions/psbt-functions.js'; +} from '../functions/bitcoin/bitcoin-functions.js'; +import { + createClosingTransaction, + createFundingTransaction, +} from '../functions/bitcoin/psbt-functions.js'; import { RequiredPayment } from '../models/bitcoin-models.js'; import { RawVault } from '../models/ethereum-models.js'; @@ -129,7 +131,8 @@ export class PrivateKeyDLCHandler { return privateKey; } - private handlePayment(vaultUUID: string, attestorGroupPublicKey: string): RequiredPayment { + + handlePayment(vaultUUID: string, attestorGroupPublicKey: string): RequiredPayment { try { const unspendablePublicKey = getUnspendableKeyCommittedToUUID(vaultUUID, this.bitcoinNetwork); const unspendableDerivedPublicKey = deriveUnhardenedPublicKey( @@ -146,7 +149,7 @@ export class PrivateKeyDLCHandler { this.derivedKeyPair.nativeSegwitDerivedKeyPair.publicKey, this.bitcoinNetwork ); - console.log(nativeSegwitPayment.address); + const taprootMultisigPayment = createTaprootMultisigPayment( unspendableDerivedPublicKey, attestorDerivedPublicKey, diff --git a/src/dlc-handlers/software-wallet-dlc-handler.ts b/src/dlc-handlers/software-wallet-dlc-handler.ts index bfe2218..89e61f3 100644 --- a/src/dlc-handlers/software-wallet-dlc-handler.ts +++ b/src/dlc-handlers/software-wallet-dlc-handler.ts @@ -1,4 +1,3 @@ -/** @format */ import { Transaction, p2wpkh } from '@scure/btc-signer'; import { P2Ret, P2TROut } from '@scure/btc-signer/payment'; import { Network } from 'bitcoinjs-lib'; @@ -10,8 +9,11 @@ import { getBalance, getFeeRate, getUnspendableKeyCommittedToUUID, -} from '../functions/bitcoin-functions.js'; -import { createClosingTransaction, createFundingTransaction } from '../functions/psbt-functions.js'; +} from '../functions/bitcoin/bitcoin-functions.js'; +import { + createClosingTransaction, + createFundingTransaction, +} from '../functions/bitcoin/psbt-functions.js'; import { RequiredPayment } from '../models/bitcoin-models.js'; import { RawVault } from '../models/ethereum-models.js'; diff --git a/src/example.ts b/src/example.ts new file mode 100644 index 0000000..9bbd0e1 --- /dev/null +++ b/src/example.ts @@ -0,0 +1,260 @@ +import { AttestorHandler } from '@attestor-handlers/attestor-handler.js'; +import { broadcastTransaction } from '@bitcoin/bitcoin-request-functions.js'; +import { ethereumArbitrumSepolia } from '@constants/ethereum-constants.js'; +import { + EXAMPLE_BITCOIN_AMOUNT, + EXAMPLE_BITCOIN_BLOCKCHAIN_FEE_RECOMMENDATION_API, + EXAMPLE_BITCOIN_EXTENDED_PRIVATE_KEY, + EXAMPLE_BITCOIN_WALLET_ACCOUNT_INDEX, + EXAMPLE_ETHEREUM_ATTESTOR_CHAIN_ID, + EXAMPLE_ETHEREUM_DEVNET_GITHUB_DEPLOYMENT_PLAN_BRANCH, + EXAMPLE_ETHEREUM_GITHUB_DEPLOYMENT_PLAN_ROOT_URL, + EXAMPLE_ETHEREUM_NODE_API, + EXAMPLE_ETHEREUM_PRIVATE_KEY, + EXAMPLE_ETHEREUM_READ_ONLY_NODE_API, + EXAMPLE_ETHEREUM_TESTNET_GITHUB_DEPLOYMENT_PLAN_BRANCH, + EXAMPLE_REGTEST_ATTESTOR_APIS, + EXAMPLE_REGTEST_BITCOIN_BLOCKCHAIN_API, + EXAMPLE_TESTNET_ATTESTOR_APIS, + EXAMPLE_TESTNET_ATTESTOR_GROUP_PUBLIC_KEY_V1, + EXAMPLE_TESTNET_BITCOIN_BLOCKCHAIN_API, +} from '@constants/example-constants.js'; +import { LEDGER_APPS_MAP } from '@constants/ledger-constants.js'; +import { LedgerDLCHandler } from '@dlc-handlers/ledger-dlc-handler.js'; +import { PrivateKeyDLCHandler } from '@dlc-handlers/private-key-dlc-handler.js'; +import { fetchEthereumDeploymentPlan } from '@ethereum/ethereum-functions.js'; +import { getLedgerApp } from '@hardware-wallet/ledger-functions.js'; +import { EthereumHandler } from '@network-handlers/ethereum-handler.js'; +import { ReadOnlyEthereumHandler } from '@network-handlers/read-only-ethereum-handler.js'; +import { bytesToHex } from '@noble/hashes/utils'; +import { ProofOfReserveHandler } from '@proof-of-reserve-handlers/proof-of-reserve-handler.js'; +import { shiftValue } from '@utilities/index.js'; +import { regtest, testnet } from 'bitcoinjs-lib/src/networks.js'; +import { Event } from 'ethers'; + +async function runFlowWithPrivateKey() { + // Fetch Ethereum Contract Deployment Plans + const deploymentPlans = await Promise.all( + ['TokenManager', 'DLCManager', 'DLCBTC'].map(contractName => { + return fetchEthereumDeploymentPlan( + contractName, + ethereumArbitrumSepolia, + EXAMPLE_ETHEREUM_DEVNET_GITHUB_DEPLOYMENT_PLAN_BRANCH, + EXAMPLE_ETHEREUM_GITHUB_DEPLOYMENT_PLAN_ROOT_URL + ); + }) + ); + + // Setup Ethereum Handler (with Private Key) + const ethereumHandler = new EthereumHandler( + deploymentPlans, + EXAMPLE_ETHEREUM_PRIVATE_KEY, + EXAMPLE_ETHEREUM_NODE_API, + EXAMPLE_ETHEREUM_READ_ONLY_NODE_API + ); + + // Setup Vault + const setupVaultTransactionReceipt = await ethereumHandler.setupVault( + shiftValue(EXAMPLE_BITCOIN_AMOUNT) + ); + + if (!setupVaultTransactionReceipt) { + throw new Error('Could not setup Vault'); + } + + const vaultUUID = setupVaultTransactionReceipt.events.find( + (event: Event) => event.event === 'SetupVault' + ).args[0]; + + // Setup DLC Handler (with Private Key) + const dlcHandler = new PrivateKeyDLCHandler( + EXAMPLE_BITCOIN_EXTENDED_PRIVATE_KEY, + EXAMPLE_BITCOIN_WALLET_ACCOUNT_INDEX, + regtest, + EXAMPLE_REGTEST_BITCOIN_BLOCKCHAIN_API, + EXAMPLE_BITCOIN_BLOCKCHAIN_FEE_RECOMMENDATION_API + ); + + // Fetch Created Vault + const vault = await ethereumHandler.getRawVault(vaultUUID); + + // Fetch Attestor Group Public Key from the Smart Contract + const attestorGroupPublicKey = await ethereumHandler.getAttestorGroupPublicKey(); + + // Create Funding Transaction + const fundingPSBT = await dlcHandler.createFundingPSBT(vault, attestorGroupPublicKey, 2); + + // Sign Funding Transaction + const fundingTransaction = dlcHandler.signPSBT(fundingPSBT, 'funding'); + + // Create Closing Transaction + const closingTransaction = await dlcHandler.createClosingPSBT(vault, fundingTransaction.id, 2); + + // Sign Closing Transaction + const partiallySignedClosingTransaction = dlcHandler.signPSBT(closingTransaction, 'closing'); + const partiallySignedClosingTransactionHex = bytesToHex( + partiallySignedClosingTransaction.toPSBT() + ); + + // Get Native Segwit Address used for the Vault + const nativeSegwitAddress = dlcHandler.getVaultRelatedAddress('p2wpkh'); + + // Setup Attestor Handler + const attestorHandler = new AttestorHandler( + EXAMPLE_REGTEST_ATTESTOR_APIS, + EXAMPLE_ETHEREUM_ATTESTOR_CHAIN_ID + ); + + // Send Required Information to Attestors to Create PSBT Event + await attestorHandler.createPSBTEvent( + vaultUUID, + fundingTransaction.hex, + partiallySignedClosingTransactionHex, + nativeSegwitAddress + ); + + // Broadcast Funding Transaction + await broadcastTransaction(fundingTransaction.hex, EXAMPLE_REGTEST_BITCOIN_BLOCKCHAIN_API); +} + +async function runFlowWithLedger() { + // Fetch Ethereum Contract Deployment Plans + const deploymentPlans = await Promise.all( + ['TokenManager', 'DLCManager', 'DLCBTC'].map(contractName => { + return fetchEthereumDeploymentPlan( + contractName, + ethereumArbitrumSepolia, + EXAMPLE_ETHEREUM_TESTNET_GITHUB_DEPLOYMENT_PLAN_BRANCH, + EXAMPLE_ETHEREUM_GITHUB_DEPLOYMENT_PLAN_ROOT_URL + ); + }) + ); + + // Setup Ethereum Handler (with Private Key) + const ethereumHandler = new EthereumHandler( + deploymentPlans, + EXAMPLE_ETHEREUM_PRIVATE_KEY, + EXAMPLE_ETHEREUM_NODE_API, + EXAMPLE_ETHEREUM_READ_ONLY_NODE_API + ); + + // Setup Vault + const setupVaultTransactionReceipt = await ethereumHandler.setupVault( + shiftValue(EXAMPLE_BITCOIN_AMOUNT) + ); + + if (!setupVaultTransactionReceipt) { + throw new Error('Could not setup Vault'); + } + + const vaultUUID = setupVaultTransactionReceipt.events.find( + (event: any) => event.event === 'SetupVault' + ).args[0]; + + const ledgerApp = await getLedgerApp(LEDGER_APPS_MAP.BITCOIN_TESTNET); + + if (!ledgerApp) { + throw new Error('Could not get Ledger App'); + } + + const masterFingerprint = await ledgerApp.getMasterFingerprint(); + + // Setup DLC Handler (with Private Key) + const dlcHandler = new LedgerDLCHandler(ledgerApp, masterFingerprint, 1, testnet); + + // Fetch Created Vault + const vault = await ethereumHandler.getRawVault(vaultUUID); + + // Fetch Attestor Group Public Key from the Smart Contract + const attestorGroupPublicKey = await ethereumHandler.getAttestorGroupPublicKey(); + + // Create Native Segwit Payment from the User's Native Segwit Extended Public Key, + // and Taproot Multisig Payment from the User's Taproot Extended Public Key, the Attestor Group Public Key, and the Unspendable Public Key committed to the Vault UUID, + // which will later be used for the Funding and Closing Transaction + await dlcHandler.createPayment(vaultUUID, attestorGroupPublicKey); + + // Create Funding Transaction + const fundingPSBT = await dlcHandler.createFundingPSBT(vault, 2); + + // Sign Funding Transaction + const fundingTransaction = await dlcHandler.signPSBT(fundingPSBT, 'funding'); + + // Create Closing Transaction + const closingTransaction = await dlcHandler.createClosingPSBT(vault, fundingTransaction.id, 2); + + // Sign Closing Transaction + const partiallySignedClosingTransaction = await dlcHandler.signPSBT( + closingTransaction, + 'closing' + ); + const partiallySignedClosingTransactionHex = bytesToHex( + partiallySignedClosingTransaction.toPSBT() + ); + + // Get Native Segwit Address used for the Vault + const nativeSegwitAddress = dlcHandler.getVaultRelatedAddress('p2wpkh'); + + // Setup Attestor Handler + const attestorHandler = new AttestorHandler( + EXAMPLE_TESTNET_ATTESTOR_APIS, + EXAMPLE_ETHEREUM_ATTESTOR_CHAIN_ID + ); + + // Send Required Information to Attestors to Create PSBT Event + await attestorHandler.createPSBTEvent( + vaultUUID, + fundingTransaction.hex, + partiallySignedClosingTransactionHex, + nativeSegwitAddress + ); + + // Broadcast Funding Transaction + await broadcastTransaction(fundingTransaction.hex, EXAMPLE_TESTNET_BITCOIN_BLOCKCHAIN_API); +} + +async function runProofOfReserveCalculation() { + // Fetch Ethereum Contract Deployment Plans + const deploymentPlans = await Promise.all( + ['TokenManager', 'DLCManager', 'DLCBTC'].map(contractName => { + return fetchEthereumDeploymentPlan( + contractName, + ethereumArbitrumSepolia, + EXAMPLE_ETHEREUM_TESTNET_GITHUB_DEPLOYMENT_PLAN_BRANCH, + EXAMPLE_ETHEREUM_GITHUB_DEPLOYMENT_PLAN_ROOT_URL + ); + }) + ); + + // Setup Read-Only Ethereum Handler + const ethereumHandler = new ReadOnlyEthereumHandler(deploymentPlans, EXAMPLE_ETHEREUM_NODE_API); + + // Fetch Attestor Group Public Key from the Smart Contract + const attestorGroupPublicKey = await ethereumHandler.getAttestorGroupPublicKey(); + + // Fetch All Funded Vaults from the Ethereum Smart Contract + const fundedVaults = await ethereumHandler.getContractFundedVaults(); + + // Setup Proof of Reserve Handler + const proofOfReserveHandler = new ProofOfReserveHandler( + EXAMPLE_TESTNET_BITCOIN_BLOCKCHAIN_API, + testnet, + EXAMPLE_TESTNET_ATTESTOR_GROUP_PUBLIC_KEY_V1, + attestorGroupPublicKey + ); + + // Calculate Proof of Reserve in Sats + const proofOfReserveInSats = await proofOfReserveHandler.calculateProofOfReserve(fundedVaults); + console.log(`Proof of Reserve in Sats: ${proofOfReserveInSats}`); +} + +async function example() { + try { + await runFlowWithPrivateKey(); + await runFlowWithLedger(); + await runProofOfReserveCalculation(); + } catch (error) { + throw new Error(`Error: ${error}`); + } +} + +example(); diff --git a/src/functions/bitcoin-functions.ts b/src/functions/bitcoin/bitcoin-functions.ts similarity index 88% rename from src/functions/bitcoin-functions.ts rename to src/functions/bitcoin/bitcoin-functions.ts index e04c475..8b839dc 100644 --- a/src/functions/bitcoin-functions.ts +++ b/src/functions/bitcoin/bitcoin-functions.ts @@ -1,4 +1,4 @@ -/** @format */ +import { hex } from '@scure/base'; import { Address, OutScript, @@ -11,6 +11,7 @@ import { } from '@scure/btc-signer'; import { P2Ret, P2TROut } from '@scure/btc-signer/payment'; import { TransactionInput } from '@scure/btc-signer/psbt'; +import { taprootTweakPubkey } from '@scure/btc-signer/utils'; import { BIP32Factory, BIP32Interface } from 'bip32'; import { Network } from 'bitcoinjs-lib'; import { bitcoin, regtest, testnet } from 'bitcoinjs-lib/src/networks.js'; @@ -18,11 +19,13 @@ import * as ellipticCurveCryptography from 'tiny-secp256k1'; import { BitcoinInputSigningConfig, + BitcoinTransaction, + BitcoinTransactionVectorOutput, FeeRates, PaymentTypes, UTXO, -} from '../models/bitcoin-models.js'; -import { createRangeFromLength, isDefined, isUndefined } from '../utilities/index.js'; +} from '../../models/bitcoin-models.js'; +import { createRangeFromLength, isDefined, isUndefined } from '../../utilities/index.js'; const TAPROOT_UNSPENDABLE_KEY_HEX = '0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0'; @@ -118,6 +121,29 @@ export function createTaprootMultisigPayment( return p2tr(unspendableDerivedPublicKeyFormatted, taprootMultiLeafWallet, bitcoinNetwork); } +export function createTaprootMultisigPaymentLegacy( + publicKeyA: string, + publicKeyB: string, + vaultUUID: string, + bitcoinNetwork: Network +): P2TROut { + const TAPROOT_UNSPENDABLE_KEY_STR = + '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0'; + const TAPROOT_UNSPENDABLE_KEY = hex.decode(TAPROOT_UNSPENDABLE_KEY_STR); + + const tweakedUnspendableTaprootKey = taprootTweakPubkey( + TAPROOT_UNSPENDABLE_KEY, + Buffer.from(vaultUUID) + )[0]; + + const multisigPayment = p2tr_ns(2, [hex.decode(publicKeyA), hex.decode(publicKeyB)]); + + const multisigTransaction = p2tr(tweakedUnspendableTaprootKey, multisigPayment, bitcoinNetwork); + multisigTransaction.tapInternalKey = tweakedUnspendableTaprootKey; + + return multisigTransaction; +} + /** * Evaluates the fee rate from the bitcoin blockchain API. * @@ -235,34 +261,6 @@ export function getFeeRecipientAddressFromPublicKey( return address; } -/** - * 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. - */ -export async function broadcastTransaction( - transaction: string, - bitcoinBlockchainAPIURL: string -): Promise { - try { - const response = await fetch(`${bitcoinBlockchainAPIURL}/tx`, { - method: 'POST', - body: transaction, - }); - - if (!response.ok) { - throw new Error(`Error while broadcasting Bitcoin Transaction: ${await response.text()}`); - } - - const transactionID = await response.text(); - - return transactionID; - } catch (error) { - throw new Error(`Error broadcasting Transaction: ${error}`); - } -} - /** * Creates an Unspendable Key Committed to the Vault UUID. * @param vaultUUID - The UUID of the Vault. @@ -430,6 +428,27 @@ export function getInputByPaymentTypeArray( }); } +export function getValueMatchingInputFromTransaction( + bitcoinTransaction: BitcoinTransaction, + bitcoinValue: number +): BitcoinTransactionVectorOutput { + const valueMatchingTransactionInput = bitcoinTransaction.vout.find( + output => output.value === bitcoinValue + ); + if (!valueMatchingTransactionInput) { + throw new Error('Could not find Value matching Input in Transaction'); + } + return valueMatchingTransactionInput; +} + +export function findMatchingScript(scripts: Uint8Array[], outputScript: Uint8Array): boolean { + return scripts.some( + script => + outputScript.length === script.length && + outputScript.every((value, index) => value === script[index]) + ); +} + /** * Converts an ECDSA Public Key to a Schnorr Public Key. * @param publicKey - The ECDSA Public Key. diff --git a/src/functions/bitcoin/bitcoin-request-functions.ts b/src/functions/bitcoin/bitcoin-request-functions.ts new file mode 100644 index 0000000..dd631d3 --- /dev/null +++ b/src/functions/bitcoin/bitcoin-request-functions.ts @@ -0,0 +1,85 @@ +import { BitcoinTransaction } from '@models/bitcoin-models.js'; + +export async function fetchBitcoinTransaction( + txID: string, + bitcoinBlockchainAPI: string +): Promise { + try { + const bitcoinBlockchainAPITransactionEndpoint = `${bitcoinBlockchainAPI}/tx/${txID}`; + + const response = await fetch(bitcoinBlockchainAPITransactionEndpoint); + + if (!response.ok) + throw new Error(`Bitcoin Network Transaction Response was not OK: ${response.statusText}`); + + return await response.json(); + } catch (error) { + throw new Error(`Error fetching Bitcoin Transaction: ${error}`); + } +} + +/** + * 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. + */ +export async function broadcastTransaction( + transaction: string, + bitcoinBlockchainAPI: string +): Promise { + try { + const response = await fetch(`${bitcoinBlockchainAPI}/tx`, { + method: 'POST', + body: transaction, + }); + + if (!response.ok) { + throw new Error(`Error while broadcasting Bitcoin Transaction: ${await response.text()}`); + } + + const transactionID = await response.text(); + + return transactionID; + } catch (error) { + throw new Error(`Error broadcasting Transaction: ${error}`); + } +} + +export async function fetchBitcoinBlockchainBlockHeight( + bitcoinBlockchainAPI: string +): Promise { + try { + const bitcoinBlockchainBlockHeightURL = `${bitcoinBlockchainAPI}/blocks/tip/height`; + + const response = await fetch(bitcoinBlockchainBlockHeightURL); + + if (!response.ok) + throw new Error( + `Bitcoin Network Block Height Network Response was not OK: ${response.statusText}` + ); + + return await response.json(); + } catch (error) { + throw new Error(`Error fetching Bitcoin Blockchain Block Height: ${error}`); + } +} + +export async function checkBitcoinTransactionConfirmations( + bitcoinTransaction: BitcoinTransaction, + bitcoinBlockHeight: number +): Promise { + try { + if (!bitcoinTransaction.status.block_height) { + throw new Error('Funding Transaction has no Block Height.'); + } + + const confirmations = bitcoinBlockHeight - (bitcoinTransaction.status.block_height + 1); + if (confirmations >= 6) { + return true; + } + return false; + } catch (error) { + throw new Error(`Error checking Bitcoin Transaction Confirmations: ${error}`); + } +} diff --git a/src/functions/bitcoin/index.ts b/src/functions/bitcoin/index.ts new file mode 100644 index 0000000..c2098b1 --- /dev/null +++ b/src/functions/bitcoin/index.ts @@ -0,0 +1,14 @@ +import { + broadcastTransaction, + fetchBitcoinBlockchainBlockHeight, + fetchBitcoinTransaction, +} from '@bitcoin/bitcoin-request-functions.js'; +import { createClosingTransaction, createFundingTransaction } from '@bitcoin/psbt-functions.js'; + +export { + createClosingTransaction, + createFundingTransaction, + broadcastTransaction, + fetchBitcoinBlockchainBlockHeight, + fetchBitcoinTransaction, +}; diff --git a/src/functions/psbt-functions.ts b/src/functions/bitcoin/psbt-functions.ts similarity index 98% rename from src/functions/psbt-functions.ts rename to src/functions/bitcoin/psbt-functions.ts index f3f0e81..4edbe0a 100644 --- a/src/functions/psbt-functions.ts +++ b/src/functions/bitcoin/psbt-functions.ts @@ -1,17 +1,16 @@ -/** @format */ +import { BitcoinInputSigningConfig, PaymentTypes } from '@models/bitcoin-models.js'; import { hexToBytes } from '@noble/hashes/utils'; import { p2wpkh, selectUTXO } from '@scure/btc-signer'; import { P2Ret, P2TROut } from '@scure/btc-signer/payment'; +import { reverseBytes } from '@utilities/index.js'; import { Network, Psbt } from 'bitcoinjs-lib'; import { PartialSignature } from 'ledger-bitcoin/build/main/lib/appClient.js'; -import { BitcoinInputSigningConfig, PaymentTypes } from '../models/bitcoin-models.js'; -import { reverseBytes } from '../utilities/index.js'; import { ecdsaPublicKeyToSchnorr, getFeeRecipientAddressFromPublicKey, getUTXOs, -} from './bitcoin-functions.js'; +} from '../bitcoin/bitcoin-functions.js'; /** * Creates a Funding Transaction to fund the Multisig Transaction. diff --git a/src/functions/ethereum-functions.ts b/src/functions/ethereum/ethereum-functions.ts similarity index 97% rename from src/functions/ethereum-functions.ts rename to src/functions/ethereum/ethereum-functions.ts index 2b3f96c..d8b5ba2 100644 --- a/src/functions/ethereum-functions.ts +++ b/src/functions/ethereum/ethereum-functions.ts @@ -1,14 +1,12 @@ -/** @format */ -import { Contract, Wallet, providers } from 'ethers'; - -import { EthereumError } from '../models/errors.js'; +import { EthereumError } from '@models/errors.js'; import { DLCEthereumContracts, EthereumDeploymentPlan, EthereumNetwork, RawVault, VaultState, -} from '../models/ethereum-models.js'; +} from '@models/ethereum-models.js'; +import { Contract, Wallet, providers } from 'ethers'; export async function fetchEthereumDeploymentPlan( contractName: string, diff --git a/src/functions/ethereum/index.ts b/src/functions/ethereum/index.ts new file mode 100644 index 0000000..9aacf80 --- /dev/null +++ b/src/functions/ethereum/index.ts @@ -0,0 +1,3 @@ +import { fetchEthereumDeploymentPlan } from '@ethereum/ethereum-functions.js'; + +export { fetchEthereumDeploymentPlan }; diff --git a/src/ledger-functions.ts b/src/functions/hardware-wallet/ledger-functions.ts similarity index 91% rename from src/ledger-functions.ts rename to src/functions/hardware-wallet/ledger-functions.ts index 1397993..999910a 100644 --- a/src/ledger-functions.ts +++ b/src/functions/hardware-wallet/ledger-functions.ts @@ -1,10 +1,8 @@ -/** @format */ +import { LEDGER_APPS_MAP } from '@constants/ledger-constants.js'; import Transport from '@ledgerhq/hw-transport-node-hid'; +import { delay } from '@utilities/index.js'; import { AppClient } from 'ledger-bitcoin'; -import { LEDGER_APPS_MAP } from './constants/ledger-constants.js'; -import { delay } from './utilities/index.js'; - type TransportInstance = Awaited>; export async function getLedgerApp(appName: string) { diff --git a/src/index.ts b/src/index.ts index 84322d5..0f82efd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,10 @@ -/** @format */ +import { AttestorHandler } from './attestor-handlers/attestor-handler.js'; import { LedgerDLCHandler } from './dlc-handlers/ledger-dlc-handler.js'; import { PrivateKeyDLCHandler } from './dlc-handlers/private-key-dlc-handler.js'; import { SoftwareWalletDLCHandler } from './dlc-handlers/software-wallet-dlc-handler.js'; import { EthereumHandler } from './network-handlers/ethereum-handler.js'; import { ReadOnlyEthereumHandler } from './network-handlers/read-only-ethereum-handler.js'; -import { AttestorHandler } from './query-handlers/attestor-handler.js'; +import { ProofOfReserveHandler } from './proof-of-reserve-handlers/proof-of-reserve-handler.js'; export { PrivateKeyDLCHandler, @@ -13,4 +13,5 @@ export { EthereumHandler, ReadOnlyEthereumHandler, AttestorHandler, + ProofOfReserveHandler, }; diff --git a/src/models/bitcoin-models.ts b/src/models/bitcoin-models.ts index eb7b8bf..f0c7c0f 100644 --- a/src/models/bitcoin-models.ts +++ b/src/models/bitcoin-models.ts @@ -1,17 +1,9 @@ -/** @format */ import { P2Ret, P2TROut } from '@scure/btc-signer/payment'; -interface TransactionStatus { - confirmed: boolean; - block_height: number; - block_hash: string; - block_time: number; -} - export interface UTXO { txid: string; vout: number; - status: TransactionStatus; + status: BitcoinTransactionStatus; value: number; } @@ -40,6 +32,72 @@ export interface PaymentInformation { taprootDerivedPublicKey: Buffer; } +interface BitcoinTransactionIssuance { + asset_id: string; + is_reissuance: boolean; + asset_blinding_nonce: number; + asset_entropy: number; + contract_hash: string; + assetamount?: number; + assetamountcommitment?: number; + tokenamount?: number; + tokenamountcommitment?: number; +} + +interface BitcoinTransactionPegOut { + genesis_hash: string; + scriptpubkey: string; + scriptpubkey_asm: string; + scriptpubkey_address: string; +} + +interface BitcoinTransactionStatus { + confirmed: boolean; + block_height?: number | null; + block_hash?: string | null; + block_time?: number | null; +} + +export interface BitcoinTransactionVectorOutput { + scriptpubkey: string; + scriptpubkey_asm: string; + scriptpubkey_type: string; + scriptpubkey_address: string; + value: number; + valuecommitment?: number; + asset?: string; + assetcommitment?: number; + pegout?: BitcoinTransactionPegOut | null; +} + +interface BitcoinTransactionVectorInput { + inner_redeemscript_asm?: string; + inner_witnessscript_asm?: string; + is_coinbase: boolean; + is_pegin?: boolean; + issuance?: BitcoinTransactionIssuance | null; + prevout: BitcoinTransactionVectorOutput; + scriptsig: string; + scriptsig_asm: string; + sequence: number; + txid: string; + vout: number; + witness: string[]; +} + +export interface BitcoinTransaction { + fee: number; + locktime: number; + size: number; + status: BitcoinTransactionStatus; + tx_type?: string; + txid: string; + version: number; + vin: BitcoinTransactionVectorInput[]; + vout: BitcoinTransactionVectorOutput[]; + weight: number; +} + export type PaymentTypes = 'p2pkh' | 'p2sh' | 'p2wpkh-p2sh' | 'p2wpkh' | 'p2tr'; export type BitcoinNetworkName = 'Mainnet' | 'Testnet' | 'Regtest'; diff --git a/src/models/errors.ts b/src/models/errors.ts index e7a5afa..67d4e05 100644 --- a/src/models/errors.ts +++ b/src/models/errors.ts @@ -1,5 +1,3 @@ -/** @format */ - export class EthereumError extends Error { constructor(message: string) { super(message); diff --git a/src/models/ethereum-models.ts b/src/models/ethereum-models.ts index b05d460..3f5d926 100644 --- a/src/models/ethereum-models.ts +++ b/src/models/ethereum-models.ts @@ -1,4 +1,3 @@ -/** @format */ import { BigNumber, Contract } from 'ethers'; export interface EthereumNetwork { diff --git a/src/models/index.ts b/src/models/index.ts index 9992519..d928923 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,10 +1,5 @@ -/** @format */ - -// ts-unused-exports:disable-next-line export { Network } from 'bitcoinjs-lib/src/networks.js'; -// ts-unused-exports:disable-next-line +export { Transaction } from '@scure/btc-signer'; export * from '../models/bitcoin-models.js'; -// ts-unused-exports:disable-next-line export * from '../models/errors.js'; -// ts-unused-exports:disable-next-line export * from '../models/ethereum-models.js'; diff --git a/src/network-handlers/ethereum-handler.ts b/src/network-handlers/ethereum-handler.ts index 943c508..bd0aaea 100644 --- a/src/network-handlers/ethereum-handler.ts +++ b/src/network-handlers/ethereum-handler.ts @@ -1,7 +1,6 @@ -/** @format */ +import { getEthereumContracts, getProvider } from '@ethereum/ethereum-functions.js'; import { Wallet, providers } from 'ethers'; -import { getEthereumContracts, getProvider } from '../functions/ethereum-functions.js'; import { EthereumError } from '../models/errors.js'; import { DLCEthereumContracts, diff --git a/src/network-handlers/read-only-ethereum-handler.ts b/src/network-handlers/read-only-ethereum-handler.ts index 0694bac..28208f9 100644 --- a/src/network-handlers/read-only-ethereum-handler.ts +++ b/src/network-handlers/read-only-ethereum-handler.ts @@ -1,14 +1,12 @@ -/** @format */ -import { Event } from 'ethers'; - -import { getProvider, getReadOnlyEthereumContracts } from '../functions/ethereum-functions.js'; -import { EthereumError } from '../models/errors.js'; +import { getProvider, getReadOnlyEthereumContracts } from '@ethereum/ethereum-functions.js'; +import { EthereumError } from '@models/errors.js'; import { DLCReadOnlyEthereumContracts, EthereumDeploymentPlan, RawVault, VaultState, -} from '../models/ethereum-models.js'; +} from '@models/ethereum-models.js'; +import { Event } from 'ethers'; export class ReadOnlyEthereumHandler { private ethereumContracts: DLCReadOnlyEthereumContracts; diff --git a/src/prompt-functions.ts b/src/prompt-functions.ts deleted file mode 100644 index be6d384..0000000 --- a/src/prompt-functions.ts +++ /dev/null @@ -1,252 +0,0 @@ -/** @format */ - -// /** @format */ - -// // @ts-ignore -// import prompts from 'prompts'; -// import { DisplayVault, RawVault, VaultState } from './models/ethereum-models.js'; -// import { Contract } from 'ethers'; -// import { -// closeVault, -// formatVaultDetailsToDisplay, -// formatVaultToDisplay, -// formatVaultsToDisplay, -// getAllVaults, -// getVault, -// setupVault, -// } from './ethereum-functions.js'; -// import chalk from 'chalk'; -// import { signFundingAndClosingTransactionWithLedger } from './ledger-functions.js'; - -// enum CommandChoices { -// SHOW_ALL_VAULTS = 'Show Vaults', -// CREATE_VAULT = 'Create Vault', -// CLOSE_VAULT = 'Close Vault', -// FUND_VAULT = 'Lock Bitcoin', -// HANDLE_VAULT = 'Handle Vault', -// SHOW_VAULT_DETAILS = 'Show Vault Details', -// MAIN_MENU = 'Main Menu', -// EXIT = 'Exit', -// } - -// interface Command { -// command: CommandChoices; -// vaultUUID?: string; -// vaultState?: VaultState; -// } - -// export async function showMenu(protocolContract: Contract, ethereumNetworkName: string, ethereumUserAddress: string) { -// try { -// let command = await selectFromMenu(); -// do { -// try { -// if (command.command === CommandChoices.MAIN_MENU) { -// command = await selectFromMenu(); -// } else if (command.command !== CommandChoices.EXIT) { -// command = await callFunctionByCommand(command, protocolContract, ethereumNetworkName, ethereumUserAddress); -// } -// } catch (error) { -// console.error(chalk.bgRed(`[Error]: ${error}`)); -// command = await selectFromMenu(); -// } -// } while (command.command !== CommandChoices.EXIT); - -// process.exit(0); -// } catch (error) { -// console.error(`Error: ${error}`); -// process.exit(1); -// } -// } - -// export async function callFunctionByCommand( -// command: Command, -// protocolContract: Contract, -// ethereumNetworkName: string, -// ethereumUserAddress: string -// ): Promise { -// switch (command.command) { -// case CommandChoices.SHOW_ALL_VAULTS: -// return showAllVaults(protocolContract, ethereumUserAddress); -// case CommandChoices.CREATE_VAULT: -// return createVault(protocolContract, ethereumNetworkName); -// case CommandChoices.CLOSE_VAULT: -// return requestVaultClosing(protocolContract, ethereumNetworkName, command.vaultUUID!); -// case CommandChoices.FUND_VAULT: -// return fundVault(protocolContract, command.vaultUUID!, command.vaultState!); -// case CommandChoices.HANDLE_VAULT: -// return fetchVault(protocolContract, command.vaultUUID!, command.vaultState!); -// default: -// throw new Error('Invalid Command'); -// } -// } - -// export async function selectFromMenu(): Promise { -// const menuCommands = [CommandChoices.SHOW_ALL_VAULTS, CommandChoices.CREATE_VAULT, CommandChoices.EXIT]; -// const menuChoices = menuCommands.map((command) => ({ title: command, value: command })); - -// const selectCommand = await prompts({ -// type: 'select', -// name: 'menuCommand', -// message: `What would you like to do?`, -// choices: menuChoices, -// }); -// return { command: selectCommand.menuCommand }; -// } - -// export async function showAllVaults(protocolContract: Contract, ethereumUserAddress: string) { -// const userVaults = await getAllVaults(protocolContract, ethereumUserAddress); - -// const filterVaults = await prompts({ -// type: 'select', -// name: 'filter', -// message: 'Which Vaults would you like to see?', -// choices: [ -// { title: 'All Vaults', value: 'All' }, -// { title: 'Ready Vaults', value: VaultState.Ready }, -// { title: 'Funded Vaults', value: VaultState.Funded }, -// { title: 'Closing Vaults', value: VaultState.Closing }, -// { title: 'Closed Vaults', value: VaultState.Closed }, -// ], -// }); - -// const displayFormattedVaults = formatVaultsToDisplay( -// filterVaults.filter === 'All' ? userVaults : userVaults.filter((vault) => vault.status === filterVaults.filter) -// ); - -// const selectVault = await prompts({ -// type: 'select', -// name: 'vault', -// message: 'Select a Vault to View Actions or Go Back to Main Menu', -// choices: displayFormattedVaults -// .map((vault: DisplayVault) => ({ -// title: `UUID: ${vault.uuid} | State: ${vault.state} | Collateral: ${vault.collateral} BTC | Created At: ${vault.createdAt}`, -// value: vault.uuid, -// })) -// .concat({ title: 'Go Back to Main Menu', value: CommandChoices.MAIN_MENU }), -// }); - -// if (selectVault.vault === CommandChoices.MAIN_MENU) { -// return { command: CommandChoices.MAIN_MENU }; -// } - -// const selectVaultIndex = userVaults.findIndex((vault) => vault.uuid === selectVault.vault); -// if (selectVaultIndex === -1) { -// throw new Error('Invalid Vault Selection'); -// } - -// return { -// command: CommandChoices.HANDLE_VAULT, -// vaultUUID: selectVault.vault, -// vaultState: userVaults[selectVaultIndex].status, -// }; -// } - -// export async function fetchVault(protocolContract: Contract, vaultUUID: string, vaultState: VaultState) { -// const vault = await getVault(protocolContract, vaultUUID, vaultState); -// const displayVault = formatVaultToDisplay(vault); - -// let choices; - -// switch (vault.status) { -// case VaultState.Ready: -// choices = [ -// { -// title: 'Fund the Vault', -// value: { command: CommandChoices.FUND_VAULT, vaultUUID: vaultUUID, vaultState: vault.status }, -// }, -// { title: 'Show Vault Details', value: { command: CommandChoices.SHOW_VAULT_DETAILS } }, -// { title: 'Go Back to Main Menu', value: { command: CommandChoices.MAIN_MENU } }, -// ]; -// break; -// case VaultState.Funded: -// choices = [ -// { -// title: 'Close the Vault', -// value: { command: CommandChoices.CLOSE_VAULT, vaultUUID: vaultUUID, vaultState: vault.status }, -// }, -// { title: 'Show Vault Details', value: { command: CommandChoices.SHOW_VAULT_DETAILS } }, -// { title: 'Go Back to Main Menu', value: { command: CommandChoices.MAIN_MENU } }, -// ]; -// break; -// case VaultState.Funding: -// case VaultState.Closing: -// case VaultState.Closed: -// default: -// choices = [ -// { title: 'Show Vault Details', value: { command: CommandChoices.SHOW_VAULT_DETAILS } }, -// { title: 'Go Back to Main Menu', value: { command: CommandChoices.MAIN_MENU } }, -// ]; -// break; -// } - -// let choice; -// do { -// const selectAction = await prompts({ -// type: 'select', -// name: 'vault', -// message: `Vault UUID: ${displayVault.uuid} is in state: ${displayVault.state}. What would you like to do?`, -// choices: choices, -// }); - -// choice = selectAction.vault; - -// if (choice.command === CommandChoices.SHOW_VAULT_DETAILS) { -// console.log('Vault Details:', formatVaultDetailsToDisplay(vault)); -// } -// } while (choice.command === CommandChoices.SHOW_VAULT_DETAILS); - -// return choice; -// } - -// export async function createVault(protocolContract: Contract, ethereumNetworkName: string) { -// const typeBitcoinAmount = await prompts({ -// type: 'number', -// name: 'value', -// message: 'How much dlcBTC would you like to mint?', -// min: 0.01, -// max: 1, -// increment: 0.01, -// float: true, -// validate: (value: number) => (value < 0.01 || value > 1 ? `You can only mint between 0.01 and 1 dlcBTC` : true), -// }); - -// if (!typeBitcoinAmount.value) { -// return { command: CommandChoices.MAIN_MENU }; -// } - -// const confirmBitcoinAmount = await prompts({ -// type: 'confirm', -// name: 'value', -// message: `You are minting ${typeBitcoinAmount.value} dlcBTC. Confirm?`, -// }); - -// if (confirmBitcoinAmount.value === false) { -// return { command: CommandChoices.CREATE_VAULT }; -// } - -// const transactionReceipt: any = await setupVault(protocolContract, ethereumNetworkName, typeBitcoinAmount.value); - -// if (!transactionReceipt) { -// throw new Error('Error while creating Vault'); -// } - -// const vaultUUID = transactionReceipt.events.find((event: any) => event.event === 'SetupVault').args[0]; - -// return { command: CommandChoices.HANDLE_VAULT, vaultUUID: vaultUUID, vaultState: VaultState.Ready }; -// } - -// export async function requestVaultClosing(protocolContract: Contract, ethereumNetworkName: string, vaultUUID: string) { -// const transactionReceipt = await closeVault(protocolContract, ethereumNetworkName, vaultUUID); - -// if (!transactionReceipt) { -// throw new Error('Error while closing Vault'); -// } - -// return { command: CommandChoices.HANDLE_VAULT, vaultUUID: vaultUUID, vaultState: VaultState.Closing }; -// } - -// export async function fundVault(protocolContract: Contract, vaultUUID: string, vaultState: VaultState) { -// const vault = await getVault(protocolContract, vaultUUID, vaultState); -// await signFundingAndClosingTransactionWithLedger(vault); -// return { command: CommandChoices.MAIN_MENU }; -// } diff --git a/src/proof-of-reserve-handlers/proof-of-reserve-handler.ts b/src/proof-of-reserve-handlers/proof-of-reserve-handler.ts new file mode 100644 index 0000000..d03e736 --- /dev/null +++ b/src/proof-of-reserve-handlers/proof-of-reserve-handler.ts @@ -0,0 +1,122 @@ +import { + createTaprootMultisigPayment, + createTaprootMultisigPaymentLegacy, + deriveUnhardenedPublicKey, + findMatchingScript, + getUnspendableKeyCommittedToUUID, + getValueMatchingInputFromTransaction, +} from '@bitcoin/bitcoin-functions.js'; +import { + checkBitcoinTransactionConfirmations, + fetchBitcoinBlockchainBlockHeight, + fetchBitcoinTransaction, +} from '@bitcoin/bitcoin-request-functions.js'; +import { RawVault } from '@models/ethereum-models.js'; +import { hex } from '@scure/base'; +import { Network } from 'bitcoinjs-lib'; + +export class ProofOfReserveHandler { + private bitcoinBlockchainAPI: string; + private bitcoinNetwork: Network; + private attestorGroupPublicKeyV1: string; + private attestorGroupPublicKeyV2: string; + + constructor( + bitcoinBlockchainAPI: string, + bitcoinNetwork: Network, + attestorGroupPublicKeyV1: string, + attestorGroupPublicKeyV2: string + ) { + this.bitcoinBlockchainAPI = bitcoinBlockchainAPI; + this.bitcoinNetwork = bitcoinNetwork; + this.attestorGroupPublicKeyV1 = attestorGroupPublicKeyV1; + this.attestorGroupPublicKeyV2 = attestorGroupPublicKeyV2; + } + + async verifyVaultDeposit( + vault: RawVault, + attestorGroupPublicKeyV1: string, + attestorGroupPublicKeyV2: Buffer, + bitcoinBlockchainBlockHeight: number + ): Promise { + try { + const fundingTransaction = await fetchBitcoinTransaction( + vault.fundingTxId, + this.bitcoinBlockchainAPI + ); + const isFundingTransactionConfirmed = await checkBitcoinTransactionConfirmations( + fundingTransaction, + bitcoinBlockchainBlockHeight + ); + + if (!isFundingTransactionConfirmed) { + return false; + } + + const closingTransactionInput = getValueMatchingInputFromTransaction( + fundingTransaction, + vault.valueLocked.toNumber() + ); + + const taprootMultisigPaymentLegacyA = createTaprootMultisigPaymentLegacy( + vault.taprootPubKey, + attestorGroupPublicKeyV1, + vault.uuid, + this.bitcoinNetwork + ); + const taprootMultisigPaymentLegacyB = createTaprootMultisigPaymentLegacy( + attestorGroupPublicKeyV1, + vault.taprootPubKey, + vault.uuid, + this.bitcoinNetwork + ); + + const unspendableKeyCommittedToUUID = deriveUnhardenedPublicKey( + getUnspendableKeyCommittedToUUID(vault.uuid, this.bitcoinNetwork), + this.bitcoinNetwork + ); + const taprootMultisigPayment = createTaprootMultisigPayment( + unspendableKeyCommittedToUUID, + attestorGroupPublicKeyV2, + Buffer.from(vault.taprootPubKey, 'hex'), + this.bitcoinNetwork + ); + + return findMatchingScript( + [ + taprootMultisigPaymentLegacyA.script, + taprootMultisigPaymentLegacyB.script, + taprootMultisigPayment.script, + ], + hex.decode(closingTransactionInput.scriptpubkey) + ); + } catch (error) { + console.error(`Error verifying Vault Deposit: ${error}`); + return false; + } + } + + async calculateProofOfReserve(vaults: RawVault[]): Promise { + const bitcoinBlockchainBlockHeight = await fetchBitcoinBlockchainBlockHeight( + this.bitcoinBlockchainAPI + ); + + const derivedAttestorGroupPublicKey = deriveUnhardenedPublicKey( + this.attestorGroupPublicKeyV2, + this.bitcoinNetwork + ); + const verifiedDeposits = await Promise.all( + vaults.map(async vault => { + return (await this.verifyVaultDeposit( + vault, + this.attestorGroupPublicKeyV1, + derivedAttestorGroupPublicKey, + bitcoinBlockchainBlockHeight + )) === true + ? vault.valueLocked.toNumber() + : 0; + }) + ); + return verifiedDeposits.reduce((a, b) => a + b, 0); + } +} diff --git a/src/test.ts b/src/test.ts deleted file mode 100644 index e47ade5..0000000 --- a/src/test.ts +++ /dev/null @@ -1,204 +0,0 @@ -/** @format */ -import { bytesToHex } from '@noble/hashes/utils'; -import { regtest } from 'bitcoinjs-lib/src/networks.js'; -import { Event } from 'ethers'; - -import { ethereumArbitrumSepolia } from './constants/ethereum-constants.js'; -import { PrivateKeyDLCHandler } from './dlc-handlers/private-key-dlc-handler.js'; -import { broadcastTransaction } from './functions/bitcoin-functions.js'; -import { fetchEthereumDeploymentPlan } from './functions/ethereum-functions.js'; -import { EthereumHandler } from './network-handlers/ethereum-handler.js'; -import { AttestorHandler } from './query-handlers/attestor-handler.js'; -import { shiftValue } from './utilities/index.js'; - -async function runFlowWithPrivateKey() { - const exampleNetwork = regtest; - const exampleBitcoinBlockchainAPI = 'https://devnet.dlc.link/electrs'; - const exampleBitcoinBlockchainFeeRecommendationAPI = - 'https://devnet.dlc.link/electrs/fee-estimates'; - const exampleAttestorURLs = [ - 'http://devnet.dlc.link/attestor-1', - 'http://devnet.dlc.link/attestor-2', - 'http://devnet.dlc.link/attestor-3', - ]; - const examplePrivateKey = - ''; - const exampleBitcoinAmount = 0.01; - const ethereumPrivateKey = ''; - - const deploymentPlansPromises = ['TokenManager', 'DLCManager', 'DLCBTC'].map(contractName => { - return fetchEthereumDeploymentPlan( - contractName, - ethereumArbitrumSepolia, - 'dev', - 'https://raw.githubusercontent.com/DLC-link/dlc-solidity' - ); - }); - - const deploymentPlans = await Promise.all(deploymentPlansPromises); - - // Setup Ethereum - const rpcEndpoint = 'https://sepolia-rollup.arbitrum.io/rpc'; - const readOnlyRPCEndpoint = 'https://sepolia-rollup.arbitrum.io/rpc'; - - if (!rpcEndpoint) { - throw new Error('Ethereum RPC Endpoint not set'); - } - - if (!ethereumPrivateKey) { - throw new Error('Ethereum Private Key not set'); - } - const ethereumHandler = new EthereumHandler( - deploymentPlans, - ethereumPrivateKey, - rpcEndpoint, - readOnlyRPCEndpoint - ); - - const setupVaultTransactionReceipt = await ethereumHandler.setupVault( - shiftValue(exampleBitcoinAmount) - ); - if (!setupVaultTransactionReceipt) { - throw new Error('Could not setup Vault'); - } - const vaultUUID = setupVaultTransactionReceipt.events.find( - (event: Event) => event.event === 'SetupVault' - ).args[0]; - - // Setup DLC Handler (with Private Key) - const dlcHandler = new PrivateKeyDLCHandler( - examplePrivateKey, - 0, - exampleNetwork, - exampleBitcoinBlockchainAPI, - exampleBitcoinBlockchainFeeRecommendationAPI - ); - - // Fetch Vault - const vault = await ethereumHandler.getRawVault(vaultUUID); - - // Fetch Attestor Group Public Key - const attestorGroupPublicKey = await ethereumHandler.getAttestorGroupPublicKey(); - - // Create Funding Transaction - const fundingPSBT = await dlcHandler.createFundingPSBT(vault, attestorGroupPublicKey, 2); - - // Sign Funding Transaction - const fundingTransaction = dlcHandler.signPSBT(fundingPSBT, 'funding'); - - // Create Closing Transaction - const closingTransaction = await dlcHandler.createClosingPSBT(vault, fundingTransaction.id, 2); - - // Sign Closing Transaction - const partiallySignedClosingTransaction = dlcHandler.signPSBT(closingTransaction, 'closing'); - const partiallySignedClosingTransactionHex = bytesToHex( - partiallySignedClosingTransaction.toPSBT() - ); - - const nativeSegwitAddress = dlcHandler.getVaultRelatedAddress('p2wpkh'); - - // Send Required Information to Attestors to Create PSBT Event - const attestorHandler = new AttestorHandler(exampleAttestorURLs, 'evm-arbsepolia'); - - await attestorHandler.createPSBTEvent( - vaultUUID, - fundingTransaction.hex, - partiallySignedClosingTransactionHex, - nativeSegwitAddress - ); - - // Broadcast Funding Transaction - const fundingTransactionID = await broadcastTransaction( - fundingTransaction.hex, - exampleBitcoinBlockchainAPI - ); - - console.log('Funding Transaction ID:', fundingTransactionID); - console.log('Success'); -} - -// async function runFlowWithLedger() { -// const exampleNetwork = testnet; -// const exampleAttestorURLs = [ -// 'https://testnet.dlc.link/attestor-1', -// 'https://testnet.dlc.link/attestor-2', -// 'https://testnet.dlc.link/attestor-3', -// ]; -// const exampleBitcoinAmount = 0.01; - -// // Setup Ethereum -// const { ethereumContracts, ethereumNetworkName } = await setupEthereum(); -// const { protocolContract } = ethereumContracts; - -// // Setup Vault -// const setupVaultTransactionReceipt: any = await setupVault( -// protocolContract, -// ethereumNetworkName, -// exampleBitcoinAmount -// ); -// if (!setupVaultTransactionReceipt) { -// throw new Error('Could not setup Vault'); -// } -// const vaultUUID = setupVaultTransactionReceipt.events.find((event: any) => event.event === 'SetupVault').args[0]; -// // const vaultUUID = '0x1e0bf7ac4dc3886bcdb1d4bd1813a0b0d923f83d61ad1776e45677cec83e4a65'; - -// const ledgerApp = await getLedgerApp(LEDGER_APPS_MAP.BITCOIN_TESTNET); - -// if (!ledgerApp) { -// throw new Error('Could not get Ledger App'); -// } - -// const masterFingerprint = await ledgerApp.getMasterFingerprint(); - -// // Setup DLC Handler (with Private Key) -// const dlcHandler = new LedgerDLCHandler(ledgerApp, masterFingerprint, 1, testnet); - -// // Fetch Vault -// const vault = await getRawVault(protocolContract, vaultUUID); - -// // Fetch Attestor Group Public Key -// const attestorGroupPublicKey = await getAttestorGroupPublicKey(ethereumArbitrumSepolia); - -// await dlcHandler.createPayment(vaultUUID, attestorGroupPublicKey); - -// // Create Funding Transaction -// const fundingPSBT = await dlcHandler.createFundingPSBT(vault, 2); - -// // Sign Funding Transaction -// const fundingTransaction = await dlcHandler.signPSBT(fundingPSBT, 'funding'); - -// // Create Closing Transaction -// const closingTransaction = await dlcHandler.createClosingPSBT(vault, fundingTransaction.id, 2); - -// // Sign Closing Transaction -// const partiallySignedClosingTransaction = await dlcHandler.signPSBT(closingTransaction, 'closing'); -// const partiallySignedClosingTransactionHex = bytesToHex(partiallySignedClosingTransaction.toPSBT()); - -// const nativeSegwitAddress = dlcHandler.getVaultRelatedAddress('p2wpkh'); - -// // Send Required Information to Attestors to Create PSBT Event -// await createPSBTEvent( -// exampleAttestorURLs, -// vaultUUID, -// fundingTransaction.hex, -// partiallySignedClosingTransactionHex, -// nativeSegwitAddress -// ); - -// // Broadcast Funding Transaction -// const fundingTransactionID = await broadcastTransaction(fundingTransaction.hex, 'https://mempool.space/testnet/api'); - -// console.log('Funding Transaction ID:', fundingTransactionID); -// console.log('Success'); -// } - -async function example() { - try { - await runFlowWithPrivateKey(); - // await runFlowWithLedger(); - } catch (error) { - throw new Error(`Error: ${error}`); - } -} - -example(); diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 3542722..dbb158e 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -1,4 +1,3 @@ -/** @format */ import { Decimal } from 'decimal.js'; export function shiftValue(value: number): number { diff --git a/tsconfig.json b/tsconfig.json index cf594b7..04568f8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,19 @@ "target": "es2022", "module": "NodeNext", "strict": true, + "baseUrl": ".", + "paths": { + "@dlc-handlers/*": ["src/dlc-handlers/*"], + "@attestor-handlers/*": ["src/attestor-handlers/*"], + "@network-handlers/*": ["src/network-handlers/*"], + "@proof-of-reserve-handlers/*": ["src/proof-of-reserve-handlers/*"], + "@models/*": ["src/models/*"], + "@constants/*": ["src/constants/*"], + "@utilities/*": ["src/utilities/*"], + "@bitcoin/*": ["src/functions/bitcoin/*"], + "@ethereum/*": ["src/functions/ethereum/*"], + "@hardware-wallet/*": ["src/functions/hardware-wallet/*"], + }, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "moduleResolution": "NodeNext",