├── .gitattributes ├── .gitignore ├── README.md ├── gas-relayer ├── .babelrc ├── .eslintrc ├── abi │ ├── ERC20Token.json │ ├── IdentityFactory.json │ ├── IdentityGasRelay.json │ └── SNTController.json ├── config │ ├── config.js │ └── config.testnet.js ├── launch-geth-testnet.sh ├── package-lock.json ├── package.json ├── plugins │ └── token-utils.js ├── src │ ├── account-parser.js │ ├── contract-settings.js │ ├── message-processor.js │ ├── service.js │ └── strategy │ │ ├── AvailabilityStrategy.js │ │ ├── BaseStrategy.js │ │ ├── IdentityStrategy.js │ │ └── SNTStrategy.js └── test │ ├── sampleContracts.sol │ ├── sendmsg.html │ ├── sendmsg.js │ └── web3.min.js ├── installation-development.md ├── installation-testnet-mainnet.md ├── javascript-library.md ├── relayer-protocol.md └── test-dapp ├── .babelrc ├── .eslintrc ├── .gitignore ├── app ├── components │ ├── approveandcallgasrelayed.js │ ├── body-identity.js │ ├── body-sntcontroller.js │ ├── callgasrelayed.js │ ├── execute.js │ ├── header.js │ ├── snackbar.js │ ├── status-identity.js │ ├── status-sntcontroller.js │ └── transfersnt.js ├── config.js ├── css │ ├── .gitkeep │ └── dapp.css ├── identity.html ├── identity.js ├── images │ └── .gitkeep ├── index.html ├── js │ ├── .gitkeep │ └── index.js ├── sntcontroller.html ├── sntcontroller.js └── status-gas-relayer.js ├── config ├── blockchain.js ├── communication.js ├── contracts.js ├── namesystem.js ├── privatenet │ ├── genesis.json │ └── password ├── storage.js ├── testnet │ └── password └── webserver.js ├── contracts ├── .gitkeep ├── common │ ├── Controlled.sol │ ├── MessageSigned.sol │ ├── Owned.sol │ └── SafeMath.sol ├── deploy │ ├── DelayedUpdatableInstance.sol │ ├── DelayedUpdatableInstanceStorage.sol │ ├── DelegatedCall.sol │ ├── Factory.sol │ ├── Instance.sol │ ├── InstanceStorage.sol │ └── UpdatableInstance.sol ├── identity │ ├── ERC725.sol │ ├── ERC735.sol │ ├── Identity.sol │ ├── IdentityFactory.sol │ ├── IdentityGasRelay.sol │ └── IdentityKernel.sol ├── status │ └── SNTController.sol ├── tests │ ├── TestContract.sol │ ├── TestMiniMeToken.sol │ └── UpdatedIdentityKernel.sol └── token │ ├── ApproveAndCallFallBack.sol │ ├── ERC20Receiver.sol │ ├── ERC20Token.sol │ ├── MiniMeToken.sol │ ├── MiniMeTokenFactory.sol │ ├── MiniMeTokenInterface.sol │ ├── StandardToken.sol │ ├── TestToken.sol │ └── TokenController.sol ├── embark.json ├── launch-geth-testnet.sh ├── package-lock.json ├── package.json ├── setup_dev_env.sh ├── test ├── contract_spec.js ├── erc20token.js ├── factory.js ├── identity.js ├── identityExtended.js ├── identityfactory.js └── testtoken.js └── utils ├── identityUtils.js └── testUtils.js /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # embark 7 | .embark/ 8 | chains.json 9 | config/production/password 10 | config/livenet/password 11 | 12 | # egg-related 13 | viper.egg-info/ 14 | build/ 15 | dist/ 16 | .eggs/ 17 | 18 | # pyenv 19 | .python-version 20 | 21 | # dotenv 22 | .env 23 | 24 | # virtualenv 25 | .venv/ 26 | venv/ 27 | ENV/ 28 | 29 | # Coverage tests 30 | .coverage 31 | .cache/ 32 | coverage/ 33 | coverageEnv/ 34 | coverage.json 35 | 36 | # node 37 | node_modules/ 38 | npm-debug.log 39 | 40 | # other 41 | .vs/ 42 | bin/ 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # token-gas-relayer 2 | Gas relayer mplementation for economic abstraction. This project consists of two elements: 3 | - `gas-relayer`: nodejs service that listens to whisper on a symmetric key, with specific topics, and processes any transaction. 4 | - `test-dapp`: DApp created for testing purposes. It allows the easy creation of the messages expected by the service. 5 | 6 | 7 | ## Documentation 8 | 1. [Installation - testnet/mainnet](installation-testnet-mainnet.md) 9 | 2. [Installation - development environment](installation-development.md) 10 | 3. [Gas relayer protocol](relayer-protocol.md) 11 | 4. [Javascript library](javascript-library.md) 12 | 5. Status Extensions (TODO) 13 | 14 | 15 | ## Deployment Details 16 | | Contract | Ropsten Address | Mainnet Address | 17 | | ---------------------------|------------------------------------------- | ------------------------------------------ | 18 | | status/SNTController | 0x1f42B87b375b8ac6C77A8CAd8E78319c18695E75 | - | 19 | | identity/IdentityFactory | 0xCf3473C2A50F7A94D3D7Dcc2BeBbeE989dAA014E | - | 20 | -------------------------------------------------------------------------------- /gas-relayer/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-object-rest-spread" 4 | ], 5 | "presets": [ 6 | "stage-2" 7 | ], 8 | "ignore": [ 9 | "config/", 10 | "node_modules" 11 | ] 12 | } -------------------------------------------------------------------------------- /gas-relayer/.eslintrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "parser": "babel-eslint", 4 | "plugins": [ ], 5 | "parserOptions": { 6 | "ecmaVersion": 2017, 7 | "sourceType": "module", 8 | "ecmaFeatures": { 9 | "jsx": true 10 | } 11 | }, 12 | "env": { 13 | "es6": true, 14 | "browser": true, 15 | "node": true, 16 | "mocha": true 17 | }, 18 | "extends": [ 19 | "eslint:recommended" 20 | ], 21 | "rules": { 22 | "accessor-pairs": "error", 23 | "array-bracket-newline": "error", 24 | "array-bracket-spacing": [ 25 | "error", 26 | "never" 27 | ], 28 | "array-callback-return": "off", 29 | "array-element-newline": "off", 30 | "arrow-body-style": "off", 31 | "arrow-parens": "off", 32 | "arrow-spacing": [ 33 | "error", 34 | { 35 | "after": true, 36 | "before": true 37 | } 38 | ], 39 | "block-scoped-var": "error", 40 | "block-spacing": "error", 41 | "brace-style": "off", 42 | "callback-return": "off", 43 | "camelcase": "off", 44 | "capitalized-comments": "off", 45 | "class-methods-use-this": "off", 46 | "comma-dangle": "error", 47 | "comma-spacing": "off", 48 | "comma-style": [ 49 | "error", 50 | "last" 51 | ], 52 | "complexity": "error", 53 | "computed-property-spacing": [ 54 | "error", 55 | "never" 56 | ], 57 | "consistent-return": "off", 58 | "consistent-this": "off", 59 | "curly": "off", 60 | "default-case": "error", 61 | "dot-location": [ 62 | "error", 63 | "property" 64 | ], 65 | "dot-notation": "off", 66 | "eol-last": "error", 67 | "eqeqeq": "off", 68 | "for-direction": "error", 69 | "func-call-spacing": "error", 70 | "func-name-matching": "error", 71 | "func-names": "off", 72 | "func-style": "off", 73 | "function-paren-newline": "off", 74 | "generator-star-spacing": "error", 75 | "getter-return": "error", 76 | "global-require": "off", 77 | "guard-for-in": "off", 78 | "handle-callback-err": "off", 79 | "id-blacklist": "error", 80 | "id-length": "off", 81 | "id-match": "error", 82 | "indent": "off", 83 | "indent-legacy": "off", 84 | "init-declarations": "off", 85 | "jsx-quotes": "error", 86 | "key-spacing": "off", 87 | "keyword-spacing": "off", 88 | "line-comment-position": "off", 89 | "linebreak-style": [ 90 | "error", 91 | "unix" 92 | ], 93 | "lines-around-comment": "error", 94 | "lines-around-directive": "error", 95 | "max-depth": "error", 96 | "max-len": "off", 97 | "max-lines": "off", 98 | "max-nested-callbacks": "error", 99 | "max-params": "off", 100 | "max-statements": "off", 101 | "max-statements-per-line": "off", 102 | "multiline-ternary": [ 103 | "error", 104 | "never" 105 | ], 106 | "new-parens": "off", 107 | "newline-after-var": "off", 108 | "newline-before-return": "off", 109 | "newline-per-chained-call": "off", 110 | "no-alert": "error", 111 | "no-array-constructor": "error", 112 | "no-await-in-loop": "error", 113 | "no-bitwise": "error", 114 | "no-buffer-constructor": "error", 115 | "no-caller": "error", 116 | "no-catch-shadow": "error", 117 | "no-confusing-arrow": "error", 118 | "no-console": "off", 119 | "no-continue": "off", 120 | "no-div-regex": "error", 121 | "no-duplicate-imports": "error", 122 | "no-else-return": "off", 123 | "no-empty-function": "off", 124 | "no-eq-null": "error", 125 | "no-eval": "off", 126 | "no-extend-native": "error", 127 | "no-extra-bind": "error", 128 | "no-extra-label": "error", 129 | "no-extra-parens": "off", 130 | "no-floating-decimal": "error", 131 | "no-implicit-coercion": "error", 132 | "no-implicit-globals": "error", 133 | "no-implied-eval": "error", 134 | "no-inline-comments": "off", 135 | "no-inner-declarations": [ 136 | "error", 137 | "functions" 138 | ], 139 | "no-invalid-this": "off", 140 | "no-iterator": "error", 141 | "no-label-var": "error", 142 | "no-labels": "error", 143 | "no-lone-blocks": "error", 144 | "no-lonely-if": "off", 145 | "no-loop-func": "off", 146 | "no-magic-numbers": "off", 147 | "no-mixed-operators": "error", 148 | "no-mixed-requires": "error", 149 | "no-multi-assign": "error", 150 | "no-multi-spaces": "off", 151 | "no-multi-str": "error", 152 | "no-multiple-empty-lines": "error", 153 | "no-native-reassign": "error", 154 | "no-negated-condition": "off", 155 | "no-negated-in-lhs": "error", 156 | "no-nested-ternary": "error", 157 | "no-new": "error", 158 | "no-new-func": "error", 159 | "no-new-object": "error", 160 | "no-new-require": "error", 161 | "no-new-wrappers": "error", 162 | "no-octal-escape": "error", 163 | "no-param-reassign": "off", 164 | "no-path-concat": "error", 165 | "no-plusplus": "off", 166 | "no-process-env": "off", 167 | "no-process-exit": "off", 168 | "no-proto": "error", 169 | "no-prototype-builtins": "off", 170 | "no-restricted-globals": "error", 171 | "no-restricted-imports": "error", 172 | "no-restricted-modules": "error", 173 | "no-restricted-properties": "error", 174 | "no-restricted-syntax": "error", 175 | "no-return-assign": "error", 176 | "no-return-await": "error", 177 | "no-script-url": "error", 178 | "no-self-compare": "error", 179 | "no-sequences": "error", 180 | "no-shadow": "off", 181 | "no-shadow-restricted-names": "error", 182 | "no-spaced-func": "error", 183 | "no-sync": "off", 184 | "no-tabs": "error", 185 | "no-template-curly-in-string": "error", 186 | "no-ternary": "off", 187 | "no-throw-literal": "error", 188 | "no-trailing-spaces": "off", 189 | "no-undef-init": "error", 190 | "no-undefined": "off", 191 | "no-underscore-dangle": "off", 192 | "no-unmodified-loop-condition": "error", 193 | "no-unneeded-ternary": "error", 194 | "no-unused-expressions": "error", 195 | "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], 196 | "no-use-before-define": "off", 197 | "no-useless-call": "off", 198 | "no-useless-computed-key": "error", 199 | "no-useless-concat": "error", 200 | "no-useless-constructor": "error", 201 | "no-useless-escape": "off", 202 | "no-useless-rename": "error", 203 | "no-useless-return": "off", 204 | "no-var": "off", 205 | "no-void": "error", 206 | "no-warning-comments": "off", 207 | "no-whitespace-before-property": "error", 208 | "no-with": "error", 209 | "nonblock-statement-body-position": "error", 210 | "object-curly-newline": "off", 211 | "object-curly-spacing": [ 212 | "error", 213 | "never" 214 | ], 215 | "object-property-newline": "off", 216 | "object-shorthand": "off", 217 | "one-var": "off", 218 | "one-var-declaration-per-line": "off", 219 | "operator-assignment": "off", 220 | "operator-linebreak": "error", 221 | "padded-blocks": "off", 222 | "padding-line-between-statements": "error", 223 | "prefer-arrow-callback": "off", 224 | "prefer-const": "off", 225 | "prefer-destructuring": "off", 226 | "prefer-numeric-literals": "error", 227 | "prefer-promise-reject-errors": "error", 228 | "prefer-reflect": "off", 229 | "prefer-rest-params": "off", 230 | "prefer-spread": "off", 231 | "prefer-template": "off", 232 | "quote-props": "off", 233 | "quotes": "off", 234 | "radix": "error", 235 | "require-await": "error", 236 | "require-jsdoc": "off", 237 | "rest-spread-spacing": "error", 238 | "semi": "error", 239 | "semi-spacing": [ 240 | "error", 241 | { 242 | "after": true, 243 | "before": false 244 | } 245 | ], 246 | "semi-style": [ 247 | "error", 248 | "last" 249 | ], 250 | "sort-imports": [2, { 251 | "ignoreCase": false, 252 | "ignoreMemberSort": false, 253 | "memberSyntaxSortOrder": ["none", "all", "multiple", "single"] 254 | }], 255 | "sort-keys": "off", 256 | "sort-vars": "off", 257 | "space-before-blocks": "off", 258 | "space-before-function-paren": "off", 259 | "space-in-parens": [ 260 | "error", 261 | "never" 262 | ], 263 | "space-infix-ops": "off", 264 | "space-unary-ops": "error", 265 | "spaced-comment": "off", 266 | "strict": "error", 267 | "switch-colon-spacing": "error", 268 | "symbol-description": "error", 269 | "template-curly-spacing": [ 270 | "error", 271 | "never" 272 | ], 273 | "template-tag-spacing": "error", 274 | "unicode-bom": [ 275 | "error", 276 | "never" 277 | ], 278 | "valid-jsdoc": "off", 279 | "vars-on-top": "off", 280 | "wrap-iife": "error", 281 | "wrap-regex": "error", 282 | "yield-star-spacing": "error", 283 | "yoda": [ 284 | "error", 285 | "never" 286 | ] 287 | } 288 | } -------------------------------------------------------------------------------- /gas-relayer/abi/ERC20Token.json: -------------------------------------------------------------------------------- 1 | { 2 | "contract_name": "ERC20Token", 3 | "code": "", 4 | "runtime_bytecode": "", 5 | "real_runtime_bytecode": "", 6 | "swarm_hash": "", 7 | "gas_estimates": null, 8 | "function_hashes": { 9 | "allowance(address,address)": "dd62ed3e", 10 | "approve(address,uint256)": "095ea7b3", 11 | "balanceOf(address)": "70a08231", 12 | "decimals()": "313ce567", 13 | "totalSupply()": "18160ddd", 14 | "transfer(address,uint256)": "a9059cbb", 15 | "transferFrom(address,address,uint256)": "23b872dd" 16 | }, 17 | "abi": [ 18 | { 19 | "constant": false, 20 | "inputs": [ 21 | { 22 | "name": "_spender", 23 | "type": "address" 24 | }, 25 | { 26 | "name": "_value", 27 | "type": "uint256" 28 | } 29 | ], 30 | "name": "approve", 31 | "outputs": [ 32 | { 33 | "name": "success", 34 | "type": "bool" 35 | } 36 | ], 37 | "payable": false, 38 | "stateMutability": "nonpayable", 39 | "type": "function" 40 | }, 41 | { 42 | "constant": true, 43 | "inputs": [], 44 | "name": "totalSupply", 45 | "outputs": [ 46 | { 47 | "name": "", 48 | "type": "uint256" 49 | } 50 | ], 51 | "payable": false, 52 | "stateMutability": "view", 53 | "type": "function" 54 | }, 55 | { 56 | "constant": false, 57 | "inputs": [ 58 | { 59 | "name": "_from", 60 | "type": "address" 61 | }, 62 | { 63 | "name": "_to", 64 | "type": "address" 65 | }, 66 | { 67 | "name": "_value", 68 | "type": "uint256" 69 | } 70 | ], 71 | "name": "transferFrom", 72 | "outputs": [ 73 | { 74 | "name": "success", 75 | "type": "bool" 76 | } 77 | ], 78 | "payable": false, 79 | "stateMutability": "nonpayable", 80 | "type": "function" 81 | }, 82 | { 83 | "constant": true, 84 | "inputs": [ 85 | { 86 | "name": "_owner", 87 | "type": "address" 88 | } 89 | ], 90 | "name": "balanceOf", 91 | "outputs": [ 92 | { 93 | "name": "balance", 94 | "type": "uint256" 95 | } 96 | ], 97 | "payable": false, 98 | "stateMutability": "view", 99 | "type": "function" 100 | }, 101 | { 102 | "constant": true, 103 | "inputs": [], 104 | "name": "decimals", 105 | "outputs": [ 106 | { 107 | "name": "", 108 | "type": "uint8" 109 | } 110 | ], 111 | "payable": false, 112 | "stateMutability": "view", 113 | "type": "function" 114 | }, 115 | { 116 | "constant": false, 117 | "inputs": [ 118 | { 119 | "name": "_to", 120 | "type": "address" 121 | }, 122 | { 123 | "name": "_value", 124 | "type": "uint256" 125 | } 126 | ], 127 | "name": "transfer", 128 | "outputs": [ 129 | { 130 | "name": "success", 131 | "type": "bool" 132 | } 133 | ], 134 | "payable": false, 135 | "stateMutability": "nonpayable", 136 | "type": "function" 137 | }, 138 | { 139 | "constant": true, 140 | "inputs": [ 141 | { 142 | "name": "_owner", 143 | "type": "address" 144 | }, 145 | { 146 | "name": "_spender", 147 | "type": "address" 148 | } 149 | ], 150 | "name": "allowance", 151 | "outputs": [ 152 | { 153 | "name": "remaining", 154 | "type": "uint256" 155 | } 156 | ], 157 | "payable": false, 158 | "stateMutability": "view", 159 | "type": "function" 160 | }, 161 | { 162 | "anonymous": false, 163 | "inputs": [ 164 | { 165 | "indexed": true, 166 | "name": "_from", 167 | "type": "address" 168 | }, 169 | { 170 | "indexed": true, 171 | "name": "_to", 172 | "type": "address" 173 | }, 174 | { 175 | "indexed": false, 176 | "name": "_value", 177 | "type": "uint256" 178 | } 179 | ], 180 | "name": "Transfer", 181 | "type": "event" 182 | }, 183 | { 184 | "anonymous": false, 185 | "inputs": [ 186 | { 187 | "indexed": true, 188 | "name": "_owner", 189 | "type": "address" 190 | }, 191 | { 192 | "indexed": true, 193 | "name": "_spender", 194 | "type": "address" 195 | }, 196 | { 197 | "indexed": false, 198 | "name": "_value", 199 | "type": "uint256" 200 | } 201 | ], 202 | "name": "Approval", 203 | "type": "event" 204 | } 205 | ] 206 | } 207 | -------------------------------------------------------------------------------- /gas-relayer/config/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "node": { 3 | "local":{ 4 | "protocol": "ws", 5 | "host": "localhost", 6 | "port": 8546 7 | }, 8 | "ganache": { 9 | "protocol": "http", 10 | "host": "localhost", 11 | "port": 8545 12 | }, 13 | "blockchain": { 14 | // DO NOT USE THIS ACCOUNT ON MAINNET - IT IS ONLY FOR DEV PURPOSES 15 | // For dev chain, address: 0x5b9b5db9cde96fda2e2c88e83f1b833f189e01f4 has this privKey 16 | privateKey: "b2ab40d549e67ba67f278781fec03b3a90515ad4d0c898a6326dd958de1e46fa" // 17 | 18 | 19 | // privateKey: "your_private_key", 20 | 21 | 22 | // privateKeyFile: "path/to/file" 23 | 24 | // mnemonic: "12 word mnemonic", 25 | // addressIndex: "0", // Optionnal. The index to start getting the address 26 | // hdpath: "m/44'/60'/0'/0/" // Optionnal. HD derivation path 27 | }, 28 | "whisper": { 29 | "symKey": "0xd0d905c1c62b810b787141430417caf2b3f54cffadb395b7bb39fdeb8f17266b", 30 | "ttl": 1000, 31 | "minPow": 0.2, 32 | "powTime": 1000 33 | } 34 | }, 35 | 36 | "tokens": { 37 | "0x0000000000000000000000000000000000000000": { 38 | "name": "Ethereum", 39 | "symbol": "ETH", 40 | "minAcceptedRate": 1, 41 | "refreshPricePeriod": 60000 42 | }, 43 | "%STTAddress%": { 44 | "name": "Status Test Token", 45 | "symbol": "SNT", 46 | "minAcceptedRate": 150000000000000, 47 | "refreshPricePeriod": 60000, 48 | "pricePlugin": "../plugins/token-utils.js" 49 | } 50 | }, 51 | 52 | "contracts":{ 53 | "IdentityGasRelay": { 54 | "abiFile": "../abi/IdentityGasRelay.json", 55 | "isIdentity": true, 56 | "factoryAddress": "%IdentityFactoryAddress%", 57 | "kernelVerification": "isKernel(bytes32)", 58 | "allowedFunctions": [ 59 | { 60 | "function": "approveAndCallGasRelayed(address,address,uint256,bytes,uint256,uint256,uint256,address,bytes)", 61 | "isToken": true 62 | }, 63 | { 64 | "function": "callGasRelayed(address,uint256,bytes,uint256,uint256,uint256,address,bytes)", 65 | "isToken": false 66 | } 67 | ], 68 | "strategy": "../src/strategy/IdentityStrategy.js" 69 | }, 70 | "SNTController": { 71 | "abiFile": "../abi/SNTController.json", 72 | "isIdentity": false, 73 | "address": "%SNTController%", 74 | "allowedFunctions": [ 75 | { 76 | "function":"transferSNT(address,uint256,uint256,uint256,bytes)" 77 | }, 78 | { 79 | "function":"executeGasRelayed(address,bytes,uint256,uint256,uint256,bytes)" 80 | } 81 | ], 82 | "strategy": "../src/strategy/SNTStrategy.js" 83 | } 84 | }, 85 | "gasPrice": { 86 | "modifier": 50000, // Added/removed to current network gas price 87 | "maxPrice": 20000000000 // 20 gwei 88 | } 89 | }; 90 | -------------------------------------------------------------------------------- /gas-relayer/config/config.testnet.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "node": { 3 | "local":{ 4 | "protocol": "ws", 5 | "host": "localhost", 6 | "port": 8546 7 | }, 8 | "ganache": { 9 | "protocol": "http", 10 | "host": "localhost", 11 | "port": 8545 12 | }, 13 | "blockchain": { 14 | privateKey: "0x......" // 15 | }, 16 | "whisper": { 17 | "symKey": "0xd0d905c1c62b810b787141430417caf2b3f54cffadb395b7bb39fdeb8f17266b", 18 | "ttl": 10, 19 | "minPow": 0.002, 20 | "powTime": 1 21 | } 22 | }, 23 | 24 | "tokens": { 25 | "0x0000000000000000000000000000000000000000": { 26 | "name": "Ethereum", 27 | "symbol": "ETH", 28 | "minAcceptedRate": 1, 29 | "refreshPricePeriod": 60000 30 | }, 31 | "0x139724523662E54447B70d043b711b2A00c5EF49": { 32 | "name": "Status Test Token", 33 | "symbol": "SNT", 34 | "minAcceptedRate": 150000000000000, 35 | "refreshPricePeriod": 60000, 36 | "pricePlugin": "../plugins/token-utils.js" 37 | } 38 | }, 39 | 40 | "contracts":{ 41 | "IdentityGasRelay": { 42 | "abiFile": "../abi/IdentityGasRelay.json", 43 | "isIdentity": true, 44 | "factoryAddress": "0xCf3473C2A50F7A94D3D7Dcc2BeBbeE989dAA014E", 45 | "kernelVerification": "isKernel(bytes32)", 46 | "allowedFunctions": [ 47 | { 48 | "function": "approveAndCallGasRelayed(address,address,uint256,bytes,uint256,uint256,uint256,address,bytes)", 49 | "isToken": true 50 | }, 51 | { 52 | "function": "callGasRelayed(address,uint256,bytes,uint256,uint256,uint256,address,bytes)", 53 | "isToken": false 54 | } 55 | ], 56 | "strategy": "../src/strategy/IdentityStrategy.js" 57 | }, 58 | "SNTController": { 59 | "abiFile": "../abi/SNTController.json", 60 | "isIdentity": false, 61 | "address": "0x1f42B87b375b8ac6C77A8CAd8E78319c18695E75", 62 | "allowedFunctions": [ 63 | { 64 | "function":"transferSNT(address,uint256,uint256,uint256,bytes)" 65 | }, 66 | { 67 | "function":"executeGasRelayed(address,bytes,uint256,uint256,uint256,bytes)" 68 | } 69 | ], 70 | "strategy": "../src/strategy/SNTStrategy.js" 71 | } 72 | }, 73 | "gasPrice": { 74 | "modifier": 50000, // Added/removed to current network gas price 75 | "maxPrice": 20000000000 // 20 gwei 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /gas-relayer/launch-geth-testnet.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #!/bin/bash 3 | geth --testnet --syncmode=light --port=30303 --ws --wsport=8546 --wsaddr=localhost --wsorigins=http://localhost:8000,embark,gas-relayer --maxpeers=25 --shh --shh.pow=0.002 --wsapi=eth,web3,net,shh 4 | 5 | -------------------------------------------------------------------------------- /gas-relayer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gas-relayer", 3 | "version": "0.0.1", 4 | "description": "Gas relayer to avoid having to hold ether to perform transactions when you already have a token", 5 | "main": "src/service.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node src/service.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "babel-eslint": "^8.2.6", 14 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 15 | "babel-preset-stage-2": "^6.24.1", 16 | "eslint": "^4.19.1", 17 | "eslint-config-standard": "^11.0.0", 18 | "eslint-plugin-import": "^2.13.0", 19 | "eslint-plugin-node": "^7.0.1", 20 | "eslint-plugin-promise": "^3.8.0", 21 | "eslint-plugin-standard": "^3.1.0" 22 | }, 23 | "dependencies": { 24 | "axios": "^0.18.0", 25 | "bip39": "^2.5.0", 26 | "consola": "^1.4.3", 27 | "daemonize2": "^0.4.2", 28 | "ethereumjs-wallet": "^0.6.2", 29 | "ganache-cli": "^6.1.0", 30 | "jsum": "^0.1.4", 31 | "memory-cache": "^0.2.0", 32 | "typedarray-to-buffer": "^3.1.5", 33 | "web3": "^1.0.0-beta.33", 34 | "winston": "^3.1.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /gas-relayer/plugins/token-utils.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | class TokenUtils { 4 | constructor(tokenConfig, gasPrice, web3){ 5 | this.gasPrice = gasPrice; 6 | this.name = tokenConfig.name || ""; 7 | this.symbol = tokenConfig.symbol || ""; 8 | this.minRelayFactor = tokenConfig.minRelayFactor || 1; 9 | this.web3 = web3; 10 | } 11 | 12 | async getRate(){ 13 | // Using cryptocompare API 14 | const {toBN, toWei} = this.web3.utils; 15 | 16 | const doc = await axios.get('https://min-api.cryptocompare.com/data/price?fsym=' + this.symbol + '&tsyms=ETH'); 17 | return toBN(toWei(doc.data.ETH.toString(), "ether")); 18 | } 19 | } 20 | 21 | 22 | module.exports = TokenUtils; 23 | -------------------------------------------------------------------------------- /gas-relayer/src/account-parser.js: -------------------------------------------------------------------------------- 1 | const bip39 = require("bip39"); 2 | const hdkey = require('ethereumjs-wallet/hdkey'); 3 | const ethereumjsWallet = require('ethereumjs-wallet'); 4 | const path = require('path'); 5 | const fs = require('fs'); 6 | 7 | 8 | class AccountParser { 9 | static get(accountConfig, web3) { 10 | if (accountConfig.privateKey) { 11 | if (!accountConfig.privateKey.startsWith('0x')) { 12 | accountConfig.privateKey = '0x' + accountConfig.privateKey; 13 | } 14 | if (!web3.utils.isHexStrict(accountConfig.privateKey)) { 15 | console.error(`Private key ending with ${accountConfig.privateKey.substr(accountConfig.privateKey.length - 5)} is not a HEX string`); 16 | return null; 17 | } 18 | return web3.eth.accounts.privateKeyToAccount(accountConfig.privateKey); 19 | } 20 | 21 | 22 | if (accountConfig.privateKeyFile) { 23 | let privateKeyFile = path.resolve(accountConfig.privateKeyFile); 24 | let fileContent = fs.readFileSync(privateKeyFile).toString(); 25 | if (accountConfig.password) { 26 | try { 27 | fileContent = JSON.parse(fileContent); 28 | if (!ethereumjsWallet['fromV' + fileContent.version]) { 29 | console.error(`Key file ${accountConfig.privateKeyFile} is not a valid keystore file`); 30 | return null; 31 | } 32 | const wallet = ethereumjsWallet['fromV' + fileContent.version](fileContent, accountConfig.password); 33 | return web3.eth.accounts.privateKeyToAccount('0x' + wallet.getPrivateKey().toString('hex')); 34 | } catch (e) { 35 | console.error('Private key file is not a keystore JSON file but a password was provided'); 36 | console.error(e.message || e); 37 | return null; 38 | } 39 | } 40 | 41 | fileContent = fileContent.trim().split(/[,;]/); 42 | return fileContent.map((key, index) => { 43 | if (!key.startsWith('0x')) { 44 | key = '0x' + key; 45 | } 46 | if (!web3.utils.isHexStrict(key)) { 47 | console.error(`Private key is not a HEX string in file ${accountConfig.privateKeyFile} at index ${index}`); 48 | return null; 49 | } 50 | return web3.eth.accounts.privateKeyToAccount(key); 51 | }); 52 | } 53 | 54 | if (accountConfig.mnemonic) { 55 | const hdwallet = hdkey.fromMasterSeed(bip39.mnemonicToSeed(accountConfig.mnemonic.trim())); 56 | const addressIndex = accountConfig.addressIndex || 0; 57 | const wallet_hdpath = accountConfig.hdpath || "m/44'/60'/0'/0/"; 58 | const wallet = hdwallet.derivePath(wallet_hdpath + addressIndex).getWallet(); 59 | return web3.eth.accounts.privateKeyToAccount('0x' + wallet.getPrivateKey().toString('hex')); 60 | } 61 | 62 | console.error('Unsupported account configuration: ' + JSON.stringify(accountConfig)); 63 | console.error('Try using one of those: ' + 64 | '{ "privateKey": "your-private-key", "privateKeyFile": "path/to/file/containing/key", "mnemonic": "12 word mnemonic" }'); 65 | 66 | return null; 67 | } 68 | } 69 | 70 | module.exports = AccountParser; 71 | -------------------------------------------------------------------------------- /gas-relayer/src/contract-settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration Settings related to contracts 3 | */ 4 | class ContractSettings { 5 | 6 | /** 7 | * @param {object} config - Configuration object obtained from `./config/config.js` 8 | * @param {object} web3 - Web3 object already configured 9 | * @param {object} eventEmitter - Event Emitter 10 | */ 11 | constructor(config, web3, eventEmitter, logger){ 12 | this.tokens = config.tokens; 13 | this.topics = []; 14 | this.contracts = config.contracts; 15 | this.config = config; 16 | this.logger = logger; 17 | 18 | this.web3 = web3; 19 | this.events = eventEmitter; 20 | 21 | this.pendingToLoad = 0; 22 | } 23 | 24 | /** 25 | * Process configuration file 26 | */ 27 | process(){ 28 | this._setTokenPricePlugin(); 29 | this._processContracts(); 30 | } 31 | 32 | /** 33 | * Set price plugin for token 34 | */ 35 | _setTokenPricePlugin(){ 36 | for(let token in this.tokens){ 37 | if(this.tokens[token].pricePlugin !== undefined){ 38 | let PricePlugin = require(this.tokens[token].pricePlugin); 39 | this.tokens[token].pricePlugin = new PricePlugin(this.tokens[token], this.config.gasPrice, this.web3); 40 | } 41 | } 42 | } 43 | 44 | /** 45 | * Get allowed tokens 46 | * @return {object} - Dictionary with allowed tokens (address as key) 47 | */ 48 | getTokens(){ 49 | return this.tokens; 50 | } 51 | 52 | /** 53 | * Get token by address 54 | * @param {string} - Token address 55 | * @return {object} - Token details 56 | */ 57 | getToken(token){ 58 | const tokenObj = this.tokens[token]; 59 | tokenObj.address = token; 60 | return tokenObj; 61 | } 62 | 63 | /** 64 | * Get token by symbol 65 | * @param {string} - Token symbol 66 | * @return {object} - Token details 67 | */ 68 | getTokenBySymbol(symbol){ 69 | for(let token in this.tokens){ 70 | if(this.tokens[token].symbol == symbol){ 71 | const tokenObj = this.tokens[token]; 72 | tokenObj.address = token; 73 | return tokenObj; 74 | } 75 | } 76 | } 77 | 78 | /** 79 | * Get contract by topicName 80 | * @param {string} topicName - Topic name that represents a contract 81 | * @return {object} - Contract details 82 | */ 83 | getContractByTopic(topicName){ 84 | return this.contracts[topicName]; 85 | } 86 | 87 | /** 88 | * Calculate the topic based on the contract's name 89 | * @param {string} contractName - Name of the contract as it appears in the configuration 90 | * @return {string} - Topic 91 | */ 92 | getTopicName(contractName){ 93 | return this.web3.utils.toHex(contractName).slice(0, 10); 94 | } 95 | 96 | /** 97 | * Set contract's bytecode in the configuration 98 | * @param {string} topicName - Topic name that represents a contract 99 | */ 100 | async _obtainContractBytecode(topicName){ 101 | if(this.contracts[topicName].isIdentity) return; 102 | 103 | this.pendingToLoad++; 104 | 105 | try { 106 | const code = await this.web3.eth.getCode(this.contracts[topicName].address); 107 | this.contracts[topicName].code = this.web3.utils.soliditySha3(code); 108 | this.pendingToLoad--; 109 | if(this.pendingToLoad == 0) this.events.emit("setup:complete", this); 110 | } catch(err) { 111 | this.logger.error("Invalid contract for " + topicName); 112 | this.logger.error(err); 113 | process.exit(); 114 | } 115 | } 116 | 117 | /** 118 | * Extract function details based on topicName 119 | * @param {string} topicName - Topic name that represents a contract 120 | */ 121 | _extractFunctions(topicName){ 122 | const contract = this.getContractByTopic(topicName); 123 | 124 | for(let i = 0; i < contract.allowedFunctions.length; i++){ 125 | contract.allowedFunctions[i].functionName = contract.allowedFunctions[i].function.slice(0, contract.allowedFunctions[i].function.indexOf('(')); 126 | 127 | // Extracting input 128 | contract.allowedFunctions[i].inputs = contract.abi.filter(x => x.name == contract.allowedFunctions[i].functionName && x.type == "function")[0].inputs; 129 | 130 | // Obtaining function signatures 131 | let functionSignature = this.web3.utils.sha3(contract.allowedFunctions[i].function).slice(0, 10); 132 | contract.allowedFunctions[functionSignature] = contract.allowedFunctions[i]; 133 | delete this.contracts[topicName].allowedFunctions[i]; 134 | } 135 | 136 | contract.functionSignatures = Object.keys(contract.allowedFunctions); 137 | this.contracts[topicName] = contract; 138 | } 139 | 140 | /** 141 | * Process contracts and setup the settings object 142 | */ 143 | _processContracts(){ 144 | for(let contractName in this.contracts){ 145 | // Obtaining the abis 146 | this.contracts[contractName].abi = require(this.contracts[contractName].abiFile).abi; 147 | 148 | const topicName = this.getTopicName(contractName); 149 | 150 | // Extracting topic 151 | this.topics.push(topicName); 152 | this.contracts[topicName] = this.contracts[contractName]; 153 | this.contracts[topicName].name = contractName; 154 | delete this.contracts[contractName]; 155 | 156 | // Obtaining strategy 157 | if(this.contracts[topicName].strategy){ 158 | this.contracts[topicName].strategy = this.buildStrategy(this.contracts[topicName].strategy, topicName); 159 | } 160 | 161 | this._obtainContractBytecode(topicName); 162 | 163 | this._extractFunctions(topicName); 164 | } 165 | } 166 | 167 | /** 168 | * Create strategy object based on source code and topicName 169 | * @param {string} strategyFile - Souce code path of strategy to build 170 | * @param {string} topicName - Hex string that represents a contract's topic 171 | */ 172 | buildStrategy(strategyFile, topicName){ 173 | const strategy = require(strategyFile); 174 | return new strategy(this.web3, this.config, this, this.contracts[topicName]); 175 | } 176 | } 177 | 178 | 179 | module.exports = ContractSettings; 180 | -------------------------------------------------------------------------------- /gas-relayer/src/message-processor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Message Processor to analyze and execute strategies based on input objects 3 | */ 4 | class MessageProcessor { 5 | 6 | /** 7 | * @param {object} config - Configuration object obtained from `./config/config.js` 8 | * @param {object} settings - Settings obtained from parsing the configuration object 9 | * @param {object} web3 - Web3 object already configured 10 | * @param {object} events - Event emitter 11 | */ 12 | constructor(config, settings, web3, events, logger, cache){ 13 | this.config = config; 14 | this.settings = settings; 15 | this.web3 = web3; 16 | this.events = events; 17 | this.logger = logger; 18 | this.cache = cache; 19 | } 20 | 21 | /** 22 | * Validate input message content 23 | * @param {object} contract - Object obtained from the settings based on the message topic 24 | * @param {object} input - Object obtained from a message. 25 | * @returns {object} State of validation 26 | */ 27 | async _validateInput(contract, input){ 28 | this.logger.info("Processing '" + input.action + "' request to contract: " + input.contract); 29 | 30 | if(contract == undefined){ 31 | return {success: false, message: 'Unknown contract'}; 32 | } 33 | 34 | if(input.functionName && !contract.functionSignatures.includes(input.functionName)){ 35 | return {success: false, message: 'Function not allowed'}; 36 | } 37 | 38 | // Get code from contract and compare it against the contract code 39 | if(!contract.isIdentity){ 40 | const code = this.web3.utils.soliditySha3(await this.web3.eth.getCode(input.contract)); 41 | if(code != contract.code){ 42 | return {success: false, message: 'Invalid contract code'}; 43 | } 44 | } else { 45 | if(!(/^0x[0-9a-f]{40}$/i).test(input.contract)){ 46 | return {success: false, message: 'Invalid contract address'}; 47 | } 48 | } 49 | 50 | if(input.address && !(/^0x[0-9a-f]{40}$/i).test(input.address)){ 51 | return {success: false, message: 'Invalid address'}; 52 | } 53 | 54 | return {success: true}; 55 | } 56 | 57 | /** 58 | * Process strategy and return validation result 59 | * @param {object} contract - Object obtained from the settings based on the message topic 60 | * @param {object} input - Object obtained from a message. 61 | * @param {function} reply - Function to reply a message 62 | * @param {object} strategy - Strategy to apply. If undefined, it will use a strategy based on the contract 63 | * @returns {object} State of validation 64 | */ 65 | async processStrategy(contract, input, reply, strategy){ 66 | const inputValidation = await this._validateInput(contract, input); 67 | if(!inputValidation.success){ 68 | // TODO Log? 69 | return inputValidation; 70 | } 71 | 72 | if(strategy || contract.strategy){ 73 | let validationResult; 74 | if(strategy){ 75 | validationResult = await strategy.execute(input, this.cache); 76 | } else { 77 | validationResult = await contract.strategy.execute(input, this.cache); 78 | } 79 | 80 | if(!validationResult.success){ 81 | reply(validationResult.message); 82 | } 83 | 84 | return validationResult; 85 | } 86 | } 87 | 88 | /** 89 | * Process strategy and based on its result, send a transaction to the blockchain 90 | * @param {object} contract - Object obtained from the settings based on the message topic 91 | * @param {object} input - Object obtained from a message. 92 | * @param {function} reply - function to reply a message 93 | * @returns {undefined} 94 | */ 95 | async processTransaction(contract, input, reply, account, cb){ 96 | const validationResult = await this.processStrategy(contract, input, reply); 97 | 98 | const {toHex} = this.web3.utils; 99 | 100 | if(!validationResult.success) return; 101 | 102 | const {toBN} = this.web3.utils; 103 | 104 | const gasPrice = toBN(await this.web3.eth.getGasPrice()).add(toBN(this.config.gasPrice.modifier)).toString(); 105 | 106 | if(!validationResult.estimatedGas){ 107 | validationResult.estimatedGas = await this.web3.eth.estimateGas(p); 108 | } 109 | 110 | const estimatedGas = parseInt(validationResult.estimatedGas, 10); 111 | 112 | let p = { 113 | from: this.config.node.blockchain.account.address, 114 | to: input.contract, 115 | value: "0x00", 116 | data: input.payload, 117 | gasPrice: parseInt(gasPrice, 10), 118 | gas: estimatedGas + 1000 // Tune this, 119 | }; 120 | 121 | const nodeBalance = await this.web3.eth.getBalance(this.config.node.blockchain.account.address); 122 | 123 | if(nodeBalance < p.gas){ 124 | reply("Relayer unavailable"); 125 | this.logger.error("Relayer doesn't have enough gas to process trx: " + nodeBalance + ", required " + p.gas); 126 | this.events.emit('exit'); 127 | } else { 128 | try { 129 | this.web3.eth.sendTransaction(p) 130 | .on('transactionHash', function(hash){ 131 | reply("Transaction broadcasted: " + hash); 132 | cb(); 133 | }) 134 | .on('receipt', function(receipt){ 135 | reply("Transaction mined", receipt); 136 | }); 137 | 138 | } catch(err){ 139 | reply("Couldn't mine transaction: " + err.message); 140 | // TODO log this? 141 | this.logger.error(err); 142 | } 143 | } 144 | } 145 | } 146 | 147 | module.exports = MessageProcessor; 148 | -------------------------------------------------------------------------------- /gas-relayer/src/service.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | const Web3 = require('web3'); 3 | const config = require('../config/config.js'); 4 | const ContractSettings = require('./contract-settings'); 5 | const MessageProcessor = require('./message-processor'); 6 | const JSum = require('jsum'); 7 | const logger = require('consola'); 8 | const winston = require('winston'); 9 | const cache = require('memory-cache'); 10 | const accountParser = require('./account-parser'); 11 | 12 | var pubKey; 13 | 14 | // Setting up logging 15 | const wLogger = winston.createLogger({ 16 | level: 'info', 17 | format: winston.format.simple(), 18 | transports: [ 19 | new winston.transports.Console(), 20 | new winston.transports.File({filename: 'gas-relayer.log'}) 21 | ] 22 | }); 23 | logger.clear().add(new logger.WinstonReporter(wLogger)); 24 | 25 | 26 | // Service Init 27 | logger.info("Starting..."); 28 | const events = new EventEmitter(); 29 | 30 | // Web3 Connection 31 | const connectionURL = `${config.node.local.protocol}://${config.node.local.host}:${config.node.local.port}`; 32 | const wsProvider = new Web3.providers.WebsocketProvider(connectionURL, {headers: {Origin: "gas-relayer"}}); 33 | const web3 = new Web3(wsProvider); 34 | let account; 35 | 36 | 37 | web3.eth.net.isListening() 38 | .then(() => events.emit('web3:connected', connectionURL)) 39 | .catch(error => { 40 | logger.error(error); 41 | process.exit(); 42 | }); 43 | 44 | 45 | events.on('web3:connected', connURL => { 46 | logger.info("Connected to '" + connURL + "'"); 47 | 48 | 49 | account = accountParser.get(config.node.blockchain, web3); 50 | web3.eth.accounts.wallet.add(account); 51 | 52 | 53 | if(!account) { 54 | process.exit(1); 55 | } else { 56 | config.node.blockchain.account = account; 57 | } 58 | 59 | let settings = new ContractSettings(config, web3, events, logger); 60 | settings.process(); 61 | }); 62 | 63 | // Setting up Whisper options 64 | const shhOptions = { 65 | ttl: config.node.whisper.ttl, 66 | minPow: config.node.whisper.minPow 67 | }; 68 | 69 | const verifyBalance = async (exitSubs) => { 70 | const nodeBalance = await web3.eth.getBalance(config.node.blockchain.account.address); 71 | if(web3.utils.toBN(nodeBalance).lte(web3.utils.toBN(100000))){ // TODO: tune minimum amount required for transactions 72 | logger.info("Not enough balance available for processing transactions"); 73 | logger.info("> Account: " + config.node.blockchain.account.address); 74 | logger.info("> Balance: " + nodeBalance); 75 | 76 | if(exitSubs){ 77 | web3.shh.clearSubscriptions(); 78 | } 79 | 80 | process.exit(0); 81 | } 82 | }; 83 | 84 | events.on('exit', () => { 85 | web3.shh.clearSubscriptions(); 86 | logger.info("Closing service..."); 87 | process.exit(0); 88 | }); 89 | 90 | events.on('setup:complete', async (settings) => { 91 | // Verifying relayer balance 92 | await verifyBalance(); 93 | 94 | shhOptions.kId = await web3.shh.newKeyPair(); 95 | 96 | const symKeyID = await web3.shh.addSymKey(config.node.whisper.symKey); 97 | pubKey = await web3.shh.getPublicKey(shhOptions.kId); 98 | 99 | // Listening to whisper 100 | // Individual subscriptions due to https://github.com/ethereum/web3.js/issues/1361 101 | // once this is fixed, we'll be able to use an array of topics and a single subs for symkey and a single subs for privKey 102 | logger.info(`Sym Key: ${config.node.whisper.symKey}`); 103 | logger.info(`Relayer Public Key: ${pubKey}`); 104 | logger.info("Topics Available:"); 105 | for(let contract in settings.contracts) { 106 | logger.info("- " + settings.getContractByTopic(contract).name + ": " + contract + " [" + (Object.keys(settings.getContractByTopic(contract).allowedFunctions).join(', ')) + "]"); 107 | shhOptions.topics = [contract]; 108 | 109 | // Listen to public channel - Used for reporting availability 110 | events.emit('server:listen', Object.assign({symKeyID}, shhOptions), settings); 111 | 112 | // Listen to private channel - Individual transactions 113 | events.emit('server:listen', Object.assign({privateKeyID: shhOptions.kId}, shhOptions), settings); 114 | } 115 | }); 116 | 117 | const replyFunction = (message) => (text, receipt) => { 118 | if(message.sig !== undefined){ 119 | 120 | let payloadContent; 121 | if(typeof text === 'object'){ 122 | payloadContent = {...text, receipt}; 123 | } else { 124 | payloadContent = {text, receipt}; 125 | } 126 | 127 | web3.shh.post({ 128 | pubKey: message.sig, 129 | sig: shhOptions.kId, 130 | ttl: config.node.whisper.ttl, 131 | powTarget:config.node.whisper.minPow, 132 | powTime: config.node.whisper.powTime, 133 | topic: message.topic, 134 | payload: web3.utils.fromAscii(JSON.stringify(payloadContent, null, " ")) 135 | }).catch(console.error); 136 | } 137 | }; 138 | 139 | const extractInput = (message) => { 140 | let obj = { 141 | contract: null, 142 | address: null, 143 | action: null 144 | }; 145 | 146 | try { 147 | const msg = web3.utils.toAscii(message.payload); 148 | let parsedObj = JSON.parse(msg); 149 | obj.contract = parsedObj.contract; 150 | obj.address = parsedObj.address; 151 | obj.action = parsedObj.action; 152 | if(obj.action == 'transaction'){ 153 | obj.functionName = parsedObj.encodedFunctionCall.slice(0, 10); 154 | obj.functionParameters = "0x" + parsedObj.encodedFunctionCall.slice(10); 155 | obj.payload = parsedObj.encodedFunctionCall; 156 | } else if(obj.action == 'availability') { 157 | obj.gasToken = parsedObj.gasToken; 158 | obj.gasPrice = parsedObj.gasPrice; 159 | } 160 | } catch(err){ 161 | logger.error("Couldn't parse " + message); 162 | } 163 | 164 | return obj; 165 | }; 166 | 167 | events.on('server:listen', (shhOptions, settings) => { 168 | let processor = new MessageProcessor(config, settings, web3, events, logger, cache); 169 | web3.shh.subscribe('messages', shhOptions, async (error, message) => { 170 | if(error){ 171 | logger.error(error); 172 | return; 173 | } 174 | 175 | verifyBalance(true); 176 | 177 | const input = extractInput(message); 178 | const inputCheckSum = JSum.digest({input}, 'SHA256', 'hex'); 179 | 180 | const reply = replyFunction(message, inputCheckSum); 181 | 182 | if(cache.get(inputCheckSum) && input.action != 'availability'){ 183 | reply("Duplicated message received"); 184 | } else { 185 | let validationResult; 186 | switch(input.action){ 187 | case 'transaction': 188 | if(message.recipientPublicKey === pubKey){ 189 | processor.processTransaction(settings.getContractByTopic(message.topic), 190 | input, 191 | reply, 192 | account, 193 | () => { 194 | cache.put(inputCheckSum, (new Date().getTime()), 3600000); 195 | } 196 | ); 197 | } 198 | break; 199 | case 'availability': 200 | validationResult = await processor.processStrategy(settings.getContractByTopic(message.topic), 201 | input, 202 | reply, 203 | settings.buildStrategy("./strategy/AvailabilityStrategy", message.topic) 204 | ); 205 | if(validationResult.success && validationResult.message) { 206 | reply(validationResult.message); 207 | } 208 | break; 209 | default: 210 | reply("unknown-action"); 211 | } 212 | } 213 | }); 214 | }); 215 | 216 | 217 | // Daemon helper functions 218 | 219 | process.on("uncaughtException", function(err) { 220 | // TODO 221 | logger.error(err); 222 | }); 223 | 224 | process.once("SIGTERM", function() { 225 | logger.info("Stopping..."); 226 | }); 227 | -------------------------------------------------------------------------------- /gas-relayer/src/strategy/AvailabilityStrategy.js: -------------------------------------------------------------------------------- 1 | const Strategy = require('./BaseStrategy'); 2 | 3 | /** 4 | * Class representing a strategy to validate an 'availability' request. 5 | * @extends Strategy 6 | */ 7 | class AvailabilityStrategy extends Strategy { 8 | 9 | /** 10 | * Process availability strategy 11 | * @param {object} input - Object obtained from an 'availability' request. It expects an object with this structure `{contract, address, action, gasToken, gasPrice}` 12 | * @returns {object} Status of validation, and minimum price 13 | */ 14 | async execute(input, cache){ 15 | 16 | if(this.contract.isIdentity){ 17 | let validInstance = await this._validateInstance(input); 18 | if(!validInstance){ 19 | return {success: false, message: "Invalid identity instance"}; 20 | } 21 | } 22 | 23 | // Verifying if token is allowed 24 | const token = this.settings.getToken(input.gasToken); 25 | if(token == undefined) return {success: false, message: "Token not allowed"}; 26 | 27 | let tokenRate = await this.getTokenRate(token, cache); 28 | if(!tokenRate){ 29 | return { 30 | success: false, 31 | message: "Token price unavailable" 32 | }; 33 | } 34 | 35 | const {toBN} = this.web3.utils; 36 | 37 | const gasPrices = await this.getGasPrices(token, tokenRate); 38 | if(tokenRate.gte(token.minAcceptedRate) && gasPrices.inEther.lte(toBN(this.config.gasPrice.maxPrice))){ 39 | return { 40 | success: true, 41 | message: { 42 | message: "Available", 43 | address: this.config.node.blockchain.account.address, 44 | minGasPrice: gasPrices.inTokens.toString(), 45 | gasPriceETH: gasPrices.inEther.add(toBN(this.config.gasPrice.modifier)).toString() 46 | } 47 | }; 48 | } 49 | 50 | return {success: true}; 51 | } 52 | 53 | } 54 | 55 | module.exports = AvailabilityStrategy; 56 | -------------------------------------------------------------------------------- /gas-relayer/src/strategy/BaseStrategy.js: -------------------------------------------------------------------------------- 1 | const ganache = require("ganache-cli"); 2 | const Web3 = require('web3'); 3 | const erc20ABI = require('../../abi/ERC20Token.json'); 4 | 5 | /** 6 | * Abstract class used for validation strategies 7 | */ 8 | class BaseStrategy { 9 | 10 | /** 11 | * Validates if the contract being invoked represents an instance created via factory 12 | * @param {object} input - Object obtained from a `transaction` request. 13 | * @returns {bool} Valid instance or not 14 | */ 15 | async _validateInstance(input){ 16 | const instanceCodeHash = this.web3.utils.soliditySha3(await this.web3.eth.getCode(input.contract)); 17 | const kernelVerifSignature = this.web3.utils.soliditySha3(this.contract.kernelVerification).slice(0, 10); 18 | 19 | if(instanceCodeHash === null) return false; 20 | 21 | let verificationResult = await this.web3.eth.call({ 22 | to: this.contract.factoryAddress, 23 | data: kernelVerifSignature + instanceCodeHash.slice(2)}); 24 | 25 | return this.web3.eth.abi.decodeParameter('bool', verificationResult); 26 | } 27 | 28 | 29 | /** 30 | * @param {object} web3 - Web3 object already configured 31 | * @param {object} config - Configuration object obtained from `./config/config.js` 32 | * @param {object} settings - Settings obtained from parsing the configuration object 33 | * @param {object} contract - Object obtained from the settings based on the message topic 34 | */ 35 | constructor(web3, config, settings, contract){ 36 | this.web3 = web3; 37 | this.settings = settings; 38 | this.contract = contract; 39 | this.config = config; 40 | } 41 | 42 | /** 43 | * Obtain the balance in tokens or ETH from an address 44 | * @param {string} address - ETH address to obtain the balance from 45 | * @param {object} token - Obtained from `settings.getToken(tokenSymbol)` 46 | * @returns {web3.utils.BN} Balance 47 | */ 48 | async getBalance(address, token){ 49 | // Determining balances of token used 50 | if(token.symbol == "ETH"){ 51 | return new this.web3.utils.BN(await this.web3.eth.getBalance(address)); 52 | } else { 53 | const Token = new this.web3.eth.Contract(erc20ABI.abi); 54 | Token.options.address = token.address; 55 | return new this.web3.utils.BN(await Token.methods.balanceOf(address).call()); 56 | } 57 | } 58 | 59 | async getTokenRate(token, cache){ 60 | // Get Price 61 | let tokenRate = cache.get(token.address); 62 | if(tokenRate === null){ 63 | try { 64 | tokenRate = await token.pricePlugin.getRate(); 65 | cache.put(token.address, tokenRate, token.refreshPricePeriod); 66 | return tokenRate; 67 | } catch (err) { 68 | console.error(err); 69 | } 70 | } else { 71 | return tokenRate; 72 | } 73 | } 74 | 75 | async getGasPrices(token, tokenRate){ 76 | const {toBN} = this.web3.utils; 77 | const Token = new this.web3.eth.Contract(erc20ABI.abi); 78 | Token.options.address = token.address; 79 | const tokenDecimals = await Token.methods.decimals().call(); 80 | const multiplier = toBN(Math.pow(10, tokenDecimals)); 81 | const currentGasPrice = toBN(await this.web3.eth.getGasPrice()); 82 | const currentGasConvertedToTokens = currentGasPrice.mul(multiplier).div(tokenRate); 83 | 84 | return {inEther: currentGasPrice, inTokens: currentGasConvertedToTokens}; 85 | } 86 | 87 | /** 88 | * Build Parameters Function 89 | * @param {object} input - Object obtained from an `transaction` request. 90 | * @returns {function} Function that simplifies accessing contract functions' parameters 91 | */ 92 | _obtainParametersFunc(input){ 93 | const parameterList = this.web3.eth.abi.decodeParameters(this.contract.allowedFunctions[input.functionName].inputs, input.functionParameters); 94 | return function(parameterName){ 95 | return parameterList[parameterName]; 96 | }; 97 | } 98 | 99 | /** 100 | * Estimate gas using web3 101 | * @param {object} input - Object obtained from an `transaction` request. 102 | * @returns {web3.utils.toBN} Estimated gas fees 103 | */ 104 | async _estimateGas(input){ 105 | let p = { 106 | from: this.config.node.blockchain.account.address, 107 | to: input.contract, 108 | data: input.payload 109 | }; 110 | const estimatedGas = await this.web3.eth.estimateGas(p); 111 | return this.web3.utils.toBN(estimatedGas); 112 | } 113 | 114 | /** 115 | * Simulate transaction using ganache. Useful for obtaining events 116 | * @param {object} input - Object obtained from an `transaction` request. 117 | * @returns {object} Simulated transaction receipt 118 | */ 119 | async _simulateTransaction(input){ 120 | let web3Sim = new Web3(ganache.provider({ 121 | fork: `${this.config.node.ganache.protocol}://${this.config.node.ganache.host}:${this.config.node.ganache.port}`, 122 | locked: false, 123 | gasLimit: 10000000 124 | })); 125 | 126 | let simAccounts = await web3Sim.eth.getAccounts(); 127 | 128 | let simulatedReceipt = await web3Sim.eth.sendTransaction({ 129 | from: simAccounts[0], 130 | to: input.contract, 131 | value: 0, 132 | data: input.payload, 133 | gasLimit: 9500000 // 95% of current chain latest gas block limit 134 | 135 | }); 136 | return simulatedReceipt; 137 | } 138 | 139 | /* 140 | async execute(input){ 141 | return { 142 | success: true, 143 | message: "Valid transaction" 144 | }; 145 | } 146 | */ 147 | } 148 | 149 | module.exports = BaseStrategy; 150 | -------------------------------------------------------------------------------- /gas-relayer/src/strategy/IdentityStrategy.js: -------------------------------------------------------------------------------- 1 | const Strategy = require('./BaseStrategy'); 2 | const erc20ABI = require('../../abi/ERC20Token.json'); 3 | 4 | const CallGasRelayed = "0xfd0dded5"; 5 | const ApproveAndCallGasRelayed = "0x59f4ac61"; 6 | 7 | /** 8 | * Class representing a strategy to validate a `transaction` request when the topic is related to Identities. 9 | * @extends Strategy 10 | */ 11 | class IdentityStrategy extends Strategy { 12 | 13 | 14 | /** 15 | * Process Identity strategy 16 | * @param {object} input - Object obtained from an 'transaction' request. It expects an object with this structure `{contract, address, action, functionName, functionParameters, payload}` 17 | * @returns {object} Status of validation and estimated gas 18 | */ 19 | async execute(input, cache){ 20 | if(this.contract.isIdentity){ 21 | let validInstance = await this._validateInstance(input); 22 | if(!validInstance){ 23 | return {success: false, message: "Invalid identity instance"}; 24 | } 25 | } 26 | 27 | const params = this._obtainParametersFunc(input); 28 | 29 | // Verifying if token is allowed 30 | const token = this.settings.getToken(params('_gasToken')); 31 | if(token == undefined) return {success: false, message: "Token not allowed"}; 32 | 33 | // Determine if enough balance for baseToken 34 | const gasPrice = this.web3.utils.toBN(params('_gasPrice')); 35 | const gasMinimal = this.web3.utils.toBN(params('_gasMinimal')); 36 | if(this.contract.allowedFunctions[input.functionName].isToken){ 37 | const Token = new this.web3.eth.Contract(erc20ABI.abi); 38 | Token.options.address = params('_baseToken'); 39 | const tokenBalance = new this.web3.utils.BN(await Token.methods.balanceOf(input.contract).call()); 40 | if(tokenBalance.lt(this.web3.utils.toBN(params('_value')))){ 41 | return {success: false, message: "Identity has not enough balance for specified value"}; 42 | } 43 | } 44 | 45 | // gasPrice * limit calculation 46 | const balance = await this.getBalance(input.contract, token); 47 | if(balance.lt(this.web3.utils.toBN(gasPrice.mul(gasMinimal)))) { 48 | return {success: false, message: "Identity has not enough tokens for gasPrice*gasMinimal"}; 49 | } 50 | 51 | 52 | let estimatedGas = 0; 53 | try { 54 | if(input.functionName == CallGasRelayed){ 55 | estimatedGas = await this._estimateGas(input); 56 | } else { 57 | const tmp = Math.floor(parseInt((await this._estimateGas(input)).toString(10), 10) * 1.05); 58 | estimatedGas = this.web3.utils.toBN(tmp); 59 | } 60 | 61 | // TODO: executing functions with gas minimal causes relayers to incur in a loss. 62 | // TODO: maybe this can be fixed by increasing the gas price for this kind of operations 63 | if(gasMinimal.add(this.web3.utils.toBN(75000)).lt(estimatedGas)) { 64 | return {success: false, message: "Gas limit below estimated gas (" + estimatedGas + ")"}; 65 | } else { 66 | estimatedGas = estimatedGas.add(this.web3.utils.toBN(75000)); 67 | } 68 | } catch(exc){ 69 | if(exc.message.indexOf("revert") > -1) return {success: false, message: "Transaction will revert"}; 70 | else { 71 | console.error(exc); 72 | return {success: false, message: "Transaction will fail"}; 73 | } 74 | } 75 | 76 | // Get Price 77 | let tokenRate = await this.getTokenRate(token, cache); 78 | if(!tokenRate){ 79 | return { 80 | success: false, 81 | message: "Token price unavailable" 82 | }; 83 | } 84 | 85 | const gasPrices = await this.getGasPrices(token, tokenRate); 86 | if(tokenRate.lt(token.minAcceptedRate)){ 87 | return {success: false, message: "Not accepting " + token.symbol + " at current rate. (Min rate: " + token.minAcceptedRate+ ")"}; 88 | } 89 | 90 | if(gasPrice.lt(gasPrices.inTokens)){ 91 | return {success: false, message: "Gas price is less than the required amount (" + gasPrices.inTokens.toString(10) + ")"}; 92 | } 93 | 94 | return { 95 | success: true, 96 | message: "Valid transaction", 97 | estimatedGas 98 | }; 99 | } 100 | 101 | } 102 | 103 | module.exports = IdentityStrategy; 104 | -------------------------------------------------------------------------------- /gas-relayer/src/strategy/SNTStrategy.js: -------------------------------------------------------------------------------- 1 | const Strategy = require('./BaseStrategy'); 2 | 3 | const TransferSNT = "0x916b6511"; 4 | const ExecuteGasRelayed = "0x754e6ab0"; 5 | 6 | /** 7 | * Class representing a strategy to validate a `transaction` request when the topic is related to SNTController. 8 | * @extends Strategy 9 | */ 10 | class SNTStrategy extends Strategy { 11 | 12 | /** 13 | * Process SNTController strategy 14 | * @param {object} input - Object obtained from an 'transaction' request. It expects an object with this structure `{contract, address, action, functionName, functionParameters, payload}` 15 | * @returns {object} Status of validation and estimated gas 16 | */ 17 | async execute(input, cache){ 18 | const params = this._obtainParametersFunc(input); 19 | 20 | // Verifying if token is allowed 21 | const token = this.settings.getTokenBySymbol("SNT"); 22 | if(token == undefined) return {success: false, message: "Token not allowed"}; 23 | 24 | const balance = await this.getBalance(input.address, token); 25 | 26 | let tokenRate = await this.getTokenRate(token, cache); 27 | if(!tokenRate){ 28 | return { 29 | success: false, 30 | message: "Token price unavailable" 31 | }; 32 | } 33 | 34 | const gasPrices = await this.getGasPrices(token, tokenRate); 35 | if(tokenRate.lt(token.minAcceptedRate)){ 36 | return {success: false, message: "Not accepting " + token.symbol + " at current rate. (Min rate: " + token.minAcceptedRate+ ")"}; 37 | } 38 | 39 | const gasPrice = this.web3.utils.toBN(params('_gasPrice')); 40 | if(gasPrice.lt(gasPrices.inTokens)){ 41 | return {success: false, message: "Gas price is less than the required amount (" + gasPrices.inTokens.toString(10) + ")"}; 42 | } 43 | 44 | 45 | let estimatedGas; 46 | try { 47 | estimatedGas = await this.web3.eth.estimateGas({ 48 | data: input.payload, 49 | from: this.config.node.blockchain.account.address, 50 | to: input.contract 51 | }); 52 | } catch(exc){ 53 | if(exc.message.indexOf("revert") > -1) return {success: false, message: "Transaction will revert"}; 54 | else { 55 | console.error(exc); 56 | return {success: false, message: "Transaction will fail"}; 57 | } 58 | } 59 | 60 | 61 | if(input.functionName == TransferSNT){ 62 | const gas = this.web3.utils.toBN(estimatedGas); 63 | const value = this.web3.utils.toBN(params('_amount')); 64 | const requiredGas = value.add(gas); 65 | 66 | if(balance.lt(requiredGas)){ 67 | return {success: false, message: "Address has not enough balance to transfer specified value + fees (" + requiredGas.toString() + ")"}; 68 | } 69 | } else if(input.functionName == ExecuteGasRelayed){ 70 | const latestBlock = await this.web3.eth.getBlock("latest"); 71 | let estimatedGas = 0; 72 | try { 73 | const simulatedReceipt = await this._simulateTransaction(input, latestBlock.gasLimit); 74 | estimatedGas = this.web3.utils.toBN(simulatedReceipt.gasUsed); 75 | } catch(exc){ 76 | if(exc.message.indexOf("revert") > -1) return {success: false, message: "Transaction will revert"}; 77 | } 78 | 79 | if(balance.lt(estimatedGas)){ 80 | return {success: false, message: "Address has not enough balance to execute the transaction (" + estimatedGas.toString() + ")"}; 81 | } 82 | 83 | const gasMinimal = this.web3.utils.toBN(params('_gasMinimal')); 84 | if(gasMinimal.lt(estimatedGas)){ 85 | return {success: false, message: "Gas minimal is less than estimated gas (" + estimatedGas.toString() + ")"}; 86 | } 87 | 88 | if(balance.lt(gasMinimal)){ 89 | return {success: false, message: "Address has not enough balance for the specified _gasMinimal"}; 90 | } 91 | } 92 | 93 | return { 94 | success: true, 95 | message: "Valid transaction", 96 | estimatedGas 97 | }; 98 | } 99 | 100 | } 101 | 102 | module.exports = SNTStrategy; 103 | -------------------------------------------------------------------------------- /gas-relayer/test/sampleContracts.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.21; 2 | 3 | contract TestIdentityGasRelay { 4 | event Debug(); 5 | 6 | function approveAndCallGasRelayed( 7 | address _baseToken, 8 | address _to, 9 | uint256 _value, 10 | bytes _data, 11 | uint _nonce, 12 | uint _gasPrice, 13 | uint _gasLimit, 14 | address _gasToken, 15 | bytes _messageSignatures 16 | ) external { 17 | emit Debug(); 18 | } 19 | 20 | function callGasRelayed( 21 | address _to, 22 | uint256 _value, 23 | bytes _data, 24 | uint _nonce, 25 | uint _gasPrice, 26 | uint _gasLimit, 27 | address _gasToken, 28 | bytes _messageSignatures 29 | ) external { 30 | emit Debug(); 31 | } 32 | 33 | function() payable { 34 | 35 | } 36 | } 37 | 38 | contract TestIdentityFactory { 39 | address public latestKernel; 40 | function TestIdentityFactory(){ 41 | latestKernel = address(new TestIdentityGasRelay()); 42 | } 43 | } 44 | 45 | contract TestSNTController { 46 | event Debug(); 47 | function transferSNT(address a,uint256 b,uint256 c,uint256 d, bytes f){ 48 | emit Debug(); 49 | } 50 | function executeGasRelayed(address a,bytes b,uint256 c,uint256 d,uint256 e,bytes f){ 51 | emit Debug(); 52 | } 53 | } -------------------------------------------------------------------------------- /gas-relayer/test/sendmsg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Send whisper message to node 8 | 13 | 14 | 15 | 16 |
17 |
18 |
19 |


