Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: api clients and add stx-20 balances #5315

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ import axios from 'axios';

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

const leatherHeaders: HeadersInit = {
'x-leather-version': VERSION,
'x-hiro-product': 'leather',
};
import { leatherHeaders } from './stacks/clients/hiro-api-client';

function isErrorCode(statusCode: number) {
return statusCode >= 400;
Expand All @@ -19,6 +16,7 @@ function trackApiError(url: string, statusCode: number) {
* @deprecated Use `axios` directly instead. Fetch only needed for interation
* with generated stacks blockchain api library
*/
// TEMPORARY RELOCATION
export async function wrappedFetch(input: RequestInfo, init: RequestInit = {}) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be removed if we refactor them all.

const initHeaders = init.headers || {};
// eslint-disable-next-line no-restricted-globals
Expand Down
15 changes: 15 additions & 0 deletions src/app/api/stacks/clients/hiro-api-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import axios from 'axios';

import { type DefaultNetworkConfigurations, defaultNetworksKeyedById } from '@shared/constants';

export const leatherHeaders = {
'x-leather-version': VERSION,
'x-hiro-product': 'leather',
};

export function hiroApiClient(network: DefaultNetworkConfigurations) {
return axios.create({
baseURL: defaultNetworksKeyedById[network].chain.stacks.url,
headers: { ...leatherHeaders },
});
Comment on lines +10 to +14
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm, actually (contrary to my earlier recommendation), this wouldn't work with custom networks, should just pass the URL

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this now, thx.

}
7 changes: 7 additions & 0 deletions src/app/api/stacks/clients/stx20-api-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import axios from 'axios';

export function stx20ApiClient() {
return axios.create({
baseURL: 'https://api.stx20.com/api/v1',
});
}
Comment on lines +3 to +7
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want to call this once, and export, vs every time.

E.g, if we want to modify all requests for stx20.com, we'd use stx20Client.interceptors, which afaik depends on a single instance

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mm, but yeah we can't do this for custom networks

Copy link
Collaborator

@kyranjamie kyranjamie Apr 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so now I'm questioning if it is a good idea to use all the separate axios clients / vs a single client where we don't set baseURL. Thoughts?

Copy link
Contributor Author

@fbwoolf fbwoolf Apr 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking on the refactor here, I might close this and separate out the STX-20 balance work just to get that merged first. I'm not sure we absolutely need to refactor the api clients rn, but I'd like to think on how to set this all up a bit better.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, this should only be called once with the config nec to handle that service.
https://blog.logrocket.com/understanding-axios-create/

16 changes: 16 additions & 0 deletions src/app/api/stacks/services/account-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { AddressBalanceResponse } from '@stacks/stacks-blockchain-api-types';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you check this lib is up to date?


import type { DefaultNetworkConfigurations } from '@shared/constants';

// import type { AddressBalanceResponse } from '@shared/models/account.model';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this and am using the one imported from the api-types bc the need for this might be outdated? I'm not seeing why we would need to keep this?

import { hiroApiClient } from '../clients/hiro-api-client';

export function fetchAccountBalances(network: DefaultNetworkConfigurations, signal?: AbortSignal) {
return async (principal: string): Promise<AddressBalanceResponse> => {
const response = await hiroApiClient(network).get<AddressBalanceResponse>(
`/extended/v1/address/${principal}/balances`,
{ signal }
);
return response.data;
};
}
7 changes: 7 additions & 0 deletions src/app/api/stacks/services/stx20-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { stx20ApiClient } from '../clients/stx20-api-client';
import type { Stx20Balance, Stx20BalanceResponse } from '../types/stx20-types';

export async function fetchStx20Balances(address: string): Promise<Stx20Balance[]> {
const response = await stx20ApiClient().get<Stx20BalanceResponse>(`/balance/${address}`);
return response.data.balances;
}
19 changes: 19 additions & 0 deletions src/app/api/stacks/types/stx20-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Money } from '@shared/models/money.model';

/* API RESPONSE TYPES */
export interface Stx20Balance {
ticker: string;
balance: string;
updateDate: string;
}

export interface Stx20BalanceResponse {
address: string;
balances: Stx20Balance[];
}

/* LEATHER TYPES */
export interface Stx20Token extends Omit<Stx20Balance, 'balance'> {
balance: Money;
marketData: null;
}
Comment on lines +16 to +19
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per comment in other PR, do wonder if these should be paired ahead of time at model-level?

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { styled } from 'leather-styles/jsx';

import type { Stx20Token } from '@app/api/stacks/types/stx20-types';
import { formatBalance } from '@app/common/format-balance';
import { Avatar } from '@app/ui/components/avatar/avatar';
import { ItemLayout } from '@app/ui/components/item-layout/item-layout';
import { BasicTooltip } from '@app/ui/components/tooltip/basic-tooltip';
import { Pressable } from '@app/ui/pressable/pressable';

interface Stx20TokenAssetItemLayoutProps {
token: Stx20Token;
}
export function Stx20TokenAssetItemLayout({ token }: Stx20TokenAssetItemLayoutProps) {
const balanceAsString = token.balance.amount.toString();
const formattedBalance = formatBalance(balanceAsString);

return (
<Pressable my="space.02">
<ItemLayout
flagImg={
// TODO: Replace, need avatar for STX-20
<Avatar.Root>
<Avatar.Image alt="STX20" src="" />
<Avatar.Fallback>STX20</Avatar.Fallback>
</Avatar.Root>
}
titleLeft={token.ticker}
captionLeft="STX-20"
titleRight={
<BasicTooltip
asChild
label={formattedBalance.isAbbreviated ? balanceAsString : undefined}
side="left"
>
<styled.span data-testid={token.ticker} fontWeight={500} textStyle="label.02">
{formattedBalance.value}
</styled.span>
</BasicTooltip>
}
/>
</Pressable>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Stx20Token } from '@app/api/stacks/types/stx20-types';

import { Stx20TokenAssetItemLayout } from './stx20-token-asset-item.layout';

interface Stx20TokenAssetListProps {
stx20Tokens: Stx20Token[];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, for me it's clear enough from component name this is for stx20, so just "tokens" is good as a prop name?

}
export function Stx20TokenAssetList({ stx20Tokens }: Stx20TokenAssetListProps) {
return stx20Tokens.map((token, i) => (
<Stx20TokenAssetItemLayout key={`${token.ticker}${i}`} token={token} />
));
}
11 changes: 11 additions & 0 deletions src/app/components/loaders/stx20-tokens-loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Stx20Token } from '@app/api/stacks/types/stx20-types';
import { useStx20Tokens } from '@app/query/stacks/stx20/stx20-tokens.hooks';

interface Stx20TokensLoaderProps {
address: string;
children(runes: Stx20Token[]): React.ReactNode;
}
export function Stx20TokensLoader({ address, children }: Stx20TokensLoaderProps) {
const { data: stx20Tokens = [] } = useStx20Tokens(address);
return children(stx20Tokens);
}
27 changes: 22 additions & 5 deletions src/app/features/asset-list/asset-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,16 @@ import {
BitcoinTaprootAccountLoader,
} from '@app/components/account/bitcoin-account-loader';
import { BitcoinContractEntryPoint } from '@app/components/bitcoin-contract-entry-point/bitcoin-contract-entry-point';
import { Brc20TokenAssetList } from '@app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-list';
import { RunesAssetList } from '@app/components/crypto-assets/bitcoin/runes-asset-list/runes-asset-list';
import { Src20TokenAssetList } from '@app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-list';
import { CryptoCurrencyAssetItemLayout } from '@app/components/crypto-assets/crypto-currency-asset/crypto-currency-asset-item.layout';
import { Stx20TokenAssetList } from '@app/components/crypto-assets/stacks/stx20-token-asset-list/stx20-token-asset-list';
import { Brc20TokensLoader } from '@app/components/loaders/brc20-tokens-loader';
import { RunesLoader } from '@app/components/loaders/runes-loader';
import { Src20TokensLoader } from '@app/components/loaders/src20-tokens-loader';
import { CurrentStacksAccountLoader } from '@app/components/loaders/stacks-account-loader';
import { Stx20TokensLoader } from '@app/components/loaders/stx20-tokens-loader';
import { useHasBitcoinLedgerKeychain } from '@app/store/accounts/blockchain/bitcoin/bitcoin.ledger';
import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
Expand All @@ -20,7 +28,6 @@ import { BtcAvatarIcon } from '@app/ui/components/avatar/btc-avatar-icon';
import { Collectibles } from '../collectibles/collectibles';
import { PendingBrc20TransferList } from '../pending-brc-20-transfers/pending-brc-20-transfers';
import { AddStacksLedgerKeysItem } from './components/add-stacks-ledger-keys-item';
import { BitcoinFungibleTokenAssetList } from './components/bitcoin-fungible-tokens-asset-list';
import { ConnectLedgerAssetBtn } from './components/connect-ledger-asset-button';
import { StacksBalanceListItem } from './components/stacks-balance-list-item';
import { StacksFungibleTokenAssetList } from './components/stacks-fungible-token-asset-list';
Expand Down Expand Up @@ -74,6 +81,9 @@ export function AssetsList() {
<>
<StacksBalanceListItem address={account.address} />
<StacksFungibleTokenAssetList address={account.address} />
<Stx20TokensLoader address={account.address}>
{stx20Tokens => <Stx20TokenAssetList stx20Tokens={stx20Tokens} />}
</Stx20TokensLoader>
</>
)}
</CurrentStacksAccountLoader>
Expand All @@ -82,10 +92,17 @@ export function AssetsList() {
{nativeSegwitAccount => (
<BitcoinTaprootAccountLoader current>
{taprootAccount => (
<BitcoinFungibleTokenAssetList
btcAddressNativeSegwit={nativeSegwitAccount.address}
btcAddressTaproot={taprootAccount.address}
/>
<>
<Brc20TokensLoader>
{brc20Tokens => <Brc20TokenAssetList brc20Tokens={brc20Tokens} />}
</Brc20TokensLoader>
<Src20TokensLoader address={nativeSegwitAccount.address}>
{src20Tokens => <Src20TokenAssetList src20Tokens={src20Tokens} />}
</Src20TokensLoader>
<RunesLoader addresses={[nativeSegwitAccount.address, taprootAccount.address]}>
{runes => <RunesAssetList runes={runes} />}
</RunesLoader>
</>
)}
</BitcoinTaprootAccountLoader>
)}
Expand Down

This file was deleted.

6 changes: 3 additions & 3 deletions src/app/query/bitcoin/bitcoin-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ interface RunesOutputsByAddressResponse {
data: RunesOutputsByAddress[];
}

class BestinslotApi {
class BestinSlotApi {
url = BESTINSLOT_API_BASE_URL_MAINNET;
testnetUrl = BESTINSLOT_API_BASE_URL_TESTNET;

Expand Down Expand Up @@ -405,13 +405,13 @@ export class BitcoinClient {
addressApi: AddressApi;
feeEstimatesApi: FeeEstimatesApi;
transactionsApi: TransactionsApi;
BestinslotApi: BestinslotApi;
bestinSlotApi: BestinSlotApi;

constructor(basePath: string) {
this.configuration = new Configuration(basePath);
this.addressApi = new AddressApi(this.configuration);
this.feeEstimatesApi = new FeeEstimatesApi(this.configuration);
this.transactionsApi = new TransactionsApi(this.configuration);
this.BestinslotApi = new BestinslotApi(this.configuration);
this.bestinSlotApi = new BestinSlotApi(this.configuration);
}
}
2 changes: 1 addition & 1 deletion src/app/query/bitcoin/ordinals/brc20/brc20-tokens.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { brc20TransferInitiated } from '@app/store/ordinals/ordinals.slice';
import type { Brc20Token } from '../../bitcoin-client';
import { useAverageBitcoinFeeRates } from '../../fees/fee-estimates.hooks';
import { useOrdinalsbotClient } from '../../ordinalsbot-client';
import { createBrc20TransferInscription, encodeBrc20TransferInscription } from './brc-20.utils';
import { createBrc20TransferInscription, encodeBrc20TransferInscription } from './brc20.utils';

// ts-unused-exports:disable-next-line
export function useBrc20FeatureFlag() {
Expand Down
4 changes: 2 additions & 2 deletions src/app/query/bitcoin/ordinals/brc20/brc20-tokens.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ export function useGetBrc20TokensQuery() {
}

const brc20TokensPromises = addressesData.map(async address => {
const brc20Tokens = await client.BestinslotApi.getBrc20Balances(address);
const brc20Tokens = await client.bestinSlotApi.getBrc20Balances(address);

const tickerPromises = await Promise.all(
brc20Tokens.data.map(token => {
return client.BestinslotApi.getBrc20TickerInfo(token.ticker);
return client.bestinSlotApi.getBrc20TickerInfo(token.ticker);
})
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { encodeBrc20TransferInscription } from './brc-20.utils';
import { encodeBrc20TransferInscription } from './brc20.utils';

describe(encodeBrc20TransferInscription.name, () => {
test('that it encodes the BRC-20 transfer correctly', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function useGetRunesOutputsByAddressQuery<T extends unknown = RunesOutput
return useQuery({
queryKey: ['runes-outputs-by-address', address],
queryFn: () =>
client.BestinslotApi.getRunesOutputsByAddress({
client.bestinSlotApi.getRunesOutputsByAddress({
address,
network: network.chain.bitcoin.bitcoinNetwork,
}),
Expand Down
2 changes: 1 addition & 1 deletion src/app/query/bitcoin/runes/runes-ticker-info.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function useGetRunesTickerInfoQuery(runeNames: string[]): UseQueryResult<
enabled: !!runeName && (network.chain.bitcoin.bitcoinNetwork === 'testnet' || runesEnabled),
queryKey: ['runes-ticker-info', runeName],
queryFn: () =>
client.BestinslotApi.getRunesTickerInfo(runeName, network.chain.bitcoin.bitcoinNetwork),
client.bestinSlotApi.getRunesTickerInfo(runeName, network.chain.bitcoin.bitcoinNetwork),
};
}),
});
Expand Down
2 changes: 1 addition & 1 deletion src/app/query/bitcoin/runes/runes-wallet-balances.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function useGetRunesWalletBalancesByAddressesQuery<T extends unknown = Ru
enabled: !!address && (network.chain.bitcoin.bitcoinNetwork === 'testnet' || runesEnabled),
queryKey: ['runes-wallet-balances', address],
queryFn: () =>
client.BestinslotApi.getRunesWalletBalances(
client.bestinSlotApi.getRunesWalletBalances(
address,
network.chain.bitcoin.bitcoinNetwork
),
Expand Down
4 changes: 2 additions & 2 deletions src/app/query/bitcoin/transaction/use-check-utxos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,15 @@ async function checkInscribedUtxosByBestinslot({
* @see https://docs.bestinslot.xyz/reference/api-reference/ordinals-and-brc-20-and-bitmap-v3-api-mainnet+testnet/inscriptions
*/
const inscriptionIdsList = await Promise.all(
txids.map(id => client.BestinslotApi.getInscriptionsByTransactionId(id))
txids.map(id => client.bestinSlotApi.getInscriptionsByTransactionId(id))
);

const inscriptionIds = inscriptionIdsList.flatMap(inscription =>
inscription.data.map(data => data.inscription_id)
);

const inscriptionsList = await Promise.all(
inscriptionIds.map(id => client.BestinslotApi.getInscriptionById(id))
inscriptionIds.map(id => client.bestinSlotApi.getInscriptionById(id))
);

const hasInscribedUtxos = inscriptionsList.some(resp => {
Expand Down
6 changes: 3 additions & 3 deletions src/app/query/stacks/balance/stacks-ft-balances.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ import {
createStacksFtCryptoAssetBalanceTypeWrapper,
} from './stacks-ft-balances.utils';
import { parseBalanceResponse } from './stx-balance.hooks';
import { useStacksAccountBalanceQuery } from './stx-balance.query';
import { useStacksAccountBalancesQuery } from './stx-balance.query';

export function useStacksCryptoCurrencyAssetBalance(address: string) {
return useStacksAccountBalanceQuery(address, {
return useStacksAccountBalancesQuery(address, {
select: resp =>
createStacksCryptoCurrencyAssetTypeWrapper(parseBalanceResponse(resp).stx.unlockedStx.amount),
});
}

function useStacksFungibleTokenAssetBalances(address: string) {
return useStacksAccountBalanceQuery(address, {
return useStacksAccountBalancesQuery(address, {
select: resp => convertFtBalancesToStacksFungibleTokenAssetBalanceType(resp.fungible_tokens),
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/query/stacks/balance/stacks-ft-balances.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function convertFtBalancesToStacksFungibleTokenAssetBalanceType(
return (
Object.entries(ftBalances)
.map(([key, value]) => {
const balance = new BigNumber(value.balance);
const balance = new BigNumber(value?.balance ?? 0);
return createStacksFtCryptoAssetBalanceTypeWrapper(balance, key);
})
// Assets users have traded will persist in the api response
Expand Down
Loading
Loading