Skip to content

Commit

Permalink
Merge pull request #4106 from hirosystems/release/broadcast-psbts
Browse files Browse the repository at this point in the history
Release/broadcast psbts
  • Loading branch information
fbwoolf authored Aug 8, 2023
2 parents 2620233 + 0b23519 commit ea946e8
Show file tree
Hide file tree
Showing 58 changed files with 806 additions and 255 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ on:
jobs:
test:
name: Shard ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }}
timeout-minutes: 5
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand Down Expand Up @@ -79,4 +79,4 @@ jobs:
with:
name: playwright-report
path: playwright-report/
retention-days: 30
retention-days: 10
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@
"@tanstack/react-query-devtools": "4.32.0",
"@tanstack/react-query-persist-client": "4.32.0",
"@tippyjs/react": "4.2.6",
"@types/lodash.uniqby": "4.7.7",
"@typescript-eslint/eslint-plugin": "5.60.1",
"@vkontakte/vk-qr": "2.0.13",
"@zondax/ledger-stacks": "1.0.4",
Expand Down Expand Up @@ -195,6 +196,7 @@
"ledger-bitcoin": "0.2.2",
"limiter": "2.1.0",
"lodash.get": "4.4.2",
"lodash.uniqby": "4.7.0",
"mdi-react": "9.2.0",
"micro-packed": "0.3.2",
"object-hash": "3.0.0",
Expand All @@ -209,6 +211,7 @@
"react-dom": "18.2.0",
"react-hot-toast": "2.4.1",
"react-icons": "4.10.1",
"react-intersection-observer": "9.5.2",
"react-lottie": "1.2.3",
"react-redux": "8.1.1",
"react-router-dom": "6.14.0",
Expand Down
9 changes: 9 additions & 0 deletions src/app/common/psbt/requests.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import * as btc from '@scure/btc-signer';
import { PsbtPayload } from '@stacks/connect';
import { decodeToken } from 'jsontokens';

import { Money } from '@shared/models/money.model';
import { isString } from '@shared/utils';

export interface SignPsbtArgs {
addressNativeSegwitTotal?: Money;
addressTaprootTotal?: Money;
fee?: Money;
inputs: btc.TransactionInput[];
}

export function getPsbtPayloadFromToken(requestToken: string): PsbtPayload {
const token = decodeToken(requestToken);
if (isString(token.payload)) throw new Error('Error decoding json token');
Expand Down
16 changes: 9 additions & 7 deletions src/app/common/psbt/use-psbt-request-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,23 @@ export function useRpcSignPsbtParams() {

const [searchParams] = useSearchParams();
const { origin, tabId } = useDefaultRequestParams();
const requestId = searchParams.get('requestId');
const psbtHex = searchParams.get('hex');
const allowedSighash = searchParams.getAll('allowedSighash');
const broadcast = searchParams.get('broadcast');
const psbtHex = searchParams.get('hex');
const requestId = searchParams.get('requestId');
const signAtIndex = searchParams.getAll('signAtIndex');

return useMemo(() => {
return {
origin,
tabId: tabId ?? 0,
requestId,
psbtHex,
allowedSighash: undefinedIfLengthZero(
allowedSighash.map(h => Number(h)) as AllowedSighashTypes[]
),
broadcast,
origin,
psbtHex,
requestId,
signAtIndex: undefinedIfLengthZero(ensureArray(signAtIndex).map(h => Number(h))),
tabId: tabId ?? 0,
};
}, [allowedSighash, origin, psbtHex, requestId, signAtIndex, tabId]);
}, [allowedSighash, broadcast, origin, psbtHex, requestId, signAtIndex, tabId]);
}
11 changes: 11 additions & 0 deletions src/app/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,3 +295,14 @@ export function isFulfilled<T>(p: PromiseSettledResult<T>): p is PromiseFulfille
export function isRejected<T>(p: PromiseSettledResult<T>): p is PromiseRejectedResult {
return p.status === 'rejected';
}

interface LinearInterpolation {
start: number;
end: number;
t: number;
}

// Linear Interpolation
export function linearInterpolation({ start, end, t }: LinearInterpolation) {
return (1 - t) * start + t * end;
}
Original file line number Diff line number Diff line change
@@ -1,38 +1,16 @@
import { FiArrowDown as IconArrowDown, FiArrowUp as IconArrowUp } from 'react-icons/fi';

import { Box, BoxProps, Circle, ColorsStringLiteral, Flex, color } from '@stacks/ui';
import { Box, BoxProps, Circle, Flex, color } from '@stacks/ui';

import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';

import { isBitcoinTxInbound } from '@app/common/transactions/bitcoin/utils';
import { BtcIcon } from '@app/components/icons/btc-icon';

import { IconForTx, colorFromTx } from './utils';

interface TransactionIconProps extends BoxProps {
transaction: BitcoinTx;
btcAddress: string;
}

type BtcTxStatus = 'pending' | 'success';
type BtcStatusColorMap = Record<BtcTxStatus, ColorsStringLiteral>;

