├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── LICENSE ├── README.md ├── lerna.json ├── package-lock.json ├── package.json └── packages ├── example ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── LICENSE ├── package-lock.json ├── package.json ├── src │ ├── config.js │ └── index.js └── truffle-config.js ├── integration ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── LICENSE ├── jest.config.js ├── package-lock.json ├── package.json ├── spec │ └── index.spec.ts ├── support │ ├── ganache │ │ ├── index.ts │ │ └── server.js │ ├── parseServer │ │ ├── cloud │ │ │ └── main.js │ │ └── index.ts │ └── truffle │ │ ├── index.ts │ │ └── truffle-config.js └── tsconfig.json ├── parse-blockchain-base ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── LICENSE ├── jest.config.js ├── package-lock.json ├── package.json ├── spec │ ├── Bridge.spec.ts │ ├── SimpleMQAdapter.spec.ts │ ├── Worker.spec.ts │ ├── index.spec.ts │ └── tsconfig.json ├── src │ ├── BlockchainAdapter.ts │ ├── Bridge.ts │ ├── MQAdapter.ts │ ├── SimpleMQAdapter.ts │ ├── Worker.ts │ ├── index.ts │ └── types.ts └── tsconfig.json └── parse-blockchain-ethereum ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── LICENSE ├── contracts ├── Migrations.sol └── Parse.sol ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── package-lock.json ├── package.json ├── src ├── EthereumAdapter.ts └── index.ts ├── test ├── EthereumAdapter.test.js ├── Parse.test.js └── index.test.js ├── truffle-config.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm test 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Parse Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![parse-repository-header](https://user-images.githubusercontent.com/5673677/139276658-d6fa5e86-da3e-446e-9daa-f717469d4c7a.png) 2 | 3 | --- 4 | 5 |

6 | Blockchain (Ethereum) dApps development made easy with Parse Server 7 |

8 | 9 |

10 | This mono repository contains packages that aim to simplify the development of blockchain dApps via Parse Server auto-generated APIs. Currently, only Ethereum networks are supported. 11 |

12 | 13 | --- 14 | 15 | - [Packages](#packages) 16 | - [How It Works](#how-it-works) 17 | - [Getting Started](#getting-started) 18 | - [Running an Ethereum Development Network](#running-an-ethereum-development-network) 19 | - [Creating the Project Folder](#creating-the-project-folder) 20 | - [Deploying the Smart Contracts](#deploying-the-smart-contracts) 21 | - [Running the Database](#running-the-database) 22 | - [Running Parse Server](#running-parse-server) 23 | - [Creating your first object](#creating-your-first-object) 24 | - [Reading the object](#reading-the-object) 25 | - [Learn More](#learn-more) 26 | - [Feedback and Contribution](#feedback-and-contribution) 27 | 28 | ## Packages 29 | 30 | | Package | Name | Version 31 | |--------|-----|------------| 32 | | [Blockchain](https://github.com/parse-community/parse-server-blockchain/tree/master/packages/parse-blockchain-base) | [@parse/blockchain-base](https://www.npmjs.com/package/@parse/blockchain-base) | [![NPM Version](https://badge.fury.io/js/%40parse%2Fblockchain.svg)](https://www.npmjs.com/package/@parse/blockchain-base) | 33 | | [Ethereum](https://github.com/parse-community/parse-server-blockchain/tree/master/packages/parse-blockchain-ethereum) | [@parse/blockchain-ethereum](https://www.npmjs.com/package/@parse/blockchain-ethereum) | [![NPM Version](https://badge.fury.io/js/%40parse%2Fethereum.svg)](https://www.npmjs.com/package/@parse/blockchain-ethereum) | 34 | 35 | ## How It Works 36 | 37 | Using these packages in aggregation to Parse Server, it is possible to easily create hybrid dApps in which part of the data is saved on blockchain and part of the data is saved on the cloud. 38 | 39 | Parse Server generates the APIs that you need to save and read data on the cloud (PostgreSQL or MongoDB). When setting up these packages, it is possible to select which objects must also be saved on blockchain. 40 | 41 | Parse Server saves a copy of the blockchain objects on cloud and make sure they are sent to special smart contracts (ready to deploy versions are included in these packages) via blockchain transactions. Parse Server stores the status of the blockchain transactions and their receipts. 42 | 43 | At anytime it is possible to query data on cloud via Parse Server APIs (including the transactions receipts) and verify the data on blockchain via smart contracts. 44 | 45 | The dApps frontend can easily integrate to the APIs, via [REST](https://docs.parseplatform.org/rest/guide/), [GraphQL](https://docs.parseplatform.org/graphql/guide/), or one of the technology specific [SDKs](https://parseplatform.org/#sdks). 46 | 47 | ## Getting Started 48 | 49 | ### Running an Ethereum Development Network 50 | 51 | You will need an Ethereum development network. With [Ganache](https://github.com/trufflesuite/ganache), it can be easily done using the command below: 52 | 53 | ```sh 54 | npm install ganache-cli --global 55 | ganache-cli --networkId 1000000000000 # it can be any id 56 | ``` 57 | 58 | Ganache will automatically start a development Ethereum network on your local machine which will listen on port 8545 by default. Once it is done, it will also automatically create and print a set of test accounts with 100 development ETH each. Copy the address and the private key of one of them to use for your smart contracts deployment and execution. 59 | 60 | ### Creating the Project Folder 61 | 62 | Create a folder for your project and initialize the npm package: 63 | 64 | ```sh 65 | mkdir my-project 66 | cd my-project 67 | npm init 68 | ``` 69 | 70 | Install the required packages: 71 | 72 | ```sh 73 | npm install express parse-server @parse/blockchain-base @parse/blockchain-ethereum web3 --save 74 | ``` 75 | 76 | ### Deploying the Smart Contracts 77 | 78 | The @parse/blockchain-ethereum package (installed on the previous step) contains all smart contracts that you need to deploy. We will use [Truffle](https://github.com/trufflesuite/truffle) for the deployment. 79 | 80 | First, you need to install Truffle: 81 | 82 | ``` 83 | npm install truffle @truffle/hdwallet-provider --global 84 | ``` 85 | 86 | Second, you need to create a `truffle-config.js` file in your project root folder with the following content: 87 | 88 | ```js 89 | const path = require('path'); 90 | const HDWalletProvider = require('@truffle/hdwallet-provider'); 91 | 92 | module.exports = { 93 | contracts_directory: path.resolve(__dirname, './node_modules/@parse/blockchain-ethereum/contracts'), 94 | contracts_build_directory: path.resolve(__dirname, './node_modules/@parse/blockchain-ethereum/build/contracts'), 95 | migrations_directory: path.resolve(__dirname, './node_modules/@parse/blockchain-ethereum/migrations'), 96 | networks: { 97 | parseserverblockchaindev: { 98 | provider: () => 99 | new HDWalletProvider( 100 | 'THE ACCOUNT PRIVATE KEY', // Copy to here the private key of one of your Ganache auto-generated accounts 101 | 'ws://127.0.0.1:8545' 102 | ), 103 | network_id: '1000000000000', // The same network id that you used on Ganache 104 | from: 'THE ACCOUNT ADDRESS', // Copy to here the address of one of your Ganache auto-generated accounts 105 | }, 106 | }, 107 | }; 108 | ``` 109 | 110 | Now, you are ready to deploy your smart contracts. From your project root folder, run: 111 | 112 | ```sh 113 | truffle migrate --config ./truffle-config.js --network parseserverblockchaindev 114 | ``` 115 | 116 | Truffle will deploy the smart contracts to the development Ethereum network and will print out two contract addresses (one for Parse and another for Migration). Please copy the Parse contract address. 117 | 118 | ### Running the Database 119 | 120 | We will use MongoDB as the database. An easy way to run MongoDB for development purposes is via [mongodb-runner](https://github.com/mongodb-js/runner): 121 | 122 | ```sh 123 | npm install mongodb-runner --global 124 | mongodb-runner start 125 | ``` 126 | 127 | mongodb-runner will automatically start a development MongoDB instance on your local machine which will listen on port 27017 by default. 128 | 129 | ### Running Parse Server 130 | 131 | Create an `index.js` file in your project root folder with the following content: 132 | 133 | ```js 134 | const express = require('express'); 135 | const { default: ParseServer } = require('parse-server'); 136 | const { SimpleMQAdapter, bridge, worker } = require('@parse/blockchain-base'); 137 | const { EthereumAdapter } = require('@parse/blockchain-ethereum'); 138 | const Web3 = require('web3'); 139 | 140 | const app = express(); 141 | 142 | const parseServer = new ParseServer({ 143 | serverURL: 'http://localhost:1337/parse', 144 | appId: 'someappid', 145 | masterKey: 'somemasterkey', 146 | databaseURI: 'mongodb://localhost:27017/parseserverblockchaindev', 147 | }); 148 | 149 | const mqAdapter = new SimpleMQAdapter(); 150 | 151 | const web3 = new Web3('ws://127.0.0.1:8545'); 152 | web3.eth.accounts.wallet.add( 153 | 'THE ACCOUNT PRIVATE KEY', // Copy to here the private key that you used to deploy the contracts 154 | ); 155 | 156 | bridge.initialize( 157 | ['SomeBlockchainClass'], // Pass here the name of the classes whose objects you want to send to blockchain 158 | mqAdapter 159 | ); 160 | worker.initialize( 161 | new EthereumAdapter( 162 | web3, 163 | 'THE CONTRACT ADDRESS', // Copy to here the Parse contract address that you copied after deploying it 164 | 'THE ACCOUNT ADDRESS', // Copy to here the address that you used to deploy the contracts 165 | ), 166 | mqAdapter 167 | ); 168 | 169 | app.use('/parse', parseServer.app); 170 | 171 | app.listen(1337, () => { 172 | console.log('REST API running on http://localhost:1337/parse'); 173 | }); 174 | ``` 175 | 176 | From your project root folder, start the server: 177 | 178 | ```sh 179 | node index.js 180 | ``` 181 | 182 | A Parse Server instance will start on your local machine listening on port 1337. 183 | 184 | ### Creating your first object 185 | 186 | You can easily create an object using the REST API: 187 | 188 | ```sh 189 | curl -X POST \ 190 | -H "X-Parse-Application-Id: someappid" \ 191 | -H "Content-Type: application/json" \ 192 | -d '{"someField":"some value"}' \ 193 | http://localhost:1337/parse/classes/SomeBlockchainClass 194 | ``` 195 | 196 | This object will be sent to Parse Server, which will store it on MongoDB and send to the Ethereum development network. 197 | 198 | ### Reading the object 199 | 200 | You can now query your objects using the REST API to see the status changes and the transaction receipt once it is confirmed. 201 | 202 | ```sh 203 | curl -X GET \ 204 | -H "X-Parse-Application-Id: someappid" \ 205 | http://localhost:1337/parse/classes/SomeBlockchainClass 206 | ``` 207 | 208 | ## Learn More 209 | 210 | Learn more about Parse Server and its capabilities: 211 | 212 | [Parse Platform Web-Site](https://parseplatform.org/) 213 | 214 | [Parse Server Repository](https://github.com/parse-community/parse-server) 215 | 216 | [Parse Community](https://community.parseplatform.org/) 217 | 218 | ## Feedback and Contribution 219 | 220 | This is a work in progress repository. Please let us know your feedback via issues and feel free to open a PR to improve any code or documentation. 221 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "1.0.0-alpha.1" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "author": "Parse Community ", 5 | "license": "MIT", 6 | "scripts": { 7 | "clean": "lerna clean && rm -r node_modules", 8 | "install": "husky install && lerna bootstrap", 9 | "build": "lerna run build", 10 | "test": "lerna run test --stream", 11 | "release": "lerna publish" 12 | }, 13 | "devDependencies": { 14 | "husky": "6.0.0", 15 | "lerna": "4.0.0", 16 | "parse-server": "4.10.10" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/example/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | 'eslint:recommended' 5 | ], 6 | env: { 7 | es2021: true, 8 | jest: true, 9 | node: true, 10 | }, 11 | parserOptions: { 12 | sourceType: 'module', 13 | }, 14 | plugins: ['prettier'], 15 | rules: { 16 | 'prettier/prettier': 'error' 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /packages/example/.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | -------------------------------------------------------------------------------- /packages/example/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true 3 | }; 4 | -------------------------------------------------------------------------------- /packages/example/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Parse Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "private": true, 4 | "author": "Parse Platform ", 5 | "license": "MIT", 6 | "scripts": { 7 | "lint": "eslint '{src,spec}/**/*.js' --fix", 8 | "pretest": "npm run lint", 9 | "test": "echo \"Warning: no test specified\" && exit 0", 10 | "prepare:network": "ganache-cli --networkId 1000000000000", 11 | "prepare:contract": "truffle migrate --config ./truffle-config.js --network parseserverblockchaindev", 12 | "start": "mongodb-runner start && node ./src/index.js" 13 | }, 14 | "dependencies": { 15 | "@parse/blockchain-base": "1.0.0-alpha.1", 16 | "@parse/blockchain-ethereum": "1.0.0-alpha.1", 17 | "@truffle/hdwallet-provider": "1.5.1", 18 | "express": "4.17.1", 19 | "ganache-cli": "6.12.2", 20 | "mongodb-runner": "4.8.3", 21 | "truffle": "5.4.16", 22 | "web3": "1.6.0" 23 | }, 24 | "devDependencies": { 25 | "eslint": "8.1.0", 26 | "eslint-plugin-prettier": "4.0.0", 27 | "prettier": "2.4.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/example/src/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * The following values need to be modified for this example to work. 4 | */ 5 | // Paste a public account address after running `npm run prepare:network` 6 | accountAddress: 'ACCOUNT_ADDRESS', 7 | // Paste a private account key after running `npm run prepare:network` 8 | accountPrivateKey: 'ACCOUNT_PRIVATE_KEY', 9 | // Paste the Parse contract address after running `npm run prepare:contract` 10 | contractAddress: 'CONTRACT_ADDRESS', 11 | 12 | /** 13 | * The following values do not need to be modified for this example to work. 14 | * If you do modify them, check whether they are hardcoded somewhere in 15 | * this package's npm scripts and modify them there as well. 16 | */ 17 | // The names of the classes whose objects you want to send to blockchain 18 | classNames: ['SomeBlockchainClass'], 19 | // The blockchain network ID; same as used for ganache in script `npm run prepare:network` 20 | networkId: '1000000000000', 21 | }; 22 | -------------------------------------------------------------------------------- /packages/example/src/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { default: ParseServer } = require('parse-server'); 3 | const { SimpleMQAdapter, bridge, worker } = require('@parse/blockchain-base'); 4 | const { EthereumAdapter } = require('@parse/blockchain-ethereum'); 5 | const Web3 = require('web3'); 6 | const config = require('./config'); 7 | 8 | const app = express(); 9 | 10 | const parseServer = new ParseServer({ 11 | serverURL: 'http://localhost:1337/parse', 12 | appId: 'someappid', 13 | masterKey: 'somemasterkey', 14 | databaseURI: 'mongodb://localhost:27017/parseserverblockchaindev', 15 | }); 16 | 17 | const mqAdapter = new SimpleMQAdapter(); 18 | 19 | const web3 = new Web3('ws://127.0.0.1:8545'); 20 | web3.eth.accounts.wallet.add(config.accountPrivateKey); 21 | 22 | bridge.initialize(config.classNames, mqAdapter); 23 | worker.initialize( 24 | new EthereumAdapter(web3, config.contractAddress, config.accountAddress), 25 | mqAdapter 26 | ); 27 | 28 | app.use('/parse', parseServer.app); 29 | 30 | app.listen(1337, () => { 31 | console.log('REST API running on http://localhost:1337/parse'); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/example/truffle-config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const config = require('./src/config'); 3 | const HDWalletProvider = require('@truffle/hdwallet-provider'); 4 | 5 | module.exports = { 6 | contracts_directory: path.resolve(__dirname, './node_modules/@parse/ethereum/contracts'), 7 | contracts_build_directory: path.resolve(__dirname, './node_modules/@parse/ethereum/build/contracts'), 8 | migrations_directory: path.resolve(__dirname, './node_modules/@parse/ethereum/migrations'), 9 | networks: { 10 | parseserverblockchaindev: { 11 | provider: () => 12 | new HDWalletProvider( 13 | config.accountPrivateKey, 14 | 'ws://127.0.0.1:8545' 15 | ), 16 | network_id: config.networkId, 17 | from: config.accountAddress, 18 | }, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /packages/integration/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/eslint-recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | ], 8 | env: { 9 | es2021: true, 10 | jest: true, 11 | node: true, 12 | }, 13 | globals: { 14 | Parse: true, 15 | }, 16 | parser: '@typescript-eslint/parser', 17 | parserOptions: { 18 | sourceType: 'module', 19 | }, 20 | plugins: ['prettier'], 21 | rules: { 22 | 'prettier/prettier': 'error', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /packages/integration/.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | -------------------------------------------------------------------------------- /packages/integration/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true 3 | }; 4 | -------------------------------------------------------------------------------- /packages/integration/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Parse Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/integration/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { '\\.[jt]s$': 'ts-jest' }, 3 | testEnvironment: 'node', 4 | }; 5 | -------------------------------------------------------------------------------- /packages/integration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integration", 3 | "private": true, 4 | "author": "Parse Community ", 5 | "license": "MIT", 6 | "scripts": { 7 | "lint": "eslint './**/*.{js,ts}' --fix", 8 | "start-mongodb-runner": "mongodb-runner start", 9 | "stop-mongodb-runner": "mongodb-runner stop", 10 | "jest": "jest --detectOpenHandles", 11 | "pretest": "npm run lint && npm run start-mongodb-runner", 12 | "test": "npm run jest", 13 | "posttest": "npm run stop-mongodb-runner" 14 | }, 15 | "devDependencies": { 16 | "@parse/blockchain-base": "1.0.0-alpha.1", 17 | "@parse/blockchain-ethereum": "1.0.0-alpha.1", 18 | "@types/jest": "27.0.1", 19 | "@typescript-eslint/eslint-plugin": "4.29.3", 20 | "@typescript-eslint/parser": "4.29.3", 21 | "eslint": "7.32.0", 22 | "eslint-plugin-prettier": "3.4.1", 23 | "express": "4.17.1", 24 | "ganache": "6.4.5", 25 | "jest": "27.0.6", 26 | "mongodb-runner": "4.8.3", 27 | "prettier": "2.3.2", 28 | "truffle": "5.4.7", 29 | "ts-jest": "27.0.5", 30 | "typescript": "4.3.5", 31 | "web3": "1.5.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/integration/spec/index.spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | import Web3 from 'web3'; 4 | import Parse from 'parse/node'; 5 | import * as ganache from '../support/ganache'; 6 | import * as truffle from '../support/truffle'; 7 | import * as parseServer from '../support/parseServer'; 8 | import { BlockchainStatus } from '@parse/blockchain-base'; 9 | 10 | let contractDefinition, contractABI, contractAddress; 11 | 12 | describe('Integration tests', () => { 13 | let web3, contract; 14 | 15 | beforeAll(async () => { 16 | await ganache.start(); 17 | await truffle.migrate(); 18 | await parseServer.start(); 19 | web3 = new Web3('ws://127.0.0.1:8545'); 20 | web3.eth.accounts.wallet.add( 21 | '86ae9c6148520e120a7f01ad06346a3b455ca181e7300bcede8c290d9fbfddbb' 22 | ); 23 | contractDefinition = JSON.parse( 24 | fs.readFileSync( 25 | path.resolve( 26 | __dirname, 27 | '../../parse-blockchain-ethereum/build/contracts/Parse.json' 28 | ), 29 | 'utf8' 30 | ) 31 | ); 32 | contractABI = contractDefinition.abi; 33 | contractAddress = 34 | contractDefinition.networks['1000000000000'].address.toLowerCase(); 35 | contract = new web3.eth.Contract(contractABI, contractAddress); 36 | }, 60000); 37 | 38 | afterAll(async () => { 39 | await ganache.stop(); 40 | await parseServer.stop(); 41 | }); 42 | 43 | it('should send blockchain objects to ethereum', async () => { 44 | const someObject = new Parse.Object('SomeBlockchainClass'); 45 | someObject.set('someField', 'someValue'); 46 | await someObject.save(); 47 | const someObjectFullJSON = someObject._toFullJSON(); 48 | 49 | while (someObject.get('blockchainStatus') !== BlockchainStatus.Sent) { 50 | await new Promise((resolve) => setTimeout(resolve, 1000)); 51 | await someObject.fetch(); 52 | } 53 | 54 | expect(someObject.get('someField')).toBe('someValue'); 55 | expect(someObject.get('blockchainStatus')).toBe(BlockchainStatus.Sent); 56 | const blockchainResult = someObject.get('blockchainResult'); 57 | expect(blockchainResult.type).toBe('Send'); 58 | expect(blockchainResult.input).toBe(JSON.stringify(someObjectFullJSON)); 59 | expect(blockchainResult.output.transactionHash.length).toBeGreaterThan(0); 60 | expect(blockchainResult.output.blockHash.length).toBeGreaterThan(0); 61 | expect(blockchainResult.output.from).toBe( 62 | '0xCE0C2Be1ce4FD3CA29Dc4f59Ceceed01591E204f'.toLowerCase() 63 | ); 64 | expect(blockchainResult.output.to).toBe(contractAddress); 65 | expect(Object.keys(blockchainResult.output.events)).toEqual([ 66 | 'AppCreated', 67 | 'ClassCreated', 68 | 'ObjectCreated', 69 | ]); 70 | expect(blockchainResult.output.events.AppCreated.type).toBe('mined'); 71 | expect(blockchainResult.output.events.AppCreated.returnValues._appId).toBe( 72 | 'someappid' 73 | ); 74 | expect(blockchainResult.output.events.ClassCreated.type).toBe('mined'); 75 | expect( 76 | blockchainResult.output.events.ClassCreated.returnValues._appId 77 | ).toBe('someappid'); 78 | expect( 79 | blockchainResult.output.events.ClassCreated.returnValues._className 80 | ).toBe('SomeBlockchainClass'); 81 | expect(blockchainResult.output.events.ObjectCreated.type).toBe('mined'); 82 | expect( 83 | blockchainResult.output.events.ObjectCreated.returnValues._appId 84 | ).toBe('someappid'); 85 | expect( 86 | blockchainResult.output.events.ObjectCreated.returnValues._className 87 | ).toBe('SomeBlockchainClass'); 88 | expect( 89 | blockchainResult.output.events.ObjectCreated.returnValues._objectId 90 | ).toBe(someObject.id); 91 | expect( 92 | blockchainResult.output.events.ObjectCreated.returnValues._objectJSON 93 | ).toBe( 94 | JSON.stringify({ 95 | someField: 'someValue', 96 | createdAt: someObject.createdAt, 97 | }) 98 | ); 99 | expect( 100 | await contract.methods 101 | .getObjectJSON('someappid', 'SomeBlockchainClass', someObject.id) 102 | .call() 103 | ).toBe( 104 | JSON.stringify({ 105 | someField: 'someValue', 106 | createdAt: someObject.createdAt, 107 | }) 108 | ); 109 | }); 110 | 111 | it('should send blockchain objects to ethereum also if their class has other triggers in place', async () => { 112 | const someObject = new Parse.Object('SomeBlockchainClassWithTriggers'); 113 | someObject.set('someField', 'someValue'); 114 | await someObject.save(); 115 | expect(someObject.get('someOtherField')).toBe('someOtherValue'); 116 | expect(someObject.get('someNotSavedField')).toBe('someNotSavedValue'); 117 | const someObjectFullJSON = someObject._toFullJSON(); 118 | delete someObjectFullJSON.someNotSavedField; 119 | 120 | while (someObject.get('blockchainStatus') !== BlockchainStatus.Sent) { 121 | await new Promise((resolve) => setTimeout(resolve, 1000)); 122 | await someObject.fetch(); 123 | } 124 | 125 | expect(someObject.get('someField')).toBe('someValue'); 126 | expect(someObject.get('someOtherField')).toBe('someOtherValue'); 127 | expect(someObject.get('someNotSavedField')).toBe(undefined); 128 | expect(someObject.get('blockchainStatus')).toBe(BlockchainStatus.Sent); 129 | const blockchainResult = someObject.get('blockchainResult'); 130 | expect(blockchainResult.type).toBe('Send'); 131 | expect(blockchainResult.input).toBe(JSON.stringify(someObjectFullJSON)); 132 | expect(blockchainResult.output.transactionHash.length).toBeGreaterThan(0); 133 | expect(blockchainResult.output.blockHash.length).toBeGreaterThan(0); 134 | expect(blockchainResult.output.from).toBe( 135 | '0xCE0C2Be1ce4FD3CA29Dc4f59Ceceed01591E204f'.toLowerCase() 136 | ); 137 | expect(blockchainResult.output.to).toBe(contractAddress); 138 | expect(Object.keys(blockchainResult.output.events)).toEqual([ 139 | 'ClassCreated', 140 | 'ObjectCreated', 141 | ]); 142 | expect(blockchainResult.output.events.ClassCreated.type).toBe('mined'); 143 | expect( 144 | blockchainResult.output.events.ClassCreated.returnValues._appId 145 | ).toBe('someappid'); 146 | expect( 147 | blockchainResult.output.events.ClassCreated.returnValues._className 148 | ).toBe('SomeBlockchainClassWithTriggers'); 149 | expect(blockchainResult.output.events.ObjectCreated.type).toBe('mined'); 150 | expect( 151 | blockchainResult.output.events.ObjectCreated.returnValues._appId 152 | ).toBe('someappid'); 153 | expect( 154 | blockchainResult.output.events.ObjectCreated.returnValues._className 155 | ).toBe('SomeBlockchainClassWithTriggers'); 156 | expect( 157 | blockchainResult.output.events.ObjectCreated.returnValues._objectId 158 | ).toBe(someObject.id); 159 | expect( 160 | blockchainResult.output.events.ObjectCreated.returnValues._objectJSON 161 | ).toBe( 162 | JSON.stringify({ 163 | someField: 'someValue', 164 | createdAt: someObject.createdAt, 165 | someOtherField: 'someOtherValue', 166 | }) 167 | ); 168 | expect( 169 | await contract.methods 170 | .getObjectJSON( 171 | 'someappid', 172 | 'SomeBlockchainClassWithTriggers', 173 | someObject.id 174 | ) 175 | .call() 176 | ).toBe( 177 | JSON.stringify({ 178 | someField: 'someValue', 179 | createdAt: someObject.createdAt, 180 | someOtherField: 'someOtherValue', 181 | }) 182 | ); 183 | }); 184 | 185 | it('should not send regular objects to ethereum', async () => { 186 | const someObject = new Parse.Object('SomeRegularClass'); 187 | someObject.set('someField', 'someValue'); 188 | await someObject.save(); 189 | 190 | await new Promise((resolve) => setTimeout(resolve, 3000)); 191 | await someObject.fetch(); 192 | 193 | expect(someObject.get('someField')).toBe('someValue'); 194 | expect(someObject.get('blockchainStatus')).toBe(undefined); 195 | expect(someObject.get('blockchainResult')).toBe(undefined); 196 | try { 197 | await contract.methods 198 | .getObjectJSON('someappid', 'SomeRegularClass', someObject.id) 199 | .call(); 200 | throw new Error('Should throw an error'); 201 | } catch (e) { 202 | expect(e.toString()).toMatch(/The object does not exist/); 203 | } 204 | }); 205 | 206 | it('should not send regular objects to ethereum also if their class has other triggers in place', async () => { 207 | const someObject = new Parse.Object('SomeRegularClassWithTriggers'); 208 | someObject.set('someField', 'someValue'); 209 | await someObject.save(); 210 | expect(someObject.get('someOtherField')).toBe('someOtherValue'); 211 | expect(someObject.get('someNotSavedField')).toBe('someNotSavedValue'); 212 | 213 | await new Promise((resolve) => setTimeout(resolve, 3000)); 214 | await someObject.fetch(); 215 | 216 | expect(someObject.get('someField')).toBe('someValue'); 217 | expect(someObject.get('someOtherField')).toBe('someOtherValue'); 218 | expect(someObject.get('someNotSavedField')).toBe(undefined); 219 | expect(someObject.get('blockchainStatus')).toBe(undefined); 220 | expect(someObject.get('blockchainResult')).toBe(undefined); 221 | try { 222 | await contract.methods 223 | .getObjectJSON( 224 | 'someappid', 225 | 'SomeRegularClassWithTriggers', 226 | someObject.id 227 | ) 228 | .call(); 229 | throw new Error('Should throw an error'); 230 | } catch (e) { 231 | expect(e.toString()).toMatch(/The object does not exist/); 232 | } 233 | }); 234 | 235 | it('should not be able to create new blockchain objects with blockchainStatus or blockchainResult', async () => { 236 | const someObject1 = new Parse.Object('SomeBlockchainClass'); 237 | someObject1.set('blockchainStatus', BlockchainStatus.Sending); 238 | try { 239 | await someObject1.save(); 240 | throw new Error('Should throw an error'); 241 | } catch (e) { 242 | expect(e.toString()).toMatch( 243 | /unauthorized: cannot set blockchainStatus field/ 244 | ); 245 | } 246 | try { 247 | await someObject1.save(null, { useMasterKey: true }); 248 | throw new Error('Should throw an error'); 249 | } catch (e) { 250 | expect(e.toString()).toMatch( 251 | /unauthorized: cannot set blockchainStatus field/ 252 | ); 253 | } 254 | const someObject2 = new Parse.Object('SomeBlockchainClass'); 255 | someObject2.set('blockchainResult', {}); 256 | try { 257 | await someObject2.save(); 258 | throw new Error('Should throw an error'); 259 | } catch (e) { 260 | expect(e.toString()).toMatch( 261 | /unauthorized: cannot set blockchainResult field/ 262 | ); 263 | } 264 | try { 265 | await someObject2.save(null, { useMasterKey: true }); 266 | throw new Error('Should throw an error'); 267 | } catch (e) { 268 | expect(e.toString()).toMatch( 269 | /unauthorized: cannot set blockchainResult field/ 270 | ); 271 | } 272 | }); 273 | 274 | it('should not be able to create new regular objects with blockchainStatus or blockchainResult', async () => { 275 | const someObject1 = new Parse.Object('SomeRegularClass'); 276 | someObject1.set('blockchainStatus', BlockchainStatus.Sending); 277 | await someObject1.save(); 278 | await someObject1.fetch(); 279 | expect(someObject1.get('blockchainStatus')).toBe(BlockchainStatus.Sending); 280 | const someObject2 = new Parse.Object('SomeRegularClass'); 281 | someObject2.set('blockchainStatus', BlockchainStatus.Sending); 282 | await someObject2.save(null, { useMasterKey: true }); 283 | await someObject2.fetch(); 284 | expect(someObject2.get('blockchainStatus')).toBe(BlockchainStatus.Sending); 285 | const someObject3 = new Parse.Object('SomeRegularClass'); 286 | someObject3.set('blockchainResult', { someField: 'someValue' }); 287 | await someObject3.save(); 288 | await someObject3.fetch(); 289 | expect(someObject3.get('blockchainResult')).toEqual({ 290 | someField: 'someValue', 291 | }); 292 | const someObject4 = new Parse.Object('SomeRegularClass'); 293 | someObject4.set('blockchainResult', { someField: 'someValue' }); 294 | await someObject4.save(null, { useMasterKey: true }); 295 | await someObject4.fetch(); 296 | expect(someObject4.get('blockchainResult')).toEqual({ 297 | someField: 'someValue', 298 | }); 299 | const someObject5 = new Parse.Object('SomeRegularClassWithTriggers'); 300 | someObject5.set('blockchainStatus', BlockchainStatus.Sending); 301 | await someObject5.save(); 302 | await someObject5.fetch(); 303 | expect(someObject5.get('blockchainStatus')).toBe(BlockchainStatus.Sending); 304 | const someObject6 = new Parse.Object('SomeRegularClassWithTriggers'); 305 | someObject6.set('blockchainStatus', BlockchainStatus.Sending); 306 | await someObject6.save(null, { useMasterKey: true }); 307 | await someObject6.fetch(); 308 | expect(someObject6.get('blockchainStatus')).toBe(BlockchainStatus.Sending); 309 | const someObject7 = new Parse.Object('SomeRegularClassWithTriggers'); 310 | someObject7.set('blockchainResult', { someField: 'someValue' }); 311 | await someObject7.save(); 312 | await someObject7.fetch(); 313 | expect(someObject7.get('blockchainResult')).toEqual({ 314 | someField: 'someValue', 315 | }); 316 | const someObject8 = new Parse.Object('SomeRegularClassWithTriggers'); 317 | someObject8.set('blockchainResult', { someField: 'someValue' }); 318 | await someObject8.save(null, { useMasterKey: true }); 319 | await someObject8.fetch(); 320 | expect(someObject8.get('blockchainResult')).toEqual({ 321 | someField: 'someValue', 322 | }); 323 | }); 324 | 325 | it('should not be able to change blockchain objects', async () => { 326 | const someObject1 = ( 327 | await new Parse.Query('SomeBlockchainClass').find() 328 | )[0]; 329 | someObject1.set('someField', 'someOtherValue'); 330 | try { 331 | await someObject1.save(); 332 | throw new Error('Should throw an error'); 333 | } catch (e) { 334 | expect(e.toString()).toMatch( 335 | /unauthorized: cannot update objects on blockchain bridge/ 336 | ); 337 | } 338 | try { 339 | await someObject1.save(null, { useMasterKey: true }); 340 | throw new Error('Should throw an error'); 341 | } catch (e) { 342 | expect(e.toString()).toMatch( 343 | /unauthorized: cannot update objects on blockchain bridge/ 344 | ); 345 | } 346 | const someObject2 = ( 347 | await new Parse.Query('SomeBlockchainClassWithTriggers').find() 348 | )[0]; 349 | someObject2.set('someField', 'someOtherValue'); 350 | try { 351 | await someObject2.save(); 352 | throw new Error('Should throw an error'); 353 | } catch (e) { 354 | expect(e.toString()).toMatch( 355 | /unauthorized: cannot update objects on blockchain bridge/ 356 | ); 357 | } 358 | try { 359 | await someObject2.save(null, { useMasterKey: true }); 360 | throw new Error('Should throw an error'); 361 | } catch (e) { 362 | expect(e.toString()).toMatch( 363 | /unauthorized: cannot update objects on blockchain bridge/ 364 | ); 365 | } 366 | }); 367 | 368 | it('should be able to change regular objects', async () => { 369 | const someObject1 = (await new Parse.Query('SomeRegularClass').find())[0]; 370 | someObject1.set('someField', 'someOtherValue'); 371 | await someObject1.save(); 372 | await someObject1.fetch(); 373 | expect(someObject1.get('someField')).toBe('someOtherValue'); 374 | someObject1.set('someField', 'someOtherValue2'); 375 | await someObject1.save(null, { useMasterKey: true }); 376 | await someObject1.fetch(); 377 | expect(someObject1.get('someField')).toBe('someOtherValue2'); 378 | const someObject2 = ( 379 | await new Parse.Query('SomeRegularClassWithTriggers').find() 380 | )[0]; 381 | someObject2.set('someField', 'someOtherValue'); 382 | await someObject2.save(); 383 | await someObject2.fetch(); 384 | expect(someObject2.get('someField')).toBe('someOtherValue'); 385 | someObject2.set('someField', 'someOtherValue2'); 386 | await someObject2.save(null, { useMasterKey: true }); 387 | await someObject2.fetch(); 388 | expect(someObject2.get('someField')).toBe('someOtherValue2'); 389 | }); 390 | 391 | it('should not be able to delete blockchain objects', async () => { 392 | const someObject1 = ( 393 | await new Parse.Query('SomeBlockchainClass').find() 394 | )[0]; 395 | try { 396 | await someObject1.destroy(); 397 | throw new Error('Should throw an error'); 398 | } catch (e) { 399 | expect(e.toString()).toMatch( 400 | /unauthorized: cannot delete objects on blockchain bridge/ 401 | ); 402 | } 403 | try { 404 | await someObject1.destroy({ useMasterKey: true }); 405 | throw new Error('Should throw an error'); 406 | } catch (e) { 407 | expect(e.toString()).toMatch( 408 | /unauthorized: cannot delete objects on blockchain bridge/ 409 | ); 410 | } 411 | const someObject2 = ( 412 | await new Parse.Query('SomeBlockchainClassWithTriggers').find() 413 | )[0]; 414 | try { 415 | await someObject2.destroy(); 416 | throw new Error('Should throw an error'); 417 | } catch (e) { 418 | expect(e.toString()).toMatch( 419 | /unauthorized: cannot delete objects on blockchain bridge/ 420 | ); 421 | } 422 | try { 423 | await someObject2.destroy({ useMasterKey: true }); 424 | throw new Error('Should throw an error'); 425 | } catch (e) { 426 | expect(e.toString()).toMatch( 427 | /unauthorized: cannot delete objects on blockchain bridge/ 428 | ); 429 | } 430 | }); 431 | 432 | it('should be able to delete regular objects', async () => { 433 | const someObject1 = (await new Parse.Query('SomeRegularClass').find())[0]; 434 | await someObject1.destroy(); 435 | try { 436 | await someObject1.fetch(); 437 | throw new Error('Should throw an error'); 438 | } catch (e) { 439 | expect(e.toString()).toMatch(/Object not found/); 440 | } 441 | const someObject2 = (await new Parse.Query('SomeRegularClass').find())[0]; 442 | await someObject2.destroy({ useMasterKey: true }); 443 | try { 444 | await someObject2.fetch(); 445 | throw new Error('Should throw an error'); 446 | } catch (e) { 447 | expect(e.toString()).toMatch(/Object not found/); 448 | } 449 | const someObject3 = ( 450 | await new Parse.Query('SomeRegularClassWithTriggers').find() 451 | )[0]; 452 | try { 453 | await someObject3.destroy(); 454 | throw new Error('Should throw an error'); 455 | } catch (e) { 456 | expect(e.toString()).toMatch(/confirmDelete is not true/); 457 | } 458 | someObject3.set('confirmDelete', true); 459 | await someObject3.save(); 460 | await someObject3.destroy(); 461 | try { 462 | await someObject3.fetch(); 463 | throw new Error('Should throw an error'); 464 | } catch (e) { 465 | expect(e.toString()).toMatch(/Object not found/); 466 | } 467 | const someObject4 = ( 468 | await new Parse.Query('SomeRegularClassWithTriggers').find() 469 | )[0]; 470 | try { 471 | await someObject4.destroy({ useMasterKey: true }); 472 | throw new Error('Should throw an error'); 473 | } catch (e) { 474 | expect(e.toString()).toMatch(/confirmDelete is not true/); 475 | } 476 | someObject4.set('confirmDelete', true); 477 | await someObject4.save(); 478 | await someObject4.destroy({ useMasterKey: true }); 479 | try { 480 | await someObject4.fetch(); 481 | throw new Error('Should throw an error'); 482 | } catch (e) { 483 | expect(e.toString()).toMatch(/Object not found/); 484 | } 485 | }); 486 | }); 487 | -------------------------------------------------------------------------------- /packages/integration/support/ganache/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { fork } from 'child_process'; 3 | 4 | let ganacheServer; 5 | 6 | export function start(): Promise { 7 | return new Promise((resolve, reject) => { 8 | try { 9 | ganacheServer = fork(path.resolve(__dirname, './server.js')); 10 | 11 | ganacheServer.on('message', (message) => { 12 | if (message === true) { 13 | resolve(); 14 | } else { 15 | console.log(message); 16 | } 17 | }); 18 | 19 | ganacheServer.on('error', (error) => { 20 | reject(error); 21 | }); 22 | } catch (e) { 23 | reject(e); 24 | } 25 | }); 26 | } 27 | 28 | export function stop(): Promise { 29 | return new Promise((resolve) => { 30 | if (ganacheServer) { 31 | ganacheServer.on('close', () => { 32 | resolve(); 33 | }); 34 | ganacheServer.kill(); 35 | } else { 36 | resolve(); 37 | } 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /packages/integration/support/ganache/server.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const ganache = require('ganache-cli'); 3 | 4 | const ganacheServer = ganache.server({ 5 | network_id: '1000000000000', 6 | accounts: [ 7 | { 8 | secretKey: 9 | '0x86ae9c6148520e120a7f01ad06346a3b455ca181e7300bcede8c290d9fbfddbb', 10 | balance: '0x100000000000000000000', 11 | }, 12 | ], 13 | secure: true, 14 | }); 15 | try { 16 | ganacheServer.listen(8545, (error) => { 17 | if (error) { 18 | console.error(error); 19 | process.exit(1); 20 | } else { 21 | process.send(true); 22 | } 23 | }); 24 | } catch (e) { 25 | console.error(e); 26 | process.exit(1); 27 | } 28 | -------------------------------------------------------------------------------- /packages/integration/support/parseServer/cloud/main.js: -------------------------------------------------------------------------------- 1 | Parse.Cloud.beforeSave('SomeBlockchainClassWithTriggers', ({ object }) => { 2 | object.set('someOtherField', 'someOtherValue'); 3 | return object; 4 | }); 5 | 6 | Parse.Cloud.afterSave('SomeBlockchainClassWithTriggers', ({ object }) => { 7 | object.set('someNotSavedField', 'someNotSavedValue'); 8 | return object; 9 | }); 10 | 11 | Parse.Cloud.beforeDelete('SomeBlockchainClassWithTriggers', () => { 12 | throw new Error('Should never file'); 13 | }); 14 | 15 | Parse.Cloud.beforeSave('SomeRegularClassWithTriggers', ({ object }) => { 16 | object.set('someOtherField', 'someOtherValue'); 17 | return object; 18 | }); 19 | 20 | Parse.Cloud.afterSave('SomeRegularClassWithTriggers', ({ object }) => { 21 | object.set('someNotSavedField', 'someNotSavedValue'); 22 | return object; 23 | }); 24 | 25 | Parse.Cloud.beforeDelete('SomeRegularClassWithTriggers', ({ object }) => { 26 | if (object.get('confirmDelete') !== true) { 27 | throw new Error('confirmDelete is not true'); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /packages/integration/support/parseServer/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | import ParseServer from 'parse-server'; 4 | import express from 'express'; 5 | import Web3 from 'web3'; 6 | import { SimpleMQAdapter, bridge, worker } from '@parse/blockchain-base'; 7 | import { EthereumAdapter } from '@parse/blockchain-ethereum'; 8 | 9 | let parseServer; 10 | let expressServer; 11 | 12 | export async function start(): Promise { 13 | await new Promise((resolve, reject) => { 14 | try { 15 | parseServer = new ParseServer({ 16 | serverURL: 'http://localhost:1337/parse', 17 | appId: 'someappid', 18 | javascriptKey: 'somejavascriptkey', 19 | masterKey: 'somemasterkey', 20 | databaseURI: 'mongodb://localhost:27017/blockchain-integration', 21 | cloud: './support/parseServer/cloud/main.js', 22 | serverStartComplete: (error) => { 23 | if (error) { 24 | reject(error); 25 | } else { 26 | resolve(); 27 | } 28 | }, 29 | }); 30 | } catch (e) { 31 | reject(e); 32 | } 33 | }); 34 | 35 | const mqAdapter = new SimpleMQAdapter(); 36 | 37 | const web3 = new Web3('ws://127.0.0.1:8545'); 38 | web3.eth.accounts.wallet.add( 39 | '86ae9c6148520e120a7f01ad06346a3b455ca181e7300bcede8c290d9fbfddbb' 40 | ); 41 | 42 | bridge.initialize( 43 | ['SomeBlockchainClass', 'SomeBlockchainClassWithTriggers'], 44 | mqAdapter 45 | ); 46 | const contract = JSON.parse( 47 | fs.readFileSync( 48 | path.resolve( 49 | __dirname, 50 | '../../../parse-blockchain-ethereum/build/contracts/Parse.json' 51 | ), 52 | 'utf8' 53 | ) 54 | ); 55 | worker.initialize( 56 | new EthereumAdapter( 57 | web3, 58 | contract.networks['1000000000000'].address, 59 | '0xCE0C2Be1ce4FD3CA29Dc4f59Ceceed01591E204f' 60 | ), 61 | mqAdapter 62 | ); 63 | 64 | const app = express(); 65 | 66 | app.use('/parse', parseServer.app); 67 | 68 | return new Promise((resolve, reject) => { 69 | try { 70 | expressServer = app.listen(1337, resolve); 71 | } catch (e) { 72 | reject(e); 73 | } 74 | }); 75 | } 76 | 77 | export function stop(): Promise<[void, void]> { 78 | const parseServerPromise = 79 | (parseServer && parseServer.handleShutdown()) || Promise.resolve(); 80 | 81 | const expressServerPromise = 82 | (expressServer && 83 | new Promise((resolve, reject) => { 84 | try { 85 | expressServer.close(resolve); 86 | } catch (e) { 87 | reject(e); 88 | } 89 | })) || 90 | Promise.resolve(); 91 | 92 | return Promise.all([parseServerPromise, expressServerPromise]); 93 | } 94 | -------------------------------------------------------------------------------- /packages/integration/support/truffle/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { exec } from 'child_process'; 3 | 4 | export function migrate(): Promise { 5 | return new Promise((resolve, reject) => { 6 | try { 7 | exec( 8 | `${path.resolve( 9 | __dirname, 10 | '../../node_modules/.bin/truffle' 11 | )} migrate --config ${path.resolve( 12 | __dirname, 13 | 'truffle-config.js' 14 | )} --network integrationtest`, 15 | (error, stdout, stderr) => { 16 | if (stdout) { 17 | console.log(stdout); 18 | } 19 | if (stderr) { 20 | console.error(stderr); 21 | } 22 | if (error) { 23 | reject(error); 24 | } else { 25 | resolve(); 26 | } 27 | } 28 | ); 29 | } catch (e) { 30 | reject(e); 31 | } 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /packages/integration/support/truffle/truffle-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this file to configure your truffle project. It's seeded with some 3 | * common settings for different networks and features like migrations, 4 | * compilation and testing. Uncomment the ones you need or modify 5 | * them to suit your project as necessary. 6 | * 7 | * More information about configuration can be found at: 8 | * 9 | * trufflesuite.com/docs/advanced/configuration 10 | * 11 | * To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider) 12 | * to sign your transactions before they're sent to a remote public node. Infura accounts 13 | * are available for free at: infura.io/register. 14 | * 15 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate 16 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this 17 | * phrase from a file you've .gitignored so it doesn't accidentally become public. 18 | * 19 | */ 20 | 21 | // eslint-disable-next-line @typescript-eslint/no-var-requires 22 | const path = require('path'); 23 | // eslint-disable-next-line @typescript-eslint/no-var-requires 24 | const HDWalletProvider = require('@truffle/hdwallet-provider'); 25 | // 26 | // const fs = require('fs'); 27 | // const mnemonic = fs.readFileSync(".secret").toString().trim(); 28 | 29 | module.exports = { 30 | contracts_directory: path.resolve( 31 | __dirname, 32 | '../../../parse-blockchain-ethereum/contracts' 33 | ), 34 | contracts_build_directory: path.resolve( 35 | __dirname, 36 | '../../../parse-blockchain-ethereum/build/contracts' 37 | ), 38 | migrations_directory: path.resolve( 39 | __dirname, 40 | '../../../parse-blockchain-ethereum/migrations' 41 | ), 42 | 43 | /** 44 | * Networks define how you connect to your ethereum client and let you set the 45 | * defaults web3 uses to send transactions. If you don't specify one truffle 46 | * will spin up a development blockchain for you on port 9545 when you 47 | * run `develop` or `test`. You can ask a truffle command to use a specific 48 | * network from the command line, e.g 49 | * 50 | * $ truffle test --network 51 | */ 52 | 53 | networks: { 54 | // Useful for testing. The `development` name is special - truffle uses it by default 55 | // if it's defined here and no other network is specified at the command line. 56 | // You should run a client (like ganache-cli, geth or parity) in a separate terminal 57 | // tab if you use this network and you must also set the `host`, `port` and `network_id` 58 | // options below to some value. 59 | // 60 | // development: { 61 | // host: "127.0.0.1", // Localhost (default: none) 62 | // port: 8545, // Standard Ethereum port (default: none) 63 | // network_id: "*", // Any network (default: none) 64 | // }, 65 | // Another network with more advanced options... 66 | // advanced: { 67 | // port: 8777, // Custom port 68 | // network_id: 1342, // Custom network 69 | // gas: 8500000, // Gas sent with each transaction (default: ~6700000) 70 | // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) 71 | // from:
, // Account to send txs from (default: accounts[0]) 72 | // websocket: true // Enable EventEmitter interface for web3 (default: false) 73 | // }, 74 | // Useful for deploying to a public network. 75 | // NB: It's important to wrap the provider as a function. 76 | // ropsten: { 77 | // provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`), 78 | // network_id: 3, // Ropsten's id 79 | // gas: 5500000, // Ropsten has a lower block limit than mainnet 80 | // confirmations: 2, // # of confs to wait between deployments. (default: 0) 81 | // timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) 82 | // skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) 83 | // }, 84 | // Useful for private networks 85 | // private: { 86 | // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), 87 | // network_id: 2111, // This network is yours, in the cloud. 88 | // production: true // Treats this network as if it was a public net. (default: false) 89 | // } 90 | integrationtest: { 91 | provider: () => 92 | new HDWalletProvider( 93 | '86ae9c6148520e120a7f01ad06346a3b455ca181e7300bcede8c290d9fbfddbb', 94 | 'ws://127.0.0.1:8545' 95 | ), 96 | network_id: '1000000000000', 97 | from: '0xCE0C2Be1ce4FD3CA29Dc4f59Ceceed01591E204f', 98 | }, 99 | }, 100 | 101 | // Set default mocha options here, use special reporters etc. 102 | mocha: { 103 | // timeout: 100000 104 | }, 105 | 106 | // Configure your compilers 107 | compilers: { 108 | solc: { 109 | // version: "0.5.1", // Fetch exact version from solc-bin (default: truffle's version) 110 | // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) 111 | // settings: { // See the solidity docs for advice about optimization and evmVersion 112 | // optimizer: { 113 | // enabled: false, 114 | // runs: 200 115 | // }, 116 | // evmVersion: "byzantium" 117 | // } 118 | }, 119 | }, 120 | 121 | // Truffle DB is currently disabled by default; to enable it, change enabled: false to enabled: true 122 | // 123 | // Note: if you migrated your contracts prior to enabling this field in your Truffle project and want 124 | // those previously migrated contracts available in the .db directory, you will need to run the following: 125 | // $ truffle migrate --reset --compile-all 126 | 127 | db: { 128 | enabled: false, 129 | }, 130 | }; 131 | -------------------------------------------------------------------------------- /packages/integration/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./"], 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "allowJs": true, 6 | "esModuleInterop": true, 7 | "skipLibCheck": true 8 | }, 9 | "typeAcquisition": { "include": ["jest"] } 10 | } 11 | -------------------------------------------------------------------------------- /packages/parse-blockchain-base/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/eslint-recommended', 6 | 'plugin:@typescript-eslint/recommended' 7 | ], 8 | env: { 9 | es2021: true, 10 | jest: true, 11 | node: true, 12 | }, 13 | parser: '@typescript-eslint/parser', 14 | parserOptions: { 15 | sourceType: 'module', 16 | }, 17 | plugins: ['prettier'], 18 | rules: { 19 | 'prettier/prettier': 'error' 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /packages/parse-blockchain-base/.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | coverage 3 | logs 4 | -------------------------------------------------------------------------------- /packages/parse-blockchain-base/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true 3 | }; 4 | -------------------------------------------------------------------------------- /packages/parse-blockchain-base/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Parse Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/parse-blockchain-base/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: {'\\.[jt]s$': 'ts-jest'}, 3 | testEnvironment: 'node' 4 | }; 5 | -------------------------------------------------------------------------------- /packages/parse-blockchain-base/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@parse/blockchain-base", 3 | "publishConfig": { 4 | "access": "public" 5 | }, 6 | "version": "1.0.0-alpha.1", 7 | "author": "Parse Community ", 8 | "description": "Connects Parse Server to blockchain networks.", 9 | "keywords": [ 10 | "parse", 11 | "parse server", 12 | "blockchain" 13 | ], 14 | "homepage": "https://parseplatform.org", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/parse-community/parse-server-blockchain", 18 | "directory": "packages/parse-blockchain-base" 19 | }, 20 | "bugs": "https://github.com/parse-community/parse-server-blockchain/issues", 21 | "license": "MIT", 22 | "main": "lib/index.js", 23 | "files": [ 24 | "lib" 25 | ], 26 | "scripts": { 27 | "build": "tsc --build ./tsconfig.json", 28 | "prepare": "npm run build", 29 | "lint": "eslint '{src,spec}/**/*.{js,ts}' --fix", 30 | "pretest": "npm run lint && npm run build", 31 | "test": "jest --coverage" 32 | }, 33 | "devDependencies": { 34 | "@types/jest": "26.0.23", 35 | "@typescript-eslint/eslint-plugin": "4.28.4", 36 | "@typescript-eslint/parser": "4.27.0", 37 | "eslint": "7.27.0", 38 | "eslint-plugin-prettier": "3.4.0", 39 | "jest": "27.0.3", 40 | "prettier": "2.3.0", 41 | "ts-jest": "27.0.2", 42 | "typescript": "4.3.2" 43 | }, 44 | "gitHead": "c7caf1eb3da898c60d9f13f765bd432f05063127" 45 | } 46 | -------------------------------------------------------------------------------- /packages/parse-blockchain-base/spec/Bridge.spec.ts: -------------------------------------------------------------------------------- 1 | import { Parse } from 'parse/node'; 2 | 3 | global.Parse = Parse; 4 | 5 | import * as triggers from 'parse-server/lib/triggers'; 6 | import { BlockchainStatus } from '../src/types'; 7 | import MQAdapter, { Listener, Subscription } from '../src/MQAdapter'; 8 | import SimpleMQAdapter from '../src/SimpleMQAdapter'; 9 | import Bridge from '../src/Bridge'; 10 | 11 | describe('Bridge', () => { 12 | beforeAll(() => { 13 | Parse.initialize('someappid'); 14 | }); 15 | 16 | describe('initialize', () => { 17 | it('should initialize', () => { 18 | new Bridge().initialize([]); 19 | }); 20 | 21 | it('should not initialize twice', () => { 22 | const bridge = new Bridge(); 23 | bridge.initialize([]); 24 | expect(() => bridge.initialize([])).toThrowError( 25 | 'The bridge is already initialized' 26 | ); 27 | }); 28 | 29 | it('should initialize with custom adapter', () => { 30 | class FakeAdapter implements MQAdapter { 31 | publish: (queue: string, message: string) => void; 32 | consume: (queue: string, listener: Listener) => Subscription; 33 | } 34 | 35 | const fakeAdapter = new FakeAdapter(); 36 | 37 | const bridge = new Bridge(); 38 | bridge.initialize([], fakeAdapter); 39 | 40 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 41 | expect((bridge as any).mqAdapter).toBe(fakeAdapter); 42 | }); 43 | }); 44 | 45 | describe('handleGetTrigger', () => { 46 | it('should return default before delete handler on blockchain classes', () => { 47 | const simpleMQAdapter = new SimpleMQAdapter(); 48 | 49 | const bridge = new Bridge(); 50 | bridge.initialize(['SomeClass'], simpleMQAdapter); 51 | 52 | expect( 53 | triggers.getTrigger( 54 | 'SomeClass', 55 | triggers.Types.beforeDelete, 56 | Parse.applicationId 57 | ) 58 | ).toThrow(/unauthorized: cannot delete objects on blockchain bridge/); 59 | }); 60 | 61 | it('should run original function otherwise', () => { 62 | const simpleMQAdapter = new SimpleMQAdapter(); 63 | 64 | const bridge = new Bridge(); 65 | bridge.initialize(['SomeClass'], simpleMQAdapter); 66 | 67 | expect( 68 | triggers.getTrigger( 69 | 'SomeOtherClass', 70 | triggers.Types.beforeDelete, 71 | Parse.applicationId 72 | ) 73 | ).toBe(undefined); 74 | expect( 75 | triggers.getTrigger( 76 | 'SomeClass', 77 | triggers.Types.afterSave, 78 | Parse.applicationId 79 | ) 80 | ).toBe(undefined); 81 | }); 82 | }); 83 | 84 | describe('handleTriggerExists', () => { 85 | it('should return true for beforeSave, afterSave, and afterDelete triggers on blockchain classes', () => { 86 | const simpleMQAdapter = new SimpleMQAdapter(); 87 | 88 | const bridge = new Bridge(); 89 | bridge.initialize(['SomeClass'], simpleMQAdapter); 90 | 91 | expect( 92 | triggers.triggerExists( 93 | 'SomeClass', 94 | triggers.Types.beforeSave, 95 | Parse.applicationId 96 | ) 97 | ).toBe(true); 98 | expect( 99 | triggers.triggerExists( 100 | 'SomeClass', 101 | triggers.Types.afterSave, 102 | Parse.applicationId 103 | ) 104 | ).toBe(true); 105 | expect( 106 | triggers.triggerExists( 107 | 'SomeClass', 108 | triggers.Types.beforeDelete, 109 | Parse.applicationId 110 | ) 111 | ).toBe(true); 112 | }); 113 | 114 | it('should return false for beforeSave, afterSave, and afterDelete triggers on regular classes without triggers', () => { 115 | const simpleMQAdapter = new SimpleMQAdapter(); 116 | 117 | const bridge = new Bridge(); 118 | bridge.initialize(['SomeClass'], simpleMQAdapter); 119 | 120 | expect( 121 | triggers.triggerExists( 122 | 'SomeOtherClass', 123 | triggers.Types.beforeSave, 124 | Parse.applicationId 125 | ) 126 | ).toBe(false); 127 | expect( 128 | triggers.triggerExists( 129 | 'SomeOtherClass', 130 | triggers.Types.afterSave, 131 | Parse.applicationId 132 | ) 133 | ).toBe(false); 134 | expect( 135 | triggers.triggerExists( 136 | 'SomeOtherClass', 137 | triggers.Types.beforeDelete, 138 | Parse.applicationId 139 | ) 140 | ).toBe(false); 141 | }); 142 | }); 143 | 144 | describe('handleMaybeRunTrigger', () => { 145 | it('should publish new blockchain classes objects on after save', async () => { 146 | const simpleMQAdapter = new SimpleMQAdapter(); 147 | 148 | const bridge = new Bridge(); 149 | bridge.initialize(['SomeClass'], simpleMQAdapter); 150 | 151 | let published = false; 152 | 153 | simpleMQAdapter.consume( 154 | `${Parse.applicationId}-parse-server-blockchain`, 155 | (message, ack) => { 156 | expect(JSON.parse(message)).toEqual({ 157 | className: 'SomeClass', 158 | __type: 'Object', 159 | objectId: 'someid', 160 | }); 161 | ack(); 162 | published = true; 163 | } 164 | ); 165 | 166 | const someObject = new Parse.Object('SomeClass'); 167 | someObject.id = 'someid'; 168 | 169 | await triggers.maybeRunTrigger( 170 | triggers.Types.afterSave, 171 | {}, 172 | someObject, 173 | null, 174 | { 175 | applicationId: Parse.applicationId, 176 | } 177 | ); 178 | 179 | expect(published).toBeTruthy(); 180 | }); 181 | 182 | it('should not publish new regular classes objects on after save', async () => { 183 | const simpleMQAdapter = new SimpleMQAdapter(); 184 | 185 | const bridge = new Bridge(); 186 | bridge.initialize(['SomeClass'], simpleMQAdapter); 187 | 188 | simpleMQAdapter.consume( 189 | `${Parse.applicationId}-parse-server-blockchain`, 190 | () => { 191 | throw new Error('Should not receive message'); 192 | } 193 | ); 194 | 195 | const someObject = new Parse.Object('SomeOtherClass'); 196 | someObject.id = 'someid'; 197 | 198 | await triggers.maybeRunTrigger( 199 | triggers.Types.afterSave, 200 | {}, 201 | someObject, 202 | null, 203 | { 204 | applicationId: Parse.applicationId, 205 | }, 206 | {} 207 | ); 208 | }); 209 | 210 | it('should throw error for new blockchain classes objects with blockchainStatus on before save', async () => { 211 | const simpleMQAdapter = new SimpleMQAdapter(); 212 | 213 | const bridge = new Bridge(); 214 | bridge.initialize(['SomeClass'], simpleMQAdapter); 215 | 216 | simpleMQAdapter.consume( 217 | `${Parse.applicationId}-parse-server-blockchain`, 218 | () => { 219 | throw new Error('Should not receive message'); 220 | } 221 | ); 222 | 223 | const someObject = new Parse.Object('SomeClass'); 224 | someObject.id = 'someid'; 225 | someObject.set('blockchainStatus', BlockchainStatus.Sending); 226 | 227 | try { 228 | await triggers.maybeRunTrigger( 229 | triggers.Types.beforeSave, 230 | {}, 231 | someObject, 232 | null, 233 | { 234 | applicationId: Parse.applicationId, 235 | }, 236 | {} 237 | ); 238 | throw new Error('Should throw error'); 239 | } catch (e) { 240 | expect(e).toBeInstanceOf(Parse.Error); 241 | expect(e.code).toBe(Parse.Error.OPERATION_FORBIDDEN); 242 | expect(e.message).toBe( 243 | 'unauthorized: cannot set blockchainStatus field' 244 | ); 245 | } 246 | }); 247 | 248 | it('should throw error for new blockchain classes objects with blockchainResult on before save', async () => { 249 | const simpleMQAdapter = new SimpleMQAdapter(); 250 | 251 | const bridge = new Bridge(); 252 | bridge.initialize(['SomeClass'], simpleMQAdapter); 253 | 254 | simpleMQAdapter.consume( 255 | `${Parse.applicationId}-parse-server-blockchain`, 256 | () => { 257 | throw new Error('Should not receive message'); 258 | } 259 | ); 260 | 261 | const someObject = new Parse.Object('SomeClass'); 262 | someObject.id = 'someid'; 263 | someObject.set('blockchainResult', {}); 264 | 265 | try { 266 | await triggers.maybeRunTrigger( 267 | triggers.Types.beforeSave, 268 | {}, 269 | someObject, 270 | null, 271 | { 272 | applicationId: Parse.applicationId, 273 | }, 274 | {} 275 | ); 276 | throw new Error('Should throw error'); 277 | } catch (e) { 278 | expect(e).toBeInstanceOf(Parse.Error); 279 | expect(e.code).toBe(Parse.Error.OPERATION_FORBIDDEN); 280 | expect(e.message).toBe( 281 | 'unauthorized: cannot set blockchainResult field' 282 | ); 283 | } 284 | }); 285 | 286 | it('should not throw error for new blockchain classes objects when not setting blockchainStatus nor blockchainResult on before save', async () => { 287 | const simpleMQAdapter = new SimpleMQAdapter(); 288 | 289 | const bridge = new Bridge(); 290 | bridge.initialize(['SomeClass'], simpleMQAdapter); 291 | 292 | simpleMQAdapter.consume( 293 | `${Parse.applicationId}-parse-server-blockchain`, 294 | () => { 295 | throw new Error('Should not receive message'); 296 | } 297 | ); 298 | 299 | const someObject = new Parse.Object('SomeClass'); 300 | someObject.id = 'someid'; 301 | 302 | await triggers.maybeRunTrigger( 303 | triggers.Types.beforeSave, 304 | {}, 305 | someObject, 306 | null, 307 | { 308 | applicationId: Parse.applicationId, 309 | }, 310 | {} 311 | ); 312 | }); 313 | 314 | it('should throw error for existing blockchain classes objects on before save', async () => { 315 | const simpleMQAdapter = new SimpleMQAdapter(); 316 | 317 | const bridge = new Bridge(); 318 | bridge.initialize(['SomeClass'], simpleMQAdapter); 319 | 320 | simpleMQAdapter.consume( 321 | `${Parse.applicationId}-parse-server-blockchain`, 322 | () => { 323 | throw new Error('Should not receive message'); 324 | } 325 | ); 326 | 327 | const someObject = new Parse.Object('SomeClass'); 328 | someObject.id = 'someid'; 329 | 330 | try { 331 | await triggers.maybeRunTrigger( 332 | triggers.Types.beforeSave, 333 | {}, 334 | someObject, 335 | Object.assign(someObject, { 336 | fetch: () => Promise.resolve(), 337 | }), 338 | { 339 | applicationId: Parse.applicationId, 340 | }, 341 | {} 342 | ); 343 | throw new Error('Should throw error'); 344 | } catch (e) { 345 | expect(e).toBeInstanceOf(Parse.Error); 346 | expect(e.code).toBe(Parse.Error.OPERATION_FORBIDDEN); 347 | expect(e.message).toBe( 348 | 'unauthorized: cannot update objects on blockchain bridge' 349 | ); 350 | } 351 | }); 352 | 353 | it('should not throw error for existing blockchain classes objects when updating blockchainStatus to sending with master key on before save', async () => { 354 | const simpleMQAdapter = new SimpleMQAdapter(); 355 | 356 | const bridge = new Bridge(); 357 | bridge.initialize(['SomeClass'], simpleMQAdapter); 358 | 359 | simpleMQAdapter.consume( 360 | `${Parse.applicationId}-parse-server-blockchain`, 361 | () => { 362 | throw new Error('Should not receive message'); 363 | } 364 | ); 365 | 366 | const someObject = new Parse.Object('SomeClass'); 367 | someObject.id = 'someid'; 368 | 369 | const sameObject = new Parse.Object('SomeClass'); 370 | sameObject.id = 'someid'; 371 | sameObject.set('blockchainStatus', BlockchainStatus.Sending); 372 | 373 | await triggers.maybeRunTrigger( 374 | triggers.Types.beforeSave, 375 | { isMaster: true }, 376 | sameObject, 377 | Object.assign(someObject, { 378 | fetch: () => Promise.resolve(), 379 | }), 380 | { 381 | applicationId: Parse.applicationId, 382 | }, 383 | {} 384 | ); 385 | }); 386 | 387 | it('should not throw error for existing blockchain classes objects when updating blockchainStatus to sent with master key on before save', async () => { 388 | const simpleMQAdapter = new SimpleMQAdapter(); 389 | 390 | const bridge = new Bridge(); 391 | bridge.initialize(['SomeClass'], simpleMQAdapter); 392 | 393 | simpleMQAdapter.consume( 394 | `${Parse.applicationId}-parse-server-blockchain`, 395 | () => { 396 | throw new Error('Should not receive message'); 397 | } 398 | ); 399 | 400 | const someObject = new Parse.Object('SomeClass'); 401 | someObject.id = 'someid'; 402 | someObject.set('blockchainStatus', BlockchainStatus.Sending); 403 | 404 | const sameObject = new Parse.Object('SomeClass'); 405 | sameObject.id = 'someid'; 406 | sameObject.set('blockchainStatus', BlockchainStatus.Sent); 407 | sameObject.set('blockchainResult', {}); 408 | 409 | await triggers.maybeRunTrigger( 410 | triggers.Types.beforeSave, 411 | { isMaster: true }, 412 | sameObject, 413 | Object.assign(someObject, { 414 | fetch: () => Promise.resolve(), 415 | }), 416 | { 417 | applicationId: Parse.applicationId, 418 | }, 419 | {} 420 | ); 421 | }); 422 | 423 | it('should not throw error for existing regular classes objects on before save', async () => { 424 | const simpleMQAdapter = new SimpleMQAdapter(); 425 | 426 | const bridge = new Bridge(); 427 | bridge.initialize(['SomeClass'], simpleMQAdapter); 428 | 429 | simpleMQAdapter.consume( 430 | `${Parse.applicationId}-parse-server-blockchain`, 431 | () => { 432 | throw new Error('Should not receive message'); 433 | } 434 | ); 435 | 436 | const someObject = new Parse.Object('SomeOtherClass'); 437 | someObject.id = 'someid'; 438 | 439 | await triggers.maybeRunTrigger( 440 | triggers.Types.beforeSave, 441 | {}, 442 | someObject, 443 | Object.assign(someObject, { 444 | fetch: () => Promise.resolve(), 445 | }), 446 | { 447 | applicationId: Parse.applicationId, 448 | }, 449 | {} 450 | ); 451 | }); 452 | 453 | it('should throw error for existing blockchain classes objects on before delete', async () => { 454 | const simpleMQAdapter = new SimpleMQAdapter(); 455 | 456 | const bridge = new Bridge(); 457 | bridge.initialize(['SomeClass'], simpleMQAdapter); 458 | 459 | simpleMQAdapter.consume( 460 | `${Parse.applicationId}-parse-server-blockchain`, 461 | () => { 462 | throw new Error('Should not receive message'); 463 | } 464 | ); 465 | 466 | const someObject = new Parse.Object('SomeClass'); 467 | someObject.id = 'someid'; 468 | 469 | try { 470 | await triggers.maybeRunTrigger( 471 | triggers.Types.beforeDelete, 472 | {}, 473 | someObject, 474 | null, 475 | { 476 | applicationId: Parse.applicationId, 477 | }, 478 | {} 479 | ); 480 | throw new Error('Should throw error'); 481 | } catch (e) { 482 | expect(e).toBeInstanceOf(Parse.Error); 483 | expect(e.code).toBe(Parse.Error.OPERATION_FORBIDDEN); 484 | expect(e.message).toBe( 485 | 'unauthorized: cannot delete objects on blockchain bridge' 486 | ); 487 | } 488 | }); 489 | 490 | it('should not throw error for existing regular classes objects on before delete', async () => { 491 | const simpleMQAdapter = new SimpleMQAdapter(); 492 | 493 | const bridge = new Bridge(); 494 | bridge.initialize(['SomeClass'], simpleMQAdapter); 495 | 496 | simpleMQAdapter.consume( 497 | `${Parse.applicationId}-parse-server-blockchain`, 498 | () => { 499 | throw new Error('Should not receive message'); 500 | } 501 | ); 502 | 503 | const someObject = new Parse.Object('SomeOtherClass'); 504 | someObject.id = 'someid'; 505 | 506 | await triggers.maybeRunTrigger( 507 | triggers.Types.beforeDelete, 508 | {}, 509 | someObject, 510 | null, 511 | { 512 | applicationId: Parse.applicationId, 513 | }, 514 | {} 515 | ); 516 | }); 517 | 518 | it('should call other triggers for new blockchain classes objects', async () => { 519 | const simpleMQAdapter = new SimpleMQAdapter(); 520 | 521 | const bridge = new Bridge(); 522 | bridge.initialize(['SomeClass'], simpleMQAdapter); 523 | 524 | let published = false; 525 | 526 | simpleMQAdapter.consume( 527 | `${Parse.applicationId}-parse-server-blockchain`, 528 | (message, ack) => { 529 | expect(JSON.parse(message)).toEqual({ 530 | className: 'SomeClass', 531 | __type: 'Object', 532 | objectId: 'someid', 533 | }); 534 | ack(); 535 | published = true; 536 | } 537 | ); 538 | 539 | let triggered = false; 540 | 541 | const someObject = new Parse.Object('SomeClass'); 542 | someObject.id = 'someid'; 543 | 544 | let someOtherObject; 545 | 546 | triggers.addTrigger( 547 | triggers.Types.afterSave, 548 | 'SomeClass', 549 | ({ object }) => { 550 | triggered = true; 551 | expect(object).toBe(someObject); 552 | someOtherObject = new Parse.Object('SomeClass'); 553 | someOtherObject.id = 'someotherid'; 554 | return someOtherObject; 555 | }, 556 | Parse.applicationId 557 | ); 558 | 559 | const result = await triggers.maybeRunTrigger( 560 | triggers.Types.afterSave, 561 | {}, 562 | someObject, 563 | null, 564 | { 565 | applicationId: Parse.applicationId, 566 | }, 567 | {} 568 | ); 569 | 570 | expect(published).toBeTruthy(); 571 | expect(triggered).toBeTruthy(); 572 | expect(result).toBe(someOtherObject); 573 | 574 | triggers.removeTrigger( 575 | triggers.Types.afterSave, 576 | 'SomeClass', 577 | Parse.applicationId 578 | ); 579 | }); 580 | 581 | it('should call other triggers for new regular classes objects', async () => { 582 | const simpleMQAdapter = new SimpleMQAdapter(); 583 | 584 | const bridge = new Bridge(); 585 | bridge.initialize(['SomeOtherClass'], simpleMQAdapter); 586 | 587 | simpleMQAdapter.consume( 588 | `${Parse.applicationId}-parse-server-blockchain`, 589 | () => { 590 | throw new Error('Should not receive message'); 591 | } 592 | ); 593 | 594 | let triggered = false; 595 | 596 | const someObject = new Parse.Object('SomeClass'); 597 | someObject.id = 'someid'; 598 | 599 | let someOtherObject; 600 | 601 | triggers.addTrigger( 602 | triggers.Types.afterSave, 603 | 'SomeClass', 604 | ({ object }) => { 605 | triggered = true; 606 | expect(object).toBe(someObject); 607 | someOtherObject = new Parse.Object('SomeClass'); 608 | someOtherObject.id = 'someotherid'; 609 | return someOtherObject; 610 | }, 611 | Parse.applicationId 612 | ); 613 | 614 | const result = await triggers.maybeRunTrigger( 615 | triggers.Types.afterSave, 616 | {}, 617 | someObject, 618 | null, 619 | { 620 | applicationId: Parse.applicationId, 621 | }, 622 | {} 623 | ); 624 | 625 | expect(triggered).toBeTruthy(); 626 | expect(result).toBe(someOtherObject); 627 | 628 | triggers.removeTrigger( 629 | triggers.Types.afterSave, 630 | 'SomeClass', 631 | Parse.applicationId 632 | ); 633 | }); 634 | }); 635 | }); 636 | -------------------------------------------------------------------------------- /packages/parse-blockchain-base/spec/SimpleMQAdapter.spec.ts: -------------------------------------------------------------------------------- 1 | import SimpleMQAdapter from '../src/SimpleMQAdapter'; 2 | 3 | describe('SimpleMQAdapter', () => { 4 | it('should publish to consumer', (done) => { 5 | const simpleMQAdapter = new SimpleMQAdapter(); 6 | 7 | simpleMQAdapter.consume('somequeue', (message, ack) => { 8 | expect(message).toBe('somemessage'); 9 | ack(); 10 | done(); 11 | }); 12 | 13 | simpleMQAdapter.publish('somequeue', 'somemessage'); 14 | }); 15 | 16 | it('should publish to random consumers', () => { 17 | const simpleMQAdapter = new SimpleMQAdapter(); 18 | 19 | let currentMessage; 20 | 21 | const consumersStats = []; 22 | for (let i = 0; i < 5; i++) { 23 | consumersStats.push(0); 24 | const index = i; 25 | simpleMQAdapter.consume('somequeue', (message, ack) => { 26 | expect(message).toBe(currentMessage); 27 | ack(); 28 | consumersStats[index]++; 29 | }); 30 | } 31 | 32 | for (let iteration = 0; iteration < 5000; iteration++) { 33 | currentMessage = `somemessage${iteration}`; 34 | simpleMQAdapter.publish('somequeue', currentMessage); 35 | } 36 | 37 | consumersStats.forEach((consumerStats) => { 38 | expect(Math.abs(consumerStats - 1000)).toBeLessThan(100); 39 | }); 40 | }); 41 | 42 | it('should re-publish if nack is called', (done) => { 43 | const simpleMQAdapter = new SimpleMQAdapter(); 44 | 45 | let publishCounter = 0; 46 | 47 | simpleMQAdapter.consume('somequeue', (message, ack, nack) => { 48 | expect(message).toBe('somemessage'); 49 | publishCounter++; 50 | if (publishCounter === 3) { 51 | ack(); 52 | expect(publishCounter).toBe(3); 53 | done(); 54 | } else { 55 | nack(); 56 | } 57 | }); 58 | 59 | simpleMQAdapter.publish('somequeue', 'somemessage'); 60 | }); 61 | 62 | it('should throw error if message is already acked', (done) => { 63 | const simpleMQAdapter = new SimpleMQAdapter(); 64 | 65 | simpleMQAdapter.consume('somequeue', (message, ack, nack) => { 66 | expect(message).toBe('somemessage'); 67 | ack(); 68 | expect(() => ack()).toThrow('The message is already acked'); 69 | expect(() => nack()).toThrow('The message is already acked'); 70 | done(); 71 | }); 72 | 73 | simpleMQAdapter.publish('somequeue', 'somemessage'); 74 | }); 75 | 76 | it('should throw error if message is already nacked', (done) => { 77 | const simpleMQAdapter = new SimpleMQAdapter(); 78 | 79 | let publishCounter = 0; 80 | 81 | simpleMQAdapter.consume('somequeue', (message, ack, nack) => { 82 | expect(message).toBe('somemessage'); 83 | publishCounter++; 84 | if (publishCounter === 1) { 85 | nack(); 86 | expect(() => ack()).toThrow('The message is already nacked'); 87 | expect(() => nack()).toThrow('The message is already nacked'); 88 | done(); 89 | } else { 90 | ack(); 91 | } 92 | }); 93 | 94 | simpleMQAdapter.publish('somequeue', 'somemessage'); 95 | }); 96 | 97 | it('should throw error if consumer has unsubscribed', (done) => { 98 | const simpleMQAdapter = new SimpleMQAdapter(); 99 | 100 | const subscription = simpleMQAdapter.consume( 101 | 'somequeue', 102 | (message, ack, nack) => { 103 | expect(message).toBe('somemessage'); 104 | subscription.unsubscribe(); 105 | expect(() => ack()).toThrow('The consumer is already unsubscribed'); 106 | expect(() => nack()).toThrow('The consumer is already unsubscribed'); 107 | done(); 108 | } 109 | ); 110 | 111 | simpleMQAdapter.publish('somequeue', 'somemessage'); 112 | }); 113 | 114 | it('should hold messages for new subscribers', (done) => { 115 | const simpleMQAdapter = new SimpleMQAdapter(); 116 | 117 | simpleMQAdapter.publish('somequeue', 'somemessage'); 118 | 119 | const subscription = simpleMQAdapter.consume('somequeue', (message) => { 120 | expect(message).toBe('somemessage'); 121 | process.nextTick(() => { 122 | subscription.unsubscribe(); 123 | 124 | simpleMQAdapter.consume('somequeue', (message, ack) => { 125 | expect(message).toBe('somemessage'); 126 | ack(); 127 | done(); 128 | }); 129 | }); 130 | }); 131 | }); 132 | 133 | it('should throw error if consumer unsubscribe twice', () => { 134 | const simpleMQAdapter = new SimpleMQAdapter(); 135 | 136 | const subscription = simpleMQAdapter.consume('somequeue', () => { 137 | throw new Error('Should not receive message'); 138 | }); 139 | 140 | subscription.unsubscribe(); 141 | 142 | expect(() => subscription.unsubscribe()).toThrow( 143 | 'The consumer is already unsubscribed' 144 | ); 145 | 146 | simpleMQAdapter.publish('somequeue', 'somemessage'); 147 | }); 148 | }); 149 | -------------------------------------------------------------------------------- /packages/parse-blockchain-base/spec/Worker.spec.ts: -------------------------------------------------------------------------------- 1 | import { CoreManager, Parse } from 'parse/node'; 2 | 3 | global.Parse = Parse; 4 | 5 | import MQAdapter from '../src/MQAdapter'; 6 | import SimpleMQAdapter from '../src/SimpleMQAdapter'; 7 | import { BlockchainStatus } from '../src/types'; 8 | import Worker from '../src/Worker'; 9 | 10 | const fakeBlockchainAdapter = { 11 | send: () => Promise.resolve({}), 12 | get: () => Promise.resolve({}), 13 | }; 14 | 15 | describe('Worker', () => { 16 | beforeAll(() => { 17 | Parse.initialize('someappid', 'somejavascriptkey', 'somemasterkey'); 18 | }); 19 | 20 | describe('initialize', () => { 21 | it('should initialize', () => { 22 | new Worker().initialize(fakeBlockchainAdapter); 23 | }); 24 | 25 | it('should not initialize twice', () => { 26 | const worker = new Worker(); 27 | worker.initialize(fakeBlockchainAdapter); 28 | expect(() => worker.initialize(fakeBlockchainAdapter)).toThrowError( 29 | 'The worker is already initialized' 30 | ); 31 | }); 32 | 33 | it('should initialize with custom adapter', () => { 34 | class FakeAdapter implements MQAdapter { 35 | publish: (queue: string, message: string) => void; 36 | consume() { 37 | return { 38 | unsubscribe: () => undefined, 39 | }; 40 | } 41 | } 42 | 43 | const fakeAdapter = new FakeAdapter(); 44 | 45 | const worker = new Worker(); 46 | worker.initialize(fakeBlockchainAdapter, fakeAdapter, { 47 | waitSendingAttempts: 30, 48 | }); 49 | 50 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 51 | expect((worker as any).mqAdapter).toBe(fakeAdapter); 52 | }); 53 | }); 54 | 55 | describe('handleMessage', () => { 56 | it('should send messages to the blockchain adapter', (done) => { 57 | const simpleMQAdapter = new SimpleMQAdapter(); 58 | 59 | const someObject = new Parse.Object('SomeClass'); 60 | someObject.id = 'someid'; 61 | 62 | const blockchainResult = { someField: 'someValue' }; 63 | 64 | let findCalls = 0; 65 | const queryController = CoreManager.getQueryController(); 66 | const originalQueryControllerFind = queryController.find; 67 | queryController.find = (className, params, options) => { 68 | findCalls++; 69 | expect(className).toBe('SomeClass'); 70 | expect(params).toEqual({ limit: 1, where: { objectId: 'someid' } }); 71 | expect(options.useMasterKey).toBe(true); 72 | return Promise.resolve({ results: [someObject] }); 73 | }; 74 | 75 | let saveCalls = 0; 76 | const objectController = CoreManager.getObjectController(); 77 | const originalObjectControllerSave = objectController.save; 78 | objectController.save = (object, options) => { 79 | saveCalls++; 80 | expect(findCalls).toEqual(1); 81 | if (saveCalls === 1) { 82 | expect(object).toEqual([]); 83 | expect(options).toEqual({ useMasterKey: true }); 84 | return Promise.resolve([]); 85 | } else if (saveCalls === 2) { 86 | expect(object._toFullJSON()).toEqual({ 87 | ...someObject._toFullJSON(), 88 | blockchainStatus: BlockchainStatus.Sending, 89 | }); 90 | expect(options).toEqual({ useMasterKey: true }); 91 | return Promise.resolve(object); 92 | } else if (saveCalls === 3) { 93 | expect(object).toEqual([]); 94 | expect(options).toEqual({ useMasterKey: true }); 95 | return Promise.resolve([]); 96 | } else if (saveCalls === 4) { 97 | expect(object._toFullJSON()).toEqual({ 98 | ...someObject._toFullJSON(), 99 | blockchainStatus: BlockchainStatus.Sent, 100 | blockchainResult: { 101 | type: 'Send', 102 | input: JSON.stringify(someObject._toFullJSON()), 103 | output: blockchainResult, 104 | }, 105 | }); 106 | expect(options).toEqual({ useMasterKey: true }); 107 | 108 | queryController.find = originalQueryControllerFind; 109 | objectController.save = originalObjectControllerSave; 110 | done(); 111 | 112 | return Promise.resolve(object); 113 | } else { 114 | throw new Error('Should call only 4 times'); 115 | } 116 | }; 117 | 118 | const worker = new Worker(); 119 | worker.initialize( 120 | { 121 | send: (parseObjectFullJSON: Record) => { 122 | expect(parseObjectFullJSON).toEqual(someObject._toFullJSON()); 123 | return Promise.resolve(blockchainResult); 124 | }, 125 | get: () => Promise.resolve({}), 126 | }, 127 | simpleMQAdapter 128 | ); 129 | 130 | simpleMQAdapter.publish( 131 | `${Parse.applicationId}-parse-server-blockchain`, 132 | JSON.stringify(someObject._toFullJSON()) 133 | ); 134 | }); 135 | 136 | it('should handle blockchain fail', (done) => { 137 | const simpleMQAdapter = new SimpleMQAdapter(); 138 | 139 | const someObject = new Parse.Object('SomeClass'); 140 | someObject.id = 'someid'; 141 | 142 | let findCalls = 0; 143 | const queryController = CoreManager.getQueryController(); 144 | const originalQueryControllerFind = queryController.find; 145 | queryController.find = (className, params, options) => { 146 | findCalls++; 147 | expect(className).toBe('SomeClass'); 148 | expect(params).toEqual({ limit: 1, where: { objectId: 'someid' } }); 149 | expect(options.useMasterKey).toBe(true); 150 | return Promise.resolve({ results: [someObject] }); 151 | }; 152 | 153 | const error = new Error('Some Error'); 154 | 155 | let saveCalls = 0; 156 | const objectController = CoreManager.getObjectController(); 157 | const originalObjectControllerSave = objectController.save; 158 | objectController.save = (object, options) => { 159 | saveCalls++; 160 | expect(findCalls).toEqual(1); 161 | if (saveCalls === 1) { 162 | expect(object).toEqual([]); 163 | expect(options).toEqual({ useMasterKey: true }); 164 | return Promise.resolve([]); 165 | } else if (saveCalls === 2) { 166 | expect(object._toFullJSON()).toEqual({ 167 | ...someObject._toFullJSON(), 168 | blockchainStatus: BlockchainStatus.Sending, 169 | }); 170 | expect(options).toEqual({ useMasterKey: true }); 171 | return Promise.resolve(object); 172 | } else if (saveCalls === 3) { 173 | expect(object).toEqual([]); 174 | expect(options).toEqual({ useMasterKey: true }); 175 | return Promise.resolve([]); 176 | } else if (saveCalls === 4) { 177 | expect(object._toFullJSON()).toEqual({ 178 | ...someObject._toFullJSON(), 179 | blockchainStatus: BlockchainStatus.Failed, 180 | blockchainResult: { 181 | type: 'Error', 182 | input: JSON.stringify(someObject._toFullJSON()), 183 | error: error.toString(), 184 | }, 185 | }); 186 | expect(options).toEqual({ useMasterKey: true }); 187 | 188 | queryController.find = originalQueryControllerFind; 189 | objectController.save = originalObjectControllerSave; 190 | done(); 191 | 192 | return Promise.resolve(object); 193 | } else { 194 | throw new Error('Should call only 4 times'); 195 | } 196 | }; 197 | 198 | const worker = new Worker(); 199 | worker.initialize( 200 | { 201 | send: () => { 202 | throw error; 203 | }, 204 | get: () => Promise.resolve({}), 205 | }, 206 | simpleMQAdapter 207 | ); 208 | 209 | simpleMQAdapter.publish( 210 | `${Parse.applicationId}-parse-server-blockchain`, 211 | JSON.stringify(someObject._toFullJSON()) 212 | ); 213 | }); 214 | 215 | it('should nack if cannot get object status', (done) => { 216 | const simpleMQAdapter = new SimpleMQAdapter(); 217 | 218 | const someObject = new Parse.Object('SomeClass'); 219 | someObject.id = 'someid'; 220 | 221 | const blockchainResult = { someField: 'someValue' }; 222 | 223 | let findCalls = 0; 224 | const queryController = CoreManager.getQueryController(); 225 | const originalQueryControllerFind = queryController.find; 226 | queryController.find = (className, params, options) => { 227 | findCalls++; 228 | expect(className).toBe('SomeClass'); 229 | expect(params).toEqual({ limit: 1, where: { objectId: 'someid' } }); 230 | expect(options.useMasterKey).toBe(true); 231 | if (findCalls === 1) { 232 | throw new Error('Some Error'); 233 | } else { 234 | return Promise.resolve({ results: [someObject] }); 235 | } 236 | }; 237 | 238 | let saveCalls = 0; 239 | const objectController = CoreManager.getObjectController(); 240 | const originalObjectControllerSave = objectController.save; 241 | objectController.save = (object, options) => { 242 | saveCalls++; 243 | expect(findCalls).toEqual(2); 244 | if (saveCalls === 1) { 245 | expect(object).toEqual([]); 246 | expect(options).toEqual({ useMasterKey: true }); 247 | return Promise.resolve([]); 248 | } else if (saveCalls === 2) { 249 | expect(object._toFullJSON()).toEqual({ 250 | ...someObject._toFullJSON(), 251 | blockchainStatus: BlockchainStatus.Sending, 252 | }); 253 | expect(options).toEqual({ useMasterKey: true }); 254 | return Promise.resolve(object); 255 | } else if (saveCalls === 3) { 256 | expect(object).toEqual([]); 257 | expect(options).toEqual({ useMasterKey: true }); 258 | return Promise.resolve([]); 259 | } else if (saveCalls === 4) { 260 | expect(object._toFullJSON()).toEqual({ 261 | ...someObject._toFullJSON(), 262 | blockchainStatus: BlockchainStatus.Sent, 263 | blockchainResult: { 264 | type: 'Send', 265 | input: JSON.stringify(someObject._toFullJSON()), 266 | output: blockchainResult, 267 | }, 268 | }); 269 | expect(options).toEqual({ useMasterKey: true }); 270 | 271 | queryController.find = originalQueryControllerFind; 272 | objectController.save = originalObjectControllerSave; 273 | done(); 274 | 275 | return Promise.resolve(object); 276 | } else { 277 | throw new Error('Should call only 4 times'); 278 | } 279 | }; 280 | 281 | const worker = new Worker(); 282 | worker.initialize( 283 | { 284 | send: (parseObjectFullJSON: Record) => { 285 | expect(parseObjectFullJSON).toEqual(someObject._toFullJSON()); 286 | return Promise.resolve(blockchainResult); 287 | }, 288 | get: () => Promise.resolve({}), 289 | }, 290 | simpleMQAdapter 291 | ); 292 | 293 | simpleMQAdapter.publish( 294 | `${Parse.applicationId}-parse-server-blockchain`, 295 | JSON.stringify(someObject._toFullJSON()) 296 | ); 297 | }); 298 | 299 | it('should nack if cannot update object status to sending', (done) => { 300 | const simpleMQAdapter = new SimpleMQAdapter(); 301 | 302 | const someObject = new Parse.Object('SomeClass'); 303 | someObject.id = 'someid'; 304 | 305 | const blockchainResult = { someField: 'someValue' }; 306 | 307 | let findCalls = 0; 308 | const queryController = CoreManager.getQueryController(); 309 | const originalQueryControllerFind = queryController.find; 310 | queryController.find = (className, params, options) => { 311 | findCalls++; 312 | expect(className).toBe('SomeClass'); 313 | expect(params).toEqual({ limit: 1, where: { objectId: 'someid' } }); 314 | expect(options.useMasterKey).toBe(true); 315 | return Promise.resolve({ results: [someObject] }); 316 | }; 317 | 318 | let saveCalls = 0; 319 | const objectController = CoreManager.getObjectController(); 320 | const originalObjectControllerSave = objectController.save; 321 | objectController.save = (object, options) => { 322 | saveCalls++; 323 | if (saveCalls === 1) { 324 | expect(findCalls).toEqual(1); 325 | expect(object).toEqual([]); 326 | expect(options).toEqual({ useMasterKey: true }); 327 | return Promise.resolve([]); 328 | } else if (saveCalls === 2) { 329 | expect(findCalls).toEqual(1); 330 | expect(object._toFullJSON()).toEqual({ 331 | ...someObject._toFullJSON(), 332 | blockchainStatus: BlockchainStatus.Sending, 333 | }); 334 | expect(options).toEqual({ useMasterKey: true }); 335 | throw new Error('Some Error'); 336 | } else if (saveCalls === 3) { 337 | expect(findCalls).toEqual(2); 338 | expect(object).toEqual([]); 339 | expect(options).toEqual({ useMasterKey: true }); 340 | return Promise.resolve([]); 341 | } else if (saveCalls === 4) { 342 | expect(findCalls).toEqual(2); 343 | expect(object._toFullJSON()).toEqual({ 344 | ...someObject._toFullJSON(), 345 | blockchainStatus: BlockchainStatus.Sending, 346 | }); 347 | expect(options).toEqual({ useMasterKey: true }); 348 | return Promise.resolve(object); 349 | } else if (saveCalls === 5) { 350 | expect(findCalls).toEqual(2); 351 | expect(object).toEqual([]); 352 | expect(options).toEqual({ useMasterKey: true }); 353 | return Promise.resolve([]); 354 | } else if (saveCalls === 6) { 355 | expect(findCalls).toEqual(2); 356 | expect(object._toFullJSON()).toEqual({ 357 | ...someObject._toFullJSON(), 358 | blockchainStatus: BlockchainStatus.Sent, 359 | blockchainResult: { 360 | type: 'Send', 361 | input: JSON.stringify(someObject._toFullJSON()), 362 | output: blockchainResult, 363 | }, 364 | }); 365 | expect(options).toEqual({ useMasterKey: true }); 366 | 367 | queryController.find = originalQueryControllerFind; 368 | objectController.save = originalObjectControllerSave; 369 | done(); 370 | 371 | return Promise.resolve(object); 372 | } else { 373 | throw new Error('Should call only 6 times'); 374 | } 375 | }; 376 | 377 | const worker = new Worker(); 378 | worker.initialize( 379 | { 380 | send: (parseObjectFullJSON: Record) => { 381 | expect(parseObjectFullJSON).toEqual(someObject._toFullJSON()); 382 | return Promise.resolve(blockchainResult); 383 | }, 384 | get: () => Promise.resolve({}), 385 | }, 386 | simpleMQAdapter 387 | ); 388 | 389 | simpleMQAdapter.publish( 390 | `${Parse.applicationId}-parse-server-blockchain`, 391 | JSON.stringify(someObject._toFullJSON()) 392 | ); 393 | }); 394 | 395 | it('should ack if object has status and it is not sending', (done) => { 396 | const simpleMQAdapter = new SimpleMQAdapter(); 397 | 398 | const someObject = new Parse.Object('SomeClass'); 399 | someObject.id = 'someid'; 400 | 401 | const blockchainResult = { someField: 'someValue' }; 402 | 403 | let findCalls = 0; 404 | const queryController = CoreManager.getQueryController(); 405 | const originalQueryControllerFind = queryController.find; 406 | queryController.find = (className, params, options) => { 407 | findCalls++; 408 | expect(className).toBe('SomeClass'); 409 | expect(params).toEqual({ limit: 1, where: { objectId: 'someid' } }); 410 | expect(options.useMasterKey).toBe(true); 411 | if (findCalls === 1) { 412 | return Promise.resolve({ 413 | results: [ 414 | { 415 | ...someObject._toFullJSON(), 416 | blockchainStatus: BlockchainStatus.Sending, 417 | }, 418 | ], 419 | }); 420 | } else { 421 | queryController.find = originalQueryControllerFind; 422 | done(); 423 | 424 | return Promise.resolve({ 425 | results: [ 426 | { 427 | ...someObject._toFullJSON(), 428 | blockchainStatus: BlockchainStatus.Sent, 429 | }, 430 | ], 431 | }); 432 | } 433 | }; 434 | 435 | const worker = new Worker(); 436 | worker.initialize( 437 | { 438 | send: (parseObjectFullJSON: Record) => { 439 | expect(parseObjectFullJSON).toEqual(someObject._toFullJSON()); 440 | return Promise.resolve(blockchainResult); 441 | }, 442 | get: () => Promise.resolve({}), 443 | }, 444 | simpleMQAdapter, 445 | { 446 | waitSendingSleepMS: 1, 447 | } 448 | ); 449 | 450 | simpleMQAdapter.publish( 451 | `${Parse.applicationId}-parse-server-blockchain`, 452 | JSON.stringify(someObject._toFullJSON()) 453 | ); 454 | }); 455 | 456 | it('should check the object on blockchain if the status is still sending after the wait time', (done) => { 457 | const simpleMQAdapter = new SimpleMQAdapter(); 458 | 459 | const someObject = new Parse.Object('SomeClass'); 460 | someObject.id = 'someid'; 461 | 462 | const blockchainResult = { someField: 'someValue' }; 463 | 464 | let findCalls = 0; 465 | const queryController = CoreManager.getQueryController(); 466 | const originalQueryControllerFind = queryController.find; 467 | queryController.find = (className, params, options) => { 468 | findCalls++; 469 | expect(className).toBe('SomeClass'); 470 | expect(params).toEqual({ limit: 1, where: { objectId: 'someid' } }); 471 | expect(options.useMasterKey).toBe(true); 472 | return Promise.resolve({ 473 | results: [ 474 | { 475 | ...someObject._toFullJSON(), 476 | blockchainStatus: BlockchainStatus.Sending, 477 | }, 478 | ], 479 | }); 480 | }; 481 | 482 | let getCalls = 0; 483 | let saveCalls = 0; 484 | const objectController = CoreManager.getObjectController(); 485 | const originalObjectControllerSave = objectController.save; 486 | objectController.save = (object, options) => { 487 | saveCalls++; 488 | expect(findCalls).toEqual(5); 489 | expect(getCalls).toEqual(1); 490 | if (saveCalls === 1) { 491 | expect(object).toEqual([]); 492 | expect(options).toEqual({ useMasterKey: true }); 493 | return Promise.resolve([]); 494 | } else if (saveCalls === 2) { 495 | expect(object._toFullJSON()).toEqual({ 496 | ...someObject._toFullJSON(), 497 | blockchainStatus: BlockchainStatus.Sent, 498 | blockchainResult: { 499 | type: 'Get', 500 | input: { 501 | className: 'SomeClass', 502 | objectId: 'someid', 503 | }, 504 | output: blockchainResult, 505 | }, 506 | }); 507 | expect(options).toEqual({ useMasterKey: true }); 508 | 509 | queryController.find = originalQueryControllerFind; 510 | objectController.save = originalObjectControllerSave; 511 | done(); 512 | 513 | return Promise.resolve(object); 514 | } else { 515 | throw new Error('Should call only 2 times'); 516 | } 517 | }; 518 | 519 | const worker = new Worker(); 520 | worker.initialize( 521 | { 522 | send: () => { 523 | throw new Error('Should not send'); 524 | }, 525 | get: () => { 526 | getCalls++; 527 | return Promise.resolve(blockchainResult); 528 | }, 529 | }, 530 | simpleMQAdapter, 531 | { 532 | waitSendingAttempts: 5, 533 | waitSendingSleepMS: 1, 534 | } 535 | ); 536 | 537 | simpleMQAdapter.publish( 538 | `${Parse.applicationId}-parse-server-blockchain`, 539 | JSON.stringify(someObject._toFullJSON()) 540 | ); 541 | }); 542 | 543 | it('should send the object again to blockchain if the status is still sending after the wait time and object was not found', (done) => { 544 | const simpleMQAdapter = new SimpleMQAdapter(); 545 | 546 | const someObject = new Parse.Object('SomeClass'); 547 | someObject.id = 'someid'; 548 | 549 | const blockchainResult = { someField: 'someValue' }; 550 | 551 | let findCalls = 0; 552 | const queryController = CoreManager.getQueryController(); 553 | const originalQueryControllerFind = queryController.find; 554 | queryController.find = (className, params, options) => { 555 | findCalls++; 556 | expect(className).toBe('SomeClass'); 557 | expect(params).toEqual({ limit: 1, where: { objectId: 'someid' } }); 558 | expect(options.useMasterKey).toBe(true); 559 | return Promise.resolve({ 560 | results: [ 561 | { 562 | ...someObject._toFullJSON(), 563 | blockchainStatus: BlockchainStatus.Sending, 564 | }, 565 | ], 566 | }); 567 | }; 568 | 569 | let getCalls = 0; 570 | let saveCalls = 0; 571 | const objectController = CoreManager.getObjectController(); 572 | const originalObjectControllerSave = objectController.save; 573 | objectController.save = (object, options) => { 574 | saveCalls++; 575 | expect(findCalls).toEqual(5); 576 | expect(getCalls).toEqual(1); 577 | if (saveCalls === 1) { 578 | expect(object).toEqual([]); 579 | expect(options).toEqual({ useMasterKey: true }); 580 | return Promise.resolve([]); 581 | } else if (saveCalls === 2) { 582 | expect(object._toFullJSON()).toEqual({ 583 | ...someObject._toFullJSON(), 584 | blockchainStatus: BlockchainStatus.Sent, 585 | blockchainResult: { 586 | type: 'Send', 587 | input: JSON.stringify(someObject._toFullJSON()), 588 | output: blockchainResult, 589 | }, 590 | }); 591 | expect(options).toEqual({ useMasterKey: true }); 592 | 593 | queryController.find = originalQueryControllerFind; 594 | objectController.save = originalObjectControllerSave; 595 | done(); 596 | 597 | return Promise.resolve(object); 598 | } else { 599 | throw new Error('Should call only 2 times'); 600 | } 601 | }; 602 | 603 | const worker = new Worker(); 604 | worker.initialize( 605 | { 606 | send: (parseObjectFullJSON: Record) => { 607 | expect(parseObjectFullJSON).toEqual(someObject._toFullJSON()); 608 | return Promise.resolve(blockchainResult); 609 | }, 610 | get: (className, objectId) => { 611 | getCalls++; 612 | expect(className).toBe('SomeClass'); 613 | expect(objectId).toBe('someid'); 614 | throw new Error('The object does not exist'); 615 | }, 616 | }, 617 | simpleMQAdapter, 618 | { 619 | waitSendingAttempts: 5, 620 | waitSendingSleepMS: 1, 621 | } 622 | ); 623 | 624 | simpleMQAdapter.publish( 625 | `${Parse.applicationId}-parse-server-blockchain`, 626 | JSON.stringify(someObject._toFullJSON()) 627 | ); 628 | }); 629 | 630 | it('should nack if cannot check the object on blockchain if the status is still sending after the wait time', (done) => { 631 | const simpleMQAdapter = new SimpleMQAdapter(); 632 | 633 | const someObject = new Parse.Object('SomeClass'); 634 | someObject.id = 'someid'; 635 | 636 | const blockchainResult = { someField: 'someValue' }; 637 | 638 | let findCalls = 0; 639 | const queryController = CoreManager.getQueryController(); 640 | const originalQueryControllerFind = queryController.find; 641 | queryController.find = (className, params, options) => { 642 | findCalls++; 643 | expect(className).toBe('SomeClass'); 644 | expect(params).toEqual({ limit: 1, where: { objectId: 'someid' } }); 645 | expect(options.useMasterKey).toBe(true); 646 | return Promise.resolve({ 647 | results: [ 648 | { 649 | ...someObject._toFullJSON(), 650 | blockchainStatus: BlockchainStatus.Sending, 651 | }, 652 | ], 653 | }); 654 | }; 655 | 656 | let getCalls = 0; 657 | let saveCalls = 0; 658 | const objectController = CoreManager.getObjectController(); 659 | const originalObjectControllerSave = objectController.save; 660 | objectController.save = (object, options) => { 661 | saveCalls++; 662 | expect(findCalls).toEqual(10); 663 | expect(getCalls).toEqual(2); 664 | if (saveCalls === 1) { 665 | expect(object).toEqual([]); 666 | expect(options).toEqual({ useMasterKey: true }); 667 | return Promise.resolve([]); 668 | } else if (saveCalls === 2) { 669 | expect(object._toFullJSON()).toEqual({ 670 | ...someObject._toFullJSON(), 671 | blockchainStatus: BlockchainStatus.Sent, 672 | blockchainResult: { 673 | type: 'Get', 674 | input: { 675 | className: 'SomeClass', 676 | objectId: 'someid', 677 | }, 678 | output: blockchainResult, 679 | }, 680 | }); 681 | expect(options).toEqual({ useMasterKey: true }); 682 | 683 | queryController.find = originalQueryControllerFind; 684 | objectController.save = originalObjectControllerSave; 685 | done(); 686 | 687 | return Promise.resolve(object); 688 | } else { 689 | throw new Error('Should call only 2 times'); 690 | } 691 | }; 692 | 693 | const worker = new Worker(); 694 | worker.initialize( 695 | { 696 | send: () => { 697 | throw new Error('Should not send'); 698 | }, 699 | get: () => { 700 | getCalls++; 701 | if (getCalls === 1) { 702 | throw new Error('Some Error'); 703 | } else { 704 | return Promise.resolve(blockchainResult); 705 | } 706 | }, 707 | }, 708 | simpleMQAdapter, 709 | { 710 | waitSendingAttempts: 5, 711 | waitSendingSleepMS: 1, 712 | } 713 | ); 714 | 715 | simpleMQAdapter.publish( 716 | `${Parse.applicationId}-parse-server-blockchain`, 717 | JSON.stringify(someObject._toFullJSON()) 718 | ); 719 | }); 720 | }); 721 | 722 | it('should nack if cannot update object status to sent or failed', (done) => { 723 | const simpleMQAdapter = new SimpleMQAdapter(); 724 | 725 | const someObject = new Parse.Object('SomeClass'); 726 | someObject.id = 'someid'; 727 | 728 | const blockchainResult = { someField: 'someValue' }; 729 | 730 | let findCalls = 0; 731 | const queryController = CoreManager.getQueryController(); 732 | const originalQueryControllerFind = queryController.find; 733 | queryController.find = (className, params, options) => { 734 | findCalls++; 735 | expect(className).toBe('SomeClass'); 736 | expect(params).toEqual({ limit: 1, where: { objectId: 'someid' } }); 737 | expect(options.useMasterKey).toBe(true); 738 | if (findCalls === 1) { 739 | return Promise.resolve({ results: [someObject] }); 740 | } else { 741 | return Promise.resolve({ 742 | results: [ 743 | { 744 | ...someObject._toFullJSON(), 745 | blockchainStatus: BlockchainStatus.Sending, 746 | }, 747 | ], 748 | }); 749 | } 750 | }; 751 | 752 | let saveCalls = 0; 753 | const objectController = CoreManager.getObjectController(); 754 | const originalObjectControllerSave = objectController.save; 755 | objectController.save = (object, options) => { 756 | saveCalls++; 757 | if (saveCalls === 1) { 758 | expect(findCalls).toEqual(1); 759 | expect(object).toEqual([]); 760 | expect(options).toEqual({ useMasterKey: true }); 761 | return Promise.resolve([]); 762 | } else if (saveCalls === 2) { 763 | expect(findCalls).toEqual(1); 764 | expect(object._toFullJSON()).toEqual({ 765 | ...someObject._toFullJSON(), 766 | blockchainStatus: BlockchainStatus.Sending, 767 | }); 768 | expect(options).toEqual({ useMasterKey: true }); 769 | return Promise.resolve(object); 770 | } else if (saveCalls === 3) { 771 | expect(findCalls).toEqual(1); 772 | expect(object).toEqual([]); 773 | expect(options).toEqual({ useMasterKey: true }); 774 | return Promise.resolve([]); 775 | } else if (saveCalls === 4) { 776 | expect(findCalls).toEqual(1); 777 | expect(object._toFullJSON()).toEqual({ 778 | ...someObject._toFullJSON(), 779 | blockchainStatus: BlockchainStatus.Sent, 780 | blockchainResult: { 781 | type: 'Send', 782 | input: JSON.stringify(someObject._toFullJSON()), 783 | output: blockchainResult, 784 | }, 785 | }); 786 | expect(options).toEqual({ useMasterKey: true }); 787 | throw new Error('Some Error'); 788 | } else if (saveCalls === 5) { 789 | expect(findCalls).toEqual(6); 790 | expect(object).toEqual([]); 791 | expect(options).toEqual({ useMasterKey: true }); 792 | return Promise.resolve([]); 793 | } else if (saveCalls === 6) { 794 | expect(findCalls).toEqual(6); 795 | expect(object._toFullJSON()).toEqual({ 796 | ...someObject._toFullJSON(), 797 | blockchainStatus: BlockchainStatus.Sent, 798 | blockchainResult: { 799 | type: 'Get', 800 | input: { 801 | className: 'SomeClass', 802 | objectId: 'someid', 803 | }, 804 | output: blockchainResult, 805 | }, 806 | }); 807 | expect(options).toEqual({ useMasterKey: true }); 808 | 809 | queryController.find = originalQueryControllerFind; 810 | objectController.save = originalObjectControllerSave; 811 | done(); 812 | 813 | return Promise.resolve(object); 814 | } else { 815 | throw new Error('Should call only 6 times'); 816 | } 817 | }; 818 | 819 | const worker = new Worker(); 820 | worker.initialize( 821 | { 822 | send: (parseObjectFullJSON: Record) => { 823 | expect(parseObjectFullJSON).toEqual(someObject._toFullJSON()); 824 | return Promise.resolve(blockchainResult); 825 | }, 826 | get: (className, objectId) => { 827 | expect(className).toBe('SomeClass'); 828 | expect(objectId).toBe('someid'); 829 | return Promise.resolve(blockchainResult); 830 | }, 831 | }, 832 | simpleMQAdapter, 833 | { 834 | waitSendingAttempts: 5, 835 | waitSendingSleepMS: 1, 836 | } 837 | ); 838 | 839 | simpleMQAdapter.publish( 840 | `${Parse.applicationId}-parse-server-blockchain`, 841 | JSON.stringify(someObject._toFullJSON()) 842 | ); 843 | }); 844 | }); 845 | -------------------------------------------------------------------------------- /packages/parse-blockchain-base/spec/index.spec.ts: -------------------------------------------------------------------------------- 1 | import * as index from '../src/index'; 2 | import SimpleMQAdapter from '../src/SimpleMQAdapter'; 3 | import Bridge from '../src/Bridge'; 4 | import Worker from '../src/Worker'; 5 | 6 | describe('index', () => { 7 | it('should export SimpleMQAdapter', () => { 8 | expect(index.SimpleMQAdapter).toBe(SimpleMQAdapter); 9 | }); 10 | 11 | it('should export a bridge instance', () => { 12 | expect(index.bridge).toBeInstanceOf(Bridge); 13 | }); 14 | 15 | it('should export a worker instance', () => { 16 | expect(index.worker).toBeInstanceOf(Worker); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/parse-blockchain-base/spec/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["./"], 4 | "typeAcquisition": { "include": ["jest"] } 5 | } 6 | -------------------------------------------------------------------------------- /packages/parse-blockchain-base/src/BlockchainAdapter.ts: -------------------------------------------------------------------------------- 1 | export default interface BlockchainAdapter { 2 | send( 3 | parseObjectFullJSON: Record 4 | ): Promise>; 5 | 6 | get(className: string, objectId: string): Promise>; 7 | } 8 | -------------------------------------------------------------------------------- /packages/parse-blockchain-base/src/Bridge.ts: -------------------------------------------------------------------------------- 1 | import * as triggers from 'parse-server/lib/triggers'; 2 | import { BlockchainStatus } from './types'; 3 | import MQAdapter from './MQAdapter'; 4 | import SimpleMQAdapter from './SimpleMQAdapter'; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | const Parse = (global as any).Parse; 8 | const getTrigger = triggers.getTrigger; 9 | const triggerExists = triggers.triggerExists; 10 | const maybeRunTrigger = triggers.maybeRunTrigger; 11 | 12 | const beforeDeleteTriggerHandler = () => { 13 | throw new Parse.Error( 14 | Parse.Error.OPERATION_FORBIDDEN, 15 | 'unauthorized: cannot delete objects on blockchain bridge' 16 | ); 17 | }; 18 | 19 | export default class Bridge { 20 | private initialized = false; 21 | private classNames: string[]; 22 | private mqAdapter: MQAdapter; 23 | 24 | initialize(classNames: string[], mqAdapter?: MQAdapter): void { 25 | if (this.initialized) { 26 | throw new Error('The bridge is already initialized'); 27 | } else { 28 | this.initialized = true; 29 | } 30 | 31 | this.classNames = classNames; 32 | this.mqAdapter = mqAdapter || new SimpleMQAdapter(); 33 | 34 | triggers.getTrigger = this.handleGetTrigger.bind(this); 35 | triggers.triggerExists = this.handleTriggerExists.bind(this); 36 | triggers.maybeRunTrigger = this.handleMaybeRunTrigger.bind(this); 37 | } 38 | 39 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 40 | private handleGetTrigger(...args: any[]) { 41 | const [className, triggerType] = args; 42 | if ( 43 | this.classNames.includes(className) && 44 | triggerType === triggers.Types.beforeDelete 45 | ) { 46 | return beforeDeleteTriggerHandler; 47 | } 48 | 49 | return getTrigger(...args); 50 | } 51 | 52 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 53 | private handleTriggerExists(...args: any[]) { 54 | const [className, triggerType] = args; 55 | if ( 56 | this.classNames.includes(className) && 57 | [ 58 | triggers.Types.beforeSave, 59 | triggers.Types.afterSave, 60 | triggers.Types.beforeDelete, 61 | ].includes(triggerType) 62 | ) { 63 | return true; 64 | } 65 | 66 | return triggerExists(...args); 67 | } 68 | 69 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 70 | private async handleMaybeRunTrigger(...args: any[]) { 71 | const [triggerType, { isMaster }, parseObject, originalParseObject] = args; 72 | 73 | if ( 74 | triggerType === triggers.Types.beforeSave && 75 | this.classNames.includes(parseObject.className) 76 | ) { 77 | if (originalParseObject) { 78 | await originalParseObject.fetch({ useMasterKey: true }); 79 | if ( 80 | !isMaster || 81 | ((originalParseObject.get('blockchainStatus') !== undefined || 82 | parseObject.get('blockchainStatus') !== BlockchainStatus.Sending || 83 | parseObject.dirtyKeys().filter((key) => key !== 'blockchainStatus') 84 | .length > 0) && 85 | (originalParseObject.get('blockchainStatus') !== 86 | BlockchainStatus.Sending || 87 | ![BlockchainStatus.Sent, BlockchainStatus.Failed].includes( 88 | parseObject.get('blockchainStatus') 89 | ) || 90 | parseObject 91 | .dirtyKeys() 92 | .filter( 93 | (key) => 94 | !['blockchainStatus', 'blockchainResult'].includes(key) 95 | ).length > 0)) 96 | ) { 97 | throw new Parse.Error( 98 | Parse.Error.OPERATION_FORBIDDEN, 99 | 'unauthorized: cannot update objects on blockchain bridge' 100 | ); 101 | } 102 | } else { 103 | if (parseObject.get('blockchainStatus') !== undefined) { 104 | throw new Parse.Error( 105 | Parse.Error.OPERATION_FORBIDDEN, 106 | 'unauthorized: cannot set blockchainStatus field' 107 | ); 108 | } else if (parseObject.get('blockchainResult') !== undefined) { 109 | throw new Parse.Error( 110 | Parse.Error.OPERATION_FORBIDDEN, 111 | 'unauthorized: cannot set blockchainResult field' 112 | ); 113 | } 114 | } 115 | } else if ( 116 | triggerType === triggers.Types.afterSave && 117 | !originalParseObject && 118 | this.classNames.includes(parseObject.className) 119 | ) { 120 | this.mqAdapter.publish( 121 | `${Parse.applicationId}-parse-server-blockchain`, 122 | JSON.stringify(parseObject._toFullJSON()) 123 | ); 124 | } else if ( 125 | triggerType === triggers.Types.beforeDelete && 126 | this.classNames.includes(parseObject.className) 127 | ) { 128 | beforeDeleteTriggerHandler(); 129 | } 130 | 131 | return maybeRunTrigger(...args); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /packages/parse-blockchain-base/src/MQAdapter.ts: -------------------------------------------------------------------------------- 1 | export type Listener = ( 2 | message: string, 3 | ack: () => void, 4 | nack: () => void 5 | ) => void; 6 | 7 | export interface Subscription { 8 | unsubscribe: () => void; 9 | } 10 | 11 | export default interface MQAdapter { 12 | publish: (queue: string, message: string) => void; 13 | consume: (queue: string, listener: Listener) => Subscription; 14 | } 15 | -------------------------------------------------------------------------------- /packages/parse-blockchain-base/src/SimpleMQAdapter.ts: -------------------------------------------------------------------------------- 1 | import MQAdapter, { Listener, Subscription } from './MQAdapter'; 2 | 3 | export default class SimpleMQAdapter implements MQAdapter { 4 | private queuesState: { 5 | [queue: string]: { 6 | consumers: { 7 | listener: Listener; 8 | deliveredMessages: string[]; 9 | }[]; 10 | notDeliveredMessages: string[]; 11 | }; 12 | } = {}; 13 | 14 | private ensureQueue(queue: string) { 15 | if (!this.queuesState[queue]) { 16 | this.queuesState[queue] = { 17 | consumers: [], 18 | notDeliveredMessages: [], 19 | }; 20 | } 21 | } 22 | 23 | publish(queue: string, message: string): void { 24 | this.ensureQueue(queue); 25 | 26 | if (this.queuesState[queue].consumers.length > 0) { 27 | const consumer = 28 | this.queuesState[queue].consumers[ 29 | Math.floor(Math.random() * this.queuesState[queue].consumers.length) 30 | ]; 31 | 32 | consumer.deliveredMessages.push(message); 33 | 34 | let acked = false; 35 | let nacked = false; 36 | const validateACK = () => { 37 | if (acked) { 38 | throw new Error('The message is already acked'); 39 | } else if (nacked) { 40 | throw new Error('The message is already nacked'); 41 | } else if (this.queuesState[queue].consumers.indexOf(consumer) < 0) { 42 | throw new Error('The consumer is already unsubscribed'); 43 | } 44 | }; 45 | 46 | const removeFromDeliveredMessages = () => { 47 | const index = consumer.deliveredMessages.indexOf(message); 48 | consumer.deliveredMessages.splice(index, 1); 49 | }; 50 | 51 | consumer.listener( 52 | message, 53 | () => { 54 | validateACK(); 55 | acked = true; 56 | removeFromDeliveredMessages(); 57 | }, 58 | () => { 59 | validateACK(); 60 | nacked = true; 61 | removeFromDeliveredMessages(); 62 | process.nextTick(() => { 63 | this.publish(queue, message); 64 | }); 65 | } 66 | ); 67 | } else { 68 | this.queuesState[queue].notDeliveredMessages.push(message); 69 | } 70 | } 71 | 72 | consume(queue: string, listener: Listener): Subscription { 73 | this.ensureQueue(queue); 74 | 75 | const consumer = { 76 | listener, 77 | deliveredMessages: [], 78 | }; 79 | this.queuesState[queue].consumers.push(consumer); 80 | 81 | process.nextTick(() => { 82 | this.queuesState[queue].notDeliveredMessages 83 | .splice(0, this.queuesState[queue].notDeliveredMessages.length) 84 | .forEach((notDeliveredMessage) => 85 | this.publish(queue, notDeliveredMessage) 86 | ); 87 | }); 88 | 89 | return { 90 | unsubscribe: () => { 91 | const index = this.queuesState[queue].consumers.indexOf(consumer); 92 | if (index < 0) { 93 | throw new Error('The consumer is already unsubscribed'); 94 | } else { 95 | this.queuesState[queue].consumers.splice(index, 1); 96 | } 97 | 98 | process.nextTick(() => { 99 | consumer.deliveredMessages.forEach((deliveredMessage) => 100 | this.publish(queue, deliveredMessage) 101 | ); 102 | }); 103 | }, 104 | }; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /packages/parse-blockchain-base/src/Worker.ts: -------------------------------------------------------------------------------- 1 | import { BlockchainStatus } from './types'; 2 | import BlockchainAdapter from './BlockchainAdapter'; 3 | import MQAdapter from './MQAdapter'; 4 | import SimpleMQAdapter from './SimpleMQAdapter'; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | const Parse = (global as any).Parse; 8 | 9 | interface Options { 10 | waitSendingAttempts?: number; 11 | waitSendingSleepMS?: number; 12 | } 13 | 14 | export default class Worker { 15 | private initialized = false; 16 | private blockchainAdapter: BlockchainAdapter; 17 | private mqAdapter: MQAdapter; 18 | private fails = 0; 19 | private options: Options; 20 | 21 | initialize( 22 | blockchainAdapter: BlockchainAdapter, 23 | mqAdapter?: MQAdapter, 24 | options?: Options 25 | ): void { 26 | if (this.initialized) { 27 | throw new Error('The worker is already initialized'); 28 | } else { 29 | this.initialized = true; 30 | } 31 | 32 | this.blockchainAdapter = blockchainAdapter; 33 | 34 | this.mqAdapter = mqAdapter || new SimpleMQAdapter(); 35 | 36 | const defaultOptions = { 37 | waitSendingAttempts: 30, // ~30 mins 38 | waitSendingSleepMS: 60 * 1000, // 1 min 39 | }; 40 | this.options = options || defaultOptions; 41 | this.options.waitSendingAttempts = 42 | this.options.waitSendingAttempts || defaultOptions.waitSendingAttempts; 43 | this.options.waitSendingSleepMS = 44 | this.options.waitSendingSleepMS || defaultOptions.waitSendingSleepMS; 45 | 46 | this.subscribe(); 47 | } 48 | 49 | private subscribe() { 50 | this.mqAdapter.consume( 51 | `${Parse.applicationId}-parse-server-blockchain`, 52 | this.handleMessage.bind(this) 53 | ); 54 | } 55 | 56 | private async handleMessage( 57 | message: string, 58 | ack: () => void, 59 | nack: () => void 60 | ) { 61 | if (this.fails > 0) { 62 | await new Promise((resolve) => setTimeout(resolve, this.fails * 1000)); 63 | } 64 | 65 | const parseObjectFullJSON = JSON.parse(message); 66 | const { className, objectId } = parseObjectFullJSON; 67 | 68 | let blockchainStatus: BlockchainStatus; 69 | for (let i = 0; i < this.options.waitSendingAttempts; i++) { 70 | // ~30 mins 71 | try { 72 | blockchainStatus = await this.getStatus(className, objectId); 73 | } catch (e) { 74 | console.error('Could not get object status', parseObjectFullJSON, e); 75 | this.fails++; 76 | nack(); 77 | return; 78 | } 79 | 80 | if (blockchainStatus) { 81 | console.warn( 82 | 'Object already has blockchain status', 83 | parseObjectFullJSON, 84 | blockchainStatus 85 | ); 86 | if (blockchainStatus === BlockchainStatus.Sending) { 87 | await new Promise((resolve) => 88 | setTimeout(resolve, this.options.waitSendingSleepMS) 89 | ); // 1 min 90 | } else { 91 | this.fails = 0; 92 | ack(); 93 | return; 94 | } 95 | } else { 96 | break; 97 | } 98 | } 99 | 100 | let blockchainResult: Record; 101 | if (blockchainStatus === BlockchainStatus.Sending) { 102 | try { 103 | blockchainResult = { 104 | type: 'Get', 105 | input: { className, objectId }, 106 | output: await this.blockchainAdapter.get(className, objectId), 107 | }; 108 | blockchainStatus = BlockchainStatus.Sent; 109 | } catch (e) { 110 | if (!/The object does not exist/.test(e)) { 111 | this.fails++; 112 | nack(); 113 | return; 114 | } 115 | } 116 | } else { 117 | try { 118 | await this.updateStatus(className, objectId, BlockchainStatus.Sending); 119 | blockchainStatus = BlockchainStatus.Sending; 120 | } catch (e) { 121 | console.error( 122 | 'Could not update object status', 123 | parseObjectFullJSON, 124 | BlockchainStatus.Sending, 125 | e 126 | ); 127 | this.fails++; 128 | nack(); 129 | return; 130 | } 131 | } 132 | 133 | if (blockchainStatus === BlockchainStatus.Sending) { 134 | try { 135 | blockchainResult = { 136 | type: 'Send', 137 | input: JSON.stringify(parseObjectFullJSON), 138 | output: await this.blockchainAdapter.send(parseObjectFullJSON), 139 | }; 140 | blockchainStatus = BlockchainStatus.Sent; 141 | } catch (e) { 142 | console.error('Could not send object', parseObjectFullJSON, e); 143 | blockchainResult = { 144 | type: 'Error', 145 | input: JSON.stringify(parseObjectFullJSON), 146 | error: e.toString(), 147 | }; 148 | blockchainStatus = BlockchainStatus.Failed; 149 | } 150 | } 151 | 152 | try { 153 | await this.updateStatus( 154 | className, 155 | objectId, 156 | blockchainStatus, 157 | blockchainResult 158 | ); 159 | this.fails = 0; 160 | } catch (e) { 161 | console.error( 162 | 'Could not update object status', 163 | parseObjectFullJSON, 164 | blockchainStatus, 165 | blockchainResult, 166 | e 167 | ); 168 | this.fails++; 169 | nack(); 170 | return; 171 | } 172 | 173 | ack(); 174 | } 175 | 176 | private async getStatus( 177 | className: string, 178 | objectId: string 179 | ): Promise { 180 | const parseQuery = new Parse.Query(className); 181 | const parseObject = await parseQuery.get(objectId, { useMasterKey: true }); 182 | return parseObject.get('blockchainStatus'); 183 | } 184 | 185 | private updateStatus( 186 | className: string, 187 | objectId: string, 188 | blockchainStatus: BlockchainStatus | null, 189 | blockchainResult?: Record 190 | ): Promise { 191 | const parseObject = new Parse.Object(className); 192 | parseObject.id = objectId; 193 | parseObject.set('blockchainStatus', blockchainStatus); 194 | if (blockchainResult) { 195 | parseObject.set('blockchainResult', blockchainResult); 196 | } 197 | return parseObject.save(null, { useMasterKey: true }); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /packages/parse-blockchain-base/src/index.ts: -------------------------------------------------------------------------------- 1 | import Bridge from './Bridge'; 2 | import Worker from './Worker'; 3 | 4 | export * from './types'; 5 | 6 | export { default as BlockchainAdapter } from './BlockchainAdapter'; 7 | 8 | export { default as MQAdapter } from './MQAdapter'; 9 | export * from './MQAdapter'; 10 | 11 | export { default as SimpleMQAdapter } from './SimpleMQAdapter'; 12 | 13 | export const bridge = new Bridge(); 14 | 15 | export const worker = new Worker(); 16 | -------------------------------------------------------------------------------- /packages/parse-blockchain-base/src/types.ts: -------------------------------------------------------------------------------- 1 | export enum BlockchainStatus { 2 | Sending = 'sending', 3 | Sent = 'sent', 4 | Failed = 'failed', 5 | } 6 | -------------------------------------------------------------------------------- /packages/parse-blockchain-base/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src"], 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "allowJs": true, 6 | "declaration": true, 7 | "skipLibCheck": true, 8 | "outDir": "./lib" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/parse-blockchain-ethereum/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/eslint-recommended', 6 | 'plugin:@typescript-eslint/recommended' 7 | ], 8 | env: { 9 | es2021: true, 10 | mocha: true, 11 | node: true, 12 | }, 13 | parser: '@typescript-eslint/parser', 14 | parserOptions: { 15 | sourceType: 'module', 16 | }, 17 | plugins: ['prettier'], 18 | rules: { 19 | 'prettier/prettier': 'error', 20 | '@typescript-eslint/no-var-requires': 'off' 21 | }, 22 | globals: { 23 | artifacts: true, 24 | contract: true, 25 | web3: true, 26 | assert: true 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /packages/parse-blockchain-ethereum/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | lib 3 | -------------------------------------------------------------------------------- /packages/parse-blockchain-ethereum/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true 3 | }; 4 | -------------------------------------------------------------------------------- /packages/parse-blockchain-ethereum/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Parse Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/parse-blockchain-ethereum/contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.4.22 <0.9.0; 3 | 4 | contract Migrations { 5 | address public owner = msg.sender; 6 | uint public last_completed_migration; 7 | 8 | modifier restricted() { 9 | require( 10 | msg.sender == owner, 11 | "This function is restricted to the contract's owner" 12 | ); 13 | _; 14 | } 15 | 16 | function setCompleted(uint completed) public restricted { 17 | last_completed_migration = completed; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/parse-blockchain-ethereum/contracts/Parse.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.4.22 <0.9.0; 3 | 4 | contract Parse { 5 | struct Class { 6 | string name; 7 | string[] objectsIds; 8 | mapping(string => string) objectJSONById; 9 | } 10 | 11 | struct App { 12 | string id; 13 | mapping (address => bool) owners; 14 | string[] classesNames; 15 | mapping(string => Class) classByName; 16 | } 17 | 18 | event AppCreated ( 19 | string _appId 20 | ); 21 | 22 | event AppOwnerAdded ( 23 | string _appId, 24 | address _owner 25 | ); 26 | 27 | event AppOwnerRemoved ( 28 | string _appId, 29 | address _owner 30 | ); 31 | 32 | event ClassCreated ( 33 | string _appId, 34 | string _className 35 | ); 36 | 37 | event ObjectCreated ( 38 | string _appId, 39 | string _className, 40 | string _objectId, 41 | string _objectJSON 42 | ); 43 | 44 | address public owner = msg.sender; 45 | string[] appsIds; 46 | mapping(string => App) appById; 47 | 48 | function addAppOwner(string memory _appId, address _owner) public { 49 | require(bytes(_appId).length > 0, "_appId is required"); 50 | App storage app = appById[_appId]; 51 | require( 52 | msg.sender == owner || app.owners[msg.sender], 53 | "This function is restricted to the contract and app owners" 54 | ); 55 | require(!app.owners[_owner], "The address is already an app owner"); 56 | if (bytes(app.id).length <= 0) { 57 | app.id = _appId; 58 | appsIds.push(_appId); 59 | emit AppCreated(_appId); 60 | } 61 | app.owners[_owner] = true; 62 | emit AppOwnerAdded(_appId, _owner); 63 | } 64 | 65 | function removeAppOwner(string memory _appId, address _owner) public { 66 | require(bytes(_appId).length > 0, "_appId is required"); 67 | App storage app = appById[_appId]; 68 | require( 69 | msg.sender == owner || app.owners[msg.sender], 70 | "This function is restricted to the contract and app owners" 71 | ); 72 | require(app.owners[_owner], "The address is not an app owner"); 73 | app.owners[_owner] = false; 74 | emit AppOwnerRemoved(_appId, _owner); 75 | } 76 | 77 | function createObject(string memory _appId, string memory _className, string memory _objectId, string memory _objectJSON) public { 78 | require(bytes(_appId).length > 0, "_appId is required"); 79 | require(bytes(_className).length > 0, "_className is required"); 80 | require(bytes(_objectId).length > 0, "_objectId is required"); 81 | require(bytes(_objectJSON).length > 0, "_objectJSON is required"); 82 | App storage app = appById[_appId]; 83 | require( 84 | msg.sender == owner || app.owners[msg.sender], 85 | "This function is restricted to the contract and app owners" 86 | ); 87 | if (bytes(app.id).length <= 0) { 88 | app.id = _appId; 89 | appsIds.push(_appId); 90 | emit AppCreated(_appId); 91 | } 92 | Class storage class = app.classByName[_className]; 93 | if (bytes(class.name).length <= 0) { 94 | class.name = _className; 95 | app.classesNames.push(_className); 96 | emit ClassCreated(_appId, _className); 97 | } 98 | require(bytes(class.objectJSONById[_objectId]).length <= 0, "_objectId must be unique"); 99 | class.objectsIds.push(_objectId); 100 | class.objectJSONById[_objectId] = _objectJSON; 101 | emit ObjectCreated(_appId, _className, _objectId, _objectJSON); 102 | } 103 | 104 | function getObjectJSON(string memory _appId, string memory _className, string memory _objectId) public view returns (string memory) { 105 | string memory objectJSON = appById[_appId].classByName[_className].objectJSONById[_objectId]; 106 | require(bytes(objectJSON).length > 0, "The object does not exist"); 107 | return objectJSON; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /packages/parse-blockchain-ethereum/migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require('Migrations'); 2 | 3 | module.exports = function (deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /packages/parse-blockchain-ethereum/migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | const Parse = artifacts.require('Parse'); 2 | 3 | module.exports = function (deployer) { 4 | deployer.deploy(Parse); 5 | }; 6 | -------------------------------------------------------------------------------- /packages/parse-blockchain-ethereum/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@parse/blockchain-ethereum", 3 | "publishConfig": { 4 | "access": "public" 5 | }, 6 | "version": "1.0.0-alpha.1", 7 | "author": "Parse Community ", 8 | "description": "Connects Parse Server to an Ethereum network.", 9 | "keywords": [ 10 | "parse", 11 | "parse server", 12 | "blockchain", 13 | "ethereum" 14 | ], 15 | "homepage": "https://parseplatform.org", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/parse-community/parse-server-blockchain", 19 | "directory": "packages/parse-blockchain-ethereum" 20 | }, 21 | "bugs": "https://github.com/parse-community/parse-server-blockchain/issues", 22 | "license": "MIT", 23 | "main": "lib/index.js", 24 | "files": [ 25 | "build", 26 | "migrations", 27 | "lib" 28 | ], 29 | "scripts": { 30 | "compile": "truffle compile", 31 | "build": "tsc --build ./tsconfig.json", 32 | "prepare": "npm run compile && npm run build", 33 | "lint": "eslint '{migrations,src,test}/**/*.{js,ts}' --fix", 34 | "pretest": "npm run lint && npm run build", 35 | "test": "truffle test" 36 | }, 37 | "devDependencies": { 38 | "@typescript-eslint/eslint-plugin": "4.28.4", 39 | "@typescript-eslint/parser": "4.27.0", 40 | "chai": "4.3.4", 41 | "chai-as-promised": "7.1.1", 42 | "eslint": "7.27.0", 43 | "eslint-plugin-prettier": "3.4.0", 44 | "mocha": "9.0.3", 45 | "prettier": "2.3.0", 46 | "truffle": "5.4.3", 47 | "typescript": "4.3.2" 48 | }, 49 | "dependencies": { 50 | "@parse/blockchain-base": "^1.0.0-alpha.1", 51 | "web3": "1.5.2" 52 | }, 53 | "gitHead": "c7caf1eb3da898c60d9f13f765bd432f05063127" 54 | } 55 | -------------------------------------------------------------------------------- /packages/parse-blockchain-ethereum/src/EthereumAdapter.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3'; 2 | import { Contract } from 'web3-eth-contract'; 3 | import { BlockchainAdapter } from '@parse/blockchain-base'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | const Parse = (global as any).Parse; 7 | const contractABI = require('../build/contracts/Parse.json').abi; 8 | 9 | export default class EthereumAdapter implements BlockchainAdapter { 10 | private web3: Web3; 11 | private contract: Contract; 12 | private ownerAddress: string; 13 | 14 | constructor(web3: Web3, contractAddress: string, ownerAddress: string) { 15 | this.web3 = web3; 16 | this.contract = new this.web3.eth.Contract(contractABI, contractAddress); 17 | this.ownerAddress = ownerAddress; 18 | } 19 | 20 | async send( 21 | parseObjectFullJSON: Record 22 | ): Promise> { 23 | const methodCall = this.contract.methods.createObject( 24 | Parse.applicationId, 25 | parseObjectFullJSON.className, 26 | parseObjectFullJSON.objectId, 27 | JSON.stringify({ 28 | ...parseObjectFullJSON, 29 | ...{ 30 | __type: undefined, 31 | className: undefined, 32 | objectId: undefined, 33 | updatedAt: undefined, 34 | }, 35 | }) 36 | ); 37 | const gas = await methodCall.estimateGas({ 38 | from: this.ownerAddress, 39 | }); 40 | return methodCall.send({ 41 | from: this.ownerAddress, 42 | gas, 43 | }); 44 | } 45 | 46 | async get( 47 | className: string, 48 | objectId: string 49 | ): Promise> { 50 | return JSON.parse( 51 | await this.contract.methods 52 | .getObjectJSON(Parse.applicationId, className, objectId) 53 | .call() 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/parse-blockchain-ethereum/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as EthereumAdapter } from './EthereumAdapter'; 2 | -------------------------------------------------------------------------------- /packages/parse-blockchain-ethereum/test/EthereumAdapter.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const Parse = require('parse/node'); 3 | 4 | global.Parse = Parse; 5 | 6 | const EthereumAdapter = require('../lib/EthereumAdapter').default; 7 | 8 | const ParseArtifact = artifacts.require('Parse'); 9 | 10 | contract('Parse', (accounts) => { 11 | let ethereumAdapter; 12 | 13 | before(async () => { 14 | Parse.initialize('someappid'); 15 | const contract = await ParseArtifact.deployed(); 16 | ethereumAdapter = new EthereumAdapter(web3, contract.address, accounts[0]); 17 | }); 18 | 19 | describe('send', () => { 20 | it('should send Parse object to smart contract', async () => { 21 | const someObject = new Parse.Object('SomeClass'); 22 | someObject.id = 'someobjectid'; 23 | someObject.set('someField', 'someValue'); 24 | const result = await ethereumAdapter.send(someObject._toFullJSON()); 25 | expect(Object.keys(result.events)).to.eql([ 26 | 'AppCreated', 27 | 'ClassCreated', 28 | 'ObjectCreated', 29 | ]); 30 | expect(result.events.AppCreated.type).to.equal('mined'); 31 | expect(result.events.AppCreated.returnValues._appId).to.equal( 32 | 'someappid' 33 | ); 34 | expect(result.events.ClassCreated.type).to.equal('mined'); 35 | expect(result.events.ClassCreated.returnValues._appId).to.equal( 36 | 'someappid' 37 | ); 38 | expect(result.events.ClassCreated.returnValues._className).to.equal( 39 | 'SomeClass' 40 | ); 41 | expect(result.events.ObjectCreated.type).to.equal('mined'); 42 | expect(result.events.ObjectCreated.returnValues._appId).to.equal( 43 | 'someappid' 44 | ); 45 | expect(result.events.ObjectCreated.returnValues._className).to.equal( 46 | 'SomeClass' 47 | ); 48 | expect(result.events.ObjectCreated.returnValues._objectId).to.equal( 49 | 'someobjectid' 50 | ); 51 | expect(result.events.ObjectCreated.returnValues._objectJSON).to.equal( 52 | JSON.stringify({ 53 | someField: 'someValue', 54 | }) 55 | ); 56 | }); 57 | }); 58 | 59 | describe('get', () => { 60 | it('should get sent objects', async () => { 61 | const objectJSON = await ethereumAdapter.get('SomeClass', 'someobjectid'); 62 | expect(JSON.stringify(objectJSON)).to.equal( 63 | JSON.stringify({ 64 | someField: 'someValue', 65 | }) 66 | ); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /packages/parse-blockchain-ethereum/test/Parse.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | 3 | require('chai').use(require('chai-as-promised')).should(); 4 | 5 | const ParseArtifact = artifacts.require('Parse'); 6 | 7 | contract('Parse', (accounts) => { 8 | let contract; 9 | 10 | before(async () => { 11 | contract = await ParseArtifact.deployed(); 12 | }); 13 | 14 | describe('contract deployment', () => { 15 | it('has an address', async () => { 16 | const address = contract.address; 17 | expect(address).to.not.equal(0x0); 18 | expect(address).to.not.equal(''); 19 | expect(address).to.not.equal(null); 20 | expect(address).to.not.equal(undefined); 21 | }); 22 | 23 | it('has an owner', async () => { 24 | const owner = await contract.owner(); 25 | expect(owner).to.equal(accounts[0]); 26 | }); 27 | }); 28 | 29 | describe('createObject', () => { 30 | it('should create object', async () => { 31 | const result = await contract.createObject( 32 | 'someappid', 33 | 'SomeClass', 34 | 'someobjectid', 35 | JSON.stringify({ someField: 'someValue' }), 36 | { 37 | from: accounts[0], 38 | } 39 | ); 40 | expect(result.logs.length).to.equal(3); 41 | expect(result.logs[0].event).to.equal('AppCreated'); 42 | expect(result.logs[0].type).to.equal('mined'); 43 | expect(result.logs[0].args._appId).to.equal('someappid'); 44 | expect(result.logs[1].event).to.equal('ClassCreated'); 45 | expect(result.logs[1].type).to.equal('mined'); 46 | expect(result.logs[1].args._appId).to.equal('someappid'); 47 | expect(result.logs[1].args._className).to.equal('SomeClass'); 48 | expect(result.logs[2].event).to.equal('ObjectCreated'); 49 | expect(result.logs[2].type).to.equal('mined'); 50 | expect(result.logs[2].args._appId).to.equal('someappid'); 51 | expect(result.logs[2].args._className).to.equal('SomeClass'); 52 | expect(result.logs[2].args._objectId).to.equal('someobjectid'); 53 | expect(result.logs[2].args._objectJSON).to.equal( 54 | JSON.stringify({ someField: 'someValue' }) 55 | ); 56 | }); 57 | 58 | it('should be restricted to the contract and app owners', async () => { 59 | await contract 60 | .createObject( 61 | 'someappid', 62 | 'SomeClass', 63 | 'someotherobjectid', 64 | JSON.stringify({ someField: 'someValue' }), 65 | { 66 | from: accounts[1], 67 | } 68 | ) 69 | .should.be.rejectedWith( 70 | /This function is restricted to the contract and app owners/ 71 | ); 72 | await contract 73 | .createObject( 74 | 'someotherappid', 75 | 'SomeClass', 76 | 'someotherobjectid', 77 | JSON.stringify({ someField: 'someValue' }), 78 | { 79 | from: accounts[1], 80 | } 81 | ) 82 | .should.be.rejectedWith( 83 | /This function is restricted to the contract and app owners/ 84 | ); 85 | const result1 = await contract.addAppOwner('someappid', accounts[1]); 86 | expect(result1.logs.length).to.equal(1); 87 | expect(result1.logs[0].event).to.equal('AppOwnerAdded'); 88 | expect(result1.logs[0].type).to.equal('mined'); 89 | expect(result1.logs[0].args._appId).to.equal('someappid'); 90 | expect(result1.logs[0].args._owner).to.equal(accounts[1]); 91 | const result2 = await contract.createObject( 92 | 'someappid', 93 | 'SomeClass', 94 | 'someotherobjectid', 95 | JSON.stringify({ someField: 'someValue' }), 96 | { 97 | from: accounts[0], 98 | } 99 | ); 100 | expect(result2.logs.length).to.equal(1); 101 | expect(result2.logs[0].event).to.equal('ObjectCreated'); 102 | expect(result2.logs[0].type).to.equal('mined'); 103 | expect(result2.logs[0].args._appId).to.equal('someappid'); 104 | expect(result2.logs[0].args._className).to.equal('SomeClass'); 105 | expect(result2.logs[0].args._objectId).to.equal('someotherobjectid'); 106 | expect(result2.logs[0].args._objectJSON).to.equal( 107 | JSON.stringify({ someField: 'someValue' }) 108 | ); 109 | await contract 110 | .createObject( 111 | 'someotherappid', 112 | 'SomeClass', 113 | 'someotherobjectid', 114 | JSON.stringify({ someField: 'someValue' }), 115 | { 116 | from: accounts[1], 117 | } 118 | ) 119 | .should.be.rejectedWith( 120 | /This function is restricted to the contract and app owners/ 121 | ); 122 | const result3 = await contract.addAppOwner('someotherappid', accounts[1]); 123 | expect(result3.logs.length).to.equal(2); 124 | expect(result3.logs[0].event).to.equal('AppCreated'); 125 | expect(result3.logs[0].type).to.equal('mined'); 126 | expect(result3.logs[0].args._appId).to.equal('someotherappid'); 127 | expect(result3.logs[1].event).to.equal('AppOwnerAdded'); 128 | expect(result3.logs[1].type).to.equal('mined'); 129 | expect(result3.logs[1].args._appId).to.equal('someotherappid'); 130 | expect(result3.logs[1].args._owner).to.equal(accounts[1]); 131 | const result4 = await contract.createObject( 132 | 'someotherappid', 133 | 'SomeClass', 134 | 'someotherobjectid', 135 | JSON.stringify({ someField: 'someValue' }), 136 | { 137 | from: accounts[0], 138 | } 139 | ); 140 | expect(result4.logs.length).to.equal(2); 141 | expect(result4.logs[0].event).to.equal('ClassCreated'); 142 | expect(result4.logs[0].type).to.equal('mined'); 143 | expect(result4.logs[0].args._appId).to.equal('someotherappid'); 144 | expect(result4.logs[0].args._className).to.equal('SomeClass'); 145 | expect(result4.logs[1].event).to.equal('ObjectCreated'); 146 | expect(result4.logs[1].type).to.equal('mined'); 147 | expect(result4.logs[1].args._appId).to.equal('someotherappid'); 148 | expect(result4.logs[1].args._className).to.equal('SomeClass'); 149 | expect(result4.logs[1].args._objectId).to.equal('someotherobjectid'); 150 | expect(result4.logs[1].args._objectJSON).to.equal( 151 | JSON.stringify({ someField: 'someValue' }) 152 | ); 153 | }); 154 | 155 | it('should require _appId', () => { 156 | return contract 157 | .createObject( 158 | '', 159 | 'SomeClass', 160 | 'someobjectid', 161 | JSON.stringify({ someField: 'someValue' }), 162 | { 163 | from: accounts[0], 164 | } 165 | ) 166 | .should.be.rejectedWith(/_appId is required/); 167 | }); 168 | 169 | it('should require _className', () => { 170 | return contract 171 | .createObject( 172 | 'someappid', 173 | '', 174 | 'someobjectid', 175 | JSON.stringify({ someField: 'someValue' }), 176 | { 177 | from: accounts[0], 178 | } 179 | ) 180 | .should.be.rejectedWith(/_className is required/); 181 | }); 182 | 183 | it('should require _objectId', () => { 184 | return contract 185 | .createObject( 186 | 'someappid', 187 | 'SomeClass', 188 | '', 189 | JSON.stringify({ someField: 'someValue' }), 190 | { 191 | from: accounts[0], 192 | } 193 | ) 194 | .should.be.rejectedWith(/_objectId is required/); 195 | }); 196 | 197 | it('should require _objectJSON', () => { 198 | return contract 199 | .createObject('someappid', 'SomeClass', 'someobjectid', '', { 200 | from: accounts[0], 201 | }) 202 | .should.be.rejectedWith(/_objectJSON is required/); 203 | }); 204 | 205 | it('should create objects for different apps ids', async () => { 206 | const result1 = await contract.createObject( 207 | 'someappid1', 208 | 'SomeClass', 209 | 'someobjectid', 210 | JSON.stringify({ someField: 'someValue' }), 211 | { 212 | from: accounts[0], 213 | } 214 | ); 215 | expect(result1.logs.length).to.equal(3); 216 | expect(result1.logs[0].event).to.equal('AppCreated'); 217 | expect(result1.logs[0].type).to.equal('mined'); 218 | expect(result1.logs[0].args._appId).to.equal('someappid1'); 219 | expect(result1.logs[1].event).to.equal('ClassCreated'); 220 | expect(result1.logs[1].type).to.equal('mined'); 221 | expect(result1.logs[1].args._appId).to.equal('someappid1'); 222 | expect(result1.logs[1].args._className).to.equal('SomeClass'); 223 | expect(result1.logs[2].event).to.equal('ObjectCreated'); 224 | expect(result1.logs[2].type).to.equal('mined'); 225 | expect(result1.logs[2].args._appId).to.equal('someappid1'); 226 | expect(result1.logs[2].args._className).to.equal('SomeClass'); 227 | expect(result1.logs[2].args._objectId).to.equal('someobjectid'); 228 | expect(result1.logs[2].args._objectJSON).to.equal( 229 | JSON.stringify({ someField: 'someValue' }) 230 | ); 231 | const result2 = await contract.createObject( 232 | 'someappid2', 233 | 'SomeClass', 234 | 'someobjectid', 235 | JSON.stringify({ someField: 'someValue' }), 236 | { 237 | from: accounts[0], 238 | } 239 | ); 240 | expect(result2.logs.length).to.equal(3); 241 | expect(result2.logs[0].event).to.equal('AppCreated'); 242 | expect(result2.logs[0].type).to.equal('mined'); 243 | expect(result2.logs[0].args._appId).to.equal('someappid2'); 244 | expect(result2.logs[1].event).to.equal('ClassCreated'); 245 | expect(result2.logs[1].type).to.equal('mined'); 246 | expect(result2.logs[1].args._appId).to.equal('someappid2'); 247 | expect(result1.logs[1].args._className).to.equal('SomeClass'); 248 | expect(result2.logs[2].event).to.equal('ObjectCreated'); 249 | expect(result2.logs[2].type).to.equal('mined'); 250 | expect(result2.logs[2].args._appId).to.equal('someappid2'); 251 | expect(result1.logs[2].args._className).to.equal('SomeClass'); 252 | expect(result2.logs[2].args._objectId).to.equal('someobjectid'); 253 | expect(result2.logs[2].args._objectJSON).to.equal( 254 | JSON.stringify({ someField: 'someValue' }) 255 | ); 256 | }); 257 | 258 | it('should create objects for different classes', async () => { 259 | const result1 = await contract.createObject( 260 | 'someappid', 261 | 'SomeClass1', 262 | 'someobjectid', 263 | JSON.stringify({ someField: 'someValue' }), 264 | { 265 | from: accounts[0], 266 | } 267 | ); 268 | expect(result1.logs.length).to.equal(2); 269 | expect(result1.logs[0].event).to.equal('ClassCreated'); 270 | expect(result1.logs[0].type).to.equal('mined'); 271 | expect(result1.logs[0].args._appId).to.equal('someappid'); 272 | expect(result1.logs[0].args._className).to.equal('SomeClass1'); 273 | expect(result1.logs[1].event).to.equal('ObjectCreated'); 274 | expect(result1.logs[1].type).to.equal('mined'); 275 | expect(result1.logs[1].args._appId).to.equal('someappid'); 276 | expect(result1.logs[1].args._className).to.equal('SomeClass1'); 277 | expect(result1.logs[1].args._objectId).to.equal('someobjectid'); 278 | expect(result1.logs[1].args._objectJSON).to.equal( 279 | JSON.stringify({ someField: 'someValue' }) 280 | ); 281 | const result2 = await contract.createObject( 282 | 'someappid', 283 | 'SomeClass2', 284 | 'someobjectid', 285 | JSON.stringify({ someField: 'someValue' }), 286 | { 287 | from: accounts[0], 288 | } 289 | ); 290 | expect(result2.logs.length).to.equal(2); 291 | expect(result2.logs[0].event).to.equal('ClassCreated'); 292 | expect(result2.logs[0].type).to.equal('mined'); 293 | expect(result2.logs[0].args._appId).to.equal('someappid'); 294 | expect(result2.logs[0].args._className).to.equal('SomeClass2'); 295 | expect(result2.logs[1].event).to.equal('ObjectCreated'); 296 | expect(result2.logs[1].type).to.equal('mined'); 297 | expect(result2.logs[1].args._appId).to.equal('someappid'); 298 | expect(result2.logs[1].args._className).to.equal('SomeClass2'); 299 | expect(result2.logs[1].args._objectId).to.equal('someobjectid'); 300 | expect(result2.logs[1].args._objectJSON).to.equal( 301 | JSON.stringify({ someField: 'someValue' }) 302 | ); 303 | }); 304 | 305 | it('should create objects for the name class', async () => { 306 | const result1 = await contract.createObject( 307 | 'someappid', 308 | 'SomeClass', 309 | 'someobjectid1', 310 | JSON.stringify({ someField: 'someValue' }), 311 | { 312 | from: accounts[0], 313 | } 314 | ); 315 | expect(result1.logs.length).to.equal(1); 316 | expect(result1.logs[0].event).to.equal('ObjectCreated'); 317 | expect(result1.logs[0].type).to.equal('mined'); 318 | expect(result1.logs[0].args._appId).to.equal('someappid'); 319 | expect(result1.logs[0].args._className).to.equal('SomeClass'); 320 | expect(result1.logs[0].args._objectId).to.equal('someobjectid1'); 321 | expect(result1.logs[0].args._objectJSON).to.equal( 322 | JSON.stringify({ someField: 'someValue' }) 323 | ); 324 | const result2 = await contract.createObject( 325 | 'someappid', 326 | 'SomeClass', 327 | 'someobjectid2', 328 | JSON.stringify({ someField: 'someValue' }), 329 | { 330 | from: accounts[0], 331 | } 332 | ); 333 | expect(result2.logs.length).to.equal(1); 334 | expect(result2.logs[0].event).to.equal('ObjectCreated'); 335 | expect(result2.logs[0].type).to.equal('mined'); 336 | expect(result2.logs[0].args._appId).to.equal('someappid'); 337 | expect(result2.logs[0].args._className).to.equal('SomeClass'); 338 | expect(result2.logs[0].args._objectId).to.equal('someobjectid2'); 339 | expect(result1.logs[0].args._objectJSON).to.equal( 340 | JSON.stringify({ someField: 'someValue' }) 341 | ); 342 | }); 343 | 344 | it('should enforce unique object ids', async () => { 345 | const result = await contract.createObject( 346 | 'someappid', 347 | 'SomeClass', 348 | 'duplicatedobjectid', 349 | JSON.stringify({ someField: 'someValue' }), 350 | { 351 | from: accounts[0], 352 | } 353 | ); 354 | expect(result.logs.length).to.equal(1); 355 | expect(result.logs[0].event).to.equal('ObjectCreated'); 356 | expect(result.logs[0].type).to.equal('mined'); 357 | expect(result.logs[0].args._appId).to.equal('someappid'); 358 | expect(result.logs[0].args._className).to.equal('SomeClass'); 359 | expect(result.logs[0].args._objectId).to.equal('duplicatedobjectid'); 360 | expect(result.logs[0].args._objectJSON).to.equal( 361 | JSON.stringify({ someField: 'someValue' }) 362 | ); 363 | await contract 364 | .createObject( 365 | 'someappid', 366 | 'SomeClass', 367 | 'duplicatedobjectid', 368 | JSON.stringify({ someField: 'someValue' }), 369 | { 370 | from: accounts[0], 371 | } 372 | ) 373 | .should.be.rejectedWith(/_objectId must be unique/); 374 | }); 375 | }); 376 | 377 | describe('getObjectJSON', () => { 378 | it('should get an object', async () => { 379 | expect( 380 | await contract.getObjectJSON('someappid', 'SomeClass', 'someobjectid') 381 | ).to.equal(JSON.stringify({ someField: 'someValue' })); 382 | expect( 383 | await contract.getObjectJSON('someappid1', 'SomeClass', 'someobjectid') 384 | ).to.equal(JSON.stringify({ someField: 'someValue' })); 385 | expect( 386 | await contract.getObjectJSON('someappid2', 'SomeClass', 'someobjectid') 387 | ).to.equal(JSON.stringify({ someField: 'someValue' })); 388 | expect( 389 | await contract.getObjectJSON('someappid', 'SomeClass1', 'someobjectid') 390 | ).to.equal(JSON.stringify({ someField: 'someValue' })); 391 | expect( 392 | await contract.getObjectJSON('someappid', 'SomeClass2', 'someobjectid') 393 | ).to.equal(JSON.stringify({ someField: 'someValue' })); 394 | expect( 395 | await contract.getObjectJSON('someappid', 'SomeClass', 'someobjectid1') 396 | ).to.equal(JSON.stringify({ someField: 'someValue' })); 397 | expect( 398 | await contract.getObjectJSON('someappid', 'SomeClass', 'someobjectid2') 399 | ).to.equal(JSON.stringify({ someField: 'someValue' })); 400 | expect( 401 | await contract.getObjectJSON( 402 | 'someappid', 403 | 'SomeClass', 404 | 'duplicatedobjectid' 405 | ) 406 | ).to.equal(JSON.stringify({ someField: 'someValue' })); 407 | }); 408 | 409 | it('should fail if the object does not exist', () => { 410 | return contract 411 | .getObjectJSON( 412 | 'inexistendappid', 413 | 'InexistentClass', 414 | 'inexistentobjectid' 415 | ) 416 | .should.be.rejectedWith(/The object does not exist/); 417 | }); 418 | }); 419 | 420 | describe('addAppOwner', () => { 421 | it('should add app owner to new app', async () => { 422 | const result = await contract.addAppOwner('somenewappid', accounts[2]); 423 | expect(result.logs.length).to.equal(2); 424 | expect(result.logs[0].event).to.equal('AppCreated'); 425 | expect(result.logs[0].type).to.equal('mined'); 426 | expect(result.logs[0].args._appId).to.equal('somenewappid'); 427 | expect(result.logs[1].event).to.equal('AppOwnerAdded'); 428 | expect(result.logs[1].type).to.equal('mined'); 429 | expect(result.logs[1].args._appId).to.equal('somenewappid'); 430 | expect(result.logs[1].args._owner).to.equal(accounts[2]); 431 | }); 432 | 433 | it('should add app owner to existing app', async () => { 434 | const result = await contract.addAppOwner('somenewappid', accounts[3]); 435 | expect(result.logs.length).to.equal(1); 436 | expect(result.logs[0].event).to.equal('AppOwnerAdded'); 437 | expect(result.logs[0].type).to.equal('mined'); 438 | expect(result.logs[0].args._appId).to.equal('somenewappid'); 439 | expect(result.logs[0].args._owner).to.equal(accounts[3]); 440 | }); 441 | 442 | it('should be restricted to the contract and app owners', async () => { 443 | await contract 444 | .addAppOwner('somenewappid', accounts[5], { from: accounts[4] }) 445 | .should.be.rejectedWith( 446 | /This function is restricted to the contract and app owners/ 447 | ); 448 | await contract 449 | .addAppOwner('someothernewappid', accounts[5], { from: accounts[4] }) 450 | .should.be.rejectedWith( 451 | /This function is restricted to the contract and app owners/ 452 | ); 453 | const result1 = await contract.addAppOwner('somenewappid', accounts[4], { 454 | from: accounts[3], 455 | }); 456 | expect(result1.logs.length).to.equal(1); 457 | expect(result1.logs[0].event).to.equal('AppOwnerAdded'); 458 | expect(result1.logs[0].type).to.equal('mined'); 459 | expect(result1.logs[0].args._appId).to.equal('somenewappid'); 460 | expect(result1.logs[0].args._owner).to.equal(accounts[4]); 461 | const result2 = await contract.addAppOwner( 462 | 'someothernewappid', 463 | accounts[4], 464 | { 465 | from: accounts[0], 466 | } 467 | ); 468 | expect(result2.logs.length).to.equal(2); 469 | expect(result2.logs[0].event).to.equal('AppCreated'); 470 | expect(result2.logs[0].type).to.equal('mined'); 471 | expect(result2.logs[0].args._appId).to.equal('someothernewappid'); 472 | expect(result2.logs[1].event).to.equal('AppOwnerAdded'); 473 | expect(result2.logs[1].type).to.equal('mined'); 474 | expect(result2.logs[1].args._appId).to.equal('someothernewappid'); 475 | expect(result2.logs[1].args._owner).to.equal(accounts[4]); 476 | }); 477 | 478 | it('should require _appId', () => { 479 | return contract 480 | .addAppOwner('', accounts[6]) 481 | .should.be.rejectedWith(/_appId is required/); 482 | }); 483 | 484 | it('should not allow duplicated owners', async () => { 485 | contract 486 | .addAppOwner('someothernewappid', accounts[4]) 487 | .should.be.rejectedWith(/The address is already an app owner/); 488 | const result1 = await contract.removeAppOwner( 489 | 'someothernewappid', 490 | accounts[4] 491 | ); 492 | expect(result1.logs.length).to.equal(1); 493 | expect(result1.logs[0].event).to.equal('AppOwnerRemoved'); 494 | expect(result1.logs[0].type).to.equal('mined'); 495 | expect(result1.logs[0].args._appId).to.equal('someothernewappid'); 496 | expect(result1.logs[0].args._owner).to.equal(accounts[4]); 497 | const result2 = await contract.addAppOwner( 498 | 'someothernewappid', 499 | accounts[4] 500 | ); 501 | expect(result2.logs.length).to.equal(1); 502 | expect(result2.logs[0].event).to.equal('AppOwnerAdded'); 503 | expect(result2.logs[0].type).to.equal('mined'); 504 | expect(result2.logs[0].args._appId).to.equal('someothernewappid'); 505 | expect(result2.logs[0].args._owner).to.equal(accounts[4]); 506 | }); 507 | }); 508 | 509 | describe('removeAppOwner', () => { 510 | it('should remove app owner', async () => { 511 | const result = await contract.removeAppOwner('somenewappid', accounts[3]); 512 | expect(result.logs.length).to.equal(1); 513 | expect(result.logs[0].event).to.equal('AppOwnerRemoved'); 514 | expect(result.logs[0].type).to.equal('mined'); 515 | expect(result.logs[0].args._appId).to.equal('somenewappid'); 516 | expect(result.logs[0].args._owner).to.equal(accounts[3]); 517 | await contract 518 | .createObject( 519 | 'somenewappid', 520 | 'SomeClass', 521 | 'someothernewobjectid', 522 | JSON.stringify({ someField: 'someValue' }), 523 | { 524 | from: accounts[3], 525 | } 526 | ) 527 | .should.be.rejectedWith( 528 | /This function is restricted to the contract and app owners/ 529 | ); 530 | await contract 531 | .addAppOwner('somenewappid', accounts[8], { 532 | from: accounts[3], 533 | }) 534 | .should.be.rejectedWith( 535 | /This function is restricted to the contract and app owners/ 536 | ); 537 | }); 538 | 539 | it('should be restricted to the contract and app owners', async () => { 540 | await contract 541 | .removeAppOwner('someappid', accounts[1], { from: accounts[7] }) 542 | .should.be.rejectedWith( 543 | /This function is restricted to the contract and app owners/ 544 | ); 545 | const result1 = await contract.addAppOwner('someappid', accounts[7], { 546 | from: accounts[1], 547 | }); 548 | expect(result1.logs.length).to.equal(1); 549 | expect(result1.logs[0].event).to.equal('AppOwnerAdded'); 550 | expect(result1.logs[0].type).to.equal('mined'); 551 | expect(result1.logs[0].args._appId).to.equal('someappid'); 552 | expect(result1.logs[0].args._owner).to.equal(accounts[7]); 553 | const result2 = await contract.removeAppOwner('someappid', accounts[1], { 554 | from: accounts[7], 555 | }); 556 | expect(result2.logs.length).to.equal(1); 557 | expect(result2.logs[0].event).to.equal('AppOwnerRemoved'); 558 | expect(result2.logs[0].type).to.equal('mined'); 559 | expect(result2.logs[0].args._appId).to.equal('someappid'); 560 | expect(result2.logs[0].args._owner).to.equal(accounts[1]); 561 | const result3 = await contract.removeAppOwner('someappid', accounts[7]); 562 | expect(result3.logs.length).to.equal(1); 563 | expect(result3.logs[0].event).to.equal('AppOwnerRemoved'); 564 | expect(result3.logs[0].type).to.equal('mined'); 565 | expect(result3.logs[0].args._appId).to.equal('someappid'); 566 | expect(result3.logs[0].args._owner).to.equal(accounts[7]); 567 | }); 568 | 569 | it('should require _appId', () => { 570 | return contract 571 | .removeAppOwner('', accounts[1]) 572 | .should.be.rejectedWith(/_appId is required/); 573 | }); 574 | 575 | it('should not allow to remove the owner twice', async () => { 576 | await contract 577 | .removeAppOwner('somenewappid', accounts[3]) 578 | .should.be.rejectedWith(/The address is not an app owner/); 579 | const result1 = await contract.addAppOwner('somenewappid', accounts[3]); 580 | expect(result1.logs.length).to.equal(1); 581 | expect(result1.logs[0].event).to.equal('AppOwnerAdded'); 582 | expect(result1.logs[0].type).to.equal('mined'); 583 | expect(result1.logs[0].args._appId).to.equal('somenewappid'); 584 | expect(result1.logs[0].args._owner).to.equal(accounts[3]); 585 | const result2 = await contract.removeAppOwner( 586 | 'somenewappid', 587 | accounts[3] 588 | ); 589 | expect(result2.logs.length).to.equal(1); 590 | expect(result2.logs[0].event).to.equal('AppOwnerRemoved'); 591 | expect(result2.logs[0].type).to.equal('mined'); 592 | expect(result2.logs[0].args._appId).to.equal('somenewappid'); 593 | expect(result2.logs[0].args._owner).to.equal(accounts[3]); 594 | }); 595 | }); 596 | }); 597 | -------------------------------------------------------------------------------- /packages/parse-blockchain-ethereum/test/index.test.js: -------------------------------------------------------------------------------- 1 | const { assert } = require('chai'); 2 | const index = require('../'); 3 | const EthereumAdapter = require('../lib/EthereumAdapter').default; 4 | 5 | describe('index', () => { 6 | it('should export EthereumAdapter', () => { 7 | assert.equal(index.EthereumAdapter, EthereumAdapter); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/parse-blockchain-ethereum/truffle-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this file to configure your truffle project. It's seeded with some 3 | * common settings for different networks and features like migrations, 4 | * compilation and testing. Uncomment the ones you need or modify 5 | * them to suit your project as necessary. 6 | * 7 | * More information about configuration can be found at: 8 | * 9 | * trufflesuite.com/docs/advanced/configuration 10 | * 11 | * To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider) 12 | * to sign your transactions before they're sent to a remote public node. Infura accounts 13 | * are available for free at: infura.io/register. 14 | * 15 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate 16 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this 17 | * phrase from a file you've .gitignored so it doesn't accidentally become public. 18 | * 19 | */ 20 | 21 | // const HDWalletProvider = require('@truffle/hdwallet-provider'); 22 | // 23 | // const fs = require('fs'); 24 | // const mnemonic = fs.readFileSync(".secret").toString().trim(); 25 | 26 | module.exports = { 27 | /** 28 | * Networks define how you connect to your ethereum client and let you set the 29 | * defaults web3 uses to send transactions. If you don't specify one truffle 30 | * will spin up a development blockchain for you on port 9545 when you 31 | * run `develop` or `test`. You can ask a truffle command to use a specific 32 | * network from the command line, e.g 33 | * 34 | * $ truffle test --network 35 | */ 36 | 37 | networks: { 38 | // Useful for testing. The `development` name is special - truffle uses it by default 39 | // if it's defined here and no other network is specified at the command line. 40 | // You should run a client (like ganache-cli, geth or parity) in a separate terminal 41 | // tab if you use this network and you must also set the `host`, `port` and `network_id` 42 | // options below to some value. 43 | // 44 | // development: { 45 | // host: "127.0.0.1", // Localhost (default: none) 46 | // port: 8545, // Standard Ethereum port (default: none) 47 | // network_id: "*", // Any network (default: none) 48 | // }, 49 | // Another network with more advanced options... 50 | // advanced: { 51 | // port: 8777, // Custom port 52 | // network_id: 1342, // Custom network 53 | // gas: 8500000, // Gas sent with each transaction (default: ~6700000) 54 | // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) 55 | // from:
, // Account to send txs from (default: accounts[0]) 56 | // websocket: true // Enable EventEmitter interface for web3 (default: false) 57 | // }, 58 | // Useful for deploying to a public network. 59 | // NB: It's important to wrap the provider as a function. 60 | // ropsten: { 61 | // provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`), 62 | // network_id: 3, // Ropsten's id 63 | // gas: 5500000, // Ropsten has a lower block limit than mainnet 64 | // confirmations: 2, // # of confs to wait between deployments. (default: 0) 65 | // timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) 66 | // skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) 67 | // }, 68 | // Useful for private networks 69 | // private: { 70 | // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), 71 | // network_id: 2111, // This network is yours, in the cloud. 72 | // production: true // Treats this network as if it was a public net. (default: false) 73 | // } 74 | }, 75 | 76 | // Set default mocha options here, use special reporters etc. 77 | mocha: { 78 | // timeout: 100000 79 | }, 80 | 81 | // Configure your compilers 82 | compilers: { 83 | solc: { 84 | // version: "0.5.1", // Fetch exact version from solc-bin (default: truffle's version) 85 | // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) 86 | // settings: { // See the solidity docs for advice about optimization and evmVersion 87 | // optimizer: { 88 | // enabled: false, 89 | // runs: 200 90 | // }, 91 | // evmVersion: "byzantium" 92 | // } 93 | } 94 | }, 95 | 96 | // Truffle DB is currently disabled by default; to enable it, change enabled: false to enabled: true 97 | // 98 | // Note: if you migrated your contracts prior to enabling this field in your Truffle project and want 99 | // those previously migrated contracts available in the .db directory, you will need to run the following: 100 | // $ truffle migrate --reset --compile-all 101 | 102 | db: { 103 | enabled: false 104 | } 105 | }; 106 | -------------------------------------------------------------------------------- /packages/parse-blockchain-ethereum/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src"], 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "allowJs": true, 6 | "declaration": true, 7 | "skipLibCheck": true, 8 | "outDir": "./lib" 9 | } 10 | } 11 | --------------------------------------------------------------------------------