├── .gitignore ├── LICENSE ├── server ├── Makefile ├── adapters │ ├── blockchains │ │ ├── errors.go │ │ ├── ethereum.go │ │ ├── mock.go │ │ └── mock_test.go │ ├── crypto │ │ ├── crypto.go │ │ └── crypto_test.go │ ├── repositories │ │ ├── mock.go │ │ └── mock_test.go │ ├── servers │ │ ├── download.go │ │ ├── http.go │ │ ├── log.go │ │ └── upload.go │ └── storages │ │ ├── mock.go │ │ └── mock_test.go ├── config │ └── config.go ├── db │ └── schema.sql ├── domain │ ├── entities │ │ └── file.go │ └── repositories │ │ ├── errors.go │ │ └── file.go ├── log │ └── log.go ├── main.go ├── presenters │ ├── http.go │ └── http_test.go ├── usecases │ ├── dto │ │ └── transaction.go │ ├── errors.go │ ├── interactor.go │ ├── ports │ │ ├── blockchain.go │ │ ├── crypto.go │ │ ├── server.go │ │ └── storage.go │ ├── usecases.go │ └── usecases_test.go └── utils │ └── utils.go └── web ├── .babelrc ├── contracts ├── Cris.sol └── Migrations.sol ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── package-lock.json ├── package.json ├── public ├── css │ ├── main.css │ └── riot-mui.css ├── index.html └── js │ ├── riot-mui.js │ └── web3.min.js ├── scripts └── setup.sh ├── src ├── js │ ├── actions │ │ ├── status.js │ │ └── upload.js │ ├── clients │ │ ├── api.js │ │ └── ethereum.js │ ├── index.js │ ├── share.js │ └── stores │ │ └── store.js └── tags │ ├── app.tag │ ├── status.tag │ └── upload.tag ├── truffle.js ├── webpack.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/8edb8a95c4c4b3dce71a378aaaf89275510b9cef/go.gitignore 2 | 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 16 | .glide/ 17 | 18 | 19 | cris 20 | ### https://raw.github.com/github/gitignore/cc542de017c606138a87ee4880e5f06b3a306def/node.gitignore 21 | 22 | # Logs 23 | logs 24 | *.log 25 | npm-debug.log* 26 | 27 | # Runtime data 28 | pids 29 | *.pid 30 | *.seed 31 | 32 | # Directory for instrumented libs generated by jscoverage/JSCover 33 | lib-cov 34 | 35 | # Coverage directory used by tools like istanbul 36 | coverage 37 | 38 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 39 | .grunt 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (http://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules 49 | jspm_packages 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional REPL history 55 | .node_repl_history 56 | 57 | web/public/js/app.js 58 | web/build 59 | web/.truffle-solidity-loader 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 lycoris0731 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /server/Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | .PHONY: test 4 | test: 5 | go test -v -race ./... 6 | 7 | -------------------------------------------------------------------------------- /server/adapters/blockchains/errors.go: -------------------------------------------------------------------------------- 1 | package blockchains 2 | 3 | import "errors" 4 | 5 | type errTxNotFound struct { 6 | error 7 | } 8 | 9 | func (e errTxNotFound) ClientError() bool { 10 | return true 11 | } 12 | 13 | var ( 14 | ErrTxNotFound = errTxNotFound{errors.New("no such transaction")} 15 | ) 16 | -------------------------------------------------------------------------------- /server/adapters/blockchains/ethereum.go: -------------------------------------------------------------------------------- 1 | package blockchains 2 | 3 | import "github.com/ktr0731/cris/server/usecases/dto" 4 | 5 | type EthereumAdapter struct{} 6 | 7 | func (a *EthereumAdapter) FindTxByHash(hash string) (*dto.Transaction, error) { 8 | return nil, nil 9 | } 10 | -------------------------------------------------------------------------------- /server/adapters/blockchains/mock.go: -------------------------------------------------------------------------------- 1 | package blockchains 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "io/ioutil" 7 | 8 | "github.com/ktr0731/cris/server/usecases/dto" 9 | "golang.org/x/sync/syncmap" 10 | ) 11 | 12 | type MockBlockchainAdapter struct { 13 | store syncmap.Map 14 | } 15 | 16 | func (a *MockBlockchainAdapter) FindTxByHash(hash string) (*dto.Transaction, error) { 17 | v, ok := a.store.Load(hash) 18 | if !ok { 19 | return nil, ErrTxNotFound 20 | } 21 | tx, ok := v.(*dto.Transaction) 22 | if !ok { 23 | return nil, errors.New("type assertion failed") 24 | } 25 | return tx, nil 26 | } 27 | 28 | // CreateTx is used to testing only 29 | func (a *MockBlockchainAdapter) CreateTx(hash string, content io.Reader) (*dto.Transaction, error) { 30 | b, err := ioutil.ReadAll(content) 31 | if err != nil { 32 | return nil, err 33 | } 34 | tx := &dto.Transaction{ 35 | HashedData: string(b), 36 | } 37 | a.store.Store(hash, tx) 38 | return tx, nil 39 | } 40 | 41 | func NewMockBlockchain() *MockBlockchainAdapter { 42 | return &MockBlockchainAdapter{} 43 | } 44 | -------------------------------------------------------------------------------- /server/adapters/blockchains/mock_test.go: -------------------------------------------------------------------------------- 1 | package blockchains 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestMockBlockchainAdapter_FindTxByHash(t *testing.T) { 12 | adapter := NewMockBlockchain() 13 | hash := "C204" 14 | content := strings.NewReader("Cristina") 15 | expected, err := adapter.CreateTx(hash, content) 16 | require.NoError(t, err) 17 | actual, err := adapter.FindTxByHash(hash) 18 | require.NoError(t, err) 19 | 20 | assert.Exactly(t, expected, actual) 21 | } 22 | -------------------------------------------------------------------------------- /server/adapters/crypto/crypto.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/sha256" 5 | 6 | "golang.org/x/crypto/ed25519" 7 | ) 8 | 9 | type CryptoAdapter struct{} 10 | 11 | func (c *CryptoAdapter) HashDigest(src []byte) string { 12 | sum := sha256.Sum256(src) 13 | return string(sum[:]) 14 | } 15 | 16 | func (c *CryptoAdapter) Verify(pubkey, msg, signature []byte) bool { 17 | return ed25519.Verify(ed25519.PublicKey(pubkey), msg, signature) 18 | } 19 | 20 | func NewCryptoAdapter() *CryptoAdapter { 21 | return &CryptoAdapter{} 22 | } 23 | -------------------------------------------------------------------------------- /server/adapters/crypto/crypto_test.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/sha256" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestCryptoAdapter_HashDigest(t *testing.T) { 11 | content := []byte("future gadget") 12 | sum := sha256.Sum256(content) 13 | expected := string(sum[:]) 14 | 15 | assert.Equal(t, expected, NewCryptoAdapter().HashDigest(content)) 16 | } 17 | -------------------------------------------------------------------------------- /server/adapters/repositories/mock.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "github.com/ktr0731/cris/server/config" 5 | "github.com/ktr0731/cris/server/domain/entities" 6 | "github.com/ktr0731/cris/server/domain/repositories" 7 | "github.com/ktr0731/cris/server/log" 8 | "golang.org/x/sync/syncmap" 9 | ) 10 | 11 | type MockFileRepositoryAdapter struct { 12 | logger *log.Logger 13 | config *config.Config 14 | 15 | storage syncmap.Map 16 | } 17 | 18 | func NewMockFileRepository(logger *log.Logger, config *config.Config) *MockFileRepositoryAdapter { 19 | return &MockFileRepositoryAdapter{ 20 | logger: logger, 21 | config: config, 22 | } 23 | } 24 | 25 | func (r *MockFileRepositoryAdapter) Store(e *entities.File) (entities.FileID, error) { 26 | r.storage.Store(e.ID, e) 27 | r.logger.Printf("[MockFileRepo] stored an entity: %s", e.ID) 28 | return e.ID, nil 29 | } 30 | 31 | func (r *MockFileRepositoryAdapter) Find(id entities.FileID) (*entities.File, error) { 32 | v, ok := r.storage.Load(id) 33 | if !ok { 34 | return nil, repositories.ErrNotFound 35 | } 36 | e, ok := v.(*entities.File) 37 | if !ok { 38 | return nil, repositories.ErrNotFound 39 | } 40 | return e, nil 41 | } 42 | 43 | func (r *MockFileRepositoryAdapter) Remove(id entities.FileID) (*entities.File, error) { 44 | e, err := r.Find(id) 45 | if err != nil { 46 | return nil, err 47 | } 48 | r.storage.Delete(id) 49 | r.logger.Printf("[MockFileRepo] remove an entity: %s", e.ID) 50 | return e, nil 51 | } 52 | -------------------------------------------------------------------------------- /server/adapters/repositories/mock_test.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ktr0731/cris/server/domain/entities" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestMockFileRepositoryAdapter_Store(t *testing.T) { 12 | repository := NewMockFileRepository(nil, nil) 13 | expected := entities.NewFile("url") 14 | _, err := repository.Store(expected) 15 | require.NoError(t, err) 16 | v, ok := repository.storage.Load(expected.ID) 17 | require.True(t, ok) 18 | actual, ok := v.(*entities.File) 19 | require.True(t, ok) 20 | assert.Equal(t, expected, actual) 21 | } 22 | 23 | func TestMockFileRepositoryAdapter_Find(t *testing.T) { 24 | repository := NewMockFileRepository(nil, nil) 25 | expected := entities.NewFile("url") 26 | _, err := repository.Store(expected) 27 | require.NoError(t, err) 28 | actual, err := repository.Find(expected.ID) 29 | require.NoError(t, err) 30 | assert.Equal(t, expected, actual) 31 | } 32 | 33 | func TestMockFileRepositoryAdapter_Remove(t *testing.T) { 34 | repository := NewMockFileRepository(nil, nil) 35 | expected := entities.NewFile("url") 36 | _, err := repository.Store(expected) 37 | require.NoError(t, err) 38 | _, err = repository.Remove(expected.ID) 39 | require.NoError(t, err) 40 | } 41 | -------------------------------------------------------------------------------- /server/adapters/servers/download.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/ktr0731/cris/server/config" 9 | "github.com/ktr0731/cris/server/domain/entities" 10 | "github.com/ktr0731/cris/server/log" 11 | "github.com/ktr0731/cris/server/usecases/ports" 12 | ) 13 | 14 | type DownloadFileHandler struct { 15 | BaseFileHandler 16 | } 17 | 18 | func newDownloadFileHandler(config *config.Config, inputPort ports.ServerInputPort) http.Handler { 19 | logger, err := log.NewLogger(config) 20 | if err != nil { 21 | panic(err) 22 | } 23 | return withLogging(config, logger, &DownloadFileHandler{ 24 | newBaseFileHandler(logger, inputPort), 25 | }) 26 | } 27 | 28 | func (h *DownloadFileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 29 | if r.Method != http.MethodGet { 30 | w.WriteHeader(http.StatusMethodNotAllowed) 31 | return 32 | } 33 | 34 | p := r.URL.Path 35 | 36 | // allowed format: /v1/files/token.hash 37 | sp := strings.Split(p, "/") 38 | if len(sp) != 4 { 39 | w.WriteHeader(http.StatusBadRequest) 40 | return 41 | } 42 | 43 | res, err := h.inputPort.DownloadFile(&ports.DownloadFileParams{ 44 | Token: entities.FileID(sp[3]), 45 | }) 46 | 47 | if err != nil { 48 | h.logger.Printf("[ERR] %s", err) 49 | handleError(w, err) 50 | return 51 | } 52 | 53 | w.Header().Set("Content-Length", fmt.Sprintf("%d", len(res.Content))) 54 | if _, err := w.Write(res.Content); err != nil { 55 | w.WriteHeader(http.StatusInternalServerError) 56 | fmt.Fprintln(w, err) 57 | return 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /server/adapters/servers/http.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/ktr0731/cris/server/config" 8 | "github.com/ktr0731/cris/server/log" 9 | "github.com/ktr0731/cris/server/usecases" 10 | "github.com/ktr0731/cris/server/usecases/ports" 11 | "github.com/rs/cors" 12 | ) 13 | 14 | type Server struct { 15 | logger *log.Logger 16 | config *config.Config 17 | 18 | mux *http.ServeMux 19 | } 20 | 21 | func NewHTTPServer(logger *log.Logger, config *config.Config, inputPort ports.ServerInputPort) *Server { 22 | s := Server{ 23 | logger: logger, 24 | config: config, 25 | mux: http.NewServeMux(), 26 | } 27 | s.mux.Handle(s.getPrefix()+"/files", newUploadFileHandler(config, inputPort)) 28 | s.mux.Handle(s.getPrefix()+"/files/", newDownloadFileHandler(config, inputPort)) 29 | return &s 30 | } 31 | 32 | func (s *Server) Listen() error { 33 | s.logger.Printf("Server listen in %s%s", s.getPrefix(), s.getAddr()) 34 | return http.ListenAndServe(s.getAddr(), cors.Default().Handler(s.mux)) 35 | } 36 | 37 | func (s *Server) getPrefix() string { 38 | return fmt.Sprintf("/%s", s.config.Meta.Version) 39 | } 40 | 41 | func (s *Server) getAddr() string { 42 | return fmt.Sprintf("%s:%s", s.config.Server.Host, s.config.Server.Port) 43 | } 44 | 45 | type BaseFileHandler struct { 46 | logger *log.Logger 47 | 48 | inputPort ports.ServerInputPort 49 | } 50 | 51 | func newBaseFileHandler(logger *log.Logger, inputPort ports.ServerInputPort) BaseFileHandler { 52 | return BaseFileHandler{ 53 | logger: logger, 54 | inputPort: inputPort, 55 | } 56 | } 57 | 58 | func handleError(w http.ResponseWriter, err error) { 59 | if _, ok := err.(usecases.ClientError); ok { 60 | w.WriteHeader(http.StatusBadRequest) 61 | } else { 62 | w.WriteHeader(http.StatusInternalServerError) 63 | } 64 | fmt.Fprintln(w, err) 65 | } 66 | -------------------------------------------------------------------------------- /server/adapters/servers/log.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/ktr0731/cris/server/config" 7 | "github.com/ktr0731/cris/server/log" 8 | ) 9 | 10 | type logHandler struct { 11 | logger *log.Logger 12 | handler http.Handler 13 | } 14 | 15 | func (h *logHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 16 | h.logger.Printf("%s %s", r.Method, r.URL.Path) 17 | h.handler.ServeHTTP(w, r) 18 | } 19 | 20 | func withLogging(config *config.Config, logger *log.Logger, h http.Handler) http.Handler { 21 | return &logHandler{ 22 | logger: logger, 23 | handler: h, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server/adapters/servers/upload.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | 12 | "github.com/ktr0731/cris/server/config" 13 | "github.com/ktr0731/cris/server/log" 14 | "github.com/ktr0731/cris/server/usecases/ports" 15 | ) 16 | 17 | type UploadFileHandler struct { 18 | BaseFileHandler 19 | } 20 | 21 | func newUploadFileHandler(config *config.Config, inputPort ports.ServerInputPort) http.Handler { 22 | logger, err := log.NewLogger(config) 23 | if err != nil { 24 | panic(err) 25 | } 26 | return withLogging(config, logger, &UploadFileHandler{ 27 | newBaseFileHandler(logger, inputPort), 28 | }) 29 | } 30 | 31 | func (h *UploadFileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 32 | defer r.Body.Close() 33 | 34 | if r.Method != http.MethodPost { 35 | w.WriteHeader(http.StatusMethodNotAllowed) 36 | return 37 | } 38 | 39 | params := struct { 40 | Content string `json:"content"` 41 | Signature string `json:"signature"` 42 | Pubkey string `json:"pubkey"` 43 | }{} 44 | 45 | // 10 MB 46 | reader := io.LimitReader(r.Body, 10000) 47 | 48 | if err := json.NewDecoder(reader).Decode(¶ms); err != nil { 49 | h.logger.Printf("[ERR] %s", err) 50 | handleError(w, err) 51 | return 52 | } 53 | 54 | content, pubkey, signature := make([]byte, base64.StdEncoding.DecodedLen(len(params.Content))), make([]byte, base64.StdEncoding.DecodedLen(len(params.Pubkey))), make([]byte, base64.StdEncoding.DecodedLen(len(params.Signature))) 55 | var err error 56 | var n int 57 | n, err = base64.StdEncoding.Decode(content, []byte(params.Content)) 58 | if err != nil { 59 | h.logger.Printf("[ERR] %s", err) 60 | handleError(w, err) 61 | return 62 | } 63 | content = content[:n] 64 | 65 | n, err = base64.StdEncoding.Decode(pubkey, []byte(params.Pubkey)) 66 | if err != nil { 67 | h.logger.Printf("[ERR] %s", err) 68 | handleError(w, err) 69 | return 70 | } 71 | pubkey = pubkey[:n] 72 | 73 | n, err = base64.StdEncoding.Decode(signature, []byte(params.Signature)) 74 | if err != nil { 75 | h.logger.Printf("[ERR] %s", err) 76 | handleError(w, err) 77 | return 78 | } 79 | signature = signature[:n] 80 | 81 | fmt.Printf("%x, %x", pubkey, sha256.Sum256(content)) 82 | 83 | res, err := h.inputPort.UploadFile(&ports.UploadFileParams{ 84 | Content: bytes.NewBuffer(content), 85 | Signature: signature, 86 | PubKey: pubkey, 87 | }) 88 | 89 | if err != nil { 90 | h.logger.Printf("[ERR] %s", err) 91 | handleError(w, err) 92 | return 93 | } 94 | 95 | if err := json.NewEncoder(w).Encode(res); err != nil { 96 | w.WriteHeader(http.StatusInternalServerError) 97 | fmt.Fprintln(w, err) 98 | return 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /server/adapters/storages/mock.go: -------------------------------------------------------------------------------- 1 | package storages 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | 10 | "github.com/ktr0731/cris/server/domain/repositories" 11 | "github.com/ktr0731/cris/server/usecases" 12 | "github.com/ktr0731/cris/server/utils" 13 | 14 | "golang.org/x/sync/syncmap" 15 | ) 16 | 17 | type MockStorageAdapter struct { 18 | storage syncmap.Map 19 | url syncmap.Map 20 | } 21 | 22 | func (s *MockStorageAdapter) Upload(name string, content io.Reader) (string, error) { 23 | // name is used to only file name 24 | // actual key url is generated by object storage 25 | b, err := ioutil.ReadAll(content) 26 | if err != nil { 27 | return "", err 28 | } 29 | if len(b) == 0 { 30 | return "", usecases.ErrEmptyContent 31 | } 32 | 33 | s.storage.Store(name, b) 34 | 35 | url := fmt.Sprintf("https://example.com/%s", utils.NewUUID()) 36 | s.url.Store(url, name) 37 | return url, nil 38 | } 39 | 40 | func (s *MockStorageAdapter) Download(url string) (io.ReadCloser, error) { 41 | v, ok := s.url.Load(url) 42 | if !ok { 43 | return nil, repositories.ErrNotFound 44 | } 45 | url, ok = v.(string) 46 | if !ok { 47 | return nil, errors.New("type assertion failed") 48 | } 49 | 50 | v, ok = s.storage.Load(url) 51 | if !ok { 52 | return nil, repositories.ErrNotFound 53 | } 54 | r, ok := v.([]byte) 55 | if !ok { 56 | return nil, errors.New("type assertion failed") 57 | } 58 | return ioutil.NopCloser(bytes.NewBuffer(r)), nil 59 | } 60 | 61 | func NewMockStorage() *MockStorageAdapter { 62 | return &MockStorageAdapter{} 63 | } 64 | -------------------------------------------------------------------------------- /server/adapters/storages/mock_test.go: -------------------------------------------------------------------------------- 1 | package storages 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/ktr0731/cris/server/usecases" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestMockStorageAdapter_Upload(t *testing.T) { 15 | t.Run("normal", func(t *testing.T) { 16 | storage := NewMockStorage() 17 | key := "kurisu" 18 | expected := strings.NewReader("kurisu makise") 19 | _, err := storage.Upload(key, expected) 20 | require.NoError(t, err) 21 | v, ok := storage.storage.Load(key) 22 | require.True(t, ok) 23 | actual, ok := v.(*strings.Reader) 24 | require.True(t, ok) 25 | assert.Equal(t, expected, actual) 26 | }) 27 | 28 | t.Run("empty content", func(t *testing.T) { 29 | storage := NewMockStorage() 30 | key := "sern" 31 | expected := strings.NewReader("") 32 | _, err := storage.Upload(key, expected) 33 | assert.Equal(t, usecases.ErrEmptyContent, err) 34 | }) 35 | } 36 | 37 | func TestMockStorageAdapter_Download(t *testing.T) { 38 | storage := NewMockStorage() 39 | key := "kurisu" 40 | expected := strings.NewReader("kurisu makise") 41 | url, err := storage.Upload(key, expected) 42 | v, err := storage.Download(url) 43 | require.NoError(t, err) 44 | actual, ok := v.(io.Reader) 45 | require.True(t, ok) 46 | assert.Equal(t, ioutil.NopCloser(expected), actual) 47 | } 48 | -------------------------------------------------------------------------------- /server/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/kelseyhightower/envconfig" 4 | 5 | type Config struct { 6 | Meta *Meta 7 | Server *Server 8 | Logger *Logger 9 | } 10 | 11 | type Meta struct { 12 | Version string `default:"v1"` 13 | } 14 | 15 | type Server struct { 16 | Host string `default:""` 17 | Port string `default:"8080"` 18 | } 19 | 20 | type Logger struct { 21 | Output string `default:"stdout"` 22 | Prefix string `default:"[cris] "` 23 | } 24 | 25 | var config Config 26 | 27 | func init() { 28 | err := envconfig.Process("cris", &config) 29 | if err != nil { 30 | panic(err) 31 | } 32 | } 33 | 34 | func Get() *Config { 35 | return &config 36 | } 37 | -------------------------------------------------------------------------------- /server/db/schema.sql: -------------------------------------------------------------------------------- 1 | DROP DATABASE IF EXISTS cris; 2 | CREATE DATABASE cris CHARACTER SET utf8mb4; 3 | 4 | USE cris; 5 | 6 | CREATE TABLE urls ( 7 | uuid CHAR(64) PRIMARY KEY, 8 | url VARCHAR(255) NOT NULL UNIQUE 9 | ) ENGINE=InnoDB; 10 | -------------------------------------------------------------------------------- /server/domain/entities/file.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import "github.com/ktr0731/cris/server/utils" 4 | 5 | type FileID string 6 | 7 | type File struct { 8 | ID FileID 9 | URL string 10 | } 11 | 12 | func NewFile(url string) *File { 13 | return &File{ 14 | ID: FileID(utils.NewUUID()), 15 | URL: url, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server/domain/repositories/errors.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNotFound = errors.New("specified entity not found") 7 | ) 8 | -------------------------------------------------------------------------------- /server/domain/repositories/file.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import "github.com/ktr0731/cris/server/domain/entities" 4 | 5 | type FileRepository interface { 6 | Store(*entities.File) (entities.FileID, error) 7 | Find(entities.FileID) (*entities.File, error) 8 | Remove(entities.FileID) (*entities.File, error) 9 | } 10 | -------------------------------------------------------------------------------- /server/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | "github.com/ktr0731/cris/server/config" 10 | ) 11 | 12 | type Logger struct { 13 | *log.Logger 14 | out io.Writer 15 | } 16 | 17 | var logger *Logger 18 | 19 | func NewLogger(config *config.Config) (*Logger, error) { 20 | if logger != nil { 21 | return logger, nil 22 | } 23 | 24 | var out io.Writer 25 | switch config.Logger.Output { 26 | case "stdout": 27 | out = os.Stdout 28 | case "stderr": 29 | out = os.Stderr 30 | case "file": 31 | f, err := os.Create(string(time.Now().Unix())) 32 | if err != nil { 33 | return nil, err 34 | } 35 | out = f 36 | } 37 | return &Logger{ 38 | log.New(out, config.Logger.Prefix, log.Lshortfile|log.Ltime), 39 | out, 40 | }, nil 41 | } 42 | 43 | func (l *Logger) Close() error { 44 | if f, ok := l.out.(io.Closer); ok { 45 | return f.Close() 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/ktr0731/cris/server/adapters/blockchains" 5 | "github.com/ktr0731/cris/server/adapters/crypto" 6 | "github.com/ktr0731/cris/server/adapters/repositories" 7 | "github.com/ktr0731/cris/server/adapters/servers" 8 | "github.com/ktr0731/cris/server/adapters/storages" 9 | "github.com/ktr0731/cris/server/config" 10 | "github.com/ktr0731/cris/server/log" 11 | "github.com/ktr0731/cris/server/presenters" 12 | "github.com/ktr0731/cris/server/usecases" 13 | ) 14 | 15 | func main() { 16 | config := config.Get() 17 | logger, err := log.NewLogger(config) 18 | if err != nil { 19 | panic(err) 20 | } 21 | defer logger.Close() 22 | 23 | interactor := usecases.NewInteractor( 24 | logger, 25 | config, 26 | presenters.NewHTTPPresenter(logger, config), 27 | storages.NewMockStorage(), 28 | blockchains.NewMockBlockchain(), 29 | crypto.NewCryptoAdapter(), 30 | repositories.NewMockFileRepository(logger, config), 31 | ) 32 | 33 | if err := servers.NewHTTPServer(logger, config, interactor).Listen(); err != nil { 34 | panic(err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /server/presenters/http.go: -------------------------------------------------------------------------------- 1 | package presenters 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | 7 | "github.com/ktr0731/cris/server/config" 8 | "github.com/ktr0731/cris/server/domain/entities" 9 | "github.com/ktr0731/cris/server/log" 10 | "github.com/ktr0731/cris/server/usecases/ports" 11 | ) 12 | 13 | type HTTPPresenter struct { 14 | logger *log.Logger 15 | config *config.Config 16 | } 17 | 18 | func NewHTTPPresenter(logger *log.Logger, config *config.Config) *HTTPPresenter { 19 | return &HTTPPresenter{ 20 | logger: logger, 21 | config: config, 22 | } 23 | } 24 | 25 | func (p *HTTPPresenter) UploadFile(token entities.FileID) (*ports.UploadFileResponse, error) { 26 | return &ports.UploadFileResponse{ 27 | Token: string(token), 28 | }, nil 29 | } 30 | 31 | func (p *HTTPPresenter) DownloadFile(content io.Reader) (*ports.DownloadFileResponse, error) { 32 | res := &ports.DownloadFileResponse{} 33 | var err error 34 | res.Content, err = ioutil.ReadAll(content) 35 | return res, err 36 | } 37 | -------------------------------------------------------------------------------- /server/presenters/http_test.go: -------------------------------------------------------------------------------- 1 | package presenters 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/ktr0731/cris/server/config" 8 | "github.com/ktr0731/cris/server/domain/entities" 9 | "github.com/ktr0731/cris/server/log" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestHTTPPresenter_UploadFile(t *testing.T) { 15 | presenter := NewHTTPPresenter(&log.Logger{}, &config.Config{}) 16 | 17 | expectedToken := entities.FileID("token") 18 | res, err := presenter.UploadFile(expectedToken) 19 | require.NoError(t, err) 20 | assert.Equal(t, res.Token, string(expectedToken)) 21 | } 22 | 23 | func TestHTTPPresenter_DownloadFile(t *testing.T) { 24 | presenter := NewHTTPPresenter(&log.Logger{}, &config.Config{}) 25 | 26 | expected := []byte("foo") 27 | r := bytes.NewReader(expected) 28 | res, err := presenter.DownloadFile(r) 29 | require.NoError(t, err) 30 | assert.Equal(t, res.Content, expected) 31 | } 32 | -------------------------------------------------------------------------------- /server/usecases/dto/transaction.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type Transaction struct { 4 | HashedData string 5 | } 6 | -------------------------------------------------------------------------------- /server/usecases/errors.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import "errors" 4 | 5 | type ClientError interface { 6 | IsClientError() bool 7 | } 8 | 9 | type errEmptyContent struct{ error } 10 | 11 | func (e errEmptyContent) IsClientError() bool { return true } 12 | 13 | type errTemperingDetected struct{ error } 14 | 15 | func (e errTemperingDetected) IsClientError() bool { return true } 16 | 17 | var ( 18 | ErrEmptyContent = errEmptyContent{errors.New("received content length is zero")} 19 | ErrTemperingDetected = errTemperingDetected{errors.New("tempering of the resource was detected")} 20 | ) 21 | -------------------------------------------------------------------------------- /server/usecases/interactor.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "github.com/ktr0731/cris/server/config" 5 | "github.com/ktr0731/cris/server/domain/repositories" 6 | "github.com/ktr0731/cris/server/log" 7 | "github.com/ktr0731/cris/server/usecases/ports" 8 | ) 9 | 10 | // interactor はすべての依存を解決する役割を持つ 11 | type Interactor struct { 12 | container *container 13 | 14 | outputPort ports.ServerOutputPort 15 | storagePort ports.StoragePort 16 | cryptoPort ports.CryptoPort 17 | fileRepository repositories.FileRepository 18 | } 19 | 20 | func (i *Interactor) UploadFile(params *ports.UploadFileParams) (*ports.UploadFileResponse, error) { 21 | return i.container.uploadFile(params, i.outputPort, i.storagePort, i.cryptoPort, i.fileRepository) 22 | } 23 | 24 | // DownloadFile downloads a file specified by the token 25 | func (i *Interactor) DownloadFile(params *ports.DownloadFileParams) (*ports.DownloadFileResponse, error) { 26 | return i.container.downloadFile(params, i.outputPort, i.storagePort, nil, i.cryptoPort, i.fileRepository) 27 | } 28 | 29 | func NewInteractor( 30 | logger *log.Logger, 31 | config *config.Config, 32 | outputPort ports.ServerOutputPort, 33 | storagePort ports.StoragePort, 34 | blockchainPort ports.BlockchainPort, 35 | cryptoPort ports.CryptoPort, 36 | fileRepository repositories.FileRepository, 37 | ) *Interactor { 38 | return &Interactor{ 39 | container: newUsecaseContainer(logger, config), 40 | 41 | outputPort: outputPort, 42 | storagePort: storagePort, 43 | cryptoPort: cryptoPort, 44 | fileRepository: fileRepository, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /server/usecases/ports/blockchain.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import "github.com/ktr0731/cris/server/usecases/dto" 4 | 5 | type BlockchainPort interface { 6 | FindTxByHash(string) (*dto.Transaction, error) 7 | } 8 | -------------------------------------------------------------------------------- /server/usecases/ports/crypto.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | type CryptoPort interface { 4 | HashDigest(src []byte) string 5 | Verify(pubkey, msg, signature []byte) bool 6 | } 7 | -------------------------------------------------------------------------------- /server/usecases/ports/server.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/ktr0731/cris/server/domain/entities" 7 | ) 8 | 9 | type ServerInputPort interface { 10 | UploadFile(*UploadFileParams) (*UploadFileResponse, error) 11 | DownloadFile(*DownloadFileParams) (*DownloadFileResponse, error) 12 | } 13 | 14 | type UploadFileParams struct { 15 | Content io.Reader 16 | Signature []byte 17 | PubKey []byte 18 | } 19 | 20 | type DownloadFileParams struct { 21 | Token entities.FileID 22 | TxHash string 23 | } 24 | 25 | type ServerOutputPort interface { 26 | UploadFile(token entities.FileID) (*UploadFileResponse, error) 27 | DownloadFile(content io.Reader) (*DownloadFileResponse, error) 28 | } 29 | 30 | type UploadFileResponse struct { 31 | Token string `json:"token"` 32 | } 33 | 34 | type DownloadFileResponse struct { 35 | Content []byte `json:"content"` 36 | } 37 | -------------------------------------------------------------------------------- /server/usecases/ports/storage.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import "io" 4 | 5 | type StoragePort interface { 6 | Upload(name string, content io.Reader) (string, error) 7 | Download(name string) (io.ReadCloser, error) 8 | } 9 | -------------------------------------------------------------------------------- /server/usecases/usecases.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | 7 | "github.com/ktr0731/cris/server/config" 8 | "github.com/ktr0731/cris/server/domain/entities" 9 | "github.com/ktr0731/cris/server/domain/repositories" 10 | "github.com/ktr0731/cris/server/log" 11 | "github.com/ktr0731/cris/server/usecases/ports" 12 | "github.com/ktr0731/cris/server/utils" 13 | ) 14 | 15 | type container struct { 16 | config *config.Config 17 | logger *log.Logger 18 | } 19 | 20 | // uploadFile uploads received file to an object storage 21 | func (c *container) uploadFile( 22 | params *ports.UploadFileParams, 23 | outputPort ports.ServerOutputPort, 24 | storagePort ports.StoragePort, 25 | cryptoPort ports.CryptoPort, 26 | fileRepository repositories.FileRepository, 27 | ) (*ports.UploadFileResponse, error) { 28 | // 雑 29 | content, err := ioutil.ReadAll(params.Content) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | if !cryptoPort.Verify(params.PubKey, content, params.Signature) { 35 | return nil, ErrTemperingDetected 36 | } 37 | 38 | url, err := storagePort.Upload(utils.NewUUID(), bytes.NewBuffer(content)) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | file := entities.NewFile(url) 44 | _, err = fileRepository.Store(file) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | return outputPort.UploadFile(file.ID) 50 | } 51 | 52 | func (c *container) downloadFile( 53 | params *ports.DownloadFileParams, 54 | outputPort ports.ServerOutputPort, 55 | storagePort ports.StoragePort, 56 | blockchainPort ports.BlockchainPort, 57 | cryptoPort ports.CryptoPort, 58 | fileRepository repositories.FileRepository, 59 | ) (*ports.DownloadFileResponse, error) { 60 | // tx, err := blockchainPort.FindTxByHash(params.TxHash) 61 | // if err != nil { 62 | // return nil, err 63 | // } 64 | 65 | file, err := fileRepository.Find(params.Token) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | res, err := storagePort.Download(file.URL) 71 | if err != nil { 72 | return nil, err 73 | } 74 | defer res.Close() 75 | 76 | // _, err := ioutil.ReadAll(res) 77 | // if err != nil { 78 | // return nil, err 79 | // } 80 | 81 | // if cryptoPort.HashDigest(b) != tx.HashedData { 82 | // return nil, ErrTemperingDetected 83 | // } 84 | 85 | return outputPort.DownloadFile(res) 86 | } 87 | 88 | func newUsecaseContainer(logger *log.Logger, config *config.Config) *container { 89 | return &container{ 90 | logger: logger, 91 | config: config, 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /server/usecases/usecases_test.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/ktr0731/cris/server/adapters/blockchains" 10 | "github.com/ktr0731/cris/server/adapters/repositories" 11 | "github.com/ktr0731/cris/server/domain/entities" 12 | "github.com/ktr0731/cris/server/usecases/ports" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | type dummyOutputAdapter struct { 18 | ports.ServerOutputPort 19 | t *testing.T 20 | } 21 | 22 | func (a *dummyOutputAdapter) UploadFile(token entities.FileID) (*ports.UploadFileResponse, error) { 23 | return &ports.UploadFileResponse{Token: string(token)}, nil 24 | } 25 | 26 | func (a *dummyOutputAdapter) DownloadFile(content io.Reader) (*ports.DownloadFileResponse, error) { 27 | b, err := ioutil.ReadAll(content) 28 | require.NoError(a.t, err) 29 | return &ports.DownloadFileResponse{Content: b}, nil 30 | } 31 | 32 | type dummyStorage struct{} 33 | 34 | func (s *dummyStorage) Upload(name string, content io.Reader) (string, error) { 35 | return "echelon", nil 36 | } 37 | 38 | func (s *dummyStorage) Download(url string) (io.ReadCloser, error) { 39 | return ioutil.NopCloser(strings.NewReader("kurisu")), nil 40 | } 41 | 42 | type dummyCryptoAdapter struct{} 43 | 44 | func (a *dummyCryptoAdapter) HashDigest(src []byte) string { 45 | return string(src) 46 | } 47 | 48 | func Test_container_uploadFile(t *testing.T) { 49 | c := newUsecaseContainer(nil, nil) 50 | output := &dummyOutputAdapter{t: t} 51 | storage := &dummyStorage{} 52 | repo := repositories.NewMockFileRepository(nil, nil) 53 | 54 | t.Run("normal", func(t *testing.T) { 55 | input := &ports.UploadFileParams{} 56 | res, err := c.uploadFile(input, output, storage, repo) 57 | require.NoError(t, err) 58 | expected, err := repo.Find(entities.FileID(res.Token)) 59 | require.NoError(t, err) 60 | assert.Equal(t, expected.URL, "echelon") 61 | }) 62 | } 63 | 64 | func Test_container_downloadFile(t *testing.T) { 65 | c := newUsecaseContainer(nil, nil) 66 | output := &dummyOutputAdapter{t: t} 67 | storage := &dummyStorage{} 68 | bc := blockchains.NewMockBlockchain() 69 | repo := repositories.NewMockFileRepository(nil, nil) 70 | 71 | t.Run("no such transaction", func(t *testing.T) { 72 | input := &ports.DownloadFileParams{} 73 | _, err := c.downloadFile(input, nil, nil, bc, nil, nil) 74 | assert.Equal(t, blockchains.ErrTxNotFound, err) 75 | }) 76 | 77 | t.Run("normal", func(t *testing.T) { 78 | input := &ports.DownloadFileParams{Token: "hououin", TxHash: "makise"} 79 | crypto := &dummyCryptoAdapter{} 80 | 81 | // setup 82 | _, err := repo.Store(&entities.File{ID: entities.FileID("hououin")}) 83 | require.NoError(t, err) 84 | _, err = bc.CreateTx("makise", strings.NewReader("kurisu")) 85 | require.NoError(t, err) 86 | 87 | // check 88 | _, err = c.downloadFile(input, output, storage, bc, crypto, repo) 89 | require.NoError(t, err) 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /server/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strings" 5 | 6 | uuid "github.com/satori/go.uuid" 7 | ) 8 | 9 | func NewUUID() string { 10 | uuid := uuid.NewV4().String() 11 | return strings.Replace(uuid, "-", "", 4) 12 | } 13 | -------------------------------------------------------------------------------- /web/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-es3-member-expression-literals"] 3 | } 4 | -------------------------------------------------------------------------------- /web/contracts/Cris.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.4; 2 | 3 | contract Cris { 4 | string public version = '0.1.0'; 5 | 6 | address public admin; 7 | 8 | // mapping (string => address) fileOwner; 9 | mapping (string => bool) hasFile; 10 | 11 | function Cris() public { 12 | admin = msg.sender; 13 | } 14 | 15 | function store(string fileHash) { 16 | // fileOwner[hash] = msg.sender; 17 | hasFile[fileHash] = true; 18 | } 19 | 20 | // function isOwner(address sender, string hash) public returns (bool) { 21 | // if (sender == fileOwner[hash]) { 22 | // return true; 23 | // } 24 | // return false; 25 | // } 26 | 27 | function has(string fileHash) constant returns (bool retVal) { 28 | return hasFile[fileHash]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /web/contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.0; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | modifier restricted() { 8 | if (msg.sender == owner) _; 9 | } 10 | 11 | function Migrations() { 12 | owner = msg.sender; 13 | } 14 | 15 | function setCompleted(uint completed) restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web/migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | module.exports = function(deployer) { 2 | deployer.deploy(Migrations); 3 | }; 4 | -------------------------------------------------------------------------------- /web/migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | module.exports = function(deployer) { 2 | deployer.deploy(Cris); 3 | }; 4 | -------------------------------------------------------------------------------- /web/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "babel-plugin-transform-es3-member-expression-literals": { 8 | "version": "6.22.0", 9 | "resolved": "https://registry.npmjs.org/babel-plugin-transform-es3-member-expression-literals/-/babel-plugin-transform-es3-member-expression-literals-6.22.0.tgz", 10 | "integrity": "sha1-cz00RPPsxBvvjtGmpOCWV7iWnrs=", 11 | "dev": true, 12 | "requires": { 13 | "babel-runtime": "6.26.0" 14 | } 15 | }, 16 | "babel-runtime": { 17 | "version": "6.26.0", 18 | "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", 19 | "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", 20 | "dev": true, 21 | "requires": { 22 | "core-js": "2.5.1", 23 | "regenerator-runtime": "0.11.0" 24 | } 25 | }, 26 | "core-js": { 27 | "version": "2.5.1", 28 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.1.tgz", 29 | "integrity": "sha1-rmh03GaTd4m4B1T/VCjfZoGcpQs=", 30 | "dev": true 31 | }, 32 | "regenerator-runtime": { 33 | "version": "0.11.0", 34 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz", 35 | "integrity": "sha512-/aA0kLeRb5N9K0d4fw7ooEbI+xDe+DKD499EQqygGqeS8N3xto15p09uY2xj7ixP81sNPXvRLnAQIqdVStgb1A==", 36 | "dev": true 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "setup": "./scripts/setup.sh", 8 | "start": "webpack-dev-server" 9 | }, 10 | "dependencies": { 11 | "arraybuffer-to-buffer": "^0.0.4", 12 | "arraybuffer-to-string": "^1.0.1", 13 | "babel-core": "^6.26.0", 14 | "babel-loader": "^7.1.2", 15 | "babel-preset-es2015-riot": "^1.1.0", 16 | "clipboard": "^1.7.1", 17 | "js-base64": "^2.4.0", 18 | "js-sha256": "^0.8.0", 19 | "password-generator": "^2.2.0", 20 | "riot": "^3.7.4", 21 | "riot-mui": "^0.1.1", 22 | "riot-tag-loader": "^1.0.0", 23 | "supercop.js": "^2.0.1", 24 | "truffle-solidity-loader": "^0.0.8", 25 | "web3": "^0.17.0-alpha", 26 | "webpack": "^3.10.0", 27 | "webpack-dev-server": "^2.9.5" 28 | }, 29 | "devDependencies": { 30 | "babel-plugin-transform-es3-member-expression-literals": "^6.22.0", 31 | "prettier": "1.9.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /web/public/css/main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | width: 100%; 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | body { 9 | background: #fafafa; 10 | } 11 | 12 | a { 13 | text-decoration: none; 14 | } 15 | 16 | app { 17 | height: 100%; 18 | width: 100%; 19 | } 20 | 21 | #container { 22 | height: 100%; 23 | width: 700px; 24 | margin: 0 auto; 25 | } 26 | 27 | .uploader-wrapper p { 28 | display: block; 29 | text-align: center; 30 | vertical-align: center; 31 | padding: 100px 20px; 32 | border: solid 1px #ccc; 33 | background: #efefef; 34 | } 35 | -------------------------------------------------------------------------------- /web/public/css/riot-mui.css: -------------------------------------------------------------------------------- 1 | material-button{border:none;border-radius:2px;display:inline-block;position:relative;height:40px;line-height:36px;background:#61bdcc;color:#fff;padding:0 2rem;cursor:pointer;text-transform:uppercase;vertical-align:middle;outline:0;-webkit-tap-highlight-color:transparent}material-button:hover material-waves{background:hsla(0,0%,100%,.2)}material-button:hover material-waves,material-button material-waves{-webkit-transition:background .2s ease-in;-ms-transition:background .2s ease-in;-moz-transition:background .2s ease-in;-o-transition:background .2s ease-in;transition:background .2s ease-in}material-button material-waves{background:hsla(0,0%,100%,0)}material-button .content{width:101%;height:100%;display:block;text-align:center}material-button .content .text,material-button .content a,material-button .content i.icon,material-button .content i.material-icons{display:inline-block;vertical-align:middle;font-size:18px;color:#fff;line-height:40px}material-button .content .text.material-icons,material-button .content a.material-icons,material-button .content i.icon.material-icons,material-button .content i.material-icons.material-icons{font-size:20px}material-button .content .text svg,material-button .content a svg,material-button .content i.icon svg,material-button .content i.material-icons svg{fill:#fff;stroke:#fff}material-button[rounded=true]{border-radius:50%;width:40px;padding:0}material-button[rounded=true] .content{width:100%;height:100%;display:flex;align-items:center;text-align:center}material-button[rounded=true] .content i.icon,material-button[rounded=true] .content i.material-icons{display:inline-block;text-align:center;width:100%;height:100%;-webkit-user-select:none;-ms-user-select:none;-moz-user-select:none;-o-user-select:none;user-select:none}material-button[shady=true]{-webkit-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-ms-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-moz-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-o-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);transition:box-shadow .2s}material-button[shady=true]:hover{-webkit-box-shadow:0 2px 7px 0 rgba(0,0,0,.14),0 2px 12px 0 rgba(0,0,0,.12);-ms-box-shadow:0 2px 7px 0 rgba(0,0,0,.14),0 2px 12px 0 rgba(0,0,0,.12);-moz-box-shadow:0 2px 7px 0 rgba(0,0,0,.14),0 2px 12px 0 rgba(0,0,0,.12);-o-box-shadow:0 2px 7px 0 rgba(0,0,0,.14),0 2px 12px 0 rgba(0,0,0,.12);box-shadow:0 2px 7px 0 rgba(0,0,0,.14),0 2px 12px 0 rgba(0,0,0,.12);transition:box-shadow .2s}material-button[disabled=true]{background:#ccc;color:#999;cursor:default}material-button[disabled=true] #content .text,material-button[disabled=true] #content a,material-button[disabled=true] #content i.icon,material-button[disabled=true] #content i.material-icons{color:#999}material-button[disabled=true] #content .text svg,material-button[disabled=true] #content a svg,material-button[disabled=true] #content i.icon svg,material-button[disabled=true] #content i.material-icons svg{fill:#999;stroke:#999}material-button[disabled=true]:hover material-waves{background:transparent}material-card{display:block;background-color:#fff;margin:0;overflow-y:auto;will-change:width,height;-webkit-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-ms-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-moz-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-o-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);transform:translateY(0);opacity:1;-webkit-transition:transform .2s ease-in,opacity .2s;-ms-transition:transform .2s ease-in,opacity .2s;-moz-transition:transform .2s ease-in,opacity .2s;-o-transition:transform .2s ease-in,opacity .2s;transition:transform .2s ease-in,opacity .2s}material-card .title{padding:20px 10px;font-size:22px;color:#25313b;-webkit-box-shadow:0 2px 3px 0 rgba(0,0,0,.08),0 2px 7px 0 rgba(0,0,0,.02);-ms-box-shadow:0 2px 3px 0 rgba(0,0,0,.08),0 2px 7px 0 rgba(0,0,0,.02);-moz-box-shadow:0 2px 3px 0 rgba(0,0,0,.08),0 2px 7px 0 rgba(0,0,0,.02);-o-box-shadow:0 2px 3px 0 rgba(0,0,0,.08),0 2px 7px 0 rgba(0,0,0,.02);box-shadow:0 2px 3px 0 rgba(0,0,0,.08),0 2px 7px 0 rgba(0,0,0,.02)}material-card .material-card-content{padding:20px}material-checkbox{display:block;-webkit-transform:translateZ(0);-ms-transform:translateZ(0);-moz-transform:translateZ(0);-o-transform:translateZ(0);transform:translateZ(0)}material-checkbox,material-checkbox .checkbox{background-color:transparent;position:relative}material-checkbox .checkbox{display:inline-block;box-sizing:border-box;height:100%;border:2px solid;border-color:#25313b;border-radius:2px;width:18px;height:18px;cursor:pointer;vertical-align:middle;-webkit-transition:background-color .14s,border-color .14s;-ms-transition:background-color .14s,border-color .14s;-moz-transition:background-color .14s,border-color .14s;-o-transition:background-color .14s,border-color .14s;transition:background-color .14s,border-color .14s}material-checkbox .checkbox .checkmark{-webkit-transform:rotate(0deg) scale(.5);-ms-transform:rotate(0deg) scale(.5);-moz-transform:rotate(0deg) scale(.5);-o-transform:rotate(0deg) scale(.5);transform:rotate(0deg) scale(.5);position:absolute;top:-1px;left:3px;width:6px;height:10px;border-style:solid;border-top:none;border-left:none;border-right-width:2px;border-bottom-width:2px;border-color:transparent;cursor:pointer}material-checkbox .checkbox.checked{background-color:#25313b}material-checkbox .checkbox.checked,material-checkbox .checkbox.checked .checkmark{-webkit-transition:background-color .14s,border-color .14s,transform .14s cubic-bezier(.23,1,.32,1) 50ms;-ms-transition:background-color .14s,border-color .14s,transform .14s 50ms cubic-bezier(.23,1,.32,1);-moz-transition:background-color .14s,border-color .14s,transform .14s 50ms cubic-bezier(.23,1,.32,1);-o-transition:background-color .14s,border-color .14s,transform .14s 50ms cubic-bezier(.23,1,.32,1);transition:background-color .14s,border-color .14s,transform .14s cubic-bezier(.23,1,.32,1) 50ms}material-checkbox .checkbox.checked .checkmark{border-color:#fff;-webkit-transform:rotate(45deg) scale(1);-ms-transform:rotate(45deg) scale(1);-moz-transform:rotate(45deg) scale(1);-o-transform:rotate(45deg) scale(1);transform:rotate(45deg) scale(1)}material-checkbox .label{color:#25313b;position:relative;display:inline-block;vertical-align:middle;padding-left:8px;white-space:normal;cursor:pointer}material-checkbox[disabled=true] .checkbox{border-color:#ccc}material-checkbox[disabled=true] .label{color:#ccc}material-combo{display:block;position:relative}material-combo [hidden]{display:none}material-combo material-input input{color:#2f6975}material-dropdown{position:absolute;z-index:100;width:100%}material-dropdown .dropdown{background-color:#fff;margin:0;min-width:100px;max-height:650px;overflow-y:auto;will-change:width,height;-webkit-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-ms-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-moz-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-o-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);transform:translateY(0);opacity:1;-webkit-transition:transform .2s ease-in,opacity .2s;-ms-transition:transform .2s ease-in,opacity .2s;-moz-transition:transform .2s ease-in,opacity .2s;-o-transition:transform .2s ease-in,opacity .2s;transition:transform .2s ease-in,opacity .2s}material-dropdown .dropdown.opening.top{transform:translateY(-50px)}material-dropdown .dropdown.opening.bottom,material-dropdown .dropdown.opening.top{opacity:0;-webkit-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;-ms-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;-moz-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;-o-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s}material-dropdown .dropdown.opening.bottom{transform:translateY(50px)}material-dropdown-list{position:absolute;z-index:100;width:100%}material-dropdown-list ul.dropdown-content{list-style:none;z-index:100;background-color:#fff;margin:0;padding:0;min-width:100px;max-height:650px;overflow-y:auto;will-change:width,height;-webkit-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-ms-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-moz-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-o-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);transform:translateY(0);opacity:1}material-dropdown-list ul.dropdown-content,material-dropdown-list ul.dropdown-content.opening{-webkit-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;-ms-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;-moz-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;-o-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s}material-dropdown-list ul.dropdown-content.opening{transform:translateY(-50px);opacity:0}material-dropdown-list ul.dropdown-content li{clear:both;cursor:pointer;line-height:1.5rem;width:100%;text-align:left;text-transform:none;background-color:#fff;-webkit-transition:background-color .2s ease-in;-ms-transition:background-color .2s ease-in;-moz-transition:background-color .2s ease-in;-o-transition:background-color .2s ease-in;transition:background-color .2s ease-in}material-dropdown-list ul.dropdown-content li a,material-dropdown-list ul.dropdown-content li span{font-size:1.2rem;color:#25313b;display:block;padding:1rem}material-dropdown-list ul.dropdown-content li:hover{background-color:#ededed;-webkit-transition:background-color .2s ease-out;-ms-transition:background-color .2s ease-out;-moz-transition:background-color .2s ease-out;-o-transition:background-color .2s ease-out;transition:background-color .2s ease-out}material-dropdown-list ul.dropdown-content li a{text-decoration:none;color:#25313b}material-dropdown-list ul.dropdown-content li.selected{background-color:#394b5a;-webkit-transition:background-color .2s ease-out;-ms-transition:background-color .2s ease-out;-moz-transition:background-color .2s ease-out;-o-transition:background-color .2s ease-out;transition:background-color .2s ease-out}material-dropdown-list ul.dropdown-content li.selected span{color:#fff}.material-dropdown-list-overlay{z-index:99;width:100%;height:100%;top:0;left:0;position:fixed;background:transparent}material-input{display:block;padding:8px 0;position:relative}material-input .label-placeholder{height:15px;width:100%}material-input .input-content{font-size:16px;color:#17242e;position:relative}material-input .input-content label{position:absolute;top:0;right:0;left:0;font:inherit;color:#2f6975;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;font-size:16px;font-weight:400;line-height:24px;-webkit-transform:none;-ms-transform:none;-moz-transform:none;-o-transform:none;transform:none;-webkit-transform-origin:left top;-ms-transform-origin:left top;-moz-transform-origin:left top;-o-transform-origin:left top;transform-origin:left top}material-input .input-content.not-empty label,material-input .input-content label{-webkit-transition:transform .2s;-ms-transition:transform .2s;-moz-transition:transform .2s;-o-transition:transform .2s;transition:transform .2s}material-input .input-content.not-empty label{-webkit-transform:translate3d(0,-70%,0) scale(.7);-ms-transform:translate3d(0,-70%,0) scale(.7);-moz-transform:translate3d(0,-70%,0) scale(.7);-o-transform:translate3d(0,-70%,0) scale(.7);transform:translate3d(0,-70%,0) scale(.7);-webkit-transform-origin:left top;-ms-transform-origin:left top;-moz-transform-origin:left top;-o-transform-origin:left top;transform-origin:left top}material-input .input-content input{position:relative;outline:none;box-shadow:none;padding:0;width:100%;background:transparent;border:none;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;font-weight:400;line-height:24px;height:24px}material-input .input-content .iconWrapper{display:inline-block;position:absolute;top:0;left:0;bottom:0;right:0;width:40px;height:40px;margin-left:-33px;left:100%;margin-top:-7px}material-input .input-content .iconWrapper material-button{background:transparent}material-input .input-content .iconWrapper material-button .content .material-icons{color:#2f6975}material-input .underline{position:relative;display:block}material-input .underline .unfocused-line{height:1px;background:#2f6975}material-input .underline .focused-line{height:2px;background:#2f6975;-webkit-transform:scaleX(0);-ms-transform:scaleX(0);-moz-transform:scaleX(0);-o-transform:scaleX(0);transform:scaleX(0);-webkit-transition:transform .2s ease-in;-ms-transition:transform .2s ease-in;-moz-transition:transform .2s ease-in;-o-transition:transform .2s ease-in;transition:transform .2s ease-in}material-input .underline.focused .focused-line{-webkit-transform:none;-ms-transform:none;-moz-transform:none;-o-transform:none;transform:none;-webkit-transition:transform .2s ease-out;-ms-transition:transform .2s ease-out;-moz-transition:transform .2s ease-out;-o-transition:transform .2s ease-out;transition:transform .2s ease-out}material-input .underline.error .focused-line,material-input .underline.error .unfocused-line{background:#941212;-webkit-transition:background .2s ease-out;-ms-transition:background .2s ease-out;-moz-transition:background .2s ease-out;-o-transition:background .2s ease-out;transition:background .2s ease-out}material-input[disabled=true] label{color:#ccc}material-input[disabled=true] .underline .unfocused-line{background:#ccc}material-navbar{display:block;color:#fff;-webkit-box-shadow:0 8px 17px 0 rgba(0,0,0,.12),0 6px 20px 0 rgba(0,0,0,.14);-ms-box-shadow:0 8px 17px 0 rgba(0,0,0,.12),0 6px 20px 0 rgba(0,0,0,.14);-moz-box-shadow:0 8px 17px 0 rgba(0,0,0,.12),0 6px 20px 0 rgba(0,0,0,.14);-o-box-shadow:0 8px 17px 0 rgba(0,0,0,.12),0 6px 20px 0 rgba(0,0,0,.14);box-shadow:0 8px 17px 0 rgba(0,0,0,.12),0 6px 20px 0 rgba(0,0,0,.14);background-color:#25313b;width:100%;height:70px;line-height:70px}material-navbar:not(material-input){line-height:0}material-navbar a{color:#fff}material-navbar .nav-wrapper{position:relative;height:100%}material-navbar .nav-wrapper .logo{line-height:66px}material-navbar[fixed]{position:relative;height:70px;z-index:998}material-pane{display:block}material-pane material-navbar .nav-wrapper{display:flex;justify-content:space-between;flex-grow:1}material-pane material-navbar .nav-wrapper .material-pane-title{font-size:26px;text-transform:uppercase;text-align:center}material-pane material-navbar .nav-wrapper material-button{background:transparent}material-pane .content .material-pane-content{padding:10px;background:#f2f2f2}material-popup .popup{z-index:100;position:fixed;left:0;top:25%;right:0;width:50%;min-width:300px;margin:auto;height:-webkit-fit-content;height:-ms-fit-content;height:-moz-fit-content;height:-o-fit-content;height:fit-content;background-color:#fff;overflow-y:auto;will-change:width,height;-webkit-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-ms-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-moz-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-o-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);transform:translateY(0);opacity:1;-webkit-transition:transform .2s ease-in,opacity .2s;-ms-transition:transform .2s ease-in,opacity .2s;-moz-transition:transform .2s ease-in,opacity .2s;-o-transition:transform .2s ease-in,opacity .2s;transition:transform .2s ease-in,opacity .2s}material-popup .popup.opening.top{transform:translateY(-50px)}material-popup .popup.opening.bottom,material-popup .popup.opening.top{opacity:0;-webkit-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;-ms-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;-moz-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;-o-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s}material-popup .popup.opening.bottom{transform:translateY(50px)}material-popup .popup>.content{padding:10px;color:#25313b}material-popup .popup>.content .material-popup-title{padding:24px 24px 0;margin:0;font-size:2.5rem}material-popup .popup>.content .material-popup-content{padding:20px 24px 24px;margin:0}material-popup .popup>.content .material-popup-action{padding:8px 8px 8px 24px;margin:0;display:flex;flex-direction:row-reverse;flex-wrap:wrap}material-popup .popup>.content .material-popup-action>*{margin-right:8px}material-popup .popup>.content .material-popup-action>:first-child{margin-right:0}material-popup .overlay{z-index:99;width:100%;height:100%;top:0;left:0;position:fixed;background:rgba(0,0,0,.2)}material-snackbar{display:block;position:fixed;bottom:10%;right:7%;z-index:1001}material-snackbar .toast{-webkit-border-radius:3px;-ms-border-radius:3px;-moz-border-radius:3px;-o-border-radius:3px;border-radius:3px;top:0;width:auto;clear:both;margin-top:10px;position:relative;height:40px;line-height:40px;background-color:#25313b;padding:0 25px;font-size:1.1rem;font-weight:300;color:#fff;z-index:1001;-webkit-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-ms-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-moz-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-o-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-flex-align:center;-ms-flex-align:center;-webkit-align-items:center;align-items:center;-webkit-justify-content:space-between;-ms-justify-content:space-between;-moz-justify-content:space-between;-o-justify-content:space-between;justify-content:space-between;transform:translateY(0);opacity:1;-webkit-transition:transform .2s ease-in,opacity .2s;-ms-transition:transform .2s ease-in,opacity .2s;-moz-transition:transform .2s ease-in,opacity .2s;-o-transition:transform .2s ease-in,opacity .2s;transition:transform .2s ease-in,opacity .2s}material-snackbar .toast.opening{transform:translateY(-15px);opacity:0;-webkit-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;-ms-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;-moz-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;-o-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s}material-snackbar .toast.error{background-color:#ab173e}material-spinner{display:inline-block}material-spinner .loader-circular{-webkit-animation:loader-rotate 2s linear infinite;-ms-animation:loader-rotate 2s linear infinite;-moz-animation:loader-rotate 2s linear infinite;-o-animation:loader-rotate 2s linear infinite;animation:loader-rotate 2s linear infinite}material-spinner .loader-path{stroke-dasharray:1,200;stroke-dashoffset:0;-webkit-animation:dash 1.5s ease-in-out infinite,color 6s ease-in-out infinite;animation:dash 1.5s ease-in-out infinite,color 6s ease-in-out infinite;stroke-linecap:round;stroke:#db652d}@-o-keyframes loader-rotate{0%{-o-transform:rotate(0deg)}to{-o-transform:rotate(1turn)}}@-moz-keyframes loader-rotate{0%{-moz-transform:rotate(0deg)}to{-moz-transform:rotate(1turn)}}@-webkit-keyframes loader-rotate{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(1turn)}}@keyframes loader-rotate{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@-webkit-keyframes dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:89,200;stroke-dashoffset:-35}to{stroke-dasharray:89,200;stroke-dashoffset:-124}}@keyframes dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:89,200;stroke-dashoffset:-35}to{stroke-dasharray:89,200;stroke-dashoffset:-124}}@-webkit-keyframes color{0%{stroke:#3f88f8}20%{stroke:#3f88f8}25%{stroke:#dd413b}45%{stroke:#dd413b}50%{stroke:#f6ae2e}70%{stroke:#f6ae2e}75%{stroke:#259a5d}95%{stroke:#259a5d}}@keyframes color{0%{stroke:#3f88f8}20%{stroke:#3f88f8}25%{stroke:#dd413b}45%{stroke:#dd413b}50%{stroke:#f6ae2e}70%{stroke:#f6ae2e}75%{stroke:#259a5d}95%{stroke:#259a5d}}material-tabs{display:inline-block;line-height:0;position:relative;width:100%}material-tabs material-button{background:#070731;border-radius:0;text-align:center;padding:0}material-tabs material-button .content{position:relative}material-tabs material-button .content .text{font-size:16px}material-tabs material-button.selected .content .text{color:#61bdcc;-webkit-transition:color .3s;-ms-transition:color .3s;-moz-transition:color .3s;-o-transition:color .3s;transition:color .3s}material-tabs .line-wrapper{width:100%;height:3px;background:#c2c7b6;position:relative}material-tabs .line-wrapper .line{height:100%;background:#61bdcc;position:absolute;-webkit-transition:left .4s;-ms-transition:left .4s;-moz-transition:left .4s;-o-transition:left .4s;transition:left .4s}material-textarea{display:block;padding:8px 0}material-textarea .label-placeholder{height:15px;width:100%}material-textarea .textarea-content{font-size:16px;color:#17242e;position:relative}material-textarea .textarea-content label{position:absolute;top:0;right:0;left:0;font:inherit;color:#2f6975;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;font-size:16px;font-weight:400;line-height:24px;-webkit-transform:none;-ms-transform:none;-moz-transform:none;-o-transform:none;transform:none;-webkit-transform-origin:left top;-ms-transform-origin:left top;-moz-transform-origin:left top;-o-transform-origin:left top;transform-origin:left top}material-textarea .textarea-content.not-empty label,material-textarea .textarea-content label{-webkit-transition:transform .2s;-ms-transition:transform .2s;-moz-transition:transform .2s;-o-transition:transform .2s;transition:transform .2s}material-textarea .textarea-content.not-empty label{-webkit-transform:translate3d(0,-70%,0) scale(.7);-ms-transform:translate3d(0,-70%,0) scale(.7);-moz-transform:translate3d(0,-70%,0) scale(.7);-o-transform:translate3d(0,-70%,0) scale(.7);transform:translate3d(0,-70%,0) scale(.7);-webkit-transform-origin:left top;-ms-transform-origin:left top;-moz-transform-origin:left top;-o-transform-origin:left top;transform-origin:left top}material-textarea .textarea-content .textarea-container{position:absolute;top:0;right:0;left:0;width:100%;height:100%;overflow:hidden}material-textarea .textarea-content .textarea-container textarea{position:relative;box-shadow:none;padding:0;height:100%;background:transparent;border:none;resize:none;overflow:hidden}material-textarea .textarea-content .mirror,material-textarea .textarea-content .textarea-container textarea{outline:none;width:100%;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;font-weight:400;line-height:24px}material-textarea .textarea-content .mirror{visibility:hidden;word-wrap:break-word;height:auto;min-height:24px}material-textarea .underline{position:relative;display:block}material-textarea .underline .unfocused-line{height:1px;background:#2f6975}material-textarea .underline .focused-line{height:2px;background:#2f6975;-webkit-transform:scaleX(0);-ms-transform:scaleX(0);-moz-transform:scaleX(0);-o-transform:scaleX(0);transform:scaleX(0);-webkit-transition:transform .2s ease-in;-ms-transition:transform .2s ease-in;-moz-transition:transform .2s ease-in;-o-transition:transform .2s ease-in;transition:transform .2s ease-in}material-textarea .underline.focused .focused-line{-webkit-transform:none;-ms-transform:none;-moz-transform:none;-o-transform:none;transform:none;-webkit-transition:transform .2s ease-out;-ms-transition:transform .2s ease-out;-moz-transition:transform .2s ease-out;-o-transition:transform .2s ease-out;transition:transform .2s ease-out}material-textarea .underline.error .focused-line,material-textarea .underline.error .unfocused-line{background:#941212;-webkit-transition:background .2s ease-out;-ms-transition:background .2s ease-out;-moz-transition:background .2s ease-out;-o-transition:background .2s ease-out;transition:background .2s ease-out}material-textarea[disabled=true] label{color:#ccc}material-textarea[disabled=true] .underline .unfocused-line{background:#ccc}material-waves{position:absolute;left:0;top:0}material-waves,material-waves #waves{display:block;width:100%;height:100%;overflow:hidden}material-waves #waves{position:relative;-webkit-user-select:none;-ms-user-select:none;-moz-user-select:none;-o-user-select:none;user-select:none;vertical-align:middle;-webkit-transform:rotate(0deg);z-index:1}material-waves #waves .wave{position:absolute;-webkit-border-radius:50%;-ms-border-radius:50%;-moz-border-radius:50%;-o-border-radius:50%;border-radius:50%;width:20px;height:20px;margin-top:-10px;margin-left:-10px;z-index:0;opacity:0;-webkit-transform:scale(0);-ms-transform:scale(0);-moz-transform:scale(0);-o-transform:scale(0);transform:scale(0);-webkit-transition-property:transform,opacity;-ms-transition-property:transform,opacity;-moz-transition-property:transform,opacity;-o-transition-property:transform,opacity;transition-property:transform,opacity;pointer-events:none}material-waves[rounded=true],material-waves[rounded=true] #waves{border-radius:50%}material-footer{display:block;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:16px;color:#999;background-color:#555}material-footer a{color:inherit;text-decoration:none}material-footer ul{list-style:none;margin:0;padding:0}material-footer .material-footer-sections{display:-moz-flex;display:-webkit-flex;display:-ms-flexbox;display:flex;-moz-flex-flow:wrap;-webkit-flex-flow:wrap;flex-flow:wrap;padding-bottom:24px}material-footer .material-footer-sections ul{margin-right:16px}material-footer .mini-footer{display:block}@media screen and (min-width:760px){material-footer .mini-footer{display:inline-flex}}material-footer .material-footer-logo,material-footer .material-footer-section-header{margin-bottom:16px;color:#fff;line-height:36px}@media screen and (min-width:760px){material-footer .material-footer-logo{margin-bottom:0;margin-right:16px}}material-footer ul.material-footer-link-list{display:-moz-flex;display:-webkit-flex;display:-ms-flexbox;display:flex}material-footer ul.material-footer-link-list li{margin-bottom:0;margin-right:16px}@media screen and (min-width:760px){material-footer ul.material-footer-link-list li{line-height:36px}} -------------------------------------------------------------------------------- /web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |ETH : { address }
8 | 9 |Uploaded files:
3 |6 | 7 | { name } 8 | | 9 |{ txHash.slice(0, 31) } | 10 |