-
Notifications
You must be signed in to change notification settings - Fork 7
ADR 015 Cardano API WASM library for browser
Status: Adopted
Date: 2025-05-22
Decentralized Applications (DApps) mainly revolve around web browsers. The Cardano blockchain platform features cardano-api
, a comprehensive Haskell library developed in conjunction with cardano-node
, and offers extensive blockchain interaction capabilities implemented in a robust way thanks to strong typing, formal specifications, and extensive testing.
For developers building on Cardano, a range of JavaScript and TypeScript libraries are available. These include tools such as Lucid Evolution, Helios, MeshJS, cardano-serialization-lib, cardano-sdk-js, Ogmios, and TyphonJS.
Concurrently, efforts that provide compilation from Haskell to WebAssembly (WASM) are reaching increasingly wider support for the functionalities provided by Haskell (recently expanding to include advanced features like Template Haskell). WebAssembly is a technology that can be run in the same environments that JavaScript and TypeScript can, and that it can be invoked from those languages. And, like those languages, WebAssembly can provide strong sandboxing and widespread portability, particularly in browsers.
This Architectural Decision Record (ADR), explores the creation of a new JavaScript and TypeScript API for Cardano. The primary method proposed is to compile the existing cardano-api
(written in Haskell) to WebAssembly (WASM) and to write a thin wrapper around it.
As described in the previous section, there already exist several JavaScript/TypeScript libraries and APIs that support DApps. Nevertheless, the extensive and robust features provided by cardano-api
would benefit DApps, so compiling it to WebAssembly and providing a JavaScript and TypeScript API would provide several advantages:
-
Comprehensiveness:
cardano-api
is especially comprehensive in its functionalities, since it is developed in sync withcardano-node
. -
Robustness: Because
cardano-api
is developed in Haskell, it relies heavily in type correctness, and because it is heavily tested, it is especially trustworthy. Because the JavaScript/TypeScript API would take advantage of the same code, the robustness would be inherited. -
Pure browser support: DApp libraries tend to be more focused on supporting execution in the backend (through Node.js and Bun). There are good reasons for doing this, but explicitly supporting JavaScript in the browser has some advantages:
- Incentivize adoption: By making the library easier to try out, we would be reducing the friction to adoption.
- Improved trust through decentralisation: Since code would be running in the browser, and thus the user's computer, it may be easier for the user to trust it than it would for an opaque web service. Transaction signing necessarily happens on the client side anyway, because it needs to be done by the user's wallet, but it would be even more trustworthy and decentralised if the whole dApp logic code would also run in the user's computer and if it was inspectable (which is typically the case for unobfuscated JavaScript). It would also reduce the amount of information that needs to be shared with the server, increasing privacy for users.
-
Easier deployment: Static web apps are easier to deploy than dynamic ones, because only a web server is needed. They also require less processing on the web server, less memory usage per user (it can be done statelessly), they benefit much more from optimisations like caching, and their execution model is simpler (since they doesn't require complex synchronisation between frontend and backend). In fact, there are services like
github.io
that allow hosting static pages free of charge, and this is much harder to find for dynamic pages. For this reason, being able to make static dApps would lower the barrier for hosting them which would, in turn, potentially lead to increased adoption as well.
Other potential opportunities and considerations to keep in mind:
-
Aditional conectivity: We can potentially provide ways to connect to a
cardano-node
directly. And we can consider offering a provider that is uses CORS, which would allow dApps run purely in the browser (see Appendix). - Ease of Use & Documentation: Most popular libraries provide introductory guides, that are comprehensive, simple, clear. Autocompletion in IDEs is crucial and often serves as de-facto documentation. In addition to all that, we should make comprehensive documentation for the whole API (reference) and make it easily accessible.
- CIP-30 Integration: Existing libraries seem to provide wallet support through CIP-30. We may want to do that too, or at least make it easy for our API to interact with already existing libraries and APIs that already support CIP-30. The later may be hard because some of the most popular libraries integrate the wallets and transaction handling very closely and not necessarily in a functionaly pure way. It may also be used as a provider of information to some extent.
-
Browser Compatibility & Testing: For the reasons we mentioned before, we should use the CI to test compatibility with all potential workflows, including vanilla JavaScript and TypeScript from the browser, as well as integration with
Node.js
. Testing pure browser workflows can be tricky because we may need to use UI testing libraries likePlaywright
. -
Standard publication: It is pretty standard for
Node.js
libraries to be distributed through channels likenpm
, so we should probably make sure to upload the library to those.
We propose to create a new JavaScript and TypeScript library API for Cardano by compiling the cardano-api
(Haskell) to WebAssembly (WASM) and adding a thin wrapper. This would allow developers to easy and cross-platform access to a subset of cardano-api
functionalities from the browser and Node.js
environments and similar.
Key Features & Design Goals:
-
WASM core: Compile
cardano-api
to WASM to leverage its extensive features, and correctness. -
JavaScript/TypeScript interface:
- Provide simple, user-friendly, and well documented JavaScript ES6 module and TypeScript bindings.
- Offer strong typing with TypeScript for enhanced robustness (types) and developer experience (autocompletion).
- Support vanilla JavaScript usage for easy testing and adoption.
-
API design:
-
Low-Level access: Expose a subset of
cardano-api
with a thin wrapper. -
High-Level abstractions (potential): Consider optional, higher-level APIs, probably as separate modules. Examples:
- A "virtual wallet" abstraction that handles UTXO management and coin selection automatically.
- Simplified (possibly monadic) transaction building.
- CIP-30 support: Potentially support CIP-30 compatibility to interact with browser wallet extensions, either as a separate module or as part of the "virtual wallet" abstraction.
- Data providers: Allow configuration of blockchain data providers (e.g., BlockFrost, Koios). Consider offering a CORS-friendly proxy service (with robust caching to mitigate DDoS risk) to provide support for frontend-only DApp development.
-
Low-Level access: Expose a subset of
-
Development Experience:
-
Minimal JS glue: Maximize Haskell code and minimize the JavaScript FFI glue code and try to generate glue code automatically if possible. This allows leveraging Haskell for most of the development, improving our productivity and the type safety of our code.
-
Haskell API mirroring: Because WASM ghc compiler target doesn't seem to support
haskell-language-server
, we should keep a pure Haskell API that closely mirrors the desired JavaScript API structure to be able to work on it using the HLS as far as possible, and use FFI just for type conversion. -
Type conversion: Implement
ToJSVal
/FromJSVal
type-classes in Haskell, analogously toToJSON
/FromJSON
in Aeson, for conversions between Haskell and JS types. JSON string serialization can be used as an efficient intermediate representation. The mechanism can follow these steps:- Check if the Haskell type has an
Aeson
instance, if it doesn't convert it into aTextEnvelope
. - Take the Haskell type with
Aeson
instance or itsTextEnvelope
(which has anAeson
instance), and use the instance to serialise it as aString
containing its JSON representation. - Convert the
String
toJSString
(using thetoJSString
function fromGHC.Wasm.Prim
in theghc-experimental
package) - Deserialise the JSON in the
JSString
using theJSON.parse
JavaScript function. This can be from Haskell by using aforeign import
, and that will give us aJSVal
, which we can pass directly to JavaScript through the API.
The same process can be followed in the reverse direction by using
JSON.stringify
function in JavaScript, and thefromJSString
function fromGHC.Wasm.Prim
. - Check if the Haskell type has an
-
Error handling: Ensure clear, descriptive, and catchable errors.
-
BigInt for large amounts: Use JavaScript
BigInt
for Ada/Lovelace amounts to prevent precision loss (also serialised/deserialised as/from strings).
-
-
Documentation & Examples:
- Comprehensive API documentation that is easy to navigate.
- Step-by-step guides for getting started, including handling
async
calls. - Clear examples for main use cases.
- Ensure good IDE autocompletion support (like in Visual Studio Code).
-
Distribution: Distribute the library via NPM and potentially in
github.io
. -
Testing:
- CI pipelines to consider for:
- WASM compilation. (Build)
- Generation and consistency of JS/TS bindings. (Like with golden tests)
- Functionality across supported environments. (Browsers via tools like Playwright, and also Node.js)
- Glue generation verification. (Like with golden tests)
- CI pipelines to consider for:
- Stability: Aim to maintain a stable API to avoid user frustration.
Haskell to JavaScript Bridging Example:
We would make a Haskell wrapper that is really close to what we want at the JavaScript level but using Haskell terms:
signTransactionImpl :: Api.TxBody Api.ConwayEra -> Api.SigningKey Api.PaymentKey -> IO (Api.Tx Api.ConwayEra)
signTransactionImpl unsignedTx signingKey = do
let sbe :: Api.ShelleyBasedEra Api.ConwayEra = Api.shelleyBasedEra
let witness = Api.WitnessPaymentKey signingKey
let oldApiSignedTx :: Api.Tx Api.ConwayEra = Api.signShelleyTransaction sbe unsignedTx [witness]
return oldApiSignedTx
Then we would write a conversion layer that is still in Haskell, but converts to and from JavaScript marshallable types and uses the pure Haskell function:
foreign export javascript "signTransaction"
signTransaction :: JSVal -> JSString -> IO JSVal
signTransaction txBody privKey = do
-- Convert JS params to Haskell types
unsignedTx <- jsValToType (Api.AsTxBody Api.AsConwayEra) txBody
let (Right signingKey) = Api.deserialiseFromBech32 (Api.AsSigningKey Api.AsPaymentKey) (Text.pack (fromJSString privKey))
-- Call the pure Haskell function
oldApiSignedTx <- signTransactionImpl unsignedTx signingKey
-- Convert Haskell type to JS
let envelope = Api.serialiseToTextEnvelope (Just "Ledger Cddl Format") oldApiSignedTx
jsonToJSVal envelope
This could be done more generically with a FromJS
and ToJS
class, as mentioned.
JavaScript Initialization and Usage Example:
The JavaScript glue can be very simple and basically packages the FFI Haskell functions:
// cardano-api.js (or similar entry point for the library)
import { WASI } from "https://unpkg.com/@bjorn3/[email protected]/dist/index.js";
import ghc_wasm_jsffi from "./cardano-wasm.js"; // This is generated automatically from WASM
const __exports = {};
const wasi = new WASI([], [], []);
// Initialisation function returns a promise to an object with the API
async function initialize() {
let {instance} = await WebAssembly.instantiateStreaming(fetch("./cardano-wasm.wasm"), {
ghc_wasm_jsffi: ghc_wasm_jsffi(__exports),
wasi_snapshot_preview1: wasi.wasiImport,
})
Object.assign(__exports, instance.exports);
wasi.initialize(instance);
// Re-export desired Haskell functions
return { mkTransaction: instance.exports.mkTransaction
, signTransaction: instance.exports.signTransaction
, mkTxIn: instance.exports.mkTxIn
};
}
// We only expose the initialization function
export default initialize;
Example usage of the JS API could be as simple as defining an async
function and then calling .then()
on it:
<html>
<body>
<script type="module">
import cardano_api from "./cardano-api.js";
let promise = cardano_api();
async function do_async_work() {
let api = await promise;
let txIn = await api.mkTxIn("be6efd42a3d7b9a00d09d77a5d41e55ceaf0bd093a8aa8a893ce70d9caafd978", 0);
console.log("Tx input:");
console.log(txIn);
let destAddr = "addr_test1vzpfxhjyjdlgk5c0xt8xw26avqxs52rtf69993j4tajehpcue4v2v";
let privKey = "addr_sk1648253w4tf6fv5fk28dc7crsjsaw7d9ymhztd4favg3cwkhz7x8sl5u3ms";
let amountInLovelace = 10_000_000n; // We pass Lovelace amounts as BigInt
let feesInLovelace = 2_000_000n;
let unsignedTx = await api.mkTransaction(txIn, destAddr, amountInLovelace, feesInLovelace);
console.log("Tx body:");
console.log(unsignedTx);
let signedTx = await api.signTransaction(unsignedTx, privKey);
console.log("Signed tx:");
console.log(signedTx);
}
do_async_work().then(() => {});
</script>
</body>
</html>
Stateful API (Wallet-like features):
If we do a stateful API (e.g., for a "virtual wallet"), the state can be managed in Haskell. Each function in the stateful API would take the state as its first parameter and return a tuple with the new state and the resulting state (this can be modeled as a State
monad). The JavaScript glue code would hold this state globally and pass it automatically for each function call, making it appear as an object-oriented API in JavaScript. This state could include loaded private keys (handled securely), UTXO sets, etc. This could be implemented as an abstraction layer that uses the core stateless functions under the hood.
For example, let's imagine we have a wallet with a derivation scheme, and we want a stateful function that takes an unsigned transaction and it derives a new key from the derivation scheme, records the number of derivated keys, it uses the newly derived key to sign the transaction and it returns it signed. In Haskell the function doing this we could have a signature like:
signTx :: WalletState -> Api.TxBody Api.ConwayEra -> IO (WalletState, Api.Tx Api.ConwayEra)
Or the State
monad equivalent. Then we would export it as JavaScript as an async function with two parameters (the wallet state, and the unsigned transaction), and it would return a list with two elements (the new wallet state, and the signed transaction), because JavaScript doesn't have tuples.
Then, as part of the JavaScript wrapper where we re-export the Haskell functions, we can instead export a wrapper function. Let's assume the Haskell function is exported with the name haskellExportedSignTxFunction
(for illustrative purpouses). Then we can create a wrapper function in JavaScript as follows:
async function signTx(unsignedTx) {
var result = await haskellExportedSignTxFunction(state, unsignedTx);
var [ newState, signedTx ] = result;
state = newState;
return signedTx;
}
Where state
is a variable declared globally in JavaScript. This way, the user doesn't need to think about the state
since it is passed automatically to the function signTx
automatically.
- A powerful, secure, and comprehensive Cardano library for browser and Node.js environments.
- Enhanced DApp developer productivity and capability.
- Greater potential for decentralization and user transparency in DApps.
- Establishes a robust foundation for future tooling and higher-level libraries.
- Ability to work primarily in Haskell with HLS for the core logic.
- Complexity of setting up and maintaining the Haskell-to-WASM compilation toolchain and FFI.
- Potential initial performance overhead or bundle size issues with WASM (mitigation through optimization).
- Potential difficulties when debugging across the Haskell-WASM-JS boundary.
- Effort required for manual creation and maintenance of TypeScript definitions if generation is not feasible or good enough.
- Effort required for developing comprehensive tests, especially browser-based integration tests (e.g., using Playwright).
- Effort required to design JS/TS API from a DApp user perspective.
- Effort required to maintain compatibility with WASM of all dependencies of
cardano-api
through changes and upgrades.
Developing a JavaScript/TypeScript API for Cardano by compiling cardano-api
to WASM is very feasible and we can make it easy and frictionless for the user. We must make sure to keep it simple, stable, and test all the user flows. We also should ensure we use pure Haskell as far as possible to take advantage of HLS as much as possible, and we can automate as much of the glue layer generation as possible in two ways:
- Generalising the boilerplate so that it is reusable and structured.
- Making the API inspectable by JavaScript and deriving part of it using JavaScript reflection capabilities.
- Using meta programming to generate boilerplate (Template Haskell).
We must also provide a TypeScript declaration file for the API to both make it easier (through auto-complete documentation) and safer (through types) for users to develop using the API. And we will aim to derive the type declaration file automatically from the Haskell code to ensure consistency. We should also ensure compatibility with the different workflows and consistency through the use of CI whenever possible.
SOP (or Same-Origin Policy) is a restriction that is enforced by browsers to protect both users and servers from potential attacks from malicious web applications to other web applications or APIs unless they opt out by using CORS (Cross-Origin Resource Sharing), which is a mechanism that lets servers allow web applications to circumvent the Same-Origin Policy restriction. In our particular case, a service that offers a public API would be protected by SOP because a random JS application wouldn't be able to access it directly from the user's browser (i.e: the user's computer). Instead, the app would be forced to access the API from the backend of the JS application, and that helps security because the source for the request is specifc to the app, since it is done from the app's backend server. And the requests from that server:
- Can easily be blocked if the API is abused.
- Cannot pass as the user (because the request won't include the user's cookies, for example). Additionally, if SOP was not in place, a very popular but malicious website could easily include a JavaScript snippet that would cause every visitor of the malicious website to silently make potentially costly requests against a victim API, and that would potentially work out as DDOS (Distribute Denegation of Services) attack, by consuming lots of resources from the victim API.
Unfortunately, this means that JavaScript cannot access public API's unless those API's use CORS to lift SOP restrictions. This ADR suggests that there could potentially exist a service like Koios that does this, as long as it can be guaranteed that the queries offered by the API are made in a way that is efficient enough, and user supplantation is made impossible or not applicable.
The cardano-node
wiki has moved. Please go to (https://github.com/input-output-hk/cardano-node-wiki/wiki) and look for the page there.