-
Notifications
You must be signed in to change notification settings - Fork 63
LW-11005 Governance action proposals projection #1421
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| wallet_api |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<void> { | ||
| 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<void> { | ||
| await queryRunner.query('ALTER TABLE "governance_action" DROP CONSTRAINT "FK_governance_action_block_slot"'); | ||
| await queryRunner.query('DROP TABLE "governance_action"'); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need a worker job to fetch those? Is there a ticket?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought for a while to the ticket to open but I wasn't able neither to choose if we need a ticket for the implementation or ATM a spike... I propose to have first at least some discussions in one of our DS to check which targets we want to achieve and / or what we need. |
||
|
|
||
| @Column('char', { length: 64, nullable: true }) | ||
| anchorHash?: Hash32ByteBase16; | ||
|
|
||
| @Column(BigIntColumnOptions) | ||
| deposit?: bigint; | ||
|
|
||
| @Column({ transformer: [serializableObj, json], type: 'varchar' }) | ||
| action?: Cardano.GovernanceAction; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Mappers.WithGovernanceActions>) => | ||
| evt.governanceActions.length > 0; | ||
|
|
||
| export const storeGovernanceAction = typeormOperator<Mappers.WithGovernanceActions>(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); | ||
| } | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<WithTypeormContext>; | ||
|
|
||
| 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 () => { | ||
ginnun marked this conversation as resolved.
Show resolved
Hide resolved
mkazlauskas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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!]); | ||
| }); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.