Qoda is a marketplace exchange for loans, currently built on Moonbeam, where you can borrow and lend on your own terms.
The UI is built with Elm and it communicates with the blockchain via ports.
Here’s a demo of the DApp in action:
The main thing I wanted to talk more about was our use of ports.
How we use ports
The blockchain is an external service we access using the ethers.js library. If you need to sign requests, for e.g. when depositing collateral, then we also interact with the MetaMask wallet. Overall, we currently have 127 possible requests that can be sent from Elm to JavaScript and 34 possible responses that can be sent from JavaScript to Elm. A naive use of ports requires 161 total ports. However, by heeding the advice Evan shares in the Elm Guide (the relevant part is quoted below) we actually only use two ports.
Definitely do not try to make a port for every JS function you need. You may really like Elm and want to do everything in Elm no matter the cost, but ports are not designed for that. Instead, focus on questions like “who owns the state?” and use one or two ports to send messages back and forth. If you are in a complex scenario, you can even simulate
Msg
values by sending JS like{ tag: "active-users-changed", list: ... }
where you have a tag for all the variants of information you might send across.
Here’s how we do it.
Port.elm
port module Port exposing (sendMessage, receive)
import Json.Encode as JE
import Message exposing (Message)
sendMessage : Message -> Cmd msg
sendMessage =
Message.encode >> send
port send : JE.Value -> Cmd msg
port receive : (JE.Value -> msg) -> Sub msg
Message.elm
module Message exposing (Message, empty, string, object, encode)
import Json.Encode as JE
type Message
= Message
{ tag : String
, value : JE.Value
}
empty : String -> Message
empty tag =
Message
{ tag = tag
, value = JE.null
}
string : String -> String -> Message
string tag s =
Message
{ tag = tag
, value = JE.string s
}
object : String -> List ( String, JE.Value ) -> Message
object tag fields =
Message
{ tag = tag
, value = JE.object fields
}
encode : Message -> JE.Value
encode (Message { tag, value }) =
JE.object
[ ( "tag", JE.string tag )
, ( "value", value )
]
So to deposit collateral at some point we need to create and send the following:
depositCollateralMessage :
{ accountAddress : Address
, assetAddress : Address
, amount : Amount
, wrapInMoonwell : Bool
}
-> Message
depositCollateralMessage { accountAddress, assetAddress, amount, wrapInMoonwell } =
Message.object
"action.depositCollateral"
[ ( "accountAddress", Address.encode accountAddress )
, ( "assetAddress", Address.encode assetAddress )
, ( "amount", Amount.encode amount )
, ( "wrapInMoonwell", JE.bool wrapInMoonwell )
]
-- ...
Port.sendMessage <| depositCollateralMessage { ... }
Key takeaways
- You only need one port module,
Port.elm
. - No application ever needs more than 2 ports.
- Ports work great and there’s no need to think it’s a subpar solution to JavaScript interoperability.
- Elm is a capable technology that can be used as it exists today. We haven’t experienced any roadblocks using it.