From 09512c01fdcd86af58d843ec84392e020f134a1a Mon Sep 17 00:00:00 2001 From: Jared Flatow Date: Wed, 19 Jun 2019 17:34:03 -0700 Subject: [PATCH 1/2] Start solidifying reporter and improve saddle slightly --- sdk/javascript/README.md | 12 ++++- sdk/javascript/src/express_endpoint.ts | 12 +++-- sdk/javascript/src/reporter.ts | 70 +++++++++++++++++--------- sdk/javascript/tests/reporter_test.ts | 7 ++- tests/DelFiPriceTest.js | 5 +- tests/ViewTest.js | 13 ++--- tsrc/contract.ts | 4 +- 7 files changed, 77 insertions(+), 46 deletions(-) diff --git a/sdk/javascript/README.md b/sdk/javascript/README.md index 8914b977..6a103dfa 100644 --- a/sdk/javascript/README.md +++ b/sdk/javascript/README.md @@ -27,6 +27,14 @@ open-oracle-reporter \ --value_type decimal ``` +Or to quickly test using yarn: + +```bash +yarn run start \ + --private_key 0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10 \ + --script examples/fixed.js +``` + ## Usage Once you've installed the Open Oracle SDK, you can sign a Open Oracle feed as follows: @@ -34,8 +42,8 @@ Once you've installed the Open Oracle SDK, you can sign a Open Oracle feed as fo ```typescript import {encode, sign} from 'open-oracle-reporter'; -let encoded = Reporter.encode('string', 'decimal', +new Date(), {'eth': 260.0, 'zrx': 0.58}); -let signature = Reporter.sign(encoded, '0x...'); +let encoded = encode('string', 'decimal', +new Date(), {'eth': 260.0, 'zrx': 0.58}); +let signature = sign(encoded, '0x...'); ``` For example, in an express app: diff --git a/sdk/javascript/src/express_endpoint.ts b/sdk/javascript/src/express_endpoint.ts index 96afcab0..2b119e85 100644 --- a/sdk/javascript/src/express_endpoint.ts +++ b/sdk/javascript/src/express_endpoint.ts @@ -6,13 +6,15 @@ export function endpoint(path: string, privateKey: string, keyName: string, keyT const app: express.Application = express(); app.get(path, async (req, res) => { - let data = await getter(); - let encoded = encode(keyType, valueType, +new Date(), getter()) - let signature = sign(encoded, privateKey); + const pairs = await getter(); + const { + message, + signature + } = sign(encode(keyType, valueType, +new Date(), pairs), privateKey); res.json({ - encoded, + message, signature, - [keyName]: data + [keyName]: pairs }); }); diff --git a/sdk/javascript/src/reporter.ts b/sdk/javascript/src/reporter.ts index 66eb8c35..6d5c77fc 100644 --- a/sdk/javascript/src/reporter.ts +++ b/sdk/javascript/src/reporter.ts @@ -2,36 +2,57 @@ import Web3 from 'web3'; const web3 = new Web3(null); // This is just for encoding, etc. -interface TypeSignature { - encValueType: string, - encoder: (any) => any +interface SignedMessage { + hash: string, + message: string, + signature: string, + signatory: string }; -function annotateType(valueType: string): TypeSignature { - let encoder = (x) => x; - let encValueType = valueType; +// XXX share these w/ tests in umbrella somehow? - if (valueType === 'decimal') { - encoder = (x) => web3.utils.toBN('1000000000000000000').muln(x).toString(); - encValueType = 'uint256'; +export function decodeFancyParameter(paramType: string, encParam: string): any { + let actualParamType = paramType, actualParamDec = (x) => x; + + if (paramType == 'decimal') { + actualParamType = 'uint256'; + actualParamDec = (x) => x / 1e18; } - return { - encValueType, - encoder - }; + return actualParamDec(web3.eth.abi.decodeParameter(actualParamType, encParam)); } -export function encode(keyType: string, valueType: string, timestamp: number, pairs: [any, any][] | object): string { - let {encValueType, encoder} = annotateType(valueType); +export function decode(keyType: string, valueType: string, message: string): [number, [any, any][] | object] { + const {0: timestamp, 1: pairsEncoded} = web3.eth.abi.decodeParameters(['uint256', 'bytes[]'], message); + const pairs = pairsEncoded.map((enc) => { + const {0: key, 1: value} = web3.eth.abi.decodeParameters(['bytes', 'bytes'], enc); + return [ + decodeFancyParameter(keyType, key), + decodeFancyParameter(valueType, value) + ]; + }); + return [timestamp, pairs]; +} - let actualPairs = Array.isArray(pairs) ? pairs : Object.entries(pairs); - let pairsEncoded = actualPairs.map(([key, value]) => { - console.log([encValueType, encoder(value)]); +export function encodeFancyParameter(paramType: string, param: any): string { + let actualParamType = paramType, actualParam = param; + // We add a decimal type for reporter convenience. + // Decimals are encoded as uints with 18 decimals of precision on-chain. + if (paramType === 'decimal') { + actualParamType = 'uint256'; + actualParam = web3.utils.toBN('1000000000000000000').muln(param).toString(); + } + + return web3.eth.abi.encodeParameter(actualParamType, actualParam); +} + +export function encode(keyType: string, valueType: string, timestamp: number, pairs: [any, any][] | object): string { + const actualPairs = Array.isArray(pairs) ? pairs : Object.entries(pairs); + const pairsEncoded = actualPairs.map(([key, value]) => { return web3.eth.abi.encodeParameters(['bytes', 'bytes'], [ - web3.eth.abi.encodeParameter(keyType, key), - web3.eth.abi.encodeParameter(encValueType, encoder(value)) + encodeFancyParameter(keyType, key), + encodeFancyParameter(valueType, value) ]); }); return web3.eth.abi.encodeParameters(['uint256', 'bytes[]'], [ @@ -40,7 +61,10 @@ export function encode(keyType: string, valueType: string, timestamp: number, pa ]); } -export function sign(data: string, privateKey: string): string { - let {r, s, v, messageHash} = web3.eth.accounts.sign(web3.utils.keccak256(data), privateKey); - return web3.eth.abi.encodeParameters(['bytes32', 'bytes32', 'uint8'], [r, s, v]); +export function sign(message: string, privateKey: string): SignedMessage { + const hash = web3.utils.keccak256(message); + const {r, s, v} = web3.eth.accounts.sign(hash, privateKey); + const signature = web3.eth.abi.encodeParameters(['bytes32', 'bytes32', 'uint8'], [r, s, v]); + const signatory = web3.eth.accounts.recover(hash, v, r, s); + return {hash, message, signature, signatory}; } diff --git a/sdk/javascript/tests/reporter_test.ts b/sdk/javascript/tests/reporter_test.ts index f79752cc..74346447 100644 --- a/sdk/javascript/tests/reporter_test.ts +++ b/sdk/javascript/tests/reporter_test.ts @@ -1,9 +1,12 @@ -import {encode, sign} from '../src/reporter'; +import {decode, encode, sign} from '../src/reporter'; test('encode', async () => { let encoded = encode('string', 'decimal', 12345678, {"eth": 5.0, "zrx": 10.0}); + let decoded = decode('string', 'decimal', encoded); + let [timestamp, pairs] = decoded; - expect(encoded).toEqual('0x0000000000000000000000000000000000000000000000000000000000bc614e00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003657468000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000004563918244f400000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000037a7278000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000008ac7230489e80000'); + expect(timestamp).numEquals(12345678); // XXX saddle not in this module + expect(pairs).toEqual([['eth', 5.0], ['zrx', 10.0]]); }); test('sign', async () => { diff --git a/tests/DelFiPriceTest.js b/tests/DelFiPriceTest.js index 1a34dcae..669db2c0 100644 --- a/tests/DelFiPriceTest.js +++ b/tests/DelFiPriceTest.js @@ -1,7 +1,6 @@ describe('Oracle', () => { it('sanity checks the delfi price view', async () => { const { - account, address, bytes, uint256, @@ -25,7 +24,7 @@ describe('Oracle', () => { '0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf18', '0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf19' ].map(web3.eth.accounts.privateKeyToAccount); - const oracle = await deploy('Oracle', [], {from: account}); + const oracle = await deploy('Oracle', []); const delfi = await deploy('DelFiPrice', [oracle.address, sources.map(a => a.address)]); const now = new Date - 0; @@ -44,7 +43,7 @@ describe('Oracle', () => { messages.push(message); signatures.push(signature); }) - return delfi.methods.postPrices(messages, signatures, symbols).send({from: account, gas: 5000000}); + return delfi.methods.postPrices(messages, signatures, symbols).send({gas: 5000000}); } async function getPrice(symbol) { diff --git a/tests/ViewTest.js b/tests/ViewTest.js index b6afa00b..ac146e4c 100644 --- a/tests/ViewTest.js +++ b/tests/ViewTest.js @@ -1,14 +1,9 @@ describe('View', () => { - it('is a valid oracle', async () => { - let oracle = await saddle.deploy('View', [], {from: saddle.account}); + it('is a valid view', async () => { + const oracle = await saddle.deploy('Oracle', []); + const view = await saddle.deploy('View', [oracle.address, []]); - expect(await oracle.methods.name.call()).toEqual('55'); - }); - - it('is still a valid oracle', async () => { - let oracle = await saddle.deploy('View', [], {from: saddle.account}); - - expect(await oracle.methods.name.call()).toEqual('66'); + expect(await view.methods.oracle.call()).toEqual(oracle.address); }); }); diff --git a/tsrc/contract.ts b/tsrc/contract.ts index cd976b58..124ca895 100644 --- a/tsrc/contract.ts +++ b/tsrc/contract.ts @@ -40,8 +40,8 @@ export async function deployContract(web3: Web3, network: string, from: string, } const contractAbi = JSON.parse(contractBuild.abi); - const contract = new web3.eth.Contract(contractAbi); - return await contract.deploy({data: '0x' + contractBuild.bin, arguments: args}).send({from: from, gas: 2000000}); + const contract = new web3.eth.Contract(contractAbi, undefined, {from, gasPrice: '1', gas: 1e6, data: ''}); + return await contract.deploy({data: '0x' + contractBuild.bin, arguments: args}).send({from, gas: 2000000}); } export async function saveContract(name: string, contract: Contract, network: string): Promise { From 797721f60d0fb0269a832910931040133db4addb Mon Sep 17 00:00:00 2001 From: Jared Flatow Date: Thu, 20 Jun 2019 12:59:25 -0700 Subject: [PATCH 2/2] Attempt to clean up reporter API a bit --- sdk/javascript/examples/fixed.js | 4 +- sdk/javascript/src/express_endpoint.ts | 37 ++++++++------- sdk/javascript/src/index.ts | 45 +++++++++++-------- sdk/javascript/tests/express_endpoint_test.ts | 23 ++++------ sdk/javascript/tests/reporter_test.ts | 6 +-- tsrc/contract.ts | 2 +- 6 files changed, 61 insertions(+), 56 deletions(-) diff --git a/sdk/javascript/examples/fixed.js b/sdk/javascript/examples/fixed.js index 790eb6cb..bdb50e2d 100644 --- a/sdk/javascript/examples/fixed.js +++ b/sdk/javascript/examples/fixed.js @@ -1,4 +1,4 @@ -module.exports = function fetchPrices() { - return Promise.resolve({'eth': 260.0, 'zrx': 0.58}); +module.exports = async function fetchPrices() { + return [+new Date, {'eth': 260.0, 'zrx': 0.58}]; } diff --git a/sdk/javascript/src/express_endpoint.ts b/sdk/javascript/src/express_endpoint.ts index 2b119e85..2eddc89c 100644 --- a/sdk/javascript/src/express_endpoint.ts +++ b/sdk/javascript/src/express_endpoint.ts @@ -1,22 +1,25 @@ import express from 'express'; import {encode, sign} from './reporter'; -export function endpoint(path: string, privateKey: string, keyName: string, keyType: string, valueType: string, getter: () => object): express.Application { - // Create a new express application instance - const app: express.Application = express(); - - app.get(path, async (req, res) => { - const pairs = await getter(); - const { - message, - signature - } = sign(encode(keyType, valueType, +new Date(), pairs), privateKey); - res.json({ - message, - signature, - [keyName]: pairs +export function endpoint( + privateKey: string, + getter: () => Promise<[number, object]>, + name: string = 'prices', + path: string = `/${name}.json`, + keyType: string = 'string', + valueType: string = 'decimal' +): express.Application { + return express() + .get(path, async (req, res) => { + const [timestamp, pairs] = await getter(); + const { + message, + signature + } = sign(encode(keyType, valueType, timestamp, pairs), privateKey); + res.json({ + message, + signature, + [name]: pairs + }); }); - }); - - return app; } diff --git a/sdk/javascript/src/index.ts b/sdk/javascript/src/index.ts index e984c68b..7cce56ac 100644 --- a/sdk/javascript/src/index.ts +++ b/sdk/javascript/src/index.ts @@ -3,19 +3,20 @@ import {endpoint} from './express_endpoint'; import yargs from 'yargs'; import {loadKey} from './key'; import * as fs from 'fs'; -import * as path from 'path'; +import * as Path from 'path'; const argv = yargs - .option('port', {alias: 'p', description: 'Port to listen on', type: 'number', default: 3000}) - .option('private_key', {alias: 'k', description: 'Private key (try: `file: or env:`', type: 'string'}) - .option('script', {alias: 's', description: 'Script for data', type: 'string'}) - .option('path', {alias: 'u', description: 'Path for endpoint', type: 'string', default: '/'}) - .option('key_type', {description: 'Key type to encode', type: 'string', default: 'string'}) - .option('value_type', {description: 'Value type to encode', type: 'string', default: 'decimal'}) - .help() - .alias('help', 'h') - .demandOption(['private_key', 'script'], 'Please provide both run and path arguments to work with this tool') - .argv; + .option('port', {alias: 'p', description: 'Port to listen on', type: 'number', default: 3000}) + .option('private_key', {alias: 'k', description: 'Private key (try: `file: or env:`', type: 'string'}) + .option('script', {alias: 's', description: 'Script for data', type: 'string'}) + .option('name', {alias: 'n', description: 'Name for data feed', type: 'string', default: 'prices'}) + .option('path', {alias: 'u', description: 'Path for endpoint', type: 'string', default: '/prices.json'}) + .option('key_type', {description: 'Key type to encode', type: 'string', default: 'string'}) + .option('value_type', {description: 'Value type to encode', type: 'string', default: 'decimal'}) + .help() + .alias('help', 'h') + .demandOption(['private_key', 'script'], 'Please provide both run and path arguments to work with this tool') + .argv; // Create a new express application instance const app: express.Application = express(); @@ -25,17 +26,23 @@ function fetchEnv(name: string): string { if (res) { return res; } - throw `Cannot find env var \`${name}\``; + throw `Cannot find env var "${name}"`; } -async function start(port: number, privateKey: string, script: string, keyType: string, valueType: string) { - const fn: any = await import(path.join(process.cwd(), script)); - - app.use(endpoint(argv.path, privateKey, 'prices', keyType, valueType, fn.default)); - +async function start( + port: number, + privateKey: string, + script: string, + name: string, + path: string, + keyType: string, + valueType: string +) { + const fn: any = await import(Path.join(process.cwd(), script)); + app.use(endpoint(privateKey, fn.default, name, path, keyType, valueType)); app.listen(port, function () { - console.log(`Reporter listening on port ${port}. Try running \`curl http://localhost:${port}${argv.path}\``); + console.log(`Reporter listening on port ${port}. Try running "curl http://localhost:${port}${path}"`); }); } -start(argv.port, argv.private_key, argv.script, argv.key_type, argv.value_type); +start(argv.port, argv.private_key, argv.script, argv.name, argv.path, argv.key_type, argv.value_type); diff --git a/sdk/javascript/tests/express_endpoint_test.ts b/sdk/javascript/tests/express_endpoint_test.ts index 1aa235ec..1fa95298 100644 --- a/sdk/javascript/tests/express_endpoint_test.ts +++ b/sdk/javascript/tests/express_endpoint_test.ts @@ -1,30 +1,25 @@ -import express from 'express'; import fetch from 'node-fetch'; import {endpoint} from '../src/express_endpoint'; test('integration test', async () => { - let privateKey = '0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10'; + const privateKey = '0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10'; + const timestamp = +new Date(2019, 6, 20); - // Create a new express application instance - const app: express.Application = express(); - - async function fetchPrices() { - return {'eth': 260.0, 'zrx': 0.58}; + async function fetchPrices(): Promise<[number, object]> { + return [timestamp, {'eth': 260.0, 'zrx': 0.58}]; } - app.use(endpoint('/prices.json', privateKey, 'prices', 'string', 'decimal', fetchPrices)); - - app.listen(10123, function () {}); - - let response = await fetch(`http://localhost:${10123}/prices.json`); + const port = 10123; + const app = endpoint(privateKey, fetchPrices).listen(port); + const response = await fetch(`http://localhost:${port}/prices.json`); expect(response.ok).toBe(true); expect(await response.json()).toEqual({ - encoded: "0x0000000000000000000000000000000000000000000000000000016b3eabcf0e00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000", + message: "0x0000000000000000000000000000000000000000000000000000016c0e2e218000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000036574680000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000e18398e76019000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000037a727800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000080069f78df770a3", prices: { eth: 260, zrx: 0.58, }, - signature: "0x3c022277153248f28d96d6f0bbcde30789d7bef96e9f7ef2d0a93130bc4531dd2a0eff595fa3294556fbdb800ff81e359cb15e57df509ecd6a96eee30def6e12000000000000000000000000000000000000000000000000000000000000001b" + signature: "0xafb2aeb4bdf9d1fca04858d0db0f6023a94d1f6b6ce637641020044249f079002a680b038c6b0c6fe9d89bb6b7a4b1c74de9792db342d0223d6ba944f1d54361000000000000000000000000000000000000000000000000000000000000001c" }); }); diff --git a/sdk/javascript/tests/reporter_test.ts b/sdk/javascript/tests/reporter_test.ts index 74346447..bda05d80 100644 --- a/sdk/javascript/tests/reporter_test.ts +++ b/sdk/javascript/tests/reporter_test.ts @@ -5,12 +5,12 @@ test('encode', async () => { let decoded = decode('string', 'decimal', encoded); let [timestamp, pairs] = decoded; - expect(timestamp).numEquals(12345678); // XXX saddle not in this module + expect(timestamp.toString()).toEqual("12345678"); // XXX saddle not in this module: use numEquals expect(pairs).toEqual([['eth', 5.0], ['zrx', 10.0]]); }); test('sign', async () => { - let signed = sign('some data', '0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10'); + let {signature} = sign('some data', '0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10'); - expect(signed).toEqual('0x04a78a7b3013f6939da19eac6fd1ad5c5a20c41bcc5d828557442aad6f07598d029ae684620bec13e13d018cba0da5096626e83cfd4d5356d808d7437a0a5076000000000000000000000000000000000000000000000000000000000000001c'); + expect(signature).toEqual('0x04a78a7b3013f6939da19eac6fd1ad5c5a20c41bcc5d828557442aad6f07598d029ae684620bec13e13d018cba0da5096626e83cfd4d5356d808d7437a0a5076000000000000000000000000000000000000000000000000000000000000001c'); }); diff --git a/tsrc/contract.ts b/tsrc/contract.ts index 124ca895..5874eeca 100644 --- a/tsrc/contract.ts +++ b/tsrc/contract.ts @@ -40,7 +40,7 @@ export async function deployContract(web3: Web3, network: string, from: string, } const contractAbi = JSON.parse(contractBuild.abi); - const contract = new web3.eth.Contract(contractAbi, undefined, {from, gasPrice: '1', gas: 1e6, data: ''}); + const contract = new web3.eth.Contract(contractAbi, undefined, {from, gasPrice: '3000000000', gas: 1e6, data: ''}); return await contract.deploy({data: '0x' + contractBuild.bin, arguments: args}).send({from, gas: 2000000}); }