Skip to content

Commit 6f34bae

Browse files
committed
Sample application before adding GSN support.
This branch is the basic implementation, before adding any GSN support. - The "workshop-with-gsn" is the same app with GSN support - The "workshop-with-paymaster" is the same app with GSN and custom paymaster
1 parent 933dd87 commit 6f34bae

File tree

12 files changed

+2930
-0
lines changed

12 files changed

+2930
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/node_modules/
2+
/.idea/
3+
/.DS_Store
4+
/build/

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# GSN v3 integration workshop
2+
3+
### (Base branch - before adding any GSN support)
4+
5+
This sample dapp emits an event with the last account that clicked on the "capture the flag" button. We will integrate
6+
this dapp to work gaslessly with GSN v3. This will allow an externally owned account without ETH to capture the flag by
7+
signing a meta transaction.
8+
9+
10+
### To run the sample:
11+
12+
1. first clone and `yarn install`
13+
2. run `yarn ganache`
14+
3. Make sure you have Metamask installed, and pointing to "localhost"
15+
4. In a different window, run `yarn start`, to deploy the contract, and start the UI
16+
5. Start a browser pointing to "http://localhost:3000"
17+
6. Click the "Capture the Flag" button. Notice that you do need an account with eth for that..
18+
19+
You can see the integrations as GitHub pull requests:
20+
21+
1. [Basic: Minimum viable GSN integration](https://github.com/opengsn/workshop/pull/1/files)
22+
2. [Advanced: Write your own custom Paymaster](https://github.com/opengsn/workshop/pull/2/files_)
23+
24+
Note: on testnet we maintain a public service "pay for everything" paymaster so writing your own is not strictly
25+
required. On mainnet, you need a custom paymaster, such as a token paymaster that allow users to pay for gas in tokens,
26+
and exchanging them for ETH ETH on Uniswap. Dapps will want to develop their own custom paymaster in order, for example
27+
to subsidize gas fees for new users during the onboarding process.
28+
29+
### Further reading
30+
31+
GSNv3 integration tutorial: https://docs.opengsn.org/tutorials
32+
33+
Documentation explaining how everything works: https://docs.opengsn.org/

contracts/CaptureTheFlag.sol

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* SPDX-License-Identifier:MIT
3+
*/
4+
pragma solidity ^0.8.7;
5+
6+
contract CaptureTheFlag {
7+
8+
event FlagCaptured(address previousHolder, address currentHolder);
9+
10+
address public currentHolder = address(0);
11+
12+
function captureTheFlag() external {
13+
address previousHolder = currentHolder;
14+
15+
currentHolder = msg.sender;
16+
17+
emit FlagCaptured(previousHolder, currentHolder);
18+
}
19+
}

contracts/Migrations.sol

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* SPDX-License-Identifier:MIT
3+
*/
4+
pragma solidity >=0.4.25 <0.9.0;
5+
6+
contract Migrations {
7+
address public owner;
8+
uint public last_completed_migration;
9+
10+
modifier restricted() {
11+
if (msg.sender == owner) _;
12+
}
13+
14+
constructor() {
15+
owner = msg.sender;
16+
}
17+
18+
function setCompleted(uint completed) public restricted {
19+
last_completed_migration = completed;
20+
}
21+
}

migrations/1_initial_migration.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const Migrations = artifacts.require("Migrations");
2+
3+
module.exports = function (deployer) {
4+
deployer.deploy(Migrations);
5+
};

migrations/2_deploy_contracts.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const CaptureTheFlag = artifacts.require('CaptureTheFlag')
2+
3+
module.exports = async function (deployer) {
4+
await deployer.deploy(CaptureTheFlag)
5+
}

package.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "gsn-sample-workshop",
3+
"version": "3.0.0",
4+
"description": "Simple example of how to use GSNv3",
5+
"private": true,
6+
"dependencies": {
7+
"browserify": "^17.0.0",
8+
"ethers": "^5.6.8",
9+
"ganache-cli": "^6.12.2",
10+
"run-with-testrpc": "^0.3.1",
11+
"serve": "^13.0.0"
12+
},
13+
"scripts": {
14+
"ganache": "yarn run ganache-cli -d --chainId 1337",
15+
"gsn-with-ganache": "run-with-testrpc -d --chainId 1337 'gsn start'",
16+
"test": "truffle test",
17+
"compile": "truffle compile",
18+
"build": "./ui/build.sh",
19+
"start": "truffle deploy && yarn build && yarn serve ./build/html"
20+
},
21+
"author": "Dror Tirosh",
22+
"license": "GPL"
23+
}

test/testcontracts.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const CaptureTheFlag = artifacts.require('CaptureTheFlag')
2+
3+
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
4+
5+
contract("CaptureTheFlag", async accounts => {
6+
7+
let account
8+
let captureFlagContract
9+
10+
before(async () => {
11+
captureFlagContract = await CaptureTheFlag.new();
12+
13+
account = accounts[0]
14+
})
15+
16+
it('Runs without GSN', async () => {
17+
const res = await captureFlagContract.captureTheFlag({from: account});
18+
assert.equal(res.logs[0].event, "FlagCaptured", "Wrong event");
19+
assert.equal(res.logs[0].args.previousHolder, ZERO_ADDRESS, "Wrong previous flag holder");
20+
assert.equal(res.logs[0].args.currentHolder, account, "Wrong current flag holder");
21+
});
22+
23+
});

ui/build.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#! /bin/bash -e
2+
3+
rm -rf ./build/html/
4+
mkdir ./build/html/
5+
6+
browserify ./ui/index.js -o ./build/html/bundle.js
7+
cp ./ui/index.html ./build/html/
8+
9+
echo "Done building \"./build/html\" at `date`"
10+
11+

ui/index.html

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<html>
2+
<head>
3+
4+
<title>GSNv3 Test</title>
5+
6+
<script src="bundle.js">
7+
</script>
8+
9+
</head>
10+
<body>
11+
<h2>GSNv3 Test App</h2>
12+
13+
<script>
14+
window.app.initContract().then(function ({contractAddress, network}) {
15+
console.log('CaptureTheFlag contract', contractAddress)
16+
console.log(`identified network: ${JSON.stringify(network)}`)
17+
document.getElementById('capture_button').disabled = false
18+
})
19+
</script>
20+
21+
<button id="capture_button" disabled onClick="window.app.contractCall()">
22+
Capture the flag
23+
</button>
24+
25+
<hr>
26+
<div id="logview"></div>
27+
</body>
28+
</html>

ui/index.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
const ethers = require('ethers')
2+
3+
const contractArtifact = require('../build/contracts/CaptureTheFlag.json')
4+
const contractAbi = contractArtifact.abi
5+
6+
let theContract
7+
let provider
8+
9+
async function initContract() {
10+
11+
if (!window.ethereum) {
12+
throw new Error('provider not found')
13+
}
14+
window.ethereum.on('accountsChanged', () => {
15+
console.log('acct');
16+
window.location.reload()
17+
})
18+
window.ethereum.on('chainChanged', () => {
19+
console.log('chainChained');
20+
window.location.reload()
21+
})
22+
const networkId = await window.ethereum.request({method: 'net_version'})
23+
24+
provider = new ethers.providers.Web3Provider(window.ethereum)
25+
26+
const network = await provider.getNetwork()
27+
const artifactNetwork = contractArtifact.networks[networkId]
28+
if (!artifactNetwork)
29+
throw new Error('Can\'t find deployment on network ' + networkId)
30+
const contractAddress = artifactNetwork.address
31+
theContract = new ethers.Contract(
32+
contractAddress, contractAbi, provider.getSigner())
33+
34+
await listenToEvents()
35+
return {contractAddress, network}
36+
}
37+
38+
async function contractCall() {
39+
await window.ethereum.send('eth_requestAccounts')
40+
41+
const txOptions = {gasPrice: await provider.getGasPrice()}
42+
const transaction = await theContract.captureTheFlag(txOptions)
43+
const hash = transaction.hash
44+
console.log(`Transaction ${hash} sent`)
45+
const receipt = await transaction.wait()
46+
console.log(`Mined in block: ${receipt.blockNumber}`)
47+
}
48+
49+
let logview
50+
51+
function log(message) {
52+
message = message.replace(/(0x\w\w\w\w)\w*(\w\w\w\w)\b/g, '<b>$1...$2</b>')
53+
if (!logview) {
54+
logview = document.getElementById('logview')
55+
}
56+
logview.innerHTML = message + "<br>\n" + logview.innerHTML
57+
}
58+
59+
async function listenToEvents() {
60+
61+
theContract.on('FlagCaptured', (previousHolder, currentHolder, rawEvent) => {
62+
log(`Flag Captured from&nbsp;${previousHolder} by&nbsp;${currentHolder}`)
63+
console.log(`Flag Captured from ${previousHolder} by ${currentHolder}`)
64+
})
65+
}
66+
67+
window.app = {
68+
initContract,
69+
contractCall,
70+
log
71+
}
72+

0 commit comments

Comments
 (0)