diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..fe9cd3122 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +.cache +.dockerignore +.gitignore +.git +.env +.pylintrc +__pycache__ +*.pyc +*.egg-info +.idea/ + diff --git a/.env_local b/.env_local new file mode 100644 index 000000000..83b209804 --- /dev/null +++ b/.env_local @@ -0,0 +1,13 @@ +PYTHONPATH=/app/safe_transaction_history +C_FORCE_ROOT=true +DEBUG=0 +USE_DOCKER=False +DJANGO_SETTINGS_MODULE=config.settings.test +DJANGO_SECRET_KEY=test-secret#-!key +DATABASE_URL=psql://postgres@localhost:5432/postgres +REDIS_URL=redis://localhost/0 +CELERY_BROKER_URL=redis://localhost/0 +ETHEREUM_NODE_URL=http://localhost:8545 +SAFE_PERSONAL_CONTRACT_ADDRESS=0xEc7C75C1548765AB51A165873b0B1b71663c1266 +SAFE_FUNDER_PRIVATE_KEY=4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d +SAFE_TX_SENDER_PRIVATE_KEY=6cbed15c793ce57650b9877cf6fa156fbef513c4e6134f022a85b1ffdd59b2a1 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..176a458f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitignore b/.gitignore index 894a44cc0..4ad64672f 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,5 @@ venv.bak/ # mypy .mypy_cache/ + +.idea/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..0b56aef46 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,44 @@ +language: python +cache: pip +python: + - "3.6" +dist: trusty +env: + global: + - DOCKERHUB_PROJECT=safe-transaction-history + - SOURCE_FOLDER=safe_transaction_history + - PIP_USE_MIRRORS=true + - DJANGO_SETTINGS_MODULE=config.settings.test + - DATABASE_URL=psql://postgres@localhost/travisci + - ETHEREUM_NODE_URL=http://localhost:8545 +services: + - docker + - postgresql + - redis-server +install: + - travis_retry pip install -r requirements-test.txt + - travis_retry pip install coveralls + - npm install -g ganache-cli +before_script: + - psql -c 'create database travisci;' -U postgres +script: + - ganache-cli -d -p 8545 --defaultBalanceEther 10000 -a 30 > /dev/null & + - sleep 5 + - coverage run --source=$SOURCE_FOLDER -m py.test +deploy: + - provider: script + script: bash scripts/deploy_docker.sh staging + on: + branch: master + - provider: script + script: bash scripts/deploy_docker.sh develop + on: + branch: develop + - provider: script + script: bash scripts/deploy_docker.sh $TRAVIS_TAG + on: + tags: true + branch: master + +after_success: + - coveralls diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt new file mode 100644 index 000000000..c4f59daf0 --- /dev/null +++ b/CONTRIBUTORS.txt @@ -0,0 +1 @@ +Gnosis diff --git a/Docs.mdj b/Docs.mdj new file mode 100644 index 000000000..036b46d49 --- /dev/null +++ b/Docs.mdj @@ -0,0 +1,5723 @@ +{ + "_type": "Project", + "_id": "AAAAAAFF+h6SjaM2Hec=", + "name": "Untitled", + "ownedElements": [ + { + "_type": "UMLCollaboration", + "_id": "AAAAAAFjYxrX1s9jJVs=", + "_parent": { + "$ref": "AAAAAAFF+h6SjaM2Hec=" + }, + "name": "Collaboration1", + "ownedElements": [ + { + "_type": "UMLInteraction", + "_id": "AAAAAAFjYxrX189kdgE=", + "_parent": { + "$ref": "AAAAAAFjYxrX1s9jJVs=" + }, + "name": "Interaction1", + "ownedElements": [ + { + "_type": "UMLSequenceDiagram", + "_id": "AAAAAAFjYxrX189lB8s=", + "_parent": { + "$ref": "AAAAAAFjYxrX189kdgE=" + }, + "name": "SafeCreation", + "ownedViews": [ + { + "_type": "UMLFrameView", + "_id": "AAAAAAFjYxrX189mYdI=", + "_parent": { + "$ref": "AAAAAAFjYxrX189lB8s=" + }, + "model": { + "$ref": "AAAAAAFjYxrX189lB8s=" + }, + "subViews": [ + { + "_type": "LabelView", + "_id": "AAAAAAFjYxrX189nUL4=", + "_parent": { + "$ref": "AAAAAAFjYxrX189mYdI=" + }, + "font": "Arial;13;0", + "left": 75.97900390625, + "top": 10, + "width": 76.8798828125, + "height": 13, + "text": "SafeCreation" + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjYxrX2M9oZUQ=", + "_parent": { + "$ref": "AAAAAAFjYxrX189mYdI=" + }, + "font": "Arial;13;1", + "left": 10, + "top": 10, + "width": 60.97900390625, + "height": 13, + "text": "interaction" + } + ], + "font": "Arial;13;0", + "left": 5, + "top": 5, + "width": 772, + "height": 508, + "nameLabel": { + "$ref": "AAAAAAFjYxrX189nUL4=" + }, + "frameTypeLabel": { + "$ref": "AAAAAAFjYxrX2M9oZUQ=" + } + }, + { + "_type": "UMLSeqLifelineView", + "_id": "AAAAAAFjY3yrqc906ec=", + "_parent": { + "$ref": "AAAAAAFjYxrX189lB8s=" + }, + "model": { + "$ref": "AAAAAAFjY3yrp89z1WE=" + }, + "subViews": [ + { + "_type": "UMLNameCompartmentView", + "_id": "AAAAAAFjY3yrqc917QU=", + "_parent": { + "$ref": "AAAAAAFjY3yrqc906ec=" + }, + "model": { + "$ref": "AAAAAAFjY3yrp89z1WE=" + }, + "subViews": [ + { + "_type": "LabelView", + "_id": "AAAAAAFjY3yrqs92cxw=", + "_parent": { + "$ref": "AAAAAAFjY3yrqc917QU=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -16, + "height": 13 + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjY3yrqs93o7I=", + "_parent": { + "$ref": "AAAAAAFjY3yrqc917QU=" + }, + "font": "Arial;13;1", + "left": 45, + "top": 47, + "width": 63, + "height": 13, + "text": "Kraken" + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjY3yrqs94hIo=", + "_parent": { + "$ref": "AAAAAAFjY3yrqc917QU=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -16, + "width": 106.20263671875, + "height": 13, + "text": "(from Interaction1)" + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjY3yrqs95q+4=", + "_parent": { + "$ref": "AAAAAAFjY3yrqc917QU=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -16, + "height": 13, + "horizontalAlignment": 1 + } + ], + "font": "Arial;13;0", + "left": 40, + "top": 40, + "width": 73, + "height": 40, + "stereotypeLabel": { + "$ref": "AAAAAAFjY3yrqs92cxw=" + }, + "nameLabel": { + "$ref": "AAAAAAFjY3yrqs93o7I=" + }, + "namespaceLabel": { + "$ref": "AAAAAAFjY3yrqs94hIo=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjY3yrqs95q+4=" + } + }, + { + "_type": "UMLLinePartView", + "_id": "AAAAAAFjY3yrq896TCI=", + "_parent": { + "$ref": "AAAAAAFjY3yrqc906ec=" + }, + "model": { + "$ref": "AAAAAAFjY3yrp89z1WE=" + }, + "font": "Arial;13;0", + "left": 77, + "top": 80, + "width": 1, + "height": 254 + } + ], + "font": "Arial;13;0", + "left": 40, + "top": 40, + "width": 73, + "height": 294, + "nameCompartment": { + "$ref": "AAAAAAFjY3yrqc917QU=" + }, + "linePart": { + "$ref": "AAAAAAFjY3yrq896TCI=" + } + }, + { + "_type": "UMLSeqLifelineView", + "_id": "AAAAAAFjY3y+i8+U/Xo=", + "_parent": { + "$ref": "AAAAAAFjYxrX189lB8s=" + }, + "model": { + "$ref": "AAAAAAFjY3y+is+TqfA=" + }, + "subViews": [ + { + "_type": "UMLNameCompartmentView", + "_id": "AAAAAAFjY3y+i8+VEJs=", + "_parent": { + "$ref": "AAAAAAFjY3y+i8+U/Xo=" + }, + "model": { + "$ref": "AAAAAAFjY3y+is+TqfA=" + }, + "subViews": [ + { + "_type": "LabelView", + "_id": "AAAAAAFjY3y+i8+Wjmg=", + "_parent": { + "$ref": "AAAAAAFjY3y+i8+VEJs=" + }, + "visible": false, + "font": "Arial;13;0", + "height": 13 + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjY3y+jc+X0gk=", + "_parent": { + "$ref": "AAAAAAFjY3y+i8+VEJs=" + }, + "font": "Arial;13;1", + "left": 197, + "top": 47, + "width": 66.341796875, + "height": 13, + "text": "Safe App" + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjY3y+jc+YPB0=", + "_parent": { + "$ref": "AAAAAAFjY3y+i8+VEJs=" + }, + "visible": false, + "font": "Arial;13;0", + "width": 106.20263671875, + "height": 13, + "text": "(from Interaction1)" + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjY3y+jc+ZDI0=", + "_parent": { + "$ref": "AAAAAAFjY3y+i8+VEJs=" + }, + "visible": false, + "font": "Arial;13;0", + "height": 13, + "horizontalAlignment": 1 + } + ], + "font": "Arial;13;0", + "left": 192, + "top": 40, + "width": 76.341796875, + "height": 40, + "stereotypeLabel": { + "$ref": "AAAAAAFjY3y+i8+Wjmg=" + }, + "nameLabel": { + "$ref": "AAAAAAFjY3y+jc+X0gk=" + }, + "namespaceLabel": { + "$ref": "AAAAAAFjY3y+jc+YPB0=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjY3y+jc+ZDI0=" + } + }, + { + "_type": "UMLLinePartView", + "_id": "AAAAAAFjY3y+jc+a7TA=", + "_parent": { + "$ref": "AAAAAAFjY3y+i8+U/Xo=" + }, + "model": { + "$ref": "AAAAAAFjY3y+is+TqfA=" + }, + "font": "Arial;13;0", + "left": 230, + "top": 80, + "width": 1, + "height": 427 + } + ], + "font": "Arial;13;0", + "left": 192, + "top": 40, + "width": 76.341796875, + "height": 467, + "nameCompartment": { + "$ref": "AAAAAAFjY3y+i8+VEJs=" + }, + "linePart": { + "$ref": "AAAAAAFjY3y+jc+a7TA=" + } + }, + { + "_type": "UMLSeqLifelineView", + "_id": "AAAAAAFjY30wVM+2jNo=", + "_parent": { + "$ref": "AAAAAAFjYxrX189lB8s=" + }, + "model": { + "$ref": "AAAAAAFjY30wVM+1WgE=" + }, + "subViews": [ + { + "_type": "UMLNameCompartmentView", + "_id": "AAAAAAFjY30wVc+37bI=", + "_parent": { + "$ref": "AAAAAAFjY30wVM+2jNo=" + }, + "model": { + "$ref": "AAAAAAFjY30wVM+1WgE=" + }, + "subViews": [ + { + "_type": "LabelView", + "_id": "AAAAAAFjY30wVc+4lbc=", + "_parent": { + "$ref": "AAAAAAFjY30wVc+37bI=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 144, + "height": 13 + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjY30wVc+5vZ0=", + "_parent": { + "$ref": "AAAAAAFjY30wVc+37bI=" + }, + "font": "Arial;13;1", + "left": 445, + "top": 47, + "width": 63, + "height": 13, + "text": "Server" + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjY30wVc+6jTs=", + "_parent": { + "$ref": "AAAAAAFjY30wVc+37bI=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 144, + "width": 106.20263671875, + "height": 13, + "text": "(from Interaction1)" + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjY30wVs+7FTk=", + "_parent": { + "$ref": "AAAAAAFjY30wVc+37bI=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 144, + "height": 13, + "horizontalAlignment": 1 + } + ], + "font": "Arial;13;0", + "left": 440, + "top": 40, + "width": 73, + "height": 40, + "stereotypeLabel": { + "$ref": "AAAAAAFjY30wVc+4lbc=" + }, + "nameLabel": { + "$ref": "AAAAAAFjY30wVc+5vZ0=" + }, + "namespaceLabel": { + "$ref": "AAAAAAFjY30wVc+6jTs=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjY30wVs+7FTk=" + } + }, + { + "_type": "UMLLinePartView", + "_id": "AAAAAAFjY30wVs+8APo=", + "_parent": { + "$ref": "AAAAAAFjY30wVM+2jNo=" + }, + "model": { + "$ref": "AAAAAAFjY30wVM+1WgE=" + }, + "font": "Arial;13;0", + "left": 477, + "top": 80, + "width": 1, + "height": 422 + } + ], + "font": "Arial;13;0", + "left": 440, + "top": 40, + "width": 73, + "height": 462, + "nameCompartment": { + "$ref": "AAAAAAFjY30wVc+37bI=" + }, + "linePart": { + "$ref": "AAAAAAFjY30wVs+8APo=" + } + }, + { + "_type": "UMLSeqLifelineView", + "_id": "AAAAAAFjY31BYs/WzxE=", + "_parent": { + "$ref": "AAAAAAFjYxrX189lB8s=" + }, + "model": { + "$ref": "AAAAAAFjY31BYs/Vb+g=" + }, + "subViews": [ + { + "_type": "UMLNameCompartmentView", + "_id": "AAAAAAFjY31BYs/XS6Q=", + "_parent": { + "$ref": "AAAAAAFjY31BYs/WzxE=" + }, + "model": { + "$ref": "AAAAAAFjY31BYs/Vb+g=" + }, + "subViews": [ + { + "_type": "LabelView", + "_id": "AAAAAAFjY31BYs/YXuA=", + "_parent": { + "$ref": "AAAAAAFjY31BYs/XS6Q=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 128, + "height": 13 + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjY31BYs/ZJ/c=", + "_parent": { + "$ref": "AAAAAAFjY31BYs/XS6Q=" + }, + "font": "Arial;13;1", + "left": 597, + "top": 47, + "width": 106.07177734375, + "height": 13, + "text": "Ethereum Node" + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjY31BYs/awAs=", + "_parent": { + "$ref": "AAAAAAFjY31BYs/XS6Q=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 128, + "width": 106.20263671875, + "height": 13, + "text": "(from Interaction1)" + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjY31BY8/b3E8=", + "_parent": { + "$ref": "AAAAAAFjY31BYs/XS6Q=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 128, + "height": 13, + "horizontalAlignment": 1 + } + ], + "font": "Arial;13;0", + "left": 592, + "top": 40, + "width": 116.07177734375, + "height": 40, + "stereotypeLabel": { + "$ref": "AAAAAAFjY31BYs/YXuA=" + }, + "nameLabel": { + "$ref": "AAAAAAFjY31BYs/ZJ/c=" + }, + "namespaceLabel": { + "$ref": "AAAAAAFjY31BYs/awAs=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjY31BY8/b3E8=" + } + }, + { + "_type": "UMLLinePartView", + "_id": "AAAAAAFjY31BY8/cmHo=", + "_parent": { + "$ref": "AAAAAAFjY31BYs/WzxE=" + }, + "model": { + "$ref": "AAAAAAFjY31BYs/Vb+g=" + }, + "font": "Arial;13;0", + "left": 650, + "top": 80, + "width": 1, + "height": 427 + } + ], + "font": "Arial;13;0", + "left": 592, + "top": 40, + "width": 116.07177734375, + "height": 467, + "nameCompartment": { + "$ref": "AAAAAAFjY31BYs/XS6Q=" + }, + "linePart": { + "$ref": "AAAAAAFjY31BY8/cmHo=" + } + }, + { + "_type": "UMLSeqMessageView", + "_id": "AAAAAAFjY32qv8/7sZg=", + "_parent": { + "$ref": "AAAAAAFjYxrX189lB8s=" + }, + "model": { + "$ref": "AAAAAAFjY32qvc/6cY8=" + }, + "subViews": [ + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY32qwM/8kkE=", + "_parent": { + "$ref": "AAAAAAFjY32qv8/7sZg=" + }, + "model": { + "$ref": "AAAAAAFjY32qvc/6cY8=" + }, + "font": "Arial;13;0", + "left": 229, + "top": 101, + "width": 243, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjY32qv8/7sZg=" + }, + "edgePosition": 1, + "text": "1 : getCreationTx(r, owners, otherParams)" + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY32qwc/9oyU=", + "_parent": { + "$ref": "AAAAAAFjY32qv8/7sZg=" + }, + "model": { + "$ref": "AAAAAAFjY32qvc/6cY8=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 350, + "top": 86, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 25, + "hostEdge": { + "$ref": "AAAAAAFjY32qv8/7sZg=" + }, + "edgePosition": 1 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY32qwc/+PMw=", + "_parent": { + "$ref": "AAAAAAFjY32qv8/7sZg=" + }, + "model": { + "$ref": "AAAAAAFjY32qvc/6cY8=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 350, + "top": 121, + "height": 13, + "alpha": -1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjY32qv8/7sZg=" + }, + "edgePosition": 1 + }, + { + "_type": "UMLActivationView", + "_id": "AAAAAAFjY32qwc//ysk=", + "_parent": { + "$ref": "AAAAAAFjY32qv8/7sZg=" + }, + "model": { + "$ref": "AAAAAAFjY32qvc/6cY8=" + }, + "font": "Arial;13;0", + "left": 470, + "top": 117, + "width": 14, + "height": 29 + } + ], + "font": "Arial;13;0", + "head": { + "$ref": "AAAAAAFjY30wVs+8APo=" + }, + "tail": { + "$ref": "AAAAAAFjY3y+jc+a7TA=" + }, + "points": "230:117;470:117", + "nameLabel": { + "$ref": "AAAAAAFjY32qwM/8kkE=" + }, + "stereotypeLabel": { + "$ref": "AAAAAAFjY32qwc/9oyU=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjY32qwc/+PMw=" + }, + "activation": { + "$ref": "AAAAAAFjY32qwc//ysk=" + } + }, + { + "_type": "UMLSeqMessageView", + "_id": "AAAAAAFjY346/NATjzs=", + "_parent": { + "$ref": "AAAAAAFjYxrX189lB8s=" + }, + "model": { + "$ref": "AAAAAAFjY346/NAShB4=" + }, + "subViews": [ + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY346/NAUxzE=", + "_parent": { + "$ref": "AAAAAAFjY346/NATjzs=" + }, + "model": { + "$ref": "AAAAAAFjY346/NAShB4=" + }, + "font": "Arial;13;0", + "left": 263, + "top": 141, + "width": 60, + "height": 13, + "alpha": 0.17367122820123848, + "distance": 57.87054518492115, + "hostEdge": { + "$ref": "AAAAAAFjY346/NATjzs=" + }, + "edgePosition": 1, + "text": "2 : v, s, tx" + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY346/dAVOIA=", + "_parent": { + "$ref": "AAAAAAFjY346/NATjzs=" + }, + "model": { + "$ref": "AAAAAAFjY346/NAShB4=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 349, + "top": 156, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 25, + "hostEdge": { + "$ref": "AAAAAAFjY346/NATjzs=" + }, + "edgePosition": 1 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY346/dAWIAo=", + "_parent": { + "$ref": "AAAAAAFjY346/NATjzs=" + }, + "model": { + "$ref": "AAAAAAFjY346/NAShB4=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 350, + "top": 121, + "height": 13, + "alpha": -1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjY346/NATjzs=" + }, + "edgePosition": 1 + }, + { + "_type": "UMLActivationView", + "_id": "AAAAAAFjY346/dAX+Ys=", + "_parent": { + "$ref": "AAAAAAFjY346/NATjzs=" + }, + "model": { + "$ref": "AAAAAAFjY346/NAShB4=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 230, + "top": 137, + "width": 14, + "height": 25 + } + ], + "font": "Arial;13;0", + "head": { + "$ref": "AAAAAAFjY3y+jc+a7TA=" + }, + "tail": { + "$ref": "AAAAAAFjY30wVs+8APo=" + }, + "points": "470:137;230:137", + "nameLabel": { + "$ref": "AAAAAAFjY346/NAUxzE=" + }, + "stereotypeLabel": { + "$ref": "AAAAAAFjY346/dAVOIA=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjY346/dAWIAo=" + }, + "activation": { + "$ref": "AAAAAAFjY346/dAX+Ys=" + } + }, + { + "_type": "UMLNoteView", + "_id": "AAAAAAFjY36/YtAtckA=", + "_parent": { + "$ref": "AAAAAAFjYxrX189lB8s=" + }, + "font": "Arial;13;0", + "left": 376, + "top": 160, + "width": 257, + "height": 57, + "text": "You can derive two addresses from this:\nDeployer\nSafe" + }, + { + "_type": "UMLNoteLinkView", + "_id": "AAAAAAFjY36/ZdAw2yw=", + "_parent": { + "$ref": "AAAAAAFjYxrX189lB8s=" + }, + "font": "Arial;13;0", + "head": { + "$ref": "AAAAAAFjY36/YtAtckA=" + }, + "tail": { + "$ref": "AAAAAAFjY346/NATjzs=" + }, + "lineStyle": 1, + "points": "350:137;416:159" + }, + { + "_type": "UMLSeqMessageView", + "_id": "AAAAAAFjY3+jodA+jx8=", + "_parent": { + "$ref": "AAAAAAFjYxrX189lB8s=" + }, + "model": { + "$ref": "AAAAAAFjY3+jodA9ypo=" + }, + "subViews": [ + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY3+jotA/DDw=", + "_parent": { + "$ref": "AAAAAAFjY3+jodA+jx8=" + }, + "model": { + "$ref": "AAAAAAFjY3+jodA9ypo=" + }, + "font": "Arial;13;0", + "left": 130, + "top": 156, + "width": 44, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjY3+jodA+jx8=" + }, + "edgePosition": 1, + "text": "3 : safe" + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY3+jotBAbqQ=", + "_parent": { + "$ref": "AAAAAAFjY3+jodA+jx8=" + }, + "model": { + "$ref": "AAAAAAFjY3+jodA9ypo=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 152, + "top": 171, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 25, + "hostEdge": { + "$ref": "AAAAAAFjY3+jodA+jx8=" + }, + "edgePosition": 1 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY3+jotBBa40=", + "_parent": { + "$ref": "AAAAAAFjY3+jodA+jx8=" + }, + "model": { + "$ref": "AAAAAAFjY3+jodA9ypo=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 153, + "top": 136, + "height": 13, + "alpha": -1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjY3+jodA+jx8=" + }, + "edgePosition": 1 + }, + { + "_type": "UMLActivationView", + "_id": "AAAAAAFjY3+jotBC+kY=", + "_parent": { + "$ref": "AAAAAAFjY3+jodA+jx8=" + }, + "model": { + "$ref": "AAAAAAFjY3+jodA9ypo=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 77, + "top": 152, + "width": 14, + "height": 25 + } + ], + "font": "Arial;13;0", + "head": { + "$ref": "AAAAAAFjY3yrq896TCI=" + }, + "tail": { + "$ref": "AAAAAAFjY3y+jc+a7TA=" + }, + "points": "230:152;77:152", + "nameLabel": { + "$ref": "AAAAAAFjY3+jotA/DDw=" + }, + "stereotypeLabel": { + "$ref": "AAAAAAFjY3+jotBAbqQ=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjY3+jotBBa40=" + }, + "activation": { + "$ref": "AAAAAAFjY3+jotBC+kY=" + } + }, + { + "_type": "UMLSeqMessageView", + "_id": "AAAAAAFjY3/FuNBUX+w=", + "_parent": { + "$ref": "AAAAAAFjYxrX189lB8s=" + }, + "model": { + "$ref": "AAAAAAFjY3/FuNBTH6c=" + }, + "subViews": [ + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY3/FuNBV1T8=", + "_parent": { + "$ref": "AAAAAAFjY3/FuNBUX+w=" + }, + "model": { + "$ref": "AAAAAAFjY3/FuNBTH6c=" + }, + "font": "Arial;13;0", + "left": 307, + "top": 275, + "width": 106, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjY3/FuNBUX+w=" + }, + "edgePosition": 1, + "text": "4 : sendETH(safe)" + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY3/FuNBWYJ0=", + "_parent": { + "$ref": "AAAAAAFjY3/FuNBUX+w=" + }, + "model": { + "$ref": "AAAAAAFjY3/FuNBTH6c=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 360, + "top": 260, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 25, + "hostEdge": { + "$ref": "AAAAAAFjY3/FuNBUX+w=" + }, + "edgePosition": 1 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY3/FuNBXYfI=", + "_parent": { + "$ref": "AAAAAAFjY3/FuNBUX+w=" + }, + "model": { + "$ref": "AAAAAAFjY3/FuNBTH6c=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 360, + "top": 295, + "height": 13, + "alpha": -1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjY3/FuNBUX+w=" + }, + "edgePosition": 1 + }, + { + "_type": "UMLActivationView", + "_id": "AAAAAAFjY3/FuNBYVaI=", + "_parent": { + "$ref": "AAAAAAFjY3/FuNBUX+w=" + }, + "model": { + "$ref": "AAAAAAFjY3/FuNBTH6c=" + }, + "font": "Arial;13;0", + "left": 643, + "top": 291, + "width": 14, + "height": 29 + } + ], + "font": "Arial;13;0", + "head": { + "$ref": "AAAAAAFjY31BY8/cmHo=" + }, + "tail": { + "$ref": "AAAAAAFjY3yrq896TCI=" + }, + "points": "77:291;643:291", + "nameLabel": { + "$ref": "AAAAAAFjY3/FuNBV1T8=" + }, + "stereotypeLabel": { + "$ref": "AAAAAAFjY3/FuNBWYJ0=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjY3/FuNBXYfI=" + }, + "activation": { + "$ref": "AAAAAAFjY3/FuNBYVaI=" + } + }, + { + "_type": "UMLSeqMessageView", + "_id": "AAAAAAFjY4BIkdBqjwI=", + "_parent": { + "$ref": "AAAAAAFjYxrX189lB8s=" + }, + "model": { + "$ref": "AAAAAAFjY4BIkdBpL+8=" + }, + "subViews": [ + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY4BIkdBryx8=", + "_parent": { + "$ref": "AAAAAAFjY4BIkdBqjwI=" + }, + "model": { + "$ref": "AAAAAAFjY4BIkdBpL+8=" + }, + "font": "Arial;13;0", + "left": 293, + "top": 312, + "width": 115, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjY4BIkdBqjwI=" + }, + "edgePosition": 1, + "text": "5 : createSafe(safe)" + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY4BIkdBst54=", + "_parent": { + "$ref": "AAAAAAFjY4BIkdBqjwI=" + }, + "model": { + "$ref": "AAAAAAFjY4BIkdBpL+8=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 350, + "top": 297, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 25, + "hostEdge": { + "$ref": "AAAAAAFjY4BIkdBqjwI=" + }, + "edgePosition": 1 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY4BIkdBtnYA=", + "_parent": { + "$ref": "AAAAAAFjY4BIkdBqjwI=" + }, + "model": { + "$ref": "AAAAAAFjY4BIkdBpL+8=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 350, + "top": 332, + "height": 13, + "alpha": -1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjY4BIkdBqjwI=" + }, + "edgePosition": 1 + }, + { + "_type": "UMLActivationView", + "_id": "AAAAAAFjY4BIkdBuYLI=", + "_parent": { + "$ref": "AAAAAAFjY4BIkdBqjwI=" + }, + "model": { + "$ref": "AAAAAAFjY4BIkdBpL+8=" + }, + "font": "Arial;13;0", + "left": 470, + "top": 328, + "width": 14, + "height": 29 + } + ], + "font": "Arial;13;0", + "head": { + "$ref": "AAAAAAFjY30wVs+8APo=" + }, + "tail": { + "$ref": "AAAAAAFjY3y+jc+a7TA=" + }, + "points": "230:328;470:328", + "nameLabel": { + "$ref": "AAAAAAFjY4BIkdBryx8=" + }, + "stereotypeLabel": { + "$ref": "AAAAAAFjY4BIkdBst54=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjY4BIkdBtnYA=" + }, + "activation": { + "$ref": "AAAAAAFjY4BIkdBuYLI=" + } + }, + { + "_type": "UMLSeqMessageView", + "_id": "AAAAAAFjY4FV/9CfqNg=", + "_parent": { + "$ref": "AAAAAAFjYxrX189lB8s=" + }, + "model": { + "$ref": "AAAAAAFjY4FV/9CeM7A=" + }, + "subViews": [ + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY4FV/9Cg8EE=", + "_parent": { + "$ref": "AAAAAAFjY4FV/9CfqNg=" + }, + "model": { + "$ref": "AAAAAAFjY4FV/9CeM7A=" + }, + "font": "Arial;13;0", + "left": 498, + "top": 322, + "width": 130.78076171875, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjY4FV/9CfqNg=" + }, + "edgePosition": 1, + "text": "6 : sendETH(deployer)" + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY4FV/9ChPrQ=", + "_parent": { + "$ref": "AAAAAAFjY4FV/9CfqNg=" + }, + "model": { + "$ref": "AAAAAAFjY4FV/9CeM7A=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 563, + "top": 307, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 25, + "hostEdge": { + "$ref": "AAAAAAFjY4FV/9CfqNg=" + }, + "edgePosition": 1 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY4FV/9Ci8TE=", + "_parent": { + "$ref": "AAAAAAFjY4FV/9CfqNg=" + }, + "model": { + "$ref": "AAAAAAFjY4FV/9CeM7A=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 563, + "top": 342, + "height": 13, + "alpha": -1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjY4FV/9CfqNg=" + }, + "edgePosition": 1 + }, + { + "_type": "UMLActivationView", + "_id": "AAAAAAFjY4FWANCjDAw=", + "_parent": { + "$ref": "AAAAAAFjY4FV/9CfqNg=" + }, + "model": { + "$ref": "AAAAAAFjY4FV/9CeM7A=" + }, + "font": "Arial;13;0", + "left": 643, + "top": 338, + "width": 14, + "height": 29 + } + ], + "font": "Arial;13;0", + "head": { + "$ref": "AAAAAAFjY31BY8/cmHo=" + }, + "tail": { + "$ref": "AAAAAAFjY30wVs+8APo=" + }, + "points": "483:338;643:338", + "nameLabel": { + "$ref": "AAAAAAFjY4FV/9Cg8EE=" + }, + "stereotypeLabel": { + "$ref": "AAAAAAFjY4FV/9ChPrQ=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjY4FV/9Ci8TE=" + }, + "activation": { + "$ref": "AAAAAAFjY4FWANCjDAw=" + } + }, + { + "_type": "UMLSeqMessageView", + "_id": "AAAAAAFjjJfjG1/9m7Q=", + "_parent": { + "$ref": "AAAAAAFjYxrX189lB8s=" + }, + "model": { + "$ref": "AAAAAAFjjJfjGV/89+I=" + }, + "subViews": [ + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjjJfjG1/+MFE=", + "_parent": { + "$ref": "AAAAAAFjjJfjG1/9m7Q=" + }, + "model": { + "$ref": "AAAAAAFjjJfjGV/89+I=" + }, + "font": "Arial;13;0", + "left": 305, + "top": 357, + "width": 89, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjjJfjG1/9m7Q=" + }, + "edgePosition": 1, + "text": "7 : safeTxHash" + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjjJfjHF//8Rs=", + "_parent": { + "$ref": "AAAAAAFjjJfjG1/9m7Q=" + }, + "model": { + "$ref": "AAAAAAFjjJfjGV/89+I=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 349, + "top": 372, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 25, + "hostEdge": { + "$ref": "AAAAAAFjjJfjG1/9m7Q=" + }, + "edgePosition": 1 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjjJfjHGAA5bM=", + "_parent": { + "$ref": "AAAAAAFjjJfjG1/9m7Q=" + }, + "model": { + "$ref": "AAAAAAFjjJfjGV/89+I=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 350, + "top": 337, + "height": 13, + "alpha": -1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjjJfjG1/9m7Q=" + }, + "edgePosition": 1 + }, + { + "_type": "UMLActivationView", + "_id": "AAAAAAFjjJfjHGAB8Qw=", + "_parent": { + "$ref": "AAAAAAFjjJfjG1/9m7Q=" + }, + "model": { + "$ref": "AAAAAAFjjJfjGV/89+I=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 230, + "top": 353, + "width": 14, + "height": 25 + } + ], + "font": "Arial;13;0", + "head": { + "$ref": "AAAAAAFjY3y+jc+a7TA=" + }, + "tail": { + "$ref": "AAAAAAFjY30wVs+8APo=" + }, + "points": "470:353;230:353", + "nameLabel": { + "$ref": "AAAAAAFjjJfjG1/+MFE=" + }, + "stereotypeLabel": { + "$ref": "AAAAAAFjjJfjHF//8Rs=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjjJfjHGAA5bM=" + }, + "activation": { + "$ref": "AAAAAAFjjJfjHGAB8Qw=" + } + }, + { + "_type": "UMLSeqMessageView", + "_id": "AAAAAAFjY4GSmNC2Hxk=", + "_parent": { + "$ref": "AAAAAAFjYxrX189lB8s=" + }, + "model": { + "$ref": "AAAAAAFjY4GSmNC1mIo=" + }, + "subViews": [ + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY4GSmNC34gE=", + "_parent": { + "$ref": "AAAAAAFjY4GSmNC2Hxk=" + }, + "model": { + "$ref": "AAAAAAFjY4GSmNC1mIo=" + }, + "font": "Arial;13;0", + "left": 525, + "top": 381, + "width": 74, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjY4GSmNC2Hxk=" + }, + "edgePosition": 1, + "text": "8 : txReceipt" + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY4GSmdC48gw=", + "_parent": { + "$ref": "AAAAAAFjY4GSmNC2Hxk=" + }, + "model": { + "$ref": "AAAAAAFjY4GSmNC1mIo=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 562, + "top": 396, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 25, + "hostEdge": { + "$ref": "AAAAAAFjY4GSmNC2Hxk=" + }, + "edgePosition": 1 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY4GSmdC5SY8=", + "_parent": { + "$ref": "AAAAAAFjY4GSmNC2Hxk=" + }, + "model": { + "$ref": "AAAAAAFjY4GSmNC1mIo=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 563, + "top": 361, + "height": 13, + "alpha": -1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjY4GSmNC2Hxk=" + }, + "edgePosition": 1 + }, + { + "_type": "UMLActivationView", + "_id": "AAAAAAFjY4GSmdC6y6w=", + "_parent": { + "$ref": "AAAAAAFjY4GSmNC2Hxk=" + }, + "model": { + "$ref": "AAAAAAFjY4GSmNC1mIo=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 477, + "top": 377, + "width": 14, + "height": 25 + } + ], + "font": "Arial;13;0", + "head": { + "$ref": "AAAAAAFjY30wVs+8APo=" + }, + "tail": { + "$ref": "AAAAAAFjY31BY8/cmHo=" + }, + "points": "650:377;477:377", + "nameLabel": { + "$ref": "AAAAAAFjY4GSmNC34gE=" + }, + "stereotypeLabel": { + "$ref": "AAAAAAFjY4GSmdC48gw=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjY4GSmdC5SY8=" + }, + "activation": { + "$ref": "AAAAAAFjY4GSmdC6y6w=" + } + }, + { + "_type": "UMLSeqMessageView", + "_id": "AAAAAAFjY4HD9tDND2A=", + "_parent": { + "$ref": "AAAAAAFjYxrX189lB8s=" + }, + "model": { + "$ref": "AAAAAAFjY4HD9dDModc=" + }, + "subViews": [ + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY4HD9tDOoIA=", + "_parent": { + "$ref": "AAAAAAFjY4HD9tDND2A=" + }, + "model": { + "$ref": "AAAAAAFjY4HD9dDModc=" + }, + "font": "Arial;13;0", + "left": 488, + "top": 402, + "width": 145, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjY4HD9tDND2A=" + }, + "edgePosition": 1, + "text": "9 : deploySafe(tx, v, r, s)" + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY4HD9tDPVMk=", + "_parent": { + "$ref": "AAAAAAFjY4HD9tDND2A=" + }, + "model": { + "$ref": "AAAAAAFjY4HD9dDModc=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 560, + "top": 387, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 25, + "hostEdge": { + "$ref": "AAAAAAFjY4HD9tDND2A=" + }, + "edgePosition": 1 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY4HD9tDQC+k=", + "_parent": { + "$ref": "AAAAAAFjY4HD9tDND2A=" + }, + "model": { + "$ref": "AAAAAAFjY4HD9dDModc=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 560, + "top": 422, + "height": 13, + "alpha": -1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjY4HD9tDND2A=" + }, + "edgePosition": 1 + }, + { + "_type": "UMLActivationView", + "_id": "AAAAAAFjY4HD9tDROe0=", + "_parent": { + "$ref": "AAAAAAFjY4HD9tDND2A=" + }, + "model": { + "$ref": "AAAAAAFjY4HD9dDModc=" + }, + "font": "Arial;13;0", + "left": 643, + "top": 418, + "width": 14, + "height": 29 + } + ], + "font": "Arial;13;0", + "head": { + "$ref": "AAAAAAFjY31BY8/cmHo=" + }, + "tail": { + "$ref": "AAAAAAFjY30wVs+8APo=" + }, + "points": "477:418;643:418", + "nameLabel": { + "$ref": "AAAAAAFjY4HD9tDOoIA=" + }, + "stereotypeLabel": { + "$ref": "AAAAAAFjY4HD9tDPVMk=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjY4HD9tDQC+k=" + }, + "activation": { + "$ref": "AAAAAAFjY4HD9tDROe0=" + } + }, + { + "_type": "UMLSeqMessageView", + "_id": "AAAAAAFjY4I6HdDmiJY=", + "_parent": { + "$ref": "AAAAAAFjYxrX189lB8s=" + }, + "model": { + "$ref": "AAAAAAFjY4I6HNDlvgU=" + }, + "subViews": [ + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY4I6HdDnj9E=", + "_parent": { + "$ref": "AAAAAAFjY4I6HdDmiJY=" + }, + "model": { + "$ref": "AAAAAAFjY4I6HNDlvgU=" + }, + "font": "Arial;13;0", + "left": 528, + "top": 443, + "width": 62.15625, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjY4I6HdDmiJY=" + }, + "edgePosition": 1, + "text": "10 : refund" + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY4I6HdDom0Y=", + "_parent": { + "$ref": "AAAAAAFjY4I6HdDmiJY=" + }, + "model": { + "$ref": "AAAAAAFjY4I6HNDlvgU=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 559, + "top": 458, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 25, + "hostEdge": { + "$ref": "AAAAAAFjY4I6HdDmiJY=" + }, + "edgePosition": 1 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY4I6HdDpZ58=", + "_parent": { + "$ref": "AAAAAAFjY4I6HdDmiJY=" + }, + "model": { + "$ref": "AAAAAAFjY4I6HNDlvgU=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 560, + "top": 423, + "height": 13, + "alpha": -1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjY4I6HdDmiJY=" + }, + "edgePosition": 1 + }, + { + "_type": "UMLActivationView", + "_id": "AAAAAAFjY4I6HdDqh4o=", + "_parent": { + "$ref": "AAAAAAFjY4I6HdDmiJY=" + }, + "model": { + "$ref": "AAAAAAFjY4I6HNDlvgU=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 477, + "top": 439, + "width": 14, + "height": 25 + } + ], + "font": "Arial;13;0", + "head": { + "$ref": "AAAAAAFjY30wVs+8APo=" + }, + "tail": { + "$ref": "AAAAAAFjY31BY8/cmHo=" + }, + "points": "643:439;477:439", + "nameLabel": { + "$ref": "AAAAAAFjY4I6HdDnj9E=" + }, + "stereotypeLabel": { + "$ref": "AAAAAAFjY4I6HdDom0Y=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjY4I6HdDpZ58=" + }, + "activation": { + "$ref": "AAAAAAFjY4I6HdDqh4o=" + } + }, + { + "_type": "UMLSeqMessageView", + "_id": "AAAAAAFjjJhLTGATClg=", + "_parent": { + "$ref": "AAAAAAFjYxrX189lB8s=" + }, + "model": { + "$ref": "AAAAAAFjjJhLS2ASPGk=" + }, + "subViews": [ + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjjJhLTGAUZDA=", + "_parent": { + "$ref": "AAAAAAFjjJhLTGATClg=" + }, + "model": { + "$ref": "AAAAAAFjjJhLS2ASPGk=" + }, + "font": "Arial;13;0", + "left": 235, + "top": 439, + "width": 150, + "height": 13, + "alpha": -3.2207917308186342, + "distance": 126.39620247459969, + "hostEdge": { + "$ref": "AAAAAAFjjJhLTGATClg=" + }, + "edgePosition": 1, + "text": "11 : checkTxHashReceipt" + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjjJhLTWAVSRI=", + "_parent": { + "$ref": "AAAAAAFjjJhLTGATClg=" + }, + "model": { + "$ref": "AAAAAAFjjJhLS2ASPGk=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 436, + "top": 425, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 25, + "hostEdge": { + "$ref": "AAAAAAFjjJhLTGATClg=" + }, + "edgePosition": 1 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjjJhLTWAWycE=", + "_parent": { + "$ref": "AAAAAAFjjJhLTGATClg=" + }, + "model": { + "$ref": "AAAAAAFjjJhLS2ASPGk=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 436, + "top": 460, + "height": 13, + "alpha": -1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjjJhLTGATClg=" + }, + "edgePosition": 1 + }, + { + "_type": "UMLActivationView", + "_id": "AAAAAAFjjJhLTWAXDYA=", + "_parent": { + "$ref": "AAAAAAFjjJhLTGATClg=" + }, + "model": { + "$ref": "AAAAAAFjjJhLS2ASPGk=" + }, + "font": "Arial;13;0", + "left": 643, + "top": 456, + "width": 14, + "height": 29 + } + ], + "font": "Arial;13;0", + "head": { + "$ref": "AAAAAAFjY31BY8/cmHo=" + }, + "tail": { + "$ref": "AAAAAAFjY3y+jc+a7TA=" + }, + "points": "230:456;643:456", + "nameLabel": { + "$ref": "AAAAAAFjjJhLTGAUZDA=" + }, + "stereotypeLabel": { + "$ref": "AAAAAAFjjJhLTWAVSRI=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjjJhLTWAWycE=" + }, + "activation": { + "$ref": "AAAAAAFjjJhLTWAXDYA=" + } + }, + { + "_type": "UMLSeqMessageView", + "_id": "AAAAAAFjY4JryND9Jt4=", + "_parent": { + "$ref": "AAAAAAFjYxrX189lB8s=" + }, + "model": { + "$ref": "AAAAAAFjY4JryND8+FM=" + }, + "subViews": [ + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY4JryND+kVs=", + "_parent": { + "$ref": "AAAAAAFjY4JryND9Jt4=" + }, + "model": { + "$ref": "AAAAAAFjY4JryND8+FM=" + }, + "font": "Arial;13;0", + "left": 263, + "top": 480, + "width": 96.1162109375, + "height": 13, + "alpha": 0.15084537350634042, + "distance": 126.43575443678897, + "hostEdge": { + "$ref": "AAAAAAFjY4JryND9Jt4=" + }, + "edgePosition": 1, + "text": "12 : safeCreated" + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY4JryND/W1E=", + "_parent": { + "$ref": "AAAAAAFjY4JryND9Jt4=" + }, + "model": { + "$ref": "AAAAAAFjY4JryND8+FM=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 435, + "top": 486, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 25, + "hostEdge": { + "$ref": "AAAAAAFjY4JryND9Jt4=" + }, + "edgePosition": 1 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjY4JryNEAOTg=", + "_parent": { + "$ref": "AAAAAAFjY4JryND9Jt4=" + }, + "model": { + "$ref": "AAAAAAFjY4JryND8+FM=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 436, + "top": 451, + "height": 13, + "alpha": -1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjY4JryND9Jt4=" + }, + "edgePosition": 1 + }, + { + "_type": "UMLActivationView", + "_id": "AAAAAAFjY4JryNEBT64=", + "_parent": { + "$ref": "AAAAAAFjY4JryND9Jt4=" + }, + "model": { + "$ref": "AAAAAAFjY4JryND8+FM=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 230, + "top": 467, + "width": 14, + "height": 25 + } + ], + "font": "Arial;13;0", + "head": { + "$ref": "AAAAAAFjY3y+jc+a7TA=" + }, + "tail": { + "$ref": "AAAAAAFjY31BY8/cmHo=" + }, + "points": "643:467;230:467", + "nameLabel": { + "$ref": "AAAAAAFjY4JryND+kVs=" + }, + "stereotypeLabel": { + "$ref": "AAAAAAFjY4JryND/W1E=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjY4JryNEAOTg=" + }, + "activation": { + "$ref": "AAAAAAFjY4JryNEBT64=" + } + }, + { + "_type": "UMLNoteView", + "_id": "AAAAAAFjjJj8XWAps6g=", + "_parent": { + "$ref": "AAAAAAFjYxrX189lB8s=" + }, + "font": "Arial;13;0", + "left": 672, + "top": 288, + "width": 97, + "height": 77, + "text": "it requires, the safe to have enough balance for refund" + } + ] + } + ], + "messages": [ + { + "_type": "UMLMessage", + "_id": "AAAAAAFjY32qvc/6cY8=", + "_parent": { + "$ref": "AAAAAAFjYxrX189kdgE=" + }, + "name": "getCreationTx", + "source": { + "$ref": "AAAAAAFjY3y+is+TqfA=" + }, + "target": { + "$ref": "AAAAAAFjY30wVM+1WgE=" + }, + "arguments": "r, owners, otherParams" + }, + { + "_type": "UMLMessage", + "_id": "AAAAAAFjY346/NAShB4=", + "_parent": { + "$ref": "AAAAAAFjYxrX189kdgE=" + }, + "name": "v, s, tx", + "source": { + "$ref": "AAAAAAFjY30wVM+1WgE=" + }, + "target": { + "$ref": "AAAAAAFjY3y+is+TqfA=" + }, + "messageSort": "reply" + }, + { + "_type": "UMLMessage", + "_id": "AAAAAAFjY3+jodA9ypo=", + "_parent": { + "$ref": "AAAAAAFjYxrX189kdgE=" + }, + "name": "safe", + "source": { + "$ref": "AAAAAAFjY3y+is+TqfA=" + }, + "target": { + "$ref": "AAAAAAFjY3yrp89z1WE=" + }, + "messageSort": "reply" + }, + { + "_type": "UMLMessage", + "_id": "AAAAAAFjY3/FuNBTH6c=", + "_parent": { + "$ref": "AAAAAAFjYxrX189kdgE=" + }, + "name": "sendETH", + "source": { + "$ref": "AAAAAAFjY3yrp89z1WE=" + }, + "target": { + "$ref": "AAAAAAFjY31BYs/Vb+g=" + }, + "arguments": "safe" + }, + { + "_type": "UMLMessage", + "_id": "AAAAAAFjY4BIkdBpL+8=", + "_parent": { + "$ref": "AAAAAAFjYxrX189kdgE=" + }, + "name": "createSafe", + "source": { + "$ref": "AAAAAAFjY3y+is+TqfA=" + }, + "target": { + "$ref": "AAAAAAFjY30wVM+1WgE=" + }, + "arguments": "safe" + }, + { + "_type": "UMLMessage", + "_id": "AAAAAAFjY4FV/9CeM7A=", + "_parent": { + "$ref": "AAAAAAFjYxrX189kdgE=" + }, + "name": "sendETH", + "source": { + "$ref": "AAAAAAFjY30wVM+1WgE=" + }, + "target": { + "$ref": "AAAAAAFjY31BYs/Vb+g=" + }, + "arguments": "deployer" + }, + { + "_type": "UMLMessage", + "_id": "AAAAAAFjjJfjGV/89+I=", + "_parent": { + "$ref": "AAAAAAFjYxrX189kdgE=" + }, + "name": "safeTxHash", + "source": { + "$ref": "AAAAAAFjY30wVM+1WgE=" + }, + "target": { + "$ref": "AAAAAAFjY3y+is+TqfA=" + }, + "messageSort": "reply" + }, + { + "_type": "UMLMessage", + "_id": "AAAAAAFjY4GSmNC1mIo=", + "_parent": { + "$ref": "AAAAAAFjYxrX189kdgE=" + }, + "name": "txReceipt", + "source": { + "$ref": "AAAAAAFjY31BYs/Vb+g=" + }, + "target": { + "$ref": "AAAAAAFjY30wVM+1WgE=" + }, + "messageSort": "reply" + }, + { + "_type": "UMLMessage", + "_id": "AAAAAAFjY4HD9dDModc=", + "_parent": { + "$ref": "AAAAAAFjYxrX189kdgE=" + }, + "name": "deploySafe", + "source": { + "$ref": "AAAAAAFjY30wVM+1WgE=" + }, + "target": { + "$ref": "AAAAAAFjY31BYs/Vb+g=" + }, + "arguments": "tx, v, r, s" + }, + { + "_type": "UMLMessage", + "_id": "AAAAAAFjY4I6HNDlvgU=", + "_parent": { + "$ref": "AAAAAAFjYxrX189kdgE=" + }, + "name": "refund", + "source": { + "$ref": "AAAAAAFjY31BYs/Vb+g=" + }, + "target": { + "$ref": "AAAAAAFjY30wVM+1WgE=" + }, + "messageSort": "reply" + }, + { + "_type": "UMLMessage", + "_id": "AAAAAAFjjJhLS2ASPGk=", + "_parent": { + "$ref": "AAAAAAFjYxrX189kdgE=" + }, + "name": "checkTxHashReceipt", + "source": { + "$ref": "AAAAAAFjY3y+is+TqfA=" + }, + "target": { + "$ref": "AAAAAAFjY31BYs/Vb+g=" + } + }, + { + "_type": "UMLMessage", + "_id": "AAAAAAFjY4JryND8+FM=", + "_parent": { + "$ref": "AAAAAAFjYxrX189kdgE=" + }, + "name": "safeCreated", + "source": { + "$ref": "AAAAAAFjY31BYs/Vb+g=" + }, + "target": { + "$ref": "AAAAAAFjY3y+is+TqfA=" + }, + "messageSort": "reply" + } + ], + "participants": [ + { + "_type": "UMLLifeline", + "_id": "AAAAAAFjY3yrp89z1WE=", + "_parent": { + "$ref": "AAAAAAFjYxrX189kdgE=" + }, + "name": "Kraken", + "represent": { + "$ref": "AAAAAAFjY3yrps9yk5c=" + }, + "isMultiInstance": false + }, + { + "_type": "UMLLifeline", + "_id": "AAAAAAFjY3y+is+TqfA=", + "_parent": { + "$ref": "AAAAAAFjYxrX189kdgE=" + }, + "name": "Safe App", + "represent": { + "$ref": "AAAAAAFjY3y+is+SIjg=" + }, + "isMultiInstance": false + }, + { + "_type": "UMLLifeline", + "_id": "AAAAAAFjY30wVM+1WgE=", + "_parent": { + "$ref": "AAAAAAFjYxrX189kdgE=" + }, + "name": "Server", + "represent": { + "$ref": "AAAAAAFjY30wVM+0m+s=" + }, + "isMultiInstance": false + }, + { + "_type": "UMLLifeline", + "_id": "AAAAAAFjY31BYs/Vb+g=", + "_parent": { + "$ref": "AAAAAAFjYxrX189kdgE=" + }, + "name": "Ethereum Node", + "represent": { + "$ref": "AAAAAAFjY31BYs/UOfY=" + }, + "isMultiInstance": false + } + ] + } + ], + "attributes": [ + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjY3yrps9yk5c=", + "_parent": { + "$ref": "AAAAAAFjYxrX1s9jJVs=" + }, + "name": "Role1", + "type": "" + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjY3y+is+SIjg=", + "_parent": { + "$ref": "AAAAAAFjYxrX1s9jJVs=" + }, + "name": "Role2", + "type": "" + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjY30wVM+0m+s=", + "_parent": { + "$ref": "AAAAAAFjYxrX1s9jJVs=" + }, + "name": "Role3", + "type": "" + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjY31BYs/UOfY=", + "_parent": { + "$ref": "AAAAAAFjYxrX1s9jJVs=" + }, + "name": "Role4", + "type": "" + } + ] + }, + { + "_type": "UMLModel", + "_id": "AAAAAAFjY6G3ZtEesk4=", + "_parent": { + "$ref": "AAAAAAFF+h6SjaM2Hec=" + }, + "name": "Model1", + "ownedElements": [ + { + "_type": "UMLClassDiagram", + "_id": "AAAAAAFjY6G3Z9EfZ3Q=", + "_parent": { + "$ref": "AAAAAAFjY6G3ZtEesk4=" + }, + "name": "Models", + "ownedViews": [ + { + "_type": "UMLClassView", + "_id": "AAAAAAFjY6HKKtElWWg=", + "_parent": { + "$ref": "AAAAAAFjY6G3Z9EfZ3Q=" + }, + "model": { + "$ref": "AAAAAAFjY6HKKdEjP+Q=" + }, + "subViews": [ + { + "_type": "UMLNameCompartmentView", + "_id": "AAAAAAFjY6HKK9Em5ts=", + "_parent": { + "$ref": "AAAAAAFjY6HKKtElWWg=" + }, + "model": { + "$ref": "AAAAAAFjY6HKKdEjP+Q=" + }, + "subViews": [ + { + "_type": "LabelView", + "_id": "AAAAAAFjY6HKK9EnTug=", + "_parent": { + "$ref": "AAAAAAFjY6HKK9Em5ts=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -304, + "height": 13 + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjY6HKK9EofkY=", + "_parent": { + "$ref": "AAAAAAFjY6HKK9Em5ts=" + }, + "font": "Arial;13;1", + "left": 13, + "top": 231, + "width": 194, + "height": 13, + "text": "SafeCreation" + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjY6HKK9EpWIY=", + "_parent": { + "$ref": "AAAAAAFjY6HKK9Em5ts=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -304, + "width": 80.9072265625, + "height": 13, + "text": "(from Model1)" + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjY6HKK9Eq1/s=", + "_parent": { + "$ref": "AAAAAAFjY6HKK9Em5ts=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -304, + "height": 13, + "horizontalAlignment": 1 + } + ], + "font": "Arial;13;0", + "left": 8, + "top": 224, + "width": 204, + "height": 25, + "stereotypeLabel": { + "$ref": "AAAAAAFjY6HKK9EnTug=" + }, + "nameLabel": { + "$ref": "AAAAAAFjY6HKK9EofkY=" + }, + "namespaceLabel": { + "$ref": "AAAAAAFjY6HKK9EpWIY=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjY6HKK9Eq1/s=" + } + }, + { + "_type": "UMLAttributeCompartmentView", + "_id": "AAAAAAFjY6HKLNEri8k=", + "_parent": { + "$ref": "AAAAAAFjY6HKKtElWWg=" + }, + "model": { + "$ref": "AAAAAAFjY6HKKdEjP+Q=" + }, + "subViews": [ + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFjY6Ivj9FRw7c=", + "_parent": { + "$ref": "AAAAAAFjY6HKLNEri8k=" + }, + "model": { + "$ref": "AAAAAAFjY6IvftFO+jk=" + }, + "font": "Arial;13;0", + "left": 13, + "top": 254, + "width": 194, + "height": 13, + "text": "+owners[1..*] {readOnly}", + "horizontalAlignment": 0 + }, + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFjY6Lb3dFY/W0=", + "_parent": { + "$ref": "AAAAAAFjY6HKLNEri8k=" + }, + "model": { + "$ref": "AAAAAAFjY6Lb1NFVU6k=" + }, + "font": "Arial;13;0", + "left": 13, + "top": 269, + "width": 194, + "height": 13, + "text": "+threshold {readOnly}", + "horizontalAlignment": 0 + }, + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFjY6Mi5dFgqGM=", + "_parent": { + "$ref": "AAAAAAFjY6HKLNEri8k=" + }, + "model": { + "$ref": "AAAAAAFjY6Mi2tFdm9w=" + }, + "font": "Arial;13;0", + "left": 13, + "top": 284, + "width": 194, + "height": 13, + "text": "+gas_price {readOnly}", + "horizontalAlignment": 0 + }, + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFjY6NijNFntqs=", + "_parent": { + "$ref": "AAAAAAFjY6HKLNEri8k=" + }, + "model": { + "$ref": "AAAAAAFjY6Nig9FkSv0=" + }, + "font": "Arial;13;0", + "left": 13, + "top": 299, + "width": 194, + "height": 13, + "text": "+gas {readOnly}", + "horizontalAlignment": 0 + }, + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFjZWLVe9farfE=", + "_parent": { + "$ref": "AAAAAAFjY6HKLNEri8k=" + }, + "model": { + "$ref": "AAAAAAFjZWLVb9fXtHo=" + }, + "font": "Arial;13;0", + "left": 13, + "top": 314, + "width": 194, + "height": 13, + "text": "+tx_hash {readOnly, unique}", + "horizontalAlignment": 0 + }, + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFjZWlkCdhCm8s=", + "_parent": { + "$ref": "AAAAAAFjY6HKLNEri8k=" + }, + "model": { + "$ref": "AAAAAAFjZWlj/dg/20w=" + }, + "font": "Arial;13;0", + "left": 13, + "top": 329, + "width": 194, + "height": 13, + "text": "+deployer {readOnly, unique}", + "horizontalAlignment": 0 + }, + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFjggzV2yZGI18=", + "_parent": { + "$ref": "AAAAAAFjY6HKLNEri8k=" + }, + "model": { + "$ref": "AAAAAAFjggzVzCZDMts=" + }, + "font": "Arial;13;0", + "left": 13, + "top": 344, + "width": 194, + "height": 13, + "text": "+signed_tx {readOnly, unique}", + "horizontalAlignment": 0 + }, + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFjjJ4EIWD0118=", + "_parent": { + "$ref": "AAAAAAFjY6HKLNEri8k=" + }, + "model": { + "$ref": "AAAAAAFjjJ4EBGDxkSY=" + }, + "font": "Arial;13;0", + "left": 13, + "top": 359, + "width": 194, + "height": 13, + "text": "+v {readOnly}", + "horizontalAlignment": 0 + }, + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFjjJ4gVGErhik=", + "_parent": { + "$ref": "AAAAAAFjY6HKLNEri8k=" + }, + "model": { + "$ref": "AAAAAAFjjJ4gSGEouXY=" + }, + "font": "Arial;13;0", + "left": 13, + "top": 374, + "width": 194, + "height": 13, + "text": "+r {readOnly}", + "horizontalAlignment": 0 + }, + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFjjJ4o82FQaP4=", + "_parent": { + "$ref": "AAAAAAFjY6HKLNEri8k=" + }, + "model": { + "$ref": "AAAAAAFjjJ4o7WFN8H4=" + }, + "font": "Arial;13;0", + "left": 13, + "top": 389, + "width": 194, + "height": 13, + "text": "+s {readOnly}", + "horizontalAlignment": 0 + } + ], + "font": "Arial;13;0", + "left": 8, + "top": 249, + "width": 204, + "height": 158 + }, + { + "_type": "UMLOperationCompartmentView", + "_id": "AAAAAAFjY6HKLNEs8mE=", + "_parent": { + "$ref": "AAAAAAFjY6HKKtElWWg=" + }, + "model": { + "$ref": "AAAAAAFjY6HKKdEjP+Q=" + }, + "subViews": [ + { + "_type": "UMLOperationView", + "_id": "AAAAAAFjZWGdINeSt7k=", + "_parent": { + "$ref": "AAAAAAFjY6HKLNEs8mE=" + }, + "model": { + "$ref": "AAAAAAFjZWGdDtePF7Y=" + }, + "font": "Arial;13;0", + "left": 13, + "top": 412, + "width": 194, + "height": 13, + "text": "+sendETHtoDeployer()", + "horizontalAlignment": 0 + } + ], + "font": "Arial;13;0", + "left": 8, + "top": 407, + "width": 204, + "height": 23 + }, + { + "_type": "UMLReceptionCompartmentView", + "_id": "AAAAAAFjY6HKLNEt5Lo=", + "_parent": { + "$ref": "AAAAAAFjY6HKKtElWWg=" + }, + "model": { + "$ref": "AAAAAAFjY6HKKdEjP+Q=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -152, + "width": 10, + "height": 10 + }, + { + "_type": "UMLTemplateParameterCompartmentView", + "_id": "AAAAAAFjY6HKLdEuhA4=", + "_parent": { + "$ref": "AAAAAAFjY6HKKtElWWg=" + }, + "model": { + "$ref": "AAAAAAFjY6HKKdEjP+Q=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -152, + "width": 10, + "height": 10 + } + ], + "font": "Arial;13;0", + "containerChangeable": true, + "left": 8, + "top": 224, + "width": 204, + "height": 206, + "nameCompartment": { + "$ref": "AAAAAAFjY6HKK9Em5ts=" + }, + "attributeCompartment": { + "$ref": "AAAAAAFjY6HKLNEri8k=" + }, + "operationCompartment": { + "$ref": "AAAAAAFjY6HKLNEs8mE=" + }, + "receptionCompartment": { + "$ref": "AAAAAAFjY6HKLNEt5Lo=" + }, + "templateParameterCompartment": { + "$ref": "AAAAAAFjY6HKLdEuhA4=" + } + }, + { + "_type": "UMLClassView", + "_id": "AAAAAAFjZVkWY9F0DJI=", + "_parent": { + "$ref": "AAAAAAFjY6G3Z9EfZ3Q=" + }, + "model": { + "$ref": "AAAAAAFjZVkWXtFy5Gw=" + }, + "subViews": [ + { + "_type": "UMLNameCompartmentView", + "_id": "AAAAAAFjZVkWZdF1ahE=", + "_parent": { + "$ref": "AAAAAAFjZVkWY9F0DJI=" + }, + "model": { + "$ref": "AAAAAAFjZVkWXtFy5Gw=" + }, + "subViews": [ + { + "_type": "LabelView", + "_id": "AAAAAAFjZVkWZdF26vI=", + "_parent": { + "$ref": "AAAAAAFjZVkWZdF1ahE=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -512, + "top": -352, + "height": 13 + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjZVkWZdF3QUQ=", + "_parent": { + "$ref": "AAAAAAFjZVkWZdF1ahE=" + }, + "font": "Arial;13;1", + "left": 277, + "top": 31, + "width": 81.30078125, + "height": 13, + "text": "SafeContract" + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjZVkWZdF42EY=", + "_parent": { + "$ref": "AAAAAAFjZVkWZdF1ahE=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -512, + "top": -352, + "width": 80.9072265625, + "height": 13, + "text": "(from Model1)" + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjZVkWZdF5brg=", + "_parent": { + "$ref": "AAAAAAFjZVkWZdF1ahE=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -512, + "top": -352, + "height": 13, + "horizontalAlignment": 1 + } + ], + "font": "Arial;13;0", + "left": 272, + "top": 24, + "width": 91.30078125, + "height": 25, + "stereotypeLabel": { + "$ref": "AAAAAAFjZVkWZdF26vI=" + }, + "nameLabel": { + "$ref": "AAAAAAFjZVkWZdF3QUQ=" + }, + "namespaceLabel": { + "$ref": "AAAAAAFjZVkWZdF42EY=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjZVkWZdF5brg=" + } + }, + { + "_type": "UMLAttributeCompartmentView", + "_id": "AAAAAAFjZVkWZtF6OAU=", + "_parent": { + "$ref": "AAAAAAFjZVkWY9F0DJI=" + }, + "model": { + "$ref": "AAAAAAFjZVkWXtFy5Gw=" + }, + "subViews": [ + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFjZVmJ19Gii5Y=", + "_parent": { + "$ref": "AAAAAAFjZVkWZtF6OAU=" + }, + "model": { + "$ref": "AAAAAAFjZVmJx9Gf9mM=" + }, + "font": "Arial;13;0", + "left": 277, + "top": 54, + "width": 81.30078125, + "height": 13, + "text": "+address", + "horizontalAlignment": 0 + } + ], + "font": "Arial;13;0", + "left": 272, + "top": 49, + "width": 91.30078125, + "height": 23 + }, + { + "_type": "UMLOperationCompartmentView", + "_id": "AAAAAAFjZVkWZtF7u8Q=", + "_parent": { + "$ref": "AAAAAAFjZVkWY9F0DJI=" + }, + "model": { + "$ref": "AAAAAAFjZVkWXtFy5Gw=" + }, + "subViews": [ + { + "_type": "UMLOperationView", + "_id": "AAAAAAFjggvgeiTqAoU=", + "_parent": { + "$ref": "AAAAAAFjZVkWZtF7u8Q=" + }, + "model": { + "$ref": "AAAAAAFjggvgYCTn+0Y=" + }, + "font": "Arial;13;0", + "left": 277, + "top": 77, + "width": 81.30078125, + "height": 13, + "text": "+getBalance()", + "horizontalAlignment": 0 + } + ], + "font": "Arial;13;0", + "left": 272, + "top": 72, + "width": 91.30078125, + "height": 23 + }, + { + "_type": "UMLReceptionCompartmentView", + "_id": "AAAAAAFjZVkWZ9F8kOI=", + "_parent": { + "$ref": "AAAAAAFjZVkWY9F0DJI=" + }, + "model": { + "$ref": "AAAAAAFjZVkWXtFy5Gw=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -256, + "top": -176, + "width": 10, + "height": 10 + }, + { + "_type": "UMLTemplateParameterCompartmentView", + "_id": "AAAAAAFjZVkWZ9F9x/w=", + "_parent": { + "$ref": "AAAAAAFjZVkWY9F0DJI=" + }, + "model": { + "$ref": "AAAAAAFjZVkWXtFy5Gw=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -256, + "top": -176, + "width": 10, + "height": 10 + } + ], + "font": "Arial;13;0", + "containerChangeable": true, + "left": 272, + "top": 24, + "width": 91.30078125, + "height": 71, + "nameCompartment": { + "$ref": "AAAAAAFjZVkWZdF1ahE=" + }, + "attributeCompartment": { + "$ref": "AAAAAAFjZVkWZtF6OAU=" + }, + "operationCompartment": { + "$ref": "AAAAAAFjZVkWZtF7u8Q=" + }, + "receptionCompartment": { + "$ref": "AAAAAAFjZVkWZ9F8kOI=" + }, + "templateParameterCompartment": { + "$ref": "AAAAAAFjZVkWZ9F9x/w=" + } + }, + { + "_type": "UMLAssociationView", + "_id": "AAAAAAFjZVyxCdKaFJU=", + "_parent": { + "$ref": "AAAAAAFjY6G3Z9EfZ3Q=" + }, + "model": { + "$ref": "AAAAAAFjZVyxCNKWAqg=" + }, + "subViews": [ + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZVyxCdKbT6s=", + "_parent": { + "$ref": "AAAAAAFjZVyxCdKaFJU=" + }, + "model": { + "$ref": "AAAAAAFjZVyxCNKWAqg=" + }, + "font": "Arial;13;0", + "left": 97, + "top": 143, + "width": 125, + "height": 13, + "alpha": -1.0246964743925102, + "distance": 80.00624975587844, + "hostEdge": { + "$ref": "AAAAAAFjZVyxCdKaFJU=" + }, + "edgePosition": 1, + "text": "+safeAddress" + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZVyxCdKcoog=", + "_parent": { + "$ref": "AAAAAAFjZVyxCdKaFJU=" + }, + "model": { + "$ref": "AAAAAAFjZVyxCNKWAqg=" + }, + "visible": null, + "font": "Arial;13;0", + "left": 262, + "top": 171, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 30, + "hostEdge": { + "$ref": "AAAAAAFjZVyxCdKaFJU=" + }, + "edgePosition": 1 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZVyxCdKdY74=", + "_parent": { + "$ref": "AAAAAAFjZVyxCdKaFJU=" + }, + "model": { + "$ref": "AAAAAAFjZVyxCNKWAqg=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 227, + "top": 143, + "height": 13, + "alpha": -1.5707963267948966, + "distance": 15, + "hostEdge": { + "$ref": "AAAAAAFjZVyxCdKaFJU=" + }, + "edgePosition": 1 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZVyxCdKeqk8=", + "_parent": { + "$ref": "AAAAAAFjZVyxCdKaFJU=" + }, + "model": { + "$ref": "AAAAAAFjZVyxCNKX3Ck=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 284, + "top": 118, + "height": 13, + "alpha": 0.5235987755982988, + "distance": 30, + "hostEdge": { + "$ref": "AAAAAAFjZVyxCdKaFJU=" + }, + "edgePosition": 2 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZVyxCdKfjL4=", + "_parent": { + "$ref": "AAAAAAFjZVyxCdKaFJU=" + }, + "model": { + "$ref": "AAAAAAFjZVyxCNKX3Ck=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 293, + "top": 128, + "height": 13, + "alpha": 0.7853981633974483, + "distance": 40, + "hostEdge": { + "$ref": "AAAAAAFjZVyxCdKaFJU=" + }, + "edgePosition": 2 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZVyxCdKgNXo=", + "_parent": { + "$ref": "AAAAAAFjZVyxCdKaFJU=" + }, + "model": { + "$ref": "AAAAAAFjZVyxCNKX3Ck=" + }, + "font": "Arial;13;0", + "left": 262, + "top": 98, + "width": 7.22998046875, + "height": 13, + "alpha": -0.5235987755982988, + "distance": 25, + "hostEdge": { + "$ref": "AAAAAAFjZVyxCdKaFJU=" + }, + "edgePosition": 2, + "text": "1" + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZVyxCdKhQHg=", + "_parent": { + "$ref": "AAAAAAFjZVyxCdKaFJU=" + }, + "model": { + "$ref": "AAAAAAFjZVyxCNKYy0Y=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 216, + "top": 205, + "height": 13, + "alpha": -0.5235987755982988, + "distance": 30, + "hostEdge": { + "$ref": "AAAAAAFjZVyxCdKaFJU=" + } + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZVyxCdKixGE=", + "_parent": { + "$ref": "AAAAAAFjZVyxCdKaFJU=" + }, + "model": { + "$ref": "AAAAAAFjZVyxCNKYy0Y=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 228, + "top": 212, + "height": 13, + "alpha": -0.7853981633974483, + "distance": 40, + "hostEdge": { + "$ref": "AAAAAAFjZVyxCdKaFJU=" + } + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZVyxCdKjYbo=", + "_parent": { + "$ref": "AAAAAAFjZVyxCdKaFJU=" + }, + "model": { + "$ref": "AAAAAAFjZVyxCNKYy0Y=" + }, + "font": "Arial;13;0", + "left": 189, + "top": 192, + "width": 7.22998046875, + "height": 13, + "alpha": 0.5235987755982988, + "distance": 25, + "hostEdge": { + "$ref": "AAAAAAFjZVyxCdKaFJU=" + }, + "text": "1" + }, + { + "_type": "UMLQualifierCompartmentView", + "_id": "AAAAAAFjZVyxCdKk1Ds=", + "_parent": { + "$ref": "AAAAAAFjZVyxCdKaFJU=" + }, + "model": { + "$ref": "AAAAAAFjZVyxCNKX3Ck=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -288, + "top": -192, + "width": 10, + "height": 10 + }, + { + "_type": "UMLQualifierCompartmentView", + "_id": "AAAAAAFjZVyxCdKl4po=", + "_parent": { + "$ref": "AAAAAAFjZVyxCdKaFJU=" + }, + "model": { + "$ref": "AAAAAAFjZVyxCNKYy0Y=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -288, + "top": -192, + "width": 10, + "height": 10 + } + ], + "font": "Arial;13;0", + "head": { + "$ref": "AAAAAAFjY6HKKtElWWg=" + }, + "tail": { + "$ref": "AAAAAAFjZVkWY9F0DJI=" + }, + "lineStyle": 1, + "points": "289:95;189:223", + "showVisibility": true, + "nameLabel": { + "$ref": "AAAAAAFjZVyxCdKbT6s=" + }, + "stereotypeLabel": { + "$ref": "AAAAAAFjZVyxCdKcoog=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjZVyxCdKdY74=" + }, + "tailRoleNameLabel": { + "$ref": "AAAAAAFjZVyxCdKeqk8=" + }, + "tailPropertyLabel": { + "$ref": "AAAAAAFjZVyxCdKfjL4=" + }, + "tailMultiplicityLabel": { + "$ref": "AAAAAAFjZVyxCdKgNXo=" + }, + "headRoleNameLabel": { + "$ref": "AAAAAAFjZVyxCdKhQHg=" + }, + "headPropertyLabel": { + "$ref": "AAAAAAFjZVyxCdKixGE=" + }, + "headMultiplicityLabel": { + "$ref": "AAAAAAFjZVyxCdKjYbo=" + }, + "tailQualifiersCompartment": { + "$ref": "AAAAAAFjZVyxCdKk1Ds=" + }, + "headQualifiersCompartment": { + "$ref": "AAAAAAFjZVyxCdKl4po=" + } + }, + { + "_type": "UMLClassView", + "_id": "AAAAAAFjZVzBLtLhCos=", + "_parent": { + "$ref": "AAAAAAFjY6G3Z9EfZ3Q=" + }, + "model": { + "$ref": "AAAAAAFjZVzBLtLf25I=" + }, + "subViews": [ + { + "_type": "UMLNameCompartmentView", + "_id": "AAAAAAFjZVzBLtLivjE=", + "_parent": { + "$ref": "AAAAAAFjZVzBLtLhCos=" + }, + "model": { + "$ref": "AAAAAAFjZVzBLtLf25I=" + }, + "subViews": [ + { + "_type": "LabelView", + "_id": "AAAAAAFjZVzBMNLjqoE=", + "_parent": { + "$ref": "AAAAAAFjZVzBLtLivjE=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -336, + "top": -128, + "height": 13 + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjZVzBMNLklug=", + "_parent": { + "$ref": "AAAAAAFjZVzBLtLivjE=" + }, + "font": "Arial;13;1", + "left": 597, + "top": 207, + "width": 132.615234375, + "height": 13, + "text": "SafeTransaction" + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjZVzBMNLltXI=", + "_parent": { + "$ref": "AAAAAAFjZVzBLtLivjE=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -336, + "top": -128, + "width": 80.9072265625, + "height": 13, + "text": "(from Model1)" + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjZVzBMNLmiEU=", + "_parent": { + "$ref": "AAAAAAFjZVzBLtLivjE=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -336, + "top": -128, + "height": 13, + "horizontalAlignment": 1 + } + ], + "font": "Arial;13;0", + "left": 592, + "top": 200, + "width": 142.615234375, + "height": 25, + "stereotypeLabel": { + "$ref": "AAAAAAFjZVzBMNLjqoE=" + }, + "nameLabel": { + "$ref": "AAAAAAFjZVzBMNLklug=" + }, + "namespaceLabel": { + "$ref": "AAAAAAFjZVzBMNLltXI=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjZVzBMNLmiEU=" + } + }, + { + "_type": "UMLAttributeCompartmentView", + "_id": "AAAAAAFjZVzBMdLn97w=", + "_parent": { + "$ref": "AAAAAAFjZVzBLtLhCos=" + }, + "model": { + "$ref": "AAAAAAFjZVzBLtLf25I=" + }, + "subViews": [ + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFjZV2FztR96OQ=", + "_parent": { + "$ref": "AAAAAAFjZVzBMdLn97w=" + }, + "model": { + "$ref": "AAAAAAFjZV2FvtR32t8=" + }, + "font": "Arial;13;0", + "left": 597, + "top": 230, + "width": 132.615234375, + "height": 13, + "text": "+value {readOnly}", + "horizontalAlignment": 0 + }, + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFjZV2RP9SiuBU=", + "_parent": { + "$ref": "AAAAAAFjZVzBMdLn97w=" + }, + "model": { + "$ref": "AAAAAAFjZV2RMtScPXE=" + }, + "font": "Arial;13;0", + "left": 597, + "top": 245, + "width": 132.615234375, + "height": 13, + "text": "+data {readOnly}", + "horizontalAlignment": 0 + }, + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFjZV2ctNTHQsY=", + "_parent": { + "$ref": "AAAAAAFjZVzBMdLn97w=" + }, + "model": { + "$ref": "AAAAAAFjZV2cotTB0YQ=" + }, + "font": "Arial;13;0", + "left": 597, + "top": 260, + "width": 132.615234375, + "height": 13, + "text": "+signatures {readOnly}", + "horizontalAlignment": 0 + }, + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFjZV3BytTsDyo=", + "_parent": { + "$ref": "AAAAAAFjZVzBMdLn97w=" + }, + "model": { + "$ref": "AAAAAAFjZV3BwNTmOho=" + }, + "font": "Arial;13;0", + "left": 597, + "top": 275, + "width": 132.615234375, + "height": 13, + "text": "+type {readOnly}", + "horizontalAlignment": 0 + }, + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFjZV3XRNURlfo=", + "_parent": { + "$ref": "AAAAAAFjZVzBMdLn97w=" + }, + "model": { + "$ref": "AAAAAAFjZV3XNNULrZI=" + }, + "font": "Arial;13;0", + "left": 597, + "top": 290, + "width": 132.615234375, + "height": 13, + "text": "+gas {readOnly}", + "horizontalAlignment": 0 + }, + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFjZV43XtVUtNQ=", + "_parent": { + "$ref": "AAAAAAFjZVzBMdLn97w=" + }, + "model": { + "$ref": "AAAAAAFjZV43UtVOUNQ=" + }, + "font": "Arial;13;0", + "left": 597, + "top": 305, + "width": 132.615234375, + "height": 13, + "text": "+gas_price {readOnly}", + "horizontalAlignment": 0 + }, + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFjjKIIvGRzMOQ=", + "_parent": { + "$ref": "AAAAAAFjZVzBMdLn97w=" + }, + "model": { + "$ref": "AAAAAAFjjKIIqWRtn+4=" + }, + "font": "Arial;13;0", + "left": 597, + "top": 320, + "width": 132.615234375, + "height": 13, + "text": "+to {readOnly}", + "horizontalAlignment": 0 + }, + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFjjK4S9WTzXx0=", + "_parent": { + "$ref": "AAAAAAFjZVzBMdLn97w=" + }, + "model": { + "$ref": "AAAAAAFjjK4S5mTt274=" + }, + "font": "Arial;13;0", + "left": 597, + "top": 335, + "width": 132.615234375, + "height": 13, + "text": "+dataGas", + "horizontalAlignment": 0 + }, + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFjjK4wumUq43A=", + "_parent": { + "$ref": "AAAAAAFjZVzBMdLn97w=" + }, + "model": { + "$ref": "AAAAAAFjjK4wrWUk2Ww=" + }, + "font": "Arial;13;0", + "left": 597, + "top": 350, + "width": 132.615234375, + "height": 13, + "text": "+safe_tx_gas", + "horizontalAlignment": 0 + }, + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFj+iRok9s7B1E=", + "_parent": { + "$ref": "AAAAAAFjZVzBMdLn97w=" + }, + "model": { + "$ref": "AAAAAAFj+iRoc9s1veE=" + }, + "font": "Arial;13;0", + "left": 597, + "top": 365, + "width": 132.615234375, + "height": 13, + "text": "+operation", + "horizontalAlignment": 0 + }, + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFj+ieB+9xkT3g=", + "_parent": { + "$ref": "AAAAAAFjZVzBMdLn97w=" + }, + "model": { + "$ref": "AAAAAAFj+ieBzdxeKrs=" + }, + "font": "Arial;13;0", + "left": 597, + "top": 380, + "width": 132.615234375, + "height": 13, + "text": "+tx_hash", + "horizontalAlignment": 0 + }, + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFj+ieg+Nzym/8=", + "_parent": { + "$ref": "AAAAAAFjZVzBMdLn97w=" + }, + "model": { + "$ref": "AAAAAAFj+ieg3tzsyws=" + }, + "font": "Arial;13;0", + "left": 597, + "top": 395, + "width": 132.615234375, + "height": 13, + "text": "+tx_mined", + "horizontalAlignment": 0 + } + ], + "font": "Arial;13;0", + "left": 592, + "top": 225, + "width": 142.615234375, + "height": 188 + }, + { + "_type": "UMLOperationCompartmentView", + "_id": "AAAAAAFjZVzBMdLoKDo=", + "_parent": { + "$ref": "AAAAAAFjZVzBLtLhCos=" + }, + "model": { + "$ref": "AAAAAAFjZVzBLtLf25I=" + }, + "subViews": [ + { + "_type": "UMLOperationView", + "_id": "AAAAAAFjZWA5iNaK1vM=", + "_parent": { + "$ref": "AAAAAAFjZVzBMdLoKDo=" + }, + "model": { + "$ref": "AAAAAAFjZWA5dtaEsV4=" + }, + "font": "Arial;13;0", + "left": 597, + "top": 418, + "width": 132.615234375, + "height": 13, + "text": "+send()", + "horizontalAlignment": 0 + }, + { + "_type": "UMLOperationView", + "_id": "AAAAAAFjZWChR9bN+Yw=", + "_parent": { + "$ref": "AAAAAAFjZVzBMdLoKDo=" + }, + "model": { + "$ref": "AAAAAAFjZWChPNbHHWo=" + }, + "font": "Arial;13;0", + "left": 597, + "top": 433, + "width": 132.615234375, + "height": 13, + "text": "+estimate()", + "horizontalAlignment": 0 + }, + { + "_type": "UMLOperationView", + "_id": "AAAAAAFjZWELGtco0U0=", + "_parent": { + "$ref": "AAAAAAFjZVzBMdLoKDo=" + }, + "model": { + "$ref": "AAAAAAFjZWELDNcia5k=" + }, + "font": "Arial;13;0", + "left": 597, + "top": 448, + "width": 132.615234375, + "height": 13, + "text": "+setGasOptions()", + "horizontalAlignment": 0 + } + ], + "font": "Arial;13;0", + "left": 592, + "top": 413, + "width": 142.615234375, + "height": 53 + }, + { + "_type": "UMLReceptionCompartmentView", + "_id": "AAAAAAFjZVzBMdLpmQ0=", + "_parent": { + "$ref": "AAAAAAFjZVzBLtLhCos=" + }, + "model": { + "$ref": "AAAAAAFjZVzBLtLf25I=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -168, + "top": -64, + "width": 10, + "height": 10 + }, + { + "_type": "UMLTemplateParameterCompartmentView", + "_id": "AAAAAAFjZVzBMdLqpCQ=", + "_parent": { + "$ref": "AAAAAAFjZVzBLtLhCos=" + }, + "model": { + "$ref": "AAAAAAFjZVzBLtLf25I=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -168, + "top": -64, + "width": 10, + "height": 10 + } + ], + "font": "Arial;13;0", + "containerChangeable": true, + "left": 592, + "top": 200, + "width": 142.615234375, + "height": 266, + "nameCompartment": { + "$ref": "AAAAAAFjZVzBLtLivjE=" + }, + "attributeCompartment": { + "$ref": "AAAAAAFjZVzBMdLn97w=" + }, + "operationCompartment": { + "$ref": "AAAAAAFjZVzBMdLoKDo=" + }, + "receptionCompartment": { + "$ref": "AAAAAAFjZVzBMdLpmQ0=" + }, + "templateParameterCompartment": { + "$ref": "AAAAAAFjZVzBMdLqpCQ=" + } + }, + { + "_type": "UMLAssociationView", + "_id": "AAAAAAFjZV0L3tNUd6c=", + "_parent": { + "$ref": "AAAAAAFjY6G3Z9EfZ3Q=" + }, + "model": { + "$ref": "AAAAAAFjZV0L3dNQLnU=" + }, + "subViews": [ + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZV0L3tNV6Kk=", + "_parent": { + "$ref": "AAAAAAFjZV0L3tNUd6c=" + }, + "model": { + "$ref": "AAAAAAFjZV0L3dNQLnU=" + }, + "font": "Arial;13;0", + "left": 482, + "top": 154, + "width": 81, + "height": 13, + "alpha": 1.1520850297798637, + "distance": 52.88667128870941, + "hostEdge": { + "$ref": "AAAAAAFjZV0L3tNUd6c=" + }, + "edgePosition": 1, + "text": "+safeAddress" + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZV0L3tNWKGU=", + "_parent": { + "$ref": "AAAAAAFjZV0L3tNUd6c=" + }, + "model": { + "$ref": "AAAAAAFjZV0L3dNQLnU=" + }, + "visible": null, + "font": "Arial;13;0", + "left": 494, + "top": 155, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 30, + "hostEdge": { + "$ref": "AAAAAAFjZV0L3tNUd6c=" + }, + "edgePosition": 1 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZV0L3tNXkiQ=", + "_parent": { + "$ref": "AAAAAAFjZV0L3tNUd6c=" + }, + "model": { + "$ref": "AAAAAAFjZV0L3dNQLnU=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 466, + "top": 190, + "height": 13, + "alpha": -1.5707963267948966, + "distance": 15, + "hostEdge": { + "$ref": "AAAAAAFjZV0L3tNUd6c=" + }, + "edgePosition": 1 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZV0L39NYZuc=", + "_parent": { + "$ref": "AAAAAAFjZV0L3tNUd6c=" + }, + "model": { + "$ref": "AAAAAAFjZV0L3dNRSb0=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 391, + "top": 93, + "height": 13, + "alpha": 0.5235987755982988, + "distance": 30, + "hostEdge": { + "$ref": "AAAAAAFjZV0L3tNUd6c=" + }, + "edgePosition": 2 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZV0L39NZN0Y=", + "_parent": { + "$ref": "AAAAAAFjZV0L3tNUd6c=" + }, + "model": { + "$ref": "AAAAAAFjZV0L3dNRSb0=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 401, + "top": 84, + "height": 13, + "alpha": 0.7853981633974483, + "distance": 40, + "hostEdge": { + "$ref": "AAAAAAFjZV0L3tNUd6c=" + }, + "edgePosition": 2 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZV0L39NaeDc=", + "_parent": { + "$ref": "AAAAAAFjZV0L3tNUd6c=" + }, + "model": { + "$ref": "AAAAAAFjZV0L3dNRSb0=" + }, + "font": "Arial;13;0", + "left": 368, + "top": 112, + "width": 7.22998046875, + "height": 13, + "alpha": -0.5235987755982988, + "distance": 25, + "hostEdge": { + "$ref": "AAAAAAFjZV0L3tNUd6c=" + }, + "edgePosition": 2, + "text": "1" + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZV0L39Nb3UA=", + "_parent": { + "$ref": "AAAAAAFjZV0L3tNUd6c=" + }, + "model": { + "$ref": "AAAAAAFjZV0L3tNSaJE=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 579, + "top": 242, + "height": 13, + "alpha": -0.5235987755982988, + "distance": 30, + "hostEdge": { + "$ref": "AAAAAAFjZV0L3tNUd6c=" + } + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZV0L39NctsQ=", + "_parent": { + "$ref": "AAAAAAFjZV0L3tNUd6c=" + }, + "model": { + "$ref": "AAAAAAFjZV0L3tNSaJE=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 586, + "top": 230, + "height": 13, + "alpha": -0.7853981633974483, + "distance": 40, + "hostEdge": { + "$ref": "AAAAAAFjZV0L3tNUd6c=" + } + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZV0L39Nd2fQ=", + "_parent": { + "$ref": "AAAAAAFjZV0L3tNUd6c=" + }, + "model": { + "$ref": "AAAAAAFjZV0L3tNSaJE=" + }, + "font": "Arial;13;0", + "left": 556, + "top": 266, + "width": 20, + "height": 13, + "alpha": 0.5235987755982988, + "distance": 25, + "hostEdge": { + "$ref": "AAAAAAFjZV0L3tNUd6c=" + }, + "text": "0..*" + }, + { + "_type": "UMLQualifierCompartmentView", + "_id": "AAAAAAFjZV0L39NedGk=", + "_parent": { + "$ref": "AAAAAAFjZV0L3tNUd6c=" + }, + "model": { + "$ref": "AAAAAAFjZV0L3dNRSb0=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -288, + "top": -192, + "width": 10, + "height": 10 + }, + { + "_type": "UMLQualifierCompartmentView", + "_id": "AAAAAAFjZV0L39Nfd44=", + "_parent": { + "$ref": "AAAAAAFjZV0L3tNUd6c=" + }, + "model": { + "$ref": "AAAAAAFjZV0L3tNSaJE=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -288, + "top": -192, + "width": 10, + "height": 10 + } + ], + "font": "Arial;13;0", + "head": { + "$ref": "AAAAAAFjZVzBLtLhCos=" + }, + "tail": { + "$ref": "AAAAAAFjZVkWY9F0DJI=" + }, + "lineStyle": 1, + "points": "362:95;591:276", + "showVisibility": true, + "nameLabel": { + "$ref": "AAAAAAFjZV0L3tNV6Kk=" + }, + "stereotypeLabel": { + "$ref": "AAAAAAFjZV0L3tNWKGU=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjZV0L3tNXkiQ=" + }, + "tailRoleNameLabel": { + "$ref": "AAAAAAFjZV0L39NYZuc=" + }, + "tailPropertyLabel": { + "$ref": "AAAAAAFjZV0L39NZN0Y=" + }, + "tailMultiplicityLabel": { + "$ref": "AAAAAAFjZV0L39NaeDc=" + }, + "headRoleNameLabel": { + "$ref": "AAAAAAFjZV0L39Nb3UA=" + }, + "headPropertyLabel": { + "$ref": "AAAAAAFjZV0L39NctsQ=" + }, + "headMultiplicityLabel": { + "$ref": "AAAAAAFjZV0L39Nd2fQ=" + }, + "tailQualifiersCompartment": { + "$ref": "AAAAAAFjZV0L39NedGk=" + }, + "headQualifiersCompartment": { + "$ref": "AAAAAAFjZV0L39Nfd44=" + } + }, + { + "_type": "UMLClassView", + "_id": "AAAAAAFj+h0p0NJDINk=", + "_parent": { + "$ref": "AAAAAAFjY6G3Z9EfZ3Q=" + }, + "model": { + "$ref": "AAAAAAFj+h0pz9JBqIo=" + }, + "subViews": [ + { + "_type": "UMLNameCompartmentView", + "_id": "AAAAAAFj+h0p0NJEQn4=", + "_parent": { + "$ref": "AAAAAAFj+h0p0NJDINk=" + }, + "model": { + "$ref": "AAAAAAFj+h0pz9JBqIo=" + }, + "subViews": [ + { + "_type": "LabelView", + "_id": "AAAAAAFj+h0p0NJFpLU=", + "_parent": { + "$ref": "AAAAAAFj+h0p0NJEQn4=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -96, + "top": -48, + "height": 13 + }, + { + "_type": "LabelView", + "_id": "AAAAAAFj+h0p0NJGxec=", + "_parent": { + "$ref": "AAAAAAFj+h0p0NJEQn4=" + }, + "font": "Arial;13;1", + "left": 277, + "top": 231, + "width": 208.5458984375, + "height": 13, + "text": "SafeFunding" + }, + { + "_type": "LabelView", + "_id": "AAAAAAFj+h0p0NJH2sk=", + "_parent": { + "$ref": "AAAAAAFj+h0p0NJEQn4=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -96, + "top": -48, + "width": 80.9072265625, + "height": 13, + "text": "(from Model1)" + }, + { + "_type": "LabelView", + "_id": "AAAAAAFj+h0p0NJIE/g=", + "_parent": { + "$ref": "AAAAAAFj+h0p0NJEQn4=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -96, + "top": -48, + "height": 13, + "horizontalAlignment": 1 + } + ], + "font": "Arial;13;0", + "left": 272, + "top": 224, + "width": 218.5458984375, + "height": 25, + "stereotypeLabel": { + "$ref": "AAAAAAFj+h0p0NJFpLU=" + }, + "nameLabel": { + "$ref": "AAAAAAFj+h0p0NJGxec=" + }, + "namespaceLabel": { + "$ref": "AAAAAAFj+h0p0NJH2sk=" + }, + "propertyLabel": { + "$ref": "AAAAAAFj+h0p0NJIE/g=" + } + }, + { + "_type": "UMLAttributeCompartmentView", + "_id": "AAAAAAFj+h0p0NJJnxA=", + "_parent": { + "$ref": "AAAAAAFj+h0p0NJDINk=" + }, + "model": { + "$ref": "AAAAAAFj+h0pz9JBqIo=" + }, + "subViews": [ + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFj+h1iU9K8uP4=", + "_parent": { + "$ref": "AAAAAAFj+h0p0NJJnxA=" + }, + "model": { + "$ref": "AAAAAAFj+h1iF9KzWdE=" + }, + "font": "Arial;13;0", + "left": 277, + "top": 254, + "width": 208.5458984375, + "height": 13, + "text": "+safe_funded = false", + "horizontalAlignment": 0 + }, + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFj+h2QktLtrKg=", + "_parent": { + "$ref": "AAAAAAFj+h0p0NJJnxA=" + }, + "model": { + "$ref": "AAAAAAFj+h2QdtLkZmM=" + }, + "font": "Arial;13;0", + "left": 277, + "top": 269, + "width": 208.5458984375, + "height": 13, + "text": "+deployer_funded = false", + "horizontalAlignment": 0 + }, + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFj+h5gT9OTIIE=", + "_parent": { + "$ref": "AAAAAAFj+h0p0NJJnxA=" + }, + "model": { + "$ref": "AAAAAAFj+h5gNNOKC3w=" + }, + "font": "Arial;13;0", + "left": 277, + "top": 284, + "width": 208.5458984375, + "height": 13, + "text": "+deployer_funded_tx_hash {unique}", + "horizontalAlignment": 0 + }, + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFj+h74VdR57q4=", + "_parent": { + "$ref": "AAAAAAFj+h0p0NJJnxA=" + }, + "model": { + "$ref": "AAAAAAFj+h74ONRw0a4=" + }, + "font": "Arial;13;0", + "left": 277, + "top": 299, + "width": 208.5458984375, + "height": 13, + "text": "+safe_deployed = false", + "horizontalAlignment": 0 + }, + { + "_type": "UMLAttributeView", + "_id": "AAAAAAFj+h8c7NSqAYc=", + "_parent": { + "$ref": "AAAAAAFj+h0p0NJJnxA=" + }, + "model": { + "$ref": "AAAAAAFj+h8cydShy4E=" + }, + "font": "Arial;13;0", + "left": 277, + "top": 314, + "width": 208.5458984375, + "height": 13, + "text": "+safe_deployed_tx_hash {unique}", + "horizontalAlignment": 0 + } + ], + "font": "Arial;13;0", + "left": 272, + "top": 249, + "width": 218.5458984375, + "height": 83 + }, + { + "_type": "UMLOperationCompartmentView", + "_id": "AAAAAAFj+h0p0NJKhn4=", + "_parent": { + "$ref": "AAAAAAFj+h0p0NJDINk=" + }, + "model": { + "$ref": "AAAAAAFj+h0pz9JBqIo=" + }, + "subViews": [ + { + "_type": "UMLOperationView", + "_id": "AAAAAAFj+h++TtXGd9Q=", + "_parent": { + "$ref": "AAAAAAFj+h0p0NJKhn4=" + }, + "model": { + "$ref": "AAAAAAFj+h++D9W9fl4=" + }, + "font": "Arial;13;0", + "left": 277, + "top": 337, + "width": 208.5458984375, + "height": 13, + "text": "+is_all_funded()", + "horizontalAlignment": 0 + }, + { + "_type": "UMLOperationView", + "_id": "AAAAAAFj+iAqAdX3eYI=", + "_parent": { + "$ref": "AAAAAAFj+h0p0NJKhn4=" + }, + "model": { + "$ref": "AAAAAAFj+iApv9XuCiQ=" + }, + "font": "Arial;13;0", + "left": 277, + "top": 352, + "width": 208.5458984375, + "height": 13, + "text": "+status()", + "horizontalAlignment": 0 + } + ], + "font": "Arial;13;0", + "left": 272, + "top": 332, + "width": 218.5458984375, + "height": 38 + }, + { + "_type": "UMLReceptionCompartmentView", + "_id": "AAAAAAFj+h0p0NJLd5o=", + "_parent": { + "$ref": "AAAAAAFj+h0p0NJDINk=" + }, + "model": { + "$ref": "AAAAAAFj+h0pz9JBqIo=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -48, + "top": -24, + "width": 10, + "height": 10 + }, + { + "_type": "UMLTemplateParameterCompartmentView", + "_id": "AAAAAAFj+h0p0NJMqn0=", + "_parent": { + "$ref": "AAAAAAFj+h0p0NJDINk=" + }, + "model": { + "$ref": "AAAAAAFj+h0pz9JBqIo=" + }, + "visible": false, + "font": "Arial;13;0", + "left": -48, + "top": -24, + "width": 10, + "height": 10 + } + ], + "font": "Arial;13;0", + "containerChangeable": true, + "left": 272, + "top": 224, + "width": 218.5458984375, + "height": 161, + "nameCompartment": { + "$ref": "AAAAAAFj+h0p0NJEQn4=" + }, + "attributeCompartment": { + "$ref": "AAAAAAFj+h0p0NJJnxA=" + }, + "operationCompartment": { + "$ref": "AAAAAAFj+h0p0NJKhn4=" + }, + "receptionCompartment": { + "$ref": "AAAAAAFj+h0p0NJLd5o=" + }, + "templateParameterCompartment": { + "$ref": "AAAAAAFj+h0p0NJMqn0=" + } + }, + { + "_type": "UMLAssociationView", + "_id": "AAAAAAFj+iFw2dfhx1U=", + "_parent": { + "$ref": "AAAAAAFjY6G3Z9EfZ3Q=" + }, + "model": { + "$ref": "AAAAAAFj+iFw2NfdgDQ=" + }, + "subViews": [ + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFj+iFw2dfiryc=", + "_parent": { + "$ref": "AAAAAAFj+iFw2dfhx1U=" + }, + "model": { + "$ref": "AAAAAAFj+iFw2NfdgDQ=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 356, + "top": 149, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 15, + "hostEdge": { + "$ref": "AAAAAAFj+iFw2dfhx1U=" + }, + "edgePosition": 1 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFj+iFw2dfjhQU=", + "_parent": { + "$ref": "AAAAAAFj+iFw2dfhx1U=" + }, + "model": { + "$ref": "AAAAAAFj+iFw2NfdgDQ=" + }, + "visible": null, + "font": "Arial;13;0", + "left": 370, + "top": 145, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 30, + "hostEdge": { + "$ref": "AAAAAAFj+iFw2dfhx1U=" + }, + "edgePosition": 1 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFj+iFw2dfkxN4=", + "_parent": { + "$ref": "AAAAAAFj+iFw2dfhx1U=" + }, + "model": { + "$ref": "AAAAAAFj+iFw2NfdgDQ=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 327, + "top": 156, + "height": 13, + "alpha": -1.5707963267948966, + "distance": 15, + "hostEdge": { + "$ref": "AAAAAAFj+iFw2dfhx1U=" + }, + "edgePosition": 1 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFj+iFw2dfl8ho=", + "_parent": { + "$ref": "AAAAAAFj+iFw2dfhx1U=" + }, + "model": { + "$ref": "AAAAAAFj+iFw2NfehiY=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 347, + "top": 110, + "height": 13, + "alpha": 0.5235987755982988, + "distance": 30, + "hostEdge": { + "$ref": "AAAAAAFj+iFw2dfhx1U=" + }, + "edgePosition": 2 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFj+iFw2dfmFmg=", + "_parent": { + "$ref": "AAAAAAFj+iFw2dfhx1U=" + }, + "model": { + "$ref": "AAAAAAFj+iFw2NfehiY=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 360, + "top": 109, + "height": 13, + "alpha": 0.7853981633974483, + "distance": 40, + "hostEdge": { + "$ref": "AAAAAAFj+iFw2dfhx1U=" + }, + "edgePosition": 2 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFj+iFw2dfneVw=", + "_parent": { + "$ref": "AAAAAAFj+iFw2dfhx1U=" + }, + "model": { + "$ref": "AAAAAAFj+iFw2NfehiY=" + }, + "font": "Arial;13;0", + "left": 316, + "top": 113, + "width": 7.22998046875, + "height": 13, + "alpha": -0.5235987755982988, + "distance": 25, + "hostEdge": { + "$ref": "AAAAAAFj+iFw2dfhx1U=" + }, + "edgePosition": 2, + "text": "1" + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFj+iFw2dfo1FE=", + "_parent": { + "$ref": "AAAAAAFj+iFw2dfhx1U=" + }, + "model": { + "$ref": "AAAAAAFj+iFw2Nffu5M=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 367, + "top": 188, + "height": 13, + "alpha": -0.5235987755982988, + "distance": 30, + "hostEdge": { + "$ref": "AAAAAAFj+iFw2dfhx1U=" + } + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFj+iFw2dfphlY=", + "_parent": { + "$ref": "AAAAAAFj+iFw2dfhx1U=" + }, + "model": { + "$ref": "AAAAAAFj+iFw2Nffu5M=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 379, + "top": 182, + "height": 13, + "alpha": -0.7853981633974483, + "distance": 40, + "hostEdge": { + "$ref": "AAAAAAFj+iFw2dfhx1U=" + } + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFj+iFw2dfqLR0=", + "_parent": { + "$ref": "AAAAAAFj+iFw2dfhx1U=" + }, + "model": { + "$ref": "AAAAAAFj+iFw2Nffu5M=" + }, + "font": "Arial;13;0", + "left": 332, + "top": 199, + "width": 19.5126953125, + "height": 13, + "alpha": 0.5235987755982988, + "distance": 25, + "hostEdge": { + "$ref": "AAAAAAFj+iFw2dfhx1U=" + }, + "text": "1" + }, + { + "_type": "UMLQualifierCompartmentView", + "_id": "AAAAAAFj+iFw2dfrnYY=", + "_parent": { + "$ref": "AAAAAAFj+iFw2dfhx1U=" + }, + "model": { + "$ref": "AAAAAAFj+iFw2NfehiY=" + }, + "visible": false, + "font": "Arial;13;0", + "width": 10, + "height": 10 + }, + { + "_type": "UMLQualifierCompartmentView", + "_id": "AAAAAAFj+iFw2dfsszc=", + "_parent": { + "$ref": "AAAAAAFj+iFw2dfhx1U=" + }, + "model": { + "$ref": "AAAAAAFj+iFw2Nffu5M=" + }, + "visible": false, + "font": "Arial;13;0", + "width": 10, + "height": 10 + } + ], + "font": "Arial;13;0", + "head": { + "$ref": "AAAAAAFj+h0p0NJDINk=" + }, + "tail": { + "$ref": "AAAAAAFjZVkWY9F0DJI=" + }, + "lineStyle": 1, + "points": "326:95;359:223", + "showVisibility": true, + "nameLabel": { + "$ref": "AAAAAAFj+iFw2dfiryc=" + }, + "stereotypeLabel": { + "$ref": "AAAAAAFj+iFw2dfjhQU=" + }, + "propertyLabel": { + "$ref": "AAAAAAFj+iFw2dfkxN4=" + }, + "tailRoleNameLabel": { + "$ref": "AAAAAAFj+iFw2dfl8ho=" + }, + "tailPropertyLabel": { + "$ref": "AAAAAAFj+iFw2dfmFmg=" + }, + "tailMultiplicityLabel": { + "$ref": "AAAAAAFj+iFw2dfneVw=" + }, + "headRoleNameLabel": { + "$ref": "AAAAAAFj+iFw2dfo1FE=" + }, + "headPropertyLabel": { + "$ref": "AAAAAAFj+iFw2dfphlY=" + }, + "headMultiplicityLabel": { + "$ref": "AAAAAAFj+iFw2dfqLR0=" + }, + "tailQualifiersCompartment": { + "$ref": "AAAAAAFj+iFw2dfrnYY=" + }, + "headQualifiersCompartment": { + "$ref": "AAAAAAFj+iFw2dfsszc=" + } + } + ] + }, + { + "_type": "UMLClass", + "_id": "AAAAAAFjY6HKKdEjP+Q=", + "_parent": { + "$ref": "AAAAAAFjY6G3ZtEesk4=" + }, + "name": "SafeCreation", + "attributes": [ + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjY6IvftFO+jk=", + "_parent": { + "$ref": "AAAAAAFjY6HKKdEjP+Q=" + }, + "name": "owners", + "type": "", + "multiplicity": "1..*", + "isReadOnly": true + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjY6Lb1NFVU6k=", + "_parent": { + "$ref": "AAAAAAFjY6HKKdEjP+Q=" + }, + "name": "threshold", + "isReadOnly": true + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjY6Mi2tFdm9w=", + "_parent": { + "$ref": "AAAAAAFjY6HKKdEjP+Q=" + }, + "name": "gas_price", + "isReadOnly": true + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjY6Nig9FkSv0=", + "_parent": { + "$ref": "AAAAAAFjY6HKKdEjP+Q=" + }, + "name": "gas", + "type": "", + "isReadOnly": true + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjZWLVb9fXtHo=", + "_parent": { + "$ref": "AAAAAAFjY6HKKdEjP+Q=" + }, + "name": "tx_hash", + "isReadOnly": true, + "isUnique": true + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjZWlj/dg/20w=", + "_parent": { + "$ref": "AAAAAAFjY6HKKdEjP+Q=" + }, + "name": "deployer", + "type": "", + "isReadOnly": true, + "isUnique": true + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjggzVzCZDMts=", + "_parent": { + "$ref": "AAAAAAFjY6HKKdEjP+Q=" + }, + "name": "signed_tx", + "isReadOnly": true, + "isUnique": true + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjjJ4EBGDxkSY=", + "_parent": { + "$ref": "AAAAAAFjY6HKKdEjP+Q=" + }, + "name": "v", + "type": "", + "isReadOnly": true + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjjJ4gSGEouXY=", + "_parent": { + "$ref": "AAAAAAFjY6HKKdEjP+Q=" + }, + "name": "r", + "type": "", + "isReadOnly": true + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjjJ4o7WFN8H4=", + "_parent": { + "$ref": "AAAAAAFjY6HKKdEjP+Q=" + }, + "name": "s", + "type": "", + "isReadOnly": true + } + ], + "operations": [ + { + "_type": "UMLOperation", + "_id": "AAAAAAFjZWGdDtePF7Y=", + "_parent": { + "$ref": "AAAAAAFjY6HKKdEjP+Q=" + }, + "name": "sendETHtoDeployer" + } + ] + }, + { + "_type": "UMLClass", + "_id": "AAAAAAFjZVkWXtFy5Gw=", + "_parent": { + "$ref": "AAAAAAFjY6G3ZtEesk4=" + }, + "name": "SafeContract", + "ownedElements": [ + { + "_type": "UMLAssociation", + "_id": "AAAAAAFjZVyxCNKWAqg=", + "_parent": { + "$ref": "AAAAAAFjZVkWXtFy5Gw=" + }, + "name": "safeAddress", + "end1": { + "_type": "UMLAssociationEnd", + "_id": "AAAAAAFjZVyxCNKX3Ck=", + "_parent": { + "$ref": "AAAAAAFjZVyxCNKWAqg=" + }, + "reference": { + "$ref": "AAAAAAFjZVkWXtFy5Gw=" + }, + "multiplicity": "1" + }, + "end2": { + "_type": "UMLAssociationEnd", + "_id": "AAAAAAFjZVyxCNKYy0Y=", + "_parent": { + "$ref": "AAAAAAFjZVyxCNKWAqg=" + }, + "reference": { + "$ref": "AAAAAAFjY6HKKdEjP+Q=" + }, + "multiplicity": "1" + } + }, + { + "_type": "UMLAssociation", + "_id": "AAAAAAFjZV0L3dNQLnU=", + "_parent": { + "$ref": "AAAAAAFjZVkWXtFy5Gw=" + }, + "name": "safeAddress", + "end1": { + "_type": "UMLAssociationEnd", + "_id": "AAAAAAFjZV0L3dNRSb0=", + "_parent": { + "$ref": "AAAAAAFjZV0L3dNQLnU=" + }, + "reference": { + "$ref": "AAAAAAFjZVkWXtFy5Gw=" + }, + "multiplicity": "1" + }, + "end2": { + "_type": "UMLAssociationEnd", + "_id": "AAAAAAFjZV0L3tNSaJE=", + "_parent": { + "$ref": "AAAAAAFjZV0L3dNQLnU=" + }, + "reference": { + "$ref": "AAAAAAFjZVzBLtLf25I=" + }, + "multiplicity": "0..*" + } + }, + { + "_type": "UMLAssociation", + "_id": "AAAAAAFj+iFw2NfdgDQ=", + "_parent": { + "$ref": "AAAAAAFjZVkWXtFy5Gw=" + }, + "end1": { + "_type": "UMLAssociationEnd", + "_id": "AAAAAAFj+iFw2NfehiY=", + "_parent": { + "$ref": "AAAAAAFj+iFw2NfdgDQ=" + }, + "reference": { + "$ref": "AAAAAAFjZVkWXtFy5Gw=" + }, + "multiplicity": "1" + }, + "end2": { + "_type": "UMLAssociationEnd", + "_id": "AAAAAAFj+iFw2Nffu5M=", + "_parent": { + "$ref": "AAAAAAFj+iFw2NfdgDQ=" + }, + "reference": { + "$ref": "AAAAAAFj+h0pz9JBqIo=" + }, + "multiplicity": "1" + } + } + ], + "attributes": [ + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjZVmJx9Gf9mM=", + "_parent": { + "$ref": "AAAAAAFjZVkWXtFy5Gw=" + }, + "name": "address", + "ownedElements": [ + { + "_type": "UMLConstraint", + "_id": "AAAAAAFjZVmwUNGmcmo=", + "_parent": { + "$ref": "AAAAAAFjZVmJx9Gf9mM=" + }, + "name": "Ethereum Address", + "stereotype": "string", + "specification": "Hexadecimal String, length 40 without prefix" + } + ], + "type": "" + } + ], + "operations": [ + { + "_type": "UMLOperation", + "_id": "AAAAAAFjggvgYCTn+0Y=", + "_parent": { + "$ref": "AAAAAAFjZVkWXtFy5Gw=" + }, + "name": "getBalance" + } + ] + }, + { + "_type": "UMLClass", + "_id": "AAAAAAFjZVzBLtLf25I=", + "_parent": { + "$ref": "AAAAAAFjY6G3ZtEesk4=" + }, + "name": "SafeTransaction", + "attributes": [ + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjZV2FvtR32t8=", + "_parent": { + "$ref": "AAAAAAFjZVzBLtLf25I=" + }, + "name": "value", + "type": "", + "isReadOnly": true + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjZV2RMtScPXE=", + "_parent": { + "$ref": "AAAAAAFjZVzBLtLf25I=" + }, + "name": "data", + "type": "", + "isReadOnly": true + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjZV2cotTB0YQ=", + "_parent": { + "$ref": "AAAAAAFjZVzBLtLf25I=" + }, + "name": "signatures", + "type": "", + "isReadOnly": true + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjZV3BwNTmOho=", + "_parent": { + "$ref": "AAAAAAFjZVzBLtLf25I=" + }, + "name": "type", + "type": "", + "isReadOnly": true + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjZV3XNNULrZI=", + "_parent": { + "$ref": "AAAAAAFjZVzBLtLf25I=" + }, + "name": "gas", + "type": "", + "isReadOnly": true + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjZV43UtVOUNQ=", + "_parent": { + "$ref": "AAAAAAFjZVzBLtLf25I=" + }, + "name": "gas_price", + "isReadOnly": true + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjjKIIqWRtn+4=", + "_parent": { + "$ref": "AAAAAAFjZVzBLtLf25I=" + }, + "name": "to", + "type": "", + "isReadOnly": true + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjjK4S5mTt274=", + "_parent": { + "$ref": "AAAAAAFjZVzBLtLf25I=" + }, + "name": "dataGas", + "type": "" + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjjK4wrWUk2Ww=", + "_parent": { + "$ref": "AAAAAAFjZVzBLtLf25I=" + }, + "name": "safe_tx_gas" + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFj+iRoc9s1veE=", + "_parent": { + "$ref": "AAAAAAFjZVzBLtLf25I=" + }, + "name": "operation" + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFj+ieBzdxeKrs=", + "_parent": { + "$ref": "AAAAAAFjZVzBLtLf25I=" + }, + "name": "tx_hash" + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFj+ieg3tzsyws=", + "_parent": { + "$ref": "AAAAAAFjZVzBLtLf25I=" + }, + "name": "tx_mined" + } + ], + "operations": [ + { + "_type": "UMLOperation", + "_id": "AAAAAAFjZWA5dtaEsV4=", + "_parent": { + "$ref": "AAAAAAFjZVzBLtLf25I=" + }, + "name": "send" + }, + { + "_type": "UMLOperation", + "_id": "AAAAAAFjZWChPNbHHWo=", + "_parent": { + "$ref": "AAAAAAFjZVzBLtLf25I=" + }, + "name": "estimate" + }, + { + "_type": "UMLOperation", + "_id": "AAAAAAFjZWELDNcia5k=", + "_parent": { + "$ref": "AAAAAAFjZVzBLtLf25I=" + }, + "name": "setGasOptions" + } + ] + }, + { + "_type": "UMLClass", + "_id": "AAAAAAFj+h0pz9JBqIo=", + "_parent": { + "$ref": "AAAAAAFjY6G3ZtEesk4=" + }, + "name": "SafeFunding", + "attributes": [ + { + "_type": "UMLAttribute", + "_id": "AAAAAAFj+h1iF9KzWdE=", + "_parent": { + "$ref": "AAAAAAFj+h0pz9JBqIo=" + }, + "name": "safe_funded", + "defaultValue": "false" + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFj+h2QdtLkZmM=", + "_parent": { + "$ref": "AAAAAAFj+h0pz9JBqIo=" + }, + "name": "deployer_funded", + "defaultValue": "false" + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFj+h5gNNOKC3w=", + "_parent": { + "$ref": "AAAAAAFj+h0pz9JBqIo=" + }, + "name": "deployer_funded_tx_hash", + "isUnique": true + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFj+h74ONRw0a4=", + "_parent": { + "$ref": "AAAAAAFj+h0pz9JBqIo=" + }, + "name": "safe_deployed", + "defaultValue": "false" + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFj+h8cydShy4E=", + "_parent": { + "$ref": "AAAAAAFj+h0pz9JBqIo=" + }, + "name": "safe_deployed_tx_hash", + "isUnique": true + } + ], + "operations": [ + { + "_type": "UMLOperation", + "_id": "AAAAAAFj+h++D9W9fl4=", + "_parent": { + "$ref": "AAAAAAFj+h0pz9JBqIo=" + }, + "name": "is_all_funded" + }, + { + "_type": "UMLOperation", + "_id": "AAAAAAFj+iApv9XuCiQ=", + "_parent": { + "$ref": "AAAAAAFj+h0pz9JBqIo=" + }, + "name": "status" + } + ] + } + ] + }, + { + "_type": "UMLCollaboration", + "_id": "AAAAAAFjZXeWS+imsis=", + "_parent": { + "$ref": "AAAAAAFF+h6SjaM2Hec=" + }, + "name": "Collaboration2", + "ownedElements": [ + { + "_type": "UMLInteraction", + "_id": "AAAAAAFjZXeWTOinlcs=", + "_parent": { + "$ref": "AAAAAAFjZXeWS+imsis=" + }, + "name": "Interaction1", + "ownedElements": [ + { + "_type": "UMLSequenceDiagram", + "_id": "AAAAAAFjZXeWTOioQJg=", + "_parent": { + "$ref": "AAAAAAFjZXeWTOinlcs=" + }, + "name": "getCreationTX", + "ownedViews": [ + { + "_type": "UMLFrameView", + "_id": "AAAAAAFjZXeWTOipy1w=", + "_parent": { + "$ref": "AAAAAAFjZXeWTOioQJg=" + }, + "model": { + "$ref": "AAAAAAFjZXeWTOioQJg=" + }, + "subViews": [ + { + "_type": "LabelView", + "_id": "AAAAAAFjZXeWTOiqo94=", + "_parent": { + "$ref": "AAAAAAFjZXeWTOipy1w=" + }, + "font": "Arial;13;0", + "left": 75.97900390625, + "top": 10, + "width": 84.82080078125, + "height": 13, + "text": "getCreationTX" + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjZXeWTeir4dE=", + "_parent": { + "$ref": "AAAAAAFjZXeWTOipy1w=" + }, + "font": "Arial;13;1", + "left": 10, + "top": 10, + "width": 60.97900390625, + "height": 13, + "text": "interaction" + } + ], + "font": "Arial;13;0", + "left": 5, + "top": 5, + "width": 580, + "height": 292, + "nameLabel": { + "$ref": "AAAAAAFjZXeWTOiqo94=" + }, + "frameTypeLabel": { + "$ref": "AAAAAAFjZXeWTeir4dE=" + } + }, + { + "_type": "UMLSeqLifelineView", + "_id": "AAAAAAFjZXhFeOi/dEU=", + "_parent": { + "$ref": "AAAAAAFjZXeWTOioQJg=" + }, + "model": { + "$ref": "AAAAAAFjZXhFd+i+KlQ=" + }, + "subViews": [ + { + "_type": "UMLNameCompartmentView", + "_id": "AAAAAAFjZXhFeOjAPPI=", + "_parent": { + "$ref": "AAAAAAFjZXhFeOi/dEU=" + }, + "model": { + "$ref": "AAAAAAFjZXhFd+i+KlQ=" + }, + "subViews": [ + { + "_type": "LabelView", + "_id": "AAAAAAFjZXhFeOjBVh0=", + "_parent": { + "$ref": "AAAAAAFjZXhFeOjAPPI=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 128, + "height": 13 + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjZXhFeOjCvPs=", + "_parent": { + "$ref": "AAAAAAFjZXhFeOjAPPI=" + }, + "font": "Arial;13;1", + "left": 173, + "top": 47, + "width": 152.3017578125, + "height": 13, + "text": "SafeCreationController" + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjZXhFeOjDUB0=", + "_parent": { + "$ref": "AAAAAAFjZXhFeOjAPPI=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 128, + "width": 106.20263671875, + "height": 13, + "text": "(from Interaction1)" + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjZXhFeOjEmq4=", + "_parent": { + "$ref": "AAAAAAFjZXhFeOjAPPI=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 128, + "height": 13, + "horizontalAlignment": 1 + } + ], + "font": "Arial;13;0", + "left": 168, + "top": 40, + "width": 162.3017578125, + "height": 40, + "stereotypeLabel": { + "$ref": "AAAAAAFjZXhFeOjBVh0=" + }, + "nameLabel": { + "$ref": "AAAAAAFjZXhFeOjCvPs=" + }, + "namespaceLabel": { + "$ref": "AAAAAAFjZXhFeOjDUB0=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjZXhFeOjEmq4=" + } + }, + { + "_type": "UMLLinePartView", + "_id": "AAAAAAFjZXhFeOjF9EQ=", + "_parent": { + "$ref": "AAAAAAFjZXhFeOi/dEU=" + }, + "model": { + "$ref": "AAAAAAFjZXhFd+i+KlQ=" + }, + "font": "Arial;13;0", + "left": 249, + "top": 80, + "width": 1, + "height": 160 + } + ], + "font": "Arial;13;0", + "left": 168, + "top": 40, + "width": 162.3017578125, + "height": 200, + "nameCompartment": { + "$ref": "AAAAAAFjZXhFeOjAPPI=" + }, + "linePart": { + "$ref": "AAAAAAFjZXhFeOjF9EQ=" + } + }, + { + "_type": "UMLEndpointView", + "_id": "AAAAAAFjZXi7gejfBHg=", + "_parent": { + "$ref": "AAAAAAFjZXeWTOioQJg=" + }, + "model": { + "$ref": "AAAAAAFjZXi7f+jezDQ=" + }, + "font": "Arial;13;0", + "left": 16, + "top": 106.5, + "width": 15, + "height": 15 + }, + { + "_type": "UMLSeqMessageView", + "_id": "AAAAAAFjZXi7g+jh8qc=", + "_parent": { + "$ref": "AAAAAAFjZXeWTOioQJg=" + }, + "model": { + "$ref": "AAAAAAFjZXi7gujgMmg=" + }, + "subViews": [ + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZXi7g+jiUFQ=", + "_parent": { + "$ref": "AAAAAAFjZXi7g+jh8qc=" + }, + "model": { + "$ref": "AAAAAAFjZXi7gujgMmg=" + }, + "font": "Arial;13;0", + "left": 37, + "top": 98, + "width": 199, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjZXi7g+jh8qc=" + }, + "edgePosition": 1, + "text": "1 : getTx(r, owners, confirmations)" + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZXi7g+jj/3w=", + "_parent": { + "$ref": "AAAAAAFjZXi7g+jh8qc=" + }, + "model": { + "$ref": "AAAAAAFjZXi7gujgMmg=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 136, + "top": 83, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 25, + "hostEdge": { + "$ref": "AAAAAAFjZXi7g+jh8qc=" + }, + "edgePosition": 1 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZXi7g+jkSGE=", + "_parent": { + "$ref": "AAAAAAFjZXi7g+jh8qc=" + }, + "model": { + "$ref": "AAAAAAFjZXi7gujgMmg=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 136, + "top": 118, + "height": 13, + "alpha": -1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjZXi7g+jh8qc=" + }, + "edgePosition": 1 + }, + { + "_type": "UMLActivationView", + "_id": "AAAAAAFjZXi7hOjlmv0=", + "_parent": { + "$ref": "AAAAAAFjZXi7g+jh8qc=" + }, + "model": { + "$ref": "AAAAAAFjZXi7gujgMmg=" + }, + "font": "Arial;13;0", + "left": 242, + "top": 114, + "width": 14, + "height": 29 + } + ], + "font": "Arial;13;0", + "head": { + "$ref": "AAAAAAFjZXhFeOjF9EQ=" + }, + "tail": { + "$ref": "AAAAAAFjZXi7gejfBHg=" + }, + "points": "30:114;242:114", + "nameLabel": { + "$ref": "AAAAAAFjZXi7g+jiUFQ=" + }, + "stereotypeLabel": { + "$ref": "AAAAAAFjZXi7g+jj/3w=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjZXi7g+jkSGE=" + }, + "activation": { + "$ref": "AAAAAAFjZXi7hOjlmv0=" + } + }, + { + "_type": "UMLSeqLifelineView", + "_id": "AAAAAAFjZXm8cuj6W0Y=", + "_parent": { + "$ref": "AAAAAAFjZXeWTOioQJg=" + }, + "model": { + "$ref": "AAAAAAFjZXm8cuj5WeQ=" + }, + "subViews": [ + { + "_type": "UMLNameCompartmentView", + "_id": "AAAAAAFjZXm8cuj7GJY=", + "_parent": { + "$ref": "AAAAAAFjZXm8cuj6W0Y=" + }, + "model": { + "$ref": "AAAAAAFjZXm8cuj5WeQ=" + }, + "subViews": [ + { + "_type": "LabelView", + "_id": "AAAAAAFjZXm8cuj8BEk=", + "_parent": { + "$ref": "AAAAAAFjZXm8cuj7GJY=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 32, + "height": 13 + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjZXm8c+j95YQ=", + "_parent": { + "$ref": "AAAAAAFjZXm8cuj7GJY=" + }, + "font": "Arial;13;1", + "left": 461, + "top": 107, + "width": 90.18994140625, + "height": 13, + "text": "SafeCreation" + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjZXm8c+j+lDU=", + "_parent": { + "$ref": "AAAAAAFjZXm8cuj7GJY=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 32, + "width": 106.20263671875, + "height": 13, + "text": "(from Interaction1)" + }, + { + "_type": "LabelView", + "_id": "AAAAAAFjZXm8c+j/GxM=", + "_parent": { + "$ref": "AAAAAAFjZXm8cuj7GJY=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 32, + "height": 13, + "horizontalAlignment": 1 + } + ], + "font": "Arial;13;0", + "left": 456, + "top": 100, + "width": 100.18994140625, + "height": 40, + "stereotypeLabel": { + "$ref": "AAAAAAFjZXm8cuj8BEk=" + }, + "nameLabel": { + "$ref": "AAAAAAFjZXm8c+j95YQ=" + }, + "namespaceLabel": { + "$ref": "AAAAAAFjZXm8c+j+lDU=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjZXm8c+j/GxM=" + } + }, + { + "_type": "UMLLinePartView", + "_id": "AAAAAAFjZXm8c+kAsIY=", + "_parent": { + "$ref": "AAAAAAFjZXm8cuj6W0Y=" + }, + "model": { + "$ref": "AAAAAAFjZXm8cuj5WeQ=" + }, + "font": "Arial;13;0", + "left": 506, + "top": 140, + "width": 1, + "height": 109 + } + ], + "font": "Arial;13;0", + "left": 456, + "top": 100, + "width": 100.18994140625, + "height": 149, + "nameCompartment": { + "$ref": "AAAAAAFjZXm8cuj7GJY=" + }, + "linePart": { + "$ref": "AAAAAAFjZXm8c+kAsIY=" + } + }, + { + "_type": "UMLSeqMessageView", + "_id": "AAAAAAFjZXyMfOnL8n4=", + "_parent": { + "$ref": "AAAAAAFjZXeWTOioQJg=" + }, + "model": { + "$ref": "AAAAAAFjZXyMe+nK3oc=" + }, + "subViews": [ + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZXyMfOnMEck=", + "_parent": { + "$ref": "AAAAAAFjZXyMfOnL8n4=" + }, + "model": { + "$ref": "AAAAAAFjZXyMe+nK3oc=" + }, + "font": "Arial;13;0", + "left": 280, + "top": 159, + "width": 153, + "height": 13, + "alpha": -3.648691393831287, + "distance": 20.591260281974, + "hostEdge": { + "$ref": "AAAAAAFjZXyMfOnL8n4=" + }, + "edgePosition": 1, + "text": "3 : generateValidSignature" + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZXyMfOnNv8Y=", + "_parent": { + "$ref": "AAAAAAFjZXyMfOnL8n4=" + }, + "model": { + "$ref": "AAAAAAFjZXyMe+nK3oc=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 374, + "top": 145, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 25, + "hostEdge": { + "$ref": "AAAAAAFjZXyMfOnL8n4=" + }, + "edgePosition": 1 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZXyMfOnOmPs=", + "_parent": { + "$ref": "AAAAAAFjZXyMfOnL8n4=" + }, + "model": { + "$ref": "AAAAAAFjZXyMe+nK3oc=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 374, + "top": 180, + "height": 13, + "alpha": -1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjZXyMfOnL8n4=" + }, + "edgePosition": 1 + }, + { + "_type": "UMLActivationView", + "_id": "AAAAAAFjZXyMfOnPyZM=", + "_parent": { + "$ref": "AAAAAAFjZXyMfOnL8n4=" + }, + "model": { + "$ref": "AAAAAAFjZXyMe+nK3oc=" + }, + "font": "Arial;13;0", + "left": 499, + "top": 176, + "width": 14, + "height": 49 + } + ], + "font": "Arial;13;0", + "head": { + "$ref": "AAAAAAFjZXm8c+kAsIY=" + }, + "tail": { + "$ref": "AAAAAAFjZXhFeOjF9EQ=" + }, + "points": "249:176;499:176", + "nameLabel": { + "$ref": "AAAAAAFjZXyMfOnMEck=" + }, + "stereotypeLabel": { + "$ref": "AAAAAAFjZXyMfOnNv8Y=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjZXyMfOnOmPs=" + }, + "activation": { + "$ref": "AAAAAAFjZXyMfOnPyZM=" + } + }, + { + "_type": "UMLSeqMessageView", + "_id": "AAAAAAFjZX2NPOoEvlQ=", + "_parent": { + "$ref": "AAAAAAFjZXeWTOioQJg=" + }, + "model": { + "$ref": "AAAAAAFjZX2NO+oDT/M=" + }, + "subViews": [ + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZX2NPOoFgB4=", + "_parent": { + "$ref": "AAAAAAFjZX2NPOoEvlQ=" + }, + "model": { + "$ref": "AAAAAAFjZX2NO+oDT/M=" + }, + "font": "Arial;13;0", + "left": 337, + "top": 212, + "width": 72, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjZX2NPOoEvlQ=" + }, + "edgePosition": 1, + "text": "4 : signedTx" + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZX2NPOoGEm8=", + "_parent": { + "$ref": "AAAAAAFjZX2NPOoEvlQ=" + }, + "model": { + "$ref": "AAAAAAFjZX2NO+oDT/M=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 373, + "top": 227, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 25, + "hostEdge": { + "$ref": "AAAAAAFjZX2NPOoEvlQ=" + }, + "edgePosition": 1 + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZX2NPOoH6Mg=", + "_parent": { + "$ref": "AAAAAAFjZX2NPOoEvlQ=" + }, + "model": { + "$ref": "AAAAAAFjZX2NO+oDT/M=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 374, + "top": 192, + "height": 13, + "alpha": -1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjZX2NPOoEvlQ=" + }, + "edgePosition": 1 + }, + { + "_type": "UMLActivationView", + "_id": "AAAAAAFjZX2NPOoIsOs=", + "_parent": { + "$ref": "AAAAAAFjZX2NPOoEvlQ=" + }, + "model": { + "$ref": "AAAAAAFjZX2NO+oDT/M=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 249, + "top": 208, + "width": 14, + "height": 25 + } + ], + "font": "Arial;13;0", + "head": { + "$ref": "AAAAAAFjZXhFeOjF9EQ=" + }, + "tail": { + "$ref": "AAAAAAFjZXm8c+kAsIY=" + }, + "points": "499:208;249:208", + "nameLabel": { + "$ref": "AAAAAAFjZX2NPOoFgB4=" + }, + "stereotypeLabel": { + "$ref": "AAAAAAFjZX2NPOoGEm8=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjZX2NPOoH6Mg=" + }, + "activation": { + "$ref": "AAAAAAFjZX2NPOoIsOs=" + } + }, + { + "_type": "UMLSeqMessageView", + "_id": "AAAAAAFjZXwt4Om028I=", + "_parent": { + "$ref": "AAAAAAFjZXeWTOioQJg=" + }, + "model": { + "$ref": "AAAAAAFjZXwt4OmzaxY=" + }, + "subViews": [ + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZXwt4Om1MF0=", + "_parent": { + "$ref": "AAAAAAFjZXwt4Om028I=" + }, + "model": { + "$ref": "AAAAAAFjZXwt4OmzaxY=" + }, + "font": "Arial;13;0", + "left": 282, + "top": 104, + "width": 146, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjZXwt4Om028I=" + }, + "edgePosition": 1, + "text": "2 : owners, confirmations" + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZXwt4Om2W4M=", + "_parent": { + "$ref": "AAAAAAFjZXwt4Om028I=" + }, + "model": { + "$ref": "AAAAAAFjZXwt4OmzaxY=" + }, + "font": "Arial;13;0", + "left": 330, + "top": 89, + "width": 50.5908203125, + "height": 13, + "alpha": 1.5707963267948966, + "distance": 25, + "hostEdge": { + "$ref": "AAAAAAFjZXwt4Om028I=" + }, + "edgePosition": 1, + "text": "«create»" + }, + { + "_type": "EdgeLabelView", + "_id": "AAAAAAFjZXwt4Om3Dx8=", + "_parent": { + "$ref": "AAAAAAFjZXwt4Om028I=" + }, + "model": { + "$ref": "AAAAAAFjZXwt4OmzaxY=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 355, + "top": 124, + "height": 13, + "alpha": -1.5707963267948966, + "distance": 10, + "hostEdge": { + "$ref": "AAAAAAFjZXwt4Om028I=" + }, + "edgePosition": 1 + }, + { + "_type": "UMLActivationView", + "_id": "AAAAAAFjZXwt4Om4bUM=", + "_parent": { + "$ref": "AAAAAAFjZXwt4Om028I=" + }, + "model": { + "$ref": "AAAAAAFjZXwt4OmzaxY=" + }, + "visible": false, + "font": "Arial;13;0", + "left": 506, + "top": 120, + "width": 14, + "height": 25 + } + ], + "font": "Arial;13;0", + "head": { + "$ref": "AAAAAAFjZXm8c+kAsIY=" + }, + "tail": { + "$ref": "AAAAAAFjZXhFeOjF9EQ=" + }, + "points": "255:120;456:120", + "nameLabel": { + "$ref": "AAAAAAFjZXwt4Om1MF0=" + }, + "stereotypeLabel": { + "$ref": "AAAAAAFjZXwt4Om2W4M=" + }, + "propertyLabel": { + "$ref": "AAAAAAFjZXwt4Om3Dx8=" + }, + "activation": { + "$ref": "AAAAAAFjZXwt4Om4bUM=" + } + } + ] + } + ], + "messages": [ + { + "_type": "UMLMessage", + "_id": "AAAAAAFjZXi7gujgMmg=", + "_parent": { + "$ref": "AAAAAAFjZXeWTOinlcs=" + }, + "name": "getTx", + "source": { + "$ref": "AAAAAAFjZXi7f+jezDQ=" + }, + "target": { + "$ref": "AAAAAAFjZXhFd+i+KlQ=" + }, + "arguments": "r, owners, confirmations" + }, + { + "_type": "UMLMessage", + "_id": "AAAAAAFjZXwt4OmzaxY=", + "_parent": { + "$ref": "AAAAAAFjZXeWTOinlcs=" + }, + "name": "owners, confirmations", + "source": { + "$ref": "AAAAAAFjZXhFd+i+KlQ=" + }, + "target": { + "$ref": "AAAAAAFjZXm8cuj5WeQ=" + }, + "messageSort": "createMessage" + }, + { + "_type": "UMLMessage", + "_id": "AAAAAAFjZXyMe+nK3oc=", + "_parent": { + "$ref": "AAAAAAFjZXeWTOinlcs=" + }, + "name": "generateValidSignature", + "source": { + "$ref": "AAAAAAFjZXhFd+i+KlQ=" + }, + "target": { + "$ref": "AAAAAAFjZXm8cuj5WeQ=" + } + }, + { + "_type": "UMLMessage", + "_id": "AAAAAAFjZX2NO+oDT/M=", + "_parent": { + "$ref": "AAAAAAFjZXeWTOinlcs=" + }, + "name": "signedTx", + "source": { + "$ref": "AAAAAAFjZXm8cuj5WeQ=" + }, + "target": { + "$ref": "AAAAAAFjZXhFd+i+KlQ=" + }, + "messageSort": "reply" + }, + { + "_type": "UMLMessage", + "_id": "AAAAAAFjZX0RS+nhUHw=", + "_parent": { + "$ref": "AAAAAAFjZXeWTOinlcs=" + }, + "name": "id", + "source": { + "$ref": "AAAAAAFjZXm8cuj5WeQ=" + }, + "target": { + "$ref": "AAAAAAFjZXhFd+i+KlQ=" + }, + "messageSort": "reply" + } + ], + "participants": [ + { + "_type": "UMLLifeline", + "_id": "AAAAAAFjZXhFd+i+KlQ=", + "_parent": { + "$ref": "AAAAAAFjZXeWTOinlcs=" + }, + "name": "SafeCreationController", + "represent": { + "$ref": "AAAAAAFjZXhFd+i9bS4=" + }, + "isMultiInstance": false + }, + { + "_type": "UMLEndpoint", + "_id": "AAAAAAFjZXi7f+jezDQ=", + "_parent": { + "$ref": "AAAAAAFjZXeWTOinlcs=" + }, + "name": "Endpoint1" + }, + { + "_type": "UMLLifeline", + "_id": "AAAAAAFjZXm8cuj5WeQ=", + "_parent": { + "$ref": "AAAAAAFjZXeWTOinlcs=" + }, + "name": "SafeCreation", + "represent": { + "$ref": "AAAAAAFjZXm8cej4OPc=" + }, + "isMultiInstance": false + } + ] + } + ], + "attributes": [ + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjZXhFd+i9bS4=", + "_parent": { + "$ref": "AAAAAAFjZXeWS+imsis=" + }, + "name": "Role1", + "type": "" + }, + { + "_type": "UMLAttribute", + "_id": "AAAAAAFjZXm8cej4OPc=", + "_parent": { + "$ref": "AAAAAAFjZXeWS+imsis=" + }, + "name": "Role2", + "type": "" + } + ] + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..351b751ee --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +[![Build Status](https://travis-ci.org/gnosis/safe-relay-service.svg?branch=master)](https://travis-ci.org/gnosis/safe-relay-service) +[![Coverage Status](https://coveralls.io/repos/github/gnosis/safe-relay-service/badge.svg?branch=master)](https://coveralls.io/github/gnosis/safe-relay-service?branch=master) +![Python 3.6](https://img.shields.io/badge/Python-3.6-blue.svg) +![Django 2](https://img.shields.io/badge/Django-2-blue.svg) + +# Gnosis Safe Notification Service +Service for Gnosis Safe push notifications + +## Index of contents + +- [Docs](#docs) +- [Contributors](#contributors) + +Docs +------------ +You can open the diagrams with [Staruml](http://staruml.io/) + +Contributors +------------ +- Stefan George (stefan@gnosis.pm) +- Denís Graña (denis@gnosis.pm) +- Giacomo Licari (giacomo.licari@gnosis.pm) +- Uxío Fuentefría (uxio@gnosis.pm) diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/config/settings/__init__.py b/config/settings/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/config/settings/base.py b/config/settings/base.py new file mode 100644 index 000000000..931839de8 --- /dev/null +++ b/config/settings/base.py @@ -0,0 +1,284 @@ +""" +Base settings to build other settings files upon. +""" + +import environ + +ROOT_DIR = environ.Path(__file__) - 3 # (safe_transaction_history/config/settings/base.py - 3 = safe-transaction-history/) +APPS_DIR = ROOT_DIR.path('safe_transaction_history') + +env = environ.Env() + +READ_DOT_ENV_FILE = env.bool('DJANGO_READ_DOT_ENV_FILE', default=False) +DOT_ENV_FILE = env('DJANGO_DOT_ENV_FILE', default=None) +if READ_DOT_ENV_FILE or DOT_ENV_FILE: + DOT_ENV_FILE = DOT_ENV_FILE or '.env' + # OS environment variables take precedence over variables from .env + env.read_env(str(ROOT_DIR.path(DOT_ENV_FILE))) + +# GENERAL +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#debug +DEBUG = env.bool('DJANGO_DEBUG', False) +# Local time zone. Choices are +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# though not all of them may be available with every OS. +# In Windows, this must be set to your system time zone. +TIME_ZONE = 'UTC' +# https://docs.djangoproject.com/en/dev/ref/settings/#language-code +LANGUAGE_CODE = 'en-us' +# https://docs.djangoproject.com/en/dev/ref/settings/#site-id +SITE_ID = 1 +# https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n +USE_I18N = True +# https://docs.djangoproject.com/en/dev/ref/settings/#use-l10n +USE_L10N = True +# https://docs.djangoproject.com/en/dev/ref/settings/#use-tz +USE_TZ = True + +# DATABASES +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#databases +DATABASES = { + 'default': env.db('DATABASE_URL'), +} +DATABASES['default']['ATOMIC_REQUESTS'] = True + +# URLS +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf +ROOT_URLCONF = 'config.urls' +# https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application +WSGI_APPLICATION = 'config.wsgi.application' + +# APPS +# ------------------------------------------------------------------------------ +DJANGO_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + # 'django.contrib.humanize', # Handy template tags + +] +THIRD_PARTY_APPS = [ + 'rest_framework', + 'rest_framework_swagger', +] +LOCAL_APPS = [ + 'safe_transaction_history.safe.apps.SafeConfig', +] +# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps +INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS + +# MIDDLEWARE +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#middleware +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +# STATIC +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#static-root +STATIC_ROOT = env('STATIC_ROOT', default=str(ROOT_DIR('staticfiles'))) +# https://docs.djangoproject.com/en/dev/ref/settings/#static-url +STATIC_URL = '/static/' +# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS +STATICFILES_DIRS = [ + str(APPS_DIR.path('static')), +] +# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders +STATICFILES_FINDERS = [ + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +] + +# MEDIA +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#media-root +MEDIA_ROOT = str(APPS_DIR('media')) +# https://docs.djangoproject.com/en/dev/ref/settings/#media-url +MEDIA_URL = '/media/' + +# TEMPLATES +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#templates +TEMPLATES = [ + { + # https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + # https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs + 'DIRS': [ + str(APPS_DIR.path('templates')), + ], + 'OPTIONS': { + # https://docs.djangoproject.com/en/dev/ref/settings/#template-debug + 'debug': DEBUG, + # https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders + # https://docs.djangoproject.com/en/dev/ref/templates/api/#loader-types + 'loaders': [ + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + ], + # https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.template.context_processors.i18n', + 'django.template.context_processors.media', + 'django.template.context_processors.static', + 'django.template.context_processors.tz', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +# FIXTURES +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#fixture-dirs +FIXTURE_DIRS = ( + str(APPS_DIR.path('fixtures')), +) + +# EMAIL +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend +EMAIL_BACKEND = env('DJANGO_EMAIL_BACKEND', default='django.core.mail.backends.smtp.EmailBackend') + +# ADMIN +# ------------------------------------------------------------------------------ +# Django Admin URL regex. +ADMIN_URL = r'^admin/' +# https://docs.djangoproject.com/en/dev/ref/settings/#admins +ADMINS = [ + ("""Gnosis""", 'dev@gnosis.pm'), +] +# https://docs.djangoproject.com/en/dev/ref/settings/#managers +MANAGERS = ADMINS + +# Celery +# ------------------------------------------------------------------------------ +INSTALLED_APPS += [ + 'safe_transaction_history.taskapp.celery.CeleryConfig', + 'django_celery_beat', +] +# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-broker_url +CELERY_BROKER_URL = env('CELERY_BROKER_URL', default='django://') +# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-result_backend +if CELERY_BROKER_URL == 'django://': + CELERY_RESULT_BACKEND = 'redis://' +else: + CELERY_RESULT_BACKEND = CELERY_BROKER_URL +# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-accept_content +CELERY_ACCEPT_CONTENT = ['json'] +# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-task_serializer +CELERY_TASK_SERIALIZER = 'json' +# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-result_serializer +CELERY_RESULT_SERIALIZER = 'json' + +# Django REST Framework +# ------------------------------------------------------------------------------ +REST_FRAMEWORK = { + 'PAGE_SIZE': 10, + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', + 'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.AllowAny',), + 'DEFAULT_RENDERER_CLASSES': ( + 'djangorestframework_camel_case.render.CamelCaseJSONRenderer', + ), + 'DEFAULT_PARSER_CLASSES': ( + 'djangorestframework_camel_case.parser.CamelCaseJSONParser', + ), + 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning', +} + +# LOGGING +# ------------------------------------------------------------------------------ +# See: https://docs.djangoproject.com/en/dev/ref/settings/#logging +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# the site admins bon every HTTP 500 error when DEBUG=False. +# See https://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse' + } + }, + 'formatters': { + 'verbose': { + 'format': '%(asctime)s [%(levelname)s] [%(processName)s] %(message)s', + }, + }, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['require_debug_false'], + 'class': 'django.utils.log.AdminEmailHandler' + }, + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'verbose', + }, + }, + 'loggers': { + '': { + 'handlers': ['console'], + 'level': 'INFO', + }, + 'celery.worker.strategy': { + 'handlers': ['console'], + 'level': 'INFO' if DEBUG else 'WARNING', + }, + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True + }, + 'django.security.DisallowedHost': { + 'level': 'ERROR', + 'handlers': ['console', 'mail_admins'], + 'propagate': True + } + } +} + +REDIS_URL = env('REDIS_URL', default='redis://localhost:6379/0') + +# Ethereum +# ------------------------------------------------------------------------------ +ETH_HASH_PREFIX = env('ETH_HASH_PREFIX', default='GNO') +ETHEREUM_NODE_URL = env('ETHEREUM_NODE_URL', default=None) + + +# Safe +# ------------------------------------------------------------------------------ +SAFE_FUNDER_PRIVATE_KEY = env('SAFE_FUNDER_PRIVATE_KEY', default=None) +# Maximum ether (no wei) for a single transaction (security limit) +SAFE_FUNDER_MAX_ETH = env.int('SAFE_FUNDER_MAX_ETH', default=0.1) +SAFE_FUNDING_CONFIRMATIONS = env.int('SAFE_FUNDING_CONFIRMATIONS', default=0) # Set to at least 3 +# Master Copy Address of Safe Personal Edition Contract +SAFE_PERSONAL_CONTRACT_ADDRESS = env('SAFE_PERSONAL_CONTRACT_ADDRESS', default='0x' + '0' * 39 + '1') +SAFE_PERSONAL_VALID_CONTRACT_ADDRESSES = env.list('SAFE_PERSONAL_VALID_CONTRACT_ADDRESSES', + default=[SAFE_PERSONAL_CONTRACT_ADDRESS]) +# If SAFE_GAS_PRICE is None, GasStation will be used +SAFE_GAS_PRICE = env.int('SAFE_GAS_PRICE', default=None) +SAFE_TX_SENDER_PRIVATE_KEY = env('SAFE_TX_SENDER_PRIVATE_KEY', default=None) + +SAFE_CHECK_DEPLOYER_FUNDED_DELAY = env.int('SAFE_CHECK_DEPLOYER_FUNDED_DELAY', default=1 * 30) +SAFE_CHECK_DEPLOYER_FUNDED_RETRIES = env.int('SAFE_CHECK_DEPLOYER_FUNDED_RETRIES', default=10) diff --git a/config/settings/local.py b/config/settings/local.py new file mode 100644 index 000000000..fd52f030d --- /dev/null +++ b/config/settings/local.py @@ -0,0 +1,82 @@ +import socket + +from .base import * # noqa +from .base import env + +# GENERAL +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#debug +DEBUG = env.bool('DJANGO_DEBUG', default=True) +# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key +SECRET_KEY = env('DJANGO_SECRET_KEY', default='aHdCBMHXuxIxEhfRGFRp7Cp3N9CqEZEEAvwZVlBCazKExkEnzvVs4bYWC8Qqh9lg') +# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts +ALLOWED_HOSTS = [ + "localhost", + "0.0.0.0", + "127.0.0.1", +] + +# CACHES +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#caches +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'LOCATION': '' + } +} + +# EMAIL +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend +EMAIL_BACKEND = env('DJANGO_EMAIL_BACKEND', default='django.core.mail.backends.console.EmailBackend') +# https://docs.djangoproject.com/en/dev/ref/settings/#email-host +EMAIL_HOST = 'localhost' +# https://docs.djangoproject.com/en/dev/ref/settings/#email-port +EMAIL_PORT = 1025 + +# django-debug-toolbar +# ------------------------------------------------------------------------------ +# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#prerequisites +INSTALLED_APPS += ['debug_toolbar'] # noqa F405 +# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#middleware +MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware'] # noqa F405 +# https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config +DEBUG_TOOLBAR_CONFIG = { + 'DISABLE_PANELS': [ + 'debug_toolbar.panels.redirects.RedirectsPanel', + ], + 'SHOW_TEMPLATE_CONTEXT': True, +} +# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#internal-ips +INTERNAL_IPS = ['127.0.0.1', '10.0.2.2'] + +# Celery +# ------------------------------------------------------------------------------ +# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-task_always_eager +CELERY_ALWAYS_EAGER = False + +if env.bool('USE_DOCKER', default=False): + hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) + INTERNAL_IPS += [ip[:-1] + '1' for ip in ips] + + CACHES = { + 'default': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': env('REDIS_URL'), + 'OPTIONS': { + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + # Mimicing memcache behavior. + # http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior + 'IGNORE_EXCEPTIONS': True, + } + } + } + + CELERY_ALWAYS_EAGER = True + +# SAFE +SAFE_GAS_PRICE = 1 +SAFE_FUNDER_PRIVATE_KEY = '4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d' +SAFE_FUNDING_CONFIRMATIONS = 0 +SAFE_PERSONAL_CONTRACT_ADDRESS = '0x2aaB3573eCFD2950a30B75B6f3651b84F4e130da' diff --git a/config/settings/production.py b/config/settings/production.py new file mode 100644 index 000000000..6c8bd89e2 --- /dev/null +++ b/config/settings/production.py @@ -0,0 +1,92 @@ +from .base import * # noqa +from .base import env + +# GENERAL +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key +SECRET_KEY = env('DJANGO_SECRET_KEY') +# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts +ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS', default=['gnosis.pm']) + +# DATABASES +# ------------------------------------------------------------------------------ +DATABASES['default'] = env.db('DATABASE_URL') # noqa F405 +DATABASES['default']['ATOMIC_REQUESTS'] = False # noqa F405 + +# CACHES +# ------------------------------------------------------------------------------ +CACHES = { + 'default': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': env('REDIS_URL'), + 'OPTIONS': { + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + # Mimicing memcache behavior. + # http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior + 'IGNORE_EXCEPTIONS': True, + } + } +} + +# SECURITY +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +# https://docs.djangoproject.com/en/dev/ref/settings/#secure-ssl-redirect +SECURE_SSL_REDIRECT = env.bool('DJANGO_SECURE_SSL_REDIRECT', default=False) +# https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-secure +SESSION_COOKIE_SECURE = True +# https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-httponly +SESSION_COOKIE_HTTPONLY = True +# https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-secure +CSRF_COOKIE_SECURE = True +# https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-httponly +CSRF_COOKIE_HTTPONLY = True +# https://docs.djangoproject.com/en/dev/topics/security/#ssl-https +# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-seconds +# TODO: set this to 60 seconds first and then to 518400 once you prove the former works +SECURE_HSTS_SECONDS = 60 +# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains +SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool('DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS', default=True) +# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload +SECURE_HSTS_PRELOAD = env.bool('DJANGO_SECURE_HSTS_PRELOAD', default=True) +# https://docs.djangoproject.com/en/dev/ref/middleware/#x-content-type-options-nosniff +SECURE_CONTENT_TYPE_NOSNIFF = env.bool('DJANGO_SECURE_CONTENT_TYPE_NOSNIFF', default=True) +# https://docs.djangoproject.com/en/dev/ref/settings/#secure-browser-xss-filter +SECURE_BROWSER_XSS_FILTER = True +# https://docs.djangoproject.com/en/dev/ref/settings/#x-frame-options +X_FRAME_OPTIONS = 'DENY' + +# TEMPLATES +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#templates +TEMPLATES[0]['OPTIONS']['loaders'] = [ # noqa F405 + ( + 'django.template.loaders.cached.Loader', + [ + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + ] + ), +] + +# EMAIL +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email +DEFAULT_FROM_EMAIL = env( + 'DJANGO_DEFAULT_FROM_EMAIL', + default='Gnosis Safe Push Service ' +) +# https://docs.djangoproject.com/en/dev/ref/settings/#server-email +SERVER_EMAIL = env('DJANGO_SERVER_EMAIL', default=DEFAULT_FROM_EMAIL) +# https://docs.djangoproject.com/en/dev/ref/settings/#email-subject-prefix +EMAIL_SUBJECT_PREFIX = env('DJANGO_EMAIL_SUBJECT_PREFIX', default='[Gnosis Safe Push Service]') + +# ADMIN +# ------------------------------------------------------------------------------ +# Django Admin URL regex. +ADMIN_URL = env('DJANGO_ADMIN_URL', default=r'^admin/') + +# Gunicorn +# ------------------------------------------------------------------------------ +INSTALLED_APPS += ['gunicorn'] # noqa F405 diff --git a/config/settings/test.py b/config/settings/test.py new file mode 100644 index 000000000..ec4020bd8 --- /dev/null +++ b/config/settings/test.py @@ -0,0 +1,49 @@ +""" +With these settings, tests run faster. +""" + +from .base import * # noqa +from .base import env + +# GENERAL +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#debug +DEBUG = False +# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key +SECRET_KEY = env("DJANGO_SECRET_KEY", default="q8lVkJGsIiHcTSQKaWIBsMVPOGnCnF6f7NDGup8KdDNmviSaZVhP0Nq3q3MolmFU") +# https://docs.djangoproject.com/en/dev/ref/settings/#test-runner +TEST_RUNNER = "django.test.runner.DiscoverRunner" + +# CACHES +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#caches +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", "LOCATION": "" + } +} + +# PASSWORDS +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers +PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] + +# EMAIL +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend +EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" +# https://docs.djangoproject.com/en/dev/ref/settings/#email-host +EMAIL_HOST = "localhost" +# https://docs.djangoproject.com/en/dev/ref/settings/#email-port +EMAIL_PORT = 1025 + +# CELERY +CELERY_ALWAYS_EAGER = True + +# SAFE +# Ganache #1 and #2 private keys +SAFE_FUNDER_PRIVATE_KEY = '4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d' +SAFE_TX_SENDER_PRIVATE_KEY = '6cbed15c793ce57650b9877cf6fa156fbef513c4e6134f022a85b1ffdd59b2a1' +SAFE_FUNDING_CONFIRMATIONS = 0 +SAFE_GAS_PRICE = 1 +SAFE_PERSONAL_CONTRACT_ADDRESS = '0x2aaB3573eCFD2950a30B75B6f3651b84F4e130da' diff --git a/config/swagger.py b/config/swagger.py new file mode 100644 index 000000000..bc7603ca7 --- /dev/null +++ b/config/swagger.py @@ -0,0 +1,79 @@ +import json +import os + +from rest_framework import exceptions, status +from rest_framework.permissions import AllowAny +from rest_framework.renderers import CoreJSONRenderer, JSONRenderer +from rest_framework.response import Response +from rest_framework.schemas import SchemaGenerator +from rest_framework.views import APIView +from rest_framework_swagger.renderers import OpenAPICodec +from rest_framework_swagger.renderers import \ + OpenAPIRenderer as BaseOpenAPIRenderer +from rest_framework_swagger.renderers import \ + SwaggerUIRenderer as BaseSwaggerUIRenderer +from rest_framework_swagger.settings import swagger_settings as settings + + +def get_swagger_view(title=None, url=None, patterns=None, urlconf=None): + """ + Returns schema view which renders Swagger/OpenAPI. + """ + class OpenAPIRenderer(BaseOpenAPIRenderer): + + def render(self, data, accepted_media_type=None, renderer_context=None): + if renderer_context['response'].status_code != status.HTTP_200_OK: + return JSONRenderer().render(data) + + request_scheme = renderer_context['request']._request._get_scheme() + scheme = os.getenv('SWAGGER_SCHEME_PROTOCOL', request_scheme) + self.scheme = scheme + + options = self.get_customizations() + return OpenAPICodec().encode(data, **options) + + def get_customizations(self, *args, **kwargs): + data = super(OpenAPIRenderer, self).get_customizations() + data["schemes"] = [self.scheme] + return data + + class SwaggerUIRenderer(BaseSwaggerUIRenderer): + + def set_context(self, data, renderer_context): + renderer_context['USE_SESSION_AUTH'] = settings.USE_SESSION_AUTH + renderer_context.update(self.get_auth_urls()) + + drs_settings = self.get_ui_settings() + renderer_context['drs_settings'] = json.dumps(drs_settings) + renderer_context['spec'] = OpenAPIRenderer().render( + data=data, + renderer_context=renderer_context + ).decode() + + class SwaggerSchemaView(APIView): + _ignore_model_permissions = True + exclude_from_schema = True + permission_classes = [AllowAny] + renderer_classes = [ + CoreJSONRenderer, + OpenAPIRenderer, + SwaggerUIRenderer + ] + + def get(self, request): + generator = SchemaGenerator( + title=title, + url=url, + patterns=patterns, + urlconf=urlconf + ) + schema = generator.get_schema(request=request) + + if not schema: + raise exceptions.ValidationError( + 'The schema generator did not return a schema Document' + ) + + return Response(schema) + + return SwaggerSchemaView.as_view() diff --git a/config/urls.py b/config/urls.py new file mode 100644 index 000000000..bf949fe1d --- /dev/null +++ b/config/urls.py @@ -0,0 +1,46 @@ +from django.conf import settings +from django.conf.urls import include, url +from django.conf.urls.static import static +from django.contrib import admin +from django.http import HttpResponse +from django.views import defaults as default_views + +from .swagger import get_swagger_view + +schema_view = get_swagger_view(title='Gnosis SAFE API') + + +urlpatterns = [ + url(r'^$', schema_view), + url(settings.ADMIN_URL, admin.site.urls), + url(r'^api/v1/', include('safe_transaction_history.safe.urls', namespace='v1')), + url(r'^check/', lambda request: HttpResponse("Ok"), name='check'), +] + +if settings.DEBUG: + # This allows the error pages to be debugged during development, just visit + # these url in browser to see how these error pages look like. + urlpatterns += [ + url( + r"^400/$", + default_views.bad_request, + kwargs={"exception": Exception("Bad Request!")}, + ), + url( + r"^403/$", + default_views.permission_denied, + kwargs={"exception": Exception("Permission Denied")}, + ), + url( + r"^404/$", + default_views.page_not_found, + kwargs={"exception": Exception("Page not Found")}, + ), + url(r"^500/$", default_views.server_error), + ] + if "debug_toolbar" in settings.INSTALLED_APPS: + import debug_toolbar + + urlpatterns = [url(r"^__debug__/", include(debug_toolbar.urls))] + urlpatterns + +admin.autodiscover() diff --git a/config/wsgi.py b/config/wsgi.py new file mode 100644 index 000000000..c53baed15 --- /dev/null +++ b/config/wsgi.py @@ -0,0 +1,42 @@ +""" +WSGI config for Gnosis Safe Push Service project. + +This module contains the WSGI application used by Django's development server +and any production WSGI deployments. It should expose a module-level variable +named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover +this application via the ``WSGI_APPLICATION`` setting. + +Usually you will have the standard Django WSGI application here, but it also +might make sense to replace the whole Django WSGI application with a custom one +that later delegates to the Django one. For example, you could introduce WSGI +middleware here, or combine a Django application with an application of another +framework. + +""" +import os +import sys + +from django.core.wsgi import get_wsgi_application + +# This allows easy placement of apps within the interior +# safe_transaction_history directory. +app_path = os.path.abspath(os.path.join( + os.path.dirname(os.path.abspath(__file__)), os.pardir)) +sys.path.append(os.path.join(app_path, 'safe_transaction_history')) + + + +# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks +# if running multiple sites in the same mod_wsgi process. To fix this, use +# mod_wsgi daemon mode with each site in its own daemon process, or use +# os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.production" +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production") + +# This application object is used by any WSGI server configured to use this +# file. This includes Django's development server, if the WSGI_APPLICATION +# setting points here. +application = get_wsgi_application() + +# Apply WSGI middleware here. +# from helloworld.wsgi import HelloWorldApplication +# application = HelloWorldApplication(application) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..0b892a77a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,64 @@ +version: '3.5' + +volumes: + nginx-shared: + +services: + nginx: + image: nginx:1.13-alpine + hostname: nginx + ports: + - "8000:8000" + volumes: + - ./docker/nginx:/etc/nginx/conf.d + - nginx-shared:/nginx + depends_on: + - web + db: + image: postgres:10-alpine + ports: + - "5432:5432" + web: + build: + context: . + dockerfile: docker/web/Dockerfile + env_file: + - .env + depends_on: + - db + - redis + - ganache + working_dir: /app + ports: + - "27017" + volumes: + - nginx-shared:/nginx + command: docker/web/run_web.sh + + redis: + image: redis:4-alpine + ports: + - "6379:6379" + + worker: &worker + build: + context: . + dockerfile: docker/web/Dockerfile + env_file: + - .env + depends_on: + - db + - redis + - ganache + command: docker/web/celery/worker/run.sh + + scheduler: + <<: *worker + command: docker/web/celery/scheduler/run.sh + + ganache: + build: + context: . + dockerfile: docker/ganache/Dockerfile + ports: + - "8545:8545" diff --git a/docker/ganache/Dockerfile b/docker/ganache/Dockerfile new file mode 100644 index 000000000..67b8de6e1 --- /dev/null +++ b/docker/ganache/Dockerfile @@ -0,0 +1,5 @@ +FROM node:9-alpine + +RUN npm install -g ganache-cli + +CMD ["ganache-cli", "-d", "--defaultBalanceEther", "10000", "-a", "10"] diff --git a/docker/nginx/safe_service.conf b/docker/nginx/safe_service.conf new file mode 100644 index 000000000..627fca47e --- /dev/null +++ b/docker/nginx/safe_service.conf @@ -0,0 +1,52 @@ +# https://github.com/KyleAMathews/docker-nginx/blob/master/nginx.conf +# https://linode.com/docs/web-servers/nginx/configure-nginx-for-optimized-performance/ + +#events { +# worker_connections 8000; # increase if you have lots of clients +# accept_mutex on; # set to 'on' if nginx worker_processes > 1 +# use epoll; # to enable for Linux 2.6+ +#} + +upstream web { + ip_hash; + # server web:8000; + server unix:/nginx/gunicorn.socket fail_timeout=0; +} + +server { + listen 8000; + charset utf-8; + + keepalive_timeout 65; + keepalive_requests 100000; + sendfile on; + tcp_nopush on; + tcp_nodelay on; + + + gzip on; + gzip_min_length 256; + gzip_comp_level 5; + + # text/html is always included by default + gzip_types text/plain text/css application/json application/javascript application/x-javascript text/javascript text/xml application/xml application/rss+xml application/atom+xml application/rdf+xml; + gzip_disable "MSIE [1-6]\."; + + + location / { + proxy_pass http://web/; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Host $server_name; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + add_header Front-End-Https on; + # we don't want nginx trying to do something clever with + # redirects, we set the Host: header above already. + proxy_redirect off; + } + + location /static { + alias /nginx/staticfiles; + } +} diff --git a/docker/web/Dockerfile b/docker/web/Dockerfile new file mode 100644 index 000000000..404fa2fa0 --- /dev/null +++ b/docker/web/Dockerfile @@ -0,0 +1,29 @@ +FROM python:3-slim-stretch + +ENV PYTHONUNBUFFERED 1 +WORKDIR /app + +# Signal handling for PID1 https://github.com/krallin/tini +ENV TINI_VERSION v0.18.0 +ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini +RUN chmod +x /tini + +COPY requirements.txt ./ +RUN set -ex \ + && buildDeps=" \ + build-essential \ + libssl-dev \ + " \ + && apt-get update \ + && apt-get install -y --no-install-recommends $buildDeps \ + && pip install --no-cache-dir -r requirements.txt \ + && apt-get purge -y --auto-remove $buildDeps \ + && rm -rf /var/lib/apt/lists/* \ + && find /usr/local \ + \( -type d -a -name test -o -name tests \) \ + -o \( -type f -a -name '*.pyc' -o -name '*.pyo' \) \ + -exec rm -rf '{}' + + +COPY . . + +ENTRYPOINT ["/tini", "--"] diff --git a/docker/web/Dockerfile_alpine b/docker/web/Dockerfile_alpine new file mode 100644 index 000000000..341c0dedd --- /dev/null +++ b/docker/web/Dockerfile_alpine @@ -0,0 +1,21 @@ +# Less size than Debian, slowest to build +FROM python:3-alpine3.7 + +ENV PYTHONUNBUFFERED 1 +WORKDIR /app + +COPY requirements.txt ./ + +# Signal handling for PID1 https://github.com/krallin/tini +RUN apk add --update --no-cache tini postgresql-client && \ + apk add --no-cache --virtual .build-dependencies postgresql-dev alpine-sdk libffi-dev autoconf automake libtool gmp-dev linux-headers && \ + pip install --no-cache-dir -r requirements.txt && \ + apk del .build-dependencies && \ + find /usr/local \ + \( -type d -a -name test -o -name tests \) \ + -o \( -type f -a -name '*.pyc' -o -name '*.pyo' \) \ + -exec rm -rf '{}' + + +COPY . . + +ENTRYPOINT ["/sbin/tini", "--"] diff --git a/docker/web/celery/scheduler/run.sh b/docker/web/celery/scheduler/run.sh new file mode 100755 index 000000000..e3395c8e1 --- /dev/null +++ b/docker/web/celery/scheduler/run.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -euo pipefail + +# DEBUG set in .env +if [ ${DEBUG:-0} = 1 ]; then + log_level="debug" +else + log_level="info" +fi + +echo "==> Running Celery beat <==" +exec celery beat -A safe_transaction_history.taskapp -S django_celery_beat.schedulers:DatabaseScheduler --loglevel $log_level diff --git a/docker/web/celery/worker/run.sh b/docker/web/celery/worker/run.sh new file mode 100755 index 000000000..f35996388 --- /dev/null +++ b/docker/web/celery/worker/run.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -euo pipefail + +celery -A safe_transaction_history.taskapp worker -l INFO diff --git a/docker/web/run_web.sh b/docker/web/run_web.sh new file mode 100755 index 000000000..6a23d1f5c --- /dev/null +++ b/docker/web/run_web.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -euo pipefail + +echo "==> Migrating Django models ... " +python manage.py migrate --noinput + +echo "==> Collecting statics ... " +DOCKER_SHARED_DIR=/nginx +rm -rf $DOCKER_SHARED_DIR/* +STATIC_ROOT=$DOCKER_SHARED_DIR/staticfiles python manage.py collectstatic --noinput + +echo "==> Running Gunicorn ... " +gunicorn --pythonpath "$PWD" config.wsgi:application --log-file=- --error-logfile=- --access-logfile '-' --log-level info -b unix:$DOCKER_SHARED_DIR/gunicorn.socket -b 127.0.0.1:8888 --worker-class gevent diff --git a/docker_instructions.txt b/docker_instructions.txt new file mode 100644 index 000000000..5f8f2f4db --- /dev/null +++ b/docker_instructions.txt @@ -0,0 +1,18 @@ +# Add the docker group if it doesn't already exist. +$ sudo groupadd docker + +# Add the connected user "${USER}" to the docker group. +# Change the user name to match your preferred user. +# You may have to logout and log back in again for +# this to take effect. +$ sudo gpasswd -a ${USER} docker + +# Restart the Docker daemon. +$ sudo service docker restart + +sudo docker-compose build +sudo docker-compose up + +# Run container in bash mode +sudo docker-compose run web bash + diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 000000000..cf4209e72 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,153 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/safe_transaction_history.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/safe_transaction_history.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/safe_transaction_history" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/safe_transaction_history" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/__init__.py b/docs/__init__.py new file mode 100644 index 000000000..8772c827b --- /dev/null +++ b/docs/__init__.py @@ -0,0 +1 @@ +# Included so that Django's startproject comment runs against the docs directory diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 000000000..09e73ef09 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,255 @@ +# Gnosis Safe Relay Service documentation build configuration file, created by +# sphinx-quickstart. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import os +import sys + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix of source filenames. +source_suffix = ".rst" + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = "index" + +# General information about the project. +project = "Gnosis Safe Relay Service" +copyright = """2018, Gnosis""" + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = "0.1" +# The full version, including alpha/beta/rc tags. +release = "0.1" + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ["_build"] + +# The reST default role (used for this markup: `text`) to use for all documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = "default" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = "safe_transaction_historydoc" + + +# -- Options for LaTeX output -------------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ( + "index", + "safe_transaction_history.tex", + "Gnosis Safe Relay Service Documentation", + """Gnosis""", + "manual", + ) +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ( + "index", + "safe_transaction_history", + "Gnosis Safe Relay Service Documentation", + ["""Gnosis"""], + 1, + ) +] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------------ + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + "index", + "safe_transaction_history", + "Gnosis Safe Relay Service Documentation", + """Gnosis""", + "Gnosis Safe Relay Service", + """Project to manage tx relay for Gnosis Safe""", + "Miscellaneous", + ) +] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' diff --git a/docs/deploy.rst b/docs/deploy.rst new file mode 100644 index 000000000..1e642c798 --- /dev/null +++ b/docs/deploy.rst @@ -0,0 +1,4 @@ +Deploy +======== + +This is where you describe how the project is deployed in production. diff --git a/docs/docker_ec2.rst b/docs/docker_ec2.rst new file mode 100644 index 000000000..606d29563 --- /dev/null +++ b/docs/docker_ec2.rst @@ -0,0 +1,186 @@ +Developing with Docker +====================== + +You can develop your application in a `Docker`_ container for simpler deployment onto bare Linux machines later. This instruction assumes an `Amazon Web Services`_ EC2 instance, but it should work on any machine with Docker > 1.3 and `Docker compose`_ installed. + +.. _Docker: https://www.docker.com/ +.. _Amazon Web Services: http://aws.amazon.com/ +.. _Docker compose: https://docs.docker.com/compose/ + +Setting up +^^^^^^^^^^ + +Docker encourages running one container for each process. This might mean one container for your web server, one for Django application and a third for your database. Once you're happy composing containers in this way you can easily add more, such as a `Redis`_ cache. + +.. _Redis: http://redis.io/ + +The Docker compose tool (previously known as `fig`_) makes linking these containers easy. An example set up for your Cookiecutter Django project might look like this: + +.. _fig: http://www.fig.sh/ + +:: + + webapp/ # Your cookiecutter project would be in here + Dockerfile + ... + database/ + Dockerfile + ... + webserver/ + Dockerfile + ... + production.yml + +Each component of your application would get its own `Dockerfile`_. The rest of this example assumes you are using the `base postgres image`_ for your database. Your database settings in `config/base.py` might then look something like: + +.. _Dockerfile: https://docs.docker.com/reference/builder/ +.. _base postgres image: https://registry.hub.docker.com/_/postgres/ + +.. code-block:: python + + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': 'postgres', + 'USER': 'postgres', + 'HOST': 'database', + 'PORT': 5432, + } + } + +The `Docker compose documentation`_ explains in detail what you can accomplish in the `production.yml` file, but an example configuration might look like this: + +.. _Docker compose documentation: https://docs.docker.com/compose/#compose-documentation + +.. code-block:: yaml + + database: + build: database + webapp: + build: webapp: + command: /usr/bin/python3.6 manage.py runserver 0.0.0.0:8000 # dev setting + # command: gunicorn -b 0.0.0.0:8000 wsgi:application # production setting + volumes: + - webapp/your_project_name:/path/to/container/workdir/ + links: + - database + webserver: + build: webserver + ports: + - "80:80" + - "443:443" + links: + - webapp + +We'll ignore the webserver for now (you'll want to comment that part out while we do). A working Dockerfile to run your cookiecutter application might look like this: + +:: + + FROM ubuntu:14.04 + ENV REFRESHED_AT 2015-01-13 + + # update packages and prepare to build software + RUN ["apt-get", "update"] + RUN ["apt-get", "-y", "install", "build-essential", "vim", "git", "curl"] + RUN ["locale-gen", "en_GB.UTF-8"] + + # install latest python + RUN ["apt-get", "-y", "build-dep", "python3-dev", "python3-imaging"] + RUN ["apt-get", "-y", "install", "python3-dev", "python3-imaging", "python3-pip"] + + # prepare postgreSQL support + RUN ["apt-get", "-y", "build-dep", "python3-psycopg2"] + + # move into our working directory + # ADD must be after chown see http://stackoverflow.com/a/26145444/1281947 + RUN ["groupadd", "python"] + RUN ["useradd", "python", "-s", "/bin/bash", "-m", "-g", "python", "-G", "python"] + ENV HOME /home/python + WORKDIR /home/python + RUN ["chown", "-R", "python:python", "/home/python"] + ADD ./ /home/python + + # manage requirements + ENV REQUIREMENTS_REFRESHED_AT 2015-02-25 + RUN ["pip3", "install", "-r", "requirements.txt"] + + # uncomment the line below to use container as a non-root user + USER python:python + +Running `sudo docker-compose -f production.yml build` will follow the instructions in your `production.yml` file and build the database container, then your webapp, before mounting your cookiecutter project files as a volume in the webapp container and linking to the database. Our example yaml file runs in development mode but changing it to production mode is as simple as commenting out the line using `runserver` and uncommenting the line using `gunicorn`. + +Both are set to run on port `0.0.0.0:8000`, which is where the Docker daemon will discover it. You can now run `sudo docker-compose -f production.yml up` and browse to `localhost:8000` to see your application running. + +Deployment +^^^^^^^^^^ + +You'll need a webserver container for deployment. An example setup for `Nginx`_ might look like this: + +.. _Nginx: http://wiki.nginx.org/Main + +:: + + FROM ubuntu:14.04 + ENV REFRESHED_AT 2015-02-11 + + # get the nginx package and set it up + RUN ["apt-get", "update"] + RUN ["apt-get", "-y", "install", "nginx"] + + # forward request and error logs to docker log collector + RUN ln -sf /dev/stdout /var/log/nginx/access.log + RUN ln -sf /dev/stderr /var/log/nginx/error.log + VOLUME ["/var/cache/nginx"] + EXPOSE 80 443 + + # load nginx conf + ADD ./site.conf /etc/nginx/sites-available/your_cookiecutter_project + RUN ["ln", "-s", "/etc/nginx/sites-available/your_cookiecutter_project", "/etc/nginx/sites-enabled/your_cookiecutter_project"] + RUN ["rm", "-rf", "/etc/nginx/sites-available/default"] + + #start the server + CMD ["nginx", "-g", "daemon off;"] + +That Dockerfile assumes you have an Nginx conf file named `site.conf` in the same directory as the webserver Dockerfile. A very basic example, which forwards traffic onto the development server or gunicorn for processing, would look like this: + +:: + + # see http://serverfault.com/questions/577370/how-can-i-use-environment-variables-in-nginx-conf#comment730384_577370 + upstream localhost { + server webapp_1:8000; + } + server { + location / { + proxy_pass http://localhost; + } + } + +Running `sudo docker-compose -f production.yml build webserver` will build your server container. Running `sudo docker-compose -f production.yml up` will now expose your application directly on `localhost` (no need to specify the port number). + +Building and running your app on EC2 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +All you now need to do to run your app in production is: + +* Create an empty EC2 Linux instance (any Linux machine should do). + +* Install your preferred source control solution, Docker and Docker compose on the news instance. + +* Pull in your code from source control. The root directory should be the one with your `production.yml` file in it. + +* Run `sudo docker-compose -f production.yml build` and `sudo docker-compose -f production.yml up`. + +* Assign an `Elastic IP address`_ to your new machine. + +.. _Elastic IP address: https://aws.amazon.com/articles/1346 + +* Point your domain name to the elastic IP. + +**Be careful with Elastic IPs** because, on the AWS free tier, if you assign one and then stop the machine you will incur charges while the machine is down (presumably because you're preventing them allocating the IP to someone else). + +Security advisory +^^^^^^^^^^^^^^^^^ + +The setup described in this instruction will get you up-and-running but it hasn't been audited for security. If you are running your own setup like this it is always advisable to, at a minimum, examine your application with a tool like `OWASP ZAP`_ to see what security holes you might be leaving open. + +.. _OWASP ZAP: https://www.owasp.org/index.php/OWASP_Zed_Attack_Proxy_Project diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 000000000..0c6caab21 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,26 @@ +.. Gnosis Safe Push Service documentation master file, created by + sphinx-quickstart. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Gnosis Safe Push Service's documentation! +==================================================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + install + deploy + docker_ec2 + tests + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/install.rst b/docs/install.rst new file mode 100644 index 000000000..1bc03335d --- /dev/null +++ b/docs/install.rst @@ -0,0 +1,4 @@ +Install +========= + +This is where you write how to get a new laptop to run this project. diff --git a/manage.py b/manage.py new file mode 100644 index 000000000..c757cca89 --- /dev/null +++ b/manage.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == '__main__': + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.local') + + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django # noqa + except ImportError: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + raise + + # This allows easy placement of apps within the interior + # safe_transaction_history directory. + current_path = os.path.dirname(os.path.abspath(__file__)) + sys.path.append(os.path.join(current_path, 'safe_transaction_history')) + + execute_from_command_line(sys.argv) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..5b4369b89 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +DJANGO_SETTINGS_MODULE=config.settings.test diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 000000000..a22e80638 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,5 @@ +-r requirements.txt +-r requirements-test.txt +isort +ipdb +flake8==3.5.0 diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 000000000..171b4e33f --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,5 @@ +-r requirements.txt +pytest==3.6.2 +pytest-django==3.3.2 +pytest-sugar==0.9.1 +coverage==4.5.1 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..636ed6a51 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,26 @@ +Django==2.0.6 +celery==4.2.0 +django-authtools==1.6.0 +django-celery-beat==1.1.1 +django-debug-toolbar==1.9.1 +django-environ==0.4.5 +django-filter==1.1.0 +django-model-utils==3.1.2 +django-redis==4.9.0 +django-rest-swagger==2.2.0 +djangorestframework-camel-case==1.0b1 +djangorestframework==3.8.2 +docutils==0.14 +ethereum==2.3.1 +factory-boy==2.11.1 +faker==0.8.16 +gevent==1.3.4 +gunicorn==19.8.1 +hexbytes==0.1.0 +jsonschema==2.6.0 +numpy==1.14.5 +psycopg2-binary==2.7.5 +redis==2.10.6 +requests==2.19.1 +rlp==0.6.0 +web3==4.4.1 diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 000000000..79c1d93b3 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -euo pipefail + +docker-compose build --force-rm +docker-compose create +docker restart safe-relay-service_db_1 safe-relay-service_redis_1 safe-relay-service_ganache_1 +DJANGO_DOT_ENV_FILE=.env_local pytest diff --git a/safe_transaction_history/__init__.py b/safe_transaction_history/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/safe_transaction_history/ether/__init__.py b/safe_transaction_history/ether/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/safe_transaction_history/ether/exceptions.py b/safe_transaction_history/ether/exceptions.py new file mode 100644 index 000000000..437e49834 --- /dev/null +++ b/safe_transaction_history/ether/exceptions.py @@ -0,0 +1,26 @@ +class UnknownBlockReorgException(Exception): + pass + + +class NoBackupException(Exception): + def __init__(self, message, errors): + super().__init__(message) + + # Now for your custom code... + self.errors = errors + + +class Web3ConnectionException(Exception): + pass + + +class UnknownBlock(Exception): + pass + + +class UnknownTransaction(Exception): + pass + + +class InvalidAddressException(Exception): + pass diff --git a/safe_transaction_history/ether/signing.py b/safe_transaction_history/ether/signing.py new file mode 100644 index 000000000..cb9bd1659 --- /dev/null +++ b/safe_transaction_history/ether/signing.py @@ -0,0 +1,69 @@ +from django.conf import settings +from ethereum import utils + + +class EthereumSignedMessage: + + def __init__(self, message: str, v: int, r: int, s: int, hash_prefix: str=settings.ETH_HASH_PREFIX): + """ + :param message: message + :type message: str + :param v: v parameter of ethereum signing + :type v: int + :param r: r parameter of ethereum signing + :type r: int + :param s: s parameter of ethereum signing + :type s: int + :param hash_prefix: hash_prefix of the message to avoid injecting transactions or other payloads + :type hash_prefix: str + """ + + self.hash_prefix = hash_prefix if hash_prefix else '' + self.message = message + self.message_hash = self.calculate_hash(message) + self.v = int(v) + self.r = int(r) + self.s = int(s) + + def calculate_hash(self, message: str) -> bytes: + return utils.sha3(self.hash_prefix + message) + + def check_message_hash(self, message: str) -> bool: + """ + :param message: message to check if hash matches + :type message: str + :return: true if message matches, false otherwise + :rtype: bool + """ + return utils.sha3(self.hash_prefix + message) == self.message_hash + + def get_signing_address(self) -> str: + """ + :return: checksum encoded address starting by 0x, for example `0x568c93675A8dEb121700A6FAdDdfE7DFAb66Ae4A` + :rtype: str + """ + encoded_64_address = utils.ecrecover_to_pub(self.message_hash, self.v, self.r, self.s) + address_bytes = utils.sha3(encoded_64_address)[-20:] + return utils.checksum_encode(address_bytes) + + def check_signing_address(self, address: str) -> bool: + """ + :param address: address in any format + :type address: str + :return: true if this address was used to sign the message, false otherwise + :rtype: bool + """ + return utils.normalize_address(address) == utils.normalize_address(self.get_signing_address()) + + +class EthereumSigner(EthereumSignedMessage): + + def __init__(self, message: str, key: bytes, hash_prefix: str=settings.ETH_HASH_PREFIX): + """ + :param message: message + :param key: ethereum key for signing the message + :param hash_prefix: prefix for hashing + """ + self.hash_prefix = hash_prefix if hash_prefix else '' + v, r, s = utils.ecsign(self.calculate_hash(message), key) + super().__init__(message, v, r, s, hash_prefix=hash_prefix) diff --git a/safe_transaction_history/ether/tests/__init__.py b/safe_transaction_history/ether/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/safe_transaction_history/ether/tests/factories.py b/safe_transaction_history/ether/tests/factories.py new file mode 100644 index 000000000..13b2aa8c4 --- /dev/null +++ b/safe_transaction_history/ether/tests/factories.py @@ -0,0 +1,24 @@ +import os + +from ethereum import utils + + +def get_eth_address_with_key() -> (str, bytes): + + # import secp256k1 + # private_key = secp256k1.PrivateKey().private_key + + private_key = utils.sha3(os.urandom(4096)) + + public_key = utils.checksum_encode(utils.privtoaddr(private_key)) + + # If you want to use secp256k1 to calculate public_key + # utils.checksum_encode(utils.sha3(p.pubkey.serialize(compressed=False)[1:])[-20:]) + + return (public_key, + private_key) + + +def get_eth_address_with_invalid_checksum() -> str: + address, _ = get_eth_address_with_key() + return '0x' + ''.join([c.lower() if c.isupper() else c.upper() for c in address[2:]]) diff --git a/safe_transaction_history/ether/tests/test_signing.py b/safe_transaction_history/ether/tests/test_signing.py new file mode 100644 index 000000000..42676802d --- /dev/null +++ b/safe_transaction_history/ether/tests/test_signing.py @@ -0,0 +1,46 @@ +from django.conf import settings +from django.test import TestCase +from ethereum import utils +from faker import Faker + +from ..signing import EthereumSignedMessage, EthereumSigner +from .factories import get_eth_address_with_key + +faker = Faker() + + +class TestSigning(TestCase): + + def test_ethereum_signer(self): + eth_address, eth_key = get_eth_address_with_key() + eth_address_bad_checksum = eth_address.lower() + + message = faker.name() + prefix = faker.name() + ethereum_signer = EthereumSigner(message, eth_key, hash_prefix=prefix) + + self.assertEqual(ethereum_signer.hash_prefix, prefix) + self.assertEqual(ethereum_signer.message_hash, utils.sha3(prefix + message)) + self.assertEqual(ethereum_signer.get_signing_address(), eth_address) + self.assertTrue(ethereum_signer.check_signing_address(eth_address_bad_checksum)) + + def test_ethereum_signed_message(self): + eth_address, eth_key = get_eth_address_with_key() + eth_address_bad_checksum = eth_address.lower() + + prefix = settings.ETH_HASH_PREFIX + message = faker.name() + prefixed_message = prefix + message + message_hash = utils.sha3(prefixed_message) + v, r, s = utils.ecsign(message_hash, eth_key) + ethereum_signed_message = EthereumSignedMessage(message, v, r, s) + + self.assertTrue(ethereum_signed_message.check_message_hash(message)) + + self.assertTrue(ethereum_signed_message.check_signing_address(eth_address)) + self.assertTrue(ethereum_signed_message.check_signing_address(eth_address.lower())) + self.assertTrue(ethereum_signed_message.check_signing_address(eth_address[2:])) + self.assertTrue(ethereum_signed_message.check_signing_address(eth_address_bad_checksum)) + + self.assertEqual(ethereum_signed_message.get_signing_address(), eth_address) + self.assertTrue(utils.check_checksum(ethereum_signed_message.get_signing_address())) diff --git a/safe_transaction_history/ether/utils.py b/safe_transaction_history/ether/utils.py new file mode 100644 index 000000000..519759a2f --- /dev/null +++ b/safe_transaction_history/ether/utils.py @@ -0,0 +1 @@ +NULL_ADDRESS = '0x' + '0' * 40 diff --git a/safe_transaction_history/ether/web3_service.py b/safe_transaction_history/ether/web3_service.py new file mode 100644 index 000000000..36c53a4d7 --- /dev/null +++ b/safe_transaction_history/ether/web3_service.py @@ -0,0 +1,282 @@ +import concurrent.futures +import logging +import socket +from typing import Dict, List + +import requests +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from requests.exceptions import ConnectionError +from web3 import HTTPProvider, IPCProvider, Web3, WebsocketProvider +from web3.middleware import geth_poa_middleware + +from .exceptions import (UnknownBlock, UnknownTransaction, + Web3ConnectionException) + +logger = logging.getLogger(__name__) + + +class Web3Service(object): + """ + Singleton Web3 object manager, returns a new object if + the argument provider is different than the current used provider. + """ + instance = None + + def __new__(cls, provider=None): + if not provider: + node_url = settings.ETHEREUM_NODE_URL + if node_url.startswith('http'): + provider = HTTPProvider(endpoint_uri=node_url) + elif node_url.startswith('ipc'): + path = node_url.replace('ipc://', '') + provider = IPCProvider(ipc_path=path) + elif node_url.startswith('ws'): + provider = WebsocketProvider(endpoint_uri=node_url) + else: + raise ImproperlyConfigured('Bad value for ETHEREUM_NODE_URL: {}'.format(node_url)) + + if not Web3Service.instance: + Web3Service.instance = Web3Service.__Web3Service(provider) + elif provider and not isinstance(provider, + Web3Service.instance.web3.providers[0].__class__): + Web3Service.instance = Web3Service.__Web3Service(provider) + return Web3Service.instance + + def __getattr__(self, item): + return getattr(self.instance, item) + + class __Web3Service: + max_workers: int = settings.ETHEREUM_MAX_WORKERS + max_batch_requests: int = settings.ETHEREUM_MAX_BATCH_REQUESTS + + def __init__(self, provider): + self.provider = provider + self.web3 = Web3(provider) + self.http_session = requests.session() + + # If not in the mainNet, inject Geth PoA middleware + # http://web3py.readthedocs.io/en/latest/middleware.html#geth-style-proof-of-authority + try: + if self.web3.net.chainId != 1: + self.web3.middleware_stack.inject(geth_poa_middleware, layer=0) + # For tests using dummy connections (like IPC) + except (ConnectionError, FileNotFoundError): + pass + + @property + def main_provider(self): + return self.web3.providers[0] + + def make_sure_cheksumed_address(self, address: str) -> str: + """ + Makes sure an address is checksumed. If not, returns it checksumed + and logs a warning + :param address: ethereum address + :return: checksumed 0x address + """ + if self.web3.isChecksumAddress(address): + return address + else: + checksumed_address = self.web3.toChecksumAddress(address) + logger.warning("Address %s is not checksumed, should be %s", address, checksumed_address) + return checksumed_address + + def is_connected(self) -> bool: + try: + return self.web3.isConnected() + except socket.timeout: + return False + + def get_current_block_number(self) -> int: + """ + :raises Web3ConnectionException + :return: + """ + try: + return self.web3.eth.blockNumber + except (socket.timeout, ConnectionError): + raise Web3ConnectionException('Web3 provider is not connected') + + def get_transaction_receipt(self, transaction_hash): + """ + :param transaction_hash: + :raises Web3ConnectionException + :raises UnknownTransaction + :return: + """ + try: + receipt = self.web3.eth.getTransactionReceipt(transaction_hash) + + # receipt sometimes is none, might be because a reorg, we exit the loop with a controlled exception + if receipt is None: + raise UnknownTransaction + return receipt + except (socket.timeout, ConnectionError): + raise Web3ConnectionException('Web3 provider is not connected') + except Exception as e: + raise UnknownTransaction from e + + def get_transaction_receipts(self, tx_hashes): + + tx_with_receipt = {} + + if isinstance(self.provider, HTTPProvider): + # Query limit for RPC is 131072 + for tx_hashes_chunk in self._chunks(tx_hashes, self.max_batch_requests): + rpc_request = [self._build_tx_receipt_request(tx_hash) for tx_hash in tx_hashes_chunk] + for rpc_response in self._do_request(rpc_request): + tx = rpc_response['result'] + if not tx: + raise UnknownTransaction + tx_hash = tx['transactionHash'] + tx_with_receipt[tx_hash] = tx + else: + with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor: + future_receipts_with_tx = {executor.submit(self.get_transaction_receipt, tx): tx + for tx in tx_hashes} + + for future in concurrent.futures.as_completed(future_receipts_with_tx): + tx = future_receipts_with_tx[future] + receipt = future.result() + if not receipt: + raise UnknownTransaction + tx_with_receipt[tx] = receipt + + return tx_with_receipt + + def get_block(self, block_identifier, full_transactions=False): + """ + :param block_identifier: + :param full_transactions: + :raises Web3ConnectionException + :raises UnknownBlock + :return: + """ + try: + block = self.web3.eth.getBlock(block_identifier, full_transactions) + if not block: + raise UnknownBlock + return block + except (socket.timeout, ConnectionError): + raise Web3ConnectionException('Web3 provider is not connected') + except Exception as e: + raise UnknownBlock from e + + def get_blocks(self, block_identifiers, full_transactions=False): + """ + :param block_identifiers: + :param full_transactions: + :raises Web3ConnectionException + :raises UnknownBlock + :return: + """ + + blocks = {} + + if isinstance(self.provider, HTTPProvider): + # Query limit for RPC is 131072 + for block_numbers_chunk in self._chunks(block_identifiers, self.max_batch_requests): + rpc_request = [self._build_block_request(block_number) for block_number in block_numbers_chunk] + for rpc_response in self._do_request(rpc_request): + block = rpc_response['result'] + if not block: + raise UnknownBlock + + block_number = int(block['number'], 16) + block['number'] = block_number + block['timestamp'] = int(block['timestamp'], 16) + blocks[block_number] = block + else: + with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor: + # Get blocks from ethereum node and mark each future with its block_id + future_to_block_id = {executor.submit(self.get_block, block_id, full_transactions): block_id + for block_id in block_identifiers} + for future in concurrent.futures.as_completed(future_to_block_id): + block_id = future_to_block_id[future] + block = future.result() + if not block: + raise UnknownBlock + blocks[block_id] = block + + return blocks + + def get_current_block(self, full_transactions=False): + """ + :param full_transactions: + :raises Web3ConnectionException + :raises UnknownBlock + :return: + """ + return self.get_block(self.get_current_block_number(), full_transactions) + + def get_logs(self, block): + """ + Extract raw logs from web3 ethereum block + :param block: web3 block to get logs from + :return: list of log dictionaries + """ + + logs = [] + tx_with_receipt = self.get_transaction_receipts(block['transactions']) + + for tx in block['transactions']: + receipt = tx_with_receipt[tx] + logs.extend(receipt.get('logs', [])) + + return logs + + def get_logs_for_blocks(self, blocks): + """ + Recover logs for every block + :param blocks: web3 blocks to get logs from + :return: a dictionary, the key is the block number and value is list of + """ + + block_number_with_logs = {} + + with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor: + # Get blocks from ethereum node and mark each future with its block_id + future_logs_to_block = {executor.submit(self.get_logs, block): block for block in blocks} + for future in concurrent.futures.as_completed(future_logs_to_block): + block = future_logs_to_block[future] + logs = future.result() + block_number_with_logs[block['number']] = logs + + return block_number_with_logs + + def get_logs_using_filter(self, from_block: int, to_block: int, address: str) -> List[any]: + """ + Recover logs using filter + """ + + return self.web3.eth.getLogs(({'fromBlock': from_block, + 'toBlock': to_block, + 'address': address})) + + def _do_request(self, rpc_request): + if isinstance(self.provider, HTTPProvider): + return self.http_session.post(self.provider.endpoint_uri, json=rpc_request).json() + # elif isinstance(self.provider, IPCProvider): + # elif isinstance(self.provider, WebsocketProvider): + # elif isinstance(self.provider, EthereumTesterProvider): + else: + raise ImproperlyConfigured('Not valid provider') + + def _build_block_request(self, block_number: int, full_transactions: bool=False) -> Dict[str, any]: + block_number_hex = '0x{:x}'.format(block_number) + return {"jsonrpc": "2.0", + "method": "eth_getBlockByNumber", + "params": [block_number_hex, full_transactions], + "id": 1 + } + + def _build_tx_receipt_request(self, tx_hash: str) -> Dict[str, any]: + return {"jsonrpc": "2.0", + "method": "eth_getTransactionReceipt", + "params": [tx_hash], + "id": 1} + + def _chunks(self, iterable, size): + for i in range(0, len(iterable), size): + yield iterable[i:i + size] diff --git a/safe_transaction_history/safe/__init__.py b/safe_transaction_history/safe/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/safe_transaction_history/safe/admin.py b/safe_transaction_history/safe/admin.py new file mode 100644 index 000000000..e69de29bb diff --git a/safe_transaction_history/safe/apps.py b/safe_transaction_history/safe/apps.py new file mode 100644 index 000000000..22dc05ec2 --- /dev/null +++ b/safe_transaction_history/safe/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class SafeConfig(AppConfig): + name = 'safe_transaction_history.safe' diff --git a/safe_transaction_history/safe/ethereum_service.py b/safe_transaction_history/safe/ethereum_service.py new file mode 100644 index 000000000..0bd6fe621 --- /dev/null +++ b/safe_transaction_history/safe/ethereum_service.py @@ -0,0 +1,144 @@ +from logging import getLogger + +from ethereum.utils import (check_checksum, checksum_encode, ecrecover_to_pub, + privtoaddr, sha3) +from web3 import HTTPProvider, Web3 +from web3.middleware import geth_poa_middleware +from web3.utils.threads import Timeout + +from safe_transaction_history.ether.utils import NULL_ADDRESS + +logger = getLogger(__name__) + + +class EthereumServiceProvider: + def __new__(cls): + if not hasattr(cls, 'instance'): + from django.conf import settings + cls.instance = EthereumService(settings.ETHEREUM_NODE_URL, + settings.SAFE_FUNDER_MAX_ETH, + settings.SAFE_FUNDER_PRIVATE_KEY) + return cls.instance + + +class EthereumService: + NULL_ADDRESS = NULL_ADDRESS + + def __init__(self, ethereum_node_url, max_eth_to_send=0.1, funder_private_key=None): + self.ethereum_node_url = ethereum_node_url + self.max_eth_to_send = max_eth_to_send + self.funder_private_key = funder_private_key + self.w3 = Web3(HTTPProvider(self.ethereum_node_url)) + try: + if self.w3.net.chainId != 1: + self.w3.middleware_stack.inject(geth_poa_middleware, layer=0) + # For tests using dummy connections (like IPC) + except (ConnectionError, FileNotFoundError): + self.w3.middleware_stack.inject(geth_poa_middleware, layer=0) + + def get_nonce_for_account(self, address): + return self.w3.eth.getTransactionCount(address, 'pending') + + @property + def current_block_number(self): + return self.w3.eth.blockNumber + + @staticmethod + def estimate_data_gas(data: bytes): + gas = 0 + for byte in data: + if not byte: + gas += 4 # Byte 0 -> 4 Gas + else: + gas += 68 # Any other byte -> 68 Gas + return gas + + def get_balance(self, address, block_identifier=None): + return self.w3.eth.getBalance(address, block_identifier) + + def get_transaction_receipt(self, tx_hash, timeout=None): + if not timeout: + return self.w3.eth.getTransactionReceipt(tx_hash) + else: + try: + return self.w3.eth.waitForTransactionReceipt(tx_hash, timeout=timeout) + except Timeout: + return None + + def send_raw_transaction(self, raw_transaction): + return self.w3.eth.sendRawTransaction(bytes(raw_transaction)) + + def send_eth_to(self, to: str, gas_price: int, value: int, gas: int=22000) -> bytes: + """ + Send ether using configured account + :param to: to + :param gas_price: gas_price + :param value: value(wei) + :param gas: gas, defaults to 22000 + :return: tx_hash + """ + + assert check_checksum(to) + + assert value < self.w3.toWei(self.max_eth_to_send, 'ether') + + private_key = self.funder_private_key + + if private_key: + ethereum_account = self.private_key_to_address(private_key) + tx = { + 'to': to, + 'value': value, + 'gas': gas, + 'gasPrice': gas_price, + 'nonce': self.get_nonce_for_account(ethereum_account), + } + + signed_tx = self.w3.eth.account.signTransaction(tx, private_key=private_key) + logger.debug('Sending %d wei from %s to %s', value, ethereum_account, to) + return self.w3.eth.sendRawTransaction(signed_tx.rawTransaction) + elif self.w3.eth.accounts: + ethereum_account = self.w3.eth.accounts[0] + tx = { + 'from': ethereum_account, + 'to': to, + 'value': value, + 'gas': gas, + 'gasPrice': gas_price, + 'nonce': self.get_nonce_for_account(ethereum_account), + } + logger.debug('Sending %d wei from %s to %s', value, ethereum_account, to) + return self.w3.eth.sendTransaction(tx) + else: + logger.error('No ethereum account configured') + raise ValueError("Ethereum account was not configured or unlocked in the node") + + def check_tx_with_confirmations(self, tx_hash: str, confirmations: int) -> bool: + """ + Check tx hash and make sure it has the confirmations required + :param w3: Web3 instance + :param tx_hash: Hash of the tx + :param confirmations: Minimum number of confirmations required + :return: True if tx was mined with the number of confirmations required, False otherwise + """ + tx_receipt = self.w3.eth.getTransactionReceipt(tx_hash) + if not tx_receipt: + return False + else: + block_number = self.w3.eth.blockNumber + tx_block_number = tx_receipt['blockNumber'] + return (block_number - tx_block_number) >= confirmations + + @staticmethod + def private_key_to_address(private_key): + return checksum_encode(privtoaddr(private_key)) + + @staticmethod + def get_signing_address(hash, v, r, s) -> str: + """ + :return: checksum encoded address starting by 0x, for example `0x568c93675A8dEb121700A6FAdDdfE7DFAb66Ae4A` + :rtype: str + """ + encoded_64_address = ecrecover_to_pub(hash, v, r, s) + address_bytes = sha3(encoded_64_address)[-20:] + return checksum_encode(address_bytes) diff --git a/safe_transaction_history/safe/filters.py b/safe_transaction_history/safe/filters.py new file mode 100644 index 000000000..42a91cfab --- /dev/null +++ b/safe_transaction_history/safe/filters.py @@ -0,0 +1,7 @@ +from django_filters import rest_framework as filters +from rest_framework.pagination import LimitOffsetPagination + + +class DefaultPagination(LimitOffsetPagination): + max_limit = 200 + default_limit = 100 diff --git a/safe_transaction_history/safe/management/__init__.py b/safe_transaction_history/safe/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/safe_transaction_history/safe/management/commands/__init__.py b/safe_transaction_history/safe/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/safe_transaction_history/safe/migrations/__init__.py b/safe_transaction_history/safe/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/safe_transaction_history/safe/models.py b/safe_transaction_history/safe/models.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/safe_transaction_history/safe/models.py @@ -0,0 +1 @@ + diff --git a/safe_transaction_history/safe/redis_service.py b/safe_transaction_history/safe/redis_service.py new file mode 100644 index 000000000..9db9bddbd --- /dev/null +++ b/safe_transaction_history/safe/redis_service.py @@ -0,0 +1,12 @@ +from django.conf import settings +from redis import Redis + + +class RedisService: + def __new__(cls): + if not hasattr(cls, 'instance'): + cls.instance = super().__new__(cls) + return cls.instance + + def __init__(self): + self.redis = Redis.from_url(settings.REDIS_URL) diff --git a/safe_transaction_history/safe/serializers.py b/safe_transaction_history/safe/serializers.py new file mode 100644 index 000000000..e69de29bb diff --git a/safe_transaction_history/safe/tasks.py b/safe_transaction_history/safe/tasks.py new file mode 100644 index 000000000..e69de29bb diff --git a/safe_transaction_history/safe/tests/__init__.py b/safe_transaction_history/safe/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/safe_transaction_history/safe/tests/factories.py b/safe_transaction_history/safe/tests/factories.py new file mode 100644 index 000000000..8605d5f5a --- /dev/null +++ b/safe_transaction_history/safe/tests/factories.py @@ -0,0 +1,11 @@ +import os +from logging import getLogger + +from ethereum.transactions import secpk1n +from faker import Factory as FakerFactory +from faker import Faker + +fakerFactory = FakerFactory.create() +faker = Faker() + +logger = getLogger(__name__) diff --git a/safe_transaction_history/safe/tests/test_serializers.py b/safe_transaction_history/safe/tests/test_serializers.py new file mode 100644 index 000000000..e69de29bb diff --git a/safe_transaction_history/safe/tests/test_tasks.py b/safe_transaction_history/safe/tests/test_tasks.py new file mode 100644 index 000000000..e69de29bb diff --git a/safe_transaction_history/safe/tests/test_views.py b/safe_transaction_history/safe/tests/test_views.py new file mode 100644 index 000000000..e69de29bb diff --git a/safe_transaction_history/safe/urls.py b/safe_transaction_history/safe/urls.py new file mode 100644 index 000000000..9dfade125 --- /dev/null +++ b/safe_transaction_history/safe/urls.py @@ -0,0 +1,12 @@ +from django.conf.urls import url +from django.urls import path + +from . import views + +app_name = "safe" + +timestamp_regex = '\\d{4}[-]?\\d{1,2}[-]?\\d{1,2} \\d{1,2}:\\d{1,2}:\\d{1,2}' + +urlpatterns = [ + +] diff --git a/safe_transaction_history/safe/views.py b/safe_transaction_history/safe/views.py new file mode 100644 index 000000000..e69de29bb diff --git a/safe_transaction_history/static/.gitignore b/safe_transaction_history/static/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/safe_transaction_history/taskapp/__init__.py b/safe_transaction_history/taskapp/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/safe_transaction_history/taskapp/celery.py b/safe_transaction_history/taskapp/celery.py new file mode 100644 index 000000000..b74856893 --- /dev/null +++ b/safe_transaction_history/taskapp/celery.py @@ -0,0 +1,30 @@ +import os + +from celery import Celery +from celery.signals import setup_logging +from django.apps import AppConfig, apps +from django.conf import settings + +if not settings.configured: + # set the default Django settings module for the 'celery' program. + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.local') # pragma: no cover + + +app = Celery('safe_transaction_history') + + +class CeleryConfig(AppConfig): + name = 'safe_transaction_history.taskapp' + verbose_name = 'Celery Config' + + # Use Django logging instead of celery logger + @setup_logging.connect + def on_celery_setup_logging(**kwargs): + pass + + def ready(self): + # Using a string here means the worker will not have to + # pickle the object when using Windows. + app.config_from_object('django.conf:settings') + installed_apps = [app_config.name for app_config in apps.get_app_configs()] + app.autodiscover_tasks(lambda: installed_apps, force=True) diff --git a/safe_transaction_history/utils/__init__.py b/safe_transaction_history/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/safe_transaction_history/utils/singleton.py b/safe_transaction_history/utils/singleton.py new file mode 100644 index 000000000..cf5e9db7d --- /dev/null +++ b/safe_transaction_history/utils/singleton.py @@ -0,0 +1,8 @@ +def singleton(clazz): + instances = {} + + def getinstance(*args, **kwargs): + if clazz not in instances: + instances[clazz] = clazz(*args, **kwargs) + return instances[clazz] + return getinstance diff --git a/safe_transaction_history/utils/tests/__init__.py b/safe_transaction_history/utils/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/safe_transaction_history/utils/tests/test_utils.py b/safe_transaction_history/utils/tests/test_utils.py new file mode 100644 index 000000000..9343ee115 --- /dev/null +++ b/safe_transaction_history/utils/tests/test_utils.py @@ -0,0 +1,23 @@ +from django.test import TestCase +from faker import Faker + +from ..singleton import singleton + +faker = Faker() + +@singleton +class MyClass: + def __init__(self, name): + self.name = name + + +class TestSigning(TestCase): + + def test_singleton(self): + name = faker.name() + my_class = MyClass(name) + + another_name = faker.name() + my_other_class = MyClass(another_name) + + self.assertEqual(my_class.name, my_other_class.name) diff --git a/safe_transaction_history/version.py b/safe_transaction_history/version.py new file mode 100644 index 000000000..46962fc9a --- /dev/null +++ b/safe_transaction_history/version.py @@ -0,0 +1,2 @@ +__version__ = '0.0.1' +__version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')]) diff --git a/scripts/deploy_docker.sh b/scripts/deploy_docker.sh new file mode 100644 index 000000000..5127ae5f0 --- /dev/null +++ b/scripts/deploy_docker.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -euo pipefail + +if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then + echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin + if [ "$1" = "develop" -o "$1" = "master" ]; then + # If image does not exist, don't use cache + docker pull gnosispm/$DOCKERHUB_PROJECT:$1 && \ + docker build -t $DOCKERHUB_PROJECT -f docker/web/Dockerfile . --cache-from gnosispm/$DOCKERHUB_PROJECT:$1 || \ + docker build -t $DOCKERHUB_PROJECT -f docker/web/Dockerfile . + else + docker pull gnosispm/$DOCKERHUB_PROJECT:staging + docker build -t $DOCKERHUB_PROJECT -f docker/web/Dockerfile . --cache-from gnosispm/$DOCKERHUB_PROJECT:staging + fi + docker tag $DOCKERHUB_PROJECT gnosispm/$DOCKERHUB_PROJECT:$1 + docker push gnosispm/$DOCKERHUB_PROJECT:$1 +fi diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..1ec89c789 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,7 @@ +[flake8] +max-line-length = 120 +exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules + +[pycodestyle] +max-line-length = 120 +exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules