Normally, dApps involve three parts:
- on-chain logic (Plutus, Plutarch or Aiken scripts)
- off-chain logic (in our case, implemented using CTL)
- user interface
Providing CTL-based JavaScript SDKs is the simplest way to connect user interfaces (most commonly, web apps) with off-chain logic. These SDKs expose app-specific APIs for web developers to plug into the user interface. SDKs are normally consumable as NPM packages.
Explore the template or CTL itself for an example setup. See the WebPack config or the esbuild config we provide.
NodeJS apps do not require to be bundled in order to be run (however, it is possible, see the Makefile options).
An NPM package with main
set to the compiled JS entry point (e.g. ./output/ApiModuleName.js
) is sufficient, but the runtime dependencies of said package must include the same package versions CTL itself uses in its package.json
.
SDKs must be bundled to be usable in the browser. We support two bundlers: esbuild and WebPack. There are two options how to approach bundling and packaging:
-
bundling a CTL-based SDK before consuming it as dependency in the app, i.e. putting the bundled sources in an NPM package. Bundling twice is not a good practice, and it is hard to even make it work, so in case this path is chosen, the developer should ensure that the SDK does not get bundled twice by the second bundler.
-
[recommended] bundling a CTL-based SDK together with the UI part of the app. This is simpler, but in case a bundler different from esbuild or WebPack is used, problems may arise due to bundler differences. It should be possible to use other bundlers, as long as they support async top-level imports, WebAssembly and
browser
package.json field.
Developers should start from reading this PureScript guide that shows how to call PureScript from JS.
Suppose we want to wrap a single Contract
into an interface to call it from JS with Nami wallet.
We have to expose functions to manage contract environment - initialization and finalization, as well as a config value we will use.
module Api where
import Prelude
import Contract.Config (ContractParams, testnetNamiConfig)
import Contract.JsSdk (mkContractEnvJS, stopContractEnvJS)
import Contract.Monad (ContractEnv, runContractInEnv)
import Control.Promise (Promise, fromAff)
import Data.Function.Uncurried (Fn1, mkFn1)
import Effect.Unsafe (unsafePerformEffect)
import Scaffold (contract) -- our contract
initialize :: Fn1 ContractParams (Promise ContractEnv)
initialize = mkContractEnvJS
finalize :: Fn1 ContractEnv (Promise Unit)
finalize = stopContractEnvJS
run :: Fn1 ContractEnv (Promise Unit)
run = mkFn1 \env ->
unsafePerformEffect $ fromAff $ runContractInEnv env contract
config :: ContractParams
config = testnetNamiConfig -- use Nami wallet
Fn1
-Fn10
types are wrappers that represent uncurried JavaScript functions with multiple arguments, andmkFn1
-mkFn10
are their constructors.Contract.JsSdk
is a module containing synonyms for someContract.Monad
functions, but adapted for use in JS SDKs.fromAff
convertsAff a
toEffect (Promise a)
, andunsafePerformEffect
removes theEffect
wrapper that is not needed on the JS side.
The module above can be imported like this:
import { initialize, config, run, finalize } from 'your-api-package';
(async () => {
const env = await initialize(config);
try {
await run(env);
} finally {
await finalize(env);
}
})();
Notice that we used finally
to finalize - this is because a running contract environment would prevent the script from exiting otherwise. Please read this guide for info on how to manage the runtime environment correctly.