├── .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 | # [![elm-ethereum](https://cmdit.ch/images/elm-ethereum-logo.svg)](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 | 2 | 3 | 4 | 5 | 6 | 7 | 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 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------