Added js demo from loom

This commit is contained in:
Richard Ramos 2018-08-01 13:08:08 -04:00
parent cdbf32c454
commit d48050fa25
25 changed files with 9129 additions and 0 deletions

6
loom_js_test/.babelrc Normal file
View File

@ -0,0 +1,6 @@
{
"presets": ["@babel/preset-env"],
"plugins": [
["@babel/plugin-transform-runtime"]
]
}

View 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

View 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

View 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
View File

@ -0,0 +1,10 @@
# gitignore
node_modules
.DS_Store
# Only apps should have lockfiles
package-lock.json
/dist/*
/.env.test
/yarn-error.log

View File

@ -0,0 +1,2 @@
src/proto/*.d.ts
src/tests/tests_pb.d.ts

7
loom_js_test/.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"parser": "typescript",
"printWidth": 99,
"semi": false,
"singleQuote": true,
"jsxBracketSameLine": true
}

59
loom_js_test/README.md Normal file
View 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
View 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"
}

View 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')

View 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 })
}
}

View 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()
})

View 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()
})

View 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()
})

View 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
})
}

View 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
View 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()
})

View 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)
}

View 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'

View 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()
})

View 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
View File

@ -0,0 +1,6 @@
{
"extends": ["tslint-config-standard", "tslint-config-prettier"],
"rules": {
"member-ordering": false
}
}

View 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

File diff suppressed because it is too large Load Diff

3
plasma_cash/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"python.pythonPath": "${workspaceFolder}/erc721plasma/bin/python"
}