diff --git a/dockers/relaydc/rdc b/dockers/relaydc/rdc index c47302f5..710d9589 100755 --- a/dockers/relaydc/rdc +++ b/dockers/relaydc/rdc @@ -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 <> .bashrc " && echo added alias on $host + exit 0 +fi if [ $host == 'local' ]; then sh -c "$RDC $args" diff --git a/package.json b/package.json index 049dd4a8..23e71caa 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/cli/commands/gsn-registry.ts b/src/cli/commands/gsn-registry.ts new file mode 100755 index 00000000..3879fe31 --- /dev/null +++ b/src/cli/commands/gsn-registry.ts @@ -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
', 'versionRegistry') + .option('-i, --id ', 'id to edit/change') + .option('--list', 'list all registered ids') + .option('-d, --delay ', 'view latest version that is at least that old (sec/min/hour/day)', '0') + .option('-h, --history', 'show all version history') + .option('-V, --ver ', 'new version to add/cancel') + .option('-d, --date', 'show date info of versions') + .option('-a, --add ', 'add this version value. if not set, show current value') + .option('-C, --cancel', 'cancel the given version') + .option('-r, --reason ', '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) + } + ) diff --git a/src/cli/commands/gsn.ts b/src/cli/commands/gsn.ts index 9ad167f6..b8f3ee7e 100755 --- a/src/cli/commands/gsn.ts +++ b/src/cli/commands/gsn.ts @@ -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) diff --git a/src/cli/utils.ts b/src/cli/utils.ts index 9fb1995b..cca72fd4 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -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)) { diff --git a/src/common/VersionRegistry.ts b/src/common/VersionRegistry.ts index 604f968b..0ddfb55a 100644 --- a/src/common/VersionRegistry.ts +++ b/src/common/VersionRegistry.ts @@ -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 { + // 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 @@ -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 } @@ -57,13 +73,19 @@ export class VersionRegistry { */ async getAllVersions (id: string): Promise { const events = await this.registryContract.getPastEvents('allEvents', { fromBlock: 1, topics: [null, string32(id)] }) - const canceled = new Set(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() 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) })) @@ -78,4 +100,30 @@ export class VersionRegistry { }) .reverse() } + + // return all IDs registered + async listIds (): Promise { + 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 { + 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 { + 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 { + 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}`) + } + } } diff --git a/src/relayserver/ServerConfigParams.ts b/src/relayserver/ServerConfigParams.ts index 5b576eda..33d73782 100644 --- a/src/relayserver/ServerConfigParams.ts +++ b/src/relayserver/ServerConfigParams.ts @@ -164,7 +164,7 @@ export async function resolveServerConfig (config: Partial, 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 diff --git a/test/VersionRegistry.test.ts b/test/VersionRegistry.test.ts index 053214af..d0d5f7c8 100644 --- a/test/VersionRegistry.test.ts +++ b/test/VersionRegistry.test.ts @@ -12,18 +12,18 @@ 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') }) @@ -31,6 +31,16 @@ context('VersionRegistry', () => { 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 () => { @@ -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: @@ -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') @@ -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 }) + }) }) }) }) diff --git a/truffle.js b/truffle.js index 3224abf0..99e65057 100644 --- a/truffle.js +++ b/truffle.js @@ -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 }, diff --git a/tsconfig.json b/tsconfig.json index 47582952..55dcd4bc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,6 +30,7 @@ "truffle-contracts", "openzeppelin__test-helpers", "ganache-core", + "date-format", "chai-bn" ] }, diff --git a/types/date-format/index.d.ts b/types/date-format/index.d.ts new file mode 100644 index 00000000..6ec3c5be --- /dev/null +++ b/types/date-format/index.d.ts @@ -0,0 +1,3 @@ +declare module 'date-format' { + export default function asString(format: string, date: Date): string +} diff --git a/yarn.lock b/yarn.lock index 6baa5f08..afad6bff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3391,6 +3391,11 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +date-format@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-3.0.0.tgz#eb8780365c7d2b1511078fb491e6479780f3ad95" + integrity sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w== + debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"