Skip to content

Commit

Permalink
Merge pull request #188 from input-output-hk/hrajchert/runtime-lifecy…
Browse files Browse the repository at this point in the history
…cle-refactor

New runtime lifecycle contracts API & Initial Account Deposits (runtime v1.0.0)
  • Loading branch information
nhenin authored May 11, 2024
2 parents 02f07d5 + a058f7f commit 2ef0439
Show file tree
Hide file tree
Showing 36 changed files with 1,327 additions and 1,407 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
### General

- Feat: **Initial Account Deposits Feature Integration (Runtime v1.0.0):**

- **Purpose:** This update introduces the capability for users to make initial deposits into their accounts upon creation. This feature aims to streamline the account setup process and enhance user experience.
- **Benefits:** This feature squashes the Contract Creation and Initial Input Deposits into 1 transaction instead of multiple ones.

- Feat: **Introduction of a New Contract API in the Runtime Lifecycle API:**
- **Purpose:** The addition of a new Contract API is designed to provide developers with more flexibility and control (contract instance concept) over smart contract management within the runtime environment.
- **Benefits:** Developers can now leverage enhanced functionalities for deploying, updating, and interacting with smart contracts. This API simplifies complex contract operations and supports more robust smart contract development.

### @marlowe.io/runtime-rest-client

