├── .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 | 
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) | [](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) | [](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 |
--------------------------------------------------------------------------------