Skip to content

Commit

Permalink
feat: sbtc integration
Browse files Browse the repository at this point in the history
  • Loading branch information
kyranjamie authored and camerow committed Dec 17, 2024
1 parent 0afa9ce commit d60d444
Show file tree
Hide file tree
Showing 62 changed files with 3,503 additions and 4,401 deletions.
7 changes: 4 additions & 3 deletions config/wallet-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,17 @@
"runesEnabled": true,
"swapsEnabled": true,
"sbtc": {
"enabled": false,
"enabled": true,
"emilyApiUrl": "https://sbtc-emily.com",
"showPromoLinkOnNetworks": ["mainnet", "testnet", "sbtcTestnet"],
"contracts": {
"mainnet": {
"address": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
},
"testnet": {
"address": "SNGWPN3XDAQE673MXYXF81016M50NHF5X5PWWM70.sbtc-token::sbtc-token"
}
},
"showPromoLinkOnNetworks": ["testnet", "sbtcTestnet"]
}
},
"tokensEnabledByDefault": [
"DOGGOTOTHEMOON",
Expand Down
4 changes: 4 additions & 0 deletions config/wallet-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@
"type": "boolean",
"description": "Determines whether or not SBTC is enabled"
},
"emilyApiUrl": {
"type": "string",
"description": "URL for the Emily API"
},
"showPromoLinkOnNetworks": {
"type": "array",
"description": "Networks on which the promo link should be shown",
Expand Down
20 changes: 10 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,15 +146,15 @@
"@hirosystems/token-metadata-api-client": "1.2.0",
"@hookform/resolvers": "3.9.1",
"@leather.io/analytics": "2.0.1",
"@leather.io/bitcoin": "0.16.5",
"@leather.io/constants": "0.13.3",
"@leather.io/crypto": "1.6.12",
"@leather.io/models": "0.21.0",
"@leather.io/query": "2.23.0",
"@leather.io/stacks": "1.3.5",
"@leather.io/bitcoin": "0.17.0",
"@leather.io/constants": "0.13.5",
"@leather.io/crypto": "1.6.14",
"@leather.io/models": "0.22.0",
"@leather.io/query": "2.26.1",
"@leather.io/stacks": "1.4.0",
"@leather.io/tokens": "0.12.1",
"@leather.io/ui": "1.37.0",
"@leather.io/utils": "0.19.1",
"@leather.io/ui": "1.39.0",
"@leather.io/utils": "0.21.1",
"@ledgerhq/hw-transport-webusb": "6.27.19",
"@noble/hashes": "1.5.0",
"@noble/secp256k1": "2.1.0",
Expand Down Expand Up @@ -193,7 +193,6 @@
"@types/lodash.uniqby": "4.7.7",
"@typescript-eslint/eslint-plugin": "7.5.0",
"@zondax/ledger-stacks": "1.0.4",
"alex-sdk": "2.1.4",
"are-passive-events-supported": "1.1.1",
"argon2-browser": "1.18.0",
"assert": "2.1.0",
Expand Down Expand Up @@ -249,6 +248,7 @@
"redux-persist": "6.0.0",
"remark-gfm": "4.0.0",
"rxjs": "7.8.1",
"sbtc": "0.3.1",
"style-loader": "3.3.4",
"ts-debounce": "4.0.0",
"url": "0.11.3",
Expand All @@ -268,7 +268,7 @@
"@btckit/types": "0.0.19",
"@chromatic-com/storybook": "3.2.2",
"@leather.io/eslint-config": "0.7.0",
"@leather.io/panda-preset": "0.5.2",
"@leather.io/panda-preset": "0.5.3",
"@leather.io/prettier-config": "0.6.0",
"@leather.io/rpc": "2.4.0",
"@ls-lint/ls-lint": "2.2.3",
Expand Down
6,305 changes: 2,244 additions & 4,061 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

Binary file added public/assets/avatars/btc-avatar-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/avatars/placeholder-avatar-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/avatars/sbtc-avatar-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions public/assets/illustrations/sbtc-earn-promo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 0 additions & 6 deletions src/app/common/asset-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,6 @@ import {
isMoneyGreaterThanZero,
} from '@leather.io/utils';

export function migratePositiveAssetBalancesToTop<T extends { balance: Money }[]>(assets: T) {
const assetsWithPositiveBalance = assets.filter(asset => asset.balance.amount.isGreaterThan(0));
const assetsWithZeroBalance = assets.filter(asset => asset.balance.amount.isEqualTo(0));
return [...assetsWithPositiveBalance, ...assetsWithZeroBalance] as T;
}

export function convertAssetBalanceToFiat<
T extends { balance: Money | null; marketData: MarketData | null },
>(asset: T) {
Expand Down
42 changes: 42 additions & 0 deletions src/app/common/hooks/use-calculate-sip10-fiat-value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useMemo } from 'react';

import { type MarketData, createMarketData, createMarketPair } from '@leather.io/models';
import {
useAlexCurrencyPriceAsMarketData,
useCryptoCurrencyMarketDataMeanAverage,
} from '@leather.io/query';
import { createMoney } from '@leather.io/utils';

import { useConfigSbtc } from '@app/query/common/remote-config/remote-config.query';

import { getPrincipalFromContractId } from '../utils';

function castBitcoinMarketDataToSbtcMarketData(bitcoinMarketData: MarketData) {
return createMarketData(
createMarketPair('sBTC', 'USD'),
createMoney(bitcoinMarketData.price.amount.toNumber(), 'USD')
);
}

export function useSip10FiatMarketData() {
const priceAsMarketData = useAlexCurrencyPriceAsMarketData();
const bitcoinMarketData = useCryptoCurrencyMarketDataMeanAverage('BTC');
const { isSbtcContract } = useConfigSbtc();

return useMemo(
() => ({
getTokenMarketData(principal: string, symbol: string) {
const lookupIdentifier = principal.includes('::')
? getPrincipalFromContractId(principal)
: principal;

if (isSbtcContract(lookupIdentifier)) {
return castBitcoinMarketDataToSbtcMarketData(bitcoinMarketData);
}

return priceAsMarketData(lookupIdentifier, symbol);
},
}),
[bitcoinMarketData, isSbtcContract, priceAsMarketData]
);
}
14 changes: 13 additions & 1 deletion src/app/common/hooks/use-manage-tokens.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useConfigTokensEnabledByDefault } from '@leather.io/query';

