diff --git a/packages/adapter/src/io-ts.ts b/packages/adapter/src/io-ts.ts index f685e202..8084f3da 100644 --- a/packages/adapter/src/io-ts.ts +++ b/packages/adapter/src/io-ts.ts @@ -150,21 +150,25 @@ export interface BrandP // This helper is a workaround to that issue, making sure that if the original codec // has the same Output than the Actual type, the new codec outputs the branded type // So a PositiveInt codec will have the type t.Type -export function preservedBrand( - codec: t.Type, - predicate: Refinement, - name: string -): BrandP { +export function preservedBrand< + C extends t.Any, + N extends string, + B extends { readonly [K in N]: symbol }, +>( + codec: C, + predicate: Refinement, t.Branded, B>>, + name: N +): BrandP, B, t.InputOf> { return new t.Type( name, - (u): u is t.Branded => codec.is(u) && predicate(u), + (u): u is t.Branded, B> => codec.is(u) && predicate(u), (u, c) => pipe( codec.validate(u, c), Either.chain((a) => predicate(a) - ? t.success(a as t.Branded) - : t.failure>( + ? t.success(a as t.Branded, B>) + : t.failure, B>>( a, c, `Value does not satisfy the ${name} constraint` diff --git a/packages/runtime/client/rest/src/contract/transaction/details.ts b/packages/runtime/client/rest/src/contract/transaction/details.ts index 98ec20ea..540c3fdf 100644 --- a/packages/runtime/client/rest/src/contract/transaction/details.ts +++ b/packages/runtime/client/rest/src/contract/transaction/details.ts @@ -23,6 +23,7 @@ import { BlockHeader, ContractIdGuard, TextEnvelope, + TxIdGuard, } from "@marlowe.io/runtime-core"; import { TxStatus } from "./status.js"; import { assertGuardEqual, proxy } from "@marlowe.io/adapter/io-ts"; @@ -97,7 +98,7 @@ export const TransactionDetailsGuard = assertGuardEqual( proxy(), t.type({ contractId: ContractIdGuard, - transactionId: TxId, + transactionId: TxIdGuard, continuations: optionFromNullable(G.BuiltinByteString), tags: TagsGuard, metadata: MetadataGuard, @@ -108,7 +109,7 @@ export const TransactionDetailsGuard = assertGuardEqual( outputUtxo: optionFromNullable(TxOutRef), outputContract: optionFromNullable(G.Contract), outputState: optionFromNullable(G.MarloweState), - consumingTx: optionFromNullable(TxId), + consumingTx: optionFromNullable(TxIdGuard), invalidBefore: ISO8601, invalidHereafter: ISO8601, txBody: optionFromNullable(TextEnvelopeGuard), diff --git a/packages/runtime/client/rest/src/contract/transaction/endpoints/collection.ts b/packages/runtime/client/rest/src/contract/transaction/endpoints/collection.ts index 8536b6d2..da74487b 100644 --- a/packages/runtime/client/rest/src/contract/transaction/endpoints/collection.ts +++ b/packages/runtime/client/rest/src/contract/transaction/endpoints/collection.ts @@ -27,6 +27,7 @@ import { unTxOutRef, ContractId, ContractIdGuard, + TxIdGuard, } from "@marlowe.io/runtime-core"; import { TxHeader, TxHeaderGuard } from "../header.js"; import { assertGuardEqual, proxy } from "@marlowe.io/adapter/io-ts"; @@ -125,7 +126,7 @@ export const GetTransactionsForContractResponseGuard = assertGuardEqual( export type TransactionTextEnvelope = t.TypeOf; export const TransactionTextEnvelope = t.type({ contractId: ContractIdGuard, - transactionId: TxId, + transactionId: TxIdGuard, tx: TextEnvelopeGuard, }); diff --git a/packages/runtime/client/rest/src/contract/transaction/endpoints/singleton.ts b/packages/runtime/client/rest/src/contract/transaction/endpoints/singleton.ts index 7f3e6b45..aa0fcaa2 100644 --- a/packages/runtime/client/rest/src/contract/transaction/endpoints/singleton.ts +++ b/packages/runtime/client/rest/src/contract/transaction/endpoints/singleton.ts @@ -14,6 +14,7 @@ import { HexTransactionWitnessSet, HexTransactionWitnessSetGuard, TxId, + TxIdGuard, transactionWitnessSetTextEnvelope, } from "@marlowe.io/runtime-core"; @@ -45,7 +46,7 @@ export const GetContractTransactionByIdRequestGuard = assertGuardEqual( proxy(), t.type({ contractId: ContractIdGuard, - txId: TxId, + txId: TxIdGuard, }) ); @@ -80,7 +81,7 @@ export const SubmitContractTransactionRequestGuard = assertGuardEqual( proxy(), t.type({ contractId: ContractIdGuard, - transactionId: TxId, + transactionId: TxIdGuard, hexTransactionWitnessSet: HexTransactionWitnessSetGuard, }) ); diff --git a/packages/runtime/client/rest/src/contract/transaction/header.ts b/packages/runtime/client/rest/src/contract/transaction/header.ts index b25c8f52..1cd02863 100644 --- a/packages/runtime/client/rest/src/contract/transaction/header.ts +++ b/packages/runtime/client/rest/src/contract/transaction/header.ts @@ -14,6 +14,7 @@ import { TxOutRef, TxId, ContractId, + TxIdGuard, } from "@marlowe.io/runtime-core"; import { TxStatus } from "./status.js"; import { BuiltinByteString } from "@marlowe.io/language-core-v1"; @@ -46,7 +47,7 @@ export interface TxHeader { */ export const TxHeaderGuard = t.type({ contractId: ContractIdGuard, - transactionId: TxId, + transactionId: TxIdGuard, continuations: optionFromNullable(G.BuiltinByteString), tags: TagsGuard, metadata: MetadataGuard, diff --git a/packages/runtime/core/src/contract/id.ts b/packages/runtime/core/src/contract/id.ts index d8d2f8d9..15b9f8e0 100644 --- a/packages/runtime/core/src/contract/id.ts +++ b/packages/runtime/core/src/contract/id.ts @@ -2,7 +2,7 @@ import * as t from "io-ts/lib/index.js"; import { split } from "fp-ts/lib/string.js"; import { pipe } from "fp-ts/lib/function.js"; import { head } from "fp-ts/lib/ReadonlyNonEmptyArray.js"; -import { TxId } from "../tx/id.js"; +import { TxId, txId } from "../tx/id.js"; import { unsafeEither } from "@marlowe.io/adapter/fp-ts"; import { preservedBrand } from "@marlowe.io/adapter/io-ts"; @@ -16,11 +16,17 @@ export const ContractIdGuard = preservedBrand( "ContractId" ); -export type ContractId = t.TypeOf; +/** + * Marlowe contract identifier. + * + * @remarks The underlying data structure is a normal string, but in the type + * level it is **Branded** with a unique symbol so that it is not confused with other strings + */ +export type ContractId = t.Branded; export const contractId = (s: string) => unsafeEither(ContractIdGuard.decode(s)); export const contractIdToTxId: (contractId: ContractId) => TxId = ( contractId -) => pipe(contractId, split("#"), head); +) => pipe(contractId, split("#"), head, txId); diff --git a/packages/runtime/core/src/payout/index.ts b/packages/runtime/core/src/payout/index.ts index 7ef009d1..63fe2c9c 100644 --- a/packages/runtime/core/src/payout/index.ts +++ b/packages/runtime/core/src/payout/index.ts @@ -4,7 +4,7 @@ import { fromNewtype } from "io-ts-types"; import { split } from "fp-ts/lib/string.js"; import { pipe } from "fp-ts/lib/function.js"; import { head } from "fp-ts/lib/ReadonlyNonEmptyArray.js"; -import { TxId } from "../tx/id.js"; +import { txId, TxId } from "../tx/id.js"; import { ContractIdGuard } from "../contract/id.js"; import { AssetId, Assets } from "../asset/index.js"; @@ -15,7 +15,7 @@ export const unPayoutId = iso().unwrap; export const payoutId = iso().wrap; export const payoutIdToTxId: (payoutId: PayoutId) => TxId = (payoutId) => - pipe(payoutId, unPayoutId, split("#"), head); + pipe(payoutId, unPayoutId, split("#"), head, txId); export type WithdrawalId = Newtype< { readonly WithdrawalId: unique symbol }, @@ -27,7 +27,7 @@ export const withdrawalId = iso().wrap; export const withdrawalIdToTxId: (withdrawalId: WithdrawalId) => TxId = ( withdrawalId -) => pipe(withdrawalId, unWithdrawalId); +) => pipe(withdrawalId, unWithdrawalId, txId); // DISCUSSION: PayoutAvailable or AvailablePayout? export type PayoutAvailable = t.TypeOf; diff --git a/packages/runtime/core/src/tx/id.ts b/packages/runtime/core/src/tx/id.ts index 2dcea5bc..63399c90 100644 --- a/packages/runtime/core/src/tx/id.ts +++ b/packages/runtime/core/src/tx/id.ts @@ -1,6 +1,22 @@ +import { unsafeEither } from "@marlowe.io/adapter/fp-ts"; import * as t from "io-ts/lib/index.js"; -// TODO: Try to make newtype as this gets replaced to string -// in the docs. -export type TxId = t.TypeOf; -export const TxId = t.string; // to refine +export interface TxIdBrand { + readonly TxId: unique symbol; +} + +/** + * Cardano transaction identifier. + * + * @remarks The underlying data structure is a normal string, but in the type + * level it is **Branded** with a unique symbol so that it is not confused with other strings + */ +export type TxId = t.Branded; + +export const TxIdGuard = t.brand( + t.string, + (s): s is t.Branded => true, + "TxId" +); + +export const txId = (s: string) => unsafeEither(TxIdGuard.decode(s)); diff --git a/typedoc.json b/typedoc.json index c2116154..86e8a00c 100644 --- a/typedoc.json +++ b/typedoc.json @@ -24,6 +24,9 @@ "fp-ts": { "Option": "https://gcanti.github.io/fp-ts/modules/Option.ts.html" }, + "io-ts": { + "Branded": "https://github.com/gcanti/io-ts/blob/master/index.md#branded-types--refinements" + }, "lucid-cardano": { "Lucid": "https://deno.land/x/lucid@0.10.7/mod.ts?s=Lucid" },