diff --git a/package.json b/package.json index c5a51907..78dbc2f5 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,10 @@ "@alien-worlds/aw-antelope": "^0.0.45", "@alien-worlds/aw-contract-alien-worlds": "^0.0.28", "@alien-worlds/aw-contract-dao-worlds": "^0.0.39", - "@alien-worlds/aw-contract-index-worlds": "^0.0.33", "@alien-worlds/aw-contract-stkvt-worlds": "^0.0.27", "@alien-worlds/aw-contract-token-worlds": "^0.0.36", + "@alien-worlds/aw-contract-index-worlds": "^0.0.33", + "@alien-worlds/aw-contract-msig-worlds": "^0.0.6", "@fastify/swagger": "6.1.1", "@shelf/winston-datadog-logs-transport": "^1.0.7", "ajv": "^8.12.0", diff --git a/src/endpoints/api.ioc.config.ts b/src/endpoints/api.ioc.config.ts index c14cbec2..b14aeff8 100644 --- a/src/endpoints/api.ioc.config.ts +++ b/src/endpoints/api.ioc.config.ts @@ -8,6 +8,7 @@ import { setupAlienWorldsContractService } from '@alien-worlds/aw-contract-alien import { setupIndexWorldsContractService } from '@alien-worlds/aw-contract-index-worlds'; import { setupStkvtWorldsDeltaRepository } from '@alien-worlds/aw-contract-stkvt-worlds'; import { setupTokenWorldsContractService } from '@alien-worlds/aw-contract-token-worlds'; +import { setupMsigWorldsContractService } from '@alien-worlds/aw-contract-msig-worlds'; import { AntelopeRpcSourceImpl } from '@alien-worlds/aw-antelope'; import ApiConfig from '@src/config/api-config'; import { MongoSource } from '@alien-worlds/aw-storage-mongodb'; @@ -19,6 +20,7 @@ import { CustodiansDependencyInjector } from './custodians/custodians.ioc'; import { HealthDependencyInjector } from './health/health.ioc'; import { VotingHistoryDependencyInjector } from './voting-history/voting-history.ioc'; import { PingDependencyInjector } from './ping/ping.ioc'; +import { MSIGSDependencyInjector } from './msigs/msigs.ioc'; export class ApiDependencyInjector extends DependencyInjector { public async setup(config: ApiConfig): Promise { @@ -37,6 +39,7 @@ export class ApiDependencyInjector extends DependencyInjector { const votingHistoryDI = new VotingHistoryDependencyInjector(container); const candidatesDI = new CandidatesDependencyInjector(container); const custodiansDI = new CustodiansDependencyInjector(container); + const msigsDI = new MSIGSDependencyInjector(container); healthDI.setup(config); pingDI.setup(); @@ -46,30 +49,37 @@ export class ApiDependencyInjector extends DependencyInjector { votingHistoryDI.setup(); candidatesDI.setup(); custodiansDI.setup(); + msigsDI.setup(); /** * SMART CONTRACT SERVICES */ - await setupIndexWorldsContractService( + setupIndexWorldsContractService( antelopeRpcSource, config.antelope.hyperionUrl, container ); - await setupAlienWorldsContractService( + setupAlienWorldsContractService( antelopeRpcSource, config.antelope.hyperionUrl, container ); - await setupDaoWorldsContractService( + setupDaoWorldsContractService( antelopeRpcSource, config.antelope.hyperionUrl, container ); - await setupTokenWorldsContractService( + setupTokenWorldsContractService( + antelopeRpcSource, + config.antelope.hyperionUrl, + container + ); + + setupMsigWorldsContractService( antelopeRpcSource, config.antelope.hyperionUrl, container diff --git a/src/endpoints/msigs/data/dtos/msigs.dto.ts b/src/endpoints/msigs/data/dtos/msigs.dto.ts new file mode 100644 index 00000000..39c3c0ea --- /dev/null +++ b/src/endpoints/msigs/data/dtos/msigs.dto.ts @@ -0,0 +1,4 @@ +export type GetMSIGSRequestQueryParams = { + dacId: string; + limit: number; +}; diff --git a/src/endpoints/msigs/data/mappers/msig-state.mapper.ts b/src/endpoints/msigs/data/mappers/msig-state.mapper.ts new file mode 100644 index 00000000..5b0ebd2d --- /dev/null +++ b/src/endpoints/msigs/data/mappers/msig-state.mapper.ts @@ -0,0 +1,23 @@ +import { + MSIGStateIndex, + MSIGStateKey, +} from '@endpoints/msigs/domain/msig.enums'; + +/** + * The `MSIGStateMapper` class is responsible for converting a numeric state into a string state. + */ +export class MSIGStateMapper { + private msigKeyMapping = new Map([ + [MSIGStateIndex.PENDING, MSIGStateKey.PENDING], + [MSIGStateIndex.EXECUTED, MSIGStateKey.EXECUTED], + [MSIGStateIndex.CANCELLED, MSIGStateKey.CANCELLED], + ]); + + /** + * Converts a list of key-value pairs into a `DacAccounts` object. + * @returns {MSIGStateKey} - The DacAccounts object representing the DAC accounts with their balances. + */ + public toLabel(state: number): MSIGStateKey { + return this.msigKeyMapping.get(state) || MSIGStateKey.UNKNOWN; + } +} diff --git a/src/endpoints/msigs/data/mappers/proposals.mapper.ts b/src/endpoints/msigs/data/mappers/proposals.mapper.ts new file mode 100644 index 00000000..cfe8d15f --- /dev/null +++ b/src/endpoints/msigs/data/mappers/proposals.mapper.ts @@ -0,0 +1,42 @@ +import * as MSIGWorldsCommon from '@alien-worlds/aw-contract-msig-worlds'; + +import { Proposal } from '@endpoints/msigs/domain/entities/proposals'; +import { MSIGStateMapper } from './msig-state.mapper'; +/** + * The `ProposalsMapper` class is responsible for converting an object representing a MSIG Proposal from the MSIG Worlds contract + * into a `Proposal` object, which is a domain entity that represents a MSIG Proposal. + */ +export class ProposalsMapper { + private mapper = new MSIGStateMapper(); + + /** + * @param {MSIGWorldsCommon.Deltas.Entities.Proposals} msigWorldsProposal - The object representing the Proposal from the Msig Worlds contract. + * @returns {Proposal} - The `Proposal` object representing the MSIG Proposal. + */ + public toProposal( + msigWorldsProposal: MSIGWorldsCommon.Deltas.Entities.Proposals + ): Proposal { + const { + proposer, + proposalName, + packedTransaction, + metadata, + modifiedDate, + state, + earliestExecTime, + id, + ...rest + } = msigWorldsProposal; + return Proposal.create( + proposer, + proposalName, + packedTransaction.raw, + metadata, + modifiedDate, + this.mapper.toLabel(state), + earliestExecTime, + id, + rest + ); + } +} diff --git a/src/endpoints/msigs/domain/__tests__/get-dacs.controller.unit.test.ts b/src/endpoints/msigs/domain/__tests__/get-dacs.controller.unit.test.ts new file mode 100644 index 00000000..b322fdbf --- /dev/null +++ b/src/endpoints/msigs/domain/__tests__/get-dacs.controller.unit.test.ts @@ -0,0 +1,248 @@ +/* +import { ExtendedSymbolRawModel, Pair } from '@alien-worlds/aw-antelope'; +import * as AlienWorldsCommon from '@alien-worlds/aw-contract-alien-worlds'; +import * as DaoWorldsCommon from '@alien-worlds/aw-contract-dao-worlds'; +import * as IndexWorldsCommon from '@alien-worlds/aw-contract-index-worlds'; +import * as TokenWorldsCommon from '@alien-worlds/aw-contract-token-worlds'; +import { Container, Failure, Result } from '@alien-worlds/aw-core'; +import { DacMapper } from '@endpoints/dacs/data/mappers/dacs.mapper'; + +import { MSIGSController } from '../msigs.controller'; +import { GetMSIGSInput } from '../models/msigs.input'; +import { CreateAggregatedMSIGRecords } from '../use-cases/create-aggregated-msig-records.use-case'; +import { GetAllMSIGSUseCase } from '../use-cases/get-all-msigs.use-case'; +import { GetApprovalsUseCase } from '../use-cases/get-approvals.use-case'; +// import { GetDacTokensUseCase } from '../use-cases/_get-dac-tokens.use-case'; +// import { GetDacTreasuryUseCase } from '../use-cases/_get-dac-treasury.use-case'; + +const getAllDacsUseCase = { + execute: jest.fn(() => + Result.withContent([ + new DacMapper().toDac( + new IndexWorldsCommon.Deltas.Mappers.DacsRawMapper().toEntity(< + IndexWorldsCommon.Deltas.Types.DacsRawModel + >{ + owner: 'eyeke.dac', + dac_id: 'eyeke', + title: 'Eyeke', + symbol: { + sym: '4,EYE', + contract: 'token.worlds', + }, + dac_state: 0, + refs: [ + { + key: '1', + value: 'QmW1SeninpNQUMLstTPFTv7tMkRdBZMHBJTgf17Znr1uKK', + }, + { + key: '2', + value: + 'Also known as Second Earth or Terra Alterna as it is the nearest, and closest, of all Alien Worlds to Earth. Humans found Eyeke inhabited by a Monastic order of Greys who believe that Eyeke is a spiritual place. Despite initial fears, Trilium mining seems to be tolerated by the Monks at this time.', + }, + { + key: '12', + value: 'QmUXjmrQ6j2ukCdPjacdQ48MmYo853u4Y5y3kb5b4HBuuF', + }, + ] as Pair[], + accounts: [{ key: '0', value: 'eyeke.world' }] as Pair< + string, + string + >[], + }) + ), + ]) + ), +}; + +const getDacTreasuryUseCase = { + execute: jest.fn(() => + Result.withContent([ + new AlienWorldsCommon.Deltas.Mappers.AccountsRawMapper().toEntity(< + AlienWorldsCommon.Deltas.Types.AccountsRawModel + >{ + balance: 'string', + }), + ]) + ), +}; +const getDacInfoUseCase = { + execute: jest.fn(() => + Result.withContent([ + new DaoWorldsCommon.Deltas.Mappers.DacglobalsRawMapper().toEntity({ + data: [ + { key: 'auth_threshold_high', value: ['uint8', 3] }, + { key: 'auth_threshold_low', value: ['uint8', 2] }, + { key: 'auth_threshold_mid', value: ['uint8', 3] }, + { key: 'budget_percentage', value: ['uint32', 200] }, + { key: 'initial_vote_quorum_percent', value: ['uint32', 2] }, + { + key: 'lastclaimbudgettime', + value: ['time_point_sec', '2023-07-12T06:59:34'], + }, + { + key: 'lastperiodtime', + value: ['time_point_sec', '2023-07-12T02:13:16'], + }, + { key: 'lockup_release_time_delay', value: ['uint32', 1] }, + { + key: 'lockupasset', + value: [ + 'extended_asset', + { quantity: '5000.0000 EYE', contract: 'token.worlds' }, + ], + }, + { key: 'maxvotes', value: ['uint8', 2] }, + { key: 'met_initial_votes_threshold', value: ['bool', 1] }, + { key: 'number_active_candidates', value: ['uint32', 16] }, + { key: 'numelected', value: ['uint8', 5] }, + { key: 'periodlength', value: ['uint32', 604800] }, + { + key: 'requested_pay_max', + value: [ + 'extended_asset', + { quantity: '0.0000 TLM', contract: 'alien.worlds' }, + ], + }, + { key: 'should_pay_via_service_provider', value: ['bool', 0] }, + { key: 'token_supply_theshold', value: ['uint64', 100000000] }, + { + key: 'total_votes_on_candidates', + value: ['int64', '419531173046'], + }, + { + key: 'total_weight_of_votes', + value: ['int64', '84271574980'], + }, + { key: 'vote_quorum_percent', value: ['uint32', 1] }, + ] as any, + }), + ]) + ), +}; + +const getDacTokensUseCase = { + execute: jest.fn(() => + Result.withContent([ + new TokenWorldsCommon.Deltas.Mappers.StatRawMapper().toEntity({ + supply: '1660485.1217 EYE', + max_supply: '10000000000.0000 EYE', + issuer: 'federation', + transfer_locked: false, + }), + ]) + ), +}; + +const input: GetDacsInput = { + dacId: 'string', + limit: 1, + toJSON: () => ({ + dacId: 'string', + limit: 1, + }), +}; + +let container: Container; +let controller: MSIGSController; + +describe('GetDacs Controller Unit tests', () => { + beforeAll(() => { + container = new Container(); + + container + .bind(GetAllMSIGSUseCase.Token) + .toConstantValue(getAllDacsUseCase as any); + container + .bind(GetDacTreasuryUseCase.Token) + .toConstantValue(getDacTreasuryUseCase as any); + container + .bind(GetApprovalsUseCase.Token) + .toConstantValue(getDacInfoUseCase as any); + container + .bind(GetDacTokensUseCase.Token) + .toConstantValue(getDacTokensUseCase as any); + container + .bind(CreateAggregatedMSIGRecords.Token) + .to(CreateAggregatedMSIGRecords); + container.bind(MSIGSController.Token).to(MSIGSController); + }); + + beforeEach(() => { + controller = container.get(MSIGSController.Token); + }); + + afterAll(() => { + jest.clearAllMocks(); + container = null; + }); + + it('"Token" should be set', () => { + expect(MSIGSController.Token).not.toBeNull(); + }); + + it('Should execute GetAllDacsUseCase', async () => { + await controller.getDacs(input); + + expect(getAllDacsUseCase.execute).toBeCalled(); + }); + + it('Should execute GetDacTreasuryUseCase', async () => { + await controller.getDacs(input); + + expect(getDacTreasuryUseCase.execute).toBeCalled(); + }); + + it('Should execute GetDacInfoUseCase', async () => { + await controller.getDacs(input); + + expect(getDacInfoUseCase.execute).toBeCalled(); + }); + + it('Should execute GetDacTokensUseCase', async () => { + await controller.getDacs(input); + + expect(getDacTokensUseCase.execute).toBeCalled(); + }); + + it('Should return failure when GetAllDacsUseCase fails', async () => { + getAllDacsUseCase.execute.mockImplementationOnce( + jest.fn(() => Result.withFailure(Failure.fromError(null))) + ); + + const output = await controller.getDacs(input); + + expect(output.result.isFailure).toBeTruthy(); + }); + + it('Should return failure when GetDacTreasuryUseCase fails', async () => { + getDacTreasuryUseCase.execute.mockImplementationOnce( + jest.fn(() => Result.withFailure(Failure.fromError(null))) + ); + + const output = await controller.getDacs(input); + + expect(output.result.isFailure).toBeTruthy(); + }); + + it('Should return failure when GetDacInfoUseCase fails', async () => { + getDacInfoUseCase.execute.mockImplementationOnce( + jest.fn(() => Result.withFailure(Failure.fromError(null))) + ); + + const output = await controller.getDacs(input); + + expect(output.result.isFailure).toBeTruthy(); + }); + + it('Should return failure when GetDacTokensUseCase fails', async () => { + getDacTokensUseCase.execute.mockImplementationOnce( + jest.fn(() => Result.withFailure(Failure.fromError(null))) + ); + + const output = await controller.getDacs(input); + + expect(output.result.isFailure).toBeTruthy(); + }); +}); +*/ diff --git a/src/endpoints/msigs/domain/entities/__tests__/dac-accounts.unit.test.ts b/src/endpoints/msigs/domain/entities/__tests__/dac-accounts.unit.test.ts new file mode 100644 index 00000000..64e3e5cd --- /dev/null +++ b/src/endpoints/msigs/domain/entities/__tests__/dac-accounts.unit.test.ts @@ -0,0 +1,105 @@ +/* +import { DacAccounts } from '../_dac-accounts'; + +describe('DacAccounts', () => { + let dacAccounts: DacAccounts; + + beforeEach(() => { + dacAccounts = new DacAccounts( + 'auth', + 'treasury', + 'custodian', + 'msigOwned', + 'service', + 'proposals', + 'escrow', + 'voteWeight', + 'activation', + 'referendum', + 'spendings', + 'external', + 'other', + 'id' + ); + }); + + it('should create DacAccounts instance', () => { + expect(dacAccounts).toBeInstanceOf(DacAccounts); + }); + + it('should have correctly assigned properties', () => { + expect(dacAccounts.auth).toBe('auth'); + expect(dacAccounts.treasury).toBe('treasury'); + expect(dacAccounts.custodian).toBe('custodian'); + expect(dacAccounts.msigOwned).toBe('msigOwned'); + expect(dacAccounts.service).toBe('service'); + expect(dacAccounts.proposals).toBe('proposals'); + expect(dacAccounts.escrow).toBe('escrow'); + expect(dacAccounts.voteWeight).toBe('voteWeight'); + expect(dacAccounts.activation).toBe('activation'); + expect(dacAccounts.referendum).toBe('referendum'); + expect(dacAccounts.spendings).toBe('spendings'); + expect(dacAccounts.external).toBe('external'); + expect(dacAccounts.other).toBe('other'); + expect(dacAccounts.id).toBe('id'); + expect(dacAccounts.rest).toBeUndefined(); + }); + + it('should convert to JSON object properly', () => { + const json = dacAccounts.toJSON(); + + expect(json).toEqual({ + auth: 'auth', + treasury: 'treasury', + custodian: 'custodian', + msigOwned: 'msigOwned', + service: 'service', + proposals: 'proposals', + escrow: 'escrow', + voteWeight: 'voteWeight', + activation: 'activation', + referendum: 'referendum', + spendings: 'spendings', + external: 'external', + other: 'other', + }); + }); + + it('should create a new DacAccounts instance using static create method', () => { + const createdDacAccounts = DacAccounts.create( + 'auth', + 'treasury', + 'custodian', + 'msigOwned', + 'service', + 'proposals', + 'escrow', + 'voteWeight', + 'activation', + 'referendum', + 'spendings', + 'external', + 'other', + 'id', + {} + ); + + expect(createdDacAccounts).toBeInstanceOf(DacAccounts); + expect(createdDacAccounts.auth).toBe('auth'); + expect(createdDacAccounts.treasury).toBe('treasury'); + expect(createdDacAccounts.custodian).toBe('custodian'); + expect(createdDacAccounts.msigOwned).toBe('msigOwned'); + expect(createdDacAccounts.service).toBe('service'); + expect(createdDacAccounts.proposals).toBe('proposals'); + expect(createdDacAccounts.escrow).toBe('escrow'); + expect(createdDacAccounts.voteWeight).toBe('voteWeight'); + expect(createdDacAccounts.activation).toBe('activation'); + expect(createdDacAccounts.referendum).toBe('referendum'); + expect(createdDacAccounts.spendings).toBe('spendings'); + expect(createdDacAccounts.external).toBe('external'); + expect(createdDacAccounts.other).toBe('other'); + expect(createdDacAccounts.id).toBe('id'); + expect(dacAccounts.rest).toBeUndefined(); + }); +}); +*/ diff --git a/src/endpoints/msigs/domain/entities/__tests__/dac-refs.unit.test.ts b/src/endpoints/msigs/domain/entities/__tests__/dac-refs.unit.test.ts new file mode 100644 index 00000000..3bba193c --- /dev/null +++ b/src/endpoints/msigs/domain/entities/__tests__/dac-refs.unit.test.ts @@ -0,0 +1,88 @@ +/* +import { DacRefs } from '../_dac-refs'; + +describe('DacRefs', () => { + let dacRefs: DacRefs; + + beforeEach(() => { + dacRefs = DacRefs.getDefault(); + }); + + it('should create new instance of DacRefs', () => { + expect(dacRefs).toBeDefined(); + expect(dacRefs).toBeInstanceOf(DacRefs); + }); + + it('should set properties correctly', () => { + dacRefs = DacRefs.create( + 'homepage', + 'logoUrl', + 'description', + 'logoNoTextUrl', + 'backgroundUrl', + 'colors', + 'clientExtension', + 'faviconUrl', + 'dacCurrencyUrl', + 'systemCurrencyUrl', + 'discordUrl', + 'telegramUrl' + ); + + expect(dacRefs.homepage).toBe('homepage'); + expect(dacRefs.logoUrl).toBe('logoUrl'); + expect(dacRefs.description).toBe('description'); + expect(dacRefs.logoNoTextUrl).toBe('logoNoTextUrl'); + expect(dacRefs.backgroundUrl).toBe('backgroundUrl'); + expect(dacRefs.colors).toBe('colors'); + expect(dacRefs.clientExtension).toBe('clientExtension'); + expect(dacRefs.faviconUrl).toBe('faviconUrl'); + expect(dacRefs.dacCurrencyUrl).toBe('dacCurrencyUrl'); + expect(dacRefs.systemCurrencyUrl).toBe('systemCurrencyUrl'); + expect(dacRefs.discordUrl).toBe('discordUrl'); + expect(dacRefs.telegramUrl).toBe('telegramUrl'); + }); + + it('should set rest property correctly', () => { + const rest = { key: 'value' }; + dacRefs.rest = rest; + + expect(dacRefs.rest).toBe(rest); + }); + + it('should convert to JSON correctly', () => { + dacRefs = DacRefs.create( + 'homepage', + 'logoUrl', + 'description', + 'logoNoTextUrl', + 'backgroundUrl', + 'colors', + 'clientExtension', + 'faviconUrl', + 'dacCurrencyUrl', + 'systemCurrencyUrl', + 'discordUrl', + 'telegramUrl' + ); + + const expectedJson = { + homepage: 'homepage', + logoUrl: 'logoUrl', + description: 'description', + logoNoTextUrl: 'logoNoTextUrl', + backgroundUrl: 'backgroundUrl', + colors: 'colors', + clientExtension: 'clientExtension', + faviconUrl: 'faviconUrl', + dacCurrencyUrl: 'dacCurrencyUrl', + systemCurrencyUrl: 'systemCurrencyUrl', + discordUrl: 'discordUrl', + telegramUrl: 'telegramUrl', + rest: undefined, + }; + + expect(dacRefs.toJSON()).toEqual(expectedJson); + }); +}); +*/ diff --git a/src/endpoints/msigs/domain/entities/__tests__/dacs.unit.test.ts b/src/endpoints/msigs/domain/entities/__tests__/dacs.unit.test.ts new file mode 100644 index 00000000..01693e6a --- /dev/null +++ b/src/endpoints/msigs/domain/entities/__tests__/dacs.unit.test.ts @@ -0,0 +1,118 @@ +/* +import { Dac } from '../proposals'; +import { DacAccounts } from '../_dac-accounts'; +import { DacRefs } from '../_dac-refs'; +import { ExtendedSymbol } from '@alien-worlds/aw-antelope'; + +describe('Dac', () => { + describe('constructor', () => { + it('should create an instance of Dac', () => { + const owner = 'testOwner'; + const dacId = 'testDacId'; + const title = 'testTitle'; + const symbol = ExtendedSymbol.getDefault(); + const refs = DacRefs.getDefault(); + const accounts = DacAccounts.getDefault(); + const dacState = 0; + + const dac = new Dac( + owner, + dacId, + title, + symbol, + refs, + accounts, + dacState + ); + + expect(dac).toBeInstanceOf(Dac); + expect(dac.owner).toBe(owner); + expect(dac.dacId).toBe(dacId); + expect(dac.title).toBe(title); + expect(dac.symbol).toEqual(symbol); + expect(dac.refs).toEqual(refs); + expect(dac.accounts).toEqual(accounts); + expect(dac.dacState).toBe(dacState); + }); + }); + + describe('toJSON', () => { + it('should convert the Dac object to JSON', () => { + const owner = 'testOwner'; + const dacId = 'testDacId'; + const title = 'testTitle'; + const symbol = ExtendedSymbol.getDefault(); + const refs = DacRefs.getDefault(); + const accounts = DacAccounts.getDefault(); + const dacState = 0; + + const dac = new Dac( + owner, + dacId, + title, + symbol, + refs, + accounts, + dacState + ); + const json = dac.toJSON(); + + expect(json).toEqual({ + owner, + dacId, + title, + symbol, + refs: refs.toJSON(), + accounts: accounts.toJSON(), + dacState, + }); + }); + }); + + describe('create', () => { + it('should create a new instance of Dac', () => { + const owner = 'testOwner'; + const dacId = 'testDacId'; + const title = 'testTitle'; + const symbol = ExtendedSymbol.getDefault(); + const refs = DacRefs.getDefault(); + const accounts = DacAccounts.getDefault(); + const dacState = 0; + + const dac = Dac.create( + owner, + dacId, + title, + symbol, + refs, + accounts, + dacState + ); + + expect(dac).toBeInstanceOf(Dac); + expect(dac.owner).toBe(owner); + expect(dac.dacId).toBe(dacId); + expect(dac.title).toBe(title); + expect(dac.symbol).toEqual(symbol); + expect(dac.refs).toEqual(refs); + expect(dac.accounts).toEqual(accounts); + expect(dac.dacState).toBe(dacState); + }); + }); + + describe('getDefault', () => { + it('should return the default Dac instance', () => { + const defaultDac = Dac.getDefault(); + + expect(defaultDac).toBeInstanceOf(Dac); + expect(defaultDac.owner).toBe(''); + expect(defaultDac.dacId).toBe(''); + expect(defaultDac.title).toBe(''); + expect(defaultDac.symbol).toEqual(ExtendedSymbol.getDefault()); + expect(defaultDac.refs).toEqual(DacRefs.getDefault()); + expect(defaultDac.accounts).toEqual(DacAccounts.getDefault()); + expect(defaultDac.dacState).toBe(0); + }); + }); +}); +*/ diff --git a/src/endpoints/msigs/domain/entities/proposals.ts b/src/endpoints/msigs/domain/entities/proposals.ts new file mode 100644 index 00000000..da039694 --- /dev/null +++ b/src/endpoints/msigs/domain/entities/proposals.ts @@ -0,0 +1,94 @@ +import { Pair } from '@alien-worlds/aw-antelope'; +import { Entity, UnknownObject } from '@alien-worlds/aw-core'; + +// import { DacAccounts } from './dac-accounts'; +// import { DacRefs } from './dac-refs'; +// import { ExtendedSymbol } from '@alien-worlds/aw-antelope'; + +/** + * Represents a `Proposal` object. + * + * @class + */ +export class Proposal implements Entity { + /** + * + * @param proposer + * @param proposalName + * @param packedTransaction + * @param metadata + * @param modifiedDate + * @param state + * @param earliestExecTime + * @param id + */ + public constructor( + public proposer: string, + public proposalName: string, + public packedTransaction: string, + public metadata: Pair[], + public modifiedDate: Date, + public state: string, + public earliestExecTime: Date, + public id: string + ) {} + + public rest?: UnknownObject; + + /** + * Converts the current instance of the `Dac` class to a JSON object. + * + * @public + * @returns {UnknownObject} The JSON representation of the instance. + */ + public toJSON(): UnknownObject { + return { + proposer: this.proposer, + proposalName: this.proposalName, + packedTransaction: this.packedTransaction, + metadata: this.metadata, + modifiedDate: this.modifiedDate, + state: this.state, + earliestExecTime: this.earliestExecTime, + id: this.id, + }; + } + + /** + * Creates an instance of the `Proposal` class. + * + * @static + * @public + * @returns `Dac` An instance of the `Dac` class. + */ + public static create( + proposer: string, + proposalName: string, + packedTransaction: string, + metadata: Pair[], + modifiedDate: Date, + state: string, + earliestExecTime: Date, + id: string, + rest: UnknownObject + ): Proposal { + const entity = new Proposal( + proposer, + proposalName, + packedTransaction, + metadata, + modifiedDate, + state, + earliestExecTime, + id + ); + + entity.rest = rest; + + return entity; + } + + public static getDefault(): Proposal { + return new Proposal('', '', '', [], new Date(), '', new Date(), ''); + } +} diff --git a/src/endpoints/msigs/domain/models/__tests__/dacs.input.unit.test.ts b/src/endpoints/msigs/domain/models/__tests__/dacs.input.unit.test.ts new file mode 100644 index 00000000..746d1429 --- /dev/null +++ b/src/endpoints/msigs/domain/models/__tests__/dacs.input.unit.test.ts @@ -0,0 +1,30 @@ +/* +import { GetDacsInput } from '../msigs.input'; + +const input = { + query: { + dacId: 'nerix', + limit: 100, + }, +}; + +describe('GetDacsInput Unit tests', () => { + it('"GetDacsInput.fromRequest" should create instance', async () => { + const fromReq = GetDacsInput.create(input.query.dacId, input.query.limit); + + expect(fromReq).toBeInstanceOf(GetDacsInput); + }); + + it('GetDacsInput instance should have proper dacId value', async () => { + const fromReq = GetDacsInput.create(input.query.dacId, input.query.limit); + + expect(fromReq.dacId).toBe(input.query.dacId); + }); + + it('GetDacsInput instance should have proper limit value', async () => { + const fromReq = GetDacsInput.create(input.query.dacId, input.query.limit); + + expect(fromReq.limit).toBe(input.query.limit); + }); +}); +*/ diff --git a/src/endpoints/msigs/domain/models/__tests__/get-dac.output.unit.test.ts b/src/endpoints/msigs/domain/models/__tests__/get-dac.output.unit.test.ts new file mode 100644 index 00000000..fe5f4a15 --- /dev/null +++ b/src/endpoints/msigs/domain/models/__tests__/get-dac.output.unit.test.ts @@ -0,0 +1,59 @@ +/* +import * as AlienWorldsCommon from '@alien-worlds/aw-contract-alien-worlds'; +import * as DaoWorldsCommon from '@alien-worlds/aw-contract-dao-worlds'; +import * as IndexWorldsCommon from '@alien-worlds/aw-contract-index-worlds'; +import * as TokenWorldsCommon from '@alien-worlds/aw-contract-token-worlds'; + +import { DacMapper } from '@endpoints/dacs/data/mappers/dacs.mapper'; +import { MSIGAggregateRecord } from '../msig-aggregate-record'; +import { Pair } from '@alien-worlds/aw-antelope'; + +const dacDir = new DacMapper().toDac( + new IndexWorldsCommon.Deltas.Mappers.DacsRawMapper().toEntity({ + accounts: [{ key: '2', value: 'dao.worlds' }] as Pair[], + symbol: { sym: 'EYE', contract: '' }, + refs: [], + }) +); + +const dacTreasury = + new AlienWorldsCommon.Deltas.Mappers.AccountsRawMapper().toEntity({ + balance: 'string', + }); + +const dacGlobals = + new DaoWorldsCommon.Deltas.Mappers.DacglobalsRawMapper().toEntity({ + data: [], + }); + +const dacStats = new TokenWorldsCommon.Deltas.Mappers.StatRawMapper().toEntity({ + supply: 'string', + max_supply: 'string', + issuer: 'string', + transfer_locked: false, +}); + +describe('GetDacOutput Unit tests', () => { + it('"GetDacOutput.create" should create instance', async () => { + const output = MSIGAggregateRecord.create( + dacDir, + dacTreasury, + dacGlobals, + dacStats + ); + + expect(output).toBeInstanceOf(MSIGAggregateRecord); + }); + + it('GetDacOutput.toJson should return json object', async () => { + const output = MSIGAggregateRecord.create( + dacDir, + dacTreasury, + dacGlobals, + dacStats + ); + + expect(output.toJSON()).toBeInstanceOf(Object); + }); +}); +*/ diff --git a/src/endpoints/msigs/domain/models/__tests__/get-dacs.output.unit.test.ts b/src/endpoints/msigs/domain/models/__tests__/get-dacs.output.unit.test.ts new file mode 100644 index 00000000..42b9741e --- /dev/null +++ b/src/endpoints/msigs/domain/models/__tests__/get-dacs.output.unit.test.ts @@ -0,0 +1,58 @@ +/* +import * as AlienWorldsCommon from '@alien-worlds/aw-contract-alien-worlds'; +import * as DaoWorldsCommon from '@alien-worlds/aw-contract-dao-worlds'; +import * as IndexWorldsCommon from '@alien-worlds/aw-contract-index-worlds'; +import * as TokenWorldsCommon from '@alien-worlds/aw-contract-token-worlds'; + +import { DacMapper } from '@endpoints/dacs/data/mappers/dacs.mapper'; +import { MSIGAggregateRecord } from '../msig-aggregate-record'; +import { GetMSIGSOutput } from '../get-msigs.output'; +import { Result } from '@alien-worlds/aw-core'; + +const dacDir = new DacMapper().toDac( + new IndexWorldsCommon.Deltas.Mappers.DacsRawMapper().toEntity({ + accounts: [], + symbol: { symbol: 'EYE', contract: '' }, + refs: [], + }) +); + +const dacTreasury = + new AlienWorldsCommon.Deltas.Mappers.AccountsRawMapper().toEntity({ + balance: 'string', + }); + +const dacGlobals = + new DaoWorldsCommon.Deltas.Mappers.DacglobalsRawMapper().toEntity({ + data: [], + }); + +const dacStats = new TokenWorldsCommon.Deltas.Mappers.StatRawMapper().toEntity({ + supply: 'string', + max_supply: 'string', + issuer: 'string', + transfer_locked: false, +}); + +describe('GetDacsOutput Unit tests', () => { + it('"GetDacsOutput.create" should create instance', async () => { + const output = GetMSIGSOutput.create( + Result.withContent([ + MSIGAggregateRecord.create(dacDir, dacTreasury, dacGlobals, dacStats), + ]) + ); + + expect(output).toBeInstanceOf(GetMSIGSOutput); + }); + + it('GetDacsOutput.toJson should return json object', async () => { + const output = GetMSIGSOutput.create( + Result.withContent([ + MSIGAggregateRecord.create(dacDir, dacTreasury, dacGlobals, dacStats), + ]) + ); + + expect(output.toJSON()).toBeInstanceOf(Object); + }); +}); +*/ diff --git a/src/endpoints/msigs/domain/models/get-msigs.output.ts b/src/endpoints/msigs/domain/models/get-msigs.output.ts new file mode 100644 index 00000000..1676934f --- /dev/null +++ b/src/endpoints/msigs/domain/models/get-msigs.output.ts @@ -0,0 +1,54 @@ +import { MSIGAggregateRecord } from './msig-aggregate-record'; +import { + IO, + Result, + UnknownObject, + removeUndefinedProperties, +} from '@alien-worlds/aw-core'; + +/** + * The `GetMSIGSOutput` class represents the output data for fetching MSIGS. + * @class + */ +export class GetMSIGSOutput implements IO { + /** + * Creates an instance of `GetMSIGSOutput` with the specified result and count. + * @param {Result} result - List of aggregated Dac data. + * @returns {GetMSIGSOutput} - The `GetMSIGSOutput` instance with the provided parameters. + */ + public static create(result?: Result): GetMSIGSOutput { + return new GetMSIGSOutput(result, result?.content?.length || 0); + } + + /** + * @private + * @constructor + * @param {Result} result - List of aggregated Dac data. + * @param {number} count - The count of DACs fetched. + */ + private constructor( + public readonly result: Result, + public readonly count: number + ) {} + + /** + * Converts the `GetMSIGSOutput` into a JSON representation. + * @returns {UnknownObject} - The JSON representation of the `GetMSIGSOutput`. + */ + public toJSON(): UnknownObject { + const { count, result } = this; + + if (result.isFailure) { + return { results: [], count: 0 }; + } + + const json = { + results: result.content.map(dac => + removeUndefinedProperties(dac.toJSON()) + ), + count, + }; + + return removeUndefinedProperties(json); + } +} diff --git a/src/endpoints/msigs/domain/models/msig-aggregate-record.ts b/src/endpoints/msigs/domain/models/msig-aggregate-record.ts new file mode 100644 index 00000000..9063c4e8 --- /dev/null +++ b/src/endpoints/msigs/domain/models/msig-aggregate-record.ts @@ -0,0 +1,126 @@ +import { AssetRawMapper, Pair } from '@alien-worlds/aw-antelope'; +import * as MSIGWorldsCommon from '@alien-worlds/aw-contract-msig-worlds'; +import { + removeUndefinedProperties, + UnknownObject, +} from '@alien-worlds/aw-core'; +import { camelCase } from 'change-case'; + +import { Proposal } from '../entities/proposals'; +import { UnPackedTransaction } from './unpacked-transaction'; + +/** + * The `MSIGAggregateRecord` class is responsible for creating an aggregated record that contains information about a MSIGProposal + * and its related data. + */ +export class MSIGAggregateRecord { + public static create( + proposal: Proposal, + approvals: MSIGWorldsCommon.Deltas.Entities.Approvals, + unpackedTxn: UnPackedTransaction + ): MSIGAggregateRecord { + return new MSIGAggregateRecord(proposal, approvals, unpackedTxn); + } + + private constructor( + public readonly proposal: Proposal, + public readonly approvals: MSIGWorldsCommon.Deltas.Entities.Approvals, + public readonly unpackedTxn: UnPackedTransaction + ) {} + + /** + * Converts the `MSIGAggregateRecord` into a JSON representation. + * @returns {UnknownObject} - The JSON representation of the `MSIGAggregateRecord`. + */ + public toJSON(): UnknownObject { + const { proposal, approvals } = this; + const { + id, + proposalName, + proposer, + packedTransaction, + earliestExecTime, + metadata, + modifiedDate, + state, + } = proposal; + + const result: UnknownObject = { + id, + proposer, + proposalName, + packedTransaction, + unpackedTxn: this.unpackedTxn.toJSON(), + earliestExecTime, + metadata, + modifiedDate, + state, + approvals, + }; + + // const assetRawMapper = new AssetRawMapper(); + + // if (dacTreasury) { + // result.dacTreasury = { + // balance: assetRawMapper.fromEntity(dacTreasury.balance), + // }; + // } + + // if (dacStats) { + // result.dacStats = { + // supply: assetRawMapper.fromEntity(dacStats.supply), + // maxSupply: assetRawMapper.fromEntity(dacStats.maxSupply), + // issuer: dacStats.issuer, + // transferLocked: dacStats.transferLocked, + // }; + // } + + // if (dacGlobals) { + // result.electionGlobals = {}; + + // dacGlobals.data.forEach(eg => { + // const { second, value } = eg; + + // let formattedValue; + // if (value && Array.isArray(value) && value.length > 1) { + // formattedValue = value[1]; + // } else if (second && Array.isArray(second) && second.length > 1) { + // formattedValue = second[1]; + // } + + // result.electionGlobals[this.getKeyName(eg)] = formattedValue; + // }); + // } + + return removeUndefinedProperties(result); + } + + // private getKeyName = (electionGlobal: Pair): string => { + // const { key, first } = electionGlobal; + + // let result = camelCase(key || first); + + // switch (result) { + // case 'lastclaimbudgettime': + // result = 'lastClaimBudgetTime'; + // break; + // case 'lastperiodtime': + // result = 'lastPeriodTime'; + // break; + // case 'lockupasset': + // result = 'lockupAsset'; + // break; + // case 'maxvotes': + // result = 'maxVotes'; + // break; + // case 'numelected': + // result = 'numElected'; + // break; + // case 'periodlength': + // result = 'periodLength'; + // break; + // } + + // return result; + // }; +} diff --git a/src/endpoints/msigs/domain/models/msigs.input.ts b/src/endpoints/msigs/domain/models/msigs.input.ts new file mode 100644 index 00000000..03e12692 --- /dev/null +++ b/src/endpoints/msigs/domain/models/msigs.input.ts @@ -0,0 +1,37 @@ +import { IO, UnknownObject } from '@alien-worlds/aw-core'; + +/** + * The `GetMSIGSInput` class represents the input data for fetching MSIGS. + * @class + */ +export class GetMSIGSInput implements IO { + /** + * Creates an instance of `GetDacsInput` with the specified DAC ID and limit. + * @param {string} dacId - The DAC ID to fetch. + * @param {number} limit - The limit of results to fetch (default is 10). + * @returns {GetMSIGSInput} - The `GetDacsInput` instance with the provided parameters. + */ + public static create(dacId: string, limit: number): GetMSIGSInput { + return new GetMSIGSInput(dacId, limit); + } + + /** + * @private + * @constructor + * @param {string} dacId - The DAC ID to fetch. + * @param {number} limit - The limit of results to fetch. + */ + private constructor( + public readonly dacId: string, + public readonly limit: number = 10 + ) {} + + /** + * Converts the `GetDacsInput` into a JSON representation. + * @returns {UnknownObject} - The JSON representation of the `GetDacsInput`. + */ + public toJSON(): UnknownObject { + const { dacId, limit } = this; + return { dacId, limit }; + } +} diff --git a/src/endpoints/msigs/domain/models/unpacked-transaction.ts b/src/endpoints/msigs/domain/models/unpacked-transaction.ts new file mode 100644 index 00000000..6ba93fcf --- /dev/null +++ b/src/endpoints/msigs/domain/models/unpacked-transaction.ts @@ -0,0 +1,31 @@ +import { Transaction } from '../use-cases/get-decoded-msig-txn.use-case'; +import { + removeUndefinedProperties, + UnknownObject, +} from '@alien-worlds/aw-core'; + +/** + * The `MSIGAggregateRecord` class is responsible for creating an aggregated record that contains information about a MSIGProposal + * and its related data. + */ +export class UnPackedTransaction { + public static create(rawTransaction: Transaction): UnPackedTransaction { + return new UnPackedTransaction(rawTransaction); + } + + private constructor(public readonly rawTransaction: Transaction) { + this.rawTransaction = rawTransaction; + } + + /** + * Converts the `MSIGAggregateRecord` into a JSON representation. + * @returns {UnknownObject} - The JSON representation of the `MSIGAggregateRecord`. + */ + public toJSON(): UnknownObject { + const result: UnknownObject = { + ...this.rawTransaction, + }; + + return removeUndefinedProperties(result); + } +} diff --git a/src/endpoints/msigs/domain/msig.enums.ts b/src/endpoints/msigs/domain/msig.enums.ts new file mode 100644 index 00000000..1c22487b --- /dev/null +++ b/src/endpoints/msigs/domain/msig.enums.ts @@ -0,0 +1,12 @@ +export enum MSIGStateIndex { + PENDING = 0, + EXECUTED = 1, + CANCELLED = 2, +} + +export enum MSIGStateKey { + PENDING = 'pending', + EXECUTED = 'executed', + CANCELLED = 'cancelled', + UNKNOWN = 'unknown', +} diff --git a/src/endpoints/msigs/domain/msigs.controller.ts b/src/endpoints/msigs/domain/msigs.controller.ts new file mode 100644 index 00000000..2ee5e23c --- /dev/null +++ b/src/endpoints/msigs/domain/msigs.controller.ts @@ -0,0 +1,47 @@ +import { inject, injectable, Result } from '@alien-worlds/aw-core'; +import { GetAllMSIGSUseCase } from './use-cases/get-all-msigs.use-case'; +import { GetMSIGSInput } from './models/msigs.input'; +import { GetMSIGSOutput } from './models/get-msigs.output'; +import { CreateAggregatedMSIGRecords } from './use-cases/create-aggregated-msig-records.use-case'; + +/** + * The `MSIGSController` class is a controller for handling MSIG-related operations. + * @class + */ +@injectable() +export class MSIGSController { + public static Token = 'MSIGS_CONTROLLER'; + + /** + * Creates an instance of the `DacsController` with the specified dependencies. + * @param {GetAllMSIGSUseCase} getAllMSIGSUseCase - The use case for getting all DACs. + * @param {CreateAggregatedMSIGRecords} createAggregatedMSIGRecords - The use case for creating aggregated DAC records. + */ + constructor( + @inject(GetAllMSIGSUseCase.Token) + private getAllMSIGSUseCase: GetAllMSIGSUseCase, + @inject(CreateAggregatedMSIGRecords.Token) + private createAggregatedMSIGRecords: CreateAggregatedMSIGRecords + ) {} + + /** + * Gets a list of DACs based on the provided input. + * @param {GetMSIGSInput} input - The input for getting DACs. + * @returns {Promise} - The result of the operation containing the list of DACs. + */ + public async getMSIGS(input: GetMSIGSInput): Promise { + const { content: proposals, failure: getAllMSIGSFailure } = + await this.getAllMSIGSUseCase.execute(input); + + if (getAllMSIGSFailure) { + return GetMSIGSOutput.create(Result.withFailure(getAllMSIGSFailure)); + } + + const aggregationResult = await this.createAggregatedMSIGRecords.execute( + proposals, + input.dacId + ); + + return GetMSIGSOutput.create(aggregationResult); + } +} diff --git a/src/endpoints/msigs/domain/use-cases/__tests__/get-all-dacs.use-case.unit.test.ts b/src/endpoints/msigs/domain/use-cases/__tests__/get-all-dacs.use-case.unit.test.ts new file mode 100644 index 00000000..22cb0f67 --- /dev/null +++ b/src/endpoints/msigs/domain/use-cases/__tests__/get-all-dacs.use-case.unit.test.ts @@ -0,0 +1,114 @@ +/* +import 'reflect-metadata'; + +import * as IndexWorldsCommon from '@alien-worlds/aw-contract-index-worlds'; + +import { Container, Failure, Result } from '@alien-worlds/aw-core'; +import { ExtendedSymbolRawModel, Pair } from '@alien-worlds/aw-antelope'; +import { Dac } from '../../entities/proposals'; +import { GetAllMSIGSUseCase } from '../get-all-msigs.use-case'; +import { GetDacsInput } from '../../models/msigs.input'; + +const indexWorldsContractService = { + fetchDacs: jest.fn(), +}; + +const input: GetDacsInput = { + dacId: 'string', + limit: 1, + toJSON: () => ({ + dacId: 'string', + limit: 1, + }), +}; + +let container: Container; +let useCase: GetAllMSIGSUseCase; + +describe('Get All Dacs Unit tests', () => { + beforeAll(() => { + container = new Container(); + + container + .bind( + IndexWorldsCommon.Services.IndexWorldsContractService.Token + ) + .toConstantValue(indexWorldsContractService as any); + container + .bind(GetAllMSIGSUseCase.Token) + .to(GetAllMSIGSUseCase); + }); + + beforeEach(() => { + useCase = container.get(GetAllMSIGSUseCase.Token); + }); + + afterAll(() => { + jest.clearAllMocks(); + container = null; + }); + + it('"Token" should be set', () => { + expect(GetAllMSIGSUseCase.Token).not.toBeNull(); + }); + + it('Should return a failure when index.worlds contract service fails', async () => { + indexWorldsContractService.fetchDacs.mockResolvedValueOnce( + Result.withFailure(Failure.fromError(null)) + ); + + const result = await useCase.execute(input); + expect(result.isFailure).toBeTruthy(); + }); + + it('should return an array of Dac', async () => { + indexWorldsContractService.fetchDacs.mockResolvedValueOnce( + Result.withContent([ + { + owner: 'eyeke.dac', + dac_id: 'eyeke', + title: 'Eyeke', + symbol: { + sym: '4,EYE', + contract: 'token.worlds', + }, + dac_state: 0, + refs: [ + { + key: '1', + value: 'QmW1SeninpNQUMLstTPFTv7tMkRdBZMHBJTgf17Znr1uKK', + }, + { + key: '2', + value: + 'Also known as Second Earth or Terra Alterna as it is the nearest, and closest, of all Alien Worlds to Earth. Humans found Eyeke inhabited by a Monastic order of Greys who believe that Eyeke is a spiritual place. Despite initial fears, Trilium mining seems to be tolerated by the Monks at this time.', + }, + { + key: '12', + value: 'QmUXjmrQ6j2ukCdPjacdQ48MmYo853u4Y5y3kb5b4HBuuF', + }, + ] as Pair[], + accounts: [{ key: '0', value: 'eyeke.world' }] as Pair< + string, + string + >[], + }, + ]) + ); + + const result = await useCase.execute(input); + + expect(result.content).toBeInstanceOf(Array); + expect(result.content[0]).toBeInstanceOf(Dac); + }); + + it('Should return a failure when index.worlds contract service returns empty result', async () => { + indexWorldsContractService.fetchDacs.mockResolvedValueOnce( + Result.withContent([]) + ); + + const result = await useCase.execute(input); + expect(result.isFailure).toBeTruthy(); + }); +}); +*/ diff --git a/src/endpoints/msigs/domain/use-cases/__tests__/get-dac-info.use-case.unit.test.ts b/src/endpoints/msigs/domain/use-cases/__tests__/get-dac-info.use-case.unit.test.ts new file mode 100644 index 00000000..00595bd2 --- /dev/null +++ b/src/endpoints/msigs/domain/use-cases/__tests__/get-dac-info.use-case.unit.test.ts @@ -0,0 +1,67 @@ +/* +import * as DaoWorldsCommon from '@alien-worlds/aw-contract-dao-worlds'; +import { Container, Failure, Result } from '@alien-worlds/aw-core'; + +import { GetApprovalsUseCase } from '../get-approvals.use-case'; +import { Pair } from '@alien-worlds/aw-antelope'; + +const daoWorldsContractService = { + fetchDacglobals: jest.fn(), +}; + +let container: Container; +let useCase: GetApprovalsUseCase; + +describe('Get Dac Info Unit tests', () => { + beforeAll(() => { + container = new Container(); + + container + .bind( + DaoWorldsCommon.Services.DaoWorldsContractService.Token + ) + .toConstantValue(daoWorldsContractService as any); + container + .bind(GetApprovalsUseCase.Token) + .to(GetApprovalsUseCase); + }); + + beforeEach(() => { + useCase = container.get(GetApprovalsUseCase.Token); + }); + + afterAll(() => { + jest.clearAllMocks(); + container = null; + }); + + it('"Token" should be set', () => { + expect(GetApprovalsUseCase.Token).not.toBeNull(); + }); + + it('Should return a failure when dao.worlds contract service fails', async () => { + daoWorldsContractService.fetchDacglobals.mockResolvedValueOnce( + Result.withFailure(Failure.fromError(null)) + ); + + const result = await useCase.execute(''); + expect(result.isFailure).toBeTruthy(); + }); + + it('should return an array of DacGlobals', async () => { + daoWorldsContractService.fetchDacglobals.mockResolvedValue( + Result.withContent([ + { + data: [{ key: 'some_key', value: [] }] as Pair[], + }, + ]) + ); + + const result = await useCase.execute(''); + expect(result.content).toBeInstanceOf(Array); + expect(result.content[0]).toBeInstanceOf( + DaoWorldsCommon.Deltas.Entities.Dacglobals + ); + }); +}); +*/ diff --git a/src/endpoints/msigs/domain/use-cases/__tests__/get-dac-tokens.use-case.unit.test.ts b/src/endpoints/msigs/domain/use-cases/__tests__/get-dac-tokens.use-case.unit.test.ts new file mode 100644 index 00000000..721ff46b --- /dev/null +++ b/src/endpoints/msigs/domain/use-cases/__tests__/get-dac-tokens.use-case.unit.test.ts @@ -0,0 +1,69 @@ +/* +import * as TokenWorldsContract from '@alien-worlds/aw-contract-token-worlds'; +import { Container, Failure, Result } from '@alien-worlds/aw-core'; + +import { GetDacTokensUseCase } from '../_get-dac-tokens.use-case'; + +const tokenWorldsContractService = { + fetchStat: jest.fn(), +}; + +let container: Container; +let useCase: GetDacTokensUseCase; + +describe('Get Dac Tokens Unit tests', () => { + beforeAll(() => { + container = new Container(); + + container + .bind( + TokenWorldsContract.Services.TokenWorldsContractService.Token + ) + .toConstantValue(tokenWorldsContractService as any); + container + .bind(GetDacTokensUseCase.Token) + .to(GetDacTokensUseCase); + }); + + beforeEach(() => { + useCase = container.get(GetDacTokensUseCase.Token); + }); + + afterAll(() => { + jest.clearAllMocks(); + container = null; + }); + + it('"Token" should be set', () => { + expect(GetDacTokensUseCase.Token).not.toBeNull(); + }); + + it('Should return a failure when token.worlds contract service fails', async () => { + tokenWorldsContractService.fetchStat.mockResolvedValueOnce( + Result.withFailure(Failure.fromError(null)) + ); + + const result = await useCase.execute(''); + expect(result.isFailure).toBeTruthy(); + }); + + it('should return an array of Stat', async () => { + tokenWorldsContractService.fetchStat.mockResolvedValueOnce( + Result.withContent([ + { + supply: '1660485.1217 EYE', + max_supply: '10000000000.0000 EYE', + issuer: 'federation', + transfer_locked: false, + }, + ]) + ); + + const result = await useCase.execute('EYE'); + expect(result.content).toBeInstanceOf(Array); + expect(result.content[0]).toBeInstanceOf( + TokenWorldsContract.Deltas.Entities.Stat + ); + }); +}); +*/ diff --git a/src/endpoints/msigs/domain/use-cases/__tests__/get-dac-treasury.use-case.unit.test.ts b/src/endpoints/msigs/domain/use-cases/__tests__/get-dac-treasury.use-case.unit.test.ts new file mode 100644 index 00000000..2ef4c5c5 --- /dev/null +++ b/src/endpoints/msigs/domain/use-cases/__tests__/get-dac-treasury.use-case.unit.test.ts @@ -0,0 +1,67 @@ +/* +import 'reflect-metadata'; +import * as AlienWorldsContract from '@alien-worlds/aw-contract-alien-worlds'; +import { Container, Failure, Result } from '@alien-worlds/aw-core'; + +import { GetDacTreasuryUseCase } from '../_get-dac-treasury.use-case'; + +const alienWorldsContractService = { + fetchAccounts: jest.fn(), +}; + +let container: Container; +let useCase: GetDacTreasuryUseCase; +const input = 'account'; + +describe('Get Dac Treasury Unit tests', () => { + beforeAll(() => { + container = new Container(); + + container + .bind( + AlienWorldsContract.Services.AlienWorldsContractService.Token + ) + .toConstantValue(alienWorldsContractService as any); + container + .bind(GetDacTreasuryUseCase.Token) + .to(GetDacTreasuryUseCase); + }); + + beforeEach(() => { + useCase = container.get(GetDacTreasuryUseCase.Token); + }); + + afterAll(() => { + jest.clearAllMocks(); + container = null; + }); + + it('"Token" should be set', () => { + expect(GetDacTreasuryUseCase.Token).not.toBeNull(); + }); + + it('Should return a failure when alien.worlds contract service fails', async () => { + alienWorldsContractService.fetchAccounts.mockResolvedValueOnce( + Result.withFailure(Failure.fromError(null)) + ); + + const result = await useCase.execute(input); + expect(result.isFailure).toBeTruthy(); + }); + + it('should return AlienWorldsAccount', async () => { + alienWorldsContractService.fetchAccounts.mockResolvedValueOnce( + Result.withContent([ + { + balance: '12237582.5498 TLM', + }, + ]) + ); + + const result = await useCase.execute(input); + expect(result.content).toBeInstanceOf( + AlienWorldsContract.Deltas.Entities.Accounts + ); + }); +}); +*/ diff --git a/src/endpoints/msigs/domain/use-cases/create-aggregated-msig-records.use-case.ts b/src/endpoints/msigs/domain/use-cases/create-aggregated-msig-records.use-case.ts new file mode 100644 index 00000000..03a15c49 --- /dev/null +++ b/src/endpoints/msigs/domain/use-cases/create-aggregated-msig-records.use-case.ts @@ -0,0 +1,68 @@ +import { inject, injectable, Result, UseCase } from '@alien-worlds/aw-core'; +import { Proposal } from '../entities/proposals'; +import { GetApprovalsUseCase } from './get-approvals.use-case'; +import { MSIGAggregateRecord } from '../models/msig-aggregate-record'; +import { GetDecodedMSIGTxnUseCase } from './get-decoded-msig-txn.use-case'; + +/** + * The `CreateAggregatedMSIGRecords` class represents a use case for creating aggregated MSIG records. + * @class + */ +@injectable() +export class CreateAggregatedMSIGRecords + implements UseCase +{ + public static Token = 'CREATE_AGGREGATED_MSIG_RECORDS'; + + /** + * Creates an instance of the `CreateAggregatedMSIGRecords` use case with the specified dependencies. + * @param {GetApprovalsUseCase} getApprovalsUseCase - The use case for fetching Approvals for each Proposal. + */ + constructor( + @inject(GetApprovalsUseCase.Token) + private getApprovalsUseCase: GetApprovalsUseCase, + @inject(GetDecodedMSIGTxnUseCase.Token) + private getDecodedMSIGTxnUseCase: GetDecodedMSIGTxnUseCase + ) {} + + /** + * Executes the use case to create aggregated DAC records for the given DACs. + * @async + * @param {Proposal[]} proposals - The list of Proposals. + * @param {string} dacId - The ID of the DAC for which to create the aggregated record. + * @returns {Promise>} - The result of the use case operation containing aggregated DAC records. + */ + public async execute( + proposals: Proposal[], + dacId: string + ): Promise> { + const list: MSIGAggregateRecord[] = []; + + const approvalPromises = proposals.map(proposal => + this.getApprovalsUseCase.execute(dacId, proposal.proposalName) + ); + + const approvals = await Promise.all(approvalPromises); + + const decodePromises = proposals.map(proposal => + this.getDecodedMSIGTxnUseCase.execute(proposal.packedTransaction) + ); + const decodedTrxs = await Promise.all(decodePromises); + + for (const [idx, proposal] of proposals.entries()) { + const { content: app, failure: getApprovalFailure } = approvals[idx]; + if (getApprovalFailure) { + return Result.withFailure(getApprovalFailure); + } + const { content: decodedTxn, failure: getDecodedTxnFailure } = + decodedTrxs[idx]; + if (getDecodedTxnFailure) { + return Result.withFailure(getDecodedTxnFailure); + } + + list.push(MSIGAggregateRecord.create(proposal, app[0], decodedTxn)); + } + + return Result.withContent(list); + } +} diff --git a/src/endpoints/msigs/domain/use-cases/get-all-msigs.use-case.ts b/src/endpoints/msigs/domain/use-cases/get-all-msigs.use-case.ts new file mode 100644 index 00000000..dd2c3108 --- /dev/null +++ b/src/endpoints/msigs/domain/use-cases/get-all-msigs.use-case.ts @@ -0,0 +1,78 @@ +import * as MSIGWorldsCommon from '@alien-worlds/aw-contract-msig-worlds'; + +import { + Failure, + GetTableRowsOptions, + inject, + injectable, + Result, + SmartContractDataNotFoundError, + UseCase, +} from '@alien-worlds/aw-core'; +import { Proposal } from '../entities/proposals'; +import { ProposalsMapper } from '@endpoints/msigs/data/mappers/proposals.mapper'; +import { GetMSIGSInput } from '../models/msigs.input'; + +/** + * The `GetAllMSIGSUseCase` class represents a use case for fetching all DACs or a specific DAC based on the provided input. + * @class + */ +@injectable() +export class GetAllMSIGSUseCase implements UseCase { + public static Token = 'GET_ALL_MSIGS_USE_CASE'; + + /** + * Creates an instance of the `GetAllMSIGSUseCase` use case with the specified dependencies. + * @param {MSIGWorldsCommon.Services.MsigWorldsContractService} msigWorldsContractService - The service for interacting with the Index Worlds smart contract. + */ + constructor( + @inject(MSIGWorldsCommon.Services.MsigWorldsContractService.Token) + private msigWorldsContractService: MSIGWorldsCommon.Services.MsigWorldsContractService + ) {} + + /** + * Executes the use case to fetch all Proposals or a specific DAC based on the provided input. + * @async + * @param {GetMSIGSInput} input - The input parameters for fetching DACs, including an optional DAC ID and a limit. + * @returns {Promise>} - The result of the use case operation containing the fetched DAC entities. + */ + public async execute(input: GetMSIGSInput): Promise> { + const options: GetTableRowsOptions = { + limit: input.limit, + code: 'msig.worlds', + table: 'proposals', + scope: input.dacId, + }; + + const { content: dacs, failure: fetchDacsFailure } = + await this.msigWorldsContractService.fetchProposals(options); + + if (fetchDacsFailure) { + return Result.withFailure(fetchDacsFailure); + } + + if (dacs.length === 0) { + return Result.withFailure( + Failure.fromError( + new SmartContractDataNotFoundError({ + ...options, + table: 'proposals', + bound: input.dacId, + }) + ) + ); + } + + const proposalsRawMapper = + new MSIGWorldsCommon.Deltas.Mappers.ProposalsRawMapper(); + const dacContractMapper = new ProposalsMapper(); + + const result = dacs.map(proposal => { + return dacContractMapper.toProposal( + proposalsRawMapper.toEntity(proposal) + ); + }); + + return Result.withContent(result); + } +} diff --git a/src/endpoints/msigs/domain/use-cases/get-approvals.use-case.ts b/src/endpoints/msigs/domain/use-cases/get-approvals.use-case.ts new file mode 100644 index 00000000..e9070520 --- /dev/null +++ b/src/endpoints/msigs/domain/use-cases/get-approvals.use-case.ts @@ -0,0 +1,52 @@ +import * as MSIGWorldsCommon from '@alien-worlds/aw-contract-msig-worlds'; +import { inject, injectable, Result, UseCase } from '@alien-worlds/aw-core'; + +/** + * The `GetApprovalsUseCase` class represents a use case for fetching MSIG Approval information from the MSIG Worlds smart contract. + * @class + */ +@injectable() +export class GetApprovalsUseCase + implements UseCase +{ + public static Token = 'GET_MSIG_APPROVALS_USE_CASE'; + + /** + * Creates an instance of the `GetApprovalsUseCase` use case with the specified dependencies. + * @param {MSIGWorldsCommon.Services.MsigWorldsContractService} msigWorldsContractService - The service for interacting with the MSIG Worlds smart contract. + */ + constructor( + @inject(MSIGWorldsCommon.Services.MsigWorldsContractService.Token) + private msigWorldsContractService: MSIGWorldsCommon.Services.MsigWorldsContractService + ) {} + + /** + * Executes the use case to fetch Approval information from the MSIG Worlds smart contract. + * @async + * @param {string} dacId - The DAC ID for which to fetch the information. + * @param {string} proposalName - The proposal name for which to fetch the information. + * @returns {Promise>} - The result of the use case operation containing the fetched DAC information. + */ + public async execute( + dacId: string, + proposalName: string + ): Promise> { + const { content: dacGlobals, failure: fetchDacGlobalsFailure } = + await this.msigWorldsContractService.fetchApprovals({ + code: 'msig.worlds', + lower_bound: proposalName, + upper_bound: proposalName, + scope: dacId, + limit: 1, + }); + + if (fetchDacGlobalsFailure) { + return Result.withFailure(fetchDacGlobalsFailure); + } + + const dacglobalsRawMapper = + new MSIGWorldsCommon.Deltas.Mappers.ApprovalsRawMapper(); + + return Result.withContent(dacGlobals.map(dacglobalsRawMapper.toEntity)); + } +} diff --git a/src/endpoints/msigs/domain/use-cases/get-decoded-msig-txn.use-case.ts b/src/endpoints/msigs/domain/use-cases/get-decoded-msig-txn.use-case.ts new file mode 100644 index 00000000..392ce7ff --- /dev/null +++ b/src/endpoints/msigs/domain/use-cases/get-decoded-msig-txn.use-case.ts @@ -0,0 +1,106 @@ +import * as AlienWorldsCommon from '@alien-worlds/aw-contract-alien-worlds'; +import { Api, JsonRpc } from 'eosjs'; +import fetch from 'node-fetch'; + +import { + inject, + injectable, + Result, + UnknownObject, + UseCase, +} from '@alien-worlds/aw-core'; +import { UnPackedTransaction } from '../models/unpacked-transaction'; + +/** The AccountAuthorization is a common structure used in actions to specify which account and associated authority is to perform a given action. The most common used AccountAuthorization will be the `active` permission for an account - often seen as `permission@actor` eg. `active@bob` */ +export interface AccountAuthorization { + actor: string; + permission: string; +} + +/** This is the basic form that any action requires to be pushed to the blockchain. These can be grouped together into a transaction to facilitate performing a group of actions within one atomic transaction that either all succeed or all fail - no half way. Any combination of actions pushing to different contracts can be bundled into one transaction which give tremendous power to be able to perform grouped actions without leaving the blockchain logic in a partial state. eg. A transaction may require a deposit of funds to one account, an action to check the deposit followed by another action to confirm some logic and mint an NFT. By having all of these within one transaction even if the final minting of the NFT fails for some reason everything back to the initial deposit will be reversed and leave the blockchain in a stable state. */ +export type AntelopeAction = { + /** The account holding the contract with the action intended to execute eg. `eosio.token` */ + account: string; + /** The name of the action to executed eg. `transfer` */ + name: string; + /** The authorizations intended to perform this action. This should be authorised for that account either via the default `active` permission or via a custom auth that has been linked to an action with `linkauth` */ + authorization: AccountAuthorization[]; + /** The data required to perform the action. These will be the action sepcific params supplied in a JSON format */ + data: UnknownObject; +}; + +export type ResourcePayer = { + payer: string; + max_cpu_us: number; + max_net_bytes: number; + max_memory_bytes: number; +}; + +export type Transaction = { + actions: AntelopeAction[]; + expiration?: string; + ref_block_num?: number; + ref_block_prefix?: number; + max_net_usage_words?: number; + max_cpu_usage_ms?: number; + delay_sec?: number; + context_free_actions?: AntelopeAction[]; + context_free_data?: Uint8Array[]; + transaction_extensions?: [number, string][]; + resource_payer?: ResourcePayer; +}; + +/** + * The `GetDacTreasuryUseCase` class represents a use case for fetching DAC treasury information from the Alien Worlds smart contract. + * @class + */ +@injectable() +export class GetDecodedMSIGTxnUseCase implements UseCase { + public static Token = 'GET_DECODED_MSIG_USE_CASE'; + + /** + * Creates an instance of the `GetDacTreasuryUseCase` use case with the specified dependencies. + * @param {AlienWorldsCommon.Services.AlienWorldsContractService} alienWorldsContractService - The service for interacting with the Alien Worlds smart contract. + */ + constructor( + @inject(AlienWorldsCommon.Services.AlienWorldsContractService.Token) + private alienWorldsContractService: AlienWorldsCommon.Services.AlienWorldsContractService + ) {} + + /** + * Executes the use case to fetch DAC treasury information from the Alien Worlds smart contract. + * @async + * @param {string} account - The account name of the DAC treasury for which to fetch the information. + * @returns {Promise>} - The result of the use case operation containing the fetched DAC treasury information. + */ + public async execute( + packedTxn: string + ): Promise> { + try { + const rpc = new JsonRpc('https://wax.eosdac.io', { fetch }); + + const api = new Api({ + rpc, + signatureProvider: null, + textDecoder: new TextDecoder(), + textEncoder: new TextEncoder(), + }); + + // First unpack the serialised transaction from within within each proposal row. + const hex = Uint8Array.from(Buffer.from(packedTxn, 'hex')); + const unserialisedTxn = api.deserializeTransaction(hex); + // Each deserialised transaction will still have actions with encoded data which need to be further deserialised. + // The deserialising of actions is asynchronous because internally it fetches (and caches) the ABI from the blockchain for each contract + // action so that is can be deserialised. + const actions = await api.deserializeActions(unserialisedTxn.actions); + const unsTxn = { + ...unserialisedTxn, + actions, + }; + + return Result.withContent(UnPackedTransaction.create(unsTxn)); + } catch (error) { + return Result.withFailure(error); + } + } +} diff --git a/src/endpoints/msigs/msigs.ioc.ts b/src/endpoints/msigs/msigs.ioc.ts new file mode 100644 index 00000000..63d6475c --- /dev/null +++ b/src/endpoints/msigs/msigs.ioc.ts @@ -0,0 +1,35 @@ +import { CreateAggregatedMSIGRecords } from './domain/use-cases/create-aggregated-msig-records.use-case'; +import { MSIGSController } from './domain/msigs.controller'; +import { DependencyInjector } from '@alien-worlds/aw-core'; +import { GetAllMSIGSUseCase } from './domain/use-cases/get-all-msigs.use-case'; +import { GetApprovalsUseCase } from './domain/use-cases/get-approvals.use-case'; +// import { GetDacTokensUseCase } from './domain/use-cases/_get-dac-tokens.use-case'; +import { GetDecodedMSIGTxnUseCase } from './domain/use-cases/get-decoded-msig-txn.use-case'; + +/** + * Represents a dependency injector for setting up the MSIGS endpoint dependencies. + */ +export class MSIGSDependencyInjector extends DependencyInjector { + /** + * Sets up the dependency injection by binding classes to tokens in the container. + * @async + * @returns {Promise} + */ + public async setup(): Promise { + const { container } = this; + + container.bind(MSIGSController.Token).to(MSIGSController); + container + .bind(GetAllMSIGSUseCase.Token) + .to(GetAllMSIGSUseCase); + container + .bind(GetDecodedMSIGTxnUseCase.Token) + .to(GetDecodedMSIGTxnUseCase); + container + .bind(GetApprovalsUseCase.Token) + .to(GetApprovalsUseCase); + container + .bind(CreateAggregatedMSIGRecords.Token) + .to(CreateAggregatedMSIGRecords); + } +} diff --git a/src/endpoints/msigs/routes/msigs.route-io.ts b/src/endpoints/msigs/routes/msigs.route-io.ts new file mode 100644 index 00000000..abb75df3 --- /dev/null +++ b/src/endpoints/msigs/routes/msigs.route-io.ts @@ -0,0 +1,53 @@ +import { + Request, + Response, + RouteIO, + SmartContractDataNotFoundError, +} from '@alien-worlds/aw-core'; +import { GetMSIGSRequestQueryParams } from '../data/dtos/msigs.dto'; +import { GetMSIGSInput } from '../domain/models/msigs.input'; +import { GetMSIGSOutput } from '../domain/models/get-msigs.output'; + +/** + * Represents the RouteIO for handling custodians list route input/output. + */ +export class GetMSIGSRouteIO extends RouteIO { + /** + * Converts the output of the route to the server's response format. + * @param {GetMSIGSOutput} output - The output of the route. + * @returns {Response} - The server's response. + */ + public toResponse(output: GetMSIGSOutput): Response { + const { result } = output; + if (result.isFailure) { + const { + failure: { error }, + } = result; + return { + status: error instanceof SmartContractDataNotFoundError ? 404 : 500, + body: { + error: error.message, + }, + }; + } + + return { + status: 200, + body: output.toJSON(), + }; + } + + /** + * Converts the request data to the input format. + * @param {Request} request - The server's request. + * @returns {GetDacsInput} - The input data. + */ + public fromRequest( + request: Request + ): GetMSIGSInput { + const { + query: { dacId, limit }, + } = request; + return GetMSIGSInput.create(dacId, limit); + } +} diff --git a/src/endpoints/msigs/routes/msigs.route.ts b/src/endpoints/msigs/routes/msigs.route.ts new file mode 100644 index 00000000..8a7fb59e --- /dev/null +++ b/src/endpoints/msigs/routes/msigs.route.ts @@ -0,0 +1,51 @@ +import * as requestSchema from '@endpoints/dacs/schemas/dacs.request.schema.json'; +import { + GetRoute, + Request, + RouteHandler, + ValidationResult, +} from '@alien-worlds/aw-core'; +import { AjvValidator } from '@src/validator/ajv-validator'; +import { GetMSIGSRequestQueryParams } from '../data/dtos/msigs.dto'; +import { GetMSIGSRouteIO } from './msigs.route-io'; +import ApiConfig from '@src/config/api-config'; + +/** + * The `GetMSIGSRoute` class represents a route for fetching MSIGs data. + * It extends the `GetRoute` class, which provides the basic functionality of handling GET requests. + */ +export class GetMSIGSRoute extends GetRoute { + /** + * Creates a new instance of the `GetMSIGSRoute`. + * @param {RouteHandler} handler - The route handler. + * @param {ApiConfig} config - The API configuration. + */ + public static create(handler: RouteHandler, config: ApiConfig) { + return new GetMSIGSRoute(handler, config); + } + + /** + * Constructs a new instance of the `GetMSIGSRoute`. + * @param {RouteHandler} handler - The route handler. + * @param {ApiConfig} config - The API configuration. + */ + private constructor(handler: RouteHandler, config: ApiConfig) { + super(`/${config.urlVersion}/dao/msigs`, handler, { + io: new GetMSIGSRouteIO(), + validators: { + request: validateRequest, + }, + }); + } +} + +/** + * Validates the request data using the AjvValidator and the defined request schema. + * @param {Request} request - The server's request. + * @returns {ValidationResult} - The result of the validation. + */ +export const validateRequest = ( + request: Request +): ValidationResult => { + return AjvValidator.initialize().validateHttpRequest(requestSchema, request); +}; diff --git a/src/endpoints/msigs/schemas/dacs.request.schema.json b/src/endpoints/msigs/schemas/dacs.request.schema.json new file mode 100644 index 00000000..d0a2d1b8 --- /dev/null +++ b/src/endpoints/msigs/schemas/dacs.request.schema.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "msigs.request.schema.json", + "title": "/msigs request schema", + "type": "object", + "properties": { + "query": { + "type": "object", + "properties": { + "dacId": { + "$ref": "#/$defs/dacId" + }, + "limit": { + "$ref": "#/$defs/positiveInteger" + } + }, + "required": [] + } + }, + "$defs": { + "dacId": { + "type": "string", + "minLength": 5, + "maxLength": 6 + }, + "positiveInteger": { + "type": "integer", + "minimum": 1 + } + } +} diff --git a/src/endpoints/msigs/schemas/dacs.response.schema.json b/src/endpoints/msigs/schemas/dacs.response.schema.json new file mode 100644 index 00000000..5fe42f54 --- /dev/null +++ b/src/endpoints/msigs/schemas/dacs.response.schema.json @@ -0,0 +1,247 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "dacs.response.schema.json", + "title": "/dacs response schema", + "type": "object", + "required": ["results", "count"], + "properties": { + "results": { + "type": "array", + "minItems": 0, + "items": { + "type": "object", + "properties": { + "dacId": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "title": { + "type": "string" + }, + "isDacActive": { + "type": "boolean" + }, + "symbol": { + "type": "object", + "properties": { + "contract": { + "type": "string" + }, + "code": { + "type": "string" + }, + "precision": { + "type": "integer" + } + }, + "required": ["contract", "code", "precision"] + }, + "refs": { + "type": "object", + "properties": { + "logoUrl": { + "type": "string" + }, + "description": { + "type": "string" + }, + "rest": { + "type": "object", + "properties": { + "12": { + "type": "string" + } + } + } + }, + "required": ["logoUrl", "description"] + }, + "accounts": { + "type": "object", + "properties": { + "auth": { + "type": "string" + }, + "treasury": { + "type": "string" + }, + "custodian": { + "type": "string" + }, + "msigOwned": { + "type": "string" + }, + "voteWeight": { + "type": "string" + }, + "activation": { + "type": "string" + }, + "spendings": { + "type": "string" + } + }, + "required": [ + "auth", + "treasury", + "custodian", + "msigOwned", + "voteWeight", + "activation", + "spendings" + ] + }, + "dacTreasury": { + "type": "object", + "properties": { + "balance": { + "$ref": "#/$defs/balance" + } + }, + "required": ["balance"] + }, + "dacStats": { + "type": "object", + "properties": { + "supply": { + "type": "string" + }, + "maxSupply": { + "type": "string" + }, + "issuer": { + "type": "string" + }, + "transferLocked": { + "type": "boolean" + } + }, + "required": ["supply", "maxSupply", "issuer", "transferLocked"] + }, + "electionGlobals": { + "type": "object", + "properties": { + "authThresholdHigh": { + "type": "integer" + }, + "authThresholdLow": { + "type": "integer" + }, + "authThresholdMid": { + "type": "integer" + }, + "budgetPercentage": { + "type": "integer" + }, + "initialVoteQuorumPercent": { + "type": "integer" + }, + "lastClaimBudgetTime": { + "type": "string" + }, + "lastPeriodTime": { + "type": "string" + }, + "lockupReleaseTimeDelay": { + "type": "integer" + }, + "lockupAsset": { + "$ref": "#/$defs/asset" + }, + "maxVotes": { + "type": "integer" + }, + "metInitialVotesThreshold": { + "type": "integer" + }, + "numberActiveCandidates": { + "type": "integer" + }, + "numElected": { + "type": "integer" + }, + "periodLength": { + "type": "integer" + }, + "requestedPayMax": { + "$ref": "#/$defs/asset" + }, + "shouldPayViaServiceProvider": { + "type": "integer" + }, + "tokenSupplyTheshold": { + "type": "integer" + }, + "totalVotesOnCandidates": { + "type": "string" + }, + "totalWeightOfVotes": { + "type": "string" + }, + "voteQuorumPercent": { + "type": "integer" + } + }, + "required": [ + "authThresholdHigh", + "authThresholdLow", + "authThresholdMid", + "budgetPercentage", + "initialVoteQuorumPercent", + "lastClaimBudgetTime", + "lastPeriodTime", + "lockupReleaseTimeDelay", + "lockupAsset", + "maxVotes", + "metInitialVotesThreshold", + "numberActiveCandidates", + "numElected", + "periodLength", + "requestedPayMax", + "shouldPayViaServiceProvider", + "tokenSupplyTheshold", + "totalVotesOnCandidates", + "totalWeightOfVotes", + "voteQuorumPercent" + ] + } + }, + "required": [ + "dacId", + "owner", + "title", + "isDacActive", + "symbol", + "refs", + "accounts", + "dacTreasury", + "dacStats", + "electionGlobals" + ] + } + }, + "count": { + "type": "integer" + } + }, + "$defs": { + "balance": { + "type": "string", + "pattern": "^\\d+(\\.\\d+)? [A-Z]{3}" + }, + "asset": { + "type": "object", + "required": ["quantity", "contract"], + "properties": { + "quantity": { + "type": "string" + }, + "contract": { + "type": "string" + } + } + } + } +} diff --git a/src/endpoints/msigs/schemas/index.ts b/src/endpoints/msigs/schemas/index.ts new file mode 100644 index 00000000..d7f0244c --- /dev/null +++ b/src/endpoints/msigs/schemas/index.ts @@ -0,0 +1,4 @@ +import DacsRequestSchema from './dacs.request.schema.json'; +import DacsResponseSchema from './dacs.response.schema.json'; + +export { DacsRequestSchema, DacsResponseSchema }; diff --git a/src/routes.ts b/src/routes.ts index b1a09958..d1d91041 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -18,6 +18,8 @@ import { HealthController } from '@endpoints/health/domain/health.controller'; import { PingController } from '@endpoints/ping'; import { ProfileController } from '@endpoints/profile/domain/profile.controller'; import { VotingHistoryController } from '@endpoints/voting-history/domain/voting-history.controller'; +import { MSIGSController } from '@endpoints/msigs/domain/msigs.controller'; +import { GetMSIGSRoute } from '@endpoints/msigs/routes/msigs.route'; export const mountRoutes = ( api: DaoApi, @@ -45,6 +47,8 @@ export const mountRoutes = ( const custodiansController: CustodiansController = container.get(CustodiansController.Token); + const msigsController = container.get(MSIGSController.Token); + // Route.mount( @@ -109,4 +113,9 @@ export const mountRoutes = ( api.framework, GetPingRoute.create(pingController.ping.bind(pingController), config) ); + + Route.mount( + api.framework, + GetMSIGSRoute.create(msigsController.getMSIGS.bind(msigsController), config) + ); };