Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PLT-9368: Strict mode for runtime-lifecycle #180

Merged
merged 11 commits into from
Jan 30, 2024
16 changes: 16 additions & 0 deletions changelog.d/20240126_162752_bjorn.wilhelm.kihlberg_PLT_9368.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
### @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))
- `createContractSources`
- `getContractById`
- `submitContract`
- `getTransactionsForContract`
- `submitContractTransaction`
- `getContractTransactionById`
- `getWithdrawalById`
- `submitWithdrawal`

### @marlowe.io/runtime-lifecycle

- `mkRuntimeLifecycle` 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))
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export async function getApplicableActions(
contractId: ContractId,
environment?: Environment
): Promise<ApplicableAction[]> {
const contractDetails = await restClient.getContractById(contractId);
const contractDetails = await restClient.getContractById({ contractId });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of await inside getApplicableActions without a try-catch block for error handling could lead to unhandled promise rejections if restClient.getContractById fails. Consider adding error handling to improve robustness.

+ try {
    const contractDetails = await restClient.getContractById({ contractId });
+ } catch (error) {
+   console.error("Failed to get contract details:", error);
+   throw error; // Rethrow or handle as appropriate for your application logic
+ }

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
const contractDetails = await restClient.getContractById({ contractId });
try {
const contractDetails = await restClient.getContractById({ contractId });
} catch (error) {
console.error("Failed to get contract details:", error);
throw error; // Rethrow or handle as appropriate for your application logic
}

const currentContract = contractDetails.currentContract
? contractDetails.currentContract
: contractDetails.initialContract;
Expand Down
10 changes: 6 additions & 4 deletions examples/nodejs/src/marlowe-object-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,8 +588,9 @@ async function validateExistingContract(
contractId: ContractId
): Promise<ValidationResults> {
// First we try to fetch the contract details and the required tags
const contractDetails =
await lifecycle.restClient.getContractById(contractId);
const contractDetails = await lifecycle.restClient.getContractById({
contractId,
});

const scheme = extractSchemeFromTags(contractDetails.tags);

Comment on lines 588 to 596
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 NOTE
This review was outside the diff hunks and was mapped to the diff hunk with the greatest overlap. Original lines [591-615]

Consider adding error handling for the asynchronous calls to getContractById, createContractSources, and getContractSourceById to manage potential failures gracefully.

+ try {
    const contractDetails = await lifecycle.restClient.getContractById({ contractId });
    ...
    const { contractSourceId } = await lifecycle.restClient.createContractSources({ bundle: contractBundle });
    ...
    const initialContract = await lifecycle.restClient.getContractSourceById({ contractSourceId });
+ } catch (error) {
+   console.error("Error fetching contract details or sources:", error);
+   throw error; // Rethrow or handle as appropriate for your application logic
+ }

Expand All @@ -609,8 +610,9 @@ async function validateExistingContract(
// Or this option which doesn't require runtime to runtime communication, and just requires
// the dapp to be able to recreate the same sources.
const contractBundle = mkDelayPayment(scheme);
const { contractSourceId } =
await lifecycle.restClient.createContractSources(contractBundle);
const { contractSourceId } = await lifecycle.restClient.createContractSources(
{ bundle: contractBundle }
);
const initialContract = await lifecycle.restClient.getContractSourceById({
contractSourceId,
});
Expand Down
10 changes: 6 additions & 4 deletions examples/survey-workshop/custodian/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ async function loadContract() {
log("Loading contract");
const contractId = document.getElementById("contract-id").value;
const restClient = await H.getRestClient();
const paginatedTxs = await restClient.getTransactionsForContract(contractId);
const paginatedTxs = await restClient.getTransactionsForContract({
contractId,
});
if (paginatedTxs.transactions.length !== 1) {
log(
"Expected 1 transaction for contract, got " +
Expand All @@ -33,10 +35,10 @@ async function loadContract() {
}
logJSON("txId", paginatedTxs.transactions[0].transactionId);
const txId = paginatedTxs.transactions[0].transactionId;
const answerTx = await restClient.getContractTransactionById(
const answerTx = await restClient.getContractTransactionById({
contractId,
txId
);
txId,
});

const answers = await getAnswers(answerTx);
logJSON("answers", answers);
Expand Down
10 changes: 5 additions & 5 deletions examples/vesting-flow/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -140,21 +140,21 @@ <h2>Console</h2>
const contractIdsAndDetails = await Promise.all(
contractIds.map((contractId) =>
restClient
.getContractById(contractId)
.getContractById({ contractId })
.then((details) => [contractId, details])
)
);
const contractIdsAndDetailsAndInputHistory = await Promise.all(
contractIdsAndDetails.map(([contractId, details]) =>
restClient
.getTransactionsForContract(contractId)
.getTransactionsForContract({ contractId })
.then((result) =>
Promise.all(
result.transactions.map((transaction) =>
restClient.getContractTransactionById(
restClient.getContractTransactionById({
contractId,
transaction.transactionId
)
txId: transaction.transactionId,
})
)
)
)
Expand Down
2 changes: 2 additions & 0 deletions jsdelivr-npm-importmap.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ const importMap = {
"https://cdn.jsdelivr.net/npm/@marlowe.io/marlowe-object@0.3.0-beta/dist/bundled/esm/marlowe-object.js",
"@marlowe.io/marlowe-object/guards":
"https://cdn.jsdelivr.net/npm/@marlowe.io/marlowe-object@0.3.0-beta/dist/bundled/esm/guards.js",
"@marlowe.io/marlowe-object/object":
"https://cdn.jsdelivr.net/npm/@marlowe.io/marlowe-object@0.3.0-beta/dist/bundled/esm/object.js",
"@marlowe.io/testing-kit":
"https://cdn.jsdelivr.net/npm/@marlowe.io/testing-kit@0.3.0-beta/dist/bundled/esm/testing-kit.js",
"lucid-cardano": "https://unpkg.com/lucid-cardano@0.10.7/web/mod.js",
Expand Down
18 changes: 18 additions & 0 deletions packages/adapter/src/io-ts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as t from "io-ts/lib/index.js";
import { withValidate } from "io-ts-types";
import { MarloweJSON } from "./codec.js";
import { Errors } from "io-ts/lib/index.js";
/**
* 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 @@ -91,3 +92,20 @@ export function expectType<T>(guard: t.Type<T>, aValue: unknown): T {
} but got ${MarloweJSON.stringify(aValue, null, 4)} `;
}
}

/**
* 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.
*/
export function strictDynamicTypeCheck(strict: unknown): strict is boolean {
return typeof strict === "boolean";
}

export class InvalidTypeError extends Error {
constructor(
public readonly errors: Errors,
message?: string
) {
super(message);
}
}
5 changes: 5 additions & 0 deletions packages/marlowe-object/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@
"import": "./dist/esm/guards.js",
"require": "./dist/bundled/cjs/guards.cjs",
"types": "./dist/esm/guards.d.ts"
},
"./object": {
"import": "./dist/esm/object.js",
"require": "./dist/bundled/cjs/object.cjs",
"types": "./dist/esm/object.d.ts"
}
},
"dependencies": {
Expand Down
60 changes: 58 additions & 2 deletions packages/runtime/client/rest/src/contract/endpoints/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import * as TE from "fp-ts/lib/TaskEither.js";
import { pipe } from "fp-ts/lib/function.js";
import * as E from "fp-ts/lib/Either.js";
import * as A from "fp-ts/lib/Array.js";
import * as O from "fp-ts/lib/Option.js";
import { formatValidationErrors } from "jsonbigint-io-ts-reporters";
import { stringify } from "qs";
import { assertGuardEqual, proxy } from "@marlowe.io/adapter/io-ts";
Expand All @@ -32,6 +31,7 @@ import {
unStakeAddressBech32,
SourceId,
SourceIdGuard,
AddressBech32Guard,
} from "@marlowe.io/runtime-core";

import { ContractHeader, ContractHeaderGuard } from "../header.js";
Expand All @@ -41,7 +41,12 @@ import {
} from "../rolesConfigurations.js";

import { ContractId, ContractIdGuard } from "@marlowe.io/runtime-core";
import { ItemRange, Page, PageGuard } from "../../pagination.js";
import {
ItemRange,
ItemRangeGuard,
Page,
PageGuard,
} from "../../pagination.js";

/**
* Request options for the {@link index.RestClient#getContracts | Get contracts } endpoint
Expand Down Expand Up @@ -69,6 +74,16 @@ export interface GetContractsRequest {
partyRoles?: AssetId[];
}

export const GetContractsRequestGuard = assertGuardEqual(
proxy<GetContractsRequest>(),
t.partial({
range: ItemRangeGuard,
tags: t.array(Tag),
partyAddresses: t.array(AddressBech32Guard),
partyRoles: t.array(AssetId),
})
);

export type GETHeadersByRange = (
range?: ItemRange
) => (kwargs: {
Expand Down Expand Up @@ -194,6 +209,31 @@ export type BuildCreateContractTxRequestWithContract = {
contract: Contract;
} & BuildCreateContractTxRequestOptions;

export const BuildCreateContractTxRequestOptionsGuard = assertGuardEqual(
proxy<BuildCreateContractTxRequestOptions>(),
t.intersection([
t.type({ changeAddress: AddressBech32Guard, version: MarloweVersion }),
t.partial({
roles: RolesConfigurationGuard,
threadRoleName: G.RoleName,
minimumLovelaceUTxODeposit: t.number,
metadata: Metadata,
tags: TagsGuard,
collateralUTxOs: t.array(TxOutRef),
usedAddresses: t.array(AddressBech32Guard),
stakeAddress: StakeAddressBech32,
}),
])
);

export const BuildCreateContractTxRequestWithContractGuard = assertGuardEqual(
proxy<BuildCreateContractTxRequestWithContract>(),
t.intersection([
t.type({ contract: G.Contract }),
BuildCreateContractTxRequestOptionsGuard,
])
);

/**
* Request for the {@link index.RestClient#buildCreateContractTx | Build Create Contract Tx } endpoint using a contract
* @category Endpoint : Build Create Contract Tx
Expand All @@ -206,6 +246,14 @@ export type BuildCreateContractTxRequestWithSourceId = {
sourceId: SourceId;
} & BuildCreateContractTxRequestOptions;

export const BuildCreateContractTxRequestWithSourceIdGuard = assertGuardEqual(
proxy<BuildCreateContractTxRequestWithSourceId>(),
t.intersection([
t.type({ sourceId: SourceIdGuard }),
BuildCreateContractTxRequestOptionsGuard,
])
);

/**
* Request options for the {@link index.RestClient#buildCreateContractTx | Build Create Contract Tx } endpoint
* @category Endpoint : Build Create Contract Tx
Expand Down Expand Up @@ -249,6 +297,14 @@ export type BuildCreateContractTxRequest =
| BuildCreateContractTxRequestWithContract
| BuildCreateContractTxRequestWithSourceId;

export const BuildCreateContractTxRequestGuard = assertGuardEqual(
proxy<BuildCreateContractTxRequest>(),
t.union([
BuildCreateContractTxRequestWithContractGuard,
BuildCreateContractTxRequestWithSourceIdGuard,
])
);

/**
* Request options for the {@link index.RestClient#buildCreateContractTx | Build Create Contract Tx } endpoint
* @category Endpoint : Build Create Contract Tx
Expand Down
30 changes: 29 additions & 1 deletion packages/runtime/client/rest/src/contract/endpoints/singleton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@ import * as HTTP from "@marlowe.io/adapter/http";
import { DecodingError } from "@marlowe.io/adapter/codec";

import {
ContractIdGuard,
HexTransactionWitnessSet,
TextEnvelope,
TextEnvelopeGuard,
transactionWitnessSetTextEnvelope,
} from "@marlowe.io/runtime-core";

import { ContractDetails, ContractDetailsGuard } from "../details.js";
import { ContractId } from "@marlowe.io/runtime-core";
import { unsafeEither, unsafeTaskEither } from "@marlowe.io/adapter/fp-ts";
import { assertGuardEqual, proxy } from "@marlowe.io/adapter/io-ts";

export type GET = (
contractId: ContractId
Expand All @@ -30,6 +33,14 @@ const GETPayload = t.type({
resource: ContractDetailsGuard,
});

/**
* Request options for the {@link index.RestClient#getContractById | Get contracts by ID } endpoint
*/
export type GetContractByIdRequest = t.TypeOf<typeof GetContractByIdRequest>;
export const GetContractByIdRequest = t.type({
contractId: ContractIdGuard,
});

/**
* @see {@link https://docs.marlowe.iohk.io/api/get-contract-by-id}
*/
Expand All @@ -56,7 +67,24 @@ export type PUT = (
hexTransactionWitnessSet: HexTransactionWitnessSet
) => TE.TaskEither<Error, void>;

export const submitContractViaAxios =
/**
* Request options for the {@link index.RestClient#submitContract | Submit contract } endpoint
* @category Endpoint : Submit contract
*/
export interface SubmitContractRequest {
contractId: ContractId;
txEnvelope: TextEnvelope;
}

export const SubmitContractRequestGuard = assertGuardEqual(
proxy<SubmitContractRequest>(),
t.type({
contractId: ContractIdGuard,
txEnvelope: TextEnvelopeGuard,
})
);

export const submitContract =
(axiosInstance: AxiosInstance) =>
(contractId: ContractId, envelope: TextEnvelope) =>
axiosInstance
Expand Down
28 changes: 28 additions & 0 deletions packages/runtime/client/rest/src/contract/endpoints/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,22 @@ import {
Label,
ContractSourceId,
ContractSourceIdGuard,
ContractBundle,
} from "@marlowe.io/marlowe-object";
import { AxiosInstance } from "axios";
import { ContractBundleGuard } from "@marlowe.io/marlowe-object/object";
import { assertGuardEqual, proxy } from "@marlowe.io/adapter/io-ts";

/**
* Request options for the {@link index.RestClient#createContractSources | Create contract sources } endpoint
* @category Endpoint : Create contract sources
*/
export interface CreateContractSourcesRequest {
bundle: ContractBundle;
}

export const CreateContractSourcesRequestGuard: t.Type<CreateContractSourcesRequest> =
t.type({ bundle: ContractBundleGuard });

export interface CreateContractSourcesResponse {
contractSourceId: ContractSourceId;
Expand Down Expand Up @@ -51,6 +65,14 @@ export interface GetContractBySourceIdRequest {
expand?: boolean;
}

export const GetContractBySourceIdRequestGuard = assertGuardEqual(
proxy<GetContractBySourceIdRequest>(),
t.intersection([
t.type({ contractSourceId: ContractSourceIdGuard }),
t.partial({ expand: t.boolean }),
])
);

export type GetContractBySourceIdResponse = Contract;

const GetContractBySourceIdResponseGuard: t.Type<GetContractBySourceIdResponse> =
Expand Down Expand Up @@ -81,6 +103,9 @@ export interface GetContractSourceAdjacencyRequest {
contractSourceId: ContractSourceId;
}

export const GetContractSourceAdjacencyRequestGuard: t.Type<GetContractSourceAdjacencyRequest> =
t.type({ contractSourceId: ContractSourceIdGuard });

export interface GetContractSourceAdjacencyResponse {
results: ContractSourceId[];
}
Expand Down Expand Up @@ -111,6 +136,9 @@ export interface GetContractSourceClosureRequest {
contractSourceId: ContractSourceId;
}

export const GetContractSourceClosureRequestGuard: t.Type<GetContractSourceClosureRequest> =
t.type({ contractSourceId: ContractSourceIdGuard });

export interface GetContractSourceClosureResponse {
results: ContractSourceId[];
}
Expand Down
Loading
Loading