Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
1f5bd03
Now install WASM file at compile time
Vinocis Jan 3, 2025
4fdb759
Fix doc typo
Vinocis Jan 3, 2025
fbbcd5d
Remove debug code
Vinocis Jan 3, 2025
5c7307a
Fix CI
Vinocis Jan 6, 2025
b23b204
Improve error handling
Vinocis Jan 16, 2025
802bffc
Now contribuitors can choose to install the wasm file or not
Vinocis Jan 16, 2025
0ed3bf5
Install flag is now an argument
Vinocis Jan 16, 2025
5426a4d
Validate wasm bianry with checksums
Vinocis Jan 21, 2025
98725e3
Fix typo
Vinocis Jan 21, 2025
49996cd
Now ignores the WASM binary
Vinocis Jan 23, 2025
e67a33c
Add docs
Vinocis Jan 23, 2025
e67e243
Fix tests
Vinocis Jan 27, 2025
ab5cf91
Refactor
Vinocis Jan 27, 2025
ad689bd
Create macro tests
Vinocis Jan 27, 2025
93b3bbe
Fix Logger message
Vinocis Jan 27, 2025
817182c
Ensure priv path exists
Vinocis Jan 27, 2025
35b2bdc
Debug CI
Vinocis Jan 27, 2025
a95371d
Raise CompileError if can't download wasm bianry
Vinocis Jan 27, 2025
8cb582f
Remove debug function
Vinocis Jan 27, 2025
f56af79
Add more debug code
Vinocis Jan 27, 2025
7d56aae
Remove debug code
Vinocis Jan 27, 2025
2041a12
Fix
Vinocis Jan 27, 2025
e4e85e9
Refactor
Vinocis Jan 28, 2025
71bae54
Fix typo
Vinocis Jan 28, 2025
1a426d8
Chore
Vinocis Jan 28, 2025
e3bd86f
Use matrix to test on macOS and Windows
Vinocis Jan 28, 2025
4f9429b
Remove macOS from matrix
Vinocis Jan 29, 2025
bf0a555
Apply review suggestions
Vinocis Jan 29, 2025
8a95192
Fix CI
Vinocis Jan 29, 2025
97d8448
Change Erlang version to 27.0
Vinocis Jan 29, 2025
06a0c1c
Revert last commit
Vinocis Jan 29, 2025
2b29cf9
Adds patch version on Erlang
Vinocis Jan 29, 2025
02ccab6
Revert last commit
Vinocis Jan 29, 2025
d0f1ca2
Does not get version from .tool-versions
Vinocis Jan 29, 2025
dc2c2da
Add gitattributes
Vinocis Jan 29, 2025
1c5a73d
Update gitattributes
Vinocis Jan 29, 2025
b353eac
Check format only on ubuntu
Vinocis Jan 29, 2025
015ea1b
Fix CI
Vinocis Jan 29, 2025
a79e920
Fix CI
Vinocis Jan 29, 2025
cc870b9
Add MIX_ENV on job 'tests'
Vinocis Jan 29, 2025
489e0fc
Fix CI
Vinocis Jan 29, 2025
b448f06
Change cache key
Vinocis Jan 29, 2025
f2d8345
Apply review suggestions
Vinocis Jan 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Handle line endings automatically for files detected as text
# and leave all files detected as binary untouched.
* text=auto

# Elixir files
*.ex diff=elixir
*.exs diff=elixir
59 changes: 52 additions & 7 deletions .github/workflows/elixir.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,21 @@ on:
permissions:
contents: read

env:
MIX_ENV: test

jobs:
test:
name: Test app
lint:
name: Lint
runs-on: ubuntu-latest
env:
MIX_ENV: test

steps:
- uses: actions/checkout@v4

- uses: erlef/setup-beam@v1
id: beam
with:
version-file: .tool-versions
otp-version: "27.2"
elixir-version: "1.18.0"
version-type: strict

- name: Restore the deps and _build cache
Expand All @@ -36,7 +37,7 @@ jobs:
path: |
deps
_build
key: ${{ runner.os }}-${{ env.OTP_VERSION }}-${{ env.ELIXIR_VERSION }}-${{ env.MIX_ENV }}-mixlockhash-${{ env.MIX_LOCK_HASH }}
key: lint-${{ env.OTP_VERSION }}-${{ env.ELIXIR_VERSION }}-${{ env.MIX_ENV }}-mixlockhash-${{ env.MIX_LOCK_HASH }}

- name: Install mix dependencies
if: steps.cache.outputs.cache-hit != 'true'
Expand All @@ -56,5 +57,49 @@ jobs:
- name: Run dialyzer
run: mix dialyzer --format github --format dialyxir

