From 336ec74e02dd8f8d9069db630276d4fe4c668fd1 Mon Sep 17 00:00:00 2001 From: Lopes Date: Tue, 30 Jul 2024 02:06:27 -0300 Subject: [PATCH] CU-86du7e4k8 BSEthereum - Implement new BlockchainDataService for NeoX using Blockscout --- .../CU-86du7e4k8_2024-07-30-19-30.json | 10 + .../CU-86du7e4k8_2024-07-30-19-30.json | 10 + .../CU-86du7e4k8_2024-07-30-19-30.json | 10 + .../CU-86du7e4k8_2024-07-30-19-30.json | 10 + .../blockchain-service/src/BSAggregator.ts | 4 +- packages/blockchain-service/src/index.ts | 4 +- packages/blockchain-service/src/interfaces.ts | 7 +- packages/bs-ethereum/src/BSEthereum.ts | 12 +- packages/bs-ethereum/src/BSEthereumHelper.ts | 32 +- .../src/BlockscoutNeoXBDSEthereum.ts | 336 ++++++++++++++++++ .../src/BlockscoutNeoXEDSEthereum.ts | 76 ++++ .../bs-ethereum/src/MoralisBDSEthereum.ts | 4 +- .../BlockscoutNeoXBDSEthereum.spec.ts | 180 ++++++++++ .../BlockscoutNeoXEDSEthereum.spec.ts | 53 +++ .../src/__tests__/MoralisBDSEthereum.spec.ts | 2 +- packages/bs-ethereum/src/index.ts | 13 +- .../bs-neo-legacy/src/DoraBDSNeoLegacy.ts | 9 +- packages/bs-neo-legacy/src/index.ts | 8 +- packages/bs-neo3/src/DoraBDSNeo3.ts | 12 +- .../bs-neo3/src/__tests__/BDSNeo3.spec.ts | 2 +- packages/bs-neo3/src/index.ts | 16 +- 21 files changed, 747 insertions(+), 63 deletions(-) create mode 100644 common/changes/@cityofzion/blockchain-service/CU-86du7e4k8_2024-07-30-19-30.json create mode 100644 common/changes/@cityofzion/bs-ethereum/CU-86du7e4k8_2024-07-30-19-30.json create mode 100644 common/changes/@cityofzion/bs-neo-legacy/CU-86du7e4k8_2024-07-30-19-30.json create mode 100644 common/changes/@cityofzion/bs-neo3/CU-86du7e4k8_2024-07-30-19-30.json create mode 100644 packages/bs-ethereum/src/BlockscoutNeoXBDSEthereum.ts create mode 100644 packages/bs-ethereum/src/BlockscoutNeoXEDSEthereum.ts create mode 100644 packages/bs-ethereum/src/__tests__/BlockscoutNeoXBDSEthereum.spec.ts create mode 100644 packages/bs-ethereum/src/__tests__/BlockscoutNeoXEDSEthereum.spec.ts diff --git a/common/changes/@cityofzion/blockchain-service/CU-86du7e4k8_2024-07-30-19-30.json b/common/changes/@cityofzion/blockchain-service/CU-86du7e4k8_2024-07-30-19-30.json new file mode 100644 index 0000000..82792bf --- /dev/null +++ b/common/changes/@cityofzion/blockchain-service/CU-86du7e4k8_2024-07-30-19-30.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@cityofzion/blockchain-service", + "comment": "Implement new BlockchainDataService for NeoX using Blockscout", + "type": "minor" + } + ], + "packageName": "@cityofzion/blockchain-service" +} \ No newline at end of file diff --git a/common/changes/@cityofzion/bs-ethereum/CU-86du7e4k8_2024-07-30-19-30.json b/common/changes/@cityofzion/bs-ethereum/CU-86du7e4k8_2024-07-30-19-30.json new file mode 100644 index 0000000..c4c8de4 --- /dev/null +++ b/common/changes/@cityofzion/bs-ethereum/CU-86du7e4k8_2024-07-30-19-30.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@cityofzion/bs-ethereum", + "comment": "Implement new BlockchainDataService for NeoX using Blockscout", + "type": "minor" + } + ], + "packageName": "@cityofzion/bs-ethereum" +} \ No newline at end of file diff --git a/common/changes/@cityofzion/bs-neo-legacy/CU-86du7e4k8_2024-07-30-19-30.json b/common/changes/@cityofzion/bs-neo-legacy/CU-86du7e4k8_2024-07-30-19-30.json new file mode 100644 index 0000000..efaad7d --- /dev/null +++ b/common/changes/@cityofzion/bs-neo-legacy/CU-86du7e4k8_2024-07-30-19-30.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@cityofzion/bs-neo-legacy", + "comment": "Implement new BlockchainDataService for NeoX using Blockscout", + "type": "minor" + } + ], + "packageName": "@cityofzion/bs-neo-legacy" +} \ No newline at end of file diff --git a/common/changes/@cityofzion/bs-neo3/CU-86du7e4k8_2024-07-30-19-30.json b/common/changes/@cityofzion/bs-neo3/CU-86du7e4k8_2024-07-30-19-30.json new file mode 100644 index 0000000..cb3058e --- /dev/null +++ b/common/changes/@cityofzion/bs-neo3/CU-86du7e4k8_2024-07-30-19-30.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@cityofzion/bs-neo3", + "comment": "Implement new BlockchainDataService for NeoX using Blockscout", + "type": "minor" + } + ], + "packageName": "@cityofzion/bs-neo3" +} \ No newline at end of file diff --git a/packages/blockchain-service/src/BSAggregator.ts b/packages/blockchain-service/src/BSAggregator.ts index df56301..e25df29 100644 --- a/packages/blockchain-service/src/BSAggregator.ts +++ b/packages/blockchain-service/src/BSAggregator.ts @@ -68,10 +68,10 @@ export class BSAggregator< if (index !== 0) { try { - const { totalCount } = await service.blockchainDataService.getTransactionsByAddress({ + const { transactions } = await service.blockchainDataService.getTransactionsByAddress({ address: generatedAccount.address, }) - if (!totalCount || totalCount <= 0) hasError = true + if (!transactions || transactions.length <= 0) hasError = true } catch { hasError = true } diff --git a/packages/blockchain-service/src/index.ts b/packages/blockchain-service/src/index.ts index 3f08102..02d3c8d 100644 --- a/packages/blockchain-service/src/index.ts +++ b/packages/blockchain-service/src/index.ts @@ -1,4 +1,4 @@ -export * from './interfaces' export * from './BSAggregator' -export * from './functions' export * from './CryptoCompareEDS' +export * from './functions' +export * from './interfaces' diff --git a/packages/blockchain-service/src/interfaces.ts b/packages/blockchain-service/src/interfaces.ts index a9a95ea..4aa1213 100644 --- a/packages/blockchain-service/src/interfaces.ts +++ b/packages/blockchain-service/src/interfaces.ts @@ -121,15 +121,12 @@ export type ContractParameter = { type: string } export type TransactionsByAddressResponse = { - totalCount?: number - limit?: number - nextCursor?: string transactions: TransactionResponse[] + nextPageParams?: any } export type TransactionsByAddressParams = { address: string - page?: number - cursor?: string + nextPageParams?: any } export type ContractMethod = { name: string diff --git a/packages/bs-ethereum/src/BSEthereum.ts b/packages/bs-ethereum/src/BSEthereum.ts index f31163d..74d5071 100644 --- a/packages/bs-ethereum/src/BSEthereum.ts +++ b/packages/bs-ethereum/src/BSEthereum.ts @@ -23,6 +23,8 @@ import Transport from '@ledgerhq/hw-transport' import { BSEthereumNetworkId, BSEthereumHelper } from './BSEthereumHelper' import { MoralisBDSEthereum } from './MoralisBDSEthereum' import { MoralisEDSEthereum } from './MoralisEDSEthereum' +import { BlockscoutNeoXBDSEthereum } from './BlockscoutNeoXBDSEthereum' +import { BlockscoutNeoXEDSEthereum } from './BlockscoutNeoXEDSEthereum' export class BSEthereum implements @@ -68,8 +70,14 @@ export class BSEthereum this.network = network - this.blockchainDataService = new MoralisBDSEthereum(network) - this.exchangeDataService = new MoralisEDSEthereum(network, this.blockchainDataService) + if (BlockscoutNeoXBDSEthereum.isSupported(network)) { + this.exchangeDataService = new BlockscoutNeoXEDSEthereum(network) + this.blockchainDataService = new BlockscoutNeoXBDSEthereum(network) + } else { + this.exchangeDataService = new MoralisEDSEthereum(network, this.blockchainDataService) + this.blockchainDataService = new MoralisBDSEthereum(network) + } + this.nftDataService = new GhostMarketNDSEthereum(network) } diff --git a/packages/bs-ethereum/src/BSEthereumHelper.ts b/packages/bs-ethereum/src/BSEthereumHelper.ts index e5b94ed..a6bd892 100644 --- a/packages/bs-ethereum/src/BSEthereumHelper.ts +++ b/packages/bs-ethereum/src/BSEthereumHelper.ts @@ -15,7 +15,8 @@ export type BSEthereumNetworkId = NetworkId< | '43114' | '59144' | '11155111' - | '12227331' + | '47763' + | '12227332' > export class BSEthereumHelper { @@ -40,7 +41,8 @@ export class BSEthereumHelper { '43114': 'AVAX', '59144': 'ETH', '11155111': 'ETH', - '12227331': 'GAS', + '47763': 'GAS', + '12227332': 'GAS', } static #RPC_LIST_BY_NETWORK_ID: Record = { @@ -132,21 +134,28 @@ export class BSEthereumHelper { 'https://1rpc.io/sepolia', 'https://eth-sepolia.api.onfinality.io/public', ], - '12227331': ['https://neoxseed1.ngd.network'], + '47763': ['https://mainnet-1.rpc.banelabs.org'], + '12227332': ['https://neoxt4seed1.ngd.network'], } static DERIVATION_PATH = "m/44'/60'/0'/0/?" static DEFAULT_PATH = "44'/60'/0'/0/0" - static NEOX_TESTNET_NETWORK_ID: BSEthereumNetworkId = '12227331' - static NEOX_NETWORK_IDS: BSEthereumNetworkId[] = [this.NEOX_TESTNET_NETWORK_ID] + static NEOX_TESTNET_NETWORK_ID: BSEthereumNetworkId = '12227332' + static NEOX_MAINNET_NETWORK_ID: BSEthereumNetworkId = '47763' + static NEOX_NETWORK_IDS: BSEthereumNetworkId[] = [this.NEOX_TESTNET_NETWORK_ID, this.NEOX_MAINNET_NETWORK_ID] static NEOX_TESTNET_NETWORK: Network = { id: this.NEOX_TESTNET_NETWORK_ID, name: 'NeoX Testnet', url: this.#RPC_LIST_BY_NETWORK_ID[this.NEOX_TESTNET_NETWORK_ID][0], } - static NEOX_NETWORKS: Network[] = [this.NEOX_TESTNET_NETWORK] + static NEOX_MAINNET_NETWORK: Network = { + id: this.NEOX_MAINNET_NETWORK_ID, + name: 'NeoX Mainnet', + url: this.#RPC_LIST_BY_NETWORK_ID[this.NEOX_MAINNET_NETWORK_ID][0], + } + static NEOX_NETWORKS: Network[] = [this.NEOX_TESTNET_NETWORK, this.NEOX_MAINNET_NETWORK] static MAINNET_NETWORK_IDS: BSEthereumNetworkId[] = [ '1', @@ -160,14 +169,9 @@ export class BSEthereumHelper { '42220', '43114', '59144', + this.NEOX_MAINNET_NETWORK_ID, ] - static TESTNET_NETWORK_IDS: BSEthereumNetworkId[] = [ - '1101', - '80002', - '11155111', - '12227331', - this.NEOX_TESTNET_NETWORK_ID, - ] + static TESTNET_NETWORK_IDS: BSEthereumNetworkId[] = ['1101', '80002', '11155111', this.NEOX_TESTNET_NETWORK_ID] static ALL_NETWORK_IDS: BSEthereumNetworkId[] = [...this.MAINNET_NETWORK_IDS, ...this.TESTNET_NETWORK_IDS] static MAINNET_NETWORKS: Network[] = [ @@ -226,6 +230,7 @@ export class BSEthereumHelper { name: 'Linea Mainnet', url: this.#RPC_LIST_BY_NETWORK_ID['59144'][0], }, + this.NEOX_MAINNET_NETWORK, ] static TESTNET_NETWORKS: Network[] = [ { @@ -251,6 +256,7 @@ export class BSEthereumHelper { static getNativeAsset(network: Network) { const symbol = this.getNativeSymbol(network) + return { ...this.#NATIVE_ASSET, symbol, name: symbol } } diff --git a/packages/bs-ethereum/src/BlockscoutNeoXBDSEthereum.ts b/packages/bs-ethereum/src/BlockscoutNeoXBDSEthereum.ts new file mode 100644 index 0000000..dafd17e --- /dev/null +++ b/packages/bs-ethereum/src/BlockscoutNeoXBDSEthereum.ts @@ -0,0 +1,336 @@ +import { + BalanceResponse, + ContractMethod, + ContractResponse, + Network, + Token, + TransactionResponse, + TransactionsByAddressParams, + TransactionsByAddressResponse, + TransactionTransferAsset, + TransactionTransferNft, +} from '@cityofzion/blockchain-service' +import axios from 'axios' +import { RpcBDSEthereum } from './RpcBDSEthereum' +import { BSEthereumHelper, BSEthereumNetworkId } from './BSEthereumHelper' +import { ethers } from 'ethers' +import { ERC20_ABI } from './assets/abis/ERC20' + +interface BlockscoutTransactionResponse { + fee: { + value: string + } + hash: string + block: number + timestamp: string + value: string + from: { + hash: string + } + to: { + hash: string + } + token_transfers: { + token: { + type: string + address: string + } + from: { + hash: string + } + to: { + hash: string + } + total: { + value: string + decimals: number + token_id: string + } + }[] +} + +interface NextPageParams { + block_number: number + fee: string + hash: string + index: number + inserted_at: string + items_count: number + value: string +} + +interface BlockscoutTransactionByAddressResponse { + items: BlockscoutTransactionResponse[] + next_page_params?: NextPageParams | null +} + +interface BlockscoutTokensResponse { + name: string + decimals: string + address: string + symbol: string +} + +interface BlockscoutBlocksResponse { + items: { + height: number + }[] +} + +interface BlockscoutBalanceResponse { + token: BlockscoutTokensResponse + token_id: string | null + value: string +} + +interface BlockscoutSmartContractResponse { + name: string + abi: typeof ERC20_ABI +} + +export class BlockscoutNeoXBDSEthereum extends RpcBDSEthereum { + static BASE_URL_BY_CHAIN_ID: Partial> = { + '12227332': 'http://ec2-34-201-150-73.compute-1.amazonaws.com/api/v2', + '47763': 'http://ec2-34-201-150-73.compute-1.amazonaws.com/api/v2', + } + + static isSupported(network: Network) { + return !!BlockscoutNeoXBDSEthereum.BASE_URL_BY_CHAIN_ID[network.id] + } + + static getClient(network: Network) { + const baseURL = BlockscoutNeoXBDSEthereum.BASE_URL_BY_CHAIN_ID[network.id] + + if (!baseURL) { + throw new Error('Unsupported network') + } + + return axios.create({ + baseURL, + }) + } + + constructor(network: Network) { + super(network) + } + + maxTimeToConfirmTransactionInMs: number = 1000 * 60 * 5 + + async getTransaction(txid: string): Promise { + if (!BlockscoutNeoXBDSEthereum.isSupported(this._network)) { + return super.getTransaction(txid) + } + + const client = BlockscoutNeoXBDSEthereum.getClient(this._network) + const { data } = await client.get(`/transactions/${txid}`) + + const nativeToken = BSEthereumHelper.getNativeAsset(this._network) + + const transfers = this.#parseTransfers(data, nativeToken) + + return { + block: data.block, + hash: data.hash, + fee: ethers.utils.formatUnits(data.fee.value, nativeToken.decimals), + time: new Date(data.timestamp).getTime(), + notifications: [], + transfers, + } + } + + async getTransactionsByAddress(params: TransactionsByAddressParams): Promise { + if (!BlockscoutNeoXBDSEthereum.isSupported(this._network)) { + return super.getTransactionsByAddress(params) + } + + const client = BlockscoutNeoXBDSEthereum.getClient(this._network) + const { data } = await client.get( + `/addresses/${params.address}/transactions`, + { + params: { + next_page_params: params.nextPageParams, + }, + } + ) + + const nativeToken = BSEthereumHelper.getNativeAsset(this._network) + + const transactions: TransactionResponse[] = [] + + data.items.forEach(item => { + const transfers = this.#parseTransfers(item, nativeToken) + + transactions.push({ + block: item.block, + hash: item.hash, + fee: ethers.utils.formatUnits(item.fee.value, nativeToken.decimals), + time: new Date(item.timestamp).getTime(), + notifications: [], + transfers, + }) + }) + + return { + transactions, + nextPageParams: data.next_page_params, + } + } + + #parseTransfers( + item: BlockscoutTransactionResponse, + nativeToken: { symbol: string; name: string; hash: string; decimals: number } + ) { + const transfers: (TransactionTransferAsset | TransactionTransferNft)[] = [] + + const hasNativeTokenBeingTransferred = item.value !== '0' + if (hasNativeTokenBeingTransferred) { + transfers.push({ + amount: ethers.utils.formatUnits(item.value, nativeToken.decimals), + from: item.from.hash, + to: item.to.hash, + type: 'token', + contractHash: nativeToken.hash, + }) + } + + const hasTokenTransfers = item.token_transfers && item.token_transfers.length > 0 + if (hasTokenTransfers) { + for (const tokenTransfer of item.token_transfers) { + if (tokenTransfer.token.type === 'ERC-20') { + transfers.push({ + amount: ethers.utils.formatUnits(tokenTransfer.total.value, tokenTransfer.total.decimals), + from: tokenTransfer.from.hash, + to: tokenTransfer.to.hash, + type: 'token', + contractHash: tokenTransfer.token.address, + }) + + continue + } + + if (tokenTransfer.token.type === 'ERC-721') { + transfers.push({ + tokenId: tokenTransfer.total.token_id, + from: tokenTransfer.from.hash, + to: tokenTransfer.to.hash, + type: 'nft', + contractHash: tokenTransfer.token.address, + }) + } + } + } + + return transfers + } + + async getContract(contractHash: string): Promise { + if (!BlockscoutNeoXBDSEthereum.isSupported(this._network)) { + return super.getContract(contractHash) + } + + try { + const client = BlockscoutNeoXBDSEthereum.getClient(this._network) + + const { data } = await client.get(`/smart-contracts/${contractHash}`) + const methods: ContractMethod[] = [] + + data.abi.forEach(abi => { + if (abi.type !== 'function') return + + const parameters = abi.inputs?.map(param => ({ + name: param.name, + type: param.type, + })) + + methods.push({ + name: abi.name ?? '', + parameters: parameters ?? [], + }) + }) + + return { + hash: contractHash, + name: data.name, + methods, + } + } catch (error) { + throw new Error('Contract not found or not supported') + } + } + + async getTokenInfo(tokenHash: string): Promise { + if (!BlockscoutNeoXBDSEthereum.isSupported(this._network)) { + return super.getTokenInfo(tokenHash) + } + + const nativeAsset = BSEthereumHelper.getNativeAsset(this._network) + + if (BSEthereumHelper.normalizeHash(nativeAsset.hash) === BSEthereumHelper.normalizeHash(tokenHash)) { + return nativeAsset + } + + if (this._tokenCache.has(tokenHash)) { + return this._tokenCache.get(tokenHash)! + } + + const client = BlockscoutNeoXBDSEthereum.getClient(this._network) + + const { data } = await client.get(`/tokens/${tokenHash}`) + + return { + decimals: parseInt(data.decimals), + hash: tokenHash, + name: data.name, + symbol: data.symbol, + } + } + + async getBalance(address: string): Promise { + if (!BlockscoutNeoXBDSEthereum.isSupported(this._network)) { + return super.getBalance(address) + } + + const client = BlockscoutNeoXBDSEthereum.getClient(this._network) + + const { data: nativeBalance } = await client.get<{ coin_balance: string }>(`/addresses/${address}`) + + const balances: BalanceResponse[] = [ + { + amount: ethers.utils.formatUnits(nativeBalance.coin_balance, 18), + token: BSEthereumHelper.getNativeAsset(this._network), + }, + ] + + const { data: erc20Balances } = await client.get( + `/addresses/${address}/token-balances` + ) + + erc20Balances.forEach(balance => { + const token: Token = { + decimals: parseInt(balance.token.decimals), + hash: balance.token.address, + name: balance.token.symbol, + symbol: balance.token.symbol, + } + + balances.push({ + amount: ethers.utils.formatUnits(balance.value, token.decimals), + token, + }) + }) + + return balances + } + + async getBlockHeight(): Promise { + if (!BlockscoutNeoXBDSEthereum.isSupported(this._network)) { + return super.getBlockHeight() + } + + const client = BlockscoutNeoXBDSEthereum.getClient(this._network) + + const { data } = await client.get('/blocks') + + return data.items[0].height + } +} diff --git a/packages/bs-ethereum/src/BlockscoutNeoXEDSEthereum.ts b/packages/bs-ethereum/src/BlockscoutNeoXEDSEthereum.ts new file mode 100644 index 0000000..e83133c --- /dev/null +++ b/packages/bs-ethereum/src/BlockscoutNeoXEDSEthereum.ts @@ -0,0 +1,76 @@ +import { + CryptoCompareEDS, + ExchangeDataService, + GetTokenPriceHistoryParams, + GetTokenPricesParams, + Network, + TokenPricesHistoryResponse, + TokenPricesResponse, +} from '@cityofzion/blockchain-service' +import { BSEthereumHelper, BSEthereumNetworkId } from './BSEthereumHelper' +import { BlockscoutNeoXBDSEthereum } from './BlockscoutNeoXBDSEthereum' + +interface BlockscoutTokenPriceResponse { + exchange_rate: string +} + +interface BlockscoutStatsResponse { + coin_price: string +} + +export class BlockscoutNeoXEDSEthereum extends CryptoCompareEDS implements ExchangeDataService { + readonly #network: Network + + constructor(network: Network) { + super() + this.#network = network + } + + async getTokenPrices(params: GetTokenPricesParams): Promise { + if (!BSEthereumHelper.isMainnet(this.#network)) throw new Error('Exchange is only available on mainnet') + if (!BlockscoutNeoXBDSEthereum.isSupported(this.#network)) { + throw new Error('Exchange is not supported on this network') + } + + const client = BlockscoutNeoXBDSEthereum.getClient(this.#network) + + const nativeToken = BSEthereumHelper.getNativeAsset(this.#network) + + const prices: TokenPricesResponse[] = [] + + const promises = params.tokens.map(async token => { + try { + if (BSEthereumHelper.normalizeHash(token.hash) !== BSEthereumHelper.normalizeHash(nativeToken.hash)) { + const { data } = await client.get(`/tokens/${token.hash}`) + + prices.push({ + token, + usdPrice: Number(data.exchange_rate), + }) + + return + } + + const { data } = await client.get(`/stats`) + + prices.push({ + token, + usdPrice: Number(data.coin_price), + }) + } catch { + prices.push({ + token, + usdPrice: 0, + }) + } + }) + + await Promise.allSettled(promises) + + return prices + } + + getTokenPriceHistory(_params: GetTokenPriceHistoryParams): Promise { + throw new Error('Blockscout does not support this feature') + } +} diff --git a/packages/bs-ethereum/src/MoralisBDSEthereum.ts b/packages/bs-ethereum/src/MoralisBDSEthereum.ts index 05e10ff..4e2d3fe 100644 --- a/packages/bs-ethereum/src/MoralisBDSEthereum.ts +++ b/packages/bs-ethereum/src/MoralisBDSEthereum.ts @@ -307,7 +307,7 @@ export class MoralisBDSEthereum extends RpcBDSEthereum { const { data } = await client.get(`/wallets/${params.address}/history`, { params: { limit: 15, - cursor: params.cursor, + cursor: params.nextPageParams, }, }) @@ -370,7 +370,7 @@ export class MoralisBDSEthereum extends RpcBDSEthereum { await Promise.allSettled(promises) return { - nextCursor: data.cursor, + nextPageParams: data.cursor, transactions, } } diff --git a/packages/bs-ethereum/src/__tests__/BlockscoutNeoXBDSEthereum.spec.ts b/packages/bs-ethereum/src/__tests__/BlockscoutNeoXBDSEthereum.spec.ts new file mode 100644 index 0000000..a1f83af --- /dev/null +++ b/packages/bs-ethereum/src/__tests__/BlockscoutNeoXBDSEthereum.spec.ts @@ -0,0 +1,180 @@ +import { + BalanceResponse, + TransactionResponse, + TransactionsByAddressResponse, + TransactionTransferAsset, + TransactionTransferNft, +} from '@cityofzion/blockchain-service' +import { BlockscoutNeoXBDSEthereum } from '../BlockscoutNeoXBDSEthereum' +import { BSEthereumHelper } from '../BSEthereumHelper' + +const network = BSEthereumHelper.TESTNET_NETWORKS.find(network => network.id === '12227332')! + +describe('BlockscoutNeoXBDSEthereum', () => { + it('Should return transaction details for native assets (GAS)', async () => { + const txId = '0xd699caf2873c4ec900767ca2c1f519a85321d90a9bb6c2440117627ddaed6905' + + const expectedTransfer: TransactionTransferAsset[] = [ + { + amount: '12.304193970691695151', + contractHash: '-', + from: '0xEEf3aA5b167081221aB0DB6999259973Fc502646', + to: '0x7553c37E4C2EF96a41AB11F2813972711D1b73F9', + type: 'token', + }, + ] + + const expectedResponse: TransactionResponse = { + block: 838903, + hash: txId, + notifications: [], + time: 1721962138000, + transfers: expectedTransfer, + fee: '0.004452', + } + + const blockscoutBDSNeoX = new BlockscoutNeoXBDSEthereum(network) + const transaction = await blockscoutBDSNeoX.getTransaction(txId) + + expect(transaction).toEqual(expectedResponse) + }) + + it('Should return transaction details for ERC-20 assets (Ethereum assets)', async () => { + const txId = '0x2dddef0da23c82fd317317b79e0e1d14efab1df8d079f47262b26c0b29afdb95' + + const expectedTransfer: TransactionTransferAsset[] = [ + { + amount: '3164.81', + contractHash: '0x42aF6A3533173eb1BC6A05d5ab3A5184612A038c', + from: '0xAa393A829CAC203a7216406041A4c6762bda2706', + to: '0xFfab316a48d30d0EB55052DAb01f706F61E87568', + type: 'token', + }, + ] + + const expectedResponse: TransactionResponse = { + block: 832605, + hash: txId, + notifications: [], + time: 1721897573000, + transfers: expectedTransfer, + fee: '0.016074688', + } + + const blockscoutBDSNeoX = new BlockscoutNeoXBDSEthereum(network) + const transaction = await blockscoutBDSNeoX.getTransaction(txId) + + expect(transaction).toEqual(expectedResponse) + }) + + it('Should return transaction details for ERC-721 assets (NFT)', async () => { + const txId = '0x62be9bf4155af9ec473a1fdb8ab4b91d42bd040f346576cd25d3d7b284a9a146' + + const expectedTransfer: (TransactionTransferAsset | TransactionTransferNft)[] = [ + { + amount: '0.001089', + contractHash: '-', + from: '0x7C08Bdb8413b5Ac3d97773c5a5ada76406D31d65', + to: '0xf180136DdC9e4F8c9b5A9FE59e2b1f07265C5D4D', + type: 'token', + }, + { + contractHash: '0xf180136DdC9e4F8c9b5A9FE59e2b1f07265C5D4D', + from: '0x0000000000000000000000000000000000000000', + to: '0x7C08Bdb8413b5Ac3d97773c5a5ada76406D31d65', + tokenId: '562', + type: 'nft', + }, + ] + + const expectedResponse: TransactionResponse = { + block: 837660, + hash: txId, + notifications: [], + time: 1721949394000, + transfers: expectedTransfer, + fee: '0.314152412', + } + + const blockscoutBDSNeoX = new BlockscoutNeoXBDSEthereum(network) + const transaction = await blockscoutBDSNeoX.getTransaction(txId) + + expect(transaction).toEqual(expectedResponse) + }) + + it('Should return transactions by address without next page', async () => { + const address = '0xdc0b6d0F38738a89BA9193B50fF4111030f0d329' + + const expectedResponse: TransactionsByAddressResponse = { + transactions: expect.arrayContaining([ + expect.objectContaining({ + block: expect.any(Number), + fee: expect.any(String), + hash: expect.any(String), + notifications: expect.any(Array), + time: expect.any(Number), + transfers: expect.any(Array), + }), + ]), + nextPageParams: null, + } + + const blockscoutBDSNeoX = new BlockscoutNeoXBDSEthereum(network) + const transaction = await blockscoutBDSNeoX.getTransactionsByAddress({ address }) + + expect(transaction).toEqual(expectedResponse) + }) + + it('Should return token info', async () => { + const tokenHash = '0x7de8952AeADA3fF7dA1377A5E14a29603f33d829' + + const expectedToken = { + decimals: 18, + hash: tokenHash, + name: 'USDT', + symbol: 'USDT', + } + + const blockscoutBDSNeoX = new BlockscoutNeoXBDSEthereum(network) + const token = await blockscoutBDSNeoX.getTokenInfo(tokenHash) + + expect(token).toEqual(expectedToken) + }) + + it('Should return balance', async () => { + const address = '0xdc0b6d0F38738a89BA9193B50fF4111030f0d329' + + const expectedBalance: BalanceResponse[] = [ + { + amount: expect.any(String), + token: { + decimals: 18, + hash: '-', + name: 'GAS', + symbol: 'GAS', + }, + }, + { + amount: expect.any(String), + token: { + decimals: 10, + hash: '0x42aF6A3533173eb1BC6A05d5ab3A5184612A038c', + name: 'IOTC', + symbol: 'IOTC', + }, + }, + ] + + const blockscoutBDSNeoX = new BlockscoutNeoXBDSEthereum(network) + const balance = await blockscoutBDSNeoX.getBalance(address) + + expect(balance).toEqual(expectedBalance) + }) + + it('Should return block height', async () => { + const blockscoutBDSNeoX = new BlockscoutNeoXBDSEthereum(network) + const blockHeight = await blockscoutBDSNeoX.getBlockHeight() + + expect(blockHeight).toBeGreaterThan(0) + }) +}) diff --git a/packages/bs-ethereum/src/__tests__/BlockscoutNeoXEDSEthereum.spec.ts b/packages/bs-ethereum/src/__tests__/BlockscoutNeoXEDSEthereum.spec.ts new file mode 100644 index 0000000..408167a --- /dev/null +++ b/packages/bs-ethereum/src/__tests__/BlockscoutNeoXEDSEthereum.spec.ts @@ -0,0 +1,53 @@ +import { GetTokenPricesParams } from '@cityofzion/blockchain-service' +import { BlockscoutNeoXEDSEthereum } from '../BlockscoutNeoXEDSEthereum' +import { BSEthereumHelper } from '../BSEthereumHelper' + +const network = BSEthereumHelper.MAINNET_NETWORKS.find(network => network.id === '47763')! + +describe('BlockscoutNeoXEDSEthereum', () => { + // This test is skipped because blockscout doesn't support token prices in testnet + it('Should return token prices', async () => { + const params: GetTokenPricesParams = { + tokens: [ + { + hash: '0x8095581030409afc716d5f35Ce5172e13d7bA316', + decimals: 18, + name: 'Wrapped GAS v10', + symbol: 'WGAS10', + }, + { + hash: '-', + decimals: 18, + name: 'GAS', + symbol: 'GAS', + }, + ], + } + + const response = await new BlockscoutNeoXEDSEthereum(network).getTokenPrices(params) + + expect(response).toEqual([ + { + token: { + hash: '-', + decimals: 18, + name: 'GAS', + symbol: 'GAS', + }, + usdPrice: 0, + }, + { + token: { + hash: '0x8095581030409afc716d5f35Ce5172e13d7bA316', + decimals: 18, + name: 'Wrapped GAS v10', + symbol: 'WGAS10', + }, + usdPrice: 0, + }, + ]) + }) + + // This test is skipped because blockscout doesn't support token price history yet + it.skip('Should return token price history', () => {}) +}) diff --git a/packages/bs-ethereum/src/__tests__/MoralisBDSEthereum.spec.ts b/packages/bs-ethereum/src/__tests__/MoralisBDSEthereum.spec.ts index 906a4b2..f329128 100644 --- a/packages/bs-ethereum/src/__tests__/MoralisBDSEthereum.spec.ts +++ b/packages/bs-ethereum/src/__tests__/MoralisBDSEthereum.spec.ts @@ -32,7 +32,7 @@ describe('MoralisBDSEthereum', () => { it('Should be able to get transactions of address - %s', async () => { const address = '0x82B5Cd984880C8A821429cFFf89f36D35BaeBE89' - const response = await moralisBDSEthereum.getTransactionsByAddress({ address: address, page: 1 }) + const response = await moralisBDSEthereum.getTransactionsByAddress({ address: address }) response.transactions.forEach(transaction => { expect(transaction).toEqual( diff --git a/packages/bs-ethereum/src/index.ts b/packages/bs-ethereum/src/index.ts index 894a992..210c220 100644 --- a/packages/bs-ethereum/src/index.ts +++ b/packages/bs-ethereum/src/index.ts @@ -1,13 +1,8 @@ export * from './BSEthereum' - -export * from './GhostMarketNDSEthereum' -export * from './RpcNDSEthereum' - export * from './BSEthereumHelper' - +export * from './EthersLedgerServiceEthereum' +export * from './GhostMarketNDSEthereum' export * from './MoralisBDSEthereum' -export * from './RpcBDSEthereum' - export * from './MoralisEDSEthereum' - -export * from './EthersLedgerServiceEthereum' +export * from './RpcBDSEthereum' +export * from './RpcNDSEthereum' diff --git a/packages/bs-neo-legacy/src/DoraBDSNeoLegacy.ts b/packages/bs-neo-legacy/src/DoraBDSNeoLegacy.ts index 9b457a8..6171a15 100644 --- a/packages/bs-neo-legacy/src/DoraBDSNeoLegacy.ts +++ b/packages/bs-neo-legacy/src/DoraBDSNeoLegacy.ts @@ -62,9 +62,9 @@ export class DoraBDSNeoLegacy implements BlockchainDataService, BDSClaimable { async getTransactionsByAddress({ address, - page = 1, + nextPageParams = 1, }: TransactionsByAddressParams): Promise { - const data = await api.NeoLegacyREST.getAddressAbstracts(address, page, this.#network.id) + const data = await api.NeoLegacyREST.getAddressAbstracts(address, nextPageParams, this.#network.id) const transactions = new Map() const promises = data.entries.map(async entry => { @@ -95,9 +95,10 @@ export class DoraBDSNeoLegacy implements BlockchainDataService, BDSClaimable { }) await Promise.all(promises) + const totalPages = Math.ceil(data.total_entries / data.page_size) + return { - totalCount: data.total_entries, - limit: data.page_size, + nextPageParams: nextPageParams < totalPages ? nextPageParams + 1 : undefined, transactions: Array.from(transactions.values()), } } diff --git a/packages/bs-neo-legacy/src/index.ts b/packages/bs-neo-legacy/src/index.ts index 5f3f8f1..2cd3cb9 100644 --- a/packages/bs-neo-legacy/src/index.ts +++ b/packages/bs-neo-legacy/src/index.ts @@ -1,9 +1,5 @@ export * from './BSNeoLegacy' - -export * from './DoraBDSNeoLegacy' - export * from './BSNeoLegacyHelper' - -export * from './NeoTubeESNeoLegacy' - export * from './CryptoCompareEDSNeoLegacy' +export * from './DoraBDSNeoLegacy' +export * from './NeoTubeESNeoLegacy' diff --git a/packages/bs-neo3/src/DoraBDSNeo3.ts b/packages/bs-neo3/src/DoraBDSNeo3.ts index b6b5430..cbd9668 100644 --- a/packages/bs-neo3/src/DoraBDSNeo3.ts +++ b/packages/bs-neo3/src/DoraBDSNeo3.ts @@ -49,13 +49,13 @@ export class DoraBDSNeo3 extends RPCBDSNeo3 { async getTransactionsByAddress({ address, - page = 1, + nextPageParams = 1, }: TransactionsByAddressParams): Promise { if (BSNeo3Helper.isCustomNet(this._network)) { - return await super.getTransactionsByAddress({ address, page }) + return await super.getTransactionsByAddress({ address, nextPageParams }) } - const data = await NeoRest.addressTXFull(address, page, this._network.id) + const data = await NeoRest.addressTXFull(address, nextPageParams, this._network.id) const promises = data.items.map(async (item): Promise => { const transferPromises: Promise[] = [] @@ -118,10 +118,12 @@ export class DoraBDSNeo3 extends RPCBDSNeo3 { const transactions = await Promise.all(promises) + const limit = 15 + const totalPages = Math.ceil(data.totalCount / limit) + return { - totalCount: data.totalCount, + nextPageParams: nextPageParams < totalPages ? nextPageParams + 1 : undefined, transactions, - limit: 15, } } diff --git a/packages/bs-neo3/src/__tests__/BDSNeo3.spec.ts b/packages/bs-neo3/src/__tests__/BDSNeo3.spec.ts index 6a1e7c3..b151918 100644 --- a/packages/bs-neo3/src/__tests__/BDSNeo3.spec.ts +++ b/packages/bs-neo3/src/__tests__/BDSNeo3.spec.ts @@ -36,7 +36,7 @@ describe('BDSNeo3', () => { 'Should be able to get transactions of address - %s', async (bdsNeo3: BlockchainDataService) => { const address = 'NPB3Cze4wki9J36nnrT45qmi6P52Bhfqph' - const response = await bdsNeo3.getTransactionsByAddress({ address, page: 1 }) + const response = await bdsNeo3.getTransactionsByAddress({ address, nextPageParams: 1 }) response.transactions.forEach(transaction => { expect(transaction).toEqual( expect.objectContaining({ diff --git a/packages/bs-neo3/src/index.ts b/packages/bs-neo3/src/index.ts index 21d17b5..3a19fe5 100644 --- a/packages/bs-neo3/src/index.ts +++ b/packages/bs-neo3/src/index.ts @@ -1,18 +1,12 @@ +export * from './flamingo-swap/FlamingoSwapControllerService' +export * from './flamingo-swap/FlamingoSwapHelper' +export * from './flamingo-swap/FlamingoSwapNeonDappKitInvocationBuilder' export * from './BSNeo3' export * from './BSNeo3Helper' - export * from './DoraBDSNeo3' -export * from './RpcBDSNeo3' - export * from './DoraESNeo3' - export * from './FlamingoEDSNeo3' - export * from './GhostMarketNDSNeo3' -export * from './RpcNDSNeo3' - export * from './NeonDappKitLedgerServiceNeo3' - -export * from './flamingo-swap/FlamingoSwapControllerService' -export * from './flamingo-swap/FlamingoSwapHelper' -export * from './flamingo-swap/FlamingoSwapNeonDappKitInvocationBuilder' +export * from './RpcBDSNeo3' +export * from './RpcNDSNeo3'