Skip to content

Commit

Permalink
Merge pull request #181 from input-output-hk/hrajchert/annotations
Browse files Browse the repository at this point in the history
Add annotations to marlowe-object
  • Loading branch information
hrajchert authored Feb 6, 2024
2 parents 19ca9ca + 7c3f2e1 commit 8b1da40
Show file tree
Hide file tree
Showing 39 changed files with 2,452 additions and 218 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
### @marlowe.io/runtime-rest-client

- `mkRestClient` provides optional `strict` parameter for performing dynamic type checking in `RestClient` methods. ([PR-180](https://github.com/input-output-hk/marlowe-ts-sdk/pull/180))
- The following `RestClient` methods uses keyword argument object instead of positional arguments. ([PR-180](https://github.com/input-output-hk/marlowe-ts-sdk/pull/180))
- **BREAKING CHANGE** The following `RestClient` methods uses keyword argument object instead of positional arguments. ([PR-180](https://github.com/input-output-hk/marlowe-ts-sdk/pull/180))
- `createContractSources`
- `getContractById`
- `submitContract`
Expand Down
8 changes: 8 additions & 0 deletions changelog.d/20240201_155716_hrajchert_annotations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
### @marlowe.io/runtime-lifecycle

- Fix: Temporal fix for converting the cardano time interval to the Marlowe time interval in getInputHistory ([PR-181](https://github.com/input-output-hk/marlowe-ts-sdk/pull/181))

### @marlowe.io/marlowe-object

- **BREAKING CHANGE** Feat: Added Annotations to the contract type. ([PR-181](https://github.com/input-output-hk/marlowe-ts-sdk/pull/181))
- Experimental Feat: Added a sourceMap API to match the annotated marlowe-object source with the ContractClosure. ([PR-181](https://github.com/input-output-hk/marlowe-ts-sdk/pull/181))
10 changes: 7 additions & 3 deletions examples/nodejs/src/experimental-features/applicable-inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,13 @@ export async function getApplicableActions(
? contractDetails.currentContract
: contractDetails.initialContract;
const oneDayFrom = (time: Timeout) => time + 24n * 60n * 60n * 1000n; // in milliseconds
const now = datetoTimeout(new Date());
const nextTimeout = getNextTimeout(currentContract, now) ?? oneDayFrom(now);
const timeInterval = { from: now, to: nextTimeout - 1n };
const status = await restClient.healthcheck();
const lowerBound = datetoTimeout(
new Date(status.tips.runtimeChain.slotTimeUTC)
);
const nextTimeout =
getNextTimeout(currentContract, lowerBound) ?? oneDayFrom(lowerBound);
const timeInterval = { from: lowerBound, to: nextTimeout - 1n };
const env = environment ?? { timeInterval };
if (typeof contractDetails.state === "undefined") {
// TODO: Check, I believe this happens when a contract is in a closed state, but it would be nice
Expand Down
30 changes: 30 additions & 0 deletions examples/nodejs/src/experimental-features/contract-closure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Contract } from "@marlowe.io/language-core-v1";
import { ContractSourceId } from "@marlowe.io/marlowe-object";
import { RestClient } from "@marlowe.io/runtime-rest-client";

export interface ContractClosure {
main: string;
contracts: Map<string, Contract>;
}

type ClosureDI = { restClient: RestClient };

// TODO: Candidate for runtime lifecycle helper
export const getContractClosure =
({ restClient }: ClosureDI) =>
async (contractSourceId: ContractSourceId): Promise<ContractClosure> => {
const ids = await restClient.getContractSourceClosure({
contractSourceId,
});
const objectEntries = await Promise.all(
ids.results.map((id) =>
restClient
.getContractSourceById({ contractSourceId: id })
.then((c) => [id, c] as const)
)
);
return {
main: contractSourceId,
contracts: new Map(objectEntries),
};
};
225 changes: 225 additions & 0 deletions examples/nodejs/src/experimental-features/source-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import * as M from "fp-ts/lib/Map.js";

import {
ContractBundleMap,
bundleMapToList,
isAnnotated,
stripAnnotations,
} from "@marlowe.io/marlowe-object";
import {
CreateContractRequestBase,
RuntimeLifecycle,
} from "@marlowe.io/runtime-lifecycle/api";

import { ContractClosure, getContractClosure } from "./contract-closure.js";
import * as Core from "@marlowe.io/language-core-v1";
import * as CoreG from "@marlowe.io/language-core-v1/guards";
import * as Obj from "@marlowe.io/marlowe-object";
import * as ObjG from "@marlowe.io/marlowe-object/guards";

import {
SingleInputTx,
TransactionOutput,
playSingleInputTxTrace,
} from "@marlowe.io/language-core-v1/semantics";
import { RestClient } from "@marlowe.io/runtime-rest-client";
import { ContractId, TxId } from "@marlowe.io/runtime-core";
import { deepEqual } from "@marlowe.io/adapter/deep-equal";

function annotateInputFromClosure(contractClosure: ContractClosure) {
return function (input: Core.Input): Core.Input {
if (input === "input_notify") return "input_notify";
if ("merkleized_continuation" in input) {
const annotatedContinuation = contractClosure.contracts.get(
input.continuation_hash
);
if (typeof annotatedContinuation === "undefined")
throw new Error(
`Cant find continuation for ${input.continuation_hash}`
);
return { ...input, merkleized_continuation: annotatedContinuation };
} else {
return input;
}
};
}

function annotateHistoryFromClosure(contractClosure: ContractClosure) {
return function (history: SingleInputTx[]): SingleInputTx[] {
return history.map((tx) => {
if (typeof tx.input === "undefined") {
return tx;
} else {
return {
...tx,
input: annotateInputFromClosure(contractClosure)(tx.input),
};
}
});
};
}

async function annotatedClosure<T>(
restClient: RestClient,
sourceObjectMap: ContractBundleMap<T>
): Promise<ContractClosure> {
const { contractSourceId, intermediateIds } =
await restClient.createContractSources({
bundle: bundleMapToList(sourceObjectMap),
});

const closure = await getContractClosure({ restClient })(contractSourceId);

// The intermediateIds is an object whose keys belong to the source code and value is the merkle hash.
// We need to reverse this object in order to annotate the closure using the source annotations.
// It is possible for two different source entries to have the same hash and different annotations.
// In that case the last annotation will prevail.
const sourceMap = Object.fromEntries(
Object.entries(intermediateIds).map(([source, hash]) => [hash, source])
);

function getSourceContract(ref: Obj.Label) {
const sourceContractObject = sourceObjectMap.objects[ref];
if (typeof sourceContractObject === "undefined")
throw new Error(`Cant find source for ${ref}`);

return sourceContractObject.value as Obj.Contract<unknown>;
}

function copyAnnotation<T extends object>(source: object, dst: T): T {
if (isAnnotated(source)) {
return { annotation: source.annotation, ...dst };
}
return dst;
}

function annotateContract(
source: Obj.Contract<unknown>,
dst: Core.Contract
): Core.Contract {
let srcContract = source;
if (ObjG.Reference.is(source)) {
srcContract = getSourceContract(source.ref);
}

if (CoreG.Close.is(dst) && ObjG.Close.is(srcContract)) {
return srcContract as Core.Close;
}

if (CoreG.Pay.is(dst) && ObjG.Pay.is(srcContract)) {
return copyAnnotation(srcContract, {
...dst,
then: annotateContract(srcContract.then, dst.then),
});
}

if (CoreG.If.is(dst) && ObjG.If.is(srcContract)) {
return copyAnnotation(srcContract, {
...dst,
then: annotateContract(srcContract.then, dst.then),
else: annotateContract(srcContract.else, dst.else),
});
}

if (CoreG.Let.is(dst) && ObjG.Let.is(srcContract)) {
return copyAnnotation(srcContract, {
...dst,
then: annotateContract(srcContract.then, dst.then),
});
}

if (CoreG.Assert.is(dst) && ObjG.Assert.is(srcContract)) {
return copyAnnotation(srcContract, {
...dst,
then: annotateContract(srcContract.then, dst.then),
});
}

if (CoreG.When.is(dst) && ObjG.When.is(srcContract)) {
const srcWhen = srcContract;
return copyAnnotation(srcWhen, {
...dst,
timeout_continuation: annotateContract(
srcWhen.timeout_continuation,
dst.timeout_continuation
),
when: dst.when.map((dstCase, index) => {
const srcCase = srcWhen.when[index];
if ("merkleized_then" in srcCase) {
throw new Error(`Merkleized not supported in source.`);
}

if ("then" in srcCase && "then" in dstCase) {
return {
...dstCase,
then: annotateContract(srcCase.then, dstCase.then),
};
} else {
return dstCase;
}
}),
});
}

throw new Error(`Cant annotate source contract.`);
}

function annotateEntry(key: string, contract: Core.Contract): Core.Contract {
const sourceContract = getSourceContract(sourceMap[key]);
return annotateContract(sourceContract, contract);
}

return {
main: closure.main,
contracts: M.mapWithIndex(annotateEntry)(closure.contracts),
};
}

export interface SourceMap<T> {
source: ContractBundleMap<T>;
closure: ContractClosure;
annotateHistory(history: SingleInputTx[]): SingleInputTx[];
playHistory(history: SingleInputTx[]): TransactionOutput;
createContract(
options: CreateContractRequestBase
): Promise<[ContractId, TxId]>;
contractInstanceOf(contractId: ContractId): Promise<boolean>;
}

export async function mkSourceMap<T>(
lifecycle: RuntimeLifecycle,
sourceObjectMap: ContractBundleMap<T>
): Promise<SourceMap<T>> {
const closure = await annotatedClosure(lifecycle.restClient, sourceObjectMap);
return {
source: sourceObjectMap,
closure,
annotateHistory: (history: SingleInputTx[]) => {
return annotateHistoryFromClosure(closure)(history);
},
playHistory: (history: SingleInputTx[]) => {
const annotatedHistory = annotateHistoryFromClosure(closure)(history);
const main = closure.contracts.get(closure.main);
if (typeof main === "undefined") throw new Error(`Cant find main.`);
return playSingleInputTxTrace(0n, main, annotatedHistory);
},
createContract: (options: CreateContractRequestBase) => {
const contract = stripAnnotations(closure.contracts.get(closure.main)!);
return lifecycle.contracts.createContract({
...options,
contract,
});
},
contractInstanceOf: async (contractId: ContractId) => {
const contractDetails = await lifecycle.restClient.getContractById({
contractId,
});

const initialContract = await lifecycle.restClient.getContractSourceById({
contractSourceId: closure.main,
});

return deepEqual(initialContract, contractDetails.initialContract);
},
};
}
Loading

0 comments on commit 8b1da40

Please sign in to comment.