import { useConfigSbtc } from '@app/query/common/remote-config/remote-config.query';
import { useCurrentAccountIndex } from '@app/store/accounts/account';
import { useUserAllTokens } from '@app/store/manage-tokens/manage-tokens.slice';

Expand All @@ -17,6 +18,7 @@ interface FilterTokensArgs<T> {

export function useManageTokens() {
const configEnabledTokens = useConfigTokensEnabledByDefault();
const { contractId: sbtcContractId, isSbtcEnabled } = useConfigSbtc();

const accountIndex = useCurrentAccountIndex();
const userTokensList = useUserAllTokens();
Expand All @@ -29,6 +31,14 @@ export function useManageTokens() {
return token?.enabled ?? isEnabledByDefault;
}

function sortTokens(tokens: any[]) {
return tokens.sort((a, b) => {
if (a.info.contractId === sbtcContractId) return -1;
if (b.info.contractId === sbtcContractId) return 1;
return 0;
});
}

function filterTokens<T>({
tokens,
filter = 'all',
Expand All @@ -37,7 +47,9 @@ export function useManageTokens() {
}: FilterTokensArgs<T>): T[] {
if (filter === 'all') return tokens;

return tokens.filter(t => {
const sortedTokens = isSbtcEnabled ? sortTokens(tokens) : tokens;

return sortedTokens.filter(t => {
const tokenId = getTokenId(t);
const tokenEnabled = isTokenEnabled({ tokenId, preEnabledTokensIds });

Expand Down
8 changes: 6 additions & 2 deletions src/app/components/nonce-setter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ import { useFormikContext } from 'formik';

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

import { StacksSendFormValues, StacksTransactionFormValues } from '@shared/models/form.model';
import {
StacksSendFormValues,
StacksTransactionFormValues,
type SwapFormValues,
} from '@shared/models/form.model';

import { useCurrentStacksAccountAddress } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';

export function NonceSetter() {
const { setFieldValue, touched, values } = useFormikContext<
StacksSendFormValues | StacksTransactionFormValues
StacksSendFormValues | StacksTransactionFormValues | SwapFormValues
>();
const stxAddress = useCurrentStacksAccountAddress();
const { data: nextNonce } = useNextNonce(stxAddress);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import SbtcAvatarIconSrc from '@assets/avatars/sbtc-avatar-icon.png';

import { Avatar, Caption, Title } from '@leather.io/ui';
import { truncateMiddle } from '@leather.io/utils';

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

import { useBitcoinExplorerLink } from '@app/common/hooks/use-bitcoin-explorer-link';
import type { SbtcDepositInfo } from '@app/query/sbtc/sbtc-deposits.query';

import { TransactionItemLayout } from '../transaction-item/transaction-item.layout';

interface SbtcDepositTransactionItemProps {
deposit: SbtcDepositInfo;
}
export function SbtcDepositTransactionItem({ deposit }: SbtcDepositTransactionItemProps) {
const { handleOpenBitcoinTxLink: handleOpenTxLink } = useBitcoinExplorerLink();

const openTxLink = () => {
void analytics.track('view_bitcoin_transaction');
handleOpenTxLink({ txid: deposit.bitcoinTxid });
};

return (
<TransactionItemLayout
openTxLink={openTxLink}
txCaption={truncateMiddle(deposit.bitcoinTxid, 4)}
txIcon={
<Avatar.Root>
<Avatar.Image alt="ST" src={SbtcAvatarIconSrc} />
</Avatar.Root>
}
txStatus={<Caption color="yellow.action-primary-default">Pending</Caption>}
txTitle={<Title textStyle="label.02">BTC → sBTC</Title>}
// Api is only returning 0 right now
txValue={''} // deposit.amount.toString()
/>
);
}
10 changes: 5 additions & 5 deletions src/app/components/stacks-asset-avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import { Box, BoxProps } from 'leather-styles/jsx';
import { Avatar, DynamicColorCircle, StxAvatarIcon, defaultFallbackDelay } from '@leather.io/ui';

interface StacksAssetAvatarProps extends BoxProps {
gradientString?: string;
imageCanonicalUri?: string;
img?: string;
gradientString: string;
isStx?: boolean;
size?: string;
}
export function StacksAssetAvatar({
children,
gradientString,
imageCanonicalUri,
img,
isStx,
size = '36',
...props
Expand All @@ -20,10 +20,10 @@ export function StacksAssetAvatar({

const { color } = props;

if (imageCanonicalUri)
if (img)
return (
<Avatar.Root>
<Avatar.Image alt="FT" src={encodeURI(imageCanonicalUri)} />
<Avatar.Image alt="FT" src={encodeURI(img)} />
<Avatar.Fallback delayMs={defaultFallbackDelay}>FT</Avatar.Fallback>
</Avatar.Root>
);
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/tx-asset-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function TxAssetItem(props: TxAssetItemProps) {
<HStack>
<StacksAssetAvatar
gradientString={iconString}
imageCanonicalUri={imageCanonicalUri}
img={imageCanonicalUri}
isStx={iconString === 'STX'}
size="32"
/>
Expand Down
12 changes: 10 additions & 2 deletions src/app/features/activity-list/activity-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {

import { LoadingSpinner } from '@app/components/loading-spinner';
import { useConfigBitcoinEnabled } from '@app/query/common/remote-config/remote-config.query';
import { useSbtcPendingDeposits } from '@app/query/sbtc/sbtc-deposits.query';
import { useZeroIndexTaprootAddress } from '@app/store/accounts/blockchain/bitcoin/bitcoin.hooks';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentStacksAccountAddress } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
Expand Down Expand Up @@ -63,6 +64,9 @@ export function ActivityList() {
[nsPendingTxs, trPendingTxs]
);

const { isLoading: isLoadingSbtcDeposits, pendingSbtcDeposits } =
useSbtcPendingDeposits(stxAddress);

const { isLoading: isLoadingStacksTransactions, data: stacksTransactionsWithTransfers } =
useGetAccountTransactionsWithTransfersQuery(stxAddress);
const {
Expand All @@ -80,7 +84,8 @@ export function ActivityList() {
isLoadingNsBitcoinTransactions ||
isLoadingTrBitcoinTransactions ||
isLoadingStacksTransactions ||
isLoadingStacksPendingTransactions;
isLoadingStacksPendingTransactions ||
isLoadingSbtcDeposits;

const transactionListBitcoinTxs = useMemo(() => {
return convertBitcoinTxsToListType(
Expand All @@ -99,7 +104,9 @@ export function ActivityList() {

const hasSubmittedTransactions = submittedTransactions.length > 0;
const hasPendingTransactions =
bitcoinPendingTxs.length > 0 || stacksPendingTransactions.length > 0;
bitcoinPendingTxs.length > 0 ||
stacksPendingTransactions.length > 0 ||
pendingSbtcDeposits.length > 0;
const hasTransactions =
transactionListBitcoinTxs.length > 0 || transactionListStacksTxs.length > 0;

Expand Down Expand Up @@ -128,6 +135,7 @@ export function ActivityList() {
{hasPendingTransactions && (
<PendingTransactionList
bitcoinTxs={isBitcoinEnabled ? bitcoinPendingTxs : []}
sBtcDeposits={pendingSbtcDeposits}
stacksTxs={stacksPendingTransactions}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,30 @@ import { MempoolTransaction } from '@stacks/stacks-blockchain-api-types';
import type { BitcoinTx } from '@leather.io/models';

import { BitcoinTransactionItem } from '@app/components/bitcoin-transaction-item/bitcoin-transaction-item';
import { SbtcDepositTransactionItem } from '@app/components/sbtc-deposit-status-item/sbtc-deposit-status-item';
import { StacksTransactionItem } from '@app/components/stacks-transaction-item/stacks-transaction-item';
import type { SbtcDepositInfo } from '@app/query/sbtc/sbtc-deposits.query';

import { PendingTransactionListLayout } from './pending-transaction-list.layout';

interface PendingTransactionListProps {
bitcoinTxs: BitcoinTx[];
sBtcDeposits: SbtcDepositInfo[];
stacksTxs: MempoolTransaction[];
}
export function PendingTransactionList({ bitcoinTxs, stacksTxs }: PendingTransactionListProps) {
export function PendingTransactionList({
bitcoinTxs,
sBtcDeposits,
stacksTxs,
}: PendingTransactionListProps) {
return (
<PendingTransactionListLayout>
{bitcoinTxs.map(tx => (
<BitcoinTransactionItem key={tx.txid} transaction={tx} />
))}
{sBtcDeposits.map(deposit => (
<SbtcDepositTransactionItem key={deposit.bitcoinTxid} deposit={deposit} />
))}
{stacksTxs.map(tx => (
<StacksTransactionItem key={tx.tx_id} transaction={tx} />
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,7 @@ export function FtTransferItem({ ftTransfer, parentTx }: FtTransferItemProps) {
const title = `${assetMetadata.name || 'Token'} Transfer`;
const value = `${isOriginator ? '-' : ''}${displayAmount.toFormat()}`;
const transferIcon = ftImageCanonicalUri ? (
<StacksAssetAvatar
color="ink.background-primary"
gradientString=""
imageCanonicalUri={ftImageCanonicalUri}
>
<StacksAssetAvatar color="ink.background-primary" gradientString="" img={ftImageCanonicalUri}>
{title}
</StacksAssetAvatar>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@ export function Sip10TokenAssetItem({
const { contractId, imageCanonicalUri, name, symbol } = info;

const icon = (
<StacksAssetAvatar
color="white"
gradientString={contractId}
imageCanonicalUri={getSafeImageCanonicalUri(imageCanonicalUri, name)}
>
{name[0]}
</StacksAssetAvatar>
<>
<StacksAssetAvatar
color="white"
gradientString={contractId}
img={getSafeImageCanonicalUri(imageCanonicalUri, name)}
>
{name[0]}
</StacksAssetAvatar>
</>
);

const captionLeft = symbol;
Expand Down
Loading

0 comments on commit d60d444

Please sign in to comment.