feat: InteractionsLoader implementation for redstone-sw-gateway #59

This commit is contained in:
asiaziola
2021-12-14 18:09:57 +01:00
committed by Piotr Pędziwiatr
parent 3dd1eb67be
commit 15bd729db3
19 changed files with 1647 additions and 867 deletions

View File

@@ -56,6 +56,7 @@
"arweave-multihost": "^0.1.0", "arweave-multihost": "^0.1.0",
"axios": "^0.21.4", "axios": "^0.21.4",
"bignumber.js": "^9.0.1", "bignumber.js": "^9.0.1",
"isomorphic-fetch": "^3.0.0",
"json-beautify": "^1.1.1", "json-beautify": "^1.1.1",
"knex": "^0.95.14", "knex": "^0.95.14",
"lodash": "^4.17.21", "lodash": "^4.17.21",
@@ -69,6 +70,8 @@
"@typescript-eslint/parser": "^4.29.2", "@typescript-eslint/parser": "^4.29.2",
"arlocal": "1.1.13", "arlocal": "1.1.13",
"cheerio": "^1.0.0-rc.10", "cheerio": "^1.0.0-rc.10",
"cli-table": "^0.3.11",
"colors": "^1.4.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",

View File

@@ -0,0 +1,104 @@
import fs from 'fs';
import path from 'path';
import Arweave from 'arweave';
import {
LoggerFactory,
RedstoneGatewayInteractionsLoader,
ArweaveGatewayInteractionsLoader,
DefaultEvaluationOptions
} from '@smartweave';
import { GQLEdgeInterface } from '../../legacy/gqlResult';
/*
TODO: two test cases have been removed from the list - gateway-interaction test is failing due to the different
amount of interactions returned from Redstone gateway and Arweave GQL gateway
should be fixed in https://github.com/redstone-finance/redstone-sw-gateway/issues/17, following cases should be
then added to ../test-cases/gateway-interactions.json
"eWB7FHyPyCYnkcbK1aINbAQ9YYTDhKGkS7lDiNPZ5Mg",
"cpXtKvM0e6cqAgjv-BCfanWQmYGupECt1MxRk1N9Mjk"
*/
const arweave = Arweave.init({
host: 'arweave.net',
port: 443,
protocol: 'https',
timeout: 600000,
logging: false
});
const gatewayUrl = 'https://gateway.redstone.finance';
LoggerFactory.INST.logLevel('fatal');
const testCases: string[] = JSON.parse(
fs.readFileSync(path.join(__dirname, 'test-cases/gateway-interactions.json'), 'utf-8')
);
/**
* These regression tests should verify whether ArweaveGatewayInteractionsLoader and RedstoneGatewayInteractionsLoader
* return same results for given variables
*/
describe.each(testCases)('testing for contract %#', (contractTxId) => {
it('returns same amount of interactions for RedstoneGatewayInteractionsLoader and ArweaveGatewayInteractionsLoader', async () => {
const redstoneInteractionsLoader = new RedstoneGatewayInteractionsLoader(gatewayUrl);
const arweaveInteractionsLoader = new ArweaveGatewayInteractionsLoader(arweave);
const responseRedstoneInteractionsLoader = await redstoneInteractionsLoader.load(contractTxId, 0, 8301901);
const responseArweaveInteractionsLoader = await arweaveInteractionsLoader.load(
contractTxId,
0,
8301901,
new DefaultEvaluationOptions()
);
expect(responseRedstoneInteractionsLoader.length).toEqual(responseArweaveInteractionsLoader.length);
}, 600000);
});
describe.each([750000, 775000, 800000, 825000, 850000])('testing for block height %#', (toBlockHeight) => {
it('returns same amount of interactions for the same block height', async () => {
const redstoneInteractionsLoader = new RedstoneGatewayInteractionsLoader(gatewayUrl);
const arweaveInteractionsLoader = new ArweaveGatewayInteractionsLoader(arweave);
const responseRedstoneInteractionsLoader: GQLEdgeInterface[] = await redstoneInteractionsLoader.load(
'Daj-MNSnH55TDfxqC7v4eq0lKzVIwh98srUaWqyuZtY',
0,
toBlockHeight
);
const responseArweaveInteractionsLoader: GQLEdgeInterface[] = await arweaveInteractionsLoader.load(
'Daj-MNSnH55TDfxqC7v4eq0lKzVIwh98srUaWqyuZtY',
0,
toBlockHeight,
new DefaultEvaluationOptions()
);
expect(responseRedstoneInteractionsLoader.length).toEqual(responseArweaveInteractionsLoader.length);
}, 600000);
});
describe.each(testCases)('testing contractId %#', (contractTxId) => {
it('returns same interactions ids for RedstoneGatewayLoader and ArweaveGatewayInteractionsLoader', async () => {
const redstoneInteractionsLoader = new RedstoneGatewayInteractionsLoader(gatewayUrl);
const arweaveInteractionsLoader = new ArweaveGatewayInteractionsLoader(arweave);
const responseRedstoneInteractionsLoader: GQLEdgeInterface[] = await redstoneInteractionsLoader.load(
contractTxId,
0,
8301901
);
const responseArweaveInteractionsLoader: GQLEdgeInterface[] = await arweaveInteractionsLoader.load(
contractTxId,
0,
8301901,
new DefaultEvaluationOptions()
);
let arr = [];
responseRedstoneInteractionsLoader.forEach((resRedstone) => {
arr.push(
responseArweaveInteractionsLoader.find((resArweave) => resArweave.node.id === resRedstone.node.id) !== undefined
);
});
const result = arr.every((a) => a === true);
expect(result).toEqual(true);
}, 600000);
});

