Skip to content

Commit

Permalink
Merge pull request #7 from compound-finance/jflatow/more-reporter
Browse files Browse the repository at this point in the history
Start solidifying reporter and improve saddle slightly
  • Loading branch information
jflatow authored Jun 20, 2019
2 parents 1548463 + 797721f commit 5a5ca41
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 93 deletions.
12 changes: 10 additions & 2 deletions sdk/javascript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,23 @@ 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:

```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:
Expand Down
4 changes: 2 additions & 2 deletions sdk/javascript/examples/fixed.js
Original file line number Diff line number Diff line change
@@ -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}];
}
35 changes: 20 additions & 15 deletions sdk/javascript/src/express_endpoint.ts
Original file line number Diff line number Diff line change
@@ -1,20 +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) => {
let data = await getter();
let encoded = encode(keyType, valueType, +new Date(), getter())
let signature = sign(encoded, privateKey);
res.json({
encoded,
signature,
[keyName]: data
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;
}
45 changes: 26 additions & 19 deletions sdk/javascript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:<file> or env:<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:<file> or env:<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();
Expand All @@ -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);
70 changes: 47 additions & 23 deletions sdk/javascript/src/reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]'], [
Expand All @@ -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};
}
23 changes: 9 additions & 14 deletions sdk/javascript/tests/express_endpoint_test.ts
Original file line number Diff line number Diff line change
@@ -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"
});
});
11 changes: 7 additions & 4 deletions sdk/javascript/tests/reporter_test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
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.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');
});
5 changes: 2 additions & 3 deletions tests/DelFiPriceTest.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
describe('Oracle', () => {
it('sanity checks the delfi price view', async () => {
const {
account,
address,
bytes,
uint256,
Expand All @@ -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;

Expand All @@ -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) {
Expand Down
13 changes: 4 additions & 9 deletions tests/ViewTest.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
4 changes: 2 additions & 2 deletions tsrc/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '3000000000', 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<void> {
Expand Down

0 comments on commit 5a5ca41

Please sign in to comment.