Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions compose/common.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions compose/placeholder-secrets/postgres_db_wallet_api
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
wallet_api
2 changes: 1 addition & 1 deletion packages/cardano-services/src/Program/options/postgres.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
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"');
}
}
4 changes: 3 additions & 1 deletion packages/cardano-services/src/Projection/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -53,5 +54,6 @@ export const migrations: ProjectionMigration[] = [
CurrentStakePollMetricsAttributesMigrations1698174358997,
PoolRewardsTableMigrations1698175956871,
HandleParentMigration1700556589063,
RewardsPledgeNumericMigration1715157190230
RewardsPledgeNumericMigration1715157190230,
GovernanceActionMigration1724168174191
];
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
BlockEntity,
CurrentPoolMetricsEntity,
DataSourceExtensions,
GovernanceActionEntity,
HandleEntity,
HandleMetadataEntity,
NftMetadataEntity,
Expand All @@ -22,6 +23,7 @@ import {
storeAddresses,
storeAssets,
storeBlock,
storeGovernanceAction,
storeHandleMetadata,
storeHandles,
storeNftMetadata,
Expand All @@ -32,6 +34,7 @@ import {
willStoreAddresses,
willStoreAssets,
willStoreBlockData,
willStoreGovernanceAction,
willStoreHandleMetadata,
willStoreHandles,
willStoreNftMetadata,
Expand All @@ -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',
Expand Down Expand Up @@ -112,6 +116,7 @@ export const storeOperators = {
storeAddresses: storeAddresses(),
storeAssets: storeAssets(),
storeBlock: storeBlock(),
storeGovernanceAction: storeGovernanceAction(),
storeHandleMetadata: storeHandleMetadata(),
storeHandles: storeHandles(),
storeNftMetadata: storeNftMetadata(),
Expand All @@ -138,6 +143,7 @@ type WillStore = {
const willStore: Partial<WillStore> = {
storeAddresses: willStoreAddresses,
storeAssets: willStoreAssets,
storeGovernanceAction: willStoreGovernanceAction,
storeHandleMetadata: willStoreHandleMetadata,
storeHandles: willStoreHandles,
storeNftMetadata: willStoreNftMetadata,
Expand All @@ -154,6 +160,7 @@ const entities = {
block: BlockEntity,
blockData: BlockDataEntity,
currentPoolMetrics: CurrentPoolMetricsEntity,
governanceAction: GovernanceActionEntity,
handle: HandleEntity,
handleMetadata: HandleMetadataEntity,
nftMetadata: NftMetadataEntity,
Expand All @@ -176,6 +183,7 @@ const storeEntities: Partial<Record<StoreName, EntityName[]>> = {
storeAddresses: ['address'],
storeAssets: ['asset'],
storeBlock: ['block', 'blockData'],
storeGovernanceAction: ['governanceAction'],
storeHandleMetadata: ['handleMetadata', 'output'],
storeHandles: ['handle', 'asset', 'tokens', 'output'],
storeNftMetadata: ['asset'],
Expand All @@ -194,6 +202,7 @@ const entityInterDependencies: Partial<Record<EntityName, EntityName[]>> = {
asset: ['block', 'nftMetadata'],
blockData: ['block'],
currentPoolMetrics: ['stakePool'],
governanceAction: ['block'],
handle: ['asset'],
handleMetadata: ['output'],
output: ['block', 'tokens'],
Expand Down Expand Up @@ -253,6 +262,7 @@ const storeMapperDependencies: Partial<Record<StoreName, MapperName[]>> = {
const storeInterDependencies: Partial<Record<StoreName, StoreName[]>> = {
storeAddresses: ['storeBlock', 'storeStakeKeyRegistrations'],
storeAssets: ['storeBlock'],
storeGovernanceAction: ['storeBlock'],
storeHandleMetadata: ['storeUtxo'],
storeHandles: ['storeUtxo', 'storeAddresses', 'storeHandleMetadata'],
storeNftMetadata: ['storeAssets'],
Expand All @@ -269,6 +279,7 @@ const projectionStoreDependencies: Record<ProjectionName, StoreName[]> = {
// 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'],
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -462,7 +464,7 @@ describe('PersonalWallet/conwayTransactions', () => {
__typename: GovernanceActionType.parameter_change_action,
governanceActionId: null,
policyHash: null,
protocolParamUpdate: { ...paramsUpdate }
protocolParamUpdate
},
rewardAccount
},
Expand Down
40 changes: 40 additions & 0 deletions packages/projection-typeorm/src/entity/GovernanceAction.entity.ts
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;
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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;
}
1 change: 1 addition & 0 deletions packages/projection-typeorm/src/entity/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions packages/projection-typeorm/src/operators/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
31 changes: 31 additions & 0 deletions packages/projection-typeorm/src/operators/storeGovernanceAction.ts
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 () => {
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!]);
});
});
Loading