Skip to content

Commit

Permalink
feat: finished bitcoin contract request page advanced view
Browse files Browse the repository at this point in the history
  • Loading branch information
Polybius93 committed Oct 26, 2023
1 parent 2089d40 commit 5d8b6d0
Show file tree
Hide file tree
Showing 24 changed files with 576 additions and 312 deletions.
82 changes: 68 additions & 14 deletions src/app/common/hooks/use-bitcoin-contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ import { useNavigate } from 'react-router-dom';

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

import { getBtcSignerLibNetworkConfigByMode } from '@shared/crypto/bitcoin/bitcoin.network';
import {
deriveAddressIndexKeychainFromAccount,
extractAddressIndexFromPath,
getAddressFromOutScript,
} from '@shared/crypto/bitcoin/bitcoin.utils';
import { Money, createMoneyFromDecimal } from '@shared/models/money.model';
import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';
import { RouteUrls } from '@shared/route-urls';
import { BitcoinContractResponseStatus } from '@shared/rpc/methods/accept-bitcoin-contract';
import { makeRpcSuccessResponse } from '@shared/rpc/rpc-methods';
Expand Down Expand Up @@ -37,7 +41,7 @@ import { useDefaultRequestParams } from './use-default-request-search-params';
export interface SimplifiedBitcoinContract {
bitcoinContractId: string;
bitcoinContractCollateralAmount: number;
bitcoinContractGasFee: number;
bitcoinTxDetails: BitcoinContractTransactionDetails;
bitcoinContractExpirationDate: string;
}

Expand All @@ -60,7 +64,7 @@ export interface BitcoinContractOfferDetails {
counterpartyWalletDetails: CounterpartyWalletDetails;
}

