From f5189c75fea3be276217ed3cd3cc43ea58db9a89 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-05-06.json | 10 + .../CU-86du7e4k8_2024-07-30-05-06.json | 10 + .../CU-86du7e4k8_2024-07-30-05-06.json | 10 + .../CU-86du7e4k8_2024-07-30-05-06.json | 10 + common/config/rush/pnpm-lock.yaml | 95 +++++ packages/blockchain-service/src/index.ts | 4 +- packages/blockchain-service/src/interfaces.ts | 11 + packages/bs-ethereum/src/BSEthereum.ts | 12 +- packages/bs-ethereum/src/BSEthereumHelper.ts | 32 +- .../src/BlockscoutNeoXBDSEthereum.ts | 356 ++++++++++++++++++ .../src/BlockscoutNeoXEDSEthereum.ts | 30 ++ .../BlockscoutNeoXBDSEthereum.spec.ts | 207 ++++++++++ packages/bs-ethereum/src/index.ts | 13 +- packages/bs-neo-legacy/src/index.ts | 8 +- packages/bs-neo3/src/index.ts | 16 +- rush.json | 2 +- 16 files changed, 782 insertions(+), 44 deletions(-) create mode 100644 common/changes/@cityofzion/blockchain-service/CU-86du7e4k8_2024-07-30-05-06.json create mode 100644 common/changes/@cityofzion/bs-ethereum/CU-86du7e4k8_2024-07-30-05-06.json create mode 100644 common/changes/@cityofzion/bs-neo-legacy/CU-86du7e4k8_2024-07-30-05-06.json create mode 100644 common/changes/@cityofzion/bs-neo3/CU-86du7e4k8_2024-07-30-05-06.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 diff --git a/common/changes/@cityofzion/blockchain-service/CU-86du7e4k8_2024-07-30-05-06.json b/common/changes/@cityofzion/blockchain-service/CU-86du7e4k8_2024-07-30-05-06.json new file mode 100644 index 0000000..147efa1 --- /dev/null +++ b/common/changes/@cityofzion/blockchain-service/CU-86du7e4k8_2024-07-30-05-06.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@cityofzion/blockchain-service", + "comment": "Implement new blokchain data service 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-05-06.json b/common/changes/@cityofzion/bs-ethereum/CU-86du7e4k8_2024-07-30-05-06.json new file mode 100644 index 0000000..36cfc70 --- /dev/null +++ b/common/changes/@cityofzion/bs-ethereum/CU-86du7e4k8_2024-07-30-05-06.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@cityofzion/bs-ethereum", + "comment": "Implement new blockchain data service 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-05-06.json b/common/changes/@cityofzion/bs-neo-legacy/CU-86du7e4k8_2024-07-30-05-06.json new file mode 100644 index 0000000..cc17cfb --- /dev/null +++ b/common/changes/@cityofzion/bs-neo-legacy/CU-86du7e4k8_2024-07-30-05-06.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@cityofzion/bs-neo-legacy", + "comment": "Implement new blockchain data service 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-05-06.json b/common/changes/@cityofzion/bs-neo3/CU-86du7e4k8_2024-07-30-05-06.json new file mode 100644 index 0000000..508a76e --- /dev/null +++ b/common/changes/@cityofzion/bs-neo3/CU-86du7e4k8_2024-07-30-05-06.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@cityofzion/bs-neo3", + "comment": "Implement new blockchain data service for neox using blockscout", + "type": "minor" + } + ], + "packageName": "@cityofzion/bs-neo3" +} \ No newline at end of file diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 81c01fd..cc272c8 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -281,6 +281,46 @@ importers: specifier: 4.9.5 version: 4.9.5 + ../../packages/bs-neox: + dependencies: + '@cityofzion/blockchain-service': + specifier: workspace:* + version: link:../blockchain-service + axios: + specifier: 1.5.1 + version: 1.5.1 + devDependencies: + '@cityofzion/neon-dappkit-types': + specifier: ~0.3.1 + version: 0.3.1 + '@types/jest': + specifier: 29.5.3 + version: 29.5.3 + '@typescript-eslint/eslint-plugin': + specifier: ^6.5.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@4.9.5) + '@typescript-eslint/parser': + specifier: ^6.5.0 + version: 6.21.0(eslint@8.57.0)(typescript@4.9.5) + dotenv: + specifier: 16.3.1 + version: 16.3.1 + eslint: + specifier: ^8.48.0 + version: 8.57.0 + jest: + specifier: ~29.7.0 + version: 29.7.0(@types/node@20.12.14)(ts-node@10.9.1) + ts-jest: + specifier: 29.1.1 + version: 29.1.1(@babel/core@7.24.7)(jest@29.7.0)(typescript@4.9.5) + ts-node: + specifier: 10.9.1 + version: 10.9.1(@types/node@20.12.14)(typescript@4.9.5) + typescript: + specifier: 4.9.5 + version: 4.9.5 + ../../packages/bs-react-native-decrypt: devDependencies: '@types/react': @@ -6474,6 +6514,27 @@ packages: - ts-node dev: true + /jest@29.7.0(@types/node@20.12.14)(ts-node@10.9.1): + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.1) + '@jest/types': 29.6.3 + import-local: 3.1.0 + jest-cli: 29.7.0(@types/node@20.12.14)(ts-node@10.9.1) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /jetifier@2.0.0: resolution: {integrity: sha512-J4Au9KuT74te+PCCCHKgAjyLlEa+2VyIAEPNCdE5aNkAJ6FAJcAqcdzEkSnzNksIa9NkGmC4tPiClk2e7tCJuQ==} hasBin: true @@ -8644,6 +8705,40 @@ packages: yargs-parser: 21.1.1 dev: true + /ts-jest@29.1.1(@babel/core@7.24.7)(jest@29.7.0)(typescript@4.9.5): + resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@babel/core': 7.24.7 + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0(@types/node@20.12.14)(ts-node@10.9.1) + jest-util: 29.7.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.6.2 + typescript: 4.9.5 + yargs-parser: 21.1.1 + dev: true + /ts-node@10.9.1(@types/node@20.12.14)(typescript@4.9.5): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: 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..de8cf50 100644 --- a/packages/blockchain-service/src/interfaces.ts +++ b/packages/blockchain-service/src/interfaces.ts @@ -120,16 +120,27 @@ export type ContractParameter = { name: string type: string } +export type NextPageParams = { + block_number: number + fee: string + hash: string + index: number + inserted_at: string + items_count: number + value: string +} export type TransactionsByAddressResponse = { totalCount?: number limit?: number nextCursor?: string transactions: TransactionResponse[] + nextPageParams?: NextPageParams | null } export type TransactionsByAddressParams = { address: string page?: number cursor?: string + nextPageParams?: NextPageParams | null } 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..cbc9c63 --- /dev/null +++ b/packages/bs-ethereum/src/BlockscoutNeoXBDSEthereum.ts @@ -0,0 +1,356 @@ +import { + BalanceResponse, + ContractMethod, + ContractResponse, + Network, + NextPageParams, + 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 BlockscoutTransactionByAddressResponse { + items: BlockscoutTransactionResponse[] + next_page_params?: NextPageParams +} + +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 +} + +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: (TransactionTransferAsset | TransactionTransferNft)[] = [] + + const hasNativeTokenBeingTransferred = data.value !== '0' + if (hasNativeTokenBeingTransferred) { + transfers.push({ + amount: ethers.utils.formatUnits(data.value, nativeToken.decimals), + from: data.from.hash, + to: data.to.hash, + type: 'token', + contractHash: nativeToken.hash, + }) + } + + const hasTokenTransfers = data.token_transfers && data.token_transfers.length > 0 + if (hasTokenTransfers) { + for (const tokenTransfer of data.token_transfers) { + switch (tokenTransfer.token.type) { + case '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, + }) + break + case 'ERC-721': + transfers.push({ + tokenId: tokenTransfer.total.token_id, + from: tokenTransfer.from.hash, + to: tokenTransfer.to.hash, + type: 'nft', + contractHash: tokenTransfer.token.address, + }) + break + default: + // Handle other token types if necessary + break + } + } + } + + 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` + ) + + const nativeToken = BSEthereumHelper.getNativeAsset(this._network) + + const transactions: TransactionResponse[] = [] + + const promises = data.items.map(async item => { + 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) { + switch (tokenTransfer.token.type) { + case '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, + }) + break + case 'ERC-721': + transfers.push({ + tokenId: tokenTransfer.total.token_id, + from: tokenTransfer.from.hash, + to: tokenTransfer.to.hash, + type: 'nft', + contractHash: tokenTransfer.token.address, + }) + break + default: + // Handle other token types if necessary + break + } + } + } + + 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, + }) + }) + + await Promise.allSettled(promises) + + return { + transactions, + nextPageParams: data.next_page_params, + } + } + + 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[] = [] + + ERC20_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..6723af0 --- /dev/null +++ b/packages/bs-ethereum/src/BlockscoutNeoXEDSEthereum.ts @@ -0,0 +1,30 @@ +import { + ExchangeDataService, + GetTokenPriceHistoryParams, + GetTokenPricesParams, + Network, + TokenPricesHistoryResponse, + TokenPricesResponse, +} from '@cityofzion/blockchain-service' +import { BSEthereumNetworkId } from './BSEthereumHelper' + +// TODO: It'll be made in another issue, it's not mapped yet +export class BlockscoutNeoXEDSEthereum implements ExchangeDataService { + readonly #network: Network + + constructor(network: Network) { + this.#network = network + } + + getTokenPrices(params: GetTokenPricesParams): Promise { + throw new Error('Method not implemented.') + } + + getTokenPriceHistory(params: GetTokenPriceHistoryParams): Promise { + throw new Error('Method not implemented.') + } + + getCurrencyRatio(currency: string): Promise { + throw new Error('Method not implemented.') + } +} 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..a609306 --- /dev/null +++ b/packages/bs-ethereum/src/__tests__/BlockscoutNeoXBDSEthereum.spec.ts @@ -0,0 +1,207 @@ +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')! + +// TODO: When all test are finished import the address 0xD81a8F3c3f8b006Ef1ae4a2Fd28699AD7E3e21C5 on NEON to test the interface +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) + }) + + // This test is being skipped because we don't have any address with more than 50 transactions yet using Dora blockscout endpoint in testnet + // Change the endpoint to see the difference: https://xt3scan.ngd.network/api/v2/addresses/0xdc0b6d0F38738a89BA9193B50fF4111030f0d329/transactions + it.skip('Should return transfer transactions by address with next page', async () => {}) + + // This test is being skipped because we don't have any smart contract deployed in testnet using Dora blockscout endpoint in testnet + // Change the endpoint to see the difference: https://xt3scan.ngd.network/api/v2/smart-contracts/0x8f9EaEe5c5df888aBA3c1Ab19689a0660d042c6d + it.skip('Should return contract info', async () => { + const contractHash = '0x8f9EaEe5c5df888aBA3c1Ab19689a0660d042c6d' + + const expectedContract = { + hash: contractHash, + name: 'FastSwitchboard', + methods: expect.arrayContaining([ + expect.objectContaining({ + name: expect.any(String), + parameters: expect.any(Array), + }), + ]), + } + + const blockscoutBDSNeoX = new BlockscoutNeoXBDSEthereum(network) + const contract = await blockscoutBDSNeoX.getContract(contractHash) + + expect(contract).toEqual(expectedContract) + }) + + 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/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/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/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' diff --git a/rush.json b/rush.json index b7f5b08..8c84d13 100644 --- a/rush.json +++ b/rush.json @@ -43,4 +43,4 @@ "shouldPublish": true } ] -} +} \ No newline at end of file