From 256a9570e3b3e330abebee8f018e548b03e372bb Mon Sep 17 00:00:00 2001 From: Hernan Rajchert Date: Wed, 6 Mar 2024 12:27:13 -0300 Subject: [PATCH] start of the implementation of the new contract api --- packages/runtime/client/rest/src/index.ts | 4 +- packages/runtime/lifecycle/src/api.ts | 14 +- .../src/generic/applicable-actions.ts | 39 +---- .../src/generic/deprecated-contracts.ts | 112 ++++---------- .../lifecycle/src/generic/new-contract-api.ts | 143 ++++++++++++++++++ .../runtime/lifecycle/src/generic/runtime.ts | 2 + 6 files changed, 197 insertions(+), 117 deletions(-) create mode 100644 packages/runtime/lifecycle/src/generic/new-contract-api.ts diff --git a/packages/runtime/client/rest/src/index.ts b/packages/runtime/client/rest/src/index.ts index 3a360b0f..c5486cd6 100644 --- a/packages/runtime/client/rest/src/index.ts +++ b/packages/runtime/client/rest/src/index.ts @@ -732,7 +732,9 @@ export interface ContractsAPI { * @description Dependency Injection for the Rest Client API * @hidden */ -export type RestDI = { deprecatedRestAPI: FPTSRestAPI; restClient: RestClient }; +export type RestDI = { restClient: RestClient }; + +export type DeprecatedRestDI = { deprecatedRestAPI: FPTSRestAPI }; /** * @hidden diff --git a/packages/runtime/lifecycle/src/api.ts b/packages/runtime/lifecycle/src/api.ts index 38efcf18..c4c22239 100644 --- a/packages/runtime/lifecycle/src/api.ts +++ b/packages/runtime/lifecycle/src/api.ts @@ -6,7 +6,11 @@ import { PayoutId, PayoutWithdrawn, } from "@marlowe.io/runtime-core"; -import { RestClient, RestDI } from "@marlowe.io/runtime-rest-client"; +import { + DeprecatedRestDI, + RestClient, + RestDI, +} from "@marlowe.io/runtime-rest-client"; import { ApplicableActionsAPI, @@ -20,10 +24,12 @@ import { CanDeposit, CanNotify, GetApplicableActionsResponse, +} from "./generic/applicable-actions.js"; +import { ActiveContract, ClosedContract, ContractDetails, -} from "./generic/applicable-actions.js"; +} from "./generic/new-contract-api.js"; import { ContractsAPI, CreateContractRequestBase, @@ -45,6 +51,7 @@ export { ContractDetails, CreateContractRequestBase, }; +import * as NewContract from "./generic/new-contract-api.js"; /** * This is the main entry point of the @marlowe.io/runtime-lifecycle package. It provides a set of APIs to @@ -63,6 +70,7 @@ export interface RuntimeLifecycle { * Access to the low-level REST API as defined in the {@link @marlowe.io/runtime-rest-client! } package. It is re-exported here for convenience. */ restClient: RestClient; + newContractAPI: NewContract.ContractsAPI; /** * The contracts API is a high level API that lets you create and interact with Marlowe contracts. */ @@ -74,7 +82,7 @@ export interface RuntimeLifecycle { /** * @hidden */ -export type PayoutsDI = WalletDI & RestDI; +export type PayoutsDI = WalletDI & RestDI & DeprecatedRestDI; /** * @category PayoutsAPI diff --git a/packages/runtime/lifecycle/src/generic/applicable-actions.ts b/packages/runtime/lifecycle/src/generic/applicable-actions.ts index 33ea81de..e49876e4 100644 --- a/packages/runtime/lifecycle/src/generic/applicable-actions.ts +++ b/packages/runtime/lifecycle/src/generic/applicable-actions.ts @@ -45,6 +45,11 @@ import { WalletAPI } from "@marlowe.io/wallet"; import * as Big from "@marlowe.io/adapter/bigint"; import { ContractSourceId } from "@marlowe.io/marlowe-object"; import { posixTimeToIso8601 } from "@marlowe.io/adapter/time"; +import { + ActiveContract, + ContractDetails, + GetContractDetailsDI, +} from "./new-contract-api.js"; /** * @experimental @@ -834,37 +839,3 @@ function getApplicableActionFromCase( }); } } - -// #region High level Contract Details -/** - * @category New ContractsAPI - */ -export type ClosedContract = { - type: "closed"; -}; - -/** - * @category New ContractsAPI - */ -export type ActiveContract = { - type: "active"; - contractId: ContractId; - currentState: MarloweState; - currentContract: Contract; - roleTokenMintingPolicyId: PolicyId; -}; - -/** - * This is the start of a high level API to get the contract details. - * The current restAPI is not clear wether the details that you get are - * from a closed or active contract. This API is just the start to get - * getApplicableInputs ready in production, but as part of a ContractsAPI - * refactoring, the whole contract details should be modeled. - * @category New ContractsAPI - */ -export type ContractDetails = ClosedContract | ActiveContract; - -type GetContractDetailsDI = { - getContractDetails: (contractId: ContractId) => Promise; -}; -// #endregion diff --git a/packages/runtime/lifecycle/src/generic/deprecated-contracts.ts b/packages/runtime/lifecycle/src/generic/deprecated-contracts.ts index 23d17a28..c4f55dcf 100644 --- a/packages/runtime/lifecycle/src/generic/deprecated-contracts.ts +++ b/packages/runtime/lifecycle/src/generic/deprecated-contracts.ts @@ -34,6 +34,7 @@ import { RestClient, RestDI, ItemRange, + DeprecatedRestDI, } from "@marlowe.io/runtime-rest-client"; import { DecodingError } from "@marlowe.io/adapter/codec"; @@ -328,13 +329,13 @@ export function mkContractLifecycle( const di = { wallet, deprecatedRestAPI, restClient }; return { createContract: createContract(di), - applyInputs: applyInputsTx(di), + applyInputs: applyInputs(di), getApplicableInputs: getApplicableInputs(di), getContractIds: getContractIds(di), getInputHistory: getInputHistory(di), }; } -const getInputHistory = +export const getInputHistory = ({ restClient }: ContractsDI) => async (contractId: ContractId): Promise => { const transactionHeaders = await restClient.getTransactionsForContract({ @@ -390,7 +391,7 @@ const getInputHistory = .flat(); }; -const createContract = +export const createContract = ({ wallet, restClient }: ContractsDI) => async ( createContractRequest: CreateContractRequest @@ -445,21 +446,8 @@ const createContract = return [contractId, contractIdToTxId(contractId)]; }; -const applyInputsTx = - ({ wallet, deprecatedRestAPI }: ContractsDI) => - async ( - contractId: ContractId, - applyInputsRequest: ApplyInputsRequest - ): Promise => { - return unsafeTaskEither( - applyInputsTxFpTs(deprecatedRestAPI)(wallet)(contractId)( - applyInputsRequest - ) - ); - }; - const getApplicableInputs = - ({ wallet, deprecatedRestAPI }: ContractsDI) => + ({ wallet, deprecatedRestAPI }: ContractsDI & DeprecatedRestDI) => async (contractId: ContractId, environement: Environment): Promise => { const contractDetails = await unsafeTaskEither( deprecatedRestAPI.contracts.contract.get(contractId) @@ -479,7 +467,7 @@ const getApplicableInputs = }; const getContractIds = - ({ deprecatedRestAPI, wallet }: ContractsDI) => + ({ deprecatedRestAPI, wallet }: ContractsDI & DeprecatedRestDI) => async (): Promise => { const partyAddresses = [ await wallet.getChangeAddress(), @@ -525,64 +513,30 @@ const getParties: ( return roles.concat([changeAddress]).concat(usedAddresses); }; -export const applyInputsTxFpTs: ( - client: FPTSRestAPI -) => ( - wallet: WalletAPI -) => ( - contractId: ContractId -) => ( - applyInputsRequest: ApplyInputsRequest -) => TE.TaskEither = - (client) => (wallet) => (contractId) => (applyInputsRequest) => - pipe( - tryCatchDefault(() => getAddressesAndCollaterals(wallet)), - TE.chain((addressesAndCollaterals: AddressesAndCollaterals) => - client.contracts.contract.transactions.post( - contractId, - { - inputs: applyInputsRequest.inputs, - version: "v1", - tags: applyInputsRequest.tags ? applyInputsRequest.tags : {}, - metadata: applyInputsRequest.metadata - ? applyInputsRequest.metadata - : {}, - invalidBefore: applyInputsRequest.invalidBefore, - invalidHereafter: applyInputsRequest.invalidHereafter, - }, - addressesAndCollaterals - ) - ), - TE.chainW((transactionTextEnvelope: TransactionTextEnvelope) => - pipe( - tryCatchDefault(() => - wallet.signTx(transactionTextEnvelope.tx.cborHex) - ), - TE.chain((hexTransactionWitnessSet: HexTransactionWitnessSet) => - client.contracts.contract.transactions.transaction.put( - contractId, - transactionTextEnvelope.transactionId, - hexTransactionWitnessSet - ) - ), - TE.map(() => transactionTextEnvelope.transactionId) - ) - ) - ); - -export const applyInputsFpTs: ( - client: FPTSRestAPI -) => ( - wallet: WalletAPI -) => ( - contractId: ContractId -) => ( - applyInputsRequest: ApplyInputsRequest -) => TE.TaskEither = - (client) => (wallet) => (contractId) => (applyInputsRequest) => - pipe( - applyInputsTxFpTs(client)(wallet)(contractId)(applyInputsRequest), - TE.chainW((txId) => - tryCatchDefault(() => wallet.waitConfirmation(txId).then((_) => txId)) - ) - ); +export const applyInputs = + ({ wallet, restClient }: ContractsDI) => + async ( + contractId: ContractId, + applyInputsRequest: ApplyInputsRequest + ): Promise => { + const addressesAndCollaterals = await getAddressesAndCollaterals(wallet); + const envelope = await restClient.applyInputsToContract({ + contractId, + changeAddress: addressesAndCollaterals.changeAddress, + usedAddresses: addressesAndCollaterals.usedAddresses, + collateralUTxOs: addressesAndCollaterals.collateralUTxOs, + inputs: applyInputsRequest.inputs, + invalidBefore: applyInputsRequest.invalidBefore, + invalidHereafter: applyInputsRequest.invalidHereafter, + version: "v1", + metadata: applyInputsRequest.metadata, + tags: applyInputsRequest.tags, + }); + const signed = await wallet.signTx(envelope.tx.cborHex); + await restClient.submitContractTransaction({ + contractId, + transactionId: envelope.transactionId, + hexTransactionWitnessSet: signed, + }); + return envelope.transactionId; + }; diff --git a/packages/runtime/lifecycle/src/generic/new-contract-api.ts b/packages/runtime/lifecycle/src/generic/new-contract-api.ts new file mode 100644 index 00000000..82a9f3f2 --- /dev/null +++ b/packages/runtime/lifecycle/src/generic/new-contract-api.ts @@ -0,0 +1,143 @@ +import { + ContractId, + PolicyId, + TxId, + contractIdToTxId, +} from "@marlowe.io/runtime-core"; +import { + ApplyInputsRequest, + CreateContractRequest, + createContract, + applyInputs, + getInputHistory, +} from "./deprecated-contracts.js"; +import { SingleInputTx } from "@marlowe.io/language-core-v1/semantics"; +import { Contract, MarloweState } from "@marlowe.io/language-core-v1"; +import { RestDI } from "@marlowe.io/runtime-rest-client"; +import { WalletDI } from "@marlowe.io/wallet"; + +/** + * + * @description Dependency Injection for the Contract API + * @hidden + */ +export type ContractsDI = WalletDI & RestDI; + +/** + * TODO comment + * @category New ContractsAPI + */ +export interface ContractsAPI { + createContract( + createContractRequest: CreateContractRequest + ): Promise; + loadContract(contractId: ContractId): Promise; +} + +export function mkContractsAPI(di: ContractsDI): ContractsAPI { + return { + createContract: async (request) => { + const [contractId, _] = await createContract(di)(request); + return mkContractInstanceAPI(di, contractId); + }, + loadContract: async (contractId) => { + return mkContractInstanceAPI(di, contractId); + }, + }; +} + +/** + * TODO comment + * @category New ContractsAPI + */ +export interface ContractInstanceAPI { + contractId: ContractId; + waitForConfirmation: () => Promise; + getContractDetails: () => Promise; + applyInputs(applyInputsRequest: ApplyInputsRequest): Promise; + // TODO: ApplicableInputs + /** + * Get a list of the applied inputs for the contract + */ + getInputHistory(): Promise; +} + +function mkContractInstanceAPI( + di: ContractsDI, + contractId: ContractId +): ContractInstanceAPI { + const contractCreationTxId = contractIdToTxId(contractId); + return { + contractId, + waitForConfirmation: async () => { + return di.wallet.waitConfirmation(contractCreationTxId); + }, + getContractDetails: async () => { + return getContractDetails(di, contractId); + }, + applyInputs: async (request) => { + return applyInputs(di)(contractId, request); + }, + getInputHistory: async () => { + // TODO: We can optimize this by only asking for the new transaction headers + // and only asking for contract details of the new transactions. + return getInputHistory(di)(contractId); + }, + }; +} + +async function getContractDetails( + di: ContractsDI, + contractId: ContractId +): Promise { + const contractDetails = await di.restClient.getContractById({ contractId }); + if ( + typeof contractDetails.state === "undefined" || + typeof contractDetails.currentContract === "undefined" + ) { + return { type: "closed" }; + } else { + return { + type: "active", + contractId, + currentState: contractDetails.state, + currentContract: contractDetails.currentContract, + roleTokenMintingPolicyId: contractDetails.roleTokenMintingPolicyId, + }; + } +} + +/** + * TODO comment + * @category New ContractsAPI + */ +export type ClosedContract = { + type: "closed"; +}; + +/** + * TODO comment + * @category New ContractsAPI + */ +export type ActiveContract = { + type: "active"; + contractId: ContractId; + currentState: MarloweState; + currentContract: Contract; + roleTokenMintingPolicyId: PolicyId; +}; + +/** + * TODO comment + * TODO: Fill with all the information we want to expose + * @category New ContractsAPI + */ +export type ContractDetails = ClosedContract | ActiveContract; + +/** + * TODO comment + * @category New ContractsAPI + */ +export type GetContractDetailsDI = { + getContractDetails: (contractId: ContractId) => Promise; +}; diff --git a/packages/runtime/lifecycle/src/generic/runtime.ts b/packages/runtime/lifecycle/src/generic/runtime.ts index c8030b8c..9cb80402 100644 --- a/packages/runtime/lifecycle/src/generic/runtime.ts +++ b/packages/runtime/lifecycle/src/generic/runtime.ts @@ -6,6 +6,7 @@ import { FPTSRestAPI, RestClient } from "@marlowe.io/runtime-rest-client"; import { mkPayoutLifecycle } from "./payouts.js"; import { mkContractLifecycle } from "./deprecated-contracts.js"; import { mkApplicableActionsAPI } from "./applicable-actions.js"; +import * as NewContract from "./new-contract-api.js"; export function mkRuntimeLifecycle( deprecatedRestAPI: FPTSRestAPI, @@ -21,6 +22,7 @@ export function mkRuntimeLifecycle( wallet: wallet, restClient, deprecatedContractAPI, + newContractAPI: NewContract.mkContractsAPI({ wallet, restClient }), payouts: mkPayoutLifecycle(wallet, deprecatedRestAPI, restClient), applicableActions: mkApplicableActionsAPI( restClient,