Skip to content

Commit a85b1b0

Browse files
committed
feat: add address projection
- new mappers: withStakeKeyRegistrations, withAddresses - new stores: storeStakeKeyRegistrations, storeAddresses - new projection type/name in cardano-services: 'address'
1 parent 1146256 commit a85b1b0

File tree

18 files changed

+744
-1
lines changed

18 files changed

+744
-1
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { AddressEntity } from '@cardano-sdk/projection-typeorm';
2+
import { MigrationInterface, QueryRunner } from 'typeorm';
3+
4+
export class AddressTableMigrations1690955710125 implements MigrationInterface {
5+
static entity = AddressEntity;
6+
7+
public async up(queryRunner: QueryRunner): Promise<void> {
8+
await queryRunner.query(
9+
"CREATE TYPE \"public\".\"address_type_enum\" AS ENUM('0', '1', '2', '3', '4', '5', '6', '7', '8', '14', '15')"
10+
);
11+
await queryRunner.query(
12+
'CREATE TABLE "address" ("address" character varying NOT NULL, "type" "public"."address_type_enum" NOT NULL, "payment_credential_hash" character(56), "stake_credential_hash" character(56), CONSTRAINT "PK_address_address" PRIMARY KEY ("address"))'
13+
);
14+
await queryRunner.query(
15+
'CREATE INDEX "IDX_address_payment_credential_hash" ON "address" ("payment_credential_hash") '
16+
);
17+
await queryRunner.query('CREATE INDEX "IDX_address_stake_credential_hash" ON "address" ("stake_credential_hash") ');
18+
}
19+
20+
public async down(queryRunner: QueryRunner): Promise<void> {
21+
await queryRunner.query('DROP INDEX "public"."IDX_address_stake_credential_hash"');
22+
await queryRunner.query('DROP INDEX "public"."IDX_address_payment_credential_hash"');
23+
await queryRunner.query('DROP TABLE "address"');
24+
await queryRunner.query('DROP TYPE "public"."address_type_enum"');
25+
}
26+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { MigrationInterface, QueryRunner } from 'typeorm';
2+
import { StakeKeyRegistrationEntity } from '@cardano-sdk/projection-typeorm';
3+
4+
export class StakeKeyRegistrationsTableMigrations1690964880195 implements MigrationInterface {
5+
static entity = StakeKeyRegistrationEntity;
6+
7+
public async up(queryRunner: QueryRunner): Promise<void> {
8+
await queryRunner.query(
9+
'CREATE TABLE "stake_key_registration" ("id" bigint NOT NULL, "stake_key_hash" character(56) NOT NULL, "block_slot" integer NOT NULL, CONSTRAINT "PK_stake_key_registration_id" PRIMARY KEY ("id"))'
10+
);
11+
await queryRunner.query(
12+
'CREATE INDEX "IDX_stake_key_registration_stake_key_hash" ON "stake_key_registration" ("stake_key_hash") '
13+
);
14+
await queryRunner.query(
15+
'ALTER TABLE "stake_key_registration" ADD CONSTRAINT "FK_stake_key_registration_block_slot" FOREIGN KEY ("block_slot") REFERENCES "block"("slot") ON DELETE CASCADE ON UPDATE NO ACTION'
16+
);
17+
18+
await queryRunner.query('ALTER TABLE "address" ADD "registration_id" bigint');
19+
await queryRunner.query(
20+
'ALTER TABLE "address" ADD CONSTRAINT "FK_address_registration_id" FOREIGN KEY ("registration_id") REFERENCES "stake_key_registration"("id") ON DELETE CASCADE ON UPDATE NO ACTION'
21+
);
22+
}
23+
24+
public async down(queryRunner: QueryRunner): Promise<void> {
25+
await queryRunner.query('ALTER TABLE "address" DROP CONSTRAINT "FK_address_registration_id"');
26+
await queryRunner.query('ALTER TABLE "address" DROP COLUMN "registration_id"');
27+
28+
await queryRunner.query(
29+
'ALTER TABLE "stake_key_registration" DROP CONSTRAINT "FK_stake_key_registration_block_slot"'
30+
);
31+
await queryRunner.query('DROP INDEX "public"."IDX_stake_key_registration_stake_key_hash"');
32+
await queryRunner.query('DROP TABLE "stake_key_registration"');
33+
}
34+
}

packages/cardano-services/src/Projection/migrations/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { AddressTableMigrations1690955710125 } from './1690955710125-address-table';
12
import { AssetTableMigration1682519108365 } from './1682519108365-asset-table';
23
import { BlockDataTableMigration1682519108359 } from './1682519108359-block-data-table';
34
import { BlockTableMigration1682519108358 } from './1682519108358-block-table';
@@ -11,6 +12,7 @@ import { PoolMetadataTableMigration1682519108363 } from './1682519108363-pool-me
1112
import { PoolMetricsMigrations1685011799580 } from './1685011799580-stake-pool-metrics-table';
1213
import { PoolRegistrationTableMigration1682519108360 } from './1682519108360-pool-registration-table';
1314
import { PoolRetirementTableMigration1682519108361 } from './1682519108361-pool-retirement-table';
15+
import { StakeKeyRegistrationsTableMigrations1690964880195 } from './1690964880195-stake-key-registrations-table';
1416
import { StakePoolTableMigration1682519108362 } from './1682519108362-stake-pool-table';
1517
import { TokensQuantityNumericMigrations1691042603934 } from './1691042603934-tokens-quantity-numeric';
1618
import { TokensTableMigration1682519108368 } from './1682519108368-tokens-table';
@@ -35,5 +37,7 @@ export const migrations: ProjectionMigration[] = [
3537
HandleTableMigration1686138943349,
3638
CostPledgeNumericMigration1689091319930,
3739
NftMetadataTableMigration1690269355640,
40+
AddressTableMigrations1690955710125,
41+
StakeKeyRegistrationsTableMigrations1690964880195,
3842
TokensQuantityNumericMigrations1691042603934
3943
];

packages/cardano-services/src/Projection/prepareTypeormProjection.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
AddressEntity,
23
AssetEntity,
34
BlockDataEntity,
45
BlockEntity,
@@ -10,14 +11,17 @@ import {
1011
PoolMetadataEntity,
1112
PoolRegistrationEntity,
1213
PoolRetirementEntity,
14+
StakeKeyRegistrationEntity,
1315
StakePoolEntity,
1416
TokensEntity,
1517
TypeormStabilityWindowBuffer,
1618
createStorePoolMetricsUpdateJob,
19+
storeAddresses,
1720
storeAssets,
1821
storeBlock,
1922
storeHandles,
2023
storeNftMetadata,
24+
storeStakeKeyRegistrations,
2125
storeStakePoolMetadataJob,
2226
storeStakePools,
2327
storeUtxo
@@ -38,7 +42,8 @@ export enum ProjectionName {
3842
StakePool = 'stake-pool',
3943
StakePoolMetadataJob = 'stake-pool-metadata-job',
4044
StakePoolMetricsJob = 'stake-pool-metrics-job',
41-
UTXO = 'utxo'
45+
UTXO = 'utxo',
46+
Address = 'address'
4247
}
4348

4449
export interface ProjectionOptions {
@@ -65,11 +70,13 @@ const createMapperOperators = (
6570
return {
6671
filterMint,
6772
filterUtxo,
73+
withAddresses: Mapper.withAddresses(),
6874
withCIP67: Mapper.withCIP67(),
6975
withCertificates: Mapper.withCertificates(),
7076
withHandles,
7177
withMint: Mapper.withMint(),
7278
withNftMetadata: Mapper.withNftMetadata({ logger }),
79+
withStakeKeyRegistrations: Mapper.withStakeKeyRegistrations(),
7380
withStakePools: Mapper.withStakePools(),
7481
withUtxo: Mapper.withUtxo()
7582
};
@@ -79,11 +86,13 @@ type MapperName = keyof MapperOperators;
7986
type MapperOperator = MapperOperators[MapperName];
8087

8188
export const storeOperators = {
89+
storeAddresses: storeAddresses(),
8290
storeAssets: storeAssets(),
8391
storeBlock: storeBlock(),
8492
storeHandles: storeHandles(),
8593
storeNftMetadata: storeNftMetadata(),
8694
storePoolMetricsUpdateJob: createStorePoolMetricsUpdateJob(POOLS_METRICS_INTERVAL_DEFAULT)(),
95+
storeStakeKeyRegistrations: storeStakeKeyRegistrations(),
8796
storeStakePoolMetadataJob: storeStakePoolMetadataJob(),
8897
storeStakePools: storeStakePools(),
8998
storeUtxo: storeUtxo()
@@ -93,6 +102,7 @@ type StoreName = keyof StoreOperators;
93102
type StoreOperator = StoreOperators[StoreName];
94103

95104
const entities = {
105+
address: AddressEntity,
96106
asset: AssetEntity,
97107
block: BlockEntity,
98108
blockData: BlockDataEntity,
@@ -103,6 +113,7 @@ const entities = {
103113
poolMetadata: PoolMetadataEntity,
104114
poolRegistration: PoolRegistrationEntity,
105115
poolRetirement: PoolRetirementEntity,
116+
stakeKeyRegistration: StakeKeyRegistrationEntity,
106117
stakePool: StakePoolEntity,
107118
tokens: TokensEntity
108119
};
@@ -112,17 +123,20 @@ type EntityName = keyof Entities;
112123
type Entity = Entities[EntityName];
113124

114125
const storeEntities: Partial<Record<StoreName, EntityName[]>> = {
126+
storeAddresses: ['address'],
115127
storeAssets: ['asset'],
116128
storeBlock: ['block'],
117129
storeHandles: ['handle', 'asset', 'tokens', 'output'],
118130
storeNftMetadata: ['asset'],
119131
storePoolMetricsUpdateJob: ['stakePool', 'currentPoolMetrics', 'poolMetadata'],
132+
storeStakeKeyRegistrations: ['block', 'stakeKeyRegistration'],
120133
storeStakePoolMetadataJob: ['stakePool', 'currentPoolMetrics', 'poolMetadata'],
121134
storeStakePools: ['stakePool', 'currentPoolMetrics', 'poolMetadata'],
122135
storeUtxo: ['tokens', 'output']
123136
};
124137

125138
const entityInterDependencies: Partial<Record<EntityName, EntityName[]>> = {
139+
address: ['stakeKeyRegistration'],
126140
asset: ['block', 'nftMetadata'],
127141
blockData: ['block'],
128142
currentPoolMetrics: ['stakePool'],
@@ -131,6 +145,7 @@ const entityInterDependencies: Partial<Record<EntityName, EntityName[]>> = {
131145
poolMetadata: ['stakePool'],
132146
poolRegistration: ['block'],
133147
poolRetirement: ['block'],
148+
stakeKeyRegistration: ['block'],
134149
stakePool: ['block', 'poolRegistration', 'poolRetirement'],
135150
tokens: ['asset']
136151
};
@@ -158,22 +173,27 @@ export const getEntities = (entityNames: EntityName[]): Entity[] => {
158173
const mapperInterDependencies: Partial<Record<MapperName, MapperName[]>> = {
159174
filterMint: ['withMint'],
160175
filterUtxo: ['withUtxo'],
176+
withAddresses: ['withUtxo'],
161177
withCIP67: ['withUtxo'],
162178
withHandles: ['withMint', 'filterMint', 'withUtxo', 'filterUtxo'],
163179
withNftMetadata: ['withCIP67', 'withMint'],
180+
withStakeKeyRegistrations: ['withCertificates'],
164181
withStakePools: ['withCertificates']
165182
};
166183

167184
const storeMapperDependencies: Partial<Record<StoreName, MapperName[]>> = {
185+
storeAddresses: ['withAddresses'],
168186
storeAssets: ['withMint'],
169187
storeHandles: ['withHandles'],
170188
storeNftMetadata: ['withNftMetadata'],
189+
storeStakeKeyRegistrations: ['withStakeKeyRegistrations'],
171190
storeStakePoolMetadataJob: ['withStakePools'],
172191
storeStakePools: ['withStakePools'],
173192
storeUtxo: ['withUtxo']
174193
};
175194

176195
const storeInterDependencies: Partial<Record<StoreName, StoreName[]>> = {
196+
storeAddresses: ['storeStakeKeyRegistrations'],
177197
storeAssets: ['storeBlock'],
178198
storeHandles: ['storeUtxo'],
179199
storeNftMetadata: ['storeAssets'],
@@ -184,6 +204,7 @@ const storeInterDependencies: Partial<Record<StoreName, StoreName[]>> = {
184204
};
185205

186206
const projectionStoreDependencies: Record<ProjectionName, StoreName[]> = {
207+
address: ['storeAddresses'],
187208
handle: ['storeHandles', 'storeNftMetadata'],
188209
'stake-pool': ['storeStakePools'],
189210
'stake-pool-metadata-job': ['storeStakePoolMetadataJob'],
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Cardano } from '@cardano-sdk/core';
2+
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
3+
import { Hash28ByteBase16 } from '@cardano-sdk/crypto';
4+
import { StakeKeyRegistrationEntity } from './StakeKeyRegistration.entity';
5+
6+
@Entity()
7+
export class AddressEntity {
8+
@PrimaryColumn()
9+
address?: Cardano.PaymentAddress;
10+
@Column({ enum: Cardano.AddressType, type: 'enum' })
11+
type?: Cardano.AddressType;
12+
@Column('char', { length: 56, nullable: true })
13+
@Index()
14+
/**
15+
* Applicable only for base/grouped, enterprise and pointer addresses
16+
*/
17+
paymentCredentialHash?: Hash28ByteBase16 | null;
18+
@Column('char', { length: 56, nullable: true })
19+
@Index()
20+
/**
21+
* Applicable only for base/grouped and pointer addresses
22+
*/
23+
stakeCredentialHash?: Hash28ByteBase16 | null;
24+
@ManyToOne(() => StakeKeyRegistrationEntity, { nullable: true, onDelete: 'CASCADE' })
25+
@JoinColumn()
26+
/**
27+
* Applicable only for pointer addresses
28+
*/
29+
registration?: StakeKeyRegistrationEntity | null;
30+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { BigIntColumnOptions, OnDeleteCascadeRelationOptions } from './util';
2+
import { BlockEntity } from './Block.entity';
3+
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
4+
import { Ed25519KeyHashHex } from '@cardano-sdk/crypto';
5+
6+
@Entity()
7+
export class StakeKeyRegistrationEntity {
8+
/**
9+
* Computed from certificate pointer.
10+
* Can be used to query by pointer using `Cardano.PointerToId` util.
11+
*/
12+
@PrimaryColumn(BigIntColumnOptions)
13+
id?: bigint;
14+
@Column('char', { length: 56 })
15+
@Index()
16+
stakeKeyHash?: Ed25519KeyHashHex;
17+
@ManyToOne(() => BlockEntity, OnDeleteCascadeRelationOptions)
18+
@JoinColumn()
19+
block?: BlockEntity;
20+
}

packages/projection-typeorm/src/entity/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ export * from './Output.entity';
1111
export * from './CurrentPoolMetrics.entity';
1212
export * from './Handle.entity';
1313
export * from './NftMetadata.entity';
14+
export * from './Address.entity';
15+
export * from './StakeKeyRegistration.entity';

packages/projection-typeorm/src/operators/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ export * from './storeHandles';
99
export * from './util';
1010
export * from './storePoolMetricsUpdateJob';
1111
export * from './storeNftMetadata';
12+
export * from './storeAddresses';
13+
export * from './storeStakeKeyRegistrations';
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { AddressEntity } from '../entity/Address.entity';
2+
import { Cardano, ChainSyncEventType } from '@cardano-sdk/core';
3+
import { Hash28ByteBase16 } from '@cardano-sdk/crypto';
4+
import { Mappers } from '@cardano-sdk/projection';
5+
import { QueryRunner } from 'typeorm';
6+
import { StakeKeyRegistrationEntity } from '../entity';
7+
import { certificatePointerToId, typeormOperator } from './util';
8+
9+
const lookupStakeKeyRegistration = async (pointer: Cardano.Pointer | undefined, queryRunner: QueryRunner) => {
10+
if (!pointer) return;
11+
const registrationId = certificatePointerToId(pointer);
12+
const stakeKeyRegistration = await queryRunner.manager
13+
.getRepository(StakeKeyRegistrationEntity)
14+
.findOne({ select: { stakeKeyHash: true }, where: { id: registrationId } });
15+
if (!stakeKeyRegistration?.stakeKeyHash) return;
16+
return Hash28ByteBase16.fromEd25519KeyHashHex(stakeKeyRegistration.stakeKeyHash);
17+
};
18+
19+
export const storeAddresses = typeormOperator<Mappers.WithAddresses>(async (evt) => {
20+
const { addresses, eventType, queryRunner } = evt;
21+
if (addresses.length === 0 || eventType !== ChainSyncEventType.RollForward) return;
22+
const addressEntities = await Promise.all(
23+
addresses.map(async ({ paymentCredentialHash, stakeCredential, address, type }): Promise<AddressEntity> => {
24+
const stakeCredentialHash =
25+
typeof stakeCredential === 'string'
26+
? stakeCredential
27+
: await lookupStakeKeyRegistration(stakeCredential, queryRunner);
28+
return {
29+
address,
30+
paymentCredentialHash,
31+
stakeCredentialHash,
32+
type
33+
};
34+
})
35+
);
36+
await queryRunner.manager
37+
.createQueryBuilder()
38+
.insert()
39+
.into(AddressEntity)
40+
.values(addressEntities)
41+
.orIgnore()
42+
.execute();
43+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ChainSyncEventType } from '@cardano-sdk/core';
2+
import { Mappers } from '@cardano-sdk/projection';
3+
import { StakeKeyRegistrationEntity } from '../entity';
4+
import { certificatePointerToId, typeormOperator } from './util';
5+
6+
export const storeStakeKeyRegistrations = typeormOperator<Mappers.WithStakeKeyRegistrations>(
7+
async ({ eventType, queryRunner, block, stakeKeyRegistrations }) => {
8+
// Deleted by db with ON DELETE CASCADE
9+
if (eventType !== ChainSyncEventType.RollForward || stakeKeyRegistrations.length === 0) return;
10+
const stakeKeyRegistrationsRepo = queryRunner.manager.getRepository(StakeKeyRegistrationEntity);
11+
await stakeKeyRegistrationsRepo.insert(
12+
stakeKeyRegistrations.map(
13+
(reg): StakeKeyRegistrationEntity => ({
14+
block: {
15+
slot: block.header.slot
16+
},
17+
id: certificatePointerToId(reg.pointer),
18+
stakeKeyHash: reg.stakeKeyHash
19+
})
20+
)
21+
);
22+
}
23+
);

0 commit comments

Comments
 (0)