Skip to content

add memoized function calls to add block tag data to every function c… #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,7 @@ const snxjs = synthetix({ network: 'mainnet' });
// If you want to interact with a contract, simply follow the convention:
// await snxjs[contractName].methodName(arguments)
// many arguments require being formatted toBytes32, which we also provide with the library
// Note can optionally pass in a { blockTag: someBlockNumber } to get data from a specific block instead of {}
E.g:
const unformattedSnxPrice = await snxjs.contracts.ExchangeRates.rateForCurrency(snxjs.toBytes32('SNX'), {});
const unformattedSnxPrice = await snxjs.contracts.ExchangeRates.rateForCurrency(snxjs.toBytes32('SNX'));
const unformattedTotalSupply = await snxjs.contracts.SynthsUSD.totalSupply({});

// We also expose ethers utils which provides handy methods for formatting responses to queries.
Expand All @@ -57,4 +55,25 @@ const totalSupply = formatEther(unformattedTotalSupply);

```

###### Historic block data

```
// NOTE for historic block data you have two options:
// both of these options require an archive node as a provider to work properly

// Option 1 (see either examples/node-example.js or examples/node-older-blocks-example.js):
const unformattedSnxPrice = await snxjs.contracts.ExchangeRates.rateForCurrency(snxjs.toBytes32('SNX'), { blockTag: 10000000 });
const unformattedTotalSupply = await snxjs.contracts.SynthsUSD.totalSupply({ blockTag: 10000000 });

// Option 2 (see examples/node-older-blocks-example.js):
// we added a helper to make life easier if you are pulling a lot of data from the same older block\
// so you don't have to add the blockTag every time.
// this will automatically pass the block 10290987 as the blockTag reference for every method called on olderContracts
const olderContracts = snxjs.contractsAtBlock(10290987);
const olderUnformattedSnxPrice = await olderContracts.ExchangeRates.rateForCurrency(
snxjs.toBytes32('SNX')
);

