├── .credo.exs ├── .formatter.exs ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── aeternity.yaml ├── aevm_contract.sophia ├── config └── config.exs ├── examples ├── listener.md └── usage.md ├── fate_contract.sophia ├── lib ├── core │ ├── account.ex │ ├── chain.ex │ ├── channel │ │ ├── offchain.ex │ │ └── onchain.ex │ ├── client.ex │ ├── contract.ex │ ├── generalized_account.ex │ ├── listener │ │ ├── listener.ex │ │ ├── peer_connection.ex │ │ ├── peer_connection_supervisor.ex │ │ ├── peers.ex │ │ └── supervisor.ex │ ├── middleware.ex │ ├── name.ex │ └── oracle.ex └── utils │ ├── account.ex │ ├── chain.ex │ ├── encoding.ex │ ├── governance.ex │ ├── hash.ex │ ├── keys.ex │ ├── serialization.ex │ ├── serialization_utils.ex │ └── transaction.ex ├── mix.exs ├── mix.lock └── test ├── core_chain_test.exs ├── core_channel_test.exs ├── core_contract_test.exs ├── core_generalized_account_test.exs ├── core_listener_test.exs ├── core_naming_test.exs ├── core_oracle_test.exs ├── serialization_test.exs ├── test_helper.exs ├── test_utils.ex ├── transaction_util_test.exs └── utils_keys_test.exs /.credo.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Credo and you are probably reading 2 | # this after creating it with `mix credo.gen.config`. 3 | # 4 | # If you find anything wrong or unclear in this file, please report an 5 | # issue on GitHub: https://github.com/rrrene/credo/issues 6 | # 7 | %{ 8 | # 9 | # You can have as many configs as you like in the `configs:` field. 10 | configs: [ 11 | %{ 12 | # 13 | # Run any exec using `mix credo -C `. If no exec name is given 14 | # "default" is used. 15 | # 16 | name: "default", 17 | # 18 | # These are the files included in the analysis: 19 | files: %{ 20 | # 21 | # You can give explicit globs or simply directories. 22 | # In the latter case `**/*.{ex,exs}` will be used. 23 | # 24 | included: ["lib/", "src/", "test/", "web/", "apps/"], 25 | excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/", ~r"/lib/aeternity_node/", ~r"/lib/middleware/"] 26 | }, 27 | # 28 | # Load and configure plugins here: 29 | # 30 | plugins: [], 31 | # 32 | # If you create your own checks, you must specify the source files for 33 | # them here, so they can be loaded by Credo before running the analysis. 34 | # 35 | requires: [], 36 | # 37 | # If you want to enforce a style guide and need a more traditional linting 38 | # experience, you can change `strict` to `true` below: 39 | # 40 | strict: true, 41 | # 42 | # If you want to use uncolored output by default, you can change `color` 43 | # to `false` below: 44 | # 45 | color: true, 46 | # 47 | # You can customize the parameters of any check by adding a second element 48 | # to the tuple. 49 | # 50 | # To disable a check put `false` as second element: 51 | # 52 | # {Credo.Check.Design.DuplicatedCode, false} 53 | # 54 | checks: [ 55 | # 56 | ## Consistency Checks 57 | # 58 | {Credo.Check.Consistency.ExceptionNames, []}, 59 | {Credo.Check.Consistency.LineEndings, []}, 60 | {Credo.Check.Consistency.ParameterPatternMatching, []}, 61 | {Credo.Check.Consistency.SpaceAroundOperators, []}, 62 | {Credo.Check.Consistency.SpaceInParentheses, []}, 63 | {Credo.Check.Consistency.TabsOrSpaces, []}, 64 | 65 | # 66 | ## Design Checks 67 | # 68 | # You can customize the priority of any check 69 | # Priority values are: `low, normal, high, higher` 70 | # 71 | {Credo.Check.Design.AliasUsage, 72 | [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, 73 | # You can also customize the exit_status of each check. 74 | # If you don't want TODO comments to cause `mix credo` to fail, just 75 | # set this value to 0 (zero). 76 | # 77 | {Credo.Check.Design.TagTODO, [exit_status: 2]}, 78 | {Credo.Check.Design.TagFIXME, []}, 79 | 80 | # 81 | ## Readability Checks 82 | # 83 | {Credo.Check.Readability.AliasOrder, []}, 84 | {Credo.Check.Readability.FunctionNames, []}, 85 | {Credo.Check.Readability.LargeNumbers, []}, 86 | {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 130]}, 87 | {Credo.Check.Readability.ModuleAttributeNames, []}, 88 | {Credo.Check.Readability.ModuleDoc, false}, 89 | {Credo.Check.Readability.ModuleNames, []}, 90 | {Credo.Check.Readability.ParenthesesInCondition, []}, 91 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, 92 | {Credo.Check.Readability.PredicateFunctionNames, []}, 93 | {Credo.Check.Readability.PreferImplicitTry, false}, 94 | {Credo.Check.Readability.RedundantBlankLines, []}, 95 | {Credo.Check.Readability.Semicolons, []}, 96 | {Credo.Check.Readability.SpaceAfterCommas, []}, 97 | {Credo.Check.Readability.StringSigils, []}, 98 | {Credo.Check.Readability.TrailingBlankLine, []}, 99 | {Credo.Check.Readability.TrailingWhiteSpace, []}, 100 | # TODO: enable by default in Credo 1.1 101 | {Credo.Check.Readability.UnnecessaryAliasExpansion, false}, 102 | {Credo.Check.Readability.VariableNames, []}, 103 | 104 | # 105 | ## Refactoring Opportunities 106 | # 107 | {Credo.Check.Refactor.CondStatements, []}, 108 | {Credo.Check.Refactor.CyclomaticComplexity, false}, 109 | {Credo.Check.Refactor.FunctionArity, false}, 110 | {Credo.Check.Refactor.MapInto, false}, 111 | {Credo.Check.Refactor.LongQuoteBlocks, []}, 112 | {Credo.Check.Refactor.MatchInCondition, []}, 113 | {Credo.Check.Refactor.NegatedConditionsInUnless, []}, 114 | {Credo.Check.Refactor.NegatedConditionsWithElse, []}, 115 | {Credo.Check.Refactor.Nesting, false}, 116 | {Credo.Check.Refactor.UnlessWithElse, []}, 117 | {Credo.Check.Refactor.WithClauses, []}, 118 | 119 | # 120 | ## Warnings 121 | # 122 | {Credo.Check.Warning.BoolOperationOnSameValues, []}, 123 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, 124 | {Credo.Check.Warning.IExPry, []}, 125 | {Credo.Check.Warning.IoInspect, []}, 126 | {Credo.Check.Warning.LazyLogging, false}, 127 | {Credo.Check.Warning.OperationOnSameValues, []}, 128 | {Credo.Check.Warning.OperationWithConstantResult, []}, 129 | {Credo.Check.Warning.RaiseInsideRescue, []}, 130 | {Credo.Check.Warning.UnusedEnumOperation, []}, 131 | {Credo.Check.Warning.UnusedFileOperation, []}, 132 | {Credo.Check.Warning.UnusedKeywordOperation, []}, 133 | {Credo.Check.Warning.UnusedListOperation, []}, 134 | {Credo.Check.Warning.UnusedPathOperation, []}, 135 | {Credo.Check.Warning.UnusedRegexOperation, []}, 136 | {Credo.Check.Warning.UnusedStringOperation, []}, 137 | {Credo.Check.Warning.UnusedTupleOperation, []}, 138 | 139 | # 140 | # Controversial and experimental checks (opt-in, just replace `false` with `[]`) 141 | # 142 | {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, 143 | {Credo.Check.Consistency.UnusedVariableNames, false}, 144 | {Credo.Check.Design.DuplicatedCode, false}, 145 | {Credo.Check.Readability.AliasAs, false}, 146 | {Credo.Check.Readability.MultiAlias, false}, 147 | {Credo.Check.Readability.Specs, false}, 148 | {Credo.Check.Readability.SinglePipe, false}, 149 | {Credo.Check.Refactor.ABCSize, false}, 150 | {Credo.Check.Refactor.AppendSingleItem, priority: :high}, 151 | {Credo.Check.Refactor.DoubleBooleanNegation, false}, 152 | {Credo.Check.Refactor.ModuleDependencies, false}, 153 | {Credo.Check.Refactor.PipeChainStart, false}, 154 | {Credo.Check.Refactor.VariableRebinding, exit_status: 0}, 155 | {Credo.Check.Warning.MapGetUnsafePass, false}, 156 | {Credo.Check.Warning.UnsafeToAtom, false}, 157 | 158 | ] 159 | } 160 | ] 161 | } 162 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Directories, where we keep autogenerated code. 11 | /lib/aeternity_node/ 12 | /lib/middleware/ 13 | 14 | # Where third-party dependencies like ExDoc output generated docs. 15 | /doc/ 16 | 17 | # Ignore .fetch files in case you like to edit your project deps locally. 18 | /.fetch 19 | 20 | # If the VM crashes, it generates a dump, let's ignore it too. 21 | erl_crash.dump 22 | 23 | # Also ignore archive artifacts (built via "mix archive.build"). 24 | *.ez 25 | 26 | /.elixir_ls/ 27 | 28 | # Igonore Intellij 29 | .idea/ 30 | *.iml 31 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | sudo: false 3 | 4 | language: elixir 5 | elixir: '1.9' 6 | 7 | env: 8 | global: 9 | - AETERNITY_VERSION=5.4.1 10 | - GENERATOR_VERSION=1.2.1 11 | - MIDDLEWARE_VERSION=0.13.0 12 | - LD_LIBRARY_PATH=$HOME/.libsodium/lib:$LD_LIBRARY_PATH 13 | - LD_RUN_PATH=$HOME/.libsodium/lib:$LD_RUN_PATH 14 | - PATH=$PATH:$HOME/.libsodium/lib:$HOME/.libsodium/include:$HOME/.kiex 15 | - LIBRARY_PATH=$HOME/.libsodium/lib:$LIBRARY_PATH 16 | - C_INCLUDE_PATH=$HOME/.libsodium/include:$C_INCLUDE_PATH 17 | - AETERNITY_TOP=./ 18 | matrix: 19 | - LIBSODIUM_VER=1.0.16 20 | 21 | before_install: 22 | - sudo apt install wget 23 | - sudo apt-get install jq 24 | - wget https://github.com/aeternity/aeternity/releases/download/v${AETERNITY_VERSION}/aeternity-${AETERNITY_VERSION}-ubuntu-x86_64.tar.gz 25 | - tar -xvf aeternity-${AETERNITY_VERSION}-ubuntu-x86_64.tar.gz 26 | - git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.7.4 27 | - echo -e '\n. $HOME/.asdf/asdf.sh' >> ~/.bashrc 28 | - echo -e '\n. $HOME/.asdf/completions/asdf.bash' >> ~/.bashrc 29 | - source ~/.bashrc 30 | - asdf plugin-add java 31 | - asdf install java amazon-corretto-8.212.04.2 32 | - asdf plugin-add maven 33 | - asdf install maven 3.3.3 34 | - asdf global maven 3.3.3 35 | - asdf global java amazon-corretto-8.212.04.2 36 | - mix local.rebar --force 37 | - mix local.hex --force 38 | - mix build_api v${GENERATOR_VERSION}-elixir v${AETERNITY_VERSION} v${MIDDLEWARE_VERSION} 39 | - mix deps.get 40 | - mix clean 41 | 42 | 43 | install: 44 | # Install libsodium 45 | - mkdir -p libsodium-src 46 | - "[ -d $HOME/.libsodium/lib ] || (wget -O libsodium-src.tar.gz https://github.com/jedisct1/libsodium/releases/download/$LIBSODIUM_VER/libsodium-$LIBSODIUM_VER.tar.gz && tar -zxf libsodium-src.tar.gz -C libsodium-src --strip-components=1)" 47 | - cd libsodium-src 48 | - "[ -d $HOME/.libsodium/lib ] || (./configure --prefix=$HOME/.libsodium && make -j$(nproc) && make install && export LIBSODIUM_NEW=yes)" 49 | - cd .. 50 | 51 | - sudo apt-get install libssl1.0.0 52 | # Recompile enacl if necessary 53 | - "[ -z $LIBSODIUM_NEW ] || (mix deps.compile enacl)" 54 | 55 | script: 56 | - ./bin/aeternity start 57 | - mix format --check-formatted 58 | - mix compile --warnings-as-errors 59 | - mix compile.xref --warnings-as-errors 60 | - mix credo --strict 61 | - mix coveralls -u --exclude disabled -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | ### Added 9 | ### Changed 10 | ### Removed 11 | 12 | ## [0.1.0] - 2019-06-11 13 | ### Added 14 | - Client implementation 15 | - Account implementation 16 | - Chain implementation 17 | - Oracles implementation 18 | - Contract implementation 19 | - Naming system implementation -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM elixir:1.9-alpine 2 | 3 | RUN apk add --no-cache libsodium-dev git g++ make 4 | 5 | COPY config ./config 6 | COPY lib ./lib 7 | COPY mix.exs . 8 | COPY mix.lock . 9 | 10 | RUN mix local.hex --force && \ 11 | mix local.rebar --force && \ 12 | mix deps.get && mix compile 13 | 14 | RUN apk del g++ make 15 | 16 | CMD ["iex", "-S", "mix"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License (ISC) 2 | Copyright © 2018 aeternity developers 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any purpose 5 | with or without fee is hereby granted, provided that the above copyright notice 6 | and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 10 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 12 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 13 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 14 | THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aepp SDK Elixir 2 | 3 | Elixir SDK targeting the [æternity node](https://github.com/aeternity/aeternity) implementation. 4 | 5 | ## Installation 6 | To start using this project, simply use source code or compiled binaries, provided in our [releases](https://github.com/aeternity/aepp-sdk-elixir/releases). 7 | ## Installation as a dependency 8 | An installation as library, basic usage guide can be found [here](https://github.com/aeternity/aepp-sdk-elixir/tree/master/examples/usage.md). 9 | ## Using code from master/other branches 10 | In order to use code from master branch: 11 | 1. First, you will need to install **Java** `v11` or higher and **Maven** `v3.3.3` or higher: 12 | ``` 13 | sudo apt-install default-jdk 14 | sudo apt-install maven 15 | ``` 16 | 2. Then, you will have to [set-up the project](https://github.com/aeternity/aepp-sdk-elixir#setup-the-project). 17 | 18 | These dependencies are used by our [OpenAPI Code Generator](https://github.com/aeternity/openapi-generator), which builds low-level API calls, needed for [Aeternity Node](https://github.com/aeternity/aeternity) and [Aeternity Middleware](https://github.com/aeternity/aepp-middleware). 19 | 20 | ## Prerequisites 21 | **Using released versions:** 22 | Ensure that you have [Elixir](https://elixir-lang.org/install.html) and [wget](https://www.gnu.org/software/wget/) installed. 23 | 24 | **Using code from master/other branches**, as mentioned before, additionally, you will have to install [Java](https://java.com/en/download/help/download_options.xml) and [Maven](https://maven.apache.org/install.html). 25 | 26 | ## Setup the project 27 | ``` 28 | git clone https://github.com/aeternity/aepp-sdk-elixir 29 | cd aepp-sdk-elixir 30 | mix build_api v1.2.1-elixir v5.4.1 v0.13.0 31 | ``` 32 | Where: 33 | - `v1.2.1-elixir` - OpenAPI client [generator](https://github.com/aeternity/openapi-generator/tree/elixir-adjustment#openapi-generator) [release](https://github.com/aeternity/openapi-generator/releases) version. 34 | - `v5.4.1` - Aeternity node API [specification file](https://github.com/aeternity/aeternity/blob/v5.4.1/apps/aehttp/priv/swagger.yaml). 35 | - `v0.13.0` - Aeternity middleware API [specification file](https://github.com/aeternity/aepp-middleware/blob/v0.13.0/swagger/swagger.json). 36 | 37 | ## Implemented functionality 38 | - [**Client module**](http://aeternity.com/aepp-sdk-elixir/AeppSDK.Client.html#content) 39 | 40 | Consists of definition of a client structure and other helper functions. Client structure helps us collect and manage all data needed to perform various requests to the HTTP endpoints. 41 | 42 | - [**Account module**](http://aeternity.com/aepp-sdk-elixir/AeppSDK.Account.html#content) 43 | 44 | Contains various functions to interact with aeternity account, e.g. getting an account, spending and etc. 45 | 46 | - [**Chain module**](http://aeternity.com/aepp-sdk-elixir/AeppSDK.Chain.html#content) 47 | 48 | In chain module we implemented chain-related activities, like getting current blocks, generations, transactions and others. 49 | 50 | - [**Oracle module**](http://aeternity.com/aepp-sdk-elixir/AeppSDK.Oracle.html#content) 51 | 52 | This module covers oracle-related activities such as: registering a new oracle, querying an oracle, responding from an oracle, extending an oracle, retrieving oracle and queries functionality. 53 | 54 | - [**Contract module**](http://aeternity.com/aepp-sdk-elixir/AeppSDK.Contract.html#content) 55 | 56 | Module implements functions needed to: deploy, call, compile Aeternity's Sophia smart contracts. 57 | 58 | - [**Naming system module**](http://aeternity.com/aepp-sdk-elixir/AeppSDK.AENS.html#content) 59 | 60 | Naming system module has many functionalities, needed to manipulate Aeternity naming system. It allows developers to: pre-claim, claim, update, transfer and revoke names. 61 | 62 | - [**Channel module**](http://aeternity.com/aepp-sdk-elixir/AeppSDK.Channel.OnChain.html#content) 63 | 64 | Module, containing implemented and tested all channel on-chain transactions and activities. They are: getting channel info by id, opening a channel, depositing to a channel, withdrawing from a channel, closing solo and closing mutually a channel and many others. 65 | 66 | - [**Noise listener module**](http://aeternity.com/aepp-sdk-elixir/AeppSDK.Listener.html#content) 67 | 68 | Module, which works with enoise protocol, which is used by Aeternity. Implemented connection between peers and listening for new blocks, transactions and other stuff. 69 | 70 | - [**Middleware high-level module**](http://aeternity.com/aepp-sdk-elixir/AeppSDK.Middleware.html#content) 71 | 72 | Simple high-module which performs various requests to exposed endpoints in [Aeternity Middleware](https://github.com/aeternity/aepp-middleware) project. 73 | 74 | **Full documentation can be found [here](http://aeternity.com/aepp-sdk-elixir/api-reference.html).** 75 | 76 | ## Use Docker image 77 | In order to play around with the **Elixir SDK**, you can use docker image: 78 | 79 | ``` 80 | docker pull aeternity/aepp-sdk-elixir:latest 81 | docker run -it aeternity/aepp-sdk-elixir:latest 82 | ``` 83 | Example to defined a client can be found [here](https://github.com/aeternity/aepp-sdk-elixir/blob/master/examples/usage.md#define-a-client). -------------------------------------------------------------------------------- /aeternity.yaml: -------------------------------------------------------------------------------- 1 | peers: 2 | - "aenode://pp_2i8N6XsjCGe1wkdMhDRs7t7xzijrjJDN4xA22RoNGCgt6ay9QB@31.13.249.70:3011" # wrong port 3 | fork_management: 4 | network_id: "my_test" 5 | chain: 6 | persist: false 7 | hard_forks: 8 | "1": 0 9 | "2": 1 10 | "3": 2 11 | "4": 3 12 | http: 13 | external: 14 | port: 3013 15 | internal: 16 | port: 3113 17 | listen_address: 0.0.0.0 18 | debug_endpoints: true 19 | mining: 20 | autostart: true 21 | expected_mine_rate: 100 22 | beneficiary_reward_delay: 2 23 | min_miner_gas_price: 1000000 24 | # Public key of beneficiary account that will receive fees from mining on a node. 25 | beneficiary: "ak_6A2vcm1Sz6aqJezkLCssUXcyZTX7X8D5UwbuS2fRJr9KkYpRU" 26 | cuckoo: 27 | miner: 28 | # Use the fast executable binary of the miner. 29 | executable: mean15-generic 30 | # Extra arguments to pass to the miner executable binary. 31 | extra_args: "-t 1" 32 | # Use the smaller graph for faster mining 33 | edge_bits: 15 34 | -------------------------------------------------------------------------------- /aevm_contract.sophia: -------------------------------------------------------------------------------- 1 | contract Identity = 2 | datatype event = 3 | SomeEvent(bool, bits, bytes(8)) 4 | | AnotherEvent(address, oracle(int, int), oracle_query(int, int)) 5 | 6 | stateful entrypoint emit_event() = 7 | Chain.event(SomeEvent(true, Bits.all, #123456789abcdef)) 8 | Chain.event(AnotherEvent(ak_2bKhoFWgQ9os4x8CaeDTHZRGzUcSwcXYUrM12gZHKTdyreGRgG, 9 | ok_2YNyxd6TRJPNrTcEDCe9ra59SVUdp9FR9qWC5msKZWYD9bP9z5, 10 | oq_2oRvyowJuJnEkxy58Ckkw77XfWJrmRgmGaLzhdqb67SKEL1gPY)) -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # By default, the umbrella project as well as each child 6 | # application will require this configuration file, as 7 | # configuration and dependencies are shared in an umbrella 8 | # project. While one could configure all applications here, 9 | # we prefer to keep the configuration of each individual 10 | # child application in their own app, but all other 11 | # dependencies, regardless if they belong to one or multiple 12 | # apps, should be configured in the umbrella to avoid confusion. 13 | import_config "../apps/*/config/config.exs" 14 | 15 | # Sample configuration (overrides the imported configuration above): 16 | # 17 | # config :logger, :console, 18 | # level: :info, 19 | # format: "$date $time [$level] $metadata$message\n", 20 | # metadata: [:user_id] 21 | -------------------------------------------------------------------------------- /examples/listener.md: -------------------------------------------------------------------------------- 1 | # Example `AeppSDK.Listener` usage 2 | 3 | The listener service notifies processes for the events that they've subscribed to. This is done by sending messages to them, meaning that they must be able to handle incoming messages. Here's an example implementation of a `GenServer` that handles a number of different events: 4 | 5 | ``` elixir 6 | defmodule EventHandler do 7 | use GenServer 8 | 9 | alias AeppSDK.Listener 10 | 11 | require Logger 12 | 13 | def start_link(args) do 14 | GenServer.start_link(__MODULE__, args, name: __MODULE__) 15 | end 16 | 17 | def init(_) do 18 | Listener.start("ae_uat") 19 | Listener.subscribe(:key_blocks, self()) 20 | Listener.subscribe(:micro_blocks, self()) 21 | 22 | Listener.subscribe( 23 | :spend_transactions, 24 | self(), 25 | "ak_Y2Hueaa44EMAgfDy9zWePFpVToikqDFq5M4pkKe2zuYsk6wau" 26 | ) 27 | 28 | {:ok, %{key_blocks: [], micro_blocks: [], spend_transactions: []}} 29 | end 30 | 31 | def handle_info({:key_blocks, key_block}, %{key_blocks: key_blocks} = state) do 32 | Logger.info(fn -> "Received new key block: #{inspect(key_block)}" end) 33 | {:noreply, %{state | key_blocks: [key_block | key_blocks]}} 34 | end 35 | 36 | def handle_info( 37 | {:spend_transactions, spend_transaction}, 38 | %{spend_transactions: spend_transactions} = state 39 | ) do 40 | Logger.info(fn -> "Received new spend transaction: #{inspect(spend_transaction)}" end) 41 | {:noreply, %{state | spend_transactions: [spend_transaction | spend_transactions]}} 42 | end 43 | 44 | def handle_info({:micro_blocks, micro_block}, %{micro_blocks: micro_blocks} = state) do 45 | Logger.info(fn -> "Received new micro block: #{inspect(micro_block)}" end) 46 | {:noreply, %{state | micro_blocks: [micro_block | micro_blocks]}} 47 | end 48 | end 49 | ``` 50 | -------------------------------------------------------------------------------- /examples/usage.md: -------------------------------------------------------------------------------- 1 | # Aepp SDK Elixir example usage 2 | 3 | ## Installation 4 | First, add **Aepp SDK Elixir** to your `mix.exs` dependencies: 5 | ``` elixir 6 | defp deps do 7 | [ 8 | {:aepp_sdk_elixir, git: "https://github.com/aeternity/aepp-sdk-elixir.git", tag: "v0.5.4"} 9 | ] 10 | end 11 | ``` 12 | 13 | Then, update your dependencies: 14 | ``` elixir 15 | mix deps.get 16 | ``` 17 | 18 | Run your project: 19 | ``` elixir 20 | iex -S mix 21 | ``` 22 | 23 | ## Usage 24 | ### Generate key-pair 25 | In order to operate with **aeternity node**, user has to generate a key-pair. 26 | 27 | **Example:** 28 | ``` elixir 29 | %{public: _, secret: secret} = AeppSDK.Utils.Keys.generate_keypair() 30 | ``` 31 | 32 | ### Store the secret key 33 | Now you have to store your newly generated secret key(for security reasons). 34 | 35 | **Example:** 36 | ``` elixir 37 | password = "my_secret_password" 38 | AeppSDK.Utils.Keys.new_keystore(secret, password, name: "aeternity-keystore.json") 39 | ``` 40 | ### Read the keystore 41 | In order to retrieve the secret key from the keystore you have to read the keystore. 42 | 43 | **Example:** 44 | ``` elixir 45 | AeppSDK.Utils.Keys.read_keystore("aeternity-keystore.json", password) 46 | ``` 47 | 48 | ### Define a client: 49 | In order to use functions that require retrieving/sending data to a node, a client structure is needed. 50 | 51 | **Example:** 52 | ``` elixir 53 | public_key = "ak_jQGc3ECvnQYDZY3i97WSHPigL9tTaVEz1oLBW5J4F1JTKS1g7" 54 | secret_key = "24865931054474805885eec12497ee398bc39bc26917c190ed435e3cd1fa954e6046ef581eef749d492360b1542c7be997b5ddca0d2e510a4312b217998bfc74" 55 | network_id = "ae_uat" 56 | url = "https://sdk-testnet.aepps.com/v2" 57 | internal_url = "https://sdk-testnet.aepps.com/v2" 58 | client = AeppSDK.Client.new(%{public: public_key, secret: secret_key}, network_id, url, internal_url) 59 | ``` 60 | **NOTE:** If you are using one of these tags `v0.1.0` or `v0.2.0` you have to call the function like: 61 | ``` elixir 62 | Core.Client.new(%{public: public_key, secret: secret_key}, network_id, url, internal_url) 63 | ``` 64 | The naming conventions were changed. `Core` is `AeppSDK` now and `Utils` is `AeppSDK.Utils`. 65 | 66 | Every module and function is documented and you can get the documentation by using, for example: 67 | ``` elixir 68 | h AeppSDK.Client 69 | ``` 70 | 71 | ## Examples 72 | 73 | #### To get current generation: 74 | ``` elixir 75 | iex> AeppSDK.Chain.get_current_generation(client) 76 | {:ok, 77 | %{ 78 | key_block: %{ 79 | beneficiary: "ak_2iBPH7HUz3cSDVEUWiHg76MZJ6tZooVNBmmxcgVK6VV8KAE688", 80 | hash: "kh_2jnRkkeFpDMpLfJxZsVdpTtLomNj1sgjcQSoThXL4Zyirca6fT", 81 | height: 96894, 82 | info: "cb_AAAAAfy4hFE=", 83 | miner: "ak_2b1hyRMEjQmYT2GTLS1N9AVcGCqGsf6ng3LHvhnDLUHLbE6s4w", 84 | nonce: 11449002324963238722, 85 | pow: [3407814, 16834736, 19393828, 20269880, 28859692, 31569835, 41776618, 86 | 54459124, 56323237, 59364915, 66530222, 74201382, 84361285, 85176466, 87 | 88059514, 100722354, 106955257, 109076253, 140840049, 222311497, 88 | 226497503, 232310835, 240999898, 300530215, 313834856, 323852493, 89 | 325445647, 339271495, 355421106, 356456684, 369648267, 376071535, 90 | 379588007, 404046811, 415371506, 426162172, 428200431, 445577051, 91 | 450889898, 466828929, ...], 92 | prev_hash: "mh_Vru9EPdyvovkMFADq8ia1AaQ8BZRdTKAwUump4XyBBwy6Yybt", 93 | prev_key_hash: "kh_VtDLXG82cQYN3qhs3qAtnSnkLBGgZwoPGNsxxAecfT14LrbQa", 94 | state_hash: "bs_wjWjvoPBWGQoATYbsZYt184chU2CeUyHPtTcLU7vnAuCVgob1", 95 | target: 538630112, 96 | time: 1560853362522, 97 | version: 3 98 | }, 99 | micro_blocks: [] 100 | }} 101 | ``` 102 | 103 | #### To get an account's balance: 104 | ``` elixir 105 | iex> AeppSDK.Account.balance(client, client.keypair.public) 106 | {:ok, 811193097223266796526} 107 | ``` -------------------------------------------------------------------------------- /fate_contract.sophia: -------------------------------------------------------------------------------- 1 | contract Identity = 2 | datatype event = 3 | SomeEvent(string) 4 | | AnotherEvent(string) 5 | 6 | entrypoint init() = 7 | () 8 | 9 | stateful entrypoint emit_event() = 10 | Chain.event(SomeEvent("some event")) 11 | Chain.event(AnotherEvent("another event")) -------------------------------------------------------------------------------- /lib/core/account.ex: -------------------------------------------------------------------------------- 1 | defmodule AeppSDK.Account do 2 | @moduledoc """ 3 | High-level module for Account-related activities. 4 | 5 | In order for its functions to be used, a client must be defined first. 6 | Client example can be found at: `AeppSDK.Client.new/4` 7 | """ 8 | alias AeppSDK.{AENS, Client, Middleware} 9 | alias AeppSDK.Utils.Account, as: AccountUtils 10 | alias AeppSDK.Utils.{Encoding, Transaction} 11 | 12 | alias AeternityNode.Api.Account, as: AccountApi 13 | alias AeternityNode.Api.Chain, as: ChainApi 14 | alias AeternityNode.Model.{Account, Error, SpendTx} 15 | 16 | alias Tesla.Env 17 | 18 | @prefix_byte_size 2 19 | @allowed_recipient_tags ["ak", "ct", "ok", "nm"] 20 | 21 | @type account :: %{id: String.t(), balance: non_neg_integer(), nonce: non_neg_integer()} 22 | @type spend_options :: [ 23 | fee: non_neg_integer(), 24 | ttl: non_neg_integer(), 25 | payload: String.t() 26 | ] 27 | 28 | @doc """ 29 | Send tokens to an account. 30 | 31 | ## Example 32 | iex> public_key = "ak_nv5B93FPzRHrGNmMdTDfGdd5xGZvep3MVSpJqzcQmMp59bBCv" 33 | iex> AeppSDK.Account.spend(client, public_key, 10_000_000) 34 | {:ok, 35 | %{ 36 | block_hash: "mh_2hM7ZkifnstA9HEdpZRwKjZgNUSrkVmrB1jmCgG7Ly2b1vF7t", 37 | block_height: 74871, 38 | tx_hash: "th_FBqci65KYGsup7GettzvWVxP91podgngX9EJK2BDiduFf8FY4" 39 | }} 40 | """ 41 | @spec spend(Client.t(), String.t(), non_neg_integer(), spend_options()) :: 42 | {:ok, 43 | %{ 44 | block_hash: Encoding.base58c(), 45 | block_height: non_neg_integer(), 46 | tx_hash: Encoding.base58c() 47 | }} 48 | | {:error, String.t()} 49 | | {:error, Env.t()} 50 | def spend( 51 | %Client{ 52 | keypair: %{ 53 | public: <> = sender_id 54 | }, 55 | connection: connection, 56 | network_id: network_id, 57 | gas_price: gas_price 58 | } = client, 59 | recipient, 60 | amount, 61 | opts \\ [] 62 | ) 63 | when sender_prefix == "ak" do 64 | user_fee = Keyword.get(opts, :fee, Transaction.dummy_fee()) 65 | 66 | with {:ok, recipient_id} <- process_recipient(recipient, client), 67 | {:ok, nonce} <- AccountUtils.next_valid_nonce(client, sender_id), 68 | {:ok, %{height: height}} <- ChainApi.get_current_key_block_height(connection), 69 | %SpendTx{} = spend_tx <- 70 | struct( 71 | SpendTx, 72 | sender_id: sender_id, 73 | recipient_id: recipient_id, 74 | amount: amount, 75 | fee: user_fee, 76 | ttl: Keyword.get(opts, :ttl, Transaction.default_ttl()), 77 | nonce: nonce, 78 | payload: Keyword.get(opts, :payload, Transaction.default_payload()) 79 | ), 80 | new_fee <- 81 | Transaction.calculate_n_times_fee( 82 | spend_tx, 83 | height, 84 | network_id, 85 | user_fee, 86 | gas_price, 87 | Transaction.default_fee_calculation_times() 88 | ), 89 | {:ok, _response} = response <- 90 | Transaction.post( 91 | client, 92 | %{spend_tx | fee: new_fee}, 93 | Keyword.get(opts, :auth, :no_auth), 94 | :one_signature 95 | ) do 96 | response 97 | else 98 | err -> prepare_result(err) 99 | end 100 | end 101 | 102 | @doc """ 103 | Get an account's balance 104 | 105 | ## Example 106 | iex> pubkey = "ak_6A2vcm1Sz6aqJezkLCssUXcyZTX7X8D5UwbuS2fRJr9KkYpRU" 107 | iex> AeppSDK.Account.balance(client, pubkey) 108 | {:ok, 1652992279192254044805} 109 | """ 110 | @spec balance(Client.t(), String.t()) :: 111 | {:ok, non_neg_integer()} | {:error, String.t()} | {:error, Env.t()} 112 | def balance(%Client{} = client, pubkey) when is_binary(pubkey) do 113 | case get(client, pubkey) do 114 | {:ok, %{balance: balance}} -> 115 | {:ok, balance} 116 | 117 | _ = response -> 118 | prepare_result(response) 119 | end 120 | end 121 | 122 | @doc """ 123 | Get an account's balance at a given height 124 | 125 | ## Example 126 | iex> pubkey = "ak_6A2vcm1Sz6aqJezkLCssUXcyZTX7X8D5UwbuS2fRJr9KkYpRU" 127 | iex> height = 80 128 | iex> AeppSDK.Account.balance(client, pubkey, height) 129 | {:ok, 1641606227460612819475} 130 | """ 131 | @spec balance(Client.t(), String.t(), non_neg_integer()) :: 132 | {:ok, non_neg_integer()} | {:error, String.t()} | {:error, Env.t()} 133 | def balance(%Client{} = client, pubkey, height) when is_binary(pubkey) and is_integer(height) do 134 | response = get(client, pubkey, height) 135 | 136 | prepare_result(response) 137 | end 138 | 139 | @doc """ 140 | Get an account's balance at a given block hash 141 | 142 | ## Example 143 | iex> pubkey = "ak_6A2vcm1Sz6aqJezkLCssUXcyZTX7X8D5UwbuS2fRJr9KkYpRU" 144 | iex> block_hash = "kh_2XteYFUyUYjnMDJzHszhHegpoV59QpWTLnMPw5eohsXntzdf6P" 145 | iex> AeppSDK.Account.balance(client, pubkey, block_hash) 146 | {:ok, 1653014562214254044805} 147 | """ 148 | @spec balance(Client.t(), String.t(), String.t()) :: 149 | {:ok, non_neg_integer()} | {:error, String.t()} | {:error, Env.t()} 150 | def balance(%Client{} = client, pubkey, block_hash) 151 | when is_binary(pubkey) and is_binary(block_hash) do 152 | response = get(client, pubkey, block_hash) 153 | 154 | prepare_result(response) 155 | end 156 | 157 | @doc """ 158 | Get an actual account information 159 | ## Example 160 | iex> AeppSDK.Account.get(client) 161 | {:ok, 162 | %{ 163 | auth_fun: nil, 164 | balance: 151688000000000000000, 165 | contract_id: nil, 166 | id: "ak_6A2vcm1Sz6aqJezkLCssUXcyZTX7X8D5UwbuS2fRJr9KkYpRU", 167 | kind: "basic", 168 | nonce: 0, 169 | payable: true 170 | }} 171 | """ 172 | @spec get(Client.t()) :: {:ok, account()} | {:error, String.t()} | {:error, Env.t()} 173 | def get(%Client{connection: connection, keypair: %{public: pubkey}}) 174 | when is_binary(pubkey) do 175 | response = AccountApi.get_account_by_pubkey(connection, pubkey) 176 | 177 | prepare_result(response) 178 | end 179 | 180 | @doc """ 181 | Get an actual account information 182 | 183 | ## Example 184 | iex> pubkey = "ak_6A2vcm1Sz6aqJezkLCssUXcyZTX7X8D5UwbuS2fRJr9KkYpRU" 185 | iex> AeppSDK.Account.get(client, pubkey) 186 | {:ok, 187 | %{ 188 | auth_fun: nil, 189 | balance: 151688000000000000000, 190 | contract_id: nil, 191 | id: "ak_6A2vcm1Sz6aqJezkLCssUXcyZTX7X8D5UwbuS2fRJr9KkYpRU", 192 | kind: "basic", 193 | nonce: 0, 194 | payable: true 195 | }} 196 | """ 197 | @spec get(Client.t(), String.t()) :: 198 | {:ok, account()} | {:error, String.t()} | {:error, Env.t()} 199 | def get(%Client{connection: connection}, pubkey) 200 | when is_binary(pubkey) do 201 | response = AccountApi.get_account_by_pubkey(connection, pubkey) 202 | 203 | prepare_result(response) 204 | end 205 | 206 | @doc """ 207 | Get an account at a given height 208 | 209 | ## Example 210 | iex> pubkey = "ak_6A2vcm1Sz6aqJezkLCssUXcyZTX7X8D5UwbuS2fRJr9KkYpRU" 211 | iex> height = 80 212 | iex> AeppSDK.Account.get(client, pubkey, height) 213 | {:ok, 214 | %{ 215 | auth_fun: nil, 216 | balance: 1641606227460612819475, 217 | contract_id: nil, 218 | id: "ak_6A2vcm1Sz6aqJezkLCssUXcyZTX7X8D5UwbuS2fRJr9KkYpRU", 219 | kind: "basic", 220 | nonce: 11215 221 | }} 222 | """ 223 | @spec get(Client.t(), String.t(), non_neg_integer()) :: 224 | {:ok, account()} | {:error, String.t()} | {:error, Env.t()} 225 | def get(%Client{connection: connection}, pubkey, height) 226 | when is_binary(pubkey) and is_integer(height) do 227 | response = AccountApi.get_account_by_pubkey_and_height(connection, pubkey, height) 228 | 229 | prepare_result(response) 230 | end 231 | 232 | @doc """ 233 | Get an account at a given block hash 234 | 235 | ## Example 236 | iex> pubkey = "ak_6A2vcm1Sz6aqJezkLCssUXcyZTX7X8D5UwbuS2fRJr9KkYpRU" 237 | iex> block_hash = "kh_2XteYFUyUYjnMDJzHszhHegpoV59QpWTLnMPw5eohsXntzdf6P" 238 | iex> AeppSDK.Account.get(client, pubkey, block_hash) 239 | {:ok, 240 | %{ 241 | auth_fun: nil, 242 | balance: 1653014562214254044805, 243 | contract_id: nil, 244 | id: "ak_6A2vcm1Sz6aqJezkLCssUXcyZTX7X8D5UwbuS2fRJr9KkYpRU", 245 | kind: "basic", 246 | nonce: 11837 247 | }} 248 | """ 249 | @spec get(Client.t(), String.t(), String.t()) :: 250 | {:ok, account()} | {:error, String.t()} | {:error, Env.t()} 251 | def get(%Client{connection: connection}, pubkey, block_hash) 252 | when is_binary(pubkey) and is_binary(block_hash) do 253 | response = AccountApi.get_account_by_pubkey_and_hash(connection, pubkey, block_hash) 254 | 255 | prepare_result(response) 256 | end 257 | 258 | defp prepare_result({:ok, %Account{} = account}) do 259 | account_map = Map.from_struct(account) 260 | 261 | {:ok, account_map} 262 | end 263 | 264 | defp prepare_result({:ok, %{balance: balance}}) do 265 | {:ok, balance} 266 | end 267 | 268 | defp prepare_result({:ok, %Error{reason: message}}) do 269 | {:error, message} 270 | end 271 | 272 | defp prepare_result({:error, _} = error) do 273 | error 274 | end 275 | 276 | defp process_recipient( 277 | <> = recipient_id, 278 | _middleware 279 | ) 280 | when recipient_prefix in @allowed_recipient_tags do 281 | {:ok, recipient_id} 282 | end 283 | 284 | defp process_recipient(recipient_id, client) when is_binary(recipient_id) do 285 | case AENS.validate_name(recipient_id) do 286 | {:ok, name} -> 287 | case Middleware.search_name(client, name) do 288 | {:ok, [%{owner: owner}]} -> {:ok, owner} 289 | {:ok, []} -> {:error, "#{__MODULE__}: No owner found"} 290 | _ -> {:error, "#{__MODULE__}: Could not connect to middleware"} 291 | end 292 | 293 | {:error, _} = err -> 294 | err 295 | end 296 | end 297 | 298 | defp process_recipient(recipient, _) do 299 | {:error, "#{__MODULE__}: Invalid recipient key/name: #{inspect(recipient)}"} 300 | end 301 | end 302 | -------------------------------------------------------------------------------- /lib/core/channel/offchain.ex: -------------------------------------------------------------------------------- 1 | defmodule AeppSDK.Channel.OffChain do 2 | @moduledoc """ 3 | Module for Aeternity Off-chain channel activities, see: [https://github.com/aeternity/protocol/blob/master/channels/OFF-CHAIN.md](https://github.com/aeternity/protocol/blob/master/channels/OFF-CHAIN.md) 4 | Contains Off-Chain channel-related functionality. 5 | """ 6 | alias AeppSDK.Utils.Serialization 7 | 8 | @update_vsn 1 9 | @updates_version 1 10 | @no_updates_version 2 11 | @meta_fields_template [data: :binary] 12 | @transfer_fields_template [from: :id, to: :id, amount: :int] 13 | @deposit_fields_template [from: :id, amount: :int] 14 | @withdraw_fields_template [to: :id, amount: :int] 15 | @create_contract_fields_template [ 16 | owner: :id, 17 | ct_version: :int, 18 | code: :binary, 19 | deposit: :int, 20 | call_data: :binary 21 | ] 22 | @call_contract_fields_template [ 23 | caller: :id, 24 | contract: :id, 25 | abi_version: :int, 26 | amount: :int, 27 | gas: :int, 28 | gas_price: :int, 29 | call_data: :binary, 30 | call_stack: [:int] 31 | ] 32 | 33 | @doc """ 34 | Creates new off-chain transactions, supporting updates, with given parameters. 35 | 36 | ## Example 37 | 38 | iex> channel = "ch_11111111111111111111111111111111273Yts" 39 | iex> state_hash = "st_11111111111111111111111111111111273Yts" 40 | iex> AeppSDK.Channel.OffChain.new(channel, 1, state_hash, 1, []) 41 | %{ 42 | channel_id: "ch_11111111111111111111111111111111273Yts", 43 | round: 1, 44 | state_hash: "st_11111111111111111111111111111111273Yts", 45 | updates: [], 46 | version: 1 47 | } 48 | """ 49 | @spec new(String.t(), integer(), String.t(), integer(), list()) :: map() 50 | def new( 51 | <<"ch_", _channel_id::binary>> = channel_id, 52 | round, 53 | <<"st_", _state_hash::binary>> = encoded_state_hash, 54 | @updates_version, 55 | updates 56 | ) 57 | when is_list(updates) do 58 | %{ 59 | channel_id: channel_id, 60 | round: round, 61 | state_hash: encoded_state_hash, 62 | version: @updates_version, 63 | updates: serialize_updates(updates) 64 | } 65 | end 66 | 67 | @doc """ 68 | Creates new off-chain transactions, without supporting updates, with given parameters. 69 | 70 | ## Example 71 | 72 | iex> channel = "ch_11111111111111111111111111111111273Yts" 73 | iex> state_hash = "st_11111111111111111111111111111111273Yts" 74 | iex> AeppSDK.Channel.OffChain.new(channel, 1, state_hash, 2) 75 | %{ 76 | channel_id: "ch_11111111111111111111111111111111273Yts", 77 | round: 1, 78 | state_hash: "st_11111111111111111111111111111111273Yts", 79 | version: 2 80 | } 81 | """ 82 | 83 | @spec new(String.t(), integer(), String.t(), integer()) :: map() 84 | def new( 85 | <<"ch_", _channel_id::binary>> = channel_id, 86 | round, 87 | <<"st_", _state_hash::binary>> = encoded_state_hash, 88 | @no_updates_version 89 | ) do 90 | %{ 91 | channel_id: channel_id, 92 | round: round, 93 | version: @no_updates_version, 94 | state_hash: encoded_state_hash 95 | } 96 | end 97 | 98 | @doc """ 99 | Serializes off-chain transactions, supports both updates and no-updates versions. 100 | 101 | ## Example 102 | 103 | iex> channel = "ch_11111111111111111111111111111111273Yts" 104 | iex> state_hash = "st_11111111111111111111111111111111273Yts" 105 | iex> channel_off_chain_tx = AeppSDK.Channel.OffChain.new(channel, 1, state_hash, 2) 106 | iex> AeppSDK.Channel.OffChain.serialize_tx(channel_off_chain_tx) 107 | <<248,70,57,2,161,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, 108 | 160,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0>> 109 | 110 | """ 111 | @spec serialize_tx(map()) :: binary() 112 | def serialize_tx( 113 | %{ 114 | channel_id: _channel_id, 115 | round: _round, 116 | state_hash: _state_hash, 117 | version: _version 118 | } = offchain_tx 119 | ) do 120 | Serialization.serialize(offchain_tx) 121 | end 122 | 123 | @doc """ 124 | Serializes off-chain updates. 125 | 126 | ## Example 127 | 128 | iex> update = %{type: :transfer, from: {:id, :account, <<0::256>>}, to: {:id, :account, <<0::256>>}, amount: 100} 129 | iex> AeppSDK.Channel.OffChain.serialize_updates(update) 130 | [<<248,73,130,2,58,1,161,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 131 | 161,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,100>>] 132 | 133 | """ 134 | @spec serialize_updates(list()) :: list(binary()) 135 | def serialize_updates(update) when is_list(update) do 136 | serialize_update(update, []) 137 | end 138 | 139 | def serialize_updates(%{type: _} = valid_update_struct) do 140 | serialize_update([valid_update_struct], []) 141 | end 142 | 143 | defp serialize_update([], acc) do 144 | acc 145 | end 146 | 147 | defp serialize_update( 148 | [ 149 | %{ 150 | type: :transfer, 151 | from: {:id, _, _from_id}, 152 | to: {:id, _, _to_id}, 153 | amount: _amount 154 | } = update 155 | | updates 156 | ], 157 | acc 158 | ) do 159 | template = @transfer_fields_template 160 | fields = prepare_fields(update, template) 161 | 162 | serialized_update = 163 | :aeser_chain_objects.serialize( 164 | :channel_offchain_update_transfer, 165 | @update_vsn, 166 | template, 167 | fields 168 | ) 169 | 170 | serialize_update(updates, [serialized_update | acc]) 171 | end 172 | 173 | defp serialize_update( 174 | [ 175 | %{type: :withdraw, to: {:id, _, _account_id}, amount: _amount} = update 176 | | updates 177 | ], 178 | acc 179 | ) do 180 | template = @withdraw_fields_template 181 | fields = prepare_fields(update, template) 182 | 183 | serialized_update = 184 | :aeser_chain_objects.serialize( 185 | :channel_offchain_update_withdraw, 186 | @update_vsn, 187 | template, 188 | fields 189 | ) 190 | 191 | serialize_update(updates, [serialized_update | acc]) 192 | end 193 | 194 | defp serialize_update( 195 | [ 196 | %{type: :deposit, from: {:id, _, _caller_id}, amount: _amount} = update 197 | | updates 198 | ], 199 | acc 200 | ) do 201 | template = @deposit_fields_template 202 | fields = prepare_fields(update, template) 203 | 204 | serialized_update = 205 | :aeser_chain_objects.serialize( 206 | :channel_offchain_update_deposit, 207 | @update_vsn, 208 | template, 209 | fields 210 | ) 211 | 212 | serialize_update(updates, [serialized_update | acc]) 213 | end 214 | 215 | defp serialize_update( 216 | [ 217 | %{ 218 | type: :create_contract, 219 | owner: {:id, _, _owner_id}, 220 | ct_version: _ct_version, 221 | code: _code, 222 | deposit: _deposit, 223 | call_data: _call_data 224 | } = update 225 | | updates 226 | ], 227 | acc 228 | ) do 229 | template = @create_contract_fields_template 230 | fields = prepare_fields(update, template) 231 | 232 | serialized_update = 233 | :aeser_chain_objects.serialize( 234 | :channel_offchain_update_create_contract, 235 | @update_vsn, 236 | template, 237 | fields 238 | ) 239 | 240 | serialize_update(updates, [serialized_update | acc]) 241 | end 242 | 243 | defp serialize_update( 244 | [ 245 | %{ 246 | type: :call_contract, 247 | caller: {:id, _, _caller_id}, 248 | contract: {:id, _, _contract_id}, 249 | abi_version: _abi_version, 250 | amount: _amount, 251 | call_data: _call_data, 252 | call_stack: _call_stack, 253 | gas_price: _gas_price, 254 | gas: _gas 255 | } = update 256 | | updates 257 | ], 258 | acc 259 | ) do 260 | template = @call_contract_fields_template 261 | fields = prepare_fields(update, template) 262 | 263 | serialized_update = 264 | :aeser_chain_objects.serialize( 265 | :channel_offchain_update_call_contract, 266 | @update_vsn, 267 | template, 268 | fields 269 | ) 270 | 271 | serialize_update(updates, [serialized_update | acc]) 272 | end 273 | 274 | defp serialize_update([%{type: :meta} = update | updates], acc) do 275 | template = @meta_fields_template 276 | fields = prepare_fields(update, template) 277 | 278 | serialized_update = 279 | :aeser_chain_objects.serialize(:channel_offchain_update_meta, @update_vsn, template, fields) 280 | 281 | serialize_update(updates, [serialized_update | acc]) 282 | end 283 | 284 | defp prepare_fields(update, template) do 285 | for {field, _type} <- template do 286 | {field, Map.get(update, field)} 287 | end 288 | end 289 | end 290 | -------------------------------------------------------------------------------- /lib/core/client.ex: -------------------------------------------------------------------------------- 1 | defmodule AeppSDK.Client do 2 | @moduledoc """ 3 | Contains the Client structure, holding all the data that is needed in order to use the SDK. 4 | """ 5 | use Tesla 6 | 7 | alias __MODULE__ 8 | alias AeternityNode.Connection 9 | 10 | @default_gas_price 1_000_000 11 | 12 | defstruct [ 13 | :keypair, 14 | :network_id, 15 | :middleware, 16 | :connection, 17 | :internal_connection, 18 | gas_price: @default_gas_price 19 | ] 20 | 21 | @type t :: %Client{ 22 | keypair: keypair(), 23 | network_id: String.t(), 24 | middleware: struct(), 25 | connection: struct(), 26 | internal_connection: struct(), 27 | gas_price: non_neg_integer() 28 | } 29 | 30 | @type keypair :: %{public: String.t(), secret: fun()} 31 | 32 | plug(Tesla.Middleware.Headers, [{"User-Agent", "Elixir"}]) 33 | plug(Tesla.Middleware.EncodeJson) 34 | 35 | @doc """ 36 | Client constructor 37 | 38 | ## Example 39 | iex> public_key = "ak_6A2vcm1Sz6aqJezkLCssUXcyZTX7X8D5UwbuS2fRJr9KkYpRU" 40 | iex> secret_key = "a7a695f999b1872acb13d5b63a830a8ee060ba688a478a08c6e65dfad8a01cd70bb4ed7927f97b51e1bcb5e1340d12335b2a2b12c8bc5221d63c4bcb39d41e61" 41 | iex> network_id = "ae_uat" 42 | iex> url = "https://sdk-testnet.aepps.com/v2" 43 | iex> internal_url = "https://sdk-testnet.aepps.com/v2" 44 | iex> AeppSDK.Client.new(%{public: public_key, secret: secret_key}, network_id, url, internal_url) 45 | %AeppSDK.Client{ 46 | connection: %Tesla.Client{ 47 | adapter: {Tesla.Adapter.Hackney, :call, [[recv_timeout: 30000]]}, 48 | fun: nil, 49 | post: [], 50 | pre: [ 51 | {Tesla.Middleware.BaseUrl, :call, ["https://sdk-testnet.aepps.com/v2"]} 52 | ] 53 | }, 54 | gas_price: 1000000, 55 | internal_connection: %Tesla.Client{ 56 | adapter: {Tesla.Adapter.Hackney, :call, [[recv_timeout: 30000]]}, 57 | fun: nil, 58 | post: [], 59 | pre: [ 60 | {Tesla.Middleware.BaseUrl, :call, ["https://sdk-testnet.aepps.com/v2"]} 61 | ] 62 | }, 63 | keypair: %{ 64 | public: "ak_6A2vcm1Sz6aqJezkLCssUXcyZTX7X8D5UwbuS2fRJr9KkYpRU", 65 | secret: #Function<1.7530086/0 in AeppSDK.Client.new/5> 66 | }, 67 | middleware: %Tesla.Client{ 68 | adapter: {Tesla.Adapter.Hackney, :call, [[recv_timeout: 30000]]}, 69 | fun: nil, 70 | post: [], 71 | pre: [ 72 | {Tesla.Middleware.BaseUrl, :call, 73 | ["https://testnet.aeternal.io/middleware"]} 74 | ] 75 | }, 76 | network_id: "ae_uat" 77 | } 78 | """ 79 | @spec new(keypair(), String.t(), String.t(), String.t(), list()) :: Client.t() 80 | def new( 81 | %{public: public_key, secret: secret_key} = keypair, 82 | network_id, 83 | url, 84 | internal_url, 85 | opts \\ [] 86 | ) 87 | when is_binary(public_key) and is_binary(secret_key) and is_binary(network_id) and 88 | is_binary(url) and is_binary(internal_url) do 89 | middleware = 90 | case network_id do 91 | "ae_uat" -> "https://testnet.aeternal.io/middleware" 92 | "ae_mainnet" -> "https://mainnet.aeternal.io/middleware" 93 | _ -> Keyword.get(opts, :middleware, :no_middleware) 94 | end 95 | 96 | build_middleware = 97 | if middleware != :no_middleware do 98 | Connection.new(middleware) 99 | else 100 | :no_middleware 101 | end 102 | 103 | connection = Connection.new(url) 104 | internal_connection = Connection.new(internal_url) 105 | 106 | %Client{ 107 | keypair: %{keypair | secret: fn -> secret_key end}, 108 | network_id: network_id, 109 | middleware: build_middleware, 110 | connection: connection, 111 | internal_connection: internal_connection, 112 | gas_price: Keyword.get(opts, :gas_price, @default_gas_price) 113 | } 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/core/generalized_account.ex: -------------------------------------------------------------------------------- 1 | defmodule AeppSDK.GeneralizedAccount do 2 | @moduledoc """ 3 | Contains all generalized accounts functionalities. 4 | 5 | In order for its functions to be used, a client must be defined first. 6 | Client example can be found at: `AeppSDK.Client.new/4`. 7 | 8 | For more information: [https://github.com/aeternity/protocol/blob/master/generalized_accounts/generalized_accounts.md](https://github.com/aeternity/protocol/blob/master/generalized_accounts/generalized_accounts.md) 9 | """ 10 | 11 | alias AeppSDK.{Client, Contract} 12 | alias AeppSDK.Utils.Account, as: AccountUtils 13 | alias AeppSDK.Utils.{Encoding, Hash, Serialization, Transaction} 14 | alias AeternityNode.Api.Chain, as: ChainApi 15 | alias AeternityNode.Model.Error 16 | 17 | @init_function "init" 18 | @default_gas 50_000 19 | 20 | @doc """ 21 | Attach a generalized account to a basic account. After a generalized account has been attached, it's possible 22 | pass an :auth option to transaction related functions in order to authorize them through the attached contract. 23 | A transaction is authorized whenever the call to the auth function returns true (and unauthorized when false). 24 | 25 | The option looks like this: 26 | auth: [ 27 | auth_contract_source: "contract Authorization = 28 | 29 | function auth(auth_value : bool) = 30 | auth_value", 31 | auth_args: ["true"], 32 | fee: 1_000_000_000_000_00, 33 | gas: 50_000, 34 | gas_price: 1_000_000_000, 35 | ttl: 0 36 | ] 37 | where gas, gas_price and ttl are optional. 38 | 39 | ## Examples 40 | iex> source_code = "contract Authorization = 41 | 42 | entrypoint auth(auth_value : bool) = 43 | auth_value" 44 | iex> auth_fun = "auth" 45 | iex> init_args = [] 46 | iex> AeppSDK.GeneralizedAccount.attach(client, source_code, auth_fun, init_args) 47 | {:ok, 48 | %{ 49 | block_hash: "mh_CfEuHm4V2omAQGNAxcdPARrkfnYbKuuF1HpGhG5oQvoVC34nD", 50 | block_height: 92967, 51 | tx_hash: "th_9LutrWD1FuFyx4MUUeMcfyF3uebfaP8t5gzatWDLyFYsqK744" 52 | }} 53 | """ 54 | @spec attach(Client.t(), String.t(), String.t(), list(String.t()), list()) :: 55 | {:ok, 56 | %{ 57 | block_hash: Encoding.base58c(), 58 | block_height: non_neg_integer(), 59 | tx_hash: Encoding.base58c() 60 | }} 61 | | {:error, String.t()} 62 | | {:error, Env.t()} 63 | def attach( 64 | %Client{ 65 | keypair: %{public: public_key}, 66 | connection: connection, 67 | network_id: network_id, 68 | gas_price: gas_price 69 | } = client, 70 | source_code, 71 | auth_fun, 72 | init_args, 73 | opts \\ [] 74 | ) do 75 | user_fee = Keyword.get(opts, :fee, Transaction.dummy_fee()) 76 | vm = Keyword.get(opts, :vm, :fate) 77 | 78 | with {:ok, nonce} <- AccountUtils.next_valid_nonce(client, public_key), 79 | {:ok, ct_version} <- Contract.get_ct_version(opts), 80 | {:ok, 81 | %{ 82 | byte_code: byte_code, 83 | compiler_version: compiler_version, 84 | type_info: type_info, 85 | payable: payable 86 | }} <- 87 | Contract.compile(source_code, vm), 88 | {:ok, calldata} <- Contract.create_calldata(source_code, @init_function, init_args, vm), 89 | {:ok, function_hash} <- hash_from_function_name(auth_fun, type_info, vm), 90 | {:ok, source_hash} <- Hash.hash(source_code), 91 | byte_code_fields = [ 92 | source_hash, 93 | type_info, 94 | byte_code, 95 | compiler_version, 96 | payable 97 | ], 98 | serialized_wrapped_code = Serialization.serialize(byte_code_fields, :sophia_byte_code), 99 | ga_attach_tx = %{ 100 | owner_id: public_key, 101 | nonce: nonce, 102 | code: serialized_wrapped_code, 103 | auth_fun: function_hash, 104 | ct_version: ct_version, 105 | fee: user_fee, 106 | ttl: Keyword.get(opts, :ttl, Transaction.default_ttl()), 107 | gas: Keyword.get(opts, :gas, Contract.default_gas()), 108 | gas_price: Keyword.get(opts, :gas_price, Contract.default_gas_price()), 109 | call_data: calldata 110 | }, 111 | {:ok, %{height: height}} <- ChainApi.get_current_key_block_height(connection), 112 | new_fee <- 113 | Transaction.calculate_n_times_fee( 114 | ga_attach_tx, 115 | height, 116 | network_id, 117 | user_fee, 118 | gas_price, 119 | Transaction.default_fee_calculation_times() 120 | ), 121 | {:ok, response} <- 122 | Transaction.post( 123 | client, 124 | %{ga_attach_tx | fee: new_fee}, 125 | Keyword.get(opts, :auth, :no_auth), 126 | :one_signature 127 | ) do 128 | {:ok, response} 129 | else 130 | {:ok, %Error{reason: message}} -> 131 | {:error, message} 132 | 133 | {:error, _} = error -> 134 | error 135 | end 136 | end 137 | 138 | @doc """ 139 | Computes an authorization id for given GA meta tx 140 | 141 | ## Examples 142 | iex> meta_tx = %{ 143 | abi_version: 3, 144 | auth_data: <<43, 17, 244, 119, 202, 45, 27, 127>>, 145 | fee: 100000000000000, 146 | ga_id: "ak_wuLXPE5pd2rvFoxHxvenBgp459rW6Y1cZ6cYTZcAcLAevPE5M", 147 | gas: 50000, 148 | gas_price: 1000000, 149 | ttl: 0, 150 | tx: <<248, 87, 11, 1, 192, 184, 82, 248, 80, 12, 1, 161, 1, 124, 169, 154, 151 | 140, 216, 36, 178, 163, 239, 195, 198, 197, 213, 0, 88, 87, 19, 67, 5, 117, 152 | 212, 206, 105, 153, 178, 2, 203, 32, 248, 96, 25, 216, 161, 1, 11, 180, 237, 153 | 121, 39, 249, 123, 81, 225, 188, 181, 225, 52, 13, 18, 51, 91, 42, 43, 18, 154 | 200, 188, 82, 33, 214, 60, 75, 203, 57, 212, 30, 97, 100, 133, 3, 223, 210, 155 | 64, 0, 0, 0, 128>> 156 | } 157 | iex> AeppSDK.GeneralizedAccount.compute_auth_id(meta_tx) 158 | {:ok, 159 | <<141, 79, 64, 237, 32, 190, 35, 175, 230, 66, 224, 247, 43, 83, 109, 142, 1, 160 | 161, 69, 1, 114, 107, 20, 99, 55, 155, 198, 212, 142, 147, 104, 117>>} 161 | """ 162 | @spec compute_auth_id(map()) :: {:ok, binary()} 163 | def compute_auth_id(%{ga_id: ga_id, auth_data: auth_data} = _meta_tx) do 164 | decoded_ga_id = Encoding.prefix_decode_base58c(ga_id) 165 | {:ok, _auth_id} = Hash.hash(decoded_ga_id <> auth_data) 166 | end 167 | 168 | @doc """ 169 | false 170 | """ 171 | def default_gas, do: @default_gas 172 | 173 | defp hash_from_function_name(auth_fun, type_info, vm) do 174 | case vm do 175 | :aevm -> 176 | :aeb_aevm_abi.type_hash_from_function_name(auth_fun, type_info) 177 | 178 | :fate -> 179 | Hash.hash(auth_fun) 180 | end 181 | end 182 | end 183 | -------------------------------------------------------------------------------- /lib/core/listener/peer_connection.ex: -------------------------------------------------------------------------------- 1 | defmodule AeppSDK.Listener.PeerConnection do 2 | @moduledoc false 3 | 4 | use GenServer 5 | 6 | alias AeppSDK.Listener 7 | alias AeppSDK.Listener.Peers 8 | alias AeppSDK.Utils.{Encoding, Hash, Serialization} 9 | 10 | @behaviour :ranch_protocol 11 | 12 | @p2p_protocol_vsn 1 13 | 14 | @msg_fragment 0 15 | @ping 1 16 | @micro_header 0 17 | @key_header 1 18 | @get_block_txs 7 19 | @txs 9 20 | @key_block 10 21 | @micro_block 11 22 | @block_txs 13 23 | @p2p_response 100 24 | 25 | @noise_timeout 5000 26 | 27 | @first_ping_timeout 30_000 28 | 29 | @get_block_txs_version 1 30 | @ping_version 1 31 | @share 32 32 | @difficulty 0 33 | # don't trigger sync attempt when pinging 34 | @sync_allowed <<0>> 35 | 36 | @hash_size 256 37 | 38 | @pool_tx_hash_prefix <<0>> 39 | @block_tx_hash_prefix <<1>> 40 | 41 | def start_link(ref, socket, transport, opts) do 42 | args = [ref, socket, transport, opts] 43 | {:ok, pid} = :proc_lib.start_link(__MODULE__, :accept_init, args) 44 | {:ok, pid} 45 | end 46 | 47 | def start_link(conn_info) do 48 | GenServer.start_link(__MODULE__, conn_info) 49 | end 50 | 51 | # called for inbound connections 52 | def accept_init(ref, socket, :ranch_tcp, %{genesis: genesis_hash} = opts) do 53 | :ok = :proc_lib.init_ack({:ok, self()}) 54 | {:ok, {host, _}} = :inet.peername(socket) 55 | host_bin = host |> :inet.ntoa() |> :binary.list_to_bin() 56 | version = <<@p2p_protocol_vsn::64>> 57 | 58 | binary_genesis = Encoding.prefix_decode_base58c(genesis_hash) 59 | 60 | state = Map.merge(opts, %{host: host_bin, version: version, genesis: binary_genesis}) 61 | 62 | noise_opts = noise_opts(state.privkey, state.pubkey, binary_genesis, version, state.network) 63 | :ok = :ranch.accept_ack(ref) 64 | :ok = :ranch_tcp.setopts(socket, [{:active, true}]) 65 | 66 | case :enoise.accept(socket, noise_opts) do 67 | {:ok, noise_socket, noise_state} -> 68 | r_pubkey = noise_state |> :enoise_hs_state.remote_keys() |> :enoise_keypair.pubkey() 69 | new_state = Map.merge(state, %{r_pubkey: r_pubkey, status: {:connected, noise_socket}}) 70 | Process.send_after(self(), :first_ping_timeout, @first_ping_timeout) 71 | :gen_server.enter_loop(__MODULE__, [], new_state) 72 | 73 | {:error, _reason} -> 74 | :ranch_tcp.close(socket) 75 | end 76 | end 77 | 78 | def init(conn_info) do 79 | updated_conn_info = Map.put(conn_info, :version, <<@p2p_protocol_vsn::64>>) 80 | 81 | # trigger a timeout so that a connection is attempted immediately 82 | {:ok, updated_conn_info, 0} 83 | end 84 | 85 | def handle_call({:send_msg_no_response, msg}, _from, %{status: {:connected, socket}} = state) do 86 | res = :enoise.send(socket, msg) 87 | {:reply, res, state} 88 | end 89 | 90 | # called when initiating a connection 91 | def handle_info( 92 | :timeout, 93 | %{ 94 | genesis: genesis, 95 | version: version, 96 | pubkey: pubkey, 97 | privkey: privkey, 98 | r_pubkey: r_pubkey, 99 | host: host, 100 | port: port, 101 | network: network_id 102 | } = state 103 | ) do 104 | case :gen_tcp.connect(host, port, [:binary, reuseaddr: true, active: false]) do 105 | {:ok, socket} -> 106 | noise_opts = noise_opts(privkey, pubkey, r_pubkey, genesis, version, network_id) 107 | 108 | :inet.setopts(socket, active: true) 109 | 110 | case :enoise.connect(socket, noise_opts) do 111 | {:ok, noise_socket, _status} -> 112 | new_state = Map.put(state, :status, {:connected, noise_socket}) 113 | peer = %{host: host, pubkey: r_pubkey, port: port, connection: self()} 114 | :ok = do_ping(noise_socket, genesis, port) 115 | Peers.add_peer(peer) 116 | {:noreply, new_state} 117 | 118 | {:error, _reason} -> 119 | :gen_tcp.close(socket) 120 | {:stop, :normal, state} 121 | end 122 | 123 | {:error, _reason} -> 124 | {:stop, :normal, state} 125 | end 126 | end 127 | 128 | def handle_info( 129 | :first_ping_timeout, 130 | %{r_pubkey: r_pubkey, status: {:connected, socket}} = state 131 | ) do 132 | case Peers.have_peer?(r_pubkey) do 133 | true -> 134 | {:noreply, state} 135 | 136 | false -> 137 | :enoise.close(socket) 138 | {:stop, :normal, state} 139 | end 140 | end 141 | 142 | def handle_info( 143 | {:noise, _, 144 | <<@msg_fragment::16, fragment_index::16, total_fragments::16, fragment::binary()>>}, 145 | state 146 | ), 147 | do: handle_fragment(state, fragment_index, total_fragments, fragment) 148 | 149 | def handle_info( 150 | {:noise, _, <>}, 151 | %{status: {:connected, socket}, network: network, genesis: genesis} = state 152 | ) do 153 | deserialized_payload = rlp_decode(type, payload) 154 | 155 | case type do 156 | @p2p_response -> 157 | handle_response(deserialized_payload, network, genesis) 158 | 159 | @txs -> 160 | handle_new_pool_txs(deserialized_payload) 161 | 162 | @key_block -> 163 | handle_new_key_block(deserialized_payload) 164 | 165 | @micro_block -> 166 | handle_new_micro_block(deserialized_payload, socket) 167 | 168 | _ -> 169 | :ok 170 | end 171 | 172 | {:noreply, state} 173 | end 174 | 175 | def handle_info({:tcp_closed, _}, state) do 176 | Peers.remove_peer(state.r_pubkey) 177 | {:stop, :normal, state} 178 | end 179 | 180 | defp handle_fragment(state, 1, _m, fragment) do 181 | {:noreply, Map.put(state, :fragments, [fragment])} 182 | end 183 | 184 | defp handle_fragment(%{fragments: fragments} = state, fragment_index, total_fragments, fragment) 185 | when fragment_index == total_fragments do 186 | msg = [fragment | fragments] |> Enum.reverse() |> :erlang.list_to_binary() 187 | send(self(), {:noise, :unused, msg}) 188 | {:noreply, Map.delete(state, :fragments)} 189 | end 190 | 191 | defp handle_fragment(%{fragments: fragments} = state, fragment_index, _m, fragment) 192 | when fragment_index == length(fragments) + 1 do 193 | {:noreply, %{state | fragments: [fragment | fragments]}} 194 | end 195 | 196 | defp handle_response( 197 | %{result: true, type: type, object: object, reason: nil}, 198 | network, 199 | genesis 200 | ) do 201 | case type do 202 | @ping -> 203 | handle_ping_msg(object, network, genesis) 204 | 205 | @block_txs -> 206 | handle_block_txs(object) 207 | end 208 | end 209 | 210 | defp handle_new_pool_txs(txs) do 211 | serialized_txs = 212 | Enum.map(txs, fn {tx, type, binary_hash} -> 213 | hash = Encoding.prefix_encode_base58c("th", binary_hash) 214 | 215 | {%{hash: hash, tx: Serialization.serialize_for_client(tx, type)}, 216 | @pool_tx_hash_prefix <> binary_hash} 217 | end) 218 | 219 | Listener.notify(:pool_transactions, serialized_txs) 220 | end 221 | 222 | defp handle_ping_msg( 223 | %{ 224 | genesis_hash: genesis_hash, 225 | peers: peers 226 | }, 227 | network, 228 | local_genesis 229 | ) do 230 | if local_genesis == genesis_hash do 231 | Enum.each(peers, fn peer -> 232 | if !Peers.have_peer?(peer.pubkey) do 233 | peer 234 | |> Map.merge(%{genesis: genesis_hash, network: network}) 235 | |> Peers.try_connect() 236 | end 237 | end) 238 | end 239 | end 240 | 241 | defp handle_new_key_block(%{block_info: block_info, hash: hash}), 242 | do: Listener.notify(:key_blocks, {block_info, hash}) 243 | 244 | defp handle_new_micro_block( 245 | %{block_info: block_info, hash: hash, tx_hashes: tx_hashes}, 246 | socket 247 | ) do 248 | Listener.notify(:micro_blocks, {block_info, hash}) 249 | do_get_block_txs(hash, tx_hashes, socket) 250 | end 251 | 252 | defp handle_block_txs(txs) do 253 | serialized_txs = 254 | Enum.map(txs, fn {tx, type, binary_hash} -> 255 | hash = Encoding.prefix_encode_base58c("th", binary_hash) 256 | 257 | {%{hash: hash, tx: Serialization.serialize_for_client(tx, type)}, 258 | @block_tx_hash_prefix <> binary_hash} 259 | end) 260 | 261 | Listener.notify(:transactions, serialized_txs) 262 | end 263 | 264 | defp rlp_decode(@p2p_response, encoded_response) do 265 | [_vsn, result, type, reason, object] = :aeser_rlp.decode(encoded_response) 266 | deserialized_result = bool_bin(result) 267 | 268 | deserialized_type = :binary.decode_unsigned(type) 269 | 270 | deserialized_reason = 271 | case reason do 272 | <<>> -> 273 | nil 274 | 275 | reason -> 276 | reason 277 | end 278 | 279 | deserialized_object = 280 | case object do 281 | <<>> -> 282 | nil 283 | 284 | object -> 285 | rlp_decode(deserialized_type, object) 286 | end 287 | 288 | %{ 289 | result: deserialized_result, 290 | type: deserialized_type, 291 | reason: deserialized_reason, 292 | object: deserialized_object 293 | } 294 | end 295 | 296 | defp rlp_decode(@txs, encoded_txs) do 297 | [_vsn, txs] = :aeser_rlp.decode(encoded_txs) 298 | 299 | Enum.map(txs, fn encoded_tx -> decode_tx(encoded_tx) end) 300 | end 301 | 302 | defp rlp_decode(@ping, encoded_ping) do 303 | [ 304 | _vsn, 305 | port, 306 | share, 307 | genesis_hash, 308 | _difficulty, 309 | _best_hash, 310 | _sync_allowed, 311 | peers 312 | ] = :aeser_rlp.decode(encoded_ping) 313 | 314 | %{ 315 | port: :binary.decode_unsigned(port), 316 | share: :binary.decode_unsigned(share), 317 | genesis_hash: genesis_hash, 318 | peers: Peers.rlp_decode_peers(peers) 319 | } 320 | end 321 | 322 | defp rlp_decode(@key_block, encoded_key_block) do 323 | [ 324 | _vsn, 325 | key_block_bin 326 | ] = :aeser_rlp.decode(encoded_key_block) 327 | 328 | <> = key_block_bin 332 | 333 | bin_pow_evidence = <> 334 | 335 | deserialized_pow_evidence = for <>, do: x 336 | 337 | {:ok, key_block_hash} = Hash.hash(key_block_bin) 338 | 339 | prev_block_type = prev_block_type(prev_hash, prev_key_hash) 340 | 341 | block_info = %{ 342 | version: version, 343 | height: height, 344 | prev_hash: :aeser_api_encoder.encode(prev_block_type, <>), 345 | prev_key_hash: :aeser_api_encoder.encode(:key_block_hash, <>), 346 | root_hash: :aeser_api_encoder.encode(:block_state_hash, <>), 347 | miner: :aeser_api_encoder.encode(:account_pubkey, <>), 348 | beneficiary: :aeser_api_encoder.encode(:account_pubkey, <>), 349 | target: target, 350 | pow_evidence: deserialized_pow_evidence, 351 | nonce: nonce, 352 | time: time, 353 | info: :aeser_api_encoder.encode(:contract_bytearray, info) 354 | } 355 | 356 | %{block_info: block_info, hash: key_block_hash} 357 | end 358 | 359 | defp rlp_decode(@micro_block, encoded_micro_block) do 360 | [ 361 | _vsn, 362 | micro_block_bin, 363 | _is_light 364 | ] = :aeser_rlp.decode(encoded_micro_block) 365 | 366 | light_micro_template = [header: :binary, tx_hashes: [:binary], pof: [:binary]] 367 | {type, version, _fields} = :aeser_chain_objects.deserialize_type_and_vsn(micro_block_bin) 368 | 369 | [header: header_bin, tx_hashes: tx_hashes, pof: _pof] = 370 | :aeser_chain_objects.deserialize( 371 | type, 372 | version, 373 | light_micro_template, 374 | micro_block_bin 375 | ) 376 | 377 | <> = header_bin 380 | 381 | pof_hash_size = pof_tag * 32 382 | 383 | <> = rest 384 | 385 | {:ok, header_hash} = Hash.hash(header_bin) 386 | 387 | prev_block_type = prev_block_type(prev_hash, prev_key_hash) 388 | 389 | block_info = %{ 390 | pof_hash: encode_pof_hash(<>), 391 | signature: :aeser_api_encoder.encode(:signature, signature), 392 | version: version_, 393 | height: height, 394 | prev_hash: :aeser_api_encoder.encode(prev_block_type, <>), 395 | prev_key_hash: :aeser_api_encoder.encode(:key_block_hash, <>), 396 | root_hash: :aeser_api_encoder.encode(:block_state_hash, <>), 397 | txs_hash: :aeser_api_encoder.encode(:block_tx_hash, <>), 398 | time: time 399 | } 400 | 401 | %{block_info: block_info, hash: header_hash, tx_hashes: tx_hashes} 402 | end 403 | 404 | defp rlp_decode(@block_txs, encoded_txs) do 405 | [ 406 | _vsn, 407 | _block_hash, 408 | txs 409 | ] = :aeser_rlp.decode(encoded_txs) 410 | 411 | Enum.map(txs, fn encoded_tx -> decode_tx(encoded_tx) end) 412 | end 413 | 414 | defp rlp_decode(_, _), do: "" 415 | 416 | defp decode_tx(tx) do 417 | {:ok, binary_hash} = Hash.hash(tx) 418 | 419 | [signatures: _signatures, transaction: transaction] = 420 | Serialization.deserialize(tx, :signed_tx) 421 | 422 | {type, _version, _fields} = :aeser_chain_objects.deserialize_type_and_vsn(transaction) 423 | deserialized_tx = Serialization.deserialize(transaction, type) 424 | 425 | {deserialized_tx, type, binary_hash} 426 | end 427 | 428 | defp do_ping(socket, genesis, port) do 429 | ping_rlp = genesis |> ping_object_fields(port) |> :aeser_rlp.encode() 430 | msg = <<@ping::16, ping_rlp::binary()>> 431 | :enoise.send(socket, msg) 432 | end 433 | 434 | defp do_get_block_txs(block_hash, tx_hashes, socket) do 435 | get_block_txs_rlp = block_hash |> get_block_txs_fields(tx_hashes) |> :aeser_rlp.encode() 436 | 437 | get_block_txs_msg = <<@get_block_txs::16, get_block_txs_rlp::binary>> 438 | 439 | :ok = :enoise.send(socket, get_block_txs_msg) 440 | end 441 | 442 | defp ping_object_fields(genesis, port), 443 | do: [ 444 | :binary.encode_unsigned(@ping_version), 445 | :binary.encode_unsigned(port), 446 | :binary.encode_unsigned(@share), 447 | genesis, 448 | :binary.encode_unsigned(@difficulty), 449 | genesis, 450 | @sync_allowed, 451 | [] 452 | ] 453 | 454 | defp get_block_txs_fields(block_hash, tx_hashes), 455 | do: [:binary.encode_unsigned(@get_block_txs_version), block_hash, tx_hashes] 456 | 457 | defp noise_opts(privkey, pubkey, r_pubkey, genesis_hash, version, network_id) do 458 | [ 459 | {:rs, :enoise_keypair.new(:dh25519, r_pubkey)} 460 | | noise_opts(privkey, pubkey, genesis_hash, version, network_id) 461 | ] 462 | end 463 | 464 | defp noise_opts(privkey, pubkey, genesis_hash, version, network_id) do 465 | [ 466 | noise: "Noise_XK_25519_ChaChaPoly_BLAKE2b", 467 | s: :enoise_keypair.new(:dh25519, privkey, pubkey), 468 | prologue: <>, 469 | timeout: @noise_timeout 470 | ] 471 | end 472 | 473 | defp encode_pof_hash(""), do: "no_fraud" 474 | 475 | defp encode_pof_hash(pof_hash), do: :aeser_api_encoder.encode(:pof_hash, pof_hash) 476 | 477 | defp bool_bin(bool) do 478 | case bool do 479 | true -> 480 | <<1>> 481 | 482 | false -> 483 | <<0>> 484 | 485 | <<1>> -> 486 | true 487 | 488 | <<0>> -> 489 | false 490 | end 491 | end 492 | 493 | defp prev_block_type(prev_hash, prev_key_hash) do 494 | if prev_hash == prev_key_hash do 495 | :key_block_hash 496 | else 497 | :micro_block_hash 498 | end 499 | end 500 | end 501 | -------------------------------------------------------------------------------- /lib/core/listener/peer_connection_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule AeppSDK.Listener.PeerConnectionSupervisor do 2 | @moduledoc false 3 | use DynamicSupervisor 4 | 5 | alias AeppSDK.Listener.PeerConnection 6 | 7 | def start_link(_args) do 8 | DynamicSupervisor.start_link(__MODULE__, :ok, name: __MODULE__) 9 | end 10 | 11 | def start_peer_connection(conn_info) do 12 | DynamicSupervisor.start_child( 13 | __MODULE__, 14 | Supervisor.child_spec( 15 | {PeerConnection, conn_info}, 16 | restart: :temporary 17 | ) 18 | ) 19 | end 20 | 21 | def init(:ok) do 22 | DynamicSupervisor.init(strategy: :one_for_one) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/core/listener/peers.ex: -------------------------------------------------------------------------------- 1 | defmodule AeppSDK.Listener.Peers do 2 | @moduledoc false 3 | use GenServer 4 | 5 | alias AeppSDK.Listener.PeerConnectionSupervisor 6 | alias AeppSDK.Utils.{Encoding, Keys} 7 | 8 | require Logger 9 | 10 | def start_link(info) do 11 | peers = %{} 12 | 13 | keypair = Keys.generate_peer_keypair() 14 | 15 | state = %{peers: peers, local_keypair: keypair} 16 | GenServer.start_link(__MODULE__, {state, info}, name: __MODULE__) 17 | end 18 | 19 | def init({state, info}) do 20 | {:ok, {state, info}, 0} 21 | end 22 | 23 | def rlp_decode_peers(encoded_peers) do 24 | Enum.map(encoded_peers, fn encoded_peer -> 25 | [host, port_bin, pubkey] = :aeser_rlp.decode(encoded_peer) 26 | 27 | %{ 28 | host: to_charlist(host), 29 | port: :binary.decode_unsigned(port_bin), 30 | pubkey: pubkey 31 | } 32 | end) 33 | end 34 | 35 | def remove_peer(pubkey) do 36 | GenServer.call(__MODULE__, {:remove_peer, pubkey}) 37 | end 38 | 39 | def have_peer?(peer_pubkey) do 40 | GenServer.call(__MODULE__, {:have_peer?, peer_pubkey}) 41 | end 42 | 43 | def state do 44 | GenServer.call(__MODULE__, :state) 45 | end 46 | 47 | def add_peer(conn_info) do 48 | GenServer.call(__MODULE__, {:add_peer, conn_info}) 49 | end 50 | 51 | @spec try_connect(map()) :: :ok 52 | def try_connect(peer_info) do 53 | GenServer.cast(__MODULE__, {:try_connect, peer_info}) 54 | end 55 | 56 | def handle_info( 57 | :timeout, 58 | {state, %{initial_peers: initial_peers, network: network_id, genesis: genesis_hash}} 59 | ) do 60 | :ok = connect_to_peers(initial_peers, network_id, genesis_hash) 61 | {:noreply, state} 62 | end 63 | 64 | def handle_call({:remove_peer, pubkey}, _from, %{peers: peers} = state) do 65 | updated_peers = Map.delete(peers, pubkey) 66 | updated_state = %{state | peers: updated_peers} 67 | {:reply, :ok, updated_state} 68 | end 69 | 70 | def handle_call({:have_peer?, peer_pubkey}, _from, %{peers: peers} = state) do 71 | have_peer = Map.has_key?(peers, peer_pubkey) 72 | {:reply, have_peer, state} 73 | end 74 | 75 | def handle_call(:state, _from, state), do: {:reply, state, state} 76 | 77 | def handle_call( 78 | {:add_peer, %{pubkey: pubkey} = peer_info}, 79 | _from, 80 | %{peers: peers} = state 81 | ) do 82 | updated_peers = Map.put(peers, pubkey, peer_info) 83 | updated_state = %{state | peers: updated_peers} 84 | {:reply, :ok, updated_state} 85 | end 86 | 87 | def handle_cast( 88 | {:try_connect, peer_info}, 89 | %{peers: peers, local_keypair: %{secret: privkey, public: pubkey}} = state 90 | ) do 91 | if peer_info.pubkey != pubkey do 92 | case Map.has_key?(peers, peer_info.pubkey) do 93 | false -> 94 | conn_info = 95 | Map.merge(peer_info, %{r_pubkey: peer_info.pubkey, privkey: privkey, pubkey: pubkey}) 96 | 97 | {:ok, _pid} = PeerConnectionSupervisor.start_peer_connection(conn_info) 98 | 99 | {:noreply, state} 100 | 101 | true -> 102 | Logger.error(fn -> "Won't add #{inspect(peer_info)}, already in peer list" end) 103 | {:noreply, state} 104 | end 105 | else 106 | Logger.info("Can't add ourself") 107 | {:noreply, state} 108 | end 109 | end 110 | 111 | def connect_to_peers([], network_id, nil) do 112 | info = %{network: network_id, genesis: genesis_hash(network_id)} 113 | 114 | network_id |> seed_nodes() |> connect_to_peers(info) 115 | end 116 | 117 | def connect_to_peers(peers, network_id, genesis_hash) do 118 | binary_genesis = Encoding.prefix_decode_base58c(genesis_hash) 119 | info = %{network: network_id, genesis: binary_genesis} 120 | 121 | connect_to_peers(peers, info) 122 | end 123 | 124 | defp connect_to_peers(peers, info) do 125 | Enum.each(peers, fn peer -> 126 | peer |> deserialize_peer() |> Map.merge(info) |> try_connect() 127 | end) 128 | end 129 | 130 | defp deserialize_peer(<<"aenode://", rest::binary>>) do 131 | [pubkey, address] = String.split(rest, "@") 132 | [host, port] = String.split(address, ":") 133 | 134 | %{ 135 | host: String.to_charlist(host), 136 | port: String.to_integer(port), 137 | pubkey: Encoding.prefix_decode_base58c(pubkey) 138 | } 139 | end 140 | 141 | def genesis_hash("ae_mainnet"), 142 | do: 143 | <<108, 21, 218, 110, 191, 175, 2, 120, 254, 175, 77, 241, 176, 241, 169, 130, 85, 7, 174, 144 | 123, 154, 73, 75, 195, 76, 145, 113, 63, 56, 221, 87, 131>> 145 | 146 | def genesis_hash("ae_uat"), 147 | do: 148 | <<123, 173, 180, 20, 178, 230, 65, 254, 59, 198, 234, 129, 117, 11, 11, 80, 142, 52, 238, 149 | 147, 191, 98, 135, 252, 203, 203, 175, 212, 7, 8, 237, 83>> 150 | 151 | defp seed_nodes("ae_mainnet"), 152 | do: [ 153 | "aenode://pp_2L8A5vSjnkLtfFNpJNgP9HbmGLD7ZAGFxoof47N8L4yyLAyyMi@18.136.37.63:3015", 154 | "aenode://pp_2gPZjuPnJnTVEbrB9Qgv7f4MdhM4Jh6PD22mB2iBA1g7FRvHTk@52.220.198.72:3015", 155 | "aenode://pp_tVdaaX4bX54rkaVEwqE81hCgv6dRGPPwEVsiZk41GXG1A4gBN@3.16.242.93:3015", 156 | "aenode://pp_2mwr9ikcyUDUWTeTQqdu8WJeQs845nYPPqjafjcGcRWUx4p85P@3.17.30.101:3015", 157 | "aenode://pp_2CAJwwmM2ZVBHYFB6na1M17roQNuRi98k6WPFcoBMfUXvsezVU@13.58.177.66:3015", 158 | "aenode://pp_7N7dkCbg39MYzQv3vCrmjVNfy6QkoVmJe3VtiZ3HRncvTWAAX@13.53.114.199:3015", 159 | "aenode://pp_22FndjTkMMXZ5gunCTUyeMPbgoL53smqpM4m1Jz5fVuJmPXm24@13.53.149.181:3015", 160 | "aenode://pp_Xgsqi4hYAjXn9BmrU4DXWT7jURy2GoBPmrHfiCoDVd3UPQYcU@13.53.164.121:3015", 161 | "aenode://pp_vTDXS3HJrwJecqnPqX3iRxKG5RBRz9MdicWGy8p9hSdyhAY4S@13.53.77.98:3015" 162 | ] 163 | 164 | defp seed_nodes("ae_uat"), 165 | do: [ 166 | "aenode://pp_QU9CvhAQH56a2kA15tCnWPRJ2srMJW8ZmfbbFTAy7eG4o16Bf@52.10.46.160:3015", 167 | "aenode://pp_2vhFb3HtHd1S7ynbpbFnEdph1tnDXFSfu4NGtq46S2eM5HCdbC@18.195.109.60:3015", 168 | "aenode://pp_27xmgQ4N1E3QwHyoutLtZsHW5DSW4zneQJ3CxT5JbUejxtFuAu@13.250.162.250:3015", 169 | "aenode://pp_DMLqy7Zuhoxe2FzpydyQTgwCJ52wouzxtHWsPGo51XDcxc5c8@13.53.161.215:3015" 170 | ] 171 | end 172 | -------------------------------------------------------------------------------- /lib/core/listener/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule AeppSDK.Listener.Supervisor do 2 | @moduledoc false 3 | 4 | use Supervisor 5 | 6 | alias AeppSDK.Listener 7 | alias AeppSDK.Listener.{PeerConnection, PeerConnectionSupervisor, Peers} 8 | alias AeppSDK.Utils.Keys 9 | 10 | @acceptors_count 10 11 | 12 | def start_link(info) do 13 | Supervisor.start_link(__MODULE__, info, name: __MODULE__) 14 | end 15 | 16 | def init(%{port: port} = info) do 17 | keypair = Keys.generate_peer_keypair() 18 | 19 | children = [ 20 | PeerConnectionSupervisor, 21 | {Peers, info}, 22 | :ranch.child_spec( 23 | :peer_pool, 24 | @acceptors_count, 25 | :ranch_tcp, 26 | [port: port], 27 | PeerConnection, 28 | Map.put( 29 | info, 30 | :keypair, 31 | keypair 32 | ) 33 | ), 34 | Listener 35 | ] 36 | 37 | Supervisor.init(children, strategy: :one_for_one) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/utils/account.ex: -------------------------------------------------------------------------------- 1 | defmodule AeppSDK.Utils.Account do 2 | @moduledoc """ 3 | Account AeppSDK.Utils. 4 | 5 | In order for its functions to be used, a client must be defined first. 6 | Client example can be found at: `AeppSDK.Client.new/4`. 7 | """ 8 | alias AeppSDK.Client 9 | alias AeternityNode.Api.Account, as: AccountApi 10 | alias AeternityNode.Model.{Account, Error} 11 | alias Tesla.Env 12 | 13 | @doc """ 14 | Get the next valid nonce for a public key 15 | 16 | ## Example 17 | iex> public_key = "ak_6A2vcm1Sz6aqJezkLCssUXcyZTX7X8D5UwbuS2fRJr9KkYpRU" 18 | iex> AeppSDK.Utils.Account.next_valid_nonce(client, public_key) 19 | {:ok, 8544} 20 | """ 21 | @spec next_valid_nonce(Client.t(), String.t()) :: 22 | {:ok, integer()} | {:error, String.t()} | {:error, Env.t()} 23 | def next_valid_nonce(%Client{connection: connection}, public_key) do 24 | response = AccountApi.get_account_by_pubkey(connection, public_key) 25 | 26 | prepare_result(response) 27 | end 28 | 29 | @doc """ 30 | Get the nonce after a block indicated by hash 31 | 32 | ## Example 33 | iex> public_key = "ak_6A2vcm1Sz6aqJezkLCssUXcyZTX7X8D5UwbuS2fRJr9KkYpRU" 34 | iex> block_hash = "kh_WPQzXtyDiwvUs54N1L88YsLPn51PERHF76bqcMhpT5vnrAEAT" 35 | iex> AeppSDK.Utils.Account.nonce_at_hash(client, public_key, block_hash) 36 | {:ok, 8327} 37 | """ 38 | @spec nonce_at_hash(Client.t(), String.t(), String.t()) :: 39 | {:ok, integer()} | {:error, String.t()} | {:error, Env.t()} 40 | def nonce_at_hash(%Client{connection: connection}, public_key, block_hash) do 41 | response = AccountApi.get_account_by_pubkey_and_hash(connection, public_key, block_hash) 42 | 43 | prepare_result(response) 44 | end 45 | 46 | defp prepare_result({:ok, %Account{nonce: nonce, kind: "basic"}}) do 47 | {:ok, nonce + 1} 48 | end 49 | 50 | defp prepare_result({:ok, %Account{kind: "generalized"}}) do 51 | {:ok, 0} 52 | end 53 | 54 | defp prepare_result({:ok, %Error{reason: message}}) do 55 | {:error, message} 56 | end 57 | 58 | defp prepare_result({:error, _} = error) do 59 | error 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/utils/chain.ex: -------------------------------------------------------------------------------- 1 | defmodule AeppSDK.Utils.Chain do 2 | @moduledoc """ 3 | Chain AeppSDK.Utils. 4 | 5 | In order for its functions to be used, a client must be defined first. 6 | Client example can be found at: `AeppSDK.Client.new/4`. 7 | """ 8 | alias AeppSDK.Client 9 | alias AeternityNode.Api.Chain, as: ChainApi 10 | alias AeternityNode.Model.{Error, KeyBlock, KeyBlockOrMicroBlockHeader, MicroBlockHeader} 11 | 12 | @doc """ 13 | Get the hash of the current top block 14 | 15 | ## Example 16 | iex> AeppSDK.Utils.Chain.get_top_block_hash(client) 17 | {:ok, "kh_7e74Hs2ThcNdjFD1i5XngUbzTHgmXn9jTaXSej1XKio7rkpgM"} 18 | """ 19 | @spec get_top_block_hash(Client.t()) :: 20 | {:ok, String.t()} | {:error, String.t()} | {:error, Env.t()} 21 | def get_top_block_hash(%Client{connection: connection}) do 22 | case ChainApi.get_top_block(connection) do 23 | {:ok, %KeyBlockOrMicroBlockHeader{key_block: %KeyBlock{hash: hash}}} -> 24 | {:ok, hash} 25 | 26 | {:ok, %KeyBlockOrMicroBlockHeader{micro_block: %MicroBlockHeader{hash: hash}}} -> 27 | {:ok, hash} 28 | 29 | {:ok, %Error{reason: message}} -> 30 | {:error, message} 31 | 32 | {:error, _} = error -> 33 | error 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/utils/encoding.ex: -------------------------------------------------------------------------------- 1 | defmodule AeppSDK.Utils.Encoding do 2 | @moduledoc """ 3 | Contains encoding/decoding utils, 4 | see: [https://github.com/aeternity/protocol/blob/master/node/api/api_encoding.md](https://github.com/aeternity/protocol/blob/master/node/api/api_encoding.md). 5 | """ 6 | 7 | @checksum_bytes 4 8 | 9 | @prefix_bits 24 10 | 11 | @typedoc """ 12 | A base58check string. 13 | """ 14 | @type base58c :: String.t() 15 | 16 | @typedoc """ 17 | A base64 string. 18 | """ 19 | @type base64 :: String.t() 20 | 21 | @typedoc """ 22 | A hexadecimal string. 23 | """ 24 | @type hex :: String.t() 25 | 26 | @doc """ 27 | Encode a binary payload to base58check and add a string prefix 28 | 29 | ## Example 30 | iex> prefix = "ak" 31 | iex> binary = <<200, 90, 234, 160, 66, 120, 244, 87, 88, 94, 87, 208, 13, 42, 126, 71, 172, 2, 81, 252, 214, 24, 155, 227, 26, 49, 210, 31, 106, 147, 200, 81>> 32 | iex> AeppSDK.Utils.Encoding.prefix_encode_base58c(prefix, binary) 33 | "ak_2XEob1Ub1DWCzeMLm1CWQKrUBsVfF9zLZBDaUXiu6Lr1qLn55n" 34 | """ 35 | @spec prefix_encode_base58c(String.t(), binary()) :: base58c() 36 | def prefix_encode_base58c(prefix, payload) when is_binary(payload), 37 | do: prefix <> "_" <> encode_base58c(payload) 38 | 39 | @doc """ 40 | Decode a base58check string to binary and remove its prefix 41 | 42 | ## Example 43 | iex> AeppSDK.Utils.Encoding.prefix_decode_base58c("ak_2XEob1Ub1DWCzeMLm1CWQKrUBsVfF9zLZBDaUXiu6Lr1qLn55n") 44 | <<200, 90, 234, 160, 66, 120, 244, 87, 88, 94, 87, 208, 13, 42, 126, 71, 172, 2, 81, 252, 214, 24, 155, 227, 26, 49, 210, 31, 106, 147, 200, 81>> 45 | """ 46 | @spec prefix_decode_base58c(base58c()) :: binary() 47 | def prefix_decode_base58c(<<_prefix::@prefix_bits, payload::binary>>), 48 | do: decode_base58c(payload) 49 | 50 | @doc """ 51 | Encode a binary payload to base58check 52 | 53 | ## Example 54 | iex> AeppSDK.Utils.Encoding.encode_base58c(<<200, 90, 234, 160, 66, 120, 244, 87, 88, 94, 87, 208, 13, 42, 126, 71, 172, 2, 81, 252, 214, 24, 155, 227, 26, 49, 210, 31, 106, 147, 200, 81>>) 55 | "2XEob1Ub1DWCzeMLm1CWQKrUBsVfF9zLZBDaUXiu6Lr1qLn55n" 56 | """ 57 | @spec encode_base58c(binary()) :: base58c() 58 | def encode_base58c(payload) do 59 | checksum = generate_checksum(payload) 60 | 61 | payload 62 | |> Kernel.<>(checksum) 63 | |> :base58.binary_to_base58() 64 | |> to_string() 65 | end 66 | 67 | @doc """ 68 | Decode a base58check string to binary 69 | 70 | iex> AeppSDK.Utils.Encoding.decode_base58c("2XEob1Ub1DWCzeMLm1CWQKrUBsVfF9zLZBDaUXiu6Lr1qLn55n") 71 | <<200, 90, 234, 160, 66, 120, 244, 87, 88, 94, 87, 208, 13, 42, 126, 71, 172, 2, 81, 252, 214, 24, 155, 227, 26, 49, 210, 31, 106, 147, 200, 81>> 72 | """ 73 | @spec decode_base58c(base58c()) :: binary() 74 | def decode_base58c(payload) do 75 | decoded_payload = 76 | payload 77 | |> String.to_charlist() 78 | |> :base58.base58_to_binary() 79 | 80 | bsize = byte_size(decoded_payload) - @checksum_bytes 81 | <> = decoded_payload 82 | 83 | data 84 | end 85 | 86 | @doc """ 87 | Encode a binary payload to base64 and add a string prefix 88 | 89 | ## Example 90 | iex> prefix = "tx" 91 | iex> binary = <<248, 156, 11, 1, 248, 66, 184, 64, 239, 168, 82, 234, 155, 137, 201, 4, 101, 92 | 138, 106, 29, 17, 149, 151, 170, 181, 55, 176, 222, 189, 77, 127, 227, 78, 93 | 202, 253, 6, 159, 235, 140, 41, 165, 77, 120, 145, 151, 173, 179, 55, 74, 138, 94 | 45, 208, 75, 138, 56, 227, 165, 195, 24, 147, 126, 191, 206, 210, 161, 170, 95 | 87, 136, 229, 30, 6, 2, 184, 84, 248, 82, 12, 1, 161, 1, 0, 0, 0, 0, 0, 0, 0, 96 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97 | 161, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 98 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 10, 10, 135, 112, 97, 121, 108, 111, 97, 99 | 100, 93, 73, 89, 98>> 100 | iex> AeppSDK.Utils.Encoding.prefix_encode_base64(prefix, binary) 101 | "tx_+JwLAfhCuEDvqFLqm4nJBGWKah0RlZeqtTew3r1Nf+NOyv0Gn+uMKaVNeJGXrbM3Soot0EuKOOOlwxiTfr/O0qGqV4jlHgYCuFT4UgwBoQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKCgoKh3BheWxvYWRdSVlidOin1A==" 102 | """ 103 | @spec prefix_encode_base64(String.t(), binary()) :: base64() 104 | def prefix_encode_base64(prefix, payload), do: prefix <> "_" <> encode_base64(payload) 105 | 106 | @doc """ 107 | Encode a binary payload to base64 108 | 109 | ## Example 110 | iex> AeppSDK.Utils.Encoding.encode_base64(<<248, 156, 11, 1, 248, 66, 184, 64, 239, 168, 82, 234, 155, 137, 201, 4, 101, 111 | 138, 106, 29, 17, 149, 151, 170, 181, 55, 176, 222, 189, 77, 127, 227, 78, 112 | 202, 253, 6, 159, 235, 140, 41, 165, 77, 120, 145, 151, 173, 179, 55, 74, 138, 113 | 45, 208, 75, 138, 56, 227, 165, 195, 24, 147, 126, 191, 206, 210, 161, 170, 114 | 87, 136, 229, 30, 6, 2, 184, 84, 248, 82, 12, 1, 161, 1, 0, 0, 0, 0, 0, 0, 0, 115 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 116 | 161, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 117 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 10, 10, 135, 112, 97, 121, 108, 111, 97, 118 | 100, 93, 73, 89, 98>>) 119 | "+JwLAfhCuEDvqFLqm4nJBGWKah0RlZeqtTew3r1Nf+NOyv0Gn+uMKaVNeJGXrbM3Soot0EuKOOOlwxiTfr/O0qGqV4jlHgYCuFT4UgwBoQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKCgoKh3BheWxvYWRdSVlidOin1A==" 120 | """ 121 | @spec encode_base64(binary()) :: base64() 122 | def encode_base64(payload) do 123 | checksum = generate_checksum(payload) 124 | 125 | payload 126 | |> Kernel.<>(checksum) 127 | |> Base.encode64() 128 | end 129 | 130 | @doc """ 131 | Decode a base64 string to binary and remove its prefix 132 | 133 | ## Example 134 | iex> AeppSDK.Utils.Encoding.prefix_decode_base64("tx_+JwLAfhCuEDvqFLqm4nJBGWKah0RlZeqtTew3r1Nf+NOyv0Gn+uMKaVNeJGXrbM3Soot0EuKOOOlwxiTfr/O0qGqV4jlHgYCuFT4UgwBoQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKCgoKh3BheWxvYWRdSVlidOin1A==") 135 | <<248, 156, 11, 1, 248, 66, 184, 64, 239, 168, 82, 234, 155, 137, 201, 4, 101, 136 | 138, 106, 29, 17, 149, 151, 170, 181, 55, 176, 222, 189, 77, 127, 227, 78, 137 | 202, 253, 6, 159, 235, 140, 41, 165, 77, 120, 145, 151, 173, 179, 55, 74, 138, 138 | 45, 208, 75, 138, 56, 227, 165, 195, 24, 147, 126, 191, 206, 210, 161, 170, 139 | 87, 136, 229, 30, 6, 2, 184, 84, 248, 82, 12, 1, 161, 1, 0, 0, 0, 0, 0, 0, 0, 140 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 141 | 161, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 142 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 10, 10, 135, 112, 97, 121, 108, 111, 97, 143 | 100, 93, 73, 89, 98>> 144 | """ 145 | @spec prefix_decode_base64(base64()) :: binary() 146 | def prefix_decode_base64(<<_prefix::@prefix_bits, payload::binary>>), do: decode_base64(payload) 147 | 148 | @doc """ 149 | Decode a base64 string to binary 150 | 151 | ## Example 152 | iex> AeppSDK.Utils.Encoding.decode_base64("+JwLAfhCuEDvqFLqm4nJBGWKah0RlZeqtTew3r1Nf+NOyv0Gn+uMKaVNeJGXrbM3Soot0EuKOOOlwxiTfr/O0qGqV4jlHgYCuFT4UgwBoQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKCgoKh3BheWxvYWRdSVlidOin1A==") 153 | <<248, 156, 11, 1, 248, 66, 184, 64, 239, 168, 82, 234, 155, 137, 201, 4, 101, 154 | 138, 106, 29, 17, 149, 151, 170, 181, 55, 176, 222, 189, 77, 127, 227, 78, 155 | 202, 253, 6, 159, 235, 140, 41, 165, 77, 120, 145, 151, 173, 179, 55, 74, 138, 156 | 45, 208, 75, 138, 56, 227, 165, 195, 24, 147, 126, 191, 206, 210, 161, 170, 157 | 87, 136, 229, 30, 6, 2, 184, 84, 248, 82, 12, 1, 161, 1, 0, 0, 0, 0, 0, 0, 0, 158 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 159 | 161, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 160 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 10, 10, 135, 112, 97, 121, 108, 111, 97, 161 | 100, 93, 73, 89, 98>> 162 | """ 163 | @spec decode_base64(base64()) :: binary() 164 | def decode_base64(payload) do 165 | {:ok, decoded_payload} = Base.decode64(payload) 166 | 167 | bsize = byte_size(decoded_payload) - @checksum_bytes 168 | <> = decoded_payload 169 | 170 | data 171 | end 172 | 173 | defp generate_checksum(payload) do 174 | <> = 175 | :crypto.hash(:sha256, :crypto.hash(:sha256, payload)) 176 | 177 | checksum 178 | end 179 | end 180 | -------------------------------------------------------------------------------- /lib/utils/governance.ex: -------------------------------------------------------------------------------- 1 | defmodule AeppSDK.Utils.Governance do 2 | @moduledoc false 3 | alias AeternityNode.Model.{ 4 | ChannelCloseMutualTx, 5 | ChannelCloseSoloTx, 6 | ChannelCreateTx, 7 | ChannelDepositTx, 8 | ChannelForceProgressTx, 9 | ChannelSettleTx, 10 | ChannelSlashTx, 11 | ChannelSnapshotSoloTx, 12 | ChannelWithdrawTx, 13 | ContractCallTx, 14 | ContractCreateTx, 15 | NameClaimTx, 16 | NamePreclaimTx, 17 | NameRevokeTx, 18 | NameTransferTx, 19 | NameUpdateTx, 20 | OracleExtendTx, 21 | OracleQueryTx, 22 | OracleRegisterTx, 23 | OracleRespondTx, 24 | SpendTx 25 | } 26 | 27 | @byte_gas 20 28 | @tx_base_gas 15_000 29 | # 32_000 as `GCREATE` i.e. an oracle-related state object costs per year as much as it costs to indefinitely create an account. 30 | @oracle_state_gas_per_year 32_000 31 | @expected_block_mine_rate_minutes 3 32 | @expected_blocks_in_a_year_floor 175_200 = div(60 * 24 * 365, @expected_block_mine_rate_minutes) 33 | 34 | @spec tx_base_gas(struct()) :: non_neg_integer() 35 | def tx_base_gas(%SpendTx{}), do: @tx_base_gas 36 | def tx_base_gas(%NamePreclaimTx{}), do: @tx_base_gas 37 | def tx_base_gas(%NameClaimTx{}), do: @tx_base_gas 38 | def tx_base_gas(%NameTransferTx{}), do: @tx_base_gas 39 | def tx_base_gas(%NameRevokeTx{}), do: @tx_base_gas 40 | def tx_base_gas(%NameUpdateTx{}), do: @tx_base_gas 41 | def tx_base_gas(%OracleRegisterTx{}), do: @tx_base_gas 42 | def tx_base_gas(%OracleQueryTx{}), do: @tx_base_gas 43 | def tx_base_gas(%OracleRespondTx{}), do: @tx_base_gas 44 | def tx_base_gas(%OracleExtendTx{}), do: @tx_base_gas 45 | def tx_base_gas(%ContractCallTx{abi_version: 3}), do: 12 * @tx_base_gas 46 | def tx_base_gas(%ContractCallTx{}), do: 30 * @tx_base_gas 47 | def tx_base_gas(%ContractCreateTx{}), do: 5 * @tx_base_gas 48 | def tx_base_gas(%ChannelDepositTx{}), do: @tx_base_gas 49 | def tx_base_gas(%ChannelCloseMutualTx{}), do: @tx_base_gas 50 | def tx_base_gas(%ChannelCloseSoloTx{}), do: @tx_base_gas 51 | def tx_base_gas(%ChannelCreateTx{}), do: @tx_base_gas 52 | def tx_base_gas(%ChannelForceProgressTx{}), do: @tx_base_gas 53 | def tx_base_gas(%ChannelSlashTx{}), do: @tx_base_gas 54 | def tx_base_gas(%ChannelSettleTx{}), do: @tx_base_gas 55 | def tx_base_gas(%ChannelSnapshotSoloTx{}), do: @tx_base_gas 56 | def tx_base_gas(%ChannelWithdrawTx{}), do: @tx_base_gas 57 | def tx_base_gas(_), do: 5 * @tx_base_gas 58 | 59 | @spec gas(struct()) :: non_neg_integer() 60 | def gas(%SpendTx{}), do: 0 61 | def gas(%NamePreclaimTx{}), do: 0 62 | def gas(%NameClaimTx{}), do: 0 63 | def gas(%NameTransferTx{}), do: 0 64 | def gas(%NameRevokeTx{}), do: 0 65 | def gas(%NameUpdateTx{}), do: 0 66 | def gas(%OracleRegisterTx{}), do: 0 67 | def gas(%OracleQueryTx{}), do: 0 68 | def gas(%OracleRespondTx{}), do: 0 69 | def gas(%OracleExtendTx{}), do: 0 70 | def gas(%ContractCallTx{gas: gas}), do: gas 71 | def gas(%ContractCreateTx{gas: gas}), do: gas 72 | def gas(%ChannelDepositTx{}), do: 0 73 | def gas(%ChannelCloseMutualTx{}), do: 0 74 | def gas(%ChannelCloseSoloTx{}), do: 0 75 | def gas(%ChannelCreateTx{}), do: 0 76 | # Have to be implemented 77 | def gas(%ChannelForceProgressTx{}), do: 0 78 | def gas(%ChannelSlashTx{}), do: 0 79 | def gas(%ChannelSettleTx{}), do: 0 80 | def gas(%ChannelSnapshotSoloTx{}), do: 0 81 | def gas(%ChannelWithdrawTx{}), do: 0 82 | def gas(%{gas: gas}), do: gas 83 | 84 | @spec byte_gas :: non_neg_integer() 85 | def byte_gas, do: @byte_gas 86 | 87 | @spec state_gas_per_block( 88 | OracleRegisterTx.t() 89 | | OracleRespondTx.t() 90 | | OracleQueryTx.t() 91 | | OracleExtendTx.t() 92 | ) :: {non_neg_integer(), non_neg_integer()} 93 | def state_gas_per_block(%struct{}) 94 | when struct in [OracleRegisterTx, OracleRespondTx, OracleQueryTx, OracleExtendTx] do 95 | {@oracle_state_gas_per_year, @expected_blocks_in_a_year_floor} 96 | end 97 | 98 | @spec state_gas(tuple(), non_neg_integer()) :: non_neg_integer() 99 | def state_gas({part, whole}, n_key_blocks) 100 | when is_integer(whole) and whole > 0 and is_integer(part) and part >= 0 and 101 | is_integer(n_key_blocks) and n_key_blocks >= 0 do 102 | tmp = n_key_blocks * part 103 | div(tmp + (whole - 1), whole) 104 | end 105 | 106 | @spec min_gas_price(non_neg_integer(), String.t()) :: non_neg_integer() 107 | def min_gas_price(height, network_id) 108 | when is_integer(height) and height >= 0 and is_binary(network_id) do 109 | case protocol_effective_at_height(height, network_id) do 110 | 1 -> 1 111 | _ -> 1_000_000 112 | end 113 | end 114 | 115 | defp protocol_effective_at_height(height, "ae_mainnet") when height < 47_800, do: 1 116 | defp protocol_effective_at_height(height, "ae_mainnet") when height >= 47_800, do: 2 117 | defp protocol_effective_at_height(height, "ae_uat") when height < 40_900, do: 1 118 | defp protocol_effective_at_height(height, "ae_uat") when height >= 40_900, do: 2 119 | end 120 | -------------------------------------------------------------------------------- /lib/utils/hash.ex: -------------------------------------------------------------------------------- 1 | defmodule AeppSDK.Utils.Hash do 2 | @moduledoc """ 3 | Contains hash-related functions. 4 | """ 5 | @hash_bytes 32 6 | @type hash :: binary() 7 | 8 | @doc """ 9 | Calculate the BLAKE2b hash of a binary. 10 | 11 | ## Example 12 | iex> AeppSDK.Utils.Hash.hash(<<0::32>>) 13 | {:ok, 14 | <<17, 218, 109, 31, 118, 29, 223, 155, 219, 76, 157, 110, 83, 3, 235, 212, 31, 15 | 97, 133, 141, 10, 86, 71, 161, 167, 191, 224, 137, 191, 146, 27, 233>>} 16 | """ 17 | @spec hash(binary()) :: {:ok, hash()} | {:error, atom()} 18 | def hash(payload) when is_binary(payload), do: :enacl.generichash(@hash_bytes, payload) 19 | end 20 | -------------------------------------------------------------------------------- /lib/utils/keys.ex: -------------------------------------------------------------------------------- 1 | defmodule AeppSDK.Utils.Keys do 2 | @moduledoc """ 3 | Key generation, handling, encoding and crypto. 4 | """ 5 | alias AeppSDK.Utils.Encoding 6 | alias Argon2.Base, as: Argon2Base 7 | 8 | @prefix_bits 24 9 | @random_salt_bytes 16 10 | @random_nonce_bytes 24 11 | @default_hash_params %{ 12 | m_cost: 16, 13 | parallelism: 1, 14 | t_cost: 3, 15 | format: :raw_hash, 16 | argon2_type: 2 17 | } 18 | # 2^ 16 = 65_536, 2^18 = 262_144 19 | @default_kdf_params %{ 20 | memlimit_kib: 65_536, 21 | opslimit: 3, 22 | salt: "", 23 | parallelism: 1 24 | } 25 | 26 | @typedoc """ 27 | A base58c encoded public key. 28 | """ 29 | @type public_key :: Encoding.base58c() 30 | 31 | @typedoc """ 32 | A hex encoded private key. 33 | """ 34 | @type secret_key :: Encoding.hex() 35 | 36 | @type keypair :: %{public: public_key(), secret: secret_key()} 37 | 38 | @typedoc """ 39 | An arbitrary binary message. 40 | """ 41 | @type message :: binary() 42 | @type signature :: binary() 43 | 44 | @typedoc """ 45 | An arbitrary string password. 46 | """ 47 | @type password :: String.t() 48 | 49 | @doc """ 50 | Generate a Curve25519 keypair 51 | 52 | ## Example 53 | iex> AeppSDK.Utils.Keys.generate_keypair() 54 | %{ 55 | public: "ak_Q9DozPaq7fZ9WnB8SwNxuniaWwUmp1M7HsFTgCdJSsU2kKtC4", 56 | secret: "227bdeedb4c3dd2b554ea6b448ac6788fbe66df1b4f87093a450bba748f296f5348bd07453735393e2ff8c03c65b4593f3bdd94f957a2e7cb314688b53441280" 57 | } 58 | """ 59 | @spec generate_keypair :: keypair() 60 | def generate_keypair do 61 | %{public: binary_public_key, secret: binary_secret_key} = :enacl.sign_keypair() 62 | 63 | %{ 64 | public: public_key_from_binary(binary_public_key), 65 | secret: secret_key_from_binary(binary_secret_key) 66 | } 67 | end 68 | 69 | @doc """ 70 | false 71 | """ 72 | def generate_peer_keypair do 73 | %{public: peer_public_key, secret: peer_secret_key} = :enacl.sign_keypair() 74 | public = :enacl.crypto_sign_ed25519_public_to_curve25519(peer_public_key) 75 | secret = :enacl.crypto_sign_ed25519_secret_to_curve25519(peer_secret_key) 76 | %{public: public, secret: secret} 77 | end 78 | 79 | @spec get_pubkey_from_secret_key(String.t()) :: String.t() 80 | def get_pubkey_from_secret_key(secret_key) do 81 | <<_::size(256), pubkey::size(256)>> = Base.decode16!(secret_key, case: :lower) 82 | 83 | Encoding.prefix_encode_base58c("ak", :binary.encode_unsigned(pubkey)) 84 | end 85 | 86 | @doc """ 87 | Create a new keystore 88 | 89 | ## Example 90 | iex> secret = "227bdeedb4c3dd2b554ea6b448ac6788fbe66df1b4f87093a450bba748f296f5348bd07453735393e2ff8c03c65b4593f3bdd94f957a2e7cb314688b53441280" 91 | iex> AeppSDK.Utils.Keys.new_keystore(secret, "1234") 92 | :ok 93 | """ 94 | @spec new_keystore(String.t(), String.t(), list()) :: :ok | {:error, atom} 95 | def new_keystore(secret_key, password, opts \\ []) do 96 | %{year: year, month: month, day: day, hour: hour, minute: minute, second: second} = 97 | DateTime.utc_now() 98 | 99 | time = "#{year}-#{month}-#{day}-#{hour}-#{minute}-#{second}" 100 | name = Keyword.get(opts, :name, time) 101 | keystore = create_keystore(secret_key, password, name) 102 | json = Poison.encode!(keystore) 103 | {:ok, file} = File.open(name, [:read, :write]) 104 | IO.write(file, json) 105 | File.close(file) 106 | end 107 | 108 | @doc """ 109 | Read the private key from the keystore 110 | 111 | ## Example: 112 | iex> AeppSDK.Utils.Keys.read_keystore("2019-10-25-9-48-48", "1234") 113 | "227bdeedb4c3dd2b554ea6b448ac6788fbe66df1b4f87093a450bba748f296f5348bd07453735393e2ff8c03c65b4593f3bdd94f957a2e7cb314688b53441280" 114 | """ 115 | @spec read_keystore(String.t(), String.t()) :: binary() | {:error, atom()} 116 | def read_keystore(path, password) when is_binary(path) and is_binary(password) do 117 | with {:ok, json_keystore} <- File.read(path), 118 | {:ok, keystore} <- Poison.decode(json_keystore, keys: :atoms!), 119 | params = process_params(keystore), 120 | derived_key = derive_key_argon2(password, params.salt, params.kdf_params), 121 | {:ok, secret} <- 122 | decrypt(params.ciphertext, params.nonce, Base.decode16!(derived_key, case: :lower)) do 123 | Base.encode16(secret, case: :lower) 124 | else 125 | {:error, _} = error -> 126 | error 127 | end 128 | end 129 | 130 | @doc """ 131 | Sign a binary message with the given private key 132 | 133 | ## Example 134 | iex> message = "some message" 135 | iex> secret_key = <<34, 123, 222, 237, 180, 195, 221, 43, 85, 78, 166, 180, 72, 172, 103, 136,251, 230, 109, 241, 180, 248, 112, 147, 164, 80, 187, 167, 72, 242, 150, 245,52, 139, 208, 116, 83, 115, 83, 147, 226, 255, 140, 3, 198, 91, 69, 147, 243,189, 217, 79, 149, 122, 46, 124, 179, 20, 104, 139, 83, 68, 18, 128>> 136 | iex> AeppSDK.Utils.Keys.sign(message, secret_key) 137 | <<94, 26, 208, 168, 230, 154, 158, 226, 188, 217, 155, 170, 157, 33, 100, 22, 138 | 247, 171, 91, 120, 249, 52, 147, 194, 188, 1, 14, 5, 15, 166, 232, 202, 97, 139 | 96, 32, 32, 227, 151, 158, 216, 22, 68, 219, 5, 169, 229, 117, 147, 179, 43, 140 | 172, 211, 243, 171, 234, 254, 210, 119, 105, 248, 154, 19, 202, 7>> 141 | """ 142 | @spec sign(binary(), binary()) :: signature() 143 | def sign(message, secret_key), do: :enacl.sign_detached(message, secret_key) 144 | 145 | @doc """ 146 | Prefixes a network ID string to a binary message and signs it with the given private key 147 | 148 | ## Example 149 | iex> message = "some message" 150 | iex> secret_key = <<34, 123, 222, 237, 180, 195, 221, 43, 85, 78, 166, 180, 72, 172, 103, 136,251, 230, 109, 241, 180, 248, 112, 147, 164, 80, 187, 167, 72, 242, 150, 245,52, 139, 208, 116, 83, 115, 83, 147, 226, 255, 140, 3, 198, 91, 69, 147, 243,189, 217, 79, 149, 122, 46, 124, 179, 20, 104, 139, 83, 68, 18, 128>> 151 | iex> AeppSDK.Utils.Keys.sign(message, secret_key, "ae_uat") 152 | <<15, 246, 136, 55, 63, 30, 144, 154, 249, 161, 243, 93, 52, 0, 218, 22, 43, 153 | 200, 145, 252, 247, 218, 197, 125, 177, 17, 60, 177, 212, 106, 249, 130, 42, 154 | 179, 233, 174, 116, 145, 154, 244, 80, 48, 142, 153, 170, 34, 199, 219, 248, 155 | 107, 115, 155, 254, 69, 37, 68, 68, 1, 174, 95, 102, 10, 6, 14>> 156 | """ 157 | @spec sign(binary(), binary(), String.t()) :: signature() 158 | def sign(message, secret_key, network_id), 159 | do: :enacl.sign_detached(network_id <> message, secret_key) 160 | 161 | @doc """ 162 | Verify that a message has been signed by a private key corresponding to the given public key 163 | 164 | ## Example 165 | iex> signature = <<94, 26, 208, 168, 230, 154, 158, 226, 188, 217, 155, 170, 157, 33, 100, 22, 247, 171, 91, 120, 249, 52, 147, 194, 188, 1, 14, 5, 15, 166, 232, 202, 97, 96, 32, 32, 227, 151, 158, 216, 22, 68, 219, 5, 169, 229, 117, 147, 179, 43, 172, 211, 243, 171, 234, 254, 210, 119, 105, 248, 154, 19, 202, 7>> 166 | iex> message = "some message" 167 | iex> public_key = <<52, 139, 208, 116, 83, 115, 83, 147, 226, 255, 140, 3, 198, 91, 69, 147, 243, 189, 217, 79, 149, 122, 46, 124, 179, 20, 104, 139, 83, 68, 18, 128>> 168 | iex> AeppSDK.Utils.Keys.verify(signature, message, public_key) 169 | {:ok, "some message"} 170 | """ 171 | @spec verify(message(), signature(), binary()) :: {:ok, message()} | {:error, atom()} 172 | def verify(signature, message, public_key), 173 | do: :enacl.sign_verify_detached(signature, message, public_key) 174 | 175 | @doc """ 176 | Save a keypair at a given path with the specified file name. The keys are encrypted with the password and saved as separate files - `name` for the private and `{ 177 | name 178 | }.pub` for the public key 179 | 180 | ## Example 181 | iex> keypair = AeppSDK.Utils.Keys.generate_keypair() 182 | iex> password = "some password" 183 | iex> path = "./keys" 184 | iex> name = "key" 185 | iex> AeppSDK.Utils.Keys.save_keypair(keypair, password, path, name) 186 | :ok 187 | """ 188 | @spec save_keypair(keypair(), password(), String.t(), String.t()) :: :ok | {:error, String.t()} 189 | def save_keypair(%{public: public_key, secret: secret_key}, password, path, name) do 190 | binary_public_key = public_key_to_binary(public_key) 191 | binary_secret_key = secret_key_to_binary(secret_key) 192 | 193 | public_key_path = Path.join(path, "#{name}.pub") 194 | 195 | secret_key_path = Path.join(path, name) 196 | 197 | case mkdir(path) do 198 | :ok -> 199 | case {File.write(public_key_path, encrypt_key(binary_public_key, password)), 200 | File.write(secret_key_path, encrypt_key(binary_secret_key, password))} do 201 | {:ok, :ok} -> 202 | :ok 203 | 204 | {{:error, public_key_reason}, {:error, secret_key_reason}} -> 205 | {:error, 206 | "Couldn't write public (#{Atom.to_string(public_key_reason)}) or private key (#{ 207 | Atom.to_string(secret_key_reason) 208 | } in #{path})"} 209 | 210 | {{:error, reason}, :ok} -> 211 | {:error, "Couldn't write public key in #{path}: #{Atom.to_string(reason)}"} 212 | 213 | {:ok, {:error, reason}} -> 214 | {:error, "Couldn't write private key in #{path}: #{Atom.to_string(reason)}"} 215 | end 216 | 217 | {:error, reason} -> 218 | {:error, "Couldn't create directory #{path}: #{Atom.to_string(reason)}"} 219 | end 220 | end 221 | 222 | @doc """ 223 | Attempt to read a keypair from a given path with the specified file name. If found, the keys will be decrypted with the password 224 | 225 | ## Example 226 | iex> password = "some password" 227 | iex> path = "./keys" 228 | iex> name = "key" 229 | iex> AeppSDK.Utils.Keys.read_keypair(password, path, name) 230 | {:ok, 231 | %{ 232 | public: "ak_2vTCdFVAvgkYUDiVpydmByybqSYZHEB189QcfjmdcxRef2W2eb", 233 | secret: "f9cebe874d90626bfcea1093e72f22e500a92e95052b88aaebd5d30346132cb1fd1096207d3e887091e3c11a953c0238be2f9d737e2076bf89866bb786bc0fbf" 234 | }} 235 | """ 236 | @spec read_keypair(password(), String.t(), String.t()) :: 237 | {:ok, keypair()} | {:error, String.t()} 238 | def read_keypair(password, path, name) do 239 | public_key_read = path |> Path.join("#{name}.pub") |> File.read() 240 | secret_key_read = path |> Path.join("#{name}") |> File.read() 241 | 242 | case {public_key_read, secret_key_read} do 243 | {{:ok, encrypted_public_key}, {:ok, encrypted_secret_key}} -> 244 | public_key = encrypted_public_key |> decrypt_key(password) |> public_key_from_binary() 245 | secret_key = encrypted_secret_key |> decrypt_key(password) |> secret_key_from_binary() 246 | 247 | {:ok, %{public: public_key, secret: secret_key}} 248 | 249 | {{:error, public_key_reason}, {:error, secret_key_reason}} -> 250 | {:error, 251 | "Couldn't read public (reason: #{Atom.to_string(public_key_reason)}) or private key (reason: #{ 252 | Atom.to_string(secret_key_reason) 253 | }) from #{path}"} 254 | 255 | {{:error, reason}, {:ok, _}} -> 256 | {:error, "Couldn't read public key from #{path}: #{Atom.to_string(reason)} "} 257 | 258 | {{:ok, _}, {:error, reason}} -> 259 | {:error, "Couldn't read private key from #{path}: #{Atom.to_string(reason)}"} 260 | end 261 | end 262 | 263 | @doc """ 264 | Convert a base58check public key string to binary 265 | 266 | ## Example 267 | iex> public_key = "ak_2vTCdFVAvgkYUDiVpydmByybqSYZHEB189QcfjmdcxRef2W2eb" 268 | iex> AeppSDK.Utils.Keys.public_key_to_binary(public_key) 269 | <<253, 16, 150, 32, 125, 62, 136, 112, 145, 227, 193, 26, 149, 60, 2, 56, 190, 47, 157, 115, 126, 32, 118, 191, 137, 134, 107, 183, 134, 188, 15, 191>> 270 | ``` 271 | """ 272 | @spec public_key_to_binary(public_key()) :: binary() 273 | def public_key_to_binary(public_key), do: Encoding.prefix_decode_base58c(public_key) 274 | 275 | @doc """ 276 | Convert a base58check public key string to tuple of prefix and binary 277 | 278 | ## Example 279 | iex> public_key = "ak_2vTCdFVAvgkYUDiVpydmByybqSYZHEB189QcfjmdcxRef2W2eb" 280 | iex> AeppSDK.Utils.Keys.public_key_to_binary(public_key, :with_prefix) 281 | {"ak_", 282 | <<253, 16, 150, 32, 125, 62, 136, 112, 145, 227, 193, 26, 149, 60, 2, 56, 190, 283 | 47, 157, 115, 126, 32, 118, 191, 137, 134, 107, 183, 134, 188, 15, 191>>} 284 | ``` 285 | """ 286 | @spec public_key_to_binary(public_key(), atom()) :: tuple() 287 | def public_key_to_binary(<> = public_key, :with_prefix), 288 | do: {<>, Encoding.prefix_decode_base58c(public_key)} 289 | 290 | @doc """ 291 | Convert a binary public key to a base58check string 292 | 293 | ## Example 294 | iex> binary_public_key = <<253, 16, 150, 32, 125, 62, 136, 112, 145, 227, 193, 26, 149, 60, 2, 56, 190, 47, 157, 115, 126, 32, 118, 191, 137, 134, 107, 183, 134, 188, 15, 191>> 295 | iex> AeppSDK.Utils.Keys.public_key_from_binary(binary_public_key) 296 | "ak_2vTCdFVAvgkYUDiVpydmByybqSYZHEB189QcfjmdcxRef2W2eb" 297 | """ 298 | @spec public_key_from_binary(binary()) :: public_key() 299 | def public_key_from_binary(binary_public_key), 300 | do: Encoding.prefix_encode_base58c("ak", binary_public_key) 301 | 302 | @doc """ 303 | Convert a hex string private key to binary 304 | 305 | ## Example 306 | iex> secret_key = "f9cebe874d90626bfcea1093e72f22e500a92e95052b88aaebd5d30346132cb1fd1096207d3e887091e3c11a953c0238be2f9d737e2076bf89866bb786bc0fbf" 307 | iex> AeppSDK.Utils.Keys.secret_key_to_binary(secret_key) 308 | <<249, 206, 190, 135, 77, 144, 98, 107, 252, 234, 16, 147, 231, 47, 34, 229, 0, 309 | 169, 46, 149, 5, 43, 136, 170, 235, 213, 211, 3, 70, 19, 44, 177, 253, 16, 310 | 150, 32, 125, 62, 136, 112, 145, 227, 193, 26, 149, 60, 2, 56, 190, 47, 157, 311 | 115, 126, 32, 118, 191, 137, 134, 107, 183, 134, 188, 15, 191>> 312 | """ 313 | @spec secret_key_to_binary(secret_key()) :: binary() 314 | def secret_key_to_binary(secret_key) do 315 | Base.decode16!(secret_key, case: :lower) 316 | end 317 | 318 | @doc """ 319 | Convert a binary private key to a hex string 320 | 321 | ## Example 322 | iex> binary_secret_key = <<249, 206, 190, 135, 77, 144, 98, 107, 252, 234, 16, 147, 231, 47, 34, 229, 0, 169, 46, 149, 5, 43, 136, 170, 235, 213, 211, 3, 70, 19, 44, 177, 253, 16, 150, 32, 125, 62, 136, 112, 145, 227, 193, 26, 149, 60, 2, 56, 190, 47, 157, 115, 126, 32, 118, 191, 137, 134, 107, 183, 134, 188, 15, 191>> 323 | iex> AeppSDK.Utils.Keys.secret_key_from_binary(binary_secret_key) 324 | "f9cebe874d90626bfcea1093e72f22e500a92e95052b88aaebd5d30346132cb1fd1096207d3e887091e3c11a953c0238be2f9d737e2076bf89866bb786bc0fbf" 325 | """ 326 | @spec secret_key_from_binary(binary()) :: secret_key() 327 | def secret_key_from_binary(binary_secret_key) do 328 | Base.encode16(binary_secret_key, case: :lower) 329 | end 330 | 331 | defp process_params(%{ 332 | crypto: %{ 333 | cipher_params: %{nonce: nonce}, 334 | ciphertext: ciphertext, 335 | kdf: kdf, 336 | kdf_params: %{ 337 | memlimit_kib: memlimit, 338 | opslimit: opslimit, 339 | parallelism: parallelism, 340 | salt: salt 341 | } 342 | } 343 | }) do 344 | kdf_algorithm = process_param(kdf) 345 | m_cost = memlimit |> :math.log2() |> round 346 | t_cost = opslimit 347 | decoded_salt = Base.decode16!(salt, case: :lower) 348 | decoded_ciphertext = Base.decode16!(ciphertext, case: :lower) 349 | decoded_nonce = Base.decode16!(nonce, case: :lower) 350 | 351 | %{ 352 | kdf_params: %{ 353 | m_cost: m_cost, 354 | t_cost: t_cost, 355 | parallelism: parallelism, 356 | argon2_type: kdf_algorithm 357 | }, 358 | salt: decoded_salt, 359 | ciphertext: decoded_ciphertext, 360 | nonce: decoded_nonce 361 | } 362 | end 363 | 364 | defp process_param("argon2d") do 365 | 0 366 | end 367 | 368 | defp process_param("argon2i") do 369 | 1 370 | end 371 | 372 | defp process_param("argon2id") do 373 | 2 374 | end 375 | 376 | defp encrypt(plaintext, nonce, derived_key) do 377 | :enacl.secretbox(plaintext, nonce, derived_key) 378 | end 379 | 380 | defp decrypt(ciphertext, nonce, derived_key) 381 | when is_binary(ciphertext) and byte_size(nonce) == 24 and byte_size(derived_key) == 32 do 382 | :enacl.secretbox_open(ciphertext, nonce, derived_key) 383 | end 384 | 385 | defp decrypt(_, _, _) do 386 | {:error, "#{__MODULE__}: Invalid data"} 387 | end 388 | 389 | defp create_keystore(secret_key, password, name) 390 | when is_binary(secret_key) and is_binary(password) do 391 | salt = :enacl.randombytes(@random_salt_bytes) 392 | nonce = :enacl.randombytes(@random_nonce_bytes) 393 | derived_key = derive_key_argon2(password, salt, @default_hash_params) 394 | 395 | encrypted_key = 396 | encrypt(secret_key_to_binary(secret_key), nonce, Base.decode16!(derived_key, case: :lower)) 397 | 398 | %{ 399 | public_key: get_pubkey_from_secret_key(secret_key), 400 | crypto: %{ 401 | secret_type: "ed25519", 402 | symmetric_alg: "xsalsa20-poly1305", 403 | ciphertext: Base.encode16(encrypted_key, case: :lower), 404 | cipher_params: %{ 405 | nonce: Base.encode16(nonce, case: :lower) 406 | }, 407 | kdf: "argon2id", 408 | kdf_params: %{@default_kdf_params | salt: Base.encode16(salt, case: :lower)} 409 | }, 410 | id: UUID.uuid4(), 411 | name: name, 412 | version: 1 413 | } 414 | end 415 | 416 | defp derive_key_argon2( 417 | password, 418 | salt, 419 | %{ 420 | memlimit_kib: memlimit_kib, 421 | opslimit: opslimit, 422 | salt: salt, 423 | parallelism: parallelism 424 | } 425 | ) do 426 | derive_key_argon2(password, salt, %{ 427 | m_cost: memlimit_kib |> :math.log2() |> round(), 428 | parallelism: parallelism, 429 | t_cost: opslimit 430 | }) 431 | end 432 | 433 | defp derive_key_argon2(password, salt, kdf_params) do 434 | processed_kdf_params = 435 | for kdf_param <- Map.keys(@default_hash_params), reduce: %{} do 436 | acc -> 437 | Map.put( 438 | acc, 439 | kdf_param, 440 | Map.get(kdf_params, kdf_param, Map.get(@default_hash_params, kdf_param)) 441 | ) 442 | end 443 | 444 | Argon2Base.hash_password(password, salt, Enum.into(processed_kdf_params, [])) 445 | end 446 | 447 | defp mkdir(path) do 448 | if File.exists?(path) do 449 | :ok 450 | else 451 | File.mkdir(path) 452 | end 453 | end 454 | 455 | defp encrypt_key(key, password), do: :crypto.block_encrypt(:aes_ecb, hash(password), key) 456 | 457 | defp decrypt_key(encrypted, password), 458 | do: :crypto.block_decrypt(:aes_ecb, hash(password), encrypted) 459 | 460 | defp hash(binary), do: :crypto.hash(:sha256, binary) 461 | end 462 | -------------------------------------------------------------------------------- /lib/utils/serialization_utils.ex: -------------------------------------------------------------------------------- 1 | defmodule AeppSDK.Utils.SerializationUtils do 2 | @moduledoc false 3 | 4 | alias AeppSDK.Utils.{Encoding, Keys} 5 | 6 | alias AeternityNode.Model.{ 7 | ChannelCloseMutualTx, 8 | ChannelCloseSoloTx, 9 | ChannelCreateTx, 10 | ChannelDepositTx, 11 | ChannelForceProgressTx, 12 | ChannelSettleTx, 13 | ChannelSlashTx, 14 | ChannelSnapshotSoloTx, 15 | ChannelWithdrawTx, 16 | ContractCallTx, 17 | ContractCreateTx, 18 | NameClaimTx, 19 | NamePreclaimTx, 20 | NameRevokeTx, 21 | NameTransferTx, 22 | NameUpdateTx, 23 | OracleExtendTx, 24 | OracleQueryTx, 25 | OracleRegisterTx, 26 | OracleRespondTx, 27 | RelativeTtl, 28 | SpendTx, 29 | Ttl 30 | } 31 | 32 | @spec process_tx_fields(struct()) :: tuple() 33 | def process_tx_fields(%SpendTx{ 34 | recipient_id: tx_recipient_id, 35 | amount: amount, 36 | fee: fee, 37 | ttl: ttl, 38 | sender_id: tx_sender_id, 39 | nonce: nonce, 40 | payload: payload 41 | }) do 42 | sender_id = proccess_id_to_record(tx_sender_id) 43 | recipient_id = proccess_id_to_record(tx_recipient_id) 44 | 45 | {:ok, 46 | [ 47 | sender_id, 48 | recipient_id, 49 | amount, 50 | fee, 51 | ttl, 52 | nonce, 53 | payload 54 | ], :spend_tx} 55 | end 56 | 57 | def process_tx_fields(%OracleRegisterTx{ 58 | query_format: query_format, 59 | response_format: response_format, 60 | query_fee: query_fee, 61 | oracle_ttl: %Ttl{type: type, value: value}, 62 | account_id: tx_account_id, 63 | nonce: nonce, 64 | fee: fee, 65 | ttl: ttl, 66 | abi_version: abi_version 67 | }) do 68 | account_id = proccess_id_to_record(tx_account_id) 69 | 70 | ttl_type = 71 | case type do 72 | :absolute -> 1 73 | :relative -> 0 74 | end 75 | 76 | {:ok, 77 | [ 78 | account_id, 79 | nonce, 80 | query_format, 81 | response_format, 82 | query_fee, 83 | ttl_type, 84 | value, 85 | fee, 86 | ttl, 87 | abi_version 88 | ], :oracle_register_tx} 89 | end 90 | 91 | def process_tx_fields(%OracleRespondTx{ 92 | query_id: query_id, 93 | response: response, 94 | response_ttl: %RelativeTtl{type: _type, value: value}, 95 | fee: fee, 96 | ttl: ttl, 97 | oracle_id: tx_oracle_id, 98 | nonce: nonce 99 | }) do 100 | oracle_id = proccess_id_to_record(tx_oracle_id) 101 | binary_query_id = Encoding.prefix_decode_base58c(query_id) 102 | 103 | {:ok, 104 | [ 105 | oracle_id, 106 | nonce, 107 | binary_query_id, 108 | response, 109 | 0, 110 | value, 111 | fee, 112 | ttl 113 | ], :oracle_response_tx} 114 | end 115 | 116 | def process_tx_fields(%OracleQueryTx{ 117 | oracle_id: tx_oracle_id, 118 | query: query, 119 | query_fee: query_fee, 120 | query_ttl: %Ttl{type: query_type, value: query_value}, 121 | response_ttl: %RelativeTtl{type: _response_type, value: response_value}, 122 | fee: fee, 123 | ttl: ttl, 124 | sender_id: tx_sender_id, 125 | nonce: nonce 126 | }) do 127 | sender_id = proccess_id_to_record(tx_sender_id) 128 | oracle_id = proccess_id_to_record(tx_oracle_id) 129 | 130 | query_ttl_type = 131 | case query_type do 132 | :absolute -> 1 133 | :relative -> 0 134 | end 135 | 136 | {:ok, 137 | [ 138 | sender_id, 139 | nonce, 140 | oracle_id, 141 | query, 142 | query_fee, 143 | query_ttl_type, 144 | query_value, 145 | 0, 146 | response_value, 147 | fee, 148 | ttl 149 | ], :oracle_query_tx} 150 | end 151 | 152 | def process_tx_fields(%OracleExtendTx{ 153 | fee: fee, 154 | oracle_ttl: %RelativeTtl{type: _type, value: value}, 155 | oracle_id: tx_oracle_id, 156 | nonce: nonce, 157 | ttl: ttl 158 | }) do 159 | oracle_id = proccess_id_to_record(tx_oracle_id) 160 | 161 | {:ok, 162 | [ 163 | oracle_id, 164 | nonce, 165 | 0, 166 | value, 167 | fee, 168 | ttl 169 | ], :oracle_extend_tx} 170 | end 171 | 172 | def process_tx_fields(%NameClaimTx{ 173 | name: name, 174 | name_salt: name_salt, 175 | name_fee: name_fee, 176 | fee: fee, 177 | ttl: ttl, 178 | account_id: tx_account_id, 179 | nonce: nonce 180 | }) do 181 | account_id = proccess_id_to_record(tx_account_id) 182 | 183 | {:ok, 184 | [ 185 | account_id, 186 | nonce, 187 | name, 188 | name_salt, 189 | name_fee, 190 | fee, 191 | ttl 192 | ], :name_claim_tx} 193 | end 194 | 195 | def process_tx_fields(%NamePreclaimTx{ 196 | commitment_id: tx_commitment_id, 197 | fee: fee, 198 | ttl: ttl, 199 | account_id: tx_account_id, 200 | nonce: nonce 201 | }) do 202 | account_id = proccess_id_to_record(tx_account_id) 203 | commitment_id = proccess_id_to_record(tx_commitment_id) 204 | 205 | {:ok, 206 | [ 207 | account_id, 208 | nonce, 209 | commitment_id, 210 | fee, 211 | ttl 212 | ], :name_preclaim_tx} 213 | end 214 | 215 | def process_tx_fields(%NameUpdateTx{ 216 | name_id: tx_name_id, 217 | name_ttl: name_ttl, 218 | pointers: pointers, 219 | client_ttl: client_ttl, 220 | fee: fee, 221 | ttl: ttl, 222 | account_id: tx_account_id, 223 | nonce: nonce 224 | }) do 225 | account_id = proccess_id_to_record(tx_account_id) 226 | name_id = proccess_id_to_record(tx_name_id) 227 | 228 | {:ok, 229 | [ 230 | account_id, 231 | nonce, 232 | name_id, 233 | name_ttl, 234 | pointers, 235 | client_ttl, 236 | fee, 237 | ttl 238 | ], :name_update_tx} 239 | end 240 | 241 | def process_tx_fields(%NameRevokeTx{ 242 | name_id: tx_name_id, 243 | fee: fee, 244 | ttl: ttl, 245 | account_id: tx_account_id, 246 | nonce: nonce 247 | }) do 248 | account_id = proccess_id_to_record(tx_account_id) 249 | name_id = proccess_id_to_record(tx_name_id) 250 | 251 | {:ok, 252 | [ 253 | account_id, 254 | nonce, 255 | name_id, 256 | fee, 257 | ttl 258 | ], :name_revoke_tx} 259 | end 260 | 261 | def process_tx_fields(%NameTransferTx{ 262 | name_id: tx_name_id, 263 | recipient_id: tx_recipient_id, 264 | fee: fee, 265 | ttl: ttl, 266 | account_id: tx_account_id, 267 | nonce: nonce 268 | }) do 269 | account_id = proccess_id_to_record(tx_account_id) 270 | name_id = proccess_id_to_record(tx_name_id) 271 | recipient_id = proccess_id_to_record(tx_recipient_id) 272 | 273 | {:ok, 274 | [ 275 | account_id, 276 | nonce, 277 | name_id, 278 | recipient_id, 279 | fee, 280 | ttl 281 | ], :name_transfer_tx} 282 | end 283 | 284 | def process_tx_fields(%ContractCreateTx{ 285 | owner_id: tx_owner_id, 286 | nonce: nonce, 287 | code: code, 288 | abi_version: ct_version, 289 | deposit: deposit, 290 | amount: amount, 291 | gas: gas, 292 | gas_price: gas_price, 293 | fee: fee, 294 | ttl: ttl, 295 | call_data: call_data 296 | }) do 297 | owner_id = proccess_id_to_record(tx_owner_id) 298 | 299 | {:ok, 300 | [ 301 | owner_id, 302 | nonce, 303 | code, 304 | ct_version, 305 | fee, 306 | ttl, 307 | deposit, 308 | amount, 309 | gas, 310 | gas_price, 311 | call_data 312 | ], :contract_create_tx} 313 | end 314 | 315 | def process_tx_fields(%ContractCallTx{ 316 | caller_id: tx_caller_id, 317 | nonce: nonce, 318 | contract_id: tx_contract_id, 319 | abi_version: abi_version, 320 | fee: fee, 321 | ttl: ttl, 322 | amount: amount, 323 | gas: gas, 324 | gas_price: gas_price, 325 | call_data: call_data 326 | }) do 327 | caller_id = proccess_id_to_record(tx_caller_id) 328 | contract_id = proccess_id_to_record(tx_contract_id) 329 | 330 | {:ok, 331 | [ 332 | caller_id, 333 | nonce, 334 | contract_id, 335 | abi_version, 336 | fee, 337 | ttl, 338 | amount, 339 | gas, 340 | gas_price, 341 | call_data 342 | ], :contract_call_tx} 343 | end 344 | 345 | def process_tx_fields(%ChannelCreateTx{ 346 | initiator_id: initiator, 347 | initiator_amount: initiator_amount, 348 | responder_id: responder, 349 | responder_amount: responder_amount, 350 | channel_reserve: channel_reserve, 351 | lock_period: lock_period, 352 | ttl: ttl, 353 | fee: fee, 354 | delegate_ids: delegate_ids, 355 | state_hash: <<"st_", state_hash::binary>>, 356 | nonce: nonce 357 | }) do 358 | decoded_state_hash = Encoding.decode_base58c(state_hash) 359 | initiator_id = proccess_id_to_record(initiator) 360 | responder_id = proccess_id_to_record(responder) 361 | 362 | list_delegate_ids = 363 | for id <- delegate_ids do 364 | proccess_id_to_record(id) 365 | end 366 | 367 | {:ok, 368 | [ 369 | initiator_id, 370 | initiator_amount, 371 | responder_id, 372 | responder_amount, 373 | channel_reserve, 374 | lock_period, 375 | ttl, 376 | fee, 377 | list_delegate_ids, 378 | decoded_state_hash, 379 | nonce 380 | ], :channel_create_tx} 381 | end 382 | 383 | def process_tx_fields(%ChannelCloseMutualTx{ 384 | channel_id: channel, 385 | fee: fee, 386 | from_id: from, 387 | initiator_amount_final: initiator_amount_final, 388 | nonce: nonce, 389 | responder_amount_final: responder_amount_final, 390 | ttl: ttl 391 | }) do 392 | channel_id = proccess_id_to_record(channel) 393 | from_id = proccess_id_to_record(from) 394 | 395 | {:ok, 396 | [ 397 | channel_id, 398 | from_id, 399 | initiator_amount_final, 400 | responder_amount_final, 401 | ttl, 402 | fee, 403 | nonce 404 | ], :channel_close_mutual_tx} 405 | end 406 | 407 | def process_tx_fields(%ChannelCloseSoloTx{ 408 | channel_id: channel, 409 | fee: fee, 410 | from_id: from, 411 | nonce: nonce, 412 | payload: payload, 413 | poi: poi, 414 | ttl: ttl 415 | }) do 416 | channel_id = proccess_id_to_record(channel) 417 | from_id = proccess_id_to_record(from) 418 | 419 | {:ok, 420 | [ 421 | channel_id, 422 | from_id, 423 | payload, 424 | poi, 425 | ttl, 426 | fee, 427 | nonce 428 | ], :channel_close_solo_tx} 429 | end 430 | 431 | def process_tx_fields(%ChannelDepositTx{ 432 | amount: amount, 433 | channel_id: channel, 434 | fee: fee, 435 | from_id: from, 436 | nonce: nonce, 437 | round: round, 438 | state_hash: <<"st_", state_hash::binary>>, 439 | ttl: ttl 440 | }) do 441 | decoded_state_hash = Encoding.decode_base58c(state_hash) 442 | channel_id = proccess_id_to_record(channel) 443 | from_id = proccess_id_to_record(from) 444 | 445 | {:ok, 446 | [ 447 | channel_id, 448 | from_id, 449 | amount, 450 | ttl, 451 | fee, 452 | decoded_state_hash, 453 | round, 454 | nonce 455 | ], :channel_deposit_tx} 456 | end 457 | 458 | def process_tx_fields(%ChannelForceProgressTx{ 459 | channel_id: channel, 460 | fee: fee, 461 | from_id: from, 462 | nonce: nonce, 463 | offchain_trees: offchain_trees, 464 | payload: payload, 465 | round: round, 466 | state_hash: <<"st_", state_hash::binary>>, 467 | ttl: ttl, 468 | update: update 469 | }) do 470 | decoded_state_hash = Encoding.decode_base58c(state_hash) 471 | channel_id = proccess_id_to_record(channel) 472 | from_id = proccess_id_to_record(from) 473 | 474 | {:ok, 475 | [ 476 | channel_id, 477 | from_id, 478 | payload, 479 | round, 480 | update, 481 | decoded_state_hash, 482 | offchain_trees, 483 | ttl, 484 | fee, 485 | nonce 486 | ], :channel_force_progress_tx} 487 | end 488 | 489 | def process_tx_fields(%ChannelSettleTx{ 490 | channel_id: channel, 491 | fee: fee, 492 | from_id: from, 493 | initiator_amount_final: initiator_amount_final, 494 | nonce: nonce, 495 | responder_amount_final: responder_amount_final, 496 | ttl: ttl 497 | }) do 498 | channel_id = proccess_id_to_record(channel) 499 | from_id = proccess_id_to_record(from) 500 | 501 | {:ok, [channel_id, from_id, initiator_amount_final, responder_amount_final, ttl, fee, nonce], 502 | :channel_settle_tx} 503 | end 504 | 505 | def process_tx_fields(%ChannelSlashTx{ 506 | channel_id: channel, 507 | fee: fee, 508 | from_id: from, 509 | nonce: nonce, 510 | payload: payload, 511 | poi: poi, 512 | ttl: ttl 513 | }) do 514 | channel_id = proccess_id_to_record(channel) 515 | from_id = proccess_id_to_record(from) 516 | 517 | {:ok, 518 | [ 519 | channel_id, 520 | from_id, 521 | payload, 522 | poi, 523 | ttl, 524 | fee, 525 | nonce 526 | ], :channel_slash_tx} 527 | end 528 | 529 | def process_tx_fields(%ChannelSnapshotSoloTx{ 530 | channel_id: channel, 531 | from_id: from, 532 | payload: payload, 533 | ttl: ttl, 534 | fee: fee, 535 | nonce: nonce 536 | }) do 537 | channel_id = proccess_id_to_record(channel) 538 | from_id = proccess_id_to_record(from) 539 | 540 | {:ok, [channel_id, from_id, payload, ttl, fee, nonce], :channel_snapshot_solo_tx} 541 | end 542 | 543 | def process_tx_fields(%ChannelWithdrawTx{ 544 | channel_id: channel, 545 | to_id: to, 546 | amount: amount, 547 | ttl: ttl, 548 | fee: fee, 549 | nonce: nonce, 550 | state_hash: <<"st_", state_hash::binary>>, 551 | round: round 552 | }) do 553 | decoded_state_hash = Encoding.decode_base58c(state_hash) 554 | channel_id = proccess_id_to_record(channel) 555 | to_id = proccess_id_to_record(to) 556 | 557 | {:ok, [channel_id, to_id, amount, ttl, fee, decoded_state_hash, round, nonce], 558 | :channel_withdraw_tx} 559 | end 560 | 561 | def process_tx_fields(%{ 562 | owner_id: owner_id, 563 | nonce: nonce, 564 | code: code, 565 | auth_fun: auth_fun, 566 | ct_version: ct_version, 567 | fee: fee, 568 | ttl: ttl, 569 | gas: gas, 570 | gas_price: gas_price, 571 | call_data: call_data 572 | }) do 573 | owner_id_record = proccess_id_to_record(owner_id) 574 | 575 | {:ok, 576 | [ 577 | owner_id_record, 578 | nonce, 579 | code, 580 | auth_fun, 581 | ct_version, 582 | fee, 583 | ttl, 584 | gas, 585 | gas_price, 586 | call_data 587 | ], :ga_attach_tx} 588 | end 589 | 590 | def process_tx_fields(%{ 591 | ga_id: ga_id, 592 | auth_data: auth_data, 593 | abi_version: abi_version, 594 | fee: fee, 595 | gas: gas, 596 | gas_price: gas_price, 597 | ttl: ttl, 598 | tx: tx 599 | }) do 600 | ga_id_record = proccess_id_to_record(ga_id) 601 | 602 | {:ok, 603 | [ 604 | ga_id_record, 605 | auth_data, 606 | abi_version, 607 | fee, 608 | gas, 609 | gas_price, 610 | ttl, 611 | tx 612 | ], :ga_meta_tx} 613 | end 614 | 615 | def process_tx_fields(%{ 616 | channel_id: channel_id, 617 | round: round, 618 | state_hash: <<"st_", state_hash::binary>>, 619 | version: 1, 620 | updates: updates 621 | }) do 622 | channel_id_record = proccess_id_to_record(channel_id) 623 | decoded_state_hash = Encoding.decode_base58c(state_hash) 624 | 625 | {:ok, 626 | [ 627 | channel_id_record, 628 | round, 629 | updates, 630 | decoded_state_hash 631 | ], :channel_offchain_tx} 632 | end 633 | 634 | def process_tx_fields(%{ 635 | channel_id: channel_id, 636 | round: round, 637 | state_hash: <<"st_", state_hash::binary>>, 638 | version: 2 639 | }) do 640 | channel_id_record = proccess_id_to_record(channel_id) 641 | decoded_state_hash = Encoding.decode_base58c(state_hash) 642 | 643 | {:ok, 644 | [ 645 | channel_id_record, 646 | round, 647 | decoded_state_hash 648 | ], :channel_offchain_tx_no_updates} 649 | end 650 | 651 | def process_tx_fields(tx) do 652 | {:error, "Unknown or invalid tx: #{inspect(tx)}"} 653 | end 654 | 655 | def ttl_type_for_client(type) do 656 | case type do 657 | 0 -> 658 | :relative 659 | 660 | 1 -> 661 | :absolute 662 | end 663 | end 664 | 665 | defp proccess_id_to_record(id) when is_binary(id) do 666 | {type, binary_data} = 667 | id 668 | |> Keys.public_key_to_binary(:with_prefix) 669 | 670 | id = 671 | case type do 672 | "ak_" -> :account 673 | "ok_" -> :oracle 674 | "ct_" -> :contract 675 | "nm_" -> :name 676 | "cm_" -> :commitment 677 | "ch_" -> :channel 678 | end 679 | 680 | {:id, id, binary_data} 681 | end 682 | end 683 | -------------------------------------------------------------------------------- /lib/utils/transaction.ex: -------------------------------------------------------------------------------- 1 | defmodule AeppSDK.Utils.Transaction do 2 | @moduledoc """ 3 | Transaction AeppSDK.Utils. 4 | 5 | In order for its functions to be used, a client must be defined first. 6 | Client example can be found at: `AeppSDK.Client.new/4`. 7 | """ 8 | alias AeppSDK.Account, as: AccountApi 9 | alias AeppSDK.{Chain, Client, Contract, GeneralizedAccount} 10 | alias AeppSDK.Utils.{Encoding, Governance, Keys, Serialization} 11 | 12 | alias AeternityNode.Api.Transaction, as: TransactionApi 13 | 14 | alias AeternityNode.Model.{ 15 | Account, 16 | ChannelCloseMutualTx, 17 | ChannelCloseSoloTx, 18 | ChannelCreateTx, 19 | ChannelDepositTx, 20 | ChannelForceProgressTx, 21 | ChannelSettleTx, 22 | ChannelSlashTx, 23 | ChannelSnapshotSoloTx, 24 | ChannelWithdrawTx, 25 | ContractCallObject, 26 | ContractCallTx, 27 | ContractCreateTx, 28 | Error, 29 | GaObject, 30 | GenericSignedTx, 31 | NameClaimTx, 32 | NamePreclaimTx, 33 | NameRevokeTx, 34 | NameTransferTx, 35 | NameUpdateTx, 36 | OracleExtendTx, 37 | OracleQueryTx, 38 | OracleRegisterTx, 39 | OracleRespondTx, 40 | PostTxResponse, 41 | SpendTx, 42 | Tx, 43 | TxInfoObject 44 | } 45 | 46 | alias Tesla.Env 47 | 48 | @struct_type [ 49 | SpendTx, 50 | OracleRegisterTx, 51 | OracleQueryTx, 52 | OracleRespondTx, 53 | OracleExtendTx, 54 | NamePreclaimTx, 55 | NameClaimTx, 56 | NameTransferTx, 57 | NameRevokeTx, 58 | NameUpdateTx, 59 | ContractCallTx, 60 | ContractCreateTx, 61 | ChannelCreateTx, 62 | ChannelCloseMutualTx, 63 | ChannelCloseSoloTx, 64 | ChannelDepositTx, 65 | ChannelForceProgressTx, 66 | ChannelSettleTx, 67 | ChannelSlashTx, 68 | ChannelSnapshotSoloTx, 69 | ChannelWithdrawTx 70 | ] 71 | 72 | @network_id_list ["ae_mainnet", "ae_uat"] 73 | 74 | @await_attempts 75 75 | @await_attempt_interval 200 76 | @default_ttl 0 77 | @dummy_fee 0 78 | @default_payload "" 79 | @fee_calculation_times 5 80 | 81 | @type tx_types :: 82 | SpendTx.t() 83 | | OracleRegisterTx.t() 84 | | OracleQueryTx.t() 85 | | OracleRespondTx.t() 86 | | OracleExtendTx.t() 87 | | NamePreclaimTx.t() 88 | | NameClaimTx.t() 89 | | NameTransferTx.t() 90 | | NameRevokeTx.t() 91 | | NameUpdateTx.t() 92 | | ContractCallTx.t() 93 | | ContractCreateTx.t() 94 | 95 | @spec default_ttl :: non_neg_integer() 96 | def default_ttl, do: @default_ttl 97 | 98 | @spec default_payload :: String.t() 99 | def default_payload, do: @default_payload 100 | 101 | @spec dummy_fee :: non_neg_integer() 102 | def dummy_fee, do: @dummy_fee 103 | 104 | @spec default_await_attempts :: non_neg_integer() 105 | def default_await_attempts, do: @await_attempts 106 | 107 | @spec default_await_attempt_interval :: non_neg_integer() 108 | def default_await_attempt_interval, do: @await_attempt_interval 109 | 110 | @spec default_fee_calculation_times :: non_neg_integer() 111 | def default_fee_calculation_times, do: @fee_calculation_times 112 | 113 | @doc """ 114 | Serialize the list of fields to RLP transaction binary, sign it with the private key and network ID, 115 | add calculated minimum fee and post it to the node. 116 | 117 | ## Example 118 | iex> spend_tx = %AeternityNode.Model.SpendTx{ 119 | amount: 10_000, 120 | fee: 16_680_000_000, 121 | nonce: 1, 122 | payload: "", 123 | recipient_id: "ak_wuLXPE5pd2rvFoxHxvenBgp459rW6Y1cZ6cYTZcAcLAevPE5M", 124 | sender_id: "ak_6A2vcm1Sz6aqJezkLCssUXcyZTX7X8D5UwbuS2fRJr9KkYpRU", 125 | ttl: 0 126 | } 127 | iex> AeppSDK.Utils.Transaction.post(client, spend_tx, :no_auth, :one_signature) 128 | {:ok, 129 | %{ 130 | block_hash: "mh_2wRRkfzcHd24cGbqdqaLAhxgpv4iMB8y1Cp5n9FAfhvDZJ7Qh", 131 | block_height: 149, 132 | tx_hash: "th_umEMGk2S1EtkeAZCVHXDoTqQMdMawK9R9j1yvDZWjvKmstg5c" 133 | }} 134 | """ 135 | @spec post( 136 | Client.t(), 137 | tx_types(), 138 | atom() | list(), 139 | list() | atom() 140 | ) :: {:ok, map()} | {:error, String.t()} | {:error, Env.t()} 141 | def post( 142 | %Client{ 143 | connection: connection, 144 | keypair: %{secret: secret_key}, 145 | network_id: network_id 146 | }, 147 | tx, 148 | :no_auth, 149 | signatures_list 150 | ) do 151 | type = Map.get(tx, :__struct__, :no_type) 152 | serialized_tx = Serialization.serialize(tx) 153 | 154 | signature = 155 | Keys.sign( 156 | serialized_tx, 157 | Keys.secret_key_to_binary(secret_key.()), 158 | network_id 159 | ) 160 | 161 | signed_tx_fields = 162 | case signatures_list do 163 | :one_signature -> [[signature], serialized_tx] 164 | _ -> [signatures_list, serialized_tx] 165 | end 166 | 167 | serialized_signed_tx = Serialization.serialize(signed_tx_fields, :signed_tx) 168 | 169 | encoded_signed_tx = Encoding.prefix_encode_base64("tx", serialized_signed_tx) 170 | 171 | with {:ok, %PostTxResponse{tx_hash: tx_hash}} <- 172 | TransactionApi.post_transaction(connection, %Tx{ 173 | tx: encoded_signed_tx 174 | }), 175 | {:ok, _} = response <- await_mining(connection, tx_hash, type) do 176 | response 177 | else 178 | {:ok, %Error{reason: message}} -> 179 | {:error, message} 180 | 181 | {:error, _} = error -> 182 | error 183 | end 184 | end 185 | 186 | def post( 187 | %Client{ 188 | connection: connection, 189 | keypair: %{public: public_key}, 190 | gas_price: gas_price, 191 | network_id: network_id 192 | } = client, 193 | tx, 194 | auth_opts, 195 | _signatures_list 196 | ) do 197 | tx = %{tx | nonce: 0} 198 | type = Map.get(tx, :__struct__, :no_type) 199 | 200 | with {:ok, %{kind: "generalized", auth_fun: auth_fun, contract_id: contract_id}} <- 201 | AccountApi.get(client, public_key), 202 | :ok <- ensure_auth_opts(auth_opts), 203 | {:ok, height} <- Chain.height(client), 204 | {:ok, %{abi_version: abi_version}} <- Contract.get(client, contract_id), 205 | new_fee <- 206 | calculate_n_times_fee( 207 | tx, 208 | height, 209 | network_id, 210 | Map.get(tx, :fee, dummy_fee()), 211 | gas_price, 212 | default_fee_calculation_times() 213 | ), 214 | {:ok, calldata} = 215 | Contract.create_calldata( 216 | Keyword.get(auth_opts, :auth_contract_source), 217 | auth_fun, 218 | Keyword.get(auth_opts, :auth_args) 219 | ), 220 | serialized_tx = wrap_in_empty_signed_tx(%{tx | fee: new_fee}), 221 | meta_tx_dummy_fee = %{ 222 | ga_id: public_key, 223 | auth_data: calldata, 224 | abi_version: abi_version, 225 | fee: dummy_fee(), 226 | gas: Keyword.get(auth_opts, :gas, GeneralizedAccount.default_gas()), 227 | gas_price: Keyword.get(auth_opts, :gas_price, gas_price), 228 | ttl: Keyword.get(auth_opts, :ttl, @default_ttl), 229 | tx: serialized_tx 230 | }, 231 | meta_tx = %{ 232 | meta_tx_dummy_fee 233 | | fee: 234 | Keyword.get( 235 | auth_opts, 236 | :fee, 237 | calculate_n_times_fee( 238 | meta_tx_dummy_fee, 239 | height, 240 | network_id, 241 | Keyword.get(auth_opts, :fee, dummy_fee()), 242 | gas_price, 243 | default_fee_calculation_times() 244 | ) 245 | ) 246 | }, 247 | serialized_meta_tx = wrap_in_empty_signed_tx(meta_tx), 248 | encoded_signed_tx = Encoding.prefix_encode_base64("tx", serialized_meta_tx), 249 | {:ok, %PostTxResponse{tx_hash: tx_hash}} <- 250 | TransactionApi.post_transaction(connection, %Tx{ 251 | tx: encoded_signed_tx 252 | }), 253 | {:ok, _} = response <- await_mining(connection, tx_hash, type) do 254 | response 255 | else 256 | {:ok, %{kind: "basic"}} -> 257 | {:error, "Account isn't generalized"} 258 | 259 | {:ok, %Error{reason: message}} -> 260 | {:error, message} 261 | 262 | {:error, _} = error -> 263 | error 264 | end 265 | end 266 | 267 | @doc """ 268 | Calculate the fee of the transaction. 269 | 270 | ## Example 271 | iex> spend_tx = %AeternityNode.Model.SpendTx{ 272 | amount: 40_000_000, 273 | fee: 0, 274 | nonce: 10_624, 275 | payload: "", 276 | recipient_id: "ak_6A2vcm1Sz6aqJezkLCssUXcyZTX7X8D5UwbuS2fRJr9KkYpRU", 277 | sender_id: "ak_6A2vcm1Sz6aqJezkLCssUXcyZTX7X8D5UwbuS2fRJr9KkYpRU", 278 | ttl: 0 279 | } 280 | iex> AeppSDK.Utils.Transaction.calculate_fee(spend_tx, 51_900, "ae_uat", 0, 1_000_000) 281 | 16660000000 282 | """ 283 | @spec calculate_fee( 284 | tx_types(), 285 | non_neg_integer(), 286 | String.t(), 287 | atom() | non_neg_integer(), 288 | non_neg_integer() 289 | ) :: non_neg_integer() 290 | def calculate_fee(tx, height, _network_id, @dummy_fee, gas_price) when gas_price > 0 do 291 | min_gas(tx, height) * gas_price 292 | end 293 | 294 | def calculate_fee(_tx, _height, _network_id, fee, _gas_price) when fee > 0 do 295 | fee 296 | end 297 | 298 | def calculate_fee(_tx, _height, _network_id, fee, gas_price) do 299 | {:error, "#{__MODULE__}: Incorrect fee: #{fee} or gas price: #{gas_price}"} 300 | end 301 | 302 | @doc """ 303 | Calculates minimum fee of given transaction, depends on height and network_id 304 | 305 | ## Example 306 | iex> name_pre_claim_tx = %AeternityNode.Model.NamePreclaimTx{ 307 | account_id: "ak_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 308 | commitment_id: "cm_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 309 | fee: 0, 310 | nonce: 0, 311 | ttl: 0 312 | } 313 | iex> AeppSDK.Utils.Transaction.calculate_min_fee(name_pre_claim_tx, 50_000, "ae_mainnet") 314 | 16500000000 315 | """ 316 | @spec calculate_min_fee(struct(), non_neg_integer(), String.t()) :: 317 | non_neg_integer() | {:error, String.t()} 318 | def calculate_min_fee(%struct{} = tx, height, network_id) 319 | when struct in @struct_type and is_integer(height) and network_id in @network_id_list do 320 | min_gas(tx, height) * Governance.min_gas_price(height, network_id) 321 | end 322 | 323 | def calculate_min_fee(tx, height, network_id) do 324 | {:error, 325 | "#{__MODULE__} Not valid tx: #{inspect(tx)} or height: #{inspect(height)} or networkid: #{ 326 | inspect(network_id) 327 | }"} 328 | end 329 | 330 | @doc """ 331 | Calculates minimum gas needed for given transaction, also depends on height. 332 | 333 | ## Example 334 | iex> spend_tx = %AeternityNode.Model.SpendTx{ 335 | amount: 5_018_857_520_000_000_000, 336 | fee: 0, 337 | nonce: 37181, 338 | payload: "", 339 | recipient_id: "ak_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 340 | sender_id: "ak_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 341 | ttl: 0 342 | } 343 | iex> AeppSDK.Utils.Transaction.min_gas(spend_tx, 50_000) 344 | 16740 345 | """ 346 | @spec min_gas(struct(), non_neg_integer()) :: non_neg_integer() | {:error, String.t()} 347 | def min_gas(%ContractCallTx{} = tx, _height) do 348 | Governance.tx_base_gas(tx) + byte_size(Serialization.serialize(tx)) * Governance.byte_gas() 349 | end 350 | 351 | def min_gas(%ContractCreateTx{} = tx, _height) do 352 | Governance.tx_base_gas(tx) + byte_size(Serialization.serialize(tx)) * Governance.byte_gas() 353 | end 354 | 355 | def min_gas( 356 | %{ga_id: _, auth_data: _, abi_version: _, fee: _, gas: _, gas_price: _, ttl: _, tx: _} = 357 | tx, 358 | _height 359 | ) do 360 | Governance.tx_base_gas(tx) + byte_size(Serialization.serialize(tx)) * Governance.byte_gas() 361 | end 362 | 363 | def min_gas(tx, height) do 364 | gas_limit(tx, height) 365 | end 366 | 367 | @doc """ 368 | Returns gas limit for given transaction, depends on height. 369 | 370 | ## Example 371 | iex> oracle_register_tx = %AeternityNode.Model.OracleRegisterTx{ 372 | abi_version: 0x60001, 373 | account_id: "ak_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 374 | fee: 0, 375 | nonce: 37_122, 376 | oracle_ttl: %AeternityNode.Model.Ttl{type: :absolute, value: 10}, 377 | query_fee: 10, 378 | query_format: "query_format", 379 | response_format: "response_format", 380 | ttl: 10 381 | } 382 | iex> AeppSDK.Utils.Transaction.gas_limit(oracle_register_tx, 5) 383 | 16581 384 | """ 385 | @spec gas_limit(struct(), non_neg_integer()) :: non_neg_integer() | {:error, String.t()} 386 | def gas_limit(%OracleRegisterTx{oracle_ttl: oracle_ttl} = tx, height) do 387 | case ttl_delta(height, {oracle_ttl.type, oracle_ttl.value}) do 388 | {:relative, _d} = ttl -> 389 | Governance.tx_base_gas(tx) + 390 | byte_size(Serialization.serialize(tx)) * Governance.byte_gas() + state_gas(tx, ttl) 391 | 392 | {:error, _reason} -> 393 | 0 394 | end 395 | end 396 | 397 | def gas_limit(%OracleExtendTx{oracle_ttl: oracle_ttl} = tx, height) do 398 | case ttl_delta(height, {oracle_ttl.type, oracle_ttl.value}) do 399 | {:relative, _d} = ttl -> 400 | Governance.tx_base_gas(tx) + 401 | byte_size(Serialization.serialize(tx)) * Governance.byte_gas() + state_gas(tx, ttl) 402 | 403 | {:error, _reason} -> 404 | 0 405 | end 406 | end 407 | 408 | def gas_limit(%OracleQueryTx{query_ttl: query_ttl} = tx, height) do 409 | case ttl_delta(height, {query_ttl.type, query_ttl.value}) do 410 | {:relative, _d} = ttl -> 411 | Governance.tx_base_gas(tx) + 412 | byte_size(Serialization.serialize(tx)) * Governance.byte_gas() + state_gas(tx, ttl) 413 | 414 | {:error, _reason} -> 415 | 0 416 | end 417 | end 418 | 419 | def gas_limit(%OracleRespondTx{response_ttl: response_ttl} = tx, height) do 420 | case ttl_delta(height, {response_ttl.type, response_ttl.value}) do 421 | {:relative, _d} = ttl -> 422 | Governance.tx_base_gas(tx) + 423 | byte_size(Serialization.serialize(tx)) * Governance.byte_gas() + state_gas(tx, ttl) 424 | 425 | {:error, _reason} -> 426 | 0 427 | end 428 | end 429 | 430 | def gas_limit(%struct{} = tx, _height) 431 | when struct in [ 432 | SpendTx, 433 | NamePreclaimTx, 434 | NameClaimTx, 435 | NameTransferTx, 436 | NameRevokeTx, 437 | NameUpdateTx, 438 | ChannelCreateTx, 439 | ChannelCloseMutualTx, 440 | ChannelCloseSoloTx, 441 | ChannelDepositTx, 442 | ChannelForceProgressTx, 443 | ChannelSettleTx, 444 | ChannelSlashTx, 445 | ChannelSnapshotSoloTx, 446 | ChannelWithdrawTx, 447 | ContractCreateTx, 448 | ContractCallTx 449 | ] do 450 | Governance.tx_base_gas(tx) + byte_size(Serialization.serialize(tx)) * Governance.byte_gas() + 451 | Governance.gas(tx) 452 | end 453 | 454 | def gas_limit(tx, _height) do 455 | Governance.tx_base_gas(tx) + byte_size(Serialization.serialize(tx)) * Governance.byte_gas() + 456 | Governance.gas(tx) 457 | end 458 | 459 | @doc """ 460 | Signs the transaction. 461 | 462 | ## Example 463 | iex> spend_tx = %AeternityNode.Model.SpendTx{ 464 | amount: 1_000_000_000_000, 465 | fee: 0, 466 | nonce: 1, 467 | payload: "", 468 | recipient_id: "ak_wuLXPE5pd2rvFoxHxvenBgp459rW6Y1cZ6cYTZcAcLAevPE5M", 469 | sender_id: "ak_6A2vcm1Sz6aqJezkLCssUXcyZTX7X8D5UwbuS2fRJr9KkYpRU", 470 | ttl: 0 471 | } 472 | iex> AeppSDK.Utils.Transaction.sign_tx(spend_tx, client) 473 | {:ok, 474 | [ 475 | %AeternityNode.Model.SpendTx{ 476 | amount: 1000000000000, 477 | fee: 0, 478 | nonce: 1, 479 | payload: "", 480 | recipient_id: "ak_wuLXPE5pd2rvFoxHxvenBgp459rW6Y1cZ6cYTZcAcLAevPE5M", 481 | sender_id: "ak_6A2vcm1Sz6aqJezkLCssUXcyZTX7X8D5UwbuS2fRJr9KkYpRU", 482 | ttl: 0 483 | }, 484 | <168, 173, 62, 192, 198, 163, 29, 224, 83, 235, 68, 152, 82, 151, 77, 68, 485 | 228, 138, 200, 45, 15, 74, 125, 222, 45, 150, 114, 79, 84, 160, 83, 255, 486 | 63, 165, 45, 146, 164, 168, 144, 116, 200, 253, 80, 119, 6, 147, ...>> 487 | ]} 488 | """ 489 | @spec sign_tx(tx_types(), Client.t(), list() | :no_opts) :: {:ok, list()} | {:error, String.t()} 490 | def sign_tx(tx, client, auth_opts \\ :no_opts) do 491 | sign_tx_(tx, client, auth_opts) 492 | end 493 | 494 | @doc """ 495 | Calculates fee for given transaction `n` times. 496 | 497 | ## Example 498 | iex> spend_tx = %AeternityNode.Model.SpendTx{ 499 | amount: 1_000_000_000_000, 500 | fee: 0, 501 | nonce: 1, 502 | payload: "", 503 | recipient_id: "ak_wuLXPE5pd2rvFoxHxvenBgp459rW6Y1cZ6cYTZcAcLAevPE5M", 504 | sender_id: "ak_6A2vcm1Sz6aqJezkLCssUXcyZTX7X8D5UwbuS2fRJr9KkYpRU", 505 | ttl: 0 506 | } 507 | iex> AeppSDK.Utils.Transaction.calculate_n_times_fee(spend_tx, 58_336, "ae_uat", 0, 1_000_000, 5) 508 | 16740000000 509 | """ 510 | @spec calculate_n_times_fee( 511 | tx_types, 512 | non_neg_integer(), 513 | String.t(), 514 | non_neg_integer(), 515 | non_neg_integer(), 516 | non_neg_integer() 517 | ) :: non_neg_integer() 518 | def calculate_n_times_fee(tx, height, network_id, fee, gas_price, times) do 519 | calculate_fee_n_times(tx, height, network_id, fee, gas_price, times, 0) 520 | end 521 | 522 | @doc false 523 | def await_mining(connection, tx_hash, type) do 524 | await_mining(connection, tx_hash, @await_attempts, type) 525 | end 526 | 527 | @doc false 528 | def await_mining(_connection, tx_hash, 0, _type), 529 | do: 530 | {:error, 531 | "Transaction #{inspect(tx_hash)} wasn't mined after #{ 532 | @await_attempts * @await_attempt_interval / 1_000 533 | } seconds"} 534 | 535 | @doc false 536 | def await_mining(connection, tx_hash, attempts, type) do 537 | Process.sleep(@await_attempt_interval) 538 | 539 | mining_status = 540 | case type do 541 | ContractCallTx -> 542 | TransactionApi.get_transaction_info_by_hash(connection, tx_hash) 543 | 544 | ContractCreateTx -> 545 | TransactionApi.get_transaction_info_by_hash(connection, tx_hash) 546 | 547 | _ -> 548 | TransactionApi.get_transaction_by_hash(connection, tx_hash) 549 | end 550 | 551 | case mining_status do 552 | {:ok, %GenericSignedTx{block_hash: "none", block_height: -1}} -> 553 | await_mining(connection, tx_hash, attempts - 1, type) 554 | 555 | {:ok, %GenericSignedTx{block_hash: block_hash, block_height: block_height, hash: tx_hash}} -> 556 | {:ok, %{block_hash: block_hash, block_height: block_height, tx_hash: tx_hash}} 557 | 558 | {:ok, 559 | %TxInfoObject{ 560 | call_info: %ContractCallObject{ 561 | log: log, 562 | return_value: return_value, 563 | return_type: return_type 564 | } 565 | }} -> 566 | {:ok, %GenericSignedTx{block_hash: block_hash, block_height: block_height, hash: tx_hash}} = 567 | TransactionApi.get_transaction_by_hash(connection, tx_hash) 568 | 569 | {:ok, 570 | %{ 571 | block_hash: block_hash, 572 | block_height: block_height, 573 | tx_hash: tx_hash, 574 | return_value: return_value, 575 | return_type: return_type, 576 | log: log 577 | }} 578 | 579 | {:ok, 580 | %TxInfoObject{ 581 | call_info: nil, 582 | ga_info: %GaObject{return_value: return_value, return_type: return_type} 583 | }} -> 584 | {:ok, %GenericSignedTx{block_hash: block_hash, block_height: block_height, hash: tx_hash}} = 585 | TransactionApi.get_transaction_by_hash(connection, tx_hash) 586 | 587 | {:ok, 588 | %{ 589 | block_hash: block_hash, 590 | block_height: block_height, 591 | tx_hash: tx_hash, 592 | return_value: return_value, 593 | return_type: return_type, 594 | log: [] 595 | }} 596 | 597 | {:ok, %Error{}} -> 598 | await_mining(connection, tx_hash, attempts - 1, type) 599 | 600 | {:error, %Env{} = env} -> 601 | {:error, env} 602 | end 603 | end 604 | 605 | defp calculate_fee_n_times(_tx, _height, _network_id, _fee, _gas_price, 0, acc) do 606 | acc 607 | end 608 | 609 | defp calculate_fee_n_times(tx, height, network_id, fee, gas_price, times, _acc) do 610 | case fee do 611 | 0 -> 612 | acc = calculate_fee(tx, height, network_id, 0, gas_price) 613 | 614 | calculate_fee_n_times( 615 | %{tx | fee: acc}, 616 | height, 617 | network_id, 618 | fee, 619 | gas_price, 620 | times - 1, 621 | acc 622 | ) 623 | 624 | fee -> 625 | calculate_fee_n_times(%{tx | fee: fee}, height, network_id, fee, gas_price, 0, fee) 626 | end 627 | end 628 | 629 | defp ttl_delta(_height, {:relative, _value} = ttl) do 630 | {:relative, oracle_ttl_delta(0, ttl)} 631 | end 632 | 633 | defp ttl_delta(height, {:absolute, _value} = ttl) do 634 | case oracle_ttl_delta(height, ttl) do 635 | ttl_delta when is_integer(ttl_delta) -> 636 | {:relative, ttl_delta} 637 | 638 | {:error, _reason} = err -> 639 | err 640 | end 641 | end 642 | 643 | defp sign_tx_( 644 | tx, 645 | %Client{ 646 | keypair: %{public: public_key, secret: secret_key}, 647 | network_id: network_id 648 | } = client, 649 | :no_opts 650 | ) 651 | when is_map(tx) do 652 | case AccountApi.get(client, public_key) do 653 | {:ok, %{kind: "basic"}} -> 654 | serialized_tx = Serialization.serialize(tx) 655 | 656 | signature = 657 | Keys.sign( 658 | serialized_tx, 659 | Keys.secret_key_to_binary(secret_key.()), 660 | network_id 661 | ) 662 | 663 | {:ok, [tx, signature]} 664 | 665 | {:ok, %{kind: other}} -> 666 | {:error, "Account can't be authorized as Basic, as it is #{inspect(other)} type"} 667 | 668 | {:error, err} -> 669 | {:error, "Unexpected error: #{inspect(err)} "} 670 | end 671 | end 672 | 673 | defp sign_tx_( 674 | tx, 675 | %Client{ 676 | keypair: %{public: public_key}, 677 | gas_price: gas_price, 678 | network_id: network_id 679 | } = client, 680 | auth_opts 681 | ) do 682 | with {:ok, %{kind: "generalized", auth_fun: auth_fun, contract_id: contract_id}} <- 683 | AccountApi.get(client, public_key), 684 | :ok <- ensure_auth_opts(auth_opts), 685 | {:ok, height} <- Chain.height(client), 686 | {:ok, %{vm_version: vm_version, abi_version: abi_version}} <- 687 | Contract.get(client, contract_id), 688 | {:ok, calldata} = 689 | Contract.create_calldata( 690 | Keyword.get(auth_opts, :auth_contract_source), 691 | auth_fun, 692 | Keyword.get(auth_opts, :auth_args), 693 | Contract.get_vm(vm_version) 694 | ), 695 | serialized_tx = wrap_in_empty_signed_tx(tx), 696 | meta_tx_dummy_fee <- %{ 697 | ga_id: public_key, 698 | auth_data: calldata, 699 | abi_version: abi_version, 700 | fee: dummy_fee(), 701 | gas: Keyword.get(auth_opts, :gas, GeneralizedAccount.default_gas()), 702 | gas_price: Keyword.get(auth_opts, :gas_price, gas_price), 703 | ttl: Keyword.get(auth_opts, :ttl, @default_ttl), 704 | tx: serialized_tx 705 | }, 706 | meta_tx <- %{ 707 | meta_tx_dummy_fee 708 | | fee: 709 | Keyword.get( 710 | auth_opts, 711 | :fee, 712 | calculate_fee( 713 | tx, 714 | height, 715 | network_id, 716 | dummy_fee(), 717 | meta_tx_dummy_fee.gas_price 718 | ) 719 | ) 720 | } do 721 | {:ok, [tx, meta_tx, []]} 722 | else 723 | {:ok, %Account{kind: "basic"}} -> 724 | {:error, "Account isn't generalized"} 725 | 726 | {:ok, %Error{reason: message}} -> 727 | {:error, message} 728 | 729 | {:error, _} = error -> 730 | error 731 | end 732 | end 733 | 734 | defp oracle_ttl_delta(_current_height, {:relative, d}), do: d 735 | 736 | defp oracle_ttl_delta(current_height, {:absolute, h}) when h > current_height, 737 | do: h - current_height 738 | 739 | defp oracle_ttl_delta(_current_height, {:absolute, _}), 740 | do: {:error, "#{__MODULE__} Too low height"} 741 | 742 | defp state_gas(tx, {:relative, ttl}) do 743 | tx 744 | |> Governance.state_gas_per_block() 745 | |> Governance.state_gas(ttl) 746 | end 747 | 748 | defp wrap_in_empty_signed_tx(tx) do 749 | serialized_tx = Serialization.serialize(tx) 750 | signed_tx_fields = [[], serialized_tx] 751 | Serialization.serialize(signed_tx_fields, :signed_tx) 752 | end 753 | 754 | defp ensure_auth_opts(auth_opts) do 755 | if Keyword.has_key?(auth_opts, :auth_contract_source) && 756 | Keyword.has_key?(auth_opts, :auth_args) do 757 | :ok 758 | else 759 | {:error, "Authorization source and function arguments are required"} 760 | end 761 | end 762 | end 763 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule AeppSDK.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :aepp_sdk_elixir, 7 | version: "0.5.4", 8 | start_permanent: Mix.env() == :prod, 9 | description: description(), 10 | package: package(), 11 | deps: deps(), 12 | aliases: aliases(), 13 | elixir: "~> 1.9", 14 | test_coverage: [tool: ExCoveralls], 15 | docs: [logo: "logo.png", filter_prefix: "AeppSDK"], 16 | preferred_cli_env: [ 17 | coveralls: :test, 18 | "coveralls.detail": :test, 19 | "coveralls.post": :test, 20 | "coveralls.html": :test 21 | ] 22 | ] 23 | end 24 | 25 | # Dependencies listed here are available only for this 26 | # project and cannot be accessed from applications inside 27 | # the apps folder. 28 | # 29 | # Run "mix help deps" for examples and options. 30 | defp deps do 31 | [ 32 | {:excoveralls, "~> 0.10", only: :test}, 33 | {:ex_doc, "~> 0.19", only: :dev, runtime: false}, 34 | {:aesophia, 35 | git: "https://github.com/aeternity/aesophia.git", manager: :rebar, tag: "v4.0.0"}, 36 | {:enoise, 37 | git: "https://github.com/aeternity/enoise.git", 38 | manager: :rebar, 39 | ref: "c06bbae07d5a6711e60254e45e57e37e270b961d"}, 40 | {:distillery, "~> 2.0"}, 41 | {:enacl, 42 | github: "aeternity/enacl", ref: "26180f42c0b3a450905d2efd8bc7fd5fd9cece75", override: true}, 43 | {:tesla, "~> 1.2.1"}, 44 | {:poison, "~> 3.0.0"}, 45 | {:ranch, "~> 1.7"}, 46 | {:hackney, "~> 1.15"}, 47 | {:argon2_elixir, "~> 2.0"}, 48 | {:uuid, "~> 1.1"}, 49 | {:credo, "~> 1.1.0", only: [:dev, :test], runtime: false} 50 | ] 51 | end 52 | 53 | defp description(), do: "Elixir SDK targeting the Æternity node implementation." 54 | 55 | defp package() do 56 | [ 57 | licenses: ["ISC License"], 58 | links: %{"GitHub" => "https://github.com/aeternity/aepp-sdk-elixir"} 59 | ] 60 | end 61 | 62 | defp aliases do 63 | [build_api: &build_api/1] 64 | end 65 | 66 | defp build_api([generator_version, aenode_spec_vsn, middleware_spec_vsn]) do 67 | Enum.each( 68 | [ 69 | get_generator(generator_version), 70 | get_swagger_spec(:aenode, aenode_spec_vsn), 71 | get_swagger_spec(:middleware, middleware_spec_vsn), 72 | {"tar", 73 | ["zxvf", "#{get_file_name(:generator)}-#{generator_version}-ubuntu-x86_64.tar.gz"]}, 74 | {"rm", ["#{get_file_name(:generator)}-#{generator_version}-ubuntu-x86_64.tar.gz"]}, 75 | prepare_java_commands(:aenode), 76 | prepare_java_commands(:middleware), 77 | {"mix", ["format"]}, 78 | {"rm", ["-f", "#{get_file_name(:generator)}.jar"]}, 79 | {"rm", ["-f", "#{get_file_name(:specification)}.yaml"]}, 80 | {"rm", ["-f", "#{get_file_name(:specification)}.json"]} 81 | ], 82 | fn {com, args} -> System.cmd(com, args) end 83 | ) 84 | end 85 | 86 | defp get_file_name(:specification) do 87 | "swagger" 88 | end 89 | 90 | defp get_file_name(:generator) do 91 | "openapi-generator-cli" 92 | end 93 | 94 | defp prepare_java_commands(:aenode) do 95 | {"java", 96 | [ 97 | "-jar", 98 | "./#{get_file_name(:generator)}.jar", 99 | "generate", 100 | "--skip-validate-spec", 101 | "-i", 102 | "./#{get_file_name(:specification)}.yaml", 103 | "-g", 104 | "elixir", 105 | "-o", 106 | "./lib/aeternity_node/" 107 | ]} 108 | end 109 | 110 | defp prepare_java_commands(:middleware) do 111 | {"java", 112 | [ 113 | "-jar", 114 | "./#{get_file_name(:generator)}.jar", 115 | "generate", 116 | "--skip-validate-spec", 117 | "-i", 118 | "./#{get_file_name(:specification)}.json", 119 | "-g", 120 | "elixir", 121 | "-o", 122 | "./lib/middleware/" 123 | ]} 124 | end 125 | 126 | defp get_generator(generator_version) do 127 | {"wget", 128 | [ 129 | "--verbose", 130 | "https://github.com/aeternity/openapi-generator/releases/download/#{generator_version}/#{ 131 | get_file_name(:generator) 132 | }-#{generator_version}-ubuntu-x86_64.tar.gz" 133 | ]} 134 | end 135 | 136 | defp get_swagger_spec(:aenode, api_specification_version) do 137 | {"wget", 138 | [ 139 | "--verbose", 140 | "https://raw.githubusercontent.com/aeternity/aeternity/#{api_specification_version}/apps/aehttp/priv/#{ 141 | get_file_name(:specification) 142 | }.yaml" 143 | ]} 144 | end 145 | 146 | defp get_swagger_spec(:middleware, api_specification_version) do 147 | {"wget", 148 | [ 149 | "--verbose", 150 | "https://raw.githubusercontent.com/aeternity/aepp-middleware/#{api_specification_version}/swagger/#{ 151 | get_file_name(:specification) 152 | }.json" 153 | ]} 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "aebytecode": {:git, "https://github.com/aeternity/aebytecode.git", "4f4d6d30cd2c46b3830454d650a424d513f69134", [ref: "4f4d6d3"]}, 3 | "aeserialization": {:git, "https://github.com/aeternity/aeserialization.git", "47aaa8f5434b365c50a35bfd1490340b19241991", [ref: "47aaa8f"]}, 4 | "aesophia": {:git, "https://github.com/aeternity/aesophia.git", "b81312a714a5f618aacc0fe535f5b442373e2533", [tag: "v4.0.0"]}, 5 | "argon2_elixir": {:hex, :argon2_elixir, "2.1.2", "c276b960f0b550a7613a9bebf8e14645ca5eb71a34a1bf0f896fe3511966b051", [:make, :mix], [{:comeonin, "~> 5.1", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.5", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "artificery": {:hex, :artificery, "0.4.2", "3ded6e29e13113af52811c72f414d1e88f711410cac1b619ab3a2666bbd7efd4", [:mix], [], "hexpm"}, 7 | "base58": {:git, "https://github.com/aeternity/erl-base58.git", "60a335668a60328a29f9731b67c4a0e9e3d50ab6", [ref: "60a3356"]}, 8 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, 9 | "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, 10 | "comeonin": {:hex, :comeonin, "5.1.3", "4c9880ed348cc0330c74086b4383ffb0b5a599aa603416497b7374c168cae340", [:mix], [], "hexpm"}, 11 | "credo": {:hex, :credo, "1.1.5", "caec7a3cadd2e58609d7ee25b3931b129e739e070539ad1a0cd7efeeb47014f4", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, 12 | "distillery": {:hex, :distillery, "2.1.1", "f9332afc2eec8a1a2b86f22429e068ef35f84a93ea1718265e740d90dd367814", [:mix], [{:artificery, "~> 0.2", [hex: :artificery, repo: "hexpm", optional: false]}], "hexpm"}, 13 | "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm"}, 14 | "eblake2": {:hex, :eblake2, "1.0.0", "ec8ad20e438aab3f2e8d5d118c366a0754219195f8a0f536587440f8f9bcf2ef", [:rebar3], [], "hexpm"}, 15 | "elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm"}, 16 | "enacl": {:git, "https://github.com/aeternity/enacl.git", "26180f42c0b3a450905d2efd8bc7fd5fd9cece75", [ref: "26180f42c0b3a450905d2efd8bc7fd5fd9cece75"]}, 17 | "enoise": {:git, "https://github.com/aeternity/enoise.git", "c06bbae07d5a6711e60254e45e57e37e270b961d", [ref: "c06bbae07d5a6711e60254e45e57e37e270b961d"]}, 18 | "ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, 19 | "excoveralls": {:hex, :excoveralls, "0.12.1", "a553c59f6850d0aff3770e4729515762ba7c8e41eedde03208182a8dc9d0ce07", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, 20 | "getopt": {:hex, :getopt, "1.0.1", "c73a9fa687b217f2ff79f68a3b637711bb1936e712b521d8ce466b29cbf7808a", [:rebar3], [], "hexpm"}, 21 | "hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, 22 | "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, 23 | "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, 24 | "jsx": {:git, "https://github.com/talentdeficit/jsx.git", "3074d4865b3385a050badf7828ad31490d860df5", [tag: "2.8.0"]}, 25 | "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 26 | "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, 27 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, 28 | "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, 29 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"}, 30 | "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm"}, 31 | "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, 32 | "poison": {:hex, :poison, "3.0.0", "625ebd64d33ae2e65201c2c14d6c85c27cc8b68f2d0dd37828fde9c6920dd131", [:mix], [], "hexpm"}, 33 | "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, 34 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm"}, 35 | "tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, 36 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, 37 | "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm"}, 38 | } 39 | -------------------------------------------------------------------------------- /test/core_chain_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CoreChainTest do 2 | use ExUnit.Case 3 | 4 | alias AeppSDK.{Chain, Contract} 5 | alias Tesla.Env 6 | 7 | setup_all do 8 | Code.require_file("test_utils.ex", "test/") 9 | TestUtils.get_test_data() 10 | end 11 | 12 | @tag :travis_test 13 | test "height operations", setup_data do 14 | height_result = Chain.height(setup_data.client) 15 | assert match?({:ok, _}, height_result) 16 | 17 | {:ok, height} = height_result 18 | assert :ok == Chain.await_height(setup_data.client, height) 19 | assert :ok == Chain.await_height(setup_data.client, height + 1) 20 | end 21 | 22 | @tag :travis_test 23 | test "transaction operations with valid input", setup_data do 24 | {:ok, 25 | %{ 26 | block_hash: block_hash, 27 | block_height: block_height, 28 | contract_id: contract_id, 29 | log: log, 30 | return_type: return_type, 31 | return_value: return_value, 32 | tx_hash: tx_hash 33 | }} = 34 | Contract.deploy( 35 | setup_data.client, 36 | setup_data.source_code, 37 | ["42"] 38 | ) 39 | 40 | assert :ok == Chain.await_transaction(setup_data.client, tx_hash) 41 | 42 | transaction_result = 43 | Chain.get_transaction( 44 | setup_data.client, 45 | tx_hash 46 | ) 47 | 48 | assert match?( 49 | {:ok, 50 | %{ 51 | block_hash: ^block_hash, 52 | block_height: ^block_height, 53 | hash: ^tx_hash 54 | }}, 55 | transaction_result 56 | ) 57 | 58 | transaction_info_result = Chain.get_transaction_info(setup_data.client, tx_hash) 59 | 60 | assert match?( 61 | {:ok, 62 | %{ 63 | call_info: %{ 64 | contract_id: ^contract_id, 65 | height: ^block_height, 66 | log: ^log, 67 | return_type: ^return_type, 68 | return_value: ^return_value 69 | } 70 | }}, 71 | transaction_info_result 72 | ) 73 | 74 | pending_transactions_result = Chain.get_pending_transactions(setup_data.client) 75 | assert match?({:ok, _}, pending_transactions_result) 76 | end 77 | 78 | @tag :travis_test 79 | test "transaction operations with invalid input", setup_data do 80 | assert {:error, "Invalid hash"} == 81 | Chain.await_transaction(setup_data.client, "invalid_tx_hash") 82 | 83 | assert {:error, "Invalid hash"} == Chain.get_transaction(setup_data.client, "invalid_tx_hash") 84 | 85 | assert {:error, "Invalid hash: hash"} == 86 | Chain.get_transaction_info(setup_data.client, "invalid_tx_hash") 87 | end 88 | 89 | @tag :travis_test 90 | test "block operations with valid input", setup_data do 91 | {:ok, 92 | %{ 93 | block_hash: micro_block_hash, 94 | block_height: height, 95 | tx_hash: tx_hash 96 | }} = 97 | Contract.deploy( 98 | setup_data.client, 99 | setup_data.source_code, 100 | ["42"] 101 | ) 102 | 103 | micro_block_header_result = Chain.get_micro_block_header(setup_data.client, micro_block_hash) 104 | assert match?({:ok, %{hash: ^micro_block_hash}}, micro_block_header_result) 105 | 106 | generation_result = Chain.get_generation(setup_data.client, height) 107 | assert match?({:ok, %{key_block: %{height: ^height}}}, generation_result) 108 | 109 | current_generation_result = Chain.get_current_generation(setup_data.client) 110 | assert match?({:ok, %{key_block: %{}}}, current_generation_result) 111 | 112 | {:ok, %{key_block: %{hash: key_block_hash, height: ^height}, micro_blocks: _}} = 113 | generation_result 114 | 115 | generation_result_ = Chain.get_generation(setup_data.client, key_block_hash) 116 | 117 | assert match?( 118 | {:ok, %{key_block: %{hash: ^key_block_hash, height: ^height}}}, 119 | generation_result_ 120 | ) 121 | 122 | micro_block_transactions_result = 123 | Chain.get_micro_block_transactions(setup_data.client, micro_block_hash) 124 | 125 | assert match?( 126 | {:ok, [%{block_hash: ^micro_block_hash, block_height: ^height, hash: ^tx_hash}]}, 127 | micro_block_transactions_result 128 | ) 129 | 130 | key_block_result = Chain.get_key_block(setup_data.client, key_block_hash) 131 | assert match?({:ok, %{hash: ^key_block_hash, height: ^height}}, key_block_result) 132 | 133 | key_block_result_ = Chain.get_key_block(setup_data.client, height) 134 | assert match?({:ok, %{hash: ^key_block_hash, height: ^height}}, key_block_result_) 135 | end 136 | 137 | @tag :travis_test 138 | test "block operations with invalid input", setup_data do 139 | assert {:error, "Invalid hash"} == 140 | Chain.get_generation(setup_data.client, "invalid_key_block_hash") 141 | 142 | {:ok, height} = Chain.height(setup_data.client) 143 | 144 | assert {:error, "Chain too short"} == Chain.get_generation(setup_data.client, height + 10) 145 | 146 | assert match?( 147 | {:error, 148 | %Env{ 149 | body: 150 | "{\"info\":{\"data\":-1,\"error\":\"not_in_range\"},\"parameter\":\"height\",\"reason\":\"validation_error\"}", 151 | status: 400 152 | }}, 153 | Chain.get_generation(setup_data.client, -1) 154 | ) 155 | 156 | assert {:error, "Invalid hash"} == 157 | Chain.get_micro_block_transactions(setup_data.client, "invalid_micro_block_hash") 158 | 159 | assert {:error, "Invalid hash"} == 160 | Chain.get_key_block(setup_data.client, "invalid_key_block_hash") 161 | 162 | assert {:error, "Block not found"} == Chain.get_key_block(setup_data.client, height + 10) 163 | 164 | assert match?( 165 | {:error, 166 | %Env{ 167 | body: 168 | "{\"info\":{\"data\":-1,\"error\":\"not_in_range\"},\"parameter\":\"height\",\"reason\":\"validation_error\"}", 169 | status: 400 170 | }}, 171 | Chain.get_key_block(setup_data.client, -1) 172 | ) 173 | 174 | assert {:error, "Invalid hash"} == 175 | Chain.get_micro_block_header(setup_data.client, "invalid_micro_block_hash") 176 | end 177 | 178 | @tag :travis_test 179 | test "node info", setup_data do 180 | node_info_result = Chain.get_node_info(setup_data.client) 181 | 182 | assert match?( 183 | {:ok, %{peer_pubkey: _, status: _, node_beneficiary: _, node_pubkey: _, peers: _}}, 184 | node_info_result 185 | ) 186 | end 187 | end 188 | -------------------------------------------------------------------------------- /test/core_contract_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CoreContractTest do 2 | use ExUnit.Case 3 | 4 | alias AeppSDK.{Account, Client, Contract, Utils.Keys} 5 | 6 | setup_all do 7 | Code.require_file("test_utils.ex", "test/") 8 | TestUtils.get_test_data() 9 | end 10 | 11 | @tag :travis_test 12 | test "create, call, call static and decode contract", setup_data do 13 | deploy_result = 14 | Contract.deploy( 15 | setup_data.client, 16 | setup_data.source_code, 17 | ["42"] 18 | ) 19 | 20 | assert match?({:ok, _}, deploy_result) 21 | 22 | {:ok, %{contract_id: ct_address}} = deploy_result 23 | 24 | on_chain_call_result = 25 | Contract.call( 26 | setup_data.client, 27 | ct_address, 28 | setup_data.source_code, 29 | "add_to_number", 30 | ["33"] 31 | ) 32 | 33 | assert match?({:ok, %{return_value: _, return_type: "ok"}}, on_chain_call_result) 34 | 35 | refute on_chain_call_result |> elem(1) |> Map.get(:log) |> Enum.empty?() 36 | 37 | static_call_result = 38 | Contract.call( 39 | setup_data.client, 40 | ct_address, 41 | setup_data.source_code, 42 | "get_number", 43 | [], 44 | fee: 10_000_000_000_000_000 45 | ) 46 | 47 | assert match?({:ok, %{return_value: _, return_type: "ok"}}, static_call_result) 48 | 49 | {:ok, %{return_value: data, return_type: "ok"}} = on_chain_call_result 50 | 51 | assert {:ok, data} == 52 | Contract.decode_return_value( 53 | "int", 54 | "cb_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEvrXnzA", 55 | "ok" 56 | ) 57 | 58 | %{public: low_balance_public_key} = low_balance_keypair = Keys.generate_keypair() 59 | Account.spend(setup_data.client, low_balance_public_key, 1) 60 | 61 | static_call_result_1 = 62 | Contract.call( 63 | %Client{setup_data.client | keypair: low_balance_keypair}, 64 | ct_address, 65 | setup_data.source_code, 66 | "get_number", 67 | [], 68 | fee: 10_000_000_000_000_000 69 | ) 70 | 71 | assert match?({:ok, %{return_value: _, return_type: "ok"}}, static_call_result_1) 72 | 73 | non_existing_keypair = Keys.generate_keypair() 74 | 75 | static_call_result_2 = 76 | Contract.call( 77 | %Client{setup_data.client | keypair: non_existing_keypair}, 78 | ct_address, 79 | setup_data.source_code, 80 | "get_number", 81 | [], 82 | fee: 10_000_000_000_000_000 83 | ) 84 | 85 | assert match?({:ok, %{return_value: _, return_type: "ok"}}, static_call_result_2) 86 | end 87 | 88 | @tag :travis_test 89 | test "create invalid contract", setup_data do 90 | invalid_source_code = String.replace(setup_data.source_code, "x : int", "x : list(int)") 91 | 92 | deploy_result = Contract.deploy(setup_data.client, invalid_source_code, ["42"]) 93 | 94 | assert match?({:error, _}, deploy_result) 95 | end 96 | 97 | @tag :travis_test 98 | test "call non-existent function", setup_data do 99 | deploy_result = 100 | Contract.deploy( 101 | setup_data.client, 102 | setup_data.source_code, 103 | ["42"] 104 | ) 105 | 106 | assert match?({:ok, _}, deploy_result) 107 | 108 | {:ok, %{contract_id: ct_address}} = deploy_result 109 | 110 | on_chain_call_result = 111 | Contract.call( 112 | setup_data.client, 113 | ct_address, 114 | setup_data.source_code, 115 | "non_existing_function", 116 | ["33"] 117 | ) 118 | 119 | assert match?({:error, "Undefined function non_existing_function"}, on_chain_call_result) 120 | end 121 | 122 | @tag :travis_test 123 | test "call static non-existent function", setup_data do 124 | deploy_result = 125 | Contract.deploy( 126 | setup_data.client, 127 | setup_data.source_code, 128 | ["42"] 129 | ) 130 | 131 | assert match?({:ok, _}, deploy_result) 132 | 133 | {:ok, %{contract_id: ct_address}} = deploy_result 134 | 135 | static_call_result = 136 | Contract.call( 137 | setup_data.client, 138 | ct_address, 139 | setup_data.source_code, 140 | "non_existing_function", 141 | ["33"], 142 | fee: 10_000_000_000_000_000 143 | ) 144 | 145 | assert match?({:error, "Undefined function non_existing_function"}, static_call_result) 146 | end 147 | 148 | @tag :travis_test 149 | test "decode data wrong type", setup_data do 150 | deploy_result = 151 | Contract.deploy( 152 | setup_data.client, 153 | setup_data.source_code, 154 | ["42"] 155 | ) 156 | 157 | assert match?({:ok, _}, deploy_result) 158 | 159 | {:ok, %{contract_id: ct_address}} = deploy_result 160 | 161 | on_chain_call_result = 162 | Contract.call( 163 | setup_data.client, 164 | ct_address, 165 | setup_data.source_code, 166 | "add_to_number", 167 | ["33"] 168 | ) 169 | 170 | assert match?({:ok, %{return_value: _, return_type: "ok"}}, on_chain_call_result) 171 | 172 | {:ok, %{return_value: _, return_type: "ok"}} = on_chain_call_result 173 | 174 | assert {:error, 175 | {:badmatch, 176 | <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 177 | 0, 0, 0, 178 | 75>>}} == 179 | Contract.decode_return_value( 180 | "list(int)", 181 | "cb_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEvrXnzA", 182 | "ok" 183 | ) 184 | end 185 | end 186 | -------------------------------------------------------------------------------- /test/core_generalized_account_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CoreGeneralizedAccountTest do 2 | use ExUnit.Case 3 | 4 | alias AeppSDK.{Account, GeneralizedAccount} 5 | 6 | setup_all do 7 | Code.require_file("test_utils.ex", "test/") 8 | TestUtils.get_test_data() 9 | end 10 | 11 | @tag :travis_test 12 | test "attach and spend with auth, attempt to spend with failing auth", setup_data do 13 | {:ok, _} = 14 | Account.spend( 15 | setup_data.client, 16 | setup_data.auth_client.keypair.public, 17 | 100_000_000_000_000_000 18 | ) 19 | 20 | assert match?( 21 | {:error, "Account isn't generalized"}, 22 | Account.spend(setup_data.auth_client, setup_data.client.keypair.public, 100, 23 | auth: [ 24 | auth_contract_source: setup_data.source_code_auth_client, 25 | auth_args: ["true"], 26 | fee: 100_000_000_000_000 27 | ] 28 | ) 29 | ) 30 | 31 | assert match?( 32 | {:ok, _}, 33 | GeneralizedAccount.attach( 34 | setup_data.auth_client, 35 | setup_data.source_code_auth_client, 36 | "auth", 37 | [] 38 | ) 39 | ) 40 | 41 | assert match?( 42 | {:ok, _}, 43 | Account.spend(setup_data.auth_client, setup_data.client.keypair.public, 100, 44 | auth: [ 45 | auth_contract_source: setup_data.source_code_auth_client, 46 | auth_args: ["true"], 47 | fee: 100_000_000_000_000 48 | ] 49 | ) 50 | ) 51 | 52 | assert match?( 53 | {:error, _}, 54 | Account.spend(setup_data.auth_client, setup_data.client.keypair.public, 100, 55 | auth: [ 56 | auth_contract_source: setup_data.source_code_auth_client, 57 | auth_args: ["false"], 58 | fee: 100_000_000_000_000 59 | ] 60 | ) 61 | ) 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/core_listener_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CoreListenerTest do 2 | use ExUnit.Case 3 | 4 | alias AeppSDK.{Account, Chain, Contract, Listener} 5 | 6 | setup_all do 7 | Code.require_file("test_utils.ex", "test/") 8 | TestUtils.get_test_data() 9 | end 10 | 11 | @tag :travis_test 12 | test "start listener, receive messages", setup_data do 13 | {:ok, %{peer_pubkey: peer_pubkey}} = Chain.get_node_info(setup_data.client) 14 | 15 | Listener.start( 16 | ["aenode://#{peer_pubkey}@localhost:3015"], 17 | "my_test", 18 | "kh_2KhFJSdz1BwrvEWe9fFBRBpWoweoaZuTiYLWwUPh21ptuDE8UQ" 19 | ) 20 | 21 | public_key = setup_data.client.keypair.public 22 | 23 | {:ok, %{contract_id: aevm_ct_address}} = 24 | Contract.deploy( 25 | setup_data.client, 26 | setup_data.aevm_source_code, 27 | [], 28 | vm: :aevm 29 | ) 30 | 31 | {:ok, %{contract_id: fate_ct_address}} = 32 | Contract.deploy( 33 | setup_data.client, 34 | setup_data.fate_source_code, 35 | [] 36 | ) 37 | 38 | Listener.subscribe_for_contract_events(setup_data.client, self(), aevm_ct_address) 39 | 40 | Listener.subscribe_for_contract_events(setup_data.client, self(), fate_ct_address) 41 | 42 | Listener.subscribe_for_contract_events( 43 | setup_data.client, 44 | self(), 45 | aevm_ct_address, 46 | "SomeEvent", 47 | [ 48 | :bool, 49 | :bits, 50 | :bytes 51 | ] 52 | ) 53 | 54 | Listener.subscribe_for_contract_events( 55 | setup_data.client, 56 | self(), 57 | fate_ct_address, 58 | "SomeEvent", 59 | [ 60 | :string 61 | ] 62 | ) 63 | 64 | Listener.subscribe_for_contract_events( 65 | setup_data.client, 66 | self(), 67 | aevm_ct_address, 68 | "AnotherEvent", 69 | [:address, :oracle, :oracle_query] 70 | ) 71 | 72 | Listener.subscribe_for_contract_events( 73 | setup_data.client, 74 | self(), 75 | fate_ct_address, 76 | "AnotherEvent", 77 | [:string] 78 | ) 79 | 80 | {:ok, %{return_type: "ok"}} = 81 | Contract.call( 82 | setup_data.client, 83 | aevm_ct_address, 84 | setup_data.aevm_source_code, 85 | "emit_event", 86 | [] 87 | ) 88 | 89 | {:ok, %{return_type: "ok"}} = 90 | Contract.call( 91 | setup_data.client, 92 | fate_ct_address, 93 | setup_data.aevm_source_code, 94 | "emit_event", 95 | [] 96 | ) 97 | 98 | Listener.subscribe(:key_blocks, self()) 99 | Listener.subscribe(:micro_blocks, self()) 100 | Listener.subscribe(:transactions, self()) 101 | Listener.subscribe(:pool_transactions, self()) 102 | Listener.subscribe(:spend_transactions, self(), public_key) 103 | Listener.subscribe(:pool_spend_transactions, self(), public_key) 104 | 105 | Account.spend(setup_data.client, public_key, 100) 106 | 107 | # receive one of each of the events that we've subscribed to, 108 | # we don't know the order in which the messages have been sent 109 | Enum.each(0..9, fn _ -> 110 | receive_and_check_message(public_key, setup_data.client, aevm_ct_address, fate_ct_address) 111 | end) 112 | 113 | :ok = Listener.stop() 114 | end 115 | 116 | defp receive_and_check_message(public_key, client, aevm_contract_address, fate_contract_address) do 117 | receive do 118 | message -> 119 | case message do 120 | {:transactions, txs} -> 121 | assert :ok = check_txs(txs, public_key, client, false) 122 | 123 | {:pool_transactions, txs} -> 124 | assert :ok = check_txs(txs, public_key, client, false) 125 | 126 | {:spend_transactions, ^public_key, txs} -> 127 | assert :ok = check_txs(txs, public_key, client, true) 128 | 129 | {:pool_spend_transactions, ^public_key, txs} -> 130 | assert :ok = check_txs(txs, public_key, client, false) 131 | 132 | {:key_blocks, _} -> 133 | :ok 134 | 135 | {:micro_blocks, _} -> 136 | :ok 137 | 138 | {:tx_confirmations, %{status: :confirmed}} -> 139 | :ok 140 | 141 | {:contract_events, 142 | [ 143 | %{ 144 | address: ^aevm_contract_address, 145 | data: "" 146 | }, 147 | %{ 148 | address: ^aevm_contract_address, 149 | data: "" 150 | } 151 | ]} -> 152 | :ok 153 | 154 | {:contract_events, 155 | [ 156 | %{ 157 | address: ^fate_contract_address, 158 | data: "another event" 159 | }, 160 | %{ 161 | address: ^fate_contract_address, 162 | data: "some event" 163 | } 164 | ]} -> 165 | :ok 166 | 167 | {:contract_events, "SomeEvent", 168 | [ 169 | %{ 170 | address: ^aevm_contract_address, 171 | data: "", 172 | topics: [ 173 | "SomeEvent", 174 | true, 175 | 115_792_089_237_316_195_423_570_985_008_687_907_853_269_984_665_640_564_039_457_584_007_913_129_639_935, 176 | 81_985_529_216_486_895 177 | ] 178 | } 179 | ]} -> 180 | :ok 181 | 182 | {:contract_events, "SomeEvent", 183 | [ 184 | %{ 185 | address: ^fate_contract_address, 186 | data: "some event", 187 | topics: [ 188 | "SomeEvent" 189 | ] 190 | } 191 | ]} -> 192 | :ok 193 | 194 | {:contract_events, "AnotherEvent", 195 | [ 196 | %{ 197 | address: ^aevm_contract_address, 198 | data: "", 199 | topics: [ 200 | "AnotherEvent", 201 | "ak_2bKhoFWgQ9os4x8CaeDTHZRGzUcSwcXYUrM12gZHKTdyreGRgG", 202 | "ok_2YNyxd6TRJPNrTcEDCe9ra59SVUdp9FR9qWC5msKZWYD9bP9z5", 203 | "oq_2oRvyowJuJnEkxy58Ckkw77XfWJrmRgmGaLzhdqb67SKEL1gPY" 204 | ] 205 | } 206 | ]} -> 207 | :ok 208 | 209 | {:contract_events, "AnotherEvent", 210 | [ 211 | %{ 212 | address: ^fate_contract_address, 213 | data: "another event", 214 | topics: [ 215 | "AnotherEvent" 216 | ] 217 | } 218 | ]} -> 219 | :ok 220 | 221 | _res -> 222 | flunk("Received invalid message") 223 | end 224 | 225 | Listener.unsubscribe(elem(message, 0), self()) 226 | after 227 | 45_000 -> flunk("Didn't receive message") 228 | end 229 | end 230 | 231 | defp check_txs(txs, public_key, client, check_confirmations) do 232 | case txs do 233 | %{ 234 | hash: hash, 235 | tx: %{ 236 | sender_id: ^public_key, 237 | recipient_id: ^public_key, 238 | amount: 100, 239 | ttl: 0, 240 | payload: "", 241 | type: :spend_tx 242 | } 243 | } -> 244 | if check_confirmations do 245 | Listener.check_tx_confirmations(client, hash, 1, self()) 246 | end 247 | 248 | :ok 249 | 250 | _ -> 251 | :error 252 | end 253 | end 254 | end 255 | -------------------------------------------------------------------------------- /test/core_naming_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CoreNamingTest do 2 | use ExUnit.Case 3 | 4 | alias AeppSDK.{Account, AENS} 5 | alias AeppSDK.Utils.{Keys, Serialization} 6 | alias AeternityNode.Api.NameService 7 | 8 | @test_name "a1234567890asdfghjkl.chain" 9 | @new_test_name "newa1234567890asdfghjkl.chain" 10 | setup_all do 11 | Code.require_file("test_utils.ex", "test/") 12 | TestUtils.get_test_data() 13 | end 14 | 15 | @tag :travis_test 16 | test "naming workflow", setup do 17 | # Pre-claim a name 18 | pre_claim = AENS.preclaim(setup.client, @test_name) 19 | assert match?({:ok, _}, pre_claim) 20 | 21 | # Claim a name 22 | {:ok, pre_claim_info} = pre_claim 23 | claim = AENS.claim(setup.client, @test_name, pre_claim_info.name_salt) 24 | assert match?({:ok, _}, claim) 25 | 26 | # Update a name 27 | list_of_pointers = [ 28 | {Keys.public_key_to_binary("ak_6A2vcm1Sz6aqJezkLCssUXcyZTX7X8D5UwbuS2fRJr9KkYpRU"), 29 | Serialization.id_to_record( 30 | Keys.public_key_to_binary("ak_6A2vcm1Sz6aqJezkLCssUXcyZTX7X8D5UwbuS2fRJr9KkYpRU"), 31 | :account 32 | )} 33 | ] 34 | 35 | update = 36 | AENS.update_name( 37 | setup.client, 38 | @test_name, 39 | name_ttl: 49_999, 40 | pointers: list_of_pointers, 41 | client_ttl: 50_000 42 | ) 43 | 44 | assert match?({:ok, _}, update) 45 | 46 | # Spending to another account, in order to transfer a name to it 47 | spend = 48 | Account.spend( 49 | %{setup.client | gas_price: 1_000_000_000_000}, 50 | setup.valid_pub_key, 51 | setup.amount 52 | ) 53 | 54 | assert match?({:ok, _}, spend) 55 | 56 | # Transfer a name to another account 57 | transfer = AENS.transfer_name(setup.client, @test_name, setup.valid_pub_key) 58 | 59 | assert match?({:ok, _}, transfer) 60 | 61 | # Pre-claim a new name (name salt = 888) 62 | pre_claim_new = AENS.preclaim(setup.client, @new_test_name, salt: 888) 63 | assert match?({:ok, _}, pre_claim_new) 64 | 65 | # Claim a new name 66 | claim_new = AENS.claim(setup.client, @new_test_name, 888) 67 | assert match?({:ok, _}, claim_new) 68 | 69 | # Revoke a new name 70 | revoke_new = AENS.revoke_name(setup.client, @new_test_name) 71 | assert match?({:ok, _}, revoke_new) 72 | end 73 | 74 | @tag :travis_test 75 | test "test naming workflow using pipe operator ", setup do 76 | # Pre-claim with claim (autogenerated salt) 77 | {:ok, claim_result} = 78 | setup.client 79 | |> AENS.preclaim("newname" <> @test_name) 80 | |> AENS.claim() 81 | 82 | # Update 83 | update_result = 84 | {:ok, claim_result} 85 | |> AENS.update() 86 | 87 | assert match?({:ok, _}, update_result) 88 | 89 | # Transfer 90 | transfer_result = 91 | {:ok, claim_result} 92 | |> AENS.transfer(setup.valid_pub_key) 93 | 94 | assert match?({:ok, _}, transfer_result) 95 | 96 | # Revoke 97 | # New preclaim, claim and revoke 98 | {:ok, new_revoke_result} = 99 | setup.client 100 | |> AENS.preclaim("newname1" <> @new_test_name) 101 | |> AENS.claim() 102 | |> AENS.revoke() 103 | 104 | assert match?( 105 | {:ok, %{reason: "Name revoked"}}, 106 | NameService.get_name_entry_by_name( 107 | setup.client.connection, 108 | new_revoke_result.name 109 | ) 110 | ) 111 | end 112 | 113 | @tag :travis_test 114 | test "test naming error handling", setup do 115 | # Claim a new name 116 | assert {:ok, _claim_result} = 117 | setup.client 118 | |> AENS.preclaim("newname2" <> @test_name) 119 | |> AENS.claim() 120 | 121 | # Try to claim again already claimed name 122 | assert {:error, _claim_result} = 123 | setup.client 124 | |> AENS.preclaim("newname2" <> @test_name) 125 | |> AENS.claim() 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /test/core_oracle_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CoreOracleTest do 2 | use ExUnit.Case 3 | 4 | alias AeppSDK.Oracle 5 | 6 | setup_all do 7 | Code.require_file("test_utils.ex", "test/") 8 | TestUtils.get_test_data() 9 | end 10 | 11 | @tag :travis_test 12 | test "register, query, respond, extend, get oracle, get queries", setup_data do 13 | {:ok, %{oracle_id: oracle_id}} = 14 | register = 15 | Oracle.register( 16 | setup_data.client, 17 | "map(string, int)", 18 | "map(string, int)", 19 | %{type: :relative, value: 500}, 20 | 30, 21 | abi_version: 1 22 | ) 23 | 24 | assert match?({:ok, _}, register) 25 | 26 | {:ok, %{query_id: query_id}} = 27 | query = 28 | Oracle.query( 29 | setup_data.client, 30 | oracle_id, 31 | %{"a" => 1}, 32 | %{type: :relative, value: 100}, 33 | 100 34 | ) 35 | 36 | {:ok, queries} = Oracle.get_queries(setup_data.client, oracle_id) 37 | 38 | assert length(queries) == 1 39 | 40 | assert match?( 41 | {:ok, %{query: %{"a" => 1}}}, 42 | Oracle.get_query(setup_data.client, oracle_id, query_id) 43 | ) 44 | 45 | assert match?({:ok, _}, query) 46 | 47 | assert match?( 48 | {:ok, _}, 49 | Oracle.respond( 50 | setup_data.client, 51 | oracle_id, 52 | query_id, 53 | %{"b" => 2}, 54 | 100 55 | ) 56 | ) 57 | 58 | assert match?( 59 | {:ok, _}, 60 | Oracle.extend(setup_data.client, oracle_id, 100) 61 | ) 62 | 63 | assert match?({:ok, _}, Oracle.get_oracle(setup_data.client, oracle_id)) 64 | end 65 | 66 | @tag :travis_test 67 | test "get oracle queries with bad oracle_id", setup_data do 68 | assert match?( 69 | {:error, _}, 70 | Oracle.get_queries(setup_data.client, setup_data.client.keypair.public) 71 | ) 72 | end 73 | 74 | @tag :travis_test 75 | test "get oracle query with bad query_id", setup_data do 76 | assert match?( 77 | {:error, _}, 78 | Oracle.get_query( 79 | setup_data.client, 80 | String.replace_prefix(setup_data.client.keypair.public, "ak", "ok"), 81 | "oq_123" 82 | ) 83 | ) 84 | end 85 | 86 | @tag :travis_test 87 | test "register oracle with bad formats", setup_data do 88 | assert match?( 89 | {:error, "Bad Sophia type: bad format"}, 90 | Oracle.register( 91 | setup_data.client, 92 | "bad format", 93 | "bad format", 94 | %{type: :relative, value: 30}, 95 | 30, 96 | abi_version: 1 97 | ) 98 | ) 99 | end 100 | 101 | @tag :travis_test 102 | test "query non-existent oracle", setup_data do 103 | assert match?( 104 | {:error, _}, 105 | Oracle.query( 106 | setup_data.client, 107 | "ok_123", 108 | "a query", 109 | %{type: :relative, value: 10}, 110 | 10 111 | ) 112 | ) 113 | end 114 | 115 | @tag :travis_test 116 | test "respond to non-existent query", setup_data do 117 | assert match?( 118 | {:error, _}, 119 | Oracle.respond( 120 | setup_data.client, 121 | String.replace_prefix(setup_data.client.keypair.public, "ak", "ok"), 122 | String.replace_prefix(setup_data.client.keypair.public, "ak", "oq"), 123 | %{"b" => 2}, 124 | 10 125 | ) 126 | ) 127 | end 128 | 129 | @tag :travis_test 130 | test "extend non-existent oracle", setup_data do 131 | assert match?( 132 | {:error, _}, 133 | Oracle.extend( 134 | setup_data.client, 135 | "ok_Aro7GgyG3gJ7Tsu4k4YvZ45P1GtNfMyRX4Xfv8VWDjbvLDphN", 136 | 10 137 | ) 138 | ) 139 | end 140 | 141 | @tag :travis_test 142 | test "get non-existent oracle", setup_data do 143 | assert match?( 144 | {:error, _}, 145 | Oracle.get_oracle( 146 | setup_data.client, 147 | "ok_Aro7GgyG3gJ7Tsu4k4YvZ45P1GtNfMyRX4Xfv8VWDjbvLDphN" 148 | ) 149 | ) 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /test/serialization_test.exs: -------------------------------------------------------------------------------- 1 | defmodule UtilsSerializationTest do 2 | use ExUnit.Case 3 | 4 | alias AeppSDK.Utils.Serialization 5 | 6 | setup_all do 7 | Code.require_file("test_utils.ex", "test/") 8 | TestUtils.get_test_data() 9 | end 10 | 11 | test "serialization of valid fields doesn't raise error", fields do 12 | Serialization.serialize(fields.spend_fields, :spend_tx) 13 | Serialization.serialize(fields.oracle_register_fields, :oracle_register_tx) 14 | Serialization.serialize(fields.oracle_query_fields, :oracle_query_tx) 15 | Serialization.serialize(fields.oracle_response_fields, :oracle_response_tx) 16 | Serialization.serialize(fields.oracle_extend_fields, :oracle_extend_tx) 17 | Serialization.serialize(fields.name_claim_fields, :name_claim_tx) 18 | Serialization.serialize(fields.name_preclaim_fields, :name_preclaim_tx) 19 | Serialization.serialize(fields.name_update_fields, :name_update_tx) 20 | Serialization.serialize(fields.name_revoke_fields, :name_revoke_tx) 21 | Serialization.serialize(fields.name_transfer_fields, :name_transfer_tx) 22 | Serialization.serialize(fields.contract_create_fields, :contract_create_tx) 23 | Serialization.serialize(fields.contract_call_fields, :contract_call_tx) 24 | end 25 | 26 | test "serialization of invalid fields raises error", fields do 27 | assert_raise ErlangError, 28 | "Erlang error: {:illegal_field, :sender_id, :id, \"invalid account\", :id, \"invalid account\"}", 29 | fn -> 30 | Serialization.serialize( 31 | List.replace_at(fields.spend_fields, 0, "invalid account"), 32 | :spend_tx 33 | ) 34 | end 35 | 36 | assert_raise ErlangError, 37 | "Erlang error: {:illegal_field, :amount, :int, \"invalid amount\", :int, \"invalid amount\"}", 38 | fn -> 39 | Serialization.serialize( 40 | List.replace_at(fields.spend_fields, 2, "invalid amount"), 41 | :spend_tx 42 | ) 43 | end 44 | 45 | assert_raise ErlangError, 46 | "Erlang error: {:illegal_field, :payload, :binary, 0, :binary, 0}", 47 | fn -> 48 | Serialization.serialize( 49 | List.replace_at(fields.spend_fields, 6, 0), 50 | :spend_tx 51 | ) 52 | end 53 | 54 | assert_raise ErlangError, 55 | "Erlang error: {:illegal_field, :pointers, [binary: :id], [{1, 2}, {\"a\", \"b\"}], :binary, 1}", 56 | fn -> 57 | Serialization.serialize( 58 | List.replace_at(fields.name_update_fields, 4, [{1, 2}, {"a", "b"}]), 59 | :name_update_tx 60 | ) 61 | end 62 | end 63 | 64 | test "valid serialization of transactions", fields do 65 | Serialization.serialize(fields.spend_tx) 66 | Serialization.serialize(fields.oracle_register_tx) 67 | Serialization.serialize(fields.oracle_respond_tx) 68 | Serialization.serialize(fields.oracle_query_tx) 69 | Serialization.serialize(fields.oracle_extend_tx) 70 | Serialization.serialize(fields.name_pre_claim_tx) 71 | Serialization.serialize(fields.name_claim_tx) 72 | Serialization.serialize(fields.name_revoke_tx) 73 | Serialization.serialize(fields.name_update_tx) 74 | Serialization.serialize(fields.name_transfer_tx) 75 | Serialization.serialize(fields.contract_create_tx) 76 | Serialization.serialize(fields.contract_call_tx) 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/test_utils.ex: -------------------------------------------------------------------------------- 1 | defmodule TestUtils do 2 | alias AeppSDK.Client 3 | alias AeppSDK.Utils.Serialization 4 | 5 | alias AeternityNode.Model.{ 6 | ContractCallTx, 7 | ContractCreateTx, 8 | NameClaimTx, 9 | NamePreclaimTx, 10 | NameRevokeTx, 11 | NameTransferTx, 12 | NameUpdateTx, 13 | OracleExtendTx, 14 | OracleQueryTx, 15 | OracleRegisterTx, 16 | OracleRespondTx, 17 | RelativeTtl, 18 | SpendTx, 19 | Ttl 20 | } 21 | 22 | def get_test_data do 23 | account_id = Serialization.id_to_record(<<0::256>>, :account) 24 | oracle_id = Serialization.id_to_record(<<0::256>>, :oracle) 25 | commitment_id = Serialization.id_to_record(<<0::256>>, :commitment) 26 | name_id = Serialization.id_to_record(<<0::256>>, :name) 27 | contract_id = Serialization.id_to_record(<<0::256>>, :contract) 28 | 29 | spend_fields = [ 30 | account_id, 31 | account_id, 32 | 10, 33 | 10, 34 | 10, 35 | 10, 36 | <<"payload">> 37 | ] 38 | 39 | oracle_register_fields = [ 40 | account_id, 41 | 10, 42 | <<"query_format">>, 43 | <<"response_format">>, 44 | 10, 45 | 1, 46 | 10, 47 | 10, 48 | 10, 49 | 1 50 | ] 51 | 52 | oracle_query_fields = [ 53 | account_id, 54 | 10, 55 | oracle_id, 56 | <<"query">>, 57 | 10, 58 | 1, 59 | 10, 60 | 1, 61 | 10, 62 | 10, 63 | 10 64 | ] 65 | 66 | oracle_response_fields = [ 67 | oracle_id, 68 | 10, 69 | <<0::256>>, 70 | <<"response">>, 71 | 1, 72 | 10, 73 | 10, 74 | 10 75 | ] 76 | 77 | oracle_extend_fields = [ 78 | oracle_id, 79 | 10, 80 | 1, 81 | 10, 82 | 10, 83 | 10 84 | ] 85 | 86 | name_claim_fields = [ 87 | account_id, 88 | 10, 89 | <<"name">>, 90 | 10, 91 | 10, 92 | 10, 93 | 10 94 | ] 95 | 96 | name_preclaim_fields = [ 97 | account_id, 98 | 10, 99 | commitment_id, 100 | 10, 101 | 10 102 | ] 103 | 104 | name_update_fields = [ 105 | account_id, 106 | 10, 107 | name_id, 108 | 10, 109 | [{<<1>>, name_id}], 110 | 10, 111 | 10, 112 | 10 113 | ] 114 | 115 | name_revoke_fields = [ 116 | account_id, 117 | 10, 118 | name_id, 119 | 10, 120 | 10 121 | ] 122 | 123 | name_transfer_fields = [ 124 | account_id, 125 | 10, 126 | name_id, 127 | account_id, 128 | 10, 129 | 10 130 | ] 131 | 132 | contract_create_fields = [ 133 | account_id, 134 | 10, 135 | <<"code">>, 136 | 10, 137 | 10, 138 | 10, 139 | 10, 140 | 10, 141 | 10, 142 | 10, 143 | <<"call data">> 144 | ] 145 | 146 | contract_call_fields = [ 147 | account_id, 148 | 10, 149 | contract_id, 150 | 10, 151 | 10, 152 | 10, 153 | 10, 154 | 10, 155 | 10, 156 | <<"call data">> 157 | ] 158 | 159 | spend_tx = %SpendTx{ 160 | amount: 5_018_857_520_000_000_000, 161 | fee: 0, 162 | nonce: 37_181, 163 | payload: "", 164 | recipient_id: "ak_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 165 | sender_id: "ak_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 166 | ttl: 0 167 | } 168 | 169 | oracle_register_tx = %OracleRegisterTx{ 170 | query_format: <<"query_format">>, 171 | response_format: <<"response_format">>, 172 | query_fee: 10, 173 | oracle_ttl: %Ttl{type: :absolute, value: 10}, 174 | account_id: "ak_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 175 | nonce: 37_122, 176 | fee: 0, 177 | ttl: 10, 178 | abi_version: 0x30001 179 | } 180 | 181 | oracle_respond_tx = %OracleRespondTx{ 182 | query_id: "oq_u7sgmMQNjZQ4ffsN9sSmEhzqsag1iEfx8SkHDeG1y8EbDB5Aq", 183 | response: <<"response_format">>, 184 | response_ttl: %RelativeTtl{type: :relative, value: 10}, 185 | fee: 0, 186 | ttl: 10, 187 | oracle_id: "ok_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 188 | nonce: 0 189 | } 190 | 191 | oracle_query_tx = %OracleQueryTx{ 192 | oracle_id: "ok_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 193 | query: <<"query">>, 194 | query_fee: 10, 195 | query_ttl: %Ttl{type: :relative, value: 10}, 196 | response_ttl: %RelativeTtl{type: "delta", value: 10}, 197 | fee: 0, 198 | ttl: 10, 199 | sender_id: "ak_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 200 | nonce: 0 201 | } 202 | 203 | oracle_extend_tx = %OracleExtendTx{ 204 | fee: 0, 205 | oracle_ttl: %RelativeTtl{type: :relative, value: 10}, 206 | oracle_id: "ok_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 207 | nonce: 0, 208 | ttl: 0 209 | } 210 | 211 | name_pre_claim_tx = %NamePreclaimTx{ 212 | commitment_id: "cm_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 213 | fee: 0, 214 | ttl: 0, 215 | account_id: "ak_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 216 | nonce: 0 217 | } 218 | 219 | name_claim_tx = %NameClaimTx{ 220 | name: "nm_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 221 | name_salt: 123, 222 | fee: 0, 223 | name_fee: 10, 224 | ttl: 0, 225 | account_id: "ak_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 226 | nonce: 0 227 | } 228 | 229 | name_revoke_tx = %NameRevokeTx{ 230 | name_id: "nm_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 231 | fee: 0, 232 | ttl: 0, 233 | account_id: "ak_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 234 | nonce: 0 235 | } 236 | 237 | name_transfer_tx = %NameTransferTx{ 238 | name_id: "nm_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 239 | recipient_id: "ak_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 240 | fee: 0, 241 | ttl: 0, 242 | account_id: "ak_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 243 | nonce: 0 244 | } 245 | 246 | name_update_tx = %NameUpdateTx{ 247 | name_id: "nm_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 248 | name_ttl: 0, 249 | pointers: [], 250 | client_ttl: 0, 251 | fee: 0, 252 | ttl: 0, 253 | account_id: "ak_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 254 | nonce: 0 255 | } 256 | 257 | contract_create_tx = %ContractCreateTx{ 258 | owner_id: "ak_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 259 | nonce: 0, 260 | code: "contract Identity = 261 | record state = { number : int } 262 | entrypoint init(x : int) = 263 | { number = x } 264 | entrypoint add_to_number(x : int) = state.number + x", 265 | abi_version: 0x30001, 266 | deposit: 1_000, 267 | amount: 1_000, 268 | gas: 10, 269 | gas_price: 1, 270 | fee: 0, 271 | ttl: 0, 272 | call_data: "call_data" 273 | } 274 | 275 | contract_call_tx = %ContractCallTx{ 276 | caller_id: "ct_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 277 | nonce: 0, 278 | contract_id: "ct_542o93BKHiANzqNaFj6UurrJuDuxU61zCGr9LJCwtTUg34kWt", 279 | abi_version: 0x30001, 280 | fee: 0, 281 | ttl: 0, 282 | amount: 1_000, 283 | gas: 10, 284 | gas_price: 1, 285 | call_data: "call_data" 286 | } 287 | 288 | client = 289 | Client.new( 290 | %{ 291 | public: "ak_6A2vcm1Sz6aqJezkLCssUXcyZTX7X8D5UwbuS2fRJr9KkYpRU", 292 | secret: 293 | "a7a695f999b1872acb13d5b63a830a8ee060ba688a478a08c6e65dfad8a01cd70bb4ed7927f97b51e1bcb5e1340d12335b2a2b12c8bc5221d63c4bcb39d41e61" 294 | }, 295 | "my_test", 296 | "http://localhost:3013/v2", 297 | "http://localhost:3113/v2" 298 | ) 299 | 300 | auth_client = 301 | Client.new( 302 | %{ 303 | public: "ak_wuLXPE5pd2rvFoxHxvenBgp459rW6Y1cZ6cYTZcAcLAevPE5M", 304 | secret: 305 | "799ef7aa9ed8e3d58cd2492b7a569ccf967f3b63dc49ac2d0c9ea916d29cf8387ca99a8cd824b2a3efc3c6c5d500585713430575d4ce6999b202cb20f86019d8" 306 | }, 307 | "my_test", 308 | "http://localhost:3013/v2", 309 | "http://localhost:3113/v2" 310 | ) 311 | 312 | source_code = "contract Identity = 313 | datatype event = AddedNumberEvent(indexed int, string) 314 | 315 | record state = { number : int } 316 | 317 | entrypoint init(x : int) = 318 | { number = x } 319 | 320 | entrypoint get_number() = 321 | state.number 322 | 323 | stateful entrypoint add_to_number(x : int) = 324 | Chain.event(AddedNumberEvent(x, \"Added a number\")) 325 | state.number + x" 326 | 327 | source_code_auth_client = "contract Authorization = 328 | 329 | entrypoint auth(auth_value : bool) = 330 | auth_value" 331 | 332 | {:ok, fate_source_code} = File.read("fate_contract.sophia") 333 | {:ok, aevm_source_code} = File.read("aevm_contract.sophia") 334 | valid_pub_key = "ak_nv5B93FPzRHrGNmMdTDfGdd5xGZvep3MVSpJqzcQmMp59bBCv" 335 | amount = 40_000_000 336 | 337 | [ 338 | spend_fields: spend_fields, 339 | oracle_register_fields: oracle_register_fields, 340 | oracle_query_fields: oracle_query_fields, 341 | oracle_response_fields: oracle_response_fields, 342 | oracle_extend_fields: oracle_extend_fields, 343 | name_claim_fields: name_claim_fields, 344 | name_preclaim_fields: name_preclaim_fields, 345 | name_revoke_fields: name_revoke_fields, 346 | name_update_fields: name_update_fields, 347 | name_transfer_fields: name_transfer_fields, 348 | contract_create_fields: contract_create_fields, 349 | contract_call_fields: contract_call_fields, 350 | spend_tx: spend_tx, 351 | oracle_register_tx: oracle_register_tx, 352 | oracle_respond_tx: oracle_respond_tx, 353 | oracle_query_tx: oracle_query_tx, 354 | oracle_extend_tx: oracle_extend_tx, 355 | name_pre_claim_tx: name_pre_claim_tx, 356 | name_claim_tx: name_claim_tx, 357 | name_revoke_tx: name_revoke_tx, 358 | name_transfer_tx: name_transfer_tx, 359 | name_update_tx: name_update_tx, 360 | contract_create_tx: contract_create_tx, 361 | contract_call_tx: contract_call_tx, 362 | client: client, 363 | valid_pub_key: valid_pub_key, 364 | amount: amount, 365 | source_code: source_code, 366 | auth_client: auth_client, 367 | source_code_auth_client: source_code_auth_client, 368 | fate_source_code: fate_source_code, 369 | aevm_source_code: aevm_source_code 370 | ] 371 | end 372 | end 373 | -------------------------------------------------------------------------------- /test/transaction_util_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TransactionUtilTest do 2 | use ExUnit.Case 3 | 4 | alias AeppSDK.Account 5 | alias AeppSDK.Utils.Transaction 6 | 7 | setup_all do 8 | Code.require_file("test_utils.ex", "test/") 9 | TestUtils.get_test_data() 10 | end 11 | 12 | test "minimum fee calculation", fields do 13 | assert 16_740_000_000 == Transaction.calculate_min_fee(fields.spend_tx, 50_000, "ae_mainnet") 14 | assert 16_581 == Transaction.calculate_min_fee(fields.oracle_register_tx, 5, "ae_mainnet") 15 | 16 | assert 16_842_000_000 == 17 | Transaction.calculate_min_fee(fields.oracle_respond_tx, 50_000, "ae_mainnet") 18 | 19 | assert 16_722_000_000 == 20 | Transaction.calculate_min_fee(fields.oracle_query_tx, 50_000, "ae_mainnet") 21 | 22 | assert 15_842_000_000 == 23 | Transaction.calculate_min_fee(fields.oracle_extend_tx, 50_000, "ae_mainnet") 24 | 25 | assert 16_500_000_000 == 26 | Transaction.calculate_min_fee(fields.name_pre_claim_tx, 50_000, "ae_mainnet") 27 | 28 | assert 16_920_000_000 == 29 | Transaction.calculate_min_fee(fields.name_claim_tx, 50_000, "ae_mainnet") 30 | 31 | assert 16_500_000_000 == 32 | Transaction.calculate_min_fee(fields.name_revoke_tx, 50_000, "ae_mainnet") 33 | 34 | assert 16_560_000_000 == 35 | Transaction.calculate_min_fee(fields.name_update_tx, 50_000, "ae_mainnet") 36 | 37 | assert 17_180_000_000 == 38 | Transaction.calculate_min_fee(fields.name_transfer_tx, 50_000, "ae_mainnet") 39 | 40 | assert 80_480_000_000 == 41 | Transaction.calculate_min_fee(fields.contract_create_tx, 50_000, "ae_mainnet") 42 | 43 | assert 451_880_000_000 == 44 | Transaction.calculate_min_fee(fields.contract_call_tx, 50_000, "ae_mainnet") 45 | end 46 | 47 | @tag :travis_test 48 | test "post valid spend transaction", fields do 49 | assert match?( 50 | {:ok, %{}}, 51 | Account.spend( 52 | fields.client, 53 | fields.valid_pub_key, 54 | fields.amount 55 | ) 56 | ) 57 | end 58 | 59 | @tag :travis_test 60 | test "post valid spend transaction by given gas price", fields do 61 | assert {:ok, %{}} = 62 | Account.spend( 63 | %{fields.client | gas_price: 1_000_000_000_000}, 64 | fields.valid_pub_key, 65 | fields.amount 66 | ) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/utils_keys_test.exs: -------------------------------------------------------------------------------- 1 | defmodule UtilsKeysTest do 2 | use ExUnit.Case 3 | 4 | doctest AeppSDK.Utils.Keys, 5 | except: [generate_keypair: 0, read_keypair: 3, read_keystore: 2, new_keystore: 3] 6 | 7 | alias AeppSDK.Utils.Keys 8 | 9 | @keys_path "./keys" 10 | @keystore_name "keystore.json" 11 | 12 | setup_all do 13 | js_cli_keypair = %{ 14 | public: "ak_eCuh2MyVXxAVZddjmJhEv2oSu1Z7AQfHbf7CHb5J8CyqSNS7N", 15 | secret: 16 | "4886693515ada9acf24df75cb6b07c6515d9746446bcd6b3b90cbd5f220cb738547aae90e8660a70228eabcf666ad71123a44b45664d0b91c0566613914b0c8d" 17 | } 18 | 19 | # test a keypair generated by the JS cli and one generated by the Keys module 20 | keys1 = bundle_keys(js_cli_keypair) 21 | keys2 = bundle_keys(Keys.generate_keypair()) 22 | 23 | on_exit(fn -> 24 | File.rm_rf!(@keys_path) 25 | File.rm_rf!(@keystore_name) 26 | end) 27 | 28 | [ 29 | {:bundled_keys, [keys1, keys2]} 30 | ] 31 | end 32 | 33 | test "save and read keys", setup_data do 34 | Enum.each(setup_data.bundled_keys, fn keys -> 35 | assert :ok == Keys.save_keypair(keys.keypair, "password123", @keys_path, "keypair1") 36 | assert {:ok, keys.keypair} == Keys.read_keypair("password123", @keys_path, "keypair1") 37 | 38 | # non-existent keys 39 | assert match?({:error, _reason}, Keys.read_keypair("password123", @keys_path, "keypair123")) 40 | 41 | # invalid file name 42 | assert match?( 43 | {:error, _reason}, 44 | Keys.save_keypair(keys.keypair, "password123", @keys_path, "") 45 | ) 46 | end) 47 | end 48 | 49 | test "signing", setup_data do 50 | Enum.each(setup_data.bundled_keys, fn keys -> 51 | signature = Keys.sign("message123", keys.secret_key_binary) 52 | assert {:ok, "message123"} == Keys.verify(signature, "message123", keys.public_key_binary) 53 | end) 54 | end 55 | 56 | test "encoding", setup_data do 57 | Enum.each(setup_data.bundled_keys, fn keys -> 58 | assert keys.public == Keys.public_key_from_binary(keys.public_key_binary) 59 | assert keys.secret == Keys.secret_key_from_binary(keys.secret_key_binary) 60 | end) 61 | end 62 | 63 | test "keystore create, read", _setup_data do 64 | %{secret: secret_key} = Keys.generate_keypair() 65 | keystore_password = "12345a" 66 | assert :ok = Keys.new_keystore(secret_key, keystore_password, name: @keystore_name) 67 | path_to_the_file = Path.join(File.cwd!(), @keystore_name) 68 | assert true = File.exists?(path_to_the_file) 69 | assert secret_key === Keys.read_keystore(path_to_the_file, keystore_password) 70 | end 71 | 72 | defp bundle_keys(%{public: public_key, secret: secret_key} = keypair) do 73 | public_key_binary = Keys.public_key_to_binary(public_key) 74 | secret_key_binary = Keys.secret_key_to_binary(secret_key) 75 | 76 | %{ 77 | keypair: keypair, 78 | public: public_key, 79 | public_key_binary: public_key_binary, 80 | secret: secret_key, 81 | secret_key_binary: secret_key_binary 82 | } 83 | end 84 | end 85 | --------------------------------------------------------------------------------