Send whisper message ws://localhost:8546

20 |

web3 is available in your browser's console: Tools > Developer Tools

21 | Keys: 22 |
    23 |
  • Public:
  • 24 |
  • Private
  • 25 |
26 |
27 |
28 | 29 |
30 |
31 | 0x 32 |
33 | 34 |
35 |
36 | Invalid Sym Key 37 |
38 |
39 |
40 |
41 | 42 |
43 |
44 | 0x 45 |
46 | 47 |
48 |
49 | Invalid Topic 50 |
51 |
52 |
53 | 54 | 55 |
56 | Invalid TTL 57 |
58 |
59 |
60 | 61 | 62 |
63 | Invalid PoW Target 64 |
65 |
66 |
67 | 68 | 69 |
70 | Invalid PoW Time 71 |
72 |
73 |
74 |
75 | 76 |
77 |
78 | 0x 79 |
80 | 81 |
82 |
83 | Invalid Payload 84 |
85 |
86 |
87 |

88 |

89 | 90 |
91 |
92 |
93 |
94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /gas-relayer/test/sendmsg.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | 3 | const connUrl = "ws://localhost:8546"; 4 | let web3 = new Web3(connUrl); 5 | window.web3 = web3; 6 | 7 | let keyPair; 8 | 9 | web3.shh.newKeyPair().then(async function(kid){ 10 | $('.pub').text(await web3.shh.getPublicKey(kid)); 11 | $('.priv').text(await web3.shh.getPrivateKey(kid)); 12 | keyPair = kid; 13 | window.signature = kid; 14 | 15 | web3.shh.subscribe('messages', { 16 | "privateKeyID": signature, 17 | "ttl": 20, 18 | "minPow": 0.8, 19 | "powTime": 1000 20 | }, function(error, message, subscription){ 21 | console.log(web3.utils.hexToAscii(message.payload)); 22 | $('#messageArea').text(web3.utils.hexToAscii(message.payload)); 23 | }); 24 | 25 | 26 | }); 27 | 28 | console.log("Connected to: %c%s", 'font-weight: bold', connUrl); 29 | 30 | const add0x = function(elem){ 31 | if(elem.val().slice(0, 2) != '0x'){ 32 | return '0x' + elem.val(); 33 | } else { 34 | let val = elem.val(); 35 | elem.val(elem.val().slice(2)); 36 | return val; 37 | } 38 | } 39 | 40 | $('button').on('click', async function(e){ 41 | e.preventDefault(); 42 | 43 | $('p.result, #messageArea').text(''); 44 | 45 | let sKey = add0x($("#sKey")); 46 | let msgTopic = add0x($('#topic')); 47 | let msgPayload = add0x($('#payload')); 48 | let timeToLive = $('#ttl').val(); 49 | let powTarget = $('#powTarget').val(); 50 | let powTime = $('#powTime').val(); 51 | 52 | $('.invalid-feedback').hide(); 53 | $('.is-invalid').removeClass('is-invalid'); 54 | 55 | if(!/^0x[0-9a-f]{64}$/i.test(sKey)){ 56 | $('#sKey').addClass('is-invalid'); 57 | $('.invalid-feedback.sKey').show(); 58 | } 59 | 60 | if(!/^0x[0-9a-f]{8}$/i.test(msgTopic)){ 61 | $('#topic').addClass('is-invalid'); 62 | $('.invalid-feedback.topic').show(); 63 | } 64 | 65 | if(!/^0x[0-9a-f]+$/i.test(msgPayload) || msgPayload.length%2 > 0){ 66 | $('#payload').addClass('is-invalid'); 67 | $('.invalid-feedback.payload').show(); 68 | } 69 | 70 | if(!/^[0-9]+$/i.test(timeToLive)){ 71 | $('#ttl').addClass('is-invalid'); 72 | $('.invalid-feedback.ttl').show(); 73 | } 74 | 75 | if(!/^[+-]?([0-9]*[.])?[0-9]+$/.test(powTarget)){ 76 | $('#powTarget').addClass('is-invalid'); 77 | $('.invalid-feedback.powTarget').show(); 78 | } 79 | 80 | if(!/^[+-]?([0-9]*[.])?[0-9]+$/.test(powTime)){ 81 | $('#powTime').addClass('is-invalid'); 82 | $('.invalid-feedback.powTime').show(); 83 | } 84 | 85 | 86 | if($('.is-invalid').length > 0) return; 87 | 88 | console.log(`%c await web3.shh.post({symKeyID: "${sKey}", sig: signature, ttl: ${timeToLive}, powTarget: ${powTarget}, powTime: ${powTime}, topic: "${msgTopic}", payload: "${msgPayload}"})`, 'font-weight: bold'); 89 | 90 | let identity; 91 | 92 | 93 | let _symKeyId = await web3.shh.addSymKey(sKey); 94 | 95 | 96 | web3.shh.post({ 97 | symKeyID: _symKeyId, 98 | sig: keyPair, 99 | ttl: parseInt(timeToLive), 100 | powTarget: parseFloat(powTarget), 101 | powTime: parseFloat(powTime), 102 | topic: msgTopic, 103 | payload: msgPayload}) 104 | .then(result => { 105 | console.log(result); 106 | $('p.result').html("Response: " + result); 107 | }); 108 | }); 109 | }); -------------------------------------------------------------------------------- /installation-development.md: -------------------------------------------------------------------------------- 1 | # Installation - Development Environment 2 | 3 | - Install the latest develop version of embark: `npm install -g https://github.com/embark-framework/embark.git` 4 | 5 | - Install `geth` 6 | 7 | - Clone the repository 8 | `git clone https://github.com/status-im/snt-gas-relay.git` 9 | 10 | - Execute the following commands 11 | ``` 12 | cd snt-gas-relay/test-dapp 13 | chmod +x setup_dev_env.sh 14 | npm install 15 | embark reset 16 | embark blockchain 17 | ``` 18 | 19 | - When Embark finishes loading, execute `embark run` to deploy the contracts. 20 | 21 | - After the contracts are deployed and the test dapp is available, execute `./setup_dev_env.sh` to create the test account 22 | 23 | ## Test DApp 24 | To run the test dapp, use `embark run` and then browse `http://localhost:8000/index.html`. 25 | 26 | The gas relayer service needs to be running, and configured correctly to process the transactions. Things to take in account are: the account used in embark, and the contract addresses. 27 | 28 | You may use the test dapp to generate SNT and fund the relayer account before running the gas relayer, as it requires ether to start. You may fund the relayer with `web3.eth.sendTransaction` or configure embark so it funds an account when it starts the chain. 29 | 30 | ## Relayer Node 31 | 32 | - `cd snt-gas-relay/gas-relayer` 33 | 34 | - This program is configured with the default values on `config/config.json` for a embark installation run from 0. To execute the gas-relayer, you may use any of the following three commands. 35 | 36 | ``` 37 | npm start 38 | node src/service.js 39 | nodemon src/service.js 40 | ``` 41 | -------------------------------------------------------------------------------- /installation-testnet-mainnet.md: -------------------------------------------------------------------------------- 1 | # Installation - Testnet 2 | 3 | ### Notes 4 | 1. This installation assumes you're using Ubuntu or similar 5 | 2. You need a non-root user that belongs to the sudo group. 6 | 7 | ### Required software install procedure 8 | 9 | 1. Install Ethereum 10 | ``` 11 | sudo apt install software-properties-common 12 | sudo add-apt-repository -y ppa:ethereum/ethereum 13 | sudo apt install ethereum 14 | ``` 15 | 16 | 2. Install NodeJS using NVM (Check NVM repo for updated procedure) 17 | ``` 18 | curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash 19 | source .bashrc 20 | nvm install --lts 21 | ``` 22 | 23 | 3. Install Python and other software 24 | ``` 25 | sudo apt install python build-essential 26 | ``` 27 | 28 | ### Clone the repo 29 | ``` 30 | git clone https://github.com/status-im/snt-gas-relay.git 31 | cd snt-gas-relay/gas-relayer 32 | npm install 33 | ``` 34 | 35 | ### Setup geth for Whisper 36 | 37 | 1. Verify `geth` light mode starts successfully. Exit geth when you see everything is ok 38 | ``` 39 | geth --testnet --syncmode=light console 40 | > exit 41 | ``` 42 | 43 | 2. There aren't enough geth peers with Whisper enabled to guarantee that messages will arrive from one node to other. We need to create a `static-nodes.json` file in `~/.ethereum/testnet/geth/`. 44 | 45 | This file needs to contain the following array: 46 | ``` 47 | [ 48 | "enode://436cc6f674928fdc9a9f7990f2944002b685d1c37f025c1be425185b5b1f0900feaf1ccc2a6130268f9901be4a7d252f37302c8335a2c1a62736e9232691cc3a@174.138.105.243:30404", 49 | "enode://5395aab7833f1ecb671b59bf0521cf20224fe8162fc3d2675de4ee4d5636a75ec32d13268fc184df8d1ddfa803943906882da62a4df42d4fccf6d17808156a87@206.189.243.57:30404", 50 | "enode://7427dfe38bd4cf7c58bb96417806fab25782ec3e6046a8053370022cbaa281536e8d64ecd1b02e1f8f72768e295d06258ba43d88304db068e6f2417ae8bcb9a6@104.154.88.123:30404", 51 | "enode://ebefab39b69bbbe64d8cd86be765b3be356d8c4b24660f65d493143a0c44f38c85a257300178f7845592a1b0332811542e9a58281c835babdd7535babb64efc1@35.202.99.224:30404", 52 | "enode://a6a2a9b3a7cbb0a15da74301537ebba549c990e3325ae78e1272a19a3ace150d03c184b8ac86cc33f1f2f63691e467d49308f02d613277754c4dccd6773b95e8@206.189.243.176:30304", 53 | "enode://207e53d9bf66be7441e3daba36f53bfbda0b6099dba9a865afc6260a2d253fb8a56a72a48598a4f7ba271792c2e4a8e1a43aaef7f34857f520c8c820f63b44c8@35.224.15.65:30304", 54 | "enode://c42f368a23fa98ee546fd247220759062323249ef657d26d357a777443aec04db1b29a3a22ef3e7c548e18493ddaf51a31b0aed6079bd6ebe5ae838fcfaf3a49@206.189.243.162:30504", 55 | "enode://7aa648d6e855950b2e3d3bf220c496e0cae4adfddef3e1e6062e6b177aec93bc6cdcf1282cb40d1656932ebfdd565729da440368d7c4da7dbd4d004b1ac02bf8@206.189.243.169:30504", 56 | "enode://8a64b3c349a2e0ef4a32ea49609ed6eb3364be1110253c20adc17a3cebbc39a219e5d3e13b151c0eee5d8e0f9a8ba2cd026014e67b41a4ab7d1d5dd67ca27427@206.189.243.168:30504", 57 | "enode://7de99e4cb1b3523bd26ca212369540646607c721ad4f3e5c821ed9148150ce6ce2e72631723002210fac1fd52dfa8bbdf3555e05379af79515e1179da37cc3db@35.188.19.210:30504", 58 | "enode://015e22f6cd2b44c8a51bd7a23555e271e0759c7d7f52432719665a74966f2da456d28e154e836bee6092b4d686fe67e331655586c57b718be3997c1629d24167@35.226.21.19:30504", 59 | "enode://531e252ec966b7e83f5538c19bf1cde7381cc7949026a6e499b6e998e695751aadf26d4c98d5a4eabfb7cefd31c3c88d600a775f14ed5781520a88ecd25da3c6@35.225.227.79:30504" 60 | ] 61 | ``` 62 | These enodes were extracted from https://github.com/status-im/status-go/blob/develop/params/cluster.go. 63 | 64 | ### Setup the gas relayer 65 | 66 | Before executing this program, `config/config.json` must be setup and `npm install` needs to be executed. Important values to verify are related to the node configuration, just like: 67 | - Host, port and protocol to connect to the geth node 68 | - Host, port and protocol Ganache will use when forking the blockchain for gas estimations and other operations 69 | - Account used for processing the transactions 70 | - Symmetric key used to receive the Whisper messages 71 | - Accepted tokens information 72 | - Contract configuration 73 | 74 | 1. For testnet, a config file is provided with the required configuration, you just need to set the account information 75 | ``` 76 | cd config 77 | rm config.js 78 | mv config.testnet.js config.js 79 | ``` 80 | 81 | 2. A node that wants to act as a relayer only needs to have a geth node with whisper enabled, and an account with ether to process the transactions. This account needs to be configured in `./config/config.js`. Edit this file and set the account: 82 | ``` 83 | "blockchain": { 84 | // Use one of these options: 85 | 86 | // Option1 =========================== 87 | // privateKey: "your_private_key", 88 | 89 | // Option2 =========================== 90 | // privateKeyFile: "path/to/file" 91 | 92 | // Option3 =========================== 93 | // mnemonic: "12 word mnemonic", 94 | // addressIndex: "0", // Optionnal. The index to start getting the address 95 | // hdpath: "m/44'/60'/0'/0/" // Optionnal. HD derivation path 96 | }, 97 | ``` 98 | 99 | 100 | ### Launching the relayer 101 | A `launch-geth-testnet.sh` script is provided in the `snt-gas-relayer/gas-relayer` folder: 102 | 103 | ``` 104 | chmod +x ./launch-geth-testnet.sh 105 | ./launch-geth-testnet.sh 106 | ``` 107 | 108 | you may use any of the following three commands to launch the relayer. 109 | 110 | ``` 111 | npm start 112 | node src/service.js 113 | nodemon src/service.js 114 | ``` 115 | 116 | 117 | ### Using the testdapp with testnet 118 | The test dapp may be used for testnet from your computer. It requires a node that allows websockets. You may use this command to launch a geth instance pointing to testnet: 119 | 120 | ``` 121 | chmod +x ./launch-geth-testnet.sh 122 | ./launch-geth-testnet.sh 123 | ``` 124 | 125 | 1. Execute `embark run testnet` to launch the dapp connected to testnet 126 | 2. Navigate in your browser to http://localhost:8000. Use metamask to connect to your local node. 127 | 3. You're now able to use the dapp normally. The status for the relayer that can be seen in the footer of the dapp won't reflect accurate information, since the relayers account are not deterministic anymore since you're not in a development environment 128 | 129 | #### NOTE 130 | Work is in progress for using the test-dapp inside status. 131 | -------------------------------------------------------------------------------- /javascript-library.md: -------------------------------------------------------------------------------- 1 | 2 | # Javascript Library 3 | 4 | To simplify the process of building the whisper messages, a js file `status-gas-relayer.js` was created in the test-dapp. It only requires to setup connection to geth, and required keypairs and symkeys. This file might be expanded upon in the future and converted to a npm package. 5 | 6 | ## Use 7 | 8 | ``` 9 | import StatusGasRelayer, {Contracts, Functions, Messages} from 'status-gas-relayer'; 10 | 11 | // Connecting to web3 12 | const web3 = new Web3('ws://localhost:8546'); 13 | const kid = await web3js.shh.newKeyPair() 14 | const skid = await web3.shh.addSymKey("0xd0d905c1c62b810b787141430417caf2b3f54cffadb395b7bb39fdeb8f17266b"); 15 | ``` 16 | 17 | #### Subscribing to messages 18 | General message subscription. Special handling is needed for handling relayer availability messages. The `sig` property is the relayer's public key that needs to be sent when sending a transaction message. More than 1 relayer can reply, so it's recommended to save these keys in a list/array. 19 | 20 | ``` 21 | StatusGasRelayer.subscribe(web3js, (error, msgObj) => { 22 | if(error) { 23 | console.error(error); 24 | return; 25 | } 26 | 27 | if(msgObj.message == Messages.available){ 28 | // Relayer availability message 29 | console.log("Relayer available: " + msgObj.sig); 30 | } else { 31 | // Normal message 32 | console.log(msgObj); 33 | } 34 | }, { 35 | privateKeyID: kid 36 | }); 37 | ``` 38 | 39 | #### Polling for relayers 40 | ``` 41 | const identityAddress = this.props.identityAddress; // Identity contract 42 | const accountAddress = web3.eth.defaultAccount; 43 | const gasToken = SNT.options.address; 44 | const gasPrice = 1000000000000; // In wei equivalent to the used token 45 | 46 | const s = new StatusGasRelayer.AvailableRelayers(Contracts.Identity, identityAddress, accountAddress) 47 | .setRelayersSymKeyID(skid) 48 | .setAsymmetricKeyID(kid) 49 | .setGasToken(gasToken) 50 | .setGas(gasPrice); 51 | await s.post(web3); 52 | ``` 53 | 54 | #### Signing a message 55 | Signing a message is similar to invoking a function. Both use mostly the same functions. The difference is that when you invoke a function, you need to specify the relayer and asymmetric key Id. 56 | 57 | ``` 58 | try { 59 | const s = new StatusGasRelayer.Identity(identityAddress, accountAddress) 60 | .setContractFunction(Functions.Identity.call) 61 | .setTransaction(to, value, data) 62 | .setGas(gasToken, gasPrice, gasLimit); 63 | 64 | const signature = await s.sign(web3); 65 | } catch(error){ 66 | console.log(error); 67 | } 68 | 69 | ``` 70 | 71 | #### Using Identity contract `call` function 72 | This functionality is used when a Identity will invoke a contract function or transfer ether without paying fees 73 | 74 | ``` 75 | try { 76 | const s = new StatusGasRelayer.Identity(identityAddress, accountAddress) 77 | .setContractFunction(Functions.Identity.call) 78 | .setTransaction(to, value, data) // 'value' is in wei, and 'data' must be a hex string 79 | .setGasToken(gasToken) 80 | .setGas(gasPrice, gasMinimal) 81 | .setRelayer(relayer) 82 | .setAsymmetricKeyID(kid); 83 | 84 | await s.post(signature, web3); 85 | } catch(error){ 86 | console.log(error); 87 | } 88 | ``` 89 | 90 | #### Using Identity contract `approveAndCall` function 91 | This functionality is used when a Identity will invoke a contract function that requires a transfer of Tokens 92 | 93 | ``` 94 | try { 95 | const s = new StatusGasRelayer.Identity(identityAddress, accountAddress) 96 | .setContractFunction(Functions.Identity.approveAndCall) 97 | .setTransaction(to, value, data) 98 | .setBaseToken(baseToken) 99 | .setGas(gasPrice, gasMinimal) 100 | .setGasToken(SNT_Address) 101 | .setRelayer(relayer) 102 | .setAsymmetricKeyID(kid); 103 | 104 | await s.post(signature, web3); 105 | } catch(error){ 106 | console.log(error); 107 | } 108 | ``` 109 | 110 | #### Using SNTController `transferSNT` function 111 | This functionality is used for simple wallets to perform SNT transfers without paying ETH fees 112 | ``` 113 | try { 114 | const accounts = await web3.eth.getAccounts(); 115 | 116 | const s = new StatusGasRelayer.SNTController(SNTController.options.address, accounts[2]) 117 | .transferSNT(to, amount) 118 | .setGas(gasPrice) 119 | .setRelayer(relayer) 120 | .setAsymmetricKeyID(kid); 121 | 122 | await s.post(signature, web3); 123 | } catch(error){ 124 | console.log(error); 125 | } 126 | ``` 127 | 128 | #### Using SNTController `execute` function 129 | ``` 130 | try { 131 | const accounts = await web3.eth.getAccounts(); 132 | 133 | const s = new StatusGasRelayer.SNTController(SNTController.options.address, accounts[2]) 134 | .execute(allowedContract, data) 135 | .setGas(gasPrice, gasMinimal) 136 | .setRelayer(relayer) 137 | .setAsymmetricKeyID(kid); 138 | 139 | await s.post(signature, web3); 140 | } catch(error){ 141 | console.log(error); 142 | } 143 | ``` 144 | -------------------------------------------------------------------------------- /relayer-protocol.md: -------------------------------------------------------------------------------- 1 | # Gas relayer protocol 2 | 3 | Current implementation of gas relayers use whisper as a communication medium to broadcast availability, and transaction details. As such, the messages require specific settings and format to be interpreted correctly by the relayer network. 4 | 5 | #### Sending a message to the gas relayer network (all accounts and private keys should be replaced by your own) 6 | ``` 7 | shh.post({ 8 | symKeyID: SYM_KEY, // If requesting availability 9 | pubKey: PUBLIC_KEY_ID, // If sending a transaction 10 | sig: WHISPER_KEY_ID, 11 | ttl: 10, 12 | powTarget: 0.002, 13 | powTime: 1, 14 | topic: TOPIC_NAME, 15 | payload: PAYLOAD_BYTES 16 | }).then(......) 17 | ``` 18 | 19 | - `symKeyID: SYM_KEY` must contain the whisper symmetric key used. It is shown on the console when running the service with `node`. With the provided configuration you can use the symmetric key `0xd0d905c1c62b810b787141430417caf2b3f54cffadb395b7bb39fdeb8f17266b`. Only used when asking for relayer availability. 20 | - `pubKey: PUBLIC_KEY_ID`. After asking for availability, once the user decides on a relayer, it needs to set the `pubKey` attribute with the relayer public key (received in the availability reply in the `sig` attribute of the message). 21 | - `WHISPER_KEY_ID` represents a keypair registered on your node, that will be used to sign the message. Can be generated with `web3W.shh.newKeyPair()` 22 | - `TOPIC_NAME` must contain one of the topic names generated based on converting the contract name to hex, and taking the first 8 bytes. For the provided configuration the following topics are available: 23 | - - IdentityGasRelay: `0x4964656e` 24 | - - SNTController: `0x534e5443` 25 | - `PAYLOAD_BYTES` a hex string that contains details on the operation to perform. 26 | 27 | 28 | #### Polling for gas relayers 29 | The first step is asking the relayers for their availability. The message payload needs to be the hex string representation of a JSON object with a specific structure: 30 | 31 | ``` 32 | const payload = web3.utils.toHex({ 33 | 'contract': "0xContractToInvoke", 34 | 'address': web3.eth.defaultAccount, 35 | 'action': 'availability', 36 | 'token': "0xGasTokenAddress", 37 | 'gasPrice': 1234 38 | }); 39 | ``` 40 | 41 | - `contract` is the address of the contract that will perform the operation, in this case it can be an Identity, or the SNTController. 42 | - `address` The address that will sign the transactions. Normally it's `web3.eth.defaultAccount` 43 | - `gasToken`: token used for paying the gas cost 44 | - `gasPrice`: The gas price used for the transaction 45 | 46 | This is a example code of how to send an 'availability' message: 47 | 48 | ``` 49 | const whisperKeyPairID = await web3W.shh.newKeyPair(); 50 | 51 | const msgObj = { 52 | symKeyId: "0xd0d905c1c62b810b787141430417caf2b3f54cffadb395b7bb39fdeb8f17266b", 53 | sig: whisperKeyPairID, 54 | ttl: 10, 55 | powTarget: 0.002, 56 | powTime: 1, 57 | topic: "0x4964656e", 58 | payload: web3.utils.toHex({ 59 | 'contract': "0x692a70d2e424a56d2c6c27aa97d1a86395877b3a", 60 | 'address': web3.eth.defaultAccount 61 | 'action': 'availability', 62 | 'gasToken': "0x744d70fdbe2ba4cf95131626614a1763df805b9e", 63 | 'gasPrice': 40000000000 // 40 gwei equivalent in SNT 64 | }) 65 | }; 66 | 67 | 68 | web3.shh.post(msgObj) 69 | .then((err, result) => { 70 | console.log(result); 71 | console.log(err); 72 | }); 73 | ``` 74 | 75 | When it replies, you need to extract the `sig` attribute to obtain the relayer's public key 76 | 77 | #### Sending transaction details 78 | 79 | Sending a transaction is similar to the previous operation, except that we send the message to an specific node, we use the action `transaction`, and also we send a `encodedFunctionCall` with the details of the transaction to execute. 80 | 81 | From the list of relayers received via whisper messages, you need to extract the `message.sig` to obtain the `pubKey`. This value is used to send the transaction to that specific relayer. 82 | 83 | `encodedFunCall` is the hex data used obtained from `web3.eth.abi.encodeFunctionCall` for the specific function we want to invoke. 84 | 85 | If we were to execute `callGasRelayed(address,uint256,bytes,uint256,uint256,uint256,address,bytes)` (part of the IdentityGasRelay) in contract `0x692a70d2e424a56d2c6c27aa97d1a86395877b3a`, with these values: `"0x11223344556677889900998877665544332211",100,"0x00",1,10,20,"0x1122334455112233445511223344551122334455"`, "0x1122334455", `PAYLOAD_BYTES` can be prepared as follows: 86 | 87 | ``` 88 | // The following values are created obtained when polling for relayers 89 | const whisperKeyPairID = await web3W.shh.newKeyPair(); 90 | const relayerPubKey = "0xRELAYER_PUBLIC_KEY_HERE"; 91 | // ... 92 | // ... 93 | const jsonAbi = ABIOfIdentityGasRelay.find(x => x.name == "callGasRelayed"); 94 | 95 | const funCall = web3.eth.abi.encodeFunctionCall(jsonAbi, 96 | [ 97 | "0x11223344556677889900998877665544332211", 98 | 100, 99 | "0x00", 100 | 1, 101 | 10, 102 | 20, 103 | "0x1122334455112233445511223344551122334455", 104 | "0x1122334455" 105 | ]); 106 | 107 | const msgObj = { 108 | pubKey: relayerPubKey, 109 | sig: whisperKeyPairID, 110 | ttl: 10, 111 | powTarget: 0.002, 112 | powTime: 1, 113 | topic: "0x4964656e", 114 | payload: web3.utils.toHex({ 115 | 'contract': "0x692a70d2e424a56d2c6c27aa97d1a86395877b3a", 116 | 'address': web3.eth.defaultAccount 117 | 'action': 'transaction', 118 | 'encodedFunctionCall': funCall, 119 | }) 120 | }; 121 | 122 | 123 | web3.shh.post(msgObj) 124 | .then((err, result) => { 125 | console.log(result); 126 | console.log(err); 127 | }); 128 | 129 | ``` 130 | 131 | Notice that sending messages use specific TTL, PoW Targets nad Time. These values specified here are those accepted by the Status.im cluster of nodes. The configuration for dev environment use greater values, since it is not expected to communicate with other nodes. -------------------------------------------------------------------------------- /test-dapp/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@babel/plugin-proposal-object-rest-spread", 4 | "@babel/plugin-syntax-dynamic-import", 5 | "@babel/plugin-syntax-import-meta", 6 | "@babel/plugin-proposal-class-properties", 7 | "@babel/plugin-proposal-json-strings", 8 | [ 9 | "@babel/plugin-proposal-decorators", 10 | { 11 | "legacy": true 12 | } 13 | ], 14 | "@babel/plugin-proposal-function-sent", 15 | "@babel/plugin-proposal-export-namespace-from", 16 | "@babel/plugin-proposal-numeric-separator", 17 | "@babel/plugin-proposal-throw-expressions" 18 | ], 19 | "presets": [], 20 | "ignore": [ 21 | "config/", 22 | "node_modules" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /test-dapp/.gitignore: -------------------------------------------------------------------------------- 1 | .embark/ 2 | node_modules/ 3 | dist/ 4 | config/production/password 5 | config/livenet/password 6 | -------------------------------------------------------------------------------- /test-dapp/app/components/body-identity.js: -------------------------------------------------------------------------------- 1 | import React, {Component, Fragment} from 'react'; 2 | import StatusGasRelayer, {Messages} from '../status-gas-relayer'; 3 | import ApproveAndCallGasRelayed from './approveandcallgasrelayed'; 4 | import CallGasRelayed from './callgasrelayed'; 5 | import Divider from '@material-ui/core/Divider'; 6 | import EmbarkJS from 'Embark/EmbarkJS'; 7 | import IdentityFactory from 'Embark/contracts/IdentityFactory'; 8 | import IdentityGasRelay from 'Embark/contracts/IdentityGasRelay'; 9 | import PropTypes from 'prop-types'; 10 | import Status from './status-identity'; 11 | import Tab from '@material-ui/core/Tab'; 12 | import Tabs from '@material-ui/core/Tabs'; 13 | import Typography from '@material-ui/core/Typography'; 14 | import Web3 from 'web3'; 15 | import config from '../config'; 16 | import {withStyles} from '@material-ui/core/styles'; 17 | 18 | window.IdentityFactory = IdentityFactory; 19 | 20 | const styles = {}; 21 | 22 | class Body extends Component { 23 | 24 | constructor(props){ 25 | super(props); 26 | this.state = { 27 | tab: 0, 28 | identityAddress: "0x0000000000000000000000000000000000000000", 29 | nonce: '0', 30 | kid: null, 31 | skid: null, 32 | message: '', 33 | relayerAddress: '0x0000000000000000000000000000000000000000', 34 | relayers: {} 35 | }; 36 | } 37 | 38 | componentDidMount(){ 39 | EmbarkJS.onReady(err => { 40 | if(err) { 41 | console.error(err); 42 | return; 43 | } 44 | 45 | const web3js = new Web3('ws://localhost:8546'); 46 | 47 | web3js.shh.setMinPoW(0.002).then(res => { 48 | console.log("Min PoW set: " + res); 49 | }) 50 | 51 | // Default for devenv 52 | web3js.eth.net.getId().then(netId => { 53 | if(netId != 1 && netId != 3){ 54 | this.setState({relayerAddress: config.relayAccount}); 55 | } 56 | }); 57 | 58 | web3js.shh.newKeyPair() 59 | .then((kid) => { 60 | web3js.shh.addSymKey(config.relaySymKey) 61 | .then((skid) => { 62 | this.setState({kid, skid}); 63 | 64 | StatusGasRelayer.subscribe(web3js, (error, msgObj) => { 65 | if(error) { 66 | console.error(error); 67 | return; 68 | } 69 | 70 | if(msgObj.message == Messages.available){ 71 | // found a relayer 72 | console.log("Relayer available: " + msgObj.sig); 73 | 74 | let relayers = this.state.relayers; 75 | relayers[msgObj.sig] = msgObj.address; 76 | 77 | if(this.state.relayerAddress == '0x0000000000000000000000000000000000000000'){ 78 | this.setState({relayerAddress: msgObj.address}); 79 | } 80 | 81 | this.setState({relayers}); 82 | } 83 | 84 | this.setState({message: JSON.stringify(msgObj, null, 2)}); 85 | }, { 86 | privateKeyID: kid 87 | }); 88 | 89 | return true; 90 | }); 91 | }); 92 | 93 | this.setState({ 94 | web3js 95 | }); 96 | }); 97 | } 98 | 99 | handleChange = (event, tab) => { 100 | this.setState({tab}); 101 | }; 102 | 103 | updateRelayer = (relayer) => { 104 | this.setState({relayerAddress: this.state.relayers[relayer]}); 105 | } 106 | 107 | updateNonce = (newNonce) => { 108 | this.setState({nonce: newNonce}); 109 | } 110 | 111 | clearMessages = () => { 112 | this.setState({message: ''}); 113 | } 114 | 115 | newIdentity = (cb) => { 116 | let toSend = IdentityFactory.methods['createIdentity()'](); 117 | toSend.estimateGas() 118 | .then(estimatedGas => { 119 | console.log("Estimated Gas: " + estimatedGas); 120 | return toSend.send({gas: estimatedGas + 1000000}); 121 | }) 122 | .then((receipt) => { 123 | console.log(receipt); 124 | const instance = receipt.events.IdentityCreated.returnValues.instance; 125 | this.setState({identityAddress: instance}); 126 | cb(); 127 | }); 128 | } 129 | 130 | render(){ 131 | const {tab, identityAddress, nonce, web3js, message, kid, skid, relayers, relayerAddress} = this.state; 132 | 133 | return 134 | 135 | 136 | 137 | 138 | 139 | {tab === 0 && } 140 | {tab === 1 && } 141 | {tab === 2 && Item Three} 142 | 143 | 144 | 145 | 146 | ; 147 | } 148 | } 149 | 150 | function Container(props) { 151 | return 152 | {props.children} 153 | ; 154 | } 155 | 156 | Container.propTypes = { 157 | children: PropTypes.node.isRequired 158 | }; 159 | 160 | Body.propTypes = { 161 | classes: PropTypes.object.isRequired 162 | }; 163 | 164 | export default withStyles(styles)(Body); 165 | -------------------------------------------------------------------------------- /test-dapp/app/components/body-sntcontroller.js: -------------------------------------------------------------------------------- 1 | import React, {Component, Fragment} from 'react'; 2 | import StatusGasRelayer, {Messages} from '../status-gas-relayer'; 3 | import Divider from '@material-ui/core/Divider'; 4 | import EmbarkJS from 'Embark/EmbarkJS'; 5 | import Execute from './execute'; 6 | import PropTypes from 'prop-types'; 7 | import STT from 'Embark/contracts/STT'; 8 | import Status from './status-sntcontroller'; 9 | import Tab from '@material-ui/core/Tab'; 10 | import Tabs from '@material-ui/core/Tabs'; 11 | import TransferSNT from './transfersnt'; 12 | import Typography from '@material-ui/core/Typography'; 13 | import Web3 from 'web3'; 14 | import config from '../config'; 15 | import web3 from 'Embark/web3'; 16 | import {withStyles} from '@material-ui/core/styles'; 17 | 18 | window.STT = STT; 19 | 20 | const styles = {}; 21 | 22 | class Body extends Component { 23 | 24 | constructor(props){ 25 | super(props); 26 | this.state = { 27 | tab: 0, 28 | walletAddress: null, 29 | nonce: '0', 30 | kid: null, 31 | skid: null, 32 | message: '', 33 | relayerAddress: '0x0000000000000000000000000000000000000000', 34 | relayers: {} 35 | }; 36 | } 37 | 38 | componentDidMount(){ 39 | EmbarkJS.onReady(err => { 40 | if(err) { 41 | console.error(err); 42 | return; 43 | } 44 | 45 | const web3js = new Web3('ws://localhost:8546'); 46 | 47 | web3js.shh.setMinPoW(0.002).then(res => { 48 | console.log("Min PoW set: " + res); 49 | }) 50 | 51 | 52 | // Default for devenv 53 | web3js.eth.net.getId().then(netId => { 54 | if(netId != 1 && netId != 3){ 55 | this.setState({relayerAddress: config.relayAccount}); 56 | } 57 | }); 58 | 59 | web3js.shh.newKeyPair() 60 | .then((kid) => { 61 | web3js.shh.addSymKey(config.relaySymKey) 62 | .then((skid) => { 63 | this.setState({kid, skid}); 64 | 65 | StatusGasRelayer.subscribe(web3js, (error, msgObj) => { 66 | if(error) { 67 | console.error(error); 68 | return; 69 | } 70 | 71 | if(msgObj.message == Messages.available){ 72 | // found a relayer 73 | console.log("Relayer available: " + msgObj.sig); 74 | 75 | let relayers = this.state.relayers; 76 | relayers[msgObj.sig] = msgObj.address; 77 | 78 | if(this.state.relayerAddress == '0x0000000000000000000000000000000000000000'){ 79 | this.setState({relayerAddress: msgObj.address}); 80 | } 81 | 82 | this.setState({relayers}); 83 | } 84 | 85 | this.setState({message: JSON.stringify(msgObj, null, 2)}); 86 | }, { 87 | privateKeyID: kid 88 | }); 89 | 90 | return true; 91 | }); 92 | }); 93 | 94 | this.setState({ 95 | web3js 96 | }); 97 | 98 | web3.eth.getAccounts() 99 | .then(accounts => { 100 | this.setState({walletAddress: accounts[0]}); 101 | }); 102 | 103 | }); 104 | } 105 | 106 | handleChange = (event, tab) => { 107 | this.setState({tab}); 108 | }; 109 | 110 | updateRelayer = (relayer) => { 111 | this.setState({relayerAddress: this.state.relayers[relayer]}); 112 | } 113 | 114 | updateNonce = (newNonce) => { 115 | this.setState({nonce: newNonce}); 116 | } 117 | 118 | clearMessages = () => { 119 | this.setState({message: ''}); 120 | } 121 | 122 | render(){ 123 | const {tab, walletAddress, nonce, web3js, message, kid, skid, relayers, relayerAddress} = this.state; 124 | 125 | return 126 | 127 | 128 | 129 | 130 | {tab === 0 && } 131 | {tab === 1 && } 132 | 133 | 134 | 135 | 136 | ; 137 | } 138 | } 139 | 140 | function Container(props) { 141 | return 142 | {props.children} 143 | ; 144 | } 145 | 146 | Container.propTypes = { 147 | children: PropTypes.node.isRequired 148 | }; 149 | 150 | Body.propTypes = { 151 | classes: PropTypes.object.isRequired 152 | }; 153 | 154 | export default withStyles(styles)(Body); 155 | -------------------------------------------------------------------------------- /test-dapp/app/components/header.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import AppBar from '@material-ui/core/AppBar'; 3 | import PropTypes from 'prop-types'; 4 | import Toolbar from '@material-ui/core/Toolbar'; 5 | import {Typography} from '@material-ui/core'; 6 | import {withStyles} from '@material-ui/core/styles'; 7 | 8 | 9 | const styles = { 10 | root: { 11 | flexGrow: 1 12 | }, 13 | flex: { 14 | flexGrow: 1 15 | }, 16 | toolBarTyp: { 17 | color: '#fff' 18 | } 19 | }; 20 | 21 | class Header extends Component { 22 | 23 | render(){ 24 | const {classes} = this.props; 25 | return ( 26 |
27 | 28 | 29 | Gas Relayer Demo 30 | 31 | 32 |
33 | ); 34 | } 35 | } 36 | 37 | Header.propTypes = { 38 | classes: PropTypes.object.isRequired 39 | }; 40 | 41 | export default withStyles(styles)(Header); 42 | -------------------------------------------------------------------------------- /test-dapp/app/components/snackbar.js: -------------------------------------------------------------------------------- 1 | import ErrorIcon from '@material-ui/icons/Error'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import SnackbarContent from '@material-ui/core/SnackbarContent'; 5 | import classNames from 'classnames'; 6 | import {withStyles} from '@material-ui/core/styles'; 7 | 8 | const variantIcon = { 9 | error: ErrorIcon 10 | }; 11 | 12 | const styles1 = theme => ({ 13 | error: { 14 | backgroundColor: theme.palette.error.dark 15 | }, 16 | icon: { 17 | fontSize: 20 18 | }, 19 | iconVariant: { 20 | opacity: 0.9, 21 | marginRight: theme.spacing.unit 22 | }, 23 | message: { 24 | display: 'flex', 25 | alignItems: 'center' 26 | } 27 | }); 28 | 29 | function MySnackbarContent(props) { 30 | const {classes, className, message, variant, ...other} = props; 31 | const Icon = variantIcon[variant]; 32 | 33 | return ( 34 | 39 | 40 | {message} 41 | 42 | } 43 | {...other} 44 | /> 45 | ); 46 | } 47 | 48 | MySnackbarContent.propTypes = { 49 | classes: PropTypes.object.isRequired, 50 | className: PropTypes.string, 51 | message: PropTypes.node, 52 | onClose: PropTypes.func, 53 | variant: PropTypes.oneOf(['success', 'warning', 'error', 'info']).isRequired 54 | }; 55 | 56 | export default withStyles(styles1)(MySnackbarContent); 57 | -------------------------------------------------------------------------------- /test-dapp/app/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | "relayAccount": "0x5b9b5db9cde96fda2e2c88e83f1b833f189e01f4", 3 | "relaySymKey": "0xd0d905c1c62b810b787141430417caf2b3f54cffadb395b7bb39fdeb8f17266b" 4 | }; 5 | 6 | export default config; 7 | -------------------------------------------------------------------------------- /test-dapp/app/css/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/status-im/snt-gas-relay/4d563f44c8bf0d53fe8165183ef705e6bfd55c8e/test-dapp/app/css/.gitkeep -------------------------------------------------------------------------------- /test-dapp/app/css/dapp.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | background: #eeeeee; 4 | } -------------------------------------------------------------------------------- /test-dapp/app/identity.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test-dapp/app/identity.js: -------------------------------------------------------------------------------- 1 | import React, {Component, Fragment} from 'react'; 2 | import Body from './components/body-identity'; 3 | import Header from './components/header'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | 7 | class App extends Component { 8 | render(){ 9 | return 10 |
11 | 12 | ; 13 | } 14 | } 15 | 16 | ReactDOM.render(, document.getElementById('app')); 17 | -------------------------------------------------------------------------------- /test-dapp/app/images/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/status-im/snt-gas-relay/4d563f44c8bf0d53fe8165183ef705e6bfd55c8e/test-dapp/app/images/.gitkeep -------------------------------------------------------------------------------- /test-dapp/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Gas Relayer Demos

13 |

The following demos are available:

14 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test-dapp/app/js/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/status-im/snt-gas-relay/4d563f44c8bf0d53fe8165183ef705e6bfd55c8e/test-dapp/app/js/.gitkeep -------------------------------------------------------------------------------- /test-dapp/app/js/index.js: -------------------------------------------------------------------------------- 1 | import EmbarkJS from 'Embark/EmbarkJS'; 2 | 3 | // import your contracts 4 | // e.g if you have a contract named SimpleStorage: 5 | //import SimpleStorage from 'Embark/contracts/SimpleStorage'; 6 | 7 | -------------------------------------------------------------------------------- /test-dapp/app/sntcontroller.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test-dapp/app/sntcontroller.js: -------------------------------------------------------------------------------- 1 | import React, {Component, Fragment} from 'react'; 2 | import BodySNTController from './components/body-sntcontroller'; 3 | import Header from './components/header'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | 7 | class App extends Component { 8 | render(){ 9 | return 10 |
11 | 12 | ; 13 | } 14 | } 15 | 16 | ReactDOM.render(, document.getElementById('app')); 17 | -------------------------------------------------------------------------------- /test-dapp/config/blockchain.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // applies to all environments 3 | default: { 4 | enabled: true, 5 | rpcHost: "localhost", // HTTP-RPC server listening interface (default: "localhost") 6 | rpcPort: 8545, // HTTP-RPC server listening port (default: 8545) 7 | rpcCorsDomain: "auto", // Comma separated list of domains from which to accept cross origin requests (browser enforced) 8 | // When set to "auto", Embark will automatically set the cors to the address of the webserver 9 | wsRPC: true, // Enable the WS-RPC server 10 | wsOrigins: "http://localhost:8000,gas-relayer", // Origins from which to accept websockets requests 11 | // When set to "auto", Embark will automatically set the cors to the address of the webserver 12 | wsHost: "localhost", // WS-RPC server listening interface (default: "localhost") 13 | wsPort: 8546 // WS-RPC server listening port (default: 8546) 14 | }, 15 | 16 | // default environment, merges with the settings in default 17 | // assumed to be the intended environment by `embark run` and `embark blockchain` 18 | development: { 19 | networkType: "custom", // Can be: testnet, rinkeby, livenet or custom, in which case, it will use the specified networkId 20 | networkId: "1337", // Network id used when networkType is custom 21 | isDev: true, // Uses and ephemeral proof-of-authority network with a pre-funded developer account, mining enabled 22 | datadir: ".embark/development/datadir", // Data directory for the databases and keystore 23 | mineWhenNeeded: true, // Uses our custom script (if isDev is false) to mine only when needed 24 | nodiscover: true, // Disables the peer discovery mechanism (manual peer addition) 25 | maxpeers: 0, // Maximum number of network peers (network disabled if set to 0) (default: 25) 26 | proxy: true, // Proxy is used to present meaningful information about transactions 27 | targetGasLimit: 8000000, // Target gas limit sets the artificial target gas floor for the blocks to mine 28 | simulatorMnemonic: "example exile argue silk regular smile grass bomb merge arm assist farm", // Mnemonic used by the simulator to generate a wallet 29 | simulatorBlocktime: 0, // Specify blockTime in seconds for automatic mining. Default is 0 and no auto-mining. 30 | account: { 31 | // numAccounts: 3, // When specified, creates accounts for use in the dapp. This option only works in the development environment, and can be used as a quick start option that bypasses the need for MetaMask in development. These accounts are unlocked and funded with the below settings. 32 | // password: "config/development/password", // Password for the created accounts (as specified in the `numAccounts` setting). If `mineWhenNeeded` is enabled (and isDev is not), this password is used to create a development account controlled by the node. 33 | // balance: "5 ether" // Balance to be given to the created accounts (as specified in the `numAccounts` setting) 34 | } 35 | }, 36 | 37 | // merges with the settings in default 38 | // used with "embark run privatenet" and/or "embark blockchain privatenet" 39 | privatenet: { 40 | networkType: "custom", 41 | networkId: "1337", 42 | isDev: false, 43 | datadir: ".embark/privatenet/datadir", 44 | // -- mineWhenNeeded -- 45 | // This options is only valid when isDev is false. 46 | // Enabling this option uses our custom script to mine only when needed. 47 | // Embark creates a development account for you (using `geth account new`) and funds the account. This account can be used for 48 | // development (and even imported in to MetaMask). To enable correct usage, a password for this account must be specified 49 | // in the `account > password` setting below. 50 | // NOTE: once `mineWhenNeeded` is enabled, you must run an `embark reset` on your dApp before running 51 | // `embark blockchain` or `embark run` for the first time. 52 | mineWhenNeeded: true, 53 | // -- genesisBlock -- 54 | // This option is only valid when mineWhenNeeded is true (which is only valid if isDev is false). 55 | // When enabled, geth uses POW to mine transactions as it would normally, instead of using POA as it does in --dev mode. 56 | // On the first `embark blockchain or embark run` after this option is enabled, geth will create a new chain with a 57 | // genesis block, which can be configured using the `genesisBlock` configuration option below. 58 | genesisBlock: "config/privatenet/genesis.json", // Genesis block to initiate on first creation of a development node 59 | nodiscover: true, 60 | maxpeers: 0, 61 | proxy: true, 62 | account: { 63 | // "address": "", // When specified, uses that address instead of the default one for the network 64 | password: "config/privatenet/password" // Password to unlock the account. If `mineWhenNeeded` is enabled (and isDev is not), this password is used to create a development account controlled by the node. 65 | }, 66 | targetGasLimit: 8000000, 67 | simulatorMnemonic: "example exile argue silk regular smile grass bomb merge arm assist farm", 68 | simulatorBlocktime: 0 69 | }, 70 | 71 | // merges with the settings in default 72 | // used with "embark run testnet" and/or "embark blockchain testnet" 73 | testnet: { 74 | networkType: "testnet", 75 | syncMode: "light", 76 | account: { 77 | password: "config/testnet/password" 78 | } 79 | }, 80 | 81 | // merges with the settings in default 82 | // used with "embark run livenet" and/or "embark blockchain livenet" 83 | livenet: { 84 | networkType: "livenet", 85 | syncMode: "light", 86 | rpcCorsDomain: "http://localhost:8000", 87 | wsOrigins: "http://localhost:8000", 88 | account: { 89 | password: "config/livenet/password" 90 | } 91 | } 92 | 93 | // you can name an environment with specific settings and then specify with 94 | // "embark run custom_name" or "embark blockchain custom_name" 95 | //custom_name: { 96 | //} 97 | }; 98 | -------------------------------------------------------------------------------- /test-dapp/config/communication.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // default applies to all environments 3 | default: { 4 | enabled: true, 5 | provider: "whisper", // Communication provider. Currently, Embark only supports whisper 6 | available_providers: ["whisper"], // Array of available providers 7 | }, 8 | 9 | // default environment, merges with the settings in default 10 | // assumed to be the intended environment by `embark run` 11 | development: { 12 | connection: { 13 | host: "localhost", // Host of the blockchain node 14 | port: 8546, // Port of the blockchain node 15 | type: "ws" // Type of connection (ws or rpc) 16 | } 17 | }, 18 | 19 | // merges with the settings in default 20 | // used with "embark run privatenet" 21 | privatenet: { 22 | }, 23 | 24 | 25 | // merges with the settings in default 26 | // used with "embark run testnet" 27 | testnet: { 28 | }, 29 | 30 | // merges with the settings in default 31 | // used with "embark run livenet" 32 | livenet: { 33 | }, 34 | 35 | // you can name an environment with specific settings and then specify with 36 | // "embark run custom_name" 37 | //custom_name: { 38 | //} 39 | }; 40 | 41 | -------------------------------------------------------------------------------- /test-dapp/config/contracts.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // default applies to all environments 3 | 4 | default: { 5 | // Blockchain node to deploy the contracts 6 | deployment: { 7 | host: "localhost", // Host of the blockchain node 8 | port: 8545, // Port of the blockchain node 9 | type: "rpc" // Type of connection (ws or rpc), 10 | // Accounts to use instead of the default account to populate your wallet 11 | /*,accounts: [ 12 | { 13 | privateKey: "your_private_key", 14 | balance: "5 ether" // You can set the balance of the account in the dev environment 15 | // Balances are in Wei, but you can specify the unit with its name 16 | }, 17 | { 18 | privateKeyFile: "path/to/file", // Either a keystore or a list of keys, separated by , or ; 19 | password: "passwordForTheKeystore" // Needed to decrypt the keystore file 20 | }, 21 | { 22 | mnemonic: "12 word mnemonic", 23 | addressIndex: "0", // Optionnal. The index to start getting the address 24 | numAddresses: "1", // Optionnal. The number of addresses to get 25 | hdpath: "m/44'/60'/0'/0/" // Optionnal. HD derivation path 26 | } 27 | ]*/ 28 | }, 29 | // order of connections the dapp should connect to 30 | dappConnection: [ 31 | "$WEB3", // uses pre existing web3 object if available (e.g in Mist) 32 | "ws://localhost:8546", 33 | "http://localhost:8545" 34 | ], 35 | gas: "auto", 36 | contracts: { 37 | "Identity": {"deploy": false}, 38 | "ERC20Receiver": {"deploy": false}, 39 | "TestToken": {"deploy": false}, 40 | "SafeMath": {"deploy": false}, 41 | "DelayedUpdatableInstance": {"deploy": false}, 42 | "DelayedUpdatableInstanceStorage": {"deploy": false}, 43 | "Factory": {"deploy": false}, 44 | "Instance": {"deploy": false}, 45 | "InstanceStorage": {"deploy": false}, 46 | "MiniMeTokenFactory": {"args":[]}, 47 | "MiniMeToken": {"deploy": false}, 48 | "TestMiniMeToken": {"deploy": false}, 49 | "UpdatedIdentityKernel": {"deploy": false}, 50 | "UpdatableInstance": {"deploy": false}, 51 | "Controlled": {"deploy": false}, 52 | "Owned": {"deploy": false}, 53 | "IdentityKernel": {"deploy": false}, 54 | "STT": { 55 | "instanceOf": "TestMiniMeToken", 56 | "args":["$MiniMeTokenFactory", "0x0", "0x0", "Status Gas Relayer Test Token", 18, "STT", true], 57 | "gasLimit": 4000000 58 | }, 59 | "SNTController": { 60 | "args": ["$accounts[0]", "$STT"], 61 | 62 | }, 63 | "IdentityGasRelay": { 64 | "deploy": true, 65 | "args": [[], [], [], 1, 1, "0x0000000000000000000000000000000000000000"] 66 | }, 67 | "IdentityFactory": { 68 | "args":[], 69 | "gasLimit": 5000000 70 | }, 71 | "TestContract": { 72 | "args": ["$STT"] 73 | } 74 | 75 | 76 | }, 77 | "afterDeploy": [ 78 | "STT.methods.changeController(SNTController.options.address).send()", 79 | "SNTController.methods.enablePublicExecution(TestContract.options.address, true).send()" 80 | ] 81 | 82 | }, 83 | 84 | // default environment, merges with the settings in default 85 | // assumed to be the intended environment by `embark run` 86 | development: { 87 | dappConnection: [ 88 | "ws://localhost:8546", 89 | "http://localhost:8545", 90 | "$WEB3" // uses pre existing web3 object if available (e.g in Mist) 91 | ] 92 | }, 93 | 94 | // merges with the settings in default 95 | // used with "embark run privatenet" 96 | privatenet: { 97 | }, 98 | 99 | // merges with the settings in default 100 | // used with "embark run testnet" 101 | testnet: { 102 | deployment: { 103 | accounts: [ 104 | { 105 | privateKey: '5AC0FF313884BC134DC5B8D92C40360BFB9FC16F8552B60587D27C942564201E', 106 | } 107 | ], 108 | host: "ropsten.infura.io/v3/e62b6ada19b042ee9c6d68746b965ccf", 109 | port: false, 110 | protocol: 'https', // <=== must be specified for infura, can also be http, or ws 111 | type: "rpc" 112 | }, 113 | contracts: { 114 | // Reuse contracts on testnet 115 | "MiniMeTokenFactory": { 116 | "address": "0x6bfa86a71a7dbc68566d5c741f416e3009804279" 117 | }, 118 | "STT": { 119 | "instanceOf": "TestMiniMeToken", 120 | "address": "0x139724523662E54447B70d043b711b2A00c5EF49" 121 | }, 122 | "SNTController": { 123 | "address": "0x1f42B87b375b8ac6C77A8CAd8E78319c18695E75" 124 | }, 125 | "IdentityGasRelay": { 126 | "deploy": false 127 | }, 128 | "IdentityFactory": { 129 | "address": "0xCf3473C2A50F7A94D3D7Dcc2BeBbeE989dAA014E" 130 | }, 131 | "TestContract": { 132 | "address": "0x19fbEE3C3eB0070Df1ab9ba4cB8ab24F0efEBdF8" 133 | } 134 | 135 | // Deploy new contracts to testnet 136 | /* 137 | "MiniMeTokenFactory": { 138 | "address": "0x6bfa86a71a7dbc68566d5c741f416e3009804279" 139 | }, 140 | "STT": { 141 | "instanceOf": "TestMiniMeToken", 142 | "args":["$MiniMeTokenFactory", "0x0", "0x0", "Status Gas Relayer Test Token", 18, "SGasT", true], 143 | "gasLimit": 4000000 144 | }, 145 | "SNTController": { 146 | "args": ["$accounts[0]", "$STT"], 147 | }, 148 | "IdentityGasRelay": { 149 | "deploy": true, 150 | "args": [[], [], [], 1, 1, "0x0000000000000000000000000000000000000000"] 151 | }, 152 | "IdentityFactory": { 153 | "args":[], 154 | "gasLimit": 5000000 155 | }, 156 | "TestContract": { 157 | "args": ["$STT"] 158 | }*/ 159 | }, 160 | "afterDeploy": [ 161 | "STT.methods.changeController(SNTController.options.address).send()", 162 | "SNTController.methods.enablePublicExecution(TestContract.options.address, true).send()" 163 | ] 164 | }, 165 | 166 | // merges with the settings in default 167 | // used with "embark run livenet" 168 | livenet: { 169 | }, 170 | 171 | // you can name an environment with specific settings and then specify with 172 | // "embark run custom_name" or "embark blockchain custom_name" 173 | //custom_name: { 174 | //} 175 | }; 176 | -------------------------------------------------------------------------------- /test-dapp/config/namesystem.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // default applies to all environments 3 | default: { 4 | enabled: false, 5 | available_providers: ["ens"], 6 | provider: "ens" 7 | }, 8 | 9 | // default environment, merges with the settings in default 10 | // assumed to be the intended environment by `embark run` 11 | development: { 12 | register: { 13 | rootDomain: "embark.eth", 14 | subdomains: { 15 | 'status': '0x1a2f3b98e434c02363f3dac3174af93c1d690914' 16 | } 17 | } 18 | }, 19 | 20 | // merges with the settings in default 21 | // used with "embark run privatenet" 22 | privatenet: { 23 | }, 24 | 25 | // merges with the settings in default 26 | // used with "embark run testnet" 27 | testnet: { 28 | }, 29 | 30 | // merges with the settings in default 31 | // used with "embark run livenet" 32 | livenet: { 33 | }, 34 | 35 | // you can name an environment with specific settings and then specify with 36 | // "embark run custom_name" or "embark blockchain custom_name" 37 | //custom_name: { 38 | //} 39 | }; 40 | -------------------------------------------------------------------------------- /test-dapp/config/privatenet/genesis.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "homesteadBlock": 0, 4 | "byzantiumBlock": 0, 5 | "daoForkSupport": true 6 | }, 7 | "nonce": "0x0000000000000042", 8 | "difficulty": "0x0", 9 | "alloc": { 10 | "0x3333333333333333333333333333333333333333": {"balance": "15000000000000000000"} 11 | }, 12 | "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000", 13 | "coinbase": "0x3333333333333333333333333333333333333333", 14 | "timestamp": "0x00", 15 | "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", 16 | "extraData": "0x", 17 | "gasLimit": "0x7a1200" 18 | } 19 | -------------------------------------------------------------------------------- /test-dapp/config/privatenet/password: -------------------------------------------------------------------------------- 1 | dev_password 2 | -------------------------------------------------------------------------------- /test-dapp/config/storage.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // default applies to all environments 3 | default: { 4 | enabled: false, 5 | ipfs_bin: "ipfs", 6 | provider: "ipfs", 7 | available_providers: ["ipfs"], 8 | upload: { 9 | host: "localhost", 10 | port: 5001 11 | }, 12 | dappConnection: [ 13 | { 14 | provider: "ipfs", 15 | host: "localhost", 16 | port: 5001, 17 | getUrl: "http://localhost:8080/ipfs/" 18 | } 19 | ] 20 | // Configuration to start Swarm in the same terminal as `embark run` 21 | /*,account: { 22 | address: "YOUR_ACCOUNT_ADDRESS", // Address of account accessing Swarm 23 | password: "PATH/TO/PASSWORD/FILE" // File containing the password of the account 24 | }, 25 | swarmPath: "PATH/TO/SWARM/EXECUTABLE" // Path to swarm executable (default: swarm)*/ 26 | }, 27 | 28 | // default environment, merges with the settings in default 29 | // assumed to be the intended environment by `embark run` 30 | development: { 31 | enabled: false, 32 | provider: "ipfs", 33 | upload: { 34 | host: "localhost", 35 | port: 5001, 36 | getUrl: "http://localhost:8080/ipfs/" 37 | } 38 | }, 39 | 40 | // merges with the settings in default 41 | // used with "embark run privatenet" 42 | privatenet: { 43 | }, 44 | 45 | // merges with the settings in default 46 | // used with "embark run testnet" 47 | testnet: { 48 | }, 49 | 50 | // merges with the settings in default 51 | // used with "embark run livenet" 52 | livenet: { 53 | }, 54 | 55 | // you can name an environment with specific settings and then specify with 56 | // "embark run custom_name" 57 | //custom_name: { 58 | //} 59 | }; 60 | -------------------------------------------------------------------------------- /test-dapp/config/testnet/password: -------------------------------------------------------------------------------- 1 | test_password 2 | -------------------------------------------------------------------------------- /test-dapp/config/webserver.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | enabled: true, 3 | host: "localhost", 4 | openBrowser: true, 5 | port: 8000 6 | }; 7 | -------------------------------------------------------------------------------- /test-dapp/contracts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/status-im/snt-gas-relay/4d563f44c8bf0d53fe8165183ef705e6bfd55c8e/test-dapp/contracts/.gitkeep -------------------------------------------------------------------------------- /test-dapp/contracts/common/Controlled.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | contract Controlled { 4 | /// @notice The address of the controller is the only address that can call 5 | /// a function with this modifier 6 | modifier onlyController { 7 | require(msg.sender == controller); 8 | _; 9 | } 10 | 11 | address public controller; 12 | 13 | constructor() internal { 14 | controller = msg.sender; 15 | } 16 | 17 | /// @notice Changes the controller of the contract 18 | /// @param _newController The new controller of the contract 19 | function changeController(address _newController) public onlyController { 20 | controller = _newController; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test-dapp/contracts/common/MessageSigned.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.21; 2 | 3 | /** 4 | * @notice Uses ethereum signed messages 5 | */ 6 | contract MessageSigned { 7 | 8 | constructor() internal { 9 | 10 | } 11 | 12 | /** 13 | * @notice recovers address who signed the message 14 | * @param _signHash operation ethereum signed message hash 15 | * @param _messageSignature message `_signHash` signature 16 | */ 17 | function recoverAddress( 18 | bytes32 _signHash, 19 | bytes _messageSignature 20 | ) 21 | pure 22 | internal 23 | returns(address) 24 | { 25 | uint8 v; 26 | bytes32 r; 27 | bytes32 s; 28 | (v,r,s) = signatureSplit(_messageSignature); 29 | return ecrecover( 30 | _signHash, 31 | v, 32 | r, 33 | s 34 | ); 35 | } 36 | 37 | /** 38 | * @notice Hash a hash with `"\x19Ethereum Signed Message:\n32"` 39 | * @param _hash Sign to hash. 40 | * @return signHash Hash to be signed. 41 | */ 42 | function getSignHash( 43 | bytes32 _hash 44 | ) 45 | pure 46 | internal 47 | returns (bytes32 signHash) 48 | { 49 | signHash = keccak256("\x19Ethereum Signed Message:\n32", _hash); 50 | } 51 | 52 | /** 53 | * @dev divides bytes signature into `uint8 v, bytes32 r, bytes32 s` 54 | */ 55 | function signatureSplit(bytes _signature) 56 | pure 57 | internal 58 | returns (uint8 v, bytes32 r, bytes32 s) 59 | { 60 | // The signature format is a compact form of: 61 | // {bytes32 r}{bytes32 s}{uint8 v} 62 | // Compact means, uint8 is not padded to 32 bytes. 63 | assembly { 64 | r := mload(add(_signature, 32)) 65 | s := mload(add(_signature, 64)) 66 | // Here we are loading the last 32 bytes, including 31 bytes 67 | // of 's'. There is no 'mload8' to do this. 68 | // 69 | // 'byte' is not working due to the Solidity parser, so lets 70 | // use the second best option, 'and' 71 | v := and(mload(add(_signature, 65)), 0xff) 72 | } 73 | 74 | require(v == 27 || v == 28); 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /test-dapp/contracts/common/Owned.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | /// @dev `Owned` is a base level contract that assigns an `owner` that can be 4 | /// later changed 5 | contract Owned { 6 | 7 | /// @dev `owner` is the only address that can call a function with this 8 | /// modifier 9 | modifier onlyOwner() { 10 | require(msg.sender == owner); 11 | _; 12 | } 13 | 14 | address public owner; 15 | 16 | /// @notice The Constructor assigns the message sender to be `owner` 17 | constructor() internal { 18 | owner = msg.sender; 19 | } 20 | 21 | address public newOwner; 22 | 23 | /// @notice `owner` can step down and assign some other address to this role 24 | /// @param _newOwner The address of the new owner. 0x0 can be used to create 25 | /// an unowned neutral vault, however that cannot be undone 26 | function changeOwner(address _newOwner) public onlyOwner { 27 | newOwner = _newOwner; 28 | } 29 | 30 | 31 | function acceptOwnership() public { 32 | if (msg.sender == newOwner) { 33 | owner = newOwner; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /test-dapp/contracts/common/SafeMath.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | /** 4 | * Math operations with safety checks 5 | */ 6 | library SafeMath { 7 | function mul(uint a, uint b) internal pure returns (uint) { 8 | uint c = a * b; 9 | assert(a == 0 || c / a == b); 10 | return c; 11 | } 12 | 13 | function div(uint a, uint b) internal pure returns (uint) { 14 | // assert(b > 0); // Solidity automatically throws when dividing by 0 15 | uint c = a / b; 16 | // assert(a == b * c + a % b); // There is no case in which this doesn't hold 17 | return c; 18 | } 19 | 20 | function sub(uint a, uint b) internal pure returns (uint) { 21 | assert(b <= a); 22 | return a - b; 23 | } 24 | 25 | function add(uint a, uint b) internal pure returns (uint) { 26 | uint c = a + b; 27 | assert(c >= a); 28 | return c; 29 | } 30 | 31 | function max64(uint64 a, uint64 b) internal pure returns (uint64) { 32 | return a >= b ? a : b; 33 | } 34 | 35 | function min64(uint64 a, uint64 b) internal pure returns (uint64) { 36 | return a < b ? a : b; 37 | } 38 | 39 | function max256(uint256 a, uint256 b) internal pure returns (uint256) { 40 | return a >= b ? a : b; 41 | } 42 | 43 | function min256(uint256 a, uint256 b) internal pure returns (uint256) { 44 | return a < b ? a : b; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test-dapp/contracts/deploy/DelayedUpdatableInstance.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.21; 2 | 3 | import "./DelayedUpdatableInstanceStorage.sol"; 4 | import "./DelegatedCall.sol"; 5 | 6 | /** 7 | * @title DelayedUpdatableInstance 8 | * @author Ricardo Guilherme Schmidt (Status Research & Development GmbH) 9 | * @dev Contract that can be updated by a call from itself. 10 | */ 11 | contract DelayedUpdatableInstance is DelayedUpdatableInstanceStorage, DelegatedCall { 12 | 13 | event UpdateRequested(address newKernel, uint256 activation); 14 | event UpdateCancelled(); 15 | event UpdateConfirmed(address oldKernel, address newKernel); 16 | 17 | constructor(address _kernel) public { 18 | kernel = _kernel; 19 | } 20 | 21 | /** 22 | * @dev delegatecall everything (but declared functions) to `_target()` 23 | * @notice Verify `kernel()` code to predict behavior 24 | */ 25 | function () 26 | external 27 | delegated 28 | { 29 | //all goes to kernel 30 | } 31 | 32 | function updateRequestUpdatableInstance( 33 | address _kernel 34 | ) 35 | external 36 | { 37 | require(msg.sender == address(this)); 38 | uint activation = block.timestamp + 30 days; 39 | update = Update(_kernel, activation); 40 | emit UpdateRequested(_kernel, activation); 41 | } 42 | 43 | function updateConfirmUpdatableInstance( 44 | address _kernel 45 | ) 46 | external 47 | { 48 | require(msg.sender == address(this)); 49 | Update memory pending = update; 50 | require(pending.kernel == _kernel); 51 | require(pending.activation < block.timestamp); 52 | kernel = pending.kernel; 53 | delete update; 54 | emit UpdateConfirmed(kernel, pending.kernel); 55 | } 56 | 57 | function updateCancelUpdatableInstance() 58 | external 59 | { 60 | require(msg.sender == address(this)); 61 | delete update; 62 | } 63 | 64 | /** 65 | * @dev returns configured kernel 66 | * @return kernel address 67 | */ 68 | function targetDelegatedCall() 69 | internal 70 | view 71 | returns(address) 72 | { 73 | return kernel; 74 | } 75 | 76 | 77 | } -------------------------------------------------------------------------------- /test-dapp/contracts/deploy/DelayedUpdatableInstanceStorage.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.17; 2 | 3 | /** 4 | * @title DelayedUpdatableInstanceStorage 5 | * @author Ricardo Guilherme Schmidt (Status Research & Development GmbH) 6 | * @dev Defines kernel vars that Kernel contract share with DelayedUpdatableInstance. 7 | * Important to avoid overwriting wrong storage pointers is that 8 | * InstanceStorage should be always the first contract at heritance. 9 | */ 10 | contract DelayedUpdatableInstanceStorage { 11 | // protected zone start (InstanceStorage vars) 12 | address public kernel; 13 | Update public update; 14 | 15 | struct Update { 16 | address kernel; 17 | uint256 activation; 18 | } 19 | // protected zone end 20 | 21 | constructor() internal { } 22 | } -------------------------------------------------------------------------------- /test-dapp/contracts/deploy/DelegatedCall.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | 4 | /** 5 | * @title DelegatedCall 6 | * @author Ricardo Guilherme Schmidt (Status Research & Development GmbH) 7 | * @dev Abstract contract that delegates calls by `delegated` modifier to result of `targetDelegatedCall()` 8 | * Important to avoid overwriting wrong storage pointers is that never define storage to this contract 9 | */ 10 | contract DelegatedCall { 11 | 12 | constructor() internal { 13 | 14 | } 15 | /** 16 | * @dev delegates the call of this function 17 | */ 18 | modifier delegated { 19 | //require successfull delegate call to remote `_target()` 20 | require(targetDelegatedCall().delegatecall(msg.data)); 21 | assembly { 22 | let outSize := returndatasize 23 | let outDataPtr := mload(0x40) //load memory 24 | returndatacopy(outDataPtr, 0, outSize) //copy last return into pointer 25 | return(outDataPtr, outSize) 26 | } 27 | assert(false); //should never reach here 28 | _; //never will execute local logic 29 | } 30 | 31 | /** 32 | * @dev defines the address for delegation of calls 33 | */ 34 | function targetDelegatedCall() 35 | internal 36 | view 37 | returns(address); 38 | 39 | } 40 | -------------------------------------------------------------------------------- /test-dapp/contracts/deploy/Factory.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | import "../common/Controlled.sol"; 4 | 5 | contract Factory is Controlled { 6 | 7 | event NewKernel(address newKernel, bytes32 codeHash); 8 | 9 | struct Version { 10 | uint256 blockNumber; 11 | uint256 timestamp; 12 | address kernel; 13 | } 14 | 15 | mapping(bytes32 => uint256) public hashToVersion; 16 | mapping(address => uint256) public versionMap; 17 | 18 | Version[] public versionLog; 19 | uint256 public latestUpdate; 20 | address public latestKernel; 21 | 22 | constructor(address _kernel) 23 | public 24 | { 25 | _setKernel(_kernel); 26 | } 27 | 28 | function setKernel(address _kernel) 29 | external 30 | onlyController 31 | { 32 | _setKernel(_kernel); 33 | } 34 | 35 | function getVersion(uint256 index) 36 | public 37 | view 38 | returns( 39 | uint256 blockNumber, 40 | uint256 timestamp, 41 | address kernel 42 | ) 43 | { 44 | return ( 45 | versionLog[index].blockNumber, 46 | versionLog[index].timestamp, 47 | versionLog[index].kernel 48 | ); 49 | } 50 | 51 | function isKernel(bytes32 _codeHash) public view returns (bool){ 52 | return hashToVersion[_codeHash] > 0; 53 | } 54 | 55 | function isKernel(address _addr) public view returns (bool){ 56 | return versionMap[_addr] > 0; 57 | } 58 | 59 | function getCodeHash(address _addr) 60 | public 61 | view 62 | returns (bytes32 codeHash) 63 | { 64 | bytes memory o_code; 65 | uint size; 66 | assembly { 67 | // retrieve the size of the code, this needs assembly 68 | size := extcodesize(_addr) 69 | } 70 | require (size > 0); 71 | assembly { 72 | // allocate output byte array - this could also be done without assembly 73 | // by using o_code = new bytes(size) 74 | o_code := mload(0x40) 75 | // new "memory end" including padding 76 | mstore(0x40, add(o_code, and(add(add(size, 0x20), 0x1f), not(0x1f)))) 77 | // store length in memory 78 | mstore(o_code, size) 79 | // actually retrieve the code, this needs assembly 80 | extcodecopy(_addr, add(o_code, 0x20), 0, size) 81 | } 82 | codeHash = keccak256(o_code); 83 | } 84 | 85 | function _setKernel(address _kernel) 86 | internal 87 | { 88 | require(_kernel != latestKernel); 89 | bytes32 _codeHash = getCodeHash(_kernel); 90 | versionMap[_kernel] = versionLog.length; 91 | versionLog.push(Version({blockNumber: block.number, timestamp: block.timestamp, kernel: _kernel})); 92 | latestUpdate = block.timestamp; 93 | latestKernel = _kernel; 94 | emit NewKernel(_kernel, _codeHash); 95 | } 96 | } -------------------------------------------------------------------------------- /test-dapp/contracts/deploy/Instance.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.21; 2 | 3 | import "./InstanceStorage.sol"; 4 | import "./DelegatedCall.sol"; 5 | 6 | /** 7 | * @title Instance 8 | * @author Ricardo Guilherme Schmidt (Status Research & Development GmbH) 9 | * @dev Contract that forward everything through delegatecall to defined kernel 10 | */ 11 | contract Instance is InstanceStorage, DelegatedCall { 12 | 13 | constructor(address _kernel) public { 14 | kernel = _kernel; 15 | } 16 | 17 | /** 18 | * @dev delegatecall everything (but declared functions) to `_target()` 19 | * @notice Verify `kernel()` code to predict behavior 20 | */ 21 | function () external delegated { 22 | //all goes to kernel 23 | } 24 | 25 | /** 26 | * @dev returns kernel if kernel that is configured 27 | * @return kernel address 28 | */ 29 | function targetDelegatedCall() 30 | internal 31 | view 32 | returns(address) 33 | { 34 | return kernel; 35 | } 36 | 37 | 38 | } -------------------------------------------------------------------------------- /test-dapp/contracts/deploy/InstanceStorage.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | 4 | /** 5 | * @title InstanceStorage 6 | * @author Ricardo Guilherme Schmidt (Status Research & Development GmbH) 7 | * @dev Defines kernel vars that Kernel contract share with Instance. 8 | * Important to avoid overwriting wrong storage pointers is that 9 | * InstanceStorage should be always the first contract at heritance. 10 | */ 11 | contract InstanceStorage { 12 | // protected zone start (InstanceStorage vars) 13 | address public kernel; 14 | // protected zone end 15 | constructor() internal { } 16 | } -------------------------------------------------------------------------------- /test-dapp/contracts/deploy/UpdatableInstance.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | import "./Instance.sol"; 4 | 5 | 6 | /** 7 | * @title UpdatableInstance 8 | * @author Ricardo Guilherme Schmidt (Status Research & Development GmbH) 9 | * @dev Contract that can be updated by a call from itself. 10 | */ 11 | contract UpdatableInstance is Instance { 12 | 13 | event InstanceUpdated(address oldKernel, address newKernel); 14 | 15 | constructor(address _kernel) 16 | Instance(_kernel) 17 | public 18 | { 19 | 20 | } 21 | 22 | function updateUpdatableInstance(address _kernel) external { 23 | require(msg.sender == address(this)); 24 | emit InstanceUpdated(kernel, _kernel); 25 | kernel = _kernel; 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /test-dapp/contracts/identity/ERC725.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.21; 2 | 3 | contract ERC725 { 4 | 5 | uint256 constant MANAGEMENT_KEY = 1; 6 | uint256 constant ACTION_KEY = 2; 7 | uint256 constant CLAIM_SIGNER_KEY = 3; 8 | uint256 constant ENCRYPTION_KEY = 4; 9 | 10 | event KeyAdded(bytes32 indexed key, uint256 indexed purpose, uint256 indexed keyType); 11 | event KeyRemoved(bytes32 indexed key, uint256 indexed purpose, uint256 indexed keyType); 12 | event ExecutionRequested(uint256 indexed executionId, address indexed to, uint256 indexed value, bytes data); 13 | event Executed(uint256 indexed executionId, address indexed to, uint256 indexed value, bytes data); 14 | event ExecutionFailed(uint256 indexed executionId, address indexed to, uint256 indexed value, bytes data); 15 | event Approved(uint256 indexed executionId, bool approved); 16 | 17 | struct Key { 18 | uint256[] purposes; //e.g., MANAGEMENT_KEY = 1, ACTION_KEY = 2, etc. 19 | uint256 keyType; // e.g. 1 = ECDSA, 2 = RSA, etc. 20 | bytes32 key; 21 | } 22 | 23 | function execute(address _to, uint256 _value, bytes _data) public returns (uint256 executionId); 24 | function approve(uint256 _id, bool _approve) public returns (bool success); 25 | function addKey(bytes32 _key, uint256 _purpose, uint256 _keyType) public returns (bool success); 26 | function removeKey(bytes32 _key, uint256 _purpose) public returns (bool success); 27 | function getKey(bytes32 _key) public view returns(uint256[] purposes, uint256 keyType, bytes32 key); 28 | function getKeyPurpose(bytes32 _key) public view returns(uint256[] purpose); 29 | function getKeysByPurpose(uint256 _purpose) public view returns(bytes32[] keys); 30 | function keyHasPurpose(bytes32 _key, uint256 purpose) public view returns(bool exists); 31 | } -------------------------------------------------------------------------------- /test-dapp/contracts/identity/ERC735.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.21; 2 | 3 | contract ERC735 { 4 | 5 | event ClaimRequested(bytes32 indexed claimRequestId, uint256 indexed topic, uint256 scheme, address indexed issuer, bytes signature, bytes data, string uri); 6 | event ClaimAdded(bytes32 indexed claimId, uint256 indexed topic, uint256 scheme, address indexed issuer, bytes signature, bytes data, string uri); 7 | event ClaimRemoved(bytes32 indexed claimId, uint256 indexed topic, uint256 scheme, address indexed issuer, bytes signature, bytes data, string uri); 8 | event ClaimChanged(bytes32 indexed claimId, uint256 indexed topic, uint256 scheme, address indexed issuer, bytes signature, bytes data, string uri); 9 | 10 | struct Claim { 11 | uint256 topic; 12 | uint256 scheme; 13 | address issuer; // msg.sender 14 | bytes signature; // this.address + topic + data 15 | bytes data; 16 | string uri; 17 | } 18 | 19 | function getClaim(bytes32 _claimId) public view returns(uint256 topic, uint256 scheme, address issuer, bytes signature, bytes data, string uri); 20 | function getClaimIdsByTopic(uint256 _topic) public view returns(bytes32[] claimIds); 21 | function addClaim(uint256 _topic, uint256 _scheme, address _issuer, bytes _signature, bytes _data, string _uri) public returns (bytes32 claimRequestId); 22 | function removeClaim(bytes32 _claimId) public returns (bool success); 23 | } -------------------------------------------------------------------------------- /test-dapp/contracts/identity/IdentityFactory.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "../deploy/Factory.sol"; 4 | import "../deploy/DelayedUpdatableInstance.sol"; 5 | import "./IdentityKernel.sol"; 6 | 7 | 8 | contract IdentityFactory is Factory { 9 | 10 | event IdentityCreated(address instance); 11 | 12 | constructor() 13 | public 14 | Factory(new IdentityKernel()) 15 | { 16 | } 17 | 18 | function createIdentity() 19 | external 20 | returns (address) 21 | { 22 | 23 | bytes32[] memory initKeys = new bytes32[](2); 24 | uint256[] memory initPurposes = new uint256[](2); 25 | uint256[] memory initTypes = new uint256[](2); 26 | initKeys[0] = keccak256(abi.encodePacked(msg.sender)); 27 | initKeys[1] = initKeys[0]; 28 | initPurposes[0] = 1; 29 | initPurposes[1] = 2; 30 | initTypes[0] = 0; 31 | initTypes[1] = 0; 32 | return createIdentity( 33 | initKeys, 34 | initPurposes, 35 | initTypes, 36 | 1, 37 | 1, 38 | 0 39 | ); 40 | } 41 | 42 | function createIdentity( 43 | bytes32[] _keys, 44 | uint256[] _purposes, 45 | uint256[] _types, 46 | uint256 _managerThreshold, 47 | uint256 _actorThreshold, 48 | address _recoveryContract 49 | ) 50 | public 51 | returns (address) 52 | { 53 | IdentityKernel instance = IdentityKernel(new DelayedUpdatableInstance(address(latestKernel))); 54 | 55 | // Saving hash for version in case it does not exist 56 | bytes32 codeHash = getCodeHash(address(instance)); 57 | if(hashToVersion[codeHash] == 0){ 58 | hashToVersion[codeHash] = versionLog.length; 59 | } 60 | 61 | instance.initIdentity(_keys,_purposes,_types,_managerThreshold,_actorThreshold,_recoveryContract); 62 | emit IdentityCreated(address(instance)); 63 | return instance; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /test-dapp/contracts/identity/IdentityKernel.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.17; 2 | 3 | import "../deploy/DelayedUpdatableInstanceStorage.sol"; 4 | import "./IdentityGasRelay.sol"; 5 | 6 | contract IdentityKernel is DelayedUpdatableInstanceStorage, IdentityGasRelay { 7 | 8 | constructor() 9 | IdentityGasRelay( 10 | new bytes32[](0), 11 | new uint256[](0), 12 | new uint256[](0), 13 | 0, 14 | 0, 15 | 0 16 | ) 17 | public 18 | { 19 | 20 | } 21 | 22 | function initIdentity( 23 | bytes32[] _keys, 24 | uint256[] _purposes, 25 | uint256[] _types, 26 | uint256 _managerThreshold, 27 | uint256 _actorThreshold, 28 | address _recoveryContract 29 | ) external { 30 | _constructIdentity( 31 | _keys, 32 | _purposes, 33 | _types, 34 | _managerThreshold, 35 | _actorThreshold, 36 | _recoveryContract); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test-dapp/contracts/status/SNTController.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.17; 2 | 3 | import "../token/TokenController.sol"; 4 | import "../common/Owned.sol"; 5 | import "../common/MessageSigned.sol"; 6 | import "../token/ERC20Token.sol"; 7 | import "../token/MiniMeToken.sol"; 8 | 9 | /** 10 | * @title SNTController 11 | * @author Ricardo Guilherme Schmidt (Status Research & Development GmbH) 12 | * @notice enables economic abstraction for SNT 13 | */ 14 | contract SNTController is TokenController, Owned, MessageSigned { 15 | 16 | 17 | bytes4 public constant TRANSFER_PREFIX = bytes4( 18 | keccak256("transferSNT(address,uint256,uint256,uint256)") 19 | ); 20 | bytes4 public constant EXECUTE_PREFIX = bytes4( 21 | keccak256("executeGasRelayed(address,bytes,uint256,uint256,uint256)") 22 | ); 23 | 24 | MiniMeToken public snt; 25 | mapping (address => uint256) public signNonce; 26 | mapping (address => bool) public allowPublicExecution; 27 | 28 | event PublicExecutionEnabled(address indexed contractAddress, bool enabled); 29 | event GasRelayedExecution(address indexed msgSigner, bytes32 signedHash, bool executed); 30 | event ClaimedTokens(address indexed _token, address indexed _controller, uint256 _amount); 31 | event ControllerChanged(address indexed _newController); 32 | 33 | /** 34 | * @notice Constructor 35 | * @param _owner Authority address 36 | * @param _snt SNT token 37 | */ 38 | constructor(address _owner, address _snt) public { 39 | owner = _owner; 40 | snt = MiniMeToken(_snt); 41 | } 42 | 43 | /** 44 | * @notice allows externally owned address sign a message to transfer SNT and pay 45 | * @param _to address receving the tokens from message signer 46 | * @param _amount total being transfered 47 | * @param _nonce current signNonce of message signer 48 | * @param _gasPrice price in SNT paid back to msg.sender for each gas unit used 49 | * @param _signature concatenated rsv of message 50 | */ 51 | function transferSNT( 52 | address _to, 53 | uint256 _amount, 54 | uint256 _nonce, 55 | uint256 _gasPrice, 56 | bytes _signature 57 | ) 58 | external 59 | { 60 | uint256 startGas = gasleft(); 61 | bytes32 msgSigned = getSignHash( 62 | getTransferSNTHash( 63 | _to, 64 | _amount, 65 | _nonce, 66 | _gasPrice 67 | ) 68 | ); 69 | 70 | address msgSigner = recoverAddress(msgSigned, _signature); 71 | require(signNonce[msgSigner] == _nonce, "Bad nonce"); 72 | signNonce[msgSigner]++; 73 | if (snt.transferFrom(msgSigner, _to, _amount)) { 74 | require( 75 | snt.transferFrom( 76 | msgSigner, 77 | msg.sender, 78 | (21000 + startGas - gasleft()) * _gasPrice 79 | ), 80 | "Gas transfer fail" 81 | ); 82 | } 83 | } 84 | 85 | /** 86 | * @notice allows externally owned address sign a message to offer SNT for a execution 87 | * @param _allowedContract address of a contracts in execution trust list; 88 | * @param _data msg.data to be sent to `_allowedContract` 89 | * @param _nonce current signNonce of message signer 90 | * @param _gasPrice price in SNT paid back to msg.sender for each gas unit used 91 | * @param _gasMinimal minimal amount of gas needed to complete the execution 92 | * @param _signature concatenated rsv of message 93 | */ 94 | function executeGasRelayed( 95 | address _allowedContract, 96 | bytes _data, 97 | uint256 _nonce, 98 | uint256 _gasPrice, 99 | uint256 _gasMinimal, 100 | bytes _signature 101 | ) 102 | external 103 | { 104 | uint256 startGas = gasleft(); 105 | require(startGas >= _gasMinimal, "Bad gas left"); 106 | require(allowPublicExecution[_allowedContract], "Unallowed call"); 107 | bytes32 msgSigned = getSignHash( 108 | getExecuteGasRelayedHash( 109 | _allowedContract, 110 | _data, 111 | _nonce, 112 | _gasPrice, 113 | _gasMinimal 114 | ) 115 | ); 116 | 117 | address msgSigner = recoverAddress(msgSigned, _signature); 118 | require(signNonce[msgSigner] == _nonce, "Bad nonce"); 119 | signNonce[msgSigner]++; 120 | bool success = _allowedContract.call(_data); 121 | emit GasRelayedExecution(msgSigner, msgSigned, success); 122 | require( 123 | snt.transferFrom( 124 | msgSigner, 125 | msg.sender, 126 | (21000 + startGas-gasleft()) * _gasPrice 127 | ), 128 | "Gas transfer fail" 129 | ); 130 | } 131 | 132 | /** 133 | * @notice The owner of this contract can change the controller of the SNT token 134 | * Please, be sure that the owner is a trusted agent or 0x0 address. 135 | * @param _newController The address of the new controller 136 | */ 137 | function changeController(address _newController) public onlyOwner { 138 | snt.changeController(_newController); 139 | emit ControllerChanged(_newController); 140 | } 141 | 142 | function enablePublicExecution(address _contract, bool _enable) public onlyOwner { 143 | allowPublicExecution[_contract] = _enable; 144 | emit PublicExecutionEnabled(_contract, _enable); 145 | } 146 | 147 | ////////// 148 | // Safety Methods 149 | ////////// 150 | 151 | /** 152 | * @notice This method can be used by the controller to extract mistakenly 153 | * sent tokens to this contract. 154 | * @param _token The address of the token contract that you want to recover 155 | * set to 0 in case you want to extract ether. 156 | */ 157 | function claimTokens(address _token) public onlyOwner { 158 | if (snt.controller() == address(this)) { 159 | snt.claimTokens(_token); 160 | } 161 | if (_token == 0x0) { 162 | address(owner).transfer(address(this).balance); 163 | return; 164 | } 165 | 166 | ERC20Token token = ERC20Token(_token); 167 | uint256 balance = token.balanceOf(this); 168 | token.transfer(owner, balance); 169 | emit ClaimedTokens(_token, owner, balance); 170 | } 171 | 172 | 173 | ////////// 174 | // MiniMe Controller Interface functions 175 | ////////// 176 | 177 | // In between the offering and the network. Default settings for allowing token transfers. 178 | function proxyPayment(address) public payable returns (bool) { 179 | return false; 180 | } 181 | 182 | function onTransfer(address, address, uint256) public returns (bool) { 183 | return true; 184 | } 185 | 186 | function onApprove(address, address, uint256) public returns (bool) { 187 | return true; 188 | } 189 | 190 | /** 191 | * @notice get execution hash 192 | * @param _allowedContract address of a contracts in execution trust list; 193 | * @param _data msg.data to be sent to `_allowedContract` 194 | * @param _nonce current signNonce of message signer 195 | * @param _gasPrice price in SNT paid back to msg.sender for each gas unit used 196 | * @param _gasMinimal minimal amount of gas needed to complete the execution 197 | */ 198 | function getExecuteGasRelayedHash( 199 | address _allowedContract, 200 | bytes _data, 201 | uint256 _nonce, 202 | uint256 _gasPrice, 203 | uint256 _gasMinimal 204 | ) 205 | public 206 | view 207 | returns (bytes32 execHash) 208 | { 209 | execHash = keccak256( 210 | abi.encodePacked( 211 | address(this), 212 | EXECUTE_PREFIX, 213 | _allowedContract, 214 | keccak256(_data), 215 | _nonce, 216 | _gasPrice, 217 | _gasMinimal 218 | ) 219 | ); 220 | } 221 | 222 | /** 223 | * @notice get transfer hash 224 | * @param _to address receving the tokens from message signer 225 | * @param _amount total being transfered 226 | * @param _nonce current signNonce of message signer 227 | * @param _gasPrice price in SNT paid back to msg.sender for each gas unit used 228 | */ 229 | function getTransferSNTHash( 230 | address _to, 231 | uint256 _amount, 232 | uint256 _nonce, 233 | uint256 _gasPrice 234 | ) 235 | public 236 | view 237 | returns (bytes32 txHash) 238 | { 239 | txHash = keccak256( 240 | abi.encodePacked( 241 | address(this), 242 | TRANSFER_PREFIX, 243 | _to, 244 | _amount, 245 | _nonce, 246 | _gasPrice 247 | ) 248 | ); 249 | } 250 | 251 | } -------------------------------------------------------------------------------- /test-dapp/contracts/tests/TestContract.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | import "../token/ERC20Token.sol"; 3 | 4 | contract TestContract { 5 | 6 | event TestFunctionExecuted(uint val); 7 | 8 | uint public val = 0; 9 | ERC20Token public token; 10 | 11 | constructor(address _token) public { 12 | token = ERC20Token(_token); 13 | } 14 | 15 | function test() public { 16 | val++; 17 | emit TestFunctionExecuted(val); 18 | } 19 | 20 | function sentSTT(uint _amount) public { 21 | require(token.allowance(msg.sender, address(this)) >= _amount); 22 | require(token.transferFrom(msg.sender, address(this), _amount)); 23 | } 24 | 25 | /* 26 | Helper function to be used in unit testing due to error in web3 27 | web3.utils.soliditySha3([1, 2, 3]) 28 | Error: Autodetection of array types is not supported. 29 | at _processSoliditySha3Args (node_modules/web3-utils/src/soliditySha3.js:176:15) 30 | */ 31 | function hash( 32 | address identity, 33 | bytes32 _revealedSecret, 34 | address _dest, 35 | bytes _data, 36 | bytes32 _newSecret, 37 | bytes32[] _newFriendsHashes) 38 | public 39 | pure 40 | returns(bytes32) 41 | { 42 | return keccak256(identity, _revealedSecret, _dest, _data, _newSecret, _newFriendsHashes); 43 | 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /test-dapp/contracts/tests/UpdatedIdentityKernel.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | import "../identity/IdentityKernel.sol"; 4 | 5 | 6 | contract UpdatedIdentityKernel is IdentityKernel { 7 | 8 | event TestFunctionExecuted(uint256 minApprovalsByManagementKeys); 9 | 10 | function test() public { 11 | emit TestFunctionExecuted(purposeThreshold[MANAGEMENT_KEY]); 12 | } 13 | } -------------------------------------------------------------------------------- /test-dapp/contracts/token/ApproveAndCallFallBack.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | contract ApproveAndCallFallBack { 4 | function receiveApproval(address from, uint256 _amount, address _token, bytes _data) public; 5 | } 6 | -------------------------------------------------------------------------------- /test-dapp/contracts/token/ERC20Receiver.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | import "./ERC20Token.sol"; 4 | 5 | contract ERC20Receiver { 6 | 7 | event TokenDeposited(address indexed token, address indexed sender, uint256 amount); 8 | event TokenWithdrawn(address indexed token, address indexed sender, uint256 amount); 9 | 10 | mapping (address => mapping(address => uint256)) tokenBalances; 11 | 12 | constructor() public { 13 | 14 | } 15 | 16 | function depositToken( 17 | ERC20Token _token 18 | ) 19 | external 20 | { 21 | _depositToken( 22 | msg.sender, 23 | _token, 24 | _token.allowance( 25 | msg.sender, 26 | address(this) 27 | ) 28 | ); 29 | } 30 | 31 | function withdrawToken( 32 | ERC20Token _token, 33 | uint256 _amount 34 | ) 35 | external 36 | { 37 | _withdrawToken(msg.sender, _token, _amount); 38 | } 39 | 40 | function depositToken( 41 | ERC20Token _token, 42 | uint256 _amount 43 | ) 44 | external 45 | { 46 | require(_token.allowance(msg.sender, address(this)) >= _amount); 47 | _depositToken(msg.sender, _token, _amount); 48 | } 49 | 50 | function tokenBalanceOf( 51 | ERC20Token _token, 52 | address _from 53 | ) 54 | external 55 | view 56 | returns(uint256 fromTokenBalance) 57 | { 58 | return tokenBalances[address(_token)][_from]; 59 | } 60 | 61 | function _depositToken( 62 | address _from, 63 | ERC20Token _token, 64 | uint256 _amount 65 | ) 66 | private 67 | { 68 | require(_amount > 0); 69 | if (_token.transferFrom(_from, address(this), _amount)) { 70 | tokenBalances[address(_token)][_from] += _amount; 71 | emit TokenDeposited(address(_token), _from, _amount); 72 | } 73 | } 74 | 75 | function _withdrawToken( 76 | address _from, 77 | ERC20Token _token, 78 | uint256 _amount 79 | ) 80 | private 81 | { 82 | require(_amount > 0); 83 | require(tokenBalances[address(_token)][_from] >= _amount); 84 | tokenBalances[address(_token)][_from] -= _amount; 85 | require(_token.transfer(_from, _amount)); 86 | emit TokenWithdrawn(address(_token), _from, _amount); 87 | } 88 | 89 | 90 | } -------------------------------------------------------------------------------- /test-dapp/contracts/token/ERC20Token.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | // Abstract contract for the full ERC 20 Token standard 4 | // https://github.com/ethereum/EIPs/issues/20 5 | 6 | interface ERC20Token { 7 | 8 | /** 9 | * @notice send `_value` token to `_to` from `msg.sender` 10 | * @param _to The address of the recipient 11 | * @param _value The amount of token to be transferred 12 | * @return Whether the transfer was successful or not 13 | */ 14 | function transfer(address _to, uint256 _value) external returns (bool success); 15 | 16 | /** 17 | * @notice `msg.sender` approves `_spender` to spend `_value` tokens 18 | * @param _spender The address of the account able to transfer the tokens 19 | * @param _value The amount of tokens to be approved for transfer 20 | * @return Whether the approval was successful or not 21 | */ 22 | function approve(address _spender, uint256 _value) external returns (bool success); 23 | 24 | /** 25 | * @notice send `_value` token to `_to` from `_from` on the condition it is approved by `_from` 26 | * @param _from The address of the sender 27 | * @param _to The address of the recipient 28 | * @param _value The amount of token to be transferred 29 | * @return Whether the transfer was successful or not 30 | */ 31 | function transferFrom(address _from, address _to, uint256 _value) external returns (bool success); 32 | 33 | /** 34 | * @param _owner The address from which the balance will be retrieved 35 | * @return The balance 36 | */ 37 | function balanceOf(address _owner) external view returns (uint256 balance); 38 | 39 | /** 40 | * @param _owner The address of the account owning tokens 41 | * @param _spender The address of the account able to transfer the tokens 42 | * @return Amount of remaining tokens allowed to spent 43 | */ 44 | function allowance(address _owner, address _spender) external view returns (uint256 remaining); 45 | 46 | /** 47 | * @notice return total supply of tokens 48 | */ 49 | function totalSupply() external view returns (uint256 supply); 50 | 51 | event Transfer(address indexed _from, address indexed _to, uint256 _value); 52 | event Approval(address indexed _owner, address indexed _spender, uint256 _value); 53 | } -------------------------------------------------------------------------------- /test-dapp/contracts/token/MiniMeTokenFactory.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.11; 2 | 3 | import "./MiniMeToken.sol"; 4 | 5 | //////////////// 6 | // MiniMeTokenFactory 7 | //////////////// 8 | 9 | /** 10 | * @dev This contract is used to generate clone contracts from a contract. 11 | * In solidity this is the way to create a contract from a contract of the 12 | * same class 13 | */ 14 | contract MiniMeTokenFactory { 15 | 16 | /** 17 | * @notice Update the DApp by creating a new token with new functionalities 18 | * the msg.sender becomes the controller of this clone token 19 | * @param _parentToken Address of the token being cloned 20 | * @param _snapshotBlock Block of the parent token that will 21 | * determine the initial distribution of the clone token 22 | * @param _tokenName Name of the new token 23 | * @param _decimalUnits Number of decimals of the new token 24 | * @param _tokenSymbol Token Symbol for the new token 25 | * @param _transfersEnabled If true, tokens will be able to be transferred 26 | * @return The address of the new token contract 27 | */ 28 | function createCloneToken( 29 | address _parentToken, 30 | uint _snapshotBlock, 31 | string _tokenName, 32 | uint8 _decimalUnits, 33 | string _tokenSymbol, 34 | bool _transfersEnabled 35 | ) public returns (MiniMeToken) { 36 | MiniMeToken newToken = new MiniMeToken( 37 | this, 38 | _parentToken, 39 | _snapshotBlock, 40 | _tokenName, 41 | _decimalUnits, 42 | _tokenSymbol, 43 | _transfersEnabled 44 | ); 45 | 46 | newToken.changeController(msg.sender); 47 | return newToken; 48 | } 49 | } -------------------------------------------------------------------------------- /test-dapp/contracts/token/MiniMeTokenInterface.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.17; 2 | 3 | import "./ERC20Token.sol"; 4 | 5 | 6 | contract MiniMeTokenInterface is ERC20Token { 7 | 8 | /** 9 | * @notice `msg.sender` approves `_spender` to send `_amount` tokens on 10 | * its behalf, and then a function is triggered in the contract that is 11 | * being approved, `_spender`. This allows users to use their tokens to 12 | * interact with contracts in one function call instead of two 13 | * @param _spender The address of the contract able to transfer the tokens 14 | * @param _amount The amount of tokens to be approved for transfer 15 | * @return True if the function call was successful 16 | */ 17 | function approveAndCall( 18 | address _spender, 19 | uint256 _amount, 20 | bytes _extraData 21 | ) 22 | public 23 | returns (bool success); 24 | 25 | /** 26 | * @notice Creates a new clone token with the initial distribution being 27 | * this token at `_snapshotBlock` 28 | * @param _cloneTokenName Name of the clone token 29 | * @param _cloneDecimalUnits Number of decimals of the smallest unit 30 | * @param _cloneTokenSymbol Symbol of the clone token 31 | * @param _snapshotBlock Block when the distribution of the parent token is 32 | * copied to set the initial distribution of the new clone token; 33 | * if the block is zero than the actual block, the current block is used 34 | * @param _transfersEnabled True if transfers are allowed in the clone 35 | * @return The address of the new MiniMeToken Contract 36 | */ 37 | function createCloneToken( 38 | string _cloneTokenName, 39 | uint8 _cloneDecimalUnits, 40 | string _cloneTokenSymbol, 41 | uint _snapshotBlock, 42 | bool _transfersEnabled 43 | ) 44 | public 45 | returns(address); 46 | 47 | /** 48 | * @notice Generates `_amount` tokens that are assigned to `_owner` 49 | * @param _owner The address that will be assigned the new tokens 50 | * @param _amount The quantity of tokens generated 51 | * @return True if the tokens are generated correctly 52 | */ 53 | function generateTokens( 54 | address _owner, 55 | uint _amount 56 | ) 57 | public 58 | returns (bool); 59 | 60 | /** 61 | * @notice Burns `_amount` tokens from `_owner` 62 | * @param _owner The address that will lose the tokens 63 | * @param _amount The quantity of tokens to burn 64 | * @return True if the tokens are burned correctly 65 | */ 66 | function destroyTokens( 67 | address _owner, 68 | uint _amount 69 | ) 70 | public 71 | returns (bool); 72 | 73 | /** 74 | * @notice Enables token holders to transfer their tokens freely if true 75 | * @param _transfersEnabled True if transfers are allowed in the clone 76 | */ 77 | function enableTransfers(bool _transfersEnabled) public; 78 | 79 | /** 80 | * @notice This method can be used by the controller to extract mistakenly 81 | * sent tokens to this contract. 82 | * @param _token The address of the token contract that you want to recover 83 | * set to 0 in case you want to extract ether. 84 | */ 85 | function claimTokens(address _token) public; 86 | 87 | /** 88 | * @dev Queries the balance of `_owner` at a specific `_blockNumber` 89 | * @param _owner The address from which the balance will be retrieved 90 | * @param _blockNumber The block number when the balance is queried 91 | * @return The balance at `_blockNumber` 92 | */ 93 | function balanceOfAt( 94 | address _owner, 95 | uint _blockNumber 96 | ) 97 | public 98 | constant 99 | returns (uint); 100 | 101 | /** 102 | * @notice Total amount of tokens at a specific `_blockNumber`. 103 | * @param _blockNumber The block number when the totalSupply is queried 104 | * @return The total amount of tokens at `_blockNumber` 105 | */ 106 | function totalSupplyAt(uint _blockNumber) public view returns(uint); 107 | 108 | } -------------------------------------------------------------------------------- /test-dapp/contracts/token/StandardToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | import "./ERC20Token.sol"; 4 | 5 | contract StandardToken is ERC20Token { 6 | 7 | uint256 private supply; 8 | mapping (address => uint256) balances; 9 | mapping (address => mapping (address => uint256)) allowed; 10 | 11 | constructor() internal { } 12 | 13 | function transfer( 14 | address _to, 15 | uint256 _value 16 | ) 17 | external 18 | returns (bool success) 19 | { 20 | return transfer(msg.sender, _to, _value); 21 | } 22 | 23 | function approve(address _spender, uint256 _value) 24 | external 25 | returns (bool success) 26 | { 27 | allowed[msg.sender][_spender] = _value; 28 | emit Approval(msg.sender, _spender, _value); 29 | return true; 30 | } 31 | 32 | function transferFrom( 33 | address _from, 34 | address _to, 35 | uint256 _value 36 | ) 37 | external 38 | returns (bool success) 39 | { 40 | if (balances[_from] >= _value && 41 | allowed[_from][msg.sender] >= _value && 42 | _value > 0) { 43 | allowed[_from][msg.sender] -= _value; 44 | return transfer(_from, _to, _value); 45 | } else { 46 | return false; 47 | } 48 | } 49 | 50 | function allowance(address _owner, address _spender) 51 | external 52 | view 53 | returns (uint256 remaining) 54 | { 55 | return allowed[_owner][_spender]; 56 | } 57 | 58 | function balanceOf(address _owner) 59 | external 60 | view 61 | returns (uint256 balance) 62 | { 63 | return balances[_owner]; 64 | } 65 | 66 | function totalSupply() 67 | external 68 | view 69 | returns(uint256 currentTotalSupply) 70 | { 71 | return supply; 72 | } 73 | 74 | function mint( 75 | address _to, 76 | uint256 _amount 77 | ) 78 | internal 79 | { 80 | balances[_to] += _amount; 81 | supply += _amount; 82 | emit Transfer(0x0, _to, _amount); 83 | } 84 | 85 | function transfer( 86 | address _from, 87 | address _to, 88 | uint256 _value 89 | ) 90 | internal 91 | returns (bool success) 92 | { 93 | if (balances[_from] >= _value && _value > 0) { 94 | balances[_from] -= _value; 95 | balances[_to] += _value; 96 | emit Transfer(_from, _to, _value); 97 | return true; 98 | } else { 99 | return false; 100 | } 101 | } 102 | 103 | 104 | } 105 | -------------------------------------------------------------------------------- /test-dapp/contracts/token/TestToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | import "./StandardToken.sol"; 4 | 5 | /** 6 | * @notice ERC20Token for test scripts, can be minted by anyone. 7 | */ 8 | contract TestToken is StandardToken { 9 | 10 | constructor() public { } 11 | 12 | /** 13 | * @notice any caller can mint any `_amount` 14 | * @param _amount how much to be minted 15 | */ 16 | function mint(uint256 _amount) public { 17 | mint(msg.sender, _amount); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test-dapp/contracts/token/TokenController.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.14; 2 | /** 3 | * @dev The token controller contract must implement these functions 4 | */ 5 | contract TokenController { 6 | /** 7 | * @notice Called when `_owner` sends ether to the MiniMe Token contract 8 | * @param _owner The address that sent the ether to create tokens 9 | * @return True if the ether is accepted, false if it throws 10 | */ 11 | function proxyPayment(address _owner) payable returns(bool); 12 | 13 | /** 14 | * @notice Notifies the controller about a token transfer allowing the 15 | * controller to react if desired 16 | * @param _from The origin of the transfer 17 | * @param _to The destination of the transfer 18 | * @param _amount The amount of the transfer 19 | * @return False if the controller does not authorize the transfer 20 | */ 21 | function onTransfer(address _from, address _to, uint _amount) returns(bool); 22 | 23 | /** 24 | * @notice Notifies the controller about an approval allowing the 25 | * controller to react if desired 26 | * @param _owner The address that calls `approve()` 27 | * @param _spender The spender in the `approve()` call 28 | * @param _amount The amount in the `approve()` call 29 | * @return False if the controller does not authorize the approval 30 | */ 31 | function onApprove(address _owner, address _spender, uint _amount) 32 | returns(bool); 33 | } 34 | -------------------------------------------------------------------------------- /test-dapp/embark.json: -------------------------------------------------------------------------------- 1 | { 2 | "contracts": ["contracts/**"], 3 | "app": { 4 | "index.html": "app/index.html", 5 | "js/identity.js": ["app/identity.js"], 6 | "identity.html": "app/identity.html", 7 | "js/sntcontroller.js": ["app/sntcontroller.js"], 8 | "sntcontroller.html": "app/sntcontroller.html", 9 | "css/dapp.css": ["app/css/**"], 10 | "images/": ["app/images/**"] 11 | }, 12 | "buildDir": "dist/", 13 | "config": "config/", 14 | "versions": { 15 | "web3": "1.0.0-beta", 16 | "solc": "0.4.25", 17 | "ipfs-api": "17.2.4" 18 | }, 19 | "plugins": { 20 | }, 21 | "options": { 22 | "solc": { 23 | "optimize": true, 24 | "optimize-runs": 200 25 | } 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /test-dapp/launch-geth-testnet.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | geth --testnet --syncmode=light --port=30303 --ws --wsport=8546 --wsaddr=localhost --wsorigins=http://localhost:8000,embark,gas-relayer --maxpeers=25 --shh --shh.pow=0.002 --wsapi=eth,web3,net,shh 3 | 4 | -------------------------------------------------------------------------------- /test-dapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "snt-gas-relay", 3 | "version": "0.0.1", 4 | "description": "", 5 | "scripts": { 6 | "test": "embark test" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/status-im/contracts.git" 11 | }, 12 | "author": "Status Research & Development GMBH", 13 | "license": "ISC", 14 | "bugs": { 15 | "url": "https://github.com/status-im/contracts/issues" 16 | }, 17 | "homepage": "https://github.com/status-im/contracts#readme", 18 | "devDependencies": { 19 | "@babel/plugin-proposal-class-properties": "^7.0.0", 20 | "@babel/plugin-proposal-decorators": "^7.0.0", 21 | "@babel/plugin-proposal-export-namespace-from": "^7.0.0", 22 | "@babel/plugin-proposal-function-sent": "^7.0.0", 23 | "@babel/plugin-proposal-json-strings": "^7.0.0", 24 | "@babel/plugin-proposal-numeric-separator": "^7.0.0", 25 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 26 | "@babel/plugin-proposal-throw-expressions": "^7.0.0", 27 | "@babel/plugin-syntax-dynamic-import": "^7.0.0", 28 | "@babel/plugin-syntax-import-meta": "^7.0.0" 29 | }, 30 | "dependencies": { 31 | "@material-ui/core": "^3.0.0", 32 | "@material-ui/icons": "^3.0.0", 33 | "@material-ui/lab": "^1.0.0-alpha.12", 34 | "react": "^16.4.2", 35 | "react-blockies": "^1.3.0", 36 | "react-bootstrap": "^0.32.1", 37 | "react-dom": "^16.4.2", 38 | "web3": "^1.0.0-beta.35" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test-dapp/setup_dev_env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | STT=`cat chains.json | awk /STT/,/}/ | grep -Pzo "0x[0-9A-Za-z]+"` 3 | IdentityFactory=`cat chains.json | awk /IdentityFactory/,/}/ | grep -Pzo "0x[0-9A-Za-z]+"` 4 | SNTController=`cat chains.json | awk /SNTController/,/}/ | grep -Pzo "0x[0-9A-Za-z]+"` 5 | 6 | git checkout ../gas-relayer/config/config.js 7 | sed -i 's/%STTAddress%/'"$STT"'/g' ../gas-relayer/config/config.js 8 | sed -i 's/%IdentityFactoryAddress%/'"$IdentityFactory"'/g' ../gas-relayer/config/config.js 9 | sed -i 's/%SNTController%/'"$SNTController"'/g' ../gas-relayer/config/config.js -------------------------------------------------------------------------------- /test-dapp/test/contract_spec.js: -------------------------------------------------------------------------------- 1 | // /*global contract, config, it, assert*/ 2 | /* 3 | const SimpleStorage = require('Embark/contracts/SimpleStorage'); 4 | 5 | let accounts; 6 | 7 | // For documentation please see https://embark.status.im/docs/contracts_testing.html 8 | config({ 9 | //deployment: { 10 | // accounts: [ 11 | // // you can configure custom accounts with a custom balance 12 | // // see https://embark.status.im/docs/contracts_testing.html#Configuring-accounts 13 | // ] 14 | //}, 15 | contracts: { 16 | "SimpleStorage": { 17 | args: [100] 18 | } 19 | } 20 | }, (_err, web3_accounts) => { 21 | accounts = web3_accounts 22 | }); 23 | 24 | contract("SimpleStorage", function () { 25 | this.timeout(0); 26 | 27 | it("should set constructor value", async function () { 28 | let result = await SimpleStorage.methods.storedData().call(); 29 | assert.strictEqual(parseInt(result, 10), 100); 30 | }); 31 | 32 | it("set storage value", async function () { 33 | await SimpleStorage.methods.set(150).send(); 34 | let result = await SimpleStorage.methods.get().call(); 35 | assert.strictEqual(parseInt(result, 10), 150); 36 | }); 37 | 38 | it("should have account with balance", async function() { 39 | let balance = await web3.eth.getBalance(accounts[0]); 40 | assert.ok(parseInt(balance, 10) > 0); 41 | }); 42 | } 43 | */ 44 | -------------------------------------------------------------------------------- /test-dapp/test/erc20token.js: -------------------------------------------------------------------------------- 1 | describe("ERC20Token", async function() { 2 | this.timeout(0); 3 | var ERC20Token; 4 | var accountsArr; 5 | before(function(done) { 6 | this.timeout(0); 7 | var contractsConfig = { 8 | "TestToken": { }, 9 | "ERC20Receiver": { } 10 | }; 11 | EmbarkSpec.deployAll(contractsConfig, async function(accounts) { 12 | ERC20Token = TestToken; 13 | accountsArr = accounts; 14 | for(i=0;i { 25 | accounts = _accounts; 26 | done(); 27 | }); 28 | }); 29 | 30 | 31 | it("Creates a new identity", async () => { 32 | let tx = await IdentityFactory.methods.createIdentity().send({from: accounts[0]}); 33 | 34 | const logEntry = tx.events.IdentityCreated; 35 | 36 | assert(logEntry !== undefined, "IdentityCreated was not triggered"); 37 | 38 | let identity = new web3.eth.Contract(identityJson.abi, logEntry.returnValues.instance, {from: accounts[0]}); 39 | 40 | assert.equal( 41 | await identity.methods.getKeyPurpose(TestUtils.addressToBytes32(accounts[0])).call(), 42 | idUtils.purposes.MANAGEMENT, 43 | identity.address + ".getKeyPurpose("+accounts[0]+") is not MANAGEMENT_KEY") 44 | }); 45 | 46 | 47 | it("Registers a updated identity contract", async() => { 48 | const infoHash = "0xbbbb"; 49 | let receipt = await IdentityFactory.methods.setKernel(UpdatedIdentityKernel.address, infoHash).send({from: accounts[0]}); 50 | 51 | const newKernel = TestUtils.eventValues(receipt, "NewKernel"); 52 | assert(newKernel.infohash, infoHash, "Infohash is not correct"); 53 | }); 54 | 55 | 56 | it("Creates a new identity using latest version", async() => { 57 | let tx = await IdentityFactory.methods.createIdentity().send({from: accounts[0]}); 58 | 59 | assert.notEqual(tx.events.IdentityCreated, undefined, "IdentityCreated wasn't triggered"); 60 | 61 | const contractAddress = tx.events.IdentityCreated.returnValues.instance; 62 | 63 | 64 | let updatedIdentity = new web3.eth.Contract(updatedIdentityKernelJson.abi, contractAddress, {from: accounts[0]}); 65 | 66 | tx = await updatedIdentity.methods.test().send({from: accounts[0]}); 67 | assert.notEqual(tx.events.TestFunctionExecuted, undefined, "TestFunctionExecuted wasn't triggered"); 68 | 69 | // Test if it still executes identity functions as expected 70 | let baseIdentity = new web3.eth.Contract(identityJson.abi, contractAddress, {from: accounts[0]}); 71 | 72 | assert.equal( 73 | await baseIdentity.methods.getKeyPurpose(TestUtils.addressToBytes32(accounts[0])).call(), 74 | 1, 75 | baseIdentity.address + ".getKeyPurpose("+accounts[0]+") is not MANAGEMENT_KEY") 76 | }); 77 | 78 | 79 | it("Updates an identity to the latest version", async() => { 80 | let tx1 = await Identity.methods.execute( 81 | Identity.address, 82 | 0, 83 | idUtils.encode.updateRequestUpdatableInstance(UpdatedIdentityKernel.address)) 84 | .send({from: accounts[0]}); 85 | 86 | assert.notEqual(tx1.events.Executed, undefined, "Executed wasn't triggered"); 87 | 88 | // Updating EVM timestamp to test delay 89 | const plus31days = 60 * 60 * 24 * 31; 90 | 91 | /* 92 | // @rramos - The following code is supposed to increase by 31 days the evm date, 93 | // and mine one block. It is commented because it seems to not be working on web3 1.0. 94 | // Also, sendAsync is supposed to be named send in this version, yet it shows an error 95 | // that it does not support synchronous executions. (?) 96 | // TODO: figure it out! 97 | 98 | web3.currentProvider.send({jsonrpc: "2.0", method: "evm_increaseTime", params: [plus31days], id: 0}, function(){console.log(1);}); 99 | web3.currentProvider.send({jsonrpc: "2.0", method: "evm_mine", params: [], id: 0}, function(){console.log(2);}) 100 | 101 | 102 | 103 | // Confirm update 104 | let tx2 = await Identity.methods.execute( 105 | Identity.address, 106 | 0, 107 | idUtils.encode.updateConfirmUpdatableInstance(UpdatedIdentityKernel.address)) 108 | .send({from: accounts[0]}); 109 | 110 | assert.notEqual(tx2.events.Executed, undefined, "Executed wasn't triggered"); 111 | 112 | 113 | let updatedIdentity1 = new web3.eth.Contract(updatedIdentityKernelJson.abi, Identity.address, {from: accounts[0]}); 114 | 115 | // Calling 116 | let tx3 = await updatedIdentity1.methods.test().send({from: accounts[0]}); 117 | assert.notEqual(tx3.events.TestFunctionExecuted, undefined, "TestFunctionExecuted wasn't triggered"); 118 | assert.equal( 119 | tx3.events.TestFunctionExecuted.returnValues.minApprovalsByManagementKeys.toString(10), 120 | 1, 121 | Identity.address + " wasn't updated to last version"); 122 | 123 | */ 124 | }) 125 | 126 | 127 | }); 128 | 129 | -------------------------------------------------------------------------------- /test-dapp/test/identityExtended.js: -------------------------------------------------------------------------------- 1 | /* 2 | COMMENTED TEMPORARLY WHILE PROJECT IS MIGRATED TO EMBARK - @rramos 3 | 4 | const TestUtils = require("../utils/testUtils.js") 5 | var ethUtils = require('ethereumjs-util') 6 | 7 | const Identity = artifacts.require("./identity/Identity.sol"); 8 | 9 | contract('Identity - Extended Functionality', function(accounts) { 10 | 11 | let identity; 12 | 13 | beforeEach(async () => { 14 | identity = await Identity.new({from: accounts[0]}); 15 | }) 16 | 17 | 18 | describe("Identity()", () => { 19 | 20 | let privateKey = new Buffer('61bffea9215f65164ad18b45aff1436c0c165d0d5dd2087ef61b4232ba6d2c1a', 'hex') 21 | let publicKey = ethUtils.privateToPublic(privateKey); 22 | let pkSha = web3.sha3(publicKey.toString('hex'), {encoding: 'hex'}); 23 | 24 | it("Add ECDSA Management Key", async () => { 25 | 26 | await identity.addKey(pkSha, 2, 1, {from: accounts[0]}) 27 | 28 | await identity.addPublicKey(pkSha, '0x' + publicKey.toString('hex'), {from: accounts[0]}); 29 | 30 | assert.equal( 31 | await identity.getPublicKey(pkSha, {from: accounts[0]}), 32 | '0x' + publicKey.toString('hex'), 33 | identity.address+".getPublicKey("+pkSha+") is not correct"); 34 | 35 | }); 36 | 37 | 38 | it("Test Execution", async () => { 39 | 40 | let to = accounts[1]; 41 | let value = 100; 42 | let data = ''; 43 | 44 | let message = ethUtils.toBuffer("SignedMessage"); 45 | let msgHash = ethUtils.hashPersonalMessage(message); 46 | let sig = ethUtils.ecsign(msgHash, privateKey); 47 | 48 | let r = '0x' + sig.r.toString('hex'); 49 | let s = '0x' + sig.s.toString('hex'); 50 | let v = sig.v; 51 | 52 | 53 | await identity.addKey(pkSha, 2, 1, {from: accounts[0]}) 54 | 55 | await identity.addPublicKey(pkSha, '0x' + publicKey.toString('hex'), {from: accounts[0]}); 56 | 57 | let tx = await identity.executeECDSA(to, value, data, pkSha, '0x' + msgHash.toString('hex'), v, r, s, {from: accounts[0]}); 58 | 59 | // TODO Assert ExecutionRequested Event 60 | console.log(tx) 61 | 62 | }); 63 | }); 64 | 65 | 66 | 67 | }); 68 | */ -------------------------------------------------------------------------------- /test-dapp/test/identityfactory.js: -------------------------------------------------------------------------------- 1 | const IdentityFactory = require('Embark/contracts/IdentityFactory'); 2 | const IdentityGasRelay = require('Embark/contracts/IdentityGasRelay'); 3 | 4 | 5 | var contractsConfig = { 6 | "IdentityFactory": { 7 | 8 | } 9 | }; 10 | 11 | config({ contracts: contractsConfig }); 12 | 13 | contract('IdentityFactory', function () { 14 | 15 | describe('createIdentity()', function () { 16 | it('should create an IdentityGasRelay', async function () { 17 | let result = await IdentityFactory.createIdentity(); 18 | createdIdentity = new web3.eth.Contract( 19 | IdentityGasRelay.options.jsonInterface, 20 | result.events.IdentityCreated.returnValues.instance 21 | ); 22 | 23 | assert(await createdIdentity.methods.nonce().call(), 0); 24 | }); 25 | 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test-dapp/test/testtoken.js: -------------------------------------------------------------------------------- 1 | describe("TestToken", async function() { 2 | this.timeout(0); 3 | var accountsArr; 4 | 5 | before(function(done) { 6 | this.timeout(0); 7 | var contractsConfig = { 8 | "TestToken": { 9 | } 10 | }; 11 | EmbarkSpec.deployAll(contractsConfig, async function(accounts) { 12 | accountsArr = accounts 13 | for(i=0;i { 19 | input = input.replace(/^0x/i,''); 20 | const stringed = "0000000000000000000000000000000000000000000000000000000000000000" + input; 21 | return "0x" + stringed.substring(stringed.length - 64, stringed.length); 22 | } 23 | 24 | const _addKey = function(key, purpose, type){ 25 | if(!/^(0x)?[0-9a-f]{0,64}$/i.test(key)) 26 | throw new Error('Key "'+ key +'" is not a valid hex string'); 27 | 28 | if (Object.values(_purposes).indexOf(purpose) == -1) 29 | throw new Error('Purpose "'+ purpose +'" is not a valid purpose'); 30 | 31 | if (Object.values(_types).indexOf(type) == -1) 32 | throw new Error('Type "'+ type +'" is not a valid type'); 33 | 34 | return web3EthAbi.encodeFunctionCall({ 35 | name: 'addKey', 36 | type: 'function', 37 | inputs: [{ 38 | type: 'bytes32', 39 | name: '_key' 40 | },{ 41 | type: 'uint256', 42 | name: '_purpose' 43 | },{ 44 | type: 'uint256', 45 | name: '_type' 46 | }] 47 | }, [hexToBytes32(key), purpose, type]); 48 | } 49 | 50 | const _removeKey = function(key, purpose){ 51 | if(!/^(0x)?[0-9a-f]{0,64}$/i.test(key)) 52 | throw new Error('Key "'+ key +'" is not a valid hex string'); 53 | 54 | if (Object.values(_purposes).indexOf(purpose) == -1) 55 | throw new Error('Purpose "'+ purpose +'" is not a valid purpose'); 56 | 57 | return web3EthAbi.encodeFunctionCall({ 58 | name: 'removeKey', 59 | type: 'function', 60 | inputs: [{ 61 | type: 'bytes32', 62 | name: '_key' 63 | },{ 64 | type: 'uint256', 65 | name: '_purpose' 66 | }] 67 | }, [hexToBytes32(key), purpose]); 68 | } 69 | 70 | 71 | const _setMinimumApprovalsByKeyType = function(type, minimumApprovals) { 72 | 73 | if (Object.values(_types).indexOf(type) == -1) 74 | throw new Error('Type "'+ type +'" is not a valid type'); 75 | 76 | // TODO valdate minimumApprovals 77 | 78 | return web3EthAbi.encodeFunctionCall({ 79 | name: 'setMinimumApprovalsByKeyType', 80 | type: 'function', 81 | inputs: [{ 82 | type: 'uint256', 83 | name: '_type' 84 | },{ 85 | type: 'uint8', 86 | name: '_minimumApprovals' 87 | }] 88 | }, arguments); 89 | } 90 | 91 | 92 | const _setupRecovery = function(address){ 93 | if(!/^(0x)?[0-9a-f]{0,40}$/i.test(address)) 94 | throw new Error('Address "'+ address +'" is not a valid Ethereum address.'); 95 | 96 | return web3EthAbi.encodeFunctionCall({ 97 | name: 'setupRecovery', 98 | type: 'function', 99 | inputs: [{ 100 | type: 'address', 101 | name: '_recoveryContract' 102 | }] 103 | }, [address]); 104 | } 105 | 106 | const _managerReset = function(address){ 107 | if(!/^(0x)?[0-9a-f]{0,40}$/i.test(address)) 108 | throw new Error('Address "'+ address +'" is not a valid Ethereum address.'); 109 | 110 | return web3EthAbi.encodeFunctionCall({ 111 | name: 'managerReset', 112 | type: 'function', 113 | inputs: [{ 114 | type: 'address', 115 | name: '_newKey' 116 | }] 117 | }, [address]); 118 | } 119 | 120 | const _updateUpdatableInstance = function(address){ 121 | if(!/^(0x)?[0-9a-f]{0,40}$/i.test(address)) 122 | throw new Error('Address "'+ address +'" is not a valid Ethereum address.'); 123 | 124 | return web3EthAbi.encodeFunctionCall({ 125 | name: 'updateUpdatableInstance', 126 | type: 'function', 127 | inputs: [{ 128 | type: 'address', 129 | name: '_kernel' 130 | }] 131 | }, [address]); 132 | } 133 | 134 | const _updateRequestUpdatableInstance = function(address){ 135 | if(!/^(0x)?[0-9a-f]{0,40}$/i.test(address)) 136 | throw new Error('Address "'+ address +'" is not a valid Ethereum address.'); 137 | 138 | return web3EthAbi.encodeFunctionCall({ 139 | name: 'updateRequestUpdatableInstance', 140 | type: 'function', 141 | inputs: [{ 142 | type: 'address', 143 | name: '_kernel' 144 | }] 145 | }, [address]); 146 | } 147 | 148 | const _updateConfirmUpdatableInstance = function(address){ 149 | if(!/^(0x)?[0-9a-f]{0,40}$/i.test(address)) 150 | throw new Error('Address "'+ address +'" is not a valid Ethereum address.'); 151 | 152 | return web3EthAbi.encodeFunctionCall({ 153 | name: 'updateConfirmUpdatableInstance', 154 | type: 'function', 155 | inputs: [{ 156 | type: 'address', 157 | name: '_kernel' 158 | }] 159 | }, [address]); 160 | } 161 | 162 | const _updateCancelUpdatableInstance = function(address){ 163 | if(!/^(0x)?[0-9a-f]{0,40}$/i.test(address)) 164 | throw new Error('Address "'+ address +'" is not a valid Ethereum address.'); 165 | 166 | return web3EthAbi.encodeFunctionCall({ 167 | name: 'updateCancelUpdatableInstance', 168 | type: 'function', 169 | inputs: [] 170 | }, []); 171 | } 172 | 173 | 174 | 175 | 176 | module.exports = { 177 | types: _types, 178 | purposes: _purposes, 179 | encode: { 180 | addKey: _addKey, 181 | removeKey: _removeKey, 182 | setMinimumApprovalsByKeyType: _setMinimumApprovalsByKeyType, 183 | setupRecovery: _setupRecovery, 184 | managerReset: _managerReset, 185 | updateUpdatableInstance: _updateUpdatableInstance, 186 | updateRequestUpdatableInstance: _updateRequestUpdatableInstance, 187 | updateConfirmUpdatableInstance: _updateConfirmUpdatableInstance, 188 | updateCancelUpdatableInstance: _updateCancelUpdatableInstance 189 | } 190 | } -------------------------------------------------------------------------------- /test-dapp/utils/testUtils.js: -------------------------------------------------------------------------------- 1 | 2 | // This has been tested with the real Ethereum network and Testrpc. 3 | // Copied and edited from: https://gist.github.com/xavierlepretre/d5583222fde52ddfbc58b7cfa0d2d0a9 4 | exports.assertReverts = (contractMethodCall, maxGasAvailable) => { 5 | return new Promise((resolve, reject) => { 6 | try { 7 | resolve(contractMethodCall()) 8 | } catch (error) { 9 | reject(error) 10 | } 11 | }) 12 | .then(tx => { 13 | assert.equal(tx.receipt.gasUsed, maxGasAvailable, "tx successful, the max gas available was not consumed") 14 | }) 15 | .catch(error => { 16 | if ((error + "").indexOf("invalid opcode") < 0 && (error + "").indexOf("out of gas") < 0) { 17 | // Checks if the error is from TestRpc. If it is then ignore it. 18 | // Otherwise relay/throw the error produced by the above assertion. 19 | // Note that no error is thrown when using a real Ethereum network AND the assertion above is true. 20 | throw error 21 | } 22 | }) 23 | } 24 | 25 | exports.listenForEvent = event => new Promise((resolve, reject) => { 26 | event({}, (error, response) => { 27 | if (!error) { 28 | resolve(response.args) 29 | } else { 30 | reject(error) 31 | } 32 | event.stopWatching() 33 | }) 34 | }); 35 | 36 | exports.eventValues = (receipt, eventName) => { 37 | if(receipt.events[eventName]) 38 | return receipt.events[eventName].returnValues; 39 | } 40 | 41 | exports.addressToBytes32 = (address) => { 42 | const stringed = "0000000000000000000000000000000000000000000000000000000000000000" + address.slice(2); 43 | return "0x" + stringed.substring(stringed.length - 64, stringed.length); 44 | } 45 | 46 | 47 | // OpenZeppelin's expectThrow helper - 48 | // Source: https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/test/helpers/expectThrow.js 49 | exports.expectThrow = async promise => { 50 | try { 51 | await promise; 52 | } catch (error) { 53 | // TODO: Check jump destination to destinguish between a throw 54 | // and an actual invalid jump. 55 | const invalidOpcode = error.message.search('invalid opcode') >= 0; 56 | // TODO: When we contract A calls contract B, and B throws, instead 57 | // of an 'invalid jump', we get an 'out of gas' error. How do 58 | // we distinguish this from an actual out of gas event? (The 59 | // testrpc log actually show an 'invalid jump' event.) 60 | const outOfGas = error.message.search('out of gas') >= 0; 61 | const revert = error.message.search('revert') >= 0; 62 | assert( 63 | invalidOpcode || outOfGas || revert, 64 | 'Expected throw, got \'' + error + '\' instead', 65 | ); 66 | return; 67 | } 68 | assert.fail('Expected throw not received'); 69 | }; 70 | 71 | 72 | 73 | exports.assertJump = (error) => { 74 | assert(error.message.search('revert') > -1, 'Revert should happen'); 75 | } 76 | 77 | 78 | var callbackToResolve = function (resolve, reject) { 79 | return function (error, value) { 80 | if (error) { 81 | reject(error); 82 | } else { 83 | resolve(value); 84 | } 85 | }; 86 | }; 87 | 88 | exports.promisify = (func) => 89 | (...args) => { 90 | return new Promise((resolve, reject) => { 91 | const callback = (err, data) => err ? reject(err) : resolve(data); 92 | func.apply(this, [...args, callback]); 93 | }); 94 | } 95 | 96 | 97 | // This has been tested with the real Ethereum network and Testrpc. 98 | // Copied and edited from: https://gist.github.com/xavierlepretre/d5583222fde52ddfbc58b7cfa0d2d0a9 99 | exports.assertReverts = (contractMethodCall, maxGasAvailable) => { 100 | return new Promise((resolve, reject) => { 101 | try { 102 | resolve(contractMethodCall()) 103 | } catch (error) { 104 | reject(error) 105 | } 106 | }) 107 | .then(tx => { 108 | assert.equal(tx.receipt.gasUsed, maxGasAvailable, "tx successful, the max gas available was not consumed") 109 | }) 110 | .catch(error => { 111 | if ((error + "").indexOf("invalid opcode") < 0 && (error + "").indexOf("out of gas") < 0) { 112 | // Checks if the error is from TestRpc. If it is then ignore it. 113 | // Otherwise relay/throw the error produced by the above assertion. 114 | // Note that no error is thrown when using a real Ethereum network AND the assertion above is true. 115 | throw error 116 | } 117 | }) 118 | } 119 | 120 | exports.listenForEvent = event => new Promise((resolve, reject) => { 121 | event({}, (error, response) => { 122 | if (!error) { 123 | resolve(response.args) 124 | } else { 125 | reject(error) 126 | } 127 | event.stopWatching() 128 | }) 129 | }); 130 | 131 | exports.eventValues = (receipt, eventName) => { 132 | if(receipt.events[eventName]) 133 | return receipt.events[eventName].returnValues; 134 | } 135 | 136 | exports.addressToBytes32 = (address) => { 137 | const stringed = "0000000000000000000000000000000000000000000000000000000000000000" + address.slice(2); 138 | return "0x" + stringed.substring(stringed.length - 64, stringed.length); 139 | } 140 | 141 | 142 | // OpenZeppelin's expectThrow helper - 143 | // Source: https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/test/helpers/expectThrow.js 144 | exports.expectThrow = async promise => { 145 | try { 146 | await promise; 147 | } catch (error) { 148 | // TODO: Check jump destination to destinguish between a throw 149 | // and an actual invalid jump. 150 | const invalidOpcode = error.message.search('invalid opcode') >= 0; 151 | // TODO: When we contract A calls contract B, and B throws, instead 152 | // of an 'invalid jump', we get an 'out of gas' error. How do 153 | // we distinguish this from an actual out of gas event? (The 154 | // testrpc log actually show an 'invalid jump' event.) 155 | const outOfGas = error.message.search('out of gas') >= 0; 156 | const revert = error.message.search('revert') >= 0; 157 | assert( 158 | invalidOpcode || outOfGas || revert, 159 | 'Expected throw, got \'' + error + '\' instead', 160 | ); 161 | return; 162 | } 163 | assert.fail('Expected throw not received'); 164 | }; 165 | 166 | exports.assertJump = (error) => { 167 | assert(error.message.search('revert') > -1, 'Revert should happen'); 168 | } 169 | 170 | var callbackToResolve = function (resolve, reject) { 171 | return function (error, value) { 172 | if (error) { 173 | reject(error); 174 | } else { 175 | resolve(value); 176 | } 177 | }; 178 | }; 179 | 180 | exports.promisify = (func) => 181 | (...args) => { 182 | return new Promise((resolve, reject) => { 183 | const callback = (err, data) => err ? reject(err) : resolve(data); 184 | func.apply(this, [...args, callback]); 185 | }); 186 | } 187 | 188 | exports.zeroAddress = '0x0000000000000000000000000000000000000000'; 189 | exports.zeroBytes32 = "0x0000000000000000000000000000000000000000000000000000000000000000"; 190 | exports.ensureException = function(error) { 191 | assert(isException(error), error.toString()); 192 | }; 193 | 194 | function isException(error) { 195 | let strError = error.toString(); 196 | return strError.includes('invalid opcode') || strError.includes('invalid JUMP') || strError.includes('revert'); 197 | } 198 | --------------------------------------------------------------------------------