Skip to content

Commit

Permalink
Sample application before adding GSN support.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
drortirosh committed Aug 1, 2022
1 parent 933dd87 commit 6f34bae
Show file tree
Hide file tree
Showing 12 changed files with 2,930 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/node_modules/
/.idea/
/.DS_Store
/build/
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# GSN v3 integration workshop

### (Base branch - before adding any GSN support)

This sample dapp emits an event with the last account that clicked on the "capture the flag" button. We will integrate
this dapp to work gaslessly with GSN v3. This will allow an externally owned account without ETH to capture the flag by
signing a meta transaction.


### To run the sample:

1. first clone and `yarn install`
2. run `yarn ganache`
3. Make sure you have Metamask installed, and pointing to "localhost"
4. In a different window, run `yarn start`, to deploy the contract, and start the UI
5. Start a browser pointing to "http://localhost:3000"
6. Click the "Capture the Flag" button. Notice that you do need an account with eth for that..

You can see the integrations as GitHub pull requests:

1. [Basic: Minimum viable GSN integration](https://github.com/opengsn/workshop/pull/1/files)
2. [Advanced: Write your own custom Paymaster](https://github.com/opengsn/workshop/pull/2/files_)

Note: on testnet we maintain a public service "pay for everything" paymaster so writing your own is not strictly
required. On mainnet, you need a custom paymaster, such as a token paymaster that allow users to pay for gas in tokens,
and exchanging them for ETH ETH on Uniswap. Dapps will want to develop their own custom paymaster in order, for example
to subsidize gas fees for new users during the onboarding process.

### Further reading

GSNv3 integration tutorial: https://docs.opengsn.org/tutorials

Documentation explaining how everything works: https://docs.opengsn.org/
19 changes: 19 additions & 0 deletions contracts/CaptureTheFlag.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* SPDX-License-Identifier:MIT
*/
pragma solidity ^0.8.7;

contract CaptureTheFlag {

event FlagCaptured(address previousHolder, address currentHolder);

address public currentHolder = address(0);

function captureTheFlag() external {
address previousHolder = currentHolder;

currentHolder = msg.sender;

emit FlagCaptured(previousHolder, currentHolder);
}
}
21 changes: 21 additions & 0 deletions contracts/Migrations.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* SPDX-License-Identifier:MIT
*/
pragma solidity >=0.4.25 <0.9.0;

contract Migrations {
address public owner;
uint public last_completed_migration;

modifier restricted() {
if (msg.sender == owner) _;
}

constructor() {
owner = msg.sender;
}

function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}
}
5 changes: 5 additions & 0 deletions migrations/1_initial_migration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const Migrations = artifacts.require("Migrations");

module.exports = function (deployer) {
deployer.deploy(Migrations);
};
5 changes: 5 additions & 0 deletions migrations/2_deploy_contracts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const CaptureTheFlag = artifacts.require('CaptureTheFlag')

module.exports = async function (deployer) {
await deployer.deploy(CaptureTheFlag)
}
23 changes: 23 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "gsn-sample-workshop",
"version": "3.0.0",
"description": "Simple example of how to use GSNv3",
"private": true,
"dependencies": {
"browserify": "^17.0.0",
"ethers": "^5.6.8",
"ganache-cli": "^6.12.2",
"run-with-testrpc": "^0.3.1",
"serve": "^13.0.0"
},
"scripts": {
"ganache": "yarn run ganache-cli -d --chainId 1337",
"gsn-with-ganache": "run-with-testrpc -d --chainId 1337 'gsn start'",
"test": "truffle test",
"compile": "truffle compile",
"build": "./ui/build.sh",
"start": "truffle deploy && yarn build && yarn serve ./build/html"
},
"author": "Dror Tirosh",
"license": "GPL"
}
23 changes: 23 additions & 0 deletions test/testcontracts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const CaptureTheFlag = artifacts.require('CaptureTheFlag')

const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'

contract("CaptureTheFlag", async accounts => {

let account
let captureFlagContract

before(async () => {
captureFlagContract = await CaptureTheFlag.new();

account = accounts[0]
})

it('Runs without GSN', async () => {
const res = await captureFlagContract.captureTheFlag({from: account});
assert.equal(res.logs[0].event, "FlagCaptured", "Wrong event");
assert.equal(res.logs[0].args.previousHolder, ZERO_ADDRESS, "Wrong previous flag holder");
assert.equal(res.logs[0].args.currentHolder, account, "Wrong current flag holder");
});

});
11 changes: 11 additions & 0 deletions ui/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#! /bin/bash -e

rm -rf ./build/html/
mkdir ./build/html/

browserify ./ui/index.js -o ./build/html/bundle.js
cp ./ui/index.html ./build/html/

echo "Done building \"./build/html\" at `date`"


28 changes: 28 additions & 0 deletions ui/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<html>
<head>

<title>GSNv3 Test</title>

<script src="bundle.js">
</script>

</head>
<body>
<h2>GSNv3 Test App</h2>

<script>
window.app.initContract().then(function ({contractAddress, network}) {
console.log('CaptureTheFlag contract', contractAddress)
console.log(`identified network: ${JSON.stringify(network)}`)
document.getElementById('capture_button').disabled = false
})
</script>

<button id="capture_button" disabled onClick="window.app.contractCall()">
Capture the flag
</button>

<hr>
<div id="logview"></div>
</body>
</html>
72 changes: 72 additions & 0 deletions ui/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
const ethers = require('ethers')

const contractArtifact = require('../build/contracts/CaptureTheFlag.json')
const contractAbi = contractArtifact.abi

let theContract
let provider

async function initContract() {

if (!window.ethereum) {
throw new Error('provider not found')
}
window.ethereum.on('accountsChanged', () => {
console.log('acct');
window.location.reload()
})
window.ethereum.on('chainChanged', () => {
console.log('chainChained');
window.location.reload()
})
const networkId = await window.ethereum.request({method: 'net_version'})

provider = new ethers.providers.Web3Provider(window.ethereum)

const network = await provider.getNetwork()
const artifactNetwork = contractArtifact.networks[networkId]
if (!artifactNetwork)
throw new Error('Can\'t find deployment on network ' + networkId)
const contractAddress = artifactNetwork.address
theContract = new ethers.Contract(
contractAddress, contractAbi, provider.getSigner())

await listenToEvents()
return {contractAddress, network}
}

async function contractCall() {
await window.ethereum.send('eth_requestAccounts')

const txOptions = {gasPrice: await provider.getGasPrice()}
const transaction = await theContract.captureTheFlag(txOptions)
const hash = transaction.hash
console.log(`Transaction ${hash} sent`)
const receipt = await transaction.wait()
console.log(`Mined in block: ${receipt.blockNumber}`)
}

let logview

function log(message) {
message = message.replace(/(0x\w\w\w\w)\w*(\w\w\w\w)\b/g, '<b>$1...$2</b>')
if (!logview) {
logview = document.getElementById('logview')
}
logview.innerHTML = message + "<br>\n" + logview.innerHTML
}

async function listenToEvents() {

theContract.on('FlagCaptured', (previousHolder, currentHolder, rawEvent) => {
log(`Flag Captured from&nbsp;${previousHolder} by&nbsp;${currentHolder}`)
console.log(`Flag Captured from ${previousHolder} by ${currentHolder}`)
})
}

window.app = {
initContract,
contractCall,
log
}

Loading

0 comments on commit 6f34bae

Please sign in to comment.