Skip to content

Commit

Permalink
feat: abstract dlc handler (#46)
Browse files Browse the repository at this point in the history
* feat: add abstract dlc handler
  • Loading branch information
Polybius93 authored Dec 16, 2024
1 parent ae07ebc commit 8545bd0
Show file tree
Hide file tree
Showing 15 changed files with 827 additions and 1,058 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"type": "module",
"name": "dlc-btc-lib",
"version": "2.4.21",
"version": "2.5.0",
"description": "This library provides a comprehensive set of interfaces and functions for minting dlcBTC tokens on supported blockchains.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
7 changes: 7 additions & 0 deletions src/constants/dlc-handler.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const DLCHandlers = {
KEYPAIR: 'keypair',
LEATHER: 'leather',
UNISAT_FORDEFI: 'unisat/fordefi',
LEDGER: 'ledger',
DFNS: 'dfns',
} as const;
252 changes: 252 additions & 0 deletions src/dlc-handlers/abstract-dlc-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
import { Transaction } from '@scure/btc-signer';
import { P2Ret, P2TROut } from '@scure/btc-signer/payment';
import { Network } from 'bitcoinjs-lib';

import {
createNativeSegwitPayment,
createTaprootMultisigPayment,
createTaprootPayment,
deriveUnhardenedPublicKey,
ecdsaPublicKeyToSchnorr,
finalizeUserInputs,
getBalance,
getFeeRate,
getUnspendableKeyCommittedToUUID,
} from '../functions/bitcoin/bitcoin-functions.js';
import {
createDepositTransaction,
createFundingTransaction,
createWithdrawTransaction,
} from '../functions/bitcoin/psbt-functions.js';
import { PaymentInformation } from '../models/bitcoin-models.js';
import {
DLCHandlerType,
FundingPaymentType,
PaymentType,
TransactionType,
} from '../models/dlc-handler.models.js';
import {
AddressNotFoundError,
InsufficientFundsError,
InvalidPaymentTypeError,
PaymentNotSetError,
} from '../models/errors/dlc-handler.errors.models.js';
import { RawVault } from '../models/ethereum-models.js';

export abstract class AbstractDLCHandler {
abstract readonly dlcHandlerType: DLCHandlerType;
protected fundingPaymentType: FundingPaymentType;
protected _payment?: PaymentInformation;
protected readonly bitcoinNetwork: Network;
protected readonly bitcoinBlockchainAPI: string;
protected readonly bitcoinBlockchainFeeRecommendationAPI: string;

constructor(
fundingPaymentType: FundingPaymentType,
bitcoinNetwork: Network,
bitcoinBlockchainAPI: string,
bitcoinBlockchainFeeRecommendationAPI: string
) {
this.fundingPaymentType = fundingPaymentType;
this.bitcoinNetwork = bitcoinNetwork;
this.bitcoinBlockchainAPI = bitcoinBlockchainAPI;
this.bitcoinBlockchainFeeRecommendationAPI = bitcoinBlockchainFeeRecommendationAPI;
}

protected set payment(payment: PaymentInformation) {
this._payment = payment;
}

protected get payment(): PaymentInformation {
if (!this._payment) {
throw new PaymentNotSetError();
}
return this._payment;
}

getVaultRelatedAddress(paymentType: PaymentType): string {
switch (paymentType) {
case 'funding':
if (!this.payment.fundingPayment.address) {
throw new AddressNotFoundError('funding');
}
return this.payment.fundingPayment.address;
case 'multisig':
if (!this.payment.multisigPayment.address) {
throw new AddressNotFoundError('multisig');
}
return this.payment.multisigPayment.address;
default:
throw new InvalidPaymentTypeError(paymentType);
}
}

protected async createPaymentInformation(
vaultUUID: string,
attestorGroupPublicKey: string
): Promise<PaymentInformation> {
let fundingPayment: P2Ret | P2TROut;

if (this.fundingPaymentType === 'wpkh') {
const fundingPublicKeyBuffer = Buffer.from(this.getUserFundingPublicKey(), 'hex');
fundingPayment = createNativeSegwitPayment(fundingPublicKeyBuffer, this.bitcoinNetwork);
} else {
const fundingPublicKeyBuffer = Buffer.from(this.getUserFundingPublicKey(), 'hex');
const fundingSchnorrPublicKeyBuffer = ecdsaPublicKeyToSchnorr(fundingPublicKeyBuffer);
fundingPayment = createTaprootPayment(fundingSchnorrPublicKeyBuffer, this.bitcoinNetwork);
}

const unspendablePublicKey = getUnspendableKeyCommittedToUUID(vaultUUID, this.bitcoinNetwork);
const unspendableDerivedPublicKey = deriveUnhardenedPublicKey(
unspendablePublicKey,
this.bitcoinNetwork
);

const attestorDerivedPublicKey = deriveUnhardenedPublicKey(
attestorGroupPublicKey,
this.bitcoinNetwork
);

const taprootPublicKeyBuffer = Buffer.from(this.getUserTaprootPublicKey(), 'hex');

const multisigPayment = createTaprootMultisigPayment(
unspendableDerivedPublicKey,
attestorDerivedPublicKey,
taprootPublicKeyBuffer,
this.bitcoinNetwork
);

const paymentInformation = { fundingPayment, multisigPayment };

this.payment = paymentInformation;
return paymentInformation;
}

private async validateFundsAvailability(
fundingPayment: P2Ret | P2TROut,
requiredAmount: bigint
): Promise<void> {
const currentBalance = BigInt(await getBalance(fundingPayment, this.bitcoinBlockchainAPI));

if (currentBalance < requiredAmount) {
throw new InsufficientFundsError(currentBalance, requiredAmount);
}
}

private async getFeeRate(feeRateMultiplier?: number, customFeeRate?: bigint): Promise<bigint> {
return (
customFeeRate ??
BigInt(await getFeeRate(this.bitcoinBlockchainFeeRecommendationAPI, feeRateMultiplier))
);
}

async createFundingPSBT(
vault: RawVault,
depositAmount: bigint,
attestorGroupPublicKey: string,
feeRateMultiplier?: number,
customFeeRate?: bigint
): Promise<Transaction> {
const { fundingPayment, multisigPayment } = await this.createPaymentInformation(
vault.uuid,
attestorGroupPublicKey
);

const feeRate = await this.getFeeRate(feeRateMultiplier, customFeeRate);

await this.validateFundsAvailability(fundingPayment, vault.valueLocked.toBigInt());

return await createFundingTransaction(
this.bitcoinBlockchainAPI,
this.bitcoinNetwork,
depositAmount,
multisigPayment,
fundingPayment,
feeRate,
vault.btcFeeRecipient,
vault.btcMintFeeBasisPoints.toBigInt()
);
}

async createWithdrawPSBT(
vault: RawVault,
withdrawAmount: bigint,
attestorGroupPublicKey: string,
fundingTransactionID: string,
feeRateMultiplier?: number,
customFeeRate?: bigint
): Promise<Transaction> {
const { fundingPayment, multisigPayment } = await this.createPaymentInformation(
vault.uuid,
attestorGroupPublicKey
);

const feeRate = await this.getFeeRate(feeRateMultiplier, customFeeRate);

return await createWithdrawTransaction(
this.bitcoinBlockchainAPI,
this.bitcoinNetwork,
withdrawAmount,
fundingTransactionID,
multisigPayment,
fundingPayment,
feeRate,
vault.btcFeeRecipient,
vault.btcRedeemFeeBasisPoints.toBigInt()
);
}

async createDepositPSBT(
vault: RawVault,
depositAmount: bigint,
attestorGroupPublicKey: string,
fundingTransactionID: string,
feeRateMultiplier?: number,
customFeeRate?: bigint
): Promise<Transaction> {
const { fundingPayment, multisigPayment } = await this.createPaymentInformation(
vault.uuid,
attestorGroupPublicKey
);

const feeRate = await this.getFeeRate(feeRateMultiplier, customFeeRate);

return await createDepositTransaction(
this.bitcoinBlockchainAPI,
this.bitcoinNetwork,
depositAmount,
fundingTransactionID,
multisigPayment,
fundingPayment,
feeRate,
vault.btcFeeRecipient,
vault.btcMintFeeBasisPoints.toBigInt()
);
}

private readonly transactionFinalizers: Record<
TransactionType,
(transaction: Transaction, payment: P2Ret | P2TROut) => void
> = {
funding: transaction => transaction.finalize(),
deposit: (transaction, payment) => finalizeUserInputs(transaction, payment),
withdraw: () => {},
};

protected finalizeTransaction(
signedTransaction: Transaction,
transactionType: TransactionType,
fundingPayment: P2Ret | P2TROut
): void {
this.transactionFinalizers[transactionType](signedTransaction, fundingPayment);
}

abstract signPSBT(
transaction: Transaction,
transactionType: TransactionType
): Promise<Transaction>;

abstract getUserTaprootPublicKey(tweaked?: boolean): string;

abstract getUserFundingPublicKey(): string;
}
Loading

0 comments on commit 8545bd0

Please sign in to comment.