diff --git a/packages/graphql/database/1.sql b/packages/graphql/database/1.sql new file mode 100644 index 000000000..4fe7f2be3 --- /dev/null +++ b/packages/graphql/database/1.sql @@ -0,0 +1,12 @@ +create table indexer.assets_contracts ( + asset_id text not null, + contract_id text not null, + transaction_id text not null, + name text, + symbol text, + decimals integer, + error text, + primary key (asset_id, contract_id) +); +create index on indexer.assets_contracts (asset_id); +create index on indexer.assets_contracts (contract_id); diff --git a/packages/graphql/src/application/uc/IndexAsset.ts b/packages/graphql/src/application/uc/IndexAsset.ts new file mode 100644 index 000000000..ed6a7eb99 --- /dev/null +++ b/packages/graphql/src/application/uc/IndexAsset.ts @@ -0,0 +1,695 @@ +import { concat, hash } from 'fuels'; +import { Contract, Provider } from 'fuels'; +import { env } from '~/config'; +import { DatabaseConnection } from '~/infra/database/DatabaseConnection'; + +export default class IndexAsset { + provider?: Provider; + + async execute(transaction: any) { + if (!this.provider) { + this.provider = await Provider.create(env.get('FUEL_PROVIDER')); + } + const connection = DatabaseConnection.getInstance(); + if (transaction.status?.receipts) { + for (const receipt of transaction.status.receipts) { + if (receipt.receiptType === 'MINT' && receipt.id && receipt.subId) { + const assetId = hash(concat([receipt.id, receipt.subId])); + const asset = { + assetId, + contractId: receipt.id, + transactionId: transaction.id, + name: null, + symbol: null, + decimals: null, + error: null, + }; + const existingAssetContract = await connection.query( + 'select * from indexer.assets_contracts where asset_id = $1 and contract_id = $2', + [asset.assetId, asset.contractId], + ); + if (existingAssetContract.length > 0) { + continue; + } + try { + const contract = new Contract(receipt.id, abi, this.provider); + const outputName = await contract.functions + .name({ bits: assetId }) + .dryRun(); + asset.name = outputName.value; + const outputSymbol = await contract.functions + .symbol({ bits: assetId }) + .dryRun(); + asset.symbol = outputSymbol.value; + const outputDecimals = await contract.functions + .decimals({ bits: assetId }) + .dryRun(); + asset.decimals = outputDecimals.value; + } catch (e: any) { + asset.error = e.message; + } + try { + connection.query( + 'insert into indexer.assets_contracts (asset_id, contract_id, transaction_id, name, symbol, decimals, error) values ($1, $2, $3, $4, $5, $6, $7) on conflict do nothing', + [ + asset.assetId, + asset.contractId, + asset.transactionId, + asset.name, + asset.symbol, + asset.decimals, + asset.error, + ], + ); + } catch (_e: any) {} + } + } + } + } +} + +const abi = { + programType: 'contract', + specVersion: '1', + encodingVersion: '1', + concreteTypes: [ + { + type: '()', + concreteTypeId: + '2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d', + }, + { + type: 'b256', + concreteTypeId: + '7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b', + }, + { + type: 'enum std::identity::Identity', + concreteTypeId: + 'ab7cd04e05be58e3fc15d424c2c4a57f824a2a2d97d67252440a3925ebdc1335', + metadataTypeId: 0, + }, + { + type: 'enum std::option::Option', + concreteTypeId: + '7c06d929390a9aeeb8ffccf8173ac0d101a9976d99dda01cce74541a81e75ac0', + metadataTypeId: 1, + typeArguments: [ + '9a7f1d3e963c10e0a4ea70a8e20a4813d1dc5682e28f74cb102ae50d32f7f98c', + ], + }, + { + type: 'enum std::option::Option', + concreteTypeId: + 'd852149004cc9ec0bbe7dc4e37bffea1d41469b759512b6136f2e865a4c06e7d', + metadataTypeId: 1, + typeArguments: [ + '1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0', + ], + }, + { + type: 'enum std::option::Option', + concreteTypeId: + '2da102c46c7263beeed95818cd7bee801716ba8303dddafdcd0f6c9efda4a0f1', + metadataTypeId: 1, + typeArguments: [ + 'c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b', + ], + }, + { + type: 'str', + concreteTypeId: + '8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a', + }, + { + type: 'str[11]', + concreteTypeId: + '48e8455800b58e79d9db5ac584872b19d307a74a81dcad1d1f9ca34da17e1b31', + }, + { + type: 'str[6]', + concreteTypeId: + 'ed705f920eb2c423c81df912430030def10f03218f0a064bfab81b68de71ae21', + }, + { + type: 'struct std::asset_id::AssetId', + concreteTypeId: + 'c0710b6731b1dd59799cf6bef33eee3b3b04a2e40e80a0724090215bbf2ca974', + metadataTypeId: 5, + }, + { + type: 'struct std::string::String', + concreteTypeId: + '9a7f1d3e963c10e0a4ea70a8e20a4813d1dc5682e28f74cb102ae50d32f7f98c', + metadataTypeId: 9, + }, + { + type: 'u64', + concreteTypeId: + '1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0', + }, + { + type: 'u8', + concreteTypeId: + 'c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b', + }, + ], + metadataTypes: [ + { + type: 'enum std::identity::Identity', + metadataTypeId: 0, + components: [ + { + name: 'Address', + typeId: 4, + }, + { + name: 'ContractId', + typeId: 8, + }, + ], + }, + { + type: 'enum std::option::Option', + metadataTypeId: 1, + components: [ + { + name: 'None', + typeId: + '2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d', + }, + { + name: 'Some', + typeId: 2, + }, + ], + typeParameters: [2], + }, + { + type: 'generic T', + metadataTypeId: 2, + }, + { + type: 'raw untyped ptr', + metadataTypeId: 3, + }, + { + type: 'struct std::address::Address', + metadataTypeId: 4, + components: [ + { + name: 'bits', + typeId: + '7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b', + }, + ], + }, + { + type: 'struct std::asset_id::AssetId', + metadataTypeId: 5, + components: [ + { + name: 'bits', + typeId: + '7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b', + }, + ], + }, + { + type: 'struct std::bytes::Bytes', + metadataTypeId: 6, + components: [ + { + name: 'buf', + typeId: 7, + }, + { + name: 'len', + typeId: + '1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0', + }, + ], + }, + { + type: 'struct std::bytes::RawBytes', + metadataTypeId: 7, + components: [ + { + name: 'ptr', + typeId: 3, + }, + { + name: 'cap', + typeId: + '1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0', + }, + ], + }, + { + type: 'struct std::contract_id::ContractId', + metadataTypeId: 8, + components: [ + { + name: 'bits', + typeId: + '7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b', + }, + ], + }, + { + type: 'struct std::string::String', + metadataTypeId: 9, + components: [ + { + name: 'bytes', + typeId: 6, + }, + ], + }, + ], + functions: [ + { + inputs: [ + { + name: 'sub_id', + concreteTypeId: + '7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b', + }, + { + name: 'amount', + concreteTypeId: + '1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0', + }, + ], + name: 'burn', + output: + '2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d', + attributes: [ + { + name: 'doc-comment', + arguments: [ + ' Unconditionally burns assets sent with the default SubId.', + ], + }, + { + name: 'doc-comment', + arguments: [''], + }, + { + name: 'doc-comment', + arguments: [' # Arguments'], + }, + { + name: 'doc-comment', + arguments: [''], + }, + { + name: 'doc-comment', + arguments: [' * `sub_id`: [SubId] - The default SubId.'], + }, + { + name: 'doc-comment', + arguments: [' * `amount`: [u64] - The quantity of coins to burn.'], + }, + { + name: 'doc-comment', + arguments: [''], + }, + { + name: 'doc-comment', + arguments: [' # Number of Storage Accesses'], + }, + { + name: 'doc-comment', + arguments: [''], + }, + { + name: 'doc-comment', + arguments: [' * Reads: `1`'], + }, + { + name: 'doc-comment', + arguments: [' * Writes: `1`'], + }, + { + name: 'doc-comment', + arguments: [''], + }, + { + name: 'doc-comment', + arguments: [' # Reverts'], + }, + { + name: 'doc-comment', + arguments: [''], + }, + { + name: 'doc-comment', + arguments: [' * When the `sub_id` is not the default SubId.'], + }, + { + name: 'doc-comment', + arguments: [ + ' * When the transaction did not include at least `amount` coins.', + ], + }, + { + name: 'doc-comment', + arguments: [ + ' * When the transaction did not include the asset minted by this contract.', + ], + }, + { + name: 'doc-comment', + arguments: [''], + }, + { + name: 'doc-comment', + arguments: [' # Examples'], + }, + { + name: 'doc-comment', + arguments: [''], + }, + { + name: 'doc-comment', + arguments: [' ```sway'], + }, + { + name: 'doc-comment', + arguments: [' use src3::SRC3;'], + }, + { + name: 'doc-comment', + arguments: [' use std::constants::DEFAULT_SUB_ID;'], + }, + { + name: 'doc-comment', + arguments: [''], + }, + { + name: 'doc-comment', + arguments: [' fn foo(contract_id: ContractId, asset_id: AssetId) {'], + }, + { + name: 'doc-comment', + arguments: [' let contract_abi = abi(SRC3, contract_id);'], + }, + { + name: 'doc-comment', + arguments: [' contract_abi {'], + }, + { + name: 'doc-comment', + arguments: [' gas: 10000,'], + }, + { + name: 'doc-comment', + arguments: [' coins: 100,'], + }, + { + name: 'doc-comment', + arguments: [' asset_id: asset_id,'], + }, + { + name: 'doc-comment', + arguments: [' }.burn(DEFAULT_SUB_ID, 100);'], + }, + { + name: 'doc-comment', + arguments: [' }'], + }, + { + name: 'doc-comment', + arguments: [' ```'], + }, + { + name: 'payable', + arguments: [], + }, + { + name: 'storage', + arguments: ['read', 'write'], + }, + ], + }, + { + inputs: [ + { + name: 'recipient', + concreteTypeId: + 'ab7cd04e05be58e3fc15d424c2c4a57f824a2a2d97d67252440a3925ebdc1335', + }, + { + name: 'sub_id', + concreteTypeId: + '7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b', + }, + { + name: 'amount', + concreteTypeId: + '1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0', + }, + ], + name: 'mint', + output: + '2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d', + attributes: [ + { + name: 'doc-comment', + arguments: [ + ' Unconditionally mints new assets using the default SubId.', + ], + }, + { + name: 'doc-comment', + arguments: [''], + }, + { + name: 'doc-comment', + arguments: [' # Arguments'], + }, + { + name: 'doc-comment', + arguments: [''], + }, + { + name: 'doc-comment', + arguments: [ + ' * `recipient`: [Identity] - The user to which the newly minted asset is transferred to.', + ], + }, + { + name: 'doc-comment', + arguments: [' * `sub_id`: [SubId] - The default SubId.'], + }, + { + name: 'doc-comment', + arguments: [' * `amount`: [u64] - The quantity of coins to mint.'], + }, + { + name: 'doc-comment', + arguments: [''], + }, + { + name: 'doc-comment', + arguments: [' # Number of Storage Accesses'], + }, + { + name: 'doc-comment', + arguments: [''], + }, + { + name: 'doc-comment', + arguments: [' * Reads: `1`'], + }, + { + name: 'doc-comment', + arguments: [' * Writes: `1`'], + }, + { + name: 'doc-comment', + arguments: [''], + }, + { + name: 'doc-comment', + arguments: [' # Reverts'], + }, + { + name: 'doc-comment', + arguments: [''], + }, + { + name: 'doc-comment', + arguments: [' * When the `sub_id` is not the default SubId.'], + }, + { + name: 'doc-comment', + arguments: [''], + }, + { + name: 'doc-comment', + arguments: [' # Examples'], + }, + { + name: 'doc-comment', + arguments: [''], + }, + { + name: 'doc-comment', + arguments: [' ```sway'], + }, + { + name: 'doc-comment', + arguments: [' use src3::SRC3;'], + }, + { + name: 'doc-comment', + arguments: [' use std::constants::DEFAULT_SUB_ID;'], + }, + { + name: 'doc-comment', + arguments: [''], + }, + { + name: 'doc-comment', + arguments: [' fn foo(contract_id: ContractId) {'], + }, + { + name: 'doc-comment', + arguments: [' let contract_abi = abi(SRC3, contract);'], + }, + { + name: 'doc-comment', + arguments: [ + ' contract_abi.mint(Identity::ContractId(contract_id), DEFAULT_SUB_ID, 100);', + ], + }, + { + name: 'doc-comment', + arguments: [' }'], + }, + { + name: 'doc-comment', + arguments: [' ```'], + }, + { + name: 'storage', + arguments: ['read', 'write'], + }, + ], + }, + { + inputs: [ + { + name: 'asset', + concreteTypeId: + 'c0710b6731b1dd59799cf6bef33eee3b3b04a2e40e80a0724090215bbf2ca974', + }, + ], + name: 'decimals', + output: + '2da102c46c7263beeed95818cd7bee801716ba8303dddafdcd0f6c9efda4a0f1', + attributes: [ + { + name: 'storage', + arguments: ['read'], + }, + ], + }, + { + inputs: [ + { + name: 'asset', + concreteTypeId: + 'c0710b6731b1dd59799cf6bef33eee3b3b04a2e40e80a0724090215bbf2ca974', + }, + ], + name: 'name', + output: + '7c06d929390a9aeeb8ffccf8173ac0d101a9976d99dda01cce74541a81e75ac0', + attributes: [ + { + name: 'storage', + arguments: ['read'], + }, + ], + }, + { + inputs: [ + { + name: 'asset', + concreteTypeId: + 'c0710b6731b1dd59799cf6bef33eee3b3b04a2e40e80a0724090215bbf2ca974', + }, + ], + name: 'symbol', + output: + '7c06d929390a9aeeb8ffccf8173ac0d101a9976d99dda01cce74541a81e75ac0', + attributes: [ + { + name: 'storage', + arguments: ['read'], + }, + ], + }, + { + inputs: [], + name: 'total_assets', + output: + '1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0', + attributes: [ + { + name: 'storage', + arguments: ['read'], + }, + ], + }, + { + inputs: [ + { + name: 'asset', + concreteTypeId: + 'c0710b6731b1dd59799cf6bef33eee3b3b04a2e40e80a0724090215bbf2ca974', + }, + ], + name: 'total_supply', + output: + 'd852149004cc9ec0bbe7dc4e37bffea1d41469b759512b6136f2e865a4c06e7d', + attributes: [ + { + name: 'storage', + arguments: ['read'], + }, + ], + }, + ], + loggedTypes: [ + { + logId: '10098701174489624218', + concreteTypeId: + '8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a', + }, + ], + messagesTypes: [], + configurables: [ + { + name: 'DECIMALS', + concreteTypeId: + 'c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b', + offset: 15736, + }, + { + name: 'NAME', + concreteTypeId: + '48e8455800b58e79d9db5ac584872b19d307a74a81dcad1d1f9ca34da17e1b31', + offset: 15744, + }, + { + name: 'SYMBOL', + concreteTypeId: + 'ed705f920eb2c423c81df912430030def10f03218f0a064bfab81b68de71ae21', + offset: 15760, + }, + ], +}; diff --git a/packages/graphql/src/application/uc/NewAddBlockRange.ts b/packages/graphql/src/application/uc/NewAddBlockRange.ts index 93f7bd5cb..9a24dcfef 100644 --- a/packages/graphql/src/application/uc/NewAddBlockRange.ts +++ b/packages/graphql/src/application/uc/NewAddBlockRange.ts @@ -12,9 +12,11 @@ import { import Block from '~/infra/dao/Block'; import Transaction from '~/infra/dao/Transaction'; import { DatabaseConnection } from '~/infra/database/DatabaseConnection'; +import IndexAsset from './IndexAsset'; export default class NewAddBlockRange { async execute(input: Input) { + const indexAsset = new IndexAsset(); const { from, to } = input; logger.info(`🔗 Syncing blocks: #${from} - #${to}`); const blocksData = await this.getBlocks(from, to); @@ -65,6 +67,13 @@ export default class NewAddBlockRange { ], }); } + if (transaction.data?.status?.receipts) { + try { + await indexAsset.execute(transaction.data); + } catch (e: any) { + logger.error('Error fetching assets', e); + } + } if (transactionData.inputs) { for (const inputData of transactionData.inputs) { queries.push({ diff --git a/packages/graphql/src/graphql/GraphQLContext.ts b/packages/graphql/src/graphql/GraphQLContext.ts index 522295418..8b3f1a8d5 100644 --- a/packages/graphql/src/graphql/GraphQLContext.ts +++ b/packages/graphql/src/graphql/GraphQLContext.ts @@ -1,4 +1,3 @@ -import { GraphQLError } from 'graphql'; import { env } from '~/config'; import { logger } from '~/core/Logger'; import { ChainEntity } from '~/domain/Chain/ChainEntity'; @@ -8,26 +7,27 @@ import type { GQLChainInfo } from './generated/sdk-provider'; export type GraphQLContext = { chain: ChainEntity | null; client: GraphQLSDK; + isAuthenticated: boolean; }; export class GraphQLContextFactory { static async create(req: Request): Promise { logger.info('GraphQLContextFactory.create'); + let isAuthenticated = true; const secret = env.get('SERVER_API_KEY'); const bearer = `Bearer ${secret}`; const token = req.headers.get('x-api-key') || req.headers.get('Authorization'); if (!token || token !== bearer) { - logger.error('Authorization header is required'); - throw new GraphQLError('Authorization header is required'); + isAuthenticated = false; } const res = await client.sdk.chain(); const chainItem = res.data?.chain; if (!chainItem) { - return { client, chain: null }; + return { client, chain: null, isAuthenticated }; } const chain = ChainEntity.create(chainItem as GQLChainInfo); - return { client, chain }; + return { client, chain, isAuthenticated }; } } diff --git a/packages/graphql/src/graphql/generated/mocks.ts b/packages/graphql/src/graphql/generated/mocks.ts index da354ff40..21813f697 100644 --- a/packages/graphql/src/graphql/generated/mocks.ts +++ b/packages/graphql/src/graphql/generated/mocks.ts @@ -1,4 +1,38 @@ -import type { GQLBalance, GQLBalanceConnection, GQLBalanceEdge, GQLBalanceFilterInput, GQLBlock, GQLBlockConnection, GQLBlockEdge, GQLBlocksDashboard, GQLBlocksDashboardConnection, GQLBreakpoint, GQLChainInfo, GQLChangeOutput, GQLCoin, GQLCoinConnection, GQLCoinEdge, GQLCoinFilterInput, GQLCoinOutput, GQLConsensusParameters, GQLConsensusParametersPurpose, GQLContract, GQLContractBalance, GQLContractBalanceConnection, GQLContractBalanceEdge, GQLContractBalanceFilterInput, GQLContractConnection, GQLContractCreated, GQLContractOutput, GQLContractParameters, GQLDryRunFailureStatus, GQLDryRunSuccessStatus, GQLDryRunTransactionExecutionStatus, GQLEstimateGasPrice, GQLExcludeInput, GQLFailureStatus, GQLFeeParameters, GQLGasCosts, GQLGenesis, GQLGroupedInputCoin, GQLGroupedInputContract, GQLGroupedInputMessage, GQLGroupedOutputChanged, GQLGroupedOutputCoin, GQLGroupedOutputContractCreated, GQLHeader, GQLHeavyOperation, GQLInputCoin, GQLInputContract, GQLInputMessage, GQLLatestGasPrice, GQLLightOperation, GQLMerkleProof, GQLMessage, GQLMessageCoin, GQLMessageConnection, GQLMessageEdge, GQLMessageProof, GQLMessageStatus, GQLMutation, GQLNodeInfo, GQLOperation, GQLOperationReceipt, GQLOperationsFilterInput, GQLOutputBreakpoint, GQLPageInfo, GQLParsedTime, GQLPeerInfo, GQLPoAConsensus, GQLPolicies, GQLPredicateItem, GQLPredicateParameters, GQLProgramState, GQLQuery, GQLReceipt, GQLRelayedTransactionFailed, GQLRunResult, GQLScriptParameters, GQLSearchAccount, GQLSearchBlock, GQLSearchContract, GQLSearchResult, GQLSearchTransaction, GQLSpendQueryElementInput, GQLSqueezedOutStatus, GQLStateTransitionPurpose, GQLSubmittedStatus, GQLSubscription, GQLSuccessStatus, GQLTps, GQLTpsConnection, GQLTransaction, GQLTransactionConnection, GQLTransactionEdge, GQLTransactionGasCosts, GQLTxParameters, GQLUtxoItem, GQLVariableOutput, GQLBlockVersion, GQLConsensusParametersVersion, GQLContractParametersVersion, GQLFeeParametersVersion, GQLGasCostsVersion, GQLGroupedInputType, GQLGroupedOutputType, GQLHeaderVersion, GQLMessageState, GQLOperationType, GQLPredicateParametersVersion, GQLReceiptType, GQLReturnType, GQLRunState, GQLScriptParametersVersion, GQLTxParametersVersion } from './sdk'; +import type { GQLAsset, GQLAssetNetworkEthereum, GQLAssetNetworkFuel, GQLBalance, GQLBalanceConnection, GQLBalanceEdge, GQLBalanceFilterInput, GQLBlock, GQLBlockConnection, GQLBlockEdge, GQLBlocksDashboard, GQLBlocksDashboardConnection, GQLBreakpoint, GQLChainInfo, GQLChangeOutput, GQLCoin, GQLCoinConnection, GQLCoinEdge, GQLCoinFilterInput, GQLCoinOutput, GQLConsensusParameters, GQLConsensusParametersPurpose, GQLContract, GQLContractBalance, GQLContractBalanceConnection, GQLContractBalanceEdge, GQLContractBalanceFilterInput, GQLContractConnection, GQLContractCreated, GQLContractOutput, GQLContractParameters, GQLDryRunFailureStatus, GQLDryRunSuccessStatus, GQLDryRunTransactionExecutionStatus, GQLEstimateGasPrice, GQLExcludeInput, GQLFailureStatus, GQLFeeParameters, GQLGasCosts, GQLGenesis, GQLGroupedInputCoin, GQLGroupedInputContract, GQLGroupedInputMessage, GQLGroupedOutputChanged, GQLGroupedOutputCoin, GQLGroupedOutputContractCreated, GQLHeader, GQLHeavyOperation, GQLInputCoin, GQLInputContract, GQLInputMessage, GQLLatestGasPrice, GQLLightOperation, GQLMerkleProof, GQLMessage, GQLMessageCoin, GQLMessageConnection, GQLMessageEdge, GQLMessageProof, GQLMessageStatus, GQLMutation, GQLNodeInfo, GQLOperation, GQLOperationReceipt, GQLOperationsFilterInput, GQLOutputBreakpoint, GQLPageInfo, GQLParsedTime, GQLPeerInfo, GQLPoAConsensus, GQLPolicies, GQLPredicateItem, GQLPredicateParameters, GQLProgramState, GQLQuery, GQLReceipt, GQLRelayedTransactionFailed, GQLRunResult, GQLScriptParameters, GQLSearchAccount, GQLSearchBlock, GQLSearchContract, GQLSearchResult, GQLSearchTransaction, GQLSpendQueryElementInput, GQLSqueezedOutStatus, GQLStateTransitionPurpose, GQLSubmittedStatus, GQLSubscription, GQLSuccessStatus, GQLTps, GQLTpsConnection, GQLTransaction, GQLTransactionConnection, GQLTransactionEdge, GQLTransactionGasCosts, GQLTxParameters, GQLUtxoItem, GQLVariableOutput, GQLBlockVersion, GQLConsensusParametersVersion, GQLContractParametersVersion, GQLFeeParametersVersion, GQLGasCostsVersion, GQLGroupedInputType, GQLGroupedOutputType, GQLHeaderVersion, GQLMessageState, GQLOperationType, GQLPredicateParametersVersion, GQLReceiptType, GQLReturnType, GQLRunState, GQLScriptParametersVersion, GQLTxParametersVersion } from './sdk'; + +export const anAsset = (overrides?: Partial): { __typename: 'Asset' } & GQLAsset => { + return { + __typename: 'Asset', + assetId: overrides && overrides.hasOwnProperty('assetId') ? overrides.assetId! : 'laboriosam', + contractId: overrides && overrides.hasOwnProperty('contractId') ? overrides.contractId! : 'corrupti', + decimals: overrides && overrides.hasOwnProperty('decimals') ? overrides.decimals! : '0x4', + icon: overrides && overrides.hasOwnProperty('icon') ? overrides.icon! : 'explicabo', + name: overrides && overrides.hasOwnProperty('name') ? overrides.name! : 'dolorem', + networks: overrides && overrides.hasOwnProperty('networks') ? overrides.networks! : [anAssetNetworkEthereum()], + symbol: overrides && overrides.hasOwnProperty('symbol') ? overrides.symbol! : 'quaerat', + verified: overrides && overrides.hasOwnProperty('verified') ? overrides.verified! : false, + }; +}; + +export const anAssetNetworkEthereum = (overrides?: Partial): { __typename: 'AssetNetworkEthereum' } & GQLAssetNetworkEthereum => { + return { + __typename: 'AssetNetworkEthereum', + address: overrides && overrides.hasOwnProperty('address') ? overrides.address! : 'porro', + decimals: overrides && overrides.hasOwnProperty('decimals') ? overrides.decimals! : '0xf', + type: overrides && overrides.hasOwnProperty('type') ? overrides.type! : 'commodi', + }; +}; + +export const anAssetNetworkFuel = (overrides?: Partial): { __typename: 'AssetNetworkFuel' } & GQLAssetNetworkFuel => { + return { + __typename: 'AssetNetworkFuel', + assetId: overrides && overrides.hasOwnProperty('assetId') ? overrides.assetId! : 'placeat', + chainId: overrides && overrides.hasOwnProperty('chainId') ? overrides.chainId! : '0x8', + contractId: overrides && overrides.hasOwnProperty('contractId') ? overrides.contractId! : 'maiores', + decimals: overrides && overrides.hasOwnProperty('decimals') ? overrides.decimals! : '0x2', + type: overrides && overrides.hasOwnProperty('type') ? overrides.type! : 'minima', + }; +}; export const aBalance = (overrides?: Partial): { __typename: 'Balance' } & GQLBalance => { return { @@ -809,6 +843,7 @@ export const aProgramState = (overrides?: Partial): { __typenam export const aQuery = (overrides?: Partial): { __typename: 'Query' } & GQLQuery => { return { __typename: 'Query', + asset: overrides && overrides.hasOwnProperty('asset') ? overrides.asset! : anAsset(), balance: overrides && overrides.hasOwnProperty('balance') ? overrides.balance! : aBalance(), balances: overrides && overrides.hasOwnProperty('balances') ? overrides.balances! : aBalanceConnection(), block: overrides && overrides.hasOwnProperty('block') ? overrides.block! : aBlock(), diff --git a/packages/graphql/src/graphql/generated/sdk-provider.ts b/packages/graphql/src/graphql/generated/sdk-provider.ts index a0a28167f..c6e5e4bf8 100644 --- a/packages/graphql/src/graphql/generated/sdk-provider.ts +++ b/packages/graphql/src/graphql/generated/sdk-provider.ts @@ -35,6 +35,36 @@ export type Scalars = { UtxoId: { input: string; output: string; } }; +export type GQLAsset = { + __typename: 'Asset'; + assetId?: Maybe; + contractId?: Maybe; + decimals?: Maybe; + icon?: Maybe; + name?: Maybe; + networks?: Maybe>>; + symbol?: Maybe; + verified?: Maybe; +}; + +export type GQLAssetNetwork = GQLAssetNetworkEthereum | GQLAssetNetworkFuel; + +export type GQLAssetNetworkEthereum = { + __typename: 'AssetNetworkEthereum'; + address?: Maybe; + decimals?: Maybe; + type?: Maybe; +}; + +export type GQLAssetNetworkFuel = { + __typename: 'AssetNetworkFuel'; + assetId?: Maybe; + chainId?: Maybe; + contractId?: Maybe; + decimals?: Maybe; + type?: Maybe; +}; + export type GQLBalance = { __typename: 'Balance'; amount: Scalars['U64']['output']; @@ -936,6 +966,7 @@ export type GQLProgramState = { export type GQLQuery = { __typename: 'Query'; + asset?: Maybe; balance: GQLBalance; balances: GQLBalanceConnection; block?: Maybe; @@ -989,6 +1020,11 @@ export type GQLQuery = { }; +export type GQLQueryAssetArgs = { + assetId: Scalars['String']['input']; +}; + + export type GQLQueryBalanceArgs = { assetId: Scalars['AssetId']['input']; owner: Scalars['Address']['input']; diff --git a/packages/graphql/src/graphql/generated/sdk.ts b/packages/graphql/src/graphql/generated/sdk.ts index d863b5a64..50b7cda1a 100644 --- a/packages/graphql/src/graphql/generated/sdk.ts +++ b/packages/graphql/src/graphql/generated/sdk.ts @@ -35,6 +35,36 @@ export type Scalars = { UtxoId: { input: string; output: string; } }; +export type GQLAsset = { + __typename: 'Asset'; + assetId?: Maybe; + contractId?: Maybe; + decimals?: Maybe; + icon?: Maybe; + name?: Maybe; + networks?: Maybe>>; + symbol?: Maybe; + verified?: Maybe; +}; + +export type GQLAssetNetwork = GQLAssetNetworkEthereum | GQLAssetNetworkFuel; + +export type GQLAssetNetworkEthereum = { + __typename: 'AssetNetworkEthereum'; + address?: Maybe; + decimals?: Maybe; + type?: Maybe; +}; + +export type GQLAssetNetworkFuel = { + __typename: 'AssetNetworkFuel'; + assetId?: Maybe; + chainId?: Maybe; + contractId?: Maybe; + decimals?: Maybe; + type?: Maybe; +}; + export type GQLBalance = { __typename: 'Balance'; amount: Scalars['U64']['output']; @@ -936,6 +966,7 @@ export type GQLProgramState = { export type GQLQuery = { __typename: 'Query'; + asset?: Maybe; balance: GQLBalance; balances: GQLBalanceConnection; block?: Maybe; @@ -989,6 +1020,11 @@ export type GQLQuery = { }; +export type GQLQueryAssetArgs = { + assetId: Scalars['String']['input']; +}; + + export type GQLQueryBalanceArgs = { assetId: Scalars['AssetId']['input']; owner: Scalars['Address']['input']; @@ -1467,6 +1503,13 @@ export type GQLVariableOutput = { to: Scalars['Address']['output']; }; +export type GQLAssetQueryVariables = Exact<{ + assetId: Scalars['String']['input']; +}>; + + +export type GQLAssetQuery = { __typename: 'Query', asset?: { __typename: 'Asset', assetId?: string | null, contractId?: string | null, name?: string | null, symbol?: string | null, decimals?: string | null, icon?: string | null, verified?: boolean | null } | null }; + export type GQLBalanceItemFragment = { __typename: 'Balance', amount: string, assetId: string, owner: string, utxos?: Array<{ __typename: 'UtxoItem', amount: string, blockCreated?: string | null, txCreatedIdx?: string | null, utxoId: string } | null> | null }; export type GQLBalancesQueryVariables = Exact<{ @@ -2061,6 +2104,19 @@ ${TransactionReceiptFragmentDoc} ${TransactionStatusFragmentDoc} ${TransactionInputFragmentDoc} ${TransactionOutputFragmentDoc}`; +export const AssetDocument = gql` + query asset($assetId: String!) { + asset(assetId: $assetId) { + assetId + contractId + name + symbol + decimals + icon + verified + } +} + `; export const BalancesDocument = gql` query balances($after: String, $before: String, $filter: BalanceFilterInput!, $first: Int, $last: Int) { balances( @@ -3545,6 +3601,7 @@ export type SdkFunctionWrapper = (action: (requestHeaders?:Record action(); +const AssetDocumentString = print(AssetDocument); const BalancesDocumentString = print(BalancesDocument); const BlockDocumentString = print(BlockDocument); const BlocksDocumentString = print(BlocksDocument); @@ -3562,6 +3619,9 @@ const TransactionsByBlockIdDocumentString = print(TransactionsByBlockIdDocument) const TransactionsByOwnerDocumentString = print(TransactionsByOwnerDocument); export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = defaultWrapper) { return { + asset(variables: GQLAssetQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise<{ data: GQLAssetQuery; errors?: GraphQLError[]; extensions?: any; headers: Headers; status: number; }> { + return withWrapper((wrappedRequestHeaders) => client.rawRequest(AssetDocumentString, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'asset', 'query', variables); + }, balances(variables: GQLBalancesQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise<{ data: GQLBalancesQuery; errors?: GraphQLError[]; extensions?: any; headers: Headers; status: number; }> { return withWrapper((wrappedRequestHeaders) => client.rawRequest(BalancesDocumentString, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'balances', 'query', variables); }, diff --git a/packages/graphql/src/graphql/queries/sdk/asset.graphql b/packages/graphql/src/graphql/queries/sdk/asset.graphql new file mode 100644 index 000000000..d75d17c1f --- /dev/null +++ b/packages/graphql/src/graphql/queries/sdk/asset.graphql @@ -0,0 +1,11 @@ +query asset($assetId: String!){ + asset (assetId: $assetId) { + assetId + contractId + name + symbol + decimals + icon + verified + } +} diff --git a/packages/graphql/src/graphql/resolvers/PublicResolver.ts b/packages/graphql/src/graphql/resolvers/PublicResolver.ts new file mode 100644 index 000000000..d6be75910 --- /dev/null +++ b/packages/graphql/src/graphql/resolvers/PublicResolver.ts @@ -0,0 +1,59 @@ +import { Provider } from 'fuels'; +import { env } from '~/config'; +import AssetDAO from '~/infra/dao/AssetDAO'; + +type Params = { + asset: { assetId: string }; +}; + +export class PublicResolver { + static create() { + const resolvers = new PublicResolver(); + return { + Query: { + asset: resolvers.asset, + }, + }; + } + + async asset(_: any, _params: Params['asset']) { + const assetDAO = new AssetDAO(); + const provider = await Provider.create(env.get('FUEL_PROVIDER')); + const chainId = provider.getChainId(); + const response = await fetch( + 'https://verified-assets.fuel.network/assets.json', + ); + const verifiedAssets = await response.json(); + for (const verifiedAsset of verifiedAssets) { + for (const network of verifiedAsset.networks) { + if (network.type === 'fuel') { + network.__typename = 'AssetNetworkFuel'; + } + if (network.type === 'ethereum') { + network.__typename = 'AssetNetworkEthereum'; + } + } + } + for (const verifiedAsset of verifiedAssets) { + for (const network of verifiedAsset.networks) { + if ( + network.chainId === chainId && + network.assetId === _params.assetId + ) { + const asset = Object.assign(verifiedAsset, { + assetId: _params.assetId, + contractId: network.contractId, + decimals: network.decimals, + verified: true, + }); + return asset; + } + } + } + const asset = await assetDAO.getByAssetId(_params.assetId); + if (!asset) return; + return Object.assign(asset, { + verified: false, + }); + } +} diff --git a/packages/graphql/src/graphql/resolvers/getBlocksDashboard.graphql b/packages/graphql/src/graphql/resolvers/getBlocksDashboard.graphql deleted file mode 100644 index e1ad2f02e..000000000 --- a/packages/graphql/src/graphql/resolvers/getBlocksDashboard.graphql +++ /dev/null @@ -1,10 +0,0 @@ -query getBlocksDashboard{ - getBlocksDashboard{ - nodes { - timestamp - gasUsed - blockNo - producer - } - } -} \ No newline at end of file diff --git a/packages/graphql/src/graphql/resolvers/index.ts b/packages/graphql/src/graphql/resolvers/index.ts index 12f690f65..3823cb6f5 100644 --- a/packages/graphql/src/graphql/resolvers/index.ts +++ b/packages/graphql/src/graphql/resolvers/index.ts @@ -1,9 +1,11 @@ +import GraphQLAuth from '~/infra/auth/GraphQLAuth'; import { BalanceResolver } from './BalanceResolver'; import { BlockResolver } from './BlockResolver'; import { ChainResolver } from './ChainResolver'; import { ContractResolver } from './ContractResolver'; import { NodeResolver } from './NodeResolver'; import { PredicateResolver } from './PredicateResolver'; +import { PublicResolver } from './PublicResolver'; import { SearchResolver } from './SearchResolver'; import { TransactionResolver } from './TransactionResolver'; @@ -16,16 +18,21 @@ const predicateResolver = PredicateResolver.create(); const searchResolver = SearchResolver.create(); const transactionResolver = TransactionResolver.create(); +const publicResolver = PublicResolver.create(); + export const resolvers = { Query: { - ...balanceResolver.Query, - ...blockResolver.Query, - ...chainResolver.Query, - ...contractResolver.Query, - ...nodeResolver.Query, - ...predicateResolver.Query, - ...searchResolver.Query, - ...transactionResolver.Query, + ...GraphQLAuth.apply({ + ...balanceResolver.Query, + ...blockResolver.Query, + ...chainResolver.Query, + ...contractResolver.Query, + ...nodeResolver.Query, + ...predicateResolver.Query, + ...searchResolver.Query, + ...transactionResolver.Query, + }), + ...publicResolver.Query, }, Balance: balanceResolver.Balance, }; diff --git a/packages/graphql/src/graphql/schemas/explorer.graphql b/packages/graphql/src/graphql/schemas/explorer.graphql index efe1b21b4..07de5c6cf 100644 --- a/packages/graphql/src/graphql/schemas/explorer.graphql +++ b/packages/graphql/src/graphql/schemas/explorer.graphql @@ -225,6 +225,7 @@ type Query { predicate(address: String!): PredicateItem tps: TPSConnection! getBlocksDashboard: BlocksDashboardConnection! + asset (assetId: String!): Asset } type TPS { @@ -247,4 +248,33 @@ type BlocksDashboard { type BlocksDashboardConnection { nodes: [BlocksDashboard!]! -} \ No newline at end of file +} + +type Asset { + assetId: String + contractId: String + name: String + symbol: String + decimals: U64 + icon: String + verified: Boolean + networks: [AssetNetwork] +} + +union AssetNetwork = + | AssetNetworkFuel + | AssetNetworkEthereum + +type AssetNetworkFuel { + type: String + decimals: U64 + assetId: String + contractId: String + chainId: U64 +} + +type AssetNetworkEthereum { + type: String + decimals: U64 + address: String +} diff --git a/packages/graphql/src/infra/auth/GraphQLAuth.ts b/packages/graphql/src/infra/auth/GraphQLAuth.ts new file mode 100644 index 000000000..2e31a47d9 --- /dev/null +++ b/packages/graphql/src/infra/auth/GraphQLAuth.ts @@ -0,0 +1,20 @@ +import { GraphQLError } from 'graphql'; +import { GraphQLContext } from '~/graphql/GraphQLContext'; + +export default class GraphQLAuth { + static apply(resolvers: any) { + for (const resolver in resolvers) { + resolvers[resolver] = GraphQLAuth.auth(resolvers[resolver]); + } + return resolvers; + } + + static auth(fn: Function) { + return (_: any, params: any, context: GraphQLContext) => { + if (!context.isAuthenticated) { + throw new GraphQLError('Authorization header is required'); + } + return fn(_, params, context); + }; + } +} diff --git a/packages/graphql/src/infra/dao/AssetDAO.ts b/packages/graphql/src/infra/dao/AssetDAO.ts new file mode 100644 index 000000000..7817369a4 --- /dev/null +++ b/packages/graphql/src/infra/dao/AssetDAO.ts @@ -0,0 +1,28 @@ +import { DatabaseConnection } from '../database/DatabaseConnection'; + +export default class BlockDAO { + databaseConnection: DatabaseConnection; + + constructor() { + this.databaseConnection = DatabaseConnection.getInstance(); + } + + async getByAssetId(assetId: string) { + const assetData = ( + await this.databaseConnection.query( + ` + select * from indexer.assets_contracts where asset_id = $1 + `, + [assetId], + ) + )[0]; + if (!assetData) return; + return { + assetId: assetData.asset_id, + contractId: assetData.contract_id, + name: assetData.name, + symbol: assetData.symbol, + decimals: assetData.decimals, + }; + } +} diff --git a/packages/graphql/src/infra/server/App.ts b/packages/graphql/src/infra/server/App.ts index c0b1e71f1..ba619d3ea 100644 --- a/packages/graphql/src/infra/server/App.ts +++ b/packages/graphql/src/infra/server/App.ts @@ -1,6 +1,7 @@ import cors from 'cors'; import express, { Request, Response } from 'express'; import GetMetrics from '~/application/uc/GetMetrics'; +import { PublicResolver } from '~/graphql/resolvers/PublicResolver'; export class Server { setup() { @@ -18,6 +19,14 @@ export class Server { res.setHeader('content-type', 'text/plain'); res.send(lines.join('\n')); }); + + app.get('/assets/:assetId', async (_req: Request, res: Response) => { + const output = await PublicResolver.create().Query.asset(null, { + assetId: _req.params.assetId, + }); + res.json(output); + }); + return app; } diff --git a/packages/graphql/src/migrate-transactions.ts b/packages/graphql/src/migrate-transactions.ts index 013420b2d..d1c76298a 100644 --- a/packages/graphql/src/migrate-transactions.ts +++ b/packages/graphql/src/migrate-transactions.ts @@ -1,34 +1,31 @@ import { setTimeout } from 'node:timers/promises'; +import IndexAsset from './application/uc/IndexAsset'; import { DatabaseConnection } from './infra/database/DatabaseConnection'; const connection = DatabaseConnection.getInstance(); +const indexAsset = new IndexAsset(); (async () => { - let index = 0; + let index = parseInt(process.argv[2]) || 0; const page = 10000; - while (index < 8000000) { + while (index < 11000000) { const from = index === 0 ? 0 : index + 1; const to = index + page; await migrate(from, to); index += page; - await setTimeout(1000); + await setTimeout(100); } })(); async function migrate(from: number, to: number) { console.log('migrate', from, to); const transactions = await connection.query( - `select tx_hash, "accountIndex" from transactions where "accountIndex" <> '' and block_id between $1 and $2`, + 'select * from indexer.transactions where block_id between $1 and $2', [from, to], ); for (const transaction of transactions) { - const accountsHash = transaction.accountIndex.split('|'); - for (const accountHash of accountsHash) { - if (!accountHash) continue; - await connection.query( - 'insert into transactions_accounts (tx_hash, account_hash) values ($1, $2) on conflict do nothing', - [transaction.tx_hash, accountHash], - ); - } + try { + await indexAsset.execute(transaction.data); + } catch (_e: any) {} } }