├── config ├── plugin.js └── config.default.js ├── app ├── middleware │ ├── transaction.js │ ├── ratelimit.js │ ├── address.js │ ├── pagination.js │ ├── contract.js │ └── block-filter.js ├── io │ ├── middleware │ │ └── connection.js │ └── controller │ │ └── default.js ├── extend │ ├── application.js │ └── helper.js ├── schedule │ ├── update-price.js │ └── update-feerate.js ├── controller │ ├── qrc721.js │ ├── info.js │ ├── statistics.js │ ├── misc.js │ ├── transaction.js │ ├── qrc20.js │ ├── block.js │ ├── contract.js │ └── address.js ├── model │ ├── contract-tag.js │ ├── qrc20-statistics.js │ ├── contract-code.js │ ├── qrc721-token.js │ ├── witness.js │ ├── contract-spend.js │ ├── rich-list.js │ ├── block.js │ ├── qrc20-balance.js │ ├── transaction.js │ ├── evm-receipt-log.js │ ├── contract.js │ ├── balance-change.js │ ├── gas-refund.js │ ├── address.js │ ├── transaction-input.js │ ├── qrc721.js │ ├── transaction-output.js │ ├── header.js │ ├── qrc20.js │ └── evm-receipt.js ├── service │ ├── statistics.js │ ├── qrc721.js │ ├── info.js │ ├── misc.js │ ├── balance.js │ ├── block.js │ └── contract.js └── router.js ├── package.json ├── doc ├── blockchain.md ├── block.md ├── qrc20.md ├── transaction.md └── contract.md ├── .gitignore ├── agent.js ├── app.js ├── .eslintrc.json └── README.md /config/plugin.js: -------------------------------------------------------------------------------- 1 | exports.cors = { 2 | enable: true, 3 | package: 'egg-cors' 4 | } 5 | 6 | exports.io = { 7 | enable: true, 8 | package: 'egg-socket.io' 9 | } 10 | 11 | exports.redis = { 12 | enable: true, 13 | package: 'egg-redis' 14 | } 15 | 16 | exports.sequelize = { 17 | enable: true, 18 | package: 'egg-sequelize' 19 | } 20 | -------------------------------------------------------------------------------- /app/middleware/transaction.js: -------------------------------------------------------------------------------- 1 | module.exports = () => async function transaction(ctx, next) { 2 | const db = ctx.model 3 | ctx.state.transaction = await db.transaction() 4 | try { 5 | await next() 6 | await ctx.state.transaction.commit() 7 | } catch (err) { 8 | await ctx.state.transaction.rollback() 9 | throw err 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/io/middleware/connection.js: -------------------------------------------------------------------------------- 1 | module.exports = () => async function connection(ctx, next) { 2 | const {app, socket} = ctx 3 | let interval = setInterval(() => { 4 | if (app.blockchainInfo.tip) { 5 | socket.emit('block-height', app.blockchainInfo.tip.height) 6 | clearInterval(interval) 7 | } 8 | }, 0) 9 | await next() 10 | } 11 | -------------------------------------------------------------------------------- /app/middleware/ratelimit.js: -------------------------------------------------------------------------------- 1 | const ratelimit = require('koa-ratelimit') 2 | 3 | module.exports = (options, app) => ratelimit({ 4 | ...options, 5 | id: ctx => `${app.name}-${ctx.get('cf-conecting-ip') || ctx.get('x-forwarded-for') || ctx.ip}`, 6 | whitelist: options.whitelist && ( 7 | ctx => options.whitelist.includes(ctx.get('cf-connecting-ip') || ctx.get('x-forwarded-for') || ctx.ip) 8 | || options.whitelist.includes(ctx.get('application-id')) 9 | ) 10 | }) 11 | -------------------------------------------------------------------------------- /app/extend/application.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const CHAIN = Symbol('qtum.chain') 4 | 5 | module.exports = { 6 | get chain() { 7 | this[CHAIN] = this[CHAIN] || this.qtuminfo.lib.Chain.get(this.config.qtum.chain) 8 | return this[CHAIN] 9 | }, 10 | get qtuminfo() { 11 | return { 12 | lib: require(path.resolve(this.config.qtuminfo.path, 'lib')), 13 | rpc: require(path.resolve(this.config.qtuminfo.path, 'rpc')) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/io/controller/default.js: -------------------------------------------------------------------------------- 1 | const {Controller} = require('egg') 2 | 3 | class DefaultController extends Controller { 4 | async subscribe() { 5 | const {ctx} = this 6 | let rooms = ctx.args 7 | if (rooms.length) { 8 | ctx.socket.join(...rooms) 9 | } 10 | } 11 | 12 | async unsubscribe() { 13 | const {ctx} = this 14 | let rooms = ctx.args 15 | if (rooms.length) { 16 | ctx.socket.leave(...rooms) 17 | } 18 | } 19 | } 20 | 21 | module.exports = DefaultController 22 | -------------------------------------------------------------------------------- /app/schedule/update-price.js: -------------------------------------------------------------------------------- 1 | const {Subscription} = require('egg') 2 | 3 | class UpdatePriceSubscription extends Subscription { 4 | static get schedule() { 5 | return { 6 | cron: '0 * * * *', 7 | type: 'worker' 8 | } 9 | } 10 | 11 | async subscribe() { 12 | let price = await this.ctx.service.misc.getPrices() 13 | await this.app.redis.hset(this.app.name, 'qtum-price', JSON.stringify(price)) 14 | this.app.io.of('/').to('coin') 15 | .emit('qtum-price', price) 16 | } 17 | } 18 | 19 | module.exports = UpdatePriceSubscription 20 | -------------------------------------------------------------------------------- /app/schedule/update-feerate.js: -------------------------------------------------------------------------------- 1 | const {Subscription} = require('egg') 2 | 3 | class UpdateFeerateSubscription extends Subscription { 4 | static get schedule() { 5 | return { 6 | cron: '0 * * * *', 7 | type: 'worker' 8 | } 9 | } 10 | 11 | async subscribe() { 12 | let feeRate = await this.ctx.service.info.getFeeRates() 13 | if (feeRate) { 14 | await this.app.redis.hset(this.app.name, 'feerate', JSON.stringify(feeRate)) 15 | this.app.io.of('/').to('blockchain') 16 | .emit('feerate', feeRate.find(item => item.blocks === 10).feeRate) 17 | } 18 | } 19 | } 20 | 21 | module.exports = UpdateFeerateSubscription 22 | -------------------------------------------------------------------------------- /app/controller/qrc721.js: -------------------------------------------------------------------------------- 1 | const {Controller} = require('egg') 2 | 3 | class QRC721Controller extends Controller { 4 | async list() { 5 | const {ctx} = this 6 | let {totalCount, tokens} = await ctx.service.qrc721.listQRC721Tokens() 7 | ctx.body = { 8 | totalCount, 9 | tokens: tokens.map(item => ({ 10 | address: item.addressHex.toString('hex'), 11 | addressHex: item.addressHex.toString('hex'), 12 | name: item.name, 13 | symbol: item.symbol, 14 | totalSupply: item.totalSupply.toString(), 15 | holders: item.holders 16 | })) 17 | } 18 | } 19 | } 20 | 21 | module.exports = QRC721Controller 22 | -------------------------------------------------------------------------------- /app/model/contract-tag.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const {BIGINT, CHAR, STRING} = app.Sequelize 3 | 4 | let ContractTag = app.model.define('contract_tag', { 5 | _id: { 6 | type: BIGINT.UNSIGNED, 7 | field: '_id', 8 | primaryKey: true 9 | }, 10 | contractAddress: CHAR(20).BINARY, 11 | tag: STRING(32) 12 | }, {freezeTableName: true, underscored: true, timestamps: false}) 13 | 14 | ContractTag.associate = () => { 15 | const {Contract} = app.model 16 | Contract.hasMany(ContractTag, {as: 'tags', foreignKey: 'contractAddress'}) 17 | ContractTag.belongsTo(Contract, {as: 'contract', foreignKey: 'contractAddress'}) 18 | } 19 | 20 | return ContractTag 21 | } 22 | -------------------------------------------------------------------------------- /app/model/qrc20-statistics.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const {INTEGER, CHAR} = app.Sequelize 3 | 4 | let QRC20Statistics = app.model.define('qrc20_statistics', { 5 | contractAddress: { 6 | type: CHAR(20).BINARY, 7 | primaryKey: true 8 | }, 9 | holders: INTEGER.UNSIGNED, 10 | transactions: INTEGER.UNSIGNED 11 | }, {freezeTableName: true, underscored: true, timestamps: false}) 12 | 13 | QRC20Statistics.associate = () => { 14 | const {Qrc20: QRC20} = app.model 15 | QRC20Statistics.belongsTo(QRC20, {as: 'qrc20', foreignKey: 'contractAddress'}) 16 | QRC20.hasOne(QRC20Statistics, {as: 'statistics', foreignKey: 'contractAddress'}) 17 | } 18 | 19 | return QRC20Statistics 20 | } 21 | -------------------------------------------------------------------------------- /app/model/contract-code.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const {CHAR, BLOB, TEXT} = app.Sequelize 3 | 4 | let ContractCode = app.model.define('contract_code', { 5 | contractAddress: { 6 | type: CHAR(20).BINARY, 7 | primaryKey: true 8 | }, 9 | code: BLOB, 10 | source: { 11 | type: TEXT('long'), 12 | allowNull: true 13 | } 14 | }, {freezeTableName: true, underscored: true, timestamps: false}) 15 | 16 | ContractCode.associate = () => { 17 | const {Contract} = app.model 18 | Contract.hasOne(ContractCode, {as: 'code', foreignKey: 'contractAddress'}) 19 | ContractCode.belongsTo(Contract, {as: 'contract', foreignKey: 'contractAddress'}) 20 | } 21 | 22 | return ContractCode 23 | } 24 | -------------------------------------------------------------------------------- /app/model/qrc721-token.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const {CHAR} = app.Sequelize 3 | 4 | let QRC721Token = app.model.define('qrc721_token', { 5 | contractAddress: { 6 | type: CHAR(20).BINARY, 7 | primaryKey: true 8 | }, 9 | tokenId: { 10 | type: CHAR(32).BINARY, 11 | primaryKey: true 12 | }, 13 | holder: CHAR(20).BINARY 14 | }, {freezeTableName: true, underscored: true, timestamps: false}) 15 | 16 | QRC721Token.associate = () => { 17 | const {Contract} = app.model 18 | Contract.hasMany(QRC721Token, {as: 'qrc721Tokens', foreignKey: 'contractAddress'}) 19 | QRC721Token.belongsTo(Contract, {as: 'contract', foreignKey: 'contractAddress'}) 20 | } 21 | 22 | return QRC721Token 23 | } 24 | -------------------------------------------------------------------------------- /app/controller/info.js: -------------------------------------------------------------------------------- 1 | const {Controller} = require('egg') 2 | 3 | class InfoController extends Controller { 4 | async index() { 5 | this.ctx.body = await this.ctx.service.info.getInfo() 6 | } 7 | 8 | async supply() { 9 | this.ctx.body = this.ctx.service.info.getTotalSupply() 10 | } 11 | 12 | async totalMaxSupply() { 13 | this.ctx.body = this.ctx.service.info.getTotalMaxSupply() 14 | } 15 | 16 | async circulatingSupply() { 17 | this.ctx.body = this.ctx.service.info.getCirculatingSupply() 18 | } 19 | 20 | async feeRates() { 21 | this.ctx.body = JSON.parse(await this.app.redis.hget(this.app.name, 'feerate')).filter(item => [2, 4, 6, 12, 24].includes(item.blocks)) 22 | } 23 | } 24 | 25 | module.exports = InfoController 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qtuminfo-api", 3 | "private": true, 4 | "scripts": { 5 | "start": "egg-scripts start --title=qtuminfo-api --sticky --daemon", 6 | "stop": "egg-scripts stop --title=qtuminfo-api", 7 | "dev": "egg-bin dev", 8 | "lint": "eslint --ext .js app config" 9 | }, 10 | "dependencies": { 11 | "egg": "^2.23.0", 12 | "egg-cors": "^2.2.0", 13 | "egg-redis": "^2.4.0", 14 | "egg-scripts": "^2.11.0", 15 | "egg-sequelize": "^5.2.0", 16 | "egg-socket.io": "^4.1.5", 17 | "ioredis": "^4.14.1", 18 | "koa-ratelimit": "^4.2.0", 19 | "mysql2": "^1.7.0", 20 | "socket.io-client": "^2.3.0" 21 | }, 22 | "devDependencies": { 23 | "babel-eslint": "^10.0.3", 24 | "egg-bin": "^4.13.1", 25 | "eslint": "^6.4.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/extend/helper.js: -------------------------------------------------------------------------------- 1 | function transformSQLArg(arg) { 2 | if (typeof arg === 'string') { 3 | return `'${arg}'` 4 | } else if (['number', 'bigint'].includes(typeof arg)) { 5 | return arg.toString() 6 | } else if (Buffer.isBuffer(arg)) { 7 | return `X'${arg.toString('hex')}'` 8 | } else if (Array.isArray(arg)) { 9 | return arg.length === 0 ? '(NULL)' : `(${arg.map(transformSQLArg).join(', ')})` 10 | } else if (arg && 'raw' in arg) { 11 | return arg.raw 12 | } 13 | return arg.toString() 14 | } 15 | 16 | exports.sql = function(strings, ...args) { 17 | let buffer = [] 18 | for (let i = 0; i < args.length; ++i) { 19 | buffer.push(strings[i].replace(/\s+/g, ' '), transformSQLArg(args[i])) 20 | } 21 | buffer.push(strings[args.length].replace(/\s+/g, ' ')) 22 | return buffer.join('') 23 | } 24 | -------------------------------------------------------------------------------- /app/model/witness.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const {INTEGER, CHAR, BLOB} = app.Sequelize 3 | 4 | let Witness = app.model.define('witness', { 5 | transactionId: { 6 | type: CHAR(32).BINARY, 7 | primaryKey: true 8 | }, 9 | inputIndex: { 10 | type: INTEGER.UNSIGNED, 11 | primaryKey: true 12 | }, 13 | witnessIndex: { 14 | type: INTEGER.UNSIGNED, 15 | primaryKey: true 16 | }, 17 | script: BLOB 18 | }, {freezeTableName: true, underscored: true, timestamps: false}) 19 | 20 | Witness.associate = () => { 21 | const {Transaction} = app.model 22 | Transaction.hasMany(Witness, {as: 'witnesses', foreignKey: 'transactionId', sourceKey: 'id'}) 23 | Witness.belongsTo(Transaction, {foreignKey: 'transactionId', targetKey: 'id'}) 24 | } 25 | 26 | return Witness 27 | } 28 | -------------------------------------------------------------------------------- /app/model/contract-spend.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const {BIGINT} = app.Sequelize 3 | 4 | let ContractSpend = app.model.define('contract_spend', { 5 | sourceId: { 6 | type: BIGINT.UNSIGNED, 7 | primaryKey: true 8 | }, 9 | destId: BIGINT.UNSIGNED 10 | }, {freezeTableName: true, underscored: true, timestamps: false}) 11 | 12 | ContractSpend.associate = () => { 13 | const {Transaction} = app.model 14 | Transaction.hasOne(ContractSpend, {as: 'contractSpendSource', foreignKey: 'sourceId'}) 15 | ContractSpend.belongsTo(Transaction, {as: 'sourceTransaction', foreignKey: 'sourceId'}) 16 | Transaction.hasMany(ContractSpend, {as: 'contractSpendDests', foreignKey: 'destId'}) 17 | ContractSpend.belongsTo(Transaction, {as: 'destTransaction', foreignKey: 'destId'}) 18 | } 19 | 20 | return ContractSpend 21 | } 22 | -------------------------------------------------------------------------------- /app/model/rich-list.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const {BIGINT} = app.Sequelize 3 | 4 | let RichList = app.model.define('rich_list', { 5 | addressId: { 6 | type: BIGINT.UNSIGNED, 7 | primaryKey: true 8 | }, 9 | balance: { 10 | type: BIGINT, 11 | get() { 12 | let balance = this.getDataValue('balance') 13 | return balance == null ? null : BigInt(balance) 14 | }, 15 | set(balance) { 16 | this.setDataValue('balance', balance.toString()) 17 | } 18 | } 19 | }, {freezeTableName: true, underscored: true, timestamps: false}) 20 | 21 | RichList.associate = () => { 22 | const {Address} = app.model 23 | Address.hasOne(RichList, {as: 'balance', foreignKey: 'addressId'}) 24 | RichList.belongsTo(Address, {as: 'address', foreignKey: 'addressId'}) 25 | } 26 | 27 | return RichList 28 | } 29 | -------------------------------------------------------------------------------- /doc/blockchain.md: -------------------------------------------------------------------------------- 1 | # Blockchain API 2 | 3 | - [Blockchain API](#Blockchain-API) 4 | - [Blockchain Information](#Blockchain-Information) 5 | - [Supply](#Supply) 6 | - [Total Max Supply](#Total-Max-Supply) 7 | 8 | 9 | ## Blockchain Information 10 | 11 | **Request** 12 | ``` 13 | GET /info 14 | ``` 15 | 16 | **Response** 17 | ```json 18 | { 19 | "height": 405961, 20 | "supply": 101603844, 21 | "circulatingSupply": 95853844, 22 | "netStakeWeight": 1095728543244388, 23 | "feeRate": 0.00401787, 24 | "dgpInfo": { 25 | "maxBlockSize": 2000000, 26 | "minGasPrice": 40, 27 | "blockGasLimit": 40000000 28 | } 29 | } 30 | ``` 31 | 32 | 33 | ## Supply 34 | 35 | **Request** 36 | ``` 37 | GET /supply 38 | ``` 39 | 40 | **Response** 41 | ```json 42 | 101603852 43 | ``` 44 | 45 | 46 | ## Total Max Supply 47 | 48 | **Request** 49 | ``` 50 | GET /total-max-supply 51 | ``` 52 | 53 | **Response** 54 | ```json 55 | 107822406.25 56 | ``` 57 | -------------------------------------------------------------------------------- /app/model/block.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const {INTEGER, BIGINT, CHAR} = app.Sequelize 3 | 4 | let Block = app.model.define('block', { 5 | hash: { 6 | type: CHAR(32).BINARY, 7 | unique: true 8 | }, 9 | height: { 10 | type: INTEGER.UNSIGNED, 11 | primaryKey: true 12 | }, 13 | size: INTEGER.UNSIGNED, 14 | weight: INTEGER.UNSIGNED, 15 | minerId: BIGINT.UNSIGNED, 16 | transactionsCount: INTEGER.UNSIGNED, 17 | contractTransactionsCount: INTEGER.UNSIGNED 18 | }, {freezeTableName: true, underscored: true, timestamps: false}) 19 | 20 | Block.associate = () => { 21 | const {Header, Address} = app.model 22 | Header.hasOne(Block, {as: 'block', foreignKey: 'height'}) 23 | Block.belongsTo(Header, {as: 'header', foreignKey: 'height'}) 24 | Address.hasOne(Block, {as: 'minedBlocks', foreignKey: 'minerId'}) 25 | Block.belongsTo(Address, {as: 'miner', foreignKey: 'minerId'}) 26 | } 27 | 28 | return Block 29 | } 30 | -------------------------------------------------------------------------------- /app/middleware/address.js: -------------------------------------------------------------------------------- 1 | module.exports = () => async function address(ctx, next) { 2 | ctx.assert(ctx.params.address, 404) 3 | const {Address: RawAddress} = ctx.app.qtuminfo.lib 4 | const chain = ctx.app.chain 5 | const {Address} = ctx.model 6 | const {in: $in} = ctx.app.Sequelize.Op 7 | 8 | let addresses = ctx.params.address.split(',') 9 | let rawAddresses = [] 10 | for (let address of addresses) { 11 | try { 12 | rawAddresses.push(RawAddress.fromString(address, chain)) 13 | } catch (err) { 14 | ctx.throw(400) 15 | } 16 | } 17 | let result = await Address.findAll({ 18 | where: {string: {[$in]: addresses}}, 19 | attributes: ['_id', 'type', 'data'], 20 | transaction: ctx.state.transaction 21 | }) 22 | ctx.state.address = { 23 | rawAddresses, 24 | addressIds: result.map(address => address._id), 25 | p2pkhAddressIds: result.filter(address => address.type === RawAddress.PAY_TO_PUBLIC_KEY_HASH).map(address => address._id), 26 | } 27 | await next() 28 | } 29 | -------------------------------------------------------------------------------- /app/model/qrc20-balance.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const {CHAR} = app.Sequelize 3 | 4 | let QRC20Balance = app.model.define('qrc20_balance', { 5 | contractAddress: { 6 | type: CHAR(20).BINARY, 7 | primaryKey: true 8 | }, 9 | address: { 10 | type: CHAR(20).BINARY, 11 | primaryKey: true 12 | }, 13 | balance: { 14 | type: CHAR(32).BINARY, 15 | get() { 16 | let balance = this.getDataValue('balance') 17 | return balance == null ? null : BigInt(`0x${balance.toString('hex')}`) 18 | }, 19 | set(balance) { 20 | this.setDataValue( 21 | 'balance', 22 | Buffer.from(balance.toString(16).padStart(64, '0'), 'hex') 23 | ) 24 | } 25 | } 26 | }, {freezeTableName: true, underscored: true, timestamps: false}) 27 | 28 | QRC20Balance.associate = () => { 29 | const {Contract} = app.model 30 | Contract.hasMany(QRC20Balance, {as: 'qrc20Balances', foreignKey: 'contractAddress'}) 31 | QRC20Balance.belongsTo(Contract, {as: 'contract', foreignKey: 'contractAddress'}) 32 | } 33 | 34 | return QRC20Balance 35 | } 36 | -------------------------------------------------------------------------------- /app/controller/statistics.js: -------------------------------------------------------------------------------- 1 | const {Controller} = require('egg') 2 | 3 | class StatisticsController extends Controller { 4 | async dailyTransactions() { 5 | const {app, ctx} = this 6 | let dailyTransactions = JSON.parse(await app.redis.hget(app.name, 'daily-transactions') || '[]') 7 | ctx.body = dailyTransactions.map(({timestamp, transactionsCount, contractTransactionsCount}) => ({ 8 | time: new Date(timestamp * 1000), 9 | transactionCount: transactionsCount, 10 | contractTransactionCount: contractTransactionsCount 11 | })) 12 | } 13 | 14 | async blockInterval() { 15 | const {app, ctx} = this 16 | let blockInterval = JSON.parse(await app.redis.hget(app.name, 'block-interval') || '[]') 17 | ctx.body = blockInterval 18 | } 19 | 20 | async addressGrowth() { 21 | const {app, ctx} = this 22 | let addressGrowth = JSON.parse(await app.redis.hget(app.name, 'address-growth') || '[]') 23 | ctx.body = addressGrowth.map(({timestamp, count}) => ({ 24 | time: new Date(timestamp * 1000), 25 | addresses: count 26 | })) 27 | } 28 | } 29 | 30 | module.exports = StatisticsController 31 | -------------------------------------------------------------------------------- /app/model/transaction.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const {INTEGER, BIGINT, CHAR} = app.Sequelize 3 | 4 | let Transaction = app.model.define('transaction', { 5 | _id: { 6 | type: BIGINT.UNSIGNED, 7 | field: '_id', 8 | primaryKey: true, 9 | autoIncrement: true 10 | }, 11 | id: { 12 | type: CHAR(32).BINARY, 13 | unique: true 14 | }, 15 | hash: CHAR(32).BINARY, 16 | version: INTEGER, 17 | flag: INTEGER(3).UNSIGNED, 18 | lockTime: INTEGER.UNSIGNED, 19 | blockHeight: INTEGER.UNSIGNED, 20 | indexInBlock: INTEGER.UNSIGNED, 21 | size: INTEGER.UNSIGNED, 22 | weight: INTEGER.UNSIGNED 23 | }, {freezeTableName: true, underscored: true, timestamps: false}) 24 | 25 | Transaction.associate = () => { 26 | const {Header, Block} = app.model 27 | Header.hasMany(Transaction, {as: 'transactions', foreignKey: 'blockHeight'}) 28 | Transaction.belongsTo(Header, {as: 'header', foreignKey: 'blockHeight'}) 29 | Block.hasMany(Transaction, {as: 'transactions', foreignKey: 'blockHeight'}) 30 | Transaction.belongsTo(Block, {as: 'block', foreignKey: 'blockHeight'}) 31 | } 32 | 33 | return Transaction 34 | } 35 | -------------------------------------------------------------------------------- /app/model/evm-receipt-log.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const {INTEGER, BIGINT, CHAR, STRING, BLOB} = app.Sequelize 3 | 4 | let EVMReceiptLog = app.model.define('evm_receipt_log', { 5 | _id: { 6 | type: BIGINT.UNSIGNED, 7 | field: '_id', 8 | primaryKey: true, 9 | autoIncrement: true 10 | }, 11 | receiptId: BIGINT.UNSIGNED, 12 | logIndex: INTEGER.UNSIGNED, 13 | blockHeight: INTEGER.UNSIGNED, 14 | address: CHAR(20).BINARY, 15 | topic1: { 16 | type: STRING(32).BINARY, 17 | allowNull: true 18 | }, 19 | topic2: { 20 | type: STRING(32).BINARY, 21 | allowNull: true 22 | }, 23 | topic3: { 24 | type: STRING(32).BINARY, 25 | allowNull: true 26 | }, 27 | topic4: { 28 | type: STRING(32).BINARY, 29 | allowNull: true 30 | }, 31 | data: BLOB 32 | }, {freezeTableName: true, underscored: true, timestamps: false}) 33 | 34 | EVMReceiptLog.associate = () => { 35 | const {EvmReceipt: EVMReceipt} = app.model 36 | EVMReceipt.hasMany(EVMReceiptLog, {as: 'logs', foreignKey: 'receiptId'}) 37 | EVMReceiptLog.belongsTo(EVMReceipt, {as: 'receipt', foreignKey: 'receiptId'}) 38 | } 39 | 40 | return EVMReceiptLog 41 | } 42 | -------------------------------------------------------------------------------- /app/model/contract.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const {CHAR, TEXT, ENUM} = app.Sequelize 3 | 4 | let Contract = app.model.define('contract', { 5 | address: { 6 | type: CHAR(20).BINARY, 7 | primaryKey: true 8 | }, 9 | addressString: CHAR(34), 10 | vm: { 11 | type: ENUM, 12 | values: ['evm', 'x86'] 13 | }, 14 | type: { 15 | type: ENUM, 16 | values: ['dgp', 'qrc20', 'qrc721'], 17 | allowNull: true 18 | }, 19 | description: { 20 | type: TEXT, 21 | defaultValue: '' 22 | } 23 | }, {freezeTableName: true, underscored: true, timestamps: false}) 24 | 25 | Contract.associate = () => { 26 | const {Address, EvmReceipt: EVMReceipt, EvmReceiptLog: EVMReceiptLog} = app.model 27 | Contract.hasOne(Address, {as: 'originalAddress', foreignKey: 'data'}) 28 | Address.belongsTo(Contract, {as: 'contract', foreignKey: 'data'}) 29 | EVMReceipt.belongsTo(Contract, {as: 'contract', foreignKey: 'contractAddress'}) 30 | Contract.hasMany(EVMReceipt, {as: 'evmReceipts', foreignKey: 'contractAddress'}) 31 | EVMReceiptLog.belongsTo(Contract, {as: 'contract', foreignKey: 'address'}) 32 | Contract.hasMany(EVMReceiptLog, {as: 'evmLogs', foreignKey: 'address'}) 33 | } 34 | 35 | return Contract 36 | } 37 | -------------------------------------------------------------------------------- /app/model/balance-change.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const {INTEGER, BIGINT} = app.Sequelize 3 | 4 | let BalanceChange = app.model.define('balance_change', { 5 | transactionId: { 6 | type: BIGINT.UNSIGNED, 7 | primaryKey: true 8 | }, 9 | blockHeight: INTEGER.UNSIGNED, 10 | indexInBlock: INTEGER.UNSIGNED, 11 | addressId: { 12 | type: BIGINT.UNSIGNED, 13 | primaryKey: true 14 | }, 15 | value: { 16 | type: BIGINT, 17 | get() { 18 | let value = this.getDataValue('value') 19 | return value == null ? null : BigInt(value) 20 | }, 21 | set(value) { 22 | this.setDataValue('value', value.toString()) 23 | } 24 | } 25 | }, {freezeTableName: true, underscored: true, timestamps: false}) 26 | 27 | BalanceChange.associate = () => { 28 | const {Header, Address, Transaction} = app.model 29 | Transaction.hasMany(BalanceChange, {as: 'balanceChanges', foreignKey: 'transactionId'}) 30 | BalanceChange.belongsTo(Transaction, {as: 'transaction', foreignKey: 'transactionId'}) 31 | Address.hasOne(BalanceChange, {as: 'balanceChanges', foreignKey: 'addressId'}) 32 | BalanceChange.belongsTo(Address, {as: 'address', foreignKey: 'addressId'}) 33 | BalanceChange.belongsTo(Header, {as: 'header', foreignKey: 'blockHeight'}) 34 | } 35 | 36 | return BalanceChange 37 | } 38 | -------------------------------------------------------------------------------- /app/controller/misc.js: -------------------------------------------------------------------------------- 1 | const {Controller} = require('egg') 2 | 3 | class MiscController extends Controller { 4 | async classify() { 5 | let {ctx} = this 6 | ctx.body = await ctx.service.misc.classify(ctx.query.query) 7 | } 8 | 9 | async richList() { 10 | let {ctx} = this 11 | let {totalCount, list} = await ctx.service.balance.getRichList() 12 | ctx.body = { 13 | totalCount, 14 | list: list.map(item => ({ 15 | address: item.addressHex ? item.addressHex.toString('hex') : item.address, 16 | addressHex: item.addressHex && item.addressHex.toString('hex'), 17 | balance: item.balance.toString() 18 | })) 19 | } 20 | } 21 | 22 | async biggestMiners() { 23 | let {ctx} = this 24 | let lastNBlocks = null 25 | if (ctx.query.blocks && /^[1-9]\d*$/.test(ctx.query.blocks)) { 26 | lastNBlocks = Number.parseInt(ctx.query.blocks) 27 | } 28 | let {totalCount, list} = await ctx.service.block.getBiggestMiners(lastNBlocks) 29 | ctx.body = { 30 | totalCount, 31 | list: list.map(item => ({ 32 | address: item.address, 33 | blocks: item.blocks, 34 | balance: item.balance.toString() 35 | })) 36 | } 37 | } 38 | 39 | async prices() { 40 | this.ctx.body = JSON.parse(await this.app.redis.hget(this.app.name, 'qtum-price')) 41 | } 42 | } 43 | 44 | module.exports = MiscController 45 | -------------------------------------------------------------------------------- /app/middleware/pagination.js: -------------------------------------------------------------------------------- 1 | module.exports = ({defaultPageSize = 100} = {}) => async function pagination(ctx, next) { 2 | if (!['GET', 'POST'].includes(ctx.method)) { 3 | return await next() 4 | } 5 | let object = {GET: ctx.query, POST: ctx.request.body}[ctx.method] 6 | let limit = defaultPageSize 7 | let offset = 0 8 | let reversed 9 | if ('limit' in object && 'offset' in object) { 10 | limit = Number.parseInt(object.limit) 11 | offset = Number.parseInt(object.offset) 12 | } 13 | if ('pageSize' in object && 'pageIndex' in object) { 14 | let pageSize = Number.parseInt(object.pageSize) 15 | let pageIndex = Number.parseInt(object.pageIndex) 16 | limit = pageSize 17 | offset = pageSize * pageIndex 18 | } 19 | if ('pageSize' in object && 'page' in object) { 20 | let pageSize = Number.parseInt(object.pageSize) 21 | let pageIndex = Number.parseInt(object.page) 22 | limit = pageSize 23 | offset = pageSize * pageIndex 24 | } 25 | if ('from' in object && 'to' in object) { 26 | let from = Number.parseInt(object.from) 27 | let to = Number.parseInt(object.to) 28 | limit = to - from + 1 29 | offset = from 30 | } 31 | ctx.assert(limit > 0 && offset >= 0, 400) 32 | if ('reversed' in object) { 33 | reversed = ![false, 'false', 0, '0'].includes(object.reversed) 34 | } 35 | ctx.state.pagination = {limit, offset, reversed} 36 | await next() 37 | } 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # parcel-bundler cache (https://parceljs.org/) 61 | .cache 62 | 63 | # next.js build output 64 | .next 65 | 66 | # nuxt.js build output 67 | .nuxt 68 | 69 | # vuepress build output 70 | .vuepress/dist 71 | 72 | # Serverless directories 73 | .serverless 74 | 75 | /run 76 | config/config.local.js 77 | -------------------------------------------------------------------------------- /app/model/gas-refund.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const {INTEGER, BIGINT} = app.Sequelize 3 | 4 | let GasRefund = app.model.define('gas_refund', { 5 | transactionId: { 6 | type: BIGINT.UNSIGNED, 7 | primaryKey: true 8 | }, 9 | outputIndex: { 10 | type: INTEGER.UNSIGNED, 11 | primaryKey: true 12 | }, 13 | refundId: { 14 | type: BIGINT.UNSIGNED, 15 | unique: 'refund' 16 | }, 17 | refundIndex: { 18 | type: INTEGER.UNSIGNED, 19 | unique: 'refund' 20 | } 21 | }, {freezeTableName: true, underscored: true, timestamps: false}) 22 | 23 | GasRefund.associate = () => { 24 | const {Transaction, TransactionOutput} = app.model 25 | Transaction.hasMany(GasRefund, {as: 'refunds', foreignKey: 'transactionId'}) 26 | GasRefund.belongsTo(Transaction, {as: 'transaction', foreignKey: 'transactionId'}) 27 | TransactionOutput.hasOne(GasRefund, {as: 'refund', foreignKey: 'transactionId'}) 28 | GasRefund.belongsTo(TransactionOutput, {as: 'refund', foreignKey: 'transactionId'}) 29 | Transaction.hasOne(GasRefund, {as: 'refundToTransaction', foreignKey: 'refundId'}) 30 | GasRefund.belongsTo(Transaction, {as: 'refundToTransaction', foreignKey: 'refundId'}) 31 | TransactionOutput.hasOne(GasRefund, {as: 'refundTo', foreignKey: 'refundId'}) 32 | GasRefund.belongsTo(TransactionOutput, {as: 'refundTo', foreignKey: 'refundId'}) 33 | } 34 | 35 | return GasRefund 36 | } 37 | -------------------------------------------------------------------------------- /config/config.default.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const Redis = require('ioredis') 3 | 4 | const redisConfig = { 5 | host: 'localhost', 6 | port: 6379, 7 | password: '', 8 | db: 0 9 | } 10 | 11 | exports.keys = 'qtuminfo-api' 12 | 13 | exports.security = { 14 | csrf: {enable: false} 15 | } 16 | 17 | exports.middleware = ['ratelimit'] 18 | 19 | exports.redis = { 20 | client: redisConfig 21 | } 22 | 23 | exports.ratelimit = { 24 | db: new Redis(redisConfig), 25 | headers: { 26 | remaining: 'Rate-Limit-Remaining', 27 | reset: 'Rate-Limit-Reset', 28 | total: 'Rate-Limit-Total', 29 | }, 30 | disableHeader: false, 31 | errorMessage: 'Rate Limit Exceeded', 32 | duration: 10 * 60 * 1000, 33 | max: 10 * 60 34 | } 35 | 36 | exports.io = { 37 | redis: { 38 | ...redisConfig, 39 | key: 'qtuminfo-api-socket.io' 40 | }, 41 | namespace: { 42 | '/': {connectionMiddleware: ['connection']} 43 | } 44 | } 45 | 46 | exports.sequelize = { 47 | dialect: 'mysql', 48 | database: 'qtum_mainnet', 49 | host: 'localhost', 50 | port: 3306, 51 | username: 'qtum', 52 | password: '' 53 | } 54 | 55 | exports.qtum = { 56 | chain: 'mainnet' 57 | } 58 | 59 | exports.qtuminfo = { 60 | path: path.resolve('..', 'qtuminfo'), 61 | port: 3001, 62 | rpc: { 63 | protocol: 'http', 64 | host: 'localhost', 65 | port: 3889, 66 | user: 'user', 67 | password: 'password' 68 | } 69 | } 70 | 71 | exports.cmcAPIKey = null 72 | -------------------------------------------------------------------------------- /app/middleware/contract.js: -------------------------------------------------------------------------------- 1 | module.exports = (paramName = 'contract') => async function contract(ctx, next) { 2 | ctx.assert(ctx.params[paramName], 404) 3 | const {Address: RawAddress} = ctx.app.qtuminfo.lib 4 | const chain = ctx.app.chain 5 | const {Address, Contract} = ctx.model 6 | const {gte: $gte} = ctx.app.Sequelize.Op 7 | 8 | let contract = {} 9 | let rawAddress 10 | try { 11 | rawAddress = RawAddress.fromString(ctx.params[paramName], chain) 12 | } catch (err) { 13 | ctx.throw(400) 14 | } 15 | let filter 16 | if (rawAddress.type === RawAddress.CONTRACT) { 17 | filter = {address: Buffer.from(ctx.params[paramName], 'hex')} 18 | } else if (rawAddress.type === RawAddress.EVM_CONTRACT) { 19 | filter = {addressString: ctx.params[paramName]} 20 | } else { 21 | ctx.throw(400) 22 | } 23 | let contractResult = await Contract.findOne({ 24 | where: filter, 25 | attributes: ['address', 'addressString', 'vm', 'type'], 26 | transaction: ctx.state.transaction 27 | }) 28 | ctx.assert(contractResult, 404) 29 | contract.contractAddress = contractResult.address 30 | contract.address = contractResult.addressString 31 | contract.vm = contractResult.vm 32 | contract.type = contractResult.type 33 | 34 | let addressList = await Address.findAll({ 35 | where: { 36 | type: {[$gte]: Address.parseType('contract')}, 37 | data: contract.contractAddress 38 | }, 39 | attributes: ['_id'], 40 | transaction: ctx.state.transaction 41 | }) 42 | contract.addressIds = addressList.map(address => address._id) 43 | ctx.state[paramName] = contract 44 | await next() 45 | } 46 | -------------------------------------------------------------------------------- /app/model/address.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | const addressTypes = { 3 | pubkeyhash: 1, 4 | scripthash: 2, 5 | witness_v0_keyhash: 3, 6 | witness_v0_scripthash: 4, 7 | contract: 0x80, 8 | evm_contract: 0x81, 9 | x86_contract: 0x82 10 | } 11 | /* eslint-enable camelcase*/ 12 | const addressTypeMap = { 13 | 1: 'pubkeyhash', 14 | 2: 'scripthash', 15 | 3: 'witness_v0_keyhash', 16 | 4: 'witness_v0_scripthash', 17 | 0x80: 'contract', 18 | 0x81: 'evm_contract', 19 | 0x82: 'x86_contract' 20 | } 21 | 22 | module.exports = app => { 23 | const {INTEGER, BIGINT, STRING} = app.Sequelize 24 | 25 | const Address = app.model.define('address', { 26 | _id: { 27 | type: BIGINT.UNSIGNED, 28 | field: '_id', 29 | primaryKey: true, 30 | autoIncrement: true 31 | }, 32 | type: { 33 | type: INTEGER(3).UNSIGNED, 34 | get() { 35 | let type = this.getDataValue('type') 36 | return addressTypeMap[type] || null 37 | }, 38 | set(type) { 39 | if (type != null) { 40 | this.setDataValue('type', addressTypes[type] || 0) 41 | } 42 | }, 43 | unique: 'address' 44 | }, 45 | data: { 46 | type: STRING(32).BINARY, 47 | unique: 'address' 48 | }, 49 | string: STRING(64), 50 | createHeight: INTEGER.UNSIGNED, 51 | createIndex: INTEGER.UNSIGNED 52 | }, {freezeTableName: true, underscored: true, timestamps: false}) 53 | 54 | Address.getType = function(type) { 55 | return addressTypeMap[type] || null 56 | } 57 | Address.parseType = function(type) { 58 | return addressTypes[type] || 0 59 | } 60 | 61 | return Address 62 | } 63 | -------------------------------------------------------------------------------- /app/model/transaction-input.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const {INTEGER, BIGINT, BLOB} = app.Sequelize 3 | 4 | let TransactionInput = app.model.define('transaction_input', { 5 | transactionId: { 6 | type: BIGINT.UNSIGNED, 7 | primaryKey: true 8 | }, 9 | inputIndex: { 10 | type: INTEGER.UNSIGNED, 11 | primaryKey: true 12 | }, 13 | scriptSig: { 14 | type: BLOB('medium'), 15 | field: 'scriptsig' 16 | }, 17 | sequence: INTEGER.UNSIGNED, 18 | blockHeight: INTEGER.UNSIGNED, 19 | value: { 20 | type: BIGINT, 21 | get() { 22 | let value = this.getDataValue('value') 23 | return value == null ? null : BigInt(value) 24 | }, 25 | set(value) { 26 | this.setDataValue('value', value.toString()) 27 | } 28 | }, 29 | addressId: BIGINT.UNSIGNED, 30 | outputId: BIGINT.UNSIGNED, 31 | outputIndex: INTEGER.UNSIGNED 32 | }, {freezeTableName: true, underscored: true, timestamps: false}) 33 | 34 | TransactionInput.associate = () => { 35 | const {Address, Transaction, TransactionOutput} = app.model 36 | Transaction.hasMany(TransactionInput, {as: 'inputs', foreignKey: 'transactionId'}) 37 | TransactionInput.belongsTo(Transaction, {as: 'inputTransaction', foreignKey: 'transactionId'}) 38 | TransactionInput.belongsTo(Transaction, {as: 'outputTransaction', foreignKey: 'outputId'}) 39 | TransactionInput.belongsTo(TransactionOutput, {as: 'output', foreignKey: 'outputId'}) 40 | Address.hasMany(TransactionInput, {as: 'inputTxos', foreignKey: 'addressId'}) 41 | TransactionInput.belongsTo(Address, {as: 'address', foreignKey: 'addressId'}) 42 | } 43 | 44 | return TransactionInput 45 | } 46 | -------------------------------------------------------------------------------- /app/model/qrc721.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const {CHAR, BLOB} = app.Sequelize 3 | 4 | let QRC721 = app.model.define('qrc721', { 5 | contractAddress: { 6 | type: CHAR(20).BINARY, 7 | primaryKey: true 8 | }, 9 | name: { 10 | type: BLOB, 11 | get() { 12 | return this.getDataValue('name').toString() 13 | }, 14 | set(name) { 15 | this.setDataValue('name', Buffer.from(name)) 16 | } 17 | }, 18 | symbol: { 19 | type: BLOB, 20 | get() { 21 | return this.getDataValue('symbol').toString() 22 | }, 23 | set(symbol) { 24 | this.setDataValue('symbol', Buffer.from(symbol)) 25 | } 26 | }, 27 | totalSupply: { 28 | type: CHAR(32).BINARY, 29 | get() { 30 | let totalSupply = this.getDataValue('totalSupply') 31 | return totalSupply == null ? null : BigInt(`0x${totalSupply.toString('hex')}`) 32 | }, 33 | set(totalSupply) { 34 | this.setDataValue( 35 | 'totalSupply', 36 | Buffer.from(totalSupply.toString(16).padStart(64, '0'), 'hex') 37 | ) 38 | } 39 | } 40 | }, {freezeTableName: true, underscored: true, timestamps: false}) 41 | 42 | QRC721.associate = () => { 43 | const {EvmReceiptLog: EVMReceiptLog, Contract} = app.model 44 | EVMReceiptLog.belongsTo(QRC721, {as: 'qrc721', foreignKey: 'address', sourceKey: 'contractAddress'}) 45 | QRC721.hasOne(EVMReceiptLog, {as: 'logs', foreignKey: 'address', sourceKey: 'contractAddress'}) 46 | Contract.hasOne(QRC721, {as: 'qrc721', foreignKey: 'contractAddress'}) 47 | QRC721.belongsTo(Contract, {as: 'contract', foreignKey: 'contractAddress'}) 48 | } 49 | 50 | return QRC721 51 | } 52 | -------------------------------------------------------------------------------- /app/model/transaction-output.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const {INTEGER, BIGINT, BOOLEAN, BLOB} = app.Sequelize 3 | 4 | let TransactionOutput = app.model.define('transaction_output', { 5 | transactionId: { 6 | type: BIGINT.UNSIGNED, 7 | primaryKey: true 8 | }, 9 | outputIndex: { 10 | type: INTEGER.UNSIGNED, 11 | primaryKey: true 12 | }, 13 | scriptPubKey: { 14 | type: BLOB('medium'), 15 | field: 'scriptpubkey' 16 | }, 17 | blockHeight: INTEGER.UNSIGNED, 18 | value: { 19 | type: BIGINT, 20 | get() { 21 | let value = this.getDataValue('value') 22 | return value == null ? null : BigInt(value) 23 | }, 24 | set(value) { 25 | this.setDataValue('value', value.toString()) 26 | } 27 | }, 28 | addressId: BIGINT.UNSIGNED, 29 | isStake: BOOLEAN, 30 | inputId: BIGINT.UNSIGNED, 31 | inputIndex: INTEGER.UNSIGNED, 32 | inputHeight: { 33 | type: INTEGER.UNSIGNED, 34 | allowNull: true 35 | } 36 | }, {freezeTableName: true, underscored: true, timestamps: false}) 37 | 38 | TransactionOutput.associate = () => { 39 | const {Address, Transaction, TransactionInput} = app.model 40 | Transaction.hasMany(TransactionOutput, {as: 'outputs', foreignKey: 'transactionId'}) 41 | TransactionOutput.belongsTo(Transaction, {as: 'outputTransaction', foreignKey: 'transactionId'}) 42 | TransactionOutput.belongsTo(Transaction, {as: 'inputTransaction', foreignKey: 'inputId'}) 43 | TransactionOutput.belongsTo(TransactionInput, {as: 'input', foreignKey: 'inputId'}) 44 | Address.hasMany(TransactionOutput, {as: 'outputTxos', foreignKey: 'addressId'}) 45 | TransactionOutput.belongsTo(Address, {as: 'address', foreignKey: 'addressId'}) 46 | } 47 | 48 | return TransactionOutput 49 | } 50 | -------------------------------------------------------------------------------- /app/middleware/block-filter.js: -------------------------------------------------------------------------------- 1 | module.exports = () => async function pagination(ctx, next) { 2 | const {Header} = ctx.model 3 | const {gte: $gte, lte: $lte} = ctx.app.Sequelize.Op 4 | 5 | if (!['GET', 'POST'].includes(ctx.method)) { 6 | return await next() 7 | } 8 | let fromBlock = 1 9 | let toBlock = null 10 | let object = {GET: ctx.query, POST: ctx.request.body}[ctx.method] 11 | if ('fromBlock' in object) { 12 | let height = Number.parseInt(object.fromBlock) 13 | ctx.assert(height >= 0 && height <= 0xffffffff, 400) 14 | if (height > fromBlock) { 15 | fromBlock = height 16 | } 17 | } 18 | if ('toBlock' in object) { 19 | let height = Number.parseInt(object.toBlock) 20 | ctx.assert(height >= 0 && height <= 0xffffffff, 400) 21 | if (toBlock == null || height < toBlock) { 22 | toBlock = height 23 | } 24 | } 25 | if ('fromTime' in object) { 26 | let timestamp = Math.floor(Date.parse(object.fromTime) / 1000) 27 | ctx.assert(timestamp >= 0 && timestamp <= 0xffffffff, 400) 28 | let header = await Header.findOne({ 29 | where: {timestamp: {[$gte]: timestamp}}, 30 | attributes: ['height'], 31 | order: [['timestamp', 'ASC']], 32 | transaction: ctx.state.transaction 33 | }) 34 | if (header && header.height > fromBlock) { 35 | fromBlock = header.height 36 | } 37 | } 38 | if ('toTime' in object) { 39 | let timestamp = Math.floor(Date.parse(object.toTime) / 1000) 40 | ctx.assert(timestamp >= 0 && timestamp <= 0xffffffff, 400) 41 | let header = await Header.findOne({ 42 | where: {timestamp: {[$lte]: timestamp}}, 43 | attributes: ['height'], 44 | order: [['timestamp', 'DESC']], 45 | transaction: ctx.state.transaction 46 | }) 47 | if (header && (toBlock == null || header.height < toBlock)) { 48 | toBlock = header.height 49 | } 50 | } 51 | ctx.state.fromBlock = fromBlock 52 | ctx.state.toBlock = toBlock 53 | await next() 54 | } 55 | -------------------------------------------------------------------------------- /app/model/header.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const {INTEGER, CHAR, BLOB} = app.Sequelize 3 | 4 | let Header = app.model.define('header', { 5 | hash: { 6 | type: CHAR(32).BINARY, 7 | unique: true 8 | }, 9 | height: { 10 | type: INTEGER.UNSIGNED, 11 | primaryKey: true 12 | }, 13 | version: INTEGER, 14 | prevHash: { 15 | type: CHAR(32).BINARY, 16 | defaultValue: Buffer.alloc(32) 17 | }, 18 | merkleRoot: CHAR(32).BINARY, 19 | timestamp: INTEGER.UNSIGNED, 20 | bits: INTEGER.UNSIGNED, 21 | nonce: INTEGER.UNSIGNED, 22 | hashStateRoot: CHAR(32).BINARY, 23 | hashUTXORoot: {type: CHAR(32).BINARY, field: 'hash_utxo_root'}, 24 | stakePrevTxId: {type: CHAR(32).BINARY, field: 'stake_prev_transaction_id'}, 25 | stakeOutputIndex: INTEGER.UNSIGNED, 26 | signature: BLOB, 27 | chainwork: { 28 | type: CHAR(32).BINARY, 29 | get() { 30 | let chainwork = this.getDataValue('chainwork') 31 | return chainwork == null ? null : BigInt(`0x${chainwork.toString('hex')}`) 32 | }, 33 | set(chainwork) { 34 | this.setDataValue( 35 | 'chainwork', 36 | Buffer.from(chainwork.toString(16).padStart(64, '0'), 'hex') 37 | ) 38 | } 39 | } 40 | }, { 41 | freezeTableName: true, underscored: true, timestamps: false, 42 | getterMethods: { 43 | difficulty() { 44 | function getTargetDifficulty(bits) { 45 | return (bits & 0xffffff) * 2 ** ((bits >>> 24) - 3 << 3) 46 | } 47 | return getTargetDifficulty(0x1d00ffff) / getTargetDifficulty(this.bits) 48 | } 49 | } 50 | }) 51 | 52 | Header.prototype.isProofOfStake = function isProofOfStake() { 53 | return Buffer.compare(this.stakePrevTxId, Buffer.alloc(32)) !== 0 && this.stakeOutputIndex !== 0xffffffff 54 | } 55 | 56 | Header.associate = () => { 57 | Header.hasOne(Header, {as: 'prevHeader', foreignKey: 'height'}) 58 | Header.hasOne(Header, {as: 'nextHeader', foreignKey: 'height'}) 59 | } 60 | 61 | return Header 62 | } 63 | -------------------------------------------------------------------------------- /app/model/qrc20.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const {INTEGER, CHAR, BLOB} = app.Sequelize 3 | 4 | let QRC20 = app.model.define('qrc20', { 5 | contractAddress: { 6 | type: CHAR(20).BINARY, 7 | primaryKey: true 8 | }, 9 | name: { 10 | type: BLOB, 11 | get() { 12 | let name = this.getDataValue('name') 13 | return name == null ? null : name.toString() 14 | }, 15 | set(name) { 16 | this.setDataValue('name', Buffer.from(name)) 17 | } 18 | }, 19 | symbol: { 20 | type: BLOB, 21 | get() { 22 | let symbol = this.getDataValue('symbol') 23 | return symbol == null ? null : symbol.toString() 24 | }, 25 | set(symbol) { 26 | this.setDataValue('symbol', Buffer.from(symbol)) 27 | } 28 | }, 29 | decimals: INTEGER(3).UNSIGNED, 30 | totalSupply: { 31 | type: CHAR(32).BINARY, 32 | get() { 33 | let totalSupply = this.getDataValue('totalSupply') 34 | return totalSupply == null ? null : BigInt(`0x${totalSupply.toString('hex')}`) 35 | }, 36 | set(totalSupply) { 37 | this.setDataValue( 38 | 'totalSupply', 39 | Buffer.from(totalSupply.toString(16).padStart(64, '0'), 'hex') 40 | ) 41 | } 42 | }, 43 | version: { 44 | type: BLOB, 45 | allowNull: true, 46 | get() { 47 | let version = this.getDataValue('version') 48 | return version == null ? null : version.toString() 49 | }, 50 | set(version) { 51 | this.setDataValue('version', Buffer.from(version)) 52 | } 53 | } 54 | }, {freezeTableName: true, underscored: true, timestamps: false}) 55 | 56 | QRC20.associate = () => { 57 | const {EvmReceiptLog: EVMReceiptLog, Contract} = app.model 58 | EVMReceiptLog.belongsTo(QRC20, {as: 'qrc20', foreignKey: 'address', sourceKey: 'contractAddress'}) 59 | QRC20.hasMany(EVMReceiptLog, {as: 'logs', foreignKey: 'address', sourceKey: 'contractAddress'}) 60 | Contract.hasOne(QRC20, {as: 'qrc20', foreignKey: 'contractAddress'}) 61 | QRC20.belongsTo(Contract, {as: 'contract', foreignKey: 'contractAddress'}) 62 | } 63 | 64 | return QRC20 65 | } 66 | -------------------------------------------------------------------------------- /agent.js: -------------------------------------------------------------------------------- 1 | const SocketClient = require('socket.io-client') 2 | 3 | module.exports = function(agent) { 4 | let tip = null 5 | 6 | agent.messenger.on('egg-ready', () => { 7 | let io = SocketClient(`http://localhost:${agent.config.qtuminfo.port}`) 8 | io.on('tip', newTip => { 9 | tip = newTip 10 | agent.messenger.sendToApp('block-tip', tip) 11 | agent.messenger.sendRandom('socket/block-tip', tip) 12 | }) 13 | io.on('block', block => { 14 | tip = block 15 | agent.messenger.sendToApp('new-block', block) 16 | agent.messenger.sendRandom('update-stakeweight') 17 | agent.messenger.sendRandom('update-dgpinfo') 18 | agent.messenger.sendRandom('socket/block-tip', block) 19 | }) 20 | io.on('reorg', block => { 21 | tip = block 22 | agent.messenger.sendToApp('reorg-to-block', block) 23 | agent.messenger.sendRandom('socket/reorg/block-tip', block) 24 | }) 25 | io.on('mempool-transaction', id => { 26 | if (id) { 27 | agent.messenger.sendRandom('socket/mempool-transaction', id) 28 | } 29 | }) 30 | }) 31 | 32 | let lastTipHash = Buffer.alloc(0) 33 | function updateStatistics() { 34 | if (tip && Buffer.compare(lastTipHash, tip.hash) !== 0) { 35 | agent.messenger.sendRandom('update-richlist') 36 | agent.messenger.sendRandom('update-qrc20-statistics') 37 | agent.messenger.sendRandom('update-daily-transactions') 38 | agent.messenger.sendRandom('update-block-interval') 39 | agent.messenger.sendRandom('update-address-growth') 40 | lastTipHash = tip.hash 41 | } 42 | } 43 | 44 | setInterval(updateStatistics, 2 * 60 * 1000).unref() 45 | 46 | agent.messenger.on('blockchain-info', () => { 47 | agent.messenger.sendToApp('blockchain-info', {tip}) 48 | }) 49 | 50 | agent.messenger.on('egg-ready', () => { 51 | let interval = setInterval(() => { 52 | if (tip) { 53 | agent.messenger.sendToApp('blockchain-info', {tip}) 54 | clearInterval(interval) 55 | updateStatistics() 56 | } 57 | }, 0) 58 | agent.messenger.sendRandom('update-stakeweight') 59 | agent.messenger.sendRandom('update-feerate') 60 | agent.messenger.sendRandom('update-dgpinfo') 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /app/model/evm-receipt.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | const addressTypes = { 3 | pubkeyhash: 1, 4 | scripthash: 2, 5 | witness_v0_keyhash: 3, 6 | witness_v0_scripthash: 4, 7 | contract: 0x80, 8 | evm_contract: 0x81, 9 | x86_contract: 0x82 10 | } 11 | /* eslint-enable camelcase*/ 12 | const addressTypeMap = { 13 | 1: 'pubkeyhash', 14 | 2: 'scripthash', 15 | 3: 'witness_v0_keyhash', 16 | 4: 'witness_v0_scripthash', 17 | 0x80: 'contract', 18 | 0x81: 'evm_contract', 19 | 0x82: 'x86_contract' 20 | } 21 | 22 | module.exports = app => { 23 | const {INTEGER, BIGINT, CHAR, STRING, TEXT} = app.Sequelize 24 | 25 | let EVMReceipt = app.model.define('evm_receipt', { 26 | _id: { 27 | type: BIGINT.UNSIGNED, 28 | field: '_id', 29 | primaryKey: true, 30 | autoIncrement: true 31 | }, 32 | transactionId: { 33 | type: BIGINT.UNSIGNED, 34 | unique: 'transaction' 35 | }, 36 | outputIndex: { 37 | type: INTEGER.UNSIGNED, 38 | unique: 'transaction' 39 | }, 40 | blockHeight: INTEGER.UNSIGNED, 41 | indexInBlock: INTEGER.UNSIGNED, 42 | senderType: { 43 | type: INTEGER(3).UNSIGNED, 44 | get() { 45 | let senderType = this.getDataValue('senderType') 46 | return addressTypeMap[senderType] || null 47 | }, 48 | set(senderType) { 49 | if (senderType != null) { 50 | this.setDataValue('senderType', addressTypes[senderType] || 0) 51 | } 52 | } 53 | }, 54 | senderData: STRING(32).BINARY, 55 | gasUsed: INTEGER.UNSIGNED, 56 | contractAddress: CHAR(20).BINARY, 57 | excepted: STRING(32), 58 | exceptedMessage: TEXT 59 | }, {freezeTableName: true, underscored: true, timestamps: false}) 60 | 61 | EVMReceipt.associate = () => { 62 | const {Header, Transaction, TransactionOutput} = app.model 63 | EVMReceipt.belongsTo(Header, {as: 'header', foreignKey: 'blockHeight'}) 64 | Transaction.hasMany(EVMReceipt, {as: 'evmReceipts', foreignKey: 'transactionId'}) 65 | EVMReceipt.belongsTo(Transaction, {as: 'transaction', foreignKey: 'transactionId'}) 66 | TransactionOutput.hasOne(EVMReceipt, {as: 'evmReceipt', foreignKey: 'transactionId'}) 67 | EVMReceipt.belongsTo(TransactionOutput, {as: 'output', foreignKey: 'transactionId'}) 68 | } 69 | 70 | return EVMReceipt 71 | } 72 | -------------------------------------------------------------------------------- /app/service/statistics.js: -------------------------------------------------------------------------------- 1 | const {Service} = require('egg') 2 | 3 | class StatisticsService extends Service { 4 | async getDailyTransactions() { 5 | const db = this.ctx.model 6 | const {sql} = this.ctx.helper 7 | let result = await db.query(sql` 8 | SELECT 9 | FLOOR(header.timestamp / 86400) AS date, 10 | SUM(block.transactions_count) AS transactionsCount, 11 | SUM(block.contract_transactions_count) AS contractTransactionsCount 12 | FROM header, block 13 | WHERE header.height = block.height 14 | GROUP BY date 15 | ORDER BY date ASC 16 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction}) 17 | return result.map(({date, transactionsCount, contractTransactionsCount}) => ({ 18 | timestamp: date * 86400, 19 | transactionsCount, 20 | contractTransactionsCount 21 | })) 22 | } 23 | 24 | async getBlockIntervalStatistics() { 25 | const db = this.ctx.model 26 | const {sql} = this.ctx.helper 27 | let result = await db.query(sql` 28 | SELECT header.timestamp - prev_header.timestamp AS blockInterval, COUNT(*) AS count FROM header 29 | INNER JOIN header prev_header ON prev_header.height = header.height - 1 30 | WHERE header.height > 5001 31 | GROUP BY blockInterval 32 | ORDER BY blockInterval ASC 33 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction}) 34 | let total = this.app.blockchainInfo.tip.height - 5001 35 | return result.map(({blockInterval, count}) => ({interval: blockInterval, count, percentage: count / total})) 36 | } 37 | 38 | async getAddressGrowth() { 39 | const db = this.ctx.model 40 | const {Address} = db 41 | const {sql} = this.ctx.helper 42 | let result = await db.query(sql` 43 | SELECT FLOOR(header.timestamp / 86400) AS date, COUNT(*) AS count FROM address, header 44 | WHERE address.create_height = header.height AND address.type < ${Address.parseType('contract')} 45 | GROUP BY date 46 | ORDER BY date ASC 47 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction}) 48 | let sum = 0 49 | return result.map(({date, count}) => { 50 | sum += count 51 | return { 52 | timestamp: date * 86400, 53 | count: sum 54 | } 55 | }) 56 | } 57 | } 58 | 59 | module.exports = StatisticsService 60 | -------------------------------------------------------------------------------- /app/service/qrc721.js: -------------------------------------------------------------------------------- 1 | const {Service} = require('egg') 2 | 3 | class QRC721Service extends Service { 4 | async listQRC721Tokens() { 5 | const db = this.ctx.model 6 | const {sql} = this.ctx.helper 7 | let {limit, offset} = this.ctx.state.pagination 8 | 9 | let [{totalCount}] = await db.query(sql` 10 | SELECT COUNT(DISTINCT(qrc721_token.contract_address)) AS count FROM qrc721_token 11 | INNER JOIN qrc721 USING (contract_address) 12 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction}) 13 | let list = await db.query(sql` 14 | SELECT 15 | contract.address_string AS address, contract.address AS addressHex, 16 | qrc721.name AS name, qrc721.symbol AS symbol, qrc721.total_supply AS totalSupply, 17 | list.holders AS holders 18 | FROM ( 19 | SELECT contract_address, COUNT(*) AS holders FROM qrc721_token 20 | INNER JOIN qrc721 USING (contract_address) 21 | GROUP BY contract_address 22 | ORDER BY holders DESC 23 | LIMIT ${offset}, ${limit} 24 | ) list 25 | INNER JOIN qrc721 USING (contract_address) 26 | INNER JOIN contract ON contract.address = list.contract_address 27 | ORDER BY holders DESC 28 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction}) 29 | 30 | return { 31 | totalCount, 32 | tokens: list.map(item => ({ 33 | address: item.addressHex.toString('hex'), 34 | addressHex: item.addressHex, 35 | name: item.name.toString(), 36 | symbol: item.symbol.toString(), 37 | totalSupply: BigInt(`0x${item.totalSupply.toString('hex')}`), 38 | holders: item.holders 39 | })) 40 | } 41 | } 42 | 43 | async getAllQRC721Balances(hexAddresses) { 44 | if (hexAddresses.length === 0) { 45 | return [] 46 | } 47 | const db = this.ctx.model 48 | const {sql} = this.ctx.helper 49 | let list = await db.query(sql` 50 | SELECT 51 | contract.address AS addressHex, contract.address_string AS address, 52 | qrc721.name AS name, 53 | qrc721.symbol AS symbol, 54 | qrc721_token.count AS count 55 | FROM ( 56 | SELECT contract_address, COUNT(*) AS count FROM qrc721_token 57 | WHERE holder IN ${hexAddresses} 58 | GROUP BY contract_address 59 | ) qrc721_token 60 | INNER JOIN contract ON contract.address = qrc721_token.contract_address 61 | INNER JOIN qrc721 ON qrc721.contract_address = qrc721_token.contract_address 62 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction}) 63 | return list.map(item => ({ 64 | address: item.addressHex.toString('hex'), 65 | addressHex: item.addressHex, 66 | name: item.name.toString(), 67 | symbol: item.symbol.toString(), 68 | count: item.count 69 | })) 70 | } 71 | } 72 | 73 | module.exports = QRC721Service 74 | -------------------------------------------------------------------------------- /app/controller/transaction.js: -------------------------------------------------------------------------------- 1 | const {Controller} = require('egg') 2 | 3 | class TransactionController extends Controller { 4 | async transaction() { 5 | const {ctx} = this 6 | ctx.assert(ctx.params.id && /^[0-9a-f]{64}$/i.test(ctx.params.id), 404) 7 | let brief = 'brief' in ctx.query 8 | let id = Buffer.from(ctx.params.id, 'hex') 9 | let transaction = await ctx.service.transaction.getTransaction(id) 10 | ctx.assert(transaction, 404) 11 | ctx.body = await ctx.service.transaction.transformTransaction(transaction, {brief}) 12 | } 13 | 14 | async transactions() { 15 | const {ctx} = this 16 | ctx.assert(ctx.params.ids, 404) 17 | let ids = ctx.params.ids.split(',') 18 | ctx.assert(ids.length <= 100 && ids.every(id => /^[0-9a-f]{64}$/i.test(id)), 404) 19 | let brief = 'brief' in ctx.query 20 | let transactions = await Promise.all(ids.map( 21 | id => ctx.service.transaction.getTransaction(Buffer.from(id, 'hex')) 22 | )) 23 | ctx.assert(transactions.every(Boolean), 404) 24 | ctx.body = await Promise.all(transactions.map( 25 | tx => ctx.service.transaction.transformTransaction(tx, {brief}) 26 | )) 27 | } 28 | 29 | async rawTransaction() { 30 | const {ctx} = this 31 | ctx.assert(/^[0-9a-f]{64}$/.test(ctx.params.id), 404) 32 | let id = Buffer.from(ctx.params.id, 'hex') 33 | let transaction = await ctx.service.transaction.getRawTransaction(id) 34 | ctx.assert(transaction, 404) 35 | ctx.body = transaction.toBuffer().toString('hex') 36 | } 37 | 38 | async recent() { 39 | const {ctx} = this 40 | let count = Number.parseInt(ctx.query.count || 10) 41 | let ids = await ctx.service.transaction.getRecentTransactions(count) 42 | let transactions = await Promise.all(ids.map( 43 | id => ctx.service.transaction.getTransaction(Buffer.from(id, 'hex')) 44 | )) 45 | ctx.body = await Promise.all(transactions.map( 46 | tx => ctx.service.transaction.transformTransaction(tx, {brief: true}) 47 | )) 48 | } 49 | 50 | async list() { 51 | const {ctx} = this 52 | let {totalCount, ids} = await ctx.service.transaction.getAllTransactions() 53 | let transactions = await Promise.all(ids.map(id => ctx.service.transaction.getTransaction(id))) 54 | ctx.body = { 55 | totalCount, 56 | transactions: await Promise.all(transactions.map(tx => ctx.service.transaction.transformTransaction(tx))) 57 | } 58 | } 59 | 60 | async send() { 61 | const {ctx} = this 62 | let {rawtx: data} = ctx.request.body 63 | if (!/^([0-9a-f][0-9a-f])+$/i.test(data)) { 64 | ctx.body = {status: 1, message: 'TX decode failed'} 65 | } 66 | try { 67 | let id = await ctx.service.transaction.sendRawTransaction(Buffer.from(data, 'hex')) 68 | ctx.body = {status: 0, id: id.toString('hex'), txid: id.toString('hex')} 69 | } catch (err) { 70 | ctx.body = {status: 1, message: err.message} 71 | } 72 | } 73 | } 74 | 75 | module.exports = TransactionController 76 | -------------------------------------------------------------------------------- /app/controller/qrc20.js: -------------------------------------------------------------------------------- 1 | const {Controller} = require('egg') 2 | 3 | class QRC20Controller extends Controller { 4 | async list() { 5 | const {ctx} = this 6 | let {totalCount, tokens} = await ctx.service.qrc20.listQRC20Tokens() 7 | ctx.body = { 8 | totalCount, 9 | tokens: tokens.map(item => ({ 10 | address: item.addressHex.toString('hex'), 11 | addressHex: item.addressHex.toString('hex'), 12 | name: item.name, 13 | symbol: item.symbol, 14 | decimals: item.decimals, 15 | totalSupply: item.totalSupply.toString(), 16 | version: item.version, 17 | holders: item.holders, 18 | transactions: item.transactions 19 | })) 20 | } 21 | } 22 | 23 | async allTransactions() { 24 | const {ctx} = this 25 | let {totalCount, transactions} = await ctx.service.qrc20.getAllQRC20TokenTransactions() 26 | ctx.body = { 27 | totalCount, 28 | transactions: transactions.map(transaction => ({ 29 | transactionId: transaction.transactionId.toString('hex'), 30 | outputIndex: transaction.outputIndex, 31 | blockHeight: transaction.blockHeight, 32 | blockHash: transaction.blockHash.toString('hex'), 33 | timestamp: transaction.timestamp, 34 | token: { 35 | name: transaction.token.name, 36 | symbol: transaction.token.symbol, 37 | decimals: transaction.token.decimals 38 | }, 39 | from: transaction.from, 40 | fromHex: transaction.fromHex && transaction.fromHex.toString('hex'), 41 | to: transaction.to, 42 | toHex: transaction.toHex && transaction.toHex.toString('hex'), 43 | value: transaction.value.toString() 44 | })) 45 | } 46 | } 47 | 48 | async transactions() { 49 | const {ctx} = this 50 | ctx.assert(ctx.state.token.type === 'qrc20', 404) 51 | let {totalCount, transactions} = await ctx.service.qrc20.getQRC20TokenTransactions(ctx.state.token.contractAddress) 52 | ctx.body = { 53 | totalCount, 54 | transactions: transactions.map(transaction => ({ 55 | transactionId: transaction.transactionId.toString('hex'), 56 | outputIndex: transaction.outputIndex, 57 | blockHeight: transaction.blockHeight, 58 | blockHash: transaction.blockHash.toString('hex'), 59 | timestamp: transaction.timestamp, 60 | from: transaction.from, 61 | fromHex: transaction.fromHex && transaction.fromHex.toString('hex'), 62 | to: transaction.to, 63 | toHex: transaction.toHex && transaction.toHex.toString('hex'), 64 | value: transaction.value.toString() 65 | })) 66 | } 67 | } 68 | 69 | async richList() { 70 | const {ctx} = this 71 | ctx.assert(ctx.state.token.type === 'qrc20', 404) 72 | let {totalCount, list} = await ctx.service.qrc20.getQRC20TokenRichList(ctx.state.token.contractAddress) 73 | ctx.body = { 74 | totalCount, 75 | list: list.map(item => ({ 76 | address: item.address, 77 | addressHex: item.addressHex, 78 | balance: item.balance.toString() 79 | })) 80 | } 81 | } 82 | } 83 | 84 | module.exports = QRC20Controller 85 | -------------------------------------------------------------------------------- /app/service/info.js: -------------------------------------------------------------------------------- 1 | const {Service} = require('egg') 2 | 3 | class InfoService extends Service { 4 | async getInfo() { 5 | let height = this.app.blockchainInfo.tip.height 6 | let stakeWeight = JSON.parse(await this.app.redis.hget(this.app.name, 'stakeweight')) || 0 7 | let feeRate = JSON.parse(await this.app.redis.hget(this.app.name, 'feerate')).find(item => item.blocks === 10).feeRate || 0.004 8 | let dgpInfo = JSON.parse(await this.app.redis.hget(this.app.name, 'dgpinfo')) || {} 9 | return { 10 | height, 11 | supply: this.getTotalSupply(), 12 | ...this.app.chain.name === 'mainnet' ? {circulatingSupply: this.getCirculatingSupply()} : {}, 13 | netStakeWeight: Math.round(stakeWeight), 14 | feeRate, 15 | dgpInfo 16 | } 17 | } 18 | 19 | getTotalSupply() { 20 | let height = this.app.blockchainInfo.tip.height 21 | if (height <= this.app.chain.lastPoWBlockHeight) { 22 | return height * 20000 23 | } else { 24 | let supply = 1e8 25 | let reward = 4 26 | let interval = 985500 27 | let stakeHeight = height - this.app.chain.lastPoWBlockHeight 28 | let halvings = 0 29 | while (halvings < 7 && stakeHeight > interval) { 30 | supply += interval * reward / (1 << halvings++) 31 | stakeHeight -= interval 32 | } 33 | supply += stakeHeight * reward / (1 << halvings) 34 | return supply 35 | } 36 | } 37 | 38 | getTotalMaxSupply() { 39 | return 1e8 + 985500 * 4 * (1 - 1 / 2 ** 7) / (1 - 1 / 2) 40 | } 41 | 42 | getCirculatingSupply() { 43 | let height = this.app.blockchainInfo.tip.height 44 | let totalSupply = this.getTotalSupply(height) 45 | if (this.app.chain.name === 'mainnet') { 46 | return totalSupply - 575e4 47 | } else { 48 | return totalSupply 49 | } 50 | } 51 | 52 | async getStakeWeight() { 53 | const {Header} = this.ctx.model 54 | const {gte: $gte} = this.app.Sequelize.Op 55 | let height = await Header.aggregate('height', 'max', {transaction: this.ctx.state.transaction}) 56 | let list = await Header.findAll({ 57 | where: {height: {[$gte]: height - 500}}, 58 | attributes: ['timestamp', 'bits'], 59 | order: [['height', 'ASC']], 60 | transaction: this.ctx.state.transaction 61 | }) 62 | let interval = list[list.length - 1].timestamp - list[0].timestamp 63 | let sum = list.slice(1) 64 | .map(x => x.difficulty) 65 | .reduce((x, y) => x + y) 66 | return sum * 2 ** 32 * 16 / interval 67 | } 68 | 69 | async getFeeRates() { 70 | let client = new this.app.qtuminfo.rpc(this.app.config.qtuminfo.rpc) 71 | let results = await Promise.all([2, 4, 6, 10, 12, 24].map(blocks => client.estimatesmartfee(blocks))) 72 | return [ 73 | {blocks: 2, feeRate: results[0].feerate || 0.004}, 74 | {blocks: 4, feeRate: results[1].feerate || 0.004}, 75 | {blocks: 6, feeRate: results[2].feerate || 0.004}, 76 | {blocks: 10, feeRate: results[3].feerate || 0.004}, 77 | {blocks: 12, feeRate: results[4].feerate || 0.004}, 78 | {blocks: 24, feeRate: results[5].feerate || 0.004} 79 | ] 80 | } 81 | 82 | async getDGPInfo() { 83 | let client = new this.app.qtuminfo.rpc(this.app.config.qtuminfo.rpc) 84 | let info = await client.getdgpinfo() 85 | return { 86 | maxBlockSize: info.maxblocksize, 87 | minGasPrice: info.mingasprice, 88 | blockGasLimit: info.blockgaslimit 89 | } 90 | } 91 | } 92 | 93 | module.exports = InfoService 94 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | app.blockchainInfo = { 3 | tip: null 4 | } 5 | const namespace = app.io.of('/') 6 | 7 | app.messenger.on('egg-ready', () => { 8 | app.messenger.sendToAgent('blockchain-info') 9 | }) 10 | 11 | app.messenger.on('update-richlist', async () => { 12 | let ctx = app.createAnonymousContext() 13 | await ctx.service.balance.updateRichList() 14 | }) 15 | 16 | app.messenger.on('update-qrc20-statistics', async () => { 17 | let ctx = app.createAnonymousContext() 18 | await ctx.service.qrc20.updateQRC20Statistics() 19 | }) 20 | 21 | app.messenger.on('update-daily-transactions', async () => { 22 | let ctx = app.createAnonymousContext() 23 | let dailyTransactions = await ctx.service.statistics.getDailyTransactions() 24 | await app.redis.hset(app.name, 'daily-transactions', JSON.stringify(dailyTransactions)) 25 | }) 26 | 27 | app.messenger.on('update-block-interval', async () => { 28 | let ctx = app.createAnonymousContext() 29 | let blockInterval = await ctx.service.statistics.getBlockIntervalStatistics() 30 | await app.redis.hset(app.name, 'block-interval', JSON.stringify(blockInterval)) 31 | }) 32 | 33 | app.messenger.on('update-address-growth', async () => { 34 | let ctx = app.createAnonymousContext() 35 | let addressGrowth = await ctx.service.statistics.getAddressGrowth() 36 | await app.redis.hset(app.name, 'address-growth', JSON.stringify(addressGrowth)) 37 | }) 38 | 39 | app.messenger.on('update-stakeweight', async () => { 40 | let ctx = app.createAnonymousContext() 41 | let stakeWeight = await ctx.service.info.getStakeWeight() 42 | await app.redis.hset(app.name, 'stakeweight', JSON.stringify(stakeWeight)) 43 | namespace.to('blockchain').emit('stakeweight', stakeWeight) 44 | }) 45 | 46 | app.messenger.on('update-feerate', async () => { 47 | await app.runSchedule('update-feerate') 48 | }) 49 | 50 | app.messenger.on('update-dgpinfo', async () => { 51 | let ctx = app.createAnonymousContext() 52 | let dgpInfo = await ctx.service.info.getDGPInfo() 53 | await app.redis.hset(app.name, 'dgpinfo', JSON.stringify(dgpInfo)) 54 | namespace.to('blockchain').emit('dgpinfo', dgpInfo) 55 | }) 56 | 57 | app.messenger.on('blockchain-info', info => { 58 | app.blockchainInfo = info 59 | }) 60 | app.messenger.on('block-tip', tip => { 61 | app.blockchainInfo.tip = tip 62 | }) 63 | app.messenger.on('new-block', block => { 64 | app.blockchainInfo.tip = block 65 | }) 66 | app.messenger.on('reorg-to-block', block => { 67 | app.blockchainInfo.tip = block 68 | }) 69 | 70 | app.messenger.on('socket/block-tip', async tip => { 71 | app.blockchainInfo.tip = tip 72 | namespace.emit('tip', tip) 73 | let ctx = app.createAnonymousContext() 74 | let transactions = (await ctx.service.block.getBlockTransactions(tip.height)).map(id => id.toString('hex')) 75 | for (let id of transactions) { 76 | namespace.to(`transaction/${id}`).emit('transaction/confirm', id) 77 | } 78 | let list = await ctx.service.block.getBlockAddressTransactions(tip.height) 79 | for (let i = 0; i < transactions.length; ++i) { 80 | for (let address of list[i] || []) { 81 | namespace.to(`address/${address}`).emit('address/transaction', {address, id: transactions[i]}) 82 | } 83 | } 84 | }) 85 | 86 | app.messenger.on('socket/reorg/block-tip', tip => { 87 | app.blockchainInfo.tip = tip 88 | namespace.emit('reorg', tip) 89 | }) 90 | 91 | app.messenger.on('socket/mempool-transaction', async id => { 92 | id = Buffer.from(id) 93 | let ctx = app.createAnonymousContext() 94 | let transaction = await ctx.service.transaction.getTransaction(id) 95 | if (!transaction) { 96 | return 97 | } 98 | namespace.to('mempool').emit('mempool/transaction', await ctx.service.transaction.transformTransaction(transaction, {brief: true})) 99 | let addresses = await ctx.service.transaction.getMempoolTransactionAddresses(id) 100 | for (let address of addresses) { 101 | namespace.to(`address/${address}`).emit('address/transaction', {address, id: id.toString('hex')}) 102 | } 103 | }) 104 | } 105 | -------------------------------------------------------------------------------- /app/service/misc.js: -------------------------------------------------------------------------------- 1 | const {Service} = require('egg') 2 | 3 | class MiscService extends Service { 4 | async classify(id) { 5 | const db = this.ctx.model 6 | const {Block, Transaction, Contract, Qrc20: QRC20, where, fn, literal} = db 7 | const {or: $or, like: $like} = this.app.Sequelize.Op 8 | const {Address} = this.app.qtuminfo.lib 9 | const {sql} = this.ctx.helper 10 | const transaction = this.ctx.state.transaction 11 | 12 | if (/^(0|[1-9]\d{0,9})$/.test(id)) { 13 | let height = Number.parseInt(id) 14 | if (height <= this.app.blockchainInfo.tip.height) { 15 | return {type: 'block'} 16 | } 17 | } 18 | if (/^[0-9a-f]{64}$/i.test(id)) { 19 | if (await Block.findOne({ 20 | where: {hash: Buffer.from(id, 'hex')}, 21 | attributes: ['height'] 22 | })) { 23 | return {type: 'block'} 24 | } else if (await Transaction.findOne({ 25 | where: {id: Buffer.from(id, 'hex')}, 26 | attributes: ['_id'], 27 | transaction 28 | })) { 29 | return {type: 'transaction'} 30 | } 31 | } 32 | 33 | try { 34 | let address = Address.fromString(id, this.app.chain) 35 | if ([Address.CONTRACT, Address.EVM_CONTRACT].includes(address.type)) { 36 | let contract = await Contract.findOne({ 37 | where: {address: address.data}, 38 | attributes: ['address'], 39 | transaction 40 | }) 41 | if (contract) { 42 | return {type: 'contract'} 43 | } 44 | } else { 45 | return {type: 'address'} 46 | } 47 | } catch (err) {} 48 | 49 | let qrc20Results = (await QRC20.findAll({ 50 | where: { 51 | [$or]: [ 52 | where(fn('LOWER', fn('CONVERT', literal('name USING utf8mb4'))), id.toLowerCase()), 53 | where(fn('LOWER', fn('CONVERT', literal('symbol USING utf8mb4'))), id.toLowerCase()) 54 | ] 55 | }, 56 | attributes: ['contractAddress'], 57 | transaction 58 | })).map(qrc20 => qrc20.contractAddress) 59 | if (qrc20Results.length === 0) { 60 | qrc20Results = (await QRC20.findAll({ 61 | where: { 62 | [$or]: [ 63 | where(fn('LOWER', fn('CONVERT', literal('name USING utf8mb4'))), {[$like]: ['', ...id.toLowerCase(), ''].join('%')}), 64 | where(fn('LOWER', fn('CONVERT', literal('name USING utf8mb4'))), {[$like]: `%${id.toLowerCase()}%`}), 65 | where(fn('LOWER', fn('CONVERT', literal('symbol USING utf8mb4'))), {[$like]: ['', ...id.toLowerCase(), ''].join('%')}), 66 | where(fn('LOWER', fn('CONVERT', literal('symbol USING utf8mb4'))), {[$like]: `%${id.toLowerCase()}%`}) 67 | ] 68 | }, 69 | attributes: ['contractAddress'], 70 | transaction 71 | })).map(qrc20 => qrc20.contractAddress) 72 | } 73 | if (qrc20Results.length) { 74 | let [{addressHex}] = await db.query(sql` 75 | SELECT contract.address_string AS address, contract.address AS addressHex FROM ( 76 | SELECT contract_address FROM qrc20_statistics 77 | WHERE contract_address IN ${qrc20Results} 78 | ORDER BY transactions DESC LIMIT 1 79 | ) qrc20_balance 80 | INNER JOIN contract ON contract.address = qrc20_balance.contract_address 81 | `, {type: db.QueryTypes.SELECT, transaction}) 82 | return {type: 'contract', address: addressHex.toString('hex'), addressHex: addressHex.toString('hex')} 83 | } 84 | 85 | return {} 86 | } 87 | 88 | async getPrices() { 89 | let apiKey = this.app.config.cmcAPIKey 90 | if (!apiKey) { 91 | return {} 92 | } 93 | const coinId = 1684 94 | let [USDResult, CNYResult] = await Promise.all([ 95 | this.ctx.curl('https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest', { 96 | headers: { 97 | 'X-CMC_PRO_API_KEY': apiKey, 98 | Accept: 'application/json' 99 | }, 100 | data: { 101 | id: coinId, 102 | convert: 'USD' 103 | }, 104 | dataType: 'json' 105 | }), 106 | this.ctx.curl('https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest', { 107 | headers: { 108 | 'X-CMC_PRO_API_KEY': apiKey, 109 | Accept: 'application/json' 110 | }, 111 | data: { 112 | id: coinId, 113 | convert: 'CNY' 114 | }, 115 | dataType: 'json' 116 | }) 117 | ]) 118 | return { 119 | USD: USDResult.data.data[coinId].quote.USD.price, 120 | CNY: CNYResult.data.data[coinId].quote.CNY.price 121 | } 122 | } 123 | } 124 | 125 | module.exports = MiscService 126 | -------------------------------------------------------------------------------- /app/controller/block.js: -------------------------------------------------------------------------------- 1 | const {Controller} = require('egg') 2 | 3 | class BlockController extends Controller { 4 | async block() { 5 | const {ctx} = this 6 | let arg = ctx.params.block 7 | ctx.assert(arg, 404) 8 | if (/^(0|[1-9]\d{0,9})$/.test(arg)) { 9 | arg = Number.parseInt(arg) 10 | } else if (/^[0-9a-f]{64}$/i.test(arg)) { 11 | arg = Buffer.from(arg, 'hex') 12 | } else { 13 | ctx.throw(400) 14 | } 15 | let block = await ctx.service.block.getBlock(arg) 16 | ctx.assert(block, 404) 17 | ctx.body = { 18 | hash: block.hash.toString('hex'), 19 | height: block.height, 20 | version: block.version, 21 | prevHash: block.prevHash.toString('hex'), 22 | ...block.nextHash ? {nextHash: block.nextHash.toString('hex')} : {}, 23 | merkleRoot: block.merkleRoot.toString('hex'), 24 | timestamp: block.timestamp, 25 | bits: block.bits.toString(16), 26 | nonce: block.nonce, 27 | hashStateRoot: block.hashStateRoot.toString('hex'), 28 | hashUTXORoot: block.hashUTXORoot.toString('hex'), 29 | prevOutStakeHash: block.stakePrevTxId.toString('hex'), 30 | prevOutStakeN: block.stakeOutputIndex, 31 | signature: block.signature.toString('hex'), 32 | chainwork: block.chainwork.toString(16).padStart(64, '0'), 33 | flags: block.proofOfStake ? 'proof-of-stake' : 'proof-of-work', 34 | ...block.height > 0 ? {interval: block.interval} : {}, 35 | size: block.size, 36 | weight: block.weight, 37 | transactions: block.transactions.map(id => id.toString('hex')), 38 | miner: block.miner, 39 | difficulty: block.difficulty, 40 | reward: block.reward.toString(), 41 | confirmations: this.app.blockchainInfo.tip.height - block.height + 1 42 | } 43 | } 44 | 45 | async rawBlock() { 46 | const {ctx} = this 47 | let arg = ctx.params.block 48 | ctx.assert(arg, 404) 49 | if (/^(0|[1-9]\d{0,9})$/.test(arg)) { 50 | arg = Number.parseInt(arg) 51 | } else if (/^[0-9a-f]{64}$/i.test(arg)) { 52 | arg = Buffer.from(arg, 'hex') 53 | } else { 54 | ctx.throw(400) 55 | } 56 | let block = await ctx.service.block.getRawBlock(arg) 57 | ctx.assert(block, 404) 58 | ctx.body = block.toBuffer().toString('hex') 59 | } 60 | 61 | async list() { 62 | const {ctx} = this 63 | let date = ctx.query.date 64 | if (!date) { 65 | let d = new Date() 66 | let yyyy = d.getUTCFullYear().toString() 67 | let mm = (d.getUTCMonth() + 1).toString() 68 | let dd = d.getUTCDate().toString() 69 | date = `${yyyy}-${mm.padStart(2, '0')}-${dd.padStart(2, '0')}` 70 | } 71 | let min = Math.floor(Date.parse(date) / 1000) 72 | let max = min + 24 * 60 * 60 73 | let {blocks} = await ctx.service.block.listBlocks({min, max}) 74 | ctx.body = blocks.map(block => ({ 75 | hash: block.hash.toString('hex'), 76 | height: block.height, 77 | timestamp: block.timestamp, 78 | ...block.height > 0 ? {interval: block.interval} : {}, 79 | size: block.size, 80 | transactionCount: block.transactionsCount, 81 | miner: block.miner, 82 | reward: block.reward.toString() 83 | })) 84 | } 85 | 86 | async blockList() { 87 | const {ctx} = this 88 | let dateFilter = null 89 | let date = ctx.query.date 90 | if (date) { 91 | let min = Math.floor(Date.parse(date) / 1000) 92 | let max = min + 24 * 60 * 60 93 | dateFilter = {min, max} 94 | } 95 | let result = await ctx.service.block.listBlocks(dateFilter) 96 | ctx.body = { 97 | totalCount: result.totalCount, 98 | blocks: result.blocks.map(block => ({ 99 | hash: block.hash.toString('hex'), 100 | height: block.height, 101 | timestamp: block.timestamp, 102 | ...block.height > 0 ? {interval: block.interval} : {}, 103 | size: block.size, 104 | transactionCount: block.transactionsCount, 105 | miner: block.miner, 106 | reward: block.reward.toString() 107 | })) 108 | } 109 | } 110 | 111 | async recent() { 112 | const {ctx} = this 113 | let count = Number.parseInt(ctx.query.count || 10) 114 | let blocks = await ctx.service.block.getRecentBlocks(count) 115 | ctx.body = blocks.map(block => ({ 116 | hash: block.hash.toString('hex'), 117 | height: block.height, 118 | timestamp: block.timestamp, 119 | ...block.height > 0 ? {interval: block.interval} : {}, 120 | size: block.size, 121 | transactionCount: block.transactionsCount, 122 | miner: block.miner, 123 | reward: block.reward.toString() 124 | })) 125 | } 126 | } 127 | 128 | module.exports = BlockController 129 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "ecmaVersion": 2018 5 | }, 6 | "env": { 7 | "es6": true, 8 | "node": true 9 | }, 10 | "globals": { 11 | "BigInt": true 12 | }, 13 | "extends": "eslint:recommended", 14 | "rules": { 15 | "for-direction": "error", 16 | "getter-return": "warn", 17 | "no-console": "off", 18 | "no-constant-condition": ["warn", {"checkLoops": false}], 19 | "no-empty": ["warn", {"allowEmptyCatch": true}], 20 | "no-extra-parens": "warn", 21 | "require-atomic-updates": "off", 22 | "curly": ["error", "multi-line"], 23 | "dot-notation": "warn", 24 | "eqeqeq": ["error", "smart"], 25 | "guard-for-in": "error", 26 | "no-caller": "error", 27 | "no-empty-pattern": "error", 28 | "no-eval": "error", 29 | "no-extend-native": "error", 30 | "no-extra-label": "error", 31 | "no-floating-decimal": "warn", 32 | "no-implicit-coercion": "error", 33 | "no-implied-eval": "error", 34 | "no-labels": ["error", {"allowLoop": true}], 35 | "no-lone-blocks": "warn", 36 | "no-multi-spaces": "warn", 37 | "no-multi-str": "warn", 38 | "no-new-func": "error", 39 | "no-new-wrappers": "error", 40 | "no-octal-escape": "error", 41 | "no-return-assign": "error", 42 | "no-self-compare": "error", 43 | "no-sequences": "error", 44 | "no-unmodified-loop-condition": "warn", 45 | "no-useless-call": "error", 46 | "no-useless-concat": "warn", 47 | "no-useless-return": "warn", 48 | "no-void": "error", 49 | "no-with": "error", 50 | "wrap-iife": "error", 51 | "yoda": "error", 52 | "no-label-var": "error", 53 | "no-shadow-restricted-names": "error", 54 | "no-undef-init": "error", 55 | "no-unused-vars": "warn", 56 | "no-use-before-define": ["error", "nofunc"], 57 | "no-buffer-constructor": "error", 58 | "no-mixed-requires": "warn", 59 | "no-new-require": "error", 60 | "no-path-concat": "warn", 61 | "array-bracket-newline": ["warn", "consistent"], 62 | "array-bracket-spacing": "warn", 63 | "brace-style": ["warn", "1tbs", {"allowSingleLine": true}], 64 | "camelcase": "warn", 65 | "comma-spacing": "warn", 66 | "comma-style": "warn", 67 | "computed-property-spacing": "warn", 68 | "consistent-this": "warn", 69 | "eol-last": "error", 70 | "func-call-spacing": "warn", 71 | "func-name-matching": "warn", 72 | "func-style": ["warn", "declaration", {"allowArrowFunctions": true}], 73 | "implicit-arrow-linebreak": "warn", 74 | "indent": ["warn", 2], 75 | "key-spacing": "warn", 76 | "keyword-spacing": "warn", 77 | "linebreak-style": "error", 78 | "lines-between-class-members": "warn", 79 | "newline-per-chained-call": "warn", 80 | "no-array-constructor": "error", 81 | "no-lonely-if": "warn", 82 | "no-multiple-empty-lines": ["error", {"max": 2, "maxEOF": 1, "maxBOF": 0}], 83 | "no-negated-condition": "warn", 84 | "no-nested-ternary": "error", 85 | "no-new-object": "error", 86 | "no-tabs": "error", 87 | "no-trailing-spaces": "warn", 88 | "no-unneeded-ternary": "warn", 89 | "no-whitespace-before-property": "warn", 90 | "nonblock-statement-body-position": "error", 91 | "object-curly-newline": "warn", 92 | "object-curly-spacing": "warn", 93 | "one-var": ["warn", "never"], 94 | "operator-assignment": "warn", 95 | "operator-linebreak": ["warn", "before"], 96 | "padded-blocks": ["warn", "never"], 97 | "quote-props": ["warn", "as-needed"], 98 | "quotes": ["warn", "single", {"avoidEscape": true, "allowTemplateLiterals": true}], 99 | "semi": ["warn", "never"], 100 | "semi-spacing": "warn", 101 | "semi-style": "warn", 102 | "space-before-blocks": "warn", 103 | "space-before-function-paren": ["warn", { 104 | "anonymous": "never", 105 | "named": "never", 106 | "asyncArrow": "always" 107 | }], 108 | "space-in-parens": "warn", 109 | "space-infix-ops": "warn", 110 | "space-unary-ops": ["warn", {"words": true, "nonwords": false}], 111 | "switch-colon-spacing": "warn", 112 | "template-curly-spacing": "warn", 113 | "unicode-bom": "error", 114 | "arrow-body-style": ["warn", "as-needed"], 115 | "arrow-parens": ["warn", "as-needed"], 116 | "arrow-spacing": "warn", 117 | "generator-star-spacing": ["warn", {"before": false, "after": true}], 118 | "no-duplicate-imports": "warn", 119 | "no-useless-computed-key": "warn", 120 | "no-useless-constructor": "warn", 121 | "no-useless-rename": "warn", 122 | "no-var": "error", 123 | "object-shorthand": "warn", 124 | "prefer-arrow-callback": "warn", 125 | "prefer-numeric-literals": "error", 126 | "prefer-rest-params": "error", 127 | "prefer-spread": "warn", 128 | "prefer-template": "warn", 129 | "rest-spread-spacing": "warn", 130 | "template-tag-spacing": "warn", 131 | "yield-star-spacing": "warn" 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qtuminfo API Documentation 2 | 3 | * [Pagination Parameters](#pagination-parameters) 4 | * [Block / Timestamp Filter Parameters](#block--timestamp-filter-parameters) 5 | * [Blockchain](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/blockchain.md) 6 | * [Blockchain Information](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/blockchain.md#Blockchain-Information) 7 | * [Supply](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/blockchain.md#Supply) 8 | * [Total Max Supply](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/blockchain.md#Total-Max-Supply) 9 | * [Block](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/block.md) 10 | * [Block Information](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/block.md#Block-Information) 11 | * [Blocks of Date](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/block.md#Blocks-of-Date) 12 | * [Recent Blocks](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/block.md#Recent-Blocks) 13 | * [Transaction](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/transaction.md) 14 | * [Transaction Information](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/transaction.md#Transaction-Information) 15 | * [Raw Transaction](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/transaction.md#Raw-Transaction) 16 | * [Send Raw Transaction](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/transaction.md#Send-Raw-Transaction) 17 | * [Address](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/address.md) 18 | * [Address Information](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/address.md#Address-Information) 19 | * [Address Balance](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/address.md#Address-Balance) 20 | * [Address Transactions](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/address.md#Address-Transactions) 21 | * [Address Basic Transactions](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/address.md#Address-Basic-Transactions) 22 | * [Address Contract Transactions](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/address.md#Address-Contract-Transactions) 23 | * [Address QRC20 Token Transactions](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/address.md#Address-QRC20-Token-Transactions) 24 | * [Address UTXO List](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/address.md#Address-UTXO-List) 25 | * [Address Balance History](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/address.md#Address-Balance-History) 26 | * [Address QRC20 Balance History](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/address.md#Address-QRC20-Balance-History) 27 | * [Contract](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/contract.md) 28 | * [Contract Information](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/contract.md#Contract-Information) 29 | * [Contract Transactions](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/contract.md#Contract-Transactions) 30 | * [Contract Basic Transactions](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/contract.md#Contract-Basic-Transactions) 31 | * [Call Contract](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/contract.md#Call-Contract) 32 | * [Search Logs](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/contract.md#Search-Logs) 33 | * [QRC20](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/contract.md) 34 | * [QRC20 list](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/contract.md#QRC20-list) 35 | * [QRC20 Transaction list](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/contract.md#QRC20-Transaction-list) 36 | 37 | 38 | ## API Endpoint 39 | * `https://qtum.info/api/` for mainnet 40 | * `https://testnet.qtum.info/api/` for testnet 41 | 42 | 43 | ## Pagination Parameters 44 | 45 | You may use one of 3 forms below, all indices count from 0, maximum 100 records per page: 46 | * `limit=20&offset=40` 47 | * `from=40&to=59` 48 | * `pageSize=20&page=2` 49 | 50 | 51 | ## Block / Timestamp Filter Parameters 52 | 53 | These params are available in some transaction list queries, 54 | records are picked only when `fromBlock <= blockHeight <= toBlock`, `fromTime <= blockTimestamp <= toTime`. 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 |
NameTypeDescription
fromBlockNumber (optional)Search blocks from height
toBlockNumber (optional)Search blocks until height
fromTimeISO 8601 Date String (optional)Search blocks from timestamp
toTimeISO 8601 Date String (optional)Search blocks until timestamp
87 | -------------------------------------------------------------------------------- /app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const {router, controller, io, middleware} = app 3 | const addressMiddleware = middleware.address() 4 | const blockFilterMiddleware = middleware.blockFilter() 5 | const contractMiddleware = middleware.contract() 6 | const paginationMiddleware = middleware.pagination() 7 | 8 | router.get('/info', controller.info.index) 9 | router.get('/supply', controller.info.supply) 10 | router.get('/total-max-supply', controller.info.totalMaxSupply) 11 | router.get('/circulating-supply', controller.info.circulatingSupply) 12 | router.get('/feerates', controller.info.feeRates) 13 | 14 | router.get('/blocks', controller.block.list) 15 | router.get( 16 | '/block/list', 17 | paginationMiddleware, 18 | controller.block.blockList 19 | ) 20 | router.get('/block/:block', controller.block.block) 21 | router.get('/raw-block/:block', controller.block.rawBlock) 22 | router.get('/recent-blocks', controller.block.recent) 23 | 24 | router.get( 25 | '/tx/list', 26 | paginationMiddleware, 27 | controller.transaction.list 28 | ) 29 | router.get('/tx/:id', controller.transaction.transaction) 30 | router.get('/txs/:ids', controller.transaction.transactions) 31 | router.get('/raw-tx/:id', controller.transaction.rawTransaction) 32 | router.get('/recent-txs', controller.transaction.recent) 33 | router.post('/tx/send', controller.transaction.send) 34 | 35 | router.get( 36 | '/address/:address', 37 | addressMiddleware, 38 | controller.address.summary 39 | ) 40 | router.get( 41 | '/address/:address/balance', 42 | addressMiddleware, 43 | controller.address.balance 44 | ) 45 | router.get( 46 | '/address/:address/balance/total-received', 47 | addressMiddleware, 48 | controller.address.totalReceived 49 | ) 50 | router.get( 51 | '/address/:address/balance/total-sent', 52 | addressMiddleware, 53 | controller.address.totalSent 54 | ) 55 | router.get( 56 | '/address/:address/balance/unconfirmed', 57 | addressMiddleware, 58 | controller.address.unconfirmedBalance 59 | ) 60 | router.get( 61 | '/address/:address/balance/staking', 62 | addressMiddleware, 63 | controller.address.stakingBalance 64 | ) 65 | router.get( 66 | '/address/:address/balance/mature', 67 | addressMiddleware, 68 | controller.address.matureBalance 69 | ) 70 | router.get( 71 | '/address/:address/qrc20-balance/:token', 72 | addressMiddleware, middleware.contract('token'), 73 | controller.address.qrc20TokenBalance 74 | ) 75 | router.get( 76 | '/address/:address/txs', 77 | addressMiddleware, paginationMiddleware, blockFilterMiddleware, 78 | controller.address.transactions 79 | ) 80 | router.get( 81 | '/address/:address/basic-txs', 82 | addressMiddleware, paginationMiddleware, blockFilterMiddleware, 83 | controller.address.basicTransactions 84 | ) 85 | router.get( 86 | '/address/:address/contract-txs', 87 | addressMiddleware, paginationMiddleware, blockFilterMiddleware, 88 | controller.address.contractTransactions 89 | ) 90 | router.get( 91 | '/address/:address/contract-txs/:contract', 92 | addressMiddleware, contractMiddleware, paginationMiddleware, 93 | controller.address.contractTransactions 94 | ) 95 | router.get( 96 | '/address/:address/qrc20-txs/:token', 97 | addressMiddleware, middleware.contract('token'), paginationMiddleware, 98 | controller.address.qrc20TokenTransactions 99 | ) 100 | router.get( 101 | '/address/:address/qrc20-mempool-txs/:token', 102 | addressMiddleware, middleware.contract('token'), 103 | controller.address.qrc20TokenMempoolTransactions 104 | ) 105 | router.get( 106 | '/address/:address/utxo', 107 | addressMiddleware, 108 | controller.address.utxo 109 | ) 110 | router.get( 111 | '/address/:address/balance-history', 112 | addressMiddleware, paginationMiddleware, 113 | controller.address.balanceHistory 114 | ) 115 | router.get( 116 | '/address/:address/qrc20-balance-history', 117 | addressMiddleware, paginationMiddleware, 118 | controller.address.qrc20BalanceHistory 119 | ) 120 | router.get( 121 | '/address/:address/qrc20-balance-history/:token', 122 | addressMiddleware, middleware.contract('token'), paginationMiddleware, 123 | controller.address.qrc20BalanceHistory 124 | ) 125 | 126 | router.get( 127 | '/contract/:contract', 128 | contractMiddleware, 129 | controller.contract.summary 130 | ) 131 | router.get( 132 | '/contract/:contract/txs', 133 | contractMiddleware, paginationMiddleware, blockFilterMiddleware, 134 | controller.contract.transactions 135 | ) 136 | router.get( 137 | '/contract/:contract/basic-txs', 138 | contractMiddleware, paginationMiddleware, blockFilterMiddleware, 139 | controller.contract.basicTransactions 140 | ) 141 | router.get( 142 | '/contract/:contract/balance-history', 143 | contractMiddleware, paginationMiddleware, 144 | controller.contract.balanceHistory 145 | ) 146 | router.get( 147 | '/contract/:contract/qrc20-balance-history', 148 | contractMiddleware, paginationMiddleware, 149 | controller.contract.qrc20BalanceHistory 150 | ) 151 | router.get( 152 | '/contract/:contract/qrc20-balance-history/:token', 153 | contractMiddleware, middleware.contract('token'), paginationMiddleware, 154 | controller.contract.qrc20BalanceHistory 155 | ) 156 | router.get( 157 | '/contract/:contract/call', 158 | contractMiddleware, 159 | controller.contract.callContract 160 | ) 161 | router.get( 162 | '/searchlogs', 163 | paginationMiddleware, blockFilterMiddleware, 164 | controller.contract.searchLogs 165 | ) 166 | router.get( 167 | '/qrc20', 168 | paginationMiddleware, 169 | controller.qrc20.list 170 | ) 171 | router.get( 172 | '/qrc20/txs', 173 | paginationMiddleware, 174 | controller.qrc20.allTransactions 175 | ) 176 | router.get( 177 | '/qrc20/:token/txs', 178 | middleware.contract('token'), paginationMiddleware, blockFilterMiddleware, 179 | controller.qrc20.transactions 180 | ) 181 | router.get( 182 | '/qrc20/:token/rich-list', 183 | middleware.contract('token'), paginationMiddleware, 184 | controller.qrc20.richList 185 | ) 186 | router.get( 187 | '/qrc721', 188 | paginationMiddleware, 189 | controller.qrc721.list 190 | ) 191 | 192 | router.get(`/search`, controller.misc.classify) 193 | router.get( 194 | '/misc/rich-list', 195 | paginationMiddleware, 196 | controller.misc.richList 197 | ) 198 | router.get( 199 | '/misc/biggest-miners', 200 | paginationMiddleware, 201 | controller.misc.biggestMiners 202 | ) 203 | router.get('/misc/prices', controller.misc.prices) 204 | 205 | router.get('/stats/daily-transactions', controller.statistics.dailyTransactions) 206 | router.get('/stats/block-interval', controller.statistics.blockInterval) 207 | router.get('/stats/address-growth', controller.statistics.addressGrowth) 208 | 209 | io.route('subscribe', io.controller.default.subscribe) 210 | io.route('unsubscribe', io.controller.default.unsubscribe) 211 | } 212 | -------------------------------------------------------------------------------- /doc/block.md: -------------------------------------------------------------------------------- 1 | # Block API 2 | 3 | - [Block API](#Block-API) 4 | - [Block Information](#Block-Information) 5 | - [Blocks of Date](#Blocks-of-Date) 6 | - [Recent Blocks](#Recent-Blocks) 7 | 8 | 9 | ## Block Information 10 | 11 | **Request URL** 12 | ``` 13 | GET /block/:height 14 | GET /block/:hash 15 | ``` 16 | 17 | **Request** 18 | ``` 19 | GET /block/400000 20 | GET /block/14f9d58d8f96d3a685808a8be3e5f2743dd71cb1af54fb8a134b9a1bc8bc20b8 21 | ``` 22 | 23 | **Response** 24 | ```json 25 | { 26 | "hash": "14f9d58d8f96d3a685808a8be3e5f2743dd71cb1af54fb8a134b9a1bc8bc20b8", 27 | "height": 400000, 28 | "version": 536870912, 29 | "prevHash": "186d8797fc3280174c0dec83457c8a1e8c1bbd003baf930efce6f23d6cf2ac5a", 30 | "nextHash": "69e8f78ded5eb2f53f28b7bcde1a4299a61d9cde89d2a8a6c8ff2793fc777119", 31 | "merkleRoot": "93ce673230dbdd33c90fceca4a1504a8a40aa8ae51464f6f00848d239f08e0b8", 32 | "timestamp": 1561809392, 33 | "bits": "1a0aa152", 34 | "nonce": 0, 35 | "hashStateRoot": "ff18f1a8d8e6584f8f2c2e921257fd245668de5fb37c8d0800de675eaf673d21", 36 | "hashUTXORoot": "9e729950c184acd011471252a0c1a4bc279cd4c1e86d543bead4af6df787b2dd", 37 | "prevOutStakeHash": "9ed785ab666b13becffa83dc2aecc17a80cb434d4ba9670be455e15e730a2d2d", 38 | "prevOutStakeN": 1, 39 | "signature": "3044022027bb84b9e75477f3ca81c2bd12612593aa91daeafdb99feef7bbd2b560fb16ba022049e9af798574c8cfc70cd145d9cb223beff9ebf3576eada1c508e21c4edade46", 40 | "chainwork": "0000000000000000000000000000000000000000000001130f72ecb5f976a847", 41 | "flags": "proof-of-stake", 42 | "interval": 16, 43 | "size": 926, 44 | "weight": 3596, 45 | "transactions": [ 46 | "50c7c324f59729da9b30f72cda43de64ef69fa7b617428c69cd1946931ff40b2", 47 | "89a16f4ce26db8029a835df02f46e0416fe729eee1da8b95225cb35a30fc78f9" 48 | ], 49 | "miner": "Qhyek8Kn36vCKSdkcmbSFdR1XfKRKGX6hP", 50 | "difficulty": 1578241.9071624815, 51 | "reward": "400000000", 52 | "confirmations": 5970 53 | } 54 | ``` 55 | 56 | 57 | ## Blocks of Date 58 | 59 | **Request URL** 60 | ``` 61 | GET /blocks 62 | ``` 63 | **Request Params** 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 77 | 80 | 81 | 82 | 83 |
NameTypeDefault
75 | date 76 | 78 | ISO 8601 Date String (optional) 79 | Today
84 | 85 | **Request** 86 | ``` 87 | GET /blocks?date=2019-01-01 88 | ``` 89 | 90 | **Response** 91 | ```json 92 | [ 93 | { 94 | "hash": "7c22bfc2192145e1db6d9c1492a0697ef7d23893798c454433fab3bcc2c2b35d", 95 | "height": 292738, 96 | "timestamp": 1546387072, 97 | "interval": 208, 98 | "size": 1668, 99 | "transactionCount": 5, 100 | "miner": "QiRx4CWwVHNqWf3dLW12kcnwRovSw8w9K6", 101 | "reward": "402883776" 102 | }, 103 | { 104 | "hash": "27687c6f8a9ad7db5d278db9326f3ac9a0b701a851b29fe81b779f95ff759056", 105 | "height": 292737, 106 | "timestamp": 1546386864, 107 | "interval": 496, 108 | "size": 3178, 109 | "transactionCount": 5, 110 | "miner": "QYHV93kbN9osowPHTWHjeYzgrmZasdatov", 111 | "reward": "404394122" 112 | }, 113 | { 114 | "hash": "37051ffc1e77a552994a6700bc32a778196f2f7165d07b4df020b134bf662021", 115 | "height": 292736, 116 | "timestamp": 1546386368, 117 | "interval": 64, 118 | "size": 928, 119 | "transactionCount": 2, 120 | "miner": "QdB6krR2kgssX7xnx8E9JPrRktfSBRBrEa", 121 | "reward": "400000000" 122 | }, 123 | // ... 592 more items ... 124 | { 125 | "hash": "ed3d13dcc7b91ec384a034c32ff701b874977c06abf25fe5e621a390e147f7f7", 126 | "height": 292142, 127 | "timestamp": 1546301312, 128 | "interval": 16, 129 | "size": 1152, 130 | "transactionCount": 3, 131 | "miner": "Qgdj4FrXXxLVkxPDYtFNNGZNd9TJrNjtpM", 132 | "reward": "404833647" 133 | }, 134 | { 135 | "hash": "0fe877ba6c90dfc7de1411e8f69378ddf94aaff718c21d11577661ad82898da1", 136 | "height": 292141, 137 | "timestamp": 1546301296, 138 | "interval": 32, 139 | "size": 1597, 140 | "transactionCount": 4, 141 | "miner": "QNLZS9sVUTTZZQ6UfaDnAwTQifbKWFZwmP", 142 | "reward": "407149600" 143 | }, 144 | { 145 | "hash": "4a6f69b82f578c4edceaf03f0015f8ccc1ef613181ec69c64f95cc44e4c6ddc2", 146 | "height": 292140, 147 | "timestamp": 1546301264, 148 | "interval": 512, 149 | "size": 2008, 150 | "transactionCount": 6, 151 | "miner": "QgYB6x5RQBGgGL2R7Qe6WJAsUE4ZkdBj7Z", 152 | "reward": "422733104" 153 | } 154 | ] 155 | ``` 156 | 157 | 158 | ## Recent Blocks 159 | 160 | **Request URL** 161 | ``` 162 | GET /recent-blocks 163 | ``` 164 | **Request Params** 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 179 | 182 | 183 | 184 | 185 | 186 |
NameTypeDefaultDescription
177 | count 178 | 180 | Number (optional) 181 | 10Number of Recent Blocks
187 | 188 | **Request** 189 | ``` 190 | GET /recent-blocks?count=5 191 | ``` 192 | 193 | **Response** 194 | ```json 195 | [ 196 | { 197 | "hash": "cda36eef9029d120c6f0ba34392954aa2a929643f33a791d7f47332721a7ab86", 198 | "height": 405981, 199 | "timestamp": 1562661728, 200 | "interval": 32, 201 | "size": 882, 202 | "transactionCount": 2, 203 | "miner": "QX8U4uMCaQzoLv6rcBvN4dBk9xYPi8TkcR", 204 | "reward": "400000000" 205 | }, 206 | { 207 | "hash": "ca51ad6bcf5c300f8ffce4847dbe881b54cfac886746f09dc41e9323447f2ca2", 208 | "height": 405980, 209 | "timestamp": 1562661696, 210 | "interval": 112, 211 | "size": 1151, 212 | "transactionCount": 3, 213 | "miner": "QQrm6av1tWtTmvkTpft3FygcmLFcrEWGWk", 214 | "reward": "400090427" 215 | }, 216 | { 217 | "hash": "e7f60b634158ae80347ca84d30388f0b7c48563dcfa9183a4a33c755668636fb", 218 | "height": 405979, 219 | "timestamp": 1562661584, 220 | "interval": 80, 221 | "size": 1406, 222 | "transactionCount": 3, 223 | "miner": "QXDkSQAFEneCaPMC4ML6w8kmcrYxhsz4Qv", 224 | "reward": "402233609" 225 | }, 226 | { 227 | "hash": "96adbe135480a585372a67739a3d64d593f7c826c5542e9c5f156de711a7df81", 228 | "height": 405978, 229 | "timestamp": 1562661504, 230 | "interval": 320, 231 | "size": 11337, 232 | "transactionCount": 7, 233 | "miner": "QhUdHBgHcW7rTYxtzbs3By4nz9sggE5EbV", 234 | "reward": "408284149" 235 | }, 236 | { 237 | "hash": "9b3dab86fd71d24cb144ae44d8106241b52b9cc2c68a9924e1a371eb897ed4fe", 238 | "height": 405977, 239 | "timestamp": 1562661184, 240 | "interval": 240, 241 | "size": 1852, 242 | "transactionCount": 5, 243 | "miner": "QWKsTwTkH5n2qp1ivWp6ropknSsqKj9NXY", 244 | "reward": "400409061" 245 | } 246 | ] 247 | ``` 248 | -------------------------------------------------------------------------------- /doc/qrc20.md: -------------------------------------------------------------------------------- 1 | # QRC20 API 2 | 3 | - [QRC20 API](#QRC20-API) 4 | - [QRC20 list](#QRC20-list) 5 | - [QRC20 Transaction list](#QRC20-Transaction-list) 6 | 7 | 8 | ## QRC20 list 9 | List all qrc20 tokens order by transfer transactions count. 10 | 11 | **Request URL** 12 | ``` 13 | GET /qrc20 14 | ``` 15 | 16 | **Request Parameter** 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 32 | 33 | 34 |
NameDefaultDescription
28 | 29 | Pagination Parameters 30 | 31 |
35 | 36 | **Request** 37 | ``` 38 | GET /qrc20?limit=10&offset=0 39 | ``` 40 | 41 | **Response** 42 | ```json 43 | { 44 | "totalCount": 184, 45 | "tokens": [ 46 | { 47 | "address": "EfDYuWmSUbZPaAe2qzeWurcDGobSnhYa6F", 48 | "addressHex": "f2033ede578e17fa6231047265010445bca8cf1c", 49 | "name": "QCASH", 50 | "symbol": "QC", 51 | "decimals": 8, 52 | "totalSupply": "1000000000000000000", 53 | "version": null, 54 | "holders": 22612, 55 | "transactions": 133347 56 | }, 57 | { 58 | "address": "EgLnhSREpcpnrmSHp3uKQT1nPjhiV7DEx9", 59 | "addressHex": "fe59cbc1704e89a698571413a81f0de9d8f00c69", 60 | "name": "INK Coin", 61 | "symbol": "INK", 62 | "decimals": 9, 63 | "totalSupply": "1000000000000000000", 64 | "version": null, 65 | "holders": 33588, 66 | "transactions": 75033 67 | }, 68 | { 69 | "address": "ERPLiez3T9MQ21tz9B8kLmajK7xQWrkS6B", 70 | "addressHex": "5a4b7889cad562d6c099bf877c8f5e3d66d579f8", 71 | "name": "FENIX.CASH", 72 | "symbol": "FENIX", 73 | "decimals": 18, 74 | "totalSupply": "432000000000000000000000000", 75 | "version": null, 76 | "holders": 58118, 77 | "transactions": 60343 78 | }, 79 | { 80 | "address": "ER8xXcVq6HmXLfPUKmQpusvtxoHoAM9uC3", 81 | "addressHex": "57931faffdec114056a49adfcaa1caac159a1a25", 82 | "name": "SpaceCash", 83 | "symbol": "SPC", 84 | "decimals": 8, 85 | "totalSupply": "100000000000000000", 86 | "version": null, 87 | "holders": 12375, 88 | "transactions": 50625 89 | }, 90 | { 91 | "address": "ESxZUjVBnbeeEyx7E1WhwviAujfRmfCgjU", 92 | "addressHex": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7", 93 | "name": "Bodhi Token", 94 | "symbol": "BOT", 95 | "decimals": 8, 96 | "totalSupply": "10000000000000000", 97 | "version": null, 98 | "holders": 33219, 99 | "transactions": 49624 100 | }, 101 | { 102 | "address": "EfFoUtTKMVcWHKJeByQate1jdCkYTXHaBC", 103 | "addressHex": "f2703e93f87b846a7aacec1247beaec1c583daa4", 104 | "name": "Hyperpay", 105 | "symbol": "HPY", 106 | "decimals": 8, 107 | "totalSupply": "265000000000000000", 108 | "version": null, 109 | "holders": 7269, 110 | "transactions": 42171 111 | }, 112 | { 113 | "address": "EPr1RrYoNpHPiHFV1R1mHjJf6TfzFJXuTd", 114 | "addressHex": "49665919e437a4bedb92faa45ed33ebb5a33ee63", 115 | "name": "AWARE Token", 116 | "symbol": "AWR", 117 | "decimals": 8, 118 | "totalSupply": "100000000000000000", 119 | "version": "1.0", 120 | "holders": 1967, 121 | "transactions": 37871 122 | }, 123 | { 124 | "address": "EZRg6Xhqe3V7Sva29irwZHj9LwcoknARtR", 125 | "addressHex": "b27d7bf95b03e02b55d5eb63d3f1692762101bf9", 126 | "name": "Halal Chain", 127 | "symbol": "HLC", 128 | "decimals": 9, 129 | "totalSupply": "1000000000000000000", 130 | "version": null, 131 | "holders": 5404, 132 | "transactions": 30136 133 | }, 134 | { 135 | "address": "EMUWzpCfRZbgyQZd6oHdkXsuaJgVy8ncdL", 136 | "addressHex": "2f65a0af11d50d2d15962db39d7f7b0619ed55ae", 137 | "name": "MED TOKEN", 138 | "symbol": "MED", 139 | "decimals": 8, 140 | "totalSupply": "1000000000000000000", 141 | "version": null, 142 | "holders": 4801, 143 | "transactions": 27933 144 | }, 145 | { 146 | "address": "ETdR6C6aeX5MqCkNBYNBdTPa5rEdc1khne", 147 | "addressHex": "72e531e37c31ecbe336208fd66e93b48df3af420", 148 | "name": "Luna Stars", 149 | "symbol": "LSTR", 150 | "decimals": 8, 151 | "totalSupply": "3800000000000000000", 152 | "version": null, 153 | "holders": 13374, 154 | "transactions": 26867 155 | } 156 | ] 157 | } 158 | ``` 159 | 160 | 161 | ## QRC20 Transaction list 162 | 163 | **Request URL** 164 | ``` 165 | GET /qrc20/:token/txs 166 | ``` 167 | 168 | **Request Parameter** 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 184 | 185 | 186 | 191 | 192 | 193 | 196 | 199 | 200 | 201 | 202 |
NameDefaultDescription
180 | 181 | Pagination Parameters 182 | 183 |
187 | 188 | Block / Timestamp Filter Parameters 189 | 190 |
194 | reversed 195 | 197 | true 198 | Return records reversed
203 | 204 | **Request** 205 | ``` 206 | GET /qrc20/f2033ede578e17fa6231047265010445bca8cf1c/txs?limit=5&offset=0 207 | ``` 208 | 209 | **Response** 210 | ```json 211 | { 212 | "totalCount": 133563, 213 | "transactions": [ 214 | { 215 | "transactionId": "56a76c38966c663bafef3247c2475af45c8d1c81adaa7a27773b05853063d06f", 216 | "outputIndex": 0, 217 | "blockHeight": 408403, 218 | "blockHash": "7feac3a08ae8f5561c39e3c1a9c4c5d79c124bf8d79a671145db6933fdb08266", 219 | "timestamp": 1563009408, 220 | "from": "QWSTGRwdScLfdr6agUqR4G7ow4Mjc4e5re", 221 | "to": "QanTJBm9NTXqiFYNe9rWLi3dJPSTfoHZrL", 222 | "value": "373300000000" 223 | }, 224 | { 225 | "transactionId": "b3cff5534d7e7720edb177b0cf36fad1c6839926116bc39175e48ef541b0718a", 226 | "outputIndex": 0, 227 | "blockHeight": 408402, 228 | "blockHash": "f74fa86a8bb9eee226eebcdac76e10e25f966b791bc05b2ab16db9e00b396af6", 229 | "timestamp": 1563009328, 230 | "from": "QUZ6sqXN5dXWUAQniy9uPVpC3QjyXhoznz", 231 | "to": "QhXS93hPpUcjoDxo192bmrbDubhH5UoQDp", 232 | "value": "143458584740" 233 | }, 234 | { 235 | "transactionId": "8b79a1fc8f1381ff05fcc4f53430a6be072ca4ba5148037a48ab2b330b66dc60", 236 | "outputIndex": 0, 237 | "blockHeight": 408402, 238 | "blockHash": "f74fa86a8bb9eee226eebcdac76e10e25f966b791bc05b2ab16db9e00b396af6", 239 | "timestamp": 1563009328, 240 | "from": "QaDcm9UeGveyVWY9UiNRN9cTsFMe8v2k2t", 241 | "to": "QhXS93hPpUcjoDxo192bmrbDubhH5UoQDp", 242 | "value": "1097495687249" 243 | }, 244 | { 245 | "transactionId": "28948af9bee1cba22b2af04fc35d47bcb5b4dd5d2a817168a4238d141d7f5a62", 246 | "outputIndex": 1, 247 | "blockHeight": 408398, 248 | "blockHash": "4da2cc6042360ade09b4a48b2512fb8e950c97d196e6729718d7301296f0bc3e", 249 | "timestamp": 1563009056, 250 | "from": "QWSTGRwdScLfdr6agUqR4G7ow4Mjc4e5re", 251 | "to": "QUZ6sqXN5dXWUAQniy9uPVpC3QjyXhoznz", 252 | "value": "70729292370" 253 | }, 254 | { 255 | "transactionId": "801c94a6b6fa331f517b3dd85dd5ba778b541e913c6590feb9e26590cfad5050", 256 | "outputIndex": 1, 257 | "blockHeight": 408398, 258 | "blockHash": "4da2cc6042360ade09b4a48b2512fb8e950c97d196e6729718d7301296f0bc3e", 259 | "timestamp": 1563009056, 260 | "from": "QWSTGRwdScLfdr6agUqR4G7ow4Mjc4e5re", 261 | "to": "QTSbcq4y5i4SgxSc6Cz5yDV9LEQBKW4eQE", 262 | "value": "1000000000" 263 | } 264 | ] 265 | } 266 | ``` 267 | -------------------------------------------------------------------------------- /app/controller/contract.js: -------------------------------------------------------------------------------- 1 | const {Controller} = require('egg') 2 | 3 | class ContractController extends Controller { 4 | async summary() { 5 | const {ctx} = this 6 | let summary = await ctx.service.contract.getContractSummary( 7 | ctx.state.contract.contractAddress, ctx.state.contract.addressIds 8 | ) 9 | ctx.body = { 10 | address: summary.addressHex.toString('hex'), 11 | addressHex: summary.addressHex.toString('hex'), 12 | vm: summary.vm, 13 | type: summary.type, 14 | ...summary.type === 'qrc20' ? { 15 | qrc20: { 16 | name: summary.qrc20.name, 17 | symbol: summary.qrc20.symbol, 18 | decimals: summary.qrc20.decimals, 19 | totalSupply: summary.qrc20.totalSupply.toString(), 20 | version: summary.qrc20.version, 21 | holders: summary.qrc20.holders, 22 | transactions: summary.qrc20.transactions 23 | } 24 | } : {}, 25 | ...summary.type === 'qrc721' ? { 26 | qrc721: { 27 | name: summary.qrc721.name, 28 | symbol: summary.qrc721.symbol, 29 | totalSupply: summary.qrc721.totalSupply.toString() 30 | } 31 | } : {}, 32 | balance: summary.balance.toString(), 33 | totalReceived: summary.totalReceived.toString(), 34 | totalSent: summary.totalSent.toString(), 35 | unconfirmed: summary.unconfirmed.toString(), 36 | qrc20Balances: summary.qrc20Balances.map(item => ({ 37 | address: item.addressHex.toString('hex'), 38 | addressHex: item.addressHex.toString('hex'), 39 | name: item.name, 40 | symbol: item.symbol, 41 | decimals: item.decimals, 42 | balance: item.balance.toString() 43 | })), 44 | qrc721Balances: summary.qrc721Balances.map(item => ({ 45 | address: item.addressHex.toString('hex'), 46 | addressHex: item.addressHex.toString('hex'), 47 | name: item.name, 48 | symbol: item.symbol, 49 | count: item.count 50 | })), 51 | transactionCount: summary.transactionCount 52 | } 53 | } 54 | 55 | async transactions() { 56 | let {ctx} = this 57 | let {totalCount, transactions} = await ctx.service.contract.getContractTransactions( 58 | ctx.state.contract.contractAddress, ctx.state.contract.addressIds 59 | ) 60 | ctx.body = { 61 | totalCount, 62 | transactions: transactions.map(id => id.toString('hex')) 63 | } 64 | } 65 | 66 | async basicTransactions() { 67 | let {ctx} = this 68 | let {totalCount, transactions} = await ctx.service.contract.getContractBasicTransactions(ctx.state.contract.contractAddress) 69 | ctx.body = { 70 | totalCount, 71 | transactions: transactions.map(transaction => ({ 72 | transactionId: transaction.transactionId.toString('hex'), 73 | outputIndex: transaction.outputIndex, 74 | blockHeight: transaction.blockHeight, 75 | blockHash: transaction.blockHash && transaction.blockHash.toString('hex'), 76 | timestamp: transaction.timestamp, 77 | confirmations: transaction.confirmations, 78 | type: transaction.scriptPubKey.type, 79 | gasLimit: transaction.scriptPubKey.gasLimit, 80 | gasPrice: transaction.scriptPubKey.gasPrice, 81 | byteCode: transaction.scriptPubKey.byteCode.toString('hex'), 82 | outputValue: transaction.value.toString(), 83 | sender: transaction.sender.toString(), 84 | gasUsed: transaction.gasUsed, 85 | contractAddress: transaction.contractAddressHex.toString('hex'), 86 | contractAddressHex: transaction.contractAddressHex.toString('hex'), 87 | excepted: transaction.excepted, 88 | exceptedMessage: transaction.exceptedMessage, 89 | evmLogs: transaction.evmLogs.map(log => ({ 90 | address: log.addressHex.toString('hex'), 91 | addressHex: log.addressHex.toString('hex'), 92 | topics: log.topics.map(topic => topic.toString('hex')), 93 | data: log.data.toString('hex') 94 | })) 95 | })) 96 | } 97 | } 98 | 99 | async balanceHistory() { 100 | let {ctx} = this 101 | let {totalCount, transactions} = await ctx.service.balance.getBalanceHistory(ctx.state.contract.addressIds, {nonZero: true}) 102 | ctx.body = { 103 | totalCount, 104 | transactions: transactions.map(tx => ({ 105 | id: tx.id.toString('hex'), 106 | ...tx.block ? { 107 | blockHash: tx.block.hash.toString('hex'), 108 | blockHeight: tx.block.height, 109 | timestamp: tx.block.timestamp 110 | } : {}, 111 | amount: tx.amount.toString(), 112 | balance: tx.balance.toString() 113 | })) 114 | } 115 | } 116 | 117 | async qrc20BalanceHistory() { 118 | let {ctx} = this 119 | let tokenAddress = null 120 | if (ctx.state.token) { 121 | if (ctx.state.token.type === 'qrc20') { 122 | tokenAddress = ctx.state.token.contractAddress 123 | } else { 124 | ctx.body = { 125 | totalCount: 0, 126 | transactions: [] 127 | } 128 | return 129 | } 130 | } 131 | let {totalCount, transactions} = await ctx.service.qrc20.getQRC20BalanceHistory([ctx.state.contract.contractAddress], tokenAddress) 132 | ctx.body = { 133 | totalCount, 134 | transactions: transactions.map(tx => ({ 135 | id: tx.id.toString('hex'), 136 | hash: tx.block.hash.toString('hex'), 137 | height: tx.block.height, 138 | timestamp: tx.block.timestamp, 139 | tokens: tx.tokens.map(item => ({ 140 | address: item.addressHex.toString('hex'), 141 | addressHex: item.addressHex.toString('hex'), 142 | name: item.name, 143 | symbol: item.symbol, 144 | decimals: item.decimals, 145 | amount: item.amount.toString(), 146 | balance: item.balance.toString() 147 | })) 148 | })) 149 | } 150 | } 151 | 152 | async callContract() { 153 | const {Address} = this.app.qtuminfo.lib 154 | let {ctx} = this 155 | let {data, sender} = ctx.query 156 | ctx.assert(ctx.state.contract.vm === 'evm', 400) 157 | ctx.assert(/^([0-9a-f]{2})+$/i.test(data), 400) 158 | if (sender != null) { 159 | try { 160 | let address = Address.fromString(sender, this.app.chain) 161 | if ([Address.PAY_TO_PUBLIC_KEY_HASH, Address.CONTRACT, Address.EVM_CONTRACT].includes(address.type)) { 162 | sender = address.data 163 | } else { 164 | ctx.throw(400) 165 | } 166 | } catch (err) { 167 | ctx.throw(400) 168 | } 169 | } 170 | ctx.body = await ctx.service.contract.callContract(ctx.state.contract.contractAddress, data, sender) 171 | } 172 | 173 | async searchLogs() { 174 | let {ctx} = this 175 | let {contract, topic1, topic2, topic3, topic4} = this.ctx.query 176 | if (contract != null) { 177 | contract = (await ctx.service.contract.getContractAddresses([contract]))[0] 178 | } 179 | if (topic1 != null) { 180 | if (/^[0-9a-f]{64}$/i.test(topic1)) { 181 | topic1 = Buffer.from(topic1, 'hex') 182 | } else { 183 | ctx.throw(400) 184 | } 185 | } 186 | if (topic2 != null) { 187 | if (/^[0-9a-f]{64}$/i.test(topic2)) { 188 | topic2 = Buffer.from(topic2, 'hex') 189 | } else { 190 | ctx.throw(400) 191 | } 192 | } 193 | if (topic3 != null) { 194 | if (/^[0-9a-f]{64}$/i.test(topic3)) { 195 | topic3 = Buffer.from(topic3, 'hex') 196 | } else { 197 | ctx.throw(400) 198 | } 199 | } 200 | if (topic4 != null) { 201 | if (/^[0-9a-f]{64}$/i.test(topic4)) { 202 | topic4 = Buffer.from(topic4, 'hex') 203 | } else { 204 | ctx.throw(400) 205 | } 206 | } 207 | 208 | let {totalCount, logs} = await ctx.service.contract.searchLogs({contract, topic1, topic2, topic3, topic4}) 209 | ctx.body = { 210 | totalCount, 211 | logs: logs.map(log => ({ 212 | transactionId: log.transactionId.toString('hex'), 213 | outputIndex: log.outputIndex, 214 | blockHash: log.blockHash.toString('hex'), 215 | blockHeight: log.blockHeight, 216 | timestamp: log.timestamp, 217 | sender: log.sender.toString(), 218 | contractAddress: log.contractAddressHex.toString('hex'), 219 | contractAddressHex: log.contractAddressHex.toString('hex'), 220 | address: log.addressHex.toString('hex'), 221 | addressHex: log.addressHex.toString('hex'), 222 | topics: log.topics.map(topic => topic.toString('hex')), 223 | data: log.data.toString('hex') 224 | })) 225 | } 226 | } 227 | } 228 | 229 | module.exports = ContractController 230 | -------------------------------------------------------------------------------- /app/controller/address.js: -------------------------------------------------------------------------------- 1 | const {Controller} = require('egg') 2 | 3 | class AddressController extends Controller { 4 | async summary() { 5 | let {ctx} = this 6 | let {address} = ctx.state 7 | let summary = await ctx.service.address.getAddressSummary(address.addressIds, address.p2pkhAddressIds, address.rawAddresses) 8 | ctx.body = { 9 | balance: summary.balance.toString(), 10 | totalReceived: summary.totalReceived.toString(), 11 | totalSent: summary.totalSent.toString(), 12 | unconfirmed: summary.unconfirmed.toString(), 13 | staking: summary.staking.toString(), 14 | mature: summary.mature.toString(), 15 | qrc20Balances: summary.qrc20Balances.map(item => ({ 16 | address: item.addressHex.toString('hex'), 17 | addressHex: item.addressHex.toString('hex'), 18 | name: item.name, 19 | symbol: item.symbol, 20 | decimals: item.decimals, 21 | balance: item.balance.toString(), 22 | unconfirmed: { 23 | received: item.unconfirmed.received.toString(), 24 | sent: item.unconfirmed.sent.toString() 25 | }, 26 | isUnconfirmed: item.isUnconfirmed 27 | })), 28 | qrc721Balances: summary.qrc721Balances.map(item => ({ 29 | address: item.addressHex.toString('hex'), 30 | addressHex: item.addressHex.toString('hex'), 31 | name: item.name, 32 | symbol: item.symbol, 33 | count: item.count 34 | })), 35 | ranking: summary.ranking, 36 | transactionCount: summary.transactionCount, 37 | blocksMined: summary.blocksMined 38 | } 39 | } 40 | 41 | async balance() { 42 | let {ctx} = this 43 | let balance = await ctx.service.balance.getBalance(ctx.state.address.addressIds) 44 | ctx.body = balance.toString() 45 | } 46 | 47 | async totalReceived() { 48 | let {ctx} = this 49 | let {totalReceived} = await ctx.service.balance.getTotalBalanceChanges(ctx.state.address.addressIds) 50 | ctx.body = totalReceived.toString() 51 | } 52 | 53 | async totalSent() { 54 | let {ctx} = this 55 | let {totalSent} = await ctx.service.balance.getTotalBalanceChanges(ctx.state.address.addressIds) 56 | ctx.body = totalSent.toString() 57 | } 58 | 59 | async unconfirmedBalance() { 60 | let {ctx} = this 61 | let unconfirmed = await ctx.service.balance.getUnconfirmedBalance(ctx.state.address.addressIds) 62 | ctx.body = unconfirmed.toString() 63 | } 64 | 65 | async stakingBalance() { 66 | let {ctx} = this 67 | let unconfirmed = await ctx.service.balance.getStakingBalance(ctx.state.address.addressIds) 68 | ctx.body = unconfirmed.toString() 69 | } 70 | 71 | async matureBalance() { 72 | let {ctx} = this 73 | let unconfirmed = await ctx.service.balance.getMatureBalance(ctx.state.address.p2pkhAddressIds) 74 | ctx.body = unconfirmed.toString() 75 | } 76 | 77 | async qrc20TokenBalance() { 78 | let {ctx} = this 79 | let {address, token} = ctx.state 80 | if (token.type !== 'qrc20') { 81 | ctx.body = {} 82 | } 83 | let {name, symbol, decimals, balance, unconfirmed} = await ctx.service.qrc20.getQRC20Balance(address.rawAddresses, token.contractAddress) 84 | ctx.body = { 85 | name, 86 | symbol, 87 | decimals, 88 | balance: balance.toString(), 89 | unconfirmed: { 90 | received: unconfirmed.received.toString(), 91 | sent: unconfirmed.sent.toString() 92 | } 93 | } 94 | } 95 | 96 | async transactions() { 97 | let {ctx} = this 98 | let {address} = ctx.state 99 | let {totalCount, transactions} = await ctx.service.address.getAddressTransactions(address.addressIds, address.rawAddresses) 100 | ctx.body = { 101 | totalCount, 102 | transactions: transactions.map(id => id.toString('hex')) 103 | } 104 | } 105 | 106 | async basicTransactions() { 107 | let {ctx} = this 108 | let {totalCount, transactions} = await ctx.service.address.getAddressBasicTransactions(ctx.state.address.addressIds) 109 | ctx.body = { 110 | totalCount, 111 | transactions: transactions.map(transaction => ({ 112 | id: transaction.id.toString('hex'), 113 | blockHeight: transaction.blockHeight, 114 | blockHash: transaction.blockHash && transaction.blockHash.toString('hex'), 115 | timestamp: transaction.timestamp, 116 | confirmations: transaction.confirmations, 117 | amount: transaction.amount.toString(), 118 | inputValue: transaction.inputValue.toString(), 119 | outputValue: transaction.outputValue.toString(), 120 | refundValue: transaction.refundValue.toString(), 121 | fees: transaction.fees.toString(), 122 | type: transaction.type 123 | })) 124 | } 125 | } 126 | 127 | async contractTransactions() { 128 | let {ctx} = this 129 | let {address, contract} = ctx.state 130 | let {totalCount, transactions} = await ctx.service.address.getAddressContractTransactions(address.rawAddresses, contract) 131 | ctx.body = { 132 | totalCount, 133 | transactions: transactions.map(transaction => ({ 134 | transactionId: transaction.transactionId.toString('hex'), 135 | outputIndex: transaction.outputIndex, 136 | blockHeight: transaction.blockHeight, 137 | blockHash: transaction.blockHash && transaction.blockHash.toString('hex'), 138 | timestamp: transaction.timestamp, 139 | confirmations: transaction.confirmations, 140 | type: transaction.scriptPubKey.type, 141 | gasLimit: transaction.scriptPubKey.gasLimit, 142 | gasPrice: transaction.scriptPubKey.gasPrice, 143 | byteCode: transaction.scriptPubKey.byteCode.toString('hex'), 144 | outputValue: transaction.value.toString(), 145 | outputAddress: transaction.outputAddressHex.toString('hex'), 146 | outputAddressHex: transaction.outputAddressHex.toString('hex'), 147 | sender: transaction.sender.toString(), 148 | gasUsed: transaction.gasUsed, 149 | contractAddress: transaction.contractAddressHex.toString('hex'), 150 | contractAddressHex: transaction.contractAddressHex.toString('hex'), 151 | excepted: transaction.excepted, 152 | exceptedMessage: transaction.exceptedMessage, 153 | evmLogs: transaction.evmLogs.map(log => ({ 154 | address: log.addressHex.toString('hex'), 155 | addressHex: log.addressHex.toString('hex'), 156 | topics: log.topics.map(topic => topic.toString('hex')), 157 | data: log.data.toString('hex') 158 | })) 159 | })) 160 | } 161 | } 162 | 163 | async qrc20TokenTransactions() { 164 | let {ctx} = this 165 | let {address, token} = ctx.state 166 | let {totalCount, transactions} = await ctx.service.address.getAddressQRC20TokenTransactions(address.rawAddresses, token) 167 | ctx.body = { 168 | totalCount, 169 | transactions: transactions.map(transaction => ({ 170 | transactionId: transaction.transactionId.toString('hex'), 171 | outputIndex: transaction.outputIndex, 172 | blockHeight: transaction.blockHeight, 173 | blockHash: transaction.blockHash.toString('hex'), 174 | timestamp: transaction.timestamp, 175 | confirmations: transaction.confirmations, 176 | from: transaction.from, 177 | fromHex: transaction.fromHex && transaction.fromHex.toString('hex'), 178 | to: transaction.to, 179 | toHex: transaction.toHex && transaction.toHex.toString('hex'), 180 | value: transaction.value.toString(), 181 | amount: transaction.amount.toString() 182 | })) 183 | } 184 | } 185 | 186 | async qrc20TokenMempoolTransactions() { 187 | let {ctx} = this 188 | let {address, token} = ctx.state 189 | let transactions = await ctx.service.address.getAddressQRC20TokenMempoolTransactions(address.rawAddresses, token) 190 | ctx.body = transactions.map(transaction => ({ 191 | transactionId: transaction.transactionId.toString('hex'), 192 | outputIndex: transaction.outputIndex, 193 | from: transaction.from, 194 | fromHex: transaction.fromHex && transaction.fromHex.toString('hex'), 195 | to: transaction.to, 196 | toHex: transaction.toHex && transaction.toHex.toString('hex'), 197 | value: transaction.value.toString(), 198 | amount: transaction.amount.toString() 199 | })) 200 | } 201 | 202 | async utxo() { 203 | let {ctx} = this 204 | let utxos = await ctx.service.address.getUTXO(ctx.state.address.addressIds) 205 | ctx.body = utxos.map(utxo => ({ 206 | transactionId: utxo.transactionId.toString('hex'), 207 | outputIndex: utxo.outputIndex, 208 | scriptPubKey: utxo.scriptPubKey.toString('hex'), 209 | address: utxo.address, 210 | value: utxo.value.toString(), 211 | isStake: utxo.isStake, 212 | blockHeight: utxo.blockHeight, 213 | confirmations: utxo.confirmations 214 | })) 215 | } 216 | 217 | async balanceHistory() { 218 | let {ctx} = this 219 | let {totalCount, transactions} = await ctx.service.balance.getBalanceHistory(ctx.state.address.addressIds) 220 | ctx.body = { 221 | totalCount, 222 | transactions: transactions.map(tx => ({ 223 | id: tx.id.toString('hex'), 224 | ...tx.block ? { 225 | blockHash: tx.block.hash.toString('hex'), 226 | blockHeight: tx.block.height, 227 | timestamp: tx.block.timestamp 228 | } : {}, 229 | amount: tx.amount.toString(), 230 | balance: tx.balance.toString() 231 | })) 232 | } 233 | } 234 | 235 | async qrc20BalanceHistory() { 236 | const {Address} = this.app.qtuminfo.lib 237 | let {ctx} = this 238 | let tokenAddress = null 239 | if (ctx.state.token) { 240 | if (ctx.state.token.type === 'qrc20') { 241 | tokenAddress = ctx.state.token.contractAddress 242 | } else { 243 | ctx.body = { 244 | totalCount: 0, 245 | transactions: [] 246 | } 247 | return 248 | } 249 | } 250 | let hexAddresses = ctx.state.address.rawAddresses 251 | .filter(address => address.type === Address.PAY_TO_PUBLIC_KEY_HASH) 252 | .map(address => address.data) 253 | let {totalCount, transactions} = await ctx.service.qrc20.getQRC20BalanceHistory(hexAddresses, tokenAddress) 254 | ctx.body = { 255 | totalCount, 256 | transactions: transactions.map(tx => ({ 257 | id: tx.id.toString('hex'), 258 | blockHash: tx.block.hash.toString('hex'), 259 | blockHeight: tx.block.height, 260 | timestamp: tx.block.timestamp, 261 | tokens: tx.tokens.map(item => ({ 262 | address: item.addressHex.toString('hex'), 263 | addressHex: item.addressHex.toString('hex'), 264 | name: item.name, 265 | symbol: item.symbol, 266 | decimals: item.decimals, 267 | amount: item.amount.toString(), 268 | balance: item.balance.toString() 269 | })) 270 | })) 271 | } 272 | } 273 | } 274 | 275 | module.exports = AddressController 276 | -------------------------------------------------------------------------------- /app/service/balance.js: -------------------------------------------------------------------------------- 1 | const {Service} = require('egg') 2 | 3 | class BalanceService extends Service { 4 | async getBalance(ids) { 5 | const {TransactionOutput} = this.ctx.model 6 | const {in: $in, gt: $gt} = this.app.Sequelize.Op 7 | let result = await TransactionOutput.aggregate('value', 'SUM', { 8 | where: { 9 | addressId: {[$in]: ids}, 10 | blockHeight: {[$gt]: 0}, 11 | inputId: 0 12 | }, 13 | transaction: this.ctx.state.transaction 14 | }) 15 | return BigInt(result || 0) 16 | } 17 | 18 | async getTotalBalanceChanges(ids) { 19 | if (ids.length === 0) { 20 | return {totalReceived: 0n, totalSent: 0n} 21 | } 22 | 23 | const db = this.ctx.model 24 | const {sql} = this.ctx.helper 25 | let totalReceived 26 | let totalSent 27 | if (ids.length === 1) { 28 | let [result] = await db.query(sql` 29 | SELECT 30 | SUM(CAST(GREATEST(value, 0) AS DECIMAL(24))) AS totalReceived, 31 | SUM(CAST(GREATEST(-value, 0) AS DECIMAL(24))) AS totalSent 32 | FROM balance_change WHERE address_id = ${ids[0]} AND block_height > 0 33 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction}) 34 | totalReceived = result.totalReceived == null ? 0n : BigInt(result.totalReceived) 35 | totalSent = result.totalSent == null ? 0n : BigInt(result.totalSent) 36 | } else { 37 | let [result] = await db.query(sql` 38 | SELECT 39 | SUM(CAST(GREATEST(value, 0) AS DECIMAL(24))) AS totalReceived, 40 | SUM(CAST(GREATEST(-value, 0) AS DECIMAL(24))) AS totalSent 41 | FROM ( 42 | SELECT SUM(value) AS value FROM balance_change 43 | WHERE address_id IN ${ids} AND block_height > 0 44 | GROUP BY transaction_id 45 | ) AS temp 46 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction}) 47 | totalReceived = result.totalReceived == null ? 0n : BigInt(result.totalReceived) 48 | totalSent = result.totalSent == null ? 0n : BigInt(result.totalSent) 49 | } 50 | return {totalReceived, totalSent} 51 | } 52 | 53 | async getUnconfirmedBalance(ids) { 54 | const {TransactionOutput} = this.ctx.model 55 | const {in: $in} = this.app.Sequelize.Op 56 | let result = await TransactionOutput.aggregate('value', 'SUM', { 57 | where: { 58 | addressId: {[$in]: ids}, 59 | blockHeight: 0xffffffff, 60 | inputHeight: null 61 | }, 62 | transaction: this.ctx.state.transaction 63 | }) 64 | return BigInt(result || 0) 65 | } 66 | 67 | async getStakingBalance(ids) { 68 | const {TransactionOutput} = this.ctx.model 69 | const {in: $in, gt: $gt} = this.app.Sequelize.Op 70 | let result = await TransactionOutput.aggregate('value', 'SUM', { 71 | where: { 72 | addressId: {[$in]: ids}, 73 | blockHeight: {[$gt]: this.app.blockchainInfo.tip.height - 500}, 74 | inputHeight: null, 75 | isStake: true 76 | }, 77 | transaction: this.ctx.state.transaction 78 | }) 79 | return BigInt(result || 0) 80 | } 81 | 82 | async getMatureBalance(ids) { 83 | const {TransactionOutput} = this.ctx.model 84 | const {in: $in, between: $between} = this.app.Sequelize.Op 85 | let result = await TransactionOutput.aggregate('value', 'SUM', { 86 | where: { 87 | addressId: {[$in]: ids}, 88 | blockHeight: {[$between]: [1, this.app.blockchainInfo.tip.height - 500]}, 89 | inputHeight: null 90 | }, 91 | transaction: this.ctx.state.transaction 92 | }) 93 | return BigInt(result || 0) 94 | } 95 | 96 | async getBalanceHistory(ids, {nonZero = false} = {}) { 97 | if (ids.length === 0) { 98 | return [] 99 | } 100 | const db = this.ctx.model 101 | const {sql} = this.ctx.helper 102 | const {Header, Transaction, BalanceChange} = db 103 | const {in: $in, ne: $ne, gt: $gt} = this.app.Sequelize.Op 104 | let {limit, offset, reversed = true} = this.ctx.state.pagination 105 | let order = reversed ? 'DESC' : 'ASC' 106 | 107 | let totalCount 108 | let transactionIds 109 | let list 110 | if (ids.length === 1) { 111 | let valueFilter = nonZero ? {value: {[$ne]: 0}} : {} 112 | totalCount = await BalanceChange.count({ 113 | where: { 114 | addressId: ids[0], 115 | blockHeight: {[$gt]: 0}, 116 | ...valueFilter 117 | }, 118 | distinct: true, 119 | col: 'transactionId', 120 | transaction: this.ctx.state.transaction 121 | }) 122 | if (totalCount === 0) { 123 | return {totalCount: 0, transactions: []} 124 | } 125 | transactionIds = (await BalanceChange.findAll({ 126 | where: {addressId: ids[0], ...valueFilter}, 127 | attributes: ['transactionId'], 128 | order: [['blockHeight', order], ['indexInBlock', order], ['transactionId', order]], 129 | limit, 130 | offset, 131 | transaction: this.ctx.state.transaction 132 | })).map(({transactionId}) => transactionId) 133 | list = await BalanceChange.findAll({ 134 | where: {transactionId: {[$in]: transactionIds}, addressId: ids[0]}, 135 | attributes: ['transactionId', 'blockHeight', 'indexInBlock', 'value'], 136 | include: [ 137 | { 138 | model: Header, 139 | as: 'header', 140 | required: false, 141 | attributes: ['hash', 'timestamp'] 142 | }, 143 | { 144 | model: Transaction, 145 | as: 'transaction', 146 | required: true, 147 | attributes: ['id'] 148 | } 149 | ], 150 | order: [['blockHeight', order], ['indexInBlock', order], ['transactionId', order]], 151 | transaction: this.ctx.state.transaction 152 | }) 153 | } else { 154 | let havingFilter = nonZero ? 'SUM(value) != 0' : null 155 | if (havingFilter) { 156 | let [{count}] = await db.query(sql` 157 | SELECT COUNT(*) AS count FROM ( 158 | SELECT transaction_id FROM balance_change 159 | WHERE address_id IN ${ids} AND block_height > 0 160 | GROUP BY transaction_id 161 | HAVING ${{raw: havingFilter}} 162 | ) list 163 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction}) 164 | totalCount = count 165 | } else { 166 | totalCount = await BalanceChange.count({ 167 | where: {addressId: {[$in]: ids}, blockHeight: {[$gt]: 0}}, 168 | distinct: true, 169 | col: 'transactionId', 170 | transaction: this.ctx.state.transaction 171 | }) 172 | } 173 | if (totalCount === 0) { 174 | return {totalCount: 0, transactions: []} 175 | } 176 | if (havingFilter) { 177 | transactionIds = (await db.query(sql` 178 | SELECT MIN(block_height) AS block_height, MIN(index_in_block) AS index_in_block, transaction_id AS transactionId 179 | FROM balance_change 180 | WHERE address_id IN ${ids} AND block_height > 0 181 | GROUP BY transaction_id 182 | HAVING ${{raw: havingFilter}} 183 | ORDER BY block_height ${{raw: order}}, index_in_block ${{raw: order}}, transaction_id ${{raw: order}} 184 | LIMIT ${offset}, ${limit} 185 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction})).map(({transactionId}) => transactionId) 186 | } else { 187 | transactionIds = (await BalanceChange.findAll({ 188 | where: {addressId: {[$in]: ids}}, 189 | attributes: ['transactionId'], 190 | order: [['blockHeight', order], ['indexInBlock', order], ['transactionId', order]], 191 | limit, 192 | offset, 193 | transaction: this.ctx.state.transaction 194 | })).map(({transactionId}) => transactionId) 195 | } 196 | list = await db.query(sql` 197 | SELECT 198 | transaction.id AS id, transaction.block_height AS blockHeight, 199 | transaction.index_in_block AS indexInBlock, transaction._id AS transactionId, 200 | header.hash AS blockHash, header.timestamp AS timestamp, 201 | list.value AS value 202 | FROM ( 203 | SELECT MIN(block_height) AS block_height, MIN(index_in_block) AS index_in_block, transaction_id, SUM(value) AS value 204 | FROM balance_change 205 | WHERE transaction_id IN ${transactionIds} AND address_id IN ${ids} 206 | GROUP BY transaction_id 207 | ORDER BY block_height ${{raw: order}}, index_in_block ${{raw: order}}, transaction_id ${{raw: order}} 208 | ) list 209 | INNER JOIN transaction ON transaction._id = list.transaction_id 210 | LEFT JOIN header ON header.height = transaction.block_height 211 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction}) 212 | } 213 | 214 | if (reversed) { 215 | list = list.reverse() 216 | } 217 | let initialBalance = 0n 218 | if (list.length > 0) { 219 | let {blockHeight, indexInBlock, transactionId} = list[0] 220 | let [{value}] = await db.query(sql` 221 | SELECT SUM(value) AS value FROM balance_change 222 | WHERE address_id IN ${ids} 223 | AND (block_height, index_in_block, transaction_id) < (${blockHeight}, ${indexInBlock}, ${transactionId}) 224 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction}) 225 | initialBalance = BigInt(value || 0n) 226 | } 227 | let transactions = list.map(item => ({ 228 | id: item.id || item.transaction.id, 229 | ...item.header ? { 230 | block: { 231 | hash: item.header.hash, 232 | height: item.blockHeight, 233 | timestamp: item.header.timestamp 234 | } 235 | } : {}, 236 | ...item.blockHash ? { 237 | block: { 238 | hash: item.blockHash, 239 | height: item.blockHeight, 240 | timestamp: item.timestamp 241 | } 242 | } : {}, 243 | amount: BigInt(item.value), 244 | })) 245 | for (let tx of transactions) { 246 | tx.balance = initialBalance += tx.amount 247 | } 248 | if (reversed) { 249 | transactions = transactions.reverse() 250 | } 251 | return {totalCount, transactions} 252 | } 253 | 254 | async getRichList() { 255 | const db = this.ctx.model 256 | const {RichList} = db 257 | const {sql} = this.ctx.helper 258 | let {limit, offset} = this.ctx.state.pagination 259 | let totalCount = await RichList.count({transaction: this.ctx.state.transaction}) 260 | let list = await db.query(sql` 261 | SELECT address.string AS address, rich_list.balance AS balance 262 | FROM (SELECT address_id FROM rich_list ORDER BY balance DESC LIMIT ${offset}, ${limit}) list 263 | INNER JOIN rich_list USING (address_id) 264 | INNER JOIN address ON address._id = list.address_id 265 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction}) 266 | return { 267 | totalCount, 268 | list: list.map(item => ({ 269 | address: item.address, 270 | balance: BigInt(item.balance) 271 | })) 272 | } 273 | } 274 | 275 | async updateRichList() { 276 | const db = this.ctx.model 277 | const {Address, RichList} = db 278 | const {sql} = this.ctx.helper 279 | let transaction = await db.transaction() 280 | try { 281 | const blockHeight = this.app.blockchainInfo.tip.height 282 | let list = await db.query(sql` 283 | SELECT list.address_id AS addressId, list.balance AS balance 284 | FROM ( 285 | SELECT address_id, SUM(value) AS balance 286 | FROM transaction_output 287 | WHERE 288 | address_id > 0 289 | AND (input_height IS NULL OR input_height > ${blockHeight}) 290 | AND (block_height BETWEEN 1 AND ${blockHeight}) 291 | AND value > 0 292 | GROUP BY address_id 293 | ) list 294 | INNER JOIN address ON address._id = list.address_id 295 | WHERE address.type < ${Address.parseType('contract')} 296 | `, {type: db.QueryTypes.SELECT, transaction}) 297 | await db.query(sql`DELETE FROM rich_list`, {transaction}) 298 | await RichList.bulkCreate( 299 | list.map(({addressId, balance}) => ({addressId, balance: BigInt(balance)})), 300 | {validate: false, transaction, logging: false} 301 | ) 302 | await transaction.commit() 303 | } catch (err) { 304 | await transaction.rollback() 305 | } 306 | } 307 | 308 | async getBalanceRanking(addressIds) { 309 | if (addressIds.length !== 1) { 310 | return null 311 | } 312 | const {RichList} = this.ctx.model 313 | const {gt: $gt} = this.app.Sequelize.Op 314 | let item = await RichList.findOne({ 315 | where: {addressId: addressIds[0]}, 316 | attributes: ['balance'], 317 | transaction: this.ctx.state.transaction 318 | }) 319 | if (item == null) { 320 | return null 321 | } else { 322 | return await RichList.count({ 323 | where: {balance: {[$gt]: item.balance.toString()}}, 324 | transaction: this.ctx.state.transaction 325 | }) + 1 326 | } 327 | } 328 | } 329 | 330 | module.exports = BalanceService 331 | -------------------------------------------------------------------------------- /doc/transaction.md: -------------------------------------------------------------------------------- 1 | # Transaction API 2 | 3 | - [Transaction API](#transaction-api) 4 | - [Transaction Information](#transaction-information) 5 | - [Raw Transaction](#raw-transaction) 6 | - [Send Raw Transaction](#send-raw-transaction) 7 | 8 | 9 | ## Transaction Information 10 | 11 | **Request URL** 12 | ``` 13 | GET /tx/:id 14 | ``` 15 | 16 | **Request #1** 17 | ``` 18 | GET /tx/870c6b51d4160b52ce2bd506d0cd7a8438b8aac9afd03c4695f6ab9648bd02dc 19 | ``` 20 | 21 | **Response #1** 22 | ```json 23 | { 24 | "id": "870c6b51d4160b52ce2bd506d0cd7a8438b8aac9afd03c4695f6ab9648bd02dc", 25 | "hash": "35559de74dd0d90908a25d39fd471829e351bffb3a3874bf54764b8d79f923aa", 26 | "version": 2, 27 | "lockTime": 246987, 28 | "blockHash": "8275eb2950279df37b8fa62076249ec7076cc030c860df004b8f3b03071bd00b", 29 | "inputs": [ 30 | { 31 | "prevTxId": "14383d782ff38067d928acffabb31b46196966fc4a31d9de855a0cbf0535922d", 32 | "outputIndex": 0, 33 | "value": "910800", 34 | "address": "qc1ql8v7fp2j2uk7gjqv5rudlecv09e9x4jdwya5vd", 35 | "scriptSig": { 36 | "type": "witness_v0_keyhash", 37 | "hex": "", 38 | "asm": "" 39 | }, 40 | "sequence": 4294967294, 41 | "witness": [ 42 | "3044022030bf256669dd7fc6ca00cd393461c09b25421e59696208e8b5cbdbf71ff5050302200707c5d085d7ed06d1e6af9a3ca1c66b0c2e1a73189b24795afc80c8080236a101", 43 | "038c9dfdb356979a56d4b94063b8fb214295bd6a310b315564083a63795556ae93" 44 | ] 45 | }, 46 | { 47 | "prevTxId": "14383d782ff38067d928acffabb31b46196966fc4a31d9de855a0cbf0535922d", 48 | "outputIndex": 1, 49 | "value": "1484306", 50 | "address": "QjHokSvA9ZGuVZhNWPsv2e8wmiLHnusurS", 51 | "scriptSig": { 52 | "type": "pubkeyhash", 53 | "hex": "47304402203db3536426e26c17a670ab3127109809f76ce366a55483a5ef9d556ea432cb240220546ae9f85150b21d3692fd89f61b947840d9b96886f830413e444536e97d3ecb0121027d026b7753f3f70ba972c801e9aed0c715dc68b89447bc0c5c18b32f25f3133c", 54 | "asm": "304402203db3536426e26c17a670ab3127109809f76ce366a55483a5ef9d556ea432cb240220546ae9f85150b21d3692fd89f61b947840d9b96886f830413e444536e97d3ecb[ALL] 027d026b7753f3f70ba972c801e9aed0c715dc68b89447bc0c5c18b32f25f3133c" 55 | }, 56 | "sequence": 4294967294, 57 | "witness": [] 58 | }, 59 | { 60 | "prevTxId": "2e1a1985c50a33e342990a475888d74b79e3b122326fc585342a39c71cb5a76e", 61 | "outputIndex": 0, 62 | "value": "1910800", 63 | "address": "qc1ql8v7fp2j2uk7gjqv5rudlecv09e9x4jdwya5vd", 64 | "scriptSig": { 65 | "type": "witness_v0_keyhash", 66 | "hex": "", 67 | "asm": "" 68 | }, 69 | "sequence": 4294967294, 70 | "witness": [ 71 | "30440220301dafcfe858ca57a1500b64f4dc3331fbf65670169362f4add3204c05b0ccf302207090d1047ee3c509b58d88734fa11afe1c1933ac8026c42ce745c46f30df9f8401", 72 | "038c9dfdb356979a56d4b94063b8fb214295bd6a310b315564083a63795556ae93" 73 | ] 74 | } 75 | ], 76 | "outputs": [ 77 | { 78 | "value": "305906", 79 | "address": "qc1ql8v7fp2j2uk7gjqv5rudlecv09e9x4jdwya5vd", 80 | "scriptPubKey": { 81 | "type": "witness_v0_keyhash", 82 | "hex": "0014f9d9e48552572de4480ca0f8dfe70c797253564d", 83 | "asm": "OP_0 f9d9e48552572de4480ca0f8dfe70c797253564d" 84 | }, 85 | "spentTxId": "adcf52f3b284195fa8a7b2a4664252f3959ae618c321b35e1deffadec717f3dd", 86 | "spentIndex": 0 87 | }, 88 | { 89 | "value": "3857200", 90 | "address": "qc1ql8v7fp2j2uk7gjqv5rudlecv09e9x4jdwya5vd", 91 | "scriptPubKey": { 92 | "type": "witness_v0_keyhash", 93 | "hex": "0014f9d9e48552572de4480ca0f8dfe70c797253564d", 94 | "asm": "OP_0 f9d9e48552572de4480ca0f8dfe70c797253564d" 95 | }, 96 | "spentTxId": "adcf52f3b284195fa8a7b2a4664252f3959ae618c321b35e1deffadec717f3dd", 97 | "spentIndex": 1 98 | } 99 | ], 100 | "isCoinbase": false, 101 | "isCoinstake": false, 102 | "blockHeight": 246989, 103 | "confirmations": 159098, 104 | "timestamp": 1539787024, 105 | "inputValue": "4305906", 106 | "outputValue": "4163106", 107 | "refundValue": "0", 108 | "fees": "142800", 109 | "size": 518, 110 | "weight": 1421 111 | } 112 | ``` 113 | 114 | **Request #2** 115 | ``` 116 | GET /tx/f56ea462337e4732e821eb7ceee5208a5c807fe5f918a342298eb152d75765ee 117 | ``` 118 | 119 | **Response #2** 120 | ```json 121 | { 122 | "id": "f56ea462337e4732e821eb7ceee5208a5c807fe5f918a342298eb152d75765ee", 123 | "hash": "f56ea462337e4732e821eb7ceee5208a5c807fe5f918a342298eb152d75765ee", 124 | "version": 2, 125 | "lockTime": 168583, 126 | "blockHash": "67833537107b014a9e3a1666c99f02708a788f81a942a6fa500a0e7c5e5446c4", 127 | "inputs": [ 128 | { 129 | "prevTxId": "732fe02e70095557854e419d472388cccc3f8fc00db4b9b1820b9a51c0f1b905", 130 | "outputIndex": 1, 131 | "value": "500000000000", 132 | "address": "QccDD4Vk5Tc5Y84ydAcj4hpNkahYcsCsRq", 133 | "scriptSig": { 134 | "type": "pubkeyhash", 135 | "hex": "483045022100cc4208e82c8d6aadbb6e5ed1465ca1675db472f9e72cef528d52973e632bbafc02202d2a894267a2d3cd1e647ef3a3206d83228c71006a4c3544616dcb58d0b9dbf9012103afb25cf82520925420f07f4b13b17efa0d7868606c66adf7979e15fb3f21721e", 136 | "asm": "3045022100cc4208e82c8d6aadbb6e5ed1465ca1675db472f9e72cef528d52973e632bbafc02202d2a894267a2d3cd1e647ef3a3206d83228c71006a4c3544616dcb58d0b9dbf9[ALL] 03afb25cf82520925420f07f4b13b17efa0d7868606c66adf7979e15fb3f21721e" 137 | }, 138 | "sequence": 4294967294 139 | } 140 | ], 141 | "outputs": [ 142 | { 143 | "value": "399989865575", 144 | "address": "QccDD4Vk5Tc5Y84ydAcj4hpNkahYcsCsRq", 145 | "scriptPubKey": { 146 | "type": "pubkeyhash", 147 | "hex": "76a914af8cf283ef7d1ad9fa824bd4de564f3b1b9fcd7a88ac", 148 | "asm": "OP_DUP OP_HASH160 af8cf283ef7d1ad9fa824bd4de564f3b1b9fcd7a OP_EQUALVERIFY OP_CHECKSIG" 149 | }, 150 | "spentTxId": "eae244f9996cf4b50bf3c35d001aa1d8f60fbd9b7da9e0a37ad4ebb336082a6e", 151 | "spentIndex": 0 152 | }, 153 | { 154 | "value": "100000000000", 155 | "address": "0439fcc94493859d9146b6b9a92daa6d6d7b581d", 156 | "addressHex": "0439fcc94493859d9146b6b9a92daa6d6d7b581d", 157 | "scriptPubKey": { 158 | "type": "evm_call", 159 | "hex": "01040390d003012824d0821b0e0000000000000000000000000000000000000000000000000000000000000001140439fcc94493859d9146b6b9a92daa6d6d7b581dc2", 160 | "asm": "4 250000 40 d0821b0e0000000000000000000000000000000000000000000000000000000000000001 0439fcc94493859d9146b6b9a92daa6d6d7b581d OP_CALL" 161 | }, 162 | "spentTxId": "4f263d6cfa910e5b763edb1349d34d9a0c97e1ad5a3eb9fd6b2347155da5a246", 163 | "spentIndex": 1, 164 | "receipt": { 165 | "sender": "QccDD4Vk5Tc5Y84ydAcj4hpNkahYcsCsRq", 166 | "gasUsed": 94008, 167 | "contractAddress": "0439fcc94493859d9146b6b9a92daa6d6d7b581d", 168 | "contractAddressHex": "0439fcc94493859d9146b6b9a92daa6d6d7b581d", 169 | "excepted": "None", 170 | "exceptedMessage": "", 171 | "logs": [ 172 | { 173 | "address": "0439fcc94493859d9146b6b9a92daa6d6d7b581d", 174 | "addressHex": "0439fcc94493859d9146b6b9a92daa6d6d7b581d", 175 | "topics": [ 176 | "fb425c0bd6840437c799f5176836b0ebc76d79351a6981cc4e5fbb0cdbf3e185", 177 | "0000000000000000000000000000000000000000000000000000000000000000", 178 | "0000000000000000000000000439fcc94493859d9146b6b9a92daa6d6d7b581d", 179 | "000000000000000000000000af8cf283ef7d1ad9fa824bd4de564f3b1b9fcd7a" 180 | ], 181 | "data": "0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000174876e8005154554d00000000000000000000000000000000000000000000000000000000" 182 | } 183 | ] 184 | } 185 | } 186 | ], 187 | "isCoinbase": false, 188 | "isCoinstake": false, 189 | "blockHeight": 168584, 190 | "confirmations": 237507, 191 | "timestamp": 1528460544, 192 | "inputValue": "500000000000", 193 | "outputValue": "499989865575", 194 | "refundValue": "6239680", 195 | "fees": "3894745", 196 | "size": 268, 197 | "weight": 1072, 198 | "contractSpends": [ 199 | { 200 | "inputs": [ 201 | { 202 | "address": "056168620105d8f73a55d8c6542b565aea3665ec", 203 | "addressHex": "056168620105d8f73a55d8c6542b565aea3665ec", 204 | "value": "202560000000" 205 | }, 206 | { 207 | "address": "0439fcc94493859d9146b6b9a92daa6d6d7b581d", 208 | "addressHex": "0439fcc94493859d9146b6b9a92daa6d6d7b581d", 209 | "value": "100000000000" 210 | } 211 | ], 212 | "outputs": [ 213 | { 214 | "address": "056168620105d8f73a55d8c6542b565aea3665ec", 215 | "addressHex": "056168620105d8f73a55d8c6542b565aea3665ec", 216 | "value": "302560000000" 217 | } 218 | ] 219 | } 220 | ] 221 | } 222 | ``` 223 | 224 | **Request #3** 225 | ``` 226 | GET /tx/ebf05fbf8dcf10f03a73331abd1ea934d66c03a72ee4a57addeab0225ad9289f 227 | ``` 228 | 229 | **Response #3** 230 | ```json 231 | { 232 | "id": "ebf05fbf8dcf10f03a73331abd1ea934d66c03a72ee4a57addeab0225ad9289f", 233 | "hash": "ebf05fbf8dcf10f03a73331abd1ea934d66c03a72ee4a57addeab0225ad9289f", 234 | "version": 2, 235 | "lockTime": 406076, 236 | "blockHash": "7fae36223bbd95b82d9899b767d4289d4fe4cd2b5ee6e248c16694d7352b3b94", 237 | "inputs": [ 238 | { 239 | "prevTxId": "6ae7a9cf0763b13185b23f345180607caa71c1cacff9ef6e4a9f9c068bd45c62", 240 | "outputIndex": 1, 241 | "value": "97820400", 242 | "address": "QeZjXLEyqKgUNyoXcTCC2nxVx77cSnmpcy", 243 | "scriptSig": { 244 | "type": "pubkeyhash", 245 | "hex": "47304402206897a7a9502314e3d5f0d62e2fae485746cbbd9524229ab42187f5a766c9ec1d02206701c50e1947dd08c4440db66bf8a534e4a2c95a0779cf1963c229a4ebcc9bda012103d2afef396be37192a1137fe103ace0dd2e861088d0634ffd792d4d43a8bed770", 246 | "asm": "304402206897a7a9502314e3d5f0d62e2fae485746cbbd9524229ab42187f5a766c9ec1d02206701c50e1947dd08c4440db66bf8a534e4a2c95a0779cf1963c229a4ebcc9bda[ALL] 03d2afef396be37192a1137fe103ace0dd2e861088d0634ffd792d4d43a8bed770" 247 | }, 248 | "sequence": 4294967294 249 | } 250 | ], 251 | "outputs": [ 252 | { 253 | "value": "0", 254 | "address": "EfDYuWmSUbZPaAe2qzeWurcDGobSnhYa6F", 255 | "addressHex": "f2033ede578e17fa6231047265010445bca8cf1c", 256 | "scriptPubKey": { 257 | "type": "evm_call", 258 | "hex": "0104032cc900012844a9059cbb000000000000000000000000bf4e5cb019865cde870642bf2a2dfb375789c23b00000000000000000000000000000000000000000000000000000002540be40014f2033ede578e17fa6231047265010445bca8cf1cc2", 259 | "asm": "4 51500 40 a9059cbb000000000000000000000000bf4e5cb019865cde870642bf2a2dfb375789c23b00000000000000000000000000000000000000000000000000000002540be400 f2033ede578e17fa6231047265010445bca8cf1c OP_CALL" 260 | }, 261 | "receipt": { 262 | "sender": "QeZjXLEyqKgUNyoXcTCC2nxVx77cSnmpcy", 263 | "gasUsed": 36359, 264 | "contractAddress": "f2033ede578e17fa6231047265010445bca8cf1c", 265 | "contractAddressHex": "f2033ede578e17fa6231047265010445bca8cf1c", 266 | "excepted": "None", 267 | "exceptedMessage": "", 268 | "logs": [ 269 | { 270 | "address": "f2033ede578e17fa6231047265010445bca8cf1c", 271 | "addressHex": "f2033ede578e17fa6231047265010445bca8cf1c", 272 | "topics": [ 273 | "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", 274 | "000000000000000000000000c50541b82f4cda2f61cbbc219595c79ffdb4f2fa", 275 | "000000000000000000000000bf4e5cb019865cde870642bf2a2dfb375789c23b" 276 | ], 277 | "data": "00000000000000000000000000000000000000000000000000000002540be400" 278 | } 279 | ] 280 | } 281 | }, 282 | { 283 | "value": "95640800", 284 | "address": "QeZjXLEyqKgUNyoXcTCC2nxVx77cSnmpcy", 285 | "scriptPubKey": { 286 | "type": "pubkeyhash", 287 | "hex": "76a914c50541b82f4cda2f61cbbc219595c79ffdb4f2fa88ac", 288 | "asm": "OP_DUP OP_HASH160 c50541b82f4cda2f61cbbc219595c79ffdb4f2fa OP_EQUALVERIFY OP_CHECKSIG" 289 | } 290 | } 291 | ], 292 | "isCoinbase": false, 293 | "isCoinstake": false, 294 | "blockHeight": 406077, 295 | "confirmations": 16, 296 | "timestamp": 1562675696, 297 | "inputValue": "97820400", 298 | "outputValue": "95640800", 299 | "refundValue": "605640", 300 | "fees": "1573960", 301 | "size": 299, 302 | "weight": 1196, 303 | "qrc20TokenTransfers": [ 304 | { 305 | "address": "f2033ede578e17fa6231047265010445bca8cf1c", 306 | "addressHex": "f2033ede578e17fa6231047265010445bca8cf1c", 307 | "name": "QCASH", 308 | "symbol": "QC", 309 | "decimals": 8, 310 | "from": "QeZjXLEyqKgUNyoXcTCC2nxVx77cSnmpcy", 311 | "to": "Qe3X3dVeVocnd6f9rsCVJyTmW1Usc8YhLE", 312 | "value": "10000000000" 313 | } 314 | ] 315 | } 316 | ``` 317 | 318 | 319 | ## Raw Transaction 320 | 321 | **Request URL** 322 | ``` 323 | GET /raw-tx/:id 324 | ``` 325 | 326 | **Request** 327 | ``` 328 | GET /tx/ebf05fbf8dcf10f03a73331abd1ea934d66c03a72ee4a57addeab0225ad9289f 329 | ``` 330 | 331 | **Response** 332 | ``` 333 | 0200000001625cd48b069c9f4a6eeff9cfcac171aa7c608051343fb28531b16307cfa9e76a010000006a47304402206897a7a9502314e3d5f0d62e2fae485746cbbd9524229ab42187f5a766c9ec1d02206701c50e1947dd08c4440db66bf8a534e4a2c95a0779cf1963c229a4ebcc9bda012103d2afef396be37192a1137fe103ace0dd2e861088d0634ffd792d4d43a8bed770feffffff020000000000000000630104032cc900012844a9059cbb000000000000000000000000bf4e5cb019865cde870642bf2a2dfb375789c23b00000000000000000000000000000000000000000000000000000002540be40014f2033ede578e17fa6231047265010445bca8cf1cc2e05cb305000000001976a914c50541b82f4cda2f61cbbc219595c79ffdb4f2fa88ac3c320600 334 | ``` 335 | 336 | 337 | ## Send Raw Transaction 338 | 339 | **Request URL** 340 | ``` 341 | POST /tx/send 342 | ``` 343 | 344 | **Request Parameters** 345 | | Name | Type | Description | 346 | | - | - | - | 347 | | `rawtx` | String | Raw Transaction Hex String | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 361 | 364 | 365 | 366 | 367 |
NameTypeDescription
359 | rawtx 360 | 362 | String 363 | Raw Transaction Hex String
368 | 369 | **Request** 370 | ``` 371 | POST /tx/send 372 | ``` 373 | 374 | **Request Body** 375 | ``` 376 | rawtx=02000000014727d9d3560b94b0cf1c10daea43920c24fa8451a75f2d5f69f6f585726fcb15000000006a47304402202ac3b7fd62837722fd4ab1dfdfe7d069dcaad348c7a039ed3cfc473ef435a167022033ae815fbcd8c04b2fb98d611a395dd96da757cae94bca780010bd9db6bda7230121027f2fac6638798fe79696bffba971976b325753541d24fc0a920b617b5f23815bfeffffff02c00064f6070000001976a9140588712645b0c536d59c9d7198f492cf6d2eb3cf88ac263f61e9030000001976a91421b92d11d3b7cbe5e76252eab7db8bd5ac47a0ae88ac781a0600 377 | ``` 378 | 379 | **Response** 380 | ```json 381 | { 382 | "status": 0, 383 | "id": "56daa4ae91c07b84aad5bcab74bfe0c12e14b228ed755c2aaabc6b027f8698a0" 384 | } 385 | /// or 386 | { 387 | "status": 1, 388 | "message": "{error message}" 389 | } 390 | ``` 391 | -------------------------------------------------------------------------------- /app/service/block.js: -------------------------------------------------------------------------------- 1 | const {Service} = require('egg') 2 | 3 | class BlockService extends Service { 4 | async getBlock(arg) { 5 | const {Header, Address, Block, Transaction} = this.ctx.model 6 | let filter 7 | if (Number.isInteger(arg)) { 8 | filter = {height: arg} 9 | } else if (Buffer.isBuffer(arg)) { 10 | filter = {hash: arg} 11 | } else { 12 | return null 13 | } 14 | let result = await Header.findOne({ 15 | where: filter, 16 | include: [{ 17 | model: Block, 18 | as: 'block', 19 | required: true, 20 | attributes: ['size', 'weight'], 21 | include: [{ 22 | model: Address, 23 | as: 'miner', 24 | attributes: ['string'] 25 | }] 26 | }], 27 | transaction: this.ctx.state.transaction 28 | }) 29 | if (!result) { 30 | return null 31 | } 32 | let [prevHeader, nextHeader, transactions, [reward]] = await Promise.all([ 33 | Header.findOne({ 34 | where: {height: result.height - 1}, 35 | attributes: ['timestamp'], 36 | transaction: this.ctx.state.transaction 37 | }), 38 | Header.findOne({ 39 | where: {height: result.height + 1}, 40 | attributes: ['hash'], 41 | transaction: this.ctx.state.transaction 42 | }), 43 | Transaction.findAll({ 44 | where: {blockHeight: result.height}, 45 | attributes: ['id'], 46 | order: [['indexInBlock', 'ASC']], 47 | transaction: this.ctx.state.transaction 48 | }), 49 | this.getBlockRewards(result.height) 50 | ]) 51 | return { 52 | hash: result.hash, 53 | height: result.height, 54 | version: result.version, 55 | prevHash: result.prevHash, 56 | nextHash: nextHeader && nextHeader.hash, 57 | merkleRoot: result.merkleRoot, 58 | timestamp: result.timestamp, 59 | bits: result.bits, 60 | nonce: result.nonce, 61 | hashStateRoot: result.hashStateRoot, 62 | hashUTXORoot: result.hashUTXORoot, 63 | stakePrevTxId: result.stakePrevTxId, 64 | stakeOutputIndex: result.stakeOutputIndex, 65 | signature: result.signature, 66 | chainwork: result.chainwork, 67 | proofOfStake: result.isProofOfStake(), 68 | interval: result.height > 0 ? result.timestamp - prevHeader.timestamp : null, 69 | size: result.block.size, 70 | weight: result.block.weight, 71 | transactions: transactions.map(tx => tx.id), 72 | miner: result.block.miner.string, 73 | difficulty: result.difficulty, 74 | reward, 75 | confirmations: this.app.blockchainInfo.tip.height - result.height + 1 76 | } 77 | } 78 | 79 | async getRawBlock(arg) { 80 | const {Header, Transaction} = this.ctx.model 81 | const {Header: RawHeader, Block: RawBlock} = this.app.qtuminfo.lib 82 | let filter 83 | if (Number.isInteger(arg)) { 84 | filter = {height: arg} 85 | } else if (Buffer.isBuffer(arg)) { 86 | filter = {hash: arg} 87 | } else { 88 | return null 89 | } 90 | let block = await Header.findOne({where: filter, transaction: this.ctx.state.transaction}) 91 | if (!block) { 92 | return null 93 | } 94 | let transactionIds = (await Transaction.findAll({ 95 | where: {blockHeight: block.height}, 96 | attributes: ['id'], 97 | order: [['indexInBlock', 'ASC']], 98 | transaction: this.ctx.state.transaction 99 | })).map(tx => tx.id) 100 | let transactions = await Promise.all(transactionIds.map(id => this.ctx.service.transaction.getRawTransaction(id))) 101 | return new RawBlock({ 102 | header: new RawHeader({ 103 | version: block.version, 104 | prevHash: block.prevHash, 105 | merkleRoot: block.merkleRoot, 106 | timestamp: block.timestamp, 107 | bits: block.bits, 108 | nonce: block.nonce, 109 | hashStateRoot: block.hashStateRoot, 110 | hashUTXORoot: block.hashUTXORoot, 111 | stakePrevTxId: block.stakePrevTxId, 112 | stakeOutputIndex: block.stakeOutputIndex, 113 | signature: block.signature 114 | }), 115 | transactions 116 | }) 117 | } 118 | 119 | async listBlocks(dateFilter) { 120 | const db = this.ctx.model 121 | const {sql} = this.ctx.helper 122 | let dateFilterString = '' 123 | if (dateFilter) { 124 | dateFilterString = sql`AND timestamp BETWEEN ${dateFilter.min} AND ${dateFilter.max - 1}` 125 | } 126 | let [{totalCount}] = await db.query(sql` 127 | SELECT COUNT(*) AS totalCount FROM header WHERE height <= ${this.app.blockchainInfo.tip.height} ${{raw: dateFilterString}} 128 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction}) 129 | let blocks 130 | if (this.ctx.state.pagination) { 131 | let {limit, offset} = this.ctx.state.pagination 132 | blocks = await db.query(sql` 133 | SELECT 134 | header.hash AS hash, l.height AS height, header.timestamp AS timestamp, 135 | block.size AS size, address.string AS miner 136 | FROM ( 137 | SELECT height FROM header 138 | WHERE height <= ${this.app.blockchainInfo.tip.height} ${{raw: dateFilterString}} 139 | ORDER BY height DESC 140 | LIMIT ${offset}, ${limit} 141 | ) l, header, block, address 142 | WHERE l.height = header.height AND l.height = block.height AND address._id = block.miner_id 143 | ORDER BY l.height ASC 144 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction}) 145 | } else { 146 | blocks = await db.query(sql` 147 | SELECT 148 | header.hash AS hash, l.height AS height, header.timestamp AS timestamp, 149 | block.size AS size, address.string AS miner 150 | FROM ( 151 | SELECT height FROM header 152 | WHERE height <= ${this.app.blockchainInfo.tip.height} ${{raw: dateFilterString}} 153 | ORDER BY height DESC 154 | ) l, header, block, address 155 | WHERE l.height = header.height AND l.height = block.height AND address._id = block.miner_id 156 | ORDER BY l.height ASC 157 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction}) 158 | } 159 | if (blocks.length === 0) { 160 | return {totalCount, blocks: []} 161 | } else { 162 | return {totalCount, blocks: await this.getBlockSummary(blocks)} 163 | } 164 | } 165 | 166 | async getRecentBlocks(count) { 167 | const db = this.ctx.model 168 | const {sql} = this.ctx.helper 169 | let blocks = await db.query(sql` 170 | SELECT 171 | l.hash AS hash, l.height AS height, header.timestamp AS timestamp, 172 | l.size AS size, address.string AS miner 173 | FROM ( 174 | SELECT hash, height, size, miner_id FROM block 175 | ORDER BY height DESC 176 | LIMIT ${count} 177 | ) l, header, address WHERE l.height = header.height AND l.miner_id = address._id 178 | ORDER BY l.height DESC 179 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction}) 180 | if (blocks.length === 0) { 181 | return [] 182 | } 183 | blocks.reverse() 184 | return await this.getBlockSummary(blocks) 185 | } 186 | 187 | async getBlockRewards(startHeight, endHeight = startHeight + 1) { 188 | const db = this.ctx.model 189 | const {sql} = this.ctx.helper 190 | let rewards = await db.query(sql` 191 | SELECT SUM(value) AS value FROM ( 192 | SELECT tx.block_height AS height, output.value AS value FROM header, transaction tx, transaction_output output 193 | WHERE 194 | tx.block_height BETWEEN ${startHeight} AND ${endHeight - 1} 195 | AND header.height = tx.block_height 196 | AND tx.index_in_block = (SELECT CASE header.stake_prev_transaction_id WHEN ${Buffer.alloc(32)} THEN 0 ELSE 1 END) 197 | AND output.transaction_id = tx._id 198 | AND NOT EXISTS ( 199 | SELECT refund_id FROM gas_refund 200 | WHERE refund_id = output.transaction_id AND refund_index = output.output_index 201 | ) 202 | UNION ALL 203 | SELECT tx.block_height AS height, -input.value AS value 204 | FROM header, transaction tx, transaction_input input 205 | WHERE 206 | tx.block_height BETWEEN ${startHeight} AND ${endHeight - 1} 207 | AND header.height = tx.block_height 208 | AND tx.index_in_block = (SELECT CASE header.stake_prev_transaction_id WHEN ${Buffer.alloc(32)} THEN 0 ELSE 1 END) 209 | AND input.transaction_id = tx._id 210 | ) block_reward 211 | GROUP BY height 212 | ORDER BY height ASC 213 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction}) 214 | let result = rewards.map(reward => BigInt(reward.value)) 215 | if (startHeight[0] === 0) { 216 | result[0] = 0n 217 | } 218 | return result 219 | } 220 | 221 | async getBlockSummary(blocks) { 222 | const db = this.ctx.model 223 | const {Header} = db 224 | const {sql} = this.ctx.helper 225 | let transactionCountMapping = new Map( 226 | (await db.query(sql` 227 | SELECT block.height AS height, MAX(transaction.index_in_block) + 1 AS transactionsCount 228 | FROM block 229 | INNER JOIN transaction ON block.height = transaction.block_height 230 | WHERE block.height BETWEEN ${blocks[0].height} AND ${blocks[blocks.length - 1].height} 231 | GROUP BY block.height 232 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction})) 233 | .map(({height, transactionsCount}) => [height, transactionsCount]) 234 | ) 235 | let [prevHeader, rewards] = await Promise.all([ 236 | Header.findOne({ 237 | where: {height: blocks[0].height - 1}, 238 | attributes: ['timestamp'], 239 | transaction: this.ctx.state.transaction 240 | }), 241 | this.getBlockRewards(blocks[0].height, blocks[blocks.length - 1].height + 1) 242 | ]) 243 | let result = [] 244 | for (let i = blocks.length; --i >= 0;) { 245 | let block = blocks[i] 246 | let interval 247 | if (i === 0) { 248 | interval = prevHeader ? block.timestamp - prevHeader.timestamp : null 249 | } else { 250 | interval = block.timestamp - blocks[i - 1].timestamp 251 | } 252 | result.push({ 253 | hash: block.hash, 254 | height: block.height, 255 | timestamp: block.timestamp, 256 | transactionsCount: transactionCountMapping.get(block.height), 257 | interval, 258 | size: block.size, 259 | miner: block.miner, 260 | reward: rewards[i] 261 | }) 262 | } 263 | return result 264 | } 265 | 266 | async getBiggestMiners(lastNBlocks) { 267 | const fromBlock = this.app.chain.lastPoWBlockHeight >= 0xffffffff ? this.app.chain.lastPoWBlockHeight : 1 268 | const db = this.ctx.model 269 | const {sql} = this.ctx.helper 270 | const {Block} = db 271 | const {gte: $gte} = this.app.Sequelize.Op 272 | let fromBlockHeight = lastNBlocks == null ? fromBlock : Math.max(this.app.blockchainInfo.height - lastNBlocks + 1, fromBlock) 273 | let {limit, offset} = this.ctx.state.pagination 274 | let totalCount = await Block.count({ 275 | where: {height: {[$gte]: fromBlockHeight}}, 276 | distinct: true, 277 | col: 'minerId', 278 | transaction: this.ctx.state.transaction 279 | }) 280 | let list = await db.query(sql` 281 | SELECT address.string AS address, list.blocks AS blocks, rich_list.balance AS balance FROM ( 282 | SELECT miner_id, COUNT(*) AS blocks FROM block 283 | WHERE height >= ${fromBlockHeight} 284 | GROUP BY miner_id 285 | ORDER BY blocks DESC 286 | LIMIT ${offset}, ${limit} 287 | ) list 288 | INNER JOIN address ON address._id = list.miner_id 289 | LEFT JOIN rich_list ON rich_list.address_id = address._id 290 | ORDER BY blocks DESC 291 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction}) 292 | return { 293 | totalCount, 294 | list: list.map(({address, blocks, balance}) => ({address, blocks, balance: BigInt(balance || 0)})) 295 | } 296 | } 297 | 298 | async getBlockTransactions(height) { 299 | const {Transaction} = this.ctx.model 300 | let transactions = await Transaction.findAll({ 301 | where: {blockHeight: height}, 302 | attributes: ['id'], 303 | transaction: this.ctx.state.transaction 304 | }) 305 | return transactions.map(tx => tx.id) 306 | } 307 | 308 | async getBlockAddressTransactions(height) { 309 | const {Address, Transaction, BalanceChange, EvmReceipt: EVMReceipt, EvmReceiptLog: EVMReceiptLog, Contract} = this.ctx.model 310 | const {Address: RawAddress} = this.app.qtuminfo.lib 311 | const TransferABI = this.app.qtuminfo.lib.Solidity.qrc20ABIs.find(abi => abi.name === 'Transfer') 312 | let result = [] 313 | let balanceChanges = await BalanceChange.findAll({ 314 | attributes: [], 315 | include: [ 316 | { 317 | model: Transaction, 318 | as: 'transaction', 319 | required: true, 320 | where: {blockHeight: height}, 321 | attributes: ['indexInBlock'] 322 | }, 323 | { 324 | model: Address, 325 | as: 'address', 326 | required: true, 327 | attributes: ['string'] 328 | } 329 | ] 330 | }) 331 | for (let {transaction, address} of balanceChanges) { 332 | result[transaction.indexInBlock] = result[transaction.indexInBlock] || new Set() 333 | result[transaction.indexInBlock].add(address.string) 334 | } 335 | let receipts = await EVMReceipt.findAll({ 336 | where: {blockHeight: height}, 337 | attributes: ['indexInBlock', 'senderType', 'senderData'] 338 | }) 339 | for (let {indexInBlock, senderType, senderData} of receipts) { 340 | result[indexInBlock] = result[indexInBlock] || new Set() 341 | result[indexInBlock].add(new RawAddress({type: senderType, data: senderData, chain: this.app.chain}).toString()) 342 | } 343 | let receiptLogs = await EVMReceiptLog.findAll({ 344 | attributes: ['topic1', 'topic2', 'topic3', 'topic4'], 345 | include: [ 346 | { 347 | model: EVMReceipt, 348 | as: 'receipt', 349 | required: true, 350 | where: {blockHeight: height}, 351 | attributes: ['indexInBlock'] 352 | }, 353 | { 354 | model: Contract, 355 | as: 'contract', 356 | required: true, 357 | attributes: ['addressString', 'type'] 358 | } 359 | ] 360 | }) 361 | for (let {topic1, topic2, topic3, topic4, receipt, contract} of receiptLogs) { 362 | let set = result[receipt.indexInBlock] = result[receipt.indexInBlock] || new Set() 363 | set.add(contract.addressString) 364 | if (Buffer.compare(topic1, TransferABI.id) === 0 && topic3) { 365 | if (contract.type === 'qrc20' && !topic4 || contract.type === 'qrc721' && topic4) { 366 | let sender = topic2.slice(12) 367 | let receiver = topic3.slice(12) 368 | if (Buffer.compare(sender, Buffer.alloc(20)) !== 0) { 369 | set.add(new RawAddress({type: Address.PAY_TO_PUBLIC_KEY_HASH, data: sender, chain: this.app.chain}).toString()) 370 | set.add(new RawAddress({type: Address.EVM_CONTRACT, data: sender, chain: this.app.chain}).toString()) 371 | } 372 | if (Buffer.compare(receiver, Buffer.alloc(20)) !== 0) { 373 | set.add(new RawAddress({type: Address.PAY_TO_PUBLIC_KEY_HASH, data: receiver, chain: this.app.chain}).toString()) 374 | set.add(new RawAddress({type: Address.EVM_CONTRACT, data: receiver, chain: this.app.chain}).toString()) 375 | } 376 | } 377 | } 378 | } 379 | return result 380 | } 381 | 382 | getBlockFilter(category = 'blockHeight') { 383 | const {gte: $gte, lte: $lte, between: $between} = this.app.Sequelize.Op 384 | let {fromBlock, toBlock} = this.ctx.state 385 | let blockFilter = null 386 | if (fromBlock != null && toBlock != null) { 387 | blockFilter = {[$between]: [fromBlock, toBlock]} 388 | } else if (fromBlock != null) { 389 | blockFilter = {[$gte]: fromBlock} 390 | } else if (toBlock != null) { 391 | blockFilter = {[$lte]: toBlock} 392 | } 393 | return blockFilter ? {[category]: blockFilter} : {} 394 | } 395 | 396 | getRawBlockFilter(category = 'block_height') { 397 | const {sql} = this.ctx.helper 398 | let {fromBlock, toBlock} = this.ctx.state 399 | let blockFilter = 'TRUE' 400 | if (fromBlock != null && toBlock != null) { 401 | blockFilter = sql`${{raw: category}} BETWEEN ${fromBlock} AND ${toBlock}` 402 | } else if (fromBlock != null) { 403 | blockFilter = sql`${{raw: category}} >= ${fromBlock}` 404 | } else if (toBlock != null) { 405 | blockFilter = sql`${{raw: category}} <= ${toBlock}` 406 | } 407 | return {raw: blockFilter} 408 | } 409 | } 410 | 411 | module.exports = BlockService 412 | -------------------------------------------------------------------------------- /app/service/contract.js: -------------------------------------------------------------------------------- 1 | const {Service} = require('egg') 2 | 3 | class ContractService extends Service { 4 | async getContractAddresses(list) { 5 | const {Address} = this.app.qtuminfo.lib 6 | const chain = this.app.chain 7 | const {Contract} = this.ctx.model 8 | 9 | let result = [] 10 | for (let item of list) { 11 | let rawAddress 12 | try { 13 | rawAddress = Address.fromString(item, chain) 14 | } catch (err) { 15 | this.ctx.throw(400) 16 | } 17 | let filter 18 | if (rawAddress.type === Address.CONTRACT) { 19 | filter = {address: Buffer.from(item, 'hex')} 20 | } else if (rawAddress.type === Address.EVM_CONTRACT) { 21 | filter = {addressString: item} 22 | } else { 23 | this.ctx.throw(400) 24 | } 25 | let contractResult = await Contract.findOne({ 26 | where: filter, 27 | attributes: ['address', 'addressString', 'vm', 'type'], 28 | transaction: this.ctx.state.transaction 29 | }) 30 | this.ctx.assert(contractResult, 404) 31 | result.push(contractResult.address) 32 | } 33 | return result 34 | } 35 | 36 | async getContractSummary(contractAddress, addressIds) { 37 | const {Contract, Qrc20: QRC20, Qrc20Statistics: QRC20Statistics, Qrc721: QRC721} = this.ctx.model 38 | const {balance: balanceService, qrc20: qrc20Service, qrc721: qrc721Service} = this.ctx.service 39 | let contract = await Contract.findOne({ 40 | where: {address: contractAddress}, 41 | attributes: ['addressString', 'vm', 'type'], 42 | include: [ 43 | { 44 | model: QRC20, 45 | as: 'qrc20', 46 | required: false, 47 | attributes: ['name', 'symbol', 'decimals', 'totalSupply', 'version'], 48 | include: [{ 49 | model: QRC20Statistics, 50 | as: 'statistics', 51 | required: true 52 | }] 53 | }, 54 | { 55 | model: QRC721, 56 | as: 'qrc721', 57 | required: false, 58 | attributes: ['name', 'symbol', 'totalSupply'] 59 | } 60 | ], 61 | transaction: this.ctx.state.transaction 62 | }) 63 | let [ 64 | {totalReceived, totalSent}, 65 | unconfirmed, 66 | qrc20Balances, 67 | qrc721Balances, 68 | transactionCount 69 | ] = await Promise.all([ 70 | balanceService.getTotalBalanceChanges(addressIds), 71 | balanceService.getUnconfirmedBalance(addressIds), 72 | qrc20Service.getAllQRC20Balances([contractAddress]), 73 | qrc721Service.getAllQRC721Balances([contractAddress]), 74 | this.getContractTransactionCount(contractAddress, addressIds) 75 | ]) 76 | return { 77 | address: contractAddress.toString('hex'), 78 | addressHex: contractAddress, 79 | vm: contract.vm, 80 | type: contract.type, 81 | ...contract.type === 'qrc20' ? { 82 | qrc20: { 83 | name: contract.qrc20.name, 84 | symbol: contract.qrc20.symbol, 85 | decimals: contract.qrc20.decimals, 86 | totalSupply: contract.qrc20.totalSupply, 87 | version: contract.qrc20.version, 88 | holders: contract.qrc20.statistics.holders, 89 | transactions: contract.qrc20.statistics.transactions 90 | } 91 | } : {}, 92 | ...contract.type === 'qrc721' ? { 93 | qrc721: { 94 | name: contract.qrc721.name, 95 | symbol: contract.qrc721.symbol, 96 | totalSupply: contract.qrc721.totalSupply 97 | } 98 | } : {}, 99 | balance: totalReceived - totalSent, 100 | totalReceived, 101 | totalSent, 102 | unconfirmed, 103 | qrc20Balances, 104 | qrc721Balances, 105 | transactionCount 106 | } 107 | } 108 | 109 | async getContractTransactionCount(contractAddress, addressIds) { 110 | const TransferABI = this.app.qtuminfo.lib.Solidity.qrc20ABIs.find(abi => abi.name === 'Transfer') 111 | const db = this.ctx.model 112 | let {sql} = this.ctx.helper 113 | let topic = Buffer.concat([Buffer.alloc(12), contractAddress]) 114 | let [{count}] = await db.query(sql` 115 | SELECT COUNT(*) AS count FROM ( 116 | SELECT transaction_id FROM balance_change 117 | WHERE address_id IN ${addressIds} AND ${this.ctx.service.block.getRawBlockFilter()} 118 | UNION 119 | SELECT transaction_id FROM evm_receipt 120 | WHERE contract_address = ${contractAddress} AND ${this.ctx.service.block.getRawBlockFilter()} 121 | UNION 122 | SELECT receipt.transaction_id AS transaction_id FROM evm_receipt receipt, evm_receipt_log log 123 | WHERE log.receipt_id = receipt._id AND log.address = ${contractAddress} 124 | AND ${this.ctx.service.block.getRawBlockFilter('receipt.block_height')} 125 | UNION 126 | SELECT receipt.transaction_id AS transaction_id FROM evm_receipt receipt, evm_receipt_log log, contract 127 | WHERE log.receipt_id = receipt._id 128 | AND ${this.ctx.service.block.getRawBlockFilter('receipt.block_height')} 129 | AND contract.address = log.address AND contract.type IN ('qrc20', 'qrc721') 130 | AND log.topic1 = ${TransferABI.id} 131 | AND (log.topic2 = ${topic} OR log.topic3 = ${topic}) 132 | AND ( 133 | (contract.type = 'qrc20' AND log.topic3 IS NOT NULL AND log.topic4 IS NULL) 134 | OR (contract.type = 'qrc721' AND log.topic4 IS NOT NULL) 135 | ) 136 | ) list 137 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction}) 138 | return count 139 | } 140 | 141 | async getContractTransactions(contractAddress, addressIds) { 142 | const TransferABI = this.app.qtuminfo.lib.Solidity.qrc20ABIs.find(abi => abi.name === 'Transfer') 143 | const db = this.ctx.model 144 | let {sql} = this.ctx.helper 145 | let {limit, offset, reversed = true} = this.ctx.state.pagination 146 | let order = reversed ? 'DESC' : 'ASC' 147 | let topic = Buffer.concat([Buffer.alloc(12), contractAddress]) 148 | let totalCount = await this.getContractTransactionCount(contractAddress, addressIds) 149 | let transactions = await db.query(sql` 150 | SELECT tx.id AS id FROM ( 151 | SELECT block_height, index_in_block, _id FROM ( 152 | SELECT block_height, index_in_block, transaction_id AS _id FROM balance_change 153 | WHERE address_id IN ${addressIds} AND ${this.ctx.service.block.getRawBlockFilter()} 154 | UNION 155 | SELECT block_height, index_in_block, transaction_id AS _id FROM evm_receipt 156 | WHERE contract_address = ${contractAddress} AND ${this.ctx.service.block.getRawBlockFilter()} 157 | UNION 158 | SELECT receipt.block_height AS block_height, receipt.index_in_block AS index_in_block, receipt.transaction_id AS _id 159 | FROM evm_receipt receipt, evm_receipt_log log 160 | WHERE log.receipt_id = receipt._id AND log.address = ${contractAddress} 161 | AND ${this.ctx.service.block.getRawBlockFilter('receipt.block_height')} 162 | UNION 163 | SELECT receipt.block_height AS block_height, receipt.index_in_block AS index_in_block, receipt.transaction_id AS _id 164 | FROM evm_receipt receipt, evm_receipt_log log, contract 165 | WHERE log.receipt_id = receipt._id 166 | AND ${this.ctx.service.block.getRawBlockFilter('receipt.block_height')} 167 | AND contract.address = log.address AND contract.type IN ('qrc20', 'qrc721') 168 | AND log.topic1 = ${TransferABI.id} 169 | AND (log.topic2 = ${topic} OR log.topic3 = ${topic}) 170 | AND ( 171 | (contract.type = 'qrc20' AND log.topic3 IS NOT NULL AND log.topic4 IS NULL) 172 | OR (contract.type = 'qrc721' AND log.topic4 IS NOT NULL) 173 | ) 174 | ) list 175 | ORDER BY block_height ${{raw: order}}, index_in_block ${{raw: order}}, _id ${{raw: order}} 176 | LIMIT ${offset}, ${limit} 177 | ) list, transaction tx 178 | WHERE tx._id = list._id 179 | ORDER BY list.block_height ${{raw: order}}, list.index_in_block ${{raw: order}}, list._id ${{raw: order}} 180 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction}).map(({id}) => id) 181 | return {totalCount, transactions} 182 | } 183 | 184 | async getContractBasicTransactionCount(contractAddress) { 185 | const {EvmReceipt: EVMReceipt} = this.ctx.model 186 | return await EVMReceipt.count({ 187 | where: { 188 | contractAddress, 189 | ...this.ctx.service.block.getBlockFilter() 190 | }, 191 | transaction: this.ctx.state.transaction 192 | }) 193 | } 194 | 195 | async getContractBasicTransactions(contractAddress) { 196 | const {Address, OutputScript} = this.app.qtuminfo.lib 197 | const { 198 | Header, Transaction, TransactionOutput, Contract, EvmReceipt: EVMReceipt, EvmReceiptLog: EVMReceiptLog, 199 | where, col 200 | } = this.ctx.model 201 | const {in: $in} = this.app.Sequelize.Op 202 | let {limit, offset, reversed = true} = this.ctx.state.pagination 203 | let order = reversed ? 'DESC' : 'ASC' 204 | let totalCount = await this.getContractBasicTransactionCount(contractAddress) 205 | let receiptIds = (await EVMReceipt.findAll({ 206 | where: { 207 | contractAddress, 208 | ...this.ctx.service.block.getBlockFilter() 209 | }, 210 | attributes: ['_id'], 211 | order: [['blockHeight', order], ['indexInBlock', order], ['transactionId', order], ['outputIndex', order]], 212 | limit, 213 | offset, 214 | transaction: this.ctx.state.transaction 215 | })).map(receipt => receipt._id) 216 | let receipts = await EVMReceipt.findAll({ 217 | where: {_id: {[$in]: receiptIds}}, 218 | include: [ 219 | { 220 | model: Header, 221 | as: 'header', 222 | required: false, 223 | attributes: ['hash', 'timestamp'] 224 | }, 225 | { 226 | model: Transaction, 227 | as: 'transaction', 228 | required: true, 229 | attributes: ['id'] 230 | }, 231 | { 232 | model: TransactionOutput, 233 | as: 'output', 234 | on: { 235 | transactionId: where(col('output.transaction_id'), '=', col('evm_receipt.transaction_id')), 236 | outputIndex: where(col('output.output_index'), '=', col('evm_receipt.output_index')) 237 | }, 238 | required: true, 239 | attributes: ['scriptPubKey', 'value'] 240 | }, 241 | { 242 | model: EVMReceiptLog, 243 | as: 'logs', 244 | required: false, 245 | include: [{ 246 | model: Contract, 247 | as: 'contract', 248 | required: true, 249 | attributes: ['addressString'] 250 | }] 251 | }, 252 | { 253 | model: Contract, 254 | as: 'contract', 255 | required: true, 256 | attributes: ['addressString'] 257 | } 258 | ], 259 | order: [['blockHeight', order], ['indexInBlock', order], ['transactionId', order], ['outputIndex', order]], 260 | transaction: this.ctx.state.transaction 261 | }) 262 | let transactions = receipts.map(receipt => ({ 263 | transactionId: receipt.transaction.id, 264 | outputIndex: receipt.outputIndex, 265 | ...receipt.header ? { 266 | blockHeight: receipt.blockHeight, 267 | blockHash: receipt.header.hash, 268 | timestamp: receipt.header.timestamp, 269 | confirmations: this.app.blockchainInfo.tip.height - receipt.blockHeight + 1 270 | } : {confirmations: 0}, 271 | scriptPubKey: OutputScript.fromBuffer(receipt.output.scriptPubKey), 272 | value: receipt.output.value, 273 | sender: new Address({type: receipt.senderType, data: receipt.senderData, chain: this.app.chain}), 274 | gasUsed: receipt.gasUsed, 275 | contractAddress: receipt.contractAddress.toString('hex'), 276 | contractAddressHex: receipt.contractAddress, 277 | excepted: receipt.excepted, 278 | exceptedMessage: receipt.exceptedMessage, 279 | evmLogs: receipt.logs.sort((x, y) => x.logIndex - y.logIndex).map(log => ({ 280 | address: log.address.toString('hex'), 281 | addressHex: log.address, 282 | topics: this.ctx.service.transaction.transformTopics(log), 283 | data: log.data 284 | })) 285 | })) 286 | return {totalCount, transactions} 287 | } 288 | 289 | async callContract(contract, data, sender) { 290 | let client = new this.app.qtuminfo.rpc(this.app.config.qtuminfo.rpc) 291 | return await client.callcontract( 292 | contract.toString('hex'), 293 | data.toString('hex'), 294 | ...sender == null ? [] : [sender.toString('hex')] 295 | ) 296 | } 297 | 298 | async searchLogs({contract, topic1, topic2, topic3, topic4} = {}) { 299 | const {Address} = this.app.qtuminfo.lib 300 | const db = this.ctx.model 301 | const {Header, Transaction, EvmReceipt: EVMReceipt, EvmReceiptLog: EVMReceiptLog, Contract} = db 302 | const {in: $in} = this.ctx.app.Sequelize.Op 303 | const {sql} = this.ctx.helper 304 | let {limit, offset} = this.ctx.state.pagination 305 | 306 | let blockFilter = this.ctx.service.block.getRawBlockFilter('receipt.block_height') 307 | let contractFilter = contract ? sql`log.address = ${contract}` : 'TRUE' 308 | let topic1Filter = topic1 ? sql`log.topic1 = ${topic1}` : 'TRUE' 309 | let topic2Filter = topic2 ? sql`log.topic2 = ${topic2}` : 'TRUE' 310 | let topic3Filter = topic3 ? sql`log.topic3 = ${topic3}` : 'TRUE' 311 | let topic4Filter = topic4 ? sql`log.topic4 = ${topic4}` : 'TRUE' 312 | 313 | let [{count: totalCount}] = await db.query(sql` 314 | SELECT COUNT(DISTINCT(log._id)) AS count from evm_receipt receipt, evm_receipt_log log 315 | WHERE receipt._id = log.receipt_id AND ${blockFilter} AND ${{raw: contractFilter}} 316 | AND ${{raw: topic1Filter}} AND ${{raw: topic2Filter}} AND ${{raw: topic3Filter}} AND ${{raw: topic4Filter}} 317 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.transaction}) 318 | if (totalCount === 0) { 319 | return {totalCount, logs: []} 320 | } 321 | 322 | let ids = (await db.query(sql` 323 | SELECT log._id AS _id from evm_receipt receipt, evm_receipt_log log 324 | WHERE receipt._id = log.receipt_id AND ${blockFilter} AND ${{raw: contractFilter}} 325 | AND ${{raw: topic1Filter}} AND ${{raw: topic2Filter}} AND ${{raw: topic3Filter}} AND ${{raw: topic4Filter}} 326 | ORDER BY log._id ASC 327 | LIMIT ${offset}, ${limit} 328 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.transaction})).map(log => log._id) 329 | 330 | let logs = await EVMReceiptLog.findAll({ 331 | where: {_id: {[$in]: ids}}, 332 | attributes: ['topic1', 'topic2', 'topic3', 'topic4', 'data'], 333 | include: [ 334 | { 335 | model: EVMReceipt, 336 | as: 'receipt', 337 | required: true, 338 | attributes: ['transactionId', 'outputIndex', 'blockHeight', 'senderType', 'senderData'], 339 | include: [ 340 | { 341 | model: Transaction, 342 | as: 'transaction', 343 | required: true, 344 | attributes: ['id'], 345 | include: [{ 346 | model: Header, 347 | as: 'header', 348 | required: true, 349 | attributes: ['hash', 'height', 'timestamp'] 350 | }] 351 | }, 352 | { 353 | model: Contract, 354 | as: 'contract', 355 | required: true, 356 | attributes: ['address', 'addressString'] 357 | } 358 | ] 359 | }, 360 | { 361 | model: Contract, 362 | as: 'contract', 363 | required: true, 364 | attributes: ['address', 'addressString'] 365 | } 366 | ], 367 | order: [['_id', 'ASC']], 368 | transaction: this.ctx.state.transaction 369 | }) 370 | 371 | return { 372 | totalCount, 373 | logs: logs.map(log => ({ 374 | transactionId: log.receipt.transaction.id, 375 | outputIndex: log.receipt.outputIndex, 376 | blockHeight: log.receipt.transaction.header.height, 377 | blockHash: log.receipt.transaction.header.hash, 378 | timestamp: log.receipt.transaction.header.timestamp, 379 | sender: new Address({type: log.receipt.senderType, data: log.receipt.senderData, chain: this.app.chain}), 380 | contractAddress: log.receipt.contract.address.toString('hex'), 381 | contractAddressHex: log.receipt.contract.address, 382 | address: log.contract.address.toString('hex'), 383 | addressHex: log.contract.address, 384 | topics: this.ctx.service.transaction.transformTopics(log), 385 | data: log.data 386 | })) 387 | } 388 | } 389 | 390 | async transformHexAddresses(addresses) { 391 | if (addresses.length === 0) { 392 | return [] 393 | } 394 | const {Contract} = this.ctx.model 395 | const {in: $in} = this.app.Sequelize.Op 396 | const {Address} = this.app.qtuminfo.lib 397 | let result = addresses.map(address => Buffer.compare(address, Buffer.alloc(20)) === 0 ? null : address) 398 | 399 | let contracts = await Contract.findAll({ 400 | where: {address: {[$in]: addresses.filter(address => Buffer.compare(address, Buffer.alloc(20)) !== 0)}}, 401 | attributes: ['address', 'addressString'], 402 | transaction: this.ctx.state.transaction 403 | }) 404 | let mapping = new Map(contracts.map(({address, addressString}) => [address.toString('hex'), addressString])) 405 | for (let i = 0; i < result.length; ++i) { 406 | if (result[i]) { 407 | let string = mapping.get(result[i].toString('hex')) 408 | if (string) { 409 | result[i] = {string, hex: result[i]} 410 | } else { 411 | result[i] = new Address({ 412 | type: Address.PAY_TO_PUBLIC_KEY_HASH, 413 | data: result[i], 414 | chain: this.app.chain 415 | }).toString() 416 | } 417 | } 418 | } 419 | return result 420 | } 421 | } 422 | 423 | module.exports = ContractService 424 | -------------------------------------------------------------------------------- /doc/contract.md: -------------------------------------------------------------------------------- 1 | # Contract API 2 | 3 | - [Contract API](#contract-api) 4 | - [Contract Information](#contract-information) 5 | - [Contract Transactions](#contract-transactions) 6 | - [Contract Basic Transactions](#contract-basic-transactions) 7 | - [Call Contract](#call-contract) 8 | - [Search Logs](#search-logs) 9 | 10 | 11 | ## Contract Information 12 | 13 | **Request URL** 14 | ``` 15 | GET /contract/:contract 16 | ``` 17 | 18 | **Request** 19 | ``` 20 | GET /contract/6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7 21 | ``` 22 | 23 | **Response** 24 | ```json 25 | { 26 | "address": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7", 27 | "addressHex": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7", 28 | "vm": "evm", 29 | "type": "qrc20", 30 | "qrc20": { 31 | "name": "Bodhi Token", 32 | "symbol": "BOT", 33 | "decimals": 8, 34 | "totalSupply": "10000000000000000", 35 | "version": null, 36 | "holders": 33219, 37 | "transactions": 49622 38 | }, 39 | "balance": "0", 40 | "totalReceived": "1086500002", 41 | "totalSent": "1086500002", 42 | "unconfirmed": "0", 43 | "qrc20Balances": [], 44 | "qrc721Balances": [], 45 | "transactionCount": 20572 46 | } 47 | ``` 48 | 49 | 50 | ## Contract Transactions 51 | 52 | **Request URL** 53 | ``` 54 | GET /contract/:contract/txs 55 | ``` 56 | 57 | **Request Parameters** 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 73 | 74 | 75 | 80 | 81 | 82 | 85 | 88 | 89 | 90 | 91 |
NameDefaultDescription
69 | 70 | Pagination Parameters 71 | 72 |
76 | 77 | Block / Timestamp Filter Parameters 78 | 79 |
83 | reversed 84 | 86 | true 87 | Return records reversed
92 | 93 | **Request** 94 | ``` 95 | GET /contract/6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7/txs?limit=10&offset=10 96 | ``` 97 | 98 | **Response** 99 | ```json 100 | { 101 | "totalCount": 20572, 102 | "transactions": [ 103 | "30563670995c63fb99301c32498bf6bd32059d2ed680491ddb87bde60c425b8c", 104 | "3ea37cf8fa77031a7470001e422a618e5307711693d1c1ed81fa90542ec6d977", 105 | "60ceddf045a6892fa95084313f551f980b087c115dbfe00f420b34b2929c78c0", 106 | "1ecd472c91f9786b34e419e46755c0543a044c2d35373fec7a24a23cc2444390", 107 | "4449fa6a69f7af45cc3e16bb56a0b29437041b80aebf0a1644151c98810232c8", 108 | "fdebd8f01a5815db6b60a256dfd2b58c51ae6c1ab25de948bed9d10c5cbd12da", 109 | "561b2c5401972f211384c9eaf5e5ab3002546dd424526295cf7261583ebe88b0", 110 | "7662edfd2f5514a9276df7d5fa779adbefebcb69ae5d6fc13eb32d212a4d6755", 111 | "e2b6a4c9fe45e96406b8434d0d0c7cd47ce4daff7044b1202ef35b45020aa6c4", 112 | "56ba80c4bf87ad00d4e63cbbe6f98799626bd2017dfa369656ba0ba11ed3a181" 113 | ] 114 | } 115 | ``` 116 | 117 | 118 | ## Contract Basic Transactions 119 | List of transactions the contract is called. 120 | 121 | **Request URL** 122 | ``` 123 | GET /contract/:contract/basic-txs 124 | ``` 125 | 126 | **Request Parameters** 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 142 | 143 | 144 | 149 | 150 | 151 | 154 | 157 | 158 | 159 | 160 |
NameDefaultDescription
138 | 139 | Pagination Parameters 140 | 141 |
145 | 146 | Block / Timestamp Filter Parameters 147 | 148 |
152 | reversed 153 | 155 | true 156 | Return records reversed
161 | 162 | **Request** 163 | ``` 164 | GET /contract/6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7/basic-txs?limit=3&offset=10 165 | ``` 166 | 167 | **Response** 168 | ```json 169 | { 170 | "totalCount": 19611, 171 | "transactions": [ 172 | { 173 | "transactionId": "30563670995c63fb99301c32498bf6bd32059d2ed680491ddb87bde60c425b8c", 174 | "outputIndex": 0, 175 | "blockHeight": 398666, 176 | "blockHash": "d958ea4817a4511ac0cd601deb7a7380cbfc12fcde21e495157b913048f6aa81", 177 | "timestamp": 1561618976, 178 | "confirmations": 8658, 179 | "type": "evm_call", 180 | "gasLimit": 100000, 181 | "gasPrice": 40, 182 | "byteCode": "a9059cbb0000000000000000000000005dbf04d00b2e13b820db7afc1bdab3d4e303e8b6000000000000000000000000000000000000000000000000000000471ca54070", 183 | "outputValue": "0", 184 | "sender": "QgAwsgVLnREV4GYTP1zishNmsfrGHNYMM3", 185 | "gasUsed": 21614, 186 | "contractAddress": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7", 187 | "contractAddressHex": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7", 188 | "excepted": "None", 189 | "exceptedMessage": "", 190 | "evmLogs": [ 191 | { 192 | "address": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7", 193 | "addressHex": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7", 194 | "topics": [ 195 | "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", 196 | "000000000000000000000000d6a6404a173de5604ed9a5334854786514c9c125", 197 | "0000000000000000000000005dbf04d00b2e13b820db7afc1bdab3d4e303e8b6" 198 | ], 199 | "data": "000000000000000000000000000000000000000000000000000000471ca54070" 200 | } 201 | ] 202 | }, 203 | { 204 | "transactionId": "3ea37cf8fa77031a7470001e422a618e5307711693d1c1ed81fa90542ec6d977", 205 | "outputIndex": 1, 206 | "blockHeight": 398660, 207 | "blockHash": "b2b8f46df7d70c587ea2c81919a7cc4ec12b80441e4e3e36230e270f1fe25fe2", 208 | "timestamp": 1561618240, 209 | "confirmations": 8664, 210 | "type": "evm_call", 211 | "gasLimit": 250000, 212 | "gasPrice": 40, 213 | "byteCode": "a9059cbb000000000000000000000000d6a6404a173de5604ed9a5334854786514c9c125000000000000000000000000000000000000000000000000000000471ca54070", 214 | "outputValue": "0", 215 | "sender": "QhHMyzZRN4neqp5SjovBYUVJZneFPcRN9A", 216 | "gasUsed": 36614, 217 | "contractAddress": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7", 218 | "contractAddressHex": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7", 219 | "excepted": "None", 220 | "exceptedMessage": "", 221 | "evmLogs": [ 222 | { 223 | "address": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7", 224 | "addressHex": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7", 225 | "topics": [ 226 | "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", 227 | "000000000000000000000000e2d4ff6a36f6307e1bc034695c8a24a11fc504c6", 228 | "000000000000000000000000d6a6404a173de5604ed9a5334854786514c9c125" 229 | ], 230 | "data": "000000000000000000000000000000000000000000000000000000471ca54070" 231 | } 232 | ] 233 | }, 234 | { 235 | "transactionId": "60ceddf045a6892fa95084313f551f980b087c115dbfe00f420b34b2929c78c0", 236 | "outputIndex": 0, 237 | "blockHeight": 397976, 238 | "blockHash": "bce13e100201df586df2b24cb6db9bfdc2a92f7f6de60ce7d9e449a3dfa344f1", 239 | "timestamp": 1561519520, 240 | "confirmations": 9348, 241 | "type": "evm_call", 242 | "gasLimit": 100000, 243 | "gasPrice": 40, 244 | "byteCode": "a9059cbb0000000000000000000000002354d9f2bbd1d14e18287aa44ec4dc2b237040b600000000000000000000000000000000000000000000000000000010b1a72d00", 245 | "outputValue": "0", 246 | "sender": "QW9VdHxy9xbeMq74bnZ2NqVQTgiXDbELDy", 247 | "gasUsed": 51550, 248 | "contractAddress": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7", 249 | "contractAddressHex": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7", 250 | "excepted": "None", 251 | "exceptedMessage": "", 252 | "evmLogs": [ 253 | { 254 | "address": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7", 255 | "addressHex": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7", 256 | "topics": [ 257 | "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", 258 | "00000000000000000000000068aeaaa277528eb19cf582784f8eda0090e13a0c", 259 | "0000000000000000000000002354d9f2bbd1d14e18287aa44ec4dc2b237040b6" 260 | ], 261 | "data": "00000000000000000000000000000000000000000000000000000010b1a72d00" 262 | } 263 | ] 264 | } 265 | ] 266 | } 267 | ``` 268 | 269 | 270 | ## Call Contract 271 | Returns RPC `callcontract` result. 272 | 273 | **Request URL** 274 | ``` 275 | GET /contract/:contract/call 276 | ``` 277 | 278 | **Request Parameters** 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 292 | 295 | 296 | 297 | 298 | 301 | 304 | 305 | 306 | 307 |
NameTypeDescription
290 | date 291 | 293 | String 294 | Hexadecimal data to send to contract
299 | sender 300 | 302 | String (optional) 303 | Base58 P2PKH or 20-byte hexadecimal sender address
308 | 309 | **Request** 310 | ``` 311 | GET /contract/6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7/call?data=313ce567 312 | ``` 313 | 314 | **Response** 315 | ```json 316 | { 317 | "address": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7", 318 | "executionResult": { 319 | "gasUsed": 21533, 320 | "excepted": "None", 321 | "newAddress": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7", 322 | "output": "0000000000000000000000000000000000000000000000000000000000000008", 323 | "codeDeposit": 0, 324 | "gasRefunded": 0, 325 | "depositSize": 0, 326 | "gasForDeposit": 0 327 | }, 328 | "transactionReceipt": { 329 | "stateRoot": "d314a238f5431c7aeb8aea73790fce0d41bd5f8d7d46b37a1f5692bdf0d14acd", 330 | "gasUsed": 21533, 331 | "bloom": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 332 | "log": [] 333 | } 334 | } 335 | ``` 336 | 337 | 338 | ## Search Logs 339 | 340 | **Request URL** 341 | ``` 342 | GET /searchlogs 343 | ``` 344 | 345 | **Request Parameters** 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 361 | 362 | 363 | 368 | 369 | 370 | 373 | 376 | 377 | 378 | 379 | 382 | 385 | 386 | 387 | 388 | 391 | 394 | 395 | 396 | 397 | 400 | 403 | 404 | 405 | 406 | 409 | 412 | 413 | 414 | 415 |
NameTypeDescription
357 | 358 | Pagination Parameters 359 | 360 |
364 | 365 | Block / Timestamp Filter Parameters 366 | 367 |
371 | contract 372 | 374 | String (optional) 375 | Filter contract address in log
380 | topic1 381 | 383 | Hexadecimal String (optional) 384 | Filter first topic in log
389 | topic2 390 | 392 | Hexadecimal String (optional) 393 | Filter second topic in log
398 | topic3 399 | 401 | Hexadecimal String (optional) 402 | Filter third topic in log
407 | topic4 408 | 410 | Hexadecimal String (optional) 411 | Filter fourth topic in log
416 | 417 | **Request** 418 | ``` 419 | GET /searchlogs?contract=056168620105d8f73a55d8c6542b565aea3665ec&topic1=2b37430897e8d659983fc8ae7ab83ad5b3be5a7db7ea0add5706731c2395f550 420 | ``` 421 | 422 | **Response** 423 | ```json 424 | { 425 | "totalCount": 5, 426 | "logs": [ 427 | { 428 | "transactionId": "4de540d9be565ac079c735d5d6b641b07be6eeb54f098bda460edf1553b08fe2", 429 | "outputIndex": 0, 430 | "blockHash": "cff6b7b7a5cc2e39228d0f3f010e94eaab7cc2bbddd99ce1860991262a653a9c", 431 | "blockHeight": 171565, 432 | "timestamp": 1528883856, 433 | "sender": "Qar3EUkbk6N9rRw2NtBj8Rsd55jbhj3wLV", 434 | "contractAddress": "056168620105d8f73a55d8c6542b565aea3665ec", 435 | "contractAddressHex": "056168620105d8f73a55d8c6542b565aea3665ec", 436 | "address": "056168620105d8f73a55d8c6542b565aea3665ec", 437 | "addressHex": "056168620105d8f73a55d8c6542b565aea3665ec", 438 | "topics": [ 439 | "2b37430897e8d659983fc8ae7ab83ad5b3be5a7db7ea0add5706731c2395f550", 440 | "0000000000000000000000000000000000000000000000000000000000000000", 441 | "0000000000000000000000009c3a312a838351572971a76aee8a195e3cd2fac7" 442 | ], 443 | "data": "00000000000000000000000000000000000000000000000000000001013e02b90000000000000000000000000000000000000000000000000000000000000000" 444 | }, 445 | { 446 | "transactionId": "97e313601a26deef95267fc1152fcc7065b204fdd2c6df1bbe5bdcaa6053859d", 447 | "outputIndex": 1, 448 | "blockHash": "95c913bec498cdedc187f72849930cbfc624cdf8f7a45b1852d4702e923a3750", 449 | "blockHeight": 171576, 450 | "timestamp": 1528884928, 451 | "sender": "QiyuDBQuvw8PFNZ4nDU7ST7PBYFRmffrh6", 452 | "contractAddress": "056168620105d8f73a55d8c6542b565aea3665ec", 453 | "contractAddressHex": "056168620105d8f73a55d8c6542b565aea3665ec", 454 | "address": "056168620105d8f73a55d8c6542b565aea3665ec", 455 | "addressHex": "056168620105d8f73a55d8c6542b565aea3665ec", 456 | "topics": [ 457 | "2b37430897e8d659983fc8ae7ab83ad5b3be5a7db7ea0add5706731c2395f550", 458 | "0000000000000000000000000000000000000000000000000000000000000000", 459 | "000000000000000000000000f577d44a6e8585cfec280afb990ffc2b414b00ca" 460 | ], 461 | "data": "0000000000000000000000000000000000000000000000000000000d65e579a30000000000000000000000000000000000000000000000000000000000000000" 462 | }, 463 | { 464 | "transactionId": "7f2a6c58440dd6de7f4742aff1969f842921c85024280bc08985b67f7d338c85", 465 | "outputIndex": 1, 466 | "blockHash": "69832b0dc05cbe2c2a35a5d0ae7a7670f8bd59ff3af1adfdae4a27d383c8a0ee", 467 | "blockHeight": 171578, 468 | "timestamp": 1528885280, 469 | "sender": "QXUANYANRVAeX2Tomy9W1FTV6LQxWiNc99", 470 | "contractAddress": "056168620105d8f73a55d8c6542b565aea3665ec", 471 | "contractAddressHex": "056168620105d8f73a55d8c6542b565aea3665ec", 472 | "address": "056168620105d8f73a55d8c6542b565aea3665ec", 473 | "addressHex": "056168620105d8f73a55d8c6542b565aea3665ec", 474 | "topics": [ 475 | "2b37430897e8d659983fc8ae7ab83ad5b3be5a7db7ea0add5706731c2395f550", 476 | "0000000000000000000000000000000000000000000000000000000000000000", 477 | "000000000000000000000000772e9f721feca4a06169938cdaf02584e6c2b529" 478 | ], 479 | "data": "00000000000000000000000000000000000000000000000000000006b2f2bcd10000000000000000000000000000000000000000000000000000000000000000" 480 | }, 481 | { 482 | "transactionId": "1ab6822f1613e15fad017410c2a732dfb40d446ec2de95ea87e29796f13285c6", 483 | "outputIndex": 0, 484 | "blockHash": "592ae3cc803d6f7ea0f00a9af1eea484e0a308767cd45a2e75d2dcf7d6501ec6", 485 | "blockHeight": 171750, 486 | "timestamp": 1528909824, 487 | "sender": "QUkSNYKHFB1HsiwPzv7Lmgr62T16XwWNUd", 488 | "contractAddress": "056168620105d8f73a55d8c6542b565aea3665ec", 489 | "contractAddressHex": "056168620105d8f73a55d8c6542b565aea3665ec", 490 | "address": "056168620105d8f73a55d8c6542b565aea3665ec", 491 | "addressHex": "056168620105d8f73a55d8c6542b565aea3665ec", 492 | "topics": [ 493 | "2b37430897e8d659983fc8ae7ab83ad5b3be5a7db7ea0add5706731c2395f550", 494 | "0000000000000000000000000000000000000000000000000000000000000000", 495 | "000000000000000000000000595a41cd3d6f997beeb1a6fc52f978c28582d2e0" 496 | ], 497 | "data": "00000000000000000000000000000000000000000000000000000000949f2e2000000000000000000000000000000000000000000000000000000002540be400" 498 | }, 499 | { 500 | "transactionId": "fab9e5bc74b504f81f3b32e2ec548746924616dbe9addf379405f38a7231cdb5", 501 | "outputIndex": 0, 502 | "blockHash": "99d238d7527a57b4bf3fc908a6a9996470a2b4ded75d30e76e46bfec57d3fe43", 503 | "blockHeight": 174491, 504 | "timestamp": 1529298256, 505 | "sender": "QaRg6Nrf5uMEggBGv8v8SpzdK6NqWZEf3F", 506 | "contractAddress": "056168620105d8f73a55d8c6542b565aea3665ec", 507 | "contractAddressHex": "056168620105d8f73a55d8c6542b565aea3665ec", 508 | "address": "056168620105d8f73a55d8c6542b565aea3665ec", 509 | "addressHex": "056168620105d8f73a55d8c6542b565aea3665ec", 510 | "topics": [ 511 | "2b37430897e8d659983fc8ae7ab83ad5b3be5a7db7ea0add5706731c2395f550", 512 | "0000000000000000000000000000000000000000000000000000000000000000", 513 | "000000000000000000000000979e8ffce8ba65cc610c22bf412841ab9861ab53" 514 | ], 515 | "data": "00000000000000000000000000000000000000000000000000000042fd7b60310000000000000000000000000000000000000000000000000000000000000000" 516 | } 517 | ] 518 | } 519 | ``` 520 | --------------------------------------------------------------------------------