├── .gitattributes ├── scratch ├── extensions │ ├── contracts │ ├── regEx.js │ ├── config.js │ ├── QueueManager.js │ ├── BaseBlocks.js │ ├── ether │ │ └── index.js │ ├── BaseContract.js │ ├── tokenBasic │ │ └── index.js │ ├── tokenNFTBasic │ │ └── index.js │ └── tokenDetailedMintableBurnable │ │ └── index.js ├── nginx.conf ├── gui │ └── index.jsx └── vm │ └── extension-manager.js ├── .gitignore ├── docs ├── AddExtension.png ├── MetaMaskFaucet.png ├── BasicTokenBlocks.png ├── ChooseAnExtension.png ├── ContractAddresses.png ├── MetaMaskNetworks.png ├── ContractAddressBlock.png ├── DeployTokenContract.png ├── TokenReporterBlocks.png ├── addExtensionButton.png ├── screenshotTokenBlocks.png ├── ApprovingMetaMaskConnection.png ├── ContractAddressReporterBlock.png ├── ContractAddress_NetworkId_Blocks.png ├── README.md └── TokenTutorial.md ├── scripts ├── initParity.sh ├── startParity.sh ├── initGeth.sh ├── startGeth.sh ├── parityDevConfig.toml ├── startGanache.sh ├── testchain.json ├── genesis.json └── testchainSpec.json ├── jest.config.js ├── .dockerignore ├── contracts ├── TokenNFTBasic.sol ├── TokenBasic.sol ├── TokenDetailedMintableBurnable.sol └── Migration.sol ├── migrations ├── 3_tokenNFTBasic.js ├── 2_tokenBasic.js └── 1_tokenDetailedMintableBurnable.js ├── test └── TokenDetailedMintableBurnable.test.js ├── heroku └── scripts │ └── release.sh ├── truffle-config.js ├── LICENSE ├── package.json ├── Dockerfile ├── .circleci └── config.yml └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /scratch/extensions/contracts: -------------------------------------------------------------------------------- 1 | ../../build/contracts -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | node_modules 4 | chaindata 5 | testchain 6 | bin 7 | logs 8 | -------------------------------------------------------------------------------- /docs/AddExtension.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/eth-scratch3/master/docs/AddExtension.png -------------------------------------------------------------------------------- /docs/MetaMaskFaucet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/eth-scratch3/master/docs/MetaMaskFaucet.png -------------------------------------------------------------------------------- /docs/BasicTokenBlocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/eth-scratch3/master/docs/BasicTokenBlocks.png -------------------------------------------------------------------------------- /docs/ChooseAnExtension.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/eth-scratch3/master/docs/ChooseAnExtension.png -------------------------------------------------------------------------------- /docs/ContractAddresses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/eth-scratch3/master/docs/ContractAddresses.png -------------------------------------------------------------------------------- /docs/MetaMaskNetworks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/eth-scratch3/master/docs/MetaMaskNetworks.png -------------------------------------------------------------------------------- /scripts/initParity.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rm -rf ../testchain/parity 4 | mkdir -p ../testchain/parity/ 5 | -------------------------------------------------------------------------------- /docs/ContractAddressBlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/eth-scratch3/master/docs/ContractAddressBlock.png -------------------------------------------------------------------------------- /docs/DeployTokenContract.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/eth-scratch3/master/docs/DeployTokenContract.png -------------------------------------------------------------------------------- /docs/TokenReporterBlocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/eth-scratch3/master/docs/TokenReporterBlocks.png -------------------------------------------------------------------------------- /docs/addExtensionButton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/eth-scratch3/master/docs/addExtensionButton.png -------------------------------------------------------------------------------- /scripts/startParity.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | parity --chain testchainSpec.json --config parityDevConfig.toml 4 | -------------------------------------------------------------------------------- /docs/screenshotTokenBlocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/eth-scratch3/master/docs/screenshotTokenBlocks.png -------------------------------------------------------------------------------- /docs/ApprovingMetaMaskConnection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/eth-scratch3/master/docs/ApprovingMetaMaskConnection.png -------------------------------------------------------------------------------- /docs/ContractAddressReporterBlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/eth-scratch3/master/docs/ContractAddressReporterBlock.png -------------------------------------------------------------------------------- /docs/ContractAddress_NetworkId_Blocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/eth-scratch3/master/docs/ContractAddress_NetworkId_Blocks.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // jest.config.js 2 | module.exports = { 3 | verbose: true, 4 | testPathIgnorePatterns: ['/test'] 5 | }; -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/*.log 3 | **/*.md 4 | **/Dockerfile* 5 | .* 6 | docker-compose* 7 | LICENSE 8 | logs 9 | bin 10 | chaindata -------------------------------------------------------------------------------- /scratch/nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | server { 3 | listen $PORT; 4 | 5 | location / { 6 | root /usr/share/nginx/html; 7 | index index.html; 8 | } 9 | } -------------------------------------------------------------------------------- /scripts/initGeth.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rm -f ../testchain/geth/chaindata/* 4 | 5 | geth --datadir ../testchain init genesis.json 6 | geth account new --keystore ../testchain/keystore 7 | -------------------------------------------------------------------------------- /contracts/TokenNFTBasic.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.8; 2 | 3 | import "../node_modules/openzeppelin-solidity/contracts/token/ERC721/ERC721.sol"; 4 | 5 | contract TokenNFTBasic is ERC721 { 6 | } 7 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Table of Content 3 | 4 | To generate the table of contents in the main README, use the [markdown-toc](https://github.com/jonschlinkert/markdown-toc) package. 5 | 6 | ```bash 7 | ./node_modules/.bin/markdown-toc README.md 8 | ``` 9 | -------------------------------------------------------------------------------- /scratch/extensions/regEx.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | ethereumAddress: /^0x([A-Fa-f0-9]{40})$/, 4 | bytes: /^0x([A-Fa-f0-9]{1,})$/, 5 | bytes32: /^0x([A-Fa-f0-9]{64})$/, 6 | bytes64: /^0x([A-Fa-f0-9]{128})$/, 7 | transactionHash: /^0x([A-Fa-f0-9]{64})$/ 8 | } 9 | -------------------------------------------------------------------------------- /contracts/TokenBasic.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.8; 2 | 3 | import "../node_modules/openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; 4 | 5 | contract TokenBasic is ERC20 { 6 | constructor(uint256 totalSupply) public { 7 | _mint(msg.sender, totalSupply); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /scratch/extensions/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | infuraProjectId: "6c9fdf9ed3a64dd4b3519ee1609493e4", 3 | privateKeys: ["29993AE0B3357C40C735BB5BC38652972FBC8CA4F5EBF6249815C358737FF711"], 4 | // infuraProjectId: "your Infura Project Id", 5 | // privateKeys: ["your private key"], 6 | } -------------------------------------------------------------------------------- /scripts/startGeth.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Used for testing 4 | geth --datadir ../testchain --ipcdisable --rpc --rpcapi "eth,net,web3,debug" --rpccorsdomain '*' --rpcport 8646 --ws --wsport 8647 --wsaddr "localhost" --wsorigins="*" --port 32323 --mine --minerthreads 1 --etherbase 0 --maxpeers 0 --cache 1024 --targetgaslimit 8000000 --verbosity 3 5 | 6 | -------------------------------------------------------------------------------- /migrations/3_tokenNFTBasic.js: -------------------------------------------------------------------------------- 1 | const Contract = artifacts.require("./TokenNFTBasic.sol") 2 | 3 | module.exports = function(deployer, network, accounts) { 4 | 5 | deployer.then(async () => { 6 | 7 | console.log(`About to deploy a basic non-fungible token contract`) 8 | await deployer.deploy(Contract, {from: accounts[0]}) 9 | const contract = await Contract.deployed() 10 | 11 | console.log(`Token contract address: ${contract.address}`) 12 | }) 13 | .catch((err) => { 14 | console.log(`Error: ${err}`) 15 | }) 16 | }; 17 | -------------------------------------------------------------------------------- /test/TokenDetailedMintableBurnable.test.js: -------------------------------------------------------------------------------- 1 | const TokenContract = artifacts.require('TokenDetailedMintableBurnable') 2 | 3 | let tokenContract 4 | 5 | contract('TokenDetailedMintableBurnable', async accounts => { 6 | 7 | before(async () => { 8 | tokenContract = await TokenContract.deployed() 9 | }) 10 | 11 | it('First card using explicit getter', async () => { 12 | assert.equal(await tokenContract.symbol.call(), 'BMB') 13 | assert.equal(await tokenContract.name.call(), 'Backer Bucks') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /contracts/TokenDetailedMintableBurnable.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.8; 2 | 3 | import "../node_modules/openzeppelin-solidity/contracts/token/ERC20/ERC20Detailed.sol"; 4 | import "../node_modules/openzeppelin-solidity/contracts/token/ERC20/ERC20Mintable.sol"; 5 | import "../node_modules/openzeppelin-solidity/contracts/token/ERC20/ERC20Burnable.sol"; 6 | 7 | contract TokenDetailedMintableBurnable is ERC20Detailed, ERC20Mintable, ERC20Burnable { 8 | constructor (string memory name, string memory symbol, uint8 decimals) public 9 | ERC20Detailed(name, symbol, decimals) 10 | {} 11 | } 12 | -------------------------------------------------------------------------------- /migrations/2_tokenBasic.js: -------------------------------------------------------------------------------- 1 | const Contract = artifacts.require("./TokenBasic.sol") 2 | 3 | module.exports = function(deployer, network, accounts) { 4 | 5 | deployer.then(async () => { 6 | 7 | const totalSupply = 1000 8 | console.log(`About to deploy a basic token contract and mint ${totalSupply} tokens to ${accounts[0]}`) 9 | await deployer.deploy(Contract, totalSupply, {from: accounts[0]}) 10 | const contract = await Contract.deployed() 11 | 12 | console.log(`Basic token contract address: ${contract.address}`) 13 | }) 14 | .catch((err) => { 15 | console.log(`Error: ${err}`) 16 | }) 17 | }; 18 | -------------------------------------------------------------------------------- /contracts/Migration.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.4.21 <0.6.0; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | constructor() public { 8 | owner = msg.sender; 9 | } 10 | 11 | modifier restricted() { 12 | if (msg.sender == owner) _; 13 | } 14 | 15 | function setCompleted(uint completed) public restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) public restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /migrations/1_tokenDetailedMintableBurnable.js: -------------------------------------------------------------------------------- 1 | const Token = artifacts.require("./TokenDetailedMintableBurnable.sol") 2 | 3 | module.exports = function(deployer, network, accounts) { 4 | 5 | deployer.then(async () => { 6 | 7 | console.log(`About to deploy Token contract`) 8 | await deployer.deploy(Token, 'Backer Bucks', 'BMB', 0, {from: accounts[0]}) 9 | const tokenContract = await Token.deployed() 10 | 11 | console.log(`Token contract address: ${tokenContract.address}`) 12 | 13 | const result = await tokenContract.mint(accounts[0], 1200000) 14 | 15 | console.log(`Mint tx hash: ${JSON.stringify(result.tx)}`) 16 | }) 17 | .catch((err) => { 18 | console.log(`Error: ${err}`) 19 | }) 20 | }; 21 | -------------------------------------------------------------------------------- /heroku/scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Adapted from https://gist.github.com/automata/a790205175a37a036feeb9e479322858 4 | 5 | appName=$1 6 | # the second parameter for process type defaults to web 7 | processType=${2:-web} 8 | 9 | imageId=$(docker inspect registry.heroku.com/$appName/$processType --format={{.Id}}) 10 | echo 'Image id for' $appName 'app and' $processType 'process type is' $imageId 11 | 12 | payload='{"updates":[{"type":"'$processType'","docker_image":"'$imageId'"}]}' 13 | 14 | curl -n -X PATCH https://api.heroku.com/apps/$appName/formation \ 15 | -d "$payload" \ 16 | -H "Content-Type: application/json" \ 17 | -H "Accept: application/vnd.heroku+json; version=3.docker-releases" \ 18 | -H "Authorization: Bearer $HEROKU_AUTH_TOKEN" 19 | -------------------------------------------------------------------------------- /scripts/parityDevConfig.toml: -------------------------------------------------------------------------------- 1 | [parity] 2 | mode = "last" 3 | 4 | chain = "Testchain" 5 | base_path = "../testchain/parity" 6 | db_path = "../testchain/parity/chains" 7 | keys_path = "../testchain/keystore" 8 | identity = "Testchain Node" 9 | light = false 10 | 11 | [rpc] 12 | disable = false 13 | port = 8646 14 | interface = "all" 15 | cors = ["all"] 16 | apis = ["private", "personal", "web3", "eth", "net", "parity", "traces", "rpc", "secretstore"] 17 | hosts = ["all"] 18 | 19 | [websockets] 20 | disable = false 21 | port = 8647 22 | interface = "local" 23 | origins = ["none"] 24 | apis = ["web3", "eth", "net", "parity", "traces", "rpc", "secretstore"] 25 | hosts = ["none"] 26 | 27 | [ui] 28 | force = false 29 | disable = false 30 | port = 8181 31 | interface = "127.0.0.1" 32 | path = "../testchain/parity/signer" 33 | 34 | [ipc] 35 | disable = true 36 | 37 | [secretstore] 38 | disable = true 39 | 40 | [ipfs] 41 | enable = false 42 | 43 | [misc] 44 | logging = "own_tx=trace" 45 | log_file = "../testchain/parity/testchain.log" 46 | color = true 47 | -------------------------------------------------------------------------------- /scripts/startGanache.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | pid=$(lsof -i:8646 -t); kill -TERM $pid || kill -KILL $pid 4 | 5 | ../node_modules/.bin/ganache-cli -p 8646 \ 6 | --noVMErrorsOnRPCResponse \ 7 | --account="0x1111111111111111111111111111111111111111111111111111111111111111,300000000000000000000000" \ 8 | --account="0x2222222222222222222222222222222222222222222222222222222222222222,300000000000000000000000" \ 9 | --account="0x3333333333333333333333333333333333333333333333333333333333333333,250000000000000000000000" \ 10 | --account="0x4444444444444444444444444444444444444444444444444444444444444444,250000000000000000000000" \ 11 | --account="0x5555555555555555555555555555555555555555555555555555555555555555,100000000000000000000000" \ 12 | --account="0x6666666666666666666666666666666666666666666666666666666666666666,100000000000000000000000" \ 13 | --account="0x7777777777777777777777777777777777777777777777777777777777777777,100000000000000000000000" \ 14 | --account="0x8888888888888888888888888888888888888888888888888888888888888888,100000000000000000000000" \ 15 | --account="0x9999999999999999999999999999999999999999999999999999999999999999,100000000000000000000000" 16 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | const HDWalletProvider = require("truffle-hdwallet-provider") 2 | const config = require('./scratch/extensions/config.js') 3 | const infuraProjectId = config.infuraProjectId 4 | const privateKeys = config.privateKeys 5 | 6 | module.exports = { 7 | networks: { 8 | develop: { 9 | host: "127.0.0.1", 10 | port: 9545, 11 | network_id: "*" // Match any network id 12 | }, 13 | ganache: { 14 | host: "127.0.0.1", 15 | port: 7545, 16 | network_id: "*" // Match any network id 17 | }, 18 | parity: { 19 | host: "127.0.0.1", 20 | port: 8646, 21 | network_id: "*" // Match any network id 22 | }, 23 | ropsten: { 24 | provider: () => { 25 | return new HDWalletProvider(privateKeys, `https://ropsten.infura.io/v3/${infuraProjectId}`) 26 | }, 27 | network_id: 3, 28 | skipDryRun: true, 29 | }, 30 | }, 31 | compilers: { 32 | solc: { 33 | evmVersion: 'petersburg', 34 | settings: { 35 | optimizer: { 36 | enabled: true, 37 | runs: 200 // Optimize for how many times you intend to run the code 38 | } 39 | }, 40 | version: "0.5.8" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Massachusetts Institute of Technology 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eth-scratch3", 3 | "version": "0.0.1", 4 | "description": "Scratch 3 extensions for common Ethereum contracts", 5 | "main": "index.js", 6 | "scripts": { 7 | "buildBaseImage": "docker build -t registry.heroku.com/eth-scratch3/base:latest --target base .", 8 | "bashBaseImage": "docker run -it registry.heroku.com/eth-scratch3/base:latest bash", 9 | "buildWebImage": "docker build -t registry.heroku.com/eth-scratch3/web:latest --target web .", 10 | "bashWebImage": "docker run -it registry.heroku.com/eth-scratch3/web:latest sh", 11 | "runWebImage": "docker run -p 8601:8601 -e PORT=8601 registry.heroku.com/eth-scratch3/web:latest", 12 | "pushWebImage": "docker push registry.heroku.com/eth-scratch3/web:latest", 13 | "deployLocal": "truffle deploy --reset", 14 | "deployRopsten": "truffle deploy --reset --network ropsten", 15 | "analyze": "npx source-map-explorer 'build/*.js'", 16 | "test": "./node_modules/.bin/jest --forceExit --detectOpenHandles --runInBand" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/naddison36/eth-scratch3.git" 21 | }, 22 | "authors": [ 23 | "Nick Addison" 24 | ], 25 | "license": "BSD-3-Clause", 26 | "dependencies": { 27 | "events": "^3.0.0", 28 | "minilog": "^3.1.0", 29 | "scratch-gui": "git+https://github.com/LLK/scratch-gui.git", 30 | "verror": "^1.10.0" 31 | }, 32 | "devDependencies": { 33 | "jest": "^24.7.1", 34 | "markdown-toc": "^1.2.0", 35 | "openzeppelin-solidity": "^2.2.0", 36 | "solc": "^0.5.8", 37 | "source-map-explorer": "^1.8.0", 38 | "truffle": "^5.0.17", 39 | "truffle-hdwallet-provider": "^1.0.6", 40 | "web3": "^0.20.7" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /scripts/testchain.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "chainId": 100, 4 | "homesteadBlock": 1, 5 | "eip150Block": 2, 6 | "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", 7 | "eip155Block": 3, 8 | "eip158Block": 3, 9 | "byzantiumBlock": 4, 10 | "clique": { 11 | "period": 0, 12 | "epoch": 30000 13 | } 14 | }, 15 | "nonce": "0x0", 16 | "timestamp": "0x5a335de6", 17 | "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000f55583ff8461db9dfbbe90b5f3324f2a290c33560000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 18 | "gasLimit": "0x47b760", 19 | "difficulty": "0x1", 20 | "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", 21 | "coinbase": "0x0000000000000000000000000000000000000000", 22 | "alloc": { 23 | "0000000000000000000000000000000000000000": { 24 | "balance": "0x1" 25 | }, 26 | "0000000000000000000000000000000000000001": { 27 | "balance": "0x1" 28 | }, 29 | "0000000000000000000000000000000000000002": { 30 | "balance": "0x1" 31 | }, 32 | "0000000000000000000000000000000000000003": { 33 | "balance": "0x1" 34 | }, 35 | "0000000000000000000000000000000000000004": { 36 | "balance": "0x1" 37 | }, 38 | "0000000000000000000000000000000000000005": { 39 | "balance": "0x1" 40 | }, 41 | "0000000000000000000000000000000000000006": { 42 | "balance": "0x1" 43 | }, 44 | "0000000000000000000000000000000000000007": { 45 | "balance": "0x1" 46 | }, 47 | "0000000000000000000000000000000000000008": { 48 | "balance": "0x1" 49 | }, 50 | "0000000000000000000000000000000000000009": { 51 | "balance": "0x1" 52 | }, 53 | "000000000000000000000000000000000000000a": { 54 | "balance": "0x1" 55 | }, 56 | "000000000000000000000000000000000000000b": { 57 | "balance": "0x1" 58 | } 59 | }, 60 | "number": "0x0", 61 | "gasUsed": "0x0", 62 | "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" 63 | } -------------------------------------------------------------------------------- /scripts/genesis.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "homesteadBlock": 0, 4 | "eip150Block": 0, 5 | "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", 6 | "eip155Block": 0, 7 | "eip158Block": 0, 8 | "byzantiumBlock": 0 9 | }, 10 | "nonce": "0", 11 | "difficulty": "0x400", 12 | "mixhash": "0x00000000000000000000000000000000000000647572616c65787365646c6578", 13 | "coinbase": "0x0000000000000000000000000000000000000000", 14 | "timestamp": "0x00", 15 | "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000f55583ff8461db9dfbbe90b5f3324f2a290c33560000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 16 | "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", 17 | "gasLimit": "0x7A1200", 18 | "alloc": { 19 | "0x19E7E376E7C213B7E7e7e46cc70A5dD086DAff2A": { 20 | "balance": "10000000000000000000000000" 21 | }, 22 | "0x1563915e194D8CfBA1943570603F7606A3115508": { 23 | "balance": "10000000000000000000000000" 24 | }, 25 | "0x5CbDd86a2FA8Dc4bDdd8a8f69dBa48572EeC07FB": { 26 | "balance": "10000000000000000000000000" 27 | }, 28 | "0x7564105E977516C53bE337314c7E53838967bDaC": { 29 | "balance": "10000000000000000000000000" 30 | }, 31 | "0xe1fAE9b4fAB2F5726677ECfA912d96b0B683e6a9": { 32 | "balance": "10000000000000000000000000" 33 | }, 34 | "0xdb2430B4e9AC14be6554d3942822BE74811A1AF9": { 35 | "balance": "10000000000000000000000000" 36 | }, 37 | "0xAe72A48c1a36bd18Af168541c53037965d26e4A8": { 38 | "balance": "10000000000000000000000000" 39 | }, 40 | "0x7C498eCfc250E0C3E6C331AD8385b0Ad4B7C4667": { 41 | "balance": "10000000000000000000000000" 42 | }, 43 | "0xa58BfF7A68B45AE39fbaa6a5746bfb7A224830B9": { 44 | "balance": "10000000000000000000000000" 45 | }, 46 | "0x3bC920527d0b401951DAE14708A97201E0EBA616": { 47 | "balance": "10000000000000000000000000" 48 | }, 49 | "0x91733F3f85ba39D75C8309829E0B33873Ff82af6": { 50 | "balance": "10000000000000000000000000" 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG NODE_VERSION=10.15.3 2 | 3 | # First build is just the base image that helps work around no layer caching in CircleCi 4 | # is pulled from the Heroku Container Registry so it's layers 5 | FROM node:${NODE_VERSION}-stretch AS base 6 | WORKDIR /scratch 7 | 8 | # Following is used in the CI build 9 | ADD https://github.com/LLK/scratch-vm/archive/develop.tar.gz /scratch/vm.tar.gz 10 | RUN tar xfz vm.tar.gz 11 | ADD https://github.com/LLK/scratch-gui/archive/develop.tar.gz /scratch/gui.tar.gz 12 | RUN tar xfz gui.tar.gz 13 | 14 | # The following is used for faster local testing 15 | # ADD scratch-vm-develop.tar.gz /scratch 16 | # ADD scratch-gui-develop.tar.gz /scratch 17 | 18 | RUN mv /scratch/scratch-vm-develop /scratch/scratch-vm 19 | RUN mv /scratch/scratch-gui-develop /scratch/scratch-gui 20 | 21 | COPY scratch/gui/index.jsx /scratch/scratch-gui/src/lib/libraries/extensions/index.jsx 22 | # Remove other extensions - especially the music extensions with large mp3 files 23 | RUN rm -r /scratch/scratch-vm/src/extensions/* 24 | COPY scratch/extensions /scratch/scratch-vm/src/extensions/custom 25 | COPY build/contracts /scratch/scratch-vm/src/extensions/custom/contracts 26 | COPY scratch/vm/extension-manager.js /scratch/scratch-vm/src/extension-support/extension-manager.js 27 | 28 | WORKDIR /scratch/scratch-gui 29 | 30 | RUN npm set progress=false && \ 31 | npm config set depth 0 && \ 32 | npm install && \ 33 | npm cache clean --force 34 | 35 | WORKDIR /scratch/scratch-vm 36 | 37 | RUN npm set progress=false && \ 38 | npm config set depth 0 && \ 39 | npm install && \ 40 | npm install web3@0.20.3 && \ 41 | npm cache clean --force 42 | 43 | RUN npm link 44 | 45 | WORKDIR /scratch/scratch-gui 46 | 47 | # Link the Scratch GUI to the modified Scratch VM 48 | RUN npm link scratch-vm 49 | 50 | # Build the react app into the /scratch/gui/build folder 51 | RUN npm run build 52 | 53 | # Build the production image 54 | FROM nginx:alpine AS web 55 | COPY --from=base /scratch/scratch-gui/build /usr/share/nginx/html 56 | COPY scratch/nginx.conf /etc/nginx/conf.d/default.conf 57 | CMD sed -i -e 's/$PORT/'"$PORT"'/g' /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;' 58 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | aws-s3: circleci/aws-s3@1.0.9 4 | jobs: 5 | build: 6 | docker: 7 | - image: circleci/node:lts-stretch 8 | steps: 9 | - checkout 10 | - setup_remote_docker: 11 | docker_layer_caching: false 12 | - run: 13 | name: Set the GIT_HASH env var for use in later steps 14 | command: | 15 | echo "Set GIT_HASH to `git log -1 --pretty=%h`" 16 | echo 'export GIT_HASH=`git log -1 --pretty=%h`' >> $BASH_ENV 17 | - run: 18 | name: Install the dependencies 19 | command: | 20 | npm install --production 21 | cd ./node_modules/scratch-gui 22 | npm install 23 | npm install web3@0.20.3 24 | - run: 25 | name: Install the custom Ethereum extensions 26 | command: | 27 | cp ./scratch/gui/index.jsx ./node_modules/scratch-gui/src/lib/libraries/extensions/index.jsx 28 | # remove the other extension examples - especially the music extensions with large mp3 files 29 | rm -r ./node_modules/scratch-gui/node_modules/scratch-vm/src/extensions/* 30 | cp -R ./scratch/extensions ./node_modules/scratch-gui/node_modules/scratch-vm/src/extensions/custom 31 | # the following is a symbolic link to ../../build/contracts which will not work for this build 32 | rm ./node_modules/scratch-gui/node_modules/scratch-vm/src/extensions/custom/contracts 33 | cp -R ./build/contracts ./node_modules/scratch-gui/node_modules/scratch-vm/src/extensions/custom/contracts 34 | cp ./scratch/vm/extension-manager.js ./node_modules/scratch-gui/node_modules/scratch-vm/src/extension-support/extension-manager.js 35 | - run: 36 | name: Build the Scratch React app 37 | command: | 38 | cd ./node_modules/scratch-gui 39 | npx webpack --progress --colors --bail 40 | - run: 41 | name: Install Python dependency for AWS CLI 42 | command: sudo apt-get install python-dev 43 | - aws-s3/sync: 44 | from: ./node_modules/scratch-gui/build 45 | to: 's3://scratch.addisonbrown.com.au/' 46 | aws-access-key-id: AWS_ACCESS_KEY_ID 47 | aws-secret-access-key: AWS_SECRET_ACCESS_KEY 48 | aws-region: AWS_REGION 49 | # arguments: | 50 | # --acl public-read \ 51 | # --cache-control "max-age=86400" 52 | overwrite: true 53 | - run: 54 | name: Clear CloudFront 55 | command: | 56 | echo "AWS_CLOUDFRONT_DISTRIBUTION_ID set to $AWS_CLOUDFRONT_DISTRIBUTION_ID" 57 | aws cloudfront create-invalidation --distribution-id $AWS_CLOUDFRONT_DISTRIBUTION_ID --paths "/*" 58 | -------------------------------------------------------------------------------- /scripts/testchainSpec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Testchain", 3 | "engine": { 4 | "instantSeal": null 5 | }, 6 | "genesis": { 7 | "seal": { 8 | "generic": "0x0" 9 | }, 10 | "difficulty": "0x20000", 11 | "gasLimit": "0x100000000" 12 | }, 13 | "params": { 14 | "networkID" : "0x0", 15 | "chainID": "0xb10cc41f5", 16 | "maximumExtraDataSize": "0x20", 17 | "minGasLimit": "0x1388", 18 | "gasLimitBoundDivisor": "0x400", 19 | "forkBlock": 1, 20 | "eip140Transition": 1, 21 | "eip211Transition": 1, 22 | "eip214Transition": 1, 23 | "eip658Transition": 1 24 | }, 25 | "accounts": { 26 | "0x0000000000000000000000000000000000000001": { "balance": "1", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } }, 27 | "0x0000000000000000000000000000000000000002": { "balance": "1", "builtin": { "name": "sha256", "pricing": { "linear": { "base": 60, "word": 12 } } } }, 28 | "0x0000000000000000000000000000000000000003": { "balance": "1", "builtin": { "name": "ripemd160", "pricing": { "linear": { "base": 600, "word": 120 } } } }, 29 | "0x0000000000000000000000000000000000000004": { "balance": "1", "builtin": { "name": "identity", "pricing": { "linear": { "base": 15, "word": 3 } } } }, 30 | "0x19E7E376E7C213B7E7e7e46cc70A5dD086DAff2A": { "balance": "10000000000000000000000000"}, 31 | "0x1563915e194D8CfBA1943570603F7606A3115508": { "balance": "10000000000000000000000000"}, 32 | "0x5CbDd86a2FA8Dc4bDdd8a8f69dBa48572EeC07FB": { "balance": "10000000000000000000000000"}, 33 | "0x7564105E977516C53bE337314c7E53838967bDaC": { "balance": "10000000000000000000000000"}, 34 | "0xe1fAE9b4fAB2F5726677ECfA912d96b0B683e6a9": { "balance": "10000000000000000000000000"}, 35 | "0xdb2430B4e9AC14be6554d3942822BE74811A1AF9": { "balance": "10000000000000000000000000"}, 36 | "0xAe72A48c1a36bd18Af168541c53037965d26e4A8": { "balance": "10000000000000000000000000"}, 37 | "0x7C498eCfc250E0C3E6C331AD8385b0Ad4B7C4667": { "balance": "10000000000000000000000000"}, 38 | "0xa58BfF7A68B45AE39fbaa6a5746bfb7A224830B9": { "balance": "10000000000000000000000000"}, 39 | "0x3bC920527d0b401951DAE14708A97201E0EBA616": { "balance": "10000000000000000000000000"}, 40 | "0x91733F3f85ba39D75C8309829E0B33873Ff82af6": { "balance": "10000000000000000000000000"}, 41 | "0x62f94E9AC9349BCCC61Bfe66ddAdE6292702EcB6": { "balance": "10000000000000000000000000"}, 42 | "0x0D8e461687b7D06f86EC348E0c270b0F279855F0": { "balance": "10000000000000000000000000"}, 43 | "0xef045a554cbb0016275E90e3002f4D21c6f263e1": { "balance": "10000000000000000000000000"}, 44 | "0xB6E610921b0a0F6F608C0E1f29a845552BC6db2c": { "balance": "10000000000000000000000000"}, 45 | "0x2BD0C9FE079c8FcA0E3352eb3D02839c371E5c41": { "balance": "10000000000000000000000000"}, 46 | "0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF": {"balance": "10000000000000000000000000"} 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /scratch/gui/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {FormattedMessage} from 'react-intl'; 3 | 4 | export default [ 5 | { 6 | name: ( 7 | 12 | ), 13 | extensionId: 'ether', 14 | collaborator: 'Nick Addison', 15 | // iconURL: boostIconURL, 16 | // insetIconURL: boostInsetIconURL, 17 | description: ( 18 | 23 | ), 24 | featured: true, 25 | disabled: false, 26 | bluetoothRequired: false, 27 | internetConnectionRequired: true 28 | }, 29 | { 30 | name: ( 31 | 36 | ), 37 | extensionId: 'tokenBasic', 38 | collaborator: 'Nick Addison', 39 | // iconURL: boostIconURL, 40 | // insetIconURL: boostInsetIconURL, 41 | description: ( 42 | 47 | ), 48 | featured: true, 49 | disabled: false, 50 | bluetoothRequired: false, 51 | internetConnectionRequired: true 52 | }, 53 | { 54 | name: ( 55 | 60 | ), 61 | extensionId: 'tokenDetailedMintableBurnable', 62 | collaborator: 'Nick Addison', 63 | // iconURL: boostIconURL, 64 | // insetIconURL: boostInsetIconURL, 65 | description: ( 66 | 71 | ), 72 | featured: true, 73 | disabled: false, 74 | bluetoothRequired: false, 75 | internetConnectionRequired: true 76 | }, 77 | { 78 | name: ( 79 | 84 | ), 85 | extensionId: 'tokenNFTBasic', 86 | collaborator: 'Nick Addison', 87 | // iconURL: boostIconURL, 88 | // insetIconURL: boostInsetIconURL, 89 | description: ( 90 | 95 | ), 96 | featured: true, 97 | disabled: false, 98 | bluetoothRequired: false, 99 | internetConnectionRequired: true 100 | }, 101 | ]; 102 | -------------------------------------------------------------------------------- /scratch/extensions/QueueManager.js: -------------------------------------------------------------------------------- 1 | /* 2 | Manages a number of different queues which includes a pendingDequeue flag so a separate execution context can watch for new items on the queue. 3 | When the separate context sees a new item it broadcasts it's seen the new item and updates the pendingDequeue flas to true. 4 | This is needed in Scratch as it's not a functional language. It can't pass arguments from one command to another. 5 | */ 6 | 7 | const log = require('minilog')('eth-scratch3:QueueManager') 8 | 9 | class QueueManager { 10 | 11 | // QueueManager is a singleton 12 | constructor() { 13 | 14 | // return single instance of QueueManager already created 15 | if (QueueManager.instance) { 16 | return QueueManager.instance 17 | } 18 | 19 | // QueueManager instance does not exist so will create one 20 | QueueManager.instance = this 21 | 22 | // queues object is of type {items: any[], pendingDequeue: boolean} 23 | this.queues = {} 24 | 25 | // Initialize object 26 | return QueueManager.instance 27 | } 28 | 29 | // initialises a queue 30 | initQueue(queueName) 31 | { 32 | this.queues[queueName] = { 33 | items: [], 34 | pendingDequeue: false 35 | } 36 | } 37 | 38 | // adds an item to a queue 39 | enqueueItem(queueName, item) 40 | { 41 | if (!queueName) { 42 | return this.errorHandler(`Failed to enqueue item as queue name "${queueName}" was invalid.`) 43 | } 44 | if (typeof item === 'undefined') { 45 | return this.errorHandler(`Failed to enqueue item to the "${queueName}" queue as the item was not passed.`) 46 | } 47 | 48 | const description = `enqueue item with id ${item.id} to the "${queueName}" queue` 49 | 50 | if (!this.queues || !this.queues[queueName]) { 51 | return this.errorHandler(`Failed to ${description} as failed to find the "${queueName}" queue.`) 52 | } 53 | 54 | if (!Array.isArray(this.queues[queueName].items)) { 55 | return this.errorHandler(`Failed to ${description} as the queue has not been initialised.`) 56 | } 57 | 58 | this.queues[queueName].items.push(item) 59 | 60 | log.info(`Successful ${description}. Queue length now ${this.queues[queueName].items.length}`) 61 | } 62 | 63 | // removes the first item from a queue 64 | dequeueItem(queueName) 65 | { 66 | if (!queueName) { 67 | return this.errorHandler(`Failed to dequeue items as queue name "${queueName}" was invalid.`) 68 | } 69 | 70 | const description = `dequeue item from the "${queueName}" queue` 71 | 72 | if (!this.queues || !this.queues[queueName]) { 73 | return this.errorHandler(`Failed to ${description} as failed to find the "${queueName}" queue.`) 74 | } 75 | 76 | const queue = this.queues[queueName] 77 | 78 | if (!queue.pendingDequeue) { 79 | return this.errorHandler(`Failed to ${description} as no items are in the queue. Current queue length is ${queue.length}.`) 80 | } 81 | 82 | // remove the oldest item from the queue 83 | const item = queue.items.shift() 84 | queue.pendingDequeue = false 85 | 86 | log.info(`Successful ${description} with id "${item.id}". ${queue.items.length} remain in queue`) 87 | } 88 | 89 | // Reads a property value of the first item in a queue 90 | readQueuedItemProperty(queueName, propertyName) 91 | { 92 | const description = `read property "${propertyName}" from first item on the "${queueName}" queue` 93 | 94 | if (!this.queues || !this.queues[queueName]) { 95 | log.error(`Failed to ${description}. The "${queueName}" queue does not exist.`) 96 | return 97 | } 98 | 99 | const queue = this.queues[queueName] 100 | 101 | if (!queue.pendingDequeue) { 102 | log.error(`Failed to ${description} as no items are on the "${queueName}" queue. Queue length ${queue.items.length}.`) 103 | return 104 | } 105 | 106 | if (!queue.items[0].hasOwnProperty(propertyName)) { 107 | log.error(`Failed to ${description} as the property does not exist on the item.`) 108 | return 109 | } 110 | 111 | log.debug(`Property "${propertyName}" from first of "${queue.items.length}" items in the "${queueName}" queue with id "${queue.items[0].id}" has value "${queue.items[0][propertyName]}"`) 112 | 113 | return queue.items[0][propertyName] 114 | } 115 | 116 | // is there a new item that can be dequeued? 117 | // this is a mutating function. 118 | isQueued(queueName) { 119 | 120 | if (!this.queues || !this.queues[queueName]) { 121 | log.error(`Failed to find the "${queueName}" queue.`) 122 | return false 123 | } 124 | 125 | const queue = this.queues[queueName] 126 | 127 | // are there any items in the queue 128 | // and is the next item not pending dequeue? 129 | if (queue.items.length > 0 && queue.pendingDequeue === false) { 130 | log.info(`New pending item on the "${queueName}" queue with id ${queue.items[0].id}`) 131 | queue.pendingDequeue = true 132 | return true 133 | } 134 | else { 135 | return false 136 | } 137 | } 138 | 139 | // Gets the number of items in a queue 140 | queueLength(queueName) { 141 | return this.queues[queueName].items.length 142 | } 143 | 144 | errorHandler(errorMessage) { 145 | log.error(errorMessage) 146 | return errorMessage 147 | } 148 | } 149 | 150 | module.exports = QueueManager -------------------------------------------------------------------------------- /scratch/extensions/BaseBlocks.js: -------------------------------------------------------------------------------- 1 | const log = require('minilog')('eth-scratch3:BaseBlocks') 2 | 3 | const formatMessage = require('format-message') 4 | const ArgumentType = require('../../extension-support/argument-type') 5 | const BlockType = require('../../extension-support/block-type') 6 | 7 | const regEx = require('./regEx') 8 | const QueueManager = require('./QueueManager') 9 | 10 | class BaseBlocks { 11 | 12 | constructor(runtimeProxy) { 13 | this.runtime = runtimeProxy 14 | 15 | // Request the user to connect MetaMask to the Scratch application 16 | ethereum.enable() 17 | 18 | this.queueManager = new QueueManager() 19 | } 20 | 21 | commonBlocks() { 22 | return [ 23 | { 24 | opcode: 'setContract', 25 | blockType: BlockType.COMMAND, 26 | text: formatMessage({ 27 | id: 'tokenBasic.setContract', 28 | default: 'Set contract [ADDRESS]', 29 | description: 'command text', 30 | }), 31 | arguments: { 32 | ADDRESS: { 33 | type: ArgumentType.STRING, 34 | defaultValue: 'tokenAddress', 35 | }, 36 | }, 37 | }, 38 | { 39 | opcode: 'getContractAddress', 40 | blockType: BlockType.REPORTER, 41 | text: formatMessage({ 42 | id: 'tokenBasic.contractAddress', 43 | default: 'Contract Address', 44 | description: 'command text', 45 | }), 46 | }, 47 | { 48 | opcode: 'getNetworkId', 49 | blockType: BlockType.REPORTER, 50 | text: formatMessage({ 51 | id: 'tokenBasic.networkId', 52 | default: 'Network Identifier', 53 | description: 'command text', 54 | }), 55 | }, 56 | { 57 | opcode: 'isQueuedEvent', 58 | text: formatMessage({ 59 | id: 'tokenBasic.isQueuedEvent', 60 | default: 'When [EVENT_NAME] event', 61 | description: 'command text', 62 | }), 63 | blockType: BlockType.HAT, 64 | arguments: { 65 | EVENT_NAME: { 66 | type: ArgumentType.STRING, 67 | menu: 'events', 68 | defaultValue: this.eventNames[0] 69 | } 70 | } 71 | }, 72 | { 73 | opcode: 'getQueuedEventProperty', 74 | text: formatMessage({ 75 | id: 'tokenBasic.getQueuedEventProperty', 76 | default: 'Property [EVENT_PROPERTY] of [EVENT_NAME] event', 77 | description: 'command text', 78 | }), 79 | blockType: BlockType.REPORTER, 80 | arguments: { 81 | EVENT_NAME: { 82 | type: ArgumentType.STRING, 83 | menu: 'events', 84 | defaultValue: this.eventNames[0] 85 | }, 86 | EVENT_PROPERTY: { 87 | type: ArgumentType.STRING, 88 | menu: 'eventProperties', 89 | defaultValue: 'TO' 90 | } 91 | } 92 | }, 93 | { 94 | opcode: 'dequeueEvent', 95 | text: formatMessage({ 96 | id: 'tokenBasic.dequeueTransfer', 97 | default: 'Clear [EVENT_NAME] event', 98 | description: 'command text', 99 | }), 100 | blockType: BlockType.COMMAND, 101 | arguments: { 102 | EVENT_NAME: { 103 | type: ArgumentType.STRING, 104 | menu: 'events', 105 | defaultValue: this.eventNames[0] 106 | } 107 | } 108 | }, 109 | ] 110 | } 111 | 112 | initEvents(eventNames) 113 | { 114 | this.eventNames = eventNames 115 | 116 | for (let eventName of eventNames) { 117 | this.registerEvent(eventName) 118 | } 119 | } 120 | 121 | registerEvent(eventName) 122 | { 123 | log.debug(`Registering event ${eventName}`) 124 | 125 | this.queueManager.initQueue(eventName) 126 | 127 | // Add event listener to add events to the queue 128 | this.contract.eventEmitter.on(eventName, (event) => { 129 | 130 | this.queueManager.enqueueItem(eventName, { 131 | id: event.transactionHash, 132 | // only storing the event args on the queue 133 | ...event.args 134 | }) 135 | }) 136 | } 137 | 138 | eventsMenu() { 139 | // create an array of Block menu items for each event 140 | return this.eventNames.map(eventName => { 141 | return { 142 | text: eventName, 143 | value: eventName, 144 | } 145 | }) 146 | } 147 | 148 | // is there a new event that can be dequeued? 149 | // this is a mutating function 150 | isQueuedEvent(args) { 151 | return this.queueManager.isQueued(args.EVENT_NAME) 152 | } 153 | 154 | // dequeue a pending event 155 | dequeueEvent(args) 156 | { 157 | this.queueManager.dequeueItem(args.EVENT_NAME) 158 | } 159 | 160 | getQueuedEventProperty(args) 161 | { 162 | return this.queueManager.readQueuedItemProperty(args.EVENT_NAME, args.EVENT_PROPERTY.toLowerCase()) 163 | } 164 | 165 | setContract(args) { 166 | const methodName = 'setContractAddress' 167 | if (!args.ADDRESS || !args.ADDRESS.match(regEx.ethereumAddress)) { 168 | return this.errorHandler(`Invalid address "${args.ADDRESS}" for the ${methodName} command. Must be a 40 char hexadecimal with a 0x prefix`) 169 | } 170 | 171 | this.contract.setContract({ 172 | contractAddress: args.ADDRESS, 173 | }) 174 | } 175 | 176 | getContractAddress() 177 | { 178 | if (!this.contract || !this.contract.contractAddress) { 179 | log.error(`Failed to get contract address as it has not been set.`) 180 | return 181 | } 182 | 183 | return this.contract.contractAddress 184 | } 185 | 186 | getNetworkId() 187 | { 188 | if (!ethereum || !ethereum.networkVersion) { 189 | log.error(`Failed to get network identifier. Make sure a browser wallet like MetaMask has been installed.`) 190 | return 191 | } 192 | 193 | return ethereum.networkVersion 194 | } 195 | 196 | errorHandler(errorMessage) { 197 | log.error(errorMessage) 198 | return errorMessage 199 | } 200 | } 201 | module.exports = BaseBlocks 202 | -------------------------------------------------------------------------------- /scratch/extensions/ether/index.js: -------------------------------------------------------------------------------- 1 | const log = require('minilog')('eth-scratch3:Ether') 2 | 3 | const formatMessage = require('format-message') 4 | const ArgumentType = require('../../../extension-support/argument-type') 5 | const BlockType = require('../../../extension-support/block-type') 6 | 7 | const EventEmitter = require('events') 8 | 9 | const regEx = require('../regEx') 10 | 11 | class ContractBlocks { 12 | 13 | constructor(runtimeProxy) { 14 | this.runtime = runtimeProxy 15 | 16 | this.eventEmitter = new EventEmitter() 17 | 18 | // For MetaMask >= 4.14.0 19 | if (window.ethereum) { 20 | this.web3Client = new Web3(ethereum) 21 | } 22 | // For MetaMask < 4.14.0 23 | else if (window.web3) { 24 | this.web3Client = new Web3(web3.currentProvider) 25 | } 26 | // This should only be used for unit testing 27 | else { 28 | log.warn('MetaMask is not installed so will load web3 locally.') 29 | Web3 = require('web3') 30 | this.web3Client = new Web3(options.provider) 31 | } 32 | } 33 | 34 | // of selected address of the browser wallet is not available, then 35 | // request access to the browser wallet from the user 36 | enableEthereum() { 37 | 38 | if (ethereum.selectedAddress) { 39 | return Promise.resolve(ethereum.selectedAddress) 40 | } 41 | 42 | log.debug(`About to request access to the browser wallet from the user`) 43 | 44 | return new Promise((resolve, reject) => { 45 | 46 | ethereum.enable() 47 | .then(() => { 48 | log.info(`Enabled browser wallet with selected address ${ethereum.selectedAddress}`) 49 | resolve(ethereum.selectedAddress) 50 | }) 51 | .catch((err) => { 52 | const error = new VError(err, `Failed to enable the browser wallet.`) 53 | log.error(error.message) 54 | reject(error) 55 | }) 56 | }) 57 | } 58 | 59 | getInfo() { 60 | 61 | return { 62 | id: 'ether', 63 | name: formatMessage({ 64 | id: 'ether.categoryName', 65 | default: 'Ether', 66 | description: 'extension name', 67 | }), 68 | blocks: [ 69 | { 70 | opcode: 'transfer', 71 | blockType: BlockType.COMMAND, 72 | text: formatMessage({ 73 | id: 'ether.transfer', 74 | default: 'Transfer [VALUE] ether to [TO]', 75 | description: 'command text', 76 | }), 77 | arguments: { 78 | TO: { 79 | type: ArgumentType.STRING, 80 | defaultValue: 'toAddress', 81 | }, 82 | VALUE: { 83 | type: ArgumentType.NUMBER, 84 | defaultValue: 0, 85 | }, 86 | }, 87 | }, 88 | { 89 | opcode: 'balanceOf', 90 | blockType: BlockType.REPORTER, 91 | text: formatMessage({ 92 | id: 'ether.balanceOf', 93 | default: 'Ether balance of [ADDRESS]', 94 | description: 'command text', 95 | }), 96 | arguments: { 97 | ADDRESS: { 98 | type: ArgumentType.STRING, 99 | defaultValue: 'ownerAddress', 100 | }, 101 | }, 102 | }, 103 | ], 104 | } 105 | } 106 | 107 | transfer(args) 108 | { 109 | const methodName = 'transfer' 110 | 111 | if (!args.TO || !args.TO.match(regEx.ethereumAddress)) { 112 | log.error(`Invalid TO address "${args.TO}" for the ${methodName} command. Must be a 40 char hexadecimal with a 0x prefix`) 113 | return 114 | } 115 | if (!args.VALUE) { 116 | log.error(`Invalid value "${args.VALUE}". Must be a positive integer.`) 117 | return 118 | } 119 | 120 | const weiAmount = web3.toWei(args.VALUE, "ether") 121 | 122 | return new Promise((resolve, reject) => { 123 | 124 | let description = `send ${args.VALUE} ether (${weiAmount} wei) to address ${args.TO}` 125 | 126 | log.debug(`About to ${description}`) 127 | 128 | this.enableEthereum() 129 | .then(account => { 130 | 131 | description = `${description} from sender address ${ethereum.selectedAddress}` 132 | 133 | try { 134 | 135 | this.web3Client.eth.sendTransaction({ 136 | to: args.TO, 137 | value: weiAmount, 138 | }, 139 | (err, transactionHash) => 140 | { 141 | if(err) { 142 | const error = new VError(err, `Failed to ${description}.`) 143 | log.error(error.stack) 144 | return reject(error) 145 | } 146 | 147 | log.info(`Got transaction hash ${transactionHash} for ${description}`) 148 | 149 | resolve(transactionHash) 150 | }) 151 | } 152 | catch (err) { 153 | const error = new VError(err, `Failed to send transaction to ${description}.`) 154 | log.error(error.stack) 155 | return reject(error) 156 | } 157 | }) 158 | .catch(err => { 159 | const error = new VError(err, `Failed to enable browser wallet before ${description}`) 160 | log.error(error.stack) 161 | reject(error) 162 | }) 163 | }) 164 | } 165 | 166 | balanceOf(args) 167 | { 168 | if (!args.ADDRESS || !args.ADDRESS.match(regEx.ethereumAddress)) { 169 | log.error(`Invalid owner address "${args.ADDRESS}" for the balanceOf reporter. Must be a 40 char hexadecimal with a 0x prefix`) 170 | return 171 | } 172 | 173 | return new Promise((resolve, reject) => { 174 | 175 | const description = `get balance of account with address ${args.ADDRESS}` 176 | 177 | log.debug(`About to ${description}`) 178 | 179 | this.web3Client.eth.getBalance(args.ADDRESS, 180 | (err, weiAmount) => 181 | { 182 | if(err) { 183 | const error = new VError(err, `Failed to ${description}.`) 184 | log.error(error.stack) 185 | return reject(error) 186 | } 187 | 188 | const ethBalance = web3.fromWei(weiAmount, 'ether'); 189 | 190 | log.info(`Got ${ethBalance} ether (${weiAmount} wei) from ${description}`) 191 | 192 | resolve(ethBalance) 193 | }) 194 | }) 195 | } 196 | } 197 | module.exports = ContractBlocks 198 | -------------------------------------------------------------------------------- /scratch/extensions/BaseContract.js: -------------------------------------------------------------------------------- 1 | const log = require('minilog')('eth-scratch3:BaseContract') 2 | const VError = require('verror') 3 | const EventEmitter = require('events') 4 | 5 | class BaseContract { 6 | 7 | constructor(Contract, options = {}) { 8 | 9 | this.Contract = Contract 10 | this.eventEmitter = new EventEmitter() 11 | 12 | // For MetaMask >= 4.14.0 13 | if (window.ethereum) { 14 | this.web3Client = new Web3(ethereum) 15 | } 16 | // For MetaMask < 4.14.0 17 | else if (window.web3) { 18 | this.web3Client = new Web3(web3.currentProvider) 19 | } 20 | // This should only be used for unit testing 21 | else { 22 | log.warn('MetaMask is not installed so will load web3 locally.') 23 | Web3 = require('web3') 24 | this.web3Client = new Web3(options.provider) 25 | } 26 | 27 | this.setContract(options) 28 | } 29 | 30 | setContract(options) 31 | { 32 | const network = options.network || ethereum.networkVersion || 3 // default to the Ropsten network 33 | this.contractAddress = options.contractAddress || this.Contract.networks[network].address 34 | 35 | this.contract = this.web3Client.eth.contract(this.Contract.abi).at(this.contractAddress) 36 | 37 | log.debug(`Set contract to address ${this.contractAddress} for network ${network}`) 38 | 39 | this.startWatchingEvents() 40 | } 41 | 42 | // return selected address of the browser wallet. If not available, 43 | // request access to the browser wallet from the user 44 | enableEthereum() { 45 | 46 | if (ethereum.selectedAddress) { 47 | return Promise.resolve(ethereum.selectedAddress) 48 | } 49 | 50 | log.debug(`About to request access to the browser wallet from the user`) 51 | 52 | return new Promise((resolve, reject) => { 53 | 54 | ethereum.enable() 55 | .then(() => { 56 | log.info(`Enabled browser wallet with selected address ${ethereum.selectedAddress}`) 57 | resolve(ethereum.selectedAddress) 58 | }) 59 | .catch((err) => { 60 | const error = new VError(err, `Failed to enable the browser wallet.`) 61 | log.error(error.message) 62 | reject(error) 63 | }) 64 | }) 65 | } 66 | 67 | deploy(params, description) 68 | { 69 | return new Promise((resolve, reject) => { 70 | 71 | description = `${description} using sender address ${ethereum.selectedAddress}, network with id ${ethereum.networkVersion} with params ${JSON.stringify(params)}` 72 | 73 | this.enableEthereum() 74 | .then(account => { 75 | 76 | description = `${description} and sender address ${ethereum.selectedAddress}` 77 | 78 | this.contract = web3.eth.contract(this.Contract.abi) 79 | 80 | try { 81 | this.contract.new( 82 | ...params, 83 | {data: this.Contract.bytecode}, 84 | (err, contract) => 85 | { 86 | if(err) { 87 | const error = new VError(err, `Failed to ${description}.`) 88 | log.error(error.stack) 89 | return reject(error) 90 | } 91 | 92 | if(!contract.address) { 93 | log.info(`Got transaction hash ${contract.transactionHash} for ${description}`) 94 | } 95 | else { 96 | this.setContract({ 97 | contractAddress: contract.address, 98 | network: ethereum.networkVersion 99 | }) 100 | 101 | resolve(contract.address) 102 | } 103 | }) 104 | } 105 | catch (err) { 106 | const error = new VError(err, `Failed to send ${description}.`) 107 | log.error(error.stack) 108 | return reject(error) 109 | } 110 | }) 111 | .catch(err => { 112 | const error = new VError(err, `Failed to enable browser wallet before ${description}`) 113 | log.error(error.stack) 114 | reject(error) 115 | }) 116 | }) 117 | } 118 | 119 | send(methodName, args, description) 120 | { 121 | return new Promise((resolve, reject) => { 122 | 123 | description = `${description} using contract with address ${this.contractAddress}, network with id ${ethereum.networkVersion}` 124 | 125 | log.debug(`About to ${description}`) 126 | 127 | this.enableEthereum() 128 | .then(account => { 129 | 130 | description = `${description} and sender address ${ethereum.selectedAddress}` 131 | 132 | try { 133 | this.contract[methodName].sendTransaction(...args, 134 | (err, transactionHash) => 135 | { 136 | if(err) { 137 | const error = new VError(err, `Failed to ${description}.`) 138 | log.error(error.stack) 139 | return reject(error) 140 | } 141 | 142 | log.info(`Got transaction hash ${transactionHash} for ${description}`) 143 | 144 | resolve(transactionHash) 145 | }) 146 | } 147 | catch (err) { 148 | const error = new VError(err, `Failed to send ${description}.`) 149 | log.error(error.stack) 150 | return reject(error) 151 | } 152 | }) 153 | .catch(err => { 154 | const error = new VError(err, `Failed to enable browser wallet before ${description}`) 155 | log.error(error.stack) 156 | reject(error) 157 | }) 158 | }) 159 | } 160 | 161 | call(methodName, args, description) 162 | { 163 | return new Promise((resolve, reject) => { 164 | 165 | const callDescription = `${description} using contract with address ${this.contractAddress}` 166 | 167 | log.debug(`About to ${callDescription} calling method name ${methodName}`) 168 | 169 | try { 170 | this.contract[methodName](...args, (err, returnedValue) => { 171 | if (err) { 172 | const error = new VError(err, `Failed to ${callDescription}.`) 173 | log.error(error.stack) 174 | return reject(error) 175 | } 176 | 177 | log.info(`Got ${returnedValue} from ${callDescription}`) 178 | 179 | resolve(returnedValue) 180 | }) 181 | } 182 | catch (err) { 183 | const error = new VError(err, `Failed to call ${description}.`) 184 | log.error(error.stack) 185 | return reject(error) 186 | } 187 | }) 188 | } 189 | 190 | startWatchingEvents() { 191 | 192 | const eventDescription = `watching for events on contract with address ${this.contractAddress}` 193 | 194 | log.debug(`Start ${eventDescription}`) 195 | 196 | this.contract.allEvents().watch((err, event) => 197 | { 198 | if (err) { 199 | const error = new VError(err, `Failed ${eventDescription}.`) 200 | log.error(error.stack) 201 | return callback(error) 202 | } 203 | 204 | log.info(`Got event ${event.event} from contract ${this.contractAddress}: ${JSON.stringify(event)}`) 205 | 206 | this.eventEmitter.emit(event.event, event) 207 | }) 208 | } 209 | } 210 | 211 | module.exports = BaseContract 212 | -------------------------------------------------------------------------------- /docs/TokenTutorial.md: -------------------------------------------------------------------------------- 1 | # Scratch Token Extension Guide 2 | 3 | This tutorial will guide you through interacting with an Ethereum token contract using the Scratch language. This includes reading state data from a contract, sending transactions that are signed by the MetaMask wallet, deploying new contracts and processing contract events. 4 | 5 | To avoid any confusion, the Scratch extensions do not replace developing contract using the [Solidity](https://solidity.readthedocs.io) or [Vyper](https://vyper.readthedocs.io) contract languages. The extensions are just a tool to make programmatic interactions with Ethereum contracts easier. 6 | 7 | # Background 8 | 9 | You can skip this background section if you already know about Scratch and Ethereum. 10 | 11 | ## Scratch 12 | 13 | [Scratch](https://scratch.mit.edu/) is designed to teach kids how to program using graphical blocks that clip together. This provides a much simpler syntax that kids and adults can learn to make animations, stories and games. See the [Scratch FAQ](https://scratch.mit.edu/info/faq) or the [Scratch Tutorials](https://scratch.mit.edu/projects/editor/?tutorial=all) to learn more about programming Scratch. 14 | 15 | ### Scratch 3.0 16 | 17 | Scratch 3.0 is the latest generation of Scratch launched on January 2, 2019. It replaces the old Scratch versions that relied on [Adobe Flash Player](https://www.adobe.com/products/flashplayer.html). Scratch 3.0 builds on Google's [Blockly](https://developers.google.com/blockly/) technology to make Scratch available on mobile devices along with many other improvements. See the [Scratch 3.0 wiki](https://en.scratch-wiki.info/wiki/Scratch_3.0) for more details on what's new in Scratch 3.0. 18 | 19 | ### Scratch Extensions 20 | 21 | Scratch 3.0 introduces [extensions](https://scratch.mit.edu/info/faq#scratch-extensions) which allow developers to add custom blocks that can interact with physical devices or internet services. Although the Scratch team is still to publish the specifications and guidelines for extensions, the [Scratch code](https://github.com/LLK/scratch-www) is open-sourced so it's possible to deploy Scratch extensions to a separately hosted Scratch environment. 22 | 23 | ### Block Shapes 24 | 25 | The block shapes used in the Scratch extensions are 26 | * [Reporter](https://en.scratch-wiki.info/wiki/Reporter_Block) - are rounded blocks that return number or string values 27 | * [Stack](https://en.scratch-wiki.info/wiki/Stack_Block) - have a notch at the top and a bump on the bottom so they can be stacked together. Each block performs a command. 28 | * [Hat](https://en.scratch-wiki.info/wiki/Hat_Block) - have a rounded top and a bump at the bottom. They are used to start executing a stack of command blocks. 29 | 30 | ## Ethereum 31 | 32 | [Ethereum](https://www.ethereum.org/) is a global, open-source platform for decentralized applications. See [Ethereum for Beginners](https://www.ethereum.org/beginners/) or [Learn about Ethereum](https://www.ethereum.org/learn/) for more information on what Ethereum is. 33 | 34 | For this guide, we'll be interacting with an Ethereum contract that complies with the [ERC-20 token standard](https://en.wikipedia.org/wiki/ERC-20). A token represents something of value and is owned by an Ethereum account. An Ethereum account can be controlled by a user's wallet, or it could be another Ethereum contract. 35 | 36 | For this tutorial, we'll be using the [MetaMask](https://metamask.io/) wallet which is a Chrome, Firefox or Opera browser add-on. See [How To Use MetaMask](https://youtu.be/ZIGUC9JAAw8) video for installation instructions. 37 | 38 | We'll be using the Ropsten Ethereum test network in the tutorial so in MetaMask make sure your Network is set to `Ropsten Test Network`. You can use [Etherscan](https://ropsten.etherscan.io/) to view the contracts and transactions on the Ropsten test network. 39 | 40 | The ERC20 contract in the Basic Token extension is an [Open Zeppelin](https://openzeppelin.org/) ERC20 contract. The Solidity source code is [here](https://github.com/naddison36/eth-scratch3/blob/master/contracts/TokenBasic.sol) for those that are interested, but it's not needed for the tutorial. The Basic Token extension defaults to pointing to the contract with address [0x999D5f944DD6f97911b2f854638d1fDEe297bE3F](https://ropsten.etherscan.io/address/0x999D5f944DD6f97911b2f854638d1fDEe297bE3F) on the Ropsten network. 41 | 42 | # Contract Extension Blocks 43 | 44 | For this tutorial, we'll be using the Scratch editor at https://scratch.addisonbrown.com.au/ rather than the official [Scratch editor from MIT](https://scratch.mit.edu/projects/editor/). This is because the official Scratch editor does not yet allow custom Scratch extensions to be loaded. 45 | 46 | The Scratch app has a large JavaScript file so please be patient while it downloads with a blank white screen. Yes, it's not a great user experience but hacking the Scratch React app to fix this is beyond my frontend skills. 47 | 48 | ## Loading contract extensions 49 | 50 | Click the blue Add Extension button at the bottom left of the screen to see the custom extensions. 51 | 52 | ![Add Extensions](./AddExtension.png "Add Extensions") 53 | 54 | For this tutorial, we'll choose the `Basic Token` extension 55 | 56 | ![Choose An Extensions](./ChooseAnExtension.png "Choose An Extension") 57 | 58 | This will load the `Basic ERC20 Token` blocks 59 | 60 | ![Basic Token Blocks](./BasicTokenBlocks.png "Basic Token Blocks") 61 | 62 | The first extension you load will ask you to connect your MetaMask wallet to the Scratch application. MetaMask is required for sending signed transactions to the Ethereum network. If you reject this request you can still read data from contracts but you will not be able to send any transactions. 63 | 64 | ![Approve MetaMask Connection](ApprovingMetaMaskConnection.png "Approve MetaMask Connection") 65 | 66 | ## Contract Address 67 | 68 | You can check what contract address the extension is pointing to by using the `Contract Address` reporter block. Reporter blocks in Scratch are rounded blocks and return a value. These can be manually invoked by double-clicking the block, or by including the reporter block in a stack of command blocks. The `Contract Address` block in the below example is after it has been manually invoked by double-clicking the block. 69 | 70 | The blocks in the middle of the examples are a common pattern for setting a Scratch variable to the value returned from a reporter block. In this example, the custom variable tokenAddress is set when the scratch game starts. 71 | 72 | The last example sets the contract address of the extension to the already deployed DAI contract [0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359](https://etherscan.io/token/0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359) when the scratch sprite is clicked. The contract address has to be in hexadecimal format with a `0x` prefix. 73 | 74 | ![Contract Address](./ContractAddresses.png "Contract Addresses") 75 | 76 | ## Network Identifier 77 | 78 | The value of the `Network Identifier` reporter block comes from which network your MetaMask wallet is pointing to. For example, Ropsten has network id 3 and mainnet is 1. See [list of network ids](https://ethereum.stackexchange.com/a/17101) for more. 79 | 80 | ![MetaMask Networks](./MetaMaskNetworks.png "MetaMask Networks") 81 | 82 | By default, the contract address of the token extension points to [0x999D5f944DD6f97911b2f854638d1fDEe297bE3F](https://ropsten.etherscan.io/address/0x999D5f944DD6f97911b2f854638d1fDEe297bE3F) on the Ropsten test network. Reading data or sending transactions to this address on other networks will fail. 83 | 84 | ## Reading data from a contract 85 | 86 | The `Basic Token` only has one reporter block in the `Total Supply` block that reads data from a token contract. The `Full Token` extension has a few more in `Symbol`, `Name` and `Decimals` as shown below. 87 | 88 | These reporter blocks use web3 provider injected by the MetaMask wallet to connect to [Infura](https://infura.io/) API. The Infura API then connect to the public Ethereum networks to call the read-only functions on the token contracts. 89 | 90 | ![Token Reporter Blocks](./TokenReporterBlocks.png "Deploy Token Contract") 91 | 92 | ## Deploying a token contract 93 | 94 | Rather than using an existing token contract, it is possible to deploy a new token contract using the Scratch extensions. As contract deployments require a signed transaction, MetaMask will be used to sign the Ethereum transaction using the private key of the user. 95 | 96 | Below is an example of deploying the basic token contract with the Scratch project is started. In the example, it will mint 1000 tokens to the account that deploys the contract. 97 | 98 | When the deploy command block is called, the MetaMask confirmation window will appear for the user to confirm the transaction. The account used to sign the transaction is whatever the selected MetaMask account is. 99 | 100 | ![Deploy Token Contract](./DeployTokenContract.png "Token Reporter Blocks") 101 | 102 | As we are sending an Ethereum transaction, the sending account needs to have enough Ether to pay for the transaction. Ether for the Ropsten network can be requested using the [MetaMask faucet](https://faucet.metamask.io/) 103 | 104 | ![MetaMask Faucet](./MetaMaskFaucet.png "MetaMask Faucet") 105 | 106 | Unlike other transactions, the deploy contract transaction will not continue executing the commands in the block stack until the transaction has been mined. Other transactions will continue once the transaction hash has been returned. 107 | 108 | ## Transferring Tokens 109 | 110 | WIP 111 | 112 | ## Processing contract events 113 | 114 | WIP 115 | 116 | ## Debugging 117 | 118 | WIP -------------------------------------------------------------------------------- /scratch/extensions/tokenBasic/index.js: -------------------------------------------------------------------------------- 1 | const log = require('minilog')('eth-scratch3:TokenBasic') 2 | const TruffleContractDetails = require('../contracts/TokenBasic.json') 3 | 4 | const formatMessage = require('format-message') 5 | const ArgumentType = require('../../../extension-support/argument-type') 6 | const BlockType = require('../../../extension-support/block-type') 7 | 8 | const regEx = require('../regEx') 9 | const BaseBlocks = require('../BaseBlocks') 10 | const BaseContract = require('../BaseContract') 11 | 12 | class ContractBlocks extends BaseBlocks { 13 | 14 | constructor(runtimeProxy) { 15 | super(runtimeProxy) 16 | this.contract = new BaseContract(TruffleContractDetails) 17 | 18 | this.initEvents(['Transfer', 'Approve']) 19 | } 20 | 21 | getInfo() { 22 | 23 | return { 24 | id: 'tokenBasic', 25 | name: formatMessage({ 26 | id: 'tokenBasic.categoryName', 27 | default: 'Basic ERC20 Token', 28 | description: 'extension name', 29 | }), 30 | menus: { 31 | events: this.eventsMenu(), 32 | eventProperties: [ 33 | {text: 'From', value: 'from'}, 34 | {text: 'To', value: 'to'}, 35 | {text: 'Value', value: 'value'}, 36 | {text: 'Owner', value: 'owner'}, 37 | {text: 'Spender', value: 'spender'}, 38 | ], 39 | }, 40 | blocks: [ 41 | ...this.commonBlocks(), 42 | { 43 | opcode: 'deploy', 44 | blockType: BlockType.COMMAND, 45 | text: formatMessage({ 46 | id: 'tokenBasic.deploy', 47 | default: 'Deploy contract with total supply [TOTAL_SUPPLY]', 48 | description: 'command text', 49 | }), 50 | arguments: { 51 | TOTAL_SUPPLY: { 52 | type: ArgumentType.NUMBER, 53 | defaultValue: 0, 54 | }, 55 | }, 56 | }, 57 | { 58 | opcode: 'transfer', 59 | blockType: BlockType.COMMAND, 60 | text: formatMessage({ 61 | id: 'tokenBasic.transfer', 62 | default: 'Transfer [VALUE] tokens to [TO]', 63 | description: 'command text', 64 | }), 65 | arguments: { 66 | TO: { 67 | type: ArgumentType.STRING, 68 | defaultValue: 'toAddress', 69 | }, 70 | VALUE: { 71 | type: ArgumentType.NUMBER, 72 | defaultValue: 0, 73 | }, 74 | }, 75 | }, 76 | { 77 | opcode: 'transferFrom', 78 | blockType: BlockType.COMMAND, 79 | text: formatMessage({ 80 | id: 'tokenBasic.transferFrom', 81 | default: 'Transfer [VALUE] tokens from [FROM] to [TO]', 82 | description: 'command text', 83 | }), 84 | arguments: { 85 | FROM: { 86 | type: ArgumentType.STRING, 87 | defaultValue: 'fromAddress', 88 | }, 89 | TO: { 90 | type: ArgumentType.STRING, 91 | defaultValue: 'toAddress', 92 | }, 93 | VALUE: { 94 | type: ArgumentType.NUMBER, 95 | defaultValue: 0, 96 | }, 97 | }, 98 | }, 99 | { 100 | opcode: 'approve', 101 | blockType: BlockType.COMMAND, 102 | text: formatMessage({ 103 | id: 'tokenBasic.approve', 104 | default: 'Approve [VALUE] tokens to be spent by spender [SPENDER]', 105 | description: 'command text', 106 | }), 107 | arguments: { 108 | SPENDER: { 109 | type: ArgumentType.STRING, 110 | defaultValue: 'spenderAddress', 111 | }, 112 | VALUE: { 113 | type: ArgumentType.NUMBER, 114 | defaultValue: 0, 115 | }, 116 | }, 117 | }, 118 | { 119 | opcode: 'balanceOf', 120 | blockType: BlockType.REPORTER, 121 | text: formatMessage({ 122 | id: 'tokenBasic.balanceOf', 123 | default: 'Balance of [ADDRESS]', 124 | description: 'command text', 125 | }), 126 | arguments: { 127 | ADDRESS: { 128 | type: ArgumentType.STRING, 129 | defaultValue: 'ownerAddress', 130 | }, 131 | }, 132 | }, 133 | { 134 | opcode: 'allowance', 135 | blockType: BlockType.REPORTER, 136 | text: formatMessage({ 137 | id: 'tokenBasic.allowance', 138 | default: 'Allowance from [OWNER] to [SPENDER]', 139 | description: 'command text', 140 | }), 141 | arguments: { 142 | OWNER: { 143 | type: ArgumentType.STRING, 144 | defaultValue: 'owner address', 145 | }, 146 | SPENDER: { 147 | type: ArgumentType.STRING, 148 | defaultValue: 'spender address', 149 | }, 150 | }, 151 | }, 152 | { 153 | opcode: 'totalSupply', 154 | blockType: BlockType.REPORTER, 155 | text: formatMessage({ 156 | id: 'tokenBasic.totalSupply', 157 | default: 'Total supply', 158 | description: 'command text', 159 | }), 160 | }, 161 | ], 162 | } 163 | } 164 | 165 | deploy(args) { 166 | return this.contract.deploy( 167 | [args.TOTAL_SUPPLY], 168 | `deploy token contract with total supply of ${args.TOTAL_SUPPLY}`) 169 | } 170 | 171 | transfer(args) 172 | { 173 | const methodName = 'transfer' 174 | 175 | if (!args.TO || !args.TO.match(regEx.ethereumAddress)) { 176 | return this.errorHandler(`Invalid TO address "${args.TO}" for the ${methodName} command. Must be a 40 char hexadecimal with a 0x prefix.`) 177 | } 178 | if (!(args.VALUE >= 0)) { 179 | return this.errorHandler(`Invalid value "${args.VALUE}" for the ${methodName} command. Must be a positive integer.`) 180 | } 181 | 182 | return this.contract.send( 183 | methodName, 184 | [args.TO, args.VALUE], 185 | `transfer ${args.VALUE} tokens to address ${args.TO}`) 186 | } 187 | 188 | transferFrom(args) 189 | { 190 | const methodName = 'transferFrom' 191 | 192 | if (!args.FROM || !args.FROM.match(regEx.ethereumAddress)) { 193 | return this.errorHandler(`Invalid from address "${args.FROM}" for the ${methodName} command. Must be a 40 char hexadecimal with a 0x prefix.`) 194 | } 195 | if (!args.TO || !args.TO.match(regEx.ethereumAddress)) { 196 | return this.errorHandler(`Invalid to address "${args.TO}" for the ${methodName} command. Must be a 40 char hexadecimal with a 0x prefix.`) 197 | } 198 | if (!(args.VALUE >= 0)) { 199 | return this.errorHandler(`Invalid value "${args.VALUE}" for the ${methodName} command. Must be a positive integer.`) 200 | } 201 | 202 | return this.contract.send( 203 | methodName, 204 | [args.TO, args.FROM, args.VALUE], 205 | `transfer ${args.VALUE} tokens from address ${args.FROM} to address ${args.TO}`) 206 | } 207 | 208 | approve(args) 209 | { 210 | const methodName = 'approve' 211 | 212 | if (!args.SPENDER || !args.SPENDER.match(regEx.ethereumAddress)) { 213 | return this.errorHandler(`Invalid spender address "${args.SPENDER}" for the ${methodName} command. Must be a 40 char hexadecimal with a 0x prefix`) 214 | } 215 | if (!(args.VALUE >= 0)) { 216 | return this.errorHandler(`Invalid value "${args.VALUE}" for the ${methodName} command. Must be a positive integer.`) 217 | } 218 | 219 | return this.contract.send( 220 | methodName, 221 | [args.SPENDER, args.VALUE], 222 | `approve ${args.VALUE} tokens to be spent by spender address ${args.SPENDER}`) 223 | } 224 | 225 | allowance(args) 226 | { 227 | const methodName = 'allowance' 228 | 229 | if (!args.OWNER || !args.OWNER.match(regEx.ethereumAddress)) { 230 | return this.errorHandler(`Invalid owner address "${args.OWNER}" for the ${methodName} reporter. Must be a 40 char hexadecimal with a 0x prefix`) 231 | } 232 | if (!args.SENDER || !args.SENDER.match(regEx.ethereumAddress)) { 233 | return this.errorHandler(`Invalid spender address "${args.SENDER}" for the ${methodName} reporter. Must be a 40 char hexadecimal with a 0x prefix`) 234 | } 235 | 236 | return this.contract.call( 237 | 'allowance', 238 | [args.OWNER, args.SENDER], 239 | `get token allowance for spender ${args.SENDER} to transfer from owner ${args.OWNER}`) 240 | } 241 | 242 | balanceOf(args) 243 | { 244 | const methodName = 'balanceOf' 245 | 246 | if (!args.ADDRESS || !args.ADDRESS.match(regEx.ethereumAddress)) { 247 | return this.errorHandler(`Invalid owner address "${args.ADDRESS}" for the ${methodName} reporter. Must be a 40 char hexadecimal with a 0x prefix`) 248 | } 249 | 250 | return this.contract.call( 251 | 'balanceOf', 252 | [args.ADDRESS], 253 | `get token balance of owner address ${args.ADDRESS}`) 254 | } 255 | 256 | totalSupply() { 257 | return this.contract.call( 258 | 'totalSupply', 259 | [], 260 | `get total supply`) 261 | } 262 | } 263 | module.exports = ContractBlocks 264 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # eth-scratch3 3 | 4 | > a Scratch 3.0 extension for interacting with Ethereum contracts. 5 | 6 | ![Token Blocks](./docs/screenshotTokenBlocks.png "Token Blocks") 7 | 8 | # Playground 9 | 10 | A Scratch 3.0 server with the Ethereum extensions has been made publicly available at https://scratch.addisonbrown.com.au/ 11 | 12 | To load an Ethereum extension, click the `Add Extension` button on the bottom left of the Scratch UI. 13 | 14 | ![Add Extension](./docs/addExtensionButton.png "Add Extension") 15 | 16 | Even though the applicatoin is served on a content delivery network (CDN), it can still take some time to download as the JavaScript file `lib.min.js` is nearly 20 MB in size. 17 | 18 | # Quick Start 19 | 20 | The easiest way to play with the extensions locally is to run a [Docker](https://www.docker.com/) container. 21 | ```bash 22 | mkdir scratch 23 | cd scratch 24 | git clone https://github.com/naddison36/eth-scratch3.git 25 | cd eth-scratch3 26 | npm install 27 | docker build -t eth-scratch3 --target web . 28 | docker run -p 8601:8601 -e PORT=8601 eth-scratch3 29 | ``` 30 | 31 | After the server starts, Scratch should be available at [http://localhost:8601](http://localhost:8601) 32 | 33 | ## Table of Contents 34 | 35 | - [Scratch](#scratch) 36 | * [What is Scratch?](#what-is-scratch) 37 | * [Scratch 3.0](#scratch-30) 38 | * [Scratch 3.0 Extensions](#scratch-30-extensions) 39 | * [Hacking Scratch 3.0 Extensions](#hacking-scratch-30-extensions) 40 | * [Scratch 3.0 Extension Development](#scratch-30-extension-development) 41 | + [Prerequisite](#prerequisite) 42 | + [Installation](#installation) 43 | + [Customization](#customization) 44 | - [Ethereum](#ethereum) 45 | * [Smart Contracts](#smart-contracts) 46 | * [MetaMask](#metamask) 47 | - [Docker](#docker) 48 | - [Hosting](#hosting) 49 | - [Continuous Integration](#continuous-integration) 50 | - [TODO](#TODO) 51 | 52 | # Scratch 53 | 54 | ## What is Scratch? 55 | [Scratch](https://scratch.mit.edu/) is a project of the Lifelong [Kindergarten Group](https://www.media.mit.edu/groups/lifelong-kindergarten/overview/) at the [MIT Media Lab](https://www.media.mit.edu/). It is provided free of charge. 56 | 57 | Scratch is designed especially for ages 8 to 16, but is used by people of all ages. Millions of people are creating Scratch projects in a wide variety of settings, including homes, schools, museums, libraries, and community centers. 58 | 59 | ## Scratch 3.0 60 | [Scratch 3.0](https://scratch.mit.edu/info/faq#scratch3) is the latest generation of Scratch, launched on January 2, 2019. It is designed to expand how, what, and where you can create with Scratch. It includes dozens of new sprites, a totally new sound editor, and many new programming blocks. And with Scratch 3.0, you’re able to create and play projects on your tablet, in addition to your laptop or desktop computer. 61 | 62 | ## Scratch 3.0 Extensions 63 | In the Scratch editor, you can add collections of extra blocks called [extensions](https://scratch.mit.edu/info/faq#scratch-extensions). 64 | 65 | The Scratch Team will be publishing specifications and guidelines for extensions in the future. Once available, you will be able to submit extensions to the Scratch Team for consideration in the official Scratch 3.0 extensions library. We’ll also provide guidelines for developing and distributing "experimental" extensions, which can be used to create projects on individual computers, but not shared in the Scratch online community. 66 | 67 | ## Hacking Scratch 3.0 Extensions 68 | Although Scratch extension specifications have not been released, a few people in the community have worked out how to hack together a Scratch 3.0 extension. Most of the work in this repository is based on the blog post [How to Develop Your Own Block for Scratch 3.0](https://medium.com/@hiroyuki.osaki/how-to-develop-your-own-block-for-scratch-3-0-1b5892026421). 69 | 70 | An example Scratch game [Scratch Wars](https://scratch.mit.edu/projects/95284179/). 71 | 72 | ## Scratch Block Error Handling 73 | 74 | There is no native error handling in Scratch so the best you can do is watch the Browser's console. In Chrome, View -> Developer -> JavaScript Console. Longer term, a hat event block for errors should be created so errors can be fed back into the Scratch application. 75 | 76 | For more Scratch information, see the [Scratch FAQ](https://scratch.mit.edu/info/faq). 77 | 78 | ## Scratch 3.0 Extension Development 79 | 80 | ### Prerequisite 81 | 82 | The following software must be installed before running the installation steps 83 | - [Git](https://git-scm.com/downloads) 84 | - [Node.js](https://nodejs.org/en/download/) 85 | - [npx](https://www.npmjs.com/package/npx) 86 | - [Docker](https://docs.docker.com/docker-for-mac/install/) 87 | 88 | ### Installation 89 | 90 | The following will install this [Eth Scratch 3](https://github.com/naddison36/eth-scratch3) repository and the Scratch repositories [scratch-gui](https://github.com/LLK/scratch-gui) and [scratch-vm](https://github.com/LLK/scratch-vm). This will allow Scratch with the custom extensions to be run locally. 91 | ```bash 92 | mkdir scratch 93 | cd scratch 94 | git clone https://github.com/naddison36/eth-scratch3.git 95 | cd eth-scratch3 96 | npm install 97 | 98 | # install the scratch gui and vm packages 99 | cd .. 100 | git clone https://github.com/LLK/scratch-gui.git 101 | cd scratch-gui 102 | npm install 103 | cd .. 104 | git clone https://github.com/LLK/scratch-vm.git 105 | cd scratch-vm 106 | npm install 107 | npm install web3@0.20.3 108 | npm link 109 | cd ../scratch-gui 110 | npm link scratch-vm 111 | 112 | # link crypto beasts to the scratch vm extensions 113 | cd ../scratch-vm/src/extensions 114 | ln -s ../../../eth-scratch3/scratch/extensions ./custom 115 | # Link the extension to Truffle's deployed contract information 116 | cd ../../../eth-scratch3/scratch/extensions/ 117 | ln -s ../../build/contracts contracts 118 | 119 | # Copy modified scratch vm and gui files into the dependent packages 120 | cd ../ 121 | cp gui/index.jsx ../../scratch-gui/src/lib/libraries/extensions/index.jsx 122 | cp vm/extension-manager.js ../../scratch-vm/src/extension-support/extension-manager.js 123 | 124 | # start the Scratch React App 125 | cd ../../scratch-gui 126 | npm start 127 | ``` 128 | 129 | After the server starts, Scratch should be available at [http://localhost:8601](http://localhost:8601) 130 | 131 | ### Customization 132 | 133 | The following steps are done in the above but a listed here for anyone who wants to write their own Scratch extension. 134 | 135 | New extensions are registered in the scratch-gui project in the `src/lib/libraries/extensions/index.jsx` file. Add this to the `extensions` array 136 | ```js 137 | { 138 | name: ( 139 | 144 | ), 145 | extensionId: 'tokenDetailedMintableBurnable', 146 | collaborator: 'Nick Addison', 147 | // iconURL: boostIconURL, 148 | // insetIconURL: boostInsetIconURL, 149 | description: ( 150 | 155 | ), 156 | featured: true, 157 | disabled: false, 158 | bluetoothRequired: false, 159 | internetConnectionRequired: true 160 | }, 161 | ``` 162 | 163 | The JavaScript in the extension file needs to be loaded via the `src/extension-support/extension-manager.js` file in the `scratch-vm` package. Add the following function property to the `builtinExtensions` object in the `src/extension-support/extension-manager.js` file 164 | ```js 165 | tokenDetailedMintableBurnable: () => require('../extensions/custom/tokenDetailedMintableBurnable'), 166 | ``` 167 | 168 | # Ethereum 169 | 170 | In order to deploy the contracts with the public test networks using [Truffle](https://truffleframework.com), the [config.js](./config.js) file needs to be updated with your private key and [Infura](https://infura.io) project id. See [Introducing the Infura Dashboard](https://blog.infura.io/introducing-the-infura-dashboard-8969b7ab94e7) for details on how to get an Infura project id. 171 | 172 | To deploy the token contract to the Ropsten public test network. 173 | ```bash 174 | npx truffle migrate --reset --network ropsten 175 | ``` 176 | 177 | The extension for the basic ERC20 contract currently works with the [0x999D5f944DD6f97911b2f854638d1fDEe297bE3F](https://ropsten.etherscan.io/address/0x999D5f944DD6f97911b2f854638d1fDEe297bE3F) contract deployed to the Ropsten testnet. 178 | 179 | ## Smart Contracts 180 | 181 | The contracts used by the Scratch extensions are based off [Open Zeppelin](https://docs.openzeppelin.org/) contract. 182 | * [TokenBasic](./contracts/TokenBasic.sol) is an Open Zeppelin [ERC20 token contract](https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/token/ERC20/ERC20.sol) 183 | * [TokenDetailedMintableBurnable](./contracts/TokenDetailedMintableBurnable.sol) is a [detailed](https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/token/ERC20/ERC20Detailed.sol) Open Zeppelin [ERC20 token contract](https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/token/ERC20/ERC20.sol) that is [Mintable](https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/token/ERC20/ERC20Mintable.sol) and [Burnable](https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/token/ERC20/ERC20Burnable.sol). This means tokens can be added by the minter or can be burnt by the token owner. 184 | * [TokenNFTBasic](./contracts/TokenNFTBasic.sol) is an Open Zeppelin [ERC721 non-fungible token contract](https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/token/ERC721/ERC721.sol) 185 | 186 | ## MetaMask 187 | 188 | [MetaMask](https://metamask.io/) is a browser extension that allows users to manage their Ethereum private keys in a variety of ways, including hardware wallets, while isolating them from the site context. MetaMask comes pre-loaded connections to Ethereum main and test networks via [Infura](https://infura.io/). 189 | 190 | See [MetaMask Developer Documentation](https://metamask.github.io/metamask-docs/) for more details on how to interact with MetaMask. [10 Web3/Metamask Use Cases Every Blockchain Developer Needs to Know](https://ylv.io/10-web3-metamask-use-cases-ever-blockchain-developer-needs/) is also very useful. The official [Web3.js 0.2x.x](https://github.com/ethereum/wiki/wiki/JavaScript-API) documentation also helps. 191 | 192 | # Docker 193 | 194 | This [Dockerfile](./Dockerfile) will add the Scratch extensions like [Detailed, mintable, burnable token](./scratch/extensions/tokenDetailedMintableBurnable/index.js) to the Scratch 3.0 react app and copy it into a nginx image. This image can then be deployed to a cloud provider. This project is currently using Heroku, but others like AWS, Azure and GCP will also work. 195 | 196 | `npm run buildWebImage` will build the Docker image which runs 197 | ``` 198 | docker build -t registry.heroku.com/eth-scratch3/web:latest --target web . 199 | ``` 200 | 201 | `npm run bashWebImage` will shell into the built web image 202 | ``` 203 | docker run -it registry.heroku.com/eth-scratch3/web:latest sh 204 | ``` 205 | 206 | `npm run runWebImage` will run the Scratch 3.0 react app with extensions locally 207 | ``` 208 | docker run -p 8601:8601 -e PORT=8601 registry.heroku.com/eth-scratch3/web:latest 209 | ``` 210 | 211 | `npm run pushWebImage` will push the web image up to the Heroku container registry 212 | ``` 213 | docker push registry.heroku.com/eth-scratch3/web:latest 214 | ``` 215 | 216 | This project is deploying to Heroku hence the `registry.heroku.com/eth-scratch3` image names. These will need to be changed if deploying to other cloud-based container registries. 217 | 218 | # Hosting 219 | 220 | The Scratch playground with Ethereum extensions has been deploed to an [AWS S3](https://aws.amazon.com/s3/) bucket called `scratch.addisonbrown.com.au`. This is made publically available via [Static Website hosting](https://docs.aws.amazon.com/AmazonS3/latest/dev/WebsiteHosting.html) at endpoint http://scratch.addisonbrown.com.au.s3-website.us-east-2.amazonaws.com 221 | 222 | To speed up the delivery of the static files, [AWS CloudFront](https://aws.amazon.com/cloudfront/) is used a content delivery network (CDN). The CloudFront distribution domain name is http://d38knlehb6x8uc.cloudfront.net 223 | 224 | [AWS Route 53](https://aws.amazon.com/route53/) has the DNS records to route the `scratch.addisonbrown.com.au` subdomain to the CloudFront distribution. Speficically, it has A and AAAA alias records to route IPv4 and IPv6 addresses. 225 | 226 | The partent `addisonbrown.com.au` domain is managed by [netregistry](https://netregistry.com.au). It delegates responsibility for the `scratch.addisonbrown.com.au` subdomain to Route 53 by adding a NS records. A separate NS record is created for each of the four name services in the Route 53 service. 227 | 228 | [AWS Certificate Manager](https://aws.amazon.com/certificate-manager/) is used to issue the certificate for the `scratch.addisonbrown.com.au` subdomain. This is required for the secure https connections to the CloudFront distribution. 229 | 230 | See [Creating a Subdomain That Uses Amazon Route 53 as the DNS Service without Migrating the Parent Domain](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/CreatingNewSubdomain.html) for more information. 231 | 232 | # Continuous Integration 233 | 234 | [CicleCi](https://circleci.com/) is used for CI. The config file is [.circleci/config.yml](.circleci/config.yml). The CircleCi jobs can be viewed at https://circleci.com/gh/naddison36/eth-scratch3/tree/master. 235 | 236 | Currently, builds are automatically pushed to Heroku https://eth-scratch3.herokuapp.com/. This will probably change to AWS in the future. 237 | 238 | # TODO 239 | * Read state of sent transactions. eg pending or number of confirmations 240 | * Need to convert event args that are integers from strings to numbers 241 | * Hat event block for feeding back errors 242 | * Extension for full [ERC721](http://erc721.org/) non-fungible tokens 243 | * Events for Ether blocks. eg account balance changes 244 | * Generating an extension from a contract’s ABI 245 | * Integrating [Assist.js](https://github.com/blocknative/assist) for easier onboarding 246 | * Extension that uses Web3.js 1.x 247 | * Extension that uses [Ethers.js](https://docs.ethers.io/ethers.js/html/) 248 | 249 | -------------------------------------------------------------------------------- /scratch/extensions/tokenNFTBasic/index.js: -------------------------------------------------------------------------------- 1 | const log = require('minilog')('eth-scratch3:TokenNFTBasic') 2 | const TruffleContractDetails = require('../contracts/TokenNFTBasic.json') 3 | 4 | const formatMessage = require('format-message') 5 | const ArgumentType = require('../../../extension-support/argument-type') 6 | const BlockType = require('../../../extension-support/block-type') 7 | 8 | const regEx = require('../regEx') 9 | const BaseBlocks = require('../BaseBlocks') 10 | const BaseContract = require('../BaseContract') 11 | 12 | class ContractBlocks extends BaseBlocks { 13 | 14 | constructor(runtimeProxy) { 15 | super(runtimeProxy) 16 | 17 | this.contract = new BaseContract(TruffleContractDetails) 18 | 19 | this.initEvents(['Transfer', 'Approve', 'ApprovalForAll']) 20 | } 21 | 22 | getInfo() { 23 | 24 | return { 25 | id: 'tokenNFTBasic', 26 | name: formatMessage({ 27 | id: 'tokenNFTBasic.categoryName', 28 | default: 'Basic Non-Fungible Token (ERC721)', 29 | description: 'extension name', 30 | }), 31 | menus: { 32 | events: this.eventsMenu(), 33 | eventProperties: [ 34 | {text: 'From', value: 'from'}, 35 | {text: 'To', value: 'to'}, 36 | {text: 'Token ID', value: 'tokenId'}, 37 | {text: 'Owner', value: 'owner'}, 38 | {text: 'Operator', value: 'operator'}, 39 | {text: 'Approved', value: 'approved'}, 40 | ], 41 | }, 42 | blocks: [ 43 | ...this.commonBlocks(), 44 | { 45 | opcode: 'deploy', 46 | blockType: BlockType.COMMAND, 47 | text: formatMessage({ 48 | id: 'tokenNFTBasic.deploy', 49 | default: 'Deploy contract', 50 | description: 'command text', 51 | }), 52 | }, 53 | { 54 | opcode: 'transferFrom', 55 | blockType: BlockType.COMMAND, 56 | text: formatMessage({ 57 | id: 'tokenNFTBasic.transferFrom', 58 | default: 'Transfer token id [TOKEN_ID] from [FROM] to [TO]', 59 | description: 'command text', 60 | }), 61 | arguments: { 62 | TO: { 63 | type: ArgumentType.STRING, 64 | defaultValue: 'toAddress', 65 | }, 66 | FROM: { 67 | type: ArgumentType.STRING, 68 | defaultValue: 'fromAddress', 69 | }, 70 | TOKEN_ID: { 71 | type: ArgumentType.NUMBER, 72 | defaultValue: 0, 73 | }, 74 | }, 75 | }, 76 | { 77 | opcode: 'safeTransferFrom', 78 | blockType: BlockType.COMMAND, 79 | text: formatMessage({ 80 | id: 'tokenNFTBasic.safeTransferFrom', 81 | default: 'Safe transfer token id [TOKEN_ID] from [FROM] to [TO] with data [DATA]', 82 | description: 'command text', 83 | }), 84 | arguments: { 85 | TO: { 86 | type: ArgumentType.STRING, 87 | defaultValue: 'toAddress', 88 | }, 89 | FROM: { 90 | type: ArgumentType.STRING, 91 | defaultValue: 'fromAddress', 92 | }, 93 | TOKEN_ID: { 94 | type: ArgumentType.NUMBER, 95 | defaultValue: 0, 96 | }, 97 | DATA: { 98 | type: ArgumentType.STRING, 99 | defaultValue: '0x0', 100 | }, 101 | }, 102 | }, 103 | { 104 | opcode: 'approve', 105 | blockType: BlockType.COMMAND, 106 | text: formatMessage({ 107 | id: 'tokenNFTBasic.approve', 108 | default: 'Approve [TO] to control token id [TOKEN_ID]', 109 | description: 'command text', 110 | }), 111 | arguments: { 112 | TO: { 113 | type: ArgumentType.STRING, 114 | defaultValue: 'toAddress', 115 | }, 116 | TOKEN_ID: { 117 | type: ArgumentType.NUMBER, 118 | defaultValue: 0, 119 | }, 120 | }, 121 | }, 122 | { 123 | opcode: 'setApprovalForAll', 124 | blockType: BlockType.COMMAND, 125 | text: formatMessage({ 126 | id: 'tokenNFTBasic.setApprovalForAll', 127 | default: 'Set operator [OPERATOR] approval for all tokens [APPROVED]', 128 | description: 'command text', 129 | }), 130 | arguments: { 131 | OPERATOR: { 132 | type: ArgumentType.STRING, 133 | defaultValue: 'operatorAddress', 134 | }, 135 | APPROVED: { 136 | type: ArgumentType.BOOLEAN, 137 | defaultValue: 0, 138 | }, 139 | }, 140 | }, 141 | { 142 | opcode: 'getApproved', 143 | blockType: BlockType.REPORTER, 144 | text: formatMessage({ 145 | id: 'tokenNFTBasic.getApproved', 146 | default: 'Approved controller of token id [TOKEN_ID]', 147 | description: 'command text', 148 | }), 149 | arguments: { 150 | TOKEN_ID: { 151 | type: ArgumentType.NUMBER, 152 | defaultValue: 0, 153 | }, 154 | }, 155 | }, 156 | { 157 | opcode: 'isApprovedForAll', 158 | blockType: BlockType.REPORTER, 159 | text: formatMessage({ 160 | id: 'tokenNFTBasic.isApprovedForAll', 161 | default: 'Is operator [OPERATOR] approved to control all owner\'s [OWNER] tokens', 162 | description: 'command text', 163 | }), 164 | arguments: { 165 | OPERATOR: { 166 | type: ArgumentType.STRING, 167 | defaultValue: 'operatorAddress', 168 | }, 169 | OWNER: { 170 | type: ArgumentType.STRING, 171 | defaultValue: 'ownerAddress', 172 | }, 173 | }, 174 | }, 175 | { 176 | opcode: 'balanceOf', 177 | blockType: BlockType.REPORTER, 178 | text: formatMessage({ 179 | id: 'tokenNFTBasic.balanceOf', 180 | default: 'Count tokens of owner [OWNER]', 181 | description: 'command text', 182 | }), 183 | arguments: { 184 | OWNER: { 185 | type: ArgumentType.STRING, 186 | defaultValue: 'ownerAddress', 187 | }, 188 | }, 189 | }, 190 | { 191 | opcode: 'ownerOf', 192 | blockType: BlockType.REPORTER, 193 | text: formatMessage({ 194 | id: 'tokenNFTBasic.ownerOf', 195 | default: 'Owner of token id [TOKEN_ID]', 196 | description: 'command text', 197 | }), 198 | arguments: { 199 | TOKEN_ID: { 200 | type: ArgumentType.NUMBER, 201 | defaultValue: 0, 202 | }, 203 | }, 204 | }, 205 | ], 206 | } 207 | } 208 | 209 | deploy(args) { 210 | return this.contract.deploy( 211 | [], 212 | `deploy non-fungible token contract`) 213 | } 214 | 215 | transferFrom(args) 216 | { 217 | const methodName = 'transferFrom' 218 | 219 | if (!args.FROM || !args.FROM.match(regEx.ethereumAddress)) { 220 | return this.errorHandler(`Invalid to address "${args.FROM}" for the ${methodName} command. Must be a 40 char hexadecimal with a 0x prefix`) 221 | } 222 | if (!args.TO || !args.TO.match(regEx.ethereumAddress)) { 223 | return this.errorHandler(`Invalid to address "${args.TO}" for the ${methodName} command. Must be a 40 char hexadecimal with a 0x prefix`) 224 | } 225 | if (!(args.TOKEN_ID >= 0)) { 226 | return this.errorHandler(`Invalid token id "${args.TOKEN_ID}" for the ${methodName} command. Must be a positive integer.`) 227 | } 228 | 229 | return this.contract.send( 230 | methodName, 231 | [args.FROM, args.TO, args.TOKEN_ID], 232 | `transfer token with id ${args.TOKEN_ID} from address ${args.FROM} to address ${args.TO}`) 233 | } 234 | 235 | safeTransferFrom(args) 236 | { 237 | const methodName = 'safeTransferFrom' 238 | 239 | if (!args.FROM || !args.FROM.match(regEx.ethereumAddress)) { 240 | return this.errorHandler(`Invalid to address "${args.FROM}" for the ${methodName} command. Must be a 40 char hexadecimal with a 0x prefix`) 241 | } 242 | if (!args.TO || !args.TO.match(regEx.ethereumAddress)) { 243 | return this.errorHandler(`Invalid to address "${args.TO}" for the ${methodName} command. Must be a 40 char hexadecimal with a 0x prefix`) 244 | } 245 | if (!(args.TOKEN_ID >= 0)) { 246 | return this.errorHandler(`Invalid token id "${args.TOKEN_ID}" for the ${methodName} command. Must be a positive integer.`) 247 | } 248 | 249 | return this.contract.send( 250 | methodName, 251 | [args.FROM, args.TO, args.TOKEN_ID, args.DATA], 252 | `transfer token with id ${args.TOKEN_ID} from address ${args.FROM} to address ${args.TO}`) 253 | } 254 | 255 | approve(args) 256 | { 257 | const methodName = 'approve' 258 | 259 | if (!args.TO || !args.TO.match(regEx.ethereumAddress)) { 260 | return this.errorHandler(`Invalid to address "${args.TO}" for the ${methodName} command. Must be a 40 char hexadecimal with a 0x prefix`) 261 | } 262 | if (!(args.TOKEN_ID >= 0)) { 263 | return this.errorHandler(`Invalid token id "${args.TOKEN_ID}" for the ${methodName} command. Must be a positive integer.`) 264 | } 265 | 266 | return this.contract.send( 267 | methodName, 268 | [args.TO, args.TOKEN_ID], 269 | `approve token with id ${args.TOKEN_ID} to be controlled by address ${args.TO}`) 270 | } 271 | 272 | getApproved(args) 273 | { 274 | const methodName = 'getApproved' 275 | 276 | if (!(args.TOKEN_ID >= 0)) { 277 | return this.errorHandler(`Invalid token id "${args.TOKEN_ID}" for the ${methodName} command. Must be a positive integer.`) 278 | } 279 | 280 | return this.contract.call( 281 | methodName, 282 | [args.TOKEN_ID], 283 | `get operator approved to control token with id ${args.TOKEN_ID}`) 284 | } 285 | 286 | setApprovalForAll(args) 287 | { 288 | const methodName = 'setApprovalForAll' 289 | 290 | if (!args.OPERATOR || !args.OPERATOR.match(regEx.ethereumAddress)) { 291 | return this.errorHandler(`Invalid operator address "${args.OPERATOR}" for the ${methodName} command. Must be a 40 char hexadecimal with a 0x prefix`) 292 | } 293 | 294 | return this.contract.send( 295 | methodName, 296 | [args.OPERATOR, args.APPROVED], 297 | `set operator ${args.OPERATOR} approval for all tokens to ${args.APPROVED}`) 298 | } 299 | 300 | 301 | isApprovedForAll(args) 302 | { 303 | const methodName = 'isApprovedForAll' 304 | 305 | if (!args.OWNER || !args.OWNER.match(regEx.ethereumAddress)) { 306 | return this.errorHandler(`Invalid owner address "${args.OWNER}" for the ${methodName} reporter. Must be a 40 char hexadecimal with a 0x prefix`) 307 | } 308 | if (!args.OPERATOR || !args.OPERATOR.match(regEx.ethereumAddress)) { 309 | return this.errorHandler(`Invalid owner address "${args.OPERATOR}" for the ${methodName} reporter. Must be a 40 char hexadecimal with a 0x prefix`) 310 | } 311 | 312 | return this.contract.call( 313 | methodName, 314 | [args.OWNER, args.OPERATOR], 315 | `is operator ${args.OPERATOR} approved to control all owner\'s [OWNER] tokens?`) 316 | } 317 | 318 | balanceOf(args) 319 | { 320 | const methodName = 'balanceOf' 321 | 322 | if (!args.OWNER || !args.OWNER.match(regEx.ethereumAddress)) { 323 | return this.errorHandler(`Invalid owner address "${args.OWNER}" for the ${methodName} reporter. Must be a 40 char hexadecimal with a 0x prefix`) 324 | } 325 | 326 | return this.contract.call( 327 | methodName, 328 | [args.OWNER], 329 | `count tokens owned by ${args.OWNER}`) 330 | } 331 | 332 | ownerOf(args) 333 | { 334 | const methodName = 'ownerOf' 335 | 336 | if (!(args.TOKEN_ID >= 0)) { 337 | return this.errorHandler(`Invalid token id "${args.TOKEN_ID}" for the ${methodName} command. Must be a positive integer.`) 338 | } 339 | 340 | return this.contract.call( 341 | methodName, 342 | [args.TOKEN_ID], 343 | `get owner of token with id ${args.TOKEN_ID}`) 344 | } 345 | } 346 | module.exports = ContractBlocks 347 | -------------------------------------------------------------------------------- /scratch/extensions/tokenDetailedMintableBurnable/index.js: -------------------------------------------------------------------------------- 1 | const log = require('minilog')('eth-scratch3:TokenDetailedMintableBurnable') 2 | const TruffleContractDetails = require('../contracts/TokenDetailedMintableBurnable.json') 3 | 4 | const formatMessage = require('format-message') 5 | const ArgumentType = require('../../../extension-support/argument-type') 6 | const BlockType = require('../../../extension-support/block-type') 7 | 8 | const regEx = require('../regEx') 9 | const BaseBlocks = require('../BaseBlocks') 10 | const BaseContract = require('../BaseContract') 11 | 12 | class ContractBlocks extends BaseBlocks { 13 | 14 | constructor(runtimeProxy) { 15 | super() 16 | this.contract = new BaseContract(TruffleContractDetails) 17 | 18 | this.initEvents(['Transfer', 'Approve']) 19 | } 20 | 21 | getInfo() { 22 | 23 | return { 24 | id: 'tokenDetailedMintableBurnable', 25 | name: formatMessage({ 26 | id: 'tokenDetailedMintableBurnable.categoryName', 27 | default: 'Full Token (ERC20)', 28 | description: 'extension name', 29 | }), 30 | menus: { 31 | events: this.eventsMenu(), 32 | eventProperties: [ 33 | {text: 'From', value: 'from'}, 34 | {text: 'To', value: 'to'}, 35 | {text: 'Value', value: 'value'}, 36 | {text: 'Owner', value: 'owner'}, 37 | {text: 'Spender', value: 'spender'}, 38 | ], 39 | }, 40 | blocks: [ 41 | ...this.commonBlocks(), 42 | { 43 | opcode: 'deploy', 44 | blockType: BlockType.COMMAND, 45 | text: formatMessage({ 46 | id: 'tokenDetailedMintableBurnable.deploy', 47 | default: 'Deploy contract with symbol [SYMBOL] name [NAME] and decimals [DECIMALS]', 48 | description: 'command text', 49 | }), 50 | arguments: { 51 | SYMBOL: { 52 | type: ArgumentType.STRING, 53 | defaultValue: 'symbol', 54 | }, 55 | NAME: { 56 | type: ArgumentType.STRING, 57 | defaultValue: 'name', 58 | }, 59 | DECIMALS: { 60 | type: ArgumentType.NUMBER, 61 | defaultValue: 0, 62 | }, 63 | }, 64 | }, 65 | { 66 | opcode: 'transfer', 67 | blockType: BlockType.COMMAND, 68 | text: formatMessage({ 69 | id: 'tokenDetailedMintableBurnable.transfer', 70 | default: 'Transfer [VALUE] tokens to [TO]', 71 | description: 'command text', 72 | }), 73 | arguments: { 74 | TO: { 75 | type: ArgumentType.STRING, 76 | defaultValue: 'toAddress', 77 | }, 78 | VALUE: { 79 | type: ArgumentType.NUMBER, 80 | defaultValue: 0, 81 | }, 82 | }, 83 | }, 84 | { 85 | opcode: 'transferFrom', 86 | blockType: BlockType.COMMAND, 87 | text: formatMessage({ 88 | id: 'tokenDetailedMintableBurnable.transferFrom', 89 | default: 'Transfer [VALUE] tokens from [FROM] to [TO]', 90 | description: 'command text', 91 | }), 92 | arguments: { 93 | FROM: { 94 | type: ArgumentType.STRING, 95 | defaultValue: 'fromAddress', 96 | }, 97 | TO: { 98 | type: ArgumentType.STRING, 99 | defaultValue: 'toAddress', 100 | }, 101 | VALUE: { 102 | type: ArgumentType.NUMBER, 103 | defaultValue: 0, 104 | }, 105 | }, 106 | }, 107 | { 108 | opcode: 'approve', 109 | blockType: BlockType.COMMAND, 110 | text: formatMessage({ 111 | id: 'tokenDetailedMintableBurnable.approve', 112 | default: 'Approve [VALUE] tokens to be spent by spender [SPENDER]', 113 | description: 'command text', 114 | }), 115 | arguments: { 116 | SPENDER: { 117 | type: ArgumentType.STRING, 118 | defaultValue: 'spenderAddress', 119 | }, 120 | VALUE: { 121 | type: ArgumentType.NUMBER, 122 | defaultValue: 0, 123 | }, 124 | }, 125 | }, 126 | { 127 | opcode: 'mint', 128 | blockType: BlockType.COMMAND, 129 | text: formatMessage({ 130 | id: 'tokenDetailedMintableBurnable.mint', 131 | default: 'Mint [VALUE] tokens to [TO]', 132 | description: 'command text', 133 | }), 134 | arguments: { 135 | TO: { 136 | type: ArgumentType.STRING, 137 | defaultValue: 'toAddress', 138 | }, 139 | VALUE: { 140 | type: ArgumentType.NUMBER, 141 | defaultValue: 0, 142 | }, 143 | }, 144 | }, 145 | { 146 | opcode: 'burn', 147 | blockType: BlockType.COMMAND, 148 | text: formatMessage({ 149 | id: 'tokenDetailedMintableBurnable.burn', 150 | default: 'Burn [VALUE] tokens', 151 | description: 'command text', 152 | }), 153 | arguments: { 154 | VALUE: { 155 | type: ArgumentType.NUMBER, 156 | defaultValue: 0, 157 | }, 158 | }, 159 | }, 160 | { 161 | opcode: 'burnFrom', 162 | blockType: BlockType.COMMAND, 163 | text: formatMessage({ 164 | id: 'tokenDetailedMintableBurnable.burnFrom', 165 | default: 'Burn [VALUE] tokens from [FROM]', 166 | description: 'command text', 167 | }), 168 | arguments: { 169 | FROM: { 170 | type: ArgumentType.STRING, 171 | defaultValue: 'fromAddress', 172 | }, 173 | VALUE: { 174 | type: ArgumentType.NUMBER, 175 | defaultValue: 0, 176 | }, 177 | }, 178 | }, 179 | { 180 | opcode: 'balanceOf', 181 | blockType: BlockType.REPORTER, 182 | text: formatMessage({ 183 | id: 'tokenDetailedMintableBurnable.balanceOf', 184 | default: 'Balance of [ADDRESS]', 185 | description: 'command text', 186 | }), 187 | arguments: { 188 | ADDRESS: { 189 | type: ArgumentType.STRING, 190 | defaultValue: 'ownerAddress', 191 | }, 192 | }, 193 | }, 194 | { 195 | opcode: 'allowance', 196 | blockType: BlockType.REPORTER, 197 | text: formatMessage({ 198 | id: 'tokenDetailedMintableBurnable.allowance', 199 | default: 'Allowance from [OWNER] to [SPENDER]', 200 | description: 'command text', 201 | }), 202 | arguments: { 203 | OWNER: { 204 | type: ArgumentType.STRING, 205 | defaultValue: 'owner address', 206 | }, 207 | SPENDER: { 208 | type: ArgumentType.STRING, 209 | defaultValue: 'spender address', 210 | }, 211 | }, 212 | }, 213 | { 214 | opcode: 'totalSupply', 215 | blockType: BlockType.REPORTER, 216 | text: formatMessage({ 217 | id: 'tokenDetailedMintableBurnable.totalSupply', 218 | default: 'Total supply', 219 | description: 'command text', 220 | }), 221 | }, 222 | { 223 | opcode: 'symbol', 224 | blockType: BlockType.REPORTER, 225 | text: formatMessage({ 226 | id: 'tokenDetailedMintableBurnable.symbol', 227 | default: 'Symbol', 228 | description: 'command text', 229 | }), 230 | }, 231 | { 232 | opcode: 'name', 233 | blockType: BlockType.REPORTER, 234 | text: formatMessage({ 235 | id: 'tokenDetailedMintableBurnable.name', 236 | default: 'Name', 237 | description: 'command text', 238 | }), 239 | }, 240 | { 241 | opcode: 'decimals', 242 | blockType: BlockType.REPORTER, 243 | text: formatMessage({ 244 | id: 'tokenDetailedMintableBurnable.decimals', 245 | default: 'Decimals', 246 | description: 'command text', 247 | }), 248 | }, 249 | ], 250 | } 251 | } 252 | 253 | deploy(args) { 254 | if (!args.SYMBOL || typeof args.SYMBOL !== 'string') { 255 | return this.errorHandler(`Invalid symbol "${args.SYMBOL}" for contract deploy command. Must be a string`) 256 | } 257 | if (!args.NAME || typeof args.NAME !== 'string') { 258 | return this.errorHandler(`Invalid name "${args.NAME}" for contract deploy command. Must be a string`) 259 | } 260 | if (!(args.DECIMALS >= 0)) { 261 | return this.errorHandler(`Invalid decimals "${args.DECIMALS}" for the ${methodName} command. Must be a positive integer.`) 262 | } 263 | 264 | return this.contract.deploy( 265 | [args.SYMBOL, args.NAME, args.DECIMALS], 266 | `deploy token contract with symbol ${args.SYMBOL}, name ${args.NAME} and decimals ${args.DECIMALS}`) 267 | } 268 | 269 | transfer(args) 270 | { 271 | const methodName = 'transfer' 272 | 273 | if (!args.TO || !args.TO.match(regEx.ethereumAddress)) { 274 | return this.errorHandler(`Invalid TO address "${args.TO}" for the ${methodName} command. Must be a 40 char hexadecimal with a 0x prefix`) 275 | } 276 | if (!(args.VALUE >= 0)) { 277 | return this.errorHandler(`Invalid value "${args.VALUE}" for the ${methodName} command. Must be a positive integer.`) 278 | } 279 | 280 | return this.contract.send( 281 | methodName, 282 | [args.TO, args.VALUE], 283 | `transfer ${args.VALUE} tokens to address ${args.TO}`) 284 | } 285 | 286 | transferFrom(args) 287 | { 288 | const methodName = 'transferFrom' 289 | 290 | if (!args.FROM || !args.FROM.match(regEx.ethereumAddress)) { 291 | return this.errorHandler(`Invalid from address "${args.FROM}" for the ${methodName} command. Must be a 40 char hexadecimal with a 0x prefix`) 292 | } 293 | if (!args.TO || !args.TO.match(regEx.ethereumAddress)) { 294 | return this.errorHandler(`Invalid to address "${args.TO}" for the ${methodName} command. Must be a 40 char hexadecimal with a 0x prefix`) 295 | } 296 | if (!(args.VALUE >= 0)) { 297 | return this.errorHandler(`Invalid value "${args.VALUE}" for the ${methodName} command. Must be a positive integer.`) 298 | } 299 | 300 | return this.contract.send( 301 | methodName, 302 | [args.TO, args.FROM, args.VALUE], 303 | `transfer ${args.VALUE} tokens from address ${args.FROM} to address ${args.TO}`) 304 | } 305 | 306 | approve(args) 307 | { 308 | const methodName = 'transferFrom' 309 | 310 | if (!args.SPENDER || !args.SPENDER.match(regEx.ethereumAddress)) { 311 | return this.errorHandler(`Invalid spender address "${args.SPENDER}" for the ${methodName} command. Must be a 40 char hexadecimal with a 0x prefix`) 312 | } 313 | if (!(args.VALUE >= 0)) { 314 | return this.errorHandler(`Invalid value "${args.VALUE}" for the ${methodName} command. Must be a positive integer.`) 315 | } 316 | 317 | return this.contract.send( 318 | methodName, 319 | [args.SPENDER, args.VALUE], 320 | `approve ${args.VALUE} tokens to be spent by spender address ${args.SPENDER}`) 321 | } 322 | 323 | mint(args) 324 | { 325 | const methodName = 'mint' 326 | 327 | if (!args.TO || !args.TO.match(regEx.ethereumAddress)) { 328 | return this.errorHandler(`Invalid TO address "${args.TO}" for the ${methodName} command. Must be a 40 char hexadecimal with a 0x prefix`) 329 | } 330 | if (!(args.VALUE >= 0)) { 331 | return this.errorHandler(`Invalid value "${args.VALUE}" for the ${methodName} command. Must be a positive integer.`) 332 | } 333 | 334 | return this.contract.send( 335 | methodName, 336 | [args.TO, args.VALUE], 337 | `mint ${args.VALUE} tokens to address ${args.TO}`) 338 | } 339 | 340 | burn(args) 341 | { 342 | const methodName = 'burn' 343 | 344 | if (!(args.VALUE >= 0)) { 345 | return this.errorHandler(`Invalid value "${args.VALUE}" for the ${methodName} command. Must be a positive integer.`) 346 | } 347 | 348 | return this.contract.send( 349 | methodName, 350 | [args.VALUE], 351 | `burn ${args.VALUE} tokens`) 352 | } 353 | 354 | burnFrom(args) 355 | { 356 | const methodName = 'burn' 357 | 358 | if (!args.FROM || !args.FROM.match(regEx.ethereumAddress)) { 359 | return this.errorHandler(`Invalid FROM address "${args.FROM}" for the ${methodName} command. Must be a 40 char hexadecimal with a 0x prefix`) 360 | } 361 | if (!(args.VALUE >= 0)) { 362 | return this.errorHandler(`Invalid value "${args.VALUE}" for the ${methodName} command. Must be a positive integer.`) 363 | } 364 | 365 | return this.contract.send( 366 | methodName, 367 | [args.FROM, args.VALUE], 368 | `mint ${args.VALUE} tokens to address ${args.TO}`) 369 | } 370 | 371 | allowance(args) 372 | { 373 | const methodName = 'allowance' 374 | 375 | if (!args.OWNER || !args.OWNER.match(regEx.ethereumAddress)) { 376 | return this.errorHandler(`Invalid owner address "${args.OWNER}" for the ${methodName} reporter. Must be a 40 char hexadecimal with a 0x prefix`) 377 | } 378 | if (!args.SENDER || !args.SENDER.match(regEx.ethereumAddress)) { 379 | return this.errorHandler(`Invalid spender address "${args.SENDER}" for the ${methodName} reporter. Must be a 40 char hexadecimal with a 0x prefix`) 380 | } 381 | 382 | return this.contract.call( 383 | 'allowance', 384 | [args.OWNER, args.SENDER], 385 | `get token allowance for spender ${args.SENDER} to transfer from owner ${args.OWNER}`) 386 | } 387 | 388 | balanceOf(args) 389 | { 390 | const methodName = 'balanceOf' 391 | 392 | if (!args.ADDRESS || !args.ADDRESS.match(regEx.ethereumAddress)) { 393 | return this.errorHandler(`Invalid owner address "${args.ADDRESS}" for the ${methodName} reporter. Must be a 40 char hexadecimal with a 0x prefix`) 394 | } 395 | 396 | return this.contract.call( 397 | 'balanceOf', 398 | [args.ADDRESS], 399 | `get token balance of owner address ${args.ADDRESS}`) 400 | } 401 | 402 | totalSupply() { 403 | return this.contract.call( 404 | 'totalSupply', 405 | [], 406 | `get total supply`) 407 | } 408 | 409 | symbol() { 410 | return this.contract.call( 411 | 'symbol', 412 | [], 413 | `get symbol`) 414 | } 415 | 416 | name() { 417 | return this.contract.call( 418 | 'name', 419 | [], 420 | `get name`) 421 | } 422 | 423 | decimals() { 424 | return this.contract.call( 425 | 'decimals', 426 | [], 427 | `get decimals`) 428 | } 429 | } 430 | module.exports = ContractBlocks 431 | -------------------------------------------------------------------------------- /scratch/vm/extension-manager.js: -------------------------------------------------------------------------------- 1 | const dispatch = require('../dispatch/central-dispatch'); 2 | const log = require('../util/log'); 3 | const maybeFormatMessage = require('../util/maybe-format-message'); 4 | 5 | const BlockType = require('./block-type'); 6 | 7 | // These extensions are currently built into the VM repository but should not be loaded at startup. 8 | // TODO: move these out into a separate repository? 9 | // TODO: change extension spec so that library info, including extension ID, can be collected through static methods 10 | 11 | const builtinExtensions = { 12 | // pen: () => require('../extensions/scratch3_pen'), 13 | // wedo2: () => require('../extensions/scratch3_wedo2'), 14 | // music: () => require('../extensions/scratch3_music'), 15 | // microbit: () => require('../extensions/scratch3_microbit'), 16 | // text2speech: () => require('../extensions/scratch3_text2speech'), 17 | // translate: () => require('../extensions/scratch3_translate'), 18 | // videoSensing: () => require('../extensions/scratch3_video_sensing'), 19 | // speech2text: () => require('../extensions/scratch3_speech2text'), 20 | // ev3: () => require('../extensions/scratch3_ev3'), 21 | // makeymakey: () => require('../extensions/scratch3_makeymakey'), 22 | // Custom extensions 23 | ether: () => require('../extensions/custom/ether'), 24 | tokenDetailedMintableBurnable: () => require('../extensions/custom/tokenDetailedMintableBurnable'), 25 | tokenBasic: () => require('../extensions/custom/tokenBasic'), 26 | tokenNFTBasic: () => require('../extensions/custom/tokenNFTBasic'), 27 | }; 28 | 29 | /** 30 | * @typedef {object} ArgumentInfo - Information about an extension block argument 31 | * @property {ArgumentType} type - the type of value this argument can take 32 | * @property {*|undefined} default - the default value of this argument (default: blank) 33 | */ 34 | 35 | /** 36 | * @typedef {object} ConvertedBlockInfo - Raw extension block data paired with processed data ready for scratch-blocks 37 | * @property {ExtensionBlockMetadata} info - the raw block info 38 | * @property {object} json - the scratch-blocks JSON definition for this block 39 | * @property {string} xml - the scratch-blocks XML definition for this block 40 | */ 41 | 42 | /** 43 | * @typedef {object} CategoryInfo - Information about a block category 44 | * @property {string} id - the unique ID of this category 45 | * @property {string} name - the human-readable name of this category 46 | * @property {string|undefined} blockIconURI - optional URI for the block icon image 47 | * @property {string} color1 - the primary color for this category, in '#rrggbb' format 48 | * @property {string} color2 - the secondary color for this category, in '#rrggbb' format 49 | * @property {string} color3 - the tertiary color for this category, in '#rrggbb' format 50 | * @property {Array.} blocks - the blocks, separators, etc. in this category 51 | * @property {Array.} menus - the menus provided by this category 52 | */ 53 | 54 | /** 55 | * @typedef {object} PendingExtensionWorker - Information about an extension worker still initializing 56 | * @property {string} extensionURL - the URL of the extension to be loaded by this worker 57 | * @property {Function} resolve - function to call on successful worker startup 58 | * @property {Function} reject - function to call on failed worker startup 59 | */ 60 | 61 | class ExtensionManager { 62 | constructor (runtime) { 63 | /** 64 | * The ID number to provide to the next extension worker. 65 | * @type {int} 66 | */ 67 | this.nextExtensionWorker = 0; 68 | 69 | /** 70 | * FIFO queue of extensions which have been requested but not yet loaded in a worker, 71 | * along with promise resolution functions to call once the worker is ready or failed. 72 | * 73 | * @type {Array.} 74 | */ 75 | this.pendingExtensions = []; 76 | 77 | /** 78 | * Map of worker ID to workers which have been allocated but have not yet finished initialization. 79 | * @type {Array.} 80 | */ 81 | this.pendingWorkers = []; 82 | 83 | /** 84 | * Set of loaded extension URLs/IDs (equivalent for built-in extensions). 85 | * @type {Set.} 86 | * @private 87 | */ 88 | this._loadedExtensions = new Map(); 89 | 90 | /** 91 | * Keep a reference to the runtime so we can construct internal extension objects. 92 | * TODO: remove this in favor of extensions accessing the runtime as a service. 93 | * @type {Runtime} 94 | */ 95 | this.runtime = runtime; 96 | 97 | dispatch.setService('extensions', this).catch(e => { 98 | log.error(`ExtensionManager was unable to register extension service: ${JSON.stringify(e)}`); 99 | }); 100 | } 101 | 102 | /** 103 | * Check whether an extension is registered or is in the process of loading. This is intended to control loading or 104 | * adding extensions so it may return `true` before the extension is ready to be used. Use the promise returned by 105 | * `loadExtensionURL` if you need to wait until the extension is truly ready. 106 | * @param {string} extensionID - the ID of the extension. 107 | * @returns {boolean} - true if loaded, false otherwise. 108 | */ 109 | isExtensionLoaded (extensionID) { 110 | return this._loadedExtensions.has(extensionID); 111 | } 112 | 113 | /** 114 | * Load an extension by URL or internal extension ID 115 | * @param {string} extensionURL - the URL for the extension to load OR the ID of an internal extension 116 | * @returns {Promise} resolved once the extension is loaded and initialized or rejected on failure 117 | */ 118 | loadExtensionURL (extensionURL) { 119 | if (builtinExtensions.hasOwnProperty(extensionURL)) { 120 | /** @TODO dupe handling for non-builtin extensions. See commit 670e51d33580e8a2e852b3b038bb3afc282f81b9 */ 121 | if (this.isExtensionLoaded(extensionURL)) { 122 | const message = `Rejecting attempt to load a second extension with ID ${extensionURL}`; 123 | log.warn(message); 124 | return Promise.reject(new Error(message)); 125 | } 126 | 127 | const extension = builtinExtensions[extensionURL](); 128 | const extensionInstance = new extension(this.runtime); 129 | return this._registerInternalExtension(extensionInstance).then(serviceName => { 130 | this._loadedExtensions.set(extensionURL, serviceName); 131 | }); 132 | } 133 | 134 | return new Promise((resolve, reject) => { 135 | // If we `require` this at the global level it breaks non-webpack targets, including tests 136 | const ExtensionWorker = require('worker-loader?name=extension-worker.js!./extension-worker'); 137 | 138 | this.pendingExtensions.push({extensionURL, resolve, reject}); 139 | dispatch.addWorker(new ExtensionWorker()); 140 | }); 141 | } 142 | 143 | /** 144 | * Regenerate blockinfo for any loaded extensions 145 | * @returns {Promise} resolved once all the extensions have been reinitialized 146 | */ 147 | refreshBlocks () { 148 | const allPromises = Array.from(this._loadedExtensions.values()).map(serviceName => 149 | dispatch.call(serviceName, 'getInfo') 150 | .then(info => { 151 | info = this._prepareExtensionInfo(serviceName, info); 152 | dispatch.call('runtime', '_refreshExtensionPrimitives', info); 153 | }) 154 | .catch(e => { 155 | log.error(`Failed to refresh buildtin extension primitives: ${JSON.stringify(e)}`); 156 | }) 157 | ); 158 | return Promise.all(allPromises); 159 | } 160 | 161 | allocateWorker () { 162 | const id = this.nextExtensionWorker++; 163 | const workerInfo = this.pendingExtensions.shift(); 164 | this.pendingWorkers[id] = workerInfo; 165 | return [id, workerInfo.extensionURL]; 166 | } 167 | 168 | /** 169 | * Collect extension metadata from the specified service and begin the extension registration process. 170 | * @param {string} serviceName - the name of the service hosting the extension. 171 | */ 172 | registerExtensionService (serviceName) { 173 | dispatch.call(serviceName, 'getInfo').then(info => { 174 | this._registerExtensionInfo(serviceName, info); 175 | }); 176 | } 177 | 178 | /** 179 | * Called by an extension worker to indicate that the worker has finished initialization. 180 | * @param {int} id - the worker ID. 181 | * @param {*?} e - the error encountered during initialization, if any. 182 | */ 183 | onWorkerInit (id, e) { 184 | const workerInfo = this.pendingWorkers[id]; 185 | delete this.pendingWorkers[id]; 186 | if (e) { 187 | workerInfo.reject(e); 188 | } else { 189 | workerInfo.resolve(id); 190 | } 191 | } 192 | 193 | /** 194 | * Register an internal (non-Worker) extension object 195 | * @param {object} extensionObject - the extension object to register 196 | * @returns {Promise} resolved once the extension is fully registered or rejected on failure 197 | */ 198 | _registerInternalExtension (extensionObject) { 199 | const extensionInfo = extensionObject.getInfo(); 200 | const fakeWorkerId = this.nextExtensionWorker++; 201 | const serviceName = `extension_${fakeWorkerId}_${extensionInfo.id}`; 202 | return dispatch.setService(serviceName, extensionObject) 203 | .then(() => { 204 | dispatch.call('extensions', 'registerExtensionService', serviceName); 205 | return serviceName; 206 | }); 207 | } 208 | 209 | /** 210 | * Sanitize extension info then register its primitives with the VM. 211 | * @param {string} serviceName - the name of the service hosting the extension 212 | * @param {ExtensionInfo} extensionInfo - the extension's metadata 213 | * @private 214 | */ 215 | _registerExtensionInfo (serviceName, extensionInfo) { 216 | extensionInfo = this._prepareExtensionInfo(serviceName, extensionInfo); 217 | dispatch.call('runtime', '_registerExtensionPrimitives', extensionInfo).catch(e => { 218 | log.error(`Failed to register primitives for extension on service ${serviceName}:`, e); 219 | }); 220 | } 221 | 222 | /** 223 | * Modify the provided text as necessary to ensure that it may be used as an attribute value in valid XML. 224 | * @param {string} text - the text to be sanitized 225 | * @returns {string} - the sanitized text 226 | * @private 227 | */ 228 | _sanitizeID (text) { 229 | return text.toString().replace(/[<"&]/, '_'); 230 | } 231 | 232 | /** 233 | * Apply minor cleanup and defaults for optional extension fields. 234 | * TODO: make the ID unique in cases where two copies of the same extension are loaded. 235 | * @param {string} serviceName - the name of the service hosting this extension block 236 | * @param {ExtensionInfo} extensionInfo - the extension info to be sanitized 237 | * @returns {ExtensionInfo} - a new extension info object with cleaned-up values 238 | * @private 239 | */ 240 | _prepareExtensionInfo (serviceName, extensionInfo) { 241 | extensionInfo = Object.assign({}, extensionInfo); 242 | if (!/^[a-z0-9]+$/i.test(extensionInfo.id)) { 243 | throw new Error('Invalid extension id'); 244 | } 245 | extensionInfo.name = extensionInfo.name || extensionInfo.id; 246 | extensionInfo.blocks = extensionInfo.blocks || []; 247 | extensionInfo.targetTypes = extensionInfo.targetTypes || []; 248 | extensionInfo.blocks = extensionInfo.blocks.reduce((results, blockInfo) => { 249 | try { 250 | let result; 251 | switch (blockInfo) { 252 | case '---': // separator 253 | result = '---'; 254 | break; 255 | default: // an ExtensionBlockMetadata object 256 | result = this._prepareBlockInfo(serviceName, blockInfo); 257 | break; 258 | } 259 | results.push(result); 260 | } catch (e) { 261 | // TODO: more meaningful error reporting 262 | log.error(`Error processing block: ${e.message}, Block:\n${JSON.stringify(blockInfo)}`); 263 | } 264 | return results; 265 | }, []); 266 | extensionInfo.menus = extensionInfo.menus || []; 267 | extensionInfo.menus = this._prepareMenuInfo(serviceName, extensionInfo.menus); 268 | return extensionInfo; 269 | } 270 | 271 | /** 272 | * Prepare extension menus. e.g. setup binding for dynamic menu functions. 273 | * @param {string} serviceName - the name of the service hosting this extension block 274 | * @param {Array.} menus - the menu defined by the extension. 275 | * @returns {Array.} - a menuInfo object with all preprocessing done. 276 | * @private 277 | */ 278 | _prepareMenuInfo (serviceName, menus) { 279 | const menuNames = Object.getOwnPropertyNames(menus); 280 | for (let i = 0; i < menuNames.length; i++) { 281 | const item = menuNames[i]; 282 | // If the value is a string, it should be the name of a function in the 283 | // extension object to call to populate the menu whenever it is opened. 284 | // Set up the binding for the function object here so 285 | // we can use it later when converting the menu for Scratch Blocks. 286 | if (typeof menus[item] === 'string') { 287 | const serviceObject = dispatch.services[serviceName]; 288 | const menuName = menus[item]; 289 | menus[item] = this._getExtensionMenuItems.bind(this, serviceObject, menuName); 290 | } 291 | } 292 | return menus; 293 | } 294 | 295 | /** 296 | * Fetch the items for a particular extension menu, providing the target ID for context. 297 | * @param {object} extensionObject - the extension object providing the menu. 298 | * @param {string} menuName - the name of the menu function to call. 299 | * @returns {Array} menu items ready for scratch-blocks. 300 | * @private 301 | */ 302 | _getExtensionMenuItems (extensionObject, menuName) { 303 | // Fetch the items appropriate for the target currently being edited. This assumes that menus only 304 | // collect items when opened by the user while editing a particular target. 305 | const editingTarget = this.runtime.getEditingTarget() || this.runtime.getTargetForStage(); 306 | const editingTargetID = editingTarget ? editingTarget.id : null; 307 | const extensionMessageContext = this.runtime.makeMessageContextForTarget(editingTarget); 308 | 309 | // TODO: Fix this to use dispatch.call when extensions are running in workers. 310 | const menuFunc = extensionObject[menuName]; 311 | const menuItems = menuFunc.call(extensionObject, editingTargetID).map( 312 | item => { 313 | item = maybeFormatMessage(item, extensionMessageContext); 314 | switch (typeof item) { 315 | case 'object': 316 | return [ 317 | maybeFormatMessage(item.text, extensionMessageContext), 318 | item.value 319 | ]; 320 | case 'string': 321 | return [item, item]; 322 | default: 323 | return item; 324 | } 325 | }); 326 | 327 | if (!menuItems || menuItems.length < 1) { 328 | throw new Error(`Extension menu returned no items: ${menuName}`); 329 | } 330 | return menuItems; 331 | } 332 | 333 | /** 334 | * Apply defaults for optional block fields. 335 | * @param {string} serviceName - the name of the service hosting this extension block 336 | * @param {ExtensionBlockMetadata} blockInfo - the block info from the extension 337 | * @returns {ExtensionBlockMetadata} - a new block info object which has values for all relevant optional fields. 338 | * @private 339 | */ 340 | _prepareBlockInfo (serviceName, blockInfo) { 341 | blockInfo = Object.assign({}, { 342 | blockType: BlockType.COMMAND, 343 | terminal: false, 344 | blockAllThreads: false, 345 | arguments: {} 346 | }, blockInfo); 347 | blockInfo.opcode = this._sanitizeID(blockInfo.opcode); 348 | blockInfo.text = blockInfo.text || blockInfo.opcode; 349 | 350 | if (blockInfo.blockType !== BlockType.EVENT) { 351 | blockInfo.func = blockInfo.func ? this._sanitizeID(blockInfo.func) : blockInfo.opcode; 352 | 353 | /** 354 | * This is only here because the VM performs poorly when blocks return promises. 355 | * @TODO make it possible for the VM to resolve a promise and continue during the same Scratch "tick" 356 | */ 357 | if (dispatch._isRemoteService(serviceName)) { 358 | blockInfo.func = dispatch.call.bind(dispatch, serviceName, blockInfo.func); 359 | } else { 360 | const serviceObject = dispatch.services[serviceName]; 361 | const func = serviceObject[blockInfo.func]; 362 | if (func) { 363 | blockInfo.func = func.bind(serviceObject); 364 | } else if (blockInfo.blockType !== BlockType.EVENT) { 365 | throw new Error(`Could not find extension block function called ${blockInfo.func}`); 366 | } 367 | } 368 | } else if (blockInfo.func) { 369 | log.warn(`Ignoring function "${blockInfo.func}" for event block ${blockInfo.opcode}`); 370 | } 371 | 372 | return blockInfo; 373 | } 374 | } 375 | 376 | module.exports = ExtensionManager; 377 | --------------------------------------------------------------------------------