const statusFromTx = (tx: BitcoinTx): BtcTxStatus => {
if (tx.status.confirmed) return 'success';
return 'pending';
};

const colorFromTx = (tx: BitcoinTx): ColorsStringLiteral => {
const colorMap: BtcStatusColorMap = {
pending: 'feedback-alert',
success: 'brand',
};

return colorMap[statusFromTx(tx)] ?? 'feedback-error';
};

function IconForTx(address: string, tx: BitcoinTx) {
if (isBitcoinTxInbound(address, tx)) return IconArrowDown;
return IconArrowUp;
}
export function BitcoinTransactionIcon({ transaction, btcAddress, ...rest }: TransactionIconProps) {
return (
<Flex position="relative">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Box, Circle, Flex, color } from '@stacks/ui';

import { SupportedInscription } from '@shared/models/inscription.model';
import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';

import { OrdinalIcon } from '../icons/ordinal-icon';
import { IconForTx, colorFromTx } from './utils';

interface BitcoinTransactionInscriptionIconProps {
inscription: SupportedInscription;
transaction: BitcoinTx;
btcAddress: string;
}

function InscriptionIcon({ inscription, ...rest }: { inscription: SupportedInscription }) {
switch (inscription.type) {
case 'image':
return (
<Circle
bg={color('accent')}
color={color('bg')}
flexShrink={0}
position="relative"
size="36px"
{...rest}
>
<img
src={inscription.src}
style={{
width: '100%',
height: '100%',
aspectRatio: '1 / 1',
objectFit: 'cover',
borderRadius: '6px',
}}
/>
</Circle>
);
default:
return <OrdinalIcon />;
}
}

export function BitcoinTransactionInscriptionIcon({
inscription,
transaction,
btcAddress,
...rest
}: BitcoinTransactionInscriptionIconProps) {
return (
<Flex position="relative">
<InscriptionIcon inscription={inscription} />
<Circle
bottom="-2px"
right="-9px"
position="absolute"
size="21px"
bg={color(colorFromTx(transaction))}
color={color('bg')}
border="2px solid"
borderColor={color('bg')}
{...rest}
>
<Box size="13px" as={IconForTx(btcAddress, transaction)} />
</Circle>
</Flex>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,43 @@ import {
isBitcoinTxInbound,
} from '@app/common/transactions/bitcoin/utils';
import { useWalletType } from '@app/common/use-wallet-type';
import { openInNewTab } from '@app/common/utils/open-in-new-tab';
import { usePressable } from '@app/components/item-hover';
import { IncreaseFeeButton } from '@app/components/stacks-transaction-item/increase-fee-button';
import { TransactionTitle } from '@app/components/transaction/transaction-title';
import {
convertInscriptionToSupportedInscriptionType,
createInscriptionInfoUrl,
} from '@app/query/bitcoin/ordinals/inscription.hooks';
import { useGetInscriptionsByOutputQuery } from '@app/query/bitcoin/ordinals/use-inscription-by-output.query';
import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';

import { CaptionDotSeparator } from '../caption-dot-separator';
import { TransactionItemLayout } from '../transaction-item/transaction-item.layout';
import { BitcoinTransactionCaption } from './bitcoin-transaction-caption';
import { BitcoinTransactionIcon } from './bitcoin-transaction-icon';
import { BitcoinTransactionInscriptionIcon } from './bitcoin-transaction-inscription-icon';
import { BitcoinTransactionStatus } from './bitcoin-transaction-status';
import { BitcoinTransactionValue } from './bitcoin-transaction-value';
import { containsTaprootInput } from './utils';

interface BitcoinTransactionItemProps extends BoxProps {
transaction?: BitcoinTx;
transaction: BitcoinTx;
}
export function BitcoinTransactionItem({ transaction, ...rest }: BitcoinTransactionItemProps) {
const [component, bind, { isHovered }] = usePressable(true);
const { pathname } = useLocation();
const navigate = useNavigate();
const { whenWallet } = useWalletType();

const { data: inscriptionData } = useGetInscriptionsByOutputQuery(transaction, {
select(data) {
const inscription = data.results[0];
if (!inscription) return;
return convertInscriptionToSupportedInscriptionType(inscription);
},
});

const bitcoinAddress = useCurrentAccountNativeSegwitAddressIndexZero();
const { handleOpenTxLink } = useExplorerLink();
const analytics = useAnalytics();
Expand All @@ -56,18 +73,39 @@ export function BitcoinTransactionItem({ transaction, ...rest }: BitcoinTransact

const openTxLink = () => {
void analytics.track('view_bitcoin_transaction');
if (inscriptionData) {
openInNewTab(createInscriptionInfoUrl(inscriptionData.id));
return;
}
handleOpenTxLink({
blockchain: 'bitcoin',
txid: transaction?.txid || '',
});
};

const isOriginator = !isBitcoinTxInbound(bitcoinAddress, transaction);
const isEnabled = isOriginator && !transaction.status.confirmed;
const isEnabled =
isOriginator && !transaction.status.confirmed && !containsTaprootInput(transaction);

const txCaption = <BitcoinTransactionCaption>{caption}</BitcoinTransactionCaption>;
const txCaption = (
<CaptionDotSeparator>
<BitcoinTransactionCaption>{caption}</BitcoinTransactionCaption>
{inscriptionData ? (
<BitcoinTransactionCaption>{inscriptionData.mime_type}</BitcoinTransactionCaption>
) : null}
</CaptionDotSeparator>
);
const txValue = <BitcoinTransactionValue>{value}</BitcoinTransactionValue>;

const txIcon = inscriptionData ? (
<BitcoinTransactionInscriptionIcon
inscription={inscriptionData}
transaction={transaction}
btcAddress={bitcoinAddress}
/>
) : (
<BitcoinTransactionIcon transaction={transaction} btcAddress={bitcoinAddress} />
);
const title = inscriptionData ? `Ordinal inscription #${inscriptionData.number}` : 'Bitcoin';
const increaseFeeButton = (
<IncreaseFeeButton
isEnabled={isEnabled}
Expand All @@ -76,13 +114,14 @@ export function BitcoinTransactionItem({ transaction, ...rest }: BitcoinTransact
onIncreaseFee={onIncreaseFee}
/>
);

return (
<TransactionItemLayout
openTxLink={openTxLink}
txCaption={txCaption}
txIcon={<BitcoinTransactionIcon transaction={transaction} btcAddress={bitcoinAddress} />}
txIcon={txIcon}
txStatus={<BitcoinTransactionStatus transaction={transaction} />}
txTitle={<TransactionTitle title="Bitcoin" />}
txTitle={<TransactionTitle title={title} />}
txValue={txValue}
belowCaptionEl={increaseFeeButton}
{...bind}
Expand Down
33 changes: 33 additions & 0 deletions src/app/components/bitcoin-transaction-item/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { FiArrowDown as IconArrowDown, FiArrowUp as IconArrowUp } from 'react-icons/fi';

import { ColorsStringLiteral } from '@stacks/ui-theme';

import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';

import { isBitcoinTxInbound } from '@app/common/transactions/bitcoin/utils';

type BtcTxStatus = 'pending' | 'success';
type BtcStatusColorMap = Record<BtcTxStatus, ColorsStringLiteral>;

const statusFromTx = (tx: BitcoinTx): BtcTxStatus => {
if (tx.status.confirmed) return 'success';
return 'pending';
};

export const colorFromTx = (tx: BitcoinTx): ColorsStringLiteral => {
const colorMap: BtcStatusColorMap = {
pending: 'feedback-alert',
success: 'brand',
};

return colorMap[statusFromTx(tx)] ?? 'feedback-error';
};

export function IconForTx(address: string, tx: BitcoinTx) {
if (isBitcoinTxInbound(address, tx)) return IconArrowDown;
return IconArrowUp;
}

export function containsTaprootInput(tx: BitcoinTx) {
return tx.vin.some(input => input.prevout.scriptpubkey_type === 'v1_p2tr');
}
36 changes: 23 additions & 13 deletions src/app/components/icons/ordinal-icon.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
export function OrdinalIcon() {
return (
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="18" cy="18" r="18" fill="#0C0C0D" />
<circle cx="18" cy="18" r="12.375" fill="white" />
<rect x="7.32143" y="7.32143" width="21.3571" height="21.3571" rx="10.6786" fill="white" />
<circle cx="18.0001" cy="18" r="4.57143" fill="#0C0C0D" />
<rect
x="7.32143"
y="7.32143"
width="21.3571"
height="21.3571"
rx="10.6786"
stroke="#0C0C0D"
strokeWidth="1.14286"
/>
<g clipPath="url(#clip0_1_5)">
<path
d="M18 36C27.9411 36 36 27.9411 36 18C36 8.05888 27.9411 0 18 0C8.05888 0 0 8.05888 0 18C0 27.9411 8.05888 36 18 36Z"
fill="#0C0C0D"
/>
<path
d="M18 31.5C25.4558 31.5 31.5 25.4558 31.5 18C31.5 10.5442 25.4558 4.5 18 4.5C10.5442 4.5 4.5 10.5442 4.5 18C4.5 25.4558 10.5442 31.5 18 31.5Z"
fill="white"
/>
<path
d="M18 24.75C21.7279 24.75 24.75 21.7279 24.75 18C24.75 14.2721 21.7279 11.25 18 11.25C14.2721 11.25 11.25 14.2721 11.25 18C11.25 21.7279 14.2721 24.75 18 24.75Z"
fill="#0C0C0D"
/>
<path
d="M36 18C36 8.05888 27.9411 0 18 0C8.05888 0 0 8.05888 0 18C0 27.9411 8.05888 36 18 36C27.9411 36 36 27.9411 36 18Z"
stroke="white"
/>
</g>
<defs>
<clipPath id="clip0_1_5">
<rect width="36" height="36" fill="white" />
</clipPath>
</defs>
</svg>
);
}
Loading

0 comments on commit ea946e8

Please sign in to comment.