diff --git a/.github/workflows/branch-coverage.yml b/.github/workflows/branch-coverage.yml index e694af53d..9e6929122 100644 --- a/.github/workflows/branch-coverage.yml +++ b/.github/workflows/branch-coverage.yml @@ -18,11 +18,3 @@ jobs: run: make build-local - name: Check test coverage run: npm run test:coverage - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: true - files: coverage-final.json - name: codecov-umbrella - verbose: true diff --git a/.github/workflows/pr-coverage.yml b/.github/workflows/pr-coverage.yml index 6d91cf5f9..9011b7f87 100644 --- a/.github/workflows/pr-coverage.yml +++ b/.github/workflows/pr-coverage.yml @@ -14,11 +14,3 @@ jobs: run: make build-local - name: Check test coverage run: npm run test:coverage - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: true - files: coverage-final.json - name: codecov-umbrella - verbose: true diff --git a/services/blockchain-connector/shared/sdk/constants/eventTopics.js b/services/blockchain-connector/shared/sdk/constants/eventTopics.js index 18a339e66..6a7ecaa48 100644 --- a/services/blockchain-connector/shared/sdk/constants/eventTopics.js +++ b/services/blockchain-connector/shared/sdk/constants/eventTopics.js @@ -73,6 +73,22 @@ const { MODULE_NAME_LEGACY, EVENT_NAME_ACCOUNT_RECLAIMED, EVENT_NAME_KEYS_REGISTERED, + + MODULE_NAME_SUBSCRIPTION, + EVENT_NAME_SUBSCRIPTION_CREATED, + EVENT_NAME_SUBSCRIPTION_PURCHASED, + + MODULE_NAME_COLLECTION, + EVENT_NAME_COLLECTION_CREATED, + EVENT_NAME_COLLECTION_TRANSFERED, + + MODULE_NAME_AUDIO, + EVENT_NAME_AUDIO_CREATED, + EVENT_NAME_AUDIO_STREAMED, + EVENT_NAME_AUDIO_INCOME_RECLAIMED, + + MODULE_NAME_PROFILE, + EVENT_NAME_PROFILE_CREATED, } = require('./names'); const COMMAND_EXECUTION_RESULT_TOPICS = ['transactionID']; @@ -140,6 +156,22 @@ const EVENT_TOPIC_MAPPINGS_BY_MODULE = { [EVENT_NAME_ACCOUNT_RECLAIMED]: ['transactionID', 'legacyAddress', 'newAddress'], [EVENT_NAME_KEYS_REGISTERED]: ['transactionID', 'validatorAddress', 'generatorKey', 'blsKey'], }, + [MODULE_NAME_SUBSCRIPTION]: { + [EVENT_NAME_SUBSCRIPTION_CREATED]: ['transactionID', 'senderAddress'], + [EVENT_NAME_SUBSCRIPTION_PURCHASED]: ['transactionID', 'senderAddress'], + }, + [MODULE_NAME_COLLECTION]: { + [EVENT_NAME_COLLECTION_CREATED]: ['transactionID', 'senderAddress'], + [EVENT_NAME_COLLECTION_TRANSFERED]: ['transactionID', 'senderAddress'], + }, + [MODULE_NAME_AUDIO]: { + [EVENT_NAME_AUDIO_CREATED]: ['transactionID', 'senderAddress'], + [EVENT_NAME_AUDIO_STREAMED]: ['transactionID', 'senderAddress'], + [EVENT_NAME_AUDIO_INCOME_RECLAIMED]: ['transactionID', 'senderAddress'], + }, + [MODULE_NAME_PROFILE]: { + [EVENT_NAME_PROFILE_CREATED]: ['transactionID', 'senderAddress'], + }, }; module.exports = { diff --git a/services/blockchain-connector/shared/sdk/constants/names.js b/services/blockchain-connector/shared/sdk/constants/names.js index 23de4b687..6ae1526ec 100644 --- a/services/blockchain-connector/shared/sdk/constants/names.js +++ b/services/blockchain-connector/shared/sdk/constants/names.js @@ -85,6 +85,26 @@ const MODULE_NAME_LEGACY = 'legacy'; const EVENT_NAME_ACCOUNT_RECLAIMED = 'accountReclaimed'; const EVENT_NAME_KEYS_REGISTERED = 'keysRegistered'; +// Subscription +const MODULE_NAME_SUBSCRIPTION = 'subscription'; +const EVENT_NAME_SUBSCRIPTION_CREATED = 'subscriptionCreated'; +const EVENT_NAME_SUBSCRIPTION_PURCHASED = 'subscriptionPurchased'; + +// Collection +const MODULE_NAME_COLLECTION = 'collection'; +const EVENT_NAME_COLLECTION_CREATED = 'collectionCreated'; +const EVENT_NAME_COLLECTION_TRANSFERED = 'collectionTransfered'; + +// Audios +const MODULE_NAME_AUDIO = 'audio'; +const EVENT_NAME_AUDIO_CREATED = 'audioCreated'; +const EVENT_NAME_AUDIO_STREAMED = 'audioStreamed'; +const EVENT_NAME_AUDIO_INCOME_RECLAIMED = 'audioIncomeReclaimed'; + +// Profiles +const MODULE_NAME_PROFILE = 'profile'; +const EVENT_NAME_PROFILE_CREATED = 'profileCreated'; + module.exports = { MODULE_NAME_AUTH, EVENT_NAME_MULTISIGNATURE_REGISTERED, @@ -146,4 +166,20 @@ module.exports = { MODULE_NAME_LEGACY, EVENT_NAME_ACCOUNT_RECLAIMED, EVENT_NAME_KEYS_REGISTERED, + + MODULE_NAME_SUBSCRIPTION, + EVENT_NAME_SUBSCRIPTION_CREATED, + EVENT_NAME_SUBSCRIPTION_PURCHASED, + + MODULE_NAME_COLLECTION, + EVENT_NAME_COLLECTION_CREATED, + EVENT_NAME_COLLECTION_TRANSFERED, + + MODULE_NAME_AUDIO, + EVENT_NAME_AUDIO_CREATED, + EVENT_NAME_AUDIO_STREAMED, + EVENT_NAME_AUDIO_INCOME_RECLAIMED, + + MODULE_NAME_PROFILE, + EVENT_NAME_PROFILE_CREATED, }; diff --git a/services/blockchain-indexer/methods/dataService/controllers/audios.js b/services/blockchain-indexer/methods/dataService/controllers/audios.js new file mode 100644 index 000000000..fa9ce364b --- /dev/null +++ b/services/blockchain-indexer/methods/dataService/controllers/audios.js @@ -0,0 +1,31 @@ +const { + HTTP: { StatusCodes: { BAD_REQUEST } }, + Exceptions: { ValidationException, InvalidParamsException }, +} = require('lisk-service-framework'); + +const dataService = require('../../../shared/dataService'); + +const getAudios = async params => { + const audios = { + data: [], + meta: {}, + }; + + try { + const response = await dataService.getAudios(params); + if (response.data) audios.data = response.data; + if (response.meta) audios.meta = response.meta; + + return audios; + } catch (err) { + let status; + if (err instanceof InvalidParamsException) status = 'INVALID_PARAMS'; + if (err instanceof ValidationException) status = BAD_REQUEST; + if (status) return { status, data: { error: err.message } }; + throw err; + } +}; + +module.exports = { + getAudios, +}; diff --git a/services/blockchain-indexer/methods/dataService/controllers/collections.js b/services/blockchain-indexer/methods/dataService/controllers/collections.js new file mode 100644 index 000000000..e34ddefa8 --- /dev/null +++ b/services/blockchain-indexer/methods/dataService/controllers/collections.js @@ -0,0 +1,31 @@ +const { + HTTP: { StatusCodes: { BAD_REQUEST } }, + Exceptions: { ValidationException, InvalidParamsException }, +} = require('lisk-service-framework'); + +const dataService = require('../../../shared/dataService'); + +const getCollections = async params => { + const collections = { + data: [], + meta: {}, + }; + + try { + const response = await dataService.getCollections(params); + if (response.data) collections.data = response.data; + if (response.meta) collections.meta = response.meta; + + return collections; + } catch (err) { + let status; + if (err instanceof InvalidParamsException) status = 'INVALID_PARAMS'; + if (err instanceof ValidationException) status = BAD_REQUEST; + if (status) return { status, data: { error: err.message } }; + throw err; + } +}; + +module.exports = { + getCollections, +}; diff --git a/services/blockchain-indexer/methods/dataService/controllers/profiles.js b/services/blockchain-indexer/methods/dataService/controllers/profiles.js new file mode 100644 index 000000000..512ea767a --- /dev/null +++ b/services/blockchain-indexer/methods/dataService/controllers/profiles.js @@ -0,0 +1,31 @@ +const { + HTTP: { StatusCodes: { BAD_REQUEST } }, + Exceptions: { ValidationException, InvalidParamsException }, +} = require('lisk-service-framework'); + +const dataService = require('../../../shared/dataService'); + +const getProfiles = async params => { + const profiles = { + data: [], + meta: {}, + }; + + try { + const response = await dataService.getProfiles(params); + if (response.data) profiles.data = response.data; + if (response.meta) profiles.meta = response.meta; + + return profiles; + } catch (err) { + let status; + if (err instanceof InvalidParamsException) status = 'INVALID_PARAMS'; + if (err instanceof ValidationException) status = BAD_REQUEST; + if (status) return { status, data: { error: err.message } }; + throw err; + } +}; + +module.exports = { + getProfiles, +}; diff --git a/services/blockchain-indexer/methods/dataService/controllers/subscriptions.js b/services/blockchain-indexer/methods/dataService/controllers/subscriptions.js new file mode 100644 index 000000000..8c33085b5 --- /dev/null +++ b/services/blockchain-indexer/methods/dataService/controllers/subscriptions.js @@ -0,0 +1,31 @@ +const { + HTTP: { StatusCodes: { BAD_REQUEST } }, + Exceptions: { ValidationException, InvalidParamsException }, +} = require('lisk-service-framework'); + +const dataService = require('../../../shared/dataService'); + +const getSubscriptions = async params => { + const subscriptions = { + data: [], + meta: {}, + }; + + try { + const response = await dataService.getSubscriptions(params); + if (response.data) subscriptions.data = response.data; + if (response.meta) subscriptions.meta = response.meta; + + return subscriptions; + } catch (err) { + let status; + if (err instanceof InvalidParamsException) status = 'INVALID_PARAMS'; + if (err instanceof ValidationException) status = BAD_REQUEST; + if (status) return { status, data: { error: err.message } }; + throw err; + } +}; + +module.exports = { + getSubscriptions, +}; diff --git a/services/blockchain-indexer/methods/dataService/modules/audio.js b/services/blockchain-indexer/methods/dataService/modules/audio.js new file mode 100644 index 000000000..e95dfb599 --- /dev/null +++ b/services/blockchain-indexer/methods/dataService/modules/audio.js @@ -0,0 +1,16 @@ +const { + getAudios, +} = require('../controllers/audios'); + +module.exports = [ + { + name: 'audios', + controller: getAudios, + params: { + creatorAddress: { optional: true, type: 'string' }, + audioID: { optional: true, type: 'string' }, + limit: { optional: true, type: 'number' }, + offset: { optional: true, type: 'number' }, + }, + }, +]; diff --git a/services/blockchain-indexer/methods/dataService/modules/collection.js b/services/blockchain-indexer/methods/dataService/modules/collection.js new file mode 100644 index 000000000..76fb7fa94 --- /dev/null +++ b/services/blockchain-indexer/methods/dataService/modules/collection.js @@ -0,0 +1,16 @@ +const { + getCollections, +} = require('../controllers/collections'); + +module.exports = [ + { + name: 'collections', + controller: getCollections, + params: { + creatorAddress: { optional: true, type: 'string' }, + collectionID: { optional: true, type: 'string' }, + limit: { optional: true, type: 'number' }, + offset: { optional: true, type: 'number' }, + }, + }, +]; diff --git a/services/blockchain-indexer/methods/dataService/modules/profile.js b/services/blockchain-indexer/methods/dataService/modules/profile.js new file mode 100644 index 000000000..b09cf5875 --- /dev/null +++ b/services/blockchain-indexer/methods/dataService/modules/profile.js @@ -0,0 +1,16 @@ +const { + getProfiles, +} = require('../controllers/profiles'); + +module.exports = [ + { + name: 'profiles', + controller: getProfiles, + params: { + creatorAddress: { optional: true, type: 'string' }, + profileID: { optional: true, type: 'string' }, + limit: { optional: true, type: 'number' }, + offset: { optional: true, type: 'number' }, + }, + }, +]; diff --git a/services/blockchain-indexer/methods/dataService/modules/subscription.js b/services/blockchain-indexer/methods/dataService/modules/subscription.js new file mode 100644 index 000000000..f13ba86ed --- /dev/null +++ b/services/blockchain-indexer/methods/dataService/modules/subscription.js @@ -0,0 +1,17 @@ +const { + getSubscriptions, +} = require('../controllers/subscriptions'); + +module.exports = [ + { + name: 'subscriptions', + controller: getSubscriptions, + params: { + creatorAddress: { optional: true, type: 'string' }, + subscriptionID: { optional: true, type: 'string' }, + limit: { optional: true, type: 'number' }, + offset: { optional: true, type: 'number' }, + memberAddress: { optional: true, type: 'string' }, + }, + }, +]; diff --git a/services/blockchain-indexer/shared/constants.js b/services/blockchain-indexer/shared/constants.js index 2dc26930c..a7d755638 100644 --- a/services/blockchain-indexer/shared/constants.js +++ b/services/blockchain-indexer/shared/constants.js @@ -132,6 +132,9 @@ const TRANSACTION_VERIFY_RESULT = { OK: 1, }; +// @todo retrieve this from Core +const DEV_ADDRESS = 'lskh96jgzfftzff2fta2zvsmba9mvs5cnz9ahr3ke'; + module.exports = { updateFinalizedHeight, getFinalizedHeight, @@ -155,4 +158,5 @@ module.exports = { KV_STORE_KEY, TRANSACTION_STATUS, TRANSACTION_VERIFY_RESULT, + DEV_ADDRESS, }; diff --git a/services/blockchain-indexer/shared/dataService/audios.js b/services/blockchain-indexer/shared/dataService/audios.js new file mode 100644 index 000000000..22b4bb6d8 --- /dev/null +++ b/services/blockchain-indexer/shared/dataService/audios.js @@ -0,0 +1,22 @@ +const { Logger } = require('lisk-service-framework'); +const util = require('util'); + +const logger = Logger(); + +const business = require('./business'); + +const getAudios = async params => { + // Store logs + if (params.audioID) logger.debug(`Retrieved audio with ID ${params.audioID} from Lisk Core`); + else if (params.creatorAddress) logger.debug(`Retrieved audio with creatorAddress: ${params.creatorAddress} from Lisk Core`); + else logger.debug(`Retrieved audios with custom search: ${util.inspect(params)} from Lisk Core`); + + // Get data from server + const response = await business.getAudios(params); + + return response; +}; + +module.exports = { + getAudios, +}; diff --git a/services/blockchain-indexer/shared/dataService/business/audios.js b/services/blockchain-indexer/shared/dataService/business/audios.js new file mode 100644 index 000000000..07e6c3fd2 --- /dev/null +++ b/services/blockchain-indexer/shared/dataService/business/audios.js @@ -0,0 +1,117 @@ +const { + MySQL: { getTableInstance }, +} = require('lisk-service-framework'); +const BluebirdPromise = require('bluebird'); + +const transactionsIndexSchema = require('../../database/schema/audios'); +const collectionsIndexSchema = require('../../database/schema/collections'); +const ownersIndexSchema = require('../../database/schema/owners'); +const featsIndexSchema = require('../../database/schema/feats'); +const config = require('../../../config'); + +const MYSQL_ENDPOINT = config.endpoints.mysql; + +const getAudiosIndex = () => getTableInstance( + transactionsIndexSchema.tableName, + transactionsIndexSchema, + MYSQL_ENDPOINT, +); + +const getCollectionsIndex = () => getTableInstance( + collectionsIndexSchema.tableName, + collectionsIndexSchema, + MYSQL_ENDPOINT, +); + +const getOwnersIndex = () => getTableInstance( + ownersIndexSchema.tableName, + ownersIndexSchema, + MYSQL_ENDPOINT, +); + +const getFeatsIndex = () => getTableInstance( + featsIndexSchema.tableName, + featsIndexSchema, + MYSQL_ENDPOINT, +); + +const getAudios = async (params = {}) => { + const audiosTable = await getAudiosIndex(); + const collectionsTable = await getCollectionsIndex(); + const ownersTable = await getOwnersIndex(); + const featsTable = await getFeatsIndex(); + + let audioData = []; + + if (params.ownerAddress) { + // audiosID + const audioIDs = await ownersTable.find( + { address: params.ownerAddress, limit: params.limit }, + ['audioID', 'shares'], + ); + + const filteredAudioIDs = audioIDs.filter(audio => audio.shares > 0); + + audioData = await BluebirdPromise.map( + filteredAudioIDs, + async (audioID) => { + const audio = await audiosTable.find( + { audioID: audioID.audioID }, + ['audioID', 'creatorAddress', 'name', 'releaseYear', 'collectionID'], + ); + + return audio[0]; + }, + { concurrency: filteredAudioIDs.length }, + ); + } else { + audioData = await audiosTable.find( + { ...params, limit: params.limit }, + ['audioID', 'creatorAddress', 'name', 'releaseYear', 'collectionID'], + ); + } + + const total = audioData.length; + + const data = await BluebirdPromise.map( + audioData, + async (audio) => { + const collectionData = await collectionsTable.find( + { collectionID: audio.collectionID }, + ['name', 'collectionType', 'releaseYear'], + ); + + const ownersData = await ownersTable.find( + { audioID: audio.audioID }, + ['address', 'shares', 'income'], + ); + + const featData = await featsTable.find( + { audioID: audio.audioID }, + ['address', 'role'], + ); + + return { + ...audio, + collection: collectionData.length ? collectionData[0] : {}, + owners: ownersData, + feat: featData, + }; + }, + { concurrency: audioData.length }, + ); + + const result = { + data, + meta: { + count: data.length, + offset: parseInt(params.offset, 10) || 0, + total, + }, + }; + return result; +}; + +module.exports = { + getAudios, +}; diff --git a/services/blockchain-indexer/shared/dataService/business/collections.js b/services/blockchain-indexer/shared/dataService/business/collections.js new file mode 100644 index 000000000..02c3d797c --- /dev/null +++ b/services/blockchain-indexer/shared/dataService/business/collections.js @@ -0,0 +1,37 @@ +const { + MySQL: { getTableInstance }, +} = require('lisk-service-framework'); +const transactionsIndexSchema = require('../../database/schema/collections'); +const config = require('../../../config'); + +const MYSQL_ENDPOINT = config.endpoints.mysql; + +const getCollectionsIndex = () => getTableInstance( + transactionsIndexSchema.tableName, + transactionsIndexSchema, + MYSQL_ENDPOINT, +); + +const getCollections = async (params = {}) => { + const collectionsTable = await getCollectionsIndex(); + + const total = await collectionsTable.count(params); + const resultSet = await collectionsTable.find( + { ...params, limit: params.limit || 10 }, + ['collectionID', 'creatorAddress', 'name', 'releaseYear', 'collectionType'], + ); + + const result = { + data: resultSet, + meta: { + count: resultSet.length, + offset: parseInt(params.offset, 10) || 0, + total, + }, + }; + return result; +}; + +module.exports = { + getCollections, +}; diff --git a/services/blockchain-indexer/shared/dataService/business/index.js b/services/blockchain-indexer/shared/dataService/business/index.js index e05a68a11..55f285076 100644 --- a/services/blockchain-indexer/shared/dataService/business/index.js +++ b/services/blockchain-indexer/shared/dataService/business/index.js @@ -95,6 +95,12 @@ const { getNetworkPeersStatistics, } = require('./network'); +// Muzikie Dedicated Modules +const { getSubscriptions } = require('./subscriptions'); +const { getCollections } = require('./collections'); +const { getAudios } = require('./audios'); +const { getProfiles } = require('./profiles'); + module.exports = { // Generators getGenerators, @@ -175,4 +181,16 @@ module.exports = { getNetworkConnectedPeers, getNetworkDisconnectedPeers, getNetworkPeersStatistics, + + // subscriptions + getSubscriptions, + + // collections + getCollections, + + // audios + getAudios, + + // profiles + getProfiles, }; diff --git a/services/blockchain-indexer/shared/dataService/business/profiles.js b/services/blockchain-indexer/shared/dataService/business/profiles.js new file mode 100644 index 000000000..7b251bbee --- /dev/null +++ b/services/blockchain-indexer/shared/dataService/business/profiles.js @@ -0,0 +1,61 @@ +const { + MySQL: { getTableInstance }, +} = require('lisk-service-framework'); +const BluebirdPromise = require('bluebird'); +const transactionsIndexSchema = require('../../database/schema/profiles'); +const socialAccountsIndexSchema = require('../../database/schema/socialAccounts'); +const config = require('../../../config'); + +const MYSQL_ENDPOINT = config.endpoints.mysql; + +const getProfilesIndex = () => getTableInstance( + transactionsIndexSchema.tableName, + transactionsIndexSchema, + MYSQL_ENDPOINT, +); + +const getSocialAccountsIndex = () => getTableInstance( + socialAccountsIndexSchema.tableName, + socialAccountsIndexSchema, + MYSQL_ENDPOINT, +); + +const getProfiles = async (params = {}) => { + const profilesTable = await getProfilesIndex(); + const total = await profilesTable.count(params); + const profilesData = await profilesTable.find( + { ...params, limit: params.limit || 10 }, + ['profileID', 'name', 'nickName', 'description', 'creatorAddress', 'avatarHash', 'avatarSignature', 'bannerHash', 'bannerSignature'], + ); + const socialAccountsTable = await getSocialAccountsIndex(); + + const data = await BluebirdPromise.map( + profilesData, + async (profile) => { + const socialData = await socialAccountsTable.find( + { profileID: profile.profileID }, + ['profileID', 'username', 'platform'], + ); + + return { + ...profile, + socialAccounts: socialData, + }; + }, + { concurrency: profilesData.length }, + ); + + const result = { + data, + meta: { + count: data.length, + offset: parseInt(params.offset, 10) || 0, + total, + }, + }; + return result; +}; + +module.exports = { + getProfiles, +}; diff --git a/services/blockchain-indexer/shared/dataService/business/subscriptions.js b/services/blockchain-indexer/shared/dataService/business/subscriptions.js new file mode 100644 index 000000000..6aa6fb0bf --- /dev/null +++ b/services/blockchain-indexer/shared/dataService/business/subscriptions.js @@ -0,0 +1,111 @@ +const { + MySQL: { getTableInstance }, +} = require('lisk-service-framework'); +const BluebirdPromise = require('bluebird'); + +const transactionsIndexSchema = require('../../database/schema/subscriptions'); +const membersIndexSchema = require('../../database/schema/members'); +const config = require('../../../config'); + +const MYSQL_ENDPOINT = config.endpoints.mysql; + +const getSubscriptionsIndex = () => getTableInstance( + transactionsIndexSchema.tableName, + transactionsIndexSchema, + MYSQL_ENDPOINT, +); + +const getMembersIndex = () => getTableInstance( + membersIndexSchema.tableName, + membersIndexSchema, + MYSQL_ENDPOINT, +); +const getActiveSubscriptionsByMemberAddress = async (params = {}) => { + const membersTable = await getMembersIndex(); + const subscriptionsTable = await getSubscriptionsIndex(); + const result = { + data: {}, + meta: { + count: 0, + offset: parseInt(params.offset, 10) || 0, + total: 0, + }, + }; + try { + const member = await membersTable.find( + { address: params.memberAddress }, + ['id', 'address', 'shared', 'addedBy', 'removedBy'], + ); + if (member) { + const data = await subscriptionsTable.find( + { subscriptionID: member[0].shared }, + ['subscriptionID', 'creatorAddress', 'price', 'consumable', 'maxMembers', 'streams'], + ); + if (data) { + return { + data, + meta: { + count: 1, + offset: parseInt(params.offset, 10) || 0, + total: 1, + }, + }; + } + } + return result; + } catch (error) { + return { + data: {}, + meta: { + count: 0, + offset: parseInt(params.offset, 10) || 0, + total: 0, + }, + }; + } +}; + +const getSubscriptionsBySubscriptionIdOrCreatorAddress = async (params = {}) => { + const subscriptionsTable = await getSubscriptionsIndex(); + const membersTable = await getMembersIndex(); + + const total = await subscriptionsTable.count(params); + const subscriptionSet = await subscriptionsTable.find( + { ...params, limit: params.limit || total }, + ['subscriptionID', 'creatorAddress', 'price', 'consumable', 'maxMembers', 'streams'], + ); + + const data = await BluebirdPromise.map( + subscriptionSet, + async subscription => { + const membersSet = await membersTable.find( + { shared: subscription.subscriptionID, removedBy: null }, + ['address'], + ); + subscription.members = membersSet.map(member => ({ address: member.address })); + return subscription; + }, + { concurrency: subscriptionSet.length }, + ); + + const result = { + data, + meta: { + count: data.length, + offset: parseInt(params.offset, 10) || 0, + total, + }, + }; + return result; +}; + +const getSubscriptions = async (params = {}) => { + if (params.memberAddress) { + return getActiveSubscriptionsByMemberAddress(params); + } + return getSubscriptionsBySubscriptionIdOrCreatorAddress(params); +}; + +module.exports = { + getSubscriptions, +}; diff --git a/services/blockchain-indexer/shared/dataService/collections.js b/services/blockchain-indexer/shared/dataService/collections.js new file mode 100644 index 000000000..06c0380ca --- /dev/null +++ b/services/blockchain-indexer/shared/dataService/collections.js @@ -0,0 +1,22 @@ +const { Logger } = require('lisk-service-framework'); +const util = require('util'); + +const logger = Logger(); + +const business = require('./business'); + +const getCollections = async params => { + // Store logs + if (params.collectionID) logger.debug(`Retrieved collection with ID ${params.collectionID} from Lisk Core`); + else if (params.creatorAddress) logger.debug(`Retrieved collection with creatorAddress: ${params.creatorAddress} from Lisk Core`); + else logger.debug(`Retrieved collections with custom search: ${util.inspect(params)} from Lisk Core`); + + // Get data from server + const response = await business.getCollections(params); + + return response; +}; + +module.exports = { + getCollections, +}; diff --git a/services/blockchain-indexer/shared/dataService/index.js b/services/blockchain-indexer/shared/dataService/index.js index e36bf8a25..d6c93a838 100644 --- a/services/blockchain-indexer/shared/dataService/index.js +++ b/services/blockchain-indexer/shared/dataService/index.js @@ -93,6 +93,10 @@ const { getIndexStatus } = require('./indexStatus'); const { getLegacyAccountInfo } = require('./legacy'); const { getValidator, validateBLSKey } = require('./validator'); const { getGenerators } = require('./generators'); +const { getSubscriptions } = require('./subscriptions'); +const { getCollections } = require('./collections'); +const { getAudios } = require('./audios'); +const { getProfiles } = require('./profiles'); module.exports = { // Blocks @@ -180,4 +184,16 @@ module.exports = { getAnnualInflation, getDefaultRewardAtHeight, getRewardConstants, + + // Subscriptions + getSubscriptions, + + // Collections + getCollections, + + // Audios + getAudios, + + // Profiles + getProfiles, }; diff --git a/services/blockchain-indexer/shared/dataService/profiles.js b/services/blockchain-indexer/shared/dataService/profiles.js new file mode 100644 index 000000000..a6741da7a --- /dev/null +++ b/services/blockchain-indexer/shared/dataService/profiles.js @@ -0,0 +1,22 @@ +const { Logger } = require('lisk-service-framework'); +const util = require('util'); + +const logger = Logger(); + +const business = require('./business'); + +const getProfiles = async params => { + // Store logs + if (params.profileID) logger.debug(`Retrieved profile with ID ${params.profileID} from Lisk Core`); + else if (params.creatorAddress) logger.debug(`Retrieved profile with creatorAddress: ${params.creatorAddress} from Lisk Core`); + else logger.debug(`Retrieved profile with custom search: ${util.inspect(params)} from Lisk Core`); + + // Get data from server + const response = await business.getProfiles(params); + + return response; +}; + +module.exports = { + getProfiles, +}; diff --git a/services/blockchain-indexer/shared/dataService/subscriptions.js b/services/blockchain-indexer/shared/dataService/subscriptions.js new file mode 100644 index 000000000..8f647cc2e --- /dev/null +++ b/services/blockchain-indexer/shared/dataService/subscriptions.js @@ -0,0 +1,23 @@ +const { Logger } = require('lisk-service-framework'); +const util = require('util'); + +const logger = Logger(); + +const business = require('./business'); + +const getSubscriptions = async params => { + // Store logs + if (params.memberAddress) logger.debug(`Retrieved active subscription for member ${params.memberAddress} from Lisk Core`); + if (params.subscriptionID) logger.debug(`Retrieved subscription with ID ${params.subscriptionID} from Lisk Core`); + else if (params.creatorAddress) logger.debug(`Retrieved subscription with creatorAddress: ${params.creatorAddress} from Lisk Core`); + else logger.debug(`Retrieved subscription with custom search: ${util.inspect(params)} from Lisk Core`); + + // Get data from server + const response = await business.getSubscriptions(params); + + return response; +}; + +module.exports = { + getSubscriptions, +}; diff --git a/services/blockchain-indexer/shared/database/schema/audios.js b/services/blockchain-indexer/shared/database/schema/audios.js new file mode 100644 index 000000000..0010a770a --- /dev/null +++ b/services/blockchain-indexer/shared/database/schema/audios.js @@ -0,0 +1,20 @@ +module.exports = { + tableName: 'audios', + primaryKey: 'audioID', + schema: { + audioID: { type: 'string' }, + name: { type: 'string' }, + releaseYear: { type: 'string' }, + collectionID: { type: 'string' }, + audioSignature: { type: 'string' }, + audioHash: { type: 'string' }, + creatorAddress: { type: 'string' }, + }, + indexes: { + creatorAddress: { type: 'string' }, + }, + purge: {}, +}; + +// genre: number[]; +// fit: Buffer[]; diff --git a/services/blockchain-indexer/shared/database/schema/collections.js b/services/blockchain-indexer/shared/database/schema/collections.js new file mode 100644 index 000000000..a4dfbd621 --- /dev/null +++ b/services/blockchain-indexer/shared/database/schema/collections.js @@ -0,0 +1,17 @@ +module.exports = { + tableName: 'collections', + primaryKey: 'collectionID', + schema: { + collectionID: { type: 'string', null: true, defaultValue: null }, + name: { type: 'string', null: true, defaultValue: null }, + releaseYear: { type: 'string', null: true, defaultValue: null }, + collectionType: { type: 'integer', null: true, defaultValue: null }, + coverSignature: { type: 'string', null: true, defaultValue: null }, + coverHash: { type: 'string', null: true, defaultValue: null }, + creatorAddress: { type: 'string', null: true, defaultValue: null }, + }, + indexes: { + creatorAddress: { type: 'string' }, + }, + purge: {}, +}; diff --git a/services/blockchain-indexer/shared/database/schema/feats.js b/services/blockchain-indexer/shared/database/schema/feats.js new file mode 100644 index 000000000..aabaff8d9 --- /dev/null +++ b/services/blockchain-indexer/shared/database/schema/feats.js @@ -0,0 +1,13 @@ +module.exports = { + tableName: 'feats', + primaryKey: ['address', 'audioID'], + schema: { + address: { type: 'string', null: true, defaultValue: null }, + audioID: { type: 'string' }, + role: { type: 'string', null: true, defaultValue: null }, + }, + indexes: { + creatorAddress: { type: 'string' }, + }, + purge: {}, +}; diff --git a/services/blockchain-indexer/shared/database/schema/members.js b/services/blockchain-indexer/shared/database/schema/members.js new file mode 100644 index 000000000..3f1aacd2a --- /dev/null +++ b/services/blockchain-indexer/shared/database/schema/members.js @@ -0,0 +1,15 @@ +module.exports = { + tableName: 'members', + primaryKey: 'id', + schema: { + id: { type: 'string' }, // memberAddress-nonce + address: { type: 'string' }, // memberAddress + shared: { type: 'string', null: true }, // subscriptionID + addedBy: { type: 'string', null: true }, // transactionID + removedBy: { type: 'string', null: true }, // transactionID + }, + indexes: { + shared: { type: 'key' }, + }, + purge: {}, +}; diff --git a/services/blockchain-indexer/shared/database/schema/owners.js b/services/blockchain-indexer/shared/database/schema/owners.js new file mode 100644 index 000000000..304748ba3 --- /dev/null +++ b/services/blockchain-indexer/shared/database/schema/owners.js @@ -0,0 +1,14 @@ +module.exports = { + tableName: 'owners', + primaryKey: ['address', 'audioID'], + schema: { + address: { type: 'string' }, + audioID: { type: 'string' }, + shares: { type: 'integer' }, + income: { type: 'bigInteger', defaultValue: 0 }, + }, + indexes: { + creatorAddress: { type: 'string' }, + }, + purge: {}, +}; diff --git a/services/blockchain-indexer/shared/database/schema/profiles.js b/services/blockchain-indexer/shared/database/schema/profiles.js new file mode 100644 index 000000000..54acb4120 --- /dev/null +++ b/services/blockchain-indexer/shared/database/schema/profiles.js @@ -0,0 +1,20 @@ +module.exports = { + tableName: 'profiles', + primaryKey: 'profileID', + schema: { + profileID: { type: 'string' }, + name: { type: 'string', null: true, defaultValue: null }, + nickName: { type: 'string', null: true, defaultValue: null }, + description: { type: 'string', null: true, defaultValue: null }, + avatarHash: { type: 'string', null: true, defaultValue: null }, + avatarSignature: { type: 'string', null: true, defaultValue: null }, + bannerHash: { type: 'string', null: true, defaultValue: null }, + bannerSignature: { type: 'string', null: true, defaultValue: null }, + // @TODO: creationDate: { type: 'string', null: true, defaultValue: null }, + creatorAddress: { type: 'string', null: true, defaultValue: null }, + }, + indexes: { + creatorAddress: { type: 'string' }, + }, + purge: {}, +}; diff --git a/services/blockchain-indexer/shared/database/schema/socialAccounts.js b/services/blockchain-indexer/shared/database/schema/socialAccounts.js new file mode 100644 index 000000000..c6d884724 --- /dev/null +++ b/services/blockchain-indexer/shared/database/schema/socialAccounts.js @@ -0,0 +1,13 @@ +module.exports = { + tableName: 'socialAccounts', + primaryKey: ['profileID', 'platform'], + schema: { + profileID: { type: 'string' }, + username: { type: 'string', null: true, defaultValue: null }, + platform: { type: 'string' }, + }, + indexes: { + profileID: { type: 'string' }, + }, + purge: {}, +}; diff --git a/services/blockchain-indexer/shared/database/schema/subscriptions.js b/services/blockchain-indexer/shared/database/schema/subscriptions.js new file mode 100644 index 000000000..9246dbf16 --- /dev/null +++ b/services/blockchain-indexer/shared/database/schema/subscriptions.js @@ -0,0 +1,16 @@ +module.exports = { + tableName: 'subscriptions', + primaryKey: 'subscriptionID', + schema: { + subscriptionID: { type: 'string', null: true, defaultValue: null }, + creatorAddress: { type: 'string', null: true, defaultValue: null }, // at first->DEV but then->user + price: { type: 'bigInteger', null: true, defaultValue: null }, + consumable: { type: 'bigInteger', null: true, defaultValue: null }, + streams: { type: 'bigInteger', null: true, defaultValue: null }, + maxMembers: { type: 'integer', null: true, defaultValue: null }, + }, + indexes: { + creatorAddress: { type: 'string' }, + }, + purge: {}, +}; diff --git a/services/blockchain-indexer/shared/indexer/transactionProcessor/audio/create.js b/services/blockchain-indexer/shared/indexer/transactionProcessor/audio/create.js new file mode 100644 index 000000000..686b273b1 --- /dev/null +++ b/services/blockchain-indexer/shared/indexer/transactionProcessor/audio/create.js @@ -0,0 +1,163 @@ +const { + Logger, + MySQL: { getTableInstance }, +} = require('lisk-service-framework'); +const BluebirdPromise = require('bluebird'); + +const { getLisk32AddressFromPublicKey } = require('../../../utils/account'); + +const config = require('../../../../config'); + +const logger = Logger(); + +const MYSQL_ENDPOINT = config.endpoints.mysql; +const accountsTableSchema = require('../../../database/schema/accounts'); +const audiosTableSchema = require('../../../database/schema/audios'); +const ownersTableSchema = require('../../../database/schema/owners'); +const featsTableSchema = require('../../../database/schema/feats'); +const { + MODULE_NAME_AUDIO, + EVENT_NAME_AUDIO_CREATED, + EVENT_NAME_COMMAND_EXECUTION_RESULT, +} = require('../../../../../blockchain-connector/shared/sdk/constants/names'); + +const getAccountsTable = () => getTableInstance( + accountsTableSchema.tableName, + accountsTableSchema, + MYSQL_ENDPOINT, +); + +const getAudiosTable = () => getTableInstance( + audiosTableSchema.tableName, + audiosTableSchema, + MYSQL_ENDPOINT, +); + +const getOwnersTable = () => getTableInstance( + ownersTableSchema.tableName, + ownersTableSchema, + MYSQL_ENDPOINT, +); + +const getFeatsTable = () => getTableInstance( + featsTableSchema.tableName, + featsTableSchema, + MYSQL_ENDPOINT, +); + +// Command specific constants +const COMMAND_NAME = 'create'; + +// eslint-disable-next-line no-unused-vars +const applyTransaction = async (blockHeader, tx, events, dbTrx) => { + // Do not process failed transactions + const { data: commandExecutedData = {} } = events.find( + ({ module, name }) => module === MODULE_NAME_AUDIO + && name === EVENT_NAME_COMMAND_EXECUTION_RESULT, + ); + if (!commandExecutedData.success) { + return false; + } + + const accountsTable = await getAccountsTable(); + const audiosTable = await getAudiosTable(); + const ownersTable = await getOwnersTable(); + const featsTable = await getFeatsTable(); + + // Use event data to get audioID + const eventData = events.find( + ({ module, name }) => module === MODULE_NAME_AUDIO + && name === EVENT_NAME_AUDIO_CREATED, + ); + const { data: audioCreatedData } = eventData || { data: {} }; + + const senderAddress = getLisk32AddressFromPublicKey(tx.senderPublicKey); + + // Store a record of the sender account + const account = { + address: senderAddress, + }; + + logger.trace(`Updating account index for the account with address ${account.address}.`); + await accountsTable.upsert(account, dbTrx); + logger.debug(`Updated account index for the account with address ${account.address}.`); + + // Store owners + await BluebirdPromise.map( + tx.params.owners, + async owner => { + const ownerInfo = { + ...owner, + audioID: audioCreatedData.audioID, + }; + logger.trace(`Updating owner index for the account with address ${owner.address}.`); + await ownersTable.upsert(ownerInfo, dbTrx); + logger.debug(`Updated owner index for the account with address ${owner.address}.`); + return true; + }, + { concurrency: tx.params.owners.length }, + ); + + // Store feats + await BluebirdPromise.map( + tx.params.feat, + async feat => { + const featInfo = { + address: feat, + role: 'co-artist', + audioID: audioCreatedData.audioID, + }; + logger.trace(`Updating feats index for the account with address ${feat}.`); + await featsTable.upsert(featInfo, dbTrx); + logger.debug(`Updated feats index for the account with address ${feat}.`); + return true; + }, + { concurrency: tx.params.feat.length }, + ); + + logger.trace(`Updating owners index for the audio with audioID ${account.address}.`); + await accountsTable.upsert(account, dbTrx); + logger.debug(`Updated account index for the account with address ${account.address}.`); + + logger.trace(`Indexing audios with address ${account.address}.`); + + // And finally, store the audio + const audiosNFT = { + ...audioCreatedData, + ...tx.params, + }; + + await audiosTable.upsert(audiosNFT, dbTrx); + logger.debug(`Indexed audio with ID ${audioCreatedData.audioID}.`); + return true; +}; + +// eslint-disable-next-line no-unused-vars +const revertTransaction = async (blockHeader, tx, events, dbTrx) => { + const audiosTable = await getAudiosTable(); + const ownersTable = await getOwnersTable(); + const featsTable = await getFeatsTable(); + + const { data: audioCreatedData = {} } = events.find( + ({ module, name }) => module === MODULE_NAME_AUDIO + && name === EVENT_NAME_AUDIO_CREATED, + ); + + logger.trace(`Deleting owners corresponding the audio ID ${audioCreatedData.audioID}.`); + await ownersTable.delete({ audioID: audioCreatedData.audioID }, dbTrx); + logger.trace(`Deleted owners corresponding the audio ID ${audioCreatedData.audioID}.`); + + logger.trace(`Deleting feats corresponding the audio ID ${audioCreatedData.audioID}.`); + await featsTable.delete({ audioID: audioCreatedData.audioID }, dbTrx); + logger.trace(`Deleted feats corresponding the audio ID ${audioCreatedData.audioID}.`); + + logger.trace(`Removing audio entry for ID ${audioCreatedData.audioID}.`); + await audiosTable.deleteByPrimaryKey(audioCreatedData.audioID, dbTrx); + logger.debug(`Removed audio entry for ID ${audioCreatedData.audioID}.`); +}; + +module.exports = { + COMMAND_NAME, + applyTransaction, + revertTransaction, +}; diff --git a/services/blockchain-indexer/shared/indexer/transactionProcessor/audio/destroy.js b/services/blockchain-indexer/shared/indexer/transactionProcessor/audio/destroy.js new file mode 100644 index 000000000..824c1a9fc --- /dev/null +++ b/services/blockchain-indexer/shared/indexer/transactionProcessor/audio/destroy.js @@ -0,0 +1,79 @@ +const { + Logger, + MySQL: { getTableInstance }, +} = require('lisk-service-framework'); + +const config = require('../../../../config'); + +const logger = Logger(); + +const MYSQL_ENDPOINT = config.endpoints.mysql; +const audiosTableSchema = require('../../../database/schema/audios'); +const ownersTableSchema = require('../../../database/schema/owners'); +const featsTableSchema = require('../../../database/schema/feats'); + +const { + MODULE_NAME_AUDIO, + EVENT_NAME_COMMAND_EXECUTION_RESULT, +} = require('../../../../../blockchain-connector/shared/sdk/constants/names'); + +const getAudiosTable = () => getTableInstance( + audiosTableSchema.tableName, + audiosTableSchema, + MYSQL_ENDPOINT, +); + +const getOwnersTable = () => getTableInstance( + ownersTableSchema.tableName, + ownersTableSchema, + MYSQL_ENDPOINT, +); + +const getFeatsTable = () => getTableInstance( + featsTableSchema.tableName, + featsTableSchema, + MYSQL_ENDPOINT, +); + +// Command specific constants +const COMMAND_NAME = 'destroy'; + +// eslint-disable-next-line no-unused-vars +const applyTransaction = async (blockHeader, tx, events, dbTrx) => { + const { data: commandExecutedData = {} } = events.find( + ({ module, name }) => module === MODULE_NAME_AUDIO + && name === EVENT_NAME_COMMAND_EXECUTION_RESULT, + ); + if (!commandExecutedData.success) { + return false; + } + + const { audioID } = tx.params; + + const audiosTable = await getAudiosTable(); + const ownersTable = await getOwnersTable(); + const featsTable = await getFeatsTable(); + + logger.trace(`Removing audio index for the audio with ID ${audioID}.`); + await audiosTable.delete({ audioID }, dbTrx); + logger.trace(`Removed audio index for the audio with ID ${audioID}.`); + + logger.trace(`Removing owners index for the audio with ID ${audioID}.`); + await ownersTable.delete({ audioID }, dbTrx); + logger.trace(`Removed owners index for the audio with ID ${audioID}.`); + + logger.trace(`Removing feats index for the audio with ID ${audioID}.`); + await featsTable.delete({ audioID }, dbTrx); + logger.trace(`Removed feats index for the audio with ID ${audioID}.`); + + return true; +}; + +// eslint-disable-next-line no-unused-vars +const revertTransaction = async (blockHeader, tx, events, dbTrx) => {}; + +module.exports = { + COMMAND_NAME, + applyTransaction, + revertTransaction, +}; diff --git a/services/blockchain-indexer/shared/indexer/transactionProcessor/audio/index.js b/services/blockchain-indexer/shared/indexer/transactionProcessor/audio/index.js new file mode 100644 index 000000000..d316851af --- /dev/null +++ b/services/blockchain-indexer/shared/indexer/transactionProcessor/audio/index.js @@ -0,0 +1,6 @@ +// Module specific constants +const MODULE_NAME = 'audio'; + +module.exports = { + MODULE_NAME, +}; diff --git a/services/blockchain-indexer/shared/indexer/transactionProcessor/audio/reclaim.js b/services/blockchain-indexer/shared/indexer/transactionProcessor/audio/reclaim.js new file mode 100644 index 000000000..9e377b4eb --- /dev/null +++ b/services/blockchain-indexer/shared/indexer/transactionProcessor/audio/reclaim.js @@ -0,0 +1,76 @@ +const { + Logger, + MySQL: { getTableInstance }, +} = require('lisk-service-framework'); +const BluebirdPromise = require('bluebird'); + +const config = require('../../../../config'); + +const logger = Logger(); + +const MYSQL_ENDPOINT = config.endpoints.mysql; +const ownersTableSchema = require('../../../database/schema/owners'); + +const { + MODULE_NAME_AUDIO, + EVENT_NAME_COMMAND_EXECUTION_RESULT, + EVENT_NAME_AUDIO_INCOME_RECLAIMED, +} = require('../../../../../blockchain-connector/shared/sdk/constants/names'); + +const getOwnersTable = () => getTableInstance( + ownersTableSchema.tableName, + ownersTableSchema, + MYSQL_ENDPOINT, +); + +// Command specific constants +const COMMAND_NAME = 'reclaim'; + +// eslint-disable-next-line no-unused-vars +const applyTransaction = async (blockHeader, tx, events, dbTrx) => { + const { data: commandExecutedData = {} } = events.find( + ({ module, name }) => module === MODULE_NAME_AUDIO + && name === EVENT_NAME_COMMAND_EXECUTION_RESULT, + ); + if (!commandExecutedData.success) { + return false; + } + + const ownersTable = await getOwnersTable(); + + // Use event data to get audioID + const eventData = events.find( + ({ module, name }) => module === MODULE_NAME_AUDIO + && name === EVENT_NAME_AUDIO_INCOME_RECLAIMED, + ); + const { data: audioIncomeReclaimedData } = eventData || { data: { claimData: { audioIDs: [] } } }; + + await BluebirdPromise.map( + audioIncomeReclaimedData.claimData.audioIDs, + async audioID => { + const owners = await ownersTable.find({ audioID }, ['address', 'shares', 'income'], dbTrx); + const sender = owners.find(owner => owner.address === tx.senderAddress); + const info = { + ...sender, + audioID, + income: 0, + }; + logger.trace(`Updating owner index for the account with address ${sender.address} and audioID: ${audioID}.`); + await ownersTable.upsert(info, dbTrx); + logger.debug(`Updated owner index for the account with address ${sender.address} and audioID: ${audioID}.`); + return true; + }, + { concurrency: audioIncomeReclaimedData.claimData.audioIDs.length }, + ); + + return true; +}; + +// eslint-disable-next-line no-unused-vars +const revertTransaction = async (blockHeader, tx, events, dbTrx) => {}; + +module.exports = { + COMMAND_NAME, + applyTransaction, + revertTransaction, +}; diff --git a/services/blockchain-indexer/shared/indexer/transactionProcessor/audio/setAttributes.js b/services/blockchain-indexer/shared/indexer/transactionProcessor/audio/setAttributes.js new file mode 100644 index 000000000..3c97464e7 --- /dev/null +++ b/services/blockchain-indexer/shared/indexer/transactionProcessor/audio/setAttributes.js @@ -0,0 +1,119 @@ +const { + Logger, + MySQL: { getTableInstance }, +} = require('lisk-service-framework'); +const BluebirdPromise = require('bluebird'); + +const config = require('../../../../config'); + +const logger = Logger(); + +const MYSQL_ENDPOINT = config.endpoints.mysql; +const audiosTableSchema = require('../../../database/schema/audios'); +const featsTableSchema = require('../../../database/schema/feats'); + +const { + MODULE_NAME_AUDIO, + EVENT_NAME_COMMAND_EXECUTION_RESULT, +} = require('../../../../../blockchain-connector/shared/sdk/constants/names'); + +const getAudiosTable = () => getTableInstance( + audiosTableSchema.tableName, + audiosTableSchema, + MYSQL_ENDPOINT, +); + +const getFitsTable = () => getTableInstance( + featsTableSchema.tableName, + featsTableSchema, + MYSQL_ENDPOINT, +); + +// Command specific constants +const COMMAND_NAME = 'setAttributes'; + +// eslint-disable-next-line no-unused-vars +const applyTransaction = async (blockHeader, tx, events, dbTrx) => { + // Do not process failed transactions + const { data: commandExecutedData = {} } = events.find( + ({ module, name }) => module === MODULE_NAME_AUDIO + && name === EVENT_NAME_COMMAND_EXECUTION_RESULT, + ); + if (!commandExecutedData.success) { + return false; + } + + const audiosTable = await getAudiosTable(); + const featsTable = await getFitsTable(); + + const { audioID } = tx.params; + + // Find the audio to process + const [audio] = await audiosTable.find( + { audioID }, + ['name', 'releaseYear', 'collectionID', 'creatorAddress'], + dbTrx, + ); + const oldFeats = await featsTable.find( + { audioID }, + ['address'], + dbTrx, + ); + const oldFeatsAddresses = oldFeats.map(({ address }) => address); + + // Define removed and added feats + const removedFeats = oldFeatsAddresses.filter( + address => !tx.params.feat.includes(address), + ); + const addedFeats = tx.params.feat.filter( + address => !oldFeatsAddresses.includes(address), + ); + + // Store new feats + await BluebirdPromise.map( + addedFeats, + async address => { + const featInfo = { + address, + role: 'co-artist', // TODO: get role from tx.params.feat + audioID, + }; + logger.trace(`Updating feats index for the account with address ${address}.`); + await featsTable.upsert(featInfo, dbTrx); + logger.debug(`Updated feats index for the account with address ${address}.`); + return true; + }, + { concurrency: tx.params.feat.length }, + ); + + // Remove old feats + await BluebirdPromise.map( + removedFeats, + async address => { + logger.trace(`Updating feats index for the account with address ${address}.`); + await featsTable.delete({ address, audioID }, dbTrx); + logger.debug(`Updated feats index for the account with address ${address}.`); + return true; + }, + { concurrency: tx.params.feat.length }, + ); + + logger.trace(`Updating audio with ID ${audioID}.`); + const audiosNFT = { + ...audio, + ...tx.params, + }; + + await audiosTable.upsert(audiosNFT, dbTrx); + logger.debug(`Updated audio with ID ${audioID}.`); + return true; +}; + +// eslint-disable-next-line no-unused-vars +const revertTransaction = async (blockHeader, tx, events, dbTrx) => {}; + +module.exports = { + COMMAND_NAME, + applyTransaction, + revertTransaction, +}; diff --git a/services/blockchain-indexer/shared/indexer/transactionProcessor/audio/stream.js b/services/blockchain-indexer/shared/indexer/transactionProcessor/audio/stream.js new file mode 100644 index 000000000..415a9a77e --- /dev/null +++ b/services/blockchain-indexer/shared/indexer/transactionProcessor/audio/stream.js @@ -0,0 +1,74 @@ +const { + Logger, + MySQL: { getTableInstance }, +} = require('lisk-service-framework'); +const BluebirdPromise = require('bluebird'); + +const config = require('../../../../config'); + +const logger = Logger(); + +const MYSQL_ENDPOINT = config.endpoints.mysql; +const ownersTableSchema = require('../../../database/schema/owners'); + +const { + MODULE_NAME_AUDIO, + EVENT_NAME_AUDIO_STREAMED, + EVENT_NAME_COMMAND_EXECUTION_RESULT, +} = require('../../../../../blockchain-connector/shared/sdk/constants/names'); + +const getOwnersTable = () => getTableInstance( + ownersTableSchema.tableName, + ownersTableSchema, + MYSQL_ENDPOINT, +); + +// Command specific constants +const COMMAND_NAME = 'stream'; + +// eslint-disable-next-line no-unused-vars +const applyTransaction = async (blockHeader, tx, events, dbTrx) => { + const { data: commandExecutedData = {} } = events.find( + ({ module, name }) => module === MODULE_NAME_AUDIO + && name === EVENT_NAME_COMMAND_EXECUTION_RESULT, + ); + if (!commandExecutedData.success) { + return false; + } + + const ownersTable = await getOwnersTable(); + + const { audioID } = tx.params; + + // Use event data to get audioID + const { data: audioStreamedData = {} } = events.find( + ({ module, name }) => module === MODULE_NAME_AUDIO + && name === EVENT_NAME_AUDIO_STREAMED, + ); + + await BluebirdPromise.map( + audioStreamedData.owners, + async owner => { + const info = { + ...owner, + audioID, + }; + logger.trace(`Updating owner index for the account with address ${owner.address}.`); + await ownersTable.upsert(info, dbTrx); + logger.debug(`Updated owner index for the account with address ${owner.address}.`); + return true; + }, + { concurrency: audioStreamedData.owners.length }, + ); + + return true; +}; + +// eslint-disable-next-line no-unused-vars +const revertTransaction = async (blockHeader, tx, events, dbTrx) => {}; + +module.exports = { + COMMAND_NAME, + applyTransaction, + revertTransaction, +}; diff --git a/services/blockchain-indexer/shared/indexer/transactionProcessor/audio/transfer.js b/services/blockchain-indexer/shared/indexer/transactionProcessor/audio/transfer.js new file mode 100644 index 000000000..53104eb2c --- /dev/null +++ b/services/blockchain-indexer/shared/indexer/transactionProcessor/audio/transfer.js @@ -0,0 +1,84 @@ +const { + Logger, + MySQL: { getTableInstance }, +} = require('lisk-service-framework'); + +const { getLisk32AddressFromPublicKey } = require('../../../utils/account'); + +const config = require('../../../../config'); + +const logger = Logger(); + +const MYSQL_ENDPOINT = config.endpoints.mysql; +const ownersTableSchema = require('../../../database/schema/owners'); + +const { + MODULE_NAME_AUDIO, + EVENT_NAME_COMMAND_EXECUTION_RESULT, +} = require('../../../../../blockchain-connector/shared/sdk/constants/names'); + +const getOwnersTable = () => getTableInstance( + ownersTableSchema.tableName, + ownersTableSchema, + MYSQL_ENDPOINT, +); + +// Command specific constants +const COMMAND_NAME = 'transfer'; + +// eslint-disable-next-line no-unused-vars +const applyTransaction = async (blockHeader, tx, events, dbTrx) => { + const { data: commandExecutedData = {} } = events.find( + ({ module, name }) => module === MODULE_NAME_AUDIO + && name === EVENT_NAME_COMMAND_EXECUTION_RESULT, + ); + if (!commandExecutedData.success) { + return false; + } + + const ownersTable = await getOwnersTable(); + + const senderAddress = getLisk32AddressFromPublicKey(tx.senderPublicKey); + const { + audioID, + address: recipientAddress, + shares: transferredShares, + } = tx.params; + + const [sender] = await ownersTable.find({ audioID, address: senderAddress }, ['address', 'audioID', 'shares', 'income'], dbTrx); + let [recipient] = await ownersTable.find({ audioID, address: recipientAddress }, ['address', 'audioID', 'shares', 'income'], dbTrx); + + // Transfer the shares + logger.trace(`Updating owner index for the account with address ${senderAddress}.`); + sender.shares -= transferredShares; + if (sender.shares === 0) { + await ownersTable.delete({ audioID: tx.params.audioID, address: senderAddress }, dbTrx); + } else { + await ownersTable.upsert(sender, dbTrx); + } + + logger.debug(`Updated owner index for the account with address ${senderAddress}.`); + + logger.trace(`Updating owner index for the account with address ${recipientAddress}.`); + if (recipient) { + recipient.shares += transferredShares; + } else { + recipient = { + audioID, + address: recipientAddress, + shares: transferredShares, + }; + } + await ownersTable.upsert(recipient, dbTrx); + logger.debug(`Updated owner index for the account with address ${recipientAddress}.`); + return true; +}; + +// eslint-disable-next-line no-unused-vars +const revertTransaction = async (blockHeader, tx, events, dbTrx) => {}; + +module.exports = { + COMMAND_NAME, + applyTransaction, + revertTransaction, +}; diff --git a/services/blockchain-indexer/shared/indexer/transactionProcessor/collection/create.js b/services/blockchain-indexer/shared/indexer/transactionProcessor/collection/create.js new file mode 100644 index 000000000..5f3accb95 --- /dev/null +++ b/services/blockchain-indexer/shared/indexer/transactionProcessor/collection/create.js @@ -0,0 +1,100 @@ +const { + Logger, + MySQL: { getTableInstance }, +} = require('lisk-service-framework'); + +const { getLisk32AddressFromPublicKey } = require('../../../utils/account'); + +const config = require('../../../../config'); + +const logger = Logger(); + +const MYSQL_ENDPOINT = config.endpoints.mysql; +const accountsTableSchema = require('../../../database/schema/accounts'); +const collectionsTableSchema = require('../../../database/schema/collections'); +const { + MODULE_NAME_COLLECTION, + EVENT_NAME_COLLECTION_CREATED, +} = require('../../../../../blockchain-connector/shared/sdk/constants/names'); + +const getAccountsTable = () => getTableInstance( + accountsTableSchema.tableName, + accountsTableSchema, + MYSQL_ENDPOINT, +); + +const getCollectionsTable = () => getTableInstance( + collectionsTableSchema.tableName, + collectionsTableSchema, + MYSQL_ENDPOINT, +); + +// Command specific constants +const COMMAND_NAME = 'create'; + +// eslint-disable-next-line no-unused-vars +const applyTransaction = async (blockHeader, tx, events, dbTrx) => { + const accountsTable = await getAccountsTable(); + const collectionsTable = await getCollectionsTable(); + + const senderAddress = getLisk32AddressFromPublicKey(tx.senderPublicKey); + + const account = { + address: senderAddress, + }; + + logger.trace(`Updating account index for the account with address ${account.address}.`); + await accountsTable.upsert(account, dbTrx); + logger.debug(`Updated account index for the account with address ${account.address}.`); + + logger.trace(`Indexing collections with address ${account.address}.`); + + const { data: eventData = {} } = events.find( + ({ module, name }) => module === MODULE_NAME_COLLECTION + && name === EVENT_NAME_COLLECTION_CREATED, + ); + + const collectionsNFT = { + ...eventData, + ...tx.params, + }; + + await collectionsTable.upsert(collectionsNFT, dbTrx); + logger.debug(`Indexed collection with ID ${eventData.collectionsID}.`); +}; + +// eslint-disable-next-line no-unused-vars +const revertTransaction = async (blockHeader, tx, events, dbTrx) => { + const accountsTable = await getAccountsTable(); + const collectionsTable = await getCollectionsTable(); + + const oldAccount = accountsTable.find( + { address: getLisk32AddressFromPublicKey(tx.senderPublicKey) }, + dbTrx, + ); + + // Remove the validator details from the table on transaction reversal + const account = { + address: getLisk32AddressFromPublicKey(tx.senderPublicKey), + publicKey: tx.senderPublicKey, + collections: { + owned: oldAccount.collections.owned.filter(id => id !== dbTrx.id), + shared: null, + }, + }; + + logger.trace(`Updating account index for the account with address ${account.address}.`); + await accountsTable.upsert(account, dbTrx); + logger.debug(`Updated account index for the account with address ${account.address}.`); + + logger.trace(`Remove collection entry for address ${account.address}.`); + const collectionPK = account[collectionsTableSchema.primaryKey]; + await collectionsTable.deleteByPrimaryKey(collectionPK, dbTrx); + logger.debug(`Removed collection entry for ID ${collectionPK}.`); +}; + +module.exports = { + COMMAND_NAME, + applyTransaction, + revertTransaction, +}; diff --git a/services/blockchain-indexer/shared/indexer/transactionProcessor/collection/destroy.js b/services/blockchain-indexer/shared/indexer/transactionProcessor/collection/destroy.js new file mode 100644 index 000000000..e398fb407 --- /dev/null +++ b/services/blockchain-indexer/shared/indexer/transactionProcessor/collection/destroy.js @@ -0,0 +1,39 @@ +const { + Logger, + MySQL: { getTableInstance }, +} = require('lisk-service-framework'); + +const config = require('../../../../config'); + +const logger = Logger(); + +const MYSQL_ENDPOINT = config.endpoints.mysql; +const collectionsTableSchema = require('../../../database/schema/collections'); + +const getCollectionsTable = () => getTableInstance( + collectionsTableSchema.tableName, + collectionsTableSchema, + MYSQL_ENDPOINT, +); + +// Command specific constants +const COMMAND_NAME = 'destroy'; + +// eslint-disable-next-line no-unused-vars +const applyTransaction = async (blockHeader, tx, events, dbTrx) => { + const collectionsTable = await getCollectionsTable(); + const { collectionID } = tx.params; + + logger.trace(`Deleting collection with ID ${collectionID}.`); + await collectionsTable.delete({ collectionID }, dbTrx); + logger.debug(`Deleted collection with ID ${collectionID}.`); +}; + +// eslint-disable-next-line no-unused-vars +const revertTransaction = async (blockHeader, tx, events, dbTrx) => {}; + +module.exports = { + COMMAND_NAME, + applyTransaction, + revertTransaction, +}; diff --git a/services/blockchain-indexer/shared/indexer/transactionProcessor/collection/index.js b/services/blockchain-indexer/shared/indexer/transactionProcessor/collection/index.js new file mode 100644 index 000000000..0862e2581 --- /dev/null +++ b/services/blockchain-indexer/shared/indexer/transactionProcessor/collection/index.js @@ -0,0 +1,6 @@ +// Module specific constants +const MODULE_NAME = 'collection'; + +module.exports = { + MODULE_NAME, +}; diff --git a/services/blockchain-indexer/shared/indexer/transactionProcessor/collection/setAttributes.js b/services/blockchain-indexer/shared/indexer/transactionProcessor/collection/setAttributes.js new file mode 100644 index 000000000..c55c74e71 --- /dev/null +++ b/services/blockchain-indexer/shared/indexer/transactionProcessor/collection/setAttributes.js @@ -0,0 +1,46 @@ +const { + Logger, + MySQL: { getTableInstance }, +} = require('lisk-service-framework'); + +const config = require('../../../../config'); + +const logger = Logger(); + +const MYSQL_ENDPOINT = config.endpoints.mysql; +const collectionsTableSchema = require('../../../database/schema/collections'); + +const getCollectionsTable = () => getTableInstance( + collectionsTableSchema.tableName, + collectionsTableSchema, + MYSQL_ENDPOINT, +); + +// Command specific constants +const COMMAND_NAME = 'setAttributes'; + +// eslint-disable-next-line no-unused-vars +const applyTransaction = async (blockHeader, tx, events, dbTrx) => { + const collectionsTable = await getCollectionsTable(); + const [collection] = await collectionsTable.find( + { collectionID: tx.params.collectionID }, + ['collectionID'], + ); + + if (typeof collection !== 'undefined') { + logger.trace(`Update collection with ID ${tx.params.collectionID}.`); + await collectionsTable.upsert(tx.params, dbTrx); + logger.debug(`Updated collection with ID ${tx.params.collectionID}.`); + } +}; + +// eslint-disable-next-line no-unused-vars +const revertTransaction = async (blockHeader, tx, events, dbTrx) => { + +}; + +module.exports = { + COMMAND_NAME, + applyTransaction, + revertTransaction, +}; diff --git a/services/blockchain-indexer/shared/indexer/transactionProcessor/collection/transfer.js b/services/blockchain-indexer/shared/indexer/transactionProcessor/collection/transfer.js new file mode 100644 index 000000000..70e5b533c --- /dev/null +++ b/services/blockchain-indexer/shared/indexer/transactionProcessor/collection/transfer.js @@ -0,0 +1,45 @@ +const { + Logger, + MySQL: { getTableInstance }, +} = require('lisk-service-framework'); + +const config = require('../../../../config'); + +const logger = Logger(); + +const MYSQL_ENDPOINT = config.endpoints.mysql; +const collectionsTableSchema = require('../../../database/schema/collections'); + +const getCollectionsTable = () => getTableInstance( + collectionsTableSchema.tableName, + collectionsTableSchema, + MYSQL_ENDPOINT, +); + +// Command specific constants +const COMMAND_NAME = 'transfer'; + +// eslint-disable-next-line no-unused-vars +const applyTransaction = async (blockHeader, tx, events, dbTrx) => { + const collectionsTable = await getCollectionsTable(); + const [collectionNFT] = await collectionsTable.find( + { collectionID: tx.params.collectionID }, + ['collectionID', 'name', 'collectionType', 'releaseYear'], + dbTrx, + ); + + collectionNFT.creatorAddress = tx.params.address; + logger.trace(`Indexing collections with new address ${collectionNFT.creatorAddress}.`); + + await collectionsTable.upsert(collectionNFT, dbTrx); + logger.debug(`Indexed collection with ID ${collectionNFT.collectionsID}.`); +}; + +// eslint-disable-next-line no-unused-vars +const revertTransaction = async (blockHeader, tx, events, dbTrx) => {}; + +module.exports = { + COMMAND_NAME, + applyTransaction, + revertTransaction, +}; diff --git a/services/blockchain-indexer/shared/indexer/transactionProcessor/profile/create.js b/services/blockchain-indexer/shared/indexer/transactionProcessor/profile/create.js new file mode 100644 index 000000000..cb4c3cb0c --- /dev/null +++ b/services/blockchain-indexer/shared/indexer/transactionProcessor/profile/create.js @@ -0,0 +1,97 @@ +const { + Logger, + MySQL: { getTableInstance }, +} = require('lisk-service-framework'); +const BluebirdPromise = require('bluebird'); + +const { getLisk32AddressFromPublicKey } = require('../../../utils/account'); + +const config = require('../../../../config'); + +const logger = Logger(); + +const MYSQL_ENDPOINT = config.endpoints.mysql; +const accountsTableSchema = require('../../../database/schema/accounts'); +const profilesTableSchema = require('../../../database/schema/profiles'); +const socialAccountsTableSchema = require('../../../database/schema/socialAccounts'); + +const { + MODULE_NAME_PROFILE, + EVENT_NAME_PROFILE_CREATED, +} = require('../../../../../blockchain-connector/shared/sdk/constants/names'); + +const getAccountsTable = () => getTableInstance( + accountsTableSchema.tableName, + accountsTableSchema, + MYSQL_ENDPOINT, +); + +const getProfilesTable = () => getTableInstance( + profilesTableSchema.tableName, + profilesTableSchema, + MYSQL_ENDPOINT, +); + +const getSocialAccountsTable = () => getTableInstance( + socialAccountsTableSchema.tableName, + socialAccountsTableSchema, + MYSQL_ENDPOINT, +); + +// Command specific constants +const COMMAND_NAME = 'create'; + +// eslint-disable-next-line no-unused-vars +const applyTransaction = async (blockHeader, tx, events, dbTrx) => { + const accountsTable = await getAccountsTable(); + const profilesTable = await getProfilesTable(); + const socialAccountsTable = await getSocialAccountsTable(); + const senderAddress = getLisk32AddressFromPublicKey(tx.senderPublicKey); + + const account = { + address: senderAddress, + }; + + logger.trace(`Updating account index for the account with address ${account.address}.`); + await accountsTable.upsert(account, dbTrx); + logger.debug(`Updated account index for the account with address ${account.address}.`); + + logger.trace(`Indexing profiles with address ${account.address}.`); + + const { data: eventData = {} } = events.find( + ({ module, name }) => module === MODULE_NAME_PROFILE + && name === EVENT_NAME_PROFILE_CREATED, + ); + + const profile = { + ...eventData, + ...tx.params, + }; + + await BluebirdPromise.map( + tx.params.socialAccounts, + async socialAccount => { + const socialInfo = { + profileID: eventData.profileID, + ...socialAccount, + }; + logger.trace(`Inserting social accounts for the profile with ID ${eventData.profileID}.`); + await socialAccountsTable.upsert(socialInfo, dbTrx); + logger.debug(`Inserted social accounts for the profile with ID ${eventData.profileID}.`); + return true; + }, + { concurrency: tx.params.socialAccounts.length }, + ); + + await profilesTable.upsert(profile, dbTrx); + logger.debug(`Indexed profile with ID ${eventData.profileID}.`); +}; + +// eslint-disable-next-line no-unused-vars +const revertTransaction = async (blockHeader, tx, events, dbTrx) => {}; + +module.exports = { + COMMAND_NAME, + applyTransaction, + revertTransaction, +}; diff --git a/services/blockchain-indexer/shared/indexer/transactionProcessor/profile/index.js b/services/blockchain-indexer/shared/indexer/transactionProcessor/profile/index.js new file mode 100644 index 000000000..290c852a1 --- /dev/null +++ b/services/blockchain-indexer/shared/indexer/transactionProcessor/profile/index.js @@ -0,0 +1,6 @@ +// Module specific constants +const MODULE_NAME = 'profile'; + +module.exports = { + MODULE_NAME, +}; diff --git a/services/blockchain-indexer/shared/indexer/transactionProcessor/profile/setAttribute.js b/services/blockchain-indexer/shared/indexer/transactionProcessor/profile/setAttribute.js new file mode 100644 index 000000000..dcfb8b44c --- /dev/null +++ b/services/blockchain-indexer/shared/indexer/transactionProcessor/profile/setAttribute.js @@ -0,0 +1,86 @@ +const { + Logger, + MySQL: { getTableInstance }, +} = require('lisk-service-framework'); +const BluebirdPromise = require('bluebird'); +const config = require('../../../../config'); +const { getLisk32AddressFromPublicKey } = require('../../../utils/account'); + +const logger = Logger(); + +const MYSQL_ENDPOINT = config.endpoints.mysql; +const profilesTableSchema = require('../../../database/schema/profiles'); +const socialAccountsTableSchema = require('../../../database/schema/socialAccounts'); + +const getProfilesTable = () => getTableInstance( + profilesTableSchema.tableName, + profilesTableSchema, + MYSQL_ENDPOINT, +); +const getSocialAccountsTable = () => getTableInstance( + socialAccountsTableSchema.tableName, + socialAccountsTableSchema, + MYSQL_ENDPOINT, +); + +// Command specific constants +const COMMAND_NAME = 'setAttributes'; + +// eslint-disable-next-line no-unused-vars +const applyTransaction = async (blockHeader, tx, events, dbTrx) => { + const profilesTable = await getProfilesTable(); + const socialAccountsTable = await getSocialAccountsTable(); + const senderAddress = getLisk32AddressFromPublicKey(tx.senderPublicKey); + + const account = { + address: senderAddress, + }; + + logger.trace(`Indexing profiles with address ${account.address}.`); + const [existingProfile] = await profilesTable.find( + { profileID: tx.params.profileID }, + ['profileID', 'name', 'nickName', 'description', 'avatarHash', 'avatarSignature', 'bannerHash', 'bannerSignature', 'creatorAddress'], + dbTrx, + ); + if (!existingProfile) { + throw new Error(`Profile with ID ${tx.params.profileID} does not exist.`); + } + + const newProfile = { + ...tx.params, + profileID: existingProfile.profileID, + creatorAddress: existingProfile.creatorAddress, + }; + + // Delete existing social account records + await socialAccountsTable.delete({ profileID: existingProfile.profileID }, dbTrx); + + // Insert new social account records + await BluebirdPromise.map( + tx.params.socialAccounts, + async (socialAccount) => { + const socialInfo = { + profileID: existingProfile.profileID, + ...socialAccount, + }; + logger.trace(`Updating social accounts for the profile with ID ${existingProfile.profileID}.`); + await socialAccountsTable.upsert(socialInfo, dbTrx); + logger.debug(`Updating social accounts for the profile with ID ${existingProfile.profileID}.`); + return true; + }, + { concurrency: tx.params.socialAccounts.length }, + ); + + // Update the profile record + await profilesTable.upsert(newProfile, dbTrx); + logger.debug(`Updated profile with ID ${existingProfile.profileID}.`); +}; + +// eslint-disable-next-line no-unused-vars +const revertTransaction = async (blockHeader, tx, events, dbTrx) => {}; + +module.exports = { + COMMAND_NAME, + applyTransaction, + revertTransaction, +}; diff --git a/services/blockchain-indexer/shared/indexer/transactionProcessor/subscription/create.js b/services/blockchain-indexer/shared/indexer/transactionProcessor/subscription/create.js new file mode 100644 index 000000000..c3deecc64 --- /dev/null +++ b/services/blockchain-indexer/shared/indexer/transactionProcessor/subscription/create.js @@ -0,0 +1,72 @@ +const { + Logger, + MySQL: { getTableInstance }, +} = require('lisk-service-framework'); + +const { getLisk32AddressFromPublicKey } = require('../../../utils/account'); + +const config = require('../../../../config'); + +const logger = Logger(); + +const MYSQL_ENDPOINT = config.endpoints.mysql; +const accountsTableSchema = require('../../../database/schema/accounts'); +const subscriptionsTableSchema = require('../../../database/schema/subscriptions'); + +const getAccountsTable = () => getTableInstance( + accountsTableSchema.tableName, + accountsTableSchema, + MYSQL_ENDPOINT, +); + +const getSubscriptionsTable = () => getTableInstance( + subscriptionsTableSchema.tableName, + subscriptionsTableSchema, + MYSQL_ENDPOINT, +); + +// Command specific constants +const COMMAND_NAME = 'create'; + +// eslint-disable-next-line no-unused-vars +const applyTransaction = async (blockHeader, tx, events, dbTrx) => { + const accountsTable = await getAccountsTable(); + const subscriptionsTable = await getSubscriptionsTable(); + + const senderAddress = getLisk32AddressFromPublicKey(tx.senderPublicKey); + const devAccount = { address: senderAddress }; + + logger.trace(`Updating account index for the account with address ${senderAddress}.`); + await accountsTable.upsert(devAccount, dbTrx); + logger.debug(`Updated account index for the account with address ${senderAddress}.`); + + logger.trace(`Indexing subscription with address ${senderAddress}.`); + + // @todo make sure the process won't break if the event doesn't exist. e.g. do not index. + const { data: eventData = {} } = events.find(e => e.module === 'subscription' && e.name === 'subscriptionCreated'); + + const subscriptionsNFT = { + ...eventData, + ...tx.params, + }; + + await subscriptionsTable.upsert(subscriptionsNFT, dbTrx); + logger.debug(`Indexed subscription with ID ${eventData.subscriptionID}.`); +}; + +// eslint-disable-next-line no-unused-vars +const revertTransaction = async (blockHeader, tx, events, dbTrx) => { + const subscriptionsTable = await getSubscriptionsTable(); + + const { data: eventData } = events.find(e => e.module === 'subscription' && e.name === 'subscriptionCreated'); + + logger.trace(`Remove subscription entry for ID ${eventData.subscriptionID}.`); + await subscriptionsTable.delete({ subscriptionID: eventData.subscriptionID }, dbTrx); + logger.debug(`Removed subscription entry for ID ${eventData.subscriptionID}.`); +}; + +module.exports = { + COMMAND_NAME, + applyTransaction, + revertTransaction, +}; diff --git a/services/blockchain-indexer/shared/indexer/transactionProcessor/subscription/index.js b/services/blockchain-indexer/shared/indexer/transactionProcessor/subscription/index.js new file mode 100644 index 000000000..7adb3a6ab --- /dev/null +++ b/services/blockchain-indexer/shared/indexer/transactionProcessor/subscription/index.js @@ -0,0 +1,6 @@ +// Module specific constants +const MODULE_NAME = 'subscription'; + +module.exports = { + MODULE_NAME, +}; diff --git a/services/blockchain-indexer/shared/indexer/transactionProcessor/subscription/purchase.js b/services/blockchain-indexer/shared/indexer/transactionProcessor/subscription/purchase.js new file mode 100644 index 000000000..9a2c16f0a --- /dev/null +++ b/services/blockchain-indexer/shared/indexer/transactionProcessor/subscription/purchase.js @@ -0,0 +1,111 @@ +const { + Logger, + MySQL: { getTableInstance }, +} = require('lisk-service-framework'); +const BluebirdPromise = require('bluebird'); + +const { getLisk32AddressFromPublicKey } = require('../../../utils/account'); +const { DEV_ADDRESS } = require('../../../constants'); + +const config = require('../../../../config'); + +const logger = Logger(); + +const MYSQL_ENDPOINT = config.endpoints.mysql; +const accountsTableSchema = require('../../../database/schema/accounts'); +const subscriptionsTableSchema = require('../../../database/schema/subscriptions'); +const membersTableSchema = require('../../../database/schema/members'); + +const getAccountsTable = () => getTableInstance( + accountsTableSchema.tableName, + accountsTableSchema, + MYSQL_ENDPOINT, +); + +const getSubscriptionsTable = () => getTableInstance( + subscriptionsTableSchema.tableName, + subscriptionsTableSchema, + MYSQL_ENDPOINT, +); + +const getMembersTable = () => getTableInstance( + membersTableSchema.tableName, + membersTableSchema, + MYSQL_ENDPOINT, +); + +// Command specific constants +const COMMAND_NAME = 'purchase'; + +// eslint-disable-next-line no-unused-vars +const applyTransaction = async (blockHeader, tx, events, dbTrx) => { + const accountsTable = await getAccountsTable(); + const subscriptionsTable = await getSubscriptionsTable(); + const membersTable = await getMembersTable(); + + const senderAddress = getLisk32AddressFromPublicKey(tx.senderPublicKey); + const { subscriptionID } = tx.params; + + logger.trace(`Indexing subscription with address ${subscriptionID}.`); + const subscriptionNFT = await subscriptionsTable.find( + { subscriptionID }, + ['subscriptionID', 'creatorAddress', 'price', 'consumable', 'streams', 'maxMembers'], + dbTrx, + ); + subscriptionNFT.creatorAddress = senderAddress; + await subscriptionsTable.upsert(subscriptionNFT, dbTrx); + logger.debug(`Indexed subscription with ID ${dbTrx.id}.`); + + // Update members in accounts and members table + await BluebirdPromise.map( + tx.params.members, + async member => { + const oldAccount = { address: member }; + logger.trace(`Updating account index for the account with address ${member}.`); + await accountsTable.upsert(oldAccount, dbTrx);// Create account for that member if not exists + logger.debug(`Updated account index for the account with address ${member}.`); + + const memberData = { + id: member.concat(`-${tx.nonce.toString()}`), + address: member, + addedBy: tx.id, + shared: subscriptionID, + }; + // subscription members + logger.trace(`Updating member index for the member with address ${member}.`); + await membersTable.upsert(memberData, dbTrx); + logger.debug(`Updated member index for the member with address ${member}.`); + return true; + }, + { concurrency: tx.params.members.length }, + ); + + // Update sender in accounts table + const senderAccount = { address: senderAddress }; + await accountsTable.upsert(senderAccount, dbTrx); + logger.trace(`Indexed subscription purchase with account address ${senderAddress}.`); +}; + +// eslint-disable-next-line no-unused-vars +const revertTransaction = async (blockHeader, tx, events, dbTrx) => { + const subscriptionsTable = await getSubscriptionsTable(); + const membersTable = await getMembersTable(); + + const { subscriptionID } = tx.params; + + const subscriptionNFT = await subscriptionsTable.find( + { subscriptionID }, + ['subscriptionID', 'creatorAddress', 'price', 'consumable', 'streams', 'maxMembers'], + dbTrx, + ); + subscriptionNFT.creatorAddress = DEV_ADDRESS; + await subscriptionsTable.upsert(subscriptionNFT, dbTrx); + + await membersTable.delete({ shared: subscriptionID }, dbTrx); +}; + +module.exports = { + COMMAND_NAME, + applyTransaction, + revertTransaction, +}; diff --git a/services/blockchain-indexer/shared/indexer/transactionProcessor/subscription/updateMembers.js b/services/blockchain-indexer/shared/indexer/transactionProcessor/subscription/updateMembers.js new file mode 100644 index 000000000..e2c035c2d --- /dev/null +++ b/services/blockchain-indexer/shared/indexer/transactionProcessor/subscription/updateMembers.js @@ -0,0 +1,105 @@ +const { + Logger, + MySQL: { getTableInstance }, +} = require('lisk-service-framework'); +const BluebirdPromise = require('bluebird'); + +const config = require('../../../../config'); + +const logger = Logger(); + +const MYSQL_ENDPOINT = config.endpoints.mysql; +const membersTableSchema = require('../../../database/schema/members'); + +const getMembersTable = () => getTableInstance( + membersTableSchema.tableName, + membersTableSchema, + MYSQL_ENDPOINT, +); + +// Command specific constants +const COMMAND_NAME = 'updateMembers'; + +// eslint-disable-next-line no-unused-vars +const applyTransaction = async (blockHeader, tx, events, dbTrx) => { + const membersTable = await getMembersTable(); + + const { subscriptionID } = tx.params; + + logger.trace(`Removing existing members with subscription ID ${subscriptionID}.`); + const currentMembers = await membersTable.find( + { shared: subscriptionID, removedBy: null }, + ['id', 'address', 'shared'], + dbTrx, + ); + + const currentAddresses = currentMembers.map(({ address }) => address); + const removed = currentMembers.filter(({ address }) => !tx.params.members.includes(address)); + const added = tx.params.members.filter(member => !currentAddresses.includes(member)); + + await BluebirdPromise.map( + removed, + async member => { + const memberData = { + ...member, + removedBy: tx.id, + }; + logger.trace(`Updating account index for the account with address ${member}.`); + await membersTable.upsert(memberData, dbTrx); + logger.debug(`Updated account index for the account with address ${member}.`); + }, + { concurrency: removed.length }, + ); + logger.trace(`Removed existing members with subscription ID ${subscriptionID}.`); + + logger.trace(`Adding new members with subscription ID ${subscriptionID}.`); + await BluebirdPromise.map( + added, + async member => { + const memberData = { + id: member.concat(`-${tx.nonce.toString()}`), + address: member, + addedBy: tx.id, + shared: subscriptionID, + }; + logger.trace(`Updating account index for the account with address ${member}.`); + await membersTable.upsert(memberData, dbTrx); + logger.debug(`Updated account index for the account with address ${member}.`); + }, + { concurrency: added.length }, + ); + logger.trace(`Added new members with subscription ID ${subscriptionID}.`); +}; + +// eslint-disable-next-line no-unused-vars +const revertTransaction = async (blockHeader, tx, events, dbTrx) => { + const membersTable = await getMembersTable(); + + const removed = await membersTable.find( + { removedBy: tx.id }, + ['id', 'address', 'shared'], + dbTrx, + ); + + await membersTable.delete({ addedBy: tx.id }, dbTrx); + + await BluebirdPromise.map( + removed, + async member => { + const memberData = { + ...member, + removedBy: null, + }; + logger.trace(`Updating account index for the account with address ${member}.`); + await membersTable.upsert(memberData, dbTrx); + logger.debug(`Updated account index for the account with address ${member}.`); + }, + { concurrency: removed.length }, + ); +}; + +module.exports = { + COMMAND_NAME, + applyTransaction, + revertTransaction, +}; diff --git a/services/gateway/apis/http-version3/methods/audios.js b/services/gateway/apis/http-version3/methods/audios.js new file mode 100644 index 000000000..6d2c52b48 --- /dev/null +++ b/services/gateway/apis/http-version3/methods/audios.js @@ -0,0 +1,42 @@ +const audiosSource = require('../../../sources/version3/audios'); +const envelope = require('../../../sources/version3/mappings/stdEnvelope'); +const regex = require('../../../shared/regex'); +const { transformParams, response, getSwaggerDescription } = require('../../../shared/utils'); + +module.exports = { + version: '2.0', + swaggerApiPath: '/audios', + rpcMethod: 'get.audios', + tags: ['Audios'], + params: { + creatorAddress: { optional: true, type: 'string', min: 3, max: 41, pattern: regex.ADDRESS_LISK32 }, + audioID: { optional: true, type: 'string', min: 1, max: 64, pattern: regex.HASH_SHA256 }, + collectionID: { optional: true, type: 'string', min: 1, max: 64, pattern: regex.HASH_SHA256 }, + ownerAddress: { optional: true, type: 'string', min: 3, max: 41, pattern: regex.ADDRESS_LISK32 }, + limit: { optional: true, type: 'number', min: 1, max: 100, default: 10 }, + offset: { optional: true, type: 'number', min: 0, default: 0 }, + }, + get schema() { + const audioSchema = {}; + audioSchema[this.swaggerApiPath] = { get: {} }; + audioSchema[this.swaggerApiPath].get.tags = this.tags; + audioSchema[this.swaggerApiPath].get.summary = 'Requests audios data'; + audioSchema[this.swaggerApiPath].get.description = getSwaggerDescription({ + rpcMethod: this.rpcMethod, + description: 'Returns audios data', + }); + audioSchema[this.swaggerApiPath].get.parameters = transformParams('audios', this.params); + audioSchema[this.swaggerApiPath].get.responses = { + 200: { + description: 'Returns a list of audios', + schema: { + $ref: '#/definitions/audiosWithEnvelope', + }, + }, + }; + Object.assign(audioSchema[this.swaggerApiPath].get.responses, response); + return audioSchema; + }, + source: audiosSource, + envelope, +}; diff --git a/services/gateway/apis/http-version3/methods/collections.js b/services/gateway/apis/http-version3/methods/collections.js new file mode 100644 index 000000000..8dc637022 --- /dev/null +++ b/services/gateway/apis/http-version3/methods/collections.js @@ -0,0 +1,38 @@ +const collectionsSource = require('../../../sources/version3/collections'); +const envelope = require('../../../sources/version3/mappings/stdEnvelope'); +const regex = require('../../../shared/regex'); +const { transformParams, response, getSwaggerDescription } = require('../../../shared/utils'); + +module.exports = { + version: '2.0', + swaggerApiPath: '/collections', + rpcMethod: 'get.collections', + tags: ['Collections'], + params: { + creatorAddress: { optional: true, type: 'string', min: 3, max: 41, pattern: regex.ADDRESS_LISK32 }, + collectionID: { optional: true, type: 'string', min: 1, max: 64, pattern: regex.HASH_SHA256 }, + }, + get schema() { + const collectionSchema = {}; + collectionSchema[this.swaggerApiPath] = { get: {} }; + collectionSchema[this.swaggerApiPath].get.tags = this.tags; + collectionSchema[this.swaggerApiPath].get.summary = 'Requests collections data'; + collectionSchema[this.swaggerApiPath].get.description = getSwaggerDescription({ + rpcMethod: this.rpcMethod, + description: 'Returns collections data', + }); + collectionSchema[this.swaggerApiPath].get.parameters = transformParams('collections', this.params); + collectionSchema[this.swaggerApiPath].get.responses = { + 200: { + description: 'Returns a list of collections', + schema: { + $ref: '#/definitions/collectionsWithEnvelope', + }, + }, + }; + Object.assign(collectionSchema[this.swaggerApiPath].get.responses, response); + return collectionSchema; + }, + source: collectionsSource, + envelope, +}; diff --git a/services/gateway/apis/http-version3/methods/profiles.js b/services/gateway/apis/http-version3/methods/profiles.js new file mode 100644 index 000000000..116882b5c --- /dev/null +++ b/services/gateway/apis/http-version3/methods/profiles.js @@ -0,0 +1,38 @@ +const profilesSource = require('../../../sources/version3/profiles'); +const envelope = require('../../../sources/version3/mappings/stdEnvelope'); +const regex = require('../../../shared/regex'); +const { transformParams, response, getSwaggerDescription } = require('../../../shared/utils'); + +module.exports = { + version: '2.0', + swaggerApiPath: '/profiles', + rpcMethod: 'get.profiles', + tags: ['Profiles'], + params: { + creatorAddress: { optional: true, type: 'string', min: 3, max: 41, pattern: regex.ADDRESS_LISK32 }, + profileID: { optional: true, type: 'string', min: 1, max: 64, pattern: regex.HASH_SHA256 }, + }, + get schema() { + const profileSchema = {}; + profileSchema[this.swaggerApiPath] = { get: {} }; + profileSchema[this.swaggerApiPath].get.tags = this.tags; + profileSchema[this.swaggerApiPath].get.summary = 'Requests profiles data'; + profileSchema[this.swaggerApiPath].get.description = getSwaggerDescription({ + rpcMethod: this.rpcMethod, + description: 'Returns profiles data', + }); + profileSchema[this.swaggerApiPath].get.parameters = transformParams('profiles', this.params); + profileSchema[this.swaggerApiPath].get.responses = { + 200: { + description: 'Returns a list of profiles', + schema: { + $ref: '#/definitions/profilesWithEnvelope', + }, + }, + }; + Object.assign(profileSchema[this.swaggerApiPath].get.responses, response); + return profileSchema; + }, + source: profilesSource, + envelope, +}; diff --git a/services/gateway/apis/http-version3/methods/subscriptions.js b/services/gateway/apis/http-version3/methods/subscriptions.js new file mode 100644 index 000000000..e8e2ae19f --- /dev/null +++ b/services/gateway/apis/http-version3/methods/subscriptions.js @@ -0,0 +1,55 @@ +/* + * LiskHQ/lisk-service + * Copyright © 2022 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + * + */ + +const subscriptionsSource = require('../../../sources/version3/subscriptions'); +const envelope = require('../../../sources/version3/mappings/stdEnvelope'); +const regex = require('../../../shared/regex'); +const { transformParams, response, getSwaggerDescription } = require('../../../shared/utils'); + +module.exports = { + version: '2.0', + swaggerApiPath: '/subscriptions', + rpcMethod: 'get.subscriptions', + tags: ['Subscriptions'], + params: { + creatorAddress: { optional: true, type: 'string', min: 3, max: 41, pattern: regex.ADDRESS_LISK32 }, + subscriptionID: { optional: true, type: 'string', min: 1, max: 64, pattern: regex.HASH_SHA256 }, + memberAddress: { optional: true, type: 'string' }, + }, + get schema() { + const subscriptionSchema = {}; + subscriptionSchema[this.swaggerApiPath] = { get: {} }; + subscriptionSchema[this.swaggerApiPath].get.tags = this.tags; + subscriptionSchema[this.swaggerApiPath].get.summary = 'Requests subscriptions data'; + subscriptionSchema[this.swaggerApiPath].get.description = getSwaggerDescription({ + rpcMethod: this.rpcMethod, + description: 'Returns subscriptions data', + }); + subscriptionSchema[this.swaggerApiPath].get.parameters = transformParams('subscriptions', this.params); + subscriptionSchema[this.swaggerApiPath].get.responses = { + 200: { + description: 'Returns a list of subscriptions', + schema: { + $ref: '#/definitions/subscriptionsWithEnvelope', + }, + }, + }; + Object.assign(subscriptionSchema[this.swaggerApiPath].get.responses, response); + return subscriptionSchema; + }, + source: subscriptionsSource, + envelope, +}; diff --git a/services/gateway/apis/http-version3/swagger/apiJson.json b/services/gateway/apis/http-version3/swagger/apiJson.json index b85162776..6434e49f9 100644 --- a/services/gateway/apis/http-version3/swagger/apiJson.json +++ b/services/gateway/apis/http-version3/swagger/apiJson.json @@ -86,6 +86,22 @@ { "name": "Market", "description": "Market prices related API calls." + }, + { + "name": "Subscription", + "description": "Subscription module related API calls." + }, + { + "name": "Collection", + "description": "Collection module related API calls." + }, + { + "name": "Audio", + "description": "Audio module related API calls." + }, + { + "name": "Profile", + "description": "Profile module related API calls." } ], "schemes": [ diff --git a/services/gateway/apis/http-version3/swagger/definitions/audios.json b/services/gateway/apis/http-version3/swagger/definitions/audios.json new file mode 100644 index 000000000..5b5e94998 --- /dev/null +++ b/services/gateway/apis/http-version3/swagger/definitions/audios.json @@ -0,0 +1,118 @@ +{ + "audio": { + "type": "object", + "required": [ + "audioID", + "name", + "releaseYear", + "collectionID", + "creatorAddress", + "feat" + ], + "properties": { + "audioID": { + "type": "string", + "format": "id", + "example": "f9593f101c4acafc3ede650ab4c10fa2ecb59b225813eddbbb17b47e96932e9b", + "minLength": 1, + "maxLength": 64, + "description": "Unique identifier of the audio.\nDerived from the audio hash." + }, + "name": { + "type": "string", + "description": "name of the audio" + }, + "releaseYear": { + "type": "string", + "example": "2023" + }, + "collectionID": { + "type": "string", + "format": "id", + "example": "f9593f101c4acafc3ede650ab4c10fa2ecb59b225813eddbbb17b47e96932e9b", + "minLength": 1, + "maxLength": 64, + "description": "Unique identifier of the collection to which the current audio belongs." + }, + "creatorAddress": { + "type": "object", + "properties": { + "address": { + "type": "string", + "format": "address", + "example": "lskdwsyfmcko6mcd357446yatromr9vzgu7eb8y99", + "description": "The Lisk Address is the human-readable representation of a blockchain account.\nIt is 41 character long identifier that begins with `lsk`." + }, + "publicKey": { + "type": "string", + "format": "publicKey", + "example": "b1d6bc6c7edd0673f5fed0681b73de6eb70539c21278b300f07ade277e1962cd", + "description": "The public key is derived from the private key of the owner of the account.\nIt can be used to validate that the private key belongs to the owner, but not provide access to the owner's private key." + }, + "name": { + "type": "string", + "example": "genesis_84", + "description": "Delegate name" + } + } + }, + "feat": { + "type": "object", + "required": [ + "address", + "name", + "role" + ], + "properties": { + "address": { + "type": "string", + "example": "lskdwsyfmcko6mcd357446yatromr9vzgu7eb8y99", + "description": "Address of the block generator." + }, + "name": { + "type": "string", + "example": "genesis_3", + "description": "Name of the block generator." + }, + "role": { + "type": "string", + "example": "guitarist", + "description": "Role of the artist in creation of the audio." + } + } + } + } + }, + "AudiosWithEnvelope": { + "type": "object", + "required": [ + "data", + "meta" + ], + "properties": { + "data": { + "description": "List of audios", + "type": "array", + "items": { + "$ref": "#/definitions/Audios" + } + }, + "meta": { + "$ref": "#/definitions/pagination" + } + } + }, + "serverErrorEnvelope": { + "type": "object", + "properties": { + "error": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Unable to reach a network node" + } + } + } +} diff --git a/services/gateway/apis/http-version3/swagger/definitions/collections.json b/services/gateway/apis/http-version3/swagger/definitions/collections.json new file mode 100644 index 000000000..b0cf3e85d --- /dev/null +++ b/services/gateway/apis/http-version3/swagger/definitions/collections.json @@ -0,0 +1,88 @@ +{ + "collection": { + "type": "object", + "required": [ + "collectionID", + "releaseYear", + "name", + "collectionType", + "creatorAddress" + ], + "properties": { + "collectionID": { + "type": "string", + "format": "id", + "example": "f9593f101c4acafc3ede650ab4c10fa2ecb59b225813eddbbb17b47e96932e9b", + "minLength": 1, + "maxLength": 64, + "description": "Unique identifier of the collection.\nDerived from the collection hash." + }, + "releaseYear": { + "type": "string", + "example": "2023" + }, + "name": { + "type": "string", + "description": "name of the collection" + }, + "collectionType": { + "type": "integer", + "example": "1" + }, + "creatorAddress": { + "type": "object", + "properties": { + "address": { + "type": "string", + "format": "address", + "example": "lskdwsyfmcko6mcd357446yatromr9vzgu7eb8y99", + "description": "The Lisk Address is the human-readable representation of a blockchain account.\nIt is 41 character long identifier that begins with `lsk`." + }, + "publicKey": { + "type": "string", + "format": "publicKey", + "example": "b1d6bc6c7edd0673f5fed0681b73de6eb70539c21278b300f07ade277e1962cd", + "description": "The public key is derived from the private key of the owner of the account.\nIt can be used to validate that the private key belongs to the owner, but not provide access to the owner's private key." + }, + "name": { + "type": "string", + "example": "genesis_84", + "description": "Delegate name" + } + } + } + } + }, + "CollectionsWithEnvelope": { + "type": "object", + "required": [ + "data", + "meta" + ], + "properties": { + "data": { + "description": "List of collections", + "type": "array", + "items": { + "$ref": "#/definitions/Collections" + } + }, + "meta": { + "$ref": "#/definitions/pagination" + } + } + }, + "serverErrorEnvelope": { + "type": "object", + "properties": { + "error": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Unable to reach a network node" + } + } + } +} diff --git a/services/gateway/apis/http-version3/swagger/definitions/profiles.json b/services/gateway/apis/http-version3/swagger/definitions/profiles.json new file mode 100644 index 000000000..3bba369c2 --- /dev/null +++ b/services/gateway/apis/http-version3/swagger/definitions/profiles.json @@ -0,0 +1,107 @@ +{ + "profile": { + "type": "object", + "required": [ + "profileID", + "name", + "nickName", + "description", + "socialAccounts", + "avatarHash", + "avatarSignature", + "bannerHash", + "bannerSignature", + "creatorAddress" + ], + "properties": { + "profileID": { + "type": "string", + "format": "id", + "example": "f9593f101c4acafc3ede650ab4c10fa2ecb59b225813eddbbb17b47e96932e9b", + "minLength": 1, + "maxLength": 64, + "description": "Unique identifier of the profile.\nDerived from the profile hash." + }, + "name": { + "type": "string", + "description": "name of the profile" + }, + "nickName": { + "type": "string", + "description": "nickName of the profile" + }, + "description": { + "type": "string", + "description": "description of the profile" + }, + "socialAccounts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "username": { + "type": "string", + "description": "username of the social media platform" + }, + "platform": { + "type": "string", + "description": "name of the social media platform" + } + } + } + }, + "avatarHash": { + "type": "string", + "example": "hash of the profile avatar" + }, + "avatarSignature": { + "type": "string", + "example": "signature of the profile avatar" + }, + "bannerHash": { + "type": "string", + "example": "hash of the profile banner" + }, + "bannerSignature": { + "type": "string", + "example": "signature of the profile banner" + }, + "creatorAddress": { + "type": "string", + "example": "creator address of the profile" + } + } + }, + "ProfilesWithEnvelope": { + "type": "object", + "required": [ + "data", + "meta" + ], + "properties": { + "data": { + "description": "List of profiles", + "type": "array", + "items": { + "$ref": "#/definitions/Profiles" + } + }, + "meta": { + "$ref": "#/definitions/pagination" + } + } + }, + "serverErrorEnvelope": { + "type": "object", + "properties": { + "error": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Unable to reach a network node" + } + } + } +} diff --git a/services/gateway/apis/http-version3/swagger/definitions/subscriptions.json b/services/gateway/apis/http-version3/swagger/definitions/subscriptions.json new file mode 100644 index 000000000..abc2410ec --- /dev/null +++ b/services/gateway/apis/http-version3/swagger/definitions/subscriptions.json @@ -0,0 +1,99 @@ +{ + "Subscription": { + "type": "object", + "required": [ + "creatorAddress", + "subscriptionID", + "members", + "price", + "consumable", + "maxMembers", + "streams" + ], + "properties": { + "subscriptionID": { + "type": "string", + "format": "id", + "example": "f9593f101c4acafc3ede650ab4c10fa2ecb59b225813eddbbb17b47e96932e9b", + "minLength": 1, + "maxLength": 64, + "description": "Unique identifier of the subscription.\nDerived from the subscription hash." + }, + "price": { + "type": "string", + "example": "0" + }, + "consumable": { + "type": "string", + "description": "Consumable value." + }, + "MaxMembers": { + "type": "integer", + "example": "1" + }, + "creatorAddress": { + "type": "object", + "properties": { + "address": { + "type": "string", + "format": "address", + "example": "lskdwsyfmcko6mcd357446yatromr9vzgu7eb8y99", + "description": "The Lisk Address is the human-readable representation of a blockchain account.\nIt is 41 character long identifier that begins with `lsk`." + }, + "publicKey": { + "type": "string", + "format": "publicKey", + "example": "b1d6bc6c7edd0673f5fed0681b73de6eb70539c21278b300f07ade277e1962cd", + "description": "The public key is derived from the private key of the owner of the account.\nIt can be used to validate that the private key belongs to the owner, but not provide access to the owner's private key." + }, + "name": { + "type": "string", + "example": "genesis_84", + "description": "Delegate name" + } + } + }, + "members": { + "type": "array", + "items": { + "address": { + "type": "string", + "description": "Member address." + } + } + } + } + }, + "SubscriptionsWithEnvelope": { + "type": "object", + "required": [ + "data", + "meta" + ], + "properties": { + "data": { + "description": "List of subscriptions", + "type": "array", + "items": { + "$ref": "#/definitions/Subscriptions" + } + }, + "meta": { + "$ref": "#/definitions/pagination" + } + } + }, + "serverErrorEnvelope": { + "type": "object", + "properties": { + "error": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Unable to reach a network node" + } + } + } +} diff --git a/services/gateway/apis/http-version3/swagger/parameters/audios.json b/services/gateway/apis/http-version3/swagger/parameters/audios.json new file mode 100644 index 000000000..3fe410c6f --- /dev/null +++ b/services/gateway/apis/http-version3/swagger/parameters/audios.json @@ -0,0 +1,19 @@ +{ + "creatorAddress": { + "name": "creatorAddress", + "in": "query", + "description": "Lisk account address", + "type": "string", + "minLength": 3, + "maxLength": 41 + }, + "audioID": { + "name": "audioID", + "in": "query", + "description": "audio ID to query", + "type": "string", + "format": "id", + "minLength": 1, + "maxLength": 64 + } +} diff --git a/services/gateway/apis/http-version3/swagger/parameters/collections.json b/services/gateway/apis/http-version3/swagger/parameters/collections.json new file mode 100644 index 000000000..3c80f4c56 --- /dev/null +++ b/services/gateway/apis/http-version3/swagger/parameters/collections.json @@ -0,0 +1,19 @@ +{ + "creatorAddress": { + "name": "creatorAddress", + "in": "query", + "description": "Lisk account address", + "type": "string", + "minLength": 3, + "maxLength": 41 + }, + "collectionID": { + "name": "collectionID", + "in": "query", + "description": "collection ID to query", + "type": "string", + "format": "id", + "minLength": 1, + "maxLength": 64 + } +} diff --git a/services/gateway/apis/http-version3/swagger/parameters/profiles.json b/services/gateway/apis/http-version3/swagger/parameters/profiles.json new file mode 100644 index 000000000..488a2795c --- /dev/null +++ b/services/gateway/apis/http-version3/swagger/parameters/profiles.json @@ -0,0 +1,19 @@ +{ + "creatorAddress": { + "name": "creatorAddress", + "in": "query", + "description": "Lisk account address", + "type": "string", + "minLength": 3, + "maxLength": 41 + }, + "profileID": { + "name": "profileID", + "in": "query", + "description": "profile ID to query", + "type": "string", + "format": "id", + "minLength": 1, + "maxLength": 64 + } +} diff --git a/services/gateway/apis/http-version3/swagger/parameters/subscriptions.json b/services/gateway/apis/http-version3/swagger/parameters/subscriptions.json new file mode 100644 index 000000000..90b8a19f1 --- /dev/null +++ b/services/gateway/apis/http-version3/swagger/parameters/subscriptions.json @@ -0,0 +1,19 @@ +{ + "creatorAddress": { + "name": "creatorAddress", + "in": "query", + "description": "Lisk account address", + "type": "string", + "minLength": 3, + "maxLength": 41 + }, + "subscriptionID": { + "name": "subscriptionID", + "in": "query", + "description": "subscription ID to query", + "type": "string", + "format": "id", + "minLength": 1, + "maxLength": 64 + } +} diff --git a/services/gateway/sources/version3/audios.js b/services/gateway/sources/version3/audios.js new file mode 100644 index 000000000..b0757a718 --- /dev/null +++ b/services/gateway/sources/version3/audios.js @@ -0,0 +1,41 @@ +const audio = require('./mappings/audio'); + +module.exports = { + type: 'moleculer', + method: 'indexer.audios', + params: { + name: '=,string', + audioID: '=,string', + creatorAddress: '=,string', + releaseYear: '=,string', + collectionID: '=,string', + collection: { + collectionType: '=,string', + releaseYear: '=,number', + name: '=,number', + }, + owners: ['owners', { + address: '=,string', + shares: '=,number', + income: '=,number', + }], + feat: ['feat', { + address: '=,string', + name: '=,string', + role: '=,string', + }], + limit: '=,number', + offset: '=,number', + sort: '=,string', + order: '=,string', + }, + definition: { + data: ['data', audio], + meta: { + count: '=,number', + offset: '=,number', + total: '=,number', + }, + links: {}, + }, +}; diff --git a/services/gateway/sources/version3/collections.js b/services/gateway/sources/version3/collections.js new file mode 100644 index 000000000..4c1776a22 --- /dev/null +++ b/services/gateway/sources/version3/collections.js @@ -0,0 +1,30 @@ +const collection = require('./mappings/collection'); + +module.exports = { + type: 'moleculer', + method: 'indexer.collections', + params: { + name: '=,string', + collectionID: '=,string', + creatorAddress: '=,string', + releaseYear: '=,string', + collectionType: '=,number', + // audios: ['audios', { + // audioID: '=,string', + // name: '=,number', + // }], + limit: '=,number', + offset: '=,number', + sort: '=,string', + order: '=,string', + }, + definition: { + data: ['data', collection], + meta: { + count: '=,number', + offset: '=,number', + total: '=,number', + }, + links: {}, + }, +}; diff --git a/services/gateway/sources/version3/mappings/audio.js b/services/gateway/sources/version3/mappings/audio.js new file mode 100644 index 000000000..a70f0c474 --- /dev/null +++ b/services/gateway/sources/version3/mappings/audio.js @@ -0,0 +1,21 @@ +module.exports = { + name: '=,string', + releaseYear: '=,string', + audioID: '=,string', + collectionID: '=,string', + creatorAddress: '=,string', + collection: { + collectionType: '=,string', + releaseYear: '=,number', + name: '=,string', + }, + owners: ['owners', { + address: '=,string', + shares: '=,number', + income: '=,number', + }], + feat: ['feat', { + address: '=,string', + role: '=,string', + }], +}; diff --git a/services/gateway/sources/version3/mappings/collection.js b/services/gateway/sources/version3/mappings/collection.js new file mode 100644 index 000000000..f444757b8 --- /dev/null +++ b/services/gateway/sources/version3/mappings/collection.js @@ -0,0 +1,22 @@ +/* + * LiskHQ/lisk-service + * Copyright © 2022 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + * + */ +module.exports = { + name: '=,string', + collectionID: '=,string', + creatorAddress: '=,string', + collectionType: '=,number', + releaseYear: '=,string', +}; diff --git a/services/gateway/sources/version3/mappings/profile.js b/services/gateway/sources/version3/mappings/profile.js new file mode 100644 index 000000000..f17e9927a --- /dev/null +++ b/services/gateway/sources/version3/mappings/profile.js @@ -0,0 +1,16 @@ +module.exports = { + name: '=,string', + nickName: '=,string', + profileID: '=,string', + description: '=,string', + socialAccounts: ['socialAccounts', { + username: '=,string', + platform: '=,number', + }], + avatarHash: '=,string', + avatarSignature: '=,string', + bannerHash: '=,string', + bannerSignature: '=,string', + creatorAddress: '=,string', +}; + diff --git a/services/gateway/sources/version3/mappings/subscription.js b/services/gateway/sources/version3/mappings/subscription.js new file mode 100644 index 000000000..bf82ee71b --- /dev/null +++ b/services/gateway/sources/version3/mappings/subscription.js @@ -0,0 +1,28 @@ +/* + * LiskHQ/lisk-service + * Copyright © 2022 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + * + */ +module.exports = { + subscriptionID: '=,string', + creatorAddress: '=,string', + maxMembers: '=,number', + streams: '=,string', + price: '=,string', + consumable: '=,string', + members: ['members', { + address: '=,string', + name: '=,string', + publicKey: '=,string', + }], +}; diff --git a/services/gateway/sources/version3/profiles.js b/services/gateway/sources/version3/profiles.js new file mode 100644 index 000000000..cb201fac2 --- /dev/null +++ b/services/gateway/sources/version3/profiles.js @@ -0,0 +1,30 @@ +const profile = require('./mappings/profile'); + +module.exports = { + type: 'moleculer', + method: 'indexer.profiles', + params: { + profileID: '=,string', + name: '=,string', + nickName: '=,string', + description: '=,string', + socialAccounts: ['socialAccounts', { + username: '=,string', + platform: '=,number', + }], + avatarHash: '=,string', + avatarSignature: '=,string', + bannerHash: '=,string', + bannerSignature: '=,string', + creatorAddress: '=,string', + }, + definition: { + data: ['data', profile], + meta: { + count: '=,number', + offset: '=,number', + total: '=,number', + }, + links: {}, + }, +}; diff --git a/services/gateway/sources/version3/subscriptions.js b/services/gateway/sources/version3/subscriptions.js new file mode 100644 index 000000000..45277ffaf --- /dev/null +++ b/services/gateway/sources/version3/subscriptions.js @@ -0,0 +1,47 @@ +/* + * LiskHQ/lisk-service + * Copyright © 2022 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + * + */ +const subscription = require('./mappings/subscription'); + +module.exports = { + type: 'moleculer', + method: 'indexer.subscriptions', + params: { + subscriptionID: '=,string', + creatorAddress: '=,string', + price: '=,string', + consumed: '=,string', + members: ['members', { + address: '=,string', + name: '=,string', + publicKey: '=,string', + }], + streams: '=,string', + maxMembers: '=,number', + limit: '=,number', + offset: '=,number', + sort: '=,string', + order: '=,string', + }, + definition: { + data: ['data', subscription], + meta: { + count: '=,number', + offset: '=,number', + total: '=,number', + }, + links: {}, + }, +}; diff --git a/services/gateway/tests/constants/generateDocs.js b/services/gateway/tests/constants/generateDocs.js index 39344b3c9..f57a35f02 100644 --- a/services/gateway/tests/constants/generateDocs.js +++ b/services/gateway/tests/constants/generateDocs.js @@ -14,6 +14,49 @@ * */ const createApiDocsExpectedResponse = { + '/audios': { + get: { + description: 'Returns audios data\n RPC => get.audios', + parameters: [ + { + $ref: '#/parameters/creatorAddress', + }, + { + $ref: '#/parameters/audioID', + }, + { + $ref: '#/parameters/collectionID', + }, + { + $ref: '#/parameters/ownerAddress', + }, + { + $ref: '#/parameters/limit', + }, + { + $ref: '#/parameters/offset', + }, + ], + responses: { + 200: { + description: 'Returns a list of audios', + schema: { + $ref: '#/definitions/audiosWithEnvelope', + }, + }, + 400: { + description: 'Bad request', + schema: { + $ref: '#/definitions/badRequest', + }, + }, + }, + summary: 'Requests audios data', + tags: [ + 'Audios', + ], + }, + }, '/blocks/assets': { get: { tags: [ @@ -407,6 +450,37 @@ const createApiDocsExpectedResponse = { }, }, }, + '/collections': { + get: { + description: 'Returns collections data\n RPC => get.collections', + parameters: [ + { + $ref: '#/parameters/creatorAddress', + }, + { + $ref: '#/parameters/collectionID', + }, + ], + responses: { + 200: { + description: 'Returns a list of collections', + schema: { + $ref: '#/definitions/collectionsWithEnvelope', + }, + }, + 400: { + description: 'Bad request', + schema: { + $ref: '#/definitions/badRequest', + }, + }, + }, + summary: 'Requests collections data', + tags: [ + 'Collections', + ], + }, + }, '/events': { get: { tags: [ @@ -692,6 +766,40 @@ const createApiDocsExpectedResponse = { }, }, }, + '/subscriptions': { + get: { + description: 'Returns subscriptions data\n RPC => get.subscriptions', + parameters: [ + { + $ref: '#/parameters/creatorAddress', + }, + { + $ref: '#/parameters/subscriptionID', + }, + { + $ref: '#/parameters/memberAddress', + }, + ], + responses: { + 200: { + description: 'Returns a list of subscriptions', + schema: { + $ref: '#/definitions/subscriptionsWithEnvelope', + }, + }, + 400: { + description: 'Bad request', + schema: { + $ref: '#/definitions/badRequest', + }, + }, + }, + summary: 'Requests subscriptions data', + tags: [ + 'Subscriptions', + ], + }, + }, '/transactions': { get: { tags: [ @@ -811,6 +919,37 @@ const createApiDocsExpectedResponse = { }, }, }, + '/profiles': { + get: { + description: 'Returns profiles data\n RPC => get.profiles', + parameters: [ + { + $ref: '#/parameters/creatorAddress', + }, + { + $ref: '#/parameters/profileID', + }, + ], + responses: { + 200: { + description: 'Returns a list of profiles', + schema: { + $ref: '#/definitions/profilesWithEnvelope', + }, + }, + 400: { + description: 'Bad request', + schema: { + $ref: '#/definitions/badRequest', + }, + }, + }, + summary: 'Requests profiles data', + tags: [ + 'Profiles', + ], + }, + }, '/schemas': { get: { tags: [ diff --git a/services/gateway/tests/constants/registerApi.js b/services/gateway/tests/constants/registerApi.js index 0eb8ea0d5..c11d29b83 100644 --- a/services/gateway/tests/constants/registerApi.js +++ b/services/gateway/tests/constants/registerApi.js @@ -16,6 +16,7 @@ // TODO: Expected response for registerApi method should be dynamically constructed const expectedResponseForRegisterHttpApi = { whitelist: [ + 'indexer.audios', 'indexer.blocks.assets', 'indexer.blockchain.apps', 'app-registry.blockchain.apps.meta.list', @@ -24,6 +25,7 @@ const expectedResponseForRegisterHttpApi = { 'app-registry.blockchain.apps.meta.tokens', 'app-registry.blockchain.apps.meta.tokens.supported', 'indexer.blocks', + 'indexer.collections', 'indexer.events', 'fees.estimates', 'indexer.generators', @@ -34,8 +36,10 @@ const expectedResponseForRegisterHttpApi = { 'indexer.network.statistics', 'indexer.network.status', 'indexer.transactions.post', + 'indexer.profiles', 'indexer.schemas', 'gateway.spec', + 'indexer.subscriptions', 'indexer.transactions', 'indexer.transactions.dryrun', 'statistics.transactions.statistics', @@ -58,6 +62,9 @@ const expectedResponseForRegisterHttpApi = { ], aliases: { 'GET blocks/assets': 'indexer.blocks.assets', + 'GET collections': 'indexer.collections', + 'GET profiles': 'indexer.profiles', + 'GET audios': 'indexer.audios', 'GET blockchain/apps': 'indexer.blockchain.apps', 'GET blockchain/apps/meta/list': 'app-registry.blockchain.apps.meta.list', 'GET blockchain/apps/meta': 'app-registry.blockchain.apps.meta', @@ -76,6 +83,7 @@ const expectedResponseForRegisterHttpApi = { 'GET network/status': 'indexer.network.status', 'POST transactions': 'indexer.transactions.post', 'GET schemas': 'indexer.schemas', + 'GET subscriptions': 'indexer.subscriptions', 'GET spec': 'gateway.spec', 'GET transactions': 'indexer.transactions', 'POST transactions/dryrun': 'indexer.transactions.dryrun', @@ -104,6 +112,7 @@ const expectedResponseForRegisterRpcApi = { events: { request: { whitelist: [ + 'indexer.audios', 'indexer.blocks.assets', 'indexer.blockchain.apps', 'app-registry.blockchain.apps.meta.list', @@ -112,6 +121,7 @@ const expectedResponseForRegisterRpcApi = { 'app-registry.blockchain.apps.meta.tokens', 'app-registry.blockchain.apps.meta.tokens.supported', 'indexer.blocks', + 'indexer.collections', 'indexer.events', 'fees.estimates', 'indexer.generators', @@ -122,7 +132,9 @@ const expectedResponseForRegisterRpcApi = { 'indexer.network.statistics', 'indexer.network.status', 'indexer.transactions.post', + 'indexer.profiles', 'indexer.schemas', + 'indexer.subscriptions', 'indexer.transactions', 'indexer.transactions.dryrun', 'statistics.transactions.statistics', @@ -144,6 +156,8 @@ const expectedResponseForRegisterRpcApi = { ], aliases: { 'get.blocks.assets': 'indexer.blocks.assets', + 'get.collections': 'indexer.collections', + 'get.audios': 'indexer.audios', 'get.blockchain.apps': 'indexer.blockchain.apps', 'get.blockchain.apps.meta.list': 'app-registry.blockchain.apps.meta.list', 'get.blockchain.apps.meta': 'app-registry.blockchain.apps.meta', @@ -160,8 +174,10 @@ const expectedResponseForRegisterRpcApi = { 'get.network.peers': 'indexer.network.peers', 'get.network.statistics': 'indexer.network.statistics', 'get.network.status': 'indexer.network.status', + 'get.profiles': 'indexer.profiles', 'post.transactions': 'indexer.transactions.post', 'get.schemas': 'indexer.schemas', + 'get.subscriptions': 'indexer.subscriptions', 'get.transactions': 'indexer.transactions', 'post.transactions.dryrun': 'indexer.transactions.dryrun', 'get.transactions.statistics': 'statistics.transactions.statistics',