diff --git a/compose/common.yml b/compose/common.yml index 5544f06b55a..b1c5d7d8144 100644 --- a/compose/common.yml +++ b/compose/common.yml @@ -44,6 +44,7 @@ x-with-postgres: &with-postgres - postgres_db_db_sync - postgres_db_handle - postgres_db_stake_pool + - postgres_db_wallet_api x-projector-environment: &projector-environment API_URL: http://0.0.0.0:3000 @@ -78,26 +79,32 @@ x-sdk-environment: &sdk-environment POSTGRES_DB_FILE_DB_SYNC: /run/secrets/postgres_db_db_sync POSTGRES_DB_FILE_HANDLE: /run/secrets/postgres_db_handle POSTGRES_DB_FILE_STAKE_POOL: /run/secrets/postgres_db_stake_pool + POSTGRES_DB_FILE_WALLET_API: /run/secrets/postgres_db_wallet_api POSTGRES_HOST_DB_SYNC: postgres POSTGRES_HOST_ASSET: postgres POSTGRES_HOST_HANDLE: postgres POSTGRES_HOST_STAKE_POOL: postgres + POSTGRES_HOST_WALLET_API: postgres POSTGRES_POOL_MAX_DB_SYNC: ${POSTGRES_POOL_MAX:-10} POSTGRES_POOL_MAX_HANDLE: ${POSTGRES_POOL_MAX:-10} POSTGRES_POOL_MAX_ASSET: ${POSTGRES_POOL_MAX:-10} POSTGRES_POOL_MAX_STAKE_POOL: ${POSTGRES_POOL_MAX:-10} + POSTGRES_POOL_MAX_WALLET_API: ${POSTGRES_POOL_MAX:-10} POSTGRES_PASSWORD_FILE_ASSET: /run/secrets/postgres_password POSTGRES_PASSWORD_FILE_DB_SYNC: /run/secrets/postgres_password POSTGRES_PASSWORD_FILE_HANDLE: /run/secrets/postgres_password POSTGRES_PASSWORD_FILE_STAKE_POOL: /run/secrets/postgres_password + POSTGRES_PASSWORD_FILE_WALLET_API: /run/secrets/postgres_password POSTGRES_PORT_DB_SYNC: 5432 POSTGRES_PORT_ASSET: 5432 POSTGRES_PORT_HANDLE: 5432 POSTGRES_PORT_STAKE_POOL: 5432 + POSTGRES_PORT_WALLET_API: 5432 POSTGRES_USER_FILE_ASSET: /run/secrets/postgres_user POSTGRES_USER_FILE_DB_SYNC: /run/secrets/postgres_user POSTGRES_USER_FILE_HANDLE: /run/secrets/postgres_user POSTGRES_USER_FILE_STAKE_POOL: /run/secrets/postgres_user + POSTGRES_USER_FILE_WALLET_API: /run/secrets/postgres_user TOKEN_METADATA_SERVER_URL: https://metadata.world.dev.cardano.org USE_WEB_SOCKET_API: true WEB_SOCKET_API_URL: ws://ws-server:3000/ws @@ -294,6 +301,21 @@ services: ports: - ${STAKE_POOL_PROJECTOR_PORT:-4002}:3000 + wallet-api-projector: + <<: + - *from-sdk + - *logging + - *projector + - *with-postgres + environment: + <<: + - *projector-environment + - *sdk-environment + POSTGRES_DB_FILE: /run/secrets/postgres_db_wallet_api + PROJECTION_NAMES: protocol-parameters + ports: + - ${WALLET_API_PROJECTOR_PORT:-4005}:3000 + provider-server: <<: - *from-sdk @@ -400,6 +422,8 @@ secrets: file: ../../compose/placeholder-secrets/postgres_db_handle postgres_db_stake_pool: file: ../../compose/placeholder-secrets/postgres_db_stake_pool + postgres_db_wallet_api: + file: ../../compose/placeholder-secrets/postgres_db_wallet_api postgres_password: file: ../../compose/placeholder-secrets/postgres_password postgres_user: diff --git a/compose/placeholder-secrets/postgres_db_wallet_api b/compose/placeholder-secrets/postgres_db_wallet_api new file mode 100644 index 00000000000..c24b91180d2 --- /dev/null +++ b/compose/placeholder-secrets/postgres_db_wallet_api @@ -0,0 +1 @@ +wallet_api \ No newline at end of file diff --git a/packages/cardano-services/src/Program/options/postgres.ts b/packages/cardano-services/src/Program/options/postgres.ts index b731493c8c3..0d9927d66cd 100644 --- a/packages/cardano-services/src/Program/options/postgres.ts +++ b/packages/cardano-services/src/Program/options/postgres.ts @@ -33,7 +33,7 @@ export interface BasePosgresProgramOptions { postgresSslCaFile?: string; } -export type ConnectionNames = 'DbSync' | 'Handle' | 'StakePool' | 'Asset' | ''; +export type ConnectionNames = 'Asset' | 'DbSync' | 'Handle' | 'StakePool' | 'WalletApi' | ''; export type PosgresProgramOptions< Suffix extends ConnectionNames, diff --git a/packages/cardano-services/src/Projection/migrations/1724168174191-governance-actions-table.ts b/packages/cardano-services/src/Projection/migrations/1724168174191-governance-actions-table.ts new file mode 100644 index 00000000000..a8a138f539c --- /dev/null +++ b/packages/cardano-services/src/Projection/migrations/1724168174191-governance-actions-table.ts @@ -0,0 +1,20 @@ +import { GovernanceActionEntity } from '@cardano-sdk/projection-typeorm'; +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class GovernanceActionMigration1724168174191 implements MigrationInterface { + static entity = GovernanceActionEntity; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'CREATE TABLE "governance_action" ("id" SERIAL NOT NULL, "tx_id" character varying NOT NULL, "index" smallint NOT NULL, "stake_credential_hash" character varying NOT NULL, "anchor_url" character varying NOT NULL, "anchor_hash" character(64), "deposit" bigint NOT NULL, "action" character varying NOT NULL, "block_slot" integer NOT NULL, CONSTRAINT "PK_governance_action_id" PRIMARY KEY ("id"))' + ); + await queryRunner.query( + 'ALTER TABLE "governance_action" ADD CONSTRAINT "FK_governance_action_block_slot" FOREIGN KEY ("block_slot") REFERENCES "block"("slot") ON DELETE CASCADE ON UPDATE NO ACTION' + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE "governance_action" DROP CONSTRAINT "FK_governance_action_block_slot"'); + await queryRunner.query('DROP TABLE "governance_action"'); + } +} diff --git a/packages/cardano-services/src/Projection/migrations/index.ts b/packages/cardano-services/src/Projection/migrations/index.ts index 004df404881..bf0a418da26 100644 --- a/packages/cardano-services/src/Projection/migrations/index.ts +++ b/packages/cardano-services/src/Projection/migrations/index.ts @@ -6,6 +6,7 @@ import { CostPledgeNumericMigration1689091319930 } from './1689091319930-cost-pl import { CurrentStakePollMetricsAttributesMigrations1698174358997 } from './1698174358997-current-pool-metrics-attributes'; import { FkPoolRegistrationMigration1682519108369 } from './1682519108369-fk-pool-registration'; import { FkPoolRetirementMigration1682519108370 } from './1682519108370-fk-pool-retirement'; +import { GovernanceActionMigration1724168174191 } from './1724168174191-governance-actions-table'; import { HandleDefaultMigrations1693830294136 } from './1693830294136-handle-default-columns'; import { HandleMetadataTableMigrations1693490983715 } from './1693490983715-handle-metadata-table'; import { HandleParentMigration1700556589063 } from './1700556589063-handle-parent'; @@ -53,5 +54,6 @@ export const migrations: ProjectionMigration[] = [ CurrentStakePollMetricsAttributesMigrations1698174358997, PoolRewardsTableMigrations1698175956871, HandleParentMigration1700556589063, - RewardsPledgeNumericMigration1715157190230 + RewardsPledgeNumericMigration1715157190230, + GovernanceActionMigration1724168174191 ]; diff --git a/packages/cardano-services/src/Projection/prepareTypeormProjection.ts b/packages/cardano-services/src/Projection/prepareTypeormProjection.ts index a00f8d050c5..a7f7912eb21 100644 --- a/packages/cardano-services/src/Projection/prepareTypeormProjection.ts +++ b/packages/cardano-services/src/Projection/prepareTypeormProjection.ts @@ -5,6 +5,7 @@ import { BlockEntity, CurrentPoolMetricsEntity, DataSourceExtensions, + GovernanceActionEntity, HandleEntity, HandleMetadataEntity, NftMetadataEntity, @@ -22,6 +23,7 @@ import { storeAddresses, storeAssets, storeBlock, + storeGovernanceAction, storeHandleMetadata, storeHandles, storeNftMetadata, @@ -32,6 +34,7 @@ import { willStoreAddresses, willStoreAssets, willStoreBlockData, + willStoreGovernanceAction, willStoreHandleMetadata, willStoreHandles, willStoreNftMetadata, @@ -53,6 +56,7 @@ export enum ProjectionName { Address = 'address', Asset = 'asset', Handle = 'handle', + ProtocolParameters = 'protocol-parameters', StakePool = 'stake-pool', StakePoolMetadataJob = 'stake-pool-metadata-job', StakePoolMetricsJob = 'stake-pool-metrics-job', @@ -112,6 +116,7 @@ export const storeOperators = { storeAddresses: storeAddresses(), storeAssets: storeAssets(), storeBlock: storeBlock(), + storeGovernanceAction: storeGovernanceAction(), storeHandleMetadata: storeHandleMetadata(), storeHandles: storeHandles(), storeNftMetadata: storeNftMetadata(), @@ -138,6 +143,7 @@ type WillStore = { const willStore: Partial = { storeAddresses: willStoreAddresses, storeAssets: willStoreAssets, + storeGovernanceAction: willStoreGovernanceAction, storeHandleMetadata: willStoreHandleMetadata, storeHandles: willStoreHandles, storeNftMetadata: willStoreNftMetadata, @@ -154,6 +160,7 @@ const entities = { block: BlockEntity, blockData: BlockDataEntity, currentPoolMetrics: CurrentPoolMetricsEntity, + governanceAction: GovernanceActionEntity, handle: HandleEntity, handleMetadata: HandleMetadataEntity, nftMetadata: NftMetadataEntity, @@ -176,6 +183,7 @@ const storeEntities: Partial> = { storeAddresses: ['address'], storeAssets: ['asset'], storeBlock: ['block', 'blockData'], + storeGovernanceAction: ['governanceAction'], storeHandleMetadata: ['handleMetadata', 'output'], storeHandles: ['handle', 'asset', 'tokens', 'output'], storeNftMetadata: ['asset'], @@ -194,6 +202,7 @@ const entityInterDependencies: Partial> = { asset: ['block', 'nftMetadata'], blockData: ['block'], currentPoolMetrics: ['stakePool'], + governanceAction: ['block'], handle: ['asset'], handleMetadata: ['output'], output: ['block', 'tokens'], @@ -253,6 +262,7 @@ const storeMapperDependencies: Partial> = { const storeInterDependencies: Partial> = { storeAddresses: ['storeBlock', 'storeStakeKeyRegistrations'], storeAssets: ['storeBlock'], + storeGovernanceAction: ['storeBlock'], storeHandleMetadata: ['storeUtxo'], storeHandles: ['storeUtxo', 'storeAddresses', 'storeHandleMetadata'], storeNftMetadata: ['storeAssets'], @@ -269,6 +279,7 @@ const projectionStoreDependencies: Record = { // TODO: remove storeNftMetadata when TypeormAssetProvider tests // are updated to use 'asset' database instead of a handle database handle: ['storeHandles', 'storeHandleMetadata', 'storeNftMetadata'], + 'protocol-parameters': ['storeGovernanceAction'], 'stake-pool': ['storeStakePools'], 'stake-pool-metadata-job': ['storeStakePoolMetadataJob'], 'stake-pool-metrics-job': ['storePoolMetricsUpdateJob'], diff --git a/packages/e2e/test/wallet_epoch_3/PersonalWallet/conwayTransactions.test.ts b/packages/e2e/test/wallet_epoch_3/PersonalWallet/conwayTransactions.test.ts index aefe59ec162..9efbe6a57c6 100644 --- a/packages/e2e/test/wallet_epoch_3/PersonalWallet/conwayTransactions.test.ts +++ b/packages/e2e/test/wallet_epoch_3/PersonalWallet/conwayTransactions.test.ts @@ -1,3 +1,5 @@ +// cSpell:ignore costmdls vasil + import * as Crypto from '@cardano-sdk/crypto'; import { BaseWallet } from '@cardano-sdk/wallet'; import { Cardano, setInConwayEra } from '@cardano-sdk/core'; @@ -82,7 +84,7 @@ export const vasilPlutusV2Costmdls = [ 32_696, 32, 43_357, 32, 32_247, 32, 38_314, 32, 35_892_428, 10, 57_996_947, 18_975, 10, 38_887_044, 32_947, 10 ]; -export const paramsUpdate: Cardano.ProtocolParametersUpdateConway = { +export const protocolParamUpdate: Cardano.ProtocolParametersUpdateConway = { coinsPerUtxoByte: 35_000, collateralPercentage: 852, committeeTermLimit: Cardano.EpochNo(200), @@ -462,7 +464,7 @@ describe('PersonalWallet/conwayTransactions', () => { __typename: GovernanceActionType.parameter_change_action, governanceActionId: null, policyHash: null, - protocolParamUpdate: { ...paramsUpdate } + protocolParamUpdate }, rewardAccount }, diff --git a/packages/projection-typeorm/src/entity/GovernanceAction.entity.ts b/packages/projection-typeorm/src/entity/GovernanceAction.entity.ts new file mode 100644 index 00000000000..8515d5fe290 --- /dev/null +++ b/packages/projection-typeorm/src/entity/GovernanceAction.entity.ts @@ -0,0 +1,40 @@ +import { BigIntColumnOptions, OnDeleteCascadeRelationOptions } from './util'; +import { BlockEntity } from './Block.entity'; +import { Cardano } from '@cardano-sdk/core'; +import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { Hash28ByteBase16, Hash32ByteBase16 } from '@cardano-sdk/crypto'; +import { json, serializableObj } from './transformers'; + +@Entity() +export class GovernanceActionEntity { + @PrimaryGeneratedColumn() + id?: number; + + // LW-11270 + // This is required to handle rollbacks, once we have transactions projection + // the OnDeleteCascadeRelationOptions can be moved on txId and this column could be removed + @ManyToOne(() => BlockEntity, OnDeleteCascadeRelationOptions) + @JoinColumn() + block?: BlockEntity; + + @Column('varchar') + txId?: Cardano.TransactionId; + + @Column('smallint') + index?: number; + + @Column('varchar') + stakeCredentialHash?: Hash28ByteBase16; + + @Column('varchar') + anchorUrl?: string; + + @Column('char', { length: 64, nullable: true }) + anchorHash?: Hash32ByteBase16; + + @Column(BigIntColumnOptions) + deposit?: bigint; + + @Column({ transformer: [serializableObj, json], type: 'varchar' }) + action?: Cardano.GovernanceAction; +} diff --git a/packages/projection-typeorm/src/entity/index.ts b/packages/projection-typeorm/src/entity/index.ts index d7bfef6aea6..a5abf1a2940 100644 --- a/packages/projection-typeorm/src/entity/index.ts +++ b/packages/projection-typeorm/src/entity/index.ts @@ -7,6 +7,7 @@ export * from './Handle.entity'; export * from './HandleMetadata.entity'; export * from './NftMetadata.entity'; export * from './Output.entity'; +export * from './GovernanceAction.entity'; export * from './PoolDelisted.entity'; export * from './PoolMetadata.entity'; export * from './PoolRegistration.entity'; diff --git a/packages/projection-typeorm/src/operators/index.ts b/packages/projection-typeorm/src/operators/index.ts index def7690f9b8..f0e7c6394cd 100644 --- a/packages/projection-typeorm/src/operators/index.ts +++ b/packages/projection-typeorm/src/operators/index.ts @@ -1,6 +1,7 @@ export * from './storeAddresses'; export * from './storeAssets'; export * from './storeBlock'; +export * from './storeGovernanceAction'; export * from './storeHandles'; export * from './storeHandleMetadata'; export * from './storeNftMetadata'; diff --git a/packages/projection-typeorm/src/operators/storeGovernanceAction.ts b/packages/projection-typeorm/src/operators/storeGovernanceAction.ts new file mode 100644 index 00000000000..69296199623 --- /dev/null +++ b/packages/projection-typeorm/src/operators/storeGovernanceAction.ts @@ -0,0 +1,31 @@ +import { Cardano, ChainSyncEventType } from '@cardano-sdk/core'; +import { GovernanceActionEntity } from '../entity'; +import { Mappers, ProjectionEvent } from '@cardano-sdk/projection'; +import { typeormOperator } from './util'; + +export const willStoreGovernanceAction = (evt: ProjectionEvent) => + evt.governanceActions.length > 0; + +export const storeGovernanceAction = typeormOperator(async (evt) => { + if (evt.eventType === ChainSyncEventType.RollBackward || !willStoreGovernanceAction(evt)) return; + + const { governanceActions, queryRunner } = evt; + const repository = queryRunner.manager.getRepository(GovernanceActionEntity); + + for (const { action, index, slot } of governanceActions) { + const { anchor, deposit, governanceAction, rewardAccount } = action; + const { actionIndex, id } = index; + const actionEntity = repository.create({ + action: governanceAction, + anchorHash: anchor.dataHash, + anchorUrl: anchor.url, + block: { slot }, + deposit, + index: actionIndex, + stakeCredentialHash: Cardano.Address.fromString(rewardAccount)!.asReward()!.getPaymentCredential().hash, + txId: id + }); + + await repository.insert(actionEntity); + } +}); diff --git a/packages/projection-typeorm/test/operators/storeGovernanceAction.test.ts b/packages/projection-typeorm/test/operators/storeGovernanceAction.test.ts new file mode 100644 index 00000000000..1ae089e816c --- /dev/null +++ b/packages/projection-typeorm/test/operators/storeGovernanceAction.test.ts @@ -0,0 +1,74 @@ +import { BlockEntity, GovernanceActionEntity } from '../../src'; +import { Cardano, ChainSyncEventType } from '@cardano-sdk/core'; +import { DataSource, QueryRunner } from 'typeorm'; +import { Mappers, ProjectionEvent } from '@cardano-sdk/projection'; +import { WithTypeormContext, storeBlock, storeGovernanceAction } from '../../src/operators'; +import { firstValueFrom, of } from 'rxjs'; +import { initializeDataSource } from '../util'; + +describe('storeGovernanceAction', () => { + let dataSource: DataSource; + let queryRunner: QueryRunner; + + const proposals = [ + { + __typename: Cardano.GovernanceActionType.hard_fork_initiation_action, + governanceActionId: null, + protocolVersion: { major: 11, minor: 0 } + }, + { __typename: Cardano.GovernanceActionType.info_action }, + { + __typename: Cardano.GovernanceActionType.parameter_change_action, + governanceActionId: null, + policyHash: null, + protocolParamUpdate: { maxBlockHeaderSize: 500, maxCollateralInputs: 100 } + } + ] as const; + + const processEvent = () => { + const anchor = { + dataHash: '3e33018e8293d319ef5b3ac72366dd28006bd315b715f7e7cfcbd3004129b80d', + url: 'https://testing.this' + }; + const deposit = 1_000_000n; + // cSpell:disable-next-line + const rewardAccount = 'stake_test1urc4mvzl2cp4gedl3yq2px7659krmzuzgnl2dpjjgsydmqqxgamj7'; + + const proposal = { anchor, deposit, rewardAccount }; + + const tx = { + body: { proposalProcedures: proposals.map((action) => ({ ...proposal, governanceAction: action })) }, + id: '5de144891eb542ef71ac75dec2265cfd0a292c8a3eb35c16591d9a7b865f48e5' + }; + const evt = { + block: { + body: [tx], + header: { blockNo: 467, hash: '69fff584eb85e83d7decd97331310b59f087a4e648555c5d1f65c6d62ff4cc45', slot: 4984 } + }, + eventType: ChainSyncEventType.RollForward, + queryRunner + } as unknown as ProjectionEvent; + + return firstValueFrom(of(evt).pipe(Mappers.withGovernanceActions(), storeBlock(), storeGovernanceAction())); + }; + + beforeEach(async () => { + dataSource = await initializeDataSource({ entities: [BlockEntity, GovernanceActionEntity] }); + queryRunner = dataSource.createQueryRunner(); + }); + + afterEach(async () => { + await queryRunner.release(); + await dataSource.destroy(); + }); + + it('inserts governance action proposals', async () => { + await processEvent(); + + expect(await queryRunner.manager.count(GovernanceActionEntity)).toBe(3); + + const savedActions = await queryRunner.manager.getRepository(GovernanceActionEntity).find(); + + for (const { action, index } of savedActions) expect(action).toEqual(proposals[index!]); + }); +}); diff --git a/packages/projection/src/operators/Mappers/index.ts b/packages/projection/src/operators/Mappers/index.ts index 7f23f8d8de2..854ef8626fd 100644 --- a/packages/projection/src/operators/Mappers/index.ts +++ b/packages/projection/src/operators/Mappers/index.ts @@ -1,6 +1,7 @@ export * from './certificates'; export * from './withUtxo'; export * from './withMint'; +export * from './withGovernanceActions'; export * from './withHandles'; export * from './withHandleMetadata'; export * from './withNftMetadata'; diff --git a/packages/projection/src/operators/Mappers/withGovernanceActions.ts b/packages/projection/src/operators/Mappers/withGovernanceActions.ts new file mode 100644 index 00000000000..97ce2df8ce2 --- /dev/null +++ b/packages/projection/src/operators/Mappers/withGovernanceActions.ts @@ -0,0 +1,24 @@ +import { Cardano } from '@cardano-sdk/core'; +import { unifiedProjectorOperator } from '../utils'; + +export type WithGovernanceActions = { + governanceActions: { + action: Cardano.ProposalProcedure; + index: Cardano.GovernanceActionId; + // LW-11270 + // This is required to handle rollbacks, once we have transactions projection this could be removed + slot: Cardano.Slot; + }[]; +}; + +export const withGovernanceActions = unifiedProjectorOperator<{}, WithGovernanceActions>((evt) => { + const { body, header } = evt.block; + const governanceActions: WithGovernanceActions['governanceActions'] = []; + + for (const tx of body) + if (tx.body.proposalProcedures) + for (const [actionIndex, action] of tx.body.proposalProcedures.entries()) + governanceActions.push({ action, index: { actionIndex, id: tx.id }, slot: header.slot }); + + return { ...evt, governanceActions }; +}); diff --git a/packages/projection/src/types.ts b/packages/projection/src/types.ts index 5a539be6291..fa7bf2db3c1 100644 --- a/packages/projection/src/types.ts +++ b/packages/projection/src/types.ts @@ -60,7 +60,7 @@ export type BootstrapExtraProps = WithNetworkInfo & WithEpochNo & WithEpochBound * These events are very similar to Chain Sync events, but there are some important differences: * * 1. Block format is compatible with types from `@cardano-sdk/core` package. - * 2. Events include some additional properties: `{eraSumaries, genesisParameters, epochNo, crossEpochBoundary}`. + * 2. Events include some additional properties: `{eraSummaries, genesisParameters, epochNo, crossEpochBoundary}`. * 3. `RollBackward` events include block data (instead of just specifying the rollback point), * and are emitted once for **each** rolled back block. * diff --git a/packages/projection/test/operators/Mappers/withGovernanceActions.test.ts b/packages/projection/test/operators/Mappers/withGovernanceActions.test.ts new file mode 100644 index 00000000000..4437a639ccc --- /dev/null +++ b/packages/projection/test/operators/Mappers/withGovernanceActions.test.ts @@ -0,0 +1,66 @@ +import { Cardano } from '@cardano-sdk/core'; +import { Mappers, ProjectionEvent } from '../../../src'; +import { firstValueFrom, of } from 'rxjs'; +import { toSerializableObject } from '@cardano-sdk/util'; + +describe('withGovernanceActions', () => { + const anchor = { + dataHash: '3e33018e8293d319ef5b3ac72366dd28006bd315b715f7e7cfcbd3004129b80d', + url: 'https://testing.this' + }; + const deposit = 1_000_000n; + // cSpell:disable-next-line + const rewardAccount = 'stake_test1urc4mvzl2cp4gedl3yq2px7659krmzuzgnl2dpjjgsydmqqxgamj7'; + + const proposal = { anchor, deposit, rewardAccount }; + + const proposals = [ + { + __typename: Cardano.GovernanceActionType.hard_fork_initiation_action, + governanceActionId: null, + protocolVersion: { major: 11, minor: 0 } + }, + { __typename: Cardano.GovernanceActionType.info_action }, + { + __typename: Cardano.GovernanceActionType.parameter_change_action, + governanceActionId: null, + policyHash: null, + protocolParamUpdate: { maxBlockHeaderSize: 500, maxCollateralInputs: 100 } + } + ] as const; + + const txId1 = '5de144891eb542ef71ac75dec2265cfd0a292c8a3eb35c16591d9a7b865f48e5'; + const txId2 = '5de144891eb542ef71ac75dec2265cfd0a292c8a3eb35c16591d9a7b865f48e6'; + + const source$ = of({ + block: { + body: [ + { + body: { + proposalProcedures: [ + { ...proposal, governanceAction: proposals[0] }, + { ...proposal, governanceAction: proposals[2] } + ] + }, + id: txId1 + }, + { + body: { proposalProcedures: [{ ...proposal, governanceAction: proposals[1] }] }, + id: txId2 + } + ], + header: { slot: 4984 } + } + } as ProjectionEvent); + + it('maps all governance actions into a flat array', async () => { + const { governanceActions } = await firstValueFrom(source$.pipe(Mappers.withGovernanceActions())); + expect(toSerializableObject(governanceActions)).toEqual( + toSerializableObject([ + { action: { ...proposal, governanceAction: proposals[0] }, index: { actionIndex: 0, id: txId1 }, slot: 4984 }, + { action: { ...proposal, governanceAction: proposals[2] }, index: { actionIndex: 1, id: txId1 }, slot: 4984 }, + { action: { ...proposal, governanceAction: proposals[1] }, index: { actionIndex: 0, id: txId2 }, slot: 4984 } + ]) + ); + }); +});