├── .gitignore
├── LICENSE
├── Notes.elm
├── README.md
├── benchmarks
├── AddressBenchmark.elm
├── DependencyBenchmark.elm
├── IntBenchmark.elm
└── elm-package.json
├── changelog
├── elm-ethereum-logo.svg
├── elm.json
├── examples
├── complex
│ ├── README.md
│ ├── elm.json
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── elm
│ │ │ ├── Contracts
│ │ │ │ └── WidgetFactory.elm
│ │ │ ├── Data
│ │ │ │ └── Chain.elm
│ │ │ ├── Extra
│ │ │ │ └── BigInt.elm
│ │ │ ├── Main.elm
│ │ │ ├── Page
│ │ │ │ ├── Home.elm
│ │ │ │ ├── Login.elm
│ │ │ │ ├── Widget.elm
│ │ │ │ └── WidgetWizard.elm
│ │ │ ├── Ports.elm
│ │ │ ├── Request
│ │ │ │ ├── Chain.elm
│ │ │ │ ├── Status.elm
│ │ │ │ └── UPort.elm
│ │ │ ├── Route.elm
│ │ │ └── Views
│ │ │ │ ├── Helpers.elm
│ │ │ │ └── Styles.elm
│ │ ├── favicon.ico
│ │ ├── solidity
│ │ │ ├── WidgetFactory.abi
│ │ │ └── WidgetFactory.sol
│ │ └── static
│ │ │ ├── img
│ │ │ ├── city_blue_denver.jpg
│ │ │ ├── elm-ethereum-logo.svg
│ │ │ ├── loader.gif
│ │ │ ├── potter-bw.jpg
│ │ │ ├── potter-solo.jpg
│ │ │ └── uport.png
│ │ │ ├── index.html
│ │ │ └── index.js
│ └── webpack.config.js
└── simple
│ ├── README.md
│ ├── elm.json
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ ├── Main.elm
│ ├── assets
│ │ ├── favicon.ico
│ │ └── static
│ │ │ └── img
│ │ │ └── .gitignore
│ └── index.js
│ └── webpack.config.js
├── integration-tests
├── elm.json
└── src
│ ├── ComplexStorage.elm
│ └── Main.elm
├── src
├── Eth.elm
├── Eth
│ ├── Abi
│ │ ├── Decode.elm
│ │ ├── Encode.elm
│ │ └── Int.elm
│ ├── Decode.elm
│ ├── Defaults.elm
│ ├── Encode.elm
│ ├── Net.elm
│ ├── RPC.elm
│ ├── Sentry
│ │ ├── ChainCmd.elm
│ │ ├── Event.elm
│ │ ├── OldEventWS.elm
│ │ ├── Tx.elm
│ │ └── Wallet.elm
│ ├── Types.elm
│ ├── Units.elm
│ └── Utils.elm
├── Internal
│ └── Types.elm
├── Legacy
│ └── Base58.elm
└── Shh.elm
└── tests
├── Address.elm
├── Constants.elm
├── DecodeAbi.elm
├── DecodeAbiBeta.elm
├── EncodeAbi.elm
└── solidity
└── ComplexStorage.sol
/.gitignore:
--------------------------------------------------------------------------------
1 | elm-stuff/
2 | Test.elm
3 | elm.js
4 | .DS_Store
5 | node_modules/
6 | *-notes.*
7 | index.html
8 | .idea/
9 | dist/
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2017-present, Coury Ditch and Nick Miller
4 |
5 | All rights reserved.
6 |
7 | Redistribution and use in source and binary forms, with or without
8 | modification, are permitted provided that the following conditions are met:
9 |
10 | * Redistributions of source code must retain the above copyright
11 | notice, this list of conditions and the following disclaimer.
12 |
13 | * Redistributions in binary form must reproduce the above
14 | copyright notice, this list of conditions and the following
15 | disclaimer in the documentation and/or other materials provided
16 | with the distribution.
17 |
18 | * Neither the name of the copyright holder nor the names of other
19 | contributors may be used to endorse or promote products derived
20 | from this software without specific prior written permission.
21 |
22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
25 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
26 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
27 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
28 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
29 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
30 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
31 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 |
--------------------------------------------------------------------------------
/Notes.elm:
--------------------------------------------------------------------------------
1 | module Notes exposing (andPrepend, callHelper, newWidget, newWidget2)
2 |
3 | import Abi.Encode as AbiEncode
4 | import BigInt exposing (BigInt)
5 | import Eth.Types exposing (Address, Call, Hex, IPFSHash)
6 | import Json.Decode as Decode
7 | import Result.Extra
8 |
9 |
10 | {-| TO-DO
11 |
12 | - Remove dependency on web3.js, and work with common provider format to communicate with RPC
13 |
14 | - Rework Sentry.Event
15 | - initHttp, initWebsocket
16 | - HTTP will have to handle filter installation, clearing, polling.
17 | - withDebug, takes Debug.log from user
18 |
19 | - Rework Abi.Encode
20 | - Support various uint, int, and byte sizes
21 | - Fail upon overflows
22 |
23 | - Use more generic error type
24 | - Helps caputure cases like uint overflows above
25 |
26 | - Update elm-ethereum-generator
27 | - Better parser
28 | - Dynamic types
29 |
30 | -}
31 |
32 |
33 |
34 | -- newWidget : Address -> BigInt -> BigInt -> Address -> Call ()
35 |
36 |
37 | newWidget contractAddress size_ cost_ owner_ =
38 | (AbiEncode.uint 256 size_ :: AbiEncode.uint 256 cost_ :: AbiEncode.address owner_ :: [])
39 | |> Result.Extra.combine
40 | |> Result.map (AbiEncode.functionCall "newWidget(uint256,uint256,address)")
41 | |> Result.map (callHelper contractAddress (Decode.succeed ()))
42 |
43 |
44 | newWidget2 : Address -> BigInt -> BigInt -> Address -> Result String (Call ())
45 | newWidget2 contractAddress size_ cost_ owner_ =
46 | Result.Extra.singleton []
47 | |> andPrepend (AbiEncode.uint 256 size_)
48 | |> andPrepend (AbiEncode.uint 256 cost_)
49 | |> andPrepend (AbiEncode.address owner_)
50 | |> Result.map (AbiEncode.functionCall "newWidget(uint256,uint256,address)")
51 | |> Result.map (callHelper contractAddress (Decode.succeed ()))
52 |
53 |
54 | andPrepend : Result e a -> Result e (List a) -> Result e (List a)
55 | andPrepend =
56 | Result.map2 (::)
57 |
58 |
59 | callHelper : Address -> Decode.Decoder a -> Hex -> Call a
60 | callHelper contractAddress decoder data =
61 | { to = Just contractAddress
62 | , from = Nothing
63 | , gas = Nothing
64 | , gasPrice = Nothing
65 | , value = Nothing
66 | , data = Just data
67 | , nonce = Nothing
68 | , decoder = decoder
69 | }
70 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [](https://github.com/cmditch/elm-ethereum) elm-ethereum
2 |
3 | **Examples:**
4 | [Simple starter example](https://github.com/cmditch/elm-ethereum/tree/master/examples/simple/src/Main.elm)
5 | [Complex example SPA Dapp](https://github.com/cmditch/elm-ethereum/tree/master/examples/complex)
6 |
7 | Cool Feature: See [here](https://github.com/cmditch/elm-ethereum/blob/master/examples/simple/src/Main.elm#L138) how you can easily track the block depth of transactions after they've been mined.
8 |
9 | -----------------------
10 |
11 | This library allows you to interact with the Ethereum blockchain much like `purescript-web3`, `ethers.js`, or `web3.js`.
12 | You can hook into web wallets like MetaMask and send transactions, as well as perform read-only operations on smart contracts.
13 |
14 | See [why elm?](#why-elm)
15 |
16 | ## Setup
17 |
18 | - **Setup** and define your node endpoint.
19 |
20 | ```elm
21 | import Eth
22 | import Eth.Types exposing (..)
23 |
24 |
25 | type alias Model =
26 | { ethNode : HttpProvider }
27 |
28 | init =
29 | { ethNode = "https://mainnet.infura.com/" }
30 | ```
31 |
32 | It's good to keep the node url in your model. This way it can be kept in sync with MetaMask.
33 | Example code of this "sync" pattern to come.
34 |
35 | ## Examples
36 |
37 | - **Simple** - Look at the blockchain
38 |
39 | Get an account balance at a specific block height.
40 |
41 | ```elm
42 | getMyBalanceInHistory : Int -> Task Http.Error BigInt
43 | getMyBalanceInHistory blockNum =
44 | Eth.getBalanceAtBlock model.ethNode myAddress (BlockNum blockNum)
45 | ```
46 |
47 | - **Advanced** - Chain tasks together
48 |
49 | Get all newly created contract addresses in the latest block. In a few lines of code.
50 |
51 | ```elm
52 | findNewestContracts : Task String (List Address)
53 | findNewestContracts =
54 | Eth.getBlockNumber model.ethNode
55 | |> Task.andThen (Eth.getBlock model.ethNode)
56 | |> Task.andThen
57 | (\block ->
58 | block.transactions
59 | |> List.map (Eth.getTxReceipt model.ethNode)
60 | |> Task.sequence
61 | )
62 | |> Task.map (MaybeExtra.values << List.map .contractAddress)
63 | |> Task.mapError prettifyHttpError
64 | ```
65 |
66 | This is an example of [Railway Oriented Programming](https://fsharpforfunandprofit.com/rop/). A [great video](https://vimeo.com/113707214) by Scott Wlaschin.
67 |
68 | ## Why Elm
69 |
70 | I'd sum up the experience of programming in Elm with two words: **Fearless Refactoring**
71 |
72 | This is by no means the only pleasantry the fine tree has to offer.
73 |
74 | Elm's claim to fame is zero runtime exceptions. It's compiler and static types are your best friends. Both from an error catching standpoint, but just as importantly, from a domain modeling standpoint.
75 |
76 | **Union Types** allow you to fully leverage the compiler when modeling your business domain. See [BlockId](http://package.elm-lang.org/packages/cmditch/elm-ethereum/latest/Eth-Types#BlockId) or [NetworkId](http://package.elm-lang.org/packages/cmditch/elm-ethereum/latest/Eth-Net#NetworkId) for instance.
77 |
78 | Union types also allow you to hide implementation details by implementing "opaque types". An [Address](https://github.com/cmditch/elm-ethereum/blob/master/src/Internal/Types.elm#L4) is just a string under the hood, but you can never directly touch that string.
79 |
80 | ### Why else
81 |
82 | - **Simplicity and cohesion**
83 |
84 | ```text
85 | Javascript Elm
86 | ---------------------------------
87 | npm/yarn built in
88 | Webpack built in
89 | React built in
90 | Redux built in
91 | Typescript/Flow built in
92 | Immutable.JS built in
93 | ```
94 |
95 | - **Phenomenal tooling and resources**
96 |
97 | [**Time traveling debugger**](http://elm-lang.org/blog/the-perfect-bug-report) - Import/Export history. QA like a champ.
98 | [**elm-format**](https://github.com/avh4/elm-format) - Adds up to hours of tedius "work" saved.
99 | [**elm-reactor**](https://github.com/elm-lang/elm-reactor) - Nice dev server.
100 | [**elm-test**](http://package.elm-lang.org/packages/elm-community/elm-test/latest) - Fuzz testing == legit.
101 | [**elm-benchmark**](http://package.elm-lang.org/packages/BrianHicks/elm-benchmark/latest) - Clone this package and give it a whirl.
102 | [**Elm Package and Docs**](http://package.elm-lang.org/) - Pleasant and consistent. Enforced semantic versioning.
103 |
104 | - **Strong static types**
105 |
106 | Find errors fast with readable compiler messages.
107 | Less [millions of dollars lost](https://twitter.com/a_ferron/status/892350579162439681?lang=en) from typos.
108 |
109 | - **No null or undefined**
110 |
111 | Never miss a potential problem.
112 |
113 | - **Purely functional**
114 |
115 | Leads to decoupled and easily refactorable code.
116 |
117 | - **Great Community**
118 |
119 | Thoughtful, responsive, intelligent, and kind.
120 | Great [Slack](https://elmlang.herokuapp.com/) and [Discourse](https://discourse.elm-lang.org/).
121 |
122 | ## Contributing
123 |
124 | Pull requests and issues are greatly appreciated!
125 | If you think there's a better way to implement parts of this library, I'd love to hear your feedback.
126 |
127 |
128 | ###### Feed the tree some ether
129 | ### 🌳Ξ🌳Ξ🌳
130 |
--------------------------------------------------------------------------------
/benchmarks/AddressBenchmark.elm:
--------------------------------------------------------------------------------
1 | module AddressBenchmark exposing (main)
2 |
3 | import Benchmark exposing (..)
4 | import Benchmark.Runner exposing (BenchmarkProgram, program)
5 | import Eth.Utils as Eth
6 | import Eth.Defaults exposing (zeroAddress)
7 |
8 |
9 | main : BenchmarkProgram
10 | main =
11 | program <|
12 | describe "Address"
13 | [ toAddress, addressToString ]
14 |
15 |
16 | toAddress : Benchmark
17 | toAddress =
18 | describe "toAddress"
19 | [ benchmark1 "from lowercase" Eth.toAddress "0xf85feea2fdd81d51177f6b8f35f0e6734ce45f5f"
20 | , benchmark1 "from uppercase" Eth.toAddress "0XF85FEEA2FDD81D51177F6B8F35F0E6734CE45F5F"
21 | , benchmark1 "from evm" Eth.toAddress "000000000000000000000000f85feea2fdd81d51177f6b8f35f0e6734ce45f5f"
22 | , benchmark1 "from already checksummed" Eth.toAddress "e4219dc25D6a05b060c2a39e3960A94a214aAeca"
23 | , benchmark1 "invalid from evm" Eth.toAddress "000000000000000100000000f85feea2fdd81d51177f6b8f35f0e6734ce45f5f"
24 | , benchmark1 "invalid checksum" Eth.toAddress "e4219dc25D6a05b060c2a39e3960a94a214aAeca"
25 | , benchmark1 "invalid hex" Eth.toAddress "e4219dc25D6a05b060c2a39e3960A94a214aAeKa"
26 | , benchmark1 "invalid size" Eth.toAddress "e4219dc25D6a05b060c2a39e3960A94a214aAeKas"
27 | ]
28 |
29 |
30 | addressToString : Benchmark
31 | addressToString =
32 | describe "addressToString"
33 | [ benchmark1 "" Eth.addressToString zeroAddress
34 | ]
35 |
--------------------------------------------------------------------------------
/benchmarks/DependencyBenchmark.elm:
--------------------------------------------------------------------------------
1 | module DependencyBenchmark exposing (main)
2 |
3 | import Benchmark exposing (..)
4 | import Benchmark.Runner exposing (BenchmarkProgram, program)
5 | import BigInt
6 | import Keccak exposing (ethereum_keccak_256)
7 |
8 |
9 | main : BenchmarkProgram
10 | main =
11 | program <|
12 | describe "Dependencies"
13 | [ keccak ]
14 |
15 |
16 | keccak : Benchmark
17 | keccak =
18 | describe "keccak_256"
19 | [ benchmark1 "10 Int array" ethereum_keccak_256 [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
20 | , benchmark1 "empty array" ethereum_keccak_256 []
21 | ]
22 |
--------------------------------------------------------------------------------
/benchmarks/IntBenchmark.elm:
--------------------------------------------------------------------------------
1 | module DependencyBenchmark exposing (main)
2 |
3 | import Abi.Int as AbiInt
4 | import Benchmark exposing (..)
5 | import Benchmark.Runner exposing (BenchmarkProgram, program)
6 | import BigInt
7 |
8 |
9 | main : BenchmarkProgram
10 | main =
11 | program <|
12 | describe ""
13 | [ bigintTwosComplement
14 | , stringyBinaryTwosComplement
15 | ]
16 |
17 |
18 | stringyBinaryTwosComplement : Benchmark
19 | stringyBinaryTwosComplement =
20 | describe "stringyBinaryTwosComplement"
21 | [ benchmark1 "twos complement" AbiInt.toString (BigInt.fromInt 99) ]
22 |
23 |
24 | bigintTwosComplement : Benchmark
25 | bigintTwosComplement =
26 | let
27 | twosComplement =
28 | (BigInt.pow (BigInt.fromInt 2) (BigInt.fromInt 256))
29 | in
30 | describe "bigintTwosComplement"
31 | [ benchmark2 "twos complement" BigInt.add twosComplement (BigInt.fromInt 99) ]
32 |
--------------------------------------------------------------------------------
/benchmarks/elm-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0.0",
3 | "summary": "benchmarks for elm-web3",
4 | "repository": "https://github.com/user/project.git",
5 | "license": "BSD3",
6 | "source-directories": [
7 | ".",
8 | "../src"
9 | ],
10 | "exposed-modules": [],
11 | "dependencies": {
12 | "elm-lang/core": "5.1.1 <= v < 6.0.0",
13 | "BrianHicks/elm-benchmark": "1.0.2 <= v < 2.0.0",
14 | "Chadtech/elm-bool-extra": "1.2.1 <= v < 2.0.0",
15 | "NoRedInk/elm-decode-pipeline": "3.0.0 <= v < 4.0.0",
16 | "Warry/ascii-table": "1.0.0 <= v < 2.0.0",
17 | "elm-community/json-extra": "2.7.0 <= v < 3.0.0",
18 | "elm-community/list-extra": "6.1.0 <= v < 7.0.0",
19 | "elm-community/maybe-extra": "4.0.0 <= v < 5.0.0",
20 | "elm-community/result-extra": "2.2.0 <= v < 3.0.0",
21 | "elm-community/string-extra": "1.4.0 <= v < 2.0.0",
22 | "elm-lang/http": "1.0.0 <= v < 2.0.0",
23 | "elm-lang/websocket": "1.0.2 <= v < 2.0.0",
24 | "hickscorp/elm-bigint": "1.0.1 <= v < 2.0.0",
25 | "nathanjohnson320/base58": "2.0.0 <= v < 3.0.0",
26 | "prozacchiwawa/elm-keccak": "1.0.1 <= v < 2.0.0",
27 | "rtfeldman/hex": "1.0.0 <= v < 2.0.0"
28 | },
29 | "elm-version": "0.18.0 <= v < 0.19.0"
30 | }
--------------------------------------------------------------------------------
/changelog:
--------------------------------------------------------------------------------
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](http://keepachangelog.com/en/1.0.0/)
5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6 |
7 | ## [1.0.0-3.0.3] - ¯\_(ツ)_/¯ sry :(
8 |
9 |
10 | ## [4.0.0] - 2019-06-22
11 | -- No need to convert from Call to Send, as the latter was removed. TxSentry will accept `Call` now.
12 | -- You can now track the latest block number if you're running an EventSentry.
13 | -- Bytes decoders in Abi.Decode now return Hex instead of String
14 | -- staticBytes encoder no longer returns Result, until new "safer" API is fully fleshed out
15 | -- Updated version of bigint library
16 | -- Encoder.functionCall now takes the 4-byte hashed function signature (ABI formatted). Elm no longer needs to do the costly Keccak work.
17 | -- Changed Abi module to Eth.Abi
18 | -- Replaced all uses of Eth.Types.Call with Eth.Sentry.Tx.Send extensible type alias
19 | -- Expose new Eth.Utils functions
20 | -- Remove Internal.Utils
--------------------------------------------------------------------------------
/elm-ethereum-logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/elm.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "package",
3 | "name": "cmditch/elm-ethereum",
4 | "summary": "feed the tree some ether.",
5 | "license": "MIT",
6 | "version": "5.0.0",
7 | "exposed-modules": [
8 | "Eth",
9 | "Eth.Decode",
10 | "Eth.Encode",
11 | "Eth.Defaults",
12 | "Eth.Net",
13 | "Eth.RPC",
14 | "Eth.Sentry.Tx",
15 | "Eth.Sentry.Event",
16 | "Eth.Sentry.Wallet",
17 | "Eth.Types",
18 | "Eth.Units",
19 | "Eth.Utils",
20 | "Eth.Abi.Decode",
21 | "Eth.Abi.Encode",
22 | "Shh"
23 | ],
24 | "elm-version": "0.19.0 <= v < 0.20.0",
25 | "dependencies": {
26 | "Chadtech/elm-bool-extra": "2.4.0 <= v < 3.0.0",
27 | "NoRedInk/elm-json-decode-pipeline": "1.0.0 <= v < 2.0.0",
28 | "cmditch/elm-bigint": "2.0.1 <= v < 3.0.0",
29 | "elm/core": "1.0.2 <= v < 2.0.0",
30 | "elm/http": "2.0.0 <= v < 3.0.0",
31 | "elm/json": "1.1.3 <= v < 2.0.0",
32 | "elm/regex": "1.0.0 <= v < 2.0.0",
33 | "elm/time": "1.0.0 <= v < 2.0.0",
34 | "elm-community/maybe-extra": "5.0.0 <= v < 6.0.0",
35 | "elm-community/result-extra": "2.2.1 <= v < 3.0.0",
36 | "elm-community/string-extra": "4.0.0 <= v < 5.0.0",
37 | "prozacchiwawa/elm-keccak": "2.0.0 <= v < 3.0.0",
38 | "rtfeldman/elm-hex": "1.0.0 <= v < 2.0.0",
39 | "zwilias/elm-utf-tools": "2.0.1 <= v < 3.0.0"
40 | },
41 | "test-dependencies": {
42 | "elm-explorations/test": "1.2.1 <= v < 2.0.0"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/examples/complex/README.md:
--------------------------------------------------------------------------------
1 | # !! WIP - updating this to Elm 0.19 and the latest version of elm-ethereum !!
2 |
3 | # elm-ethereum complex example
4 | ### Single Page Decentralized App (SPDA)
5 |
6 |
7 | ```bash
8 | git clone git@github.com:cmditch/elm-ethereum.git
9 | cd elm-ethereum/examples/complex
10 | npm reinstall
11 | npm run dev
12 |
13 | open http://localhost:8080
14 | ```
15 |
16 | App includes:
17 |
18 | * Use of Style Elements Library
19 | * SPA Navigation (Route Handling / URL Parser / Browser History)
20 | * Msg passing between pages (see [elm-spa-example](https://github.com/rtfeldman/elm-spa-example/) for similar architecture)
21 | * Use of [elm-ethereum-generator](https://github.com/cmditch/elm-ethereum-generator/) for (Contract ABI -> Elm) Help
22 | * Eth.Sentry.ChainCmd for help with SPA Msg passing
23 | * Eth.Sentry.Tx for Wallet Integration and Tx sending
24 | * Eth.Sentry.Event for Event listening over websockets
25 | * Eth.Sentry.Wallet for Wallet Info (Account, NetworkId)
26 | * UPort Integration Demo (With some JWT action)
27 |
--------------------------------------------------------------------------------
/examples/complex/elm.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "application",
3 | "source-directories": [
4 | "../../src",
5 | "src"
6 | ],
7 | "elm-version": "0.19.0",
8 | "dependencies": {
9 | "direct": {
10 | "Chadtech/elm-bool-extra": "2.3.0",
11 | "NoRedInk/elm-json-decode-pipeline": "1.0.0",
12 | "NoRedInk/elm-string-conversions": "1.0.1",
13 | "cmditch/elm-bigint": "2.0.0",
14 | "elm/browser": "1.0.1",
15 | "elm/core": "1.0.2",
16 | "elm/html": "1.0.0",
17 | "elm/http": "2.0.0",
18 | "elm/json": "1.1.3",
19 | "elm/regex": "1.0.0",
20 | "elm/time": "1.0.0",
21 | "elm-community/json-extra": "4.0.0",
22 | "elm-community/list-extra": "8.1.0",
23 | "elm-community/maybe-extra": "5.0.0",
24 | "elm-community/result-extra": "2.2.1",
25 | "elm-community/string-extra": "4.0.0",
26 | "prozacchiwawa/elm-keccak": "2.0.0",
27 | "rtfeldman/elm-hex": "1.0.0",
28 | "zwilias/elm-utf-tools": "2.0.1"
29 | },
30 | "indirect": {
31 | "elm/bytes": "1.0.8",
32 | "elm/file": "1.0.4",
33 | "elm/parser": "1.1.0",
34 | "elm/url": "1.0.0",
35 | "elm/virtual-dom": "1.0.2",
36 | "rtfeldman/elm-iso8601-date-strings": "1.1.2"
37 | }
38 | },
39 | "test-dependencies": {
40 | "direct": {},
41 | "indirect": {}
42 | }
43 | }
--------------------------------------------------------------------------------
/examples/complex/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Coury Ditch",
3 | "name": "elm-ethereum-simple-example",
4 | "version": "0.0.2",
5 | "description": "Elm 0.19 and elm-ethereum 3.0.x",
6 | "main": "index.js",
7 | "scripts": {
8 | "test": "elm-test",
9 | "start": "npm run dev",
10 | "dev": "webpack-dev-server --hot --colors --port 3000",
11 | "build": "webpack",
12 | "prod": "webpack -p",
13 | "analyse": "elm-analyse -s -p 3001 -o"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/cmditch/elm-ethereum.git"
18 | },
19 | "license": "MIT",
20 | "devDependencies": {
21 | "@babel/core": "^7.3.4",
22 | "@babel/preset-env": "^7.3.4",
23 | "babel-loader": "^8.0.5",
24 | "clean-webpack-plugin": "^2.0.0",
25 | "copy-webpack-plugin": "^5.0.0",
26 | "css-loader": "^2.1.0",
27 | "elm": "^0.19.0-bugfix6",
28 | "elm-analyse": "^0.16.3",
29 | "elm-hot-webpack-loader": "^1.0.2",
30 | "elm-minify": "^2.0.4",
31 | "elm-test": "^0.19.0",
32 | "elm-webpack-loader": "^5.0.0",
33 | "file-loader": "^3.0.1",
34 | "html-webpack-plugin": "^3.2.0",
35 | "mini-css-extract-plugin": "^0.5.0",
36 | "node-sass": "^4.13.1",
37 | "resolve-url-loader": "^3.0.1",
38 | "sass-loader": "^7.1.0",
39 | "style-loader": "^0.23.1",
40 | "url-loader": "^1.1.2",
41 | "webpack": "^4.29.6",
42 | "webpack-cli": "^3.2.3",
43 | "webpack-dev-server": "^3.2.1",
44 | "webpack-merge": "^4.2.1"
45 | },
46 | "dependencies": {
47 | "purecss": "^1.0.0",
48 | "elm-ethereum-ports": "^1.0.1",
49 | "elm-ethereum-generator": "^2.0.0"
50 | },
51 | "prettier": {
52 | "tabWidth": 4
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/examples/complex/src/elm/Contracts/WidgetFactory.elm:
--------------------------------------------------------------------------------
1 | module Contracts.WidgetFactory exposing
2 | ( Widget
3 | , WidgetCreated
4 | , WidgetSold
5 | , newWidget
6 | , sellWidget
7 | , widgetCount
8 | , widgetCreatedDecoder
9 | , widgetCreatedEvent
10 | , widgetSoldDecoder
11 | , widgetSoldEvent
12 | , widgets
13 | , widgetsDecoder
14 | )
15 |
16 | import Abi.Decode as AbiDecode exposing (abiDecode, andMap, data, toElmDecoder, topic)
17 | import Abi.Encode as AbiEncode exposing (Encoding(..), abiEncode)
18 | import BigInt exposing (BigInt)
19 | import Eth.Types exposing (..)
20 | import Eth.Utils as U
21 | import Json.Decode as Decode exposing (Decoder)
22 | import Json.Decode.Pipeline exposing (custom, decode)
23 |
24 |
25 |
26 | {-
27 |
28 | This file was generated by https://github.com/cmditch/elm-ethereum-generator
29 |
30 | -}
31 |
32 |
33 | {-| "newWidget(uint256,uint256,address)" function
34 | -}
35 | newWidget : Address -> BigInt -> BigInt -> Address -> Call ()
36 | newWidget contractAddress size_ cost_ owner_ =
37 | { to = Just contractAddress
38 | , from = Nothing
39 | , gas = Nothing
40 | , gasPrice = Nothing
41 | , value = Nothing
42 | , data = Just <| AbiEncode.functionCall "newWidget(uint256,uint256,address)" [ AbiEncode.uint size_, AbiEncode.uint cost_, AbiEncode.address owner_ ]
43 | , nonce = Nothing
44 | , decoder = Decode.succeed ()
45 | }
46 |
47 |
48 | {-| "sellWidget(uint256)" function
49 | -}
50 | sellWidget : Address -> BigInt -> Call ()
51 | sellWidget contractAddress id_ =
52 | { to = Just contractAddress
53 | , from = Nothing
54 | , gas = Nothing
55 | , gasPrice = Nothing
56 | , value = Nothing
57 | , data = Just <| AbiEncode.functionCall "sellWidget(uint256)" [ AbiEncode.uint id_ ]
58 | , nonce = Nothing
59 | , decoder = Decode.succeed ()
60 | }
61 |
62 |
63 | {-| "widgetCount()" function
64 | -}
65 | widgetCount : Address -> Call BigInt
66 | widgetCount contractAddress =
67 | { to = Just contractAddress
68 | , from = Nothing
69 | , gas = Nothing
70 | , gasPrice = Nothing
71 | , value = Nothing
72 | , data = Just <| AbiEncode.functionCall "widgetCount()" []
73 | , nonce = Nothing
74 | , decoder = toElmDecoder AbiDecode.uint
75 | }
76 |
77 |
78 | {-| "widgets(uint256)" function
79 | -}
80 | type alias Widget =
81 | { id : BigInt
82 | , size : BigInt
83 | , cost : BigInt
84 | , owner : Address
85 | , wasSold : Bool
86 | }
87 |
88 |
89 | widgets : Address -> BigInt -> Call Widget
90 | widgets contractAddress a =
91 | { to = Just contractAddress
92 | , from = Nothing
93 | , gas = Nothing
94 | , gasPrice = Nothing
95 | , value = Nothing
96 | , data = Just <| AbiEncode.functionCall "widgets(uint256)" [ AbiEncode.uint a ]
97 | , nonce = Nothing
98 | , decoder = widgetsDecoder
99 | }
100 |
101 |
102 | widgetsDecoder : Decoder Widget
103 | widgetsDecoder =
104 | abiDecode Widget
105 | |> andMap AbiDecode.uint
106 | |> andMap AbiDecode.uint
107 | |> andMap AbiDecode.uint
108 | |> andMap AbiDecode.address
109 | |> andMap AbiDecode.bool
110 | |> toElmDecoder
111 |
112 |
113 | {-| "WidgetCreated(uint256,uint256,uint256,address)" event
114 | -}
115 | type alias WidgetCreated =
116 | { id : BigInt
117 | , size : BigInt
118 | , cost : BigInt
119 | , owner : Address
120 | }
121 |
122 |
123 | widgetCreatedEvent : Address -> LogFilter
124 | widgetCreatedEvent contractAddress =
125 | { fromBlock = LatestBlock
126 | , toBlock = LatestBlock
127 | , address = contractAddress
128 | , topics = [ Just <| U.keccak256 "WidgetCreated(uint256,uint256,uint256,address)" ]
129 | }
130 |
131 |
132 | widgetCreatedDecoder : Decoder WidgetCreated
133 | widgetCreatedDecoder =
134 | decode WidgetCreated
135 | |> custom (data 0 AbiDecode.uint)
136 | |> custom (data 1 AbiDecode.uint)
137 | |> custom (data 2 AbiDecode.uint)
138 | |> custom (data 3 AbiDecode.address)
139 |
140 |
141 | {-| "WidgetSold(uint256)" event
142 | -}
143 | type alias WidgetSold =
144 | { id : BigInt }
145 |
146 |
147 | widgetSoldEvent : Address -> LogFilter
148 | widgetSoldEvent contractAddress =
149 | { fromBlock = LatestBlock
150 | , toBlock = LatestBlock
151 | , address = contractAddress
152 | , topics = [ Just <| U.keccak256 "WidgetSold(uint256)" ]
153 | }
154 |
155 |
156 | widgetSoldDecoder : Decoder WidgetSold
157 | widgetSoldDecoder =
158 | decode WidgetSold
159 | |> custom (data 0 AbiDecode.uint)
160 |
--------------------------------------------------------------------------------
/examples/complex/src/elm/Data/Chain.elm:
--------------------------------------------------------------------------------
1 | module Data.Chain exposing (..)
2 |
3 | import Json.Decode as Decode exposing (Decoder)
4 | import Eth.Types exposing (Address)
5 | import Eth.Utils as EthUtils
6 | import Eth.Net as EthNet exposing (NetworkId(..))
7 | import Eth.Decode as EthDecode
8 | import Eth.Types exposing (HttpProvider, WebsocketProvider)
9 |
10 |
11 | widgetFactory : Address
12 | widgetFactory =
13 | EthUtils.unsafeToAddress "0x36dde2719a01ec108304d830d537aec3fb7c1bbf"
14 |
15 |
16 | metamaskAccountDecoder : Decoder (Maybe Address)
17 | metamaskAccountDecoder =
18 | Decode.maybe EthDecode.address
19 |
20 |
21 | networkIdDecoder : Decoder (Maybe NetworkId)
22 | networkIdDecoder =
23 | Decode.maybe EthNet.networkIdDecoder
24 |
25 |
26 | type alias NodePath =
27 | { http : HttpProvider
28 | , ws : WebsocketProvider
29 | }
30 |
31 |
32 | nodePath : NetworkId -> NodePath
33 | nodePath networkId =
34 | case networkId of
35 | Mainnet ->
36 | NodePath "https://mainnet.infura.io/" "wss://mainnet.infura.io/ws"
37 |
38 | Ropsten ->
39 | NodePath "https://ropsten.infura.io/" "wss://ropsten.infura.io/ws"
40 |
41 | Rinkeby ->
42 | NodePath "https://rinkeby.infura.io/" "wss://rinkeby.infura.io/ws"
43 |
44 | _ ->
45 | NodePath "UnknownEthNetwork" "UnknownEthNetwork"
46 |
--------------------------------------------------------------------------------
/examples/complex/src/elm/Extra/BigInt.elm:
--------------------------------------------------------------------------------
1 | module Extra.BigInt exposing (..)
2 |
3 | import BigInt exposing (BigInt)
4 |
5 |
6 | countDownFrom : BigInt -> List BigInt
7 | countDownFrom num =
8 | let
9 | countDownHelper num acc =
10 | case BigInt.compare num zero of
11 | EQ ->
12 | zero :: acc
13 |
14 | _ ->
15 | countDownHelper (BigInt.sub num one) (num :: acc)
16 | in
17 | case BigInt.lte num zero of
18 | True ->
19 | []
20 |
21 | False ->
22 | countDownHelper (BigInt.sub num one) []
23 |
24 |
25 | zero : BigInt
26 | zero =
27 | BigInt.fromInt 0
28 |
29 |
30 | one : BigInt
31 | one =
32 | BigInt.fromInt 1
33 |
34 |
35 | {-| Allows for more accurate bigInt percentage calculations
36 | -}
37 | percentageOf : BigInt -> BigInt -> BigInt
38 | percentageOf val percentage =
39 | let
40 | levelOfAccuracy =
41 | BigInt.fromInt 100
42 |
43 | expandedPercentage =
44 | (BigInt.fromInt 100)
45 | |> BigInt.mul levelOfAccuracy
46 | in
47 | BigInt.div expandedPercentage percentage
48 | |> BigInt.div (BigInt.mul levelOfAccuracy val)
49 |
--------------------------------------------------------------------------------
/examples/complex/src/elm/Main.elm:
--------------------------------------------------------------------------------
1 | module Main exposing
2 | ( EthNetworkId
3 | , Flags
4 | , Modal(..)
5 | , Model
6 | , Msg(..)
7 | , Page(..)
8 | , init
9 | , main
10 | , modalOpen
11 | , pageSubscriptions
12 | , setRoute
13 | , subscriptions
14 | , update
15 | , updatePage
16 | , view
17 | , viewNetworkStatus
18 | , viewOverlay
19 | , viewSidebar
20 | )
21 |
22 | -- Libraries
23 | --Internal
24 |
25 | import Data.Chain as ChainData
26 | import Element exposing (..)
27 | import Element.Attributes exposing (..)
28 | import Eth.Net as EthNet exposing (NetworkId(..))
29 | import Eth.Sentry.ChainCmd as ChainCmd exposing (ChainCmd)
30 | import Eth.Sentry.Event as EventSentry exposing (EventSentry)
31 | import Eth.Sentry.Tx as TxSentry exposing (TxSentry)
32 | import Eth.Sentry.Wallet as WalletSentry exposing (WalletSentry)
33 | import Eth.Types exposing (..)
34 | import Html exposing (Html)
35 | import Navigation exposing (Location)
36 | import Page.Home as Home
37 | import Page.Login as Login
38 | import Page.Widget as Widget
39 | import Page.WidgetWizard as WidgetWizard
40 | import Ports
41 | import Request.UPort as UPort
42 | import Route exposing (Route)
43 | import Views.Styles exposing (Styles(..), Variations(..), stylesheet)
44 |
45 |
46 | main : Program Flags Model Msg
47 | main =
48 | Navigation.programWithFlags (Route.fromLocation >> SetRoute)
49 | { init = init
50 | , view = view
51 | , update = update
52 | , subscriptions = subscriptions
53 | }
54 |
55 |
56 | type alias EthNetworkId =
57 | Int
58 |
59 |
60 | type alias Flags =
61 | Maybe EthNetworkId
62 |
63 |
64 | type alias Model =
65 | { page : Page
66 | , account : Maybe Address
67 | , uPortUser : Maybe UPort.User
68 | , networkId : Maybe NetworkId
69 | , nodePath : ChainData.NodePath
70 | , isLoggedIn : Bool
71 | , eventSentry : EventSentry Msg
72 | , txSentry : TxSentry Msg
73 | , errors : List String
74 | }
75 |
76 |
77 | type Modal
78 | = OppWizard WidgetWizard.Model
79 |
80 |
81 | type Page
82 | = NotFound
83 | | Home Home.Model
84 | | Login Login.Model
85 | | Widget Widget.Model
86 |
87 |
88 | init : Flags -> Location -> ( Model, Cmd Msg )
89 | init rawNetworkID location =
90 | let
91 | networkId =
92 | Maybe.map EthNet.toNetworkId rawNetworkID
93 |
94 | nodePath =
95 | Maybe.withDefault Mainnet networkId
96 | |> ChainData.nodePath
97 | in
98 | setRoute (Route.fromLocation location)
99 | { page = Login (Tuple.first Login.init)
100 | , account = Nothing
101 | , uPortUser = Nothing
102 | , networkId = networkId
103 | , nodePath = nodePath
104 | , isLoggedIn = False
105 | , eventSentry =
106 | EventSentry.init EventSentryMsg nodePath.ws
107 | |> EventSentry.withDebug
108 | , txSentry =
109 | TxSentry.init ( Ports.txOut, Ports.txIn ) TxSentryMsg nodePath.http
110 | |> TxSentry.withDebug
111 | , errors = []
112 | }
113 |
114 |
115 |
116 | -- VIEW
117 |
118 |
119 | view : Model -> Html Msg
120 | view model =
121 | Element.viewport stylesheet <|
122 | row None
123 | [ width fill
124 | , height (percent 100)
125 | , minHeight (percent 100)
126 | , inlineStyle [ ( "position", "fixed" ) ]
127 | ]
128 | [ when (modalOpen model.page) viewOverlay
129 | , when model.isLoggedIn <| viewSidebar model
130 | , el None [ height fill, width fill, yScrollbar ] <|
131 | case model.page of
132 | NotFound ->
133 | text "Page Not Found"
134 |
135 | Home homeModel ->
136 | Home.view model.account homeModel
137 | |> Element.map HomeMsg
138 |
139 | Login loginModel ->
140 | Login.view model.account loginModel
141 | |> Element.map LoginMsg
142 |
143 | Widget oppModel ->
144 | Widget.view model.account oppModel
145 | |> Element.map WidgetMsg
146 | ]
147 |
148 |
149 | viewOverlay : Element Styles Variations Msg
150 | viewOverlay =
151 | el None
152 | [ inlineStyle
153 | [ ( "position", "fixed" )
154 | , ( "display", "block" )
155 | , ( "width", "100%" )
156 | , ( "height", "100%" )
157 | , ( "top", "0" )
158 | , ( "bottom", "0" )
159 | , ( "left", "0" )
160 | , ( "right", "0" )
161 | , ( "background-color", "rgba(0, 0, 0, 0.5)" )
162 | , ( "z-index", "1001" )
163 | ]
164 | ]
165 | empty
166 |
167 |
168 | viewSidebar : Model -> Element Styles Variations Msg
169 | viewSidebar model =
170 | let
171 | imageUrl path =
172 | "url(\"" ++ path ++ "\")"
173 |
174 | avatar user =
175 | column None
176 | [ spacing 10, center ]
177 | [ circle 50
178 | ProfileImage
179 | [ inlineStyle
180 | [ ( "background-image", imageUrl user.avatar )
181 | , ( "background-size", "cover" )
182 | , ( "background-repeat", "no-repeat" )
183 | , ( "background-position", "center" )
184 | ]
185 | ]
186 | empty
187 | , el WidgetText [ vary WidgetWhite True ] <| text user.name
188 | , el WidgetText [ vary WidgetWhite True, vary Small True ] <| text user.email
189 | ]
190 | in
191 | column Sidebar
192 | [ center, height (percent 100), minHeight (percent 100), padding 30, spacing 30 ]
193 | [ whenJust model.uPortUser (\user -> avatar user)
194 | , viewNetworkStatus model.networkId
195 | ]
196 |
197 |
198 | viewNetworkStatus : Maybe NetworkId -> Element Styles Variations msg
199 | viewNetworkStatus networkId =
200 | let
201 | ( style, display ) =
202 | case networkId of
203 | Nothing ->
204 | ( StatusFailure, "Disconnected" )
205 |
206 | Just Mainnet ->
207 | ( StatusSuccess, "Mainnet" )
208 |
209 | Just network ->
210 | ( StatusAlert, EthNet.networkIdToString network )
211 | in
212 | row Status
213 | [ verticalCenter, center, spacing 5, height fill, width fill, alignBottom ]
214 | [ circle 5.0 style [ verticalCenter ] empty
215 | , text display
216 | ]
217 |
218 |
219 | modalOpen : Page -> Bool
220 | modalOpen page =
221 | case page of
222 | Home model ->
223 | Home.modalOpen model
224 |
225 | _ ->
226 | False
227 |
228 |
229 |
230 | -- UPDATE
231 |
232 |
233 | type Msg
234 | = NoOp
235 | | SetRoute (Maybe Route)
236 | -- Page Msgs
237 | | HomeMsg Home.Msg
238 | | LoginMsg Login.Msg
239 | | WidgetMsg Widget.Msg
240 | -- Port/Sub Related Msgs
241 | | WalletStatus WalletSentry
242 | | EventSentryMsg EventSentry.Msg
243 | | TxSentryMsg TxSentry.Msg
244 | | Fail String
245 |
246 |
247 | update : Msg -> Model -> ( Model, Cmd Msg )
248 | update msg model =
249 | updatePage model.page msg model
250 |
251 |
252 | updatePage : Page -> Msg -> Model -> ( Model, Cmd Msg )
253 | updatePage page msg model =
254 | let
255 | toPage toModel toMsg subUpdate subMsg subModel =
256 | let
257 | ( newModel, newCmd ) =
258 | subUpdate subMsg subModel
259 | in
260 | { model | page = toModel newModel }
261 | ! [ Cmd.map toMsg newCmd ]
262 |
263 | toChainEffPage toModel toMsg subUpdate subMsg subModel =
264 | let
265 | ( newModel, newCmd, chainEff ) =
266 | subUpdate subMsg subModel
267 |
268 | ( ( newTxSentry, newEventSentry ), chainCmds ) =
269 | ChainCmd.execute ( model.txSentry, model.eventSentry ) (ChainCmd.map toMsg chainEff)
270 | in
271 | { model
272 | | txSentry = newTxSentry
273 | , eventSentry = newEventSentry
274 | , page = toModel newModel
275 | }
276 | ! [ chainCmds, Cmd.map toMsg newCmd ]
277 | in
278 | case ( page, msg ) of
279 | {- Route Updates -}
280 | ( _, SetRoute route ) ->
281 | setRoute route model
282 |
283 | {- Page Updates -}
284 | ( Login subModel, LoginMsg subMsg ) ->
285 | case subMsg of
286 | Login.LoggedIn user ->
287 | { model | isLoggedIn = True, uPortUser = Just user } ! [ Navigation.newUrl "#" ]
288 |
289 | Login.SkipLogin ->
290 | { model | isLoggedIn = True } ! [ Navigation.newUrl "#" ]
291 |
292 | _ ->
293 | toPage Login LoginMsg Login.update subMsg subModel
294 |
295 | ( Home subModel, HomeMsg subMsg ) ->
296 | toChainEffPage Home HomeMsg (Home.update model.nodePath) subMsg subModel
297 |
298 | ( Widget subModel, WidgetMsg subMsg ) ->
299 | toChainEffPage Widget WidgetMsg (Widget.update model.nodePath) subMsg subModel
300 |
301 | {- Sentry -}
302 | ( _, WalletStatus walletSentry ) ->
303 | { model
304 | | account = walletSentry.account
305 | , nodePath = ChainData.nodePath walletSentry.networkId
306 | }
307 | ! []
308 |
309 | ( _, TxSentryMsg subMsg ) ->
310 | let
311 | ( newTxSentry, newMsg ) =
312 | TxSentry.update subMsg model.txSentry
313 | in
314 | { model | txSentry = newTxSentry }
315 | ! [ newMsg ]
316 |
317 | ( _, EventSentryMsg subMsg ) ->
318 | let
319 | ( newEventSentry, newSubMsg ) =
320 | EventSentry.update subMsg model.eventSentry
321 | in
322 | { model | eventSentry = newEventSentry }
323 | ! [ newSubMsg ]
324 |
325 | {- Failures and NoOps -}
326 | ( _, NoOp ) ->
327 | model ! []
328 |
329 | ( _, Fail str ) ->
330 | { model | errors = str :: model.errors }
331 | ! []
332 |
333 | ( _, _ ) ->
334 | model ! []
335 |
336 |
337 | setRoute : Maybe Route -> Model -> ( Model, Cmd Msg )
338 | setRoute maybeRoute model =
339 | case ( maybeRoute, model.isLoggedIn ) of
340 | ( Just Route.Login, False ) ->
341 | let
342 | ( subModel, subCmd ) =
343 | Login.init
344 | in
345 | { model | page = Login subModel }
346 | ! [ Cmd.map LoginMsg subCmd ]
347 |
348 | ( Just Route.Login, True ) ->
349 | model ! [ Navigation.newUrl "#" ]
350 |
351 | ( _, False ) ->
352 | model ! [ Navigation.newUrl "#login" ]
353 |
354 | ( Nothing, _ ) ->
355 | { model | page = NotFound } ! []
356 |
357 | ( Just Route.Home, _ ) ->
358 | let
359 | ( subModel, subCmd ) =
360 | Home.init model.nodePath
361 | in
362 | { model | page = Home subModel }
363 | ! [ Cmd.map HomeMsg subCmd ]
364 |
365 | ( Just (Route.Widget id), _ ) ->
366 | let
367 | ( widgetSubModel, widgetSubCmd ) =
368 | Widget.init model.nodePath id
369 | in
370 | { model | page = Widget widgetSubModel }
371 | ! [ Cmd.map WidgetMsg widgetSubCmd ]
372 |
373 |
374 |
375 | -- SUBSCRIPTION
376 |
377 |
378 | subscriptions : Model -> Sub Msg
379 | subscriptions model =
380 | Sub.batch
381 | [ pageSubscriptions model.page
382 | , Ports.walletSentry (WalletSentry.decodeToMsg Fail WalletStatus)
383 | , EventSentry.listen model.eventSentry
384 | , TxSentry.listen model.txSentry
385 | ]
386 |
387 |
388 | pageSubscriptions : Page -> Sub Msg
389 | pageSubscriptions page =
390 | case page of
391 | Login model ->
392 | Sub.map LoginMsg <| Login.subscriptions model
393 |
394 | _ ->
395 | Sub.none
396 |
--------------------------------------------------------------------------------
/examples/complex/src/elm/Page/Home.elm:
--------------------------------------------------------------------------------
1 | module Page.Home exposing (Model, Msg, init, update, view, modalOpen)
2 |
3 | -- Library
4 |
5 | import BigInt
6 | import Eth.Types exposing (Address)
7 | import Element exposing (..)
8 | import Element.Attributes exposing (..)
9 | import Element.Events exposing (onClick)
10 | import Http
11 | import Task
12 |
13 |
14 | --Internal
15 |
16 | import Data.Chain as ChainData exposing (NodePath)
17 | import Contracts.WidgetFactory as Widget exposing (Widget)
18 | import Request.Chain as ChainReq
19 | import Request.Status exposing (RemoteData(..))
20 | import Route
21 | import Views.Styles exposing (Styles(..), Variations(..))
22 | import Eth.Sentry.ChainCmd as ChainCmd exposing (ChainCmd)
23 | import Page.WidgetWizard as WidgetWizard
24 |
25 |
26 | type alias Model =
27 | { modal : Maybe WidgetWizard.Model
28 | , widgets : RemoteData Http.Error (List Widget)
29 | , errors : List Http.Error
30 | }
31 |
32 |
33 | init : NodePath -> ( Model, Cmd Msg )
34 | init nodePath =
35 | { modal = Nothing, widgets = Loading, errors = [] }
36 | ! [ Task.attempt WidgetResponse <| ChainReq.getWidgetList nodePath.http ChainData.widgetFactory ]
37 |
38 |
39 | view : Maybe Address -> Model -> Element Styles Variations Msg
40 | view mAccount model =
41 | let
42 | widgetList =
43 | case model.widgets of
44 | Success [] ->
45 | text "Make sure you are on the Ropsten Network"
46 |
47 | Success widgets ->
48 | column None
49 | [ spacing 10 ]
50 | (List.map viewWidget widgets)
51 |
52 | Failure e ->
53 | column None
54 | [ spacing 10 ]
55 | [ text "Failure loading widgets", text <| toString e ]
56 |
57 | Loading ->
58 | column None
59 | [ spacing 10, center, paddingTop 250 ]
60 | [ el Header [ vary H4 True ] <| text "Loading Widgets..."
61 | , decorativeImage None [ width (percent 10), center, paddingTop 15 ] { src = "static/img/loader.gif" }
62 | ]
63 |
64 | NotAsked ->
65 | text "Shouldn't be seeing this"
66 | in
67 | column None
68 | [ padding 30, width fill, height fill, center ]
69 | [ whenJust model.modal <| Element.map ModalMsg << WidgetWizard.view mAccount
70 | , column None
71 | [ width (percent 75), spacing 20 ]
72 | [ row None
73 | [ verticalCenter, spread ]
74 | [ el Header [ vary H2 True ] <| text "WidgetList"
75 | , el Button [ padding 10, onClick ModalOpen ] <| text "+ New Widget"
76 | ]
77 | , widgetList
78 | ]
79 | ]
80 |
81 |
82 | viewWidget : Widget -> Element Styles Variations Msg
83 | viewWidget widget =
84 | Route.link (Route.Widget widget.id) <|
85 | column WidgetSummary
86 | [ width (px 500), height (px 75), center, verticalCenter ]
87 | [ el WidgetText [ vary H2 True ] (text <| "Widget # " ++ BigInt.toString widget.id)
88 | , el WidgetText [] (text "Click for more info")
89 | ]
90 |
91 |
92 | modalOpen : Model -> Bool
93 | modalOpen model =
94 | case model.modal of
95 | Nothing ->
96 | False
97 |
98 | Just _ ->
99 | True
100 |
101 |
102 | type Msg
103 | = NoOp
104 | | WidgetResponse (Result Http.Error (List Widget))
105 | | ModalOpen
106 | | ModalMsg WidgetWizard.Msg
107 |
108 |
109 | update : NodePath -> Msg -> Model -> ( Model, Cmd Msg, ChainCmd Msg )
110 | update nodePath msg model =
111 | case msg of
112 | NoOp ->
113 | ( model, Cmd.none, ChainCmd.none )
114 |
115 | WidgetResponse result ->
116 | case result of
117 | Ok ops ->
118 | ( { model | widgets = Success <| List.reverse ops }, Cmd.none, ChainCmd.none )
119 |
120 | Err e ->
121 | ( { model | widgets = Failure e, errors = e :: model.errors }, Cmd.none, ChainCmd.none )
122 |
123 | ModalOpen ->
124 | ( { model | modal = Just WidgetWizard.init }, Cmd.none, ChainCmd.none )
125 |
126 | ModalMsg subMsg ->
127 | case model.modal of
128 | Nothing ->
129 | ( model, Cmd.none, ChainCmd.none )
130 |
131 | Just modal ->
132 | let
133 | ( newModel, newCmds, newChainEffs ) =
134 | let
135 | ( newModal, newModalCmd, newModalChainEff ) =
136 | WidgetWizard.update subMsg modal
137 | in
138 | ( { model | modal = Just newModal }
139 | , Cmd.map ModalMsg newModalCmd
140 | , ChainCmd.map ModalMsg newModalChainEff
141 | )
142 | in
143 | case subMsg of
144 | WidgetWizard.Close ->
145 | ( { model | modal = Nothing }, Cmd.none, ChainCmd.none )
146 |
147 | WidgetWizard.WidgetDeployed _ ->
148 | ( newModel
149 | , Cmd.batch
150 | [ Task.attempt WidgetResponse <| ChainReq.getWidgetList nodePath.http ChainData.widgetFactory
151 | , newCmds
152 | ]
153 | , newChainEffs
154 | )
155 |
156 | _ ->
157 | ( newModel, newCmds, newChainEffs )
158 |
--------------------------------------------------------------------------------
/examples/complex/src/elm/Page/Login.elm:
--------------------------------------------------------------------------------
1 | module Page.Login exposing (Model, Msg(LoggedIn, SkipLogin), init, update, view, subscriptions)
2 |
3 | --Library
4 |
5 | import Animation
6 | import Element exposing (..)
7 | import Element.Events exposing (..)
8 | import Element.Attributes exposing (..)
9 | import Eth.Types exposing (Address)
10 | import Html
11 | import Html.Attributes
12 | import Process
13 | import WebSocket
14 |
15 |
16 | --Internal
17 |
18 | import Views.Styles exposing (Styles(..), Variations(..))
19 | import Task
20 | import Time
21 | import Request.UPort as UPort
22 |
23 |
24 | type alias Model =
25 | { errors : List String
26 | , animationPage : Animation.State
27 | , loginRequested : Bool
28 | , loginRequest : Maybe UPort.RequestData
29 | , loginSuccess : Bool
30 | }
31 |
32 |
33 | init : ( Model, Cmd Msg )
34 | init =
35 | { errors = []
36 | , animationPage = Animation.style [ Animation.opacity 0.0 ]
37 | , loginRequested = False
38 | , loginRequest = Nothing
39 | , loginSuccess = False
40 | }
41 | ! [ Task.perform StartAnimation (Task.succeed ()) ]
42 |
43 |
44 | view : Maybe Address -> Model -> Element Styles Variations Msg
45 | view mAccount model =
46 | column LoginPage
47 | (List.concat
48 | [ List.map toAttr <| Animation.render model.animationPage
49 | , [ width fill, height fill, center, verticalCenter ]
50 | ]
51 | )
52 | [ (when << not) model.loginRequested <|
53 | column None
54 | [ spacing 20, verticalCenter, center, height fill ]
55 | [ decorativeImage None
56 | [ width <| px 400 ]
57 | { src = "static/img/elm-ethereum-logo.svg" }
58 | , el Header
59 | [ vary H2 True
60 | , vary Bold True
61 | , vary WidgetWhite True
62 | ]
63 | (text "elm-ethereum example")
64 | , case mAccount of
65 | Nothing ->
66 | column LoginBox
67 | [ padding 20 ]
68 | [ el Header [ vary H4 True, vary WidgetWhite True ] <| text "Please Unlock Metamask" ]
69 |
70 | Just _ ->
71 | column None
72 | [ spacing 90 ]
73 | [ row LoginBox
74 | [ width (px 250)
75 | , padding 10
76 | , spacing 15
77 | , center
78 | , verticalCenter
79 | , onClick Login
80 | ]
81 | [ viewUPortLogo 40
82 | , text "Sign in with uPort"
83 | ]
84 | , button LoginBox
85 | [ onClick SkipLogin, width (px 100), center ]
86 | (text "Skip")
87 | ]
88 | ]
89 | , whenJust model.loginRequest <|
90 | (\request ->
91 | column LoginBox
92 | [ center, verticalCenter, padding 20, spacing 20 ]
93 | [ viewUPortLogo 60
94 | , text "Scan with the uPort app"
95 | , image None [ width (px 400) ] { src = request.qr, caption = request.uri }
96 | , (when << not) model.loginSuccess <|
97 | row None
98 | [ spacing 10, verticalCenter ]
99 | [ image None
100 | [ width (px 35), height (px 35) ]
101 | { src = "static/img/loader.gif", caption = "loading..." }
102 | , text "Waiting for response from uPort..."
103 | ]
104 | , when model.loginSuccess <|
105 | row None
106 | [ spacing 10, verticalCenter ]
107 | [ el WidgetText [ vary WidgetBlue True ] <|
108 | html <|
109 | Html.i [ Html.Attributes.class "far fa-check-circle" ] []
110 | , text "Success!"
111 | ]
112 | ]
113 | )
114 | ]
115 |
116 |
117 | viewUPortLogo : Float -> Element Styles Variations Msg
118 | viewUPortLogo dim =
119 | image None
120 | [ width <| px dim, height <| px dim ]
121 | { src = "static/img/uport.png", caption = "uPort Logo" }
122 |
123 |
124 | type Msg
125 | = NoOp
126 | | Login
127 | | SkipLogin
128 | | PollWSConnection Int
129 | | LoggedIn UPort.User
130 | | UPortMessage UPort.Message
131 | | Animate Animation.Msg
132 | | StartAnimation ()
133 |
134 |
135 | update : Msg -> Model -> ( Model, Cmd Msg )
136 | update msg model =
137 | case msg of
138 | NoOp ->
139 | model ! []
140 |
141 | LoggedIn _ ->
142 | -- captured in parent module
143 | model ! []
144 |
145 | Login ->
146 | { model | loginRequested = True }
147 | ! [ WebSocket.send UPort.authEndpoint "login"
148 | , Task.perform PollWSConnection (Task.succeed 20)
149 | ]
150 |
151 | SkipLogin ->
152 | { model | loginRequested = True } ! []
153 |
154 | PollWSConnection times ->
155 | if model.loginRequested && times > 0 then
156 | model
157 | ! [ WebSocket.send UPort.authEndpoint "keepalive"
158 | , Task.perform (\_ -> PollWSConnection <| times - 1) (Process.sleep 15000)
159 | ]
160 | else
161 | model ! []
162 |
163 | UPortMessage message ->
164 | case message of
165 | UPort.Request r ->
166 | { model | loginRequest = Just r } ! []
167 |
168 | UPort.Success s ->
169 | { model | loginSuccess = True }
170 | ! [ Task.perform LoggedIn
171 | -- delay signal for a moment to swap out loading gif
172 | (Process.sleep Time.second |> Task.andThen (\() -> Task.succeed s.user))
173 | ]
174 |
175 | UPort.Error e ->
176 | let
177 | _ =
178 | Debug.log "uport error" e
179 | in
180 | model ! []
181 |
182 | Animate aMsg ->
183 | { model | animationPage = Animation.update aMsg model.animationPage } ! []
184 |
185 | StartAnimation () ->
186 | { model
187 | | animationPage =
188 | Animation.interrupt
189 | [ Animation.wait (0.5 * Time.second)
190 | , Animation.toWith (Animation.easing { duration = Time.second, ease = identity })
191 | [ Animation.opacity 1.0 ]
192 | ]
193 | model.animationPage
194 | }
195 | ! []
196 |
197 |
198 | subscriptions : Model -> Sub Msg
199 | subscriptions model =
200 | Sub.batch
201 | [ Animation.subscription Animate [ model.animationPage ]
202 | , if model.loginRequested then
203 | WebSocket.listen UPort.authEndpoint (UPort.decodeMessage UPortMessage)
204 | else
205 | Sub.none
206 | ]
207 |
--------------------------------------------------------------------------------
/examples/complex/src/elm/Page/Widget.elm:
--------------------------------------------------------------------------------
1 | module Page.Widget exposing (Model, Msg, init, update, view)
2 |
3 | -- Library
4 |
5 | import BigInt exposing (BigInt)
6 | import Eth as Eth
7 | import Eth.Types exposing (..)
8 | import Eth.Utils as EthUtils
9 | import Eth.Sentry.ChainCmd as ChainCmd exposing (ChainCmd)
10 | import Eth.Units exposing (gwei)
11 | import Element exposing (..)
12 | import Element.Attributes exposing (..)
13 | import Element.Events exposing (onClick)
14 | import Task
15 | import Http
16 |
17 |
18 | --Internal
19 |
20 | import Request.Status exposing (RemoteData(..))
21 | import Contracts.WidgetFactory as Widget exposing (Widget)
22 | import Data.Chain as ChainData exposing (NodePath)
23 | import Views.Styles exposing (Styles(..), Variations(..))
24 |
25 |
26 | type alias Model =
27 | { widgetId : BigInt
28 | , widget : RemoteData Http.Error Widget
29 | , widgetSellPending : RemoteData String ()
30 | , errors : List String
31 | }
32 |
33 |
34 | init : NodePath -> BigInt -> ( Model, Cmd Msg )
35 | init nodePath widgetId =
36 | { widgetId = widgetId
37 | , widget = Loading
38 | , widgetSellPending = NotAsked
39 | , errors = []
40 | }
41 | ! [ Eth.call nodePath.http (Widget.widgets ChainData.widgetFactory widgetId)
42 | |> Task.attempt WidgetInfo
43 | ]
44 |
45 |
46 |
47 | -- VIEW
48 |
49 |
50 | view : Maybe Address -> Model -> Element Styles Variations Msg
51 | view mAccount model =
52 | let
53 | loadingView strMsg =
54 | row None
55 | [ width fill, height fill, center, verticalCenter ]
56 | [ column None
57 | [ center, paddingTop 20, moveDown 70 ]
58 | [ text strMsg
59 | , decorativeImage None [ width (percent 30), center, paddingTop 15 ] { src = "static/img/loader.gif" }
60 | ]
61 | ]
62 | in
63 | case ( mAccount, model.widget ) of
64 | ( Nothing, _ ) ->
65 | text "Log into metamask please"
66 |
67 | ( _, NotAsked ) ->
68 | loadingView "Loading Widget"
69 |
70 | ( _, Loading ) ->
71 | loadingView "Loading Widget"
72 |
73 | ( _, Failure e ) ->
74 | text <| "Error loading widget data\n" ++ toString e
75 |
76 | ( Just account, Success w ) ->
77 | row None
78 | [ width fill, height fill, center, verticalCenter ]
79 | [ column WidgetText
80 | [ spacing 5 ]
81 | [ text <| "Id : " ++ BigInt.toString w.id
82 | , text <| "Size : " ++ BigInt.toString w.size
83 | , text <| "Cost : " ++ BigInt.toString w.cost
84 | , text <| "Owner : " ++ EthUtils.addressToString w.owner
85 | , text <| "Sold : " ++ toString w.wasSold
86 | , when (not w.wasSold) <|
87 | case model.widgetSellPending of
88 | NotAsked ->
89 | button Button [ width (px 100), moveDown 10, onClick (Sell w.id) ] (text "Sell Me")
90 |
91 | Loading ->
92 | loadingView "Selling Widget"
93 |
94 | Failure _ ->
95 | text "Error selling"
96 |
97 | Success _ ->
98 | text "Sold!"
99 | ]
100 | ]
101 |
102 |
103 |
104 | -- UPDATE
105 |
106 |
107 | type Msg
108 | = NoOp
109 | | WidgetInfo (Result Http.Error Widget)
110 | | Sell BigInt
111 | | SellPending (Result String Tx)
112 | | Sold (Result String TxReceipt)
113 | | Fail String
114 |
115 |
116 | update : NodePath -> Msg -> Model -> ( Model, Cmd Msg, ChainCmd Msg )
117 | update nodePath msg model =
118 | case msg of
119 | NoOp ->
120 | ( model
121 | , Cmd.none
122 | , ChainCmd.none
123 | )
124 |
125 | WidgetInfo (Ok widget) ->
126 | ( { model | widget = Success widget }
127 | , Cmd.none
128 | , ChainCmd.none
129 | )
130 |
131 | WidgetInfo (Err err) ->
132 | ( { model | widget = Failure err }
133 | , Cmd.none
134 | , ChainCmd.none
135 | )
136 |
137 | Sell id ->
138 | let
139 | txParams =
140 | Widget.sellWidget ChainData.widgetFactory id
141 | |> (\txp -> { txp | gasPrice = Just <| gwei 20 })
142 | |> Eth.toSend
143 | in
144 | ( model
145 | , Cmd.none
146 | , ChainCmd.sendWithReceipt SellPending Sold txParams
147 | )
148 |
149 | SellPending (Ok _) ->
150 | ( { model | widgetSellPending = Loading }
151 | , Cmd.none
152 | , ChainCmd.none
153 | )
154 |
155 | SellPending (Err err) ->
156 | ( { model | widgetSellPending = Failure err }
157 | , Cmd.none
158 | , ChainCmd.none
159 | )
160 |
161 | Sold (Ok _) ->
162 | ( { model | widgetSellPending = Success () }
163 | , Cmd.none
164 | , ChainCmd.none
165 | )
166 |
167 | Sold (Err err) ->
168 | ( { model | widgetSellPending = Failure err }
169 | , Cmd.none
170 | , ChainCmd.none
171 | )
172 |
173 | Fail error ->
174 | ( { model | errors = toString error :: model.errors }
175 | , Cmd.none
176 | , ChainCmd.none
177 | )
178 |
--------------------------------------------------------------------------------
/examples/complex/src/elm/Ports.elm:
--------------------------------------------------------------------------------
1 | port module Ports exposing (..)
2 |
3 | import Json.Decode exposing (Value)
4 |
5 |
6 | port walletSentry : (Value -> msg) -> Sub msg
7 |
8 |
9 | port output : Value -> Cmd msg
10 |
11 |
12 | port input : (Value -> msg) -> Sub msg
13 |
14 |
15 | port txOut : Value -> Cmd msg
16 |
17 |
18 | port txIn : (Value -> msg) -> Sub msg
19 |
--------------------------------------------------------------------------------
/examples/complex/src/elm/Request/Chain.elm:
--------------------------------------------------------------------------------
1 | module Request.Chain exposing (..)
2 |
3 | -- Library
4 |
5 | import Eth as Eth
6 | import Eth.Types as Eth
7 | import Task exposing (Task)
8 | import Http
9 |
10 |
11 | -- Internal
12 |
13 | import Extra.BigInt exposing (countDownFrom)
14 | import Contracts.WidgetFactory as Widget exposing (Widget)
15 |
16 |
17 | getWidgetList : Eth.HttpProvider -> Eth.Address -> Task Http.Error (List Widget)
18 | getWidgetList ethNode widgetFactory =
19 | Eth.call ethNode (Widget.widgetCount widgetFactory)
20 | |> Task.andThen
21 | (\num ->
22 | countDownFrom num
23 | |> List.map
24 | (\id -> Eth.call ethNode (Widget.widgets widgetFactory id))
25 | |> Task.sequence
26 | )
27 |
--------------------------------------------------------------------------------
/examples/complex/src/elm/Request/Status.elm:
--------------------------------------------------------------------------------
1 | module Request.Status exposing (..)
2 |
3 |
4 | type RemoteData e a
5 | = NotAsked
6 | | Loading
7 | | Failure e
8 | | Success a
9 |
--------------------------------------------------------------------------------
/examples/complex/src/elm/Request/UPort.elm:
--------------------------------------------------------------------------------
1 | module Request.UPort exposing (ErrorData, Message(..), RequestData, SuccessData, User, authEndpoint, decodeMessage, errorDecoder, messageDecoder, requestDecoder, successDecoder, userDecoder, userJWTDecoder)
2 |
3 | import Base64
4 | import Json.Decode as Decode exposing (Decoder, decodeString, map, oneOf, string)
5 | import Json.Decode.Pipeline exposing (custom, decode, required, requiredAt)
6 | import List.Extra as ListExtra
7 |
8 |
9 | authEndpoint : String
10 | authEndpoint =
11 | "wss://uport-staging.opolis.co"
12 |
13 |
14 | type Message
15 | = Request RequestData
16 | | Success SuccessData
17 | | Error ErrorData
18 |
19 |
20 | type alias RequestData =
21 | { uri : String
22 | , qr : String
23 | }
24 |
25 |
26 | type alias SuccessData =
27 | { user : User }
28 |
29 |
30 | type alias ErrorData =
31 | { error : String }
32 |
33 |
34 | type alias User =
35 | { publicKey : String
36 | , publicEncKey : String
37 | , name : String
38 | , email : String
39 | , avatar : String
40 | , address : String
41 | , networkAddress : String
42 | }
43 |
44 |
45 | decodeMessage : (Message -> msg) -> String -> msg
46 | decodeMessage tag raw =
47 | case decodeString messageDecoder raw of
48 | Err error ->
49 | tag <| Error { error = error }
50 |
51 | Ok message ->
52 | tag message
53 |
54 |
55 | messageDecoder : Decoder Message
56 | messageDecoder =
57 | oneOf
58 | [ map Request requestDecoder
59 | , map Success successDecoder
60 | , map Error errorDecoder
61 | ]
62 |
63 |
64 | requestDecoder : Decoder RequestData
65 | requestDecoder =
66 | decode RequestData
67 | |> required "uri" string
68 | |> required "qr" string
69 |
70 |
71 | errorDecoder : Decoder ErrorData
72 | errorDecoder =
73 | decode ErrorData
74 | |> required "error" string
75 |
76 |
77 | {-| Turns a JWT into a User.
78 |
79 | 1. Receive JWT from uPort a service: {'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXQiOnsiQGN...M0NjQ3fQ.3b9Io8IFmmGjJWljGBGzKR7U2AR209QF\_WYp61qpgbc'}
80 | 2. Convert "token" field data from base64 to json string. "{'dat': { 'name': ..., 'email': ...} }"
81 | 3. Decode "dat" field into User type
82 |
83 | -}
84 | successDecoder : Decoder SuccessData
85 | successDecoder =
86 | decode SuccessData
87 | |> required "token" userJWTDecoder
88 |
89 |
90 | userJWTDecoder : Decoder User
91 | userJWTDecoder =
92 | let
93 | base64ToUser : String -> Result String User
94 | base64ToUser s =
95 | String.split "." s
96 | |> ListExtra.getAt 1
97 | |> Result.fromMaybe "Error decoding JWT"
98 | |> Result.andThen Base64.decode
99 | |> Result.andThen (Decode.decodeString (Decode.field "dat" userDecoder))
100 | in
101 | Decode.string
102 | |> Decode.andThen
103 | (\str ->
104 | case base64ToUser str of
105 | Err err ->
106 | Decode.fail err
107 |
108 | Ok suc ->
109 | Decode.succeed suc
110 | )
111 |
112 |
113 | userDecoder : Decoder User
114 | userDecoder =
115 | decode User
116 | |> required "publicKey" string
117 | |> required "publicEncKey" string
118 | |> required "name" string
119 | |> required "email" string
120 | |> requiredAt [ "avatar", "uri" ] string
121 | |> required "address" string
122 | |> required "networkAddress" string
123 |
--------------------------------------------------------------------------------
/examples/complex/src/elm/Route.elm:
--------------------------------------------------------------------------------
1 | module Route exposing (Route(..), fromLocation, link, modifyUrl)
2 |
3 | -- Library
4 |
5 | import BigInt exposing (BigInt)
6 | import Element as El exposing (Attribute, Element)
7 | import Navigation exposing (Location)
8 | import UrlParser as Url exposing ((>))
9 |
10 |
11 | type Route
12 | = Home
13 | | Login
14 | | Widget BigInt
15 |
16 |
17 | route : Url.Parser (Route -> a) a
18 | route =
19 | Url.oneOf
20 | [ Url.map Home Url.top
21 | , Url.map Login (Url.s "login")
22 | , Url.map Widget (Url.s "widget" > bigIntParser)
23 | ]
24 |
25 |
26 | routeToString : Route -> String
27 | routeToString route =
28 | let
29 | pieces =
30 | case route of
31 | Home ->
32 | []
33 |
34 | Login ->
35 | [ "login" ]
36 |
37 | Widget id ->
38 | [ "widget", BigInt.toString id ]
39 | in
40 | "#/" ++ String.join "/" pieces
41 |
42 |
43 |
44 | -- PUBLIC HELPERS --
45 |
46 |
47 | link : Route -> Element style variation msg -> Element style variation msg
48 | link route =
49 | El.link (routeToString route)
50 |
51 |
52 | modifyUrl : Route -> Cmd msg
53 | modifyUrl =
54 | routeToString >> Navigation.modifyUrl
55 |
56 |
57 | fromLocation : Location -> Maybe Route
58 | fromLocation location =
59 | if String.isEmpty location.hash then
60 | Just Home
61 | else
62 | Url.parseHash route location
63 |
64 |
65 |
66 | -- PARSERS
67 |
68 |
69 | bigIntParser : Url.Parser (BigInt -> b) b
70 | bigIntParser =
71 | Url.custom "BIGINT" (BigInt.fromString >> Result.fromMaybe "BigInt.toString error")
72 |
--------------------------------------------------------------------------------
/examples/complex/src/elm/Views/Helpers.elm:
--------------------------------------------------------------------------------
1 | module Views.Helpers exposing (..)
2 |
3 | -- Libraries
4 |
5 | import Element exposing (..)
6 | import Element.Attributes exposing (..)
7 | import Element.Events exposing (..)
8 | import List.Extra as List
9 | import SelectList as SL exposing (SelectList)
10 |
11 |
12 | -- Internal
13 |
14 | import Views.Styles exposing (..)
15 | import Eth.Types exposing (Address)
16 | import Eth.Utils exposing (addressToString)
17 |
18 |
19 | etherscanLink : Address -> Element Styles Variations msg
20 | etherscanLink address =
21 | let
22 | address_ =
23 | addressToString address
24 | in
25 | newTab ("https://ropsten.etherscan.io/address/" ++ address_) <|
26 | underline (String.left 5 address_ ++ "..." ++ String.right 5 address_)
27 |
28 |
29 | viewBreadcrumbs : (formStep -> String) -> (formStep -> msg) -> formStep -> SelectList formStep -> Element Styles Variations msg
30 | viewBreadcrumbs stepToString changeStep lastStep steps =
31 | let
32 | stepsList =
33 | SL.toList steps
34 |
35 | selected =
36 | SL.selected steps
37 |
38 | lastIndex =
39 | (List.length stepsList) - 1
40 |
41 | previousStep step =
42 | SL.before steps
43 | |> List.last
44 | |> Maybe.map ((==) step)
45 | |> Maybe.withDefault False
46 |
47 | -- First items in the list have the highest z-index
48 | viewCrumb index step =
49 | el BreadcrumbItemWrapper
50 | [ vary Selected (previousStep step)
51 | , vary LastItemSelected (index == lastIndex - 1 && lastStep == selected)
52 | , vary LastItem (index == lastIndex)
53 | , vary FirstItem (index == 0)
54 | , inlineStyle [ ( "z-index", toString (30 - index) ) ]
55 | ]
56 | (el BreadcrumbItem
57 | [ paddingXY 15 10.1
58 | , inlineStyle [ ( "z-index", toString (30 - index) ) ]
59 | , vary Shadowed (index /= lastIndex)
60 | , vary Selected (step == selected && index /= lastIndex)
61 | , vary FirstItem (index == 0)
62 | , vary LastItemSelected (index == lastIndex && lastStep == selected)
63 | , onClick <| changeStep step
64 | ]
65 | (text <| stepToString step)
66 | )
67 | in
68 | row None [] (List.indexedMap viewCrumb stepsList)
69 |
--------------------------------------------------------------------------------
/examples/complex/src/elm/Views/Styles.elm:
--------------------------------------------------------------------------------
1 | module Views.Styles exposing (..)
2 |
3 | import Color exposing (rgb, rgba)
4 | import Style exposing (..)
5 | import Style.Color as Color
6 | import Style.Background as Background
7 | import Style.Border as Border
8 | import Style.Font as Font
9 | import Style.Filter as Filter
10 | import Style.Shadow as Shadow
11 | import Style.Transition as Transition
12 |
13 |
14 | type Styles
15 | = None
16 | | Button
17 | | Header
18 | | WidgetSummary
19 | | WidgetConfirm
20 | | WidgetText
21 | | ProfileImage
22 | | Sidebar
23 | | Status
24 | | StatusSuccess
25 | | StatusFailure
26 | | StatusAlert
27 | --Login
28 | | LoginBox
29 | | LoginPage
30 | -- Modal
31 | | ModalBox
32 | | ModalBoxSelection
33 | | ModalBoxSelectionMultiline
34 | | BreadcrumbItem
35 | | BreadcrumbItemWrapper
36 |
37 |
38 | type Variations
39 | = H2
40 | | H3
41 | | H4
42 | | Small
43 | | Bold
44 | | Selected
45 | | Shadowed
46 | | FirstItem
47 | | LastItem
48 | | LastItemSelected
49 | | WidgetBlue
50 | | WidgetWhite
51 |
52 |
53 | stylesheet : StyleSheet Styles Variations
54 | stylesheet =
55 | let
56 | fontSourceSans =
57 | Font.typeface
58 | [ Font.font "Source Sans Pro", Font.sansSerif ]
59 |
60 | textGray =
61 | rgb 112 112 112
62 |
63 | widgetGray =
64 | rgb 64 64 64
65 |
66 | widgetGrayOpacity a =
67 | rgba 64 64 64 a
68 |
69 | widgetOrange =
70 | rgb 245 132 33
71 |
72 | widgetBlue =
73 | rgb 0 122 255
74 |
75 | widgetRed =
76 | rgb 226 64 54
77 |
78 | widgetLightGray =
79 | rgb 171 180 189
80 |
81 | widgetGreen =
82 | rgb 116 203 52
83 | in
84 | Style.styleSheet
85 | [ style None []
86 | , style Button
87 | [ Border.all 1
88 | , Border.rounded 4
89 | , Color.border widgetGray
90 | , Color.background widgetGray
91 | , Color.text Color.white
92 | , fontSourceSans
93 | , hover
94 | [ Color.background <| widgetGrayOpacity 0.8
95 | , Color.border <| widgetGrayOpacity 0.8
96 | , Style.cursor "pointer"
97 | ]
98 | ]
99 | , style Header
100 | [ fontSourceSans
101 | , Color.text widgetGray
102 | , Font.weight 75
103 | , variation H2 [ Font.size 30 ]
104 | , variation H3 [ Font.size 25 ]
105 | , variation H4 [ Font.size 20 ]
106 | , variation Bold [ Font.bold ]
107 | , variation WidgetBlue [ Color.text widgetBlue ]
108 | , variation WidgetWhite [ Color.text Color.white ]
109 | ]
110 | , style WidgetSummary
111 | [ Border.all 1
112 | , Border.rounded 30
113 | , Color.border textGray
114 | , Transition.all
115 | , Shadow.deep
116 | , hover
117 | [ Shadow.simple ]
118 | ]
119 | , style WidgetConfirm
120 | [ Border.all 1
121 | , Border.rounded 20
122 | , Color.border textGray
123 | ]
124 | , style WidgetText
125 | [ fontSourceSans
126 | , Font.weight 2
127 | , Font.size 16
128 | , Font.light
129 | , variation Bold [ Font.bold ]
130 | , variation Small [ Font.size 13 ]
131 | , variation WidgetBlue [ Color.text widgetBlue ]
132 | , variation WidgetWhite [ Color.text Color.white ]
133 | ]
134 | , style ProfileImage
135 | [ Border.all 3
136 | , Color.border widgetLightGray
137 | , Color.background Color.white
138 | ]
139 | , style Sidebar
140 | [ Border.right 1
141 | , Color.border Color.black
142 | , Color.background widgetGray
143 | , Shadow.box
144 | { offset = ( 0, 0 )
145 | , size = 1
146 | , blur = 2
147 | , color = widgetLightGray
148 | }
149 | ]
150 | , style Status
151 | [ fontSourceSans
152 | , Color.text Color.white
153 | ]
154 | , style StatusSuccess
155 | [ Color.background Color.green ]
156 | , style StatusFailure
157 | [ Color.background Color.red ]
158 | , style StatusAlert
159 | [ Color.background Color.orange ]
160 | , style LoginBox
161 | [ fontSourceSans
162 | , Color.background <| widgetGrayOpacity 0.8
163 | , Color.text Color.white
164 | , Border.rounded 5
165 | , Font.size 20
166 | , Shadow.box
167 | { offset = ( 0, 0 )
168 | , size = 3
169 | , blur = 10
170 | , color = rgba 240 240 240 0.2
171 | }
172 | , hover [ Style.cursor "pointer" ]
173 | ]
174 | , style LoginPage
175 | [ Background.imageWith
176 | { src = "static/img/potter-bw.jpg"
177 | , position = ( 0, 0 )
178 | , repeat = Background.noRepeat
179 | , size = Background.cover
180 | }
181 | ]
182 | , style ModalBox
183 | [ Border.rounded 5
184 | , Border.all 1
185 | , Color.background Color.white
186 | , Color.border widgetLightGray
187 | ]
188 | , style ModalBoxSelection
189 | [ Border.bottom 1
190 | , Color.border widgetLightGray
191 | , focus [ Color.border widgetBlue ]
192 | ]
193 | , style ModalBoxSelectionMultiline
194 | [ Border.all 1
195 | , Color.border widgetLightGray
196 | , focus [ Color.border widgetBlue ]
197 | ]
198 | , style BreadcrumbItem
199 | [ Color.background Color.darkGrey
200 | , Border.roundTopRight 100
201 | , Border.roundBottomRight 100
202 | , Color.text Color.white
203 | , Style.cursor "pointer"
204 | , variation Shadowed [ Shadow.drop { offset = ( 5, 0 ), blur = 5, color = widgetGrayOpacity 0.2 } ]
205 | , variation FirstItem [ Border.roundTopLeft 100, Border.roundBottomLeft 100 ]
206 | , variation Selected [ Color.text widgetBlue, Color.background Color.white ]
207 | , variation LastItemSelected [ Color.background widgetGreen ]
208 | , Font.size 13
209 | , fontSourceSans
210 | ]
211 | , style BreadcrumbItemWrapper
212 | [ Color.background Color.darkGrey
213 | , variation Selected [ Color.background Color.white ]
214 | , variation FirstItem [ Border.roundTopLeft 100, Border.roundBottomLeft 100 ]
215 | , variation LastItem [ Border.roundTopRight 100, Border.roundBottomRight 100 ]
216 | , variation LastItemSelected [ Color.background widgetGreen ]
217 | ]
218 | ]
219 |
--------------------------------------------------------------------------------
/examples/complex/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cmditch/elm-ethereum/e6796692cb830b115f2b71d91e4ddcb88f5a5f20/examples/complex/src/favicon.ico
--------------------------------------------------------------------------------
/examples/complex/src/solidity/WidgetFactory.abi:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "constant": true,
4 | "inputs": [
5 | {
6 | "name": "",
7 | "type": "uint256"
8 | }
9 | ],
10 | "name": "widgets",
11 | "outputs": [
12 | {
13 | "name": "id",
14 | "type": "uint256"
15 | },
16 | {
17 | "name": "size",
18 | "type": "uint256"
19 | },
20 | {
21 | "name": "cost",
22 | "type": "uint256"
23 | },
24 | {
25 | "name": "owner",
26 | "type": "address"
27 | },
28 | {
29 | "name": "wasSold",
30 | "type": "bool"
31 | }
32 | ],
33 | "payable": false,
34 | "stateMutability": "view",
35 | "type": "function"
36 | },
37 | {
38 | "constant": false,
39 | "inputs": [
40 | {
41 | "name": "size_",
42 | "type": "uint256"
43 | },
44 | {
45 | "name": "cost_",
46 | "type": "uint256"
47 | },
48 | {
49 | "name": "owner_",
50 | "type": "address"
51 | }
52 | ],
53 | "name": "newWidget",
54 | "outputs": [],
55 | "payable": false,
56 | "stateMutability": "nonpayable",
57 | "type": "function"
58 | },
59 | {
60 | "constant": false,
61 | "inputs": [
62 | {
63 | "name": "id_",
64 | "type": "uint256"
65 | }
66 | ],
67 | "name": "sellWidget",
68 | "outputs": [],
69 | "payable": false,
70 | "stateMutability": "nonpayable",
71 | "type": "function"
72 | },
73 | {
74 | "constant": true,
75 | "inputs": [],
76 | "name": "widgetCount",
77 | "outputs": [
78 | {
79 | "name": "",
80 | "type": "uint256"
81 | }
82 | ],
83 | "payable": false,
84 | "stateMutability": "view",
85 | "type": "function"
86 | },
87 | {
88 | "anonymous": false,
89 | "inputs": [
90 | {
91 | "indexed": false,
92 | "name": "id",
93 | "type": "uint256"
94 | },
95 | {
96 | "indexed": false,
97 | "name": "size",
98 | "type": "uint256"
99 | },
100 | {
101 | "indexed": false,
102 | "name": "cost",
103 | "type": "uint256"
104 | },
105 | {
106 | "indexed": false,
107 | "name": "owner",
108 | "type": "address"
109 | }
110 | ],
111 | "name": "WidgetCreated",
112 | "type": "event"
113 | },
114 | {
115 | "anonymous": false,
116 | "inputs": [
117 | {
118 | "indexed": false,
119 | "name": "id",
120 | "type": "uint256"
121 | }
122 | ],
123 | "name": "WidgetSold",
124 | "type": "event"
125 | }
126 | ]
--------------------------------------------------------------------------------
/examples/complex/src/solidity/WidgetFactory.sol:
--------------------------------------------------------------------------------
1 | pragma solidity 0.4.24;
2 |
3 | contract WidgetFactory {
4 |
5 | event WidgetCreated(uint id, uint size, uint cost, address owner);
6 | event WidgetSold(uint id);
7 |
8 | struct Widget {
9 | uint id;
10 | uint size;
11 | uint cost;
12 | address owner;
13 | bool wasSold;
14 | }
15 |
16 | Widget[] public widgets;
17 |
18 | function newWidget(uint size_, uint cost_, address owner_) public {
19 | uint id = widgets.length;
20 | widgets.push(Widget(id, size_, cost_, owner_, false));
21 | emit WidgetCreated(id, size_, cost_, owner_);
22 | }
23 |
24 | function sellWidget(uint id_) public {
25 | Widget storage widget = widgets[id_];
26 | widget.wasSold = true;
27 | emit WidgetSold(id_);
28 | }
29 |
30 | function widgetCount() public view returns(uint) {
31 | return widgets.length;
32 | }
33 | }
--------------------------------------------------------------------------------
/examples/complex/src/static/img/city_blue_denver.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cmditch/elm-ethereum/e6796692cb830b115f2b71d91e4ddcb88f5a5f20/examples/complex/src/static/img/city_blue_denver.jpg
--------------------------------------------------------------------------------
/examples/complex/src/static/img/elm-ethereum-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/complex/src/static/img/loader.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cmditch/elm-ethereum/e6796692cb830b115f2b71d91e4ddcb88f5a5f20/examples/complex/src/static/img/loader.gif
--------------------------------------------------------------------------------
/examples/complex/src/static/img/potter-bw.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cmditch/elm-ethereum/e6796692cb830b115f2b71d91e4ddcb88f5a5f20/examples/complex/src/static/img/potter-bw.jpg
--------------------------------------------------------------------------------
/examples/complex/src/static/img/potter-solo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cmditch/elm-ethereum/e6796692cb830b115f2b71d91e4ddcb88f5a5f20/examples/complex/src/static/img/potter-solo.jpg
--------------------------------------------------------------------------------
/examples/complex/src/static/img/uport.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cmditch/elm-ethereum/e6796692cb830b115f2b71d91e4ddcb88f5a5f20/examples/complex/src/static/img/uport.png
--------------------------------------------------------------------------------
/examples/complex/src/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | elm-webpack-starter
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/examples/complex/src/static/index.js:
--------------------------------------------------------------------------------
1 | var elm_ethereum_ports = require('elm-ethereum-ports');
2 |
3 | var Elm = require( '../elm/Main' );
4 |
5 | window.addEventListener('load', function () {
6 | if (typeof web3 !== 'undefined') {
7 | web3.version.getNetwork(function (e, networkId) {
8 | app = Elm.Main.fullscreen(parseInt(networkId || 0));
9 | elm_ethereum_ports.txSentry(app.ports.txOut, app.ports.txIn, web3);
10 | elm_ethereum_ports.walletSentry(app.ports.walletSentry, web3);
11 | });
12 | } else {
13 | app = Elm.Main.fullscreen(0);
14 | console.log("Metamask not detected.");
15 | }
16 | });
17 |
18 |
--------------------------------------------------------------------------------
/examples/complex/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const webpack = require("webpack");
3 | const merge = require("webpack-merge");
4 | const elmMinify = require("elm-minify");
5 |
6 | const CopyWebpackPlugin = require("copy-webpack-plugin");
7 | const HTMLWebpackPlugin = require("html-webpack-plugin");
8 | const CleanWebpackPlugin = require("clean-webpack-plugin");
9 | // to extract the css as a separate file
10 | const MiniCssExtractPlugin = require("mini-css-extract-plugin");
11 |
12 | var MODE =
13 | process.env.npm_lifecycle_event === "prod" ? "production" : "development";
14 | var withDebug = !process.env["npm_config_nodebug"];
15 | // this may help for Yarn users
16 | // var withDebug = !npmParams.includes("--nodebug");
17 | console.log('\x1b[36m%s\x1b[0m', `** elm-webpack-starter: mode "${MODE}", withDebug: ${withDebug}\n`);
18 |
19 | var common = {
20 | mode: MODE,
21 | entry: "./src/index.js",
22 | output: {
23 | path: path.join(__dirname, "dist"),
24 | publicPath: "/",
25 | // FIXME webpack -p automatically adds hash when building for production
26 | filename: MODE == "production" ? "[name]-[hash].js" : "index.js"
27 | },
28 | plugins: [
29 | new HTMLWebpackPlugin({
30 | // Use this template to get basic responsive meta tags
31 | template: "src/index.html",
32 | // inject details of output file at end of body
33 | inject: "body"
34 | })
35 | ],
36 | resolve: {
37 | modules: [path.join(__dirname, "src"), "node_modules"],
38 | extensions: [".js", ".elm", ".scss", ".png"]
39 | },
40 | module: {
41 | rules: [
42 | {
43 | test: /\.js$/,
44 | exclude: /node_modules/,
45 | use: {
46 | loader: "babel-loader"
47 | }
48 | },
49 | {
50 | test: /\.scss$/,
51 | exclude: [/elm-stuff/, /node_modules/],
52 | // see https://github.com/webpack-contrib/css-loader#url
53 | loaders: ["style-loader", "css-loader?url=false", "sass-loader"]
54 | },
55 | {
56 | test: /\.css$/,
57 | exclude: [/elm-stuff/, /node_modules/],
58 | loaders: ["style-loader", "css-loader?url=false"]
59 | },
60 | {
61 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
62 | exclude: [/elm-stuff/, /node_modules/],
63 | loader: "url-loader",
64 | options: {
65 | limit: 10000,
66 | mimetype: "application/font-woff"
67 | }
68 | },
69 | {
70 | test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
71 | exclude: [/elm-stuff/, /node_modules/],
72 | loader: "file-loader"
73 | },
74 | {
75 | test: /\.(jpe?g|png|gif|svg)$/i,
76 | exclude: [/elm-stuff/, /node_modules/],
77 | loader: "file-loader"
78 | }
79 | ]
80 | }
81 | };
82 |
83 | if (MODE === "development") {
84 | module.exports = merge(common, {
85 | plugins: [
86 | // Suggested for hot-loading
87 | new webpack.NamedModulesPlugin(),
88 | // Prevents compilation errors causing the hot loader to lose state
89 | new webpack.NoEmitOnErrorsPlugin()
90 | ],
91 | module: {
92 | rules: [
93 | {
94 | test: /\.elm$/,
95 | exclude: [/elm-stuff/, /node_modules/],
96 | use: [
97 | { loader: "elm-hot-webpack-loader" },
98 | {
99 | loader: "elm-webpack-loader",
100 | options: {
101 | // add Elm's debug overlay to output
102 | debug: withDebug,
103 | //
104 | forceWatch: true
105 | }
106 | }
107 | ]
108 | }
109 | ]
110 | },
111 | devServer: {
112 | inline: true,
113 | stats: "errors-only",
114 | contentBase: path.join(__dirname, "src/assets"),
115 | historyApiFallback: true,
116 | // feel free to delete this section if you don't need anything like this
117 | before(app) {
118 | // on port 3000
119 | app.get("/test", function(req, res) {
120 | res.json({ result: "OK" });
121 | });
122 | }
123 | }
124 | });
125 | }
126 | if (MODE === "production") {
127 | module.exports = merge(common, {
128 | plugins: [
129 | // Minify elm code
130 | new elmMinify.WebpackPlugin(),
131 | // Delete everything from output-path (/dist) and report to user
132 | new CleanWebpackPlugin({
133 | root: __dirname,
134 | exclude: [],
135 | verbose: true,
136 | dry: false
137 | }),
138 | // Copy static assets
139 | new CopyWebpackPlugin([
140 | {
141 | from: "src/assets"
142 | }
143 | ]),
144 | new MiniCssExtractPlugin({
145 | // Options similar to the same options in webpackOptions.output
146 | // both options are optional
147 | filename: "[name]-[hash].css"
148 | })
149 | ],
150 | module: {
151 | rules: [
152 | {
153 | test: /\.elm$/,
154 | exclude: [/elm-stuff/, /node_modules/],
155 | use: {
156 | loader: "elm-webpack-loader",
157 | options: {
158 | optimize: true
159 | }
160 | }
161 | },
162 | {
163 | test: /\.css$/,
164 | exclude: [/elm-stuff/, /node_modules/],
165 | loaders: [
166 | MiniCssExtractPlugin.loader,
167 | "css-loader?url=false"
168 | ]
169 | },
170 | {
171 | test: /\.scss$/,
172 | exclude: [/elm-stuff/, /node_modules/],
173 | loaders: [
174 | MiniCssExtractPlugin.loader,
175 | "css-loader?url=false",
176 | "sass-loader"
177 | ]
178 | }
179 | ]
180 | }
181 | });
182 | }
183 |
--------------------------------------------------------------------------------
/examples/simple/README.md:
--------------------------------------------------------------------------------
1 | # elm-ethereum simple example
2 |
3 | ```bash
4 | git clone git@github.com:cmditch/elm-ethereum.git
5 | cd elm-ethereum/examples/simple
6 | npm reinstall
7 | npm run dev
8 |
9 | open http://localhost:8080
10 | ```
11 |
--------------------------------------------------------------------------------
/examples/simple/elm.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "application",
3 | "source-directories": [
4 | "../../src",
5 | "src"
6 | ],
7 | "elm-version": "0.19.0",
8 | "dependencies": {
9 | "direct": {
10 | "Chadtech/elm-bool-extra": "2.3.0",
11 | "NoRedInk/elm-json-decode-pipeline": "1.0.0",
12 | "NoRedInk/elm-string-conversions": "1.0.1",
13 | "cmditch/elm-bigint": "2.0.0",
14 | "elm/browser": "1.0.1",
15 | "elm/core": "1.0.2",
16 | "elm/html": "1.0.0",
17 | "elm/http": "2.0.0",
18 | "elm/json": "1.1.3",
19 | "elm/regex": "1.0.0",
20 | "elm/time": "1.0.0",
21 | "elm-community/json-extra": "4.0.0",
22 | "elm-community/list-extra": "8.1.0",
23 | "elm-community/maybe-extra": "5.0.0",
24 | "elm-community/result-extra": "2.2.1",
25 | "elm-community/string-extra": "4.0.0",
26 | "prozacchiwawa/elm-keccak": "2.0.0",
27 | "rtfeldman/elm-hex": "1.0.0",
28 | "zwilias/elm-utf-tools": "2.0.1"
29 | },
30 | "indirect": {
31 | "elm/bytes": "1.0.8",
32 | "elm/file": "1.0.4",
33 | "elm/parser": "1.1.0",
34 | "elm/url": "1.0.0",
35 | "elm/virtual-dom": "1.0.2",
36 | "rtfeldman/elm-iso8601-date-strings": "1.1.2"
37 | }
38 | },
39 | "test-dependencies": {
40 | "direct": {},
41 | "indirect": {}
42 | }
43 | }
--------------------------------------------------------------------------------
/examples/simple/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Coury Ditch",
3 | "name": "elm-ethereum-simple-example",
4 | "version": "0.0.2",
5 | "description": "Elm 0.19 and elm-ethereum 3.0.x",
6 | "main": "index.js",
7 | "scripts": {
8 | "test": "elm-test",
9 | "start": "npm run dev",
10 | "dev": "webpack-dev-server --hot --colors --port 3000",
11 | "build": "webpack",
12 | "prod": "webpack -p",
13 | "analyse": "elm-analyse -s -p 3001 -o"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/cmditch/elm-ethereum.git"
18 | },
19 | "license": "MIT",
20 | "devDependencies": {
21 | "@babel/core": "^7.3.4",
22 | "@babel/preset-env": "^7.3.4",
23 | "babel-loader": "^8.0.5",
24 | "clean-webpack-plugin": "^2.0.0",
25 | "copy-webpack-plugin": "^5.0.0",
26 | "css-loader": "^2.1.0",
27 | "elm": "^0.19.0-bugfix6",
28 | "elm-analyse": "^0.16.3",
29 | "elm-hot-webpack-loader": "^1.0.2",
30 | "elm-minify": "^2.0.4",
31 | "elm-test": "^0.19.0",
32 | "elm-webpack-loader": "^5.0.0",
33 | "file-loader": "^3.0.1",
34 | "html-webpack-plugin": "^3.2.0",
35 | "mini-css-extract-plugin": "^0.5.0",
36 | "node-sass": "^4.13.1",
37 | "resolve-url-loader": "^3.0.1",
38 | "sass-loader": "^7.1.0",
39 | "style-loader": "^0.23.1",
40 | "url-loader": "^1.1.2",
41 | "webpack": "^4.29.6",
42 | "webpack-cli": "^3.2.3",
43 | "webpack-dev-server": "^3.2.1",
44 | "webpack-merge": "^4.2.1"
45 | },
46 | "dependencies": {
47 | "purecss": "^1.0.0",
48 | "elm-ethereum-ports": "^1.0.1"
49 | },
50 | "prettier": {
51 | "tabWidth": 4
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/examples/simple/src/Main.elm:
--------------------------------------------------------------------------------
1 | port module Main exposing
2 | ( EthNode
3 | , Model
4 | , Msg(..)
5 | , ethNode
6 | , init
7 | , main
8 | , subscriptions
9 | , txIn
10 | , txOut
11 | , update
12 | , view
13 | , viewThing
14 | , walletSentry
15 | )
16 |
17 | import Browser exposing (document)
18 | import Eth
19 | import Eth.Net as Net exposing (NetworkId(..))
20 | import Eth.Sentry.Tx as TxSentry exposing (..)
21 | import Eth.Sentry.Wallet as WalletSentry exposing (WalletSentry)
22 | import Eth.Types exposing (..)
23 | import Eth.Units exposing (gwei)
24 | import Eth.Utils
25 | import Html exposing (..)
26 | import Html.Events exposing (onClick)
27 | import Http
28 | import Json.Decode as Decode exposing (Value)
29 | import Process
30 | import Task
31 |
32 |
33 | main : Program Int Model Msg
34 | main =
35 | Browser.element
36 | { init = init
37 | , view = view
38 | , update = update
39 | , subscriptions = subscriptions
40 | }
41 |
42 |
43 | type alias Model =
44 | { txSentry : TxSentry Msg
45 | , account : Maybe Address
46 | , node : EthNode
47 | , blockNumber : Maybe Int
48 | , txHash : Maybe TxHash
49 | , tx : Maybe Tx
50 | , txReceipt : Maybe TxReceipt
51 | , blockDepth : Maybe TxTracker
52 | , errors : List String
53 | }
54 |
55 |
56 | init : Int -> ( Model, Cmd Msg )
57 | init networkId =
58 | let
59 | node =
60 | Net.toNetworkId networkId
61 | |> ethNode
62 | in
63 | ( { txSentry = TxSentry.init ( txOut, txIn ) TxSentryMsg node.http
64 | , account = Nothing
65 | , node = node
66 | , blockNumber = Nothing
67 | , txHash = Nothing
68 | , tx = Nothing
69 | , txReceipt = Nothing
70 | , blockDepth = Nothing
71 | , errors = []
72 | }
73 | , Task.attempt PollBlock (Eth.getBlockNumber node.http)
74 | )
75 |
76 |
77 | type alias EthNode =
78 | { http : HttpProvider
79 | , ws : WebsocketProvider
80 | }
81 |
82 |
83 | ethNode : NetworkId -> EthNode
84 | ethNode networkId =
85 | case networkId of
86 | Mainnet ->
87 | EthNode "https://mainnet.infura.io/" "wss://mainnet.infura.io/ws"
88 |
89 | Ropsten ->
90 | EthNode "https://ropsten.infura.io/" "wss://ropsten.infura.io/ws"
91 |
92 | Rinkeby ->
93 | EthNode "https://rinkeby.infura.io/" "wss://rinkeby.infura.io/ws"
94 |
95 | _ ->
96 | EthNode "UnknownEthNetwork" "UnknownEthNetwork"
97 |
98 |
99 |
100 | -- View
101 |
102 |
103 | view : Model -> Html Msg
104 | view model =
105 | div []
106 | [ div []
107 | (List.map viewThing
108 | [ ( "Current Block", maybeToString String.fromInt "No blocknumber found yet" model.blockNumber )
109 | , ( "--------------------", "" )
110 | , ( "TxHash", maybeToString Eth.Utils.txHashToString "No TxHash yet" model.txHash )
111 | ]
112 | )
113 | , viewTxTracker model.blockDepth
114 | , div [] [ button [ onClick InitTx ] [ text "Send 0 value Tx to yourself as a test" ] ]
115 | , div [] (List.map (\e -> div [] [ text e ]) model.errors)
116 | ]
117 |
118 |
119 | viewThing : ( String, String ) -> Html Msg
120 | viewThing ( name, val ) =
121 | div []
122 | [ div [] [ text name ]
123 | , div [] [ text val ]
124 | ]
125 |
126 |
127 |
128 | -- Update
129 |
130 |
131 | type Msg
132 | = TxSentryMsg TxSentry.Msg
133 | | WalletStatus WalletSentry
134 | | PollBlock (Result Http.Error Int)
135 | | InitTx
136 | | WatchTxHash (Result String TxHash)
137 | | WatchTx (Result String Tx)
138 | | WatchTxReceipt (Result String TxReceipt)
139 | | TrackTx TxTracker
140 | | Fail String
141 | | NoOp
142 |
143 |
144 | update : Msg -> Model -> ( Model, Cmd Msg )
145 | update msg model =
146 | case msg of
147 | TxSentryMsg subMsg ->
148 | let
149 | ( subModel, subCmd ) =
150 | TxSentry.update subMsg model.txSentry
151 | in
152 | ( { model | txSentry = subModel }, subCmd )
153 |
154 | WalletStatus walletSentry_ ->
155 | ( { model
156 | | account = walletSentry_.account
157 | , node = ethNode walletSentry_.networkId
158 | }
159 | , Cmd.none
160 | )
161 |
162 | PollBlock (Ok blockNumber) ->
163 | ( { model | blockNumber = Just blockNumber }
164 | , Task.attempt PollBlock <|
165 | Task.andThen (\_ -> Eth.getBlockNumber model.node.http) (Process.sleep 1000)
166 | )
167 |
168 | PollBlock (Err error) ->
169 | ( model, Cmd.none )
170 |
171 | InitTx ->
172 | let
173 | txParams =
174 | { to = model.account
175 | , from = model.account
176 | , gas = Nothing
177 | , gasPrice = Just <| gwei 4
178 | , value = Just <| gwei 1
179 | , data = Nothing
180 | , nonce = Nothing
181 | }
182 |
183 | ( newSentry, sentryCmd ) =
184 | TxSentry.customSend
185 | model.txSentry
186 | { onSign = Just WatchTxHash
187 | , onBroadcast = Just WatchTx
188 | , onMined = Just ( WatchTxReceipt, Just { confirmations = 3, toMsg = TrackTx } )
189 | }
190 | txParams
191 | in
192 | ( { model | txSentry = newSentry }, sentryCmd )
193 |
194 | WatchTxHash (Ok txHash) ->
195 | ( { model | txHash = Just txHash }, Cmd.none )
196 |
197 | WatchTxHash (Err err) ->
198 | ( { model | errors = ("Error Retrieving TxHash: " ++ err) :: model.errors }, Cmd.none )
199 |
200 | WatchTx (Ok tx) ->
201 | ( { model | tx = Just tx }, Cmd.none )
202 |
203 | WatchTx (Err err) ->
204 | ( { model | errors = ("Error Retrieving Tx: " ++ err) :: model.errors }, Cmd.none )
205 |
206 | WatchTxReceipt (Ok txReceipt) ->
207 | ( { model | txReceipt = Just txReceipt }, Cmd.none )
208 |
209 | WatchTxReceipt (Err err) ->
210 | ( { model | errors = ("Error Retrieving TxReceipt: " ++ err) :: model.errors }, Cmd.none )
211 |
212 | TrackTx blockDepth ->
213 | ( { model | blockDepth = Just blockDepth }, Cmd.none )
214 |
215 | Fail str ->
216 | let
217 | _ =
218 | Debug.log str
219 | in
220 | ( model, Cmd.none )
221 |
222 | NoOp ->
223 | ( model, Cmd.none )
224 |
225 |
226 | subscriptions : Model -> Sub Msg
227 | subscriptions model =
228 | Sub.batch
229 | [ walletSentry (WalletSentry.decodeToMsg Fail WalletStatus)
230 | , TxSentry.listen model.txSentry
231 | ]
232 |
233 |
234 |
235 | -- Ports
236 |
237 |
238 | port walletSentry : (Value -> msg) -> Sub msg
239 |
240 |
241 | port txOut : Value -> Cmd msg
242 |
243 |
244 | port txIn : (Value -> msg) -> Sub msg
245 |
246 |
247 |
248 | -- Helpers
249 |
250 |
251 | maybeToString : (a -> String) -> String -> Maybe a -> String
252 | maybeToString toString onNothing mVal =
253 | case mVal of
254 | Nothing ->
255 | onNothing
256 |
257 | Just a ->
258 | toString a
259 |
260 |
261 | viewTxTracker : Maybe TxTracker -> Html msg
262 | viewTxTracker mTxTracker =
263 | case mTxTracker of
264 | Nothing ->
265 | text "Waiting for tx to be sent or mined...."
266 |
267 | Just txTracker ->
268 | [ " TxTracker"
269 | , " { currentDepth : " ++ String.fromInt txTracker.currentDepth
270 | , " , minedInBlock : " ++ String.fromInt txTracker.minedInBlock
271 | , " , stopWatchingAtBlock : " ++ String.fromInt txTracker.stopWatchingAtBlock
272 | , " , lastCheckedBlock : " ++ String.fromInt txTracker.lastCheckedBlock
273 | , " , txHash : " ++ Eth.Utils.txHashToString txTracker.txHash
274 | , " , doneWatching : " ++ boolToString txTracker.doneWatching
275 | , " , reOrg : " ++ boolToString txTracker.reOrg
276 | , " }"
277 | , ""
278 | ]
279 | |> List.map (\n -> div [] [ text n ])
280 | |> div []
281 |
282 |
283 | boolToString : Bool -> String
284 | boolToString b =
285 | case b of
286 | True ->
287 | "True"
288 |
289 | False ->
290 | "False"
291 |
--------------------------------------------------------------------------------
/examples/simple/src/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cmditch/elm-ethereum/e6796692cb830b115f2b71d91e4ddcb88f5a5f20/examples/simple/src/assets/favicon.ico
--------------------------------------------------------------------------------
/examples/simple/src/assets/static/img/.gitignore:
--------------------------------------------------------------------------------
1 | # Just here to allow the img folder to be added to git.
2 | # ignore everything in this directory except this file.
3 | *
4 | !.gitignore
5 |
--------------------------------------------------------------------------------
/examples/simple/src/index.js:
--------------------------------------------------------------------------------
1 | var elm_ethereum_ports = require('elm-ethereum-ports');
2 |
3 | const {Elm} = require('./Main');
4 | var node = document.getElementById("elm-app")
5 |
6 | window.addEventListener('load', function () {
7 | if (typeof web3 !== 'undefined') {
8 | web3.version.getNetwork(function (e, networkId) {
9 | app = Elm.Main.init({flags: parseInt(networkId), node: node});
10 | elm_ethereum_ports.txSentry(app.ports.txOut, app.ports.txIn, web3);
11 | elm_ethereum_ports.walletSentry(app.ports.walletSentry, web3);
12 | ethereum.enable();
13 | });
14 | } else {
15 | app = Elm.Main.init({flags: 0, node: node});
16 | console.log("Metamask not detected.");
17 | }
18 | });
--------------------------------------------------------------------------------
/examples/simple/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const webpack = require("webpack");
3 | const merge = require("webpack-merge");
4 | const elmMinify = require("elm-minify");
5 |
6 | const CopyWebpackPlugin = require("copy-webpack-plugin");
7 | const HTMLWebpackPlugin = require("html-webpack-plugin");
8 | const CleanWebpackPlugin = require("clean-webpack-plugin");
9 | // to extract the css as a separate file
10 | const MiniCssExtractPlugin = require("mini-css-extract-plugin");
11 |
12 | var MODE =
13 | process.env.npm_lifecycle_event === "prod" ? "production" : "development";
14 | var withDebug = !process.env["npm_config_nodebug"];
15 | // this may help for Yarn users
16 | // var withDebug = !npmParams.includes("--nodebug");
17 | console.log('\x1b[36m%s\x1b[0m', `** elm-webpack-starter: mode "${MODE}", withDebug: ${withDebug}\n`);
18 |
19 | var common = {
20 | mode: MODE,
21 | entry: "./src/index.js",
22 | output: {
23 | path: path.join(__dirname, "dist"),
24 | publicPath: "/",
25 | // FIXME webpack -p automatically adds hash when building for production
26 | filename: MODE == "production" ? "[name]-[hash].js" : "index.js"
27 | },
28 | plugins: [
29 | new HTMLWebpackPlugin({
30 | // Use this template to get basic responsive meta tags
31 | template: "src/index.html",
32 | // inject details of output file at end of body
33 | inject: "body"
34 | })
35 | ],
36 | resolve: {
37 | modules: [path.join(__dirname, "src"), "node_modules"],
38 | extensions: [".js", ".elm", ".scss", ".png"]
39 | },
40 | module: {
41 | rules: [
42 | {
43 | test: /\.js$/,
44 | exclude: /node_modules/,
45 | use: {
46 | loader: "babel-loader"
47 | }
48 | },
49 | {
50 | test: /\.scss$/,
51 | exclude: [/elm-stuff/, /node_modules/],
52 | // see https://github.com/webpack-contrib/css-loader#url
53 | loaders: ["style-loader", "css-loader?url=false", "sass-loader"]
54 | },
55 | {
56 | test: /\.css$/,
57 | exclude: [/elm-stuff/, /node_modules/],
58 | loaders: ["style-loader", "css-loader?url=false"]
59 | },
60 | {
61 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
62 | exclude: [/elm-stuff/, /node_modules/],
63 | loader: "url-loader",
64 | options: {
65 | limit: 10000,
66 | mimetype: "application/font-woff"
67 | }
68 | },
69 | {
70 | test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
71 | exclude: [/elm-stuff/, /node_modules/],
72 | loader: "file-loader"
73 | },
74 | {
75 | test: /\.(jpe?g|png|gif|svg)$/i,
76 | exclude: [/elm-stuff/, /node_modules/],
77 | loader: "file-loader"
78 | }
79 | ]
80 | }
81 | };
82 |
83 | if (MODE === "development") {
84 | module.exports = merge(common, {
85 | plugins: [
86 | // Suggested for hot-loading
87 | new webpack.NamedModulesPlugin(),
88 | // Prevents compilation errors causing the hot loader to lose state
89 | new webpack.NoEmitOnErrorsPlugin()
90 | ],
91 | module: {
92 | rules: [
93 | {
94 | test: /\.elm$/,
95 | exclude: [/elm-stuff/, /node_modules/],
96 | use: [
97 | { loader: "elm-hot-webpack-loader" },
98 | {
99 | loader: "elm-webpack-loader",
100 | options: {
101 | // add Elm's debug overlay to output
102 | debug: withDebug,
103 | //
104 | forceWatch: true
105 | }
106 | }
107 | ]
108 | }
109 | ]
110 | },
111 | devServer: {
112 | inline: true,
113 | stats: "errors-only",
114 | contentBase: path.join(__dirname, "src/assets"),
115 | historyApiFallback: true,
116 | // feel free to delete this section if you don't need anything like this
117 | before(app) {
118 | // on port 3000
119 | app.get("/test", function(req, res) {
120 | res.json({ result: "OK" });
121 | });
122 | }
123 | }
124 | });
125 | }
126 | if (MODE === "production") {
127 | module.exports = merge(common, {
128 | plugins: [
129 | // Minify elm code
130 | new elmMinify.WebpackPlugin(),
131 | // Delete everything from output-path (/dist) and report to user
132 | new CleanWebpackPlugin({
133 | root: __dirname,
134 | exclude: [],
135 | verbose: true,
136 | dry: false
137 | }),
138 | // Copy static assets
139 | new CopyWebpackPlugin([
140 | {
141 | from: "src/assets"
142 | }
143 | ]),
144 | new MiniCssExtractPlugin({
145 | // Options similar to the same options in webpackOptions.output
146 | // both options are optional
147 | filename: "[name]-[hash].css"
148 | })
149 | ],
150 | module: {
151 | rules: [
152 | {
153 | test: /\.elm$/,
154 | exclude: [/elm-stuff/, /node_modules/],
155 | use: {
156 | loader: "elm-webpack-loader",
157 | options: {
158 | optimize: true
159 | }
160 | }
161 | },
162 | {
163 | test: /\.css$/,
164 | exclude: [/elm-stuff/, /node_modules/],
165 | loaders: [
166 | MiniCssExtractPlugin.loader,
167 | "css-loader?url=false"
168 | ]
169 | },
170 | {
171 | test: /\.scss$/,
172 | exclude: [/elm-stuff/, /node_modules/],
173 | loaders: [
174 | MiniCssExtractPlugin.loader,
175 | "css-loader?url=false",
176 | "sass-loader"
177 | ]
178 | }
179 | ]
180 | }
181 | });
182 | }
183 |
--------------------------------------------------------------------------------
/integration-tests/elm.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "application",
3 | "source-directories": [
4 | "../src",
5 | "src"
6 | ],
7 | "elm-version": "0.19.0",
8 | "dependencies": {
9 | "direct": {
10 | "Chadtech/elm-bool-extra": "2.3.0",
11 | "NoRedInk/elm-json-decode-pipeline": "1.0.0",
12 | "NoRedInk/elm-string-conversions": "1.0.1",
13 | "cmditch/elm-bigint": "1.0.1",
14 | "elm/browser": "1.0.1",
15 | "elm/core": "1.0.2",
16 | "elm/html": "1.0.0",
17 | "elm/http": "2.0.0",
18 | "elm/json": "1.1.3",
19 | "elm/regex": "1.0.0",
20 | "elm/time": "1.0.0",
21 | "elm-community/json-extra": "4.0.0",
22 | "elm-community/list-extra": "8.1.0",
23 | "elm-community/maybe-extra": "5.0.0",
24 | "elm-community/result-extra": "2.2.1",
25 | "elm-community/string-extra": "4.0.0",
26 | "prozacchiwawa/elm-keccak": "2.0.0",
27 | "rtfeldman/elm-hex": "1.0.0",
28 | "zwilias/elm-utf-tools": "2.0.1"
29 | },
30 | "indirect": {
31 | "elm/bytes": "1.0.8",
32 | "elm/file": "1.0.4",
33 | "elm/parser": "1.1.0",
34 | "elm/url": "1.0.0",
35 | "elm/virtual-dom": "1.0.2",
36 | "rtfeldman/elm-iso8601-date-strings": "1.1.2"
37 | }
38 | },
39 | "test-dependencies": {
40 | "direct": {},
41 | "indirect": {}
42 | }
43 | }
--------------------------------------------------------------------------------
/integration-tests/src/Main.elm:
--------------------------------------------------------------------------------
1 | module Main exposing (main)
2 |
3 | -- Internal
4 |
5 | import Abi.Encode
6 | import BigInt
7 | import Browser
8 | import Eth
9 | import Eth.Decode as Decode
10 | import Eth.Sentry.Event as EventSentry exposing (EventSentry)
11 | import Eth.Types exposing (..)
12 | import Eth.Units exposing (eth, gwei)
13 | import Eth.Utils as Utils exposing (functionSig, unsafeToHex)
14 | import Html exposing (Html, div, text)
15 | import Http
16 | import Process
17 | import String.Conversions
18 | import Task
19 |
20 |
21 |
22 | -- Program
23 |
24 |
25 | main : Program () Model Msg
26 | main =
27 | Browser.element
28 | { init = \_ -> init
29 | , view = view
30 | , update = update
31 | , subscriptions = \_ -> Sub.none
32 | }
33 |
34 |
35 | ethNode =
36 | "https://mainnet.infura.io/v3/f04200fc1dd4419aa93210e3f799adbf"
37 |
38 |
39 |
40 | -- Model
41 |
42 |
43 | type alias Model =
44 | { responses : List String
45 | , pendingTxHashes : List TxHash
46 | , eventSentry : EventSentry Msg
47 | }
48 |
49 |
50 | init : ( Model, Cmd Msg )
51 | init =
52 | let
53 | ( esModel, esCmds ) =
54 | EventSentry.init EventSentryMsg ethNode
55 | in
56 | ( { responses = []
57 | , pendingTxHashes = []
58 | , eventSentry = esModel
59 | }
60 | , Cmd.batch [ Task.perform (\_ -> InitTest) (Task.succeed ()), esCmds ]
61 | )
62 |
63 |
64 |
65 | -- View
66 |
67 |
68 | view : Model -> Html Msg
69 | view model =
70 | let
71 | br =
72 | Html.br [] []
73 |
74 | header =
75 | [ String.fromInt (List.length model.responses)
76 | ++ "/8 tests complete (Continous watching will keep adding \"successes\")."
77 | , "EventSentry tests are watching for DAI ERC20 transfer events."
78 | , "Give it a minute to pick one up."
79 | , ""
80 | ]
81 | |> List.map text
82 | |> List.intersperse br
83 |
84 | testData =
85 | List.intersperse "" model.responses
86 | |> List.map text
87 | |> List.intersperse br
88 | in
89 | div [] (header ++ [ br, br ] ++ testData)
90 |
91 |
92 |
93 | -- Update
94 |
95 |
96 | type Msg
97 | = InitTest
98 | | WatchLatest
99 | | WatchRanged
100 | | WatchOnceRangeToLatest
101 | | NewResponse String
102 | | EventSentryMsg EventSentry.Msg
103 |
104 |
105 | update : Msg -> Model -> ( Model, Cmd Msg )
106 | update msg model =
107 | case msg of
108 | InitTest ->
109 | ( model
110 | , Cmd.batch
111 | [ logCmd
112 | , addressCmd
113 | , transactionCmd
114 | , blockCmd
115 | , contractCmds
116 | , watchLatest
117 | , watchRanged
118 | , watchOnceRangeToLatest
119 | ]
120 | )
121 |
122 | WatchLatest ->
123 | let
124 | ( subModel, subCmd, _ ) =
125 | EventSentry.watch watchLatestHelper
126 | model.eventSentry
127 | filterLatest
128 | in
129 | ( { model | eventSentry = subModel }, subCmd )
130 |
131 | WatchRanged ->
132 | let
133 | ( subModel, subCmd, _ ) =
134 | EventSentry.watch watchRangedHelper
135 | model.eventSentry
136 | filterRanged
137 | in
138 | ( { model | eventSentry = subModel }, subCmd )
139 |
140 | WatchOnceRangeToLatest ->
141 | let
142 | ( subModel, subCmd ) =
143 | EventSentry.watchOnce watchOnceRangeToLatestHelper
144 | model.eventSentry
145 | filterRangeToLatest
146 | in
147 | ( { model | eventSentry = subModel }, subCmd )
148 |
149 | NewResponse response ->
150 | ( { model | responses = response :: model.responses }, Cmd.none )
151 |
152 | EventSentryMsg subMsg ->
153 | let
154 | ( subModel, subCmd ) =
155 | EventSentry.update subMsg model.eventSentry
156 | in
157 | ( { model | eventSentry = subModel }, subCmd )
158 |
159 |
160 |
161 | -- Test Cmds
162 |
163 |
164 | logCmd : Cmd Msg
165 | logCmd =
166 | Eth.getLogs ethNode erc20TransferFilter
167 | |> Task.attempt
168 | (responseToString
169 | (List.map logToString >> String.join ", " >> (++) "Log Cmds: ")
170 | >> NewResponse
171 | )
172 |
173 |
174 | addressCmd : Cmd Msg
175 | addressCmd =
176 | Eth.getBalance ethNode wrappedEthContract
177 | |> Task.andThen (\_ -> Eth.getTxCount ethNode wrappedEthContract)
178 | |> Task.andThen (\_ -> Eth.getTxCountAtBlock ethNode wrappedEthContract (BlockNum 4620856))
179 | |> Task.andThen (\_ -> Process.sleep 700)
180 | |> Task.andThen (\_ -> Eth.getBalanceAtBlock ethNode wrappedEthContract (BlockNum 5744072))
181 | |> Task.map BigInt.toString
182 | |> Task.attempt
183 | (responseToString
184 | ((++) "Address Cmds: ")
185 | >> NewResponse
186 | )
187 |
188 |
189 | transactionCmd : Cmd Msg
190 | transactionCmd =
191 | Eth.getTx ethNode txHash
192 | |> Task.andThen (.hash >> Eth.getTxReceipt ethNode)
193 | |> Task.andThen
194 | (\txReceipt ->
195 | Eth.getTxByBlockHashAndIndex ethNode txReceipt.blockHash 0
196 | |> Task.andThen (\_ -> Eth.getTxByBlockNumberAndIndex ethNode txReceipt.blockNumber 0)
197 | )
198 | |> Task.attempt
199 | (responseToString
200 | (.hash >> Utils.txHashToString >> (++) "Tx Cmds: ")
201 | >> NewResponse
202 | )
203 |
204 |
205 | blockCmd : Cmd Msg
206 | blockCmd =
207 | Eth.getBlockNumber ethNode
208 | |> Task.andThen (Eth.getBlock ethNode)
209 | |> Task.andThen (\block -> Eth.getBlockByHash ethNode block.hash)
210 | |> Task.andThen (\block -> Eth.getBlockWithTxObjs ethNode 5487588)
211 | |> Task.andThen (\block -> Eth.getBlockByHashWithTxObjs ethNode block.hash)
212 | |> Task.andThen
213 | (\block ->
214 | Eth.getBlockTxCount ethNode block.number
215 | |> Task.andThen (\_ -> Process.sleep 500)
216 | |> Task.andThen (\_ -> Eth.getBlockTxCountByHash ethNode block.hash)
217 | |> Task.andThen (\_ -> Eth.getUncleCount ethNode block.number)
218 | |> Task.andThen (\_ -> Eth.getUncleCountByHash ethNode block.hash)
219 | |> Task.andThen (\_ -> Eth.getUncleAtIndex ethNode block.number 0)
220 | |> Task.andThen (\_ -> Process.sleep 500)
221 | |> Task.andThen (\_ -> Eth.getUncleByBlockHashAtIndex ethNode block.hash 0)
222 | )
223 | |> Task.attempt
224 | (responseToString
225 | (.hash >> Utils.blockHashToString >> (++) "Block Cmds: ")
226 | >> NewResponse
227 | )
228 |
229 |
230 | contractCmds : Cmd Msg
231 | contractCmds =
232 | let
233 | call =
234 | { to = Just erc20Contract
235 | , from = Nothing
236 | , gas = Just <| 400000
237 | , gasPrice = Just <| gwei 20
238 | , value = Nothing
239 | , data = Just <| Abi.Encode.functionCall "owner()" []
240 | , nonce = Nothing
241 | , decoder = Decode.address
242 | }
243 | in
244 | Eth.callAtBlock ethNode call (BlockNum 4620856)
245 | |> Task.andThen (\_ -> Eth.call ethNode call)
246 | |> Task.andThen (\_ -> Eth.estimateGas ethNode call)
247 | |> Task.andThen (\_ -> Eth.getStorageAt ethNode erc20Contract 0)
248 | |> Task.andThen (\_ -> Eth.getStorageAtBlock ethNode erc20Contract 0 (BlockNum 4620856))
249 | |> Task.andThen (\_ -> Process.sleep 500)
250 | |> Task.andThen (\_ -> Eth.getCode ethNode erc20Contract)
251 | |> Task.andThen (\_ -> Eth.getCodeAtBlock ethNode erc20Contract (BlockNum 4620856))
252 | |> Task.attempt
253 | (responseToString
254 | ((++) "Contract Cmds: \n\t")
255 | >> NewResponse
256 | )
257 |
258 |
259 |
260 | -- Data
261 |
262 |
263 | erc20TransferFilter : LogFilter
264 | erc20TransferFilter =
265 | { fromBlock = BlockNum 5488303
266 | , toBlock = BlockNum 5488353
267 | , address = Utils.unsafeToAddress "0xd850942ef8811f2a866692a623011bde52a462c1"
268 | , topics = [ Just <| Utils.unsafeToHex "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" ]
269 | }
270 |
271 |
272 | erc20Contract : Address
273 | erc20Contract =
274 | Utils.unsafeToAddress "0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2"
275 |
276 |
277 | wrappedEthContract : Address
278 | wrappedEthContract =
279 | Utils.unsafeToAddress "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
280 |
281 |
282 | txHash : TxHash
283 | txHash =
284 | Utils.unsafeToTxHash "0x5c9b0f9c6c32d2690771169ec62dd648fef7bce3d45fe8a6505d99fdcbade27a"
285 |
286 |
287 | blockHash : BlockHash
288 | blockHash =
289 | Utils.unsafeToBlockHash "0x4f4b2cedbf641cf7213ea9612ed549ed39732ce3eb640500ca813af41ab16cd1"
290 |
291 |
292 | logToString : Log -> String
293 | logToString log =
294 | Utils.txHashToString log.transactionHash
295 |
296 |
297 | responseToString : (a -> String) -> Result Http.Error a -> String
298 | responseToString okToString result =
299 | case result of
300 | Ok res ->
301 | okToString res
302 |
303 | Err err ->
304 | String.Conversions.fromHttpError err
305 |
306 |
307 |
308 | -- EventSentry Helpers
309 | -- ( Using DAI transfer event )
310 |
311 |
312 | watchLatest : Cmd Msg
313 | watchLatest =
314 | Task.perform (\_ -> WatchLatest) (Task.succeed ())
315 |
316 |
317 | watchLatestHelper =
318 | logToString >> (++) "WatchLatest Cmd: " >> NewResponse
319 |
320 |
321 | filterLatest : LogFilter
322 | filterLatest =
323 | { fromBlock = LatestBlock
324 | , toBlock = LatestBlock
325 | , address = Utils.unsafeToAddress "0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359"
326 | , topics = [ Just <| Utils.unsafeToHex "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" ]
327 | }
328 |
329 |
330 |
331 | --
332 |
333 |
334 | watchRanged : Cmd Msg
335 | watchRanged =
336 | Task.perform (\_ -> WatchRanged) (Task.succeed ())
337 |
338 |
339 | watchRangedHelper =
340 | logToString >> (++) "WatchRanged Cmd: " >> NewResponse
341 |
342 |
343 | filterRanged : LogFilter
344 | filterRanged =
345 | { filterLatest
346 | | fromBlock = BlockNum 7396400
347 | , toBlock = BlockNum 7396404
348 | }
349 |
350 |
351 |
352 | --
353 |
354 |
355 | watchOnceRangeToLatest : Cmd Msg
356 | watchOnceRangeToLatest =
357 | Task.perform (\_ -> WatchOnceRangeToLatest) (Task.succeed ())
358 |
359 |
360 | watchOnceRangeToLatestHelper =
361 | logToString >> (++) "WatchOnceRangeToLatest Cmd: " >> NewResponse
362 |
363 |
364 | filterRangeToLatest : LogFilter
365 | filterRangeToLatest =
366 | { filterLatest
367 | | fromBlock = BlockNum 7396400
368 | , toBlock = LatestBlock
369 | }
370 |
--------------------------------------------------------------------------------
/src/Eth/Abi/Int.elm:
--------------------------------------------------------------------------------
1 | module Eth.Abi.Int exposing (fromBinaryUnsafe, fromString, isNegIntUnsafe, toBinaryUnsafe, toString, twosComplementUnsafe)
2 |
3 | import BigInt exposing (BigInt)
4 | import Eth.Utils exposing (add0x, remove0x)
5 | import String.Extra as StringExtra
6 |
7 |
8 | fromString : String -> Maybe BigInt
9 | fromString str =
10 | let
11 | no0x =
12 | remove0x str
13 | in
14 | if isNegIntUnsafe no0x then
15 | no0x
16 | |> String.toList
17 | |> List.map toBinaryUnsafe
18 | |> String.join ""
19 | |> twosComplementUnsafe
20 | |> StringExtra.break 4
21 | |> List.map fromBinaryUnsafe
22 | |> String.fromList
23 | |> add0x
24 | |> String.cons '-'
25 | |> BigInt.fromHexString
26 |
27 | else
28 | BigInt.fromHexString (add0x str)
29 |
30 |
31 | toString : BigInt -> String
32 | toString num =
33 | let
34 | ( xs_, twosComplementOrNotTwosComplement ) =
35 | case BigInt.toHexString num |> String.toList of
36 | '-' :: xs ->
37 | ( xs, twosComplementUnsafe >> String.padLeft 256 '1' )
38 |
39 | xs ->
40 | ( xs, String.padLeft 256 '0' )
41 | in
42 | List.map toBinaryUnsafe xs_
43 | |> String.join ""
44 | |> twosComplementOrNotTwosComplement
45 | |> StringExtra.break 4
46 | |> List.map fromBinaryUnsafe
47 | |> String.fromList
48 |
49 |
50 | {-| Bit-Flip-Fold-Holla-for-a-Dolla
51 |
52 | The string is folded from the right.
53 | When the first '1' is encountered, all remaining bits are flipped
54 |
55 | e.g.
56 | Input: "1000100"
57 | Output: "0111100"
58 |
59 | -}
60 | twosComplementUnsafe : String -> String
61 | twosComplementUnsafe str =
62 | let
63 | reducer char ( accum, isFlipping ) =
64 | case ( char, isFlipping ) of
65 | ( '0', False ) ->
66 | ( String.cons '0' accum, False )
67 |
68 | ( '0', True ) ->
69 | ( String.cons '1' accum, True )
70 |
71 | -- Flip to True when encountering first '1'
72 | ( '1', False ) ->
73 | ( String.cons '1' accum, True )
74 |
75 | ( '1', True ) ->
76 | ( String.cons '0' accum, True )
77 |
78 | -- This is the unsafe part. Assumes every char is '1' or '0'
79 | _ ->
80 | ( accum, True )
81 | in
82 | String.foldr reducer ( "", False ) str
83 | |> Tuple.first
84 |
85 |
86 | toBinaryUnsafe : Char -> String
87 | toBinaryUnsafe char =
88 | case char of
89 | '0' ->
90 | "0000"
91 |
92 | '1' ->
93 | "0001"
94 |
95 | '2' ->
96 | "0010"
97 |
98 | '3' ->
99 | "0011"
100 |
101 | '4' ->
102 | "0100"
103 |
104 | '5' ->
105 | "0101"
106 |
107 | '6' ->
108 | "0110"
109 |
110 | '7' ->
111 | "0111"
112 |
113 | '8' ->
114 | "1000"
115 |
116 | '9' ->
117 | "1001"
118 |
119 | 'a' ->
120 | "1010"
121 |
122 | 'b' ->
123 | "1011"
124 |
125 | 'c' ->
126 | "1100"
127 |
128 | 'd' ->
129 | "1101"
130 |
131 | 'e' ->
132 | "1110"
133 |
134 | 'f' ->
135 | "1111"
136 |
137 | _ ->
138 | "error converting hex to binary"
139 |
140 |
141 | fromBinaryUnsafe : String -> Char
142 | fromBinaryUnsafe str =
143 | case str of
144 | "0000" ->
145 | '0'
146 |
147 | "0001" ->
148 | '1'
149 |
150 | "0010" ->
151 | '2'
152 |
153 | "0011" ->
154 | '3'
155 |
156 | "0100" ->
157 | '4'
158 |
159 | "0101" ->
160 | '5'
161 |
162 | "0110" ->
163 | '6'
164 |
165 | "0111" ->
166 | '7'
167 |
168 | "1000" ->
169 | '8'
170 |
171 | "1001" ->
172 | '9'
173 |
174 | "1010" ->
175 | 'a'
176 |
177 | "1011" ->
178 | 'b'
179 |
180 | "1100" ->
181 | 'c'
182 |
183 | "1101" ->
184 | 'd'
185 |
186 | "1110" ->
187 | 'e'
188 |
189 | "1111" ->
190 | 'f'
191 |
192 | _ ->
193 | '!'
194 |
195 |
196 | isNegIntUnsafe : String -> Bool
197 | isNegIntUnsafe str =
198 | case String.left 1 str of
199 | "0" ->
200 | False
201 |
202 | "1" ->
203 | False
204 |
205 | "2" ->
206 | False
207 |
208 | "3" ->
209 | False
210 |
211 | "4" ->
212 | False
213 |
214 | "5" ->
215 | False
216 |
217 | "6" ->
218 | False
219 |
220 | "7" ->
221 | False
222 |
223 | "8" ->
224 | True
225 |
226 | "9" ->
227 | True
228 |
229 | "a" ->
230 | True
231 |
232 | "b" ->
233 | True
234 |
235 | "c" ->
236 | True
237 |
238 | "d" ->
239 | True
240 |
241 | "e" ->
242 | True
243 |
244 | "f" ->
245 | True
246 |
247 | _ ->
248 | False
249 |
--------------------------------------------------------------------------------
/src/Eth/Decode.elm:
--------------------------------------------------------------------------------
1 | module Eth.Decode exposing (address, bigInt, block, blockHash, blockHead, hex, hexBool, hexInt, hexTime, log, event, nonZero, resultToDecoder, stringInt, syncStatus, tx, txHash, txReceipt, uncle)
2 |
3 | {-|
4 |
5 | @docs address, bigInt, block, blockHash, blockHead, hex, hexBool, hexInt, hexTime, log, event, nonZero, resultToDecoder, stringInt, syncStatus, tx, txHash, txReceipt, uncle
6 |
7 | -}
8 |
9 | import BigInt exposing (BigInt)
10 | import Eth.Encode
11 | import Eth.Types exposing (..)
12 | import Eth.Utils exposing (add0x, remove0x, toAddress, toBlockHash, toHex, toTxHash)
13 | import Hex
14 | import Json.Decode as Decode exposing (..)
15 | import Json.Decode.Pipeline exposing (custom, optional, required)
16 | import Json.Encode as Encode
17 | import Time exposing (Posix)
18 |
19 |
20 | {-| -}
21 | block : Decoder a -> Decoder (Block a)
22 | block txsDecoder =
23 | succeed Block
24 | |> required "number" hexInt
25 | |> required "hash" blockHash
26 | |> required "parentHash" blockHash
27 | |> optional "nonce" string "not provided by node"
28 | |> required "sha3Uncles" string
29 | |> required "logsBloom" string
30 | |> required "transactionsRoot" string
31 | |> required "stateRoot" string
32 | |> required "receiptsRoot" string
33 | |> required "miner" address
34 | |> required "difficulty" bigInt
35 | |> optional "totalDifficulty" bigInt (BigInt.fromInt 0)
36 | -- Noticed nodes will occasionally return null values in block responses. Have only tested this on Infura metamask-mainnet endpoint
37 | |> required "extraData" string
38 | |> required "size" hexInt
39 | |> required "gasLimit" hexInt
40 | |> required "gasUsed" hexInt
41 | |> optional "timestamp" hexTime (Time.millisToPosix 0)
42 | -- See comment above
43 | |> optional "transactions" (list txsDecoder) []
44 | |> optional "uncles" (list string) []
45 |
46 |
47 | {-| -}
48 | uncle : Decoder (Block ())
49 | uncle =
50 | block (succeed ())
51 |
52 |
53 | {-| -}
54 | blockHead : Decoder BlockHead
55 | blockHead =
56 | succeed BlockHead
57 | |> required "number" hexInt
58 | |> required "hash" blockHash
59 | |> required "parentHash" blockHash
60 | |> optional "nonce" string "not provided by node"
61 | |> required "sha3Uncles" string
62 | |> required "logsBloom" string
63 | |> required "transactionsRoot" string
64 | |> required "stateRoot" string
65 | |> required "receiptsRoot" string
66 | |> required "miner" address
67 | |> required "difficulty" bigInt
68 | |> required "extraData" string
69 | |> required "gasLimit" hexInt
70 | |> required "gasUsed" hexInt
71 | |> required "mixHash" string
72 | |> required "timestamp" hexTime
73 |
74 |
75 | {-| -}
76 | tx : Decoder Tx
77 | tx =
78 | succeed Tx
79 | |> required "hash" txHash
80 | |> required "nonce" hexInt
81 | |> required "blockHash" (nonZero blockHash)
82 | |> required "blockNumber" (nullable hexInt)
83 | |> required "transactionIndex" hexInt
84 | |> required "from" address
85 | |> required "to" (nullable address)
86 | |> required "value" bigInt
87 | |> required "gasPrice" bigInt
88 | |> required "gas" hexInt
89 | |> required "input" string
90 |
91 |
92 | {-| -}
93 | txReceipt : Decoder TxReceipt
94 | txReceipt =
95 | succeed TxReceipt
96 | |> required "transactionHash" txHash
97 | |> required "transactionIndex" hexInt
98 | |> required "blockHash" blockHash
99 | |> required "blockNumber" hexInt
100 | |> required "gasUsed" bigInt
101 | |> required "cumulativeGasUsed" bigInt
102 | |> custom (maybe (field "contractAddress" address))
103 | |> required "logs" (list log)
104 | |> required "logsBloom" string
105 | |> custom (maybe (field "root" string))
106 | |> custom (maybe (field "status" hexBool))
107 |
108 |
109 | {-| -}
110 | log : Decoder Log
111 | log =
112 | succeed Log
113 | |> required "address" address
114 | |> required "data" string
115 | |> required "topics" (list hex)
116 | |> optional "removed" bool False
117 | |> required "logIndex" hexInt
118 | |> required "transactionIndex" hexInt
119 | |> required "transactionHash" txHash
120 | |> required "blockHash" blockHash
121 | |> required "blockNumber" hexInt
122 |
123 |
124 | {-| -}
125 | event : Decoder a -> Log -> Event (Result Error a)
126 | event decoder log_ =
127 | { address = log_.address
128 | , data = log_.data
129 | , topics = log_.topics
130 | , removed = log_.removed
131 | , logIndex = log_.logIndex
132 | , transactionIndex = log_.transactionIndex
133 | , transactionHash = log_.transactionHash
134 | , blockHash = log_.blockHash
135 | , blockNumber = log_.blockNumber
136 | , returnData =
137 | Encode.object
138 | [ ( "data", Encode.string log_.data )
139 | , ( "topics", Encode.list Eth.Encode.hex log_.topics )
140 | ]
141 | |> Decode.decodeValue decoder
142 | }
143 |
144 |
145 | {-| -}
146 | syncStatus : Decoder (Maybe SyncStatus)
147 | syncStatus =
148 | succeed SyncStatus
149 | |> required "startingBlock" int
150 | |> required "currentBlock" int
151 | |> required "highestBlock" int
152 | |> required "knownStates" int
153 | |> required "pulledStates" int
154 | |> maybe
155 |
156 |
157 |
158 | -- Primitives
159 |
160 |
161 | {-| -}
162 | address : Decoder Address
163 | address =
164 | resultToDecoder toAddress
165 |
166 |
167 | {-| -}
168 | txHash : Decoder TxHash
169 | txHash =
170 | resultToDecoder toTxHash
171 |
172 |
173 | {-| -}
174 | blockHash : Decoder BlockHash
175 | blockHash =
176 | resultToDecoder toBlockHash
177 |
178 |
179 | {-| -}
180 | hex : Decoder Hex
181 | hex =
182 | resultToDecoder toHex
183 |
184 |
185 | {-| -}
186 | stringInt : Decoder Int
187 | stringInt =
188 | (String.toInt >> Result.fromMaybe "Failure decoding stringy int")
189 | |> resultToDecoder
190 |
191 |
192 | {-| -}
193 | hexInt : Decoder Int
194 | hexInt =
195 | resultToDecoder (remove0x >> Hex.fromString)
196 |
197 |
198 | {-| -}
199 | bigInt : Decoder BigInt
200 | bigInt =
201 | resultToDecoder (add0x >> BigInt.fromHexString >> Result.fromMaybe "Error decoding hex to BigInt")
202 |
203 |
204 | {-| -}
205 | hexTime : Decoder Posix
206 | hexTime =
207 | resultToDecoder (remove0x >> Hex.fromString >> Result.map (\v -> v * 1000 |> Time.millisToPosix))
208 |
209 |
210 | {-| -}
211 | hexBool : Decoder Bool
212 | hexBool =
213 | let
214 | isBool n =
215 | case n of
216 | "0x0" ->
217 | Ok False
218 |
219 | "0x1" ->
220 | Ok True
221 |
222 | _ ->
223 | Err <| "Error decoding " ++ n ++ "as bool."
224 | in
225 | resultToDecoder isBool
226 |
227 |
228 |
229 | -- Utils
230 |
231 |
232 | {-| -}
233 | resultToDecoder : (String -> Result String a) -> Decoder a
234 | resultToDecoder strToResult =
235 | let
236 | convert n =
237 | case strToResult n of
238 | Ok val ->
239 | Decode.succeed val
240 |
241 | Err error ->
242 | Decode.fail error
243 | in
244 | Decode.string |> Decode.andThen convert
245 |
246 |
247 | {-| -}
248 | nonZero : Decoder a -> Decoder (Maybe a)
249 | nonZero decoder =
250 | let
251 | checkZero str =
252 | if str == "0x" || str == "0x0" then
253 | Decode.succeed Nothing
254 |
255 | else if remove0x str |> String.all (\s -> s == '0') then
256 | Decode.succeed Nothing
257 |
258 | else
259 | Decode.map Just decoder
260 | in
261 | Decode.string |> Decode.andThen checkZero
262 |
--------------------------------------------------------------------------------
/src/Eth/Defaults.elm:
--------------------------------------------------------------------------------
1 | module Eth.Defaults exposing (invalidAddress, zeroAddress, emptyBlockHash, emptyTxHash, emptyLogFilter)
2 |
3 | {-| Default values.
4 | For those withDefault shenanigans.
5 |
6 | @docs invalidAddress, zeroAddress, emptyBlockHash, emptyTxHash, emptyLogFilter
7 |
8 | -}
9 |
10 | import Eth.Types exposing (..)
11 | import Internal.Types as Internal
12 |
13 |
14 | {-| -}
15 | invalidAddress : Address
16 | invalidAddress =
17 | Internal.Address "invalid address to break things"
18 |
19 |
20 | {-| Danger Will Robinson, why are you using this?
21 | Only to burn things should it be used.
22 | -}
23 | zeroAddress : Address
24 | zeroAddress =
25 | Internal.Address "0000000000000000000000000000000000000000"
26 |
27 |
28 | {-| -}
29 | emptyBlockHash : BlockHash
30 | emptyBlockHash =
31 | Internal.BlockHash "0000000000000000000000000000000000000000000000000000000000000000"
32 |
33 |
34 | {-| -}
35 | emptyTxHash : TxHash
36 | emptyTxHash =
37 | Internal.TxHash "0000000000000000000000000000000000000000000000000000000000000000"
38 |
39 |
40 | {-| -}
41 | emptyLogFilter : LogFilter
42 | emptyLogFilter =
43 | { fromBlock = LatestBlock
44 | , toBlock = LatestBlock
45 | , address = zeroAddress
46 | , topics = []
47 | }
48 |
--------------------------------------------------------------------------------
/src/Eth/Encode.elm:
--------------------------------------------------------------------------------
1 | module Eth.Encode exposing (address, bigInt, blockHash, blockId, hex, hexInt, listOfMaybesToVal, logFilter, topicsList, txCall, txHash)
2 |
3 | {-|
4 |
5 | @docs address, bigInt, blockHash, blockId, hex, hexInt, listOfMaybesToVal, logFilter, topicsList, txCall, txHash
6 |
7 | -}
8 |
9 | import BigInt exposing (BigInt)
10 | import Eth.Types exposing (..)
11 | import Eth.Utils exposing (..)
12 | import Hex
13 | import Json.Encode as Encode exposing (Value, int, list, null, object, string)
14 |
15 |
16 |
17 | -- Simple
18 |
19 |
20 | {-| -}
21 | address : Address -> Value
22 | address =
23 | addressToString >> string
24 |
25 |
26 | {-| -}
27 | txHash : TxHash -> Value
28 | txHash =
29 | txHashToString >> string
30 |
31 |
32 | {-| -}
33 | blockHash : BlockHash -> Value
34 | blockHash =
35 | blockHashToString >> string
36 |
37 |
38 |
39 | -- Complex
40 |
41 |
42 | {-| -}
43 | listOfMaybesToVal : List ( String, Maybe Value ) -> Value
44 | listOfMaybesToVal keyValueList =
45 | keyValueList
46 | |> List.filter (\( k, v ) -> v /= Nothing)
47 | |> List.map (\( k, v ) -> ( k, Maybe.withDefault Encode.null v ))
48 | |> Encode.object
49 |
50 |
51 |
52 | -- {-| -}
53 | -- txCall : Call a -> Value
54 | -- txCall { to, from, gas, gasPrice, value, data } =
55 | -- let
56 | -- toVal callData =
57 | -- listOfMaybesToVal
58 | -- [ ( "to", Maybe.map address to )
59 | -- , ( "from", Maybe.map address from )
60 | -- , ( "gas", Maybe.map hexInt gas )
61 | -- , ( "gasPrice", Maybe.map bigInt gasPrice )
62 | -- , ( "value", Maybe.map bigInt value )
63 | -- , ( "data", Maybe.map hex callData )
64 | -- ]
65 | -- in
66 | -- case data of
67 | -- Nothing ->
68 | -- Ok <| toVal Nothing
69 | -- Just (Ok data_) ->
70 | -- Ok <| toVal (Just data_)
71 | -- Just (Err err) ->
72 | -- Err err
73 |
74 |
75 | {-| -}
76 | txCall : Call a -> Value
77 | txCall { to, from, gas, gasPrice, value, data } =
78 | listOfMaybesToVal
79 | [ ( "to", Maybe.map address to )
80 | , ( "from", Maybe.map address from )
81 | , ( "gas", Maybe.map hexInt gas )
82 | , ( "gasPrice", Maybe.map bigInt gasPrice )
83 | , ( "value", Maybe.map bigInt value )
84 | , ( "data", Maybe.map hex data )
85 | ]
86 |
87 |
88 | {-| -}
89 | blockId : BlockId -> Value
90 | blockId blockId_ =
91 | case blockId_ of
92 | BlockNum num ->
93 | Hex.toString num
94 | |> add0x
95 | |> string
96 |
97 | EarliestBlock ->
98 | string "earliest"
99 |
100 | LatestBlock ->
101 | string "latest"
102 |
103 | PendingBlock ->
104 | string "pending"
105 |
106 |
107 | {-| -}
108 | logFilter : LogFilter -> Value
109 | logFilter lf =
110 | object
111 | [ ( "fromBlock", blockId lf.fromBlock )
112 | , ( "toBlock", blockId lf.toBlock )
113 | , ( "address", address lf.address )
114 | , ( "topics", topicsList lf.topics )
115 | ]
116 |
117 |
118 | {-| -}
119 | topicsList : List (Maybe Hex) -> Value
120 | topicsList topics =
121 | let
122 | toVal val =
123 | case val of
124 | Just hexVal ->
125 | string (hexToString hexVal)
126 |
127 | Nothing ->
128 | null
129 | in
130 | list toVal topics
131 |
132 |
133 |
134 | -- Rudiments
135 |
136 |
137 | {-| -}
138 | bigInt : BigInt -> Value
139 | bigInt =
140 | BigInt.toHexString >> add0x >> Encode.string
141 |
142 |
143 | {-| -}
144 | hex : Hex -> Value
145 | hex =
146 | hexToString >> Encode.string
147 |
148 |
149 | {-| -}
150 | hexInt : Int -> Value
151 | hexInt =
152 | Hex.toString >> add0x >> Encode.string
153 |
--------------------------------------------------------------------------------
/src/Eth/Net.elm:
--------------------------------------------------------------------------------
1 | module Eth.Net exposing (NetworkId(..), version, clientVersion, listening, peerCount, toNetworkId, networkIdToInt, networkIdToString, networkIdDecoder)
2 |
3 | {-| NetworkId and RPC Methods
4 |
5 | @docs NetworkId, version, clientVersion, listening, peerCount, toNetworkId, networkIdToInt, networkIdToString, networkIdDecoder
6 |
7 | -}
8 |
9 | import Eth.Decode as Decode
10 | import Eth.RPC as RPC
11 | import Eth.Types exposing (HttpProvider)
12 | import Http
13 | import Json.Decode as Decode exposing (Decoder)
14 | import Task exposing (Task)
15 |
16 |
17 | {-| -}
18 | type NetworkId
19 | = Mainnet
20 | | Expanse
21 | | Ropsten
22 | | Rinkeby
23 | | RskMain
24 | | RskTest
25 | | Kovan
26 | | ETCMain
27 | | ETCTest
28 | | Private Int
29 |
30 |
31 | {-| Get the current network id.
32 |
33 | Ok Mainnet
34 |
35 | -}
36 | version : HttpProvider -> Task Http.Error NetworkId
37 | version ethNode =
38 | RPC.toTask
39 | { url = ethNode
40 | , method = "net_version"
41 | , params = []
42 | , decoder = networkIdDecoder
43 | }
44 |
45 |
46 | {-| Get the current client version.
47 |
48 | Ok "Mist/v0.9.3/darwin/go1.4.1"
49 |
50 | -}
51 | clientVersion : HttpProvider -> Task Http.Error String
52 | clientVersion ethNode =
53 | RPC.toTask
54 | { url = ethNode
55 | , method = "web3_clientVersion"
56 | , params = []
57 | , decoder = Decode.string
58 | }
59 |
60 |
61 | {-| Returns true if the node is actively listening for network connections.
62 | -}
63 | listening : HttpProvider -> Task Http.Error Bool
64 | listening ethNode =
65 | RPC.toTask
66 | { url = ethNode
67 | , method = "net_listening"
68 | , params = []
69 | , decoder = Decode.bool
70 | }
71 |
72 |
73 | {-| Get the number of peers currently connected to the client.
74 | -}
75 | peerCount : HttpProvider -> Task Http.Error Int
76 | peerCount ethNode =
77 | RPC.toTask
78 | { url = ethNode
79 | , method = "net_peerCount"
80 | , params = []
81 | , decoder = Decode.stringInt
82 | }
83 |
84 |
85 | {-| Decode a JSON stringy int or JSON int to a NetworkId
86 |
87 | decodeString networkIdDecoder "1" == Ok Mainnet
88 | decodeString networkIdDecoder 3 == Ok Ropsten
89 | decodeString networkIdDecoder "five" == Err ...
90 |
91 | -}
92 | networkIdDecoder : Decoder NetworkId
93 | networkIdDecoder =
94 | Decode.oneOf
95 | [ stringyIdDecoder
96 | , intyIdDecoder
97 | ]
98 |
99 |
100 | stringyIdDecoder : Decoder NetworkId
101 | stringyIdDecoder =
102 | (String.toInt >> Result.fromMaybe "Failure decoding stringy int" >> Result.map toNetworkId)
103 | |> Decode.resultToDecoder
104 |
105 |
106 | intyIdDecoder : Decoder NetworkId
107 | intyIdDecoder =
108 | Decode.int |> Decode.map toNetworkId
109 |
110 |
111 | {-| Convert an int into it's NetworkId
112 | -}
113 | toNetworkId : Int -> NetworkId
114 | toNetworkId idInt =
115 | case idInt of
116 | 1 ->
117 | Mainnet
118 |
119 | 2 ->
120 | Expanse
121 |
122 | 3 ->
123 | Ropsten
124 |
125 | 4 ->
126 | Rinkeby
127 |
128 | 30 ->
129 | RskMain
130 |
131 | 31 ->
132 | RskTest
133 |
134 | 42 ->
135 | Kovan
136 |
137 | 41 ->
138 | ETCMain
139 |
140 | 62 ->
141 | ETCTest
142 |
143 | _ ->
144 | Private idInt
145 |
146 |
147 | {-| Convert an int into it's NetworkId
148 | -}
149 | networkIdToInt : NetworkId -> Int
150 | networkIdToInt networkId =
151 | case networkId of
152 | Mainnet ->
153 | 1
154 |
155 | Expanse ->
156 | 2
157 |
158 | Ropsten ->
159 | 3
160 |
161 | Rinkeby ->
162 | 4
163 |
164 | RskMain ->
165 | 30
166 |
167 | RskTest ->
168 | 31
169 |
170 | Kovan ->
171 | 42
172 |
173 | ETCMain ->
174 | 41
175 |
176 | ETCTest ->
177 | 62
178 |
179 | Private id ->
180 | id
181 |
182 |
183 | {-| Get a NetworkId's name
184 | -}
185 | networkIdToString : NetworkId -> String
186 | networkIdToString networkId =
187 | case networkId of
188 | Mainnet ->
189 | "Mainnet"
190 |
191 | Expanse ->
192 | "Expanse"
193 |
194 | Ropsten ->
195 | "Ropsten"
196 |
197 | Rinkeby ->
198 | "Rinkeby"
199 |
200 | RskMain ->
201 | "Rootstock"
202 |
203 | RskTest ->
204 | "Rootstock Test"
205 |
206 | Kovan ->
207 | "Kovan"
208 |
209 | ETCMain ->
210 | "ETC Mainnet"
211 |
212 | ETCTest ->
213 | "ETC Testnet"
214 |
215 | Private num ->
216 | "Private Chain: " ++ String.fromInt num
217 |
--------------------------------------------------------------------------------
/src/Eth/RPC.elm:
--------------------------------------------------------------------------------
1 | module Eth.RPC exposing
2 | ( RpcRequest, toTask
3 | , encode, toHttpBody
4 | )
5 |
6 | {-| Json RPC Helpers
7 |
8 | @docs RpcRequest, toTask
9 |
10 |
11 | # Low Level
12 |
13 | @docs encode, toHttpBody
14 |
15 | -}
16 |
17 | import Http
18 | import Json.Decode as Decode exposing (Decoder)
19 | import Json.Encode as Encode exposing (Value, int, list, object, string)
20 | import Task exposing (Task)
21 |
22 |
23 | jsonRPCVersion : String
24 | jsonRPCVersion =
25 | "2.0"
26 |
27 |
28 | {-| -}
29 | type alias RpcRequest a =
30 | { url : String
31 | , method : String
32 | , params : List Value
33 | , decoder : Decoder a
34 | }
35 |
36 |
37 | {-| -}
38 | toTask : RpcRequest a -> Task Http.Error a
39 | toTask { url, method, params, decoder } =
40 | Http.task
41 | { method = "POST"
42 | , headers = []
43 | , url = url
44 | , body = toHttpBody 1 method params
45 | , resolver = Http.stringResolver (expectJson decoder)
46 | , timeout = Nothing
47 | }
48 |
49 |
50 |
51 | -- Http.post url (toHttpBody 1 method params) (Decode.field "result" decoder)
52 | -- |> Http.toTask
53 |
54 |
55 | expectJson : Decoder a -> Http.Response String -> Result Http.Error a
56 | expectJson decoder response =
57 | case response of
58 | Http.BadUrl_ url ->
59 | Err (Http.BadUrl url)
60 |
61 | Http.Timeout_ ->
62 | Err Http.Timeout
63 |
64 | Http.NetworkError_ ->
65 | Err Http.NetworkError
66 |
67 | Http.BadStatus_ metadata body ->
68 | Err (Http.BadStatus metadata.statusCode)
69 |
70 | Http.GoodStatus_ metadata body ->
71 | case Decode.decodeString (Decode.field "result" decoder) body of
72 | Ok value ->
73 | Ok value
74 |
75 | Err err ->
76 | Err (Http.BadBody (Decode.errorToString err))
77 |
78 |
79 |
80 | -- Low Level
81 |
82 |
83 | {-| -}
84 | toHttpBody : Int -> String -> List Value -> Http.Body
85 | toHttpBody id method params =
86 | encode id method params
87 | |> Http.jsonBody
88 |
89 |
90 | {-| -}
91 | encode : Int -> String -> List Value -> Value
92 | encode id method params =
93 | object
94 | [ ( "id", int id )
95 | , ( "jsonrpc", string jsonRPCVersion )
96 | , ( "method", string method )
97 | , ( "params", list identity params )
98 | ]
99 |
--------------------------------------------------------------------------------
/src/Eth/Sentry/ChainCmd.elm:
--------------------------------------------------------------------------------
1 | module Eth.Sentry.ChainCmd exposing (a)
2 |
3 |
4 | a =
5 | 1
6 |
7 |
8 |
9 | -- ( ChainCmd, Sentry, execute, batch, none, map
10 | -- , sendTx, sendWithReceipt, customSend
11 | -- , watchEvent, watchEventOnce, unWatch
12 | -- )
13 | -- {-| For dApp Single Page Applications
14 | -- If your EventSentry or TxSentry live at the top level of your model, and you are sending txs or listening to event in your sub-pages,
15 | -- use ChainCmd. See examples.
16 | -- # Core
17 | -- @docs ChainCmd, Sentry, execute, batch, none, map
18 | -- # TxSentry
19 | -- @docs sendTx, sendWithReceipt, customSend
20 | -- # EventSentry
21 | -- @docs watchEvent, watchEventOnce, unWatch
22 | -- -}
23 | -- import Eth.Sentry.Event as EventSentry
24 | -- import Eth.Sentry.Tx as TxSentry
25 | -- import Eth.Types exposing (..)
26 | -- import Json.Decode exposing (Value)
27 | -- {-| -}
28 | -- type ChainCmd msg
29 | -- = SendTx (Result String Tx -> msg) Send
30 | -- | SendWithReceipt (Result String Tx -> msg) (Result String TxReceipt -> msg) Send
31 | -- | CustomSend (TxSentry.CustomSend msg) Send
32 | -- | WatchEvent (Value -> msg) LogFilter
33 | -- | WatchEventOnce (Value -> msg) LogFilter
34 | -- | UnWatch LogFilter
35 | -- | Many (List (ChainCmd msg))
36 | -- | None
37 | -- {-| -}
38 | -- type alias Sentry msg =
39 | -- ( TxSentry.TxSentry msg, EventSentry.EventSentry msg )
40 | -- {-| -}
41 | -- execute : Sentry msg -> ChainCmd msg -> ( Sentry msg, Cmd msg )
42 | -- execute sentry chainEff =
43 | -- executeHelp [] sentry [ chainEff ]
44 | -- {-| -}
45 | -- batch : List (ChainCmd msg) -> ChainCmd msg
46 | -- batch =
47 | -- Many
48 | -- {-| -}
49 | -- none : ChainCmd msg
50 | -- none =
51 | -- None
52 | -- {-| -}
53 | -- map : (subMsg -> msg) -> ChainCmd subMsg -> ChainCmd msg
54 | -- map f subEff =
55 | -- case subEff of
56 | -- SendTx subMsg send ->
57 | -- SendTx (subMsg >> f) send
58 | -- SendWithReceipt subMsg1 subMsg2 send ->
59 | -- SendWithReceipt (subMsg1 >> f) (subMsg2 >> f) send
60 | -- CustomSend { onSign, onBroadcast, onMined } send ->
61 | -- let
62 | -- newCustomSend =
63 | -- TxSentry.CustomSend
64 | -- (Maybe.map ((<<) f) onSign)
65 | -- (Maybe.map ((<<) f) onBroadcast)
66 | -- (Maybe.map
67 | -- (\( subMsg1, trackerConfig ) ->
68 | -- ( subMsg1 >> f
69 | -- , Maybe.map
70 | -- (\tracker -> { tracker | toMsg = tracker.toMsg >> f })
71 | -- trackerConfig
72 | -- )
73 | -- )
74 | -- onMined
75 | -- )
76 | -- in
77 | -- CustomSend newCustomSend send
78 | -- WatchEvent subMsg logFilter ->
79 | -- WatchEvent (subMsg >> f) logFilter
80 | -- WatchEventOnce subMsg logFilter ->
81 | -- WatchEventOnce (subMsg >> f) logFilter
82 | -- UnWatch logFilter ->
83 | -- UnWatch logFilter
84 | -- Many effs ->
85 | -- Many <| List.map (map f) effs
86 | -- None ->
87 | -- None
88 | -- {-| -}
89 | -- sendTx : (Result String Tx -> msg) -> Send -> ChainCmd msg
90 | -- sendTx =
91 | -- SendTx
92 | -- {-| -}
93 | -- sendWithReceipt : (Result String Tx -> msg) -> (Result String TxReceipt -> msg) -> Send -> ChainCmd msg
94 | -- sendWithReceipt =
95 | -- SendWithReceipt
96 | -- {-| -}
97 | -- customSend : TxSentry.CustomSend msg -> Send -> ChainCmd msg
98 | -- customSend =
99 | -- CustomSend
100 | -- {-| -}
101 | -- watchEvent : (Value -> msg) -> LogFilter -> ChainCmd msg
102 | -- watchEvent =
103 | -- WatchEvent
104 | -- {-| -}
105 | -- watchEventOnce : (Value -> msg) -> LogFilter -> ChainCmd msg
106 | -- watchEventOnce =
107 | -- WatchEventOnce
108 | -- {-| -}
109 | -- unWatch : LogFilter -> ChainCmd msg
110 | -- unWatch =
111 | -- UnWatch
112 | -- -- External
113 | -- {- TODO
114 | -- Make impossible states impossible
115 | -- e.g, running SendTx if you have only supplied EventEff Sentry
116 | -- -}
117 | -- executeHelp : List (Cmd msg) -> Sentry msg -> List (ChainCmd msg) -> ( Sentry msg, Cmd msg )
118 | -- executeHelp cmds sentry chainEffs =
119 | -- case chainEffs of
120 | -- [] ->
121 | -- ( sentry, Cmd.batch cmds )
122 | -- (SendTx toMsg txParams) :: xs ->
123 | -- sendTxHelp toMsg txParams cmds sentry xs
124 | -- (SendWithReceipt toMsg1 toMsg2 txParams) :: xs ->
125 | -- sendWithReceiptHelp toMsg1 toMsg2 txParams cmds sentry xs
126 | -- (CustomSend customSend_ txParams) :: xs ->
127 | -- customSendHelp customSend_ txParams cmds sentry xs
128 | -- (WatchEvent toMsg logFilter) :: xs ->
129 | -- watchEventHelp toMsg logFilter cmds sentry xs
130 | -- (WatchEventOnce toMsg logFilter) :: xs ->
131 | -- watchEventOnceHelp toMsg logFilter cmds sentry xs
132 | -- (UnWatch logFilter) :: xs ->
133 | -- unWatchHelp logFilter cmds sentry xs
134 | -- (Many chainEffs_) :: xs ->
135 | -- executeHelp cmds sentry (chainEffs_ ++ xs)
136 | -- None :: xs ->
137 | -- executeHelp cmds sentry xs
138 | -- -- TxSentry Helpers
139 | -- sendTxHelp :
140 | -- (Result String Tx -> msg)
141 | -- -> Send
142 | -- -> List (Cmd msg)
143 | -- -> Sentry msg
144 | -- -> List (ChainCmd msg)
145 | -- -> ( Sentry msg, Cmd msg )
146 | -- sendTxHelp toMsg txParams cmds ( txSentry, eventSentry ) xs =
147 | -- let
148 | -- ( newTxSentry, txCmd ) =
149 | -- TxSentry.send toMsg txSentry txParams
150 | -- in
151 | -- executeHelp (txCmd :: cmds) ( newTxSentry, eventSentry ) xs
152 | -- sendWithReceiptHelp :
153 | -- (Result String Tx -> msg)
154 | -- -> (Result String TxReceipt -> msg)
155 | -- -> Send
156 | -- -> List (Cmd msg)
157 | -- -> Sentry msg
158 | -- -> List (ChainCmd msg)
159 | -- -> ( Sentry msg, Cmd msg )
160 | -- sendWithReceiptHelp toMsg1 toMsg2 txParams cmds ( txSentry, eventSentry ) xs =
161 | -- let
162 | -- ( newTxSentry, txCmd ) =
163 | -- TxSentry.sendWithReceipt toMsg1 toMsg2 txSentry txParams
164 | -- in
165 | -- executeHelp (txCmd :: cmds) ( newTxSentry, eventSentry ) xs
166 | -- customSendHelp :
167 | -- TxSentry.CustomSend msg
168 | -- -> Send
169 | -- -> List (Cmd msg)
170 | -- -> Sentry msg
171 | -- -> List (ChainCmd msg)
172 | -- -> ( Sentry msg, Cmd msg )
173 | -- customSendHelp customSend_ txParams cmds ( txSentry, eventSentry ) xs =
174 | -- let
175 | -- ( newTxSentry, txCmd ) =
176 | -- TxSentry.customSend txSentry customSend_ txParams
177 | -- in
178 | -- executeHelp (txCmd :: cmds) ( newTxSentry, eventSentry ) xs
179 | -- -- EventSentry Helpers
180 | -- watchEventHelp :
181 | -- (Value -> msg)
182 | -- -> LogFilter
183 | -- -> List (Cmd msg)
184 | -- -> Sentry msg
185 | -- -> List (ChainCmd msg)
186 | -- -> ( Sentry msg, Cmd msg )
187 | -- watchEventHelp toMsg logFilter cmds ( txSentry, eventSentry ) xs =
188 | -- let
189 | -- ( newEventSentry, eventCmd ) =
190 | -- EventSentry.watch toMsg eventSentry logFilter
191 | -- in
192 | -- executeHelp (eventCmd :: cmds) ( txSentry, newEventSentry ) xs
193 | -- watchEventOnceHelp :
194 | -- (Value -> msg)
195 | -- -> LogFilter
196 | -- -> List (Cmd msg)
197 | -- -> Sentry msg
198 | -- -> List (ChainCmd msg)
199 | -- -> ( Sentry msg, Cmd msg )
200 | -- watchEventOnceHelp toMsg logFilter cmds ( txSentry, eventSentry ) xs =
201 | -- let
202 | -- ( newEventSentry, eventCmd ) =
203 | -- EventSentry.watchOnce toMsg eventSentry logFilter
204 | -- in
205 | -- executeHelp (eventCmd :: cmds) ( txSentry, newEventSentry ) xs
206 | -- unWatchHelp :
207 | -- LogFilter
208 | -- -> List (Cmd msg)
209 | -- -> Sentry msg
210 | -- -> List (ChainCmd msg)
211 | -- -> ( Sentry msg, Cmd msg )
212 | -- unWatchHelp logFilter cmds ( txSentry, eventSentry ) xs =
213 | -- let
214 | -- ( newEventSentry, eventCmd ) =
215 | -- EventSentry.unWatch eventSentry logFilter
216 | -- in
217 | -- executeHelp (eventCmd :: cmds) ( txSentry, newEventSentry ) xs
218 |
--------------------------------------------------------------------------------
/src/Eth/Sentry/Event.elm:
--------------------------------------------------------------------------------
1 | module Eth.Sentry.Event exposing
2 | ( EventSentry, Msg, Ref, init, stopWatching, update, watch, watchOnce
3 | , currentBlock
4 | )
5 |
6 | {-| Event Sentry - HTTP Style - Polling ftw
7 |
8 | @docs EventSentry, Msg, Ref, init, stopWatching, update, watch, watchOnce
9 | @docs currentBlock
10 |
11 | -}
12 |
13 | import Dict exposing (Dict)
14 | import Eth
15 | import Eth.Types exposing (..)
16 | import Http
17 | import Maybe.Extra
18 | import Process
19 | import Set exposing (Set)
20 | import Task exposing (Task)
21 |
22 |
23 |
24 | {-
25 | HTTP Polling Event Sentry - How it works:
26 | Upon EventySentry initialization, the block number is polled every 2 seconds.
27 |
28 | When you want to watch for a particular event, it is added to a set of events to be watched for (`watching`).
29 | When a new block is mined, we check to see if it contains any events we are interested in watching.
30 |
31 | If any watches/requests are made before a block-number is found, the requests are marked as pending,
32 | and requested once a block-number is received.
33 |
34 |
35 | Note: We do not use eth_newFilter, or any of the filter RPC endpoints,
36 | as these are not supported by Infura (in favor of websockets).
37 |
38 | -}
39 | {-
40 |
41 | nodePath : HTTP Address of Ethereum Node
42 | tagger : Wrap an Sentry.Event.Msg in your applications Msg
43 | requests : Dictionary to keep track of user's event requests
44 | ref : RPC ID Reference
45 | blockNumber : The last known block number - `Nothing` if response to first block number request is yet to come.
46 | watching : List of events currently being watched for.
47 | pending : List of events to be requested once the sentry.blockNumber is received.
48 | errors : Any HTTP errors made during RPC calls.
49 | -}
50 |
51 |
52 | {-| -}
53 | type EventSentry msg
54 | = EventSentry
55 | { nodePath : HttpProvider
56 | , tagger : Msg -> msg
57 | , requests : Dict Ref (RequestState msg)
58 | , ref : Ref
59 | , blockNumber : Maybe Int
60 | , watching : Set Int
61 | , pending : Set Int
62 | , errors : List Http.Error
63 | }
64 |
65 |
66 | {-| -}
67 | init : (Msg -> msg) -> HttpProvider -> ( EventSentry msg, Cmd msg )
68 | init tagger nodePath =
69 | ( EventSentry
70 | { nodePath = nodePath
71 | , tagger = tagger
72 | , requests = Dict.empty
73 | , ref = 1
74 | , blockNumber = Nothing
75 | , watching = Set.empty
76 | , pending = Set.empty
77 | , errors = []
78 | }
79 | , Task.attempt (BlockNumber >> tagger) (Eth.getBlockNumber nodePath)
80 | )
81 |
82 |
83 | {-| Returns the first log found.
84 |
85 | If a block range is defined in the LogFilter,
86 | this will only return the first log found within that given block range.
87 |
88 | -}
89 | watchOnce : (Log -> msg) -> EventSentry msg -> LogFilter -> ( EventSentry msg, Cmd msg )
90 | watchOnce onReceive eventSentry logFilter =
91 | watch_ True onReceive eventSentry logFilter
92 | |> (\( eventSentry_, cmd, _ ) -> ( eventSentry_, cmd ))
93 |
94 |
95 | {-| Continuously polls for logs in newly mined blocks.
96 |
97 | If the range within the LogFilter includes past blocks,
98 | then all events within the given block range are returned,
99 | along with events in the latest block.
100 |
101 | Polling continues until `stopWatching` is called.
102 |
103 | -}
104 | watch : (Log -> msg) -> EventSentry msg -> LogFilter -> ( EventSentry msg, Cmd msg, Ref )
105 | watch =
106 | watch_ False
107 |
108 |
109 | {-| -}
110 | stopWatching : Ref -> EventSentry msg -> EventSentry msg
111 | stopWatching ref (EventSentry sentry) =
112 | EventSentry { sentry | watching = Set.remove ref sentry.watching }
113 |
114 |
115 | {-| The Event Sentry polls for the latest block. Might as well allow the user to see it.
116 | -}
117 | currentBlock : EventSentry msg -> Maybe Int
118 | currentBlock (EventSentry { blockNumber }) =
119 | blockNumber
120 |
121 |
122 |
123 | -- Internal
124 |
125 |
126 | {-| -}
127 | type alias RequestState msg =
128 | { tagger : Log -> msg
129 | , ref : Ref
130 | , logFilter : LogFilter
131 | , watchOnce : Bool
132 | , logCount : Int
133 | }
134 |
135 |
136 | {-| -}
137 | type alias Ref =
138 | Int
139 |
140 |
141 | watch_ : Bool -> (Log -> msg) -> EventSentry msg -> LogFilter -> ( EventSentry msg, Cmd msg, Ref )
142 | watch_ onlyOnce onReceive (EventSentry sentry) logFilter =
143 | let
144 | requestState =
145 | { tagger = onReceive
146 | , ref = sentry.ref
147 | , logFilter = logFilter
148 | , watchOnce = onlyOnce
149 | , logCount = 0
150 | }
151 |
152 | newSentry =
153 | { sentry
154 | | requests = Dict.insert sentry.ref requestState sentry.requests
155 | , ref = sentry.ref + 1
156 | }
157 |
158 | return task =
159 | ( EventSentry { newSentry | watching = Set.insert sentry.ref newSentry.watching }
160 | , Task.attempt (GetLogs sentry.ref >> sentry.tagger) task
161 | , sentry.ref
162 | )
163 | in
164 | case sentry.blockNumber of
165 | Just blockNum ->
166 | requestInitialEvents sentry.nodePath logFilter ( blockNum, blockNum )
167 | |> return
168 |
169 | Nothing ->
170 | -- If sentry is still waiting for blocknumber, mark request as pending.
171 | ( EventSentry { newSentry | pending = Set.insert sentry.ref newSentry.pending }
172 | , Cmd.none
173 | , sentry.ref
174 | )
175 |
176 |
177 |
178 | -- Update
179 |
180 |
181 | {-| -}
182 | type Msg
183 | = BlockNumber (Result Http.Error Int)
184 | | GetLogs Ref (Result Http.Error (List Log))
185 |
186 |
187 | {-| -}
188 | update : Msg -> EventSentry msg -> ( EventSentry msg, Cmd msg )
189 | update msg ((EventSentry sentry) as sentry_) =
190 | case msg of
191 | BlockNumber (Ok newBlockNum) ->
192 | let
193 | requestHelper blockRange set toTask =
194 | Set.toList set
195 | |> List.map (\ref -> Dict.get ref sentry.requests)
196 | |> Maybe.Extra.values
197 | |> List.map
198 | (\requestState ->
199 | toTask sentry.nodePath requestState.logFilter blockRange
200 | |> Task.attempt (GetLogs requestState.ref >> sentry.tagger)
201 | )
202 | |> Cmd.batch
203 | in
204 | case sentry.blockNumber of
205 | Just oldBlockNum ->
206 | if newBlockNum - oldBlockNum == 0 then
207 | ( sentry_
208 | , pollBlockNumber sentry.nodePath sentry.tagger
209 | )
210 |
211 | else
212 | ( EventSentry { sentry | blockNumber = Just newBlockNum }
213 | , Cmd.batch
214 | [ pollBlockNumber sentry.nodePath sentry.tagger
215 | , requestHelper ( oldBlockNum + 1, newBlockNum ) sentry.watching requestWatchedEvents
216 | ]
217 | )
218 |
219 | Nothing ->
220 | ( EventSentry
221 | { sentry
222 | | blockNumber = Just newBlockNum
223 | , pending = Set.empty
224 | , watching = Set.union sentry.watching sentry.pending
225 | }
226 | , Cmd.batch
227 | [ pollBlockNumber sentry.nodePath sentry.tagger
228 | , requestHelper ( newBlockNum, newBlockNum ) sentry.pending requestInitialEvents
229 | , requestHelper ( newBlockNum, newBlockNum ) sentry.watching requestWatchedEvents
230 | ]
231 | )
232 |
233 | BlockNumber (Err err) ->
234 | ( EventSentry { sentry | errors = err :: sentry.errors }
235 | , pollBlockNumber sentry.nodePath sentry.tagger
236 | )
237 |
238 | GetLogs ref (Ok logs) ->
239 | handleLogs sentry_ ref logs
240 |
241 | GetLogs _ (Err err) ->
242 | ( EventSentry { sentry | errors = err :: sentry.errors }
243 | , Cmd.none
244 | )
245 |
246 |
247 |
248 | -- BlockNumber Helpers
249 |
250 |
251 | pollBlockNumber : HttpProvider -> (Msg -> msg) -> Cmd msg
252 | pollBlockNumber ethNode tagger =
253 | Process.sleep 2000
254 | |> Task.andThen (\_ -> Eth.getBlockNumber ethNode)
255 | |> Task.attempt (BlockNumber >> tagger)
256 |
257 |
258 | {-| Request logs found within the latest block range.
259 |
260 | Defined as a "latest block range" instead of "latest block",
261 | since the possibility of multiple blocks being mined between Eth.getBlockNumber requests is a possibility.
262 |
263 | -}
264 | requestWatchedEvents : HttpProvider -> LogFilter -> ( Int, Int ) -> Task Http.Error (List Log)
265 | requestWatchedEvents nodePath logFilter ( fromBlock, toBlock ) =
266 | Eth.getLogs nodePath
267 | { logFilter | fromBlock = BlockNum fromBlock, toBlock = BlockNum toBlock }
268 |
269 |
270 | {-| Request logs within the LogFilter's initially defined range,
271 | and combine it with any logs found in the latest block range.
272 | -}
273 | requestInitialEvents : HttpProvider -> LogFilter -> ( Int, Int ) -> Task Http.Error (List Log)
274 | requestInitialEvents nodePath logFilter ( fromBlock, toBlock ) =
275 | case logFilter.toBlock of
276 | BlockNum _ ->
277 | -- Grab logs in the intitially defined block range, then grab the latest blocks events.
278 | Eth.getLogs nodePath logFilter
279 | |> Task.andThen
280 | (\logs ->
281 | Eth.getLogs nodePath
282 | { logFilter | fromBlock = BlockNum fromBlock, toBlock = BlockNum toBlock }
283 | |> Task.map ((++) logs)
284 | )
285 |
286 | _ ->
287 | -- Otherwise, just grab the full block range, where we'll include the latest.
288 | Eth.getLogs nodePath logFilter
289 |
290 |
291 |
292 | -- GetLog Helpers
293 |
294 |
295 | handleLogs : EventSentry msg -> Ref -> List Log -> ( EventSentry msg, Cmd msg )
296 | handleLogs (EventSentry sentry) ref logs =
297 | case Dict.get ref sentry.requests of
298 | Nothing ->
299 | ( EventSentry sentry, Cmd.none )
300 |
301 | Just requestState ->
302 | case ( requestState.watchOnce, List.head logs ) of
303 | ( _, Nothing ) ->
304 | ( EventSentry { sentry | requests = updateRequests ref logs sentry.requests }
305 | , Cmd.none
306 | )
307 |
308 | ( True, Just log ) ->
309 | ( EventSentry
310 | { sentry
311 | | watching = Set.remove ref sentry.watching
312 | , requests = updateRequests ref logs sentry.requests
313 | }
314 | , Task.perform requestState.tagger (Task.succeed log)
315 | )
316 |
317 | ( False, _ ) ->
318 | ( EventSentry { sentry | requests = updateRequests ref logs sentry.requests }
319 | , List.map (\log -> Task.perform requestState.tagger (Task.succeed log)) logs
320 | |> Cmd.batch
321 | )
322 |
323 |
324 | {-| Keeps track of log count for each request.
325 | -}
326 | updateRequests : Ref -> List Log -> Dict Ref (RequestState msg) -> Dict Ref (RequestState msg)
327 | updateRequests ref logs requests =
328 | Dict.update ref
329 | (Maybe.map
330 | (\requestState -> { requestState | logCount = List.length logs + requestState.logCount })
331 | )
332 | requests
333 |
--------------------------------------------------------------------------------
/src/Eth/Sentry/Wallet.elm:
--------------------------------------------------------------------------------
1 | module Eth.Sentry.Wallet exposing (WalletSentry, default, decoder, decodeToMsg)
2 |
3 | {-| Wallet Sentry
4 |
5 | @docs WalletSentry, default, decoder, decodeToMsg
6 |
7 | -}
8 |
9 | import Eth.Decode as Decode
10 | import Eth.Net as Net exposing (NetworkId(..))
11 | import Eth.Types exposing (Address)
12 | import Json.Decode as Decode exposing (Decoder, Value)
13 |
14 |
15 | {-| -}
16 | type alias WalletSentry =
17 | { account : Maybe Address
18 | , networkId : NetworkId
19 | }
20 |
21 |
22 | {-| -}
23 | default : WalletSentry
24 | default =
25 | WalletSentry Nothing (Private 0)
26 |
27 |
28 | {-| -}
29 | decoder : Decoder WalletSentry
30 | decoder =
31 | Decode.map2 WalletSentry
32 | (Decode.field "account" (Decode.maybe Decode.address))
33 | (Decode.field "networkId" Net.networkIdDecoder)
34 |
35 |
36 | {-| -}
37 | decodeToMsg : (String -> msg) -> (WalletSentry -> msg) -> Value -> msg
38 | decodeToMsg failMsg successMsg val =
39 | case Decode.decodeValue decoder val of
40 | Err error ->
41 | failMsg (Decode.errorToString error)
42 |
43 | Ok walletSentry ->
44 | successMsg walletSentry
45 |
--------------------------------------------------------------------------------
/src/Eth/Types.elm:
--------------------------------------------------------------------------------
1 | module Eth.Types exposing
2 | ( Address, TxHash, BlockHash, Hex
3 | , Call, Send, Tx, TxReceipt, BlockId(..), Block, Uncle, BlockHead, Log, Event, LogFilter, SyncStatus
4 | , HttpProvider, WebsocketProvider, FilterId
5 | )
6 |
7 | {-| Types
8 |
9 |
10 | # Simple
11 |
12 | @docs Address, TxHash, BlockHash, Hex
13 |
14 |
15 | # Complex
16 |
17 | @docs Call, Send, Tx, TxReceipt, BlockId, Block, Uncle, BlockHead, Log, Event, LogFilter, SyncStatus
18 |
19 |
20 | # Misc
21 |
22 | @docs HttpProvider, WebsocketProvider, FilterId
23 |
24 | -}
25 |
26 | import BigInt exposing (BigInt)
27 | import Http
28 | import Internal.Types as Internal
29 | import Json.Decode exposing (Decoder)
30 | import Time exposing (Posix)
31 |
32 |
33 | type Error
34 | = Http Http.Error -- Standard HTTP Errors
35 | | Encoding String -- Most likely an overflow of int/uint
36 | -- Call returns 0x, could mean:
37 | -- Contract doesn't exist
38 | -- Contract function doesn't exist
39 | -- Other things (look at the talk by Augur team at Devcon4 on mainstage)
40 | | ZeroX String
41 | -- TxSentry Errors:
42 | | UserRejected -- User dissapproved of tx in Wallet
43 | | Web3Undefined -- Web3 object, or provider not found.
44 |
45 |
46 |
47 | -- Simple
48 |
49 |
50 | {-| -}
51 | type alias Address =
52 | Internal.Address
53 |
54 |
55 | {-| -}
56 | type alias TxHash =
57 | Internal.TxHash
58 |
59 |
60 | {-| -}
61 | type alias BlockHash =
62 | Internal.BlockHash
63 |
64 |
65 | {-| -}
66 | type alias Hex =
67 | Internal.Hex
68 |
69 |
70 |
71 | -- Complex
72 |
73 |
74 | {-| -}
75 | type alias Call a =
76 | { to : Maybe Address
77 | , from : Maybe Address
78 | , gas : Maybe Int
79 | , gasPrice : Maybe BigInt
80 | , value : Maybe BigInt
81 | , data : Maybe Hex
82 | , nonce : Maybe Int
83 | , decoder : Decoder a
84 | }
85 |
86 |
87 | {-| -}
88 | type alias Send =
89 | { to : Maybe Address
90 | , from : Maybe Address
91 | , gas : Maybe Int
92 | , gasPrice : Maybe BigInt
93 | , value : Maybe BigInt
94 | , data : Maybe Hex
95 | , nonce : Maybe Int
96 | }
97 |
98 |
99 | {-| -}
100 | type alias Tx =
101 | { hash : TxHash
102 | , nonce : Int
103 | , blockHash : Maybe BlockHash
104 | , blockNumber : Maybe Int
105 | , transactionIndex : Int
106 | , from : Address
107 | , to : Maybe Address
108 | , value : BigInt
109 | , gasPrice : BigInt
110 | , gas : Int
111 | , input : String
112 | }
113 |
114 |
115 | {-| -}
116 | type alias TxReceipt =
117 | { hash : TxHash
118 | , index : Int
119 | , blockHash : BlockHash
120 | , blockNumber : Int
121 | , gasUsed : BigInt
122 | , cumulativeGasUsed : BigInt
123 | , contractAddress : Maybe Address
124 | , logs : List Log
125 | , logsBloom : String
126 | , root : Maybe String
127 | , status : Maybe Bool
128 | }
129 |
130 |
131 | {-| -}
132 | type BlockId
133 | = BlockNum Int
134 | | EarliestBlock
135 | | LatestBlock
136 | | PendingBlock
137 |
138 |
139 | {-| -}
140 | type alias Block a =
141 | { number : Int
142 | , hash : BlockHash
143 | , parentHash : BlockHash
144 | , nonce : String
145 | , sha3Uncles : String
146 | , logsBloom : String
147 | , transactionsRoot : String
148 | , stateRoot : String
149 | , receiptsRoot : String
150 | , miner : Address
151 | , difficulty : BigInt
152 | , totalDifficulty : BigInt
153 | , extraData : String
154 | , size : Int
155 | , gasLimit : Int
156 | , gasUsed : Int
157 | , timestamp : Posix
158 | , transactions : List a
159 | , uncles : List String
160 | }
161 |
162 |
163 | {-| -}
164 | type alias Uncle =
165 | Block ()
166 |
167 |
168 | {-| -}
169 | type alias BlockHead =
170 | { number : Int
171 | , hash : BlockHash
172 | , parentHash : BlockHash
173 | , nonce : String
174 | , sha3Uncles : String
175 | , logsBloom : String
176 | , transactionsRoot : String
177 | , stateRoot : String
178 | , receiptsRoot : String
179 | , miner : Address
180 | , difficulty : BigInt
181 | , extraData : String
182 | , gasLimit : Int
183 | , gasUsed : Int
184 | , mixHash : String
185 | , timestamp : Posix
186 | }
187 |
188 |
189 | {-| -}
190 | type alias Log =
191 | { address : Address
192 | , data : String
193 | , topics : List Hex
194 | , removed : Bool
195 | , logIndex : Int
196 | , transactionIndex : Int
197 | , transactionHash : TxHash
198 | , blockHash : BlockHash
199 | , blockNumber : Int
200 | }
201 |
202 |
203 | {-| -}
204 | type alias Event a =
205 | { address : Address
206 | , data : String
207 | , topics : List Hex
208 | , removed : Bool
209 | , logIndex : Int
210 | , transactionIndex : Int
211 | , transactionHash : TxHash
212 | , blockHash : BlockHash
213 | , blockNumber : Int
214 | , returnData : a
215 | }
216 |
217 |
218 | {-| NOTE: Different from JSON RPC API, removed some optionality to reduce complexity (array with array)
219 | -}
220 | type alias LogFilter =
221 | { fromBlock : BlockId
222 | , toBlock : BlockId
223 | , address : Address
224 | , topics : List (Maybe Hex)
225 | }
226 |
227 |
228 | {-| -}
229 | type alias SyncStatus =
230 | { startingBlock : Int
231 | , currentBlock : Int
232 | , highestBlock : Int
233 | , knownStates : Int
234 | , pulledStates : Int
235 | }
236 |
237 |
238 |
239 | -- Misc
240 |
241 |
242 | {-| -}
243 | type alias HttpProvider =
244 | String
245 |
246 |
247 | {-| -}
248 | type alias WebsocketProvider =
249 | String
250 |
251 |
252 | {-| -}
253 | type alias FilterId =
254 | String
255 |
--------------------------------------------------------------------------------
/src/Eth/Units.elm:
--------------------------------------------------------------------------------
1 | module Eth.Units exposing
2 | ( gwei, eth
3 | , EthUnit(..), toWei, fromWei, bigIntToWei
4 | )
5 |
6 | {-| Conversions and Helpers
7 |
8 |
9 | # Concise Units
10 |
11 | Useful helpers for concise value declarations.
12 |
13 | txParams : Send
14 | txParams =
15 | { to = Just myContract
16 | , from = Nothing
17 | , gas = Nothing
18 | , gasPrice = Just (gwei 3)
19 | , value = Just (eth 3)
20 | , data = Just data
21 | , nonce = Nothing
22 | }
23 |
24 | @docs gwei, eth
25 |
26 |
27 | # Precise Units
28 |
29 | Helpers for dealing with floats.
30 |
31 | @docs EthUnit, toWei, fromWei, bigIntToWei
32 |
33 | -}
34 |
35 | import BigInt exposing (BigInt)
36 | import Regex
37 |
38 |
39 |
40 | -- fromInts, useful for building contract params
41 |
42 |
43 | {-| -}
44 | gwei : Int -> BigInt
45 | gwei =
46 | BigInt.fromInt >> BigInt.mul (BigInt.fromInt 1000000000)
47 |
48 |
49 | {-| -}
50 | eth : Int -> BigInt
51 | eth =
52 | let
53 | oneEth =
54 | BigInt.mul (BigInt.fromInt 100) (BigInt.fromInt 10000000000000000)
55 | in
56 | BigInt.fromInt >> BigInt.mul oneEth
57 |
58 |
59 | {-| Eth Unit
60 | Useful for displaying to, and taking user input from, the UI
61 | -}
62 | type EthUnit
63 | = Wei
64 | | Kwei
65 | | Mwei
66 | | Gwei
67 | | Microether
68 | | Milliether
69 | | Ether
70 | | Kether
71 | | Mether
72 | | Gether
73 | | Tether
74 |
75 |
76 | {-| Convert a given stringy EthUnit to it's Wei equivalent
77 |
78 | toWei Gwei "50" == Ok (BigInt.fromInt 50000000000)
79 |
80 | toWei Wei "40.9123" == Ok (BigInt.fromInt 40)
81 |
82 | toWei Kwei "40.9123" == Ok (BigInt.fromInt 40912)
83 |
84 | toWei Gwei "ten" == Err
85 |
86 | -}
87 | toWei : EthUnit -> String -> Result String BigInt
88 | toWei unit amount =
89 | -- check to make sure input string is formatted correctly, should never error in here.
90 | if Regex.contains (Maybe.withDefault Regex.never (Regex.fromString "^\\d*\\.?\\d+$")) amount then
91 | let
92 | decimalPoints =
93 | decimalShift unit
94 |
95 | formatMantissa =
96 | String.slice 0 decimalPoints >> String.padRight decimalPoints '0'
97 |
98 | finalResult =
99 | case String.split "." amount of
100 | [ a, b ] ->
101 | a ++ formatMantissa b
102 |
103 | [ a ] ->
104 | a ++ formatMantissa ""
105 |
106 | _ ->
107 | "ImpossibleError"
108 | in
109 | case BigInt.fromIntString finalResult of
110 | Just result ->
111 | Ok result
112 |
113 | Nothing ->
114 | Err ("There was an error calculating toWei result. However, the fault is not yours; please report this bug on github. Logs: " ++ finalResult)
115 |
116 | else
117 | Err "Malformed number string passed to `toWei` method."
118 |
119 |
120 | {-| Convert stringy Wei to a given EthUnit
121 |
122 | fromWei Gwei (BigInt.fromInt 123456789) == "0.123456789"
123 |
124 | fromWei Ether (BigInt.fromInt 123456789) == "0.000000000123456789"
125 |
126 | **Note** Do not pass anything larger than MAX\_SAFE\_INTEGER into BigInt.fromInt
127 | MAX\_SAFE\_INTEGER == 9007199254740991
128 |
129 | -}
130 | fromWei : EthUnit -> BigInt -> String
131 | fromWei unit amount =
132 | let
133 | decimalIndex =
134 | decimalShift unit
135 |
136 | -- There are under 10^27 wei in existance (so we safe for the next couple of millennia).
137 | amountStr =
138 | BigInt.toString amount |> String.padLeft 27 '0'
139 |
140 | result =
141 | String.left (27 - decimalIndex) amountStr
142 | ++ "."
143 | ++ String.right decimalIndex amountStr
144 | in
145 | result
146 | |> Regex.replace
147 | (Maybe.withDefault Regex.never (Regex.fromString "(^0*(?=0\\.|[1-9]))|(\\.?0*$)"))
148 | (\i -> "")
149 |
150 |
151 | {-| Convert a given BigInt EthUnit to it's Wei equivalent
152 | -}
153 | bigIntToWei : EthUnit -> BigInt -> BigInt
154 | bigIntToWei unit amount =
155 | BigInt.pow (BigInt.fromInt 10) (BigInt.fromInt <| decimalShift unit)
156 | |> BigInt.mul amount
157 |
158 |
159 |
160 | -- Internal
161 |
162 |
163 | decimalShift : EthUnit -> Int
164 | decimalShift unit =
165 | case unit of
166 | Wei ->
167 | 0
168 |
169 | Kwei ->
170 | 3
171 |
172 | Mwei ->
173 | 6
174 |
175 | Gwei ->
176 | 9
177 |
178 | Microether ->
179 | 12
180 |
181 | Milliether ->
182 | 15
183 |
184 | Ether ->
185 | 18
186 |
187 | Kether ->
188 | 21
189 |
190 | Mether ->
191 | 24
192 |
193 | Gether ->
194 | 27
195 |
196 | Tether ->
197 | 30
198 |
--------------------------------------------------------------------------------
/src/Internal/Types.elm:
--------------------------------------------------------------------------------
1 | module Internal.Types exposing (Address(..), BlockHash(..), DebugLogger, Hex(..), TxHash(..), WhisperId(..))
2 |
3 |
4 | type Address
5 | = Address String
6 |
7 |
8 | type TxHash
9 | = TxHash String
10 |
11 |
12 | type BlockHash
13 | = BlockHash String
14 |
15 |
16 | type WhisperId
17 | = WhisperId String
18 |
19 |
20 | type Hex
21 | = Hex String
22 |
23 |
24 | type alias DebugLogger a =
25 | String -> a -> a
26 |
--------------------------------------------------------------------------------
/src/Legacy/Base58.elm:
--------------------------------------------------------------------------------
1 | module Legacy.Base58 exposing (decode, encode)
2 |
3 | {-| Handles encoding/decoding base58 data
4 |
5 |
6 | # Transformations
7 |
8 | @docs decode, encode
9 |
10 | -}
11 |
12 | import Array exposing (Array)
13 | import BigInt exposing (BigInt)
14 | import String
15 |
16 |
17 | alphabet : String
18 | alphabet =
19 | "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
20 |
21 |
22 | alphabetArr : Array Char
23 | alphabetArr =
24 | alphabet
25 | |> String.toList
26 | |> Array.fromList
27 |
28 |
29 | alphabetLength : BigInt
30 | alphabetLength =
31 | BigInt.fromInt (String.length alphabet)
32 |
33 |
34 | getIndex : Char -> Result String BigInt
35 | getIndex char =
36 | String.indexes (String.fromChar char) alphabet
37 | |> List.head
38 | |> Result.fromMaybe ("'" ++ String.fromChar char ++ "' is not a valid base58 character.")
39 | |> Result.map BigInt.fromInt
40 |
41 |
42 | {-| Decodes a string into a BigInt
43 |
44 | "ANYBx47k26vP81XFbQXh6XKUj7ptQRJMLt"
45 | |> Base58.decode
46 | |> Result.toMaybe
47 | == BigInt.fromString "146192635802076751054841979942155177482410195601230638449945"
48 |
49 | -}
50 | decode : String -> Result String BigInt
51 | decode str =
52 | let
53 | strList =
54 | String.toList str
55 |
56 | ( _, decodedResult ) =
57 | List.foldr
58 | (\letter ( multi, dec ) ->
59 | let
60 | result =
61 | getIndex letter
62 | |> Result.map (BigInt.mul multi)
63 | |> Result.andThen (\n -> Result.map (BigInt.add n) dec)
64 |
65 | mul =
66 | BigInt.mul multi alphabetLength
67 | in
68 | ( mul, result )
69 | )
70 | ( BigInt.fromInt 1, Ok (BigInt.fromInt 0) )
71 | strList
72 | in
73 | if str == "" then
74 | Err "An empty string is not valid base58"
75 |
76 | else
77 | decodedResult
78 |
79 |
80 | {-| Encodes a BigInt into a string
81 |
82 | BigInt.fromString "146192635802076751054841979942155177482410195601230638449945"
83 | |> Maybe.map Base58.encode
84 | == Ok "ANYBx47k26vP81XFbQXh6XKUj7ptQRJMLt"
85 |
86 | -}
87 | encode : BigInt -> String
88 | encode num =
89 | let
90 | ( _, encoded ) =
91 | encodeReduce num ( "", BigInt.fromInt 0 )
92 | in
93 | encoded
94 |
95 |
96 | encodeReduce : BigInt -> ( String, BigInt ) -> ( BigInt, String )
97 | encodeReduce num ( encoded, n ) =
98 | if BigInt.gte num alphabetLength then
99 | let
100 | dv =
101 | BigInt.div num alphabetLength
102 |
103 | md =
104 | BigInt.sub num (BigInt.mul alphabetLength dv)
105 |
106 | index =
107 | Maybe.withDefault 0 (String.toInt (BigInt.toString md))
108 |
109 | i =
110 | String.fromChar (Maybe.withDefault '0' (Array.get index alphabetArr))
111 |
112 | newEncoded =
113 | i ++ encoded
114 | in
115 | encodeReduce dv ( newEncoded, dv )
116 |
117 | else
118 | let
119 | index =
120 | Maybe.withDefault 0 (String.toInt (BigInt.toString num))
121 |
122 | i =
123 | String.fromChar (Maybe.withDefault '0' (Array.get index alphabetArr))
124 |
125 | newEncoded =
126 | i ++ encoded
127 | in
128 | ( BigInt.fromInt 0, newEncoded )
129 |
--------------------------------------------------------------------------------
/src/Shh.elm:
--------------------------------------------------------------------------------
1 | module Shh exposing
2 | ( Post, post
3 | , WhisperId, newIdentity, whisperIdToString, toWhisperId, version
4 | )
5 |
6 | {-| Whipser API (Use at your own risk! Work in progress)
7 |
8 |
9 | # Whisper messaging
10 |
11 | @docs Post, post
12 |
13 |
14 | # Whisper Id's
15 |
16 | @docs WhisperId, newIdentity, whisperIdToString, toWhisperId, version
17 |
18 | -}
19 |
20 | import Eth.Decode as Decode
21 | import Eth.Encode as Encode exposing (listOfMaybesToVal)
22 | import Eth.RPC as RPC
23 | import Eth.Types exposing (..)
24 | import Eth.Utils exposing (..)
25 | import Http
26 | import Internal.Types as Internal
27 | import Json.Decode as Decode exposing (Decoder)
28 | import Json.Encode as Encode exposing (Value)
29 | import Task exposing (Task)
30 |
31 |
32 |
33 | -- Whisper Messaging
34 |
35 |
36 | {-| -}
37 | type alias Post =
38 | { from : Maybe String
39 | , to : Maybe String
40 | , topics : List String
41 | , payload : String
42 | , priority : Int
43 | , ttl : Int
44 | }
45 |
46 |
47 | {-| -}
48 | post : HttpProvider -> Post -> Task Http.Error Bool
49 | post ethNode post_ =
50 | RPC.toTask
51 | { url = ethNode
52 | , method = "shh_post"
53 | , params = [ encodePost post_ ]
54 | , decoder = Decode.bool
55 | }
56 |
57 |
58 |
59 | -- Whisper Id's
60 |
61 |
62 | {-| -}
63 | type alias WhisperId =
64 | Internal.WhisperId
65 |
66 |
67 | {-| -}
68 | newIdentity : HttpProvider -> Task Http.Error WhisperId
69 | newIdentity ethNode =
70 | RPC.toTask
71 | { url = ethNode
72 | , method = "shh_newIdentity"
73 | , params = []
74 | , decoder = Decode.resultToDecoder toWhisperId
75 | }
76 |
77 |
78 | {-| -}
79 | whisperIdToString : WhisperId -> String
80 | whisperIdToString (Internal.WhisperId str) =
81 | str
82 |
83 |
84 | {-| -}
85 | toWhisperId : String -> Result String WhisperId
86 | toWhisperId str =
87 | case isHex str && String.length str == 122 of
88 | True ->
89 | Ok <| Internal.WhisperId str
90 |
91 | False ->
92 | Err <| "Couldn't convert " ++ str ++ "into whisper id"
93 |
94 |
95 | {-| -}
96 | version : HttpProvider -> Task Http.Error Int
97 | version ethNode =
98 | RPC.toTask
99 | { url = ethNode
100 | , method = "shh_version"
101 | , params = []
102 | , decoder = Decode.stringInt
103 | }
104 |
105 |
106 |
107 | -- Internal Decoder/Encoder
108 |
109 |
110 | encodePost : Post -> Value
111 | encodePost { to, from, topics, payload, priority, ttl } =
112 | listOfMaybesToVal
113 | [ ( "to", Maybe.map Encode.string to )
114 | , ( "from", Maybe.map Encode.string from )
115 | , ( "topics", Just (Encode.list Encode.string topics) )
116 | , ( "payload", Maybe.map Encode.string (Just payload) )
117 | , ( "priority", Maybe.map Encode.hexInt (Just priority) )
118 | , ( "ttl", Maybe.map Encode.hexInt (Just ttl) )
119 | ]
120 |
--------------------------------------------------------------------------------
/tests/Address.elm:
--------------------------------------------------------------------------------
1 | module Address exposing (toAddressTests)
2 |
3 | -- import Fuzz exposing (Fuzzer, int, list, string)
4 |
5 | import Eth.Utils as Eth
6 | import Expect
7 | import Internal.Types as Internal
8 | import Test exposing (..)
9 |
10 |
11 | toAddressTests : Test
12 | toAddressTests =
13 | describe "toAddress"
14 | [ describe "toAddress success"
15 | [ test "from lowercase address with 0x" <|
16 | \_ ->
17 | Eth.toAddress "0xe4219dc25d6a05b060c2a39e3960a94a214aaeca"
18 | |> Expect.equal (Ok <| Internal.Address "e4219dc25d6a05b060c2a39e3960a94a214aaeca")
19 | , test "from uppercase address with 0x" <|
20 | \_ ->
21 | Eth.toAddress "0XF85FEEA2FDD81D51177F6B8F35F0E6734CE45F5F"
22 | |> Expect.equal (Ok <| Internal.Address "f85feea2fdd81d51177f6b8f35f0e6734ce45f5f")
23 | , test "from evm" <|
24 | \_ ->
25 | Eth.toAddress "000000000000000000000000f85feea2fdd81d51177f6b8f35f0e6734ce45f5f"
26 | |> Expect.equal (Ok <| Internal.Address "f85feea2fdd81d51177f6b8f35f0e6734ce45f5f")
27 | , test "from already checksummed" <|
28 | \_ ->
29 | Eth.toAddress "0xe4219dc25D6a05b060c2a39e3960A94a214aAeca"
30 | |> Expect.equal (Ok <| Internal.Address "e4219dc25d6a05b060c2a39e3960a94a214aaeca")
31 | , test "addressToString" <|
32 | \_ ->
33 | Eth.toAddress "0XF85FEEA2FDD81D51177F6B8F35F0E6734CE45F5F"
34 | |> Result.map Eth.addressToString
35 | |> Expect.equal (Ok "0xf85feea2fdd81d51177f6b8f35f0e6734ce45f5f")
36 | , test "addressToChecksumString" <|
37 | \_ ->
38 | Eth.toAddress "0xe4219dc25d6a05b060c2a39e3960a94a214aaeca"
39 | |> Result.map Eth.addressToChecksumString
40 | |> Expect.equal (Ok "0xe4219dc25D6a05b060c2a39e3960A94a214aAeca")
41 | ]
42 | , describe "toAddress fails"
43 | [ test "from short address with 0x" <|
44 | \_ ->
45 | Eth.toAddress "0x4219dc25d6a05b060c2a39e3960a94a214aaeca"
46 | |> Expect.err
47 | , test "from short address without 0x" <|
48 | \_ ->
49 | Eth.toAddress "4219dc25d6a05b060c2a39e3960a94a214aaeca"
50 | |> Expect.err
51 | , test "from invalid char evm" <|
52 | \_ ->
53 | Eth.toAddress
54 | "000000010000000000000000f85feea2fdd81d51177f6b8f35f0e6734ce45f5f"
55 | |> Expect.err
56 | , test "from invalid length evm" <|
57 | \_ ->
58 | Eth.toAddress
59 | "00000000000000000000000f85feea2fdd81d51177f6b8f35f0e6734ce45f5f"
60 | |> Expect.err
61 | , test "from invalid checksummed" <|
62 | \_ ->
63 | Eth.toAddress "0xe4219dc25D6a05b060c2a39e3960a94a214aAeca"
64 | |> Expect.err
65 | ]
66 | ]
67 |
--------------------------------------------------------------------------------
/tests/Constants.elm:
--------------------------------------------------------------------------------
1 | module Constants exposing (..)
2 |
3 |
4 | minedTx : String
5 | minedTx =
6 | """
7 | {"blockHash":"0x35b610a3eb284179c6b5771ca4f8454a6beecc94eda8886a66e6010646ecddad","blockNumber":"0x5397b7","from":"0x829bd824b016326a401d083b33d092293333a830","gas":"0x15f90","gasPrice":"0x7d2b7500","hash":"0xa5e508d3be8a9c69942024e3b419df2f5d864ff4c168dc82b07918559197b14a","input":"0x","nonce":"0x42d9c6","to":"0x25a78e4ff3df10a2156636e386df0220ed1787d3","transactionIndex":"0xc6","value":"0x2075091894264d6","v":"0x25","r":"0xfbddf71b456ff575fc66172f554a2cb8e4acc7a3bc0ac5daa7a5fae6768a6e83","s":"0x567451250e47067908a193ffa8452d1c0848f50e8b6c1a1e2eeb94a07784d232"}
8 | """
9 |
10 |
11 | blockWithTxHashes : String
12 | blockWithTxHashes =
13 | """
14 | {"difficulty":"0xb5de5139161c4","extraData":"0x65746865726d696e652d657539","gasLimit":"0x7a121d","gasUsed":"0x78bc5f","hash":"0xca38acd4d80c899dd8a97d54ee305236cb828708326c77ebf55f636842f5413e","logsBloom":"0x00000000204040402000000000043002000001000000240004000502502c09088c800010a8942220601002864000000200000000200900010400101000a0800020040040000004580090400c08a462084008400280201000000102002106600004241020005414100543401004000000100000114844000002004016004020003020000000040008000000109080000000000084000004904c000820080602100200110800d041000200801140009028002804030000a10400000000001c10010400001240040000000000000280104201004024000802010a000c09080240010810010020a02040000000801180000d00800110000200000000000202000884","miner":"0xea674fdde714fd979de3edf0f56aa9716b898ec8","mixHash":"0xdedfff15fd3b6b81723e6fa3d98fa2f02babdb18bea65408d76b8684daae6105","nonce":"0x67b6b0802f81eb63","number":"0x53bc29","parentHash":"0x7554fef8dd3a866f7782ecbdd2ec91e8e0880f8ab42a07c4a989b5a77534b8ff","receiptsRoot":"0x5aa537fa4e1d94360f0ea11fec6417bf4790f4b503cab83167a509a73d2bc9a1","sha3Uncles":"0xd234709bc6162311d6f81f1a0a016a7cdbe35fbf987ddf205d163a419599861b","size":"0x4c99","stateRoot":"0x062ef3a3a98a945f39e44498c5e0872bc48aad1c7f70270201fa1b8fe4676141","timestamp":"0x5adce726","totalDifficulty":"0xcd483bc533f29dd69b","transactions":["0x39819abbf672486944a7221114ccf5b69a2186c81d357f6f57b4e5b228eed5d2","0x071c06316476a8df24ea3b7bd60c0b5bc00cea4b802cea3e5a68b098d522186f","0xc45393bab157d4afbdab3c8894d97da23d562b06025691818b50ddf7f4ac1cde","0x216ad0475e17396964f66e488e97e07518b6b485a00c12f6d445fdd7a5216315","0x18110f9cc7870df11c17ace22a4ae42a21152e1b80bffd5e05a024253c18f659","0x0d32bdfc5c0bc7570aaf099a0c2659428d66bcb80149179e5da6638961e2fd8f","0x2306be10396f46a106d2d586a89baff3fd1e485e01dea08b5ba86213dd487530","0xbc00d0d90a7c195010386898635732f1bd97e8c0aacfc021c1f9d7e31f441bf2","0xebf6477b5088d90b9dd2220114de8b389ba6c2852d63afb3f859cfaf9e230cde","0xb04facf69719eca1f48aa2e31b5db120718c143f258e757008833e236179d852","0x02e2df289d6f42c7b550a1bb7f29c78c57a051cb9e808c08cc7f077bd856e4f9","0xfcb254d31380cb5275c89877e9882cc5198d685cbb241f037624ec778eb06665","0xfec2fc07f2601999115e9b0fa5bdda9dc16c45c19197758f04a39b0aad2e088b","0xcaf5404526749bb556abb7b0e1c7df6353e59ab475190c1bb0545f4815ae1aa6","0x0648ec2aee8ce31aca0838356bd7bf4754446ecf9738bbed19469c65105b9485","0xb27b3d032484d0182d227cc79f9381b4aac0e300aed07009062008c132fef9f0","0xc0599d0e3af084994c43ac371978eecd1c30b446fac1114194a534c73bc652e3","0x6dde6111a543d5ab6316fa81bfffcd929ae7066d27325476f52a9b78330b3e27","0xc068dbb8e95aa3b611d6e699b2054f55f05cafe2b13d6893a64c3f1f9e8f14e1","0x3e2e11ad83a268b92a8a667b65128277f72e92d6aeee23da2b91ba6098db319f","0xf26eda92816ab4e7db6f3cfd922fa76ae4433144bb947fccbd75286426cd7b18","0x7bb5933e8bb90b48619514a99c4bded7924be0f0f0cc7f74467c2e07f353c715","0x8605236d239bad050bd704ee3d3f89f0743c37cd92cb586e206e8c18d0804f73","0xdedd3920d6f6fd7f28bf46d87e6e16a7ae5f4ea8793a37a168688e1b27361776","0x4a7d58291de4df0d61f57a980630cdabf804b66d371d2bd08895fb0a407c11de","0x87c9f978435584a1ab4746e68e6ea89352bef67f2fcd9025c2160e036a3f597e","0x9f750cdf9d67ede89e2737fa3f9d0cabd4200379f1dcce735723c8d43a0d781a","0xd64ce24bbbf861ca5d2ca39224572b7c5b6cd803d3366e4318eaaf2c5157a3ae","0xf1e8f1197b9ffa1e4f381f13644232f67311d157a0d4d75df0c4a28b6f3d91d4","0xe52a0cd120d79e61f5812cafdac3194f9b656ae9a28e2b5536e1aa005aa9d724","0x1f0a11bab11625f90bafd1a549ae06408a7dd9f73b5f49e1b58fbc7ff35de4b8","0x2cda44482447335faf4a0224e6960db81c430afcb2c2250aaa909fc4db3cb0ca","0x0658015f179271489274c0f6296d86747ca7a45c660f406f3002cbab51adcf09","0xda5eabc9fa346d9ada0324f5138929757979a675416a07d9c301caf5c0635cc4","0xe7762a9d247d571867aa98be8e1a956ae457385654ad3a4232e409bddd1668d5","0x71b5764e315bd20d44e985a0577f9e201734fe5dc46c9f24fee05f5b5159c495","0x80d869a6098b2bc0cbb8ac85b037225618d0e03c3350ef85e78d3fe4823ddf22","0x8a6d178dbc5813defffcc5ba069ef0a2828b742183e42c1873d514829febcd1e","0xf748d852c2d434adb1b220fa10247b42cdc9e75ea2b06c1270aa61abcb4f965c","0x1eb87c0efa87b6deec9db779261257fc1b162a757a1cd293f2c1499fdacaa23c","0xae603b1581eb986cb9497898183d08dc9ab72144b6f6cb9bc616263622e2b93f","0xc258c3f1c3518c3e7020f77d7379bda1fedd3b9c62b55b71a9cb0d9fc018cadd","0x1cfd7e43911a8d338aea9f94bd8bff9fc303184d75557f992d68f27e9f567b0b","0x2b6f8ff1f26cdb0c4d3a82a4786acfc26f510632bb615e51ae7ac046116df466","0xb1a6ea133e90a52443ae8390631533222f9b43b46c176f09af1ee8de6641c363","0x3ef95dba3220c0753144c2e4041929e9cdb1fa0027e88ff0ea7e0f648ff1e0ff","0xaf76fe4697e62c21a8cdb0993acb615986ae6d3bf59cd31cc4b65175afd00d00","0x5d6a7dde09cc3440d70134d98ea1eb52ba5416cfbf5ac0bb753c198bcb3971dd","0xabf3ecff018d4c14bfc9687d75755e2c2398fd0a1071cfc167bce7521a2fd1b2","0x2da99b537c96342a897ec926a9d1e5b750fde824157d51783d21890dffaa4c61","0xfc9e16c71fcff3021e774f9a7e29a05f4b4bf908be70810bf4f955d6890cc393","0x3d4329a8693d4ed7bfa1c7d0061f0217d5771865359b05614010bb7fad733333","0x33866cd47466fd5734771ea5c5435718dbc614b02b6ae77a0507adf1d5e99761","0x50c1e6fbc9b24dbcc60344d39ebe8b692b4a7e1becd2bb3d67d1d75cd43f86a3","0xd902dcb477c144da1013a10721db79635c6c858cc515555ebb9fc5140d0d9f6d","0x69d89b4b685b4c6e9515abe52e3fa00ed90ccd15e6853741c90816eadb21f002","0x3e755512278c282c4b8772f66158aadc673c9ae20ec523f142774cf73ab05462","0xa4b878098e7978a5aa3b1342c92197d59860ef013d214926778600fed7f9ea65","0x9cef8b10997a4f5f09d4562b59aa15e0b04ca289871eb98166a96dbbaf1c2ae9","0xdad1bda310f1197ae588b8d1dfc66327ff078e92a24cbf7364e8746c1f0a49d5","0xdb0306ed3b81dfc81d4694dae83f9d4be74ad5c23d7d6758e06e64e966afd2c3","0xd4f1e5f3a2d7fb1f3ea89e3547f3df2d81a39d17f13ce7c2a9d2ff531b413da4","0x990a77c322359f944ecf07c51273fd7c1c37faa5954f2e6d854bb40d7a8d8932","0x5e26c8c1a621c3193100c54e9eafdfec798b7c8e07ba244eb919cd4a152d4b52","0x69430180dad1ef75bdc8bba2641f0501a9b50dab1b527ac79597c8a19e81a5d3","0x229d42eb4ba99fcc2faa4925e5408774acbebb365e4c0d59b54eb3c69dfd6e3c","0x01ad01b1943d5b184ab5c2999cbc93889d0a997143f1915e2523b144acd6ed2d","0x84851c3bb7d0d98a35f827428cfa18e8f4ac76467b5e8f45653b6dfd5b42d95c","0xcb9d687c37aa035def32bae091f9643a3f1c71dd124f63aced99d4c0c583b9dc","0x0c892684e1e3a43206d633bbf0b1b2350842e7b353f1bae94da6620eb8c72895","0xf4cd31724ac7fec8fc035e98abf1a05cacf1b1444b604143b8d87f4466feb60c","0x7d6e155ddb392969ce22dd4b7f3ea4f7b94cf23734b264afed12cb9532bb03f7","0xc714fa766d1524992400a8467ee6503b4a96f9119272f4a8976ec351a3c74539","0x18b1b4e3feb42e0c92836577fe449d23b438bc7ed5a003e7ba788026a56d0011","0x94bced8629d15d34205fd121b2b26d5507cbd3937550276fe0c0221ac1515a36","0xec5ca1cb697d1eed0b013e836ebcbf8af7ddaf5750f309879a4ba25bc66008a0","0x7a32bcee583d2455a94fd13e5c9935d75b1a38424eaa47ac64f2abbd14f586d3","0xed7c9ef0f116ee2b005b99887d386786d71272931d91f33378e37a7e81eee78b","0x9467b2eecf52f7337be4041746fe54ea7db6a9eeba433cd555a48e7745a62c47","0xc652695bb720802bce30f320c84c077893356bebab3137f11559cbec3a99c4f8","0x128fa0276dfb1043ab3430f815e1c1dea87f072af5bfa4dd4fc99d38e6ad59c1","0xc8f2b63a9a044679c797f2f4e028623811dbf46bd90209f30fcdb4bd9cff4f59","0x8a889bdd025fb2eef4050146f04486e5d90271e568b93b8b23adbbeda71c62d8","0x83c38d9cc4aace7170d51b38bc0b896e45e8a4aa87f8f0d5e47202c7e290fe63","0x816173d7fac4ec6420b90b05afeb2d18cef6daa5af0c15b0c48a8016ee3126f1","0x2e2615b945add9b2967a990b376be26b903ed78760f6117a6472bc876a05a1c0","0x987ec9692db1b113fbfbd7cca8490a83a6b6cb9eaec7f71f56c823517938bc22","0x2fa8cef7edda824002635ed3cb9567bfe4ca86900f9cab1176633e0866129a86","0xbe8d680c1102d3d654065688557efc94de53c2fe7082642750fe6cb545b5f09c","0xa9f8214fde8ce8d1e134100b5480c52c3c8ef052899d46f9f9ac94e84a2a35f0","0xcb86613e80456e662851fc9116d1a6cbffa5df52c85ec3836287d6a3fdc23ab2","0x1434ed00c57d6ec6d762892175909620387a6677af782f13635fd8efd2869765","0xc45b68555ad25c776feb4fffa60884334e697cb40738218715c803699d2bf814","0x29bb916f644e6858b5d603654bd03919376218ef7292bf157c057899da2bb6b8","0x2d5e8256a1b9ff19a90232b99f1495cba75b85027341e5c8ccdda299119d69fb","0x3262008150f38f2b72ab40058662cf46ca413d01b0d8c1c101a8654fe8404e22","0xf677300fdc3820cb1b7f5aa622346b354a92f6a6e6c6d4cf734f680ee7001b51","0x4d8dd9992247d027e19e8b89334742f3adcd68b77c4afe3b2fe3b52c1a31ada3","0xde007a0b908b015f682f28ed70e66608154dbd3fa8ccd3bcda64ebb9625d861d"],"transactionsRoot":"0xf695ea23497dfc7989cce834952d6c4b4ed590f5fb717ba771f624c665eaef84","uncles":["0x92207ba94f011e20be1228cb7699274b38f9472828217a9956ede2d91ddd03a3"]}
15 | """
16 |
17 |
18 | blockWithTxObjects : String
19 | blockWithTxObjects =
20 | """
21 | """
22 |
--------------------------------------------------------------------------------
/tests/DecodeAbiBeta.elm:
--------------------------------------------------------------------------------
1 | module DecodeAbiBeta exposing (arrayOfBytesData)
2 |
3 | -- import Fuzz exposing (Fuzzer, int, list, string)
4 |
5 | import Abi.Decode as Abi
6 | import BigInt exposing (BigInt)
7 | import Expect
8 | import Test exposing (..)
9 |
10 |
11 |
12 | -- FAILS RIGHT NOW
13 | -- arrayOfString : Test
14 | -- arrayOfString =
15 | -- describe "Array of String Decoding"
16 | -- [ test "decode ComplexStorageBeta.arrayOfStrings()" <|
17 | -- \_ ->
18 | -- Abi.fromString arrayOfStringDecoder arrayOfStringsData
19 | -- |> Expect.equal arrayOfStringExpect
20 | -- ]
21 | -- arrayOfStringDecoder : Abi.AbiDecoder (List String)
22 | -- arrayOfStringDecoder =
23 | -- Abi.dynamicArray Abi.string
24 | -- arrayOfStringExpect : Result String (List String)
25 | -- arrayOfStringExpect =
26 | -- Ok [ "testingthisshouldbequiteabitlongerthan1word", "shorter", "s" ]
27 | -- arrayOfStringsData : String
28 | -- arrayOfStringsData =
29 | -- "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000002b74657374696e677468697373686f756c6462657175697465616269746c6f6e6765727468616e31776f7264000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000773686f727465720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000017300000000000000000000000000000000000000000000000000000000000000"
30 | -- 0000000000000000000000000000000000000000000000000000000000000020 -- 0 - Dyn Array starts at hex 32
31 | -- 0000000000000000000000000000000000000000000000000000000000000003 -- 32 - Dyn Array is 3 long
32 | -- 0000000000000000000000000000000000000000000000000000000000000060 -- 64 - 1st element (string) starts at 96
33 | -- 00000000000000000000000000000000000000000000000000000000000000c0 -- 96 - 2nd element (string) starts at 192
34 | -- 0000000000000000000000000000000000000000000000000000000000000100 -- 128 - 3rd element (string) starts at 256
35 | -- 000000000000000000000000000000000000000000000000000000000000002b -- 160 - 43 hex-length data
36 | -- 74657374696e677468697373686f756c6462657175697465616269746c6f6e6765727468616e31776f7264000000000000000000000000000000000000000000 -- 192 & 224 - 43 char hex string plus padding ("testingthisshouldbequiteabitlongerthan1word")
37 | -- 0000000000000000000000000000000000000000000000000000000000000007 -- 256 - 7 hex-length data
38 | -- 73686f7274657200000000000000000000000000000000000000000000000000 -- 288 - 7 char hex string plus padding ("shorter")
39 | -- 0000000000000000000000000000000000000000000000000000000000000001 -- 318 - 1 hex-length data
40 | -- 7300000000000000000000000000000000000000000000000000000000000000 -- 340 - 1 char hex string plus padding ("s")
41 |
42 |
43 | {--}
44 | ---------------------------------------------------------------------------------------------
45 | -- arrayOfBytes : Test
46 | -- arrayOfBytes =
47 | -- describe "Array of String Decoding"
48 | -- [ test "decode ComplexStorageBeta.arrayOfStrings()" <|
49 | -- \_ ->
50 | -- Abi.fromString arrayOfStringDecoder arrayOfStringsData
51 | -- |> Expect.equal arrayOfStringExpect
52 | -- ]
53 | -- arrayOfBytesDecoder : Abi.AbiDecoder (List String)
54 | -- arrayOfBytesDecoder =
55 | -- Abi.dynamicArray Abi.dynamicBytes
56 | -- arrayOfBytesExpect : Result String (List String)
57 | -- arrayOfBytesExpect =
58 | -- Ok [ "testingthisshouldbequiteabitlongerthan1word", "shorter", "s" ]
59 |
60 |
61 | arrayOfBytesData : String
62 | arrayOfBytesData =
63 | "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000002b74657374696e677468697373686f756c6462657175697465616269746c6f6e6765727468616e31776f7264000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000773686f727465720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000017300000000000000000000000000000000000000000000000000000000000000"
64 |
--------------------------------------------------------------------------------
/tests/EncodeAbi.elm:
--------------------------------------------------------------------------------
1 | module EncodeAbi exposing (encodeInt)
2 |
3 | import Abi.Encode as Abi
4 | import BigInt exposing (BigInt)
5 | import Eth.Utils
6 | import Expect
7 | import Test exposing (..)
8 |
9 |
10 |
11 | -- Abi Encoders
12 |
13 |
14 | encodeInt : Test
15 | encodeInt =
16 | describe "Int Encoding"
17 | [ test "-120" <|
18 | \_ ->
19 | Abi.abiEncode (Abi.int <| BigInt.fromInt -120)
20 | |> Eth.Utils.hexToString
21 | |> Expect.equal "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff88"
22 | , test "120" <|
23 | \_ ->
24 | Abi.abiEncode (Abi.int <| BigInt.fromInt 120)
25 | |> Eth.Utils.hexToString
26 | |> Expect.equal "0x0000000000000000000000000000000000000000000000000000000000000078"
27 | , test "max positive int256" <|
28 | \_ ->
29 | BigInt.fromString "57896044618658097711785492504343953926634992332820282019728792003956564819967"
30 | |> Maybe.map (Abi.int >> Abi.abiEncode >> Eth.Utils.hexToString)
31 | |> Expect.equal (Just "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
32 | , test "max negative int256" <|
33 | \_ ->
34 | BigInt.fromString "-57896044618658097711785492504343953926634992332820282019728792003956564819968"
35 | |> Maybe.map (Abi.int >> Abi.abiEncode >> Eth.Utils.hexToString)
36 | |> Expect.equal (Just "0x8000000000000000000000000000000000000000000000000000000000000000")
37 | ]
38 |
39 |
40 |
41 | -- encodeComplex : Hex
42 | -- encodeComplex =
43 | -- let
44 | -- testAddr =
45 | -- Internal.Address "89d24a6b4ccb1b6faa2625fe562bdd9a23260359"
46 | -- testAddr2 =
47 | -- Internal.Address "c1cc40ccc2441d1e6170cc40a60aa35127cc6e7"
48 | -- testAmount =
49 | -- BigInt.fromString "0xde0b6b3a7640000"
50 | -- |> Maybe.withDefault (BigInt.fromInt 0)
51 | -- zer =
52 | -- (BigInt.fromInt 0)
53 | -- functionSig =
54 | -- EthUtils.functionSig "transfer(address,uint256)"
55 | -- |> EthUtils.hexToString
56 | -- testBytes =
57 | -- functionCall "transfer(address,uint256)" [ address testAddr2, uint testAmount ]
58 | -- in
59 | -- functionCall "propose(address,bytes,uint256)"
60 | -- [ address testAddr, dynamicBytes testBytes, uint zer, dynamicBytes testBytes, dynamicBytes testBytes ]
61 |
--------------------------------------------------------------------------------
/tests/solidity/ComplexStorage.sol:
--------------------------------------------------------------------------------
1 | pragma solidity ^0.5.1;
2 | pragma experimental ABIEncoderV2;
3 |
4 |
5 | contract ComplexStorage {
6 | uint uintVal = 123;
7 | int intVal = -128;
8 | bool boolVal = true;
9 | int224 int224Val = -999999999999999999999999999999999999999999999999999999999999999;
10 | bool[2] boolVectorVal = [true, false];
11 | int[] intListVal = [1, 2, 3, int224Val, -10, 1, 2, 34];
12 | uint[] public uintListVal = [1, 2, 3];
13 | address[] addressList = [address(this), address(0x123123123), address(this)];
14 | string stringVal = "wtf mate";
15 | bytes16 bytes16Val = "1234567890123456";
16 | bytes2 a = 0x1234;
17 | bytes2 b = 0x5678;
18 | bytes2 c = 0xffff;
19 | bytes2[4] bytes2Vector = [a, b, c];
20 | bytes2[4][] bytes2VectorListVal = [bytes2Vector, bytes2Vector, bytes2Vector];
21 | string[] arrayOfString = ["testingthisshouldbequiteabitlongerthan1word", "", "shorter", "s"];
22 | string[][] dynArrayOfDynVal = [["testingthisshouldbequiteabitlongerthan1word"], [""], ["shorter"], ["s"]];
23 | uint[] emptyArray;
24 | string emptyString;
25 | bytes emptyBytes;
26 |
27 | struct StructOne {
28 | bool structBool;
29 | uint[] structUintArray;
30 | }
31 |
32 | struct StructTwo {
33 | address[] structDynArray;
34 | int structInt;
35 | StructOne structOne;
36 | }
37 |
38 | struct StructThree {
39 | uint aaa;
40 | bool bbb;
41 | address ccc;
42 | }
43 |
44 | StructOne public structOne = StructOne(true, uintListVal);
45 | StructTwo structTwo = StructTwo(addressList, -100, structOne);
46 | StructThree public structThree = StructThree(9, true, address(this));
47 |
48 | event ValsSet(uint a, int b, bool c, int224 d, bool[2] e, int[] f, string g, string h, bytes16 i, bytes2[4][] j);
49 |
50 | function setValues(uint _uintVal, int _intVal, bool _boolVal, int224 _int224Val, bool[2] memory _boolVectorVal, int[] memory _intListVal, string memory _stringVal, string memory _emptyString, bytes16 _bytes16Val, bytes2[4][] memory _bytes2VectorListVal) public {
51 | uintVal = _uintVal;
52 | intVal = _intVal;
53 | boolVal = _boolVal;
54 | int224Val = _int224Val;
55 | boolVectorVal = _boolVectorVal;
56 | intListVal = _intListVal;
57 | stringVal = _stringVal;
58 | bytes16Val = _bytes16Val;
59 | bytes2VectorListVal = _bytes2VectorListVal;
60 | emptyString = _emptyString;
61 |
62 | emit ValsSet(_uintVal, _intVal, _boolVal, _int224Val, _boolVectorVal, _intListVal, _stringVal, emptyString, _bytes16Val, _bytes2VectorListVal);
63 | }
64 |
65 | function test1 () public view returns (
66 | uint,
67 | int,
68 | bool,
69 | int224,
70 | bool[2] memory,
71 | int[] memory,
72 | uint[] memory,
73 | string memory,
74 | string memory,
75 | bytes16,
76 | bytes2[4][] memory,
77 | bytes memory
78 | )
79 | {
80 | return (
81 | uintVal,
82 | intVal,
83 | boolVal,
84 | int224Val,
85 | boolVectorVal,
86 | intListVal,
87 | emptyArray,
88 | stringVal,
89 | emptyString,
90 | bytes16Val,
91 | bytes2VectorListVal,
92 | emptyBytes
93 | );
94 | }
95 |
96 |
97 | function test2 () public view returns (
98 | string[][] memory,
99 | string[] memory
100 | )
101 | {
102 | return (
103 | dynArrayOfDynVal,
104 | arrayOfString
105 | );
106 | }
107 |
108 |
109 | function test3 () public view returns (
110 | StructThree memory,
111 | StructOne memory,
112 | StructTwo memory
113 | )
114 | {
115 | return (
116 | structThree,
117 | structOne,
118 | structTwo
119 | );
120 | }
121 | }
122 |
123 |
--------------------------------------------------------------------------------