Skip to content

Commit

Permalink
feat: stacks history api (#216)
Browse files Browse the repository at this point in the history
* feat: Stacks history utils

* chore: backward compatible export

---------

Co-authored-by: Son Le <[email protected]>
Co-authored-by: fede erbes <[email protected]>
  • Loading branch information
3 people authored Oct 10, 2024
1 parent 170bbad commit f6253a9
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 182 deletions.
12 changes: 12 additions & 0 deletions api/stacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import {
standardPrincipalCV,
TupleCV,
tupleCV,
uintCV,
UIntCV,
} from '@stacks/transactions';
import axios from 'axios';
import BigNumber from 'bignumber.js';
import { API_TIMEOUT_MILLI } from '../constant';
import {
Account,
AccountAssetsListData,
AddressToBnsResponse,
CoinMetaData,
Expand All @@ -27,6 +29,7 @@ import {
DelegationInfo,
EsploraTransaction,
FungibleToken,
NftData,
NftEventsResponse,
NftsListData,
NonFungibleToken,
Expand All @@ -52,6 +55,7 @@ import {
parseStxTransactionData,
} from './helper';
import { MempoolFeePriorities } from '@stacks/stacks-blockchain-api-types';
import StacksApiProvider from './stacksApi';

// TODO: these methods needs to be refactored
// reference https://github.com/secretkeylabs/xverse-core/pull/217/files#r1298242728
Expand Down Expand Up @@ -428,6 +432,14 @@ export async function getStacksInfo(network: string) {
}
}

export const isNftOwnedByAccount = async (nft: NftData, account: Account, stackApi: StacksApiProvider) => {
const assetIdentifier = `${nft.collection_contract_id}::${nft.asset_id}`;
const value = cvToHex(uintCV(nft.token_id.toString()));
const history = await stackApi.getNftHistory(assetIdentifier, value, 1);

return history.results[0]?.recipient === account.stxAddress;
};

export async function fetchDelegationState(stxAddress: string, network: StacksNetwork): Promise<DelegationInfo> {
const poxContractAddress = 'SP000000000000000000002Q6VF78';
const poxContractName = 'pox-4';
Expand Down
202 changes: 20 additions & 182 deletions api/stacksApi/index.ts
Original file line number Diff line number Diff line change
@@ -1,186 +1,24 @@
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import { API_TIMEOUT_MILLI, HIRO_MAINNET_DEFAULT, HIRO_TESTNET_DEFAULT } from '../../constant';
import { StacksNetwork } from '../../types';
import {
AccountDataResponse,
AddressTransaction,
AddressTransactionEventListResponse,
AddressTransactionsV2ListResponse,
MempoolTransaction,
MempoolTransactionListResponse,
Transaction,
} from '@stacks/stacks-blockchain-api-types';
import BigNumber from 'bignumber.js';

export interface StacksApiProviderOptions {
network: StacksNetwork;
}

const LIMIT = 100;
const OFFSET = 0;

export class StacksApiProvider {
StacksApi: AxiosInstance;

_network: StacksNetwork;

constructor(options: StacksApiProviderOptions) {
const { network } = options;
let baseURL = network.coreApiUrl;

if (!baseURL) {
if (!network.isMainnet()) {
baseURL = HIRO_TESTNET_DEFAULT;
}
baseURL = HIRO_MAINNET_DEFAULT;
}
const axiosConfig: AxiosRequestConfig = { baseURL, timeout: API_TIMEOUT_MILLI };

this._network = network;
this.StacksApi = axios.create(axiosConfig);
import { StacksNetwork, StacksNetworkName } from '@stacks/network';
import { NetworkType } from '../../types';
import { StacksApiProvider } from './provider';

const clients: Partial<Record<StacksNetworkName, StacksApiProvider>> = {};
const stacksNetworksMap: Record<NetworkType, StacksNetworkName> = {
Mainnet: 'mainnet',
Testnet: 'testnet',
Signet: 'testnet',
};

export const getStacksApiClient = (networkType: NetworkType): StacksApiProvider => {
const networkName = stacksNetworksMap[networkType];

if (!clients[networkName]) {
const network = StacksNetwork.fromNameOrNetwork(networkName);
clients[networkName] = new StacksApiProvider({ network });
}

private async httpGet<T>(
url: string,
params: unknown = {},
reqConfig: Omit<AxiosRequestConfig, 'params'> = {},
): Promise<T> {
const response = await this.StacksApi.get<T>(url, { ...reqConfig, params });
return response.data;
}

private async httpPost<T>(url: string, data: unknown): Promise<T> {
const response = await this.StacksApi.post(url, data);
return response.data;
}

getAddressBalance = async (stxAddress: string) => {
const apiUrl = `/v2/accounts/${stxAddress}?proof=0`;
const response = await this.httpGet<AccountDataResponse>(apiUrl);
const availableBalance = new BigNumber(response.balance);
const lockedBalance = new BigNumber(response.locked);
return {
availableBalance,
lockedBalance,
totalBalance: availableBalance.plus(lockedBalance),
nonce: response.nonce,
};
};

getAddressTransactions = async ({
stxAddress,
offset,
limit,
}: {
stxAddress: string;
offset?: number;
limit?: number;
}): Promise<{
list: AddressTransaction[];
total: number;
}> => {
const apiUrl = `/extended/v2/address/${stxAddress}/transactions`;

const response = await this.httpGet<AddressTransactionsV2ListResponse>(apiUrl, {
params: {
limit,
offset,
},
});

return {
list: response.results,
total: response.total,
};
};

getAddressMempoolTransactions = async ({
stxAddress,
offset,
limit,
}: {
stxAddress: string;
offset: number;
limit: number;
}): Promise<{
list: MempoolTransaction[];
total: number;
}> => {
const apiUrl = `/extended/v1/tx/mempool?address=${stxAddress}`;

const response = await this.httpGet<MempoolTransactionListResponse>(apiUrl, {
params: {
limit: limit,
offset: offset,
},
});

return {
list: response.results,
total: response.total,
};
};

getTransactionEvents = async ({
txid,
stxAddress,
limit,
offset,
}: {
txid: string;
stxAddress: string;
limit: number;
offset: number;
}): Promise<AddressTransactionEventListResponse> => {
const apiUrl = `/extended/v2/addresses/${stxAddress}/transactions/${txid}/events`;
const response = await this.httpGet<AddressTransactionEventListResponse>(apiUrl, {
params: {
limit,
offset,
},
});
return response;
};

getAllTransactions = async ({
stxAddress,
offset = OFFSET,
limit = LIMIT,
}: {
stxAddress: string;
offset: number;
limit: number;
}): Promise<(AddressTransaction | MempoolTransaction)[]> => {
let allTransactions: (AddressTransaction | MempoolTransaction)[] = [];
let hasMore = true;

while (hasMore) {
const [transactionsWithTransfers, mempoolTransactions] = await Promise.all([
this.getAddressTransactions({ stxAddress, offset, limit }),
this.getAddressMempoolTransactions({ stxAddress, limit, offset }),
]);

const combinedTransactions = [...mempoolTransactions.list, ...transactionsWithTransfers.list];

allTransactions = [...allTransactions, ...combinedTransactions];

// Check if we received fewer transactions than the limit, indicating no more transactions
if (combinedTransactions.length < limit) {
hasMore = false;
} else {
offset += limit;
}
}

return allTransactions;
};

getTransaction = async (txid: string): Promise<Transaction> => {
const response = await this.httpGet<Transaction>(`/extended/v1/tx/${txid}`, {
method: 'GET',
});
return response;
};
}
return clients[networkName] as StacksApiProvider;
};

export default StacksApiProvider;
export * from './provider';
Loading

0 comments on commit f6253a9

Please sign in to comment.