mirror of
https://github.com/status-im/visual-identity.git
synced 2025-02-08 02:33:53 +00:00
Added js demo from loom
This commit is contained in:
parent
cdbf32c454
commit
d48050fa25
6
loom_js_test/.babelrc
Normal file
6
loom_js_test/.babelrc
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env"],
|
||||
"plugins": [
|
||||
["@babel/plugin-transform-runtime"]
|
||||
]
|
||||
}
|
13
loom_js_test/.editorconfig
Normal file
13
loom_js_test/.editorconfig
Normal file
@ -0,0 +1,13 @@
|
||||
# http://editorconfig.org
|
||||
|
||||
# This is the top-level config
|
||||
root = true
|
||||
|
||||
[*]
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{js,ts}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
charset = utf-8
|
4
loom_js_test/.env.test.example
Normal file
4
loom_js_test/.env.test.example
Normal file
@ -0,0 +1,4 @@
|
||||
TEST_LOOM_DAPP_WS_WRITE_URL=ws://127.0.0.1:46657/websocket
|
||||
TEST_LOOM_DAPP_WS_READ_URL=ws://127.0.0.1:9999/queryws
|
||||
TEST_LOOM_DAPP_HTTP_WRITE_URL=http://127.0.0.1:46658/rpc
|
||||
TEST_LOOM_DAPP_HTTP_READ_URL=http://127.0.0.1:46658/query
|
4
loom_js_test/.env.test.jenkins
Normal file
4
loom_js_test/.env.test.jenkins
Normal file
@ -0,0 +1,4 @@
|
||||
TEST_LOOM_DAPP_WS_WRITE_URL=ws://127.0.0.1:46657/websocket
|
||||
TEST_LOOM_DAPP_WS_READ_URL=ws://127.0.0.1:9999/queryws
|
||||
TEST_LOOM_DAPP_HTTP_WRITE_URL=http://127.0.0.1:46658/rpc
|
||||
TEST_LOOM_DAPP_HTTP_READ_URL=http://127.0.0.1:46658/query
|
10
loom_js_test/.gitignore
vendored
Normal file
10
loom_js_test/.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
# gitignore
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
|
||||
# Only apps should have lockfiles
|
||||
package-lock.json
|
||||
/dist/*
|
||||
/.env.test
|
||||
/yarn-error.log
|
2
loom_js_test/.prettierignore
Normal file
2
loom_js_test/.prettierignore
Normal file
@ -0,0 +1,2 @@
|
||||
src/proto/*.d.ts
|
||||
src/tests/tests_pb.d.ts
|
7
loom_js_test/.prettierrc
Normal file
7
loom_js_test/.prettierrc
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"parser": "typescript",
|
||||
"printWidth": 99,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"jsxBracketSameLine": true
|
||||
}
|
59
loom_js_test/README.md
Normal file
59
loom_js_test/README.md
Normal file
@ -0,0 +1,59 @@
|
||||
# @rramos notes
|
||||
|
||||
This project in particular requires yarn, and also a specific version of loom for plasma.
|
||||
Again, I only made it able to run the first demo (demo.ts).
|
||||
|
||||
The process is as follows:
|
||||
|
||||
0. nodejs must have been installed with `nvm`
|
||||
1. Install yarn
|
||||
```
|
||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
|
||||
sudo apt-get update && sudo apt-get install --no-install-recommends yarn
|
||||
```
|
||||
2. Install plasma-loom
|
||||
```
|
||||
wget https://private.delegatecall.com/loom/linux/build-246/loom
|
||||
chmod +x loom
|
||||
```
|
||||
|
||||
3. Create a loom.yml in the same directory that contains the line: `PlasmaCashEnabled: true`. This file may need additional information if youre going to use multiple nodes
|
||||
|
||||
4. Execute `./loom init`. In this step you may configure the genesis.json files for multiple nodes.
|
||||
|
||||
5. In separate terminal, or as a background process, go to the `contracts` repo root folder, and execute `embark simulator`, and `embark run` to deploy the contracts
|
||||
|
||||
6. Execute `./loom run` to start the loom process. This might require more options if using multiple nodes. Can be launched as a background process too.
|
||||
|
||||
7. Build and launch the demo.
|
||||
```
|
||||
yarn install
|
||||
yarn build
|
||||
yarn copy-contracts
|
||||
yarn tape
|
||||
```
|
||||
|
||||
This will execute the demo. If you want to execute it more than once, You need to start the simulator from scratch, because this demo assumes a specific chain state in ganache.
|
||||
|
||||
|
||||
|
||||
# [Loom.js](https://loomx.io) Plasma Cash E2E Tests
|
||||
|
||||
NodeJS & browser tests for Loom Plama Cash implementation.
|
||||
|
||||
## Development
|
||||
|
||||
The e2e test environment can be configured by changing `.env.test` (see `.env.test.example` for
|
||||
default values).
|
||||
|
||||
```shell
|
||||
# build for NodeJS
|
||||
yarn build
|
||||
# build for Browser (TBD!)
|
||||
yarn build:browser
|
||||
# run e2e tests using NodeJS
|
||||
yarn test
|
||||
# auto-format source files
|
||||
yarn format
|
||||
```
|
57
loom_js_test/package.json
Normal file
57
loom_js_test/package.json
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "loom-js-plasma-cash-tests",
|
||||
"description": "NodeJS & browser tests for Loom Plama Cash implementation.",
|
||||
"author": {
|
||||
"name": "Loom Network",
|
||||
"url": "https://loomx.io"
|
||||
},
|
||||
"version": "0.1.0",
|
||||
"keywords": [
|
||||
"blockchain",
|
||||
"dappchain"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build:browser": "tsc && webpack",
|
||||
"format": "prettier --write \"src/**/*.ts\"",
|
||||
"test": "yarn copy-contracts && tsc && yarn tape",
|
||||
"copy-contracts": "node ./scripts/copy-contracts.js",
|
||||
"tape": "tape -r dotenv/config dotenv_config_path=./.env.test dist/index.js | tap-spec",
|
||||
"jenkins:tape": "tape -r dotenv/config dotenv_config_path=./.env.test.jenkins dist/index.js | tap-spec"
|
||||
},
|
||||
"dependencies": {
|
||||
"bn.js": "^4.11.8",
|
||||
"ethereumjs-util": "^5.2.0",
|
||||
"loom-js": "^1.12.0",
|
||||
"rlp": "^2.1.0",
|
||||
"web3": "^1.0.0-beta.34"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0-beta.46",
|
||||
"@babel/plugin-transform-runtime": "^7.0.0-beta.46",
|
||||
"@babel/preset-env": "^7.0.0-beta.46",
|
||||
"@babel/runtime": "^7.0.0-beta.46",
|
||||
"@types/bn.js": "^4.11.1",
|
||||
"@types/ethereumjs-util": "^5.2.0",
|
||||
"@types/node": "^10.0.3",
|
||||
"@types/tape": "^4.2.32",
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-loader": "^8.0.0-beta.2",
|
||||
"dotenv": "^5.0.1",
|
||||
"dotenv-webpack": "^1.5.5",
|
||||
"prettier": "1.12.1",
|
||||
"shelljs": "^0.8.2",
|
||||
"tap-spec": "^5.0.0",
|
||||
"tape": "4.9",
|
||||
"tslint": "^5.9.1",
|
||||
"tslint-config-prettier": "^1.12.0",
|
||||
"tslint-config-standard": "^7.0.0",
|
||||
"typescript": "^2.9.2",
|
||||
"webpack": "^4.6.0",
|
||||
"webpack-cli": "^2.1.2",
|
||||
"webpack-tape-run": "^0.0.7"
|
||||
},
|
||||
"browserslist": "last 2 versions"
|
||||
}
|
8
loom_js_test/scripts/copy-contracts.js
Normal file
8
loom_js_test/scripts/copy-contracts.js
Normal file
@ -0,0 +1,8 @@
|
||||
// This script copies Solidity contract ABI files to the dist directory
|
||||
|
||||
const shell = require('shelljs')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
|
||||
shell.mkdir('-p', './dist/contracts')
|
||||
shell.cp('./src/contracts/cards-abi.json', './dist/contracts/cards-abi.json')
|
15
loom_js_test/src/cards-contract.ts
Normal file
15
loom_js_test/src/cards-contract.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import BN from 'bn.js'
|
||||
|
||||
import { EthErc721Contract } from 'loom-js'
|
||||
import { DEFAULT_GAS } from './config'
|
||||
|
||||
export class EthCardsContract extends EthErc721Contract {
|
||||
registerAsync(address: string): Promise<object> {
|
||||
return this.contract.methods.register().send({ from: address, gas: DEFAULT_GAS })
|
||||
}
|
||||
|
||||
depositToPlasmaAsync(params: { tokenId: BN | number; from: string }): Promise<object> {
|
||||
const { tokenId, from } = params
|
||||
return this.contract.methods.depositToPlasma(tokenId).send({ from, gas: DEFAULT_GAS })
|
||||
}
|
||||
}
|
117
loom_js_test/src/challenge-after-demo.ts
Normal file
117
loom_js_test/src/challenge-after-demo.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import test from 'tape'
|
||||
import Web3 from 'web3'
|
||||
import BN from 'bn.js'
|
||||
import { IPlasmaDeposit, marshalDepositEvent } from 'loom-js'
|
||||
|
||||
import { increaseTime, getEthBalanceAtAddress } from './ganache-helpers'
|
||||
import { createTestEntity, ADDRESSES, ACCOUNTS } from './config'
|
||||
import { EthCardsContract } from './cards-contract'
|
||||
|
||||
// All the contracts are expected to have been deployed to Ganache when this function is called.
|
||||
function setupContracts(web3: Web3): { cards: EthCardsContract } {
|
||||
const abi = require('./contracts/cards-abi.json')
|
||||
const cards = new EthCardsContract(new web3.eth.Contract(abi, ADDRESSES.token_contract))
|
||||
return { cards }
|
||||
}
|
||||
|
||||
test('Plasma Cash Challenge After Demo', async t => {
|
||||
const web3 = new Web3('http://localhost:8545')
|
||||
const { cards } = setupContracts(web3)
|
||||
const authority = createTestEntity(web3, ACCOUNTS.authority)
|
||||
const mallory = createTestEntity(web3, ACCOUNTS.mallory)
|
||||
const dan = createTestEntity(web3, ACCOUNTS.dan)
|
||||
|
||||
// Give Mallory 5 tokens
|
||||
await cards.registerAsync(mallory.ethAddress)
|
||||
|
||||
const danTokensStart = await cards.balanceOfAsync(dan.ethAddress)
|
||||
t.equal(danTokensStart.toNumber(), 0, 'START: Dan has correct number of tokens')
|
||||
const malloryTokensStart = await cards.balanceOfAsync(mallory.ethAddress)
|
||||
t.equal(malloryTokensStart.toNumber(), 5, 'START: Mallory has correct number of tokens')
|
||||
|
||||
const startBlockNum = await web3.eth.getBlockNumber()
|
||||
|
||||
// Mallory deposits one of her coins to the plasma contract
|
||||
await cards.depositToPlasmaAsync({ tokenId: 6, from: mallory.ethAddress })
|
||||
await cards.depositToPlasmaAsync({ tokenId: 7, from: mallory.ethAddress })
|
||||
|
||||
const depositEvents: any[] = await authority.plasmaCashContract.getPastEvents('Deposit', {
|
||||
fromBlock: startBlockNum
|
||||
})
|
||||
const deposits = depositEvents.map<IPlasmaDeposit>(event =>
|
||||
marshalDepositEvent(event.returnValues)
|
||||
)
|
||||
t.equal(deposits.length, 2, 'Mallory has correct number of deposits')
|
||||
|
||||
const malloryTokensPostDeposit = await cards.balanceOfAsync(mallory.ethAddress)
|
||||
t.equal(
|
||||
malloryTokensPostDeposit.toNumber(),
|
||||
3,
|
||||
'POST-DEPOSIT: Mallory has correct number of tokens'
|
||||
)
|
||||
|
||||
// NOTE: In practice the Plasma Cash Oracle will submit the deposits to the DAppChain,
|
||||
// we're doing it here manually to simplify the test setup.
|
||||
for (let i = 0; i < deposits.length; i++) {
|
||||
await authority.submitPlasmaDepositAsync(deposits[i])
|
||||
}
|
||||
|
||||
const plasmaBlock1 = await authority.submitPlasmaBlockAsync()
|
||||
const plasmaBlock2 = await authority.submitPlasmaBlockAsync()
|
||||
|
||||
const deposit1Slot = deposits[0].slot
|
||||
|
||||
// Mallory -> Dan
|
||||
// Coin 6 was the first deposit of
|
||||
const coin = await mallory.getPlasmaCoinAsync(deposit1Slot)
|
||||
await mallory.transferTokenAsync({
|
||||
slot: deposit1Slot,
|
||||
prevBlockNum: coin.depositBlockNum,
|
||||
denomination: 1,
|
||||
newOwner: dan
|
||||
})
|
||||
|
||||
//incl_proofs, excl_proofs = mallory.get_coin_history(deposit1_utxo)
|
||||
//assert dan.verify_coin_history(deposit1_utxo, incl_proofs, excl_proofs)
|
||||
|
||||
const plasmaBlock3 = await authority.submitPlasmaBlockAsync()
|
||||
//dan.watch_exits(deposit1_utxo)
|
||||
|
||||
// Mallory attempts to exit spent coin (the one sent to Dan)
|
||||
await mallory.startExitAsync({
|
||||
slot: deposit1Slot,
|
||||
prevBlockNum: new BN(0),
|
||||
exitBlockNum: coin.depositBlockNum
|
||||
})
|
||||
|
||||
// Mallory's exit should be auto-challenged by Dan's client, but watching/auto-challenge hasn't
|
||||
// been implemented yet, so challenge the exit manually for now...
|
||||
await dan.challengeAfterAsync({ slot: deposit1Slot, challengingBlockNum: plasmaBlock3 })
|
||||
|
||||
// Having successufly challenged Mallory's exit Dan should be able to exit the coin
|
||||
await dan.startExitAsync({
|
||||
slot: deposit1Slot,
|
||||
prevBlockNum: coin.depositBlockNum,
|
||||
exitBlockNum: plasmaBlock3
|
||||
})
|
||||
//dan.stop_watching_exits(deposit1_utxo)
|
||||
|
||||
// Jump forward in time by 8 days
|
||||
await increaseTime(web3, 8 * 24 * 3600)
|
||||
|
||||
await authority.finalizeExitsAsync()
|
||||
|
||||
await dan.withdrawAsync(deposit1Slot)
|
||||
|
||||
const danBalanceBefore = await getEthBalanceAtAddress(web3, dan.ethAddress)
|
||||
await dan.withdrawBondsAsync()
|
||||
const danBalanceAfter = await getEthBalanceAtAddress(web3, dan.ethAddress)
|
||||
t.ok(danBalanceBefore.cmp(danBalanceAfter) < 0, 'END: Dan withdrew his bonds')
|
||||
|
||||
const malloryTokensEnd = await cards.balanceOfAsync(mallory.ethAddress)
|
||||
t.equal(malloryTokensEnd.toNumber(), 3, 'END: Mallory has correct number of tokens')
|
||||
const danTokensEnd = await cards.balanceOfAsync(dan.ethAddress)
|
||||
t.equal(danTokensEnd.toNumber(), 1, 'END: Dan has correct number of tokens')
|
||||
|
||||
t.end()
|
||||
})
|
118
loom_js_test/src/challenge-before-demo.ts
Normal file
118
loom_js_test/src/challenge-before-demo.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import test from 'tape'
|
||||
import BN from 'bn.js'
|
||||
import Web3 from 'web3'
|
||||
import { IPlasmaDeposit, marshalDepositEvent } from 'loom-js'
|
||||
|
||||
import { increaseTime, getEthBalanceAtAddress } from './ganache-helpers'
|
||||
import { createTestEntity, ADDRESSES, ACCOUNTS } from './config'
|
||||
import { EthCardsContract } from './cards-contract'
|
||||
|
||||
// Alice registers and has 5 coins, and she deposits 3 of them.
|
||||
const ALICE_INITIAL_COINS = 5
|
||||
const ALICE_DEPOSITED_COINS = 3
|
||||
const COINS = [1, 2, 3]
|
||||
|
||||
// All the contracts are expected to have been deployed to Ganache when this function is called.
|
||||
function setupContracts(web3: Web3): { cards: EthCardsContract } {
|
||||
const abi = require('./contracts/cards-abi.json')
|
||||
const cards = new EthCardsContract(new web3.eth.Contract(abi, ADDRESSES.token_contract))
|
||||
return { cards }
|
||||
}
|
||||
|
||||
test('Plasma Cash Challenge Before Demo', async t => {
|
||||
const web3 = new Web3('http://localhost:8545')
|
||||
const { cards } = setupContracts(web3)
|
||||
const authority = createTestEntity(web3, ACCOUNTS.authority)
|
||||
const dan = createTestEntity(web3, ACCOUNTS.dan)
|
||||
const trudy = createTestEntity(web3, ACCOUNTS.trudy)
|
||||
const mallory = createTestEntity(web3, ACCOUNTS.mallory)
|
||||
|
||||
// Give Dan 5 tokens
|
||||
await cards.registerAsync(dan.ethAddress)
|
||||
let balance = await cards.balanceOfAsync(dan.ethAddress)
|
||||
t.equal(balance.toNumber(), 6)
|
||||
|
||||
const startBlockNum = await web3.eth.getBlockNumber()
|
||||
|
||||
// Dan deposits a coin
|
||||
await cards.depositToPlasmaAsync({ tokenId: 16, from: dan.ethAddress })
|
||||
|
||||
const depositEvents: any[] = await authority.plasmaCashContract.getPastEvents('Deposit', {
|
||||
fromBlock: startBlockNum
|
||||
})
|
||||
const deposits = depositEvents.map<IPlasmaDeposit>(event =>
|
||||
marshalDepositEvent(event.returnValues)
|
||||
)
|
||||
t.equal(deposits.length, 1, 'All deposit events accounted for')
|
||||
|
||||
await authority.submitPlasmaDepositAsync(deposits[0])
|
||||
|
||||
const plasmaBlock1 = await authority.submitPlasmaBlockAsync()
|
||||
const plasmaBlock2 = await authority.submitPlasmaBlockAsync()
|
||||
const deposit1Slot = deposits[0].slot
|
||||
|
||||
// Trudy creates an invalid spend of the coin to Mallory
|
||||
const coin = await trudy.getPlasmaCoinAsync(deposit1Slot)
|
||||
await trudy.transferTokenAsync({
|
||||
slot: deposit1Slot,
|
||||
prevBlockNum: coin.depositBlockNum,
|
||||
denomination: 1,
|
||||
newOwner: mallory
|
||||
})
|
||||
|
||||
// Operator includes it
|
||||
const trudyToMalloryBlock = await authority.submitPlasmaBlockAsync()
|
||||
|
||||
// Mallory gives the coin back to Trudy.
|
||||
await mallory.transferTokenAsync({
|
||||
slot: deposit1Slot,
|
||||
prevBlockNum: trudyToMalloryBlock,
|
||||
denomination: 1,
|
||||
newOwner: trudy
|
||||
})
|
||||
|
||||
// Operator includes it
|
||||
const malloryToTrudyBlock = await authority.submitPlasmaBlockAsync()
|
||||
|
||||
// Having successufly challenged Mallory's exit Dan should be able to exit the coin
|
||||
await trudy.startExitAsync({
|
||||
slot: deposit1Slot,
|
||||
prevBlockNum: trudyToMalloryBlock,
|
||||
exitBlockNum: malloryToTrudyBlock
|
||||
})
|
||||
|
||||
// Dan challenges with his coin that hasn't moved
|
||||
await dan.challengeBeforeAsync({
|
||||
slot: deposit1Slot,
|
||||
prevBlockNum: new BN(0),
|
||||
challengingBlockNum: coin.depositBlockNum
|
||||
})
|
||||
|
||||
// 8 days pass without any response to the challenge
|
||||
await increaseTime(web3, 8 * 24 * 3600)
|
||||
await authority.finalizeExitsAsync()
|
||||
|
||||
// Having successfully challenged Trudy-Mallory's exit Dan should be able to exit the coin
|
||||
await dan.startExitAsync({
|
||||
slot: deposit1Slot,
|
||||
prevBlockNum: new BN(0),
|
||||
exitBlockNum: coin.depositBlockNum
|
||||
})
|
||||
|
||||
// Jump forward in time by 8 days
|
||||
await increaseTime(web3, 8 * 24 * 3600)
|
||||
|
||||
await authority.finalizeExitsAsync()
|
||||
|
||||
await dan.withdrawAsync(deposit1Slot)
|
||||
|
||||
const danBalanceBefore = await getEthBalanceAtAddress(web3, dan.ethAddress)
|
||||
await dan.withdrawBondsAsync()
|
||||
const danBalanceAfter = await getEthBalanceAtAddress(web3, dan.ethAddress)
|
||||
t.ok(danBalanceBefore.cmp(danBalanceAfter) < 0, 'END: Dan withdrew his bonds')
|
||||
|
||||
const danTokensEnd = await cards.balanceOfAsync(dan.ethAddress)
|
||||
t.equal(danTokensEnd.toNumber(), 6, 'END: Dan has correct number of tokens')
|
||||
|
||||
t.end()
|
||||
})
|
117
loom_js_test/src/challenge-between-demo.ts
Normal file
117
loom_js_test/src/challenge-between-demo.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import test from 'tape'
|
||||
import Web3 from 'web3'
|
||||
import { IPlasmaDeposit, marshalDepositEvent } from 'loom-js'
|
||||
|
||||
import { increaseTime, getEthBalanceAtAddress } from './ganache-helpers'
|
||||
import { ADDRESSES, ACCOUNTS, createTestEntity } from './config'
|
||||
import { EthCardsContract } from './cards-contract'
|
||||
|
||||
// All the contracts are expected to have been deployed to Ganache when this function is called.
|
||||
function setupContracts(web3: Web3): { cards: EthCardsContract } {
|
||||
const abi = require('./contracts/cards-abi.json')
|
||||
const cards = new EthCardsContract(new web3.eth.Contract(abi, ADDRESSES.token_contract))
|
||||
return { cards }
|
||||
}
|
||||
|
||||
test('Plasma Cash Challenge Between Demo', async t => {
|
||||
const web3 = new Web3('http://localhost:8545')
|
||||
const { cards } = setupContracts(web3)
|
||||
const authority = createTestEntity(web3, ACCOUNTS.authority)
|
||||
const alice = createTestEntity(web3, ACCOUNTS.alice)
|
||||
const bob = createTestEntity(web3, ACCOUNTS.bob)
|
||||
const eve = createTestEntity(web3, ACCOUNTS.eve)
|
||||
|
||||
const bobTokensStart = await cards.balanceOfAsync(bob.ethAddress)
|
||||
|
||||
// Give Eve 5 tokens
|
||||
await cards.registerAsync(eve.ethAddress)
|
||||
|
||||
const startBlockNum = await web3.eth.getBlockNumber()
|
||||
|
||||
// Eve deposits a coin
|
||||
await cards.depositToPlasmaAsync({ tokenId: 11, from: eve.ethAddress })
|
||||
const depositEvents: any[] = await authority.plasmaCashContract.getPastEvents('Deposit', {
|
||||
fromBlock: startBlockNum
|
||||
})
|
||||
const deposits = depositEvents.map<IPlasmaDeposit>(event =>
|
||||
marshalDepositEvent(event.returnValues)
|
||||
)
|
||||
t.equal(deposits.length, 1, 'Eve has correct number of deposits')
|
||||
|
||||
// NOTE: In practice the Plasma Cash Oracle will submit the deposits to the DAppChain,
|
||||
// we're doing it here manually to simplify the test setup.
|
||||
for (let i = 0; i < deposits.length; i++) {
|
||||
await authority.submitPlasmaDepositAsync(deposits[i])
|
||||
}
|
||||
|
||||
const deposit1Slot = deposits[0].slot
|
||||
|
||||
// wait to make sure that events get fired correctly
|
||||
//time.sleep(2)
|
||||
|
||||
// Eve sends her plasma coin to Bob
|
||||
const coin = await eve.getPlasmaCoinAsync(deposit1Slot)
|
||||
await eve.transferTokenAsync({
|
||||
slot: deposit1Slot,
|
||||
prevBlockNum: coin.depositBlockNum,
|
||||
denomination: 1,
|
||||
newOwner: bob
|
||||
})
|
||||
|
||||
const eveToBobBlockNum = await authority.submitPlasmaBlockAsync()
|
||||
// bob.watch_exits(deposit1_utxo)
|
||||
|
||||
// Eve sends this same plasma coin to Alice
|
||||
await eve.transferTokenAsync({
|
||||
slot: deposit1Slot,
|
||||
prevBlockNum: coin.depositBlockNum,
|
||||
denomination: 1,
|
||||
newOwner: alice
|
||||
})
|
||||
|
||||
const eveToAliceBlockNum = await authority.submitPlasmaBlockAsync()
|
||||
|
||||
// Alice attempts to exit here double-spent coin
|
||||
await alice.startExitAsync({
|
||||
slot: deposit1Slot,
|
||||
prevBlockNum: coin.depositBlockNum,
|
||||
exitBlockNum: eveToAliceBlockNum
|
||||
})
|
||||
|
||||
// Alice's exit should be auto-challenged by Bob's client, but watching/auto-challenge hasn't
|
||||
// been implemented yet, so challenge the exit manually for now...
|
||||
await bob.challengeBetweenAsync({ slot: deposit1Slot, challengingBlockNum: eveToBobBlockNum })
|
||||
|
||||
await bob.startExitAsync({
|
||||
slot: deposit1Slot,
|
||||
prevBlockNum: coin.depositBlockNum,
|
||||
exitBlockNum: eveToBobBlockNum
|
||||
})
|
||||
|
||||
// bob.stop_watching_exits(deposit1_utxo)
|
||||
|
||||
// Jump forward in time by 8 days
|
||||
await increaseTime(web3, 8 * 24 * 3600)
|
||||
|
||||
await authority.finalizeExitsAsync()
|
||||
|
||||
await bob.withdrawAsync(deposit1Slot)
|
||||
|
||||
const bobBalanceBefore = await getEthBalanceAtAddress(web3, bob.ethAddress)
|
||||
|
||||
await bob.withdrawBondsAsync()
|
||||
|
||||
const bobBalanceAfter = await getEthBalanceAtAddress(web3, bob.ethAddress)
|
||||
|
||||
t.ok(bobBalanceBefore.cmp(bobBalanceAfter) < 0, 'END: Bob withdrew his bonds')
|
||||
|
||||
const bobTokensEnd = await cards.balanceOfAsync(bob.ethAddress)
|
||||
|
||||
t.equal(
|
||||
bobTokensEnd.toNumber(),
|
||||
bobTokensStart.toNumber() + 1,
|
||||
'END: Bob has correct number of tokens'
|
||||
)
|
||||
|
||||
t.end()
|
||||
})
|
68
loom_js_test/src/config.ts
Normal file
68
loom_js_test/src/config.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import Web3 from 'web3'
|
||||
import {
|
||||
Entity,
|
||||
EthereumPlasmaClient,
|
||||
CryptoUtils,
|
||||
NonceTxMiddleware,
|
||||
SignedTxMiddleware,
|
||||
Address,
|
||||
LocalAddress,
|
||||
DAppChainPlasmaClient,
|
||||
Client,
|
||||
createJSONRPCClient
|
||||
} from 'loom-js'
|
||||
|
||||
export const DEFAULT_GAS = '3141592'
|
||||
export const CHILD_BLOCK_INTERVAL = 1000
|
||||
|
||||
// TODO: these should be pulled out of a config file generated by a Truffle migration
|
||||
export const ADDRESSES = {
|
||||
validator_manager: '0xd8a512EBD6fd82f44dFFD968EEB0835265497d20',
|
||||
root_chain: '0x494748735312D87C54Ff36E9dc71f90fb800D7Df',
|
||||
token_contract: '0x6427A6200Bed37FC1e512BdeA49D25Aa9089CF47'
|
||||
}
|
||||
|
||||
// TODO: these should be pulled out of a config file generated by a Truffle migration
|
||||
export const ACCOUNTS = {
|
||||
authority: '0xf942d5d524ec07158df4354402bfba8d928c99d0ab34d0799a6158d56156d986',
|
||||
alice: '0x88f37cfbaed8c0c515c62a17a3a1ce2f397d08bbf20dcc788b69f11b5a5c9791',
|
||||
bob: '0xf4ebc8adae40bfc741b0982c206061878bffed3ad1f34d67c94fa32c3d33eac8',
|
||||
charlie: '0xca67021a16478270ede4fddd65d0c031c75cd36c13b6a56bcb767928c1c2cf86',
|
||||
dan: '0x9955b1e01b2a7d8c22df41754d48b08dff3c0f3dd79d43e091c6311f97f0605a',
|
||||
mallory: '0x130137aa9a7fbc7cadc98c079cda47a999ff41931d9feaab621855beceed71f7',
|
||||
eve: '0xead83d04f741d2b3ab50be1299c18aa1a82c241606861a9a6d3122443496522d',
|
||||
trudy: '0xe6e893ac9f1c1db066a8a83a376554084b0a786e4cdcd91559d68bd4a1dac396'
|
||||
}
|
||||
|
||||
export function getTestUrls() {
|
||||
return {
|
||||
wsWriteUrl: process.env.TEST_LOOM_DAPP_WS_WRITE_URL || 'ws://127.0.0.1:46657/websocket',
|
||||
wsReadUrl: process.env.TEST_LOOM_DAPP_WS_READ_URL || 'ws://127.0.0.1:9999/queryws',
|
||||
httpWriteUrl: process.env.TEST_LOOM_DAPP_HTTP_WRITE_URL || 'http://127.0.0.1:46658/rpc',
|
||||
httpReadUrl: process.env.TEST_LOOM_DAPP_HTTP_READ_URL || 'http://127.0.0.1:46658/query'
|
||||
}
|
||||
}
|
||||
|
||||
export function createTestEntity(web3: Web3, ethPrivateKey: string): Entity {
|
||||
const ethAccount = web3.eth.accounts.privateKeyToAccount(ethPrivateKey)
|
||||
const ethPlasmaClient = new EthereumPlasmaClient(web3, ADDRESSES.root_chain)
|
||||
const writer = createJSONRPCClient({ protocols: [{ url: getTestUrls().httpWriteUrl }] })
|
||||
const reader = createJSONRPCClient({ protocols: [{ url: getTestUrls().httpReadUrl }] })
|
||||
const dAppClient = new Client('default', writer, reader)
|
||||
// TODO: move keys to config file
|
||||
const privKey = CryptoUtils.generatePrivateKey()
|
||||
const pubKey = CryptoUtils.publicKeyFromPrivateKey(privKey)
|
||||
dAppClient.txMiddleware = [
|
||||
new NonceTxMiddleware(pubKey, dAppClient),
|
||||
new SignedTxMiddleware(privKey)
|
||||
]
|
||||
const callerAddress = new Address('default', LocalAddress.fromPublicKey(pubKey))
|
||||
const dAppPlasmaClient = new DAppChainPlasmaClient({ dAppClient, callerAddress })
|
||||
return new Entity(web3, {
|
||||
ethAccount,
|
||||
ethPlasmaClient,
|
||||
dAppPlasmaClient,
|
||||
defaultGas: DEFAULT_GAS,
|
||||
childBlockInterval: CHILD_BLOCK_INTERVAL
|
||||
})
|
||||
}
|
428
loom_js_test/src/contracts/cards-abi.json
Normal file
428
loom_js_test/src/contracts/cards-abi.json
Normal file
@ -0,0 +1,428 @@
|
||||
[
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "name",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "getApproved",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "approve",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "totalSupply",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "transferFrom",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_index",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "tokenOfOwnerByIndex",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "safeTransferFrom",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "exists",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_index",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "tokenByIndex",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "ownerOf",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_owner",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "balanceOf",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "symbol",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_approved",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"name": "setApprovalForAll",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_tokenId",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "_data",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "safeTransferFrom",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "tokenURI",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_operator",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "isApprovedForAll",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_plasma",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "constructor"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "_from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "_to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "_tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Transfer",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "_owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "_approved",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "_tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Approval",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "_owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "_operator",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "_approved",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"name": "ApprovalForAll",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [],
|
||||
"name": "register",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "tokenId",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "_data",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "depositToPlasmaWithData",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "depositToPlasma",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
127
loom_js_test/src/demo.ts
Normal file
127
loom_js_test/src/demo.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import test from 'tape'
|
||||
import BN from 'bn.js'
|
||||
import Web3 from 'web3'
|
||||
import { IPlasmaDeposit, marshalDepositEvent } from 'loom-js'
|
||||
|
||||
import { increaseTime } from './ganache-helpers'
|
||||
import { createTestEntity, ADDRESSES, ACCOUNTS } from './config'
|
||||
import { EthCardsContract } from './cards-contract'
|
||||
|
||||
// Alice registers and has 5 coins, and she deposits 3 of them.
|
||||
const ALICE_INITIAL_COINS = 5
|
||||
const ALICE_DEPOSITED_COINS = 3
|
||||
const COINS = [1, 2, 3]
|
||||
|
||||
// All the contracts are expected to have been deployed to Ganache when this function is called.
|
||||
function setupContracts(web3: Web3): { cards: EthCardsContract } {
|
||||
const abi = require('./contracts/cards-abi.json')
|
||||
const cards = new EthCardsContract(new web3.eth.Contract(abi, ADDRESSES.token_contract))
|
||||
return { cards }
|
||||
}
|
||||
|
||||
test('Plasma Cash with ERC721 Demo', async t => {
|
||||
const web3 = new Web3('http://localhost:8545')
|
||||
const { cards } = setupContracts(web3)
|
||||
const authority = createTestEntity(web3, ACCOUNTS.authority)
|
||||
const alice = createTestEntity(web3, ACCOUNTS.alice)
|
||||
const bob = createTestEntity(web3, ACCOUNTS.bob)
|
||||
const charlie = createTestEntity(web3, ACCOUNTS.charlie)
|
||||
|
||||
await cards.registerAsync(alice.ethAddress)
|
||||
let balance = await cards.balanceOfAsync(alice.ethAddress)
|
||||
t.equal(balance.toNumber(), 5)
|
||||
|
||||
const startBlockNum = await web3.eth.getBlockNumber()
|
||||
|
||||
for (let i = 0; i < ALICE_DEPOSITED_COINS; i++) {
|
||||
await cards.depositToPlasmaAsync({ tokenId: COINS[i], from: alice.ethAddress })
|
||||
}
|
||||
|
||||
const depositEvents: any[] = await authority.plasmaCashContract.getPastEvents('Deposit', {
|
||||
fromBlock: startBlockNum
|
||||
})
|
||||
const deposits = depositEvents.map<IPlasmaDeposit>(event =>
|
||||
marshalDepositEvent(event.returnValues)
|
||||
)
|
||||
t.equal(deposits.length, ALICE_DEPOSITED_COINS, 'All deposit events accounted for')
|
||||
for (let i = 0; i < deposits.length; i++) {
|
||||
const deposit = deposits[i]
|
||||
t.equal(deposit.blockNumber.toNumber(), i + 1, `Deposit ${i + 1} block number is correct`)
|
||||
t.equal(deposit.denomination.toNumber(), 1, `Deposit ${i + 1} denomination is correct`)
|
||||
t.equal(deposit.from, alice.ethAddress, `Deposit ${i + 1} sender is correct`)
|
||||
}
|
||||
|
||||
balance = await cards.balanceOfAsync(alice.ethAddress)
|
||||
t.equal(
|
||||
balance.toNumber(),
|
||||
ALICE_INITIAL_COINS - ALICE_DEPOSITED_COINS,
|
||||
'alice should have 2 tokens in cards contract'
|
||||
)
|
||||
balance = await cards.balanceOfAsync(ADDRESSES.root_chain)
|
||||
t.equal(
|
||||
balance.toNumber(),
|
||||
ALICE_DEPOSITED_COINS,
|
||||
'plasma contract should have 3 tokens in cards contract'
|
||||
)
|
||||
|
||||
// Alice to Bob, and Alice to Charlie. We care about the Alice to Bob
|
||||
// transaction
|
||||
const deposit3 = deposits[2]
|
||||
const deposit2 = deposits[1]
|
||||
// Alice -> Bob
|
||||
await alice.transferTokenAsync({
|
||||
slot: deposit3.slot,
|
||||
prevBlockNum: deposit3.blockNumber,
|
||||
denomination: 1,
|
||||
newOwner: bob
|
||||
})
|
||||
// Alice -> Charlie
|
||||
await alice.transferTokenAsync({
|
||||
slot: deposit2.slot,
|
||||
prevBlockNum: deposit2.blockNumber,
|
||||
denomination: 1,
|
||||
newOwner: charlie
|
||||
})
|
||||
const plasmaBlockNum1 = await authority.submitPlasmaBlockAsync()
|
||||
// Add an empty block in between (for proof of exclusion)
|
||||
await authority.submitPlasmaBlockAsync()
|
||||
// Bob -> Charlie
|
||||
await bob.transferTokenAsync({
|
||||
slot: deposit3.slot,
|
||||
prevBlockNum: new BN(1000),
|
||||
denomination: 1,
|
||||
newOwner: charlie
|
||||
})
|
||||
|
||||
// TODO: get coin history of deposit3.slot from bob
|
||||
// TODO: charlie should verify coin history of deposit3.slot
|
||||
|
||||
const plasmaBlockNum2 = await authority.submitPlasmaBlockAsync()
|
||||
|
||||
// TODO: charlie should watch exits of deposit3.slot
|
||||
|
||||
await charlie.startExitAsync({
|
||||
slot: deposit3.slot,
|
||||
prevBlockNum: plasmaBlockNum1,
|
||||
exitBlockNum: plasmaBlockNum2
|
||||
})
|
||||
|
||||
// TODO: charlie should stop watching exits of deposit3.slot
|
||||
|
||||
// Jump forward in time by 8 days
|
||||
await increaseTime(web3, 8 * 24 * 3600)
|
||||
// Charlie's exit should be finalizable...
|
||||
await authority.finalizeExitsAsync()
|
||||
// Charlie should now be able to withdraw the UTXO (plasma token) which contains ERC721 token #2
|
||||
// into his wallet.
|
||||
await charlie.withdrawAsync(deposit3.slot)
|
||||
|
||||
balance = await cards.balanceOfAsync(alice.ethAddress)
|
||||
t.equal(balance.toNumber(), 2, 'alice should have 2 tokens in cards contract')
|
||||
balance = await cards.balanceOfAsync(bob.ethAddress)
|
||||
t.equal(balance.toNumber(), 0, 'bob should have no tokens in cards contract')
|
||||
balance = await cards.balanceOfAsync(charlie.ethAddress)
|
||||
t.equal(balance.toNumber(), 1, 'charlie should have 1 token in cards contract')
|
||||
|
||||
t.end()
|
||||
})
|
62
loom_js_test/src/ganache-helpers.ts
Normal file
62
loom_js_test/src/ganache-helpers.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import Web3 from 'web3'
|
||||
import BN from 'bn.js'
|
||||
|
||||
/**
|
||||
* @returns The time of the last mined block in seconds.
|
||||
*/
|
||||
export async function latestBlockTime(web3: Web3): Promise<number> {
|
||||
const block = await web3.eth.getBlock('latest')
|
||||
return block.timestamp
|
||||
}
|
||||
|
||||
function sendAsync<T>(web3: Web3, method: string, id: number, params?: any): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
web3.currentProvider.send(
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
method,
|
||||
params,
|
||||
id
|
||||
},
|
||||
(error, response) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(response.result)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export async function increaseTime(web3: Web3, duration: number): Promise<void> {
|
||||
const id = Date.now()
|
||||
const adj = await sendAsync<number>(web3, 'evm_increaseTime', id, [duration])
|
||||
return sendAsync<void>(web3, 'evm_mine', id + 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Beware that due to the need of calling two separate ganache methods and rpc calls overhead
|
||||
* it's hard to increase time precisely to a target point so design your test to tolerate
|
||||
* small fluctuations from time to time.
|
||||
*
|
||||
* @param target Time in seconds
|
||||
*/
|
||||
export async function increaseTimeTo(web3: Web3, target: number) {
|
||||
const now = await latestBlockTime(web3)
|
||||
if (target < now) {
|
||||
throw Error(`Cannot increase current time (${now}) to a moment in the past (${target})`)
|
||||
}
|
||||
let diff = target - now
|
||||
increaseTime(web3, diff)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the ETH balance of a particular Ethereum address.
|
||||
*
|
||||
* @param address Hex-encoded Ethereum address.
|
||||
*/
|
||||
export async function getEthBalanceAtAddress(web3: Web3, address: string): Promise<BN> {
|
||||
const balance = await web3.eth.getBalance(address)
|
||||
return new BN(balance)
|
||||
}
|
9
loom_js_test/src/index.ts
Normal file
9
loom_js_test/src/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
// NOTE: The order of the imports is important since that's the order the demos will run in,
|
||||
// each demo assumes a specific starting state left by the preceeding demos (at the moment
|
||||
// the leaky state consists of ERC721 token IDs).
|
||||
// TODO: Redeploy the Solidity contracts before each demo so the demos don't share any state.
|
||||
import './demo'
|
||||
// import './challenge-after-demo'
|
||||
// import './challenge-between-demo'
|
||||
// import './challenge-before-demo'
|
||||
// import './respond-challenge-before-demo'
|
102
loom_js_test/src/respond-challenge-before-demo.ts
Normal file
102
loom_js_test/src/respond-challenge-before-demo.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import test from 'tape'
|
||||
import BN from 'bn.js'
|
||||
import Web3 from 'web3'
|
||||
import { IPlasmaDeposit, marshalDepositEvent } from 'loom-js'
|
||||
|
||||
import { increaseTime, getEthBalanceAtAddress } from './ganache-helpers'
|
||||
import { createTestEntity, ADDRESSES, ACCOUNTS } from './config'
|
||||
import { EthCardsContract } from './cards-contract'
|
||||
|
||||
// Alice registers and has 5 coins, and she deposits 3 of them.
|
||||
const ALICE_INITIAL_COINS = 5
|
||||
const ALICE_DEPOSITED_COINS = 3
|
||||
const COINS = [1, 2, 3]
|
||||
|
||||
// All the contracts are expected to have been deployed to Ganache when this function is called.
|
||||
function setupContracts(web3: Web3): { cards: EthCardsContract } {
|
||||
const abi = require('./contracts/cards-abi.json')
|
||||
const cards = new EthCardsContract(new web3.eth.Contract(abi, ADDRESSES.token_contract))
|
||||
return { cards }
|
||||
}
|
||||
|
||||
test('Plasma Cash Respond Challenge Before Demo', async t => {
|
||||
const web3 = new Web3('http://localhost:8545')
|
||||
const { cards } = setupContracts(web3)
|
||||
const authority = createTestEntity(web3, ACCOUNTS.authority)
|
||||
const dan = createTestEntity(web3, ACCOUNTS.dan)
|
||||
const trudy = createTestEntity(web3, ACCOUNTS.trudy)
|
||||
|
||||
// Give Trudy 5 tokens
|
||||
await cards.registerAsync(trudy.ethAddress)
|
||||
let balance = await cards.balanceOfAsync(trudy.ethAddress)
|
||||
t.equal(balance.toNumber(), 5)
|
||||
|
||||
const startBlockNum = await web3.eth.getBlockNumber()
|
||||
// Trudy deposits a coin
|
||||
await cards.depositToPlasmaAsync({ tokenId: 21, from: trudy.ethAddress })
|
||||
|
||||
const depositEvents: any[] = await authority.plasmaCashContract.getPastEvents('Deposit', {
|
||||
fromBlock: startBlockNum
|
||||
})
|
||||
const deposits = depositEvents.map<IPlasmaDeposit>(event =>
|
||||
marshalDepositEvent(event.returnValues)
|
||||
)
|
||||
t.equal(deposits.length, 1, 'All deposit events accounted for')
|
||||
|
||||
await authority.submitPlasmaDepositAsync(deposits[0])
|
||||
|
||||
const plasmaBlock1 = await authority.submitPlasmaBlockAsync()
|
||||
const plasmaBlock2 = await authority.submitPlasmaBlockAsync()
|
||||
const deposit1Slot = deposits[0].slot
|
||||
|
||||
// Trudy sends her coin to Dan
|
||||
const coin = await trudy.getPlasmaCoinAsync(deposit1Slot)
|
||||
await trudy.transferTokenAsync({
|
||||
slot: deposit1Slot,
|
||||
prevBlockNum: coin.depositBlockNum,
|
||||
denomination: 1,
|
||||
newOwner: dan
|
||||
})
|
||||
|
||||
// Operator includes it
|
||||
const trudyToDanBlock = await authority.submitPlasmaBlockAsync()
|
||||
|
||||
// Dan exits the coin received by Trudy
|
||||
await dan.startExitAsync({
|
||||
slot: deposit1Slot,
|
||||
prevBlockNum: coin.depositBlockNum,
|
||||
exitBlockNum: trudyToDanBlock
|
||||
})
|
||||
|
||||
// Trudy tries to challengeBefore Dan's exit
|
||||
await trudy.challengeBeforeAsync({
|
||||
slot: deposit1Slot,
|
||||
prevBlockNum: new BN(0),
|
||||
challengingBlockNum: coin.depositBlockNum
|
||||
})
|
||||
|
||||
// Dan responds to the invalid challenge
|
||||
await dan.respondChallengeBeforeAsync({
|
||||
slot: deposit1Slot,
|
||||
challengingBlockNum: trudyToDanBlock
|
||||
})
|
||||
|
||||
// Jump forward in time by 8 days
|
||||
await increaseTime(web3, 8 * 24 * 3600)
|
||||
|
||||
await authority.finalizeExitsAsync()
|
||||
|
||||
await dan.withdrawAsync(deposit1Slot)
|
||||
|
||||
const danBalanceBefore = await getEthBalanceAtAddress(web3, dan.ethAddress)
|
||||
await dan.withdrawBondsAsync()
|
||||
const danBalanceAfter = await getEthBalanceAtAddress(web3, dan.ethAddress)
|
||||
t.ok(danBalanceBefore.cmp(danBalanceAfter) < 0, 'END: Dan withdrew his bonds')
|
||||
|
||||
const danTokensEnd = await cards.balanceOfAsync(dan.ethAddress)
|
||||
// Dan had initially 5 from when he registered and he received 2 coins
|
||||
// 1 in this demo and 1 in a previous one.
|
||||
t.equal(danTokensEnd.toNumber(), 7, 'END: Dan has correct number of tokens')
|
||||
|
||||
t.end()
|
||||
})
|
61
loom_js_test/tsconfig.json
Normal file
61
loom_js_test/tsconfig.json
Normal file
@ -0,0 +1,61 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
"target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
||||
"lib": ["es2017", "dom"], /* Specify library files to be included in the compilation. */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
"declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
"sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "./dist", /* Redirect output structure to the directory. */
|
||||
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "removeComments": true, /* Do not emit comments to output. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Module Resolution Options */
|
||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
},
|
||||
"include": [
|
||||
"src/index.ts"
|
||||
]
|
||||
}
|
6
loom_js_test/tslint.json
Normal file
6
loom_js_test/tslint.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": ["tslint-config-standard", "tslint-config-prettier"],
|
||||
"rules": {
|
||||
"member-ordering": false
|
||||
}
|
||||
}
|
47
loom_js_test/webpack.e2e.test.config.js
Normal file
47
loom_js_test/webpack.e2e.test.config.js
Normal file
@ -0,0 +1,47 @@
|
||||
// This config is used to run tests in the browser.
|
||||
|
||||
const path = require('path');
|
||||
const WebpackTapeRun = require('webpack-tape-run');
|
||||
const WebpackDotEnv = require('dotenv-webpack');
|
||||
|
||||
module.exports = {
|
||||
mode: 'production',
|
||||
entry: './dist/tests/e2e_tests.js',
|
||||
output: {
|
||||
path: path.resolve(__dirname, './dist'),
|
||||
filename: 'browser_e2e_tests.js',
|
||||
libraryTarget: 'umd',
|
||||
globalObject: 'this',
|
||||
// libraryExport: 'default',
|
||||
library: 'loom_e2e_tests'
|
||||
},
|
||||
node: {
|
||||
fs: 'empty',
|
||||
crypto: true,
|
||||
util: true,
|
||||
stream: true,
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js)$/,
|
||||
exclude: /(node_modules)/,
|
||||
use: 'babel-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new WebpackDotEnv({
|
||||
path: './.env.test',
|
||||
safe: './.env.test.example'
|
||||
}),
|
||||
// Be default tests will run in Electron, but can use other browsers too,
|
||||
// see https://github.com/syarul/webpack-tape-run for plugin settings.
|
||||
new WebpackTapeRun()
|
||||
],
|
||||
// silence irrelevant messages
|
||||
performance: {
|
||||
hints: false
|
||||
},
|
||||
stats: 'errors-only'
|
||||
};
|
7679
loom_js_test/yarn.lock
Normal file
7679
loom_js_test/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
3
plasma_cash/.vscode/settings.json
vendored
Normal file
3
plasma_cash/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"python.pythonPath": "${workspaceFolder}/erc721plasma/bin/python"
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user