feat: updated bitcoin contract api, modified responses, added error handling

This commit is contained in:
Polybius93
2023-08-10 18:00:26 +02:00
committed by Polybius93
parent 3f7ef947fe
commit 3a77bfce5b
7 changed files with 152 additions and 82 deletions

View File

@@ -184,7 +184,7 @@
"coinselect": "3.1.13",
"compare-versions": "4.1.3",
"dayjs": "1.11.8",
"dlc-wasm-wallet": "0.4.3",
"dlc-wasm-wallet": "0.4.7",
"dompurify": "3.0.4",
"downshift": "6.1.7",
"ecdsa-sig-formatter": "1.0.11",

View File

@@ -1,5 +1,6 @@
import { useNavigate } from 'react-router-dom';
import { RpcErrorCode } from '@btckit/types';
import { bytesToHex } from '@stacks/common';
import { JsDLCInterface } from 'dlc-wasm-wallet';
@@ -10,7 +11,9 @@ import {
} from '@shared/crypto/bitcoin/bitcoin.utils';
import { createMoneyFromDecimal } from '@shared/models/money.model';
import { RouteUrls } from '@shared/route-urls';
import { BitcoinContractResponseStatus } from '@shared/rpc/methods/accept-bitcoin-contract';
import { makeRpcSuccessResponse } from '@shared/rpc/rpc-methods';
import { makeRpcErrorResponse } from '@shared/rpc/rpc-methods';
import { sendAcceptedBitcoinContractOfferToProtocolWallet } from '@app/query/bitcoin/contract/send-accepted-bitcoin-contract-offer';
import {
@@ -54,9 +57,10 @@ export function useBitcoinContracts() {
const getNativeSegwitSigner = useCurrentAccountNativeSegwitSigner();
const currentIndex = useCurrentAccountIndex();
const nativeSegwitPrivateKeychain = useNativeSegwitAccountBuilder()?.(currentIndex);
const oracleAPI = 'https://testnet.dlc.link/oracle';
async function getBitcoinContractInterface(): Promise<JsDLCInterface | undefined> {
async function getBitcoinContractInterface(
attestorURLs: string[]
): Promise<JsDLCInterface | undefined> {
const bitcoinAccountDetails = getNativeSegwitSigner?.(0);
if (!nativeSegwitPrivateKeychain || !bitcoinAccountDetails) return;
@@ -83,7 +87,7 @@ export function useBitcoinContracts() {
currentAddress,
currentBitcoinNetwork,
blockchainAPI,
oracleAPI
JSON.stringify(attestorURLs)
);
return bitcoinContractInterface;
@@ -91,11 +95,10 @@ export function useBitcoinContracts() {
function handleOffer(
bitcoinContractOfferJSON: string,
counterpartyWalletURL: string,
counterpartyWalletName: string,
counterpartyWalletIcon: string
counterpartyWalletDetailsJSON: string
): BitcoinContractOfferDetails {
const bitcoinContractOffer = JSON.parse(bitcoinContractOfferJSON);
const counterpartyWalletDetails = JSON.parse(counterpartyWalletDetailsJSON);
const bitcoinContractId = bitcoinContractOffer.temporaryContractId;
const bitcoinContractCollateralAmount =
@@ -110,12 +113,6 @@ export function useBitcoinContracts() {
bitcoinContractExpirationDate,
};
const counterpartyWalletDetails: CounterpartyWalletDetails = {
counterpartyWalletURL,
counterpartyWalletName,
counterpartyWalletIcon,
};
const bitcoinContractOfferDetails: BitcoinContractOfferDetails = {
simplifiedBitcoinContract: simplifiedBitcoinContractOffer,
counterpartyWalletDetails,
@@ -126,27 +123,27 @@ export function useBitcoinContracts() {
async function handleAccept(
bitcoinContractJSON: string,
counterpartyWalletDetails: CounterpartyWalletDetails
counterpartyWalletDetails: CounterpartyWalletDetails,
attestorURLs: string[]
) {
let bitcoinContractInterface: JsDLCInterface | undefined;
try {
bitcoinContractInterface = await getBitcoinContractInterface();
bitcoinContractInterface = await getBitcoinContractInterface(attestorURLs);
} catch (error) {
navigate(RouteUrls.BitcoinContractLockError, {
state: {
error,
title: 'There was an error with getting the bitcoin contract interface',
body: 'Unable to setup interface',
title: 'There was an error with getting the Bitcoin Contract Interface',
body: 'Unable to setup Bitcoin Contract Interface',
},
});
sendRpcResponse('none', '', 'failed');
sendRpcResponse(BitcoinContractResponseStatus.INTERFACE_ERROR);
}
if (!bitcoinContractInterface) return;
const bitcoinContractOffer = JSON.parse(bitcoinContractJSON);
const bitcoinContractId = bitcoinContractOffer.temporaryContractId;
const bitcoinContractCollateralAmount =
bitcoinContractOffer.contractInfo.singleContractInfo.totalCollateral;
@@ -162,6 +159,8 @@ export function useBitcoinContracts() {
counterpartyWalletDetails.counterpartyWalletURL
);
const bitcoinContractId = signedBitcoinContract.contractId;
const txId = await bitcoinContractInterface.countersign_and_broadcast(
JSON.stringify(signedBitcoinContract)
);
@@ -182,7 +181,7 @@ export function useBitcoinContracts() {
},
});
sendRpcResponse(bitcoinContractId, txId, 'accept');
sendRpcResponse(BitcoinContractResponseStatus.SUCCESS, bitcoinContractId, txId);
} catch (error) {
navigate(RouteUrls.BitcoinContractLockError, {
state: {
@@ -191,12 +190,12 @@ export function useBitcoinContracts() {
body: 'Unable to lock bitcoin',
},
});
sendRpcResponse(bitcoinContractId, '', 'failed');
sendRpcResponse(BitcoinContractResponseStatus.BROADCAST_ERROR);
}
}
function handleReject(bitcoinContractId: string) {
sendRpcResponse(bitcoinContractId, '', 'reject');
function handleReject() {
sendRpcResponse(BitcoinContractResponseStatus.REJECTED);
close();
}
@@ -217,25 +216,66 @@ export function useBitcoinContracts() {
};
}
function sendRpcResponse(bitcoinContractId: string, txId: string, action: string) {
function sendRpcResponse(
responseStatus: BitcoinContractResponseStatus,
bitcoinContractId?: string,
txId?: string
) {
if (!defaultParams.tabId || !initialSearchParams.get('requestId')) return;
chrome.tabs.sendMessage(
defaultParams.tabId,
makeRpcSuccessResponse('acceptBitcoinContractOffer', {
id: initialSearchParams.get('requestId') as string,
result: {
contractId: bitcoinContractId,
txId,
action,
},
})
);
const requestId = initialSearchParams.get('requestId') as string;
let response;
switch (responseStatus) {
case BitcoinContractResponseStatus.REJECTED:
response = makeRpcErrorResponse('acceptBitcoinContractOffer', {
id: requestId,
error: {
code: RpcErrorCode.USER_REJECTION,
message: responseStatus,
},
});
break;
case BitcoinContractResponseStatus.NETWORK_ERROR:
response = makeRpcErrorResponse('acceptBitcoinContractOffer', {
id: requestId,
error: {
code: RpcErrorCode.INVALID_REQUEST,
message: responseStatus,
},
});
break;
case BitcoinContractResponseStatus.BROADCAST_ERROR:
case BitcoinContractResponseStatus.INTERFACE_ERROR:
response = makeRpcErrorResponse('acceptBitcoinContractOffer', {
id: requestId,
error: {
code: RpcErrorCode.INTERNAL_ERROR,
message: responseStatus,
},
});
break;
default:
response = makeRpcSuccessResponse('acceptBitcoinContractOffer', {
id: requestId,
result: {
contractId: bitcoinContractId,
txId,
},
});
break;
}
chrome.tabs.sendMessage(defaultParams.tabId, response);
}
return {
handleOffer,
handleAccept,
handleReject,
sendRpcResponse,
};
}

View File

@@ -1,4 +1,8 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { RouteUrls } from '@shared/route-urls';
import { BitcoinContractResponseStatus } from '@shared/rpc/methods/accept-bitcoin-contract';
import { useBitcoinContracts } from '@app/common/hooks/use-bitcoin-contracts';
import { BitcoinContractOfferDetails } from '@app/common/hooks/use-bitcoin-contracts';
@@ -14,48 +18,68 @@ import { BitcoinContractRequestWarningLabel } from './components/bitcoin-contrac
export function BitcoinContractRequest() {
const getNativeSegwitSigner = useCurrentAccountNativeSegwitSigner();
const navigate = useNavigate();
const { handleOffer, handleAccept, handleReject } = useBitcoinContracts();
const { handleOffer, handleAccept, handleReject, sendRpcResponse } = useBitcoinContracts();
const [bitcoinContractJSON, setBitcoinContractJSON] = useState<string>();
const [bitcoinContractOfferDetails, setBitcoinContractOfferDetails] =
useState<BitcoinContractOfferDetails>();
const [bitcoinAddress, setBitcoinAddress] = useState<string>();
const [attestorURLs, setAttestorURLs] = useState<string[]>([]);
const [isLoading, setLoading] = useState(true);
const [isProcessing, setProcessing] = useState(false);
const handleAcceptClick = async () => {
if (!bitcoinContractJSON || !bitcoinContractOfferDetails) return;
await handleAccept(bitcoinContractJSON, bitcoinContractOfferDetails.counterpartyWalletDetails);
setProcessing(true);
await handleAccept(
bitcoinContractJSON,
bitcoinContractOfferDetails.counterpartyWalletDetails,
attestorURLs
);
setProcessing(false);
};
const handleRejectClick = async () => {
if (!bitcoinContractOfferDetails) return;
await handleReject(bitcoinContractOfferDetails.simplifiedBitcoinContract.bitcoinContractId);
handleReject();
};
useOnMount(() => {
const bitcoinContractOfferJSON = initialSearchParams.get('bitcoinContractOffer');
const counterpartyWalletURL = initialSearchParams.get('counterpartyWalletURL');
const counterpartyWalletName = initialSearchParams.get('counterpartyWalletName');
const counterpartyWalletIcon = initialSearchParams.get('counterpartyWalletIcon');
const counterpartyWalletDetailsJSON = initialSearchParams.get('counterpartyWalletDetails');
const attestorURLs = initialSearchParams.get('attestorURLs');
const bitcoinAccountDetails = getNativeSegwitSigner?.(0);
if (!bitcoinAccountDetails) return;
const currentBitcoinNetwork = bitcoinAccountDetails.network;
if (currentBitcoinNetwork !== 'testnet') {
navigate(RouteUrls.BitcoinContractLockError, {
state: {
error: new Error('Invalid Network'),
title: "Network doesn't support Bitcoin Contracts",
body: "The wallet's current selected network doesn't support Bitcoin Contracts",
},
});
sendRpcResponse(BitcoinContractResponseStatus.NETWORK_ERROR);
}
if (
!getNativeSegwitSigner ||
!bitcoinContractOfferJSON ||
!counterpartyWalletURL ||
!counterpartyWalletName ||
!counterpartyWalletIcon
!counterpartyWalletDetailsJSON ||
!attestorURLs
)
return;
const currentBitcoinContractOfferDetails = handleOffer(
bitcoinContractOfferJSON,
counterpartyWalletURL,
counterpartyWalletName,
counterpartyWalletIcon
counterpartyWalletDetailsJSON
);
const currentAddress = getNativeSegwitSigner(0).address;
@@ -63,6 +87,7 @@ export function BitcoinContractRequest() {
setBitcoinContractJSON(bitcoinContractOfferJSON);
setBitcoinContractOfferDetails(currentBitcoinContractOfferDetails);
setBitcoinAddress(currentAddress);
setAttestorURLs(JSON.parse(attestorURLs));
setLoading(false);
});
@@ -82,7 +107,7 @@ export function BitcoinContractRequest() {
appName={bitcoinContractOfferDetails.counterpartyWalletDetails.counterpartyWalletName}
/>
<BitcoinContractRequestActions
isLoading={isLoading}
isLoading={isProcessing}
bitcoinAddress={bitcoinAddress}
requiredAmount={
bitcoinContractOfferDetails.simplifiedBitcoinContract.bitcoinContractCollateralAmount

View File

@@ -2,11 +2,17 @@ export async function sendAcceptedBitcoinContractOfferToProtocolWallet(
acceptedBitcoinContractOffer: string,
counterpartyWalletURL: string
) {
return fetch(`${counterpartyWalletURL}/offer/accept`, {
const response = await fetch(`${counterpartyWalletURL}/offer/accept`, {
method: 'put',
body: JSON.stringify({
acceptMessage: acceptedBitcoinContractOffer,
}),
headers: { 'Content-Type': 'application/json' },
}).then(res => res.json());
});
if (!response.ok) {
throw new Error('The counterparty was unable to process the Bitcoin Contract.');
}
return response.json();
}

View File

@@ -2,6 +2,7 @@ import { RpcErrorCode } from '@btckit/types';
import { RouteUrls } from '@shared/route-urls';
import { BitcoinContractRequest } from '@shared/rpc/methods/accept-bitcoin-contract';
import { BitcoinContractResponseStatus } from '@shared/rpc/methods/accept-bitcoin-contract';
import { makeRpcErrorResponse } from '@shared/rpc/rpc-methods';
import {
@@ -16,12 +17,20 @@ export async function rpcAcceptBitcoinContractOffer(
message: BitcoinContractRequest,
port: chrome.runtime.Port
) {
if (!message.params) {
if (
!message.params ||
!message.params.bitcoinContractOffer ||
!message.params.attestorURLs ||
!message.params.counterpartyWalletDetails
) {
chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('acceptBitcoinContractOffer', {
id: message.id,
error: { code: RpcErrorCode.INVALID_PARAMS, message: 'Invalid parameters' },
error: {
code: RpcErrorCode.INVALID_PARAMS,
message: 'The provided parameters are not valid.',
},
})
);
return;
@@ -29,26 +38,11 @@ export async function rpcAcceptBitcoinContractOffer(
const params: RequestParams = [
['bitcoinContractOffer', message.params.bitcoinContractOffer],
['counterpartyWalletURL', message.params.counterpartyWalletURL],
['counterpartyWalletName', message.params.counterpartyWalletName],
['counterpartyWalletIcon', message.params.counterpartyWalletIcon],
['attestorURLs', message.params.attestorURLs],
['counterpartyWalletDetails', message.params.counterpartyWalletDetails],
['requestId', message.id],
];
if (message.params.bitcoinContractOffer.includes('Invalid state: Not enough fund in utxos')) {
chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('acceptBitcoinContractOffer', {
id: message.id,
error: {
code: RpcErrorCode.INVALID_REQUEST,
message: 'The counterparty does not have enough funds to complete the offer',
},
})
);
return;
}
const { urlParams, tabId } = makeSearchParamsWithDefaults(port, params);
const { id } = await triggerRequestWindowOpen(
@@ -63,7 +57,7 @@ export async function rpcAcceptBitcoinContractOffer(
id: message.id,
error: {
code: RpcErrorCode.USER_REJECTION,
message: 'User rejected the offer',
message: BitcoinContractResponseStatus.REJECTED,
},
}),
});

View File

@@ -3,16 +3,21 @@ import { AllowAdditionalProperties } from '@btckit/types/dist/types/utils';
interface BitcoinContractResponseParams extends AllowAdditionalProperties {
bitcoinContractOffer: string;
counterpartyWalletURL: string;
counterpartyWalletName: string;
counterpartyWalletIcon: string;
attestorURLs: string;
counterpartyWalletDetails: string;
}
interface BitcoinContractResponseBody extends AllowAdditionalProperties {
contractId: string;
action: string;
contractId?: string;
txId?: string;
error?: string;
}
export enum BitcoinContractResponseStatus {
SUCCESS = 'Accepting Bitcoin Contract offer was successful',
BROADCAST_ERROR = 'There was an error while broadcasting the Bitcoin Contract transaction',
INTERFACE_ERROR = 'There was an error while interacting with the Bitcoin Contract interface',
NETWORK_ERROR = "The wallet's current selected network is not supported",
REJECTED = 'Bitcoin Contract offer was rejected',
}
export type BitcoinContractRequest = RpcRequest<

View File

@@ -9763,10 +9763,10 @@ dir-glob@^3.0.1:
dependencies:
path-type "^4.0.0"
dlc-wasm-wallet@0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/dlc-wasm-wallet/-/dlc-wasm-wallet-0.4.3.tgz#1070e3a79edb2330ef912a0e182741b37ce49615"
integrity sha512-y8eZu1thQKYgU0EFYTqY0YswYJDenqR4tjMaBymp6vh//YTtsXSyK1LLRSz3oSL60Y1ZwBD9JBWDm1JnQigxlg==
dlc-wasm-wallet@0.4.7:
version "0.4.7"
resolved "https://registry.yarnpkg.com/dlc-wasm-wallet/-/dlc-wasm-wallet-0.4.7.tgz#5c726ab83bf549d09e4ae8c7827ca63a9cff5060"
integrity sha512-Q5ZW3jknqFf99/pNFPE/ETK/Kvoo3M98mACkZJ+s8XVQ4rxRjmNw+9m9HumItdGDLGBNFzCsew1joNrwfkl9Ow==
dlv@^1.1.3:
version "1.1.3"