```

See the examples folder for more usage details
2 changes: 1 addition & 1 deletion build/index.js

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions build/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,12 @@ export declare type SynthetixJS = {
}) => {
method: {
name: string;
params: Array<any>;
params: Array<unknown>;
};
contract: string;
};
defaults: {
[key: string]: any;
[key: string]: unknown;
};
feeds: {
[symbol: string]: Feed;
Expand All @@ -102,11 +102,13 @@ export declare type SynthetixJS = {
toBytes32: (key: string) => string;
utils: typeof ethers.utils;
contracts: ContractsMap;
contractsAtBlock: (block: number) => ContractsMap;
};
export declare type SourceData = {
bytecode: string;
abi: ethers.ContractInterface;
};
export declare type FunctionUnknown = (...args: unknown[]) => unknown;
export declare type Target = {
name: string;
source: string;
Expand Down
19 changes: 5 additions & 14 deletions examples/browser-example.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,36 +18,27 @@
const { formatEther } = snxjs.utils;

const synths = snxjs.synths.map(({ name }) => name);
const fromBlock = 10260987;
const blockOptions = fromBlock ? { blockTag: Number(fromBlock) } : {};

let totalInUSD = 0;

const unformattedSnxPrice = await snxjs.contracts.ExchangeRates.rateForCurrency(
snxjs.toBytes32('SNX'),
blockOptions
); // note blockOptions must be passed to `ethers.Contract` as the final parameter (and fails if no archive node)
snxjs.toBytes32('SNX')
);
const snxPrice = formatEther(unformattedSnxPrice);
console.log('snxPrice', snxPrice);

let results = await Promise.all(
synths.map(async (synth) => {
const unformattedTotalSupply = await snxjs.contracts[`Synth${synth}`].totalSupply(
blockOptions
);
const unformattedTotalSupply = await snxjs.contracts[`Synth${synth}`].totalSupply({});
const totalSupply = formatEther(unformattedTotalSupply);

const rateForSynth = formatEther(
await snxjs.contracts.ExchangeRates.rateForCurrency(
snxjs.toBytes32(synth),
blockOptions
)
await snxjs.contracts.ExchangeRates.rateForCurrency(snxjs.toBytes32(synth))
);
const totalSupplyInUSD = rateForSynth * totalSupply;
totalInUSD += totalSupplyInUSD;
const rateIsFrozen = await snxjs.contracts.ExchangeRates.rateIsFrozen(
snxjs.toBytes32(synth),
blockOptions
snxjs.toBytes32(synth)
);

return {
Expand Down
14 changes: 12 additions & 2 deletions examples/node-example.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
/// <reference types="../src/missing-types" />
const { synthetix } = require('../src/index.ts');
const { ethers } = require('ethers');

(async () => {
// this instance exposes props for the given network: synths, sources, targets, users, as well as helper function toBytes32 - as per synthetix: https://github.com/Synthetixio/synthetix/blob/develop/index.js#L199.
const snxjs = synthetix({ network: 'mainnet' });
let snxjs;
if (process.env.INFURA_KEY) {
console.log(
'you need to run the npm command using an archive node project id as the environment variable in order to get historic data'
);
const provider = new ethers.providers.InfuraProvider('homestead', process.env.INFURA_KEY);
snxjs = synthetix({ network: 'mainnet', provider });
} else {
snxjs = synthetix({ network: 'mainnet' });
}

const { formatEther } = snxjs.utils;

const synths = snxjs.synths.map(({ name }) => name);
const fromBlock = 10260987;
const fromBlock = 10929392;
const blockOptions = fromBlock ? { blockTag: Number(fromBlock) } : {};

let totalInUSD = 0;
Expand Down
61 changes: 61 additions & 0 deletions examples/node-older-blocks-example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/// <reference types="../src/missing-types" />
const { synthetix } = require('../src/index.ts');
const { ethers } = require('ethers');

(async () => {
let snxjs;
if (process.env.INFURA_KEY) {
console.log(
'you need to run the npm command using an archive node project id as the environment variable in order to get historic data'
);
const provider = new ethers.providers.InfuraProvider('homestead', process.env.INFURA_KEY);
snxjs = synthetix({ network: 'mainnet', provider });
} else {
snxjs = synthetix({ network: 'mainnet' });
}
const { formatEther } = snxjs.utils;
const fromBlock = 10929392;

// method 1: using contractsAtBlock
// NOTE: instead of using block options like we did in browser-example.html and node-example.js,
// you can use the contractsAtBlock method to store the block in the ethers contract instance and
// then every call you make will reference the old block data
const olderContracts = snxjs.contractsAtBlock(fromBlock);
try {
const unformattedSnxPriceFromOlderContracts = await olderContracts.ExchangeRates.rateForCurrency(
snxjs.toBytes32('SNX')
);
const snxPriceFromOlderContracts = formatEther(unformattedSnxPriceFromOlderContracts);
console.log('snxPriceFromOlderContracts', snxPriceFromOlderContracts);
} catch (olderContractsErr) {
console.log('olderContractsErr', olderContractsErr);
}

// method 2: manually adding block tag like we did in browser-example.html and node-example.js
try {
const unformattedSnxPriceFromBlockTag = await snxjs.contracts.ExchangeRates.rateForCurrency(
snxjs.toBytes32('SNX'),
{ blockTag: fromBlock }
);
const snxPriceFromBlockTag = formatEther(unformattedSnxPriceFromBlockTag);
console.log('snxPriceFromBlockTag', snxPriceFromBlockTag);
} catch (blockTagErr) {
console.log('blockTagErr', blockTagErr);
}

try {
const unformattedSnxPrice = await snxjs.contracts.ExchangeRates.rateForCurrency(
snxjs.toBytes32('SNX')
); // note blockOptions must be passed to `ethers.Contract` as the final parameter (and fails if no archive node)
const snxPrice = formatEther(unformattedSnxPrice);
console.log('snxPrice', snxPrice);
} catch (latestBlockErr) {
console.log('latestBlockErr', latestBlockErr);
}

console.log(
'there will only be a different value with prices at older blocks if you use an archive node provider'
);
})().catch((e) => {
console.log('error', e);
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"build-browser": "webpack --mode=production --max-old-space-size=4096",
"build-report": "webpack-bundle-analyzer --port 4200 ./build/stats.json",
"examples:node": "ts-node --project tsconfig.node.json ./examples/node-example.js",
"examples:node-older-blocks": "ts-node --project tsconfig.node.json ./examples/node-older-blocks-example.js",
"examples:browser": "npm run build-browser && http-server -o ./examples/browser-example.html -c-1",
"lint": "eslint './src/**/*.{js,ts,tsx}' && tsc",
"lint:fix": "eslint --fix './src/**/*.{js,ts,tsx}'",
Expand Down
55 changes: 54 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@ import {
SynthetixJS,
Synth,
Token,
FunctionUnknown,
} from './types';
import { ERRORS } from './constants';

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
const synthetix = ({ networkId, network, signer, provider }: Config): SynthetixJS => {
const [currentNetwork, currentNetworkId] = selectNetwork(networkId, network);
const contracts = getSynthetixContracts(currentNetwork, signer, provider);
const contractsCopy = { ...contracts };
return {
network: {
id: currentNetworkId,
Expand All @@ -52,7 +55,8 @@ const synthetix = ({ networkId, network, signer, provider }: Config): SynthetixJ
suspensionReasons: getSuspensionReasons(),
toBytes32,
utils: ethers.utils,
contracts: getSynthetixContracts(currentNetwork, signer, provider),
contracts,
contractsAtBlock: (block: number) => memoizedBlockContracts(block, contractsCopy),
};
};

Expand Down Expand Up @@ -110,6 +114,55 @@ const getSynthetixContracts = (
}, {});
};

const memoizedBlockContracts = (block: number, contractsCopy: ContractsMap): ContractsMap => {
return Object.entries(contractsCopy).reduce(
(acc: ContractsMap, [contractName, contractInstance]: [string, ethers.Contract]) => {
acc[contractName] = memoizedEthersContract(block, contractInstance);
return acc;
},
{}
);
};

const memoizedEthersContract = (
block: number,
contractInstance: ethers.Contract
): ethers.Contract => {
const returnObj = Object.entries(contractInstance).reduce(
(acc: ethers.Contract, [contractKey, contractItem]: [string, unknown | FunctionUnknown]) => {
let newContractFunction = null;
if (typeof contractItem === 'function') {
newContractFunction = updateEthersMethodWithBlock(contractItem as FunctionUnknown, block);
}
if (contractKey != 'functions') {
// @ts-ignore
acc[contractKey] = newContractFunction || contractItem;
} else {
// @ts-ignore
acc[contractKey] = Object.entries(contractItem).reduce(
// @ts-ignore
(acc: Record<string, unknown>, [fnName, fn]: [string, FunctionUnknown]) => {
// @ts-ignore
acc[fnName] = updateEthersMethodWithBlock(fn, block);
return acc;
},
{}
);
}
return acc;
},
{} as ethers.Contract
);
return returnObj;
};

const updateEthersMethodWithBlock = (contractFunction: FunctionUnknown, block: number): unknown => {
return (...args: unknown[]) => {
const newArgs = args.concat({ blockTag: block });
return contractFunction(...newArgs);
};
};

export { synthetix, Network, NetworkId };
export type { Config, Target, TargetsRecord, ContractsMap, SynthetixJS, Synth, Token };
export default synthetix;
7 changes: 5 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ export type SynthetixJS = {
network: Network;
data: string;
target: Target;
}) => { method: { name: string; params: Array<any> }; contract: string };
defaults: { [key: string]: any };
}) => { method: { name: string; params: Array<unknown> }; contract: string };
defaults: { [key: string]: unknown };
feeds: { [symbol: string]: Feed };
tokens: Array<Token>;
network: {
Expand All @@ -92,13 +92,16 @@ export type SynthetixJS = {
toBytes32: (key: string) => string;
utils: typeof ethers.utils;
contracts: ContractsMap;
contractsAtBlock: (block: number) => ContractsMap;
};

export type SourceData = {
bytecode: string;
abi: ethers.ContractInterface;
};

export type FunctionUnknown = (...args: unknown[]) => unknown;

export type Target = {
name: string;
source: string;
Expand Down
15 changes: 14 additions & 1 deletion test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { ERRORS } from '../src/constants';

describe('@synthetixio/js tests', () => {
let snxjs;
const fromBlock = 10929392;

beforeAll(() => {
snxjs = synthetix({ network: Network.Kovan });
Expand Down Expand Up @@ -58,6 +59,19 @@ describe('@synthetixio/js tests', () => {
expect(invalidContract).toBe(undefined);
});

test('should return valid older contracts at a block', () => {
const olderContracts = snxjs.contractsAtBlock(fromBlock);
const validContract = olderContracts[validContractName];
expect(validContract).not.toBe(undefined);
expect(validContract.claimFees).not.toBe(undefined);
});

test('should not return invalid older contracts at a block', () => {
const olderContracts = snxjs.contractsAtBlock(fromBlock);
const invalidContract = olderContracts[badContractName];
expect(invalidContract).toBe(undefined);
});

test('should get the right sources data', () => {
const validSource = snxjs.sources[validContractName];
expect(validSource.bytecode).not.toBe(undefined);
Expand All @@ -82,7 +96,6 @@ describe('@synthetixio/js tests', () => {
test('should have a list of staking rewards', () => {
expect(snxjs.stakingRewards.length).toEqual(0);
const mainnetSnxjs = synthetix({ network: Network.Mainnet });
console.log(mainnetSnxjs.stakingRewards);
expect(mainnetSnxjs.stakingRewards[0].name).toBeTruthy();
});

Expand Down