- name: Check formatted
run: mix format --check-formatted

test:
needs: lint
strategy:
matrix:
os: [ubuntu-latest, windows-2022]
name: Test
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

- uses: erlef/setup-beam@v1
id: beam
with:
otp-version: "27.2"
elixir-version: "1.18.0"
version-type: strict

- name: Restore the deps and _build cache
uses: actions/cache@v4
id: cache
env:
OTP_VERSION: ${{ steps.beam.outputs.otp-version }}
ELIXIR_VERSION: ${{ steps.beam.outputs.elixir-version }}
MIX_LOCK_HASH: ${{ hashFiles('**/mix.lock') }}
with:
path: |
deps
_build
key: check_and_test-${{ runner.os }}-${{ env.OTP_VERSION }}-${{ env.ELIXIR_VERSION }}-${{ env.MIX_ENV }}-mixlockhash-${{ env.MIX_LOCK_HASH }}

- name: Install mix dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: mix deps.get

- name: Compile dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: mix deps.compile

- name: Compile
run: mix compile --warnings-as-errors --force

- name: Check and Test
run: mix ci
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ numscriptex-*.tar

# Temporary files, for example, from tests.
/tmp/

# Numscript-WASM binary file
/priv/numscript.wasm
36 changes: 26 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# NumscripEx
# NumscriptEx
NumscriptEx is a library that allows its users to check and run Numscripts in Elixir. And if you don't know what are
Numscripts, here a quick explanation:

