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-7701, PLT-7702, PLT-7703, PLT-8427: Implement remaining 1-1 endpoints #128

Merged
merged 12 commits into from
Dec 14, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
### @marlowe.io/runtime-rest-client

- PLT-7701: Extend the rest client with procedure `getContractSourceById`. (Implemented in [PR-128](https://github.com/input-output-hk/marlowe-ts-sdk/pull/128))
- PLT-7702: Extend the rest client with procedure `getContractSourceAdjacency`. (Implemented in [PR-128](https://github.com/input-output-hk/marlowe-ts-sdk/pull/128))
- PLT-7703: Extend the rest client with procedure `getContractSourceClosure`. (Implemented in [PR-128](https://github.com/input-output-hk/marlowe-ts-sdk/pull/128))
- PLT-8427: Extend the rest client with procedure `getNextStepsForContract`. (Implemented in [PR-128](https://github.com/input-output-hk/marlowe-ts-sdk/pull/128))
2 changes: 1 addition & 1 deletion examples/js/poc-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function logJSON(message, json) {
export function getRuntimeUrl() {
const runtimeUrlInput = document.getElementById("runtimeUrl");
return (
runtimeUrlInput.value ||
(runtimeUrlInput && runtimeUrlInput.value) ||
"https://marlowe-runtime-preprod-web.demo.scdev.aws.iohkdev.io/"
);
}
Expand Down
37 changes: 37 additions & 0 deletions examples/rest-client-flow/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,43 @@ <h2>Request</h2>
>/contracts/:contractId/transactions/:transactionId</a
>
<br />
<input
id="getContractSourceById"
type="button"
value="Get contract source by contract Source id"
class="endpoint"
/>
GETs contract source by contract Source ID
<a
href="https://input-output-hk.github.io/marlowe-ts-sdk/interfaces/_marlowe_io_runtime_rest_client.index.RestClient.html#getContractSourceById"
>/contracts/sources/:contractSourceId</a
>
<br />
<input
id="getContractSourceAdjacency"
type="button"
value="Get adjacent contract Source IDs"
class="endpoint"
/>
GETs the contract Source IDs which are adjacent to a contract
<a
href="https://input-output-hk.github.io/marlowe-ts-sdk/interfaces/_marlowe_io_runtime_rest_client.index.RestClient.html#getContractSourceAdjacency"
>/contracts/sources/:contractSourceId/adjacency</a
>
<br />
<input
id="getContractSourceClosure"
type="button"
value="Get contract Source IDs in contract"
class="endpoint"
/>
GETs the contract Source IDs which appear in the full hierarchy of a
contract source
<a
href="https://input-output-hk.github.io/marlowe-ts-sdk/interfaces/_marlowe_io_runtime_rest_client.index.RestClient.html#getContractSourceClosure"
>/contracts/sources/:contractSourceId/closure</a
>
<br />
</div>

<h2>Console</h2>
Expand Down
93 changes: 92 additions & 1 deletion packages/runtime/client/rest/src/contract/endpoints/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as E from "fp-ts/lib/Either.js";
import { pipe } from "fp-ts/lib/function.js";
import { formatValidationErrors } from "jsonbigint-io-ts-reporters";

import { BuiltinByteString } from "@marlowe.io/language-core-v1";
import { BuiltinByteString, Contract } from "@marlowe.io/language-core-v1";
import { Bundle, Label } from "@marlowe.io/marlowe-object";
import { AxiosInstance } from "axios";

Expand Down Expand Up @@ -40,3 +40,94 @@ export const createContractSources = (axiosInstance: AxiosInstance) => {
);
};
};

export interface GetContractBySourceIdRequest {
contractSourceId: BuiltinByteString;
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 BuiltinByteString directly as a type for contractSourceId is noted. Consider revisiting the previous discussion on using a type alias or branded type to clarify intent and ensure type safety.

expand?: boolean;
}

export type GetContractBySourceIdResponse = Contract;

const GetContractBySourceIdResponseGuard: t.Type<GetContractBySourceIdResponse> =
G.Contract;

export const getContractSourceById =
(axiosInstance: AxiosInstance) =>
async ({
contractSourceId,
expand,
}: GetContractBySourceIdRequest): Promise<GetContractBySourceIdResponse> => {
const response = await axiosInstance.get(
`/contracts/sources/${encodeURIComponent(contractSourceId)}`,
{ params: { expand } }
);
Comment on lines +65 to +68
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider adding error handling for the HTTP request to handle network errors or non-200 status codes.

Also applies to: 96-98, 126-128

return pipe(
GetContractBySourceIdResponseGuard.decode(response.data),
E.match(
(e) => {
throw formatValidationErrors(e);
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider improving error handling by wrapping validation errors in a user-friendly message or a custom error type to avoid exposing internal details and to provide more informative messages to the caller.

Also applies to: 103-103, 133-133

},
(e) => e
)
);
};
bjornkihlberg marked this conversation as resolved.
Show resolved Hide resolved

export interface GetContractSourceAdjacencyRequest {
contractSourceId: BuiltinByteString;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I believe a type alias might clarify the intent.

type ContractSourceId = string 
// or 
type ContractSourceId = BuiltinByteString

We could asses also if a Branded type makes sense to show that not all strings are the same

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Have we figured out yet how we want to do branded types?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, using t.brand from io-ts

Copy link
Contributor

Choose a reason for hiding this comment

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

Reconsider the implementation of a type alias for BuiltinByteString to improve code readability and maintainability, as previously discussed.

}

export interface GetContractSourceAdjacencyResponse {
results: BuiltinByteString[];
Copy link
Collaborator

Choose a reason for hiding this comment

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

This shows the importance of the type alias

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair point.

}

const GetContractSourceAdjacencyResponseGuard: t.Type<GetContractSourceAdjacencyResponse> =
t.type({ results: t.array(G.BuiltinByteString) });

export const getContractSourceAdjacency =
(axiosInstance: AxiosInstance) =>
async ({
contractSourceId,
}: GetContractSourceAdjacencyRequest): Promise<GetContractSourceAdjacencyResponse> => {
const response = await axiosInstance.get(
`/contracts/sources/${encodeURIComponent(contractSourceId)}/adjacency`
);
return pipe(
GetContractSourceAdjacencyResponseGuard.decode(response.data),
E.match(
(e) => {
throw formatValidationErrors(e);
},
(e) => e
)
);
};
bjornkihlberg marked this conversation as resolved.
Show resolved Hide resolved

export interface GetContractSourceClosureRequest {
contractSourceId: BuiltinByteString;
}

export interface GetContractSourceClosureResponse {
results: BuiltinByteString[];
}

const GetContractSourceClosureResponseGuard: t.Type<GetContractSourceClosureResponse> =
t.type({ results: t.array(G.BuiltinByteString) });

export const getContractSourceClosure =
(axiosInstance: AxiosInstance) =>
async ({
contractSourceId,
}: GetContractSourceClosureRequest): Promise<GetContractSourceClosureResponse> => {
const response = await axiosInstance.get(
`/contracts/sources/${encodeURIComponent(contractSourceId)}/closure`
);
return pipe(
GetContractSourceClosureResponseGuard.decode(response.data),
E.match(
(e) => {
throw formatValidationErrors(e);
},
(e) => e
)
);
};
bjornkihlberg marked this conversation as resolved.
Show resolved Hide resolved
45 changes: 45 additions & 0 deletions packages/runtime/client/rest/src/contract/next/endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,48 @@ export const getViaAxios: (axiosInstance: AxiosInstance) => GET =

const contractNextEndpoint = (contractId: ContractId): string =>
`/contracts/${encodeURIComponent(unContractId(contractId))}/next`;

export interface GetNextStepsForContractRequest {
contractId: ContractId;
validityStart: bigint;
validityEnd: bigint;
parties?: Party[];
}

export type GetNextStepsForContractResponse = Next;

const GetNextStepsForContractResponseGuard = Next;
bjornkihlberg marked this conversation as resolved.
Show resolved Hide resolved

export const getNextStepsForContract =
(axiosInstance: AxiosInstance) =>
async ({
contractId,
validityStart,
validityEnd,
parties,
}: GetNextStepsForContractRequest): Promise<GetNextStepsForContractResponse> => {
const response = await axiosInstance.get(
`/contracts/${encodeURIComponent(
unContractId(contractId)
)}/next?${stringify({
validityStart: posixTimeToIso8601(validityStart),
validityEnd: posixTimeToIso8601(validityEnd),
party: parties,
})}`,
{
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
}
);
return pipe(
GetNextStepsForContractResponseGuard.decode(response.data),
E.match(
(e) => {
throw formatValidationErrors(e);
},
(e) => e
)
);
bjornkihlberg marked this conversation as resolved.
Show resolved Hide resolved
};
70 changes: 57 additions & 13 deletions packages/runtime/client/rest/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ import * as Contracts from "./contract/endpoints/collection.js";
import * as Transaction from "./contract/transaction/endpoints/singleton.js";
import * as Transactions from "./contract/transaction/endpoints/collection.js";
import * as Sources from "./contract/endpoints/sources.js";
import * as Next from "./contract/next/endpoint.js";
import { TransactionsRange } from "./contract/transaction/endpoints/collection.js";
import * as ContractNext from "./contract/next/endpoint.js";
import { unsafeTaskEither } from "@marlowe.io/adapter/fp-ts";
import {
ContractId,
Expand All @@ -40,8 +40,6 @@ import {
import { submitContractViaAxios } from "./contract/endpoints/singleton.js";
import { ContractDetails } from "./contract/details.js";
import { TransactionDetails } from "./contract/transaction/details.js";
import { CreateContractSourcesResponse } from "./contract/endpoints/sources.js";
import { BuildCreateContractTxRequestWithContract } from "./contract/index.js";
// import curlirize from 'axios-curlirize';

/**
Expand Down Expand Up @@ -89,14 +87,52 @@ export interface RestClient {
createContractSources(
mainId: Label,
bundle: Bundle
): Promise<CreateContractSourcesResponse>;
): Promise<Sources.CreateContractSourcesResponse>;

/**
* Gets the contract associated with given source id
* @throws DecodingError - If the response from the server can't be decoded
* @see {@link https://docs.marlowe.iohk.io/api/get-contract-source-by-id | The backend documentation}
*/
getContractSourceById(
request: Sources.GetContractBySourceIdRequest
): Promise<Sources.GetContractBySourceIdResponse>;

/**
* Get the contract source IDs which are adjacent to a contract source (they appear directly in the contract source).
* @throws DecodingError - If the response from the server can't be decoded
* @see {@link https://docs.marlowe.iohk.io/api/get-adjacent-contract-source-i-ds-by-id | The backend documentation}
*/
getContractSourceAdjacency(
request: Sources.GetContractSourceAdjacencyRequest
): Promise<Sources.GetContractSourceAdjacencyResponse>;

/**
* Get the contract source IDs which appear in the full hierarchy of a contract source (including the ID of the contract source itself).
* @throws DecodingError - If the response from the server can't be decoded
* @see {@link https://docs.marlowe.iohk.io/api/get-contract-source-closure-by-id | The backend documentation}
*/
getContractSourceClosure(
request: Sources.GetContractSourceClosureRequest
): Promise<Sources.GetContractSourceClosureResponse>;

/**
* Get inputs which could be performed on a contract within a time range by the requested parties.
* @throws DecodingError - If the response from the server can't be decoded
* @see {@link https://docs.marlowe.iohk.io/api/get-next-contract-steps | The backend documentation}
*/
getNextStepsForContract(
request: Next.GetNextStepsForContractRequest
): Promise<Next.GetNextStepsForContractResponse>;

/**
* Gets a single contract by id
* @param contractId The id of the contract to get
* @throws DecodingError - If the response from the server can't be decoded
* @see {@link https://docs.marlowe.iohk.io/api/get-contract-by-id | The backend documentation}
*/
getContractById(contractId: ContractId): Promise<ContractDetails>;

/**
* Submits a signed contract creation transaction
* @see {@link https://docs.marlowe.iohk.io/api/submit-contract-to-chain | The backend documentation}
Expand All @@ -105,6 +141,7 @@ export interface RestClient {
contractId: ContractId,
txEnvelope: TextEnvelope
): Promise<void>;

/**
* Gets a paginated list of {@link contract.TxHeader } for a given contract.
* @see {@link https://docs.marlowe.iohk.io/api/get-transactions-for-contract | The backend documentation }
Expand All @@ -116,6 +153,7 @@ export interface RestClient {
contractId: ContractId,
range?: TransactionsRange
): Promise<Transactions.GetTransactionsForContractResponse>;

/**
* Create an unsigned transaction which applies inputs to a contract.
* @see {@link https://docs.marlowe.iohk.io/api/apply-inputs-to-contract | The backend documentation}
Expand All @@ -126,6 +164,7 @@ export interface RestClient {
applyInputsToContract(
request: Transactions.ApplyInputsToContractRequest
): Promise<Transactions.TransactionTextEnvelope>;

// getTransactionById: Transaction.GET; // - https://docs.marlowe.iohk.io/api/get-transaction-by-id
/**
* Submit a signed transaction (generated with {@link @marlowe.io/runtime-rest-client!index.RestClient#applyInputsToContract} and signed with the {@link @marlowe.io/wallet!api.WalletAPI#signTx} procedure) that applies inputs to a contract.
Expand Down Expand Up @@ -195,13 +234,6 @@ export interface RestClient {
*/
healthcheck(): Promise<Boolean>;
Copy link
Contributor

Choose a reason for hiding this comment

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

The healthcheck method in mkRestClient returns a promise that resolves to a boolean, which is inconsistent with the healthcheck method in the FPTSRestAPI interface that returns a TE.TaskEither. Consider aligning the return types for consistency.


// getNextStepsForContract: Next.GET; // - Jamie, is it this one? https://docs.marlowe.iohk.io/api/get-transaction-output-by-id? if so lets unify

// postContractSource: ContractSources.POST; // - Jamie, is it this one? https://docs.marlowe.iohk.io/api/access-contract-import if so lets unify
// getContractSource: ContractSource.GET; // - Jamie, is it this one? https://docs.marlowe.iohk.io/api/get-contract-import-by-id
// getContractAdjacency: ContractSource.GET_ADJACENCY; // - Jamie, is it this one? https://docs.marlowe.iohk.io/api/get-adjacency-by-id if so lets unify
// getContractClosure: ContractSource.GET_CLOSURE; // Jamie is it this one? - https://docs.marlowe.iohk.io/api/get-closure-by-id

/**
* Get payouts to parties from role-based contracts.
* @see {@link https://docs.marlowe.iohk.io/api/get-role-payouts | The backend documentation}
Expand Down Expand Up @@ -280,6 +312,18 @@ export function mkRestClient(baseURL: string): RestClient {
createContractSources(mainId, bundle) {
return Sources.createContractSources(axiosInstance)(mainId, bundle);
},
getContractSourceById(request) {
return Sources.getContractSourceById(axiosInstance)(request);
},
getContractSourceAdjacency(request) {
return Sources.getContractSourceAdjacency(axiosInstance)(request);
},
getContractSourceClosure(request) {
return Sources.getContractSourceClosure(axiosInstance)(request);
},
getNextStepsForContract(request) {
return Next.getNextStepsForContract(axiosInstance)(request);
},
submitContract(contractId, txEnvelope) {
return submitContractViaAxios(axiosInstance)(contractId, txEnvelope);
},
Expand Down Expand Up @@ -470,7 +514,7 @@ export interface ContractsAPI {
/**
* @see {@link }
*/
next: ContractNext.GET;
next: Next.GET;
Copy link
Contributor

Choose a reason for hiding this comment

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

The next property in the ContractsAPI interface is assigned a Next.GET type, but there is no corresponding implementation in the mkFPTSRestClient function. Add the missing implementation to match the interface.

+        next: Next.getViaAxios(axiosInstance),

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
next: Next.GET;
next: Next.getViaAxios(axiosInstance);

transactions: {
/**
* @see {@link }
Expand Down Expand Up @@ -550,7 +594,7 @@ export function mkFPTSRestClient(baseURL: string): FPTSRestAPI {
contract: {
get: Contract.getViaAxios(axiosInstance),
put: Contract.putViaAxios(axiosInstance),
next: ContractNext.getViaAxios(axiosInstance),
next: Next.getViaAxios(axiosInstance),
transactions: {
getHeadersByRange:
Transactions.getHeadersByRangeViaAxios(axiosInstance),
Expand Down