diff --git a/cabal.project b/cabal.project
index fda7816fd2..a5de8a9c8e 100644
--- a/cabal.project
+++ b/cabal.project
@@ -29,6 +29,10 @@ if impl(ghc < 9.8)
constraints: process >= 1.6.26.1
+-- It may slow down build plan preparation, but without it cabal has problems
+-- with solving constraints. Remove this when not needed anymore.
+max-backjumps: 50000
+
program-options
ghc-options: -Werror
diff --git a/cardano-wasm/README.md b/cardano-wasm/README.md
index 93beb1a378..fb6a591698 100644
--- a/cardano-wasm/README.md
+++ b/cardano-wasm/README.md
@@ -203,7 +203,7 @@ That will create it with the name `cardano-wasm.js` in the current folder.
You can find more information in [this url](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/wasm.html).
-And you can find an example of how to use it in the `example` subfolder. This example assumes that the generated `.wasm` and `.js` files reside in the same folder as the code in `example` subfolder.
+And you can find an example of how to use it in the `example` subfolder. This example assumes that the generated `.wasm` and `.js` files as well as the files from the `lib-wrapper` subfolder, all reside in the same folder as the code in `example` subfolder.
## Running the example
@@ -214,12 +214,13 @@ To run the example in the `example` subfolder:
echo "$(env -u CABAL_CONFIG wasm32-wasi-cabal list-bin exe:cardano-wasm | tail -n1)"
```
2. Copy the generated `cardano-wasm.js` file (generated by the `post-link.mjs` command in the section above) to the `example` subfolder.
-3. Navigate to the `example` subfolder in your terminal.
-4. Due to browser security restrictions (CORS policy), you may need to serve the `index.html` file through a local HTTP server. A simple way to do this is using Python's built-in HTTP server:
+3. Copy the wrapper files from the `lib-wrapper` folder into the `example` subfolder.
+4. Navigate to the `example` subfolder in your terminal.
+5. Due to browser security restrictions (CORS policy), you may need to serve the `index.html` file through a local HTTP server. A simple way to do this is using Python's built-in HTTP server:
```console
python3 -m http.server 8001
```
-5. Open your web browser and navigate to `http://localhost:8001/`. You should see a blank page, and if you open the developer console you should be able to see an output like the following:
+6. Open your web browser and navigate to `http://localhost:8001/`. You should see a blank page, and if you open the developer console you should be able to see an output like the following:
```console
[Log] wasi: – 0 – 0 (wasi.js, line 1)
[Log] wasi: – 0 – 0 (wasi.js, line 1)
diff --git a/cardano-wasm/app/Main.hs b/cardano-wasm/app/Main.hs
index a31c5a029d..af9b3cd321 100644
--- a/cardano-wasm/app/Main.hs
+++ b/cardano-wasm/app/Main.hs
@@ -1,4 +1,8 @@
-module Main (main) where
+module Main where
+
+import Cardano.Wasm.Internal.Api.Info (apiInfo)
+import Cardano.Wasm.Internal.Api.InfoToTypeScript (apiInfoToTypeScriptFile)
+import Cardano.Wasm.Internal.Api.TypeScriptDefs (printTypeScriptFile)
main :: IO ()
-main = pure ()
+main = printTypeScriptFile (apiInfoToTypeScriptFile apiInfo)
diff --git a/cardano-wasm/cardano-wasm.cabal b/cardano-wasm/cardano-wasm.cabal
index bce800b981..47b66bc41f 100644
--- a/cardano-wasm/cardano-wasm.cabal
+++ b/cardano-wasm/cardano-wasm.cabal
@@ -40,7 +40,9 @@ executable cardano-wasm
"-optl-Wl,--strip-all,--export=getApiInfo,--export=newConwayTx,--export=addTxInput,--export=addSimpleTxOut,--export=setFee,--export=estimateMinFee,--export=signWithPaymentKey,--export=alsoSignWithPaymentKey,--export=txToCbor"
other-modules:
Cardano.Wasm.Internal.Api.Info
+ Cardano.Wasm.Internal.Api.InfoToTypeScript
Cardano.Wasm.Internal.Api.Tx
+ Cardano.Wasm.Internal.Api.TypeScriptDefs
Cardano.Wasm.Internal.ExceptionHandling
Cardano.Wasm.Internal.JavaScript.Bridge
@@ -59,3 +61,31 @@ executable cardano-wasm
build-depends:
ghc-experimental,
utf8-string,
+
+test-suite cardano-wasm-golden
+ type: exitcode-stdio-1.0
+ main-is: cardano-wasm-golden.hs
+ hs-source-dirs: test/cardano-wasm-golden
+
+ if !arch(wasm32)
+ import: project-config
+ build-depends:
+ hedgehog >=1.1,
+ hedgehog-extras ^>=0.8,
+ tasty,
+ tasty-hedgehog,
+
+ ghc-options:
+ -threaded
+ -rtsopts
+ "-with-rtsopts=-N -T"
+
+ build-tool-depends:
+ cardano-wasm:cardano-wasm,
+ tasty-discover:tasty-discover,
+
+ other-modules:
+ Test.Golden.Cardano.Wasm.TypeScript
+ else
+ build-depends:
+ base
diff --git a/cardano-wasm/example/example.js b/cardano-wasm/example/example.js
new file mode 100644
index 0000000000..4c58158830
--- /dev/null
+++ b/cardano-wasm/example/example.js
@@ -0,0 +1,38 @@
+import cardano_api from "./cardano-api.js";
+
+let promise = cardano_api();
+
+async function get_protocol_params() {
+ const response = await fetch("./preview_pparams.json");
+ return (await response.json());
+}
+
+let protocolParams = await get_protocol_params();
+
+async function do_async_work() {
+ let api = await promise;
+ console.log("Api object:");
+ console.log(api);
+
+ let emptyTx = await api.newConwayTx();
+ console.log("UnsignedTx object:");
+ console.log(emptyTx);
+
+ let tx = await emptyTx.addTxInput("be6efd42a3d7b9a00d09d77a5d41e55ceaf0bd093a8aa8a893ce70d9caafd978", 0)
+ .addSimpleTxOut("addr_test1vzpfxhjyjdlgk5c0xt8xw26avqxs52rtf69993j4tajehpcue4v2v", 10_000_000n)
+
+ let feeEstimate = await tx.estimateMinFee(protocolParams, 1, 0, 0);
+ console.log("Estimated fee:");
+ console.log(feeEstimate);
+
+ let signedTx = await tx.setFee(feeEstimate)
+ .signWithPaymentKey("addr_sk1648253w4tf6fv5fk28dc7crsjsaw7d9ymhztd4favg3cwkhz7x8sl5u3ms");
+ console.log("SignedTx object:");
+ console.log(signedTx);
+
+ let txCbor = await signedTx.txToCbor();
+ console.log("Tx CBOR:");
+ console.log(txCbor);
+}
+
+do_async_work().then(() => { });
diff --git a/cardano-wasm/example/index.html b/cardano-wasm/example/index.html
index 25be4749c7..2efd25a184 100644
--- a/cardano-wasm/example/index.html
+++ b/cardano-wasm/example/index.html
@@ -1,41 +1,7 @@
-
+
\ No newline at end of file
diff --git a/cardano-wasm/lib-wrapper/cardano-api.d.ts b/cardano-wasm/lib-wrapper/cardano-api.d.ts
new file mode 100644
index 0000000000..3ea228b2ca
--- /dev/null
+++ b/cardano-wasm/lib-wrapper/cardano-api.d.ts
@@ -0,0 +1,99 @@
+// cardano-api.d.ts
+
+export default initialise;
+
+/**
+ * Initialises the Cardano API.
+ * @returns A promise that resolves to the main `CardanoAPI` object.
+ */
+declare function initialise(): Promise;
+
+/**
+ * Represents an unsigned transaction.
+ */
+declare interface UnsignedTx {
+ /**
+ * The type of the object, used for identification (the "UnsignedTx" string).
+ */
+ objectType: string;
+
+ /**
+ * Adds a simple transaction input to the transaction.
+ * @param txId The transaction ID of the input UTxO.
+ * @param txIx The index of the input within the UTxO.
+ * @returns The `UnsignedTx` object with the added input.
+ */
+ addTxInput(txId: string, txIx: number): UnsignedTx;
+
+ /**
+ * Adds a simple transaction output to the transaction.
+ * @param destAddr The destination address.
+ * @param lovelaceAmount The amount in lovelaces to output.
+ * @returns The `UnsignedTx` object with the added output.
+ */
+ addSimpleTxOut(destAddr: string, lovelaceAmount: bigint): UnsignedTx;
+
+ /**
+ * Sets the fee for the transaction.
+ * @param lovelaceAmount The fee amount in lovelaces.
+ * @returns The `UnsignedTx` object with the set fee.
+ */
+ setFee(lovelaceAmount: bigint): UnsignedTx;
+
+ /**
+ * Estimates the minimum fee for the transaction.
+ * @param protocolParams The protocol parameters.
+ * @param numKeyWitnesses The number of key witnesses.
+ * @param numByronKeyWitnesses The number of Byron key witnesses.
+ * @param totalRefScriptSize The total size of reference scripts in bytes.
+ * @returns A promise that resolves to the estimated minimum fee in lovelaces.
+ */
+ estimateMinFee(protocolParams: any, numKeyWitnesses: number, numByronKeyWitnesses: number, totalRefScriptSize: number): Promise;
+
+ /**
+ * Signs the transaction with a payment key.
+ * @param signingKey The signing key to witness the transaction.
+ * @returns A promise that resolves to a `SignedTx` object.
+ */
+ signWithPaymentKey(signingKey: string): Promise;
+}
+
+/**
+ * Represents a signed transaction.
+ */
+declare interface SignedTx {
+ /**
+ * The type of the object, used for identification (the "SignedTx" string).
+ */
+ objectType: string;
+
+ /**
+ * Adds an extra signature to the transaction with a payment key.
+ * @param signingKey The signing key to witness the transaction.
+ * @returns The `SignedTx` object with the additional signature.
+ */
+ alsoSignWithPaymentKey(signingKey: string): SignedTx;
+
+ /**
+ * Converts the signed transaction to its CBOR representation.
+ * @returns A promise that resolves to the CBOR representation of the transaction as a hex string.
+ */
+ txToCbor(): Promise;
+}
+
+/**
+ * The main Cardano API object with static methods.
+ */
+declare interface CardanoAPI {
+ /**
+ * The type of the object, used for identification (the "CardanoAPI" string).
+ */
+ objectType: string;
+
+ /**
+ * Creates a new Conway-era transaction.
+ * @returns A promise that resolves to a new `UnsignedTx` object.
+ */
+ newConwayTx(): Promise;
+}
+
diff --git a/cardano-wasm/example/cardano-api.js b/cardano-wasm/lib-wrapper/cardano-api.js
similarity index 93%
rename from cardano-wasm/example/cardano-api.js
rename to cardano-wasm/lib-wrapper/cardano-api.js
index 91f0d96814..1440a18233 100644
--- a/cardano-wasm/example/cardano-api.js
+++ b/cardano-wasm/lib-wrapper/cardano-api.js
@@ -1,8 +1,10 @@
+///
+
import { WASI } from "https://unpkg.com/@bjorn3/browser_wasi_shim@0.4.1/dist/index.js";
import ghc_wasm_jsffi from "./cardano-wasm.js";
const __exports = {};
const wasi = new WASI([], [], []);
-async function initialize() {
+async function initialise() {
let { instance } = await WebAssembly.instantiateStreaming(fetch("./cardano-wasm.wasm"), {
ghc_wasm_jsffi: ghc_wasm_jsffi(__exports),
wasi_snapshot_preview1: wasi.wasiImport,
@@ -12,7 +14,7 @@ async function initialize() {
// Wrap a function with variable arguments to make the parameters inspectable
function fixateArgs(params, func) {
- const paramString = params.join(',');
+ const paramString = params.map(p => p.name).join(',');
// Dynamically create a function that captures 'func' from the closure.
// 'this' and 'arguments' are passed through from the wrapper to 'func'.
// Using eval allows the returned function to have named parameters for inspectability.
@@ -26,7 +28,7 @@ async function initialize() {
// Same as fixateArgs but for async functions
async function fixateArgsAsync(params, func) {
- const paramString = params.join(',');
+ const paramString = params.map(p => p.name).join(',');
// Dynamically create an async function.
const wrapper = eval(`
(async function(${paramString}) {
@@ -80,7 +82,7 @@ async function initialize() {
});
// Populate the main API object with static methods
- apiInfo.staticMethods.forEach(method => {
+ apiInfo.mainObject.methods.forEach(method => {
cardanoAPI[method.name] = async function (...args) {
const resultPromise = instance.exports[method.name](...args);
@@ -93,4 +95,4 @@ async function initialize() {
});
return cardanoAPI;
}
-export default initialize;
+export default initialise;
diff --git a/cardano-wasm/src/Cardano/Wasm/Internal/Api/Info.hs b/cardano-wasm/src/Cardano/Wasm/Internal/Api/Info.hs
index 7f79d34ee4..76a0e6af63 100644
--- a/cardano-wasm/src/Cardano/Wasm/Internal/Api/Info.hs
+++ b/cardano-wasm/src/Cardano/Wasm/Internal/Api/Info.hs
@@ -1,6 +1,14 @@
{-# LANGUAGE InstanceSigs #-}
-module Cardano.Wasm.Internal.Api.Info (apiInfo) where
+module Cardano.Wasm.Internal.Api.Info
+ ( apiInfo
+ , ApiInfo (..)
+ , VirtualObjectInfo (..)
+ , MethodInfo (..)
+ , ParamInfo (..)
+ , MethodReturnTypeInfo (..)
+ )
+where
import Data.Aeson qualified as Aeson
import Data.Text qualified as Text
@@ -18,56 +26,98 @@ data MethodReturnTypeInfo
deriving (Show, Eq)
instance Aeson.ToJSON MethodReturnTypeInfo where
+ toJSON :: MethodReturnTypeInfo -> Aeson.Value
toJSON Fluent = Aeson.object ["type" Aeson..= Text.pack "fluent"]
toJSON (NewObject objTypeName) = Aeson.object ["type" Aeson..= Text.pack "newObject", "objectType" Aeson..= objTypeName]
toJSON (OtherType typeName) = Aeson.object ["type" Aeson..= Text.pack "other", "typeName" Aeson..= typeName]
+-- | Information about a single parameter of a method.
+data ParamInfo = ParamInfo
+ { paramName :: String
+ -- ^ Name of the parameter.
+ , paramType :: String
+ -- ^ Type of the parameter (as a TypeScript type).
+ , paramDoc :: String
+ -- ^ Documentation for the parameter.
+ }
+ deriving (Show, Eq)
+
+instance Aeson.ToJSON ParamInfo where
+ toJSON :: ParamInfo -> Aeson.Value
+ toJSON (ParamInfo name pType doc) =
+ Aeson.object
+ [ "name" Aeson..= name
+ , "type" Aeson..= pType
+ , "doc" Aeson..= doc
+ ]
+
-- | Information about a single method of a virtual object.
data MethodInfo = MethodInfo
{ methodName :: String
- , methodParams :: [String]
- -- ^ Names of parameters, excluding 'this'.
+ -- ^ Name of the method in the virtual object of the JS API (which should match the exported function).
+ , methodDoc :: String
+ -- ^ General documentation for the method.
+ , methodParams :: [ParamInfo]
+ -- ^ Info about parameters, excluding 'this'.
, methodReturnType :: MethodReturnTypeInfo
+ -- ^ Return type of the method.
+ , methodReturnDoc :: String
+ -- ^ Documentation for the return value of the method.
}
deriving (Show, Eq)
instance Aeson.ToJSON MethodInfo where
toJSON :: MethodInfo -> Aeson.Value
- toJSON (MethodInfo name params retType) =
+ toJSON (MethodInfo name doc params retType retDoc) =
Aeson.object
[ "name" Aeson..= name
+ , "doc" Aeson..= doc
, "params" Aeson..= params
, "return" Aeson..= retType
+ , "returnDoc" Aeson..= retDoc
]
-- | Information about a virtual object and its methods.
data VirtualObjectInfo = VirtualObjectInfo
{ virtualObjectName :: String
+ -- ^ Name of the virtual object.
+ , virtualObjectDoc :: String
+ -- ^ Documentation for the virtual object.
, virtualObjectMethods :: [MethodInfo]
+ -- ^ Information about the methods of the virtual object.
}
deriving (Show, Eq)
instance Aeson.ToJSON VirtualObjectInfo where
toJSON :: VirtualObjectInfo -> Aeson.Value
- toJSON (VirtualObjectInfo name methods) =
+ toJSON (VirtualObjectInfo name doc methods) =
Aeson.object
[ "objectName" Aeson..= name
+ , "doc" Aeson..= doc
, "methods" Aeson..= methods
]
-- | Aggregate type for all API information.
data ApiInfo = ApiInfo
- { staticMethods :: [MethodInfo]
+ { mainObject :: VirtualObjectInfo
+ -- ^ Information about the main virtual object of the API, which serves as the entry point.
, virtualObjects :: [VirtualObjectInfo]
+ -- ^ Info about all other (non-main) virtual objects and their methods.
+ , initialiseFunctionDoc :: String
+ -- ^ Documentation for the initialise function of the API (which creates the main virtual object).
+ , initialiseFunctionReturnDoc :: String
+ -- ^ Documentation for the return value of the initialise function (which is the main virtual object).
}
deriving (Show, Eq)
instance Aeson.ToJSON ApiInfo where
toJSON :: ApiInfo -> Aeson.Value
- toJSON (ApiInfo staticObjs virtualObjs) =
+ toJSON (ApiInfo mainObj virtualObjs initDoc initRetDoc) =
Aeson.object
- [ "staticMethods" Aeson..= staticObjs
+ [ "mainObject" Aeson..= mainObj
, "virtualObjects" Aeson..= virtualObjs
+ , "initialiseFunctionDoc" Aeson..= initDoc
+ , "initialiseFunctionReturnDoc" Aeson..= initRetDoc
]
-- | Provides metadata about the "virtual objects" and their methods.
@@ -77,43 +127,59 @@ apiInfo =
let unsignedTxObjectName = "UnsignedTx"
signedTxObjectName = "SignedTx"
- staticApiMethods =
- [ MethodInfo
- { methodName = "newConwayTx"
- , methodParams = []
- , methodReturnType = NewObject unsignedTxObjectName
- }
- ]
-
unsignedTxObj =
VirtualObjectInfo
{ virtualObjectName = unsignedTxObjectName
+ , virtualObjectDoc = "Represents an unsigned transaction."
, virtualObjectMethods =
[ MethodInfo
{ methodName = "addTxInput"
- , methodParams = ["txId", "txIx"]
+ , methodDoc = "Adds a simple transaction input to the transaction."
+ , methodParams =
+ [ ParamInfo "txId" "string" "The transaction ID of the input UTxO."
+ , ParamInfo "txIx" "number" "The index of the input within the UTxO."
+ ]
, methodReturnType = Fluent
+ , methodReturnDoc = "The `UnsignedTx` object with the added input."
}
, MethodInfo
{ methodName = "addSimpleTxOut"
- , methodParams = ["destAddr", "lovelaceAmount"]
+ , methodDoc = "Adds a simple transaction output to the transaction."
+ , methodParams =
+ [ ParamInfo "destAddr" "string" "The destination address."
+ , ParamInfo "lovelaceAmount" "bigint" "The amount in lovelaces to output."
+ ]
, methodReturnType = Fluent
+ , methodReturnDoc = "The `UnsignedTx` object with the added output."
}
, MethodInfo
{ methodName = "setFee"
- , methodParams = ["lovelaceAmount"]
+ , methodDoc = "Sets the fee for the transaction."
+ , methodParams = [ParamInfo "lovelaceAmount" "bigint" "The fee amount in lovelaces."]
, methodReturnType = Fluent
- }
- , MethodInfo
- { methodName = "signWithPaymentKey"
- , methodParams = ["signingKey"]
- , methodReturnType = NewObject signedTxObjectName
+ , methodReturnDoc = "The `UnsignedTx` object with the set fee."
}
, MethodInfo
{ methodName = "estimateMinFee"
+ , methodDoc = "Estimates the minimum fee for the transaction."
, methodParams =
- ["protocolParams", "numExtraKeyWitnesses", "numExtraByronKeyWitnesses", "totalRefScriptSize"]
+ [ ParamInfo "protocolParams" "any" "The protocol parameters."
+ , ParamInfo
+ "numKeyWitnesses"
+ "number"
+ "The number of key witnesses."
+ , ParamInfo "numByronKeyWitnesses" "number" "The number of Byron key witnesses."
+ , ParamInfo "totalRefScriptSize" "number" "The total size of reference scripts in bytes."
+ ]
, methodReturnType = OtherType "BigInt"
+ , methodReturnDoc = "A promise that resolves to the estimated minimum fee in lovelaces."
+ }
+ , MethodInfo
+ { methodName = "signWithPaymentKey"
+ , methodDoc = "Signs the transaction with a payment key."
+ , methodParams = [ParamInfo "signingKey" "string" "The signing key to witness the transaction."]
+ , methodReturnType = NewObject signedTxObjectName
+ , methodReturnDoc = "A promise that resolves to a `SignedTx` object."
}
]
}
@@ -121,20 +187,41 @@ apiInfo =
signedTxObj =
VirtualObjectInfo
{ virtualObjectName = signedTxObjectName
+ , virtualObjectDoc = "Represents a signed transaction."
, virtualObjectMethods =
[ MethodInfo
{ methodName = "alsoSignWithPaymentKey"
- , methodParams = ["signingKey"]
+ , methodDoc = "Adds an extra signature to the transaction with a payment key."
+ , methodParams = [ParamInfo "signingKey" "string" "The signing key to witness the transaction."]
, methodReturnType = Fluent
+ , methodReturnDoc = "The `SignedTx` object with the additional signature."
}
, MethodInfo
{ methodName = "txToCbor"
+ , methodDoc = "Converts the signed transaction to its CBOR representation."
, methodParams = []
, methodReturnType = OtherType "string"
+ , methodReturnDoc =
+ "A promise that resolves to the CBOR representation of the transaction as a hex string."
}
]
}
in ApiInfo
- { staticMethods = staticApiMethods
+ { mainObject =
+ VirtualObjectInfo
+ { virtualObjectName = "CardanoAPI"
+ , virtualObjectDoc = "The main Cardano API object with static methods."
+ , virtualObjectMethods =
+ [ MethodInfo
+ { methodName = "newConwayTx"
+ , methodDoc = "Creates a new Conway-era transaction."
+ , methodParams = []
+ , methodReturnType = NewObject unsignedTxObjectName
+ , methodReturnDoc = "A promise that resolves to a new `UnsignedTx` object."
+ }
+ ]
+ }
, virtualObjects = [unsignedTxObj, signedTxObj]
+ , initialiseFunctionDoc = "Initialises the Cardano API."
+ , initialiseFunctionReturnDoc = "A promise that resolves to the main `CardanoAPI` object."
}
diff --git a/cardano-wasm/src/Cardano/Wasm/Internal/Api/InfoToTypeScript.hs b/cardano-wasm/src/Cardano/Wasm/Internal/Api/InfoToTypeScript.hs
new file mode 100644
index 0000000000..043356c833
--- /dev/null
+++ b/cardano-wasm/src/Cardano/Wasm/Internal/Api/InfoToTypeScript.hs
@@ -0,0 +1,72 @@
+module Cardano.Wasm.Internal.Api.InfoToTypeScript where
+
+import Cardano.Wasm.Internal.Api.Info qualified as Info
+import Cardano.Wasm.Internal.Api.TypeScriptDefs qualified as TypeScript
+
+-- | Converts the Cardano API information to a TypeScript declaration file AST.
+apiInfoToTypeScriptFile :: Info.ApiInfo -> TypeScript.TypeScriptFile
+apiInfoToTypeScriptFile apiInfo =
+ TypeScript.TypeScriptFile
+ { TypeScript.typeScriptFileName = "cardano-api.d.ts"
+ , TypeScript.typeScriptFileContent =
+ [ TypeScript.Declaration [] (TypeScript.ExportDec True "initialise")
+ , TypeScript.Declaration
+ [ Info.initialiseFunctionDoc apiInfo
+ , "@returns " <> Info.initialiseFunctionReturnDoc apiInfo
+ ]
+ ( TypeScript.FunctionDec $
+ TypeScript.FunctionHeader
+ { TypeScript.functionName = "initialise"
+ , TypeScript.functionParams = []
+ , TypeScript.functionReturnType =
+ "Promise<" <> Info.virtualObjectName (Info.mainObject apiInfo) <> ">"
+ }
+ )
+ ]
+ <> virtualObjectInterfaces
+ }
+ where
+ virtualObjectInterfaces =
+ map virtualObjectInfoToInterfaceDec (Info.virtualObjects apiInfo <> [Info.mainObject apiInfo])
+
+virtualObjectInfoToInterfaceDec :: Info.VirtualObjectInfo -> TypeScript.Declaration
+virtualObjectInfoToInterfaceDec vo =
+ TypeScript.Declaration
+ [Info.virtualObjectDoc vo]
+ ( TypeScript.InterfaceDec
+ (Info.virtualObjectName vo)
+ ( [ TypeScript.InterfaceContent
+ [ "The type of the object, used for identification (the \""
+ <> Info.virtualObjectName vo
+ <> "\" string)."
+ ]
+ (TypeScript.InterfaceProperty "objectType" "string")
+ ]
+ <> map (methodInfoToInterfaceContent (Info.virtualObjectName vo)) (Info.virtualObjectMethods vo)
+ )
+ )
+
+methodInfoToInterfaceContent :: String -> Info.MethodInfo -> TypeScript.InterfaceContent
+methodInfoToInterfaceContent selfTypeName method =
+ TypeScript.InterfaceContent
+ ( [Info.methodDoc method]
+ <> map (\p -> "@param " <> Info.paramName p <> " " <> Info.paramDoc p) (Info.methodParams method)
+ <> ["@returns " <> Info.methodReturnDoc method]
+ )
+ ( TypeScript.InterfaceMethod
+ (Info.methodName method)
+ (map paramInfoToFunctionParam $ Info.methodParams method)
+ (methodReturnTypeToString selfTypeName $ Info.methodReturnType method)
+ )
+
+paramInfoToFunctionParam :: Info.ParamInfo -> TypeScript.FunctionParam
+paramInfoToFunctionParam p =
+ TypeScript.FunctionParam
+ { TypeScript.paramName = Info.paramName p
+ , TypeScript.paramType = Info.paramType p
+ }
+
+methodReturnTypeToString :: String -> Info.MethodReturnTypeInfo -> String
+methodReturnTypeToString selfTypeName Info.Fluent = selfTypeName
+methodReturnTypeToString _ (Info.NewObject objTypeName) = "Promise<" <> objTypeName <> ">"
+methodReturnTypeToString _ (Info.OtherType typeName) = "Promise<" <> typeName <> ">"
diff --git a/cardano-wasm/src/Cardano/Wasm/Internal/Api/Tx.hs b/cardano-wasm/src/Cardano/Wasm/Internal/Api/Tx.hs
index 3d54c42502..55999b9c49 100644
--- a/cardano-wasm/src/Cardano/Wasm/Internal/Api/Tx.hs
+++ b/cardano-wasm/src/Cardano/Wasm/Internal/Api/Tx.hs
@@ -45,9 +45,9 @@ import Lens.Micro ((%~), (&), (.~), (<>~))
-- * @UnsignedTx@ object
--- | An object representing a transaction that is being built and hasn't
+-- | An object representing a transaction that is being built and has not
-- been signed yet. It abstracts over the era of the transaction.
--- It is meant to be an opaque object in JavaScript API.
+-- It is meant to be an opaque object in the JavaScript API.
data UnsignedTxObject
= forall era. UnsignedTxObject
{ unsignedTxEra :: Exp.Era era
@@ -88,7 +88,7 @@ addTxInputImpl (UnsignedTxObject era (Exp.UnsignedTx tx)) txId txIx =
in UnsignedTxObject era $ Exp.UnsignedTx tx'
-- | Add a simple transaction output to an unsigned transaction object.
--- It takes a destination address and an amount in lovelace.
+-- It takes a destination address and an amount in lovelaces.
addSimpleTxOutImpl
:: (HasCallStack, MonadThrow m) => UnsignedTxObject -> String -> Ledger.Coin -> m UnsignedTxObject
addSimpleTxOutImpl (UnsignedTxObject era (Exp.UnsignedTx tx)) destAddr lovelaceAmount =
@@ -148,7 +148,7 @@ estimateMinFeeImpl
=> UnsignedTxObject
-- ^ The unsigned transaction object to estimate fees for.
-> ProtocolParamsJSON
- -- ^ The JSON for the protocol parameters of the right era and network.
+ -- ^ The JSON for the protocol parameters of the correct era and network.
-> Int
-- ^ The number of key witnesses still to be added to the transaction.
-> Int
diff --git a/cardano-wasm/src/Cardano/Wasm/Internal/Api/TypeScriptDefs.hs b/cardano-wasm/src/Cardano/Wasm/Internal/Api/TypeScriptDefs.hs
new file mode 100644
index 0000000000..1e034f38cb
--- /dev/null
+++ b/cardano-wasm/src/Cardano/Wasm/Internal/Api/TypeScriptDefs.hs
@@ -0,0 +1,190 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | This module defines a basic AST for generating TypeScript
+-- declaration files, and basic pretty-printing functionality.
+--
+-- The reason we define a custom tool for generating TypeScript
+-- declaration files is that existing libraries like `aeson-typescript`
+-- and `servant-typescript` are not aimed at generating custom
+-- TypeScript interface declaration files for a specific API,
+-- but rather at generating TypeScript interfaces for Haskell
+-- data types and servant HTTP APIs respectively. And other libraries
+-- that align with our needs, like `language-typescript`, are not
+-- actively maintained.
+module Cardano.Wasm.Internal.Api.TypeScriptDefs where
+
+import Data.List.NonEmpty qualified as LNE
+import Data.Text.Lazy qualified as TL
+import Data.Text.Lazy.Builder qualified as TLB
+
+-- | Prints the TypeScript declaration file to stdout.
+printTypeScriptFile :: TypeScriptFile -> IO ()
+printTypeScriptFile tsFile = do
+ let content = buildTypeScriptFile tsFile
+ putStrLn $ TL.unpack $ TLB.toLazyText content
+
+-- | Creates a builder for a JavaScript-style multiline comment
+-- with the specified indentation level (in spaces) and each line
+-- in the list of strings as a separate line in the comment.
+-- The first line starts with `/**`, subsequent lines (corresponding
+-- to each line in the list) start with ` * `,
+-- and the last line ends with ` */`.
+-- The indentation level is used to indent the entire comment block.
+buildMultilineComment :: Int -> [String] -> TLB.Builder
+buildMultilineComment indentLevel commentLines =
+ let indentation = TLB.fromLazyText $ TL.replicate (fromIntegral indentLevel) " "
+ bodyIndentation = indentation <> " * "
+ firstLine = indentation <> "/**"
+ indentedCommentLines = map (\line -> (bodyIndentation <>) . TLB.fromString $ line <> "\n") commentLines
+ lastLine = indentation <> " */"
+ in mconcat [firstLine, "\n", mconcat indentedCommentLines, lastLine]
+
+-- | Represents the top-level structure of a TypeScript declaration file.
+data TypeScriptFile = TypeScriptFile
+ { typeScriptFileName :: String
+ -- ^ Name of the TypeScript file.
+ , typeScriptFileContent :: [Declaration]
+ -- ^ List of declarations in the file.
+ }
+
+-- | Creates a builder for a TypeScript declaration file.
+-- It adds a comment to the top of the file with the file name.
+buildTypeScriptFile :: TypeScriptFile -> TLB.Builder
+buildTypeScriptFile (TypeScriptFile name decls) =
+ let header = TLB.fromString $ "// " ++ name ++ "\n"
+ declarations = mconcat $ map (\dec -> "\n" <> buildDeclaration dec <> "\n") decls
+ in header <> declarations
+
+-- | Wraps a TypeScript declaration with a comment.
+-- The TypeScript declaration can have any of the types
+-- defined by 'DeclarationType'.
+data Declaration = Declaration
+ { declarationComment :: [String]
+ -- ^ Comments for the declaration, can be empty if no comments are needed.
+ -- Each element in the list is a separate line in the comment.
+ , declarationContent :: DeclarationType
+ -- ^ The type and content of the declaration.
+ }
+
+-- | Creates a builder for a TypeScript declaration.
+buildDeclaration :: Declaration -> TLB.Builder
+buildDeclaration (Declaration [] declarationType) = buildDeclarationType declarationType
+buildDeclaration (Declaration comments declarationType) =
+ buildMultilineComment 0 comments <> "\n" <> buildDeclarationType declarationType
+
+-- | Represents a TypeScript declaration content of some type.
+data DeclarationType
+ = -- | Export declaration.
+ ExportDec
+ Bool
+ -- ^ Is it a default export?
+ String
+ -- ^ Name of the symbol to export.
+ | -- | Function declaration.
+ FunctionDec FunctionHeader
+ | -- | Interface declaration.
+ InterfaceDec
+ String
+ -- ^ Name of the interface.
+ [InterfaceContent]
+ -- ^ Definitions of the interface.
+
+-- | Creates a builder for a TypeScript declaration type and content.
+buildDeclarationType :: DeclarationType -> TLB.Builder
+buildDeclarationType (ExportDec isDefault symbolName) =
+ "export "
+ <> (if isDefault then "default " else "")
+ <> TLB.fromString symbolName
+ <> ";"
+buildDeclarationType (FunctionDec header) =
+ "declare function " <> buildFunctionHeader header <> ";"
+buildDeclarationType (InterfaceDec name properties) =
+ "declare interface "
+ <> TLB.fromString name
+ <> " {"
+ <> mconcat (map (\prop -> "\n" <> buildInterfaceContent prop <> "\n") properties)
+ <> "}"
+
+-- | Represents a function parameter in TypeScript.
+data FunctionParam = FunctionParam
+ { paramName :: String
+ -- ^ Name of the parameter.
+ , paramType :: String
+ -- ^ Type of the parameter.
+ }
+
+-- | Creates a builder for a TypeScript function parameter.
+buildFunctionParam :: FunctionParam -> TLB.Builder
+buildFunctionParam (FunctionParam name pType) =
+ TLB.fromString name <> ": " <> TLB.fromString pType
+
+-- | Represents a TypeScript function header.
+data FunctionHeader = FunctionHeader
+ { functionName :: String
+ -- ^ Name of the function.
+ , functionParams :: [FunctionParam]
+ -- ^ List of parameters of the function.
+ , functionReturnType :: String
+ -- ^ Return type of the function.
+ }
+
+-- | Creates a builder for a TypeScript function header.
+buildFunctionHeader :: FunctionHeader -> TLB.Builder
+buildFunctionHeader (FunctionHeader name params returnType) =
+ TLB.fromString name
+ <> "("
+ <> mconcatWith ", " (map buildFunctionParam params)
+ <> "): "
+ <> TLB.fromString returnType
+
+-- | Represents a TypeScript interface content of some type
+-- out of the ones defined by 'InterfaceContentType'.
+data InterfaceContent = InterfaceContent
+ { interfaceContentComment :: [String]
+ -- ^ Comments for the interface content.
+ , interfaceContentValue :: InterfaceContentType
+ -- ^ The type and content of the interface.
+ }
+
+-- | Creates a builder for a TypeScript interface content.
+buildInterfaceContent :: InterfaceContent -> TLB.Builder
+buildInterfaceContent (InterfaceContent [] interfaceType) = buildInterfaceContentType interfaceType
+buildInterfaceContent (InterfaceContent comments interfaceType) =
+ let indentationAmount = 4
+ indentation = TLB.fromLazyText $ TL.replicate (fromIntegral indentationAmount) " "
+ comment = buildMultilineComment indentationAmount comments
+ in comment <> "\n" <> indentation <> buildInterfaceContentType interfaceType
+
+-- | Represents a TypeScript interface type and content.
+data InterfaceContentType
+ = -- | Defines a property in the interface.
+ InterfaceProperty
+ String
+ -- ^ Property name.
+ String
+ -- ^ Property type.
+ | -- | Defines a method in the interface.
+ InterfaceMethod
+ String
+ -- ^ Method name.
+ [FunctionParam]
+ -- ^ Method parameters.
+ String
+ -- ^ Return type of the method.
+
+-- | Creates a builder for a TypeScript interface content and type.
+buildInterfaceContentType :: InterfaceContentType -> TLB.Builder
+buildInterfaceContentType (InterfaceProperty name pType) =
+ TLB.fromString name <> ": " <> TLB.fromString pType <> ";"
+buildInterfaceContentType (InterfaceMethod name params returnType) =
+ TLB.fromString name
+ <> "("
+ <> mconcatWith ", " (map buildFunctionParam params)
+ <> "): "
+ <> TLB.fromString returnType
+ <> ";"
+
+-- | Concatenates a list of builders with a separator.
+-- If the list is empty, it returns an empty builder.
+mconcatWith :: TLB.Builder -> [TLB.Builder] -> TLB.Builder
+mconcatWith separator = maybe mempty (foldr1 (\a b -> a <> separator <> b)) . LNE.nonEmpty
diff --git a/cardano-wasm/test/cardano-wasm-golden/Test/Golden/Cardano/Wasm/TypeScript.hs b/cardano-wasm/test/cardano-wasm-golden/Test/Golden/Cardano/Wasm/TypeScript.hs
new file mode 100644
index 0000000000..0248839921
--- /dev/null
+++ b/cardano-wasm/test/cardano-wasm-golden/Test/Golden/Cardano/Wasm/TypeScript.hs
@@ -0,0 +1,10 @@
+module Test.Golden.Cardano.Wasm.TypeScript where
+
+import Hedgehog as H
+import Hedgehog.Extras qualified as H
+
+hprop_cardano_wasm_typescript_declarations_match_generated :: Property
+hprop_cardano_wasm_typescript_declarations_match_generated =
+ H.propertyOnce $ do
+ result <- H.execFlex "cardano-wasm" "CARDANO_WASM" []
+ H.diffVsGoldenFile result "lib-wrapper/cardano-api.d.ts"
diff --git a/cardano-wasm/test/cardano-wasm-golden/cardano-wasm-golden.hs b/cardano-wasm/test/cardano-wasm-golden/cardano-wasm-golden.hs
new file mode 100644
index 0000000000..de4594616b
--- /dev/null
+++ b/cardano-wasm/test/cardano-wasm-golden/cardano-wasm-golden.hs
@@ -0,0 +1,8 @@
+{-# LANGUAGE CPP #-}
+
+#if !defined(wasm32_HOST_ARCH)
+{-# OPTIONS_GHC -F -pgmF tasty-discover -optF --hide-successes #-}
+#else
+main = return ()
+#endif
+
diff --git a/flake.lock b/flake.lock
index 8db53f7699..3b72672e95 100644
--- a/flake.lock
+++ b/flake.lock
@@ -565,6 +565,24 @@
"type": "github"
}
},
+ "incl": {
+ "inputs": {
+ "nixlib": "nixlib"
+ },
+ "locked": {
+ "lastModified": 1693483555,
+ "narHash": "sha256-Beq4WhSeH3jRTZgC1XopTSU10yLpK1nmMcnGoXO0XYo=",
+ "owner": "divnix",
+ "repo": "incl",
+ "rev": "526751ad3d1e23b07944b14e3f6b7a5948d3007b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "divnix",
+ "repo": "incl",
+ "type": "github"
+ }
+ },
"iohkNix": {
"inputs": {
"blst": "blst",
@@ -603,6 +621,21 @@
"type": "github"
}
},
+ "nixlib": {
+ "locked": {
+ "lastModified": 1667696192,
+ "narHash": "sha256-hOdbIhnpWvtmVynKcsj10nxz9WROjZja+1wRAJ/C9+s=",
+ "owner": "nix-community",
+ "repo": "nixpkgs.lib",
+ "rev": "babd9cd2ca6e413372ed59fbb1ecc3c3a5fd3e5b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-community",
+ "repo": "nixpkgs.lib",
+ "type": "github"
+ }
+ },
"nixpkgs": {
"locked": {
"lastModified": 1744098102,
@@ -772,6 +805,7 @@
"ghc-wasm-meta": "ghc-wasm-meta",
"hackageNix": "hackageNix",
"haskellNix": "haskellNix",
+ "incl": "incl",
"iohkNix": "iohkNix",
"nixpkgs": "nixpkgs_3",
"wasm-nixpkgs": [
diff --git a/flake.nix b/flake.nix
index 2a394098b2..59567ac51e 100644
--- a/flake.nix
+++ b/flake.nix
@@ -16,6 +16,7 @@
nixpkgs.url = "github:NixOS/nixpkgs/4284c2b73c8bce4b46a6adf23e16d9e2ec8da4bb";
iohkNix.url = "github:input-output-hk/iohk-nix";
flake-utils.url = "github:hamishmack/flake-utils/hkm/nested-hydraJobs";
+ incl.url = "github:divnix/incl";
# non-flake nix compatibility
flake-compat = {
url = "github:edolstra/flake-compat";
@@ -137,7 +138,7 @@
# package customizations as needed. Where cabal.project is not
# specific enough, or doesn't allow setting these.
modules = [
- ({pkgs, ...}: {
+ ({...}: {
packages.cardano-api = {
configureFlags = ["--ghc-option=-Werror"];
components = {
@@ -149,6 +150,17 @@
};
};
})
+ ({pkgs, config, ...}: let
+ generatedExampleFiles = ["cardano-wasm/lib-wrapper/cardano-api.d.ts"];
+ exportWasmPath = "export CARDANO_WASM=${config.hsPkgs.cardano-wasm.components.exes.cardano-wasm}/bin/cardano-wasm${pkgs.stdenv.hostPlatform.extensions.executable}";
+ in {
+ packages.cardano-wasm.components.tests.cardano-wasm-golden.preCheck = let
+ filteredProjectBase = inputs.incl ./. generatedExampleFiles;
+ in ''
+ ${exportWasmPath}
+ cp -r ${filteredProjectBase}/* ..
+ '';
+ })
{
packages.crypton-x509-system.postPatch = ''
substituteInPlace crypton-x509-system.cabal --replace 'Crypt32' 'crypt32'