[Numscript](https://docs.formance.com/numscript/) is a DSL made by [Formance](https://www.formance.com/)
that simplifies complex financial transactions with scripts that are easy to read,
so yout don't need a big, complex and error-prone codebase to deal with your finances.

You can see and execute some examples on the [Numscript Playground](https://playground.numscript.org/?template=simple-send).
so you don't need a big, complex and error-prone codebase to deal with your finances.

`NumscriptEx` is a library that allows its users to check and run said numscripts in Elixir.
You can see and execute some examples at the [Numscript Playground](https://playground.numscript.org/?template=simple-send).

## Installation
You will just need to add `:numscriptex` as a dependency on your `mix.exs`, and run the `mix deps.get` command:
Expand All @@ -17,6 +18,21 @@ def deps do
]
end
```
### Configuration
NumscriptEx needs some external assets ([Numscript-WASM](https://github.com/PagoPlus/numscript-wasm)),
and you can configure said assets if you want.

Available configurations:
- `:version`: is the release version to be downloaded;
- `:retries`: number of download retries in case of failure.

Ex:
```elixir
config :numscriptex,
version: "0.0.2",
retries: 3
```
These above are the default values.

## Usage
This library basically has two core functions: `Numscriptex.check/1` and `Numscriptex.run/2`.
Expand All @@ -30,7 +46,7 @@ But before introducing these two functions, you will need to know what is the `N
A numscript needs some other data aside the script itself to run correctly, and
`Numscriptex.Run` solves this problem.

If you want to know what exactly these additional data are, you can see the
If you want to know what exactly these additional data are, you can see the
[numscript playground](https://playground.numscript.org/?template=simple-send) for examples.

The abstraction is made by creating a struct:
Expand Down Expand Up @@ -124,7 +140,7 @@ iex> {:ok, %{
...> }
...> }
```
`:script` is the only key that will always return if your script is valid, the
`:script` is the only key that will always return if your script is valid, the
other three are optional.
### Run
To use `run/2` your first argument must be your script (the same you used in `check/1`), and the second must be the `%Numscriptex.Run{}` struct. Ex:
Expand All @@ -134,7 +150,7 @@ iex> Numscriptex.run(script, struct)
{:ok, result}
```

Where result will be something like this:
Where result will be something like this:
```elixir
iex> %{
...> postings: [
Expand Down Expand Up @@ -165,8 +181,8 @@ iex> %{
...> initial_balance: 0
...> }
...> ],
...> accountMeta: %{}
...> txMeta: %{}
...> accountMeta: %{}
...> txMeta: %{}
...> }
```

Expand Down
82 changes: 52 additions & 30 deletions lib/numscriptex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ defmodule Numscriptex do
Already checked the script and want to execute him? Use the `run/2` function.
"""

alias Numscriptex.AssetsManager
alias Numscriptex.Balances
alias Numscriptex.CheckLog
alias Numscriptex.Utilities

require AssetsManager
require Logger

@type check_log() :: CheckLog.t()

@type check_result() :: %{
Expand Down Expand Up @@ -46,10 +50,7 @@ defmodule Numscriptex do
optional(:details) => any()
}

@binary :numscriptex
|> :code.priv_dir()
|> Path.join("numscript.wasm")
|> File.read!()
AssetsManager.ensure_wasm_binary_is_valid()

@doc """
To use `check/1` you just need to pass your numscript as its argument.
Expand All @@ -65,13 +66,13 @@ defmodule Numscriptex do
"""
@spec check(binary()) :: {:ok, check_result()} | {:error, errors()}
def check(input) do
case process(input, :check) do
{:ok, details} ->
{:ok, %{script: input, details: normalize_check_logs(details)}}

case execute_command(input, :check) do
:ok ->
{:ok, %{script: input}}

{:ok, details} ->
{:ok, %{script: input, details: normalize_check_logs(details)}}

{:error, %{reason: errors}} ->
{:error, %{reason: normalize_check_logs(errors)}}
end
Expand All @@ -81,12 +82,12 @@ defmodule Numscriptex do
To use `run/2` your first argument must be your script, and the second must
be a `%Numscriptex.Run{}` (go to Numscriptex.Run module to see more) struct.
Ex:

```elixir
iex> script = "send [USD/2 100] ( source = @foo destination = @bar)"
...> balances = %{"foo" => %{"USD/2" => 500, "EUR/2" => 300}}
...>
...> struct =
...>
...> struct =
...> Numscriptex.Run.new()
...> |> Numscriptex.Run.put!(:balances, balances)
...> |> Numscriptex.Run.put!(:metadata, %{})
Expand All @@ -103,7 +104,7 @@ defmodule Numscriptex do
|> Map.from_struct()
|> Map.merge(%{script: numscript})
|> JSON.encode!()
|> process(:run)
|> execute_command(:run)
|> maybe_put_final_balance(initial_balance)
|> standardize_run_result()
end
Expand Down Expand Up @@ -135,7 +136,7 @@ defmodule Numscriptex do
defp maybe_put_final_balance({:error, _reason} = error, _initial_balance),
do: error

defp process(input, operation) do
defp execute_command(input, operation) do
{:ok, stdout_pipe} = Wasmex.Pipe.new()
{:ok, stdin_pipe} = Wasmex.Pipe.new()
{:ok, stderr_pipe} = Wasmex.Pipe.new()
Expand All @@ -150,40 +151,61 @@ defmodule Numscriptex do
stderr: stderr_pipe
}

{:ok, pid} = Wasmex.start_link(%{bytes: @binary, wasi: wasi})
binary_path = AssetsManager.binary_path()

case Wasmex.call_function(pid, :_start, []) do
{:ok, []} ->
with {:ok, binary} <- File.read(binary_path),
{:ok, pid} <- Wasmex.start_link(%{bytes: binary, wasi: wasi}),
{{:ok, _}, _pid} <- {Wasmex.call_function(pid, :_start, []), pid} do
GenServer.stop(pid, :normal)
process(pid, stdout_pipe, stderr_pipe)
else
{{:error, _reason}, pid} ->
GenServer.stop(pid)
process(pid, stdout_pipe, stderr_pipe)

Wasmex.Pipe.seek(stderr_pipe, 0)
error = Wasmex.Pipe.read(stderr_pipe)
{:error, reason} when is_atom(reason) ->
{:error, %{reason: handle_posix_errors(reason)}}

Wasmex.Pipe.seek(stdout_pipe, 0)
{:error, reason} ->
{:error, %{reason: reason}}
end
end

stdout_pipe
|> Wasmex.Pipe.read()
|> JSON.decode()
defp process(pid, stdout_pipe, stderr_pipe) when is_pid(pid) do
Wasmex.Pipe.seek(stdout_pipe, 0)
stdout = Wasmex.Pipe.read(stdout_pipe)

Wasmex.Pipe.seek(stderr_pipe, 0)
error = Wasmex.Pipe.read(stderr_pipe)

case JSON.decode(stdout) do
{:ok, _content} = result ->
result
|> handle_process()
|> maybe_put_stderr(error)
|> handle_errors()

{:error, _reason} ->
GenServer.stop(pid)

Wasmex.Pipe.seek(stderr_pipe, 0)
error = Wasmex.Pipe.read(stderr_pipe)

Wasmex.Pipe.seek(stdout_pipe, 0)
stdout = Wasmex.Pipe.read(stdout_pipe)

{:error, stdout}
|> handle_process()
|> maybe_put_stderr(error)
|> handle_errors()
end
end

defp handle_posix_errors(reason) do
file_read_error =
reason
|> :file.format_error()
|> to_string()

if file_read_error =~ "unknown POSIX error" do
reason
else
"Can't read the WASM binary due to: #{file_read_error}"
end
end

defp maybe_put_stderr({:error, reason}, stderr) when is_map(reason) do
is_stderr_empty? =
stderr
Expand Down
Loading