View File

@@ -4,7 +4,6 @@ import path from 'path';
import { interactRead, readContract } from 'smartweave'; import { interactRead, readContract } from 'smartweave';
import Arweave from 'arweave'; import Arweave from 'arweave';
import { LoggerFactory, SmartWeaveNodeFactory } from '@smartweave'; import { LoggerFactory, SmartWeaveNodeFactory } from '@smartweave';
import { TsLogFactory } from '../../logging/node/TsLogFactory';
function* chunks(arr, n) { function* chunks(arr, n) {
for (let i = 0; i < arr.length; i += n) { for (let i = 0; i < arr.length; i += n) {
@@ -23,7 +22,7 @@ const arweave = Arweave.init({
LoggerFactory.INST.logLevel('fatal'); LoggerFactory.INST.logLevel('fatal');
const testCases: string[] = JSON.parse(fs.readFileSync(path.join(__dirname, 'test-cases.json'), 'utf-8')); const testCases: string[] = JSON.parse(fs.readFileSync(path.join(__dirname, 'test-cases/read-state.json'), 'utf-8'));
const chunked: string[][][] = [...chunks(testCases, 10)]; const chunked: string[][][] = [...chunks(testCases, 10)];
@@ -39,7 +38,6 @@ describe.each(chunked)('.suite %#', (contracts: string[]) => {
console.log('readState', contractTxId); console.log('readState', contractTxId);
const result2 = await SmartWeaveNodeFactory.memCached(arweave, 5).contract(contractTxId).readState(); const result2 = await SmartWeaveNodeFactory.memCached(arweave, 5).contract(contractTxId).readState();
const result2String = JSON.stringify(result2.state).trim(); const result2String = JSON.stringify(result2.state).trim();
expect(result2String).toEqual(resultString); expect(result2String).toEqual(resultString);
}, },
600000 600000

View File

@@ -0,0 +1,15 @@
[
"Daj-MNSnH55TDfxqC7v4eq0lKzVIwh98srUaWqyuZtY",
"-8A6RexFkpfWwuyVO98wzSFZh0d6VJuI-buTJvlwOJQ",
"AVTqjPQGCCXim7Nl_gn3HMjE4k0Zi_eTFRJCNEVXZxw",
"6eTVr8IKPNYbMHVcpHFXr-XNaL5hT6zRJXimcP-owmo",
"mzvUgNc8YFk0w5K5H7c8pyT-FC5Y_ba0r7_8766Kx74",
"SJ3l7474UHh3Dw6dWVT1bzsJ-8JvOewtGoDdOecWIZo",
"ydjfv0hRQdD2c-MASv4L5Qahjap_LXJyD9tNyKWvf50",
"c25-RdheC6khcACLv23-XXg1W7YuA-VSZ_1_qnNFbhw",
"ZT-70ovBlkF6cRIqvyHy5lC2LcjudsmCz9z19M4_QC4",
"o-qJmQ4B0d6TnyA_awjhiBdiq0O4Vt_dNWU3pTnhTu8",
"BYRf00nIE4pGkJ8GyUY2PIJ1rBtxFPoWIu-0dd8iukg",
"5NgGX4OToJ4M5ohWP4yxaTz_2oPsnk7vmR0v3mqXi_A",
"4o-2xMPa45BXjGuII_LbOMQWfhE1F0qugdEUZvRlXRY"
]

View File

@@ -0,0 +1,149 @@
import { RedstoneGatewayInteractionsLoader, LoggerFactory } from '@smartweave';
import { GQLEdgeInterface } from '../../legacy/gqlResult';
const responseData = {
paging: {
total: '1',
limit: 500,
items: 1,
page: 1,
pages: 1
},
interactions: [
{
status: 'confirmed',
confirming_peers: '94.130.135.178,159.203.49.13,95.217.114.57',
confirmations: '172044,172044,172044',
interaction: {
id: 'XyJm1OERe__Q-YcwTQrCeYsI14_ylASey6eYdPg-HYg',
fee: {
winston: '48173811033'
},
tags: [],
block: {
id: 'w8y2bxCQd3-26lvvy2NOt6Qz0kVooN9h4rwy6UIeC5mEfVnbftqcnWEavZfT14vY',
height: 655393,
timestamp: 1617060107
},
owner: {
address: 'oZjQWwcTYbEvnwr6zkxFqpEoDTPvWkaL3zO3-SFq2g0'
},
parent: null,
quantity: {
winston: '0'
},
recipient: ''
}
},
{
status: 'confirmed',
confirming_peers: '94.130.135.178,159.203.49.13,95.217.114.57',
confirmations: '172044,172044,172044',
interaction: {
id: 'XyJm1OERe__Q-YcwTQrCeYsI14_ylASey6eYdPg-HYg',
fee: {
winston: '48173811033'
},
tags: [],
block: {
id: 'w8y2bxCQd3-26lvvy2NOt6Qz0kVooN9h4rwy6UIeC5mEfVnbftqcnWEavZfT14vY',
height: 655393,
timestamp: 1617060107
},
owner: {
address: 'oZjQWwcTYbEvnwr6zkxFqpEoDTPvWkaL3zO3-SFq2g0'
},
parent: null,
quantity: {
winston: '0'
},
recipient: ''
}
}
]
};
const responseDataPaging = {
paging: {
total: '5',
limit: 500,
items: 1,
page: 1,
pages: 5
},
interactions: []
};
LoggerFactory.INST.logLevel('error');
const contractId = 'SJ3l7474UHh3Dw6dWVT1bzsJ-8JvOewtGoDdOecWIZo';
const fromBlockHeight = 600000;
const toBlockHeight = 655393;
const baseUrl = `http://baseUrl/gateway/interactions?contractId=SJ3l7474UHh3Dw6dWVT1bzsJ-8JvOewtGoDdOecWIZo&from=600000&to=655393`;
const fetchMock = jest
.spyOn(global, 'fetch')
.mockImplementation(
() => Promise.resolve({ json: () => Promise.resolve(responseData), ok: true, status: 200 }) as Promise<Response>
);
describe('RedstoneGatewayInteractionsLoader -> load', () => {
it('should return correct number of interactions', async () => {
const loader = new RedstoneGatewayInteractionsLoader('http://baseUrl');
const response: GQLEdgeInterface[] = await loader.load(contractId, fromBlockHeight, toBlockHeight);
expect(fetchMock).toHaveBeenCalled();
expect(response.length).toEqual(2);
});
it('should be called with correct params', async () => {
const loader = new RedstoneGatewayInteractionsLoader('http://baseUrl');
await loader.load(contractId, fromBlockHeight, toBlockHeight);
expect(fetchMock).toBeCalledWith(`${baseUrl}&page=1`);
});
it('should be called accordingly to the amount of pages', async () => {
const fetchMock = jest.spyOn(global, 'fetch').mockImplementation(
() =>
Promise.resolve({
json: () => Promise.resolve(responseDataPaging),
ok: true,
status: 200
}) as Promise<Response>
);
const loader = new RedstoneGatewayInteractionsLoader('http://baseUrl');
await loader.load(contractId, fromBlockHeight, toBlockHeight);
expect(fetchMock).toBeCalledWith(`${baseUrl}&page=1`);
expect(fetchMock).toBeCalledWith(`${baseUrl}&page=2`);
expect(fetchMock).toBeCalledWith(`${baseUrl}&page=3`);
expect(fetchMock).toBeCalledWith(`${baseUrl}&page=4`);
expect(fetchMock).toBeCalledWith(`${baseUrl}&page=4`);
expect(fetchMock).toHaveBeenCalledTimes(5);
});
it('should be called with confirmationStatus set to "confirmed"', async () => {
const loader = new RedstoneGatewayInteractionsLoader('http://baseUrl', { confirmed: true });
await loader.load(contractId, fromBlockHeight, toBlockHeight);
expect(fetchMock).toBeCalledWith(`${baseUrl}&page=1&confirmationStatus=confirmed`);
});
it('should be called with confirmationStatus set to "not_corrupted"', async () => {
const loader = new RedstoneGatewayInteractionsLoader('http://baseUrl', { notCorrupted: true });
await loader.load(contractId, fromBlockHeight, toBlockHeight);
expect(fetchMock).toBeCalledWith(`${baseUrl}&page=1&confirmationStatus=not_corrupted`);
});
it('should throw an error in case of timeout', async () => {
jest.spyOn(global, 'fetch').mockImplementation(() => Promise.reject({ status: 504, ok: false }));
const loader = new RedstoneGatewayInteractionsLoader('http://baseUrl');
try {
await loader.load(contractId, fromBlockHeight, toBlockHeight);
} catch (e) {
expect(e).toEqual(new Error('Unable to retrieve transactions. Redstone gateway responded with status 504.'));
}
});
it('should throw an error when request fails', async () => {
jest
.spyOn(global, 'fetch')
.mockImplementation(() => Promise.reject({ status: 500, ok: false, body: { message: 'request fails' } }));
const loader = new RedstoneGatewayInteractionsLoader('http://baseUrl');
try {
await loader.load(contractId, fromBlockHeight, toBlockHeight);
} catch (e) {
expect(e).toEqual(new Error('Unable to retrieve transactions. Redstone gateway responded with status 500.'));
}
});
});

View File

@@ -273,8 +273,14 @@ export class HandlerBasedContract<State> implements Contract<State> {
blockHeight?: number, blockHeight?: number,
forceDefinitionLoad = false forceDefinitionLoad = false
): Promise<ExecutionContext<State, HandlerApi<State>>> { ): Promise<ExecutionContext<State, HandlerApi<State>>> {
const { arweave, definitionLoader, interactionsLoader, interactionsSorter, executorFactory, stateEvaluator } = const {
this.smartweave; arweave,
definitionLoader,
cacheableContractInteractionsLoader,
interactionsSorter,
executorFactory,
stateEvaluator
} = this.smartweave;
let currentNetworkInfo; let currentNetworkInfo;
@@ -324,7 +330,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
// (eg. if contract is calling different contracts on different block heights). // (eg. if contract is calling different contracts on different block heights).
// This basically limits the amount of interactions with Arweave GraphQL endpoint - // This basically limits the amount of interactions with Arweave GraphQL endpoint -
// each such interaction takes at least ~500ms. // each such interaction takes at least ~500ms.
interactionsLoader.load( cacheableContractInteractionsLoader.load(
contractTxId, contractTxId,
cachedBlockHeight + 1, cachedBlockHeight + 1,
this._rootBlockHeight || this._networkInfo.height, this._rootBlockHeight || this._networkInfo.height,
@@ -362,8 +368,13 @@ export class HandlerBasedContract<State> implements Contract<State> {
transaction: GQLNodeInterface transaction: GQLNodeInterface
): Promise<ExecutionContext<State, HandlerApi<State>>> { ): Promise<ExecutionContext<State, HandlerApi<State>>> {
const benchmark = Benchmark.measure(); const benchmark = Benchmark.measure();
const { definitionLoader, interactionsLoader, interactionsSorter, executorFactory, stateEvaluator } = const {
this.smartweave; definitionLoader,
cacheableContractInteractionsLoader,
interactionsSorter,
executorFactory,
stateEvaluator
} = this.smartweave;
const blockHeight = transaction.block.height; const blockHeight = transaction.block.height;
const caller = transaction.owner.address; const caller = transaction.owner.address;
@@ -380,7 +391,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
if (cachedBlockHeight != blockHeight) { if (cachedBlockHeight != blockHeight) {
[contractDefinition, interactions] = await Promise.all([ [contractDefinition, interactions] = await Promise.all([
definitionLoader.load<State>(contractTxId), definitionLoader.load<State>(contractTxId),
await interactionsLoader.load(contractTxId, 0, blockHeight, this._evaluationOptions) await cacheableContractInteractionsLoader.load(contractTxId, 0, blockHeight, this._evaluationOptions)
]); ]);
sortedInteractions = await interactionsSorter.sort(interactions); sortedInteractions = await interactionsSorter.sort(interactions);
} else { } else {

View File

@@ -12,6 +12,7 @@ import {
import Arweave from 'arweave'; import Arweave from 'arweave';
import { Contract, HandlerBasedContract, PstContract, PstContractImpl } from '@smartweave/contract'; import { Contract, HandlerBasedContract, PstContract, PstContractImpl } from '@smartweave/contract';
import { GQLNodeInterface } from '@smartweave/legacy'; import { GQLNodeInterface } from '@smartweave/legacy';
import { CacheableContractInteractionsLoader } from '@smartweave/plugins';
/** /**
* The SmartWeave "motherboard" ;-). * The SmartWeave "motherboard" ;-).
@@ -28,6 +29,7 @@ export class SmartWeave {
readonly arweave: Arweave, readonly arweave: Arweave,
readonly definitionLoader: DefinitionLoader, readonly definitionLoader: DefinitionLoader,
readonly interactionsLoader: InteractionsLoader, readonly interactionsLoader: InteractionsLoader,
readonly cacheableContractInteractionsLoader: CacheableContractInteractionsLoader,
readonly interactionsSorter: InteractionsSorter, readonly interactionsSorter: InteractionsSorter,
readonly executorFactory: ExecutorFactory<HandlerApi<unknown>>, readonly executorFactory: ExecutorFactory<HandlerApi<unknown>>,
readonly stateEvaluator: StateEvaluator readonly stateEvaluator: StateEvaluator

View File

@@ -1,12 +1,13 @@
import Arweave from 'arweave'; import Arweave from 'arweave';
import { import {
BlockHeightSwCache, CacheableContractInteractionsLoader,
DebuggableExecutorFactory, DebuggableExecutorFactory,
DefinitionLoader, DefinitionLoader,
ExecutorFactory, ExecutorFactory,
HandlerApi, HandlerApi,
InteractionsLoader, InteractionsLoader,
InteractionsSorter, InteractionsSorter,
MemBlockHeightSwCache,
SmartWeave, SmartWeave,
StateEvaluator StateEvaluator
} from '@smartweave'; } from '@smartweave';
@@ -14,6 +15,7 @@ import {
export class SmartWeaveBuilder { export class SmartWeaveBuilder {
private _definitionLoader?: DefinitionLoader; private _definitionLoader?: DefinitionLoader;
private _interactionsLoader?: InteractionsLoader; private _interactionsLoader?: InteractionsLoader;
private _cacheableContractInteractionsLoader?: CacheableContractInteractionsLoader;
private _interactionsSorter?: InteractionsSorter; private _interactionsSorter?: InteractionsSorter;
private _executorFactory?: ExecutorFactory<HandlerApi<unknown>>; private _executorFactory?: ExecutorFactory<HandlerApi<unknown>>;
private _stateEvaluator?: StateEvaluator; private _stateEvaluator?: StateEvaluator;
@@ -30,6 +32,17 @@ export class SmartWeaveBuilder {
return this; return this;
} }
public setCacheableInteractionsLoader(
value: InteractionsLoader,
maxStoredInMemoryBlockHeights: number = Number.MAX_SAFE_INTEGER
): SmartWeaveBuilder {
this._cacheableContractInteractionsLoader = new CacheableContractInteractionsLoader(
value,
new MemBlockHeightSwCache(maxStoredInMemoryBlockHeights)
);
return this;
}
public setInteractionsSorter(value: InteractionsSorter): SmartWeaveBuilder { public setInteractionsSorter(value: InteractionsSorter): SmartWeaveBuilder {
this._interactionsSorter = value; this._interactionsSorter = value;
return this; return this;
@@ -58,6 +71,7 @@ export class SmartWeaveBuilder {
this._arweave, this._arweave,
this._definitionLoader, this._definitionLoader,
this._interactionsLoader, this._interactionsLoader,
this._cacheableContractInteractionsLoader,
this._interactionsSorter, this._interactionsSorter,
this._executorFactory, this._executorFactory,
this._stateEvaluator this._stateEvaluator

View File

@@ -7,7 +7,8 @@ export * from './modules/CreateContract';
export * from './modules/impl/BlockHeightInteractionsSorter'; export * from './modules/impl/BlockHeightInteractionsSorter';
export * from './modules/impl/ContractDefinitionLoader'; export * from './modules/impl/ContractDefinitionLoader';
export * from './modules/impl/ContractInteractionsLoader'; export * from './modules/impl/ArweaveGatewayInteractionsLoader';
export * from './modules/impl/RedstoneGatewayInteractionsLoader';
export * from './modules/impl/DefaultStateEvaluator'; export * from './modules/impl/DefaultStateEvaluator';
export * from './modules/impl/CacheableStateEvaluator'; export * from './modules/impl/CacheableStateEvaluator';
export * from './modules/impl/HandlerExecutorFactory'; export * from './modules/impl/HandlerExecutorFactory';

View File

@@ -11,6 +11,6 @@ export interface InteractionsLoader {
contractId: string, contractId: string,
fromBlockHeight: number, fromBlockHeight: number,
toBlockHeight: number, toBlockHeight: number,
evaluationOptions: EvaluationOptions evaluationOptions?: EvaluationOptions
): Promise<GQLEdgeInterface[]>; ): Promise<GQLEdgeInterface[]>;
} }

View File

@@ -30,8 +30,8 @@ interface ReqVariables {
after?: string; after?: string;
} }
export class ContractInteractionsLoader implements InteractionsLoader { export class ArweaveGatewayInteractionsLoader implements InteractionsLoader {
private readonly logger = LoggerFactory.INST.create('ContractInteractionsLoader'); private readonly logger = LoggerFactory.INST.create('ArweaveGatewayInteractionsLoader');
private static readonly query = `query Transactions($tags: [TagFilter!]!, $blockFilter: BlockFilter!, $first: Int!, $after: String) { private static readonly query = `query Transactions($tags: [TagFilter!]!, $blockFilter: BlockFilter!, $first: Int!, $after: String) {
transactions(tags: $tags, block: $blockFilter, first: $first, sort: HEIGHT_ASC, after: $after) { transactions(tags: $tags, block: $blockFilter, first: $first, sort: HEIGHT_ASC, after: $after) {
@@ -147,18 +147,18 @@ export class ContractInteractionsLoader implements InteractionsLoader {
private async getNextPage(variables: ReqVariables): Promise<GQLTransactionsResultInterface> { private async getNextPage(variables: ReqVariables): Promise<GQLTransactionsResultInterface> {
const benchmark = Benchmark.measure(); const benchmark = Benchmark.measure();
let response = await this.arweave.api.post('graphql', { let response = await this.arweave.api.post('graphql', {
query: ContractInteractionsLoader.query, query: ArweaveGatewayInteractionsLoader.query,
variables variables
}); });
this.logger.debug('GQL page load:', benchmark.elapsed()); this.logger.debug('GQL page load:', benchmark.elapsed());
while (response.status === 403) { while (response.status === 403) {
this.logger.warn(`GQL rate limiting, waiting ${ContractInteractionsLoader._30seconds}ms before next try.`); this.logger.warn(`GQL rate limiting, waiting ${ArweaveGatewayInteractionsLoader._30seconds}ms before next try.`);
await sleep(ContractInteractionsLoader._30seconds); await sleep(ArweaveGatewayInteractionsLoader._30seconds);
response = await this.arweave.api.post('graphql', { response = await this.arweave.api.post('graphql', {
query: ContractInteractionsLoader.query, query: ArweaveGatewayInteractionsLoader.query,
variables variables
}); });
} }

View File

@@ -0,0 +1,112 @@
import { Benchmark, InteractionsLoader, LoggerFactory } from '@smartweave';
import { GQLEdgeInterface, GQLNodeInterface } from 'legacy/gqlResult';
import 'isomorphic-fetch';
interface Paging {
total: string;
limit: number;
items: number;
page: number;
pages: number;
}
interface Interaction {
status: string;
confirming_peers: string;
confirmations: string;
interaction: GQLNodeInterface;
}
export interface RedstoneGatewayInteractions {
paging: Paging;
interactions: Interaction[];
message?: string;
}
type ConfirmationStatus =
| {
notCorrupted?: boolean;
confirmed?: null;
}
| {
notCorrupted?: null;
confirmed?: boolean;
};
/**
* The aim of this implementation of the {@link InteractionsLoader} is to make use of Redstone Gateway endpoint
* and retrieve contracts' interactions. Optionally - it is possible to pass skipOrphans flag in the constructor
* and therefore receive only these transactions which are confirmed. To learn more about Redstone Gateway please visit
* {@link https://github.com/redstone-finance/redstone-sw-gateway}.
* Please note that currently caching is switched off for RedstoneGatewayInteractionsLoader due to the issue mentioned in the
* following comment {@link https://github.com/redstone-finance/redstone-smartcontracts/pull/62#issuecomment-995249264}
*/
export class RedstoneGatewayInteractionsLoader implements InteractionsLoader {
constructor(private readonly baseUrl: string, private readonly confirmationStatus: ConfirmationStatus = {}) {
Object.assign(this, confirmationStatus);
}
private readonly logger = LoggerFactory.INST.create('RedstoneGatewayInteractionsLoader');
async load(contractId: string, fromBlockHeight: number, toBlockHeight: number): Promise<GQLEdgeInterface[]> {
this.logger.debug('Loading interactions: for ', { contractId, fromBlockHeight, toBlockHeight });
const interactions: GQLEdgeInterface[] = [];
let page = 0;
let totalPages = 0;
const benchmarkTotalTime = Benchmark.measure();
do {
const benchmarkRequestTime = Benchmark.measure();
const response = await fetch(
`${this.baseUrl}/gateway/interactions?${new URLSearchParams({
contractId: contractId,
from: fromBlockHeight.toString(),
to: toBlockHeight.toString(),
page: (++page).toString(),
...(this.confirmationStatus.confirmed ? { confirmationStatus: 'confirmed' } : ''),
...(this.confirmationStatus.notCorrupted ? { confirmationStatus: 'not_corrupted' } : '')
})}`
)
.then((res) => {
if (res.ok) {
return res.json();
} else {
return Promise.reject(res);
}
})
.then((data) => {
return data;
})
.catch((error) => {
if (error.body?.message) {
this.logger.error(error.body.message);
}
throw new Error(`Unable to retrieve transactions. Redstone gateway responded with status ${error.status}.`);
});
this.logger.debug(`Loading interactions: page ${page}, time: `, benchmarkRequestTime.elapsed());
totalPages = response.paging.pages;
this.logger.debug(`Loading interactions: page ${page} of ${totalPages} loaded`);
response.interactions.forEach((interaction) =>
interactions.push({
cursor: '',
node: interaction.interaction
})
);
this.logger.debug(`Loaded interactions length: ${interactions.length}`);
} while (page < totalPages);
this.logger.debug(`Loading interactions for ${contractId}:`, benchmarkTotalTime.elapsed());
this.logger.debug('All loaded interactions:', {
from: fromBlockHeight,
to: toBlockHeight,
loaded: interactions.length
});
return interactions;
}
}

View File

@@ -2,15 +2,15 @@ import Arweave from 'arweave';
import { import {
CacheableStateEvaluator, CacheableStateEvaluator,
ContractDefinitionLoader, ContractDefinitionLoader,
ContractInteractionsLoader, ArweaveGatewayInteractionsLoader,
HandlerExecutorFactory, HandlerExecutorFactory,
LexicographicalInteractionsSorter, LexicographicalInteractionsSorter,
SmartWeave, SmartWeave,
SmartWeaveBuilder, SmartWeaveBuilder,
SmartWeaveWebFactory SmartWeaveWebFactory
} from '@smartweave/core'; } from '@smartweave/core';
import { CacheableContractInteractionsLoader, CacheableExecutorFactory, Evolve } from '@smartweave/plugins'; import { CacheableExecutorFactory, Evolve } from '@smartweave/plugins';
import { FileBlockHeightSwCache, MemBlockHeightSwCache, MemCache } from '@smartweave/cache'; import { FileBlockHeightSwCache, MemCache } from '@smartweave/cache';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { KnexStateCache } from '../../cache/impl/KnexStateCache'; import { KnexStateCache } from '../../cache/impl/KnexStateCache';
@@ -45,10 +45,7 @@ export class SmartWeaveNodeFactory extends SmartWeaveWebFactory {
): SmartWeaveBuilder { ): SmartWeaveBuilder {
const definitionLoader = new ContractDefinitionLoader(arweave, new MemCache()); const definitionLoader = new ContractDefinitionLoader(arweave, new MemCache());
const interactionsLoader = new CacheableContractInteractionsLoader( const gatewayInteractionsLoader = new ArweaveGatewayInteractionsLoader(arweave);
new ContractInteractionsLoader(arweave),
new MemBlockHeightSwCache()
);
const executorFactory = new CacheableExecutorFactory(arweave, new HandlerExecutorFactory(arweave), new MemCache()); const executorFactory = new CacheableExecutorFactory(arweave, new HandlerExecutorFactory(arweave), new MemCache());
@@ -62,7 +59,7 @@ export class SmartWeaveNodeFactory extends SmartWeaveWebFactory {
return SmartWeave.builder(arweave) return SmartWeave.builder(arweave)
.setDefinitionLoader(definitionLoader) .setDefinitionLoader(definitionLoader)
.setInteractionsLoader(interactionsLoader) .setCacheableInteractionsLoader(gatewayInteractionsLoader)
.setInteractionsSorter(interactionsSorter) .setInteractionsSorter(interactionsSorter)
.setExecutorFactory(executorFactory) .setExecutorFactory(executorFactory)
.setStateEvaluator(stateEvaluator); .setStateEvaluator(stateEvaluator);
@@ -85,10 +82,7 @@ export class SmartWeaveNodeFactory extends SmartWeaveWebFactory {
): Promise<SmartWeaveBuilder> { ): Promise<SmartWeaveBuilder> {
const definitionLoader = new ContractDefinitionLoader(arweave, new MemCache()); const definitionLoader = new ContractDefinitionLoader(arweave, new MemCache());
const interactionsLoader = new CacheableContractInteractionsLoader( const gatewayInteractionsLoader = new ArweaveGatewayInteractionsLoader(arweave);
new ContractInteractionsLoader(arweave),
new MemBlockHeightSwCache()
);
const executorFactory = new CacheableExecutorFactory(arweave, new HandlerExecutorFactory(arweave), new MemCache()); const executorFactory = new CacheableExecutorFactory(arweave, new HandlerExecutorFactory(arweave), new MemCache());
@@ -102,7 +96,7 @@ export class SmartWeaveNodeFactory extends SmartWeaveWebFactory {
return SmartWeave.builder(arweave) return SmartWeave.builder(arweave)
.setDefinitionLoader(definitionLoader) .setDefinitionLoader(definitionLoader)
.setInteractionsLoader(interactionsLoader) .setCacheableInteractionsLoader(gatewayInteractionsLoader)
.setInteractionsSorter(interactionsSorter) .setInteractionsSorter(interactionsSorter)
.setExecutorFactory(executorFactory) .setExecutorFactory(executorFactory)
.setStateEvaluator(stateEvaluator); .setStateEvaluator(stateEvaluator);

View File

@@ -3,7 +3,7 @@ import { CacheableContractInteractionsLoader, CacheableExecutorFactory, Evolve }
import { import {
CacheableStateEvaluator, CacheableStateEvaluator,
ContractDefinitionLoader, ContractDefinitionLoader,
ContractInteractionsLoader, ArweaveGatewayInteractionsLoader,
HandlerExecutorFactory, HandlerExecutorFactory,
LexicographicalInteractionsSorter, LexicographicalInteractionsSorter,
SmartWeave, SmartWeave,
@@ -34,7 +34,7 @@ export class SmartWeaveWebFactory {
const definitionLoader = new ContractDefinitionLoader(arweave, new MemCache()); const definitionLoader = new ContractDefinitionLoader(arweave, new MemCache());
const interactionsLoader = new CacheableContractInteractionsLoader( const interactionsLoader = new CacheableContractInteractionsLoader(
new ContractInteractionsLoader(arweave), new ArweaveGatewayInteractionsLoader(arweave),
new RemoteBlockHeightCache('INTERACTIONS', cacheBaseURL) new RemoteBlockHeightCache('INTERACTIONS', cacheBaseURL)
); );
@@ -74,10 +74,7 @@ export class SmartWeaveWebFactory {
): SmartWeaveBuilder { ): SmartWeaveBuilder {
const definitionLoader = new ContractDefinitionLoader(arweave, new MemCache()); const definitionLoader = new ContractDefinitionLoader(arweave, new MemCache());
const interactionsLoader = new CacheableContractInteractionsLoader( const interactionsLoader = new ArweaveGatewayInteractionsLoader(arweave);
new ContractInteractionsLoader(arweave),
new MemBlockHeightSwCache(maxStoredBlockHeights)
);
const executorFactory = new CacheableExecutorFactory(arweave, new HandlerExecutorFactory(arweave), new MemCache()); const executorFactory = new CacheableExecutorFactory(arweave, new HandlerExecutorFactory(arweave), new MemCache());
@@ -91,7 +88,7 @@ export class SmartWeaveWebFactory {
return SmartWeave.builder(arweave) return SmartWeave.builder(arweave)
.setDefinitionLoader(definitionLoader) .setDefinitionLoader(definitionLoader)
.setInteractionsLoader(interactionsLoader) .setCacheableInteractionsLoader(interactionsLoader, maxStoredBlockHeights)
.setInteractionsSorter(interactionsSorter) .setInteractionsSorter(interactionsSorter)
.setExecutorFactory(executorFactory) .setExecutorFactory(executorFactory)
.setStateEvaluator(stateEvaluator); .setStateEvaluator(stateEvaluator);

View File

@@ -24,7 +24,7 @@ export class CacheableContractInteractionsLoader implements InteractionsLoader {
contractId: string, contractId: string,
fromBlockHeight: number, fromBlockHeight: number,
toBlockHeight: number, toBlockHeight: number,
evaluationOptions: EvaluationOptions evaluationOptions?: EvaluationOptions
): Promise<GQLEdgeInterface[]> { ): Promise<GQLEdgeInterface[]> {
this.logger.debug('Loading interactions', { this.logger.debug('Loading interactions', {
contractId, contractId,

View File

@@ -0,0 +1,59 @@
/* eslint-disable */
import Arweave from 'arweave';
import {
ArweaveGatewayInteractionsLoader,
DefaultEvaluationOptions,
LoggerFactory,
RedstoneGatewayInteractionsLoader,
Benchmark
} from '@smartweave';
import { TsLogFactory } from '../src/logging/node/TsLogFactory';
import Table from 'cli-table';
import colors from 'colors/safe';
/*
Script allows to benchmark loading interactions response time based on the given gateway
To run this script properly, one need to pass [gateway name][contract id][from][to] variables as script's arguments
e.g yarn ts-node -r tsconfig-paths/register tools/gateways-comparison-benchmark.ts arweave Daj-MNSnH55TDfxqC7v4eq0lKzVIwh98srUaWqyuZtY 0 70000
*/
async function gatewayBenchmark() {
let table = new Table({
head: ['gateway', 'contractId', 'fromBlockHeight', 'toBlockHeight', 'timeSpent'],
colWidths: [10, 50, 20, 20, 20]
});
const arweave = Arweave.init({
host: 'arweave.net',
port: 443,
protocol: 'https',
logging: false
});
LoggerFactory.use(new TsLogFactory());
LoggerFactory.INST.logLevel('debug');
const gateway = process.argv[2];
const contractId = process.argv[3];
const fromBlockHeight = process.argv[4];
const toBlockHeight = process.argv[5];
const loader =
gateway == 'arweave'
? new ArweaveGatewayInteractionsLoader(arweave)
: new RedstoneGatewayInteractionsLoader('https://d1o5nlqr4okus2.cloudfront.net');
const options = gateway == 'arweave' ? new DefaultEvaluationOptions() : null;
const benchmark = Benchmark.measure();
await loader.load(contractId, parseInt(fromBlockHeight), parseInt(toBlockHeight), options);
const timeSpent = benchmark.elapsed();
table.push([gateway, contractId, fromBlockHeight, toBlockHeight, timeSpent.toString()].map((el) => colors.blue(el)));
console.log(table.toString());
}
gatewayBenchmark().catch((e) => console.error(e));

View File

@@ -0,0 +1,75 @@
/* eslint-disable */
import Arweave from 'arweave';
import {
ArweaveGatewayInteractionsLoader,
DefaultEvaluationOptions,
LoggerFactory,
RedstoneGatewayInteractionsLoader,
Benchmark
} from '@smartweave';
import { TsLogFactory } from '../src/logging/node/TsLogFactory';
import Table from 'cli-table';
import colors from 'colors/safe';
/*
Script allows to benchmark loading interactions response time for given contract for both Arweave and Redstone gateways
To run this script properly, one need to pass [contract id][from][to] variables as script's arugments
e.g yarn ts-node -r tsconfig-paths/register tools/gateways-comparison-benchmark.ts Daj-MNSnH55TDfxqC7v4eq0lKzVIwh98srUaWqyuZtY 0 70000
*/
async function gatewayComparisonBenchmark() {
let table = new Table({
head: ['contractId', 'fromBlockHeight', 'toBlockHeight', 'arweave', 'redstone'],
colWidths: [50, 20, 20, 15, 15]
});
LoggerFactory.use(new TsLogFactory());
LoggerFactory.INST.logLevel('debug');
const contractId = process.argv[2];
const fromBlockHeight = process.argv[3];
const toBlockHeight = process.argv[4];
const timeSpentArweave = await loadFromAweaveGateway(contractId, fromBlockHeight, toBlockHeight);
const timeSpentRedstone = await loadFromRedstoneGateway(contractId, fromBlockHeight, toBlockHeight);
table.push(
[contractId, fromBlockHeight, toBlockHeight, timeSpentArweave.toString(), `${timeSpentRedstone.toString()}ms`].map(
(el) => colors.blue(el)
)
);
console.log(table.toString());
}
async function loadFromRedstoneGateway(contractId: string, fromBlockHeight?: string, toBlockHeight?: string) {
const loader = new RedstoneGatewayInteractionsLoader('https://d1o5nlqr4okus2.cloudfront.net');
const benchmark = Benchmark.measure();
for (let i = 0; i < 3; i++) {
await loader.load(contractId, parseInt(fromBlockHeight), parseInt(toBlockHeight));
}
return Math.round(parseInt(benchmark.elapsed().toString().replace('ms', ''))) / 3;
}
async function loadFromAweaveGateway(contractId: string, fromBlockHeight?: string, toBlockHeight?: string) {
const arweave = Arweave.init({
host: 'arweave.net',
port: 443,
protocol: 'https',
logging: false
});
const loader = new ArweaveGatewayInteractionsLoader(arweave);
const benchmark = Benchmark.measure();
await loader.load(
contractId,
fromBlockHeight ? parseInt(fromBlockHeight) : 0,
toBlockHeight ? parseInt(toBlockHeight) : 831900,
new DefaultEvaluationOptions()
);
return benchmark.elapsed();
}
gatewayComparisonBenchmark().catch((e) => console.error(e));

1902
yarn.lock

File diff suppressed because it is too large Load Diff