type BitcoinTransaction = {
export interface RawBitcoinContractTransaction {
input: {
previous_output: string;
script_sig: string;
Expand All @@ -73,7 +77,25 @@ type BitcoinTransaction = {
value: number;
}[];
version: number;
};
}

export interface BitcoinContractInput {
txId: string;
address: string;
value: number;
}

export interface BitcoinContractOutput {
address: string;
value: number;
}

export interface BitcoinContractTransactionDetails {
inputs: BitcoinContractInput[];
outputs: BitcoinContractOutput[];
fee: number;
rawTx: RawBitcoinContractTransaction;
}

export function useBitcoinContracts() {
const navigate = useNavigate();
Expand All @@ -84,31 +106,59 @@ export function useBitcoinContracts() {
const currentIndex = useCurrentAccountIndex();
const nativeSegwitPrivateKeychain = useNativeSegwitAccountBuilder()?.(currentIndex);
const currentBitcoinNetwork = useCurrentNetwork();
const btcSignerLibNetworkConfig = getBtcSignerLibNetworkConfigByMode(
currentBitcoinNetwork.chain.bitcoin.network
);
const bitcoinClient = useBitcoinClient();
const [bitcoinContractCollateralAmount, setBitcoinContractCollateralAmount] = useState(0);
const [acceptedBitcoinContract, setAcceptedBitcoinContract] = useState<any>();
const [counterpartyWalletDetails, setCounterpartyWalletDetails] = useState<any>();

async function calculateFee(fundingTX: BitcoinTransaction) {
async function getTxInputDetails(txId: string, outputIndex: number) {
const bitcoinTx: BitcoinTx = await bitcoinClient.transactionsApi.getBitcoinTransaction(txId);
const bitcoinAddress = bitcoinTx.vout[outputIndex].scriptpubkey_address;
const inputAmount = bitcoinTx.vout[outputIndex].value;
return { bitcoinAddress, inputAmount };
}

async function getBitcoinTxDetails(fundingTX: RawBitcoinContractTransaction) {
const inputs = fundingTX.input;
const outputs = fundingTX.output;

let outputAmount = 0;
let inputAmount = 0;
let inputValueSum = 0;
let outputValueSum = 0;

const bitcoinTxDetails: BitcoinContractTransactionDetails = {
inputs: [],
outputs: [],
fee: 0,
rawTx: fundingTX,
};

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);
const { bitcoinAddress, inputAmount } = await getTxInputDetails(txId, parseInt(outputIndex));
const txInput = { txId, address: bitcoinAddress, value: inputAmount };
bitcoinTxDetails.inputs.push(txInput);
inputValueSum += inputAmount;
}

for (const output of outputs) {
outputAmount += output.value;
const outputAmount = output.value;
const outputAddress = getAddressFromOutScript(
hexToBytes(output.script_pubkey),
btcSignerLibNetworkConfig
);
const txOutput = { address: outputAddress, value: outputAmount };
bitcoinTxDetails.outputs.push(txOutput);
outputValueSum += outputAmount;
}

const fee = inputAmount - outputAmount;
const fee = inputValueSum - outputValueSum;

bitcoinTxDetails.fee = fee;

return fee;
return bitcoinTxDetails;
}

async function getBitcoinContractInterface(): Promise<JsDLCInterface | undefined> {
Expand Down Expand Up @@ -150,7 +200,11 @@ export function useBitcoinContracts() {
const bitcoinContractCollateralAmount =
bitcoinContractOffer.contractInfo.singleContractInfo.totalCollateral;

let bitcoinContractGasFee = 0;
let bitcoinTxDetails: BitcoinContractTransactionDetails = {
inputs: [],
outputs: [],
fee: 0,
};

const bitcoinContractExpirationDate = new Date(
bitcoinContractOffer.cetLocktime * 1000
Expand Down Expand Up @@ -183,7 +237,7 @@ export function useBitcoinContracts() {

const acceptedBitcoinContract = JSON.parse(acceptedBitcoinContractJSON);

bitcoinContractGasFee = await calculateFee(acceptedBitcoinContract.fundingTX);
bitcoinTxDetails = await getBitcoinTxDetails(acceptedBitcoinContract.fundingTX);

setAcceptedBitcoinContract(acceptedBitcoinContract);
} catch (error) {
Expand All @@ -201,7 +255,7 @@ export function useBitcoinContracts() {
bitcoinContractId,
bitcoinContractCollateralAmount,
bitcoinContractExpirationDate,
bitcoinContractGasFee,
bitcoinTxDetails,
};

const bitcoinContractOfferDetails: BitcoinContractOfferDetails = {
Expand Down
170 changes: 78 additions & 92 deletions src/app/pages/bitcoin-contract-request/bitcoin-contract-request.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,48 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';

import { token } from 'leather-styles/tokens';

import { createMoney } from '@shared/models/money.model';
import { RouteUrls } from '@shared/route-urls';
import { BitcoinContractResponseStatus } from '@shared/rpc/methods/accept-bitcoin-contract';
import { closeWindow } from '@shared/utils';

import { useBitcoinContracts } from '@app/common/hooks/use-bitcoin-contracts';
import { BitcoinContractOfferDetails } from '@app/common/hooks/use-bitcoin-contracts';
import { useBtcAssetBalance } from '@app/common/hooks/balance/btc/use-btc-balance';
import {
BitcoinContractOfferDetails,
useBitcoinContracts,
} from '@app/common/hooks/use-bitcoin-contracts';
import { useOnMount } from '@app/common/hooks/use-on-mount';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { initialSearchParams } from '@app/common/initial-search-params';
import { FormAddressDisplayer } from '@app/components/address-displayer/form-address-displayer';
import { InfoCard, InfoCardRow, InfoCardSeparator } from '@app/components/info-card/info-card';

import { BitcoinContractOfferInput } from './components/bitcoin-contract-offer/bitcoin-contract-offer-input';
import { BitcoinContractRequestActions } from './components/bitcoin-contract-request-actions';
import { BitcoinContractRequestLayout } from './components/bitcoin-contract-request-layout';
import { BitcoinContractRequestHeader } from './components/bitcoin-contract-request-header';
import { useRouteHeader } from '@app/common/hooks/use-route-header';
import { initialSearchParams } from '@app/common/initial-search-params';
import { LoadingSpinner } from '@app/components/loading-spinner';
import { PopupHeader } from '@app/features/current-account/popup-header';
import { useOnOriginTabClose } from '@app/routes/hooks/use-on-tab-closed';
import { closeWindow } from '@shared/utils';
import { LoadingSpinner } from '@app/components/loading-spinner';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
import { Stack } from 'leather-styles/jsx';
import { LeatherButton } from '@app/components/button/button';

import { BitcoinContractCollateralAmount } from './components/bitcoin-contract-collateral-amount';
import { BitcoinContractExpirationDate } from './components/bitcoin-contract-expiration-date';
import { BitcoinContractFee } from './components/bitcoin-contract-fee';
import { BitcoinContractInputsAndOutputs } from './components/bitcoin-contract-inputs-outputs';
import { BitcoinContractRequestRawTransaction } from './components/bitcoin-contract-raw-transaction';
import { BitcoinContractRequestActions } from './components/bitcoin-contract-request-actions';
import { BitcoinContractRequestDetails } from './components/bitcoin-contract-request-details';
import { BitcoinContractRequestDetailsHeader } from './components/bitcoin-contract-request-details-header';
import { BitcoinContractRequestHeader } from './components/bitcoin-contract-request-header';
import { BitcoinContractRequestLayout } from './components/bitcoin-contract-request-layout';

export function BitcoinContractRequest() {
const navigate = useNavigate();
const network = useCurrentNetwork();
const { address: bitcoinAddress } = useCurrentAccountNativeSegwitIndexZeroSigner();
const { handleOffer, handleSigning, handleReject, sendRpcResponse } = useBitcoinContracts();
const { btcAvailableAssetBalance } = useBtcAssetBalance(bitcoinAddress);

const [requiredAmount, setRequiredAmount] = useState(0);
const [bitcoinContractOfferDetails, setBitcoinContractOfferDetails] =
useState<BitcoinContractOfferDetails>();
const [isProcessing, setProcessing] = useState(false);
const [showTransactionDetails, setShowTransactionDetails] = useState(false);
const [canAccept, setCanAccept] = useState(false);

useRouteHeader(<PopupHeader displayAddresssBalanceOf="all" />);
useOnOriginTabClose(() => closeWindow());

Expand Down Expand Up @@ -76,91 +80,73 @@ export function BitcoinContractRequest() {
counterpartyWalletDetailsJSON
);
if (!currentBitcoinContractOfferDetails) return;
setRequiredAmount(
currentBitcoinContractOfferDetails?.simplifiedBitcoinContract
.bitcoinContractCollateralAmount +
currentBitcoinContractOfferDetails?.simplifiedBitcoinContract.bitcoinContractGasFee

setCanAccept(
btcAvailableAssetBalance.balance.amount.isGreaterThan(
currentBitcoinContractOfferDetails.simplifiedBitcoinContract
.bitcoinContractCollateralAmount +
currentBitcoinContractOfferDetails?.simplifiedBitcoinContract.bitcoinTxDetails.fee
)
);
setBitcoinContractOfferDetails(currentBitcoinContractOfferDetails);
};
handleBitcoinContractOffer();
});

if (!bitcoinContractOfferDetails || !bitcoinContractOfferDetails.counterpartyWalletDetails
.counterpartyWalletAddress) return <LoadingSpinner height="600px" />;
if (
!bitcoinContractOfferDetails ||
!bitcoinContractOfferDetails.counterpartyWalletDetails.counterpartyWalletAddress
)
return <LoadingSpinner height="600px" />;

return (
<>
<BitcoinContractRequestLayout>
<BitcoinContractRequestHeader
counterpartyWalletName={
bitcoinContractOfferDetails.counterpartyWalletDetails.counterpartyWalletName
}
counterpartyWalletIcon={
bitcoinContractOfferDetails.counterpartyWalletDetails.counterpartyWalletIcon
<BitcoinContractRequestLayout>
<BitcoinContractRequestHeader
counterpartyWalletName={
bitcoinContractOfferDetails.counterpartyWalletDetails.counterpartyWalletName
}
counterpartyWalletIcon={
bitcoinContractOfferDetails.counterpartyWalletDetails.counterpartyWalletIcon
}
/>
<BitcoinContractRequestDetails>
<BitcoinContractRequestDetailsHeader />
<BitcoinContractCollateralAmount
bitcoinAddress={bitcoinAddress}
collateralAmount={createMoney(
bitcoinContractOfferDetails.simplifiedBitcoinContract.bitcoinContractCollateralAmount,
'BTC'
)}
/>
<BitcoinContractInputsAndOutputs
inputs={bitcoinContractOfferDetails.simplifiedBitcoinContract.bitcoinTxDetails.inputs}
outputs={bitcoinContractOfferDetails.simplifiedBitcoinContract.bitcoinTxDetails.outputs}
/>
<BitcoinContractRequestRawTransaction
bitcoinContractTransaction={
bitcoinContractOfferDetails.simplifiedBitcoinContract.bitcoinTxDetails.rawTx
}
/>
<BitcoinContractRequestActions
isLoading={isProcessing}
bitcoinAddress={bitcoinAddress}
requiredAmount={requiredAmount}
onRejectBitcoinContractOffer={handleRejectClick}
onAcceptBitcoinContractOffer={handleSignClick}
<BitcoinContractFee
fee={createMoney(
bitcoinContractOfferDetails.simplifiedBitcoinContract.bitcoinTxDetails.fee,
'BTC'
)}
/>
<BitcoinContractOfferInput
addressNativeSegwit={bitcoinAddress}
bitcoinContractOffer={bitcoinContractOfferDetails.simplifiedBitcoinContract}
<BitcoinContractExpirationDate
expirationDate={
bitcoinContractOfferDetails.simplifiedBitcoinContract.bitcoinContractExpirationDate
}
/>
<Stack alignItems="flex-end" width="100%">
<LeatherButton
variant="link"
onClick={() => setShowTransactionDetails(!showTransactionDetails)}
>
{showTransactionDetails ? 'Hide Transaction Details' : 'Show Transaction Details'}
</LeatherButton>
</Stack>
{showTransactionDetails && (
<InfoCard mt="loose">
<Stack
width="100%"
padding="24px"
backgroundColor={token('colors.accent.background-secondary')}
>
<InfoCardRow
title="To"
value={
<FormAddressDisplayer
address={
bitcoinContractOfferDetails.counterpartyWalletDetails
.counterpartyWalletAddress
}
/>
}
/>
<InfoCardSeparator />
<InfoCardRow
title="Total spend"
value={`${
bitcoinContractOfferDetails.simplifiedBitcoinContract
.bitcoinContractCollateralAmount +
bitcoinContractOfferDetails.simplifiedBitcoinContract.bitcoinContractGasFee
} sats`}
/>
<InfoCardRow
title="Fee"
value={`${bitcoinContractOfferDetails.simplifiedBitcoinContract.bitcoinContractGasFee} sats`}
/>
<InfoCardRow
title="Expiration Date"
value={
bitcoinContractOfferDetails.simplifiedBitcoinContract
.bitcoinContractExpirationDate
}
/>
</Stack>
</InfoCard>
)}
</BitcoinContractRequestLayout>
</BitcoinContractRequestDetails>
<BitcoinContractRequestActions
isLoading={isProcessing}
canAccept={canAccept}
onRejectBitcoinContractOffer={handleRejectClick}
onAcceptBitcoinContractOffer={handleSignClick}
/>
</BitcoinContractRequestLayout>
</>
);
}
Loading

0 comments on commit 5d8b6d0

Please sign in to comment.