Skip to content

Commit

Permalink
feat(ogmios): complete Ogmios tx to core mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
Ivaylo Andonov committed Dec 5, 2022
1 parent fa1c487 commit bcac56b
Show file tree
Hide file tree
Showing 8 changed files with 1,149 additions and 249 deletions.
7 changes: 7 additions & 0 deletions packages/core/src/Cardano/util/primitives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ export type HexBlob = OpaqueString<'HexBlob'>;
export const HexBlob = (target: string): HexBlob => typedHex(target);
HexBlob.fromBytes = (bytes: Uint8Array) => Buffer.from(bytes).toString('hex') as unknown as HexBlob;

/**
* Converts a base64 string into a hex (base16) encoded string.
*
* @param rawData The base64 encoded string.
*/
HexBlob.fromBase64 = (rawData: string) => Buffer.from(rawData, 'base64').toString('hex') as unknown as HexBlob;

/**
* Converts a hex string into a typed bech32 encoded string.
*
Expand Down
10 changes: 10 additions & 0 deletions packages/core/test/Cardano/util/primitives.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable max-len */
/* eslint-disable sonarjs/no-duplicate-string */
import {
Base64Blob,
Expand Down Expand Up @@ -101,6 +102,15 @@ describe('Cardano.util/primitives', () => {
it('fromBytes converts byte array into HexBlob', () => {
expect(HexBlob.fromBytes(new Uint8Array([112]))).toEqual('70');
});

it('fromBase64 converts a base64 encoded string into HexBlob', () => {
const base64String = 'o+KixEeK/nzXNPpZPOM/BoQSVWVtwx06z/SIhM6UeNVjFN1rqHKN5BdBOnmKtuh/aF+5F/gwCzl3KPCGMcFuOQ==';
const expectedHexString =
'a3e2a2c4478afe7cd734fa593ce33f06841255656dc31d3acff48884ce9478d56314dd6ba8728de417413a798ab6e87f685fb917f8300b397728f08631c16e39';
const hexString = HexBlob.fromBase64(base64String);
expect(hexString).toEqual(expectedHexString);
expect(hexString).toHaveLength(128);
});
});

describe('Base64Blob', () => {
Expand Down
12 changes: 6 additions & 6 deletions packages/ogmios/src/ogmiosToCore/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ const mapByronBlock = (block: Schema.StandardBlock): Cardano.Block => ({
vrf: undefined // no vrf key for byron. DbSync doesn't have one either
});

const mapCommonBlock = (block: CommonBlock): Cardano.Block => ({
body: mapCommonBlockBody(block),
const mapCommonBlock = (block: CommonBlock, kind: BlockKind): Cardano.Block => ({
body: mapCommonBlockBody(block, kind),
fees: mapCommonFees(block),
header: mapCommonBlockHeader(block),
issuerVk: mapCommonSlotLeader(block),
Expand All @@ -130,7 +130,7 @@ const mapCommonBlock = (block: CommonBlock): Cardano.Block => ({
const mapBlock = <R>(
ogmiosBlock: Schema.Block,
mapStandardBlock: (b: Schema.StandardBlock) => R,
mapOtherBlock: (b: CommonBlock) => R
mapOtherBlock: (b: CommonBlock, k: BlockKind) => R
) => {
const b = getBlockAndKind(ogmiosBlock);
if (!b) return null;
Expand All @@ -144,7 +144,7 @@ const mapBlock = <R>(
case 'alonzo':
case 'mary':
case 'shelley': {
return mapOtherBlock(b.block);
return mapOtherBlock(b.block, b.kind);
}
default: {
// eslint-disable-next-line sonarjs/prefer-immediate-return
Expand All @@ -162,7 +162,7 @@ const mapBlock = <R>(
* - `null` if `block` is the ByronEpochBoundaryBlock. This block can be skipped.
*/
export const blockHeader = (ogmiosBlock: Schema.Block): Cardano.PartialBlockHeader | null =>
mapBlock(ogmiosBlock, mapStandardBlockHeader, mapCommonBlockHeader);
mapBlock<Cardano.PartialBlockHeader>(ogmiosBlock, mapStandardBlockHeader, mapCommonBlockHeader);

/**
* Translate `Ogmios` block to `Cardano.BlockMinimal`
Expand All @@ -173,6 +173,6 @@ export const blockHeader = (ogmiosBlock: Schema.Block): Cardano.PartialBlockHead
* - `null` if `block` is the ByronEpochBoundaryBlock. This block can be skipped.
*/
export const block = (ogmiosBlock: Schema.Block): Cardano.Block | null =>
mapBlock(ogmiosBlock, mapByronBlock, mapCommonBlock);
mapBlock<Cardano.Block>(ogmiosBlock, mapByronBlock, mapCommonBlock);

// byron-shelley-allegra-mary-alonzo-babbage
254 changes: 242 additions & 12 deletions packages/ogmios/src/ogmiosToCore/tx.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
import { Cardano, NotImplementedError, addressNetworkId, createRewardAccount } from '@cardano-sdk/core';
import { CommonBlock } from './types';
/* eslint-disable max-len */
import {
BYRON_TX_FEE_COEFFICIENT,
BYRON_TX_FEE_CONSTANT,
isAlonzoOrAbove,
isExpiresAt,
isMaryOrAbove,
isNativeScript,
isPlutusV1Script,
isPlutusV2Script,
isRequireAllOf,
isRequireAnyOf,
isRequireNOf,
isShelleyTx,
isStartsAt
} from './util';
import { BlockKind, CommonBlock } from './types';
import {
Cardano,
NotImplementedError,
ProviderUtil,
SerializationError,
SerializationFailure,
addressNetworkId,
createRewardAccount
} from '@cardano-sdk/core';
import { Schema } from '@cardano-ogmios/client';
import Fraction from 'fraction.js';
import omit from 'lodash/omit';

// TODO: implement byron block body mapping
export const mapByronBlockBody = (_: Schema.BlockByron): Cardano.Block['body'] => [];

const mapMargin = (margin: string): Cardano.Fraction => {
const { n: numerator, d: denominator } = new Fraction(margin);
return { denominator, numerator };
Expand Down Expand Up @@ -109,12 +130,221 @@ const mapCertificate = (certificate: Schema.Certificate): Cardano.Certificate =>
throw new NotImplementedError('Unknown certificate mapping');
};

// TODO: implement full block body mapping
const mapCommonTx = (tx: CommonBlock['body'][0]): Cardano.Tx =>
({
export const nativeScript = (script: Schema.ScriptNative): Cardano.NativeScript => {
let coreScript: Cardano.NativeScript;

if (typeof script === 'string') {
coreScript = {
__type: Cardano.ScriptType.Native,
keyHash: Cardano.Ed25519KeyHash(script),
kind: Cardano.NativeScriptKind.RequireSignature
};
} else if (isRequireAllOf(script)) {
coreScript = {
__type: Cardano.ScriptType.Native,
kind: Cardano.NativeScriptKind.RequireAllOf,
scripts: new Array<Cardano.NativeScript>()
};
for (let i = 0; i < script.all.length; ++i) {
coreScript.scripts.push(nativeScript(script.all[i]));
}
} else if (isRequireAnyOf(script)) {
coreScript = {
__type: Cardano.ScriptType.Native,
kind: Cardano.NativeScriptKind.RequireAnyOf,
scripts: new Array<Cardano.NativeScript>()
};
for (let i = 0; i < script.any.length; ++i) {
coreScript.scripts.push(nativeScript(script.any[i]));
}
} else if (isRequireNOf(script)) {
const required = Number.parseInt(Object.keys(script)[0]);
coreScript = {
__type: Cardano.ScriptType.Native,
kind: Cardano.NativeScriptKind.RequireNOf,
required,
scripts: new Array<Cardano.NativeScript>()
};

for (let i = 0; i < script[required].length; ++i) {
coreScript.scripts.push(nativeScript(script[required][i]));
}
} else if (isExpiresAt(script)) {
coreScript = {
__type: Cardano.ScriptType.Native,
kind: Cardano.NativeScriptKind.RequireTimeBefore,
slot: Cardano.Slot(script.expiresAt)
};
} else if (isStartsAt(script)) {
coreScript = {
__type: Cardano.ScriptType.Native,
kind: Cardano.NativeScriptKind.RequireTimeAfter,
slot: Cardano.Slot(script.startsAt)
};
} else {
throw new SerializationError(
SerializationFailure.InvalidNativeScriptKind,
`Native Script value '${script}' is not supported.`
);
}

return coreScript;
};

const mapPlutusScript = (script: Schema.PlutusV1 | Schema.PlutusV2): Cardano.PlutusScript => {
const version = isPlutusV1Script(script) ? Cardano.PlutusLanguageVersion.V1 : Cardano.PlutusLanguageVersion.V2;
const plutusScript = isPlutusV1Script(script) ? script['plutus:v1'] : script['plutus:v2'];
return {
__type: Cardano.ScriptType.Plutus,
bytes: Cardano.util.HexBlob(plutusScript),
version
};
};

export const mapScript = (script: Schema.Script): Cardano.Script => {
if (isNativeScript(script)) {
return nativeScript(script.native);
} else if (isPlutusV1Script(script) || isPlutusV2Script(script)) return mapPlutusScript(script);

throw new SerializationError(SerializationFailure.InvalidScriptType, `Script '${script}' is not supported.`);
};

const mapBootstrapWitness = (b: Schema.BootstrapWitness): Cardano.BootstrapWitness => ({
// Based on the Ogmios maintainer answer https://github.com/CardanoSolutions/ogmios/discussions/285#discussioncomment-4271726
addressAttributes: b.addressAttributes ? Cardano.util.Base64Blob(b.addressAttributes) : undefined,
chainCode: b.chainCode ? Cardano.util.HexBlob(b.chainCode) : undefined,
key: Cardano.Ed25519PublicKey(b.key!),
signature: Cardano.Ed25519Signature(Cardano.util.HexBlob.fromBase64(b.signature!).toString())
});

const mapRedeemer = (key: string, redeemer: Schema.Redeemer): Cardano.Redeemer => {
const purposeAndIndex = key.split(':');

return {
executionUnits: redeemer.executionUnits,
index: Number(purposeAndIndex[1]),
purpose: purposeAndIndex[0] as Cardano.RedeemerPurpose,
scriptHash: Cardano.util.Hash28ByteBase16(redeemer.redeemer)
};
};

const mapAuxiliaryData = (data: Schema.AuxiliaryData | null): Cardano.AuxiliaryData | undefined => {
if (data === null) return undefined;

return {
body: {
certificates: tx.body.certificates.map(mapCertificate)
} as Cardano.TxBody
} as Cardano.Tx);
blob: data.body.blob
? new Map(
Object.entries(data.body.blob).map(([key, value]) => [BigInt(key), ProviderUtil.jsonToMetadatum(value)])
)
: undefined,
scripts: data.body.scripts ? data.body.scripts.map(mapScript) : undefined
},
hash: Cardano.util.Hash32ByteBase16(data.hash)
};
};

const mapTxIn = (txIn: Schema.TxIn): Cardano.TxIn => ({
index: txIn.index,
txId: Cardano.TransactionId(txIn.txId)
});

const mapDatum = (datum: Schema.TxOut['datum']) => {
if (!datum) return;
if (typeof datum === 'string') return Cardano.util.Hash32ByteBase16(datum);
if (typeof datum === 'object') return Cardano.util.Hash32ByteBase16(JSON.stringify(datum));
};

const mapTxOut = (txOut: Schema.TxOut): Cardano.TxOut => ({
address: Cardano.Address(txOut.address),
datum: mapDatum(txOut.datum),
value: {
assets: txOut.value.assets
? new Map(Object.entries(txOut.value.assets).map(([key, value]) => [Cardano.AssetId(key), value]))
: undefined,
coins: txOut.value.coins
}
});

const mapMint = (tx: Schema.TxMary): Cardano.TokenMap | undefined => {
if (tx.body.mint.assets === undefined) return undefined;
return new Map(Object.entries(tx.body.mint.assets).map(([key, value]) => [Cardano.AssetId(key), value]));
};

const mapScriptIntegrityHash = ({
body: { scriptIntegrityHash }
}: Schema.TxAlonzo): Cardano.util.Hash32ByteBase16 | undefined => {
if (scriptIntegrityHash === null) return undefined;
return Cardano.util.Hash32ByteBase16(scriptIntegrityHash);
};

const mapValidityInterval = ({
invalidBefore,
invalidHereafter
}: Schema.ValidityInterval): Cardano.ValidityInterval => ({
invalidBefore: invalidBefore ? Cardano.Slot(invalidBefore) : undefined,
invalidHereafter: invalidHereafter ? Cardano.Slot(invalidHereafter) : undefined
});

const mapCommonTx = (tx: CommonBlock['body'][0], kind: BlockKind): Cardano.Tx => ({
auxiliaryData: mapAuxiliaryData(tx.metadata),
body: {
certificates: tx.body.certificates.map(mapCertificate),
collaterals: isAlonzoOrAbove(kind) ? (tx as Schema.TxAlonzo).body.collaterals.map(mapTxIn) : undefined,
fee: tx.body.fee,
inputs: tx.body.inputs.map(mapTxIn),
mint: isMaryOrAbove(kind) ? mapMint(tx as Schema.TxMary) : undefined,
outputs: tx.body.outputs.map(mapTxOut),
requiredExtraSignatures: isAlonzoOrAbove(kind)
? (tx as Schema.TxAlonzo).body.requiredExtraSignatures.map(Cardano.Ed25519KeyHash)
: undefined,
scriptIntegrityHash: isAlonzoOrAbove(kind) ? mapScriptIntegrityHash(tx as Schema.TxAlonzo) : undefined,
validityInterval: isShelleyTx(kind)
? undefined
: mapValidityInterval((tx as Schema.TxAlonzo).body.validityInterval),
withdrawals: Object.entries(tx.body.withdrawals).map(([key, value]) => ({
quantity: value,
stakeAddress: Cardano.RewardAccount(key)
}))
},
id: Cardano.TransactionId(tx.id),
witness: {
bootstrap: tx.witness.bootstrap.map(mapBootstrapWitness),
datums: isAlonzoOrAbove(kind)
? Object.values((tx as Schema.TxAlonzo).witness.datums).map((d) => Cardano.util.HexBlob(d))
: undefined,
redeemers: isAlonzoOrAbove(kind)
? Object.entries((tx as Schema.TxAlonzo).witness.redeemers).map(([key, value]) => mapRedeemer(key, value))
: undefined,
scripts: [...Object.values(tx.witness.scripts).map(mapScript)],
signatures: new Map(
Object.entries(tx.witness.signatures).map(([key, value]) => [
Cardano.Ed25519PublicKey(key),
Cardano.Ed25519Signature(Cardano.util.HexBlob.fromBase64(value).toString())
])
)
}
});

export const mapCommonBlockBody = ({ body }: CommonBlock, kind: BlockKind): Cardano.Block['body'] =>
body.map((blockBody) => mapCommonTx(blockBody, kind));

export const mapByronTxFee = ({ raw }: Schema.TxByron) => {
const txSize = Buffer.from(Cardano.util.Base64Blob(raw).toString(), 'base64').length;
return BigInt(BYRON_TX_FEE_COEFFICIENT * txSize + BYRON_TX_FEE_CONSTANT);
};

const mapByronTx = (tx: Schema.TxByron): Cardano.Tx => ({
body: {
fee: mapByronTxFee(tx),
inputs: tx.body.inputs.map(mapTxIn),
outputs: tx.body.outputs.map(mapTxOut)
},
id: Cardano.TransactionId(tx.id),
witness: {
signatures: new Map()
}
});

export const mapCommonBlockBody = ({ body }: CommonBlock): Cardano.Block['body'] => body.map(mapCommonTx);
export const mapByronBlockBody = ({ body }: Schema.StandardBlock): Cardano.Block['body'] =>
body.txPayload.map((txPayload) => mapByronTx(txPayload));
32 changes: 32 additions & 0 deletions packages/ogmios/src/ogmiosToCore/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { BlockKind } from './types';
import { Schema } from '@cardano-ogmios/client';

export const BYRON_TX_FEE_COEFFICIENT = 43_946_000_000;
export const BYRON_TX_FEE_CONSTANT = 155_381_000_000_000;

export const isNativeScript = (script: Schema.Script): script is Schema.Native => 'native' in script;

export const isPlutusV1Script = (script: Schema.Script): script is Schema.PlutusV1 => 'plutus:v1' in script;

export const isPlutusV2Script = (script: Schema.Script): script is Schema.PlutusV2 => 'plutus:v2' in script;

export const isRequireAllOf = (nativeScript: Schema.ScriptNative): nativeScript is Schema.All =>
typeof nativeScript === 'object' && 'all' in nativeScript;

export const isRequireAnyOf = (nativeScript: Schema.ScriptNative): nativeScript is Schema.Any =>
typeof nativeScript === 'object' && 'any' in nativeScript;

export const isExpiresAt = (nativeScript: Schema.ScriptNative): nativeScript is Schema.ExpiresAt =>
typeof nativeScript === 'object' && 'expiresAt' in nativeScript;

export const isStartsAt = (nativeScript: Schema.ScriptNative): nativeScript is Schema.StartsAt =>
typeof nativeScript === 'object' && 'startsAt' in nativeScript;

export const isRequireNOf = (nativeScript: Schema.ScriptNative): nativeScript is Schema.NOf =>
typeof nativeScript === 'object' && !Number.isNaN(Number(Object.keys(nativeScript)[0]));

export const isAlonzoOrAbove = (kind: BlockKind) => kind === 'babbage' || kind === 'alonzo';

export const isMaryOrAbove = (kind: BlockKind) => isAlonzoOrAbove(kind) || kind === 'mary';

export const isShelleyTx = (kind: BlockKind) => kind === 'shelley';
Loading

0 comments on commit bcac56b

Please sign in to comment.