Packages

DiodeClient allows direct P2P connection to any other DiodeClient user on the planet using Ethereum Addresses (e.g. 0xb794f5ea0ba39494ce839613fffba74279579268) and BNS names (e.g. yourname.diode) instead of IPv4 or IPv6

Current section

Files

Jump to
diode_client README.md
Raw

README.md

# DiodeClient
DiodeClient secure end-to-end encrypted connections between any two machines. Connections are established
either through direct peer-to-peer TCP connections or bridged via the Diode network. To learn more about the
decentralized Diode network visit https://diode.io/
Example usage with a simple server + client. For this to work open each in individual terminal:
```elixir
# Server
DiodeClient.interface_add("example_server_interface")
address = DiodeClient.Base16.encode(DiodeClient.address())
{:ok, port} = DiodeClient.port_listen(5000)
spawn_link(fn ->
IO.puts("server #{address} started")
{:ok, ssl} = DiodeClient.port_accept(port)
peer = DiodeClient.Port.peer(ssl)
IO.puts("got a connection from #{Base.encode16(peer)}")
:ssl.controlling_process(ssl, self())
:ssl.setopts(ssl, [packet: :line, active: true])
for x <- 1..10 do
IO.puts("sending message #{x}")
:ssl.send(ssl, "Hello #{Base.encode16(peer)} this is message #{x}\n")
end
receive do
{:ssl_closed, _ssl} -> IO.puts("closed!")
end
end)
```
And the client. Here insert in the server address the address that has been printed above.
For example `server_address = "0x389eba94b330140579cdce1feb1a6e905ff876e6"`
```elixir
# Client: Below enter your server address
server_address = "0x389eba94b330140579cdce1feb1a6e905ff876e6"
DiodeClient.interface_add("example_client_interface")
spawn_link(fn ->
{:ok, ssl} = DiodeClient.port_connect(server_address, 5000)
:ssl.controlling_process(ssl, self())
:ssl.setopts(ssl, [packet: :line, active: true])
Enum.reduce_while(1..10, nil, fn _, _ ->
receive do
{:ssl, _ssl, msg} -> {:cont, IO.inspect(msg)}
other -> {:halt, IO.inspect(other)}
end
end)
:ssl.close(ssl)
IO.puts("closed!")
end)
```
And the client. Here insert in the server address the address that has been printed above.
For example `server_address = "0x389eba94b330140579cdce1feb1a6e905ff876e6"`
```elixir
# Client:
server_address = "0x389eba94b330140579cdce1feb1a6e905ff876e6"
DiodeClient.interface_add("example_client_interface")
spawn_link(fn ->
{:ok, ssl} = DiodeClient.port_connect(server_address, 5000)
:ssl.controlling_process(ssl, self())
:ssl.setopts(ssl, [packet: :line, active: true])
Enum.reduce_while(1..10, nil, fn _, _ ->
receive do
{:ssl, _ssl, msg} -> {:cont, IO.inspect(msg)}
other -> {:halt, IO.inspect(other)}
end
end)
:ssl.close(ssl)
IO.puts("closed!")
end)
```
## Blockchain Interaction
For limited access to supported blockchain source of truth data :diode_client supports reading from smart contracts and calling contract methods. For each supported blockchain there is a `Shell` configured, currently supported blockchains are:
- Diode L1 (DiodeClient.Shell) - deprecated
- Moonbeam (Diodeclient.Shell.Moonbeam) - https://moonbeam.network/
- Oasis Sapphire (DiodeClient.Shell.OasisSapphire) - https://oasis.net/sapphire
Each of these support `call/5` and other methods to read contract data and send transactions.
- **Anvil** (DiodeClient.Shell.Anvil) – local test chain from [Foundry](https://getfoundry.sh/). Use it in **unit and integration tests** so downstream libraries can run tests against a temporary chain without hitting real networks. RPC URL and chain ID are configurable via `ANVIL_RPC_URL` (default `http://127.0.0.1:8545`) and `ANVIL_CHAIN_ID` (default `31337`).
Example of making a ZTNA contract call on Oasis Sapphire:
```elixir
alias Diodeclient.{Base16, Shell}
Shell.OasisSapphire.call(
Base16.decode("0xb78700e7254F54b418bdF6DE7109128D1Fe8E8DD"),
"getPropertyValue",
["address", "string"],
[Base16.decode("0x90983fc294577b6f00CBd5D3b26aDf2e85Ca2Cac"), "public"],
result_types: "string"
)
```
### Using the Anvil shell in downstream unit tests
Libraries that depend on `:diode_client` can run tests against a local Anvil chain so they don’t touch real networks. Add the following to your **test helper** and tag tests that need Anvil.
**Prerequisites**
- [Foundry](https://getfoundry.sh/) (`anvil` and `forge` on `PATH`). Install with: `curl -L https://foundry.paradigm.xyz | bash` then `foundryup`.
- Optional: [diode_contract](https://github.com/diodechain/diode_contract) and `ANVIL_CONTRACT_REPO_PATH` if you need `DiodeClient.Contracts.Factory.contracts(DiodeClient.Shell.Anvil)` (e.g. identity/factory tests).
**Starting Anvil for tests**
- **Manual**: In a separate terminal run `anvil` (default: `http://127.0.0.1:8545`). Leave it running while you run `mix test`.
- **Helper (recommended)**: Call `DiodeClient.Anvil.Helper.start_anvil()` from your `test/test_helper.exs` before `ExUnit.start()`. It spawns Anvil in the background and waits until the RPC is reachable (or times out). If Foundry is not installed or Anvil fails to start, exclude `:anvil` tests so `mix test` still passes. See the test_helper examples below.
**Initialization in `test/test_helper.exs`**
1. **Start Anvil in background + wallet** (recommended; `mix test` works with no manual Anvil):
```elixir
case DiodeClient.Anvil.Helper.start_anvil() do
{:ok, _} -> :ok
{:error, _} -> ExUnit.configure(exclude: [anvil: true])
end
DiodeClient.Anvil.Helper.ensure_test_env(wallet: "test_anvil")
ExUnit.start()
```
2. **Anvil only** (you start Anvil manually; no contract deployment):
```elixir
DiodeClient.Anvil.Helper.ensure_test_env(wallet: "test_anvil")
ExUnit.start()
```
3. **Exclude `:anvil` when Anvil is not running** (so `mix test` passes without Foundry):
```elixir
if not DiodeClient.Anvil.Helper.anvil_reachable?() do
ExUnit.configure(exclude: [anvil: true])
end
DiodeClient.Anvil.Helper.ensure_test_env(wallet: "test_anvil")
ExUnit.start()
```
4. **Anvil + deploy diode_contract** (for tests that need `Factory.contracts(DiodeClient.Shell.Anvil)`):
```elixir
case DiodeClient.Anvil.Helper.ensure_test_env(wallet: "test_anvil", deploy_contracts: true) do
:ok -> :ok
{:error, :anvil_not_reachable} -> ExUnit.configure(exclude: [anvil: true])
{:error, _} -> ExUnit.configure(exclude: [anvil: true])
end
ExUnit.start()
```
**In your tests**
- Use `DiodeClient.Shell.Anvil` like any other shell: `Anvil.peak()`, `Anvil.get_account(address)`, `Anvil.call(...)`, etc.
- Tag tests that require Anvil with `@tag :anvil` so you can exclude them when Anvil is not running: `mix test --exclude anvil`.
- If you use `ensure_test_env(deploy_contracts: true)`, you can call `DiodeClient.Contracts.Factory.contracts(DiodeClient.Shell.Anvil)` and use factory/drive/BNS addresses in tests.
**Helpers**
- `DiodeClient.Anvil.Helper.start_anvil(opts \\ [])` – spawns Anvil in the background and waits until the RPC is reachable. Options: `:rpc_url`, `:timeout` (ms), `:port`, `:args`. Returns `{:ok, port}` or `{:error, :executable_not_found}` / `{:error, :timeout}`. Use in test_helper so `mix test` works without manually starting Anvil.
- `DiodeClient.Anvil.Helper.anvil_reachable?(rpc_url \\ nil)` – returns whether the Anvil RPC endpoint is reachable (e.g. to conditionally exclude `:anvil` tests).
- `DiodeClient.Anvil.Helper.ensure_test_env(opts \\ [])` – one-shot setup: optional wallet (`:wallet`), optional deploy of diode_contract (`:deploy_contracts`), optional `:rpc_url`. Returns `:ok` or `{:error, reason}` (e.g. `:anvil_not_reachable`).
**Environment variables**
| Variable | Default | Description |
| -------- | ------- | ----------- |
| `ANVIL_RPC_URL` | `http://127.0.0.1:8545` | Anvil JSON-RPC URL. |
| `ANVIL_CHAIN_ID` | `31337` | Anvil chain ID. |
| `ANVIL_CONTRACT_REPO_PATH` | (none) | Path to a clone of [diode_contract](https://github.com/diodechain/diode_contract); if unset, deployment clones to a temp dir. |
## Encryption and Authentication
For encryption standard TLS as builtin into Erlang from OpenSSL is used. For authentication though the Ethereum signature scheme using the elliptic curve `secp256k1` is used. The generated public addresses of the form `0x389eba94b330140579cdce1feb1a6e905ff876e6` actually represent hashes of public keys. When opening a port using `DiodeClient.port_open("0x389eba94b330140579cdce1feb1a6e905ff876e6", 5000)` this first locates the correct peer and then uses cryptographic handshakes to ensure the peer is in fact in possession of the corresponding private key.
To this regard the `DiodeClient` will by default store private keys in local files. In the example above `example_client_interface` and `example_server_interface`. These represent both the address as well as the private key needed to authenticate as such.
## Todos
* Add actual support for multiple interfaces in a single session
* Add standard contract call interfaces e.g. for BNS to be able to resolve human readable names such as `somename.diode`
## Installation
The package can be installed by adding `diode_client` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:diode_client, "~> 1.1"}
]
end
```