Skip to content

Commit

Permalink
refactor: add v6 tx signing for swaps
Browse files Browse the repository at this point in the history
  • Loading branch information
fbwoolf committed Jan 24, 2025
1 parent 3a79248 commit 1a42a5f
Show file tree
Hide file tree
Showing 18 changed files with 381 additions and 97 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,11 @@
"@stacks/connect": "7.4.0",
"@stacks/encryption": "7.0.2",
"@stacks/network": "7.0.2",
"@stacks/network-v6": "npm:@stacks/[email protected]",
"@stacks/profile": "7.0.2",
"@stacks/rpc-client": "1.0.3",
"@stacks/transactions": "7.0.2",
"@stacks/transactions-v6": "npm:@stacks/[email protected]",
"@stacks/wallet-sdk": "7.0.2",
"@stitches/react": "1.2.8",
"@storybook/addon-styling-webpack": "1.0.1",
Expand Down
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 64 additions & 0 deletions src/app/common/hooks/use-submit-stx-transaction-v6.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useCallback } from 'react';

import { StacksTransaction, broadcastTransaction } from '@stacks/transactions-v6';

import { delay, isError } from '@leather.io/utils';

import { logger } from '@shared/logger';
import { analytics } from '@shared/utils/analytics';

import { useRefreshAllAccountData } from '@app/common/hooks/account/use-refresh-all-account-data';
import { useLoading } from '@app/common/hooks/use-loading';
import { safelyFormatHexTxid } from '@app/common/utils/safe-handle-txid';
import { useToast } from '@app/features/toasts/use-toast';
import { useCurrentStacksNetworkStateV6 } from '@app/store/networks/networks.hooks';

const timeForApiToUpdate = 250;

interface UseSubmitTransactionArgs {
loadingKey: string;
}
interface UseSubmitTransactionCallbackV6Args {
replaceByFee?: boolean;
onSuccess(txId: string): void;
onError(error: Error | string): void;
}
export function useSubmitTransactionCallbackV6({ loadingKey }: UseSubmitTransactionArgs) {
const toast = useToast();
const refreshAccountData = useRefreshAllAccountData();

const { setIsLoading, setIsIdle } = useLoading(loadingKey);
const stacksNetwork = useCurrentStacksNetworkStateV6();

return useCallback(
({ onSuccess, onError }: UseSubmitTransactionCallbackV6Args) =>
async (transaction: StacksTransaction) => {
setIsLoading();
try {
const response = await broadcastTransaction(transaction, stacksNetwork);
if (response.error) {
logger.error('Transaction broadcast', response);
if (response.reason) toast.error('Broadcast error');
onError(response.error);
setIsIdle();
} else {
logger.info('Transaction broadcast', response);

await delay(500);

void analytics.track('broadcast_transaction', {
symbol: 'stx',
});
onSuccess(safelyFormatHexTxid(response.txid));
setIsIdle();
await refreshAccountData(timeForApiToUpdate);
}
} catch (error) {
logger.error('Transaction callback', { error });
onError(isError(error) ? error : { name: '', message: '' });
setIsIdle();
}
},
[setIsLoading, stacksNetwork, toast, setIsIdle, refreshAccountData]
);
}
5 changes: 5 additions & 0 deletions src/app/common/publish-subscribe.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Transaction } from '@scure/btc-signer';
import type { SignatureData } from '@stacks/connect';
import type { StacksTransactionWire } from '@stacks/transactions';
import type { StacksTransaction } from '@stacks/transactions-v6';

import type { UnsignedMessage } from '@shared/signature/signature-types';

Expand Down Expand Up @@ -48,6 +49,10 @@ function createPublishSubscribe<E>(): PubSubType<E> {
// Global app events. Only add events if your feature isn't capable of
// communicating internally.
export interface GlobalAppEvents {
ledgerStacksTxSignedV6: {
unsignedTx: string;
signedTx: StacksTransaction;
};
ledgerStacksTxSigned: {
unsignedTx: string;
signedTx: StacksTransactionWire;
Expand Down
8 changes: 5 additions & 3 deletions src/app/common/transactions/stacks/generate-unsigned-txs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
ClarityVersion,
PostConditionMode,
type UnsignedContractCallOptions,
type UnsignedContractDeployOptions,
deserializeCV,
Expand All @@ -22,7 +23,7 @@ import {
type STXTransferPayload,
} from '@shared/utils/legacy-requests';

function initNonce(nonce?: number) {
export function initNonce(nonce?: number) {
return nonce !== undefined ? new BN(nonce, 10) : undefined;
}

Expand Down Expand Up @@ -58,11 +59,12 @@ function generateUnsignedContractCallTx(args: GenerateUnsignedContractCallTxArgs
functionArgs: fnArgs,
nonce: initNonce(nonce)?.toString(),
fee: new BN(fee, 10).toString(),
postConditionMode: postConditionMode,
postConditionMode: postConditionMode ?? PostConditionMode.Deny,
postConditions,
network,
sponsored,
} satisfies UnsignedContractCallOptions;

return makeUnsignedContractCall(options);
}

Expand All @@ -77,7 +79,7 @@ function generateUnsignedContractDeployTx(args: GenerateUnsignedContractDeployTx
nonce: initNonce(nonce)?.toString(),
fee: new BN(fee, 10)?.toString(),
publicKey,
postConditionMode: postConditionMode,
postConditionMode,
postConditions: getPostConditions(postConditions?.map(pc => ensurePostConditionWireFormat(pc))),
network,
clarityVersion: ClarityVersion.Clarity3,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,32 @@
import type { StacksTransactionWire } from '@stacks/transactions';
import type { StacksTransaction } from '@stacks/transactions-v6';

import { GlobalAppEvents, appEvents } from '@app/common/publish-subscribe';

export async function listenForStacksTxLedgerSigningV6(
unsignedTx: string
): Promise<StacksTransaction> {
return new Promise((resolve, reject) => {
function txSignedHandler(msg: GlobalAppEvents['ledgerStacksTxSignedV6']) {
if (msg.unsignedTx === unsignedTx) {
appEvents.unsubscribe('ledgerStacksTxSignedV6', txSignedHandler);
appEvents.unsubscribe('ledgerStacksTxSigningCancelled', signingAbortedHandler);
resolve(msg.signedTx);
}
}
appEvents.subscribe('ledgerStacksTxSignedV6', txSignedHandler);

function signingAbortedHandler(msg: GlobalAppEvents['ledgerStacksTxSigningCancelled']) {
if (msg.unsignedTx === unsignedTx) {
appEvents.unsubscribe('ledgerStacksTxSigningCancelled', signingAbortedHandler);
appEvents.unsubscribe('ledgerStacksTxSignedV6', txSignedHandler);
reject(new Error('User cancelled the signing operation'));
}
}
appEvents.subscribe('ledgerStacksTxSigningCancelled', signingAbortedHandler);
});
}

export async function listenForStacksTxLedgerSigning(
unsignedTx: string
): Promise<StacksTransactionWire> {
Expand Down
5 changes: 2 additions & 3 deletions src/app/features/ledger/hooks/use-ledger-navigate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { useMemo } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';

import { bytesToHex } from '@stacks/common';
import { StacksTransactionWire } from '@stacks/transactions';

import type { SupportedBlockchains } from '@leather.io/models';

Expand All @@ -28,11 +27,11 @@ export function useLedgerNavigate() {
});
},

toConnectAndSignStacksTransactionStep(transaction: StacksTransactionWire) {
toConnectAndSignStacksTransactionStep(transaction: string) {
return navigate(RouteUrls.ConnectLedger, {
replace: true,
relative: 'path',
state: { tx: transaction.serialize() },
state: { tx: transaction },
});
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ export function SwapAssetList({ assets, type }: SwapAssetListProps) {

return (
<Stack mb="space.05" p="space.05" width="100%" data-testid={SwapSelectors.SwapAssetList}>
{selectableAssets.map(asset => (
<SwapAssetItem asset={asset} key={asset.tokenId} onClick={() => onSelectAsset(asset)} />
{selectableAssets.map((asset, idx) => (
<SwapAssetItem
asset={asset}
key={`${asset.tokenId}${idx}`}
onClick={() => onSelectAsset(asset)}
/>
))}
</Stack>
);
Expand Down
20 changes: 10 additions & 10 deletions src/app/pages/swap/hooks/use-sponsor-tx-fees.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';

import type { StacksTransactionWire } from '@stacks/transactions';
import type { StacksTransaction } from '@stacks/transactions-v6';

import { FeeTypes } from '@leather.io/models';
import { defaultFeesMaxValuesAsMoney } from '@leather.io/query';
Expand All @@ -13,21 +13,21 @@ import { LoadingKeys, useLoading } from '@app/common/hooks/use-loading';
import { useToast } from '@app/features/toasts/use-toast';
import { useConfigSbtc } from '@app/query/common/remote-config/remote-config.query';
import {
type TransactionBase,
submitSponsoredSbtcTransaction,
verifySponsoredSbtcTransaction,
type TransactionBaseV6,
submitSponsoredSbtcTransactionV6,
verifySponsoredSbtcTransactionV6,
} from '@app/query/sbtc/sponsored-transactions.query';
import { useSignStacksTransaction } from '@app/store/transactions/transaction.hooks';
import { useSignStacksTransactionV6 } from '@app/store/transactions/transaction.hooks';

export function useSponsorTransactionFees() {
const { sponsorshipApiUrl } = useConfigSbtc();
const { setIsIdle } = useLoading(LoadingKeys.SUBMIT_SWAP_TRANSACTION);
const signTx = useSignStacksTransaction();
const signTx = useSignStacksTransactionV6();
const navigate = useNavigate();
const toast = useToast();

const checkEligibilityForSponsor = async (baseTx: TransactionBase) => {
return await verifySponsoredSbtcTransaction({
const checkEligibilityForSponsor = async (baseTx: TransactionBaseV6) => {
return await verifySponsoredSbtcTransactionV6({
apiUrl: sponsorshipApiUrl,
baseTx,
nonce: Number(baseTx.options.nonce),
Expand All @@ -36,12 +36,12 @@ export function useSponsorTransactionFees() {
};

const submitSponsoredTx = useCallback(
async (unsignedSponsoredTx: StacksTransactionWire) => {
async (unsignedSponsoredTx: StacksTransaction) => {
try {
const signedSponsoredTx = await signTx(unsignedSponsoredTx);
if (!signedSponsoredTx) return logger.error('Unable to sign sponsored transaction');

const result = await submitSponsoredSbtcTransaction(sponsorshipApiUrl, signedSponsoredTx);
const result = await submitSponsoredSbtcTransactionV6(sponsorshipApiUrl, signedSponsoredTx);
if (!result.txid) {
navigate(RouteUrls.SwapError, { state: { message: result.error } });
return;
Expand Down
8 changes: 4 additions & 4 deletions src/app/pages/swap/hooks/use-stacks-broadcast-swap.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';

import { StacksTransactionWire } from '@stacks/transactions';
import type { StacksTransaction } from '@stacks/transactions-v6';

import { isError, isString } from '@leather.io/utils';

import { logger } from '@shared/logger';
import { RouteUrls } from '@shared/route-urls';

import { LoadingKeys, useLoading } from '@app/common/hooks/use-loading';
import { useSubmitTransactionCallback } from '@app/common/hooks/use-submit-stx-transaction';
import { useSubmitTransactionCallbackV6 } from '@app/common/hooks/use-submit-stx-transaction-v6';
import { useToast } from '@app/features/toasts/use-toast';

export function useStacksBroadcastSwap() {
const { setIsIdle } = useLoading(LoadingKeys.SUBMIT_SWAP_TRANSACTION);
const navigate = useNavigate();
const toast = useToast();

const broadcastTransactionFn = useSubmitTransactionCallback({
const broadcastTransactionFn = useSubmitTransactionCallbackV6({
loadingKey: LoadingKeys.SUBMIT_SWAP_TRANSACTION,
});

return useCallback(
async (signedTx: StacksTransactionWire) => {
async (signedTx: StacksTransaction) => {
if (!signedTx) {
logger.error('Cannot broadcast transaction, no tx in state');
toast.error('Unable to broadcast transaction');
Expand Down
10 changes: 4 additions & 6 deletions src/app/pages/swap/providers/stacks-swap-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { Outlet } from 'react-router-dom';

import type { StacksTransaction } from '@stacks/transactions-v6';
import type { RouteQuote } from 'bitflow-sdk';

import { defaultSwapFee } from '@leather.io/query';

import type {
SbtcSponsorshipEligibility,
TransactionBase,
} from '@app/query/sbtc/sponsored-transactions.query';
import type { SbtcSponsorshipEligibilityV6 } from '@app/query/sbtc/sponsored-transactions.query';

import { SwapForm } from '../form/swap-form';
import { useAllSwappableAssets } from '../hooks/use-all-swappable-assets';
Expand All @@ -19,8 +17,8 @@ export interface StacksSwapContext extends BaseSwapContext<StacksSwapContext> {
nonce: number | string;
routeQuote?: RouteQuote;
slippage: number;
sponsorship?: SbtcSponsorshipEligibility;
unsignedTx?: TransactionBase;
sponsorship?: SbtcSponsorshipEligibilityV6;
unsignedTx?: StacksTransaction;
}

interface StacksSwapProviderProps {
Expand Down
Loading

0 comments on commit 1a42a5f

Please sign in to comment.