Skip to content

Commit

Permalink
feat: added advanced view with additional details for bitcoin contrac…
Browse files Browse the repository at this point in the history
…t requests
  • Loading branch information
Polybius93 committed Oct 25, 2023
1 parent a0717e3 commit a946ca2
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 126 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@
"dependencies": {
"@bitcoinerlab/secp256k1": "1.0.2",
"@coinbase/cbpay-js": "1.0.2",
"@dlc-link/dlc-tools": "1.0.2",
"@fungible-systems/zone-file": "2.0.0",
"@hirosystems/token-metadata-api-client": "1.1.0",
"@ledgerhq/hw-transport-webusb": "6.27.19",
Expand Down Expand Up @@ -184,6 +183,7 @@
"compare-versions": "4.1.3",
"css-loader": "6.8.1",
"dayjs": "1.11.8",
"dlc-tools": "file:../new-dlc-stack/wasm-wallet/pkg",
"dompurify": "3.0.6",
"dotenv": "16.3.1",
"ecdsa-sig-formatter": "1.0.11",
Expand Down
174 changes: 116 additions & 58 deletions src/app/common/hooks/use-bitcoin-contracts.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';

import { RpcErrorCode } from '@btckit/types';
import { JsDLCInterface } from '@dlc-link/dlc-tools';
import { bytesToHex } from '@stacks/common';
import { JsDLCInterface } from 'dlc-tools';

import {
deriveAddressIndexKeychainFromAccount,
Expand All @@ -14,7 +15,7 @@ import { BitcoinContractResponseStatus } from '@shared/rpc/methods/accept-bitcoi
import { makeRpcSuccessResponse } from '@shared/rpc/rpc-methods';
import { makeRpcErrorResponse } from '@shared/rpc/rpc-methods';

import { checkDlcLinkAttestorHealth } from '@app/query/bitcoin/contract/check-dlc-link-attestor-health';
import { fetchBitcoinContractCounterpartyAddress } from '@app/query/bitcoin/contract/fetch-bitcoin-contract-counterparty-address';
import { sendAcceptedBitcoinContractOfferToProtocolWallet } from '@app/query/bitcoin/contract/send-accepted-bitcoin-contract-offer';
import {
useCalculateBitcoinFiatValue,
Expand All @@ -25,6 +26,7 @@ import {
useCurrentAccountNativeSegwitIndexZeroSigner,
useNativeSegwitAccountBuilder,
} from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useBitcoinClient } from '@app/store/common/api-clients.hooks';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';

import { initialSearchParams } from '../initial-search-params';
Expand All @@ -35,13 +37,15 @@ import { useDefaultRequestParams } from './use-default-request-search-params';
export interface SimplifiedBitcoinContract {
bitcoinContractId: string;
bitcoinContractCollateralAmount: number;
bitcoinContractGasFee: number;
bitcoinContractExpirationDate: string;
}

interface CounterpartyWalletDetails {
counterpartyWalletURL: string;
counterpartyWalletName: string;
counterpartyWalletIcon: string;
counterpartyWalletAddress?: string;
}

export interface BitcoinContractListItem {
Expand All @@ -56,6 +60,21 @@ export interface BitcoinContractOfferDetails {
counterpartyWalletDetails: CounterpartyWalletDetails;
}

type BitcoinTransaction = {
input: {
previous_output: string;
script_sig: string;
sequence: number;
witness: any[];
}[];
lock_time: number;
output: {
script_pubkey: string;
value: number;
}[];
version: number;
};

export function useBitcoinContracts() {
const navigate = useNavigate();
const defaultParams = useDefaultRequestParams();
Expand All @@ -65,10 +84,34 @@ export function useBitcoinContracts() {
const currentIndex = useCurrentAccountIndex();
const nativeSegwitPrivateKeychain = useNativeSegwitAccountBuilder()?.(currentIndex);
const currentBitcoinNetwork = useCurrentNetwork();
const bitcoinClient = useBitcoinClient();
const [bitcoinContractCollateralAmount, setBitcoinContractCollateralAmount] = useState(0);
const [acceptedBitcoinContract, setAcceptedBitcoinContract] = useState<any>();
const [counterpartyWalletDetails, setCounterpartyWalletDetails] = useState<any>();

async function calculateFee(fundingTX: BitcoinTransaction) {
const inputs = fundingTX.input;
const outputs = fundingTX.output;

let outputAmount = 0;
let inputAmount = 0;

for (const input of inputs) {
const [txId, outputIndex] = input.previous_output.split(':');
const txDetails = await bitcoinClient.transactionsApi.getBitcoinTransaction(txId);
inputAmount += parseInt(txDetails.vout[parseInt(outputIndex)].value);
}

for (const output of outputs) {
outputAmount += output.value;
}

async function getBitcoinContractInterface(
attestorURLs: string[]
): Promise<JsDLCInterface | undefined> {
const fee = inputAmount - outputAmount;

return fee;
}

async function getBitcoinContractInterface(): Promise<JsDLCInterface | undefined> {
if (!nativeSegwitPrivateKeychain || !bitcoinAccountDetails) return;

const currentAddress = bitcoinAccountDetails.address;
Expand All @@ -84,31 +127,81 @@ export function useBitcoinContracts() {
bytesToHex(currentAddressPrivateKey),
currentAddress,
currentBitcoinNetwork.chain.bitcoin.network,
currentBitcoinNetwork.chain.bitcoin.url,
JSON.stringify(attestorURLs)
currentBitcoinNetwork.chain.bitcoin.url
);

return bitcoinContractInterface;
}

function handleOffer(
async function handleOffer(
bitcoinContractOfferJSON: string,
counterpartyWalletDetailsJSON: string
): BitcoinContractOfferDetails {
) {
const bitcoinContractOffer = JSON.parse(bitcoinContractOfferJSON);
const counterpartyWalletDetails = JSON.parse(counterpartyWalletDetailsJSON);

const bitcoinContractId = bitcoinContractOffer.temporaryContractId;

const bitcoinContractCounterpartyBitcoinAddress = await fetchBitcoinContractCounterpartyAddress(
counterpartyWalletDetails.counterpartyWalletURL
);
counterpartyWalletDetails.counterpartyWalletAddress = bitcoinContractCounterpartyBitcoinAddress;

const bitcoinContractCollateralAmount =
bitcoinContractOffer.contractInfo.singleContractInfo.totalCollateral;

let bitcoinContractGasFee = 0;

const bitcoinContractExpirationDate = new Date(
bitcoinContractOffer.cetLocktime * 1000
).toLocaleDateString();

setBitcoinContractCollateralAmount(bitcoinContractCollateralAmount);
setCounterpartyWalletDetails(counterpartyWalletDetails);

let bitcoinContractInterface: JsDLCInterface | undefined;

try {
bitcoinContractInterface = await getBitcoinContractInterface();
} catch (error) {
navigate(RouteUrls.BitcoinContractLockError, {
state: {
error,
title: 'There was an error with getting the Bitcoin Contract Interface',
body: 'Unable to setup Bitcoin Contract Interface',
},
});
sendRpcResponse(BitcoinContractResponseStatus.INTERFACE_ERROR);
}

if (!bitcoinContractInterface) return;

try {
await bitcoinContractInterface.get_wallet_balance();
const acceptedBitcoinContractJSON =
await bitcoinContractInterface.accept_offer(bitcoinContractOfferJSON);

const acceptedBitcoinContract = JSON.parse(acceptedBitcoinContractJSON);

bitcoinContractGasFee = await calculateFee(acceptedBitcoinContract.fundingTX);

setAcceptedBitcoinContract(acceptedBitcoinContract);
} catch (error) {
navigate(RouteUrls.BitcoinContractLockError, {
state: {
error,
title: 'There was an error with your Bitcoin Contract',
body: 'Unable to lock bitcoin',
},
});
sendRpcResponse(BitcoinContractResponseStatus.BROADCAST_ERROR);
}

const simplifiedBitcoinContractOffer: SimplifiedBitcoinContract = {
bitcoinContractId,
bitcoinContractCollateralAmount,
bitcoinContractExpirationDate,
bitcoinContractGasFee,
};

const bitcoinContractOfferDetails: BitcoinContractOfferDetails = {
Expand All @@ -119,14 +212,11 @@ export function useBitcoinContracts() {
return bitcoinContractOfferDetails;
}

async function handleAccept(
bitcoinContractJSON: string,
counterpartyWalletDetails: CounterpartyWalletDetails,
attestorURLs: string[]
) {
async function handleSigning() {
let bitcoinContractInterface: JsDLCInterface | undefined;

try {
bitcoinContractInterface = await getBitcoinContractInterface(attestorURLs);
bitcoinContractInterface = await getBitcoinContractInterface();
} catch (error) {
navigate(RouteUrls.BitcoinContractLockError, {
state: {
Expand All @@ -140,27 +230,16 @@ export function useBitcoinContracts() {

if (!bitcoinContractInterface) return;

const bitcoinContractOffer = JSON.parse(bitcoinContractJSON);

const bitcoinContractCollateralAmount =
bitcoinContractOffer.contractInfo.singleContractInfo.totalCollateral;

await bitcoinContractInterface.get_wallet_balance();

try {
const acceptedBitcoinContract =
await bitcoinContractInterface.accept_offer(bitcoinContractJSON);

const signedBitcoinContract = await sendAcceptedBitcoinContractOfferToProtocolWallet(
acceptedBitcoinContract,
JSON.stringify(acceptedBitcoinContract.acceptMessage),
counterpartyWalletDetails.counterpartyWalletURL
);

const bitcoinContractId = signedBitcoinContract.contractId;
const signedBitcoinContractJSON = JSON.stringify(signedBitcoinContract);

const txId = await bitcoinContractInterface.countersign_and_broadcast(
JSON.stringify(signedBitcoinContract)
);
const txId =
await bitcoinContractInterface.countersign_and_broadcast(signedBitcoinContractJSON);

const { txMoney, txFiatValue, txFiatValueSymbol, txLink, symbol } = getTransactionDetails(
txId,
Expand All @@ -178,7 +257,11 @@ export function useBitcoinContracts() {
},
});

sendRpcResponse(BitcoinContractResponseStatus.SUCCESS, bitcoinContractId, txId);
sendRpcResponse(
BitcoinContractResponseStatus.SUCCESS,
signedBitcoinContract.contractId,
txId
);
} catch (error) {
navigate(RouteUrls.BitcoinContractLockError, {
state: {
Expand All @@ -196,36 +279,11 @@ export function useBitcoinContracts() {
close();
}

async function getHealthyDlcLinkAttestor(): Promise<string> {
const dlcLinkAttestorUrls = [
'https://devnet.dlc.link/attestor-1/',
'https://devnet.dlc.link/attestor-2/',
'https://devnet.dlc.link/attestor-3/',
];

let currentAttestorUrl: string | undefined;

for (const attestorURL of dlcLinkAttestorUrls) {
const isAttestorHealthy = await checkDlcLinkAttestorHealth(attestorURL);
if (isAttestorHealthy) {
currentAttestorUrl = attestorURL;
break;
}
}

if (!currentAttestorUrl) {
throw new Error('Unable to find a healthy DLC.Link attestor');
}

return currentAttestorUrl;
}

async function getAllSignedBitcoinContracts() {
let bitcoinContractInterface: JsDLCInterface | undefined;

try {
const currentAttestorUrl = await getHealthyDlcLinkAttestor();
bitcoinContractInterface = await getBitcoinContractInterface([currentAttestorUrl]);
bitcoinContractInterface = await getBitcoinContractInterface();
} catch (error) {
navigate(RouteUrls.BitcoinContractLockError, {
state: {
Expand Down Expand Up @@ -335,7 +393,7 @@ export function useBitcoinContracts() {

return {
handleOffer,
handleAccept,
handleSigning,
handleReject,
getAllSignedBitcoinContracts,
sumBitcoinContractCollateralAmounts,
Expand Down
Loading

0 comments on commit a946ca2

Please sign in to comment.