- Feat: `initial account deposits` (runtime v1.0.0) for Contract Creation (`BuildCreateContractTxRequest` via `buildCreateContractTx`):([PR-188](https://github.com/input-output-hk/marlowe-ts-sdk/pull/188))

### @marlowe.io/runtime-core

- Feat: `initial account deposits` (runtime v1.0.0) for Contract Creation ([PR-188](https://github.com/input-output-hk/marlowe-ts-sdk/pull/188)):
- Added `export type AccountDeposits = { [key in AddressOrRole]: AssetsMap };` and associated utility functions.

### @marlowe.io/runtime-lifecycle

- Feat: New Contract API `packages/runtime/lifecycle/src/generic/new-contract-api.ts` ([PR-188](https://github.com/input-output-hk/marlowe-ts-sdk/pull/188)):
- Generic `waitConfirmation()` : same for contract creation and apply inputs
- Seamless Integration of Applicable Actions API
- simplfied interface (`create` and `load` with a concept of `ContractInstance` object)
- see end-to-end tests for examples (e.g : `swap.ada.token.e2e.spec.ts`)
- Feat: `initial account deposits` feature (runtime v1.0.0) for Contract Creation ([PR-188](https://github.com/input-output-hk/marlowe-ts-sdk/pull/188)):
- new parameter field `accountDeposits` in
- e.g.

```ts
const sellerContractInstance = await sellerLifecycle.newContractAPI.create({
contract: swapContract,
roles: { [scheme.ask.buyer.role_token]: mintRole("OpenRole") },
accountDeposits: mkaccountDeposits([[scheme.offer.seller, seller.assetsProvisioned]]),
});
```

### @marlowe.io/language-examples

- Feat: `Atomic swap v2` : Simplified version using the new runtime `v1.0.0` feature (`initial account deposits`)
- see end-to-end tests for examples (e.g : `swap.ada.token.e2e.spec.ts`)
7 changes: 3 additions & 4 deletions examples/nodejs/src/escrow-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,14 @@ async function main(action: "buy" | "sell", otherAddress: string, amount: number
console.log("Mediator: " + Mediator);
console.log("Amount: " + amount);

const [contractId, txId] = await runtime.contracts.createContract({
const contractInstance = await runtime.newContractAPI.create({
contract: escrow,
roles: { Buyer, Seller, Mediator },
});

console.log("Contract ID: " + contractId);
console.log("Transaction ID: " + txId);
console.log("Contract ID: " + contractInstance.id);

console.log("Waiting for confirmation...");
await wallet.waitConfirmation(txId);
await contractInstance.waitForConfirmation();
console.log("Contract created successfully!");
}
15 changes: 10 additions & 5 deletions examples/nodejs/src/experimental-features/source-map.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
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 { ContractInstanceAPI, 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 {
SingleInputTx,
TransactionOutput,
emptyState,
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";
Expand Down Expand Up @@ -154,7 +159,7 @@ export interface SourceMap<T> {
closure: ContractClosure;
annotateHistory(history: SingleInputTx[]): SingleInputTx[];
playHistory(history: SingleInputTx[]): TransactionOutput;
createContract(options: CreateContractRequestBase): Promise<[ContractId, TxId]>;
createContract(options: CreateContractRequestBase): Promise<ContractInstanceAPI>;
contractInstanceOf(contractId: ContractId): Promise<boolean>;
}

Expand All @@ -173,11 +178,11 @@ export async function mkSourceMap<T>(
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);
return playSingleInputTxTrace(emptyState(0n), main, annotatedHistory);
},
createContract: (options: CreateContractRequestBase) => {
const contract = stripAnnotations(closure.contracts.get(closure.main)!);
return lifecycle.contracts.createContract({
return lifecycle.newContractAPI.create({
...options,
contract,
});
Expand Down
52 changes: 25 additions & 27 deletions examples/nodejs/src/marlowe-object-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
*/
import { mkLucidWallet, WalletAPI } from "@marlowe.io/wallet";
import { mkRuntimeLifecycle } from "@marlowe.io/runtime-lifecycle";
import { ContractInstanceAPI } from "@marlowe.io/runtime-lifecycle/api";
import { Lucid, Blockfrost, C } from "lucid-cardano";
import { readConfig } from "./config.js";
import { datetoTimeout, When } from "@marlowe.io/language-core-v1";
import {
addressBech32,
contractId,
ContractId,
contractIdToTxId,
stakeAddressBech32,
StakeAddressBech32,
TxId,
Expand Down Expand Up @@ -171,17 +173,17 @@ async function createContractMenu(lifecycle: RuntimeLifecycle, rewardAddress?: S
};
const metadata = delayPaymentTemplate.toMetadata(scheme);
const sourceMap = await mkSourceMap(lifecycle, mkDelayPayment(scheme));
const [contractId, txId] = await sourceMap.createContract({
const contractInstance = await sourceMap.createContract({
stakeAddress: rewardAddress,
tags: { DELAY_PAYMENT_VERSION: "2" },
metadata,
});

console.log(`Contract created with id ${contractId}`);

await waitIndicator(lifecycle.wallet, txId);
await waitIndicator(lifecycle.wallet, contractIdToTxId(contractInstance.id));

return contractMenu(lifecycle, scheme, sourceMap, contractId);
return contractMenu(lifecycle.wallet, contractInstance, scheme, sourceMap);
}

/**
Expand Down Expand Up @@ -213,43 +215,39 @@ async function loadContractMenu(lifecycle: RuntimeLifecycle) {
console.log(` * Amount: ${validationResult.scheme.amount} lovelaces`);
console.log(` * Deposit deadline: ${validationResult.scheme.depositDeadline}`);
console.log(` * Release deadline: ${validationResult.scheme.releaseDeadline}`);

return contractMenu(lifecycle, validationResult.scheme, validationResult.sourceMap, cid);
const contractInstance = await lifecycle.newContractAPI.load(cid);
return contractMenu(lifecycle.wallet, contractInstance, validationResult.scheme, validationResult.sourceMap);
}

/**
* This is an Inquirer.js flow to interact with a contract
*/
async function contractMenu(
lifecycle: RuntimeLifecycle,
wallet: WalletAPI,
contractInstance: ContractInstanceAPI,
scheme: DelayPaymentParameters,
sourceMap: SourceMap<DelayPaymentAnnotations>,
contractId: ContractId
sourceMap: SourceMap<DelayPaymentAnnotations>
): Promise<void> {
// Get and print the contract logical state.
const inputHistory = await lifecycle.contracts.getInputHistory(contractId);

const inputHistory = await contractInstance.getInputHistory();
const contractState = getState(datetoTimeout(new Date()), inputHistory, sourceMap);
if (contractState.type === "Closed") return;

printState(contractState, scheme);

// See what actions are applicable to the current contract state
const { contractDetails, actions } = await lifecycle.applicableActions.getApplicableActions(contractId);

if (contractDetails.type === "closed") return;

const myActionsFilter = await lifecycle.applicableActions.mkFilter(contractDetails);
const myActions = actions.filter(myActionsFilter);
const applicableActions = await contractInstance.evaluateApplicableActions();

const choices: Array<{
name: string;
value: CanDeposit | CanAdvance | { actionType: "check-state" } | { actionType: "return" };
value: CanDeposit | CanAdvance | { type: "check-state" } | { type: "return" };
}> = [
{
name: "Re-check contract state",
value: { actionType: "check-state" },
value: { type: "check-state" },
},
...myActions.map((action) => {
switch (action.actionType) {
...applicableActions.myActions.map((action) => {
switch (action.type) {
case "Advance":
return {
name: "Close contract",
Expand All @@ -271,29 +269,29 @@ async function contractMenu(
}),
{
name: "Return to main menu",
value: { actionType: "return" },
value: { type: "return" },
},
];

const selectedAction = await select({
message: "Contract menu",
choices,
});
switch (selectedAction.actionType) {
switch (selectedAction.type) {
case "check-state":
return contractMenu(lifecycle, scheme, sourceMap, contractId);
return contractMenu(wallet, contractInstance, scheme, sourceMap);
case "return":
return;
case "Advance":
case "Deposit":
console.log("Applying input");
const applicableInput = await lifecycle.applicableActions.getInput(contractDetails, selectedAction);
const txId = await lifecycle.applicableActions.applyInput(contractId, {
const applicableInput = await applicableActions.toInput(selectedAction);
const txId = await applicableActions.apply({
input: applicableInput,
});
console.log(`Input applied with txId ${txId}`);
await waitIndicator(lifecycle.wallet, txId);
return contractMenu(lifecycle, scheme, sourceMap, contractId);
await waitIndicator(wallet, txId);
return contractMenu(wallet, contractInstance, scheme, sourceMap);
}
}

Expand Down
13 changes: 5 additions & 8 deletions examples/rest-client-flow/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ <h2>Request</h2>
This should be filled with a JSON object that starts with an array, where each element is a numbered
parameter.
</p>
<textarea id="parameter-json" type="text" style="width: 100%; height: 20em">[]</textarea>
<textarea id="parameter-json" type="text" style="width: 100%; height: 20em">{}</textarea>
</div>
<br />
<input id="healthcheck" type="button" value="Healthcheck" class="endpoint" />
Expand Down Expand Up @@ -148,7 +148,7 @@ <h2>Console</h2>
switch (action) {
case "getContracts":
log(`Getting contracts from ${H.getRuntimeUrl()}`);
result = await restClient.getContracts(...params);
result = await restClient.getContracts(params);
console.log("Contracts", result);
const nextRange = result.nextRange?.value ?? "-";
const prevRange = result.prevRange?.value ?? "-";
Expand All @@ -158,12 +158,12 @@ <h2>Console</h2>
break;
case "submitContract":
log(`Submitting contract on ${H.getRuntimeUrl()}`);
await restClient.submitContract(...getParams());
await restClient.submitContract(getParams());
log(`Done`);
break;
default:
log(`Calling ${action} on ${H.getRuntimeUrl()}`);
result = await restClient[action](...params);
result = await restClient[action](params);
logJSON("Result:", result);
}
} catch (e) {
Expand All @@ -182,10 +182,7 @@ <h2>Console</h2>
try {
params = JSON.parse(jsonParams);
} catch (e) {
throw new Error("Parameters must be a valid JSON array: " + e);
}
if (!Array.isArray(params)) {
throw new Error("Parameters must be an array");
throw new Error("Parameters must be a valid JSON: " + e);
}
return params;
}
Expand Down
31 changes: 25 additions & 6 deletions packages/adapter/src/io-ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Errors } from "io-ts/lib/index.js";
import { Refinement } from "fp-ts/lib/Refinement.js";
import { pipe } from "fp-ts/lib/function.js";
import * as Either from "fp-ts/lib/Either.js";
import * as BigIntReporter from "jsonbigint-io-ts-reporters";

/**
* In the TS-SDK we duplicate the type and guard definition for each type as the
* inferred type from io-ts does not produce a good type export when used with
Expand Down Expand Up @@ -88,20 +90,37 @@ export function expectType<T>(guard: t.Type<T>, aValue: unknown): T {
}

/**
* A mechanism for validating the type of a strict in a dynamically type context.
* @param strict Whether to perform runtime checking to provide helpful error messages. May have a slight negative performance impact.
* Formats validation errors into a string.
* @param errors - The validation errors to format.
* @returns A string representation of the validation errors.
*/
export function strictDynamicTypeCheck(strict: unknown): strict is boolean {
return typeof strict === "boolean";
export function formatValidationErrors(errors: Errors): string {
return BigIntReporter.formatValidationErrors(errors, {
truncateLongTypes: true,
}).join("\n");
}

export function dynamicAssertType<G extends t.Any>(guard: G, value: unknown, message?: string): t.TypeOf<G> {
const result = guard.decode(value);
if (Either.isLeft(result)) {
throw new InvalidTypeError(guard, value, result.left, message);
}
return result.right;
}

/**
* This error is thrown when we are dynamicly checking for the type of a value and the type is not
* the expected one.
*/
export class InvalidTypeError extends Error {
constructor(
public readonly guard: t.Any,
public readonly value: unknown,
public readonly errors: Errors,
public readonly value: any,
message?: string
) {
super(message);
const msg = message ?? `Unexpected type for value:\n${formatValidationErrors(errors)}`;
super(msg);
}
}

Expand Down
3 changes: 0 additions & 3 deletions packages/adapter/src/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ export const waitForPredicatePromise = async (
// Predicate is already true, no need to wait
return;
}
// Use a promise to wait for the specified interval
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

// Wait for the specified interval
await sleep(seconds);

Expand Down
8 changes: 4 additions & 4 deletions packages/language/core/v1/src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const mkEnvironment =
});

/**
* TODO: Comment
* Time interval in which the contract is executed. It is defined by a start and end time. The time is represented as a POSIX time.
* @see Appendix E.16 of the {@link https://github.com/input-output-hk/marlowe/releases/download/v3/Marlowe.pdf | Marlowe specification}
* @category Environment
*/
Expand All @@ -22,7 +22,7 @@ export interface TimeInterval {
}

/**
* TODO: Comment
* Guard for {@link TimeInterval}
* @see Appendix E.16 of the {@link https://github.com/input-output-hk/marlowe/releases/download/v3/Marlowe.pdf | Marlowe specification}
* @category Environment
*/
Expand All @@ -32,7 +32,7 @@ export const TimeIntervalGuard: t.Type<TimeInterval> = t.type({
});

/**
* TODO: Comment
* Time interval in which the contract is executed.
* @see Section 2.1.10 and appendix E.22 of the {@link https://github.com/input-output-hk/marlowe/releases/download/v3/Marlowe.pdf | Marlowe specification}
* @category Environment
*/
Expand All @@ -41,7 +41,7 @@ export interface Environment {
}

/**
* TODO: Comment
* Guard for {@link Environment}
* @see Section 2.1.10 and appendix E.22 of the {@link https://github.com/input-output-hk/marlowe/releases/download/v3/Marlowe.pdf | Marlowe specification}
* @category Environment
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/language/core/v1/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export {
MerkleizedNotify,
} from "./inputs.js";

export { role, Party, Address, Role, RoleName } from "./participants.js";
export { role, Party, Address, Role, RoleName, partiesToStrings, partyToString } from "./participants.js";

export { Payee, PayeeAccount, PayeeParty, AccountId } from "./payee.js";

Expand Down
Loading

0 comments on commit 2ef0439

Please sign in to comment.