Skip to content

Commit

Permalink
added "gsn registry" command-line tool
Browse files Browse the repository at this point in the history
list all ids:
`gsn registry --list`
latest version for id:
`gsn registry --id`
all versions for id:
`gsn registry --id <id> --history`

add version
`gsn registry --id <id> --ver <ver> --add <value> `

cancel version
`gsn registry --id <id> --ver <ver> --cancel `
  • Loading branch information
drortirosh committed Aug 20, 2020
1 parent 4e8df88 commit 505f921
Show file tree
Hide file tree
Showing 12 changed files with 226 additions and 18 deletions.
14 changes: 11 additions & 3 deletions dockers/relaydc/rdc
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
#!/bin/bash -e

# performs SSH into given machine and executes a relaydc command on it with args

#you can also add this as alias on the host itself:
RDC='docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -e ROOT=$PWD -v $PWD/.env:/.env opengsn/relaydc'

if [ -z $2 ]; then
cat <<EOF
rdc - remote relaydc (to google cloud)
usage: $0 {user@host} {docker-compose params}
To run on local machine:
$0 addalias - add the "rdc" alias to .bashrc
To run on local machine without ssh:
$0 local {docker-compose params}
EOF
exit 1
Expand All @@ -15,8 +20,11 @@ host=$1
shift
args="$@"


RDC='docker run --rm -ti -v /var/run/docker.sock:/var/run/docker.sock -e ROOT=$PWD -v $PWD/.env:/.env opengsn/relaydc'
if [ $args == 'addalias' ]; then
ssh $host "grep rdc= .bashrc" || \
ssh $host "echo alias rdc=\'$RDC\' >> .bashrc " && echo added alias on $host
exit 0
fi

if [ $host == 'local' ]; then
sh -c "$RDC $args"
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"chai": "4.2.0",
"commander": "5.1.0",
"console-read-write": "^0.1.1",
"date-format": "^3.0.0",
"eth-sig-util": "2.5.2",
"ethereumjs-common": "^1.5.0",
"ethereumjs-tx": "2.1.2",
Expand Down
118 changes: 118 additions & 0 deletions src/cli/commands/gsn-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import CommandsLogic from '../CommandsLogic'
import { configureGSN } from '../../relayclient/GSNConfigurator'
import DateFormatter from 'date-format'
import {
getMnemonic,
getNetworkUrl,
getRegistryAddress,
gsnCommander
} from '../utils'
import { VersionInfo, VersionRegistry } from '../../common/VersionRegistry'

function error (s: string): never {
console.error(s)
process.exit(1)
}

function parseTime (t: string): number {
const m = t.match(/^\s*([\d.]+)\s*([smhdw]?)/i)
if (m == null) error('invalid --delay parameter: must be number with sec/min/hour/day suffix')
const n = parseFloat(m[1])
switch (m[2].toLowerCase()) {
case 'm':
return n * 60
case 'h':
return n * 3600
case 'd':
return n * 3600 * 24
case 'w':
return n * 3600 * 24 * 7
default: // either 'sec' or nothing
return n
}
}

const commander = gsnCommander(['n', 'f', 'm', 'g'])
.option('--registry <address>', 'versionRegistry')
.option('-i, --id <string>', 'id to edit/change')
.option('--list', 'list all registered ids')
.option('-d, --delay <string>', 'view latest version that is at least that old (sec/min/hour/day)', '0')
.option('-h, --history', 'show all version history')
.option('-V, --ver <string>', 'new version to add/cancel')
.option('-d, --date', 'show date info of versions')
.option('-a, --add <string>', 'add this version value. if not set, show current value')
.option('-C, --cancel', 'cancel the given version')
.option('-r, --reason <string>', 'cancel reason')
.parse(process.argv)

function formatVersion (id: string, versionInfo: VersionInfo, showDate = false): string {
const dateInfo = showDate ? `[${DateFormatter('yyyy-MM-dd hh:mm', new Date(versionInfo.time * 1000))}] ` : ''
return `${id} @ ${versionInfo.version} = ${dateInfo} ${versionInfo.value} ${versionInfo.canceled ? `- CANCELED ${versionInfo.cancelReason}` : ''}`.trim()
}

(async () => {
const nodeURL = getNetworkUrl(commander.network)

const mnemonic = getMnemonic(commander.mnemonic)
const logic = new CommandsLogic(nodeURL, configureGSN({}), mnemonic)
const provider = (logic as any).web3.currentProvider
const versionRegistryAddress = getRegistryAddress(commander.registry) ?? error('must specify --registry')
console.log('Using registry at address: ', versionRegistryAddress)
const versionRegistry = new VersionRegistry(provider, versionRegistryAddress)
if (!await versionRegistry.isValid()) {
error(`Not a valid registry address: ${versionRegistryAddress}`)
}

if (commander.list != null) {
const ids = await versionRegistry.listIds()
console.log('All registered IDs:')
ids.forEach(id => console.log('-', id))
return
}

const id: string = commander.id ?? error('must specify --id')
const add = commander.add as (string | undefined)
const cancel = commander.cancel

const version: string | undefined = commander.ver
if (add == null && cancel == null) {
// view mode

if (version != null) {
error('cannot specify --ver without --add or --cancel')
}
const showDate = commander.date
if (commander.history != null) {
if (commander.delay !== '0') error('cannot specify --delay and --history')
console.log((await versionRegistry.getAllVersions(id)).map(v => formatVersion(id, v, showDate)))
} else {
const delayPeriod = parseTime(commander.delay)
console.log(formatVersion(id, await versionRegistry.getVersion(id, delayPeriod), showDate))
}
} else {
if ((add == null) === (cancel == null)) error('must specify --add or --cancel, but not both')
const from = commander.from ?? await logic.findWealthyAccount()
const sendOptions = {
gasPrice: commander.gasPrice,
from
}
if (version == null) {
error('--add/--cancel commands require both --id and --ver')
}
if (add != null) {
await versionRegistry.addVersion(id, version, add, sendOptions)
console.log(`== Added version ${id} @ ${version}`)
} else {
const reason = commander.reason ?? ''
await versionRegistry.cancelVersion(id, version, reason, sendOptions)
console.log(`== Canceled version ${id} @ ${version}`)
}
}
})()
.then(() => process.exit(0))
.catch(
reason => {
console.error(reason)
process.exit(1)
}
)
1 change: 1 addition & 0 deletions src/cli/commands/gsn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ commander
.command('paymaster-fund', 'fund a paymaster contract so it can pay for relayed calls')
.command('paymaster-balance', 'query a paymaster GSN balance')
.command('status', 'status of the GSN network')
.command('registry', 'VersionRegistry management')
.parse(process.argv)
4 changes: 4 additions & 0 deletions src/cli/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export function getRelayHubAddress (defaultAddress?: string): string | undefined
return getAddressFromFile('build/gsn/RelayHub.json', defaultAddress)
}

export function getRegistryAddress (defaultAddress?: string): string | undefined {
return getAddressFromFile('build/gsn/VersionRegistry.json', defaultAddress)
}

function getAddressFromFile (path: string, defaultAddress?: string): string | undefined {
if (defaultAddress == null) {
if (fs.existsSync(path)) {
Expand Down
56 changes: 52 additions & 4 deletions src/common/VersionRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,34 @@ export interface VersionInfo {
version: string
time: number
canceled: boolean
cancelReason: string
}

export class VersionRegistry {
registryContract: Contract
web3: Web3

constructor (web3provider: any, registryAddress: PrefixedHexString) {
constructor (web3provider: any, registryAddress: PrefixedHexString, readonly sendOptions = {}) {
this.web3 = new Web3(web3provider)
this.registryContract = new this.web3.eth.Contract(versionRegistryAbi as any, registryAddress)
}

async isValid (): Promise<boolean> {
// validate the contract exists, and has the registry API
if (await this.web3.eth.getCode(this.registryContract.options.address) === '0x') { return false }
// this check return 'true' only for owner
// return this.registryContract.methods.addVersion('0x414243', '0x313233', '0x313233').estimateGas(this.sendOptions)
// .then(() => true)
// .catch(() => false)
return true
}

/**
* return the latest "mature" version from the registry
*
* @dev: current time is last block's timestamp. This resolves any client time-zone discrepancies,
* but on local ganache, note that the time doesn't advance unless you mine transactions.
*
* @param id object id to return a version for
* @param delayPeriod - don't return entries younger than that (in seconds)
* @param optInVersion - if set, return this version even if it is young
Expand All @@ -46,7 +60,9 @@ export class VersionRegistry {
])
const ver = versions
.find(v => !v.canceled && (v.time + delayPeriod <= now || v.version === optInVersion))
if (ver == null) { throw new Error(`getVersion(${id}) - no version found`) }
if (ver == null) {
throw new Error(`getVersion(${id}) - no version found`)
}

return ver
}
Expand All @@ -57,13 +73,19 @@ export class VersionRegistry {
*/
async getAllVersions (id: string): Promise<VersionInfo[]> {
const events = await this.registryContract.getPastEvents('allEvents', { fromBlock: 1, topics: [null, string32(id)] })
const canceled = new Set<string>(events.filter(e => e.event === 'VersionCanceled').map(e => e.returnValues.version))
// map of ver=>reason, for every canceled version
const cancelReasons: { [key: string]: string } = events.filter(e => e.event === 'VersionCanceled').reduce((set, e) => ({
...set,
[e.returnValues.version]: e.returnValues.reason
}), {})

const found = new Set<string>()
return events
.filter(e => e.event === 'VersionAdded')
.map(e => ({
version: bytes32toString(e.returnValues.version),
canceled: canceled.has(e.returnValues.version),
canceled: cancelReasons[e.returnValues.version] != null,
cancelReason: cancelReasons[e.returnValues.version],
value: e.returnValues.value,
time: parseInt(e.returnValues.time)
}))
Expand All @@ -78,4 +100,30 @@ export class VersionRegistry {
})
.reverse()
}

// return all IDs registered
async listIds (): Promise<string[]> {
const events = await this.registryContract.getPastEvents('VersionAdded', { fromBlock: 1 })
const ids = new Set(events.map(e => bytes32toString(e.returnValues.id)))
return Array.from(ids)
}

async addVersion (id: string, version: string, value: string, sendOptions = {}): Promise<void> {
await this.checkVersion(id, version, false)
await this.registryContract.methods.addVersion(string32(id), string32(version), value)
.send({ ...this.sendOptions, ...sendOptions })
}

async cancelVersion (id: string, version: string, cancelReason = '', sendOptions = {}): Promise<void> {
await this.checkVersion(id, version, true)
await this.registryContract.methods.cancelVersion(string32(id), string32(version), cancelReason)
.send({ ...this.sendOptions, ...sendOptions })
}

private async checkVersion (id: string, version: string, validateExists: boolean): Promise<void> {
const versions = await this.getAllVersions(id).catch(() => [])
if ((versions.find(v => v.version === version) != null) !== validateExists) {
throw new Error(`version ${validateExists ? 'does not exist' : 'already exists'}: ${id} @ ${version}`)
}
}
}
2 changes: 1 addition & 1 deletion src/relayserver/ServerConfigParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export async function resolveServerConfig (config: Partial<ServerConfigParams>,

const versionRegistry = new VersionRegistry(web3provider, config.versionRegistryAddress)
const { version, value, time } = await versionRegistry.getVersion(relayHubId, config.versionRegistryDelayPeriod ?? DefaultRegistryDelayPeriod)
contractInteractor.validateAddress(value, `Invalid param relayHubId ${relayHubId}@${version}: not an address:`)
contractInteractor.validateAddress(value, `Invalid param relayHubId ${relayHubId} @ ${version}: not an address:`)

console.log(`Using RelayHub ID:${relayHubId} version:${version} address:${value} . created at: ${new Date(time * 1000).toString()}`)
config.relayHubAddress = value
Expand Down
33 changes: 26 additions & 7 deletions test/VersionRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,35 @@ const { expect, assert } = chai.use(chaiAsPromised)
require('source-map-support').install({ errorFormatterForce: true })
const VersionRegistryContract = artifacts.require('VersionRegistry')

context('VersionRegistry', () => {
contract('VersionRegistry', ([account]) => {
let now: number
let registryContract: VersionRegistryInstance
let jsRegistry: VersionRegistry

before('create registry', async () => {
registryContract = await VersionRegistryContract.new()
await registryContract.addVersion(string32('id'), string32('ver'), 'value')
await registryContract.addVersion(string32('another'), string32('ver'), 'anothervalue')
jsRegistry = new VersionRegistry(web3.currentProvider, registryContract.address)
jsRegistry = new VersionRegistry(web3.currentProvider, registryContract.address, { from: account })
await jsRegistry.addVersion('id', 'ver', 'value')
await jsRegistry.addVersion('another', 'ver', 'anothervalue')
})
context('param validations', () => {
context('contract param validations', () => {
it('should fail to add without id', async () => {
await expectRevert(registryContract.addVersion(string32(''), string32(''), 'value'), 'missing id')
})
it('should fail to add without version', async () => {
await expectRevert(registryContract.addVersion(string32('id'), string32(''), 'value'), 'missing version')
})
})
context('javascript param validations', () => {
it('should reject adding the same version again', async () => {
await expect(jsRegistry.addVersion('id', 'ver', 'changevalue'))
.to.eventually.be.rejectedWith('version already exists')
})
it('should rejecting canceling non-existent version', async () => {
await expect(jsRegistry.cancelVersion('nosuchid', 'ver', 'changevalue'))
.to.eventually.be.rejectedWith('version does not exist')
})
})

context('basic getAllVersions', () => {
it('should return nothing for unknown id', async () => {
Expand All @@ -45,9 +55,9 @@ context('VersionRegistry', () => {
context('with more versions', () => {
before(async () => {
await increaseTime(100)
await registryContract.addVersion(string32('id'), string32('ver2'), 'value2')
await jsRegistry.addVersion('id', 'ver2', 'value2')
await increaseTime(100)
await registryContract.addVersion(string32('id'), string32('ver3'), 'value3')
await jsRegistry.addVersion('id', 'ver3', 'value3')
await increaseTime(100)

// at this point:
Expand All @@ -72,6 +82,7 @@ context('VersionRegistry', () => {
})

it('should ignore repeated added version (can\'t modify history: only adding to it)', async () => {
// note that the javascript class reject such double-adding. we add directly through the contract API:
await registryContract.addVersion(string32('id'), string32('ver2'), 'new-value2')
const versions = await jsRegistry.getAllVersions('id')

Expand Down Expand Up @@ -139,6 +150,14 @@ context('VersionRegistry', () => {
const { version, value } = await jsRegistry.getVersion('id', 150)
assert.deepEqual({ version, value }, { version: 'ver', value: 'value' })
})
it('getAllVersions should return also canceled versions', async () => {
const versions = await jsRegistry.getAllVersions('id')

assert.equal(versions.length, 3)
assert.deepInclude(versions[0], { version: 'ver3', value: 'value3', canceled: false, cancelReason: undefined })
assert.deepInclude(versions[1], { version: 'ver2', value: 'value2', canceled: true, cancelReason: 'reason' })
assert.deepInclude(versions[2], { version: 'ver', value: 'value', canceled: false, cancelReason: undefined })
})
})
})
})
6 changes: 3 additions & 3 deletions truffle.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,19 @@ module.exports = {
},
mainnet: {
provider: function () {
return new HDWalletProvider(mnemonic, 'https://mainnet.infura.io/v3/c3422181d0594697a38defe7706a1e5b')
return new HDWalletProvider(mnemonic, 'https://mainnet.infura.io/v3/f40be2b1a3914db682491dc62a19ad43')
},
network_id: 1
},
kovan: {
provider: function () {
return new HDWalletProvider(mnemonic, 'https://kovan.infura.io/v3/c3422181d0594697a38defe7706a1e5b')
return new HDWalletProvider(mnemonic, 'https://kovan.infura.io/v3/f40be2b1a3914db682491dc62a19ad43')
},
network_id: 42
},
ropsten: {
provider: function () {
return new HDWalletProvider(mnemonic, 'https://ropsten.infura.io/v3/c3422181d0594697a38defe7706a1e5b')
return new HDWalletProvider(mnemonic, 'https://ropsten.infura.io/v3/f40be2b1a3914db682491dc62a19ad43')
},
network_id: 3
},
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"truffle-contracts",
"openzeppelin__test-helpers",
"ganache-core",
"date-format",
"chai-bn"
]
},
Expand Down
3 changes: 3 additions & 0 deletions types/date-format/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare module 'date-format' {
export default function asString(format: string, date: Date): string
}
Loading

0 comments on commit 505f921

Please sign in to comment.