-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core): ogmios to core translation of block type
- Loading branch information
1 parent
3533e90
commit b077cb7
Showing
7 changed files
with
1,011 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import * as Ogmios from '@cardano-ogmios/client'; | ||
|
||
export * as ogmiosToCore from './ogmiosToCore'; | ||
export * as Ogmios from '@cardano-ogmios/client'; | ||
export { Schema } from '@cardano-ogmios/client'; | ||
export type OgmiosTypescriptLib = typeof Ogmios; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './ogmiosToCore'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
import { BigIntMath } from '@cardano-sdk/util'; | ||
import { Schema, isByronStandardBlock } from '@cardano-ogmios/client'; | ||
|
||
import { Cardano, Ogmios } from '../..'; | ||
|
||
type KeysOfUnion<T> = T extends T ? keyof T : never; | ||
/** | ||
* Ogmios has actual block under a property named like the era (e.g. `block.alonzo`). | ||
* This type creates a union with all the properties. It is later used in | ||
* [exhaustive switches](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking) | ||
* to make sure all block types are handled and future blocks types will generate compile time errors. | ||
*/ | ||
type BlockKind = KeysOfUnion<Schema.Block>; | ||
type OgmiosBlockType = | ||
| Schema.BlockAllegra | ||
| Schema.BlockAlonzo | ||
| Schema.BlockBabbage | ||
| Schema.StandardBlock | ||
| Schema.BlockMary | ||
| Schema.BlockShelley; | ||
|
||
type CommonBlock = Exclude<OgmiosBlockType, Schema.StandardBlock>; | ||
|
||
interface Block<B extends OgmiosBlockType, T extends BlockKind> { | ||
block: B; | ||
kind: T; | ||
} | ||
|
||
type BlockAndKind = | ||
| Block<Schema.BlockAllegra, 'allegra'> | ||
| Block<Schema.BlockAlonzo, 'alonzo'> | ||
| Block<Schema.BlockBabbage, 'babbage'> | ||
| Block<Schema.StandardBlock, 'byron'> | ||
| Block<Schema.BlockMary, 'mary'> | ||
| Block<Schema.BlockShelley, 'shelley'>; | ||
|
||
/** | ||
* @returns | ||
* - {BlockAndKind} that unlocks type narrowing in switch statements based on `kind`. | ||
* Another advantage of using switch is using exhaustive check of case branches, making sure that future new | ||
* block kinds will cause compilation errors, instead of silently fail or runtime errors. | ||
* - `null` if `block` is the ByronEpochBoundaryBlock. This block can be skipped | ||
*/ | ||
// eslint-disable-next-line complexity | ||
const getBlockAndKind = (block: Schema.Block): BlockAndKind | null => { | ||
let propName: BlockKind = 'alonzo'; | ||
if (Ogmios.isAllegraBlock(block)) propName = 'allegra'; | ||
if (Ogmios.isAlonzoBlock(block)) propName = 'alonzo'; | ||
if (Ogmios.isBabbageBlock(block)) propName = 'babbage'; | ||
if (Ogmios.isByronBlock(block)) propName = 'byron'; | ||
if (Ogmios.isMaryBlock(block)) propName = 'mary'; | ||
if (Ogmios.isShelleyBlock(block)) propName = 'shelley'; | ||
|
||
// If it complains because a branch is not handled, please add logic for the new block type. | ||
switch (propName) { | ||
case 'allegra': | ||
return { block: (block as Schema.Allegra).allegra, kind: 'allegra' }; | ||
case 'alonzo': | ||
return { block: (block as Schema.Alonzo).alonzo, kind: 'alonzo' }; | ||
case 'babbage': | ||
return { block: (block as Schema.Babbage).babbage, kind: 'babbage' }; | ||
case 'byron': | ||
// Return `null` if it is the EBB block to signal that it can be skipped | ||
return isByronStandardBlock(block) ? { block: block.byron, kind: 'byron' } : null; | ||
case 'mary': | ||
return { block: (block as Schema.Mary).mary, kind: 'mary' }; | ||
case 'shelley': | ||
return { block: (block as Schema.Shelley).shelley, kind: 'shelley' }; | ||
default: { | ||
// will fail at compile time if not all branches are handled | ||
// eslint-disable-next-line sonarjs/prefer-immediate-return | ||
const _exhaustiveCheck: never = propName; | ||
return _exhaustiveCheck; | ||
} | ||
} | ||
}; | ||
|
||
// Mappers that apply to all Block types | ||
const mapBlockHeight = (block: OgmiosBlockType): number => block.header.blockHeight; | ||
const mapBlockSlot = (block: OgmiosBlockType): number => block.header.slot; | ||
const mapPreviousBlock = (block: OgmiosBlockType): Cardano.BlockId => Cardano.BlockId(block.header.prevHash); | ||
|
||
// Mappers specific to Byron block properties | ||
const mapByronHash = (block: Schema.StandardBlock): Cardano.BlockId => Cardano.BlockId(block.hash); | ||
const mapByronTotalOutputs = (block: Schema.StandardBlock): bigint => | ||
BigIntMath.sum( | ||
block.body.txPayload.map(({ body: { outputs } }) => BigIntMath.sum(outputs.map(({ value: { coins } }) => coins))) | ||
); | ||
const mapByronTxCount = (block: Schema.StandardBlock): number => block.body.txPayload.length; | ||
|
||
// Mappers for the rest of Block types | ||
const mapCommonTxCount = (block: CommonBlock): number => block.body.length; | ||
const mapCommonHash = (block: CommonBlock): Cardano.BlockId => Cardano.BlockId(block.headerHash); | ||
const mapCommonTotalOutputs = (block: CommonBlock): Cardano.Lovelace => | ||
BigIntMath.sum( | ||
block.body.map(({ body: { outputs } }) => BigIntMath.sum(outputs.map(({ value: { coins } }) => coins))) | ||
); | ||
const mapCommonBlockSize = (block: CommonBlock): number => block.header.blockSize; | ||
const mapCommonFees = (block: CommonBlock): Cardano.Lovelace => | ||
block.body.map(({ body: { fee } }) => fee).reduce((prev, current) => prev + current, 0n); | ||
// This is the VRF verification key, An Ed25519 verification key. | ||
const mapCommonVrf = (block: CommonBlock): Cardano.VrfVkBech32 => Cardano.VrfVkBech32FromBase64(block.header.issuerVrf); | ||
// SlotLeader is the producer pool id. It can be calculated from the issuer verification key | ||
// which is actually the cold verification key | ||
const mapCommonSlotLeader = (block: CommonBlock): Cardano.Ed25519PublicKey => | ||
Cardano.Ed25519PublicKey(block.header.issuerVk); | ||
|
||
export const mapByronBlock = (block: Schema.StandardBlock): Cardano.BlockMinimal => ({ | ||
fees: undefined, // TODO: figure out how to calculate fees | ||
header: { | ||
blockNo: mapBlockHeight(block), | ||
hash: mapByronHash(block), | ||
slot: mapBlockSlot(block) | ||
}, | ||
// TODO: use the genesisKey to provide a value here, but it needs more work. Leaving as undefined for now | ||
issuerVk: undefined, | ||
previousBlock: mapPreviousBlock(block), | ||
// TODO: calculate byron blocksize by transforming into CSL Block object | ||
size: undefined, | ||
totalOutput: mapByronTotalOutputs(block), | ||
txCount: mapByronTxCount(block), | ||
vrf: undefined // no vrf key for byron. DbSync doesn't have one either | ||
}); | ||
|
||
export const mapCommonBlock = (block: CommonBlock): Cardano.BlockMinimal => ({ | ||
fees: mapCommonFees(block), | ||
header: { | ||
blockNo: mapBlockHeight(block), | ||
hash: mapCommonHash(block), | ||
slot: mapBlockSlot(block) | ||
}, | ||
issuerVk: mapCommonSlotLeader(block), | ||
previousBlock: mapPreviousBlock(block), | ||
size: mapCommonBlockSize(block), | ||
totalOutput: mapCommonTotalOutputs(block), | ||
txCount: mapCommonTxCount(block), | ||
vrf: mapCommonVrf(block) | ||
}); | ||
|
||
/** | ||
* Translate `Ogmios` block to `Cardano.BlockMinimal` | ||
* | ||
* @param ogmiosBlock the block to translate into a `Cardano.BlockMinimal` | ||
* @returns | ||
* - {Cardano.BlockMinimal} a minimal block type encompassing information extracted from Ogmios block type. | ||
* - `null` if `block` is the ByronEpochBoundaryBlock. This block can be skipped. | ||
*/ | ||
export const getBlock = (ogmiosBlock: Schema.Block): Cardano.BlockMinimal | null => { | ||
const b = getBlockAndKind(ogmiosBlock); | ||
if (!b) return null; | ||
|
||
switch (b.kind) { | ||
case 'byron': { | ||
return mapByronBlock(b.block); | ||
} | ||
case 'babbage': | ||
case 'allegra': | ||
case 'alonzo': | ||
case 'mary': | ||
case 'shelley': { | ||
return mapCommonBlock(b.block); | ||
} | ||
default: { | ||
// eslint-disable-next-line sonarjs/prefer-immediate-return | ||
const _exhaustiveCheck: never = b; | ||
return _exhaustiveCheck; | ||
} | ||
} | ||
}; | ||
|
||
// byron-shelley-allegra-mary-alonzo-babbage |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import { Cardano, ogmiosToCore } from '../../src'; | ||
|
||
import { | ||
mockAllegraBlock, | ||
mockAlonzoBlock, | ||
mockBabbageBlock, | ||
mockByronBlock, | ||
mockMaryBlock, | ||
mockShelleyBlock | ||
} from './testData'; | ||
|
||
describe('ogmiosToCore', () => { | ||
it('can translate from byron block', () => { | ||
// using https://preprod.cardanoscan.io/block/42 as source of truth | ||
expect(ogmiosToCore.getBlock(mockByronBlock)).toEqual(<Cardano.BlockMinimal>{ | ||
fees: undefined, | ||
header: { | ||
blockNo: 42, | ||
hash: Cardano.BlockId('5c3103bd0ff5ea85a62b202a1d2500cf3ebe0b9d793ed09e7febfe27ef12c968'), | ||
slot: 77_761 | ||
}, | ||
issuerVk: undefined, | ||
previousBlock: Cardano.BlockId('dd8d7559a9b6c1177c0f5a328eb82967af68155d58cbcdc0a59de39a38aaf3f0'), | ||
// got size: 626 by querying the postgres db populated by db-sync. | ||
// Using size: undefined until we can calculate it | ||
size: undefined, | ||
totalOutput: 0n, | ||
txCount: 0, | ||
vrf: undefined | ||
}); | ||
}); | ||
|
||
it('can translate from shelley block', () => { | ||
// using https://preprod.cardanoscan.io/block/1087 as source of truth | ||
expect(ogmiosToCore.getBlock(mockShelleyBlock)).toEqual(<Cardano.BlockMinimal>{ | ||
fees: 0n, | ||
header: { | ||
blockNo: 1087, | ||
hash: Cardano.BlockId('071fceb6c20a412b9a9b57baedfe294e3cd9de641cd44c4cf8d0d56217e083ac'), | ||
slot: 107_220 | ||
}, | ||
issuerVk: Cardano.Ed25519PublicKey('8b0960d234bda67d52432c5d1a26aca2bfb5b9a09f966d9592a7bf0c728a1ecd'), | ||
previousBlock: Cardano.BlockId('8d5d930981710fc8c6ca9fc8e0628665283f7efb28c7e6bddeee2d289f012dee'), | ||
// got size by querying the postgres db populated by db-sync | ||
size: 3, | ||
totalOutput: 0n, | ||
txCount: 0, | ||
// vrf from https://preprod.cexplorer.io/block/071fceb6c20a412b9a9b57baedfe294e3cd9de641cd44c4cf8d0d56217e083ac | ||
vrf: Cardano.VrfVkBech32('vrf_vk15c2edf9h66wllthgvyttzhzwrngq0rvd0wchzqlw8qray60fq5usfngf29') | ||
}); | ||
}); | ||
|
||
it('can translate from allegra block', () => { | ||
// Verify data extracted from mock structure | ||
const ogmiosBlock = mockAllegraBlock.allegra; | ||
expect(ogmiosToCore.getBlock(mockAllegraBlock)).toEqual(<Cardano.BlockMinimal>{ | ||
fees: ogmiosBlock.body[0].body.fee, | ||
header: { | ||
blockNo: ogmiosBlock.header.blockHeight, | ||
hash: Cardano.BlockId(ogmiosBlock.headerHash), | ||
slot: ogmiosBlock.header.slot | ||
}, | ||
issuerVk: Cardano.Ed25519PublicKey(ogmiosBlock.header.issuerVk), | ||
previousBlock: Cardano.BlockId(ogmiosBlock.header.prevHash), | ||
size: ogmiosBlock.header.blockSize, | ||
totalOutput: 0n, | ||
txCount: ogmiosBlock.body.length, | ||
vrf: Cardano.VrfVkBech32FromBase64(ogmiosBlock.header.issuerVrf) | ||
}); | ||
}); | ||
|
||
it('can translate from mary block', () => { | ||
// Verify data extracted from mock structure | ||
const ogmiosBlock = mockMaryBlock.mary; | ||
expect(ogmiosToCore.getBlock(mockMaryBlock)).toEqual(<Cardano.BlockMinimal>{ | ||
fees: ogmiosBlock.body[0].body.fee + ogmiosBlock.body[1].body.fee, | ||
header: { | ||
blockNo: ogmiosBlock.header.blockHeight, | ||
hash: Cardano.BlockId(ogmiosBlock.headerHash), | ||
slot: ogmiosBlock.header.slot | ||
}, | ||
issuerVk: Cardano.Ed25519PublicKey(ogmiosBlock.header.issuerVk), | ||
previousBlock: Cardano.BlockId(ogmiosBlock.header.prevHash), | ||
size: ogmiosBlock.header.blockSize, | ||
totalOutput: | ||
ogmiosBlock.body[0].body.outputs[0].value.coins + | ||
ogmiosBlock.body[1].body.outputs[0].value.coins + | ||
ogmiosBlock.body[1].body.outputs[1].value.coins, | ||
txCount: ogmiosBlock.body.length, | ||
vrf: Cardano.VrfVkBech32FromBase64(ogmiosBlock.header.issuerVrf) | ||
}); | ||
}); | ||
|
||
it('can translate from alonzo block', () => { | ||
// using https://preprod.cardanoscan.io/block/100000 as source of truth | ||
expect(ogmiosToCore.getBlock(mockAlonzoBlock)).toEqual(<Cardano.BlockMinimal>{ | ||
fees: 202_549n, | ||
header: { | ||
blockNo: 100_000, | ||
hash: Cardano.BlockId('514f8be63ef25c46bee47a90658977f815919c06222c0b480be1e29efbd72c49'), | ||
slot: 5_481_752 | ||
}, | ||
issuerVk: Cardano.Ed25519PublicKey('a9d974fd26bfaf385749113f260271430276bed6ef4dad6968535de6778471ce'), | ||
|
||
previousBlock: Cardano.BlockId('518a24a3fb0cc6ee1a31668a63994e4dbda70ede5ff13be494a3b4c1bb7709c8'), | ||
// got size by querying the postgres db populated by db-sync | ||
size: 836, | ||
totalOutput: 8_287_924_709n, | ||
txCount: 1, | ||
// vrf from https://preprod.cexplorer.io/block/514f8be63ef25c46bee47a90658977f815919c06222c0b480be1e29efbd72c49 | ||
vrf: Cardano.VrfVkBech32('vrf_vk1p8s5ysf7dgsvfrw0p0q7zczdytkxc95zsq3p9sfshk9s3z86jfdql5fdft') | ||
}); | ||
}); | ||
|
||
it('can translate from babbage block', () => { | ||
// Verify data extracted from mock structure | ||
const ogmiosBlock = mockBabbageBlock.babbage; | ||
expect(ogmiosToCore.getBlock(mockBabbageBlock)).toEqual(<Cardano.BlockMinimal>{ | ||
fees: ogmiosBlock.body[0].body.fee, | ||
header: { | ||
blockNo: ogmiosBlock.header.blockHeight, | ||
hash: Cardano.BlockId(ogmiosBlock.headerHash), | ||
slot: ogmiosBlock.header.slot | ||
}, | ||
issuerVk: Cardano.Ed25519PublicKey(ogmiosBlock.header.issuerVk), | ||
previousBlock: Cardano.BlockId(ogmiosBlock.header.prevHash), | ||
size: ogmiosBlock.header.blockSize, | ||
totalOutput: ogmiosBlock.body[0].body.outputs[0].value.coins, | ||
txCount: ogmiosBlock.body.length, | ||
vrf: Cardano.VrfVkBech32FromBase64(ogmiosBlock.header.issuerVrf) | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.