feat: sortKey interactions cache

This commit is contained in:
ppe
2022-06-01 16:51:43 +02:00
committed by just_ppe
parent 165bb38f06
commit 74e8696838
79 changed files with 2421 additions and 3294 deletions

View File

@@ -20,3 +20,4 @@ jobs:
run: yarn test:integration:wasm
- name: Run regression tests
run: yarn test:regression

102
README.md
View File

@@ -12,7 +12,7 @@ and modularity (e.g. ability to use different types of caches, imported from ext
We're already using the new SDK on production, both in our webapp and nodes.
However, if you'd like to use it in production as well, please contact us on [discord](https://discord.com/invite/PVxBZKFr46) to ensure a smooth transition and get help with testing.
To further improve contract state evaluation time, one can additionally use AWS CloudFront based Arweave cache described [here](https://github.com/warp-contracts/warp/blob/main/docs/CACHE.md).
To further improve contract state evaluation time, one can additionally use AWS CloudFront based Arweave cache described [here](https://github.com/redstone-finance/warp/blob/main/docs/CACHE.md).
- [Architecture](#architecture)
- [State evaluation diagram](#state-evaluation-diagram)
@@ -45,16 +45,16 @@ Warp SDK consists of main 3 layers:
2. The `Caching` layer - is build on top of the `Core Protocol` layer and allows caching results of each of the `Core Protocol` modules separately.
The main interfaces of this layer are the:
1. `WarpCache` - simple key-value cache, useful for modules like `Definition Loader`
2. `BlockHeightWarpCache` - a block height aware cache, crucial for modules like `Interactions Loader` and `State Evaluator`.
2. `SortKeyCache` - a transaction sort key aware cache, crucial for modules like `Interactions Loader` and `State Evaluator`.
These interfaces - used in conjunction with cache-aware versions of the core modules (like `CacheableContractInteractionsLoader` or `CacheableStateEvaluator`)
allow to greatly improve performance and SmartWeave contract's state evaluation time - especially for contracts that heavily interact with other contracts.
allow to greatly improve performance and Warp contract's state evaluation time - especially for contracts that heavily interact with other contracts.
3. The `Extensions` layer - includes everything that can be built on top of the core SDK - including Command Line Interface, Debugging tools, different logging implementations,
so called "dry-runs" (i.e. actions that allow to quickly verify the result of given contract interaction - without writing anything on Arweave).
This modular architecture has several advantages:
1. Each module can be separately tested and developed.
2. The SmartWeave client can be customized depending on user needs (e.g. different type of caches for web and node environment)
2. The Warp client can be customized depending on user needs (e.g. different type of caches for web and node environment)
3. It makes it easier to add new features on top of the core protocol - without the risk of breaking the functionality of the core layer.
## State evaluation diagram
@@ -88,7 +88,7 @@ Warp SDK is just part of the whole Warp smart contracts platform. It makes trans
## Development
PRs are welcome! :-) Also, feel free to submit [issues](https://github.com/warp-contracts/warp/issues) - with both bugs and feature proposals.
PRs are welcome! :-) Also, feel free to submit [issues](https://github.com/redstone-finance/warp/issues) - with both bugs and feature proposals.
In case of creating a PR - please use [semantic commit messages](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716).
### Installation
@@ -140,52 +140,65 @@ All exports are stored under `warp` global variable.
```html
<script>
const warp = warp.WarpWebFactory.memCachedBased(arweave);
const warp = warp.WarpFactory.warpGw(arweave);
</script>
```
### Using the Warp Gateway
#### SDK version >= `0.5.0`
From version `0.5.0`, the Warp Gateway is the default gateway used by the SDK.
By default, the `{notCorrupted: true}` mode is used (as describe below).
If you want to use the Arweave gateway in version >= `0.5.0`:
```ts
const warp = WarpNodeFactory.memCachedBased(arweave).useArweaveGateway().build();
import {WarpFactory} from "./WarpFactory";
const warp = WarpFactory.warpGw(arweave);
```
#### SDK version < `0.5.0`
In order to use the [Warp Gateway](https://github.com/warp-contracts/sw-gateway) for loading the contract interactions,
configure the smartweave instance in the following way:
By default, the `{notCorrupted: true}` mode is used.
Optionally - you can pass the second argument to the `warpGw` method that will determine the options for the Warp gateway.
The options object has three fields:
1. `confirmationStatus` - with default set to `{ notCorrupted: true }` - which returns all confirmed and non-yet processed/confirmed transactions.
Other possible values are:
- `{confirmed: true}` - returns only the confirmed transactions - keep in mind that transaction confirmation takes around 20-30 minutes.
- `null` - compatible with how the Arweave Gateway GQL endpoint works - returns
all the interactions. There is a risk of returning [corrupted transactions](https://github.com/redstone-finance/redstone-sw-gateway#corrupted-transactions).
2. `source` - with default set to `null` - which loads interactions registered both directly on Arweave and through Warp Sequencer.
Other possible values are: `SourceType.ARWEAVE` and `SourceType.WARP_SEQUENCER`
3. `address` - the gateway address, by default set to `WARP_GW_URL`
E.g.:
```ts
const warp = WarpNodeFactory.memCachedBased(arweave).useWarpGateway().build();
import {defaultWarpGwOptions, WarpFactory} from "./WarpFactory";
const warp = WarpFactory.warpGw(arweave, {...defaultWarpGwOptions, confirmationStatus: { confirmed: true }});
```
The gateway is currently available under [https://gateway.redstone.finance](https://gateway.redstone.finance) url.
Full API reference is available [here](https://github.com/warp-contracts/sw-gateway#http-api-reference).
The Warp gateway is currently available under [https://gateway.redstone.finance](https://gateway.redstone.finance) url.
Full API reference is available [here](https://github.com/redstone-finance/redstone-sw-gateway#http-api-reference).
Optionally - you can pass the second argument to the `useWarpGateway` method that will determine which transactions will be loaded:
1. no parameter - default mode, compatible with how the Arweave Gateway GQL endpoint works - returns
all the interactions. There is a risk of returning [corrupted transactions](https://github.com/warp-contracts/sw-gateway#corrupted-transactions).
2. `{confirmed: true}` - returns only confirmed transactions - the most safe mode, eg:
### Using the Arweave gateway
```ts
const warp = WarpNodeFactory.memCachedBased(arweave).useWarpGateway({ confirmed: true }).build();
import {WarpFactory} from "./WarpFactory";
const warp = WarpFactory.arweaveGw(arweave);
```
3. `{notCorrupted: true}` - returns both confirmed and not yet verified interactions (i.e. the latest ones).
Not as safe as previous mode, but good if you want combine high level of safety with the most recent data.
More examples can be found [here](https://github.com/redstone-finance/redstone-smartcontracts-examples/blob/main/src/redstone-gateway-example.ts).
### Testing on ArLocal
All our integration tests are using ArLocal and we strongly suggest using it for testing your
own contracts.
In order to make the testing easier, a dedicated factory method has been added:
```ts
const warp = WarpNodeFactory.memCachedBased(arweave).useWarpGateway({ notCorrupted: true }).build();
import {WarpFactory} from "./WarpFactory";
const warp = WarpFactory.forTesting(arweave);
```
More examples can be found [here](https://github.com/warp-contracts/warp-contracts-examples/blob/main/src/redstone-gateway-example.ts).
It creates a `Warp` instance that can be used with the ArLocal.
Additionally, it is using the in-memory implementation of the LevelDB
([memory-level](https://www.npmjs.com/package/memory-level)) - so that manually cleaning
the cache storage is not required.
### Contract methods
@@ -210,7 +223,7 @@ Allows to connect wallet to a contract. Connecting a wallet MAY be done before "
<summary>Example</summary>
```typescript
const contract = smartweave.contract('YOUR_CONTRACT_TX_ID').connect(jwk);
const contract = warp.contract('YOUR_CONTRACT_TX_ID').connect(jwk);
```
</details>
@@ -233,7 +246,7 @@ Allows to set (EvaluationOptions)
<summary>Example</summary>
```typescript
const contract = smartweave.contract('YOUR_CONTRACT_TX_ID').setEvaluationOptions({
const contract = warp.contract('YOUR_CONTRACT_TX_ID').setEvaluationOptions({
waitForConfirmation: true,
ignoreExceptions: false
});
@@ -354,20 +367,9 @@ const result = await contract.writeInteraction({
</details>
### Evolve
Evolve is a feature that allows to change contract's source code, without having to deploy a new contract. In order to properly perform evolving you need to follow these steps:
1. indicate in your initial state that your contract can be evolved like [here](https://github.com/warp-contracts/warp/blob/main/src/__tests__/integration/data/token-pst.json).
2. optionally you can also set in initial state `evolve` property to `null`.
3. write an evolve interaction in your contract, an example [here](https://github.com/warp-contracts/warp/blob/main/src/__tests__/integration/data/token-pst.js#L84).
4. post a transaction to arweave with the new source, wait for it to be confirmed by the network and point to this transaction when calling evolve interaction. Warp SDK provides two methods which ease the process (`save` for saving new contract source and `evolve` for indicating new evolved contract source) - you can see how they are used in the [following test](https://github.com/warp-contracts/warp/blob/main/src/__tests__/integration/basic/pst.test.ts#L128).
Please note, that currently you can use evolve on all the contracts - including bundled ones and WASM contracts (an example [in the test](https://github.com/warp-contracts/warp/blob/main/src/__tests__/integration/wasm/rust-deploy-write-read.test.ts#L228)). You can also bundle your `evolve` interaction. Bundling for saving new contract source is not yet supported.
### WASM
WASM provides proper sandboxing ensuring execution environment isolation which guarantees security to the contracts execution. As for now - **Assemblyscript**, **Rust** and **Go** languages are supported. WASM contracts templates containing example PST contract implementation within tools for compiling contracts to WASM, testing, deploying (locally, on testnet and mainnet) and writing interactions are available in a [dedicated repository](https://github.com/warp-contracts/warp-wasm-templates).
WASM provides proper sandboxing ensuring execution environment isolation which guarantees security to the contracts execution. As for now - **Assemblyscript**, **Rust** and **Go** languages are supported. WASM contracts templates containing example PST contract implementation within tools for compiling contracts to WASM, testing, deploying (locally, on testnet and mainnet) and writing interactions are available in a [dedicated repository](https://github.com/redstone-finance/redstone-smartcontracts-wasm-templates).
Using SDKs' methods works exactly the same as in case of a regular JS contract.
@@ -426,7 +428,7 @@ You can also perform internal read to the contract (originally introduced by the
await SmartWeave.contracts.readContractState(action.input.contractId);
```
You can view some more examples in the [internal writes test directory](https://github.com/warp-contracts/warp/tree/main/src/__tests__/integration/internal-writes). If you would like to read whole specification and motivation which stands behind introducing internal writes feature, please read [following issue](https://github.com/warp-contracts/warp/issues/37).
You can view some more examples in the [internal writes test directory](https://github.com/redstone-finance/redstone-smartcontracts/tree/main/src/__tests__/integration/internal-writes). If you would like to read whole specification and motivation which stands behind introducing internal writes feature, please read [following issue](https://github.com/redstone-finance/redstone-smartcontracts/issues/37).
### Performance - best practices
@@ -443,7 +445,7 @@ LoggerFactory.INST.logLevel('fatal');
LoggerFactory.INST.logLevel('error');
// then create an instance of smartweave sdk
const warp = WarpWebFactory.memCached(arweave);
const warp = WarpFactory.memCached(arweave);
```
Logging on `info` or `debug` level is good for development, but turning it on globally might slow down the evaluation by a factor of 2.
@@ -457,15 +459,15 @@ LoggerFactory.INST.logLevel('fatal');
LoggerFactory.INST.logLevel('debug', 'ArweaveGatewayInteractionsLoader');
// then create an instance of smartweave sdk
const smartweave = SmartWeaveWebFactory.memCached(arweave);
const smartweave = SmartWeaveFactory.memCached(arweave);
```
### Examples
Usage examples can be found in
a dedicated [repository](https://github.com/warp-contracts/warp-contracts-examples).
a dedicated [repository](https://github.com/redstone-finance/redstone-smartweave-examples).
Please follow instructions in its README.md (and detail-ish comments in the examples files) to learn more.
There is also a separate repository with a web application [example](https://github.com/warp-contracts/warp-app).
There is also a separate repository with a web application [example](https://github.com/redstone-finance/redstone-smartcontracts-app).
We've also created an [academy](https://redstone.academy/) that introduces to the process of writing your own SmartWeave contract from scratch and describes how to interact with it using Warp SDK.
@@ -474,4 +476,4 @@ A community package - [arweave-jest-fuzzing](https://github.com/Hansa-Network/ar
### Migration Guide
If you're already using Arweave smartweave.js SDK and would like to smoothly migrate to Warp SDK -
check out the [migration guide](https://github.com/warp-contracts/warp/blob/main/docs/MIGRATION_GUIDE.md).
check out the [migration guide](https://github.com/redstone-finance/warp/blob/main/docs/MIGRATION_GUIDE.md).

View File

@@ -16,9 +16,10 @@ const runBuild = async () => {
bundle: true,
outfile: './bundles/web.bundle.js',
platform: 'browser',
target: ['es2020', 'chrome58', 'firefox57', 'safari11'],
target: ['esnext'],
format: 'iife',
globalName: 'warp'
globalName: 'warp',
external: ['events']
}).catch((e) => {
console.log(e);
process.exit(1);
@@ -30,9 +31,10 @@ const runBuild = async () => {
bundle: true,
outfile: './bundles/web.bundle.min.js',
platform: 'browser',
target: ['es2020', 'chrome58', 'firefox57', 'safari11'],
target: ['esnext'],
format: 'iife',
globalName: 'warp'
globalName: 'warp',
external: ['events']
}).catch((e) => {
console.log(e);
process.exit(1);
@@ -44,9 +46,10 @@ const runBuild = async () => {
bundle: true,
outfile: './bundles/esm.bundle.js',
platform: 'browser',
target: ['es2020', 'chrome58', 'firefox57', 'safari11'],
target: ['esnext'],
format: 'esm',
globalName: 'warp'
globalName: 'warp',
external: ['events']
}).catch((e) => {
console.log(e);
process.exit(1);
@@ -58,9 +61,10 @@ const runBuild = async () => {
bundle: true,
outfile: './bundles/esm.bundle.min.js',
platform: 'browser',
target: ['es2020', 'chrome58', 'firefox57', 'safari11'],
target: ['esnext'],
format: 'esm',
globalName: 'warp'
globalName: 'warp',
external: ['events']
}).catch((e) => {
console.log(e);
process.exit(1);

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
# Migration Guide from Arweave's SmartWeave SDK to Warp SDK
This guide describes <strong>the simplest</strong> way to switch to the new version of SmartWeave. It uses `WebNodeFactory` for Node and `WarpWebFactory` for Web to quickly obtain fully configured, mem-cacheable SmartWeave instance. To see a more detailed explanation of all the core modules check out the [source code.](https://github.com/redstone-finance/warp)
This guide describes <strong>the simplest</strong> way to switch to the new version of SmartWeave. It uses `WarpFactory` for Web to quickly obtain fully configured, mem-cacheable SmartWeave instance. To see a more detailed explanation of all the core modules check out the [source code.](https://github.com/redstone-finance/warp)
### You can watch this tutorial on YouTube 🎬
@@ -66,8 +66,6 @@ const arweave = Arweave.init({
const smartweave = WarpNodeFactory.memCached(arweave);
```
For Web environment you should use `WarpWebFactory` instead of `WarpNodeFactory`.
In this example we've used the `memCached` method. You can see other available methods in documentation:
- [For Web](https://smartweave.docs.redstone.finance/classes/SmartWeaveWebFactory.html)

View File

@@ -1,6 +1,6 @@
{
"name": "warp-contracts",
"version": "1.1.14",
"version": "1.2.0-beta.1",
"description": "An implementation of the SmartWeave smart contract protocol.",
"main": "./lib/cjs/index.js",
"module": "./lib/esm/index.js",
@@ -71,7 +71,9 @@
"elliptic": "^6.5.4",
"fast-copy": "^2.1.1",
"knex": "^0.95.14",
"level": "^8.0.0",
"lodash": "^4.17.21",
"memory-level": "^1.0.0",
"redstone-isomorphic": "1.1.8",
"redstone-wasm-metering": "1.0.0",
"safe-stable-stringify": "2.3.1",
@@ -97,6 +99,7 @@
"eslint-plugin-prettier": "^3.4.1",
"express": "^4.17.1",
"jest": "^28.1.3",
"lmdb": "^2.5.2",
"pg": "^8.7.1",
"prettier": "^2.3.2",
"rimraf": "^3.0.2",

View File

@@ -0,0 +1,283 @@
import fs from 'fs';
import ArLocal from 'arlocal';
import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import {
ArweaveGatewayInteractionsLoader,
Contract,
DefaultEvaluationOptions,
GQLNodeInterface,
LexicographicalInteractionsSorter,
LoggerFactory,
Warp,
WarpFactory
} from '@warp';
import path from 'path';
import { addFunds, mineBlock } from '../_helpers';
let arweave: Arweave;
let arlocal: ArLocal;
let warp: Warp;
let contract: Contract<ExampleContractState>;
interface ExampleContractState {
counter: number;
}
describe('Testing the Arweave interactions loader', () => {
let contractSrc: string;
let wallet: JWKInterface;
let loader: ArweaveGatewayInteractionsLoader;
const evalOptions = new DefaultEvaluationOptions();
let sorter: LexicographicalInteractionsSorter;
let interactions: GQLNodeInterface[];
beforeAll(async () => {
LoggerFactory.INST.logLevel('error');
// note: each tests suit (i.e. file with tests that Jest is running concurrently
// with another files has to have ArLocal set to a different port!)
arlocal = new ArLocal(1831, false);
await arlocal.start();
arweave = Arweave.init({
host: 'localhost',
port: 1831,
protocol: 'http'
});
loader = new ArweaveGatewayInteractionsLoader(arweave);
sorter = new LexicographicalInteractionsSorter(arweave);
warp = WarpFactory.forTesting(arweave);
wallet = await arweave.wallets.generate();
await addFunds(arweave, wallet);
contractSrc = fs.readFileSync(path.join(__dirname, '../data/inf-loop-contract.js'), 'utf8');
const contractTxId = await warp.createContract.deploy({
wallet,
initState: JSON.stringify({
counter: 10
}),
src: contractSrc
});
contract = warp
.contract<ExampleContractState>(contractTxId)
.setEvaluationOptions({
maxInteractionEvaluationTimeSeconds: 1
})
.connect(wallet);
await mineBlock(arweave);
});
afterAll(async () => {
await arlocal.stop();
});
it('should add interactions on multiple blocks', async () => {
await contract.writeInteraction({ function: 'add' });
await contract.writeInteraction({ function: 'add' });
await contract.writeInteraction({ function: 'add' });
await contract.writeInteraction({ function: 'add' });
await contract.writeInteraction({ function: 'add' });
await mineBlock(arweave);
await contract.writeInteraction({ function: 'add' });
await contract.writeInteraction({ function: 'add' });
await contract.writeInteraction({ function: 'add' });
await contract.writeInteraction({ function: 'add' });
await contract.writeInteraction({ function: 'add' });
await contract.writeInteraction({ function: 'add' });
await contract.writeInteraction({ function: 'add' });
await contract.writeInteraction({ function: 'add' });
await contract.writeInteraction({ function: 'add' });
await contract.writeInteraction({ function: 'add' });
await mineBlock(arweave);
await contract.writeInteraction({ function: 'add' });
await contract.writeInteraction({ function: 'add' });
await contract.writeInteraction({ function: 'add' });
await contract.writeInteraction({ function: 'add' });
await contract.writeInteraction({ function: 'add' });
await mineBlock(arweave);
});
it('should load all interactions', async () => {
interactions = await loader.load(contract.txId(), null, null, evalOptions);
expect(interactions.length).toBe(20);
});
it('should return properly sorted interactions', async () => {
const sorted = await sorter.sort(
interactions.map((i) => ({
node: i,
cursor: null
}))
);
for (let i = 0; i < interactions.length; i++) {
const interaction = interactions[i];
expect(sorted[i].node.sortKey).toEqual(interaction.sortKey);
}
});
it('should properly limit results (0,1,2)', async () => {
const interactions2 = await loader.load(contract.txId(), null, interactions[2].sortKey, evalOptions);
expect(interactions2.length).toBe(3);
expect(interactions2[0].sortKey).toEqual(interactions[0].sortKey);
expect(interactions2[1].sortKey).toEqual(interactions[1].sortKey);
expect(interactions2[2].sortKey).toEqual(interactions[2].sortKey);
});
it('should properly limit results (1,2)', async () => {
const interactions2 = await loader.load(
contract.txId(),
interactions[0].sortKey,
interactions[2].sortKey,
evalOptions
);
expect(interactions2.length).toBe(2);
expect(interactions2[0].sortKey).toEqual(interactions[1].sortKey);
expect(interactions2[1].sortKey).toEqual(interactions[2].sortKey);
});
it('should properly limit results (3,4,5,6)', async () => {
const interactions2 = await loader.load(
contract.txId(),
interactions[2].sortKey,
interactions[6].sortKey,
evalOptions
);
expect(interactions2.length).toBe(4);
expect(interactions2[0].sortKey).toEqual(interactions[3].sortKey);
expect(interactions2[1].sortKey).toEqual(interactions[4].sortKey);
expect(interactions2[2].sortKey).toEqual(interactions[5].sortKey);
expect(interactions2[3].sortKey).toEqual(interactions[6].sortKey);
});
it('should properly limit results (6,7,8,9)', async () => {
const interactions2 = await loader.load(
contract.txId(),
interactions[5].sortKey,
interactions[9].sortKey,
evalOptions
);
expect(interactions2.length).toBe(4);
expect(interactions2[0].sortKey).toEqual(interactions[6].sortKey);
expect(interactions2[1].sortKey).toEqual(interactions[7].sortKey);
expect(interactions2[2].sortKey).toEqual(interactions[8].sortKey);
expect(interactions2[3].sortKey).toEqual(interactions[9].sortKey);
});
it('should properly limit results (6-19) - no upper bound', async () => {
const interactions2 = await loader.load(contract.txId(), interactions[5].sortKey, null, evalOptions);
expect(interactions2.length).toBe(14);
expect(interactions2[0].sortKey).toEqual(interactions[6].sortKey);
expect(interactions2[1].sortKey).toEqual(interactions[7].sortKey);
expect(interactions2[2].sortKey).toEqual(interactions[8].sortKey);
expect(interactions2[3].sortKey).toEqual(interactions[9].sortKey);
});
it('should properly limit results (2-11)', async () => {
const interactions2 = await loader.load(
contract.txId(),
interactions[1].sortKey,
interactions[11].sortKey,
evalOptions
);
expect(interactions2.length).toBe(10);
expect(interactions2[0].sortKey).toEqual(interactions[2].sortKey);
expect(interactions2[0].block.height).toEqual(2);
expect(interactions2[1].sortKey).toEqual(interactions[3].sortKey);
expect(interactions2[1].block.height).toEqual(2);
expect(interactions2[2].sortKey).toEqual(interactions[4].sortKey);
expect(interactions2[2].block.height).toEqual(2);
expect(interactions2[3].sortKey).toEqual(interactions[5].sortKey);
expect(interactions2[3].block.height).toEqual(3);
expect(interactions2[4].sortKey).toEqual(interactions[6].sortKey);
expect(interactions2[4].block.height).toEqual(3);
expect(interactions2[5].sortKey).toEqual(interactions[7].sortKey);
expect(interactions2[5].block.height).toEqual(3);
expect(interactions2[6].sortKey).toEqual(interactions[8].sortKey);
expect(interactions2[6].block.height).toEqual(3);
expect(interactions2[7].sortKey).toEqual(interactions[9].sortKey);
expect(interactions2[7].block.height).toEqual(3);
expect(interactions2[8].sortKey).toEqual(interactions[10].sortKey);
expect(interactions2[8].block.height).toEqual(3);
expect(interactions2[9].sortKey).toEqual(interactions[11].sortKey);
expect(interactions2[9].block.height).toEqual(3);
});
it('should properly limit results (13-17)', async () => {
const interactions2 = await loader.load(
contract.txId(),
interactions[12].sortKey,
interactions[17].sortKey,
evalOptions
);
expect(interactions2.length).toBe(5);
expect(interactions2[0].sortKey).toEqual(interactions[13].sortKey);
expect(interactions2[0].block.height).toEqual(3);
expect(interactions2[1].sortKey).toEqual(interactions[14].sortKey);
expect(interactions2[1].block.height).toEqual(3);
expect(interactions2[2].sortKey).toEqual(interactions[15].sortKey);
expect(interactions2[2].block.height).toEqual(4);
expect(interactions2[3].sortKey).toEqual(interactions[16].sortKey);
expect(interactions2[3].block.height).toEqual(4);
expect(interactions2[4].sortKey).toEqual(interactions[17].sortKey);
expect(interactions2[4].block.height).toEqual(4);
});
it('should properly limit results (13-17)', async () => {
const interactions2 = await loader.load(
contract.txId(),
interactions[3].sortKey,
interactions[17].sortKey,
evalOptions
);
expect(interactions2.length).toBe(14);
expect(interactions2[0].sortKey).toEqual(interactions[4].sortKey);
expect(interactions2[0].block.height).toEqual(2);
expect(interactions2[1].sortKey).toEqual(interactions[5].sortKey);
expect(interactions2[1].block.height).toEqual(3);
expect(interactions2[2].sortKey).toEqual(interactions[6].sortKey);
expect(interactions2[2].block.height).toEqual(3);
expect(interactions2[3].sortKey).toEqual(interactions[7].sortKey);
expect(interactions2[3].block.height).toEqual(3);
expect(interactions2[4].sortKey).toEqual(interactions[8].sortKey);
expect(interactions2[4].block.height).toEqual(3);
expect(interactions2[5].sortKey).toEqual(interactions[9].sortKey);
expect(interactions2[5].block.height).toEqual(3);
expect(interactions2[6].sortKey).toEqual(interactions[10].sortKey);
expect(interactions2[6].block.height).toEqual(3);
expect(interactions2[7].sortKey).toEqual(interactions[11].sortKey);
expect(interactions2[7].block.height).toEqual(3);
expect(interactions2[8].sortKey).toEqual(interactions[12].sortKey);
expect(interactions2[8].block.height).toEqual(3);
expect(interactions2[9].sortKey).toEqual(interactions[13].sortKey);
expect(interactions2[9].block.height).toEqual(3);
expect(interactions2[10].sortKey).toEqual(interactions[14].sortKey);
expect(interactions2[10].block.height).toEqual(3);
expect(interactions2[11].sortKey).toEqual(interactions[15].sortKey);
expect(interactions2[11].block.height).toEqual(4);
expect(interactions2[12].sortKey).toEqual(interactions[16].sortKey);
expect(interactions2[12].block.height).toEqual(4);
expect(interactions2[13].sortKey).toEqual(interactions[17].sortKey);
expect(interactions2[13].block.height).toEqual(4);
});
});

View File

@@ -3,7 +3,7 @@ import fs from 'fs';
import ArLocal from 'arlocal';
import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import { Contract, LoggerFactory, Warp, WarpNodeFactory } from '@warp';
import { Contract, LoggerFactory, Warp, WarpFactory } from '@warp';
import path from 'path';
import { addFunds, mineBlock } from '../_helpers';
@@ -32,7 +32,7 @@ describe('Testing the Warp client', () => {
LoggerFactory.INST.logLevel('error');
warp = WarpNodeFactory.forTesting(arweave);
warp = WarpFactory.forTesting(arweave);
wallet = await arweave.wallets.generate();
await addFunds(arweave, wallet);

View File

@@ -3,7 +3,7 @@ import fs from 'fs';
import ArLocal from 'arlocal';
import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import { Contract, LoggerFactory, Warp, WarpNodeFactory } from '@warp';
import { Contract, LoggerFactory, Warp, WarpFactory } from '@warp';
import path from 'path';
import { addFunds, mineBlock } from '../_helpers';
@@ -47,7 +47,7 @@ describe('Testing the Warp client', () => {
LoggerFactory.INST.logLevel('error');
warp = WarpNodeFactory.forTesting(arweave);
warp = WarpFactory.forTesting(arweave);
wallet = await arweave.wallets.generate();
await addFunds(arweave, wallet);

View File

@@ -1,188 +0,0 @@
import fs from 'fs';
import ArLocal from 'arlocal';
import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import { Contract, LoggerFactory, Warp, WarpNodeFactory } from '@warp';
import path from 'path';
import { addFunds, mineBlock } from '../_helpers';
interface ExampleContractState {
counter: number;
}
/**
* This integration test should verify whether the basic functions of the Warp client
* work properly when file-based cache is being used.
*/
describe('Testing the Warp client', () => {
let contractSrc: string;
let initialState: string;
let wallet: JWKInterface;
let arweave: Arweave;
let arlocal: ArLocal;
let warp: Warp;
let contract: Contract<ExampleContractState>;
let contractVM: Contract<ExampleContractState>;
const cacheDir = path.join(__dirname, 'cache');
beforeAll(async () => {
removeCacheDir();
fs.mkdirSync(cacheDir);
// note: each tests suit (i.e. file with tests that Jest is running concurrently
// with another files has to have ArLocal set to a different port!)
arlocal = new ArLocal(1790, false);
await arlocal.start();
arweave = Arweave.init({
host: 'localhost',
port: 1790,
protocol: 'http'
});
LoggerFactory.INST.logLevel('error');
warp = WarpNodeFactory.fileCachedBased(arweave, cacheDir).useArweaveGateway().build();
wallet = await arweave.wallets.generate();
await addFunds(arweave, wallet);
contractSrc = fs.readFileSync(path.join(__dirname, '../data/example-contract.js'), 'utf8');
initialState = fs.readFileSync(path.join(__dirname, '../data/example-contract-state.json'), 'utf8');
// deploying contract using the new SDK.
const { contractTxId } = await warp.createContract.deploy({
wallet,
initState: initialState,
src: contractSrc
});
contract = warp.contract(contractTxId);
contractVM = warp.contract<ExampleContractState>(contractTxId).setEvaluationOptions({
useVM2: true
});
contract.connect(wallet);
contractVM.connect(wallet);
await mineBlock(arweave);
});
afterAll(async () => {
await arlocal.stop();
removeCacheDir();
});
it('should properly deploy contract with initial state', async () => {
expect((await contract.readState()).state.counter).toEqual(555);
expect((await contractVM.readState()).state.counter).toEqual(555);
});
it('should properly add new interaction', async () => {
await contract.writeInteraction({ function: 'add' });
await mineBlock(arweave);
expect((await contract.readState()).state.counter).toEqual(556);
expect((await contractVM.readState()).state.counter).toEqual(556);
});
it('should properly add another interactions', async () => {
await contract.writeInteraction({ function: 'add' });
await mineBlock(arweave);
await contract.writeInteraction({ function: 'add' });
await mineBlock(arweave);
await contract.writeInteraction({ function: 'add' });
await mineBlock(arweave);
expect((await contract.readState()).state.counter).toEqual(559);
expect((await contractVM.readState()).state.counter).toEqual(559);
});
it('should properly view contract state', async () => {
const interactionResult = await contract.viewState<unknown, number>({ function: 'value' });
const interactionResult2 = await contractVM.viewState<unknown, number>({ function: 'value' });
expect(interactionResult.result).toEqual(559);
expect(interactionResult2.result).toEqual(559);
});
it('should properly read state with a fresh client', async () => {
const contract2 = WarpNodeFactory.fileCachedBased(arweave, cacheDir)
.useArweaveGateway()
.build()
.contract<ExampleContractState>(contract.txId())
.connect(wallet);
const contract2VM = WarpNodeFactory.fileCachedBased(arweave, cacheDir)
.useArweaveGateway()
.build()
.contract<ExampleContractState>(contract.txId())
.connect(wallet);
expect((await contract2.readState()).state.counter).toEqual(559);
expect((await contract2VM.readState()).state.counter).toEqual(559);
await contract.writeInteraction({ function: 'add' });
await mineBlock(arweave);
await contract.writeInteraction({ function: 'add' });
await mineBlock(arweave);
expect((await contract2.readState()).state.counter).toEqual(561);
expect((await contract2VM.readState()).state.counter).toEqual(561);
});
it('should properly read state with another fresh client', async () => {
const contract3 = WarpNodeFactory.fileCachedBased(arweave, cacheDir)
.useArweaveGateway()
.build()
.contract<ExampleContractState>(contract.txId())
.connect(wallet);
const contract3VM = WarpNodeFactory.fileCachedBased(arweave, cacheDir)
.useArweaveGateway()
.build()
.contract<ExampleContractState>(contract.txId())
.setEvaluationOptions({
useVM2: true
})
.connect(wallet);
expect((await contract3.readState()).state.counter).toEqual(561);
expect((await contract3VM.readState()).state.counter).toEqual(561);
await contract.writeInteraction({ function: 'add' });
await mineBlock(arweave);
await contract.writeInteraction({ function: 'add' });
await mineBlock(arweave);
expect((await contract3.readState()).state.counter).toEqual(563);
expect((await contract3VM.readState()).state.counter).toEqual(563);
});
it('should properly eval state for missing interactions', async () => {
await contract.writeInteraction({ function: 'add' });
await mineBlock(arweave);
await contract.writeInteraction({ function: 'add' });
await mineBlock(arweave);
const contract4 = WarpNodeFactory.fileCachedBased(arweave, cacheDir)
.useArweaveGateway()
.build()
.contract<ExampleContractState>(contract.txId())
.connect(wallet);
const contract4VM = WarpNodeFactory.fileCachedBased(arweave, cacheDir)
.useArweaveGateway()
.build()
.contract<ExampleContractState>(contract.txId())
.setEvaluationOptions({
useVM2: true
})
.connect(wallet);
expect((await contract4.readState()).state.counter).toEqual(565);
expect((await contract4VM.readState()).state.counter).toEqual(565);
expect((await contract.readState()).state.counter).toEqual(565);
expect((await contractVM.readState()).state.counter).toEqual(565);
});
function removeCacheDir() {
if (fs.existsSync(cacheDir)) {
fs.rmSync(cacheDir, { recursive: true });
}
}
});

View File

@@ -3,7 +3,7 @@ import fs from 'fs';
import ArLocal from 'arlocal';
import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import { Contract, LoggerFactory, Warp, WarpNodeFactory, timeout } from '@warp';
import { Contract, LoggerFactory, timeout, Warp, WarpFactory } from '@warp';
import path from 'path';
import { addFunds, mineBlock } from '../_helpers';
@@ -35,7 +35,7 @@ describe('Testing the Warp client', () => {
LoggerFactory.INST.logLevel('error');
warp = WarpNodeFactory.forTesting(arweave);
warp = WarpFactory.forTesting(arweave);
wallet = await arweave.wallets.generate();
await addFunds(arweave, wallet);

View File

@@ -1,319 +0,0 @@
import fs from 'fs';
import ArLocal from 'arlocal';
import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import { Contract, LoggerFactory, Warp, WarpNodeFactory } from '@warp';
import path from 'path';
import { addFunds, mineBlock } from '../_helpers';
import knex, { Knex } from 'knex';
interface ExampleContractState {
counter: number;
}
async function getWarp(arweave: Arweave, knexConfig: Knex<any, unknown[]>) {
return (await WarpNodeFactory.knexCachedBased(arweave, knexConfig)).useArweaveGateway().build();
}
/**
* This integration test should verify whether the basic functions of the Warp client
* work properly when Knex cache is being used.
*/
describe('Testing the Warp client', () => {
let contractSrc: string;
let initialState: string;
let wallet: JWKInterface;
let arweave: Arweave;
let arlocal: ArLocal;
let warp: Warp;
let contract_1: Contract<ExampleContractState>;
let contract_1VM: Contract<ExampleContractState>;
let contract_2: Contract<ExampleContractState>;
let contract_2VM: Contract<ExampleContractState>;
const cacheDir = path.join(__dirname, 'db');
const knexConfig = knex({
client: 'sqlite3',
connection: {
filename: `${cacheDir}/db.sqlite`
},
useNullAsDefault: true
});
const knexConfig2 = knex({
client: 'sqlite3',
connection: {
filename: `${cacheDir}/db-manual.sqlite`
},
useNullAsDefault: true
});
beforeAll(async () => {
removeCacheDir();
fs.mkdirSync(cacheDir);
// note: each tests suit (i.e. file with tests that Jest is running concurrently
// with another files has to have ArLocal set to a different port!)
arlocal = new ArLocal(1780, false);
await arlocal.start();
arweave = Arweave.init({
host: 'localhost',
port: 1780,
protocol: 'http'
});
LoggerFactory.INST.logLevel('error');
warp = await getWarp(arweave, knexConfig);
wallet = await arweave.wallets.generate();
await addFunds(arweave, wallet);
contractSrc = fs.readFileSync(path.join(__dirname, '../data/example-contract.js'), 'utf8');
initialState = fs.readFileSync(path.join(__dirname, '../data/example-contract-state.json'), 'utf8');
// deploying contract using the new SDK.
const { contractTxId } = await warp.createContract.deploy({
wallet,
initState: initialState,
src: contractSrc
});
const { contractTxId: contractTxId2 } = await warp.createContract.deploy({
wallet,
initState: '{"counter": 100}',
src: contractSrc
});
contract_1 = warp.contract<ExampleContractState>(contractTxId).connect(wallet);
contract_1VM = warp
.contract<ExampleContractState>(contractTxId)
.setEvaluationOptions({ useVM2: true })
.connect(wallet);
contract_2 = warp.contract<ExampleContractState>(contractTxId2).connect(wallet);
contract_2VM = warp
.contract<ExampleContractState>(contractTxId2)
.setEvaluationOptions({ useVM2: true })
.connect(wallet);
await mineBlock(arweave);
});
afterAll(async () => {
await arlocal.stop();
await knexConfig.destroy();
await knexConfig2.destroy();
removeCacheDir();
});
it('should properly deploy contract with initial state', async () => {
expect((await contract_1.readState()).state.counter).toEqual(555);
expect((await contract_1VM.readState()).state.counter).toEqual(555);
expect((await contract_2.readState()).state.counter).toEqual(100);
expect((await contract_2VM.readState()).state.counter).toEqual(100);
});
it('should properly add new interaction', async () => {
await contract_1.writeInteraction({ function: 'add' });
await contract_2.writeInteraction({ function: 'add' });
await contract_2.writeInteraction({ function: 'add' });
await mineBlock(arweave);
expect((await contract_1.readState()).state.counter).toEqual(556);
expect((await contract_1VM.readState()).state.counter).toEqual(556);
expect((await contract_2.readState()).state.counter).toEqual(102);
expect((await contract_2VM.readState()).state.counter).toEqual(102);
expect(await cachedStates(knexConfig)).toEqual(2);
});
it('should properly add another interactions', async () => {
await contract_1.writeInteraction({ function: 'add' });
await contract_2.writeInteraction({ function: 'add' });
await mineBlock(arweave);
await contract_1.writeInteraction({ function: 'add' });
await contract_2.writeInteraction({ function: 'add' });
await mineBlock(arweave);
await contract_1.writeInteraction({ function: 'add' });
await contract_2.writeInteraction({ function: 'add' });
await mineBlock(arweave);
expect((await contract_1.readState()).state.counter).toEqual(559);
expect((await contract_1VM.readState()).state.counter).toEqual(559);
expect((await contract_2.readState()).state.counter).toEqual(105);
expect((await contract_2VM.readState()).state.counter).toEqual(105);
expect(await cachedStates(knexConfig)).toEqual(4);
});
it('should properly view contract state', async () => {
const interactionResult = await contract_1.viewState<unknown, number>({ function: 'value' });
const interactionResultVM = await contract_1VM.viewState<unknown, number>({ function: 'value' });
expect(interactionResultVM.result).toEqual(559);
const interactionResult2 = await contract_2.viewState<unknown, number>({ function: 'value' });
const interactionResult2VM = await contract_2.viewState<unknown, number>({ function: 'value' });
expect(interactionResult2VM.result).toEqual(105);
expect(await cachedStates(knexConfig)).toEqual(4);
});
it('should properly read state with a fresh client', async () => {
const contract_1_2 = (await getWarp(arweave, knexConfig))
.contract<ExampleContractState>(contract_1.txId())
.connect(wallet);
const contract_1_2VM = (await getWarp(arweave, knexConfig))
.contract<ExampleContractState>(contract_1.txId())
.setEvaluationOptions({ useVM2: true })
.connect(wallet);
expect((await contract_1_2.readState()).state.counter).toEqual(559);
expect((await contract_1_2VM.readState()).state.counter).toEqual(559);
const contract_2_2 = (await getWarp(arweave, knexConfig))
.contract<ExampleContractState>(contract_2.txId())
.connect(wallet);
const contract_2_2VM = (await getWarp(arweave, knexConfig))
.contract<ExampleContractState>(contract_2.txId())
.setEvaluationOptions({ useVM2: true })
.connect(wallet);
expect((await contract_2_2.readState()).state.counter).toEqual(105);
expect((await contract_2_2VM.readState()).state.counter).toEqual(105);
await contract_1.writeInteraction({ function: 'add' });
await contract_2.writeInteraction({ function: 'add' });
await mineBlock(arweave);
await contract_1.writeInteraction({ function: 'add' });
await contract_2.writeInteraction({ function: 'add' });
await mineBlock(arweave);
expect((await contract_1_2.readState()).state.counter).toEqual(561);
expect((await contract_1_2VM.readState()).state.counter).toEqual(561);
expect((await contract_2_2.readState()).state.counter).toEqual(107);
expect((await contract_2_2VM.readState()).state.counter).toEqual(107);
expect(await cachedStates(knexConfig)).toEqual(6);
});
it('should properly read state with another fresh client', async () => {
const contract_1_3 = (await getWarp(arweave, knexConfig))
.contract<ExampleContractState>(contract_1.txId())
.connect(wallet);
const contract_1_3VM = (await getWarp(arweave, knexConfig))
.contract<ExampleContractState>(contract_1.txId())
.setEvaluationOptions({ useVM2: true })
.connect(wallet);
const contract_2_3 = (await getWarp(arweave, knexConfig))
.contract<ExampleContractState>(contract_2.txId())
.connect(wallet);
const contract_2_3VM = (await getWarp(arweave, knexConfig))
.contract<ExampleContractState>(contract_2.txId())
.setEvaluationOptions({ useVM2: true })
.connect(wallet);
expect((await contract_1_3.readState()).state.counter).toEqual(561);
expect((await contract_1_3VM.readState()).state.counter).toEqual(561);
expect((await contract_2_3.readState()).state.counter).toEqual(107);
expect((await contract_2_3VM.readState()).state.counter).toEqual(107);
expect(await cachedStates(knexConfig)).toEqual(6);
await contract_1.writeInteraction({ function: 'add' });
await contract_2.writeInteraction({ function: 'add' });
await mineBlock(arweave);
await contract_1.writeInteraction({ function: 'add' });
await contract_2.writeInteraction({ function: 'add' });
await mineBlock(arweave);
expect((await contract_1_3.readState()).state.counter).toEqual(563);
expect((await contract_1_3VM.readState()).state.counter).toEqual(563);
expect((await contract_2_3.readState()).state.counter).toEqual(109);
expect((await contract_2_3VM.readState()).state.counter).toEqual(109);
expect(await cachedStates(knexConfig)).toEqual(8);
});
it('should properly eval state for missing interactions', async () => {
await contract_1.writeInteraction({ function: 'add' });
await contract_2.writeInteraction({ function: 'add' });
await mineBlock(arweave);
await contract_1.writeInteraction({ function: 'add' });
await contract_2.writeInteraction({ function: 'add' });
await mineBlock(arweave);
const contract_1_4 = (await getWarp(arweave, knexConfig))
.contract<ExampleContractState>(contract_1.txId())
.connect(wallet);
const contract_1_4VM = (await getWarp(arweave, knexConfig))
.contract<ExampleContractState>(contract_1.txId())
.setEvaluationOptions({ useVM2: true })
.connect(wallet);
const contract_2_4 = (await getWarp(arweave, knexConfig))
.contract<ExampleContractState>(contract_2.txId())
.connect(wallet);
const contract_2_4VM = (await getWarp(arweave, knexConfig))
.contract<ExampleContractState>(contract_2.txId())
.setEvaluationOptions({ useVM2: true })
.connect(wallet);
expect((await contract_1.readState()).state.counter).toEqual(565);
expect((await contract_1VM.readState()).state.counter).toEqual(565);
expect((await contract_1_4.readState()).state.counter).toEqual(565);
expect((await contract_1_4VM.readState()).state.counter).toEqual(565);
expect((await contract_2.readState()).state.counter).toEqual(111);
expect((await contract_2VM.readState()).state.counter).toEqual(111);
expect((await contract_2_4.readState()).state.counter).toEqual(111);
expect((await contract_2_4VM.readState()).state.counter).toEqual(111);
expect(await cachedStates(knexConfig)).toEqual(10);
});
it('should allow to manually flush cache', async () => {
const warp = await getWarp(arweave, knexConfig2);
const contract = warp
.contract<ExampleContractState>(contract_1.txId())
.setEvaluationOptions({
manualCacheFlush: true
})
.connect(wallet);
const contractVM = warp
.contract<ExampleContractState>(contract_1.txId())
.setEvaluationOptions({
useVM2: true,
manualCacheFlush: true
})
.connect(wallet);
await contract.writeInteraction({ function: 'add' });
await mineBlock(arweave);
await contract.writeInteraction({ function: 'add' });
await mineBlock(arweave);
await contract.writeInteraction({ function: 'add' });
await mineBlock(arweave);
expect((await contract.readState()).state.counter).toEqual(568);
expect((await contractVM.readState()).state.counter).toEqual(568);
expect(await cachedStates(knexConfig2)).toEqual(0);
await warp.flushCache();
expect(await cachedStates(knexConfig2)).toEqual(1);
await contract.writeInteraction({ function: 'add' });
await mineBlock(arweave);
await contract.writeInteraction({ function: 'add' });
await mineBlock(arweave);
await contract.writeInteraction({ function: 'add' });
await mineBlock(arweave);
expect((await contract.readState()).state.counter).toEqual(571);
expect((await contractVM.readState()).state.counter).toEqual(571);
expect(await cachedStates(knexConfig2)).toEqual(1);
await warp.flushCache();
expect(await cachedStates(knexConfig2)).toEqual(2);
});
async function cachedStates(db: Knex): Promise<number> {
const result = await db.raw('select count(*) as cached from states');
return result[0].cached;
}
function removeCacheDir() {
if (fs.existsSync(cacheDir)) {
fs.rmSync(cacheDir, { recursive: true });
}
}
});

View File

@@ -1,141 +0,0 @@
import fs from 'fs';
import ArLocal from 'arlocal';
import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import { LoggerFactory, PstContract, PstState, Warp, WarpNodeFactory } from '@warp';
import path from 'path';
import { addFunds, mineBlock } from '../_helpers';
describe('Testing the Profit Sharing Token', () => {
let contractSrc: string;
let wallet: JWKInterface;
let walletAddress: string;
let initialState: PstState;
let arweave: Arweave;
let arlocal: ArLocal;
let warp: Warp;
let pst: PstContract;
let contractTxId;
const cacheDir = path.join(__dirname, 'cache-pst');
beforeAll(async () => {
removeCacheDir();
fs.mkdirSync(cacheDir);
// note: each tests suit (i.e. file with tests that Jest is running concurrently
// with another files has to have ArLocal set to a different port!)
arlocal = new ArLocal(1680, false);
await arlocal.start();
arweave = Arweave.init({
host: 'localhost',
port: 1680,
protocol: 'http'
});
LoggerFactory.INST.logLevel('error');
warp = WarpNodeFactory.fileCachedBased(arweave, cacheDir).useArweaveGateway().build();
wallet = await arweave.wallets.generate();
await addFunds(arweave, wallet);
walletAddress = await arweave.wallets.jwkToAddress(wallet);
contractSrc = fs.readFileSync(path.join(__dirname, '../data/token-pst.js'), 'utf8');
const stateFromFile: PstState = JSON.parse(fs.readFileSync(path.join(__dirname, '../data/token-pst.json'), 'utf8'));
initialState = {
...stateFromFile,
...{
owner: walletAddress,
balances: {
...stateFromFile.balances,
[walletAddress]: 555669
}
}
};
// deploying contract using the new SDK.
({ contractTxId: contractTxId } = await warp.createContract.deploy({
wallet,
initState: JSON.stringify(initialState),
src: contractSrc
}));
// connecting to the PST contract
pst = warp.pst(contractTxId);
// connecting wallet to the PST contract
pst.connect(wallet);
await mineBlock(arweave);
});
afterAll(async () => {
await arlocal.stop();
removeCacheDir();
});
it('should read pst state and balance data', async () => {
expect(await pst.currentState()).toEqual(initialState);
expect((await pst.currentBalance('uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M')).balance).toEqual(10000000);
expect((await pst.currentBalance('33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA')).balance).toEqual(23111222);
expect((await pst.currentBalance(walletAddress)).balance).toEqual(555669);
});
it('should properly transfer tokens', async () => {
await pst.transfer({
target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M',
qty: 555
});
await mineBlock(arweave);
expect((await pst.currentState()).balances[walletAddress]).toEqual(555669 - 555);
expect((await pst.currentState()).balances['uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M']).toEqual(10000000 + 555);
});
it('should properly view contract state', async () => {
const result = await pst.currentBalance('uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M');
expect(result.balance).toEqual(10000000 + 555);
expect(result.ticker).toEqual('EXAMPLE_PST_TOKEN');
expect(result.target).toEqual('uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M');
});
it("should properly evolve contract's source code", async () => {
expect((await pst.currentState()).balances[walletAddress]).toEqual(555114);
const newSource = fs.readFileSync(path.join(__dirname, '../data/token-evolve.js'), 'utf8');
const newSrcTxId = await pst.save({ src: newSource });
await mineBlock(arweave);
await pst.evolve(newSrcTxId);
await mineBlock(arweave);
// note: the evolved balance always adds 555 to the result
expect((await pst.currentBalance(walletAddress)).balance).toEqual(555114 + 555);
});
it('should load updated source code', async () => {
const warp2 = WarpNodeFactory.fileCachedBased(arweave, cacheDir).useArweaveGateway().build();
// connecting to the PST contract
pst = warp2.pst(contractTxId);
// connecting wallet to the PST contract
pst.connect(wallet);
expect((await pst.currentBalance(walletAddress)).balance).toEqual(555114 + 555);
});
function removeCacheDir() {
if (fs.existsSync(cacheDir)) {
fs.rmSync(cacheDir, { recursive: true });
}
}
});

View File

@@ -3,7 +3,7 @@ import fs from 'fs';
import ArLocal from 'arlocal';
import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import { InteractionResult, LoggerFactory, PstContract, PstState, Warp, WarpNodeFactory } from '@warp';
import { InteractionResult, LoggerFactory, PstContract, PstState, Warp, WarpFactory } from '@warp';
import path from 'path';
import { addFunds, mineBlock } from '../_helpers';
@@ -35,7 +35,7 @@ describe('Testing the Profit Sharing Token', () => {
LoggerFactory.INST.logLevel('error');
warp = WarpNodeFactory.forTesting(arweave);
warp = WarpFactory.forTesting(arweave);
wallet = await arweave.wallets.generate();
await addFunds(arweave, wallet);

View File

@@ -3,7 +3,7 @@ import fs from 'fs';
import ArLocal from 'arlocal';
import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import { Contract, LoggerFactory, Warp, WarpNodeFactory } from '@warp';
import { Contract, LoggerFactory, Warp, WarpFactory } from '@warp';
import path from 'path';
import { addFunds, mineBlock } from '../_helpers';
@@ -32,7 +32,7 @@ describe('Testing the Warp client', () => {
LoggerFactory.INST.logLevel('error');
warp = WarpNodeFactory.forTesting(arweave);
warp = WarpFactory.forTesting(arweave);
wallet = await arweave.wallets.generate();
await addFunds(arweave, wallet);

View File

@@ -5,6 +5,7 @@ import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import {
ArweaveGatewayInteractionsLoader,
defaultCacheOptions,
EvaluationOptions,
GQLEdgeInterface,
InteractionsLoader,
@@ -13,7 +14,7 @@ import {
PstContract,
PstState,
Warp,
WarpNodeFactory
WarpFactory
} from '@warp';
import path from 'path';
import { addFunds, mineBlock } from '../_helpers';
@@ -56,7 +57,13 @@ describe('Testing the Profit Sharing Token', () => {
loader = new VrfDecorator(arweave);
LoggerFactory.INST.logLevel('error');
warp = WarpNodeFactory.memCachedBased(arweave).useArweaveGateway().setInteractionsLoader(loader).build();
warp = WarpFactory.levelDbCached(arweave, {
...defaultCacheOptions,
inMemory: true
})
.useArweaveGateway()
.setInteractionsLoader(loader)
.build();
wallet = await arweave.wallets.generate();
await addFunds(arweave, wallet);

View File

@@ -2,6 +2,11 @@ export async function handle(state, action) {
if (state.counter === undefined) {
state.counter = 0;
}
if (action.input.function === 'div') {
state.counter = state.counter / 2;
return { state };
}
if (action.input.function === 'add') {
state.counter++;
return { state };

View File

@@ -75,6 +75,8 @@ export async function handle(state, action) {
// we need to refresh the state here manually.
state = await SmartWeave.contracts.refreshState();
console.log('State after refresh', state);
if (result.state.counter > 2059) {
state.counter -= result.state.counter;
} else {

View File

@@ -4,7 +4,7 @@ import fs from 'fs';
import ArLocal from 'arlocal';
import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import { Contract, LoggerFactory, Warp, WarpNodeFactory } from '@warp';
import { Contract, LoggerFactory, Warp, WarpFactory } from '@warp';
import path from 'path';
import { addFunds, mineBlock } from '../_helpers';
@@ -73,7 +73,7 @@ describe('Testing internal writes', () => {
});
async function deployContracts() {
warp = WarpNodeFactory.forTesting(arweave);
warp = WarpFactory.forTesting(arweave);
wallet = await arweave.wallets.generate();
await addFunds(arweave, wallet);
@@ -177,11 +177,11 @@ describe('Testing internal writes', () => {
});
it('should properly evaluate state with a new client', async () => {
const contractA2 = WarpNodeFactory.forTesting(arweave)
const contractA2 = WarpFactory.forTesting(arweave)
.contract<any>(contractATxId)
.setEvaluationOptions({ internalWrites: true })
.connect(wallet);
const contractB2 = WarpNodeFactory.forTesting(arweave)
const contractB2 = WarpFactory.forTesting(arweave)
.contract<any>(contractBTxId)
.setEvaluationOptions({ internalWrites: true })
.connect(wallet);
@@ -216,12 +216,12 @@ describe('Testing internal writes', () => {
expect((await contractB.readState()).state.counter).toEqual(2060);
});
it('should properly evaluate state with a new client', async () => {
const contractA2 = WarpNodeFactory.forTesting(arweave)
xit('should properly evaluate state with a new client', async () => {
const contractA2 = WarpFactory.forTesting(arweave)
.contract<any>(contractATxId)
.setEvaluationOptions({ internalWrites: true })
.connect(wallet);
const contractB2 = WarpNodeFactory.forTesting(arweave)
const contractB2 = WarpFactory.forTesting(arweave)
.contract<any>(contractBTxId)
.setEvaluationOptions({ internalWrites: true })
.connect(wallet);
@@ -280,11 +280,11 @@ describe('Testing internal writes', () => {
});
xit('should properly evaluate state with a new client', async () => {
const contractA2 = WarpNodeFactory.forTesting(arweave)
const contractA2 = WarpFactory.forTesting(arweave)
.contract<any>(contractATxId)
.setEvaluationOptions({ internalWrites: true })
.connect(wallet);
const contractB2 = WarpNodeFactory.forTesting(arweave)
const contractB2 = WarpFactory.forTesting(arweave)
.contract<any>(contractBTxId)
.setEvaluationOptions({ internalWrites: true })
.connect(wallet);
@@ -339,11 +339,11 @@ describe('Testing internal writes', () => {
});
it('should properly evaluate state with a new client', async () => {
const contractA2 = WarpNodeFactory.forTesting(arweave)
const contractA2 = WarpFactory.forTesting(arweave)
.contract<any>(contractATxId)
.setEvaluationOptions({ internalWrites: true })
.connect(wallet);
const contractB2 = WarpNodeFactory.forTesting(arweave)
const contractB2 = WarpFactory.forTesting(arweave)
.contract<any>(contractBTxId)
.setEvaluationOptions({ internalWrites: true })
.connect(wallet);

View File

@@ -4,7 +4,7 @@ import fs from 'fs';
import ArLocal from 'arlocal';
import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import { Contract, LoggerFactory, Warp, WarpNodeFactory } from '@warp';
import { Contract, LoggerFactory, Warp, WarpFactory } from '@warp';
import path from 'path';
import { addFunds, mineBlock } from '../_helpers';
@@ -76,7 +76,7 @@ describe('Testing internal writes', () => {
});
async function deployContracts() {
warp = WarpNodeFactory.forTesting(arweave);
warp = WarpFactory.forTesting(arweave);
wallet = await arweave.wallets.generate();
await addFunds(arweave, wallet);
@@ -253,6 +253,7 @@ describe('Testing internal writes', () => {
await mineBlock(arweave);
await calleeContract.writeInteraction({ function: 'add' });
await mineBlock(arweave);
expect((await calleeContract.readState()).state.counter).toEqual(634);
expect((await calleeContractVM.readState()).state.counter).toEqual(634);
});
@@ -263,12 +264,12 @@ describe('Testing internal writes', () => {
});
it('should properly evaluate state again with a new client', async () => {
const calleeContract2 = WarpNodeFactory.forTesting(arweave)
const calleeContract2 = WarpFactory.forTesting(arweave)
.contract<ExampleContractState>(calleeTxId)
.setEvaluationOptions({
internalWrites: true
});
const calleeContract2VM = WarpNodeFactory.forTesting(arweave)
const calleeContract2VM = WarpFactory.forTesting(arweave)
.contract<ExampleContractState>(calleeTxId)
.setEvaluationOptions({
internalWrites: true,

View File

@@ -4,7 +4,7 @@ import fs from 'fs';
import ArLocal from 'arlocal';
import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import { Contract, LoggerFactory, Warp, WarpNodeFactory } from '@warp';
import { Contract, LoggerFactory, Warp, WarpFactory } from '@warp';
import path from 'path';
import { TsLogFactory } from '../../../logging/node/TsLogFactory';
import { addFunds, mineBlock } from '../_helpers';
@@ -88,7 +88,7 @@ describe('Testing internal writes', () => {
});
async function deployContracts() {
warp = WarpNodeFactory.forTesting(arweave);
warp = WarpFactory.forTesting(arweave);
wallet = await arweave.wallets.generate();
await addFunds(arweave, wallet);

View File

@@ -4,7 +4,7 @@ import fs from 'fs';
import ArLocal from 'arlocal';
import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import { Contract, LoggerFactory, Warp, WarpNodeFactory } from '@warp';
import { Contract, LoggerFactory, Warp, WarpFactory } from '@warp';
import path from 'path';
import { TsLogFactory } from '../../../logging/node/TsLogFactory';
import { addFunds, mineBlock } from '../_helpers';
@@ -89,7 +89,7 @@ describe('Testing internal writes', () => {
});
async function deployContracts() {
warp = WarpNodeFactory.forTesting(arweave);
warp = WarpFactory.forTesting(arweave);
wallet = await arweave.wallets.generate();
await addFunds(arweave, wallet);
@@ -208,11 +208,12 @@ describe('Testing internal writes', () => {
});
it('should properly evaluate state with a new client', async () => {
const contractB2 = WarpNodeFactory.forTesting(arweave)
const contractB2 = WarpFactory.forTesting(arweave)
.contract<any>(contractBTxId)
.setEvaluationOptions({ internalWrites: true })
.connect(wallet);
const contractC2 = WarpNodeFactory.forTesting(arweave)
const contractC2 = WarpFactory.forTesting(arweave)
.contract<any>(contractCTxId)
.setEvaluationOptions({ internalWrites: true })
.connect(wallet);

View File

@@ -4,7 +4,7 @@ import fs from 'fs';
import ArLocal from 'arlocal';
import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import { Contract, LoggerFactory, Warp, WarpNodeFactory } from '@warp';
import { Contract, LoggerFactory, Warp, WarpFactory } from '@warp';
import path from 'path';
import { TsLogFactory } from '../../../logging/node/TsLogFactory';
import { addFunds, mineBlock } from '../_helpers';
@@ -83,7 +83,7 @@ describe('Testing internal writes', () => {
});
async function deployContracts() {
warp = WarpNodeFactory.forTesting(arweave);
warp = WarpFactory.forTesting(arweave);
wallet = await arweave.wallets.generate();
await addFunds(arweave, wallet);
@@ -340,11 +340,11 @@ describe('Testing internal writes', () => {
});
it('should properly evaluate state with a new client', async () => {
const contractB2 = WarpNodeFactory.forTesting(arweave)
const contractB2 = WarpFactory.forTesting(arweave)
.contract<any>(contractBTxId)
.setEvaluationOptions({ internalWrites: true })
.connect(wallet);
const contractC2 = WarpNodeFactory.forTesting(arweave)
const contractC2 = WarpFactory.forTesting(arweave)
.contract<any>(contractCTxId)
.setEvaluationOptions({ internalWrites: true })
.connect(wallet);

View File

@@ -4,7 +4,7 @@ import fs from 'fs';
import ArLocal from 'arlocal';
import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import { Contract, LoggerFactory, Warp, WarpNodeFactory } from '@warp';
import { Contract, LoggerFactory, Warp, WarpFactory } from '@warp';
import path from 'path';
import { TsLogFactory } from '../../../logging/node/TsLogFactory';
import { addFunds, mineBlock } from '../_helpers';
@@ -57,7 +57,7 @@ describe('Testing internal writes', () => {
});
async function deployContracts() {
warp = WarpNodeFactory.forTesting(arweave);
warp = WarpFactory.forTesting(arweave);
wallet = await arweave.wallets.generate();
await addFunds(arweave, wallet);
@@ -71,7 +71,6 @@ describe('Testing internal writes', () => {
'utf8'
);
console.log('wallet address', walletAddress);
({ contractTxId: tokenContractTxId } = await warp.createContract.deploy({
wallet,
initState: JSON.stringify({

View File

@@ -3,7 +3,7 @@ import fs from 'fs';
import ArLocal from 'arlocal';
import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import { Contract, getTag, LoggerFactory, Warp, WarpNodeFactory, SmartWeaveTags } from '@warp';
import { Contract, getTag, LoggerFactory, Warp, WarpFactory, SmartWeaveTags } from '@warp';
import path from 'path';
import { addFunds, mineBlock } from '../_helpers';
@@ -39,7 +39,7 @@ describe('Testing the Warp client for AssemblyScript WASM contract', () => {
LoggerFactory.INST.logLevel('error');
warp = WarpNodeFactory.forTesting(arweave);
warp = WarpFactory.forTesting(arweave);
wallet = await arweave.wallets.generate();
await addFunds(arweave, wallet);

View File

@@ -3,7 +3,7 @@ import fs from 'fs';
import ArLocal from 'arlocal';
import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import { getTag, LoggerFactory, PstContract, PstState, Warp, WarpNodeFactory, SmartWeaveTags } from '@warp';
import { getTag, LoggerFactory, PstContract, PstState, SmartWeaveTags, Warp, WarpFactory } from '@warp';
import path from 'path';
import { addFunds, mineBlock } from '../_helpers';
@@ -37,7 +37,7 @@ describe('Testing the Go WASM Profit Sharing Token', () => {
LoggerFactory.INST.logLevel('error');
warp = WarpNodeFactory.forTesting(arweave);
warp = WarpFactory.forTesting(arweave);
wallet = await arweave.wallets.generate();
await addFunds(arweave, wallet);

View File

@@ -3,16 +3,7 @@ import fs from 'fs';
import ArLocal from 'arlocal';
import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import {
ArweaveWrapper,
getTag,
LoggerFactory,
PstContract,
PstState,
Warp,
WarpNodeFactory,
SmartWeaveTags
} from '@warp';
import { ArweaveWrapper, getTag, LoggerFactory, PstContract, PstState, SmartWeaveTags, Warp, WarpFactory } from '@warp';
import path from 'path';
import { addFunds, mineBlock } from '../_helpers';
import { WasmSrc } from '../../../core/modules/impl/wasm/WasmSrc';
@@ -51,7 +42,7 @@ describe('Testing the Rust WASM Profit Sharing Token', () => {
LoggerFactory.INST.logLevel('error');
warp = WarpNodeFactory.forTesting(arweave);
warp = WarpFactory.forTesting(arweave);
wallet = await arweave.wallets.generate();
await addFunds(arweave, wallet);

View File

@@ -3,7 +3,7 @@ import fs from 'fs';
import path from 'path';
import { interactRead, readContract } from 'smartweave';
import Arweave from 'arweave';
import { LoggerFactory, WarpNodeFactory, WarpWebFactory, SourceType } from '@warp';
import { defaultCacheOptions, LoggerFactory, SourceType, WarpFactory } from '@warp';
const stringify = require('safe-stable-stringify');
@@ -50,7 +50,10 @@ describe.each(chunked)('v1 compare.suite %#', (contracts: string[]) => {
console.log('readState', contractTxId);
try {
console.log = function () {}; // to hide any logs from contracts...
const result2 = await WarpNodeFactory.memCachedBased(arweave, 1)
const result2 = await WarpFactory.levelDbCached(arweave, {
...defaultCacheOptions,
inMemory: true
})
.useWarpGateway(null, SourceType.ARWEAVE)
.build()
.contract(contractTxId)
@@ -79,7 +82,10 @@ describe.each(chunkedVm)('v1 compare.suite (VM2) %#', (contracts: string[]) => {
.readFileSync(path.join(__dirname, 'test-cases', 'contracts', `${contractTxId}.json`), 'utf-8')
.trim();
console.log('readState', contractTxId);
const result2 = await WarpNodeFactory.memCachedBased(arweave, 1)
const result2 = await WarpFactory.levelDbCached(arweave, {
...defaultCacheOptions,
inMemory: true
})
.useWarpGateway(null, SourceType.ARWEAVE)
.build()
.contract(contractTxId)
@@ -104,12 +110,20 @@ describe.each(chunkedGw)('gateways compare.suite %#', (contracts: string[]) => {
async (contractTxId: string) => {
const blockHeight = 855134;
console.log('readState Warp Gateway', contractTxId);
const warpR = WarpWebFactory.memCachedBased(arweave, 1).useWarpGateway(null, SourceType.ARWEAVE).build();
const warpR = await WarpFactory.levelDbCached(arweave, {
...defaultCacheOptions,
inMemory: true
})
.useWarpGateway(null, SourceType.ARWEAVE)
.build();
const result = await warpR.contract(contractTxId).readState(blockHeight);
const resultString = stringify(result.state).trim();
console.log('readState Arweave Gateway', contractTxId);
const result2 = await WarpNodeFactory.memCachedBased(arweave, 1)
const result2 = await WarpFactory.levelDbCached(arweave, {
...defaultCacheOptions,
inMemory: true
})
.useArweaveGateway()
.build()
.contract(contractTxId)
@@ -128,7 +142,10 @@ describe('readState', () => {
const result = await readContract(arweave, contractTxId, blockHeight);
const resultString = stringify(result).trim();
const result2 = await WarpNodeFactory.memCachedBased(arweave, 1)
const result2 = await WarpFactory.levelDbCached(arweave, {
...defaultCacheOptions,
inMemory: true
})
.useWarpGateway(null, SourceType.ARWEAVE)
.build()
.contract(contractTxId)
@@ -149,7 +166,10 @@ describe('readState', () => {
target: '6Z-ifqgVi1jOwMvSNwKWs6ewUEQ0gU9eo4aHYC3rN1M'
});
const v2Result = await WarpNodeFactory.memCachedBased(arweave, 1)
const v2Result = await WarpFactory.levelDbCached(arweave, {
...defaultCacheOptions,
inMemory: true
})
.useWarpGateway(null, SourceType.ARWEAVE)
.build()
.contract(contractTxId)

View File

@@ -1,5 +1,10 @@
import { LoggerFactory, WarpGatewayInteractionsLoader } from '@warp';
import { GQLEdgeInterface } from '../../legacy/gqlResult';
import {
GQLNodeInterface,
LexicographicalInteractionsSorter,
LoggerFactory,
WarpGatewayInteractionsLoader
} from '@warp';
import Arweave from 'arweave';
const responseData = {
paging: {
@@ -76,10 +81,11 @@ const responseDataPaging = {
LoggerFactory.INST.logLevel('error');
const sorter = new LexicographicalInteractionsSorter(Arweave.init({}));
const contractId = 'SJ3l7474UHh3Dw6dWVT1bzsJ-8JvOewtGoDdOecWIZo';
const fromBlockHeight = 600000;
const toBlockHeight = 655393;
const baseUrl = `http://baseUrl/gateway/interactions-sort-key?contractId=SJ3l7474UHh3Dw6dWVT1bzsJ-8JvOewtGoDdOecWIZo&from=600000&to=655393`;
const fromBlockHeight = sorter.generateLastSortKey(600000);
const toBlockHeight = sorter.generateLastSortKey(655393);
const baseUrl = `http://baseUrl/gateway/interactions-sort-key?contractId=SJ3l7474UHh3Dw6dWVT1bzsJ-8JvOewtGoDdOecWIZo&from=000000600000%2C9999999999999%2Czzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz&to=000000655393%2C9999999999999%2Czzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz`;
const fetchMock = jest
.spyOn(global, 'fetch')
.mockImplementation(
@@ -94,7 +100,7 @@ describe('WarpGatewayInteractionsLoader -> load', () => {
});
it('should return correct number of interactions', async () => {
const loader = new WarpGatewayInteractionsLoader('http://baseUrl');
const response: GQLEdgeInterface[] = await loader.load(contractId, fromBlockHeight, toBlockHeight);
const response: GQLNodeInterface[] = await loader.load(contractId, fromBlockHeight, toBlockHeight);
expect(fetchMock).toHaveBeenCalled();
expect(response.length).toEqual(2);
});

View File

@@ -19,7 +19,7 @@ describe('Sequencer', () => {
const wallet = JSON.parse(fs.readFileSync(path.join(__dirname, '/test-wallet.json')).toString());
function mapSorted(s: GQLEdgeInterface) {
return `[${s.node.id}] : ${s.sortKey}`;
return `[${s.node.id}] : ${s.node.sortKey}`;
}
it('should properly assign sequence for transactions within the same block', async () => {

View File

@@ -1,4 +1,4 @@
import { GQLEdgeInterface, TagsParser } from '@warp';
import { GQLNodeInterface, TagsParser } from '@warp';
describe('TagsParser', () => {
const sut = new TagsParser();
@@ -8,7 +8,6 @@ describe('TagsParser', () => {
it('should return input tag (1)', () => {
// given
const interactionTx = {
node: {
tags: [
{ name: 'foo', value: 'bar' },
{ name: 'wtf', value: 'omg' },
@@ -18,13 +17,12 @@ describe('TagsParser', () => {
{ name: 'Contract', value: 'contractTxId_2' },
{ name: 'Input', value: 'contractTxId2_value' }
]
}
};
// when
const input1 = sut.getInputTag(interactionTx as GQLEdgeInterface, 'contractTxId_1');
const input2 = sut.getInputTag(interactionTx as GQLEdgeInterface, 'contractTxId_2');
const allContracts = sut.getContractsWithInputs(interactionTx as GQLEdgeInterface);
const input1 = sut.getInputTag(interactionTx as GQLNodeInterface, 'contractTxId_1');
const input2 = sut.getInputTag(interactionTx as GQLNodeInterface, 'contractTxId_2');
const allContracts = sut.getContractsWithInputs(interactionTx as GQLNodeInterface);
// then
expect(input1.value).toEqual('contractTxId1_value');
@@ -40,20 +38,18 @@ describe('TagsParser', () => {
it('should return input tag (2)', () => {
// given
const interactionTx = {
node: {
tags: [
{ name: 'Contract', value: 'contractTxId_1' },
{ name: 'Input', value: 'contractTxId11_value' },
{ name: 'Contract', value: 'contractTxId_2' },
{ name: 'Input', value: 'contractTxId22_value' }
]
}
};
// when
const input1 = sut.getInputTag(interactionTx as GQLEdgeInterface, 'contractTxId_1');
const input2 = sut.getInputTag(interactionTx as GQLEdgeInterface, 'contractTxId_2');
const allContracts = sut.getContractsWithInputs(interactionTx as GQLEdgeInterface);
const input1 = sut.getInputTag(interactionTx as GQLNodeInterface, 'contractTxId_1');
const input2 = sut.getInputTag(interactionTx as GQLNodeInterface, 'contractTxId_2');
const allContracts = sut.getContractsWithInputs(interactionTx as GQLNodeInterface);
// then
expect(input1.value).toEqual('contractTxId11_value');
@@ -69,7 +65,6 @@ describe('TagsParser', () => {
it('should return input tag (3)', () => {
// given
const interactionTx = {
node: {
tags: [
{ name: 'Contract', value: 'contractTxId_2' },
{ name: 'Input', value: 'contractTxId22_value' },
@@ -81,15 +76,14 @@ describe('TagsParser', () => {
{ name: 'Contract', value: 'contractTxId_4' },
{ name: 'Input', value: 'contractTxId44_value' }
]
}
};
// when
const input1 = sut.getInputTag(interactionTx as GQLEdgeInterface, 'contractTxId_1');
const input2 = sut.getInputTag(interactionTx as GQLEdgeInterface, 'contractTxId_2');
const input3 = sut.getInputTag(interactionTx as GQLEdgeInterface, 'contractTxId_3');
const input4 = sut.getInputTag(interactionTx as GQLEdgeInterface, 'contractTxId_4');
const allContracts = sut.getContractsWithInputs(interactionTx as GQLEdgeInterface);
const input1 = sut.getInputTag(interactionTx as GQLNodeInterface, 'contractTxId_1');
const input2 = sut.getInputTag(interactionTx as GQLNodeInterface, 'contractTxId_2');
const input3 = sut.getInputTag(interactionTx as GQLNodeInterface, 'contractTxId_3');
const input4 = sut.getInputTag(interactionTx as GQLNodeInterface, 'contractTxId_4');
const allContracts = sut.getContractsWithInputs(interactionTx as GQLNodeInterface);
// then
expect(input1.value).toEqual('contractTxId11_value');
@@ -109,7 +103,6 @@ describe('TagsParser', () => {
it('should return undefined if ordering is not proper', () => {
// given
const interactionTx = {
node: {
tags: [
{ name: 'Contract', value: 'contractTxId_2' },
{ name: 'foo', value: 'bar' },
@@ -120,14 +113,13 @@ describe('TagsParser', () => {
{ name: 'Contract', value: 'contractTxId_3' },
{ name: 'Input', value: 'contractTxId33_value' }
]
}
};
// when
const input1 = sut.getInputTag(interactionTx as GQLEdgeInterface, 'contractTxId_1');
const input2 = sut.getInputTag(interactionTx as GQLEdgeInterface, 'contractTxId_2');
const input3 = sut.getInputTag(interactionTx as GQLEdgeInterface, 'contractTxId_3');
const allContracts = sut.getContractsWithInputs(interactionTx as GQLEdgeInterface);
const input1 = sut.getInputTag(interactionTx as GQLNodeInterface, 'contractTxId_1');
const input2 = sut.getInputTag(interactionTx as GQLNodeInterface, 'contractTxId_2');
const input3 = sut.getInputTag(interactionTx as GQLNodeInterface, 'contractTxId_3');
const allContracts = sut.getContractsWithInputs(interactionTx as GQLNodeInterface);
// then
expect(input1).toBeUndefined();
@@ -148,7 +140,6 @@ describe('TagsParser', () => {
it('should return the first occurrence of the input tag (1)', () => {
// given
const interactionTx = {
node: {
tags: [
{ name: 'Contract', value: 'contractTxId_1' },
{ name: 'foo', value: 'bar' },
@@ -156,12 +147,11 @@ describe('TagsParser', () => {
{ name: 'duh', value: 'blah' },
{ name: 'Input', value: 'contractTxId1_value' }
]
}
};
// when
const input1 = sut.getInputTag(interactionTx as GQLEdgeInterface, 'contractTxId_1');
const allContracts = sut.getContractsWithInputs(interactionTx as GQLEdgeInterface);
const input1 = sut.getInputTag(interactionTx as GQLNodeInterface, 'contractTxId_1');
const allContracts = sut.getContractsWithInputs(interactionTx as GQLNodeInterface);
// then
expect(input1.value).toEqual('contractTxId1_value');
@@ -172,18 +162,16 @@ describe('TagsParser', () => {
it('should return the first occurrence of the input tag (2)', () => {
// given
const interactionTx = {
node: {
tags: [
{ name: 'Input', value: 'contractTxId1_value' },
{ name: 'foo', value: 'bar' },
{ name: 'duh', value: 'blah' }
]
}
};
// when
const input1 = sut.getInputTag(interactionTx as GQLEdgeInterface, 'contractTxId_1');
const allContracts = sut.getContractsWithInputs(interactionTx as GQLEdgeInterface);
const input1 = sut.getInputTag(interactionTx as GQLNodeInterface, 'contractTxId_1');
const allContracts = sut.getContractsWithInputs(interactionTx as GQLNodeInterface);
// then
expect(input1.value).toEqual('contractTxId1_value');
@@ -193,18 +181,16 @@ describe('TagsParser', () => {
it('should return the first occurrence of the input tag (3)', () => {
// given
const interactionTx = {
node: {
tags: [
{ name: 'Input', value: 'contractTxId1_value' },
{ name: 'Input', value: 'contractTxId2_value' },
{ name: 'Input', value: 'contractTxId3_value' }
]
}
};
// when
const input1 = sut.getInputTag(interactionTx as GQLEdgeInterface, 'contractTxId_1');
const allContracts = sut.getContractsWithInputs(interactionTx as GQLEdgeInterface);
const input1 = sut.getInputTag(interactionTx as GQLNodeInterface, 'contractTxId_1');
const allContracts = sut.getContractsWithInputs(interactionTx as GQLNodeInterface);
// then
expect(input1.value).toEqual('contractTxId1_value');
@@ -214,7 +200,6 @@ describe('TagsParser', () => {
it('should return the first occurrence of the input tag (4)', () => {
// given
const interactionTx = {
node: {
tags: [
{ name: 'foo', value: 'bar' },
{ name: 'Input', value: 'contractTxId666_value' },
@@ -222,12 +207,11 @@ describe('TagsParser', () => {
{ name: 'duh', value: 'blah' },
{ name: 'Contract', value: 'contractTxId_1' }
]
}
};
// when
const input1 = sut.getInputTag(interactionTx as GQLEdgeInterface, 'contractTxId_1');
const allContracts = sut.getContractsWithInputs(interactionTx as GQLEdgeInterface);
const input1 = sut.getInputTag(interactionTx as GQLNodeInterface, 'contractTxId_1');
const allContracts = sut.getContractsWithInputs(interactionTx as GQLNodeInterface);
// then
expect(input1.value).toEqual('contractTxId666_value');
@@ -237,18 +221,16 @@ describe('TagsParser', () => {
it('should return undefined if no "Input" tag', () => {
// given
const interactionTx = {
node: {
tags: [
{ name: 'foo', value: 'bar' },
{ name: 'duh', value: 'blah' },
{ name: 'one', value: 'two' }
]
}
};
// when
const input1 = sut.getInputTag(interactionTx as GQLEdgeInterface, 'contractTxId_1');
const allContracts = sut.getContractsWithInputs(interactionTx as GQLEdgeInterface);
const input1 = sut.getInputTag(interactionTx as GQLNodeInterface, 'contractTxId_1');
const allContracts = sut.getContractsWithInputs(interactionTx as GQLNodeInterface);
// then
expect(input1).toBeUndefined();
@@ -258,18 +240,16 @@ describe('TagsParser', () => {
it('should return single interact write contract', () => {
// given
const interactionTx = {
node: {
tags: [
{ name: 'foo', value: 'bar' },
{ name: 'duh', value: 'blah' },
{ name: 'Interact-Write', value: 'Contract A' },
{ name: 'one', value: 'two' }
]
}
};
// when
const result = sut.getInteractWritesContracts(interactionTx as GQLEdgeInterface);
const result = sut.getInteractWritesContracts(interactionTx as GQLNodeInterface);
// then
expect(result).toEqual(['Contract A']);
});
@@ -277,7 +257,6 @@ describe('TagsParser', () => {
it('should return multiple interact write contracts', () => {
// given
const interactionTx = {
node: {
tags: [
{ name: 'foo', value: 'bar' },
{ name: 'Interact-Write', value: 'Contract C' },
@@ -287,11 +266,10 @@ describe('TagsParser', () => {
{ name: 'one', value: 'two' },
{ name: 'Interact-Write', value: 'Contract E' }
]
}
};
// when
const result = sut.getInteractWritesContracts(interactionTx as GQLEdgeInterface);
const result = sut.getInteractWritesContracts(interactionTx as GQLNodeInterface);
// then
expect(result).toEqual(['Contract C', 'Contract D', 'Contract A', 'Contract E']);
});
@@ -299,18 +277,16 @@ describe('TagsParser', () => {
it('should return empty interact write contract', () => {
// given
const interactionTx = {
node: {
tags: [
{ name: 'foo', value: 'bar' },
{ name: 'duh', value: 'blah' },
{ name: 'one', value: 'two' },
{ name: 'Interact-Writee', value: 'Contract E' }
]
}
};
// when
const result = sut.getInteractWritesContracts(interactionTx as GQLEdgeInterface);
const result = sut.getInteractWritesContracts(interactionTx as GQLNodeInterface);
// then
expect(result).toEqual([]);
});

View File

@@ -1,46 +0,0 @@
/**
* A cache that stores its values depending on block height (eg.: contract's state cache).
* See {@link BsonFileBlockHeightWarpCache} or {@link MemBlockHeightWarpCache}
*
* @typeParam V - type of value stored in cache, defaults to `any`.
*/
export interface BlockHeightWarpCache<V> {
/**
* returns cached value for the highest available in cache block that is not higher than `blockHeight`.
*/
getLessOrEqual(key: string, blockHeight: number): Promise<BlockHeightCacheResult<V> | null>;
/**
* returns latest value stored for given key
*/
getLast(key: string): Promise<BlockHeightCacheResult<V> | null>;
/**
* returns value for the key and exact blockHeight
*/
get(key: string, blockHeight: number, returnDeepCopy?: boolean): Promise<BlockHeightCacheResult<V> | null>;
/**
* puts new value in cache under given {@link BlockHeightKey.key} and {@link BlockHeightKey.blockHeight}.
*/
put(blockHeightKey: BlockHeightKey, value: V): Promise<void>;
/**
* checks whether cache has any value stored for given cache key
*/
contains(key: string): Promise<boolean>;
/**
* flushes cache to underneath storage (if available)
*/
flush(): Promise<void>;
}
export class BlockHeightKey {
constructor(readonly cacheKey: string, readonly blockHeight: number) {}
}
// tslint:disable-next-line:max-classes-per-file
export class BlockHeightCacheResult<V> {
constructor(readonly cachedHeight: number, readonly cachedValue: V) {}
}

42
src/cache/SortKeyCache.ts vendored Normal file
View File

@@ -0,0 +1,42 @@
/**
* A cache that stores its values per contract tx id and sort key.
* A sort key is a value that the SmartWeave protocol is using
* to sort contract transactions ({@link LexicographicalInteractionsSorter}.
*
* All values should be stored in a lexicographical order (per contract) -
* sorted by the sort key.
*/
export interface SortKeyCache<V> {
getLessOrEqual(key: string, sortKey: string): Promise<SortKeyCacheResult<V> | null>;
/**
* returns latest value stored for given key
*/
getLast(key: string): Promise<SortKeyCacheResult<V> | null>;
/**
* returns value for the key and exact blockHeight
*/
get(contractTxId: string, sortKey: string, returnDeepCopy?: boolean): Promise<SortKeyCacheResult<V> | null>;
/**
* puts new value in cache under given {@link StateCacheKey.key} and {@link StateCacheKey.blockHeight}.
*/
put(stateCacheKey: StateCacheKey, value: V): Promise<void>;
close(): Promise<void>;
/**
* used mostly for debugging, allows to dump the current content cache
*/
dump(): Promise<any>;
}
export class StateCacheKey {
constructor(readonly contractTxId: string, readonly sortKey: string) {}
}
// tslint:disable-next-line:max-classes-per-file
export class SortKeyCacheResult<V> {
constructor(readonly sortKey: string, readonly cachedValue: V) {}
}

View File

@@ -1,129 +0,0 @@
import fs from 'fs';
import path from 'path';
import { BlockHeightKey, MemBlockHeightWarpCache } from '@warp/cache';
import { Benchmark, LoggerFactory } from '@warp/logging';
import stringify from 'safe-stable-stringify';
/**
* An implementation of {@link BlockHeightWarpCache} that stores its data in JSON files.
*
* Main use-case is the per block height state cache for contracts.
*
* This class extends standard {@link MemBlockHeightWarpCache} and add features of
* 1. Loading cache from files to memory (during initialization)
* 2. Flushing cache to files (only the "last" (ie. highest) block stored currently in memory
* is being saved).
*
* A separate file is created for each block height - otherwise it was common to
* hit 16 megabytes file size limit for json files.
*
* The files are organised in the following structure:
* --/basePath
* --/contractTxId_1
* --1.cache.json
* --2.cache.json
* --<blockHeight>.cache.json
* --...
* --748832.cache.json
* --/contractTxId_2
* --1.cache.json
* --323332.cache.json
* ...etc.
*
* Note: this is not performance-optimized for reading LARGE amount of contracts.
* Note: BSON has issues with top-level arrays - https://github.com/mongodb/js-bson/issues/319
* - so moving back to plain JSON...
*
* @Deprecated - a more mature persistent cache, based on LevelDB (or similar storage)
* should be implemented.
*/
export class FileBlockHeightWarpCache<V = any> extends MemBlockHeightWarpCache<V> {
private readonly fLogger = LoggerFactory.INST.create('FileBlockHeightWarpCache');
private isFlushing = false;
private isDirty = false;
constructor(
private readonly basePath = path.join(__dirname, 'storage', 'state'),
maxStoredInMemoryBlockHeights: number = Number.MAX_SAFE_INTEGER
) {
super(maxStoredInMemoryBlockHeights);
this.saveCache = this.saveCache.bind(this);
this.flush = this.flush.bind(this);
if (!fs.existsSync(this.basePath)) {
fs.mkdirSync(this.basePath);
}
const contracts = fs.readdirSync(this.basePath);
this.fLogger.info('Loading cache from files');
contracts.forEach((contract) => {
const cacheDirPath = path.join(this.basePath, contract);
if (this.storage[contract] == null) {
this.storage[contract] = new Map<number, V>();
}
const benchmark = Benchmark.measure();
const files = fs.readdirSync(cacheDirPath);
files.forEach((file) => {
const cacheFilePath = path.join(cacheDirPath, file);
const height = file.split('.')[0];
// FIXME: "state" and "validity" should be probably split into separate json files
const cacheValue = JSON.parse(fs.readFileSync(path.join(cacheFilePath), 'utf-8'));
this.putSync({ cacheKey: contract, blockHeight: +height }, cacheValue);
});
this.fLogger.info(`loading cache for ${contract}`, benchmark.elapsed());
this.fLogger.debug(`Amount of elements loaded for ${contract} to mem: ${this.storage[contract].size}`);
});
this.fLogger.debug('Storage keys', this.storage);
}
private async saveCache() {
this.isFlushing = true;
this.fLogger.info(`==== Persisting cache ====`);
// TODO: switch to async, as currently writing cache files may slow down contract execution.
try {
const directoryPath = this.basePath;
for (const key of Object.keys(this.storage)) {
const directory = key;
if (!fs.existsSync(path.join(directoryPath, directory))) {
fs.mkdirSync(path.join(directoryPath, directory));
}
// store only highest cached height
const toStore = await this.getLast(key);
// this check is a bit paranoid, since we're iterating on storage keys..
if (toStore !== null) {
const { cachedHeight, cachedValue } = toStore;
fs.writeFileSync(path.join(directoryPath, directory, `${cachedHeight}.cache.json`), stringify(cachedValue));
}
}
this.isDirty = false;
} catch (e) {
this.fLogger.error('Error while flushing cache', e);
} finally {
this.isFlushing = false;
this.fLogger.info(`==== Cache persisted ====`);
}
}
async put({ cacheKey, blockHeight }: BlockHeightKey, value: V): Promise<void> {
this.isDirty = true;
return super.put({ cacheKey, blockHeight }, value);
}
async flush(): Promise<void> {
if (this.isFlushing || !this.isDirty) {
return;
}
await this.saveCache();
}
}

View File

@@ -1,125 +0,0 @@
import { BlockHeightKey, MemBlockHeightWarpCache } from '@warp/cache';
import { LoggerFactory } from '@warp/logging';
import { Knex } from 'knex';
import { StateCache } from '@warp';
import stringify from 'safe-stable-stringify';
type DbResult = {
contract_id: string;
height: number;
state: string;
};
/**
* An implementation of {@link BlockHeightWarpCache} that stores its data (ie. contracts state)
* in a Knex-compatible storage (PostgreSQL, CockroachDB, MSSQL, MySQL, MariaDB, SQLite3, Oracle, and Amazon Redshift)
* https://knexjs.org
*/
export class KnexStateCache extends MemBlockHeightWarpCache<StateCache<any>> {
private readonly kLogger = LoggerFactory.INST.create('KnexBlockHeightWarpCache');
private readonly lastFlushHeight: Map<string, number> = new Map();
private isFlushing = false;
private isDirty = false;
private constructor(
private readonly knex: Knex,
maxStoredInMemoryBlockHeights: number = Number.MAX_SAFE_INTEGER,
cache: DbResult[]
) {
super(maxStoredInMemoryBlockHeights);
this.saveCache = this.saveCache.bind(this);
this.flush = this.flush.bind(this);
this.kLogger.info(`Loaded ${cache.length} cache entries from db`);
cache.forEach((entry) => {
this.putSync(
{
cacheKey: entry.contract_id,
blockHeight: entry.height
},
JSON.parse(entry.state)
);
this.lastFlushHeight.set(entry.contract_id, entry.height);
});
}
public static async init(
knex: Knex,
maxStoredInMemoryBlockHeights: number = Number.MAX_SAFE_INTEGER
): Promise<KnexStateCache> {
if (!(await knex.schema.hasTable('states'))) {
await knex.schema.createTable('states', (table) => {
table.string('contract_id', 64).notNullable().index();
table.integer('height').notNullable().index();
table.text('state').notNullable();
table.unique(['contract_id', 'height'], { indexName: 'states_composite_index' });
});
}
const cache: DbResult[] = await knex
.select(['contract_id', 'height', 'state'])
.from('states')
.max('height')
.groupBy(['contract_id']);
return new KnexStateCache(knex, maxStoredInMemoryBlockHeights, cache);
}
private async saveCache() {
this.isFlushing = true;
this.kLogger.info(`==== Persisting cache ====`);
try {
const contracts = Object.keys(this.storage);
for (const contractTxId of contracts) {
// store only highest cached height
const toStore = await this.getLast(contractTxId);
// this check is a bit paranoid, since we're iterating on storage keys..
if (toStore !== null) {
const { cachedHeight, cachedValue } = toStore;
if (this.lastFlushHeight.has(contractTxId) && this.lastFlushHeight.get(contractTxId) >= cachedHeight) {
continue;
}
const jsonState = stringify(cachedValue);
// FIXME: batch insert
await this.knex
.insert({
contract_id: contractTxId,
height: cachedHeight,
state: jsonState
})
.into('states')
.onConflict(['contract_id', 'height'])
.merge();
this.lastFlushHeight.set(contractTxId, cachedHeight);
}
}
this.isDirty = false;
} catch (e) {
this.kLogger.error('Error while flushing cache', e);
} finally {
this.isFlushing = false;
this.kLogger.info(`==== Cache persisted ====`);
}
}
async put({ cacheKey, blockHeight }: BlockHeightKey, value: StateCache<any>): Promise<void> {
this.isDirty = true;
return super.put({ cacheKey, blockHeight }, value);
}
async flush(): Promise<void> {
if (this.isFlushing || !this.isDirty) {
return;
}
await this.saveCache();
}
}

120
src/cache/impl/LevelDbCache.ts vendored Normal file
View File

@@ -0,0 +1,120 @@
import { SortKeyCache, StateCacheKey, SortKeyCacheResult } from '../SortKeyCache';
import { CacheOptions, LoggerFactory } from '@warp';
import { Level } from 'level';
import { MemoryLevel } from 'memory-level';
export const DEFAULT_LEVEL_DB_LOCATION = './cache/warp';
export const DEFAULT_MAX_ENTRIES_PER_CONTRACT = 5;
/**
* The LevelDB is a lexicographically sorted key-value database - so it's ideal for this use case
* - as it simplifies cache look-ups (e.g. lastly stored value or value "lower-or-equal" than given sortKey).
* The cache for contracts are implemented as sub-levels - https://www.npmjs.com/package/level#sublevel--dbsublevelname-options.
*
* The default location for the node.js cache is ./cache/warp.
* The default name for the browser IndexedDB cache is warp-cache
*
* In order to reduce the cache size, the oldest entries are automatically pruned.
*/
export class LevelDbCache<V = any> implements SortKeyCache<V> {
private readonly logger = LoggerFactory.INST.create('LevelDbCache');
/**
* not using the Level type, as it is not compatible with MemoryLevel (i.e. has more properties)
* and there doesn't seem to be any public interface/abstract type for all Level implementations
* (the AbstractLevel is not exported from the package...)
*/
private db: MemoryLevel;
private readonly maxStoredTransactions: number;
/**
* The LevelDB does not have an API that returns the current amount of stored elements
* - in order to get this value, each time all values have to be iterated and "manually" counted.
* That's not very good from the performance perspective, that's why the suggested
* approach is to store this (i.e. amount of cached entries) information externally.
*/
private entriesLength: { [contractTxId: string]: number } = {};
constructor(cacheOptions: CacheOptions) {
if (cacheOptions.inMemory) {
this.db = new MemoryLevel({ valueEncoding: 'json' });
} else {
const dbLocation = cacheOptions.dbLocation || DEFAULT_LEVEL_DB_LOCATION;
this.logger.info(`Using location ${dbLocation}`);
this.db = new Level<string, any>(dbLocation, { valueEncoding: 'json' });
}
// note: setting to 0 is obv. wrong, so in that case we also fall back to a default value.
this.maxStoredTransactions = cacheOptions.maxStoredTransactions || DEFAULT_MAX_ENTRIES_PER_CONTRACT;
}
async get(contractTxId: string, sortKey: string, returnDeepCopy?: boolean): Promise<SortKeyCacheResult<V> | null> {
const contractCache = this.db.sublevel<string, any>(contractTxId, { valueEncoding: 'json' });
try {
const result = await contractCache.get(sortKey);
return {
sortKey: sortKey,
cachedValue: result
};
} catch (e: any) {
if (e.code == 'LEVEL_NOT_FOUND') {
return null;
} else {
throw e;
}
}
}
async getLast(contractTxId: string): Promise<SortKeyCacheResult<V> | null> {
const contractCache = this.db.sublevel<string, any>(contractTxId, { valueEncoding: 'json' });
const keys = await contractCache.keys({ reverse: true, limit: 1 }).all();
if (keys.length) {
return {
sortKey: keys[0],
cachedValue: await contractCache.get(keys[0])
};
} else {
return null;
}
}
async getLessOrEqual(contractTxId: string, sortKey: string): Promise<SortKeyCacheResult<V> | null> {
const contractCache = this.db.sublevel<string, any>(contractTxId, { valueEncoding: 'json' });
const keys = await contractCache.keys({ reverse: true, lte: sortKey, limit: 1 }).all();
if (keys.length) {
return {
sortKey: keys[0],
cachedValue: await contractCache.get(keys[0])
};
} else {
return null;
}
}
async put(stateCacheKey: StateCacheKey, value: V): Promise<void> {
const contractCache = this.db.sublevel<string, any>(stateCacheKey.contractTxId, { valueEncoding: 'json' });
let entries = this.entriesLength[stateCacheKey.contractTxId];
if (entries == undefined) {
const allEntries = await contractCache.iterator().all();
entries = this.entriesLength[stateCacheKey.contractTxId] = allEntries.length;
}
if (entries >= this.maxStoredTransactions * 2) {
await contractCache.clear({ limit: this.maxStoredTransactions });
entries = this.entriesLength[stateCacheKey.contractTxId] = entries - this.maxStoredTransactions;
}
await contractCache.put(stateCacheKey.sortKey, value);
this.entriesLength[stateCacheKey.contractTxId] = entries + 1;
}
close(): Promise<void> {
return this.db.close();
}
async dump(): Promise<any> {
const result = await this.db.iterator().all();
return result;
}
}

View File

@@ -1,95 +0,0 @@
import { BlockHeightCacheResult, BlockHeightKey, BlockHeightWarpCache } from '@warp/cache';
import { asc, deepCopy, desc } from '@warp/utils';
import { LoggerFactory } from '@warp/logging';
/**
* A simple, in-memory cache implementation of the BlockHeightWarpCache
*/
export class MemBlockHeightWarpCache<V = any> implements BlockHeightWarpCache<V> {
private readonly logger = LoggerFactory.INST.create('MemBlockHeightWarpCache');
protected storage: { [key: string]: Map<number, V> } = {};
constructor(private maxStoredBlockHeights: number = Number.MAX_SAFE_INTEGER) {}
async getLast(key: string): Promise<BlockHeightCacheResult<V> | null> {
if (!(await this.contains(key))) {
return null;
}
const cached: Map<number, V> = this.storage[key];
// sort keys (ie. block heights) in asc order and get
// the last element (ie. highest cached block height).
const highestBlockHeight = [...cached.keys()].sort(asc).pop();
return {
cachedHeight: highestBlockHeight,
cachedValue: deepCopy(cached.get(highestBlockHeight))
};
}
async getLessOrEqual(key: string, blockHeight: number): Promise<BlockHeightCacheResult<V> | null> {
if (!(await this.contains(key))) {
return null;
}
const cached: Map<number, V> = this.storage[key];
// find first element in and desc-sorted keys array that is not higher than requested block height
const highestBlockHeight = [...cached.keys()].sort(desc).find((cachedBlockHeight) => {
return cachedBlockHeight <= blockHeight;
});
return highestBlockHeight === undefined
? null
: {
cachedHeight: highestBlockHeight,
cachedValue: deepCopy(cached.get(highestBlockHeight))
};
}
async put({ cacheKey, blockHeight }: BlockHeightKey, value: V): Promise<void> {
this.putSync({ cacheKey, blockHeight }, value);
}
protected putSync({ cacheKey, blockHeight }: BlockHeightKey, value: V): void {
if (!this.containsSync(cacheKey)) {
this.storage[cacheKey] = new Map();
}
const cached = this.storage[cacheKey];
if (cached.size >= this.maxStoredBlockHeights) {
const toRemove = [...cached.keys()].sort(asc).shift();
cached.delete(toRemove);
}
cached.set(blockHeight, value);
}
async contains(key: string): Promise<boolean> {
return this.containsSync(key);
}
protected containsSync(key: string): boolean {
return Object.prototype.hasOwnProperty.call(this.storage, key);
}
async get(key: string, blockHeight: number, returnDeepCopy = true): Promise<BlockHeightCacheResult<V> | null> {
if (!(await this.contains(key))) {
return null;
}
if (!this.storage[key].has(blockHeight)) {
return null;
}
return {
cachedHeight: blockHeight,
cachedValue: returnDeepCopy ? deepCopy(this.storage[key].get(blockHeight)) : this.storage[key].get(blockHeight)
};
}
flush(): Promise<void> {
return Promise.resolve(undefined);
}
}

View File

@@ -1,74 +0,0 @@
import { BlockHeightCacheResult, BlockHeightKey, BlockHeightWarpCache } from '@warp/cache';
import axios, { AxiosInstance } from 'axios';
/**
* A {@link BlockHeightWarpCache} implementation that delegates all its methods
* to remote endpoints.
*
* TODO: this could be further optimised - i.e. with the help of "level 1" memory cache
* that would store max X elements - and would be backed up by the "level 2" remote cache.
*/
export class RemoteBlockHeightCache<V = any> implements BlockHeightWarpCache<V> {
private axios: AxiosInstance;
/**
* @param type - id/type of the cache, that will allow to identify
* it server side (e.g. "STATE" or "INTERACTIONS")
* @param baseURL - the base url of the remote endpoint that serves
* cache data (e.g. "http://localhost:3000")
*/
constructor(private type: string, private baseURL: string) {
this.axios = axios.create({
baseURL: baseURL
});
}
/**
* GET '/last/:type/:key
*/
async getLast(key: string): Promise<BlockHeightCacheResult<V> | null> {
const response = await this.axios.get<BlockHeightCacheResult<V> | null>(`/last/${this.type}/${key}`);
return response.data || null;
}
/**
* GET '/less-or-equal/:type/:key/:blockHeight
*/
async getLessOrEqual(key: string, blockHeight: number): Promise<BlockHeightCacheResult<V> | null> {
const response = await this.axios.get<BlockHeightCacheResult<V> | null>(
`/less-or-equal/${this.type}/${key}/${blockHeight}`
);
return response.data || null;
}
/**
* TODO: data should "flushed" in batches...
* PUT '/:type/:key/:blockHeight' {data: value}
*/
async put({ cacheKey, blockHeight }: BlockHeightKey, value: V): Promise<void> {
if (!value) {
return;
}
await this.axios.put(`/${this.type}/${cacheKey}/${blockHeight}`, value);
}
/**
* GET '/contains/:type/:key'
*/
async contains(key: string): Promise<boolean> {
const response = await this.axios.get<boolean>(`/contains/${this.type}/${key}`);
return response.data;
}
/**
* GET '/:type/:key/:blockHeight'
*/
async get(key: string, blockHeight: number): Promise<BlockHeightCacheResult<V> | null> {
const response = await this.axios.get<BlockHeightCacheResult<V> | null>(`/${this.type}/${key}/${blockHeight}`);
return response.data || null;
}
flush(): Promise<void> {
return Promise.resolve(undefined);
}
}

12
src/cache/index.ts vendored
View File

@@ -1,12 +1,4 @@
export * from './impl/MemBlockHeightCache';
// FileBlockHeightCache has to be exported after MemBlockHeightCache,
// otherwise ts-jest complains with
// "TypeError: Class extends value undefined is not a constructor or null".
// Funny that standard tsc does not have such issues..
export * from './impl/FileBlockHeightCache';
export * from './impl/KnexStateCache';
export * from './impl/RemoteBlockHeightCache';
export * from './impl/MemCache';
export * from './BlockHeightWarpCache';
export * from './impl/LevelDbCache';
export * from './WarpCache';
export * from './SortKeyCache';

View File

@@ -8,7 +8,6 @@ import {
InteractionResult,
Tags
} from '@warp';
import { NetworkInfoInterface } from 'arweave/node/network';
import Transaction from 'arweave/node/lib/transaction';
import { Source } from './deploy/Source';
@@ -74,20 +73,13 @@ export interface Contract<State = unknown> extends Source {
* Returns state of the contract at required blockHeight.
* Similar to {@link readContract} from the current version.
*
* @param blockHeight - block height at which state should be read. If not passed
* current Arweave block height from the network info will be used.
* @param sortKeyOrBlockHeight - either a sortKey or block height at which the contract should be read
*
* @param currentTx - a set of currently evaluating interactions, that should
* be skipped during contract inner calls - to prevent the infinite call loop issue
* (mostly related to contracts that use the Foreign Call Protocol)
* (mostly related to contract that use the Foreign Call Protocol)
*/
readState(blockHeight?: number, currentTx?: CurrentTx[]): Promise<EvalStateResult<State>>;
readStateSequencer(
blockHeight: number,
upToTransactionId: string,
currentTx?: CurrentTx[]
): Promise<EvalStateResult<State>>;
readState(sortKeyOrBlockHeight?: string | number, currentTx?: CurrentTx[]): Promise<EvalStateResult<State>>;
/**
* Returns the "view" of the state, computed by the SWC -
@@ -100,15 +92,12 @@ export interface Contract<State = unknown> extends Source {
* with specified input.
*
* @param input - the input to the contract - eg. function name and parameters
* @param blockHeight - the height at which the contract state will be evaluated
* before applying last interaction transaction - ie. transaction with 'input'
* @param tags - a set of tags that can be added to the interaction transaction
* @param transfer - additional {@link ArTransfer} data that can be attached to the interaction
* transaction
*/
viewState<Input = unknown, View = unknown>(
input: Input,
blockHeight?: number,
tags?: Tags,
transfer?: ArTransfer
): Promise<InteractionResult<State, View>>;
@@ -189,23 +178,6 @@ export interface Contract<State = unknown> extends Source {
*/
getCallStack(): ContractCallStack;
/**
* Gets network info assigned to this contract.
* Network info is refreshed between interactions with
* given contract (eg. between consecutive calls to {@link Contract.readState})
* but reused within given execution tree (ie. only "root" contract loads the
* network info - eg. if readState calls other contracts, these calls will use the
* "root" contract network info - so that the whole execution is performed with the
* same network info)
*/
getNetworkInfo(): Partial<NetworkInfoInterface>;
/**
* Get the block height requested by user for the given interaction with contract
* (eg. readState or viewState call)
*/
getRootBlockHeight(): number | null;
/**
* Gets the parent contract - ie. contract that called THIS contract during the
* state evaluation.
@@ -255,4 +227,6 @@ export interface Contract<State = unknown> extends Source {
* @param newSrcTxId - result of the {@link save} method call.
*/
evolve(newSrcTxId: string, useBundler?: boolean): Promise<BundleInteractionResponse | string | null>;
dumpCache(): Promise<any>;
}

View File

@@ -8,7 +8,7 @@ import {
ContractCallStack,
ContractInteraction,
createDummyTx,
createTx,
createInteractionTx,
CurrentTx,
DefaultEvaluationOptions,
emptyTransfer,
@@ -16,26 +16,25 @@ import {
EvaluationOptions,
Evolve,
ExecutionContext,
GQLEdgeInterface,
GQLNodeInterface,
HandlerApi,
InnerWritesEvaluator,
InteractionCall,
InteractionData,
InteractionResult,
InteractionsSorter,
LexicographicalInteractionsSorter,
LoggerFactory,
SigningFunction,
sleep,
Warp,
SmartWeaveTags,
SourceType,
Tags,
SourceImpl,
SourceData,
BundleInteractionResponse
} from '@warp';
import { TransactionStatusResponse } from 'arweave/node/transactions';
import { NetworkInfoInterface } from 'arweave/node/network';
import stringify from 'safe-stable-stringify';
import * as crypto from 'crypto';
import Transaction from 'arweave/node/lib/transaction';
@@ -51,23 +50,11 @@ export class HandlerBasedContract<State> implements Contract<State> {
private _callStack: ContractCallStack;
private _evaluationOptions: EvaluationOptions = new DefaultEvaluationOptions();
/**
* current Arweave networkInfo that will be used for all operations of the SmartWeave protocol.
* Only the 'root' contract call should read this data from Arweave - all the inner calls ("child" contracts)
* should reuse this data from the parent ("calling") contract.
*/
private _networkInfo?: Partial<NetworkInfoInterface> = null;
private _rootBlockHeight: number = null;
private readonly _innerWritesEvaluator = new InnerWritesEvaluator();
private readonly _callDepth: number;
private _benchmarkStats: BenchmarkStats = null;
private readonly _arweaveWrapper: ArweaveWrapper;
private _sorter: InteractionsSorter;
/**
* wallet connected to this contract
@@ -82,9 +69,8 @@ export class HandlerBasedContract<State> implements Contract<State> {
) {
this.waitForConfirmation = this.waitForConfirmation.bind(this);
this._arweaveWrapper = new ArweaveWrapper(warp.arweave);
this._sorter = new LexicographicalInteractionsSorter(warp.arweave);
if (_parentContract != null) {
this._networkInfo = _parentContract.getNetworkInfo();
this._rootBlockHeight = _parentContract.getRootBlockHeight();
this._evaluationOptions = _parentContract.evaluationOptions();
this._callDepth = _parentContract.callDepth() + 1;
const interaction: InteractionCall = _parentContract.getCallStack().getInteraction(_callingInteraction.id);
@@ -96,10 +82,6 @@ export class HandlerBasedContract<State> implements Contract<State> {
)}`
);
}
// sanity-check...
if (this._networkInfo == null) {
throw Error('Calling contract should have the network info already set!');
}
this.logger.debug('Calling interaction id', _callingInteraction.id);
const callStack = new ContractCallStack(_contractTxId, this._callDepth);
interaction.interactionInput.foreignContractCalls.set(_contractTxId, callStack);
@@ -110,35 +92,30 @@ export class HandlerBasedContract<State> implements Contract<State> {
}
}
async readState(blockHeight?: number, currentTx?: CurrentTx[]): Promise<EvalStateResult<State>> {
return this.readStateSequencer(blockHeight, undefined, currentTx);
}
async readStateSequencer(
blockHeight: number,
upToTransactionId: string,
currentTx?: CurrentTx[]
): Promise<EvalStateResult<State>> {
async readState(sortKeyOrBlockHeight?: string | number, currentTx?: CurrentTx[]): Promise<EvalStateResult<State>> {
this.logger.info('Read state for', {
contractTxId: this._contractTxId,
currentTx
currentTx,
sortKeyOrBlockHeight
});
const initBenchmark = Benchmark.measure();
this.maybeResetRootContract(blockHeight);
this.maybeResetRootContract();
if (this._parentContract != null && sortKeyOrBlockHeight == null) {
throw new Error('SortKey MUST be always set for non-root contract calls');
}
const { stateEvaluator } = this.warp;
const executionContext = await this.createExecutionContext(
this._contractTxId,
blockHeight,
false,
upToTransactionId
);
const sortKey =
typeof sortKeyOrBlockHeight == 'number'
? this._sorter.generateLastSortKey(sortKeyOrBlockHeight)
: sortKeyOrBlockHeight;
const executionContext = await this.createExecutionContext(this._contractTxId, sortKey, false);
this.logger.info('Execution Context', {
blockHeight: executionContext.blockHeight,
srcTxId: executionContext.contractDefinition?.srcTxId,
missingInteractions: executionContext.sortedInteractions.length,
cachedStateHeight: executionContext.cachedState?.cachedHeight,
upToTransactionId
missingInteractions: executionContext.sortedInteractions?.length,
cachedSortKey: executionContext.cachedState?.sortKey
});
initBenchmark.stop();
@@ -165,12 +142,11 @@ export class HandlerBasedContract<State> implements Contract<State> {
async viewState<Input, View>(
input: Input,
blockHeight?: number,
tags: Tags = [],
transfer: ArTransfer = emptyTransfer
): Promise<InteractionResult<State, View>> {
this.logger.info('View state for', this._contractTxId);
return await this.callContract<Input, View>(input, undefined, blockHeight, tags, transfer);
return await this.callContract<Input, View>(input, undefined, undefined, tags, transfer);
}
async viewStateForTx<Input, View>(
@@ -337,7 +313,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
});
}
const interactionTx = await createTx(
const interactionTx = await createInteractionTx(
this.warp.arweave,
this.signer,
this._contractTxId,
@@ -358,10 +334,6 @@ export class HandlerBasedContract<State> implements Contract<State> {
return this._callStack;
}
getNetworkInfo(): Partial<NetworkInfoInterface> {
return this._networkInfo;
}
connect(signer: ArWallet | SigningFunction): Contract<State> {
if (typeof signer == 'function') {
this.signer = signer;
@@ -381,10 +353,6 @@ export class HandlerBasedContract<State> implements Contract<State> {
return this;
}
getRootBlockHeight(): number {
return this._rootBlockHeight;
}
private async waitForConfirmation(transactionId: string): Promise<TransactionStatusResponse> {
const { arweave } = this.warp;
@@ -402,106 +370,46 @@ export class HandlerBasedContract<State> implements Contract<State> {
private async createExecutionContext(
contractTxId: string,
blockHeight?: number,
forceDefinitionLoad = false,
upToTransactionId: string = undefined
upToSortKey?: string,
forceDefinitionLoad = false
): Promise<ExecutionContext<State, HandlerApi<State>>> {
const { definitionLoader, interactionsLoader, interactionsSorter, executorFactory, stateEvaluator, useWarpGwInfo } =
this.warp;
let currentNetworkInfo;
const { definitionLoader, interactionsLoader, executorFactory, stateEvaluator } = this.warp;
const benchmark = Benchmark.measure();
// if this is a "root" call (ie. original call from Warp's client)
if (this._parentContract == null) {
if (blockHeight) {
this._networkInfo = {
height: blockHeight
};
} else {
this.logger.debug('Reading network info for root call');
currentNetworkInfo = useWarpGwInfo ? await this._arweaveWrapper.rGwInfo() : await this._arweaveWrapper.info();
this._networkInfo = currentNetworkInfo;
}
} else {
// if that's a call from within contract's source code
this.logger.debug('Reusing network info from the calling contract');
// note: the whole execution tree should use the same network info!
// this requirement was not fulfilled in the "v1" SDK - each subsequent
// call to contract (from contract's source code) was loading network info independently
// if the contract was evaluating for many minutes/hours, this could effectively lead to reading
// state on different block heights...
currentNetworkInfo = (this._parentContract as HandlerBasedContract<State>)._networkInfo;
}
if (blockHeight == null) {
blockHeight = currentNetworkInfo.height;
}
this.logger.debug('network info', benchmark.elapsed());
benchmark.reset();
const cachedState = await stateEvaluator.latestAvailableState<State>(contractTxId, blockHeight);
let cachedBlockHeight = -1;
if (cachedState != null) {
cachedBlockHeight = cachedState.cachedHeight;
}
const cachedState = await stateEvaluator.latestAvailableState<State>(contractTxId, upToSortKey);
this.logger.debug('cache lookup', benchmark.elapsed());
benchmark.reset();
const evolvedSrcTxId = Evolve.evolvedSrcTxId(cachedState?.cachedValue?.state);
let handler, contractDefinition, sortedInteractions;
let contractDefinition,
interactions: GQLEdgeInterface[] = [],
sortedInteractions: GQLEdgeInterface[] = [],
handler;
if (cachedBlockHeight != blockHeight) {
[contractDefinition, interactions] = await Promise.all([
definitionLoader.load<State>(contractTxId, evolvedSrcTxId),
// note: "eagerly" loading all of the interactions up to the originally requested block height
// (instead of the blockHeight requested for this specific read state call).
// as dumb as it may seem - this in fact significantly speeds up the processing
// - because the InteractionsLoader (usually CacheableContractInteractionsLoader)
// doesn't have to download missing interactions during the contract execution
// (eg. if contract is calling different contracts on different block heights).
// This basically limits the amount of interactions with Arweave GraphQL endpoint -
// each such interaction takes at least ~500ms.
interactionsLoader.load(
contractTxId,
cachedBlockHeight + 1,
this._rootBlockHeight || this._networkInfo.height,
this._evaluationOptions,
upToTransactionId
)
]);
this.logger.debug('contract and interactions load', benchmark.elapsed());
sortedInteractions = await interactionsSorter.sort(interactions);
this.logger.trace('Sorted interactions', sortedInteractions);
handler = (await executorFactory.create(contractDefinition, this._evaluationOptions)) as HandlerApi<State>;
} else {
this.logger.debug('Cached state', cachedState, upToSortKey);
if (cachedState && cachedState.sortKey == upToSortKey) {
this.logger.debug('State fully cached, not loading interactions.');
if (forceDefinitionLoad || evolvedSrcTxId) {
contractDefinition = await definitionLoader.load<State>(contractTxId, evolvedSrcTxId);
handler = (await executorFactory.create(contractDefinition, this._evaluationOptions)) as HandlerApi<State>;
}
} else {
[contractDefinition, sortedInteractions] = await Promise.all([
definitionLoader.load<State>(contractTxId, evolvedSrcTxId),
interactionsLoader.load(contractTxId, cachedState?.sortKey, upToSortKey, this._evaluationOptions)
]);
this.logger.debug('contract and interactions load', benchmark.elapsed());
handler = (await executorFactory.create(contractDefinition, this._evaluationOptions)) as HandlerApi<State>;
}
const containsInteractionsFromSequencer = interactions.some((i) => i.node.source == SourceType.WARP_SEQUENCER);
this.logger.debug('containsInteractionsFromSequencer', containsInteractionsFromSequencer);
return {
contractDefinition,
blockHeight,
sortedInteractions,
handler,
warp: this.warp,
contract: this,
contractDefinition,
sortedInteractions,
evaluationOptions: this._evaluationOptions,
currentNetworkInfo,
handler,
cachedState,
containsInteractionsFromSequencer,
upToTransactionId
requestedSortKey: upToSortKey
};
}
@@ -509,64 +417,28 @@ export class HandlerBasedContract<State> implements Contract<State> {
contractTxId: string,
transaction: GQLNodeInterface
): Promise<ExecutionContext<State, HandlerApi<State>>> {
const benchmark = Benchmark.measure();
const { definitionLoader, interactionsLoader, interactionsSorter, executorFactory, stateEvaluator } = this.warp;
const blockHeight = transaction.block.height;
const caller = transaction.owner.address;
const sortKey = transaction.sortKey;
const cachedState = await stateEvaluator.latestAvailableState<State>(contractTxId, blockHeight);
let cachedBlockHeight = -1;
if (cachedState != null) {
cachedBlockHeight = cachedState.cachedHeight;
}
let contractDefinition,
interactions = [],
sortedInteractions = [];
if (cachedBlockHeight != blockHeight) {
[contractDefinition, interactions] = await Promise.all([
definitionLoader.load<State>(contractTxId),
await interactionsLoader.load(contractTxId, 0, blockHeight, this._evaluationOptions)
]);
sortedInteractions = await interactionsSorter.sort(interactions);
} else {
this.logger.debug('State fully cached, not loading interactions.');
contractDefinition = await definitionLoader.load<State>(contractTxId);
}
const handler = (await executorFactory.create(contractDefinition, this._evaluationOptions)) as HandlerApi<State>;
this.logger.debug('Creating execution context from tx:', benchmark.elapsed());
const containsInteractionsFromSequencer = interactions.some((i) => i.node.source == SourceType.WARP_SEQUENCER);
const baseContext = await this.createExecutionContext(contractTxId, sortKey, true);
return {
contractDefinition,
blockHeight,
sortedInteractions,
handler,
warp: this.warp,
contract: this,
evaluationOptions: this._evaluationOptions,
caller,
cachedState,
containsInteractionsFromSequencer
...baseContext,
caller
};
}
private maybeResetRootContract(blockHeight?: number) {
private maybeResetRootContract() {
if (this._parentContract == null) {
this.logger.debug('Clearing network info and call stack for the root contract');
this._networkInfo = null;
this.logger.debug('Clearing call stack for the root contract');
this._callStack = new ContractCallStack(this.txId(), 0);
this._rootBlockHeight = blockHeight;
}
}
private async callContract<Input, View = unknown>(
input: Input,
caller?: string,
blockHeight?: number,
sortKey?: string,
tags: Tags = [],
transfer: ArTransfer = emptyTransfer
): Promise<InteractionResult<State, View>> {
@@ -577,27 +449,20 @@ export class HandlerBasedContract<State> implements Contract<State> {
}
const { arweave, stateEvaluator } = this.warp;
// create execution context
let executionContext = await this.createExecutionContext(this._contractTxId, blockHeight, true);
let executionContext = await this.createExecutionContext(this._contractTxId, sortKey, true);
// add block data to execution context
if (!executionContext.currentBlockData) {
const currentBlockData = executionContext.currentNetworkInfo?.current
? // trying to optimise calls to arweave as much as possible...
await arweave.blocks.get(executionContext.currentNetworkInfo.current)
: await arweave.blocks.getCurrent();
executionContext = {
...executionContext,
currentBlockData
};
}
const currentBlockData = await arweave.blocks.getCurrent();
// add caller info to execution context
let effectiveCaller;
if (caller) {
effectiveCaller = caller;
} else if (this.signer) {
const dummyTx = await arweave.createTransaction({ data: Math.random().toString().slice(-4) });
const dummyTx = await arweave.createTransaction({
data: Math.random().toString().slice(-4),
reward: '72600854',
last_tx: 'p7vc1iSP6bvH_fCeUFa9LqoV5qiyW-jdEKouAT0XMoSwrNraB9mgpi29Q10waEpO'
});
await this.signer(dummyTx);
effectiveCaller = await arweave.wallets.ownerToAddress(dummyTx.owner);
} else {
@@ -612,6 +477,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
// eval current state
const evalStateResult = await stateEvaluator.eval<State>(executionContext, []);
this.logger.info('Current state', evalStateResult.state);
// create interaction transaction
const interaction: ContractInteraction<Input> = {
@@ -620,16 +486,18 @@ export class HandlerBasedContract<State> implements Contract<State> {
};
this.logger.debug('interaction', interaction);
const tx = await createTx(
const tx = await createInteractionTx(
arweave,
this.signer,
this._contractTxId,
input,
tags,
transfer.target,
transfer.winstonQty
transfer.winstonQty,
true
);
const dummyTx = createDummyTx(tx, executionContext.caller, executionContext.currentBlockData);
const dummyTx = createDummyTx(tx, executionContext.caller, currentBlockData);
dummyTx.sortKey = await this._sorter.createSortKey(dummyTx.block.id, dummyTx.id, dummyTx.block.height);
const handleResult = await this.evalInteraction<Input, View>(
{
@@ -699,7 +567,6 @@ export class HandlerBasedContract<State> implements Contract<State> {
interactionCall.update({
cacheHit: false,
intermediaryCacheHit: false,
outputState: this._evaluationOptions.stackTrace.saveState ? result.state : undefined,
executionTime: benchmark.elapsed(true) as number,
valid: result.type === 'ok',
@@ -756,13 +623,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
throw new Error(`Unable to retrieve state. ${error.status}: ${error.body?.message}`);
});
await stateEvaluator.syncState(
this._contractTxId,
response.height,
response.lastTransactionId,
response.state,
response.validity
);
await stateEvaluator.syncState(this._contractTxId, response.sortKey, response.state, response.validity);
return this;
}
@@ -786,4 +647,13 @@ export class HandlerBasedContract<State> implements Contract<State> {
return srcTx.id;
}
async dumpCache(): Promise<any> {
const { stateEvaluator } = this.warp;
return await stateEvaluator.dumpCache();
}
get callingInteraction(): GQLNodeInterface | null {
return this._callingInteraction;
}
}

View File

@@ -15,6 +15,7 @@ export class ContractCallStack {
const interactionCall = InteractionCall.create(
new InteractionInput(
interactionTx.id,
interactionTx.sortKey,
interactionTx.block.height,
interactionTx.block.timestamp,
interaction?.caller,
@@ -55,6 +56,7 @@ export class InteractionCall {
export class InteractionInput {
constructor(
public readonly txId: string,
public readonly sortKey: string,
public readonly blockHeight: number,
public readonly blockTimestamp: number,
public readonly caller: string,
@@ -68,7 +70,6 @@ export class InteractionInput {
export class InteractionOutput {
constructor(
public readonly cacheHit: boolean,
public readonly intermediaryCacheHit: boolean,
public readonly outputState: any,
public readonly executionTime: number,
public readonly valid: boolean,

View File

@@ -1,14 +1,12 @@
import {
BlockHeightCacheResult,
Contract,
ContractDefinition,
EvalStateResult,
EvaluationOptions,
GQLEdgeInterface,
Warp
GQLNodeInterface,
Warp,
SortKeyCacheResult
} from '@warp';
import { NetworkInfoInterface } from 'arweave/node/network';
import { BlockData } from 'arweave/node/blocks';
/**
* current execution context of the contract - contains all elements
@@ -31,14 +29,10 @@ export type ExecutionContext<State, Api = unknown> = {
*/
contractDefinition: ContractDefinition<State>;
/**
* block height used for all operations - either requested block height or current network block height
*/
blockHeight: number;
/**
* interaction sorted using either {@link LexicographicalInteractionsSorter} or {@link BlockHeightInteractionsSorter}
* interaction sorted using either {@link LexicographicalInteractionsSorter}
* - crucial for proper and deterministic state evaluation
*/
sortedInteractions: GQLEdgeInterface[];
sortedInteractions: GQLNodeInterface[];
/**
* evaluation options currently being used
* TODO: this can be removed, as it should be accessible from the {@link Contract}
@@ -49,15 +43,7 @@ export type ExecutionContext<State, Api = unknown> = {
* performs all the computation.
*/
handler: Api;
currentNetworkInfo?: NetworkInfoInterface;
currentBlockData?: BlockData;
caller?: string; // note: this is only set for "viewState" operations
cachedState?: BlockHeightCacheResult<EvalStateResult<State>>;
// if the interactions list contains interactions from sequencer
// we cannot cache at requested block height - as it may happen that after this state
// evaluation - new transactions will be available on the same block height.
containsInteractionsFromSequencer: boolean;
upToTransactionId?: string;
cachedState?: SortKeyCacheResult<EvalStateResult<State>>;
requestedSortKey?: string;
};

View File

@@ -3,7 +3,6 @@ import {
ExecutorFactory,
HandlerApi,
InteractionsLoader,
InteractionsSorter,
WarpBuilder,
StateEvaluator
} from '@warp/core';
@@ -33,10 +32,8 @@ export class Warp {
readonly arweave: Arweave,
readonly definitionLoader: DefinitionLoader,
readonly interactionsLoader: InteractionsLoader,
readonly interactionsSorter: InteractionsSorter,
readonly executorFactory: ExecutorFactory<HandlerApi<unknown>>,
readonly stateEvaluator: StateEvaluator,
readonly useWarpGwInfo: boolean = false
readonly stateEvaluator: StateEvaluator
) {
this.createContract = new DefaultCreateContract(arweave);
}
@@ -65,8 +62,4 @@ export class Warp {
pst(contractTxId: string): PstContract {
return new PstContractImpl(contractTxId, this);
}
async flushCache(): Promise<void> {
await this.stateEvaluator.flushCache();
}
}

View File

@@ -1,18 +1,13 @@
import Arweave from 'arweave';
import {
ArweaveGatewayInteractionsLoader,
CacheableContractInteractionsLoader,
ConfirmationStatus,
ContractDefinitionLoader,
DebuggableExecutorFactory,
DefinitionLoader,
EmptyInteractionsSorter,
ExecutorFactory,
HandlerApi,
InteractionsLoader,
InteractionsSorter,
LexicographicalInteractionsSorter,
MemBlockHeightWarpCache,
MemCache,
WarpGatewayContractDefinitionLoader,
WarpGatewayInteractionsLoader,
@@ -21,15 +16,13 @@ import {
StateEvaluator
} from '@warp';
export const R_GW_URL = 'https://d1o5nlqr4okus2.cloudfront.net';
export const WARP_GW_URL = 'https://d1o5nlqr4okus2.cloudfront.net';
export class WarpBuilder {
private _definitionLoader?: DefinitionLoader;
private _interactionsLoader?: InteractionsLoader;
private _interactionsSorter?: InteractionsSorter;
private _executorFactory?: ExecutorFactory<HandlerApi<unknown>>;
private _stateEvaluator?: StateEvaluator;
private _useWarpGwInfo = false;
constructor(private readonly _arweave: Arweave) {}
@@ -43,19 +36,6 @@ export class WarpBuilder {
return this;
}
public setCacheableInteractionsLoader(value: InteractionsLoader, maxStoredInMemoryBlockHeights = 1): WarpBuilder {
this._interactionsLoader = new CacheableContractInteractionsLoader(
value,
new MemBlockHeightWarpCache(maxStoredInMemoryBlockHeights)
);
return this;
}
public setInteractionsSorter(value: InteractionsSorter): WarpBuilder {
this._interactionsSorter = value;
return this;
}
public setExecutorFactory(value: ExecutorFactory<HandlerApi<unknown>>): WarpBuilder {
this._executorFactory = value;
return this;
@@ -77,28 +57,16 @@ export class WarpBuilder {
public useWarpGateway(
confirmationStatus: ConfirmationStatus = null,
source: SourceType = null,
address = R_GW_URL
address = WARP_GW_URL
): WarpBuilder {
this._interactionsLoader = new WarpGatewayInteractionsLoader(address, confirmationStatus, source);
this._definitionLoader = new WarpGatewayContractDefinitionLoader(address, this._arweave, new MemCache());
this._interactionsSorter = new EmptyInteractionsSorter();
this._useWarpGwInfo = true;
return this;
}
public useArweaveGateway(): WarpBuilder {
this._definitionLoader = new ContractDefinitionLoader(this._arweave, new MemCache());
this._interactionsLoader = new CacheableContractInteractionsLoader(
new ArweaveGatewayInteractionsLoader(this._arweave),
new MemBlockHeightWarpCache(1)
);
this._interactionsSorter = new LexicographicalInteractionsSorter(this._arweave);
this._useWarpGwInfo = false;
return this;
}
public useWarpGwInfo(): WarpBuilder {
this._useWarpGwInfo = true;
this._interactionsLoader = new ArweaveGatewayInteractionsLoader(this._arweave);
return this;
}
@@ -107,10 +75,8 @@ export class WarpBuilder {
this._arweave,
this._definitionLoader,
this._interactionsLoader,
this._interactionsSorter,
this._executorFactory,
this._stateEvaluator,
this._useWarpGwInfo
this._stateEvaluator
);
}
}

94
src/core/WarpFactory.ts Normal file
View File

@@ -0,0 +1,94 @@
import Arweave from 'arweave';
import { CacheableExecutorFactory, Evolve } from '@warp/plugins';
import {
CacheableStateEvaluator,
ConfirmationStatus,
EvalStateResult,
HandlerExecutorFactory,
WARP_GW_URL,
SourceType,
Warp,
WarpBuilder
} from '@warp/core';
import { LevelDbCache, MemCache } from '@warp/cache';
export type GatewayOptions = {
confirmationStatus: ConfirmationStatus;
source: SourceType;
address: string;
};
export type CacheOptions = {
maxStoredTransactions: number;
inMemory: boolean;
dbLocation?: string;
};
export const defaultWarpGwOptions: GatewayOptions = {
confirmationStatus: { notCorrupted: true },
source: null,
address: WARP_GW_URL
};
export const defaultCacheOptions: CacheOptions = {
maxStoredTransactions: 10,
inMemory: false
};
/**
* A factory that simplifies the process of creating different versions of {@link Warp}.
* All versions use the {@link Evolve} plugin.
*/
export class WarpFactory {
/**
* Returns a fully configured {@link Warp} that is using arweave.net compatible gateway
* (with a GQL endpoint) for loading the interactions and in memory cache.
* Suitable for testing.
*/
static forTesting(arweave: Arweave): Warp {
return this.arweaveGw(arweave, {
maxStoredTransactions: 20,
inMemory: true
});
}
/**
* Returns a fully configured {@link Warp} that is using arweave.net compatible gateway
* (with a GQL endpoint) for loading the interactions.
*/
static arweaveGw(
arweave: Arweave,
cacheOptions: CacheOptions = {
maxStoredTransactions: 20,
inMemory: false
}
): Warp {
return this.levelDbCached(arweave, cacheOptions).useArweaveGateway().build();
}
/**
* Returns a fully configured {@link Warp} that is using Warp gateway for loading the interactions.
*/
static warpGw(
arweave: Arweave,
gatewayOptions: GatewayOptions = defaultWarpGwOptions,
cacheOptions: CacheOptions = {
maxStoredTransactions: 20,
inMemory: false
}
): Warp {
return this.levelDbCached(arweave, cacheOptions)
.useWarpGateway(gatewayOptions.confirmationStatus, gatewayOptions.source, gatewayOptions.address)
.build();
}
static levelDbCached(arweave: Arweave, cacheOptions: CacheOptions): WarpBuilder {
const executorFactory = new CacheableExecutorFactory(arweave, new HandlerExecutorFactory(arweave), new MemCache());
const stateEvaluator = new CacheableStateEvaluator(
arweave,
new LevelDbCache<EvalStateResult<unknown>>(cacheOptions),
[new Evolve()]
);
return Warp.builder(arweave).setExecutorFactory(executorFactory).setStateEvaluator(stateEvaluator);
}
}

View File

@@ -4,7 +4,6 @@ export * from './modules/InteractionsLoader';
export * from './modules/InteractionsSorter';
export * from './modules/StateEvaluator';
export * from './modules/impl/BlockHeightInteractionsSorter';
export * from './modules/impl/ContractDefinitionLoader';
export * from './modules/impl/WarpGatewayContractDefinitionLoader';
export * from './modules/impl/ArweaveGatewayInteractionsLoader';
@@ -13,7 +12,6 @@ export * from './modules/impl/DefaultStateEvaluator';
export * from './modules/impl/CacheableStateEvaluator';
export * from './modules/impl/HandlerExecutorFactory';
export * from './modules/impl/LexicographicalInteractionsSorter';
export * from './modules/impl/EmptyInteractionsSorter';
export * from './modules/impl/TagsParser';
export * from './modules/impl/normalize-source';
export * from './modules/impl/StateCache';
@@ -25,7 +23,6 @@ export * from './ExecutionContext';
export * from './ContractDefinition';
export * from './ContractCallStack';
export * from './web/WarpWebFactory';
export * from './node/WarpNodeFactory';
export * from './WarpFactory';
export * from './Warp';
export * from './WarpBuilder';

View File

@@ -1,17 +1,24 @@
import { EvaluationOptions, GQLEdgeInterface } from '@warp';
import { EvaluationOptions, GQLNodeInterface } from '@warp';
/**
* Implementors of this interface add functionality of loading contract's interaction transactions.
* These transactions are then used to evaluate contract's state to a required block height.
*
* Note: InteractionsLoaders are not responsible for sorting interaction transactions!
* Returned interactions MUST be sorted according to protocol specification ({@link LexicographicalInteractionsSorter}
*/
export interface InteractionsLoader {
/**
* This method loads interactions for a given contract.
* If param fromSortKey and/or param toSortKey are present, the loaded interactions do
* conform the condition: i.sortKey > fromSortKey && i.sortKey <= toSortKey
*
* @param contractTxId - contract tx id to load the interactions
* @param fromSortKey - exclusive, optional - sortKey, from which the interactions should be loaded
* @param toSortKey - inclusive, optional - sortKey, to which then interactions should be loaded
* @param evaluationOptions, optional - {@link EvaluationOptions}
*/
load(
contractId: string,
fromBlockHeight: number,
toBlockHeight: number,
evaluationOptions?: EvaluationOptions,
upToTransactionId?: string
): Promise<GQLEdgeInterface[]>;
contractTxId: string,
fromSortKey?: string,
toSortKey?: string,
evaluationOptions?: EvaluationOptions
): Promise<GQLNodeInterface[]>;
}

View File

@@ -5,4 +5,20 @@ import { GQLEdgeInterface } from '@warp';
*/
export interface InteractionsSorter {
sort(transactions: GQLEdgeInterface[]): Promise<GQLEdgeInterface[]>;
/**
* generates a sort key according to protocol specs
*/
createSortKey(blockId: string, transactionId: string, blockHeight: number): Promise<string>;
/**
* retreives the block height from the sort key
*/
extractBlockHeight(sortKey?: string): number | null;
/**
* generates a sort key for given block height - with a guarantee,
* that it will the last possible sort key for a given block height
*/
generateLastSortKey(blockHeight: number): string;
}

View File

@@ -1,4 +1,4 @@
import { BlockHeightCacheResult, CurrentTx, ExecutionContext, GQLNodeInterface } from '@warp';
import { CurrentTx, ExecutionContext, GQLNodeInterface, SortKeyCacheResult } from '@warp';
/**
* Implementors of this class are responsible for evaluating contract's state
@@ -53,33 +53,33 @@ export interface StateEvaluator {
): Promise<void>;
/**
* loads latest available state for given contract for given blockHeight.
* - implementors should be aware that there might multiple interactions
* for single block - and sort them according to protocol specification.
* loads the latest available state for given contract for given sortKey.
*/
latestAvailableState<State>(
contractTxId: string,
blockHeight: number
): Promise<BlockHeightCacheResult<EvalStateResult<State>> | null>;
sortKey?: string
): Promise<SortKeyCacheResult<EvalStateResult<State>> | null>;
/**
* allows to manually flush state cache into underneath storage.
*/
flushCache(): Promise<void>;
putInCache<State>(contractTxId: string, transaction: GQLNodeInterface, state: EvalStateResult<State>): Promise<void>;
/**
* allows to syncState with an external state source (like Warp Distributed Execution Network)
*/
syncState(contractTxId: string, blockHeight: number, transactionId: string, state: any, validity: any): Promise<void>;
syncState(contractTxId: string, sortKey: string, state: any, validity: any): Promise<void>;
dumpCache(): Promise<any>;
internalWriteState<State>(
contractTxId: string,
sortKey: string
): Promise<SortKeyCacheResult<EvalStateResult<State>> | null>;
}
export class EvalStateResult<State> {
constructor(
readonly state: State,
readonly validity: Record<string, boolean>,
readonly errorMessages: Record<string, string>,
readonly transactionId?: string,
readonly blockId?: string
readonly errorMessages: Record<string, string>
) {}
}
@@ -120,7 +120,7 @@ export class DefaultEvaluationOptions implements EvaluationOptions {
walletBalanceUrl = 'http://nyc-1.dev.arweave.net:1984/';
}
// an interface for the contract EvaluationOptions - can be used to change the behaviour of some of the features.
// an interface for the contract EvaluationOptions - can be used to change the behaviour of some features.
export interface EvaluationOptions {
// whether exceptions from given transaction interaction should be ignored
ignoreExceptions: boolean;

View File

@@ -3,9 +3,12 @@ import {
Benchmark,
EvaluationOptions,
GQLEdgeInterface,
GQLNodeInterface,
GQLResultInterface,
GQLTransactionsResultInterface,
InteractionsLoader,
InteractionsSorter,
LexicographicalInteractionsSorter,
LoggerFactory,
sleep,
SmartWeaveTags
@@ -21,7 +24,7 @@ interface TagFilter {
interface BlockFilter {
min?: number;
max: number;
max?: number;
}
export interface GqlReqVariables {
@@ -70,18 +73,24 @@ export class ArweaveGatewayInteractionsLoader implements InteractionsLoader {
private static readonly _30seconds = 30 * 1000;
private readonly arweaveWrapper: ArweaveWrapper;
private readonly sorter: InteractionsSorter;
constructor(protected readonly arweave: Arweave) {
this.arweaveWrapper = new ArweaveWrapper(arweave);
this.sorter = new LexicographicalInteractionsSorter(arweave);
}
async load(
contractId: string,
fromBlockHeight: number,
toBlockHeight: number,
evaluationOptions: EvaluationOptions
): Promise<GQLEdgeInterface[]> {
this.logger.debug('Loading interactions for', { contractId, fromBlockHeight, toBlockHeight });
fromSortKey?: string,
toSortKey?: string,
evaluationOptions?: EvaluationOptions
): Promise<GQLNodeInterface[]> {
this.logger.debug('Loading interactions for', { contractId, fromSortKey, toSortKey });
const fromBlockHeight = this.sorter.extractBlockHeight(fromSortKey);
const toBlockHeight = this.sorter.extractBlockHeight(toSortKey);
const mainTransactionsVariables: GqlReqVariables = {
tags: [
{
@@ -123,14 +132,59 @@ export class ArweaveGatewayInteractionsLoader implements InteractionsLoader {
interactions = interactions.concat(innerWritesInteractions);
}
/**
* Because the behaviour of the Arweave gateway in case of passing null to min/max block height
* in the gql query params is unknown (https://discord.com/channels/908759493943394334/908766823342801007/983643012947144725)
* - we're removing all the interactions, that have null block data.
*/
interactions = interactions.filter((i) => i.node.block && i.node.block.id && i.node.block.height);
// note: this operation adds the "sortKey" to the interactions
let sortedInteractions = await this.sorter.sort(interactions);
if (fromSortKey || toSortKey) {
let fromIndex = null;
const maxIndex = sortedInteractions.length - 1;
let toIndex = null;
let breakFrom = false;
let breakTo = false;
for (let i = 0; i < sortedInteractions.length; i++) {
const sortedInteraction = sortedInteractions[i];
if (sortedInteraction.node.sortKey == fromSortKey) {
fromIndex = i + 1; // +1, because fromSortKey is exclusive
}
if (sortedInteraction.node.sortKey == toSortKey) {
toIndex = i + 1; // + 1, because "end" parameter in slice does not include the last element
}
if ((fromSortKey && fromIndex != null) || !fromSortKey) {
breakFrom = true;
}
if ((toSortKey && toIndex != null) || !toSortKey) {
breakTo = true;
}
if (breakFrom && breakTo) {
break;
}
}
this.logger.debug('Slicing:', {
fromIndex,
toIndex
});
// maxIndex + 1, because "end" parameter in slice does not include the last element
sortedInteractions = sortedInteractions.slice(fromIndex || 0, toIndex || maxIndex + 1);
}
this.logger.info('All loaded interactions:', {
from: fromBlockHeight,
to: toBlockHeight,
loaded: interactions.length,
from: fromSortKey,
to: toSortKey,
loaded: sortedInteractions.length,
time: loadingBenchmark.elapsed()
});
return interactions;
return sortedInteractions.map((i) => i.node);
}
private async loadPages(variables: GqlReqVariables) {

View File

@@ -1,15 +0,0 @@
import { GQLEdgeInterface, InteractionsSorter } from '@warp';
/**
* an implementation based on https://github.com/ArweaveTeam/SmartWeave/pull/82
*/
export class BlockHeightInteractionsSorter implements InteractionsSorter {
async sort(transactions: GQLEdgeInterface[]): Promise<GQLEdgeInterface[]> {
const copy = [...transactions];
return copy.sort(
(a: GQLEdgeInterface, b: GQLEdgeInterface) =>
a.node.block.height - b.node.block.height || a.node.id.localeCompare(b.node.id)
);
}
}

View File

@@ -1,11 +1,10 @@
import { BlockHeightCacheResult, BlockHeightKey, BlockHeightWarpCache } from '@warp/cache';
import { SortKeyCacheResult, SortKeyCache, StateCacheKey } from '@warp/cache';
import {
DefaultStateEvaluator,
EvalStateResult,
ExecutionContext,
ExecutionContextModifier,
HandlerApi,
StateCache
HandlerApi
} from '@warp/core';
import Arweave from 'arweave';
import { GQLNodeInterface } from '@warp/legacy';
@@ -25,7 +24,7 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
constructor(
arweave: Arweave,
private readonly cache: BlockHeightWarpCache<StateCache<unknown>>,
private readonly cache: SortKeyCache<EvalStateResult<unknown>>,
executionContextModifiers: ExecutionContextModifier[] = []
) {
super(arweave, executionContextModifiers);
@@ -35,67 +34,48 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
executionContext: ExecutionContext<State, HandlerApi<State>>,
currentTx: CurrentTx[]
): Promise<EvalStateResult<State>> {
const requestedBlockHeight = executionContext.blockHeight;
this.cLogger.debug(`Requested state block height: ${requestedBlockHeight}`);
const cachedState = executionContext.cachedState;
if (cachedState?.cachedHeight === requestedBlockHeight) {
if (cachedState && cachedState.sortKey == executionContext.requestedSortKey) {
this.cLogger.info(
`Exact cache hit for sortKey ${executionContext?.contractDefinition?.txId}:${cachedState.sortKey}`
);
executionContext.handler?.initState(cachedState.cachedValue.state);
return cachedState.cachedValue;
}
this.cLogger.debug('executionContext.sortedInteractions', executionContext.sortedInteractions.length);
const sortedInteractionsUpToBlock = executionContext.sortedInteractions.filter((tx) => {
return tx.node.block.height <= executionContext.blockHeight;
});
let missingInteractions = sortedInteractionsUpToBlock.slice();
this.cLogger.debug('missingInteractions', missingInteractions.length);
// if there was anything to cache...
if (sortedInteractionsUpToBlock.length > 0) {
if (cachedState != null) {
this.cLogger.debug(`Cached state for ${executionContext.contractDefinition.txId}`, {
cachedHeight: cachedState.cachedHeight,
requestedBlockHeight
});
// verify if for the requested block height there are any interactions
// with higher block height than latest value stored in cache - basically if there are any non-cached interactions.
missingInteractions = sortedInteractionsUpToBlock.filter(
({ node }) => node.block.height > cachedState.cachedHeight && node.block.height <= requestedBlockHeight
);
}
this.cLogger.debug(`Interactions until [${requestedBlockHeight}]`, {
total: sortedInteractionsUpToBlock.length,
cached: sortedInteractionsUpToBlock.length - missingInteractions.length
});
const missingInteractions = executionContext.sortedInteractions;
// TODO: this is tricky part, needs proper description
// for now: it prevents from infinite loop calls between calls that are making
// internal interact writes.
const contractTxId = executionContext.contractDefinition.txId;
// sanity check...
if (!contractTxId) {
throw new Error('Contract tx id not set in the execution context');
}
for (const entry of currentTx || []) {
if (entry.contractTxId === executionContext.contractDefinition.txId) {
const index = missingInteractions.findIndex((tx) => tx.node.id === entry.interactionTxId);
const index = missingInteractions.findIndex((tx) => tx.id === entry.interactionTxId);
if (index !== -1) {
this.cLogger.debug('Inf. Loop fix - removing interaction', {
height: missingInteractions[index].node.block.height,
height: missingInteractions[index].block.height,
contractTxId: entry.contractTxId,
interactionTxId: entry.interactionTxId
interactionTxId: entry.interactionTxId,
sortKey: missingInteractions[index].sortKey
});
missingInteractions.splice(index);
}
}
}
// if cache is up-to date - return immediately to speed-up the whole process
if (missingInteractions.length === 0 && cachedState) {
this.cLogger.debug(`State up to requested height [${requestedBlockHeight}] fully cached!`);
if (missingInteractions.length == 0) {
this.cLogger.info(`No missing interactions ${contractTxId}`);
if (cachedState) {
executionContext.handler?.initState(cachedState.cachedValue.state);
return cachedState.cachedValue;
} else {
executionContext.handler?.initState(executionContext.contractDefinition.initState);
return new EvalStateResult(executionContext.contractDefinition.initState, {}, {});
}
}
@@ -105,7 +85,7 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
const baseValidity = cachedState == null ? {} : cachedState.cachedValue.validity;
const baseErrorMessages = cachedState == null ? {} : cachedState.cachedValue.errorMessages;
// eval state for the missing transactions - starting from latest value from cache.
// eval state for the missing transactions - starting from the latest value from cache.
return await this.doReadState(
missingInteractions,
new EvalStateResult(baseState, baseValidity, baseErrorMessages || {}),
@@ -120,20 +100,11 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
state: EvalStateResult<State>
): Promise<void> {
const contractTxId = executionContext.contractDefinition.txId;
this.cLogger.debug(`onStateEvaluated: cache update for contract ${contractTxId} [${transaction.block.height}]`);
this.cLogger.debug(`onStateEvaluated: cache update for contract ${contractTxId} [${transaction.sortKey}]`);
// this will be problematic if we decide to cache only "onStateEvaluated" and containsInteractionsFromSequencer = true
// as a workaround, we're now caching every 100 interactions
await this.putInCache(
contractTxId,
transaction,
state,
executionContext.blockHeight,
executionContext.containsInteractionsFromSequencer
);
if (!executionContext.evaluationOptions.manualCacheFlush) {
await this.cache.flush();
}
await this.putInCache(contractTxId, transaction, state);
}
async onStateUpdate<State>(
@@ -143,36 +114,38 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
nthInteraction?: number
): Promise<void> {
if (
executionContext.evaluationOptions.updateCacheForEachInteraction ||
executionContext.evaluationOptions.internalWrites ||
(nthInteraction || 1) % 100 == 0
executionContext.evaluationOptions.updateCacheForEachInteraction /*||
executionContext.evaluationOptions.internalWrites*/ /*||
(nthInteraction || 1) % 100 == 0*/
) {
await this.putInCache(
executionContext.contractDefinition.txId,
transaction,
state,
executionContext.blockHeight,
executionContext.containsInteractionsFromSequencer
this.cLogger.debug(
`onStateUpdate: cache update for contract ${executionContext.contractDefinition.txId} [${transaction.sortKey}]`,
{
contract: executionContext.contractDefinition.txId,
state: state.state,
sortKey: transaction.sortKey
}
);
await this.putInCache(executionContext.contractDefinition.txId, transaction, state);
}
}
async latestAvailableState<State>(
contractTxId: string,
blockHeight: number
): Promise<BlockHeightCacheResult<EvalStateResult<State>> | null> {
this.cLogger.debug('Searching for', { contractTxId, blockHeight });
const stateCache = (await this.cache.getLessOrEqual(contractTxId, blockHeight)) as BlockHeightCacheResult<
StateCache<State>
sortKey?: string
): Promise<SortKeyCacheResult<EvalStateResult<State>> | null> {
this.cLogger.debug('Searching for', { contractTxId, sortKey });
if (sortKey) {
const stateCache = (await this.cache.getLessOrEqual(contractTxId, sortKey)) as SortKeyCacheResult<
EvalStateResult<State>
>;
this.cLogger.debug('Latest available state at', stateCache?.cachedHeight);
if (stateCache == null) {
return null;
if (stateCache) {
this.cLogger.debug(`Latest available state at ${contractTxId}: ${stateCache.sortKey}`);
}
return stateCache;
} else {
return (await this.cache.getLast(contractTxId)) as SortKeyCacheResult<EvalStateResult<State>>;
}
return new BlockHeightCacheResult<EvalStateResult<State>>(stateCache.cachedHeight, stateCache.cachedValue);
}
async onInternalWriteStateUpdate<State>(
@@ -181,9 +154,10 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
state: EvalStateResult<State>
): Promise<void> {
this.cLogger.debug('Internal write state update:', {
height: transaction.block.height,
sortKey: transaction.sortKey,
dry: transaction.dry,
contractTxId,
state
state: state.state
});
await this.putInCache(contractTxId, transaction, state);
}
@@ -193,17 +167,22 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
executionContext: ExecutionContext<State>,
state: EvalStateResult<State>
): Promise<void> {
// TODO: this has been properly fixed in the "leveldb" branch (1.2.0 version)
// switching off for now here, as in some very rare situations it can cause issues
// await this.putInCache(executionContext.contractDefinition.txId, transaction, state);
if (executionContext.sortedInteractions?.length == 0) {
return;
}
const txIndex = executionContext.sortedInteractions.indexOf(transaction);
const prevIndex = Math.max(0, txIndex - 1);
await this.putInCache(
executionContext.contractDefinition.txId,
executionContext.sortedInteractions[prevIndex],
state
);
}
protected async putInCache<State>(
public async putInCache<State>(
contractTxId: string,
transaction: GQLNodeInterface,
state: EvalStateResult<State>,
requestedBlockHeight: number = null,
containsInteractionsFromSequencer = false
state: EvalStateResult<State>
): Promise<void> {
if (transaction.dry) {
return;
@@ -211,43 +190,32 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
if (transaction.confirmationStatus !== undefined && transaction.confirmationStatus !== 'confirmed') {
return;
}
// example:
// requested - 10
// tx - 9, 10 - caching should be skipped
const txBlockHeight = transaction.block.height;
this.cLogger.debug(`requestedBlockHeight: ${requestedBlockHeight}, txBlockHeight: ${txBlockHeight}`);
if (
requestedBlockHeight !== null &&
txBlockHeight >= requestedBlockHeight - 1 &&
containsInteractionsFromSequencer
) {
this.cLogger.debug(`skipping caching of the last blocks`);
return;
}
const transactionId = transaction.id;
const stateToCache = new EvalStateResult(
state.state,
state.validity,
state.errorMessages || {},
transactionId,
transaction.block.id
);
const stateToCache = new EvalStateResult(state.state, state.validity, state.errorMessages || {});
await this.cache.put(new BlockHeightKey(contractTxId, txBlockHeight), stateToCache);
this.cLogger.debug('Putting into cache', {
contractTxId,
transaction: transaction.id,
sortKey: transaction.sortKey,
dry: transaction.dry,
state: stateToCache.state
});
await this.cache.put(new StateCacheKey(contractTxId, transaction.sortKey), stateToCache);
}
async flushCache(): Promise<void> {
return await this.cache.flush();
async syncState(contractTxId: string, sortKey: string, state: any, validity: any): Promise<void> {
const stateToCache = new EvalStateResult(state, validity, {});
await this.cache.put(new StateCacheKey(contractTxId, sortKey), stateToCache);
}
async syncState(
async dumpCache(): Promise<any> {
return await this.cache.dump();
}
async internalWriteState<State>(
contractTxId: string,
blockHeight: number,
transactionId: string,
state: any,
validity: any
): Promise<void> {
const stateToCache = new EvalStateResult(state, validity, {}, transactionId);
await this.cache.put(new BlockHeightKey(contractTxId, blockHeight), stateToCache);
sortKey: string
): Promise<SortKeyCacheResult<EvalStateResult<State>> | null> {
return (await this.cache.get(contractTxId, sortKey)) as SortKeyCacheResult<EvalStateResult<State>>;
}
}

View File

@@ -129,8 +129,10 @@ export class ContractHandlerApi<State> implements HandlerApi<State> {
await executionContext.warp.stateEvaluator.onInternalWriteStateUpdate(this.swGlobal._activeTx, contractTxId, {
state: result.state as State,
validity: {
...result.originalValidity,
[this.swGlobal._activeTx.id]: result.type == 'ok'
},
errorMessages: {
[this.swGlobal._activeTx.id]: result.errorMessage
}
});
@@ -161,16 +163,11 @@ export class ContractHandlerApi<State> implements HandlerApi<State> {
currentResult: EvalStateResult<State>,
interactionTx: GQLNodeInterface
) {
this.swGlobal.contracts.readContractState = async (
contractTxId: string,
height?: number,
returnValidity?: boolean
) => {
const requestedHeight = height || this.swGlobal.block.height;
this.swGlobal.contracts.readContractState = async (contractTxId: string, returnValidity?: boolean) => {
this.logger.debug('swGlobal.readContractState call:', {
from: this.contractDefinition.txId,
to: contractTxId,
height: requestedHeight,
sortKey: interactionTx.sortKey,
transaction: this.swGlobal.transaction.id
});
@@ -179,14 +176,13 @@ export class ContractHandlerApi<State> implements HandlerApi<State> {
await stateEvaluator.onContractCall(interactionTx, executionContext, currentResult);
const stateWithValidity = await childContract.readState(requestedHeight, [
const stateWithValidity = await childContract.readState(interactionTx.sortKey, [
...(currentTx || []),
{
contractTxId: this.contractDefinition.txId,
interactionTxId: this.swGlobal.transaction.id
}
]);
// TODO: it should be up to the client's code to decide which part of the result to use
// (by simply using destructuring operator)...
// but this (i.e. returning always stateWithValidity from here) would break backwards compatibility
@@ -198,7 +194,10 @@ export class ContractHandlerApi<State> implements HandlerApi<State> {
private assignRefreshState(executionContext: ExecutionContext<State>) {
this.swGlobal.contracts.refreshState = async () => {
const stateEvaluator = executionContext.warp.stateEvaluator;
const result = await stateEvaluator.latestAvailableState(this.swGlobal.contract.id, this.swGlobal.block.height);
const result = await stateEvaluator.latestAvailableState(
this.swGlobal.contract.id,
this.swGlobal._activeTx.sortKey
);
return result?.cachedValue.state;
};
}

View File

@@ -1,19 +1,18 @@
import {
Benchmark,
BlockHeightCacheResult,
canBeCached,
ContractInteraction,
CurrentTx,
EvalStateResult,
ExecutionContext,
ExecutionContextModifier,
GQLEdgeInterface,
GQLNodeInterface,
GQLTagInterface,
HandlerApi,
InteractionCall,
InteractionResult,
LoggerFactory,
SortKeyCacheResult,
StateEvaluator,
TagsParser,
VrfData
@@ -55,7 +54,7 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
}
protected async doReadState<State>(
missingInteractions: GQLEdgeInterface[],
missingInteractions: GQLNodeInterface[],
baseState: EvalStateResult<State>,
executionContext: ExecutionContext<State, HandlerApi<State>>,
currentTx: CurrentTx[]
@@ -83,16 +82,14 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
const missingInteraction = missingInteractions[i];
const singleInteractionBenchmark = Benchmark.measure();
const interactionTx: GQLNodeInterface = missingInteraction.node;
if (interactionTx.vrf) {
if (!this.verifyVrf(interactionTx.vrf, interactionTx.sortKey, this.arweave)) {
if (missingInteraction.vrf) {
if (!this.verifyVrf(missingInteraction.vrf, missingInteraction.sortKey, this.arweave)) {
throw new Error('Vrf verification failed.');
}
}
this.logger.debug(
`[${contractDefinition.txId}][${missingInteraction.node.id}][${missingInteraction.node.block.height}]: ${
`[${contractDefinition.txId}][${missingInteraction.id}][${missingInteraction.block.height}]: ${
missingInteractions.indexOf(missingInteraction) + 1
}/${missingInteractions.length} [of all:${sortedInteractions.length}]`
);
@@ -107,34 +104,34 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
const interactionCall: InteractionCall = contract
.getCallStack()
.addInteractionData({ interaction: null, interactionTx, currentTx });
.addInteractionData({ interaction: null, interactionTx: missingInteraction, currentTx });
// creating a Contract instance for the "writing" contract
const writingContract = executionContext.warp.contract(
writingContractTxId,
executionContext.contract,
interactionTx
missingInteraction
);
this.logger.debug('Reading state of the calling contract', interactionTx.block.height);
this.logger.debug('Reading state of the calling contract', missingInteraction.block.height);
/**
Reading the state of the writing contract.
This in turn will cause the state of THIS contract to be
updated in cache - see {@link ContractHandlerApi.assignWrite}
*/
await writingContract.readState(interactionTx.block.height, [
await writingContract.readState(missingInteraction.sortKey, [
...(currentTx || []),
{
contractTxId: contractDefinition.txId, //not: writingContractTxId!
interactionTxId: missingInteraction.node.id
interactionTxId: missingInteraction.id
}
]);
// loading latest state of THIS contract from cache
const newState = await this.latestAvailableState<State>(contractDefinition.txId, interactionTx.block.height);
const newState = await this.internalWriteState<State>(contractDefinition.txId, missingInteraction.sortKey);
this.logger.debug('New state:', {
height: interactionTx.block.height,
sortKey: missingInteraction.sortKey,
newState,
txId: contractDefinition.txId
});
@@ -143,29 +140,30 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
currentState = newState.cachedValue.state;
// we need to update the state in the wasm module
executionContext?.handler.initState(currentState);
validity[interactionTx.id] = newState.cachedValue.validity[interactionTx.id];
validity[missingInteraction.id] = newState.cachedValue.validity[missingInteraction.id];
if (newState.cachedValue.errorMessages?.[missingInteraction.id]) {
errorMessages[missingInteraction.id] = newState.cachedValue.errorMessages[missingInteraction.id];
}
const toCache = new EvalStateResult(currentState, validity, errorMessages);
// TODO: probably a separate hook should be created here
// to fix https://github.com/redstone-finance/warp/issues/109
await this.onStateUpdate<State>(interactionTx, executionContext, toCache);
if (canBeCached(interactionTx)) {
await this.onStateUpdate<State>(missingInteraction, executionContext, toCache);
if (canBeCached(missingInteraction)) {
lastConfirmedTxState = {
tx: interactionTx,
tx: missingInteraction,
state: toCache
};
}
} else {
validity[interactionTx.id] = false;
validity[missingInteraction.id] = false;
}
interactionCall.update({
cacheHit: false,
intermediaryCacheHit: false,
outputState: stackTrace.saveState ? currentState : undefined,
executionTime: singleInteractionBenchmark.elapsed(true) as number,
valid: validity[interactionTx.id],
valid: validity[missingInteraction.id],
errorMessage: errorMessage,
gasUsed: 0 // TODO...
});
@@ -175,23 +173,23 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
// "direct" interaction with this contract - "standard" processing
const inputTag = this.tagsParser.getInputTag(missingInteraction, executionContext.contractDefinition.txId);
if (!inputTag) {
this.logger.error(`Skipping tx - Input tag not found for ${interactionTx.id}`);
this.logger.error(`Skipping tx - Input tag not found for ${missingInteraction.id}`);
continue;
}
const input = this.parseInput(inputTag);
if (!input) {
this.logger.error(`Skipping tx - invalid Input tag - ${interactionTx.id}`);
this.logger.error(`Skipping tx - invalid Input tag - ${missingInteraction.id}`);
continue;
}
const interaction: ContractInteraction<unknown> = {
input,
caller: interactionTx.owner.address
caller: missingInteraction.owner.address
};
const interactionData = {
interaction,
interactionTx,
interactionTx: missingInteraction,
currentTx
};
@@ -206,19 +204,18 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
);
errorMessage = result.errorMessage;
if (result.type !== 'ok') {
errorMessages[interactionTx.id] = errorMessage;
errorMessages[missingInteraction.id] = errorMessage;
}
this.logResult<State>(result, interactionTx, executionContext);
this.logResult<State>(result, missingInteraction, executionContext);
this.logger.debug('Interaction evaluation', singleInteractionBenchmark.elapsed());
interactionCall.update({
cacheHit: false,
intermediaryCacheHit: false,
outputState: stackTrace.saveState ? currentState : undefined,
executionTime: singleInteractionBenchmark.elapsed(true) as number,
valid: validity[interactionTx.id],
valid: validity[missingInteraction.id],
errorMessage: errorMessage,
gasUsed: result.gasUsed
});
@@ -227,17 +224,17 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
throw new Error(`Exception while processing ${JSON.stringify(interaction)}:\n${result.errorMessage}`);
}
validity[interactionTx.id] = result.type === 'ok';
validity[missingInteraction.id] = result.type === 'ok';
currentState = result.state;
const toCache = new EvalStateResult(currentState, validity, errorMessages);
if (canBeCached(interactionTx)) {
if (canBeCached(missingInteraction)) {
lastConfirmedTxState = {
tx: interactionTx,
tx: missingInteraction,
state: toCache
};
}
await this.onStateUpdate<State>(interactionTx, executionContext, toCache, i);
await this.onStateUpdate<State>(missingInteraction, executionContext, toCache, i);
}
// I'm really NOT a fan of this "modify" feature, but I don't have idea how to better
@@ -306,8 +303,8 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
abstract latestAvailableState<State>(
contractTxId: string,
blockHeight: number
): Promise<BlockHeightCacheResult<EvalStateResult<State>> | null>;
sortKey?: string
): Promise<SortKeyCacheResult<EvalStateResult<State>> | null>;
abstract onContractCall<State>(
transaction: GQLNodeInterface,
@@ -334,13 +331,18 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
nthInteraction?: number
): Promise<void>;
abstract flushCache(): Promise<void>;
abstract syncState(
abstract putInCache<State>(
contractTxId: string,
blockHeight: number,
transactionId: string,
state: any,
validity: any
transaction: GQLNodeInterface,
state: EvalStateResult<State>
): Promise<void>;
abstract syncState(contractTxId: string, sortKey: string, state: any, validity: any): Promise<void>;
abstract dumpCache(): Promise<any>;
abstract internalWriteState<State>(
contractTxId: string,
sortKey: string
): Promise<SortKeyCacheResult<EvalStateResult<State>> | null>;
}

View File

@@ -1,12 +0,0 @@
import { GQLEdgeInterface, InteractionsSorter } from '@warp';
/**
* An implementation of {@link InteractionsSorter} that is meant to be used
* with Warp gateway (or any other gateway, that returns interactions
* sorted according to the protocol specs)
*/
export class EmptyInteractionsSorter implements InteractionsSorter {
async sort(transactions: GQLEdgeInterface[]): Promise<GQLEdgeInterface[]> {
return transactions;
}
}

View File

@@ -6,6 +6,8 @@ const defaultArweaveMs = ''.padEnd(13, '9');
const defaultArweaveMs_After_Block_973730 = ''.padEnd(13, '0');
export const block_973730 = 973730;
const sortingLast = ''.padEnd(64, 'z');
/**
* implementation that is based on current's SDK sorting alg.
*/
@@ -19,19 +21,7 @@ export class LexicographicalInteractionsSorter implements InteractionsSorter {
const addKeysFuncs = copy.map((tx) => this.addSortKey(tx));
await Promise.all(addKeysFuncs);
return copy.sort((a, b) => a.sortKey.localeCompare(b.sortKey));
}
private async addSortKey(txInfo: GQLEdgeInterface) {
const { node } = txInfo;
// might have been already set by the Warp Sequencer
if (txInfo.node.sortKey !== undefined && txInfo.node.source == SourceType.WARP_SEQUENCER) {
this.logger.debug('Using sortkey from sequencer', txInfo.node.sortKey);
txInfo.sortKey = txInfo.node.sortKey;
} else {
txInfo.sortKey = await this.createSortKey(node.block.id, node.id, node.block.height);
}
return copy.sort((a, b) => a.node.sortKey.localeCompare(b.node.sortKey));
}
public async createSortKey(blockId: string, transactionId: string, blockHeight: number) {
@@ -46,4 +36,25 @@ export class LexicographicalInteractionsSorter implements InteractionsSorter {
return `${blockHeightString},${arweaveMs},${hashed}`;
}
public extractBlockHeight(sortKey?: string): number | null {
// I feel sorry for myself...
return sortKey ? parseInt(sortKey.split(',')[0]) : null;
}
private async addSortKey(txInfo: GQLEdgeInterface) {
const { node } = txInfo;
// might have been already set by the Warp Sequencer
if (txInfo.node.sortKey !== undefined && txInfo.node.source == SourceType.WARP_SEQUENCER) {
this.logger.debug('Using sortKey from sequencer', txInfo.node.sortKey);
} else {
txInfo.node.sortKey = await this.createSortKey(node.block.id, node.id, node.block.height);
}
}
generateLastSortKey(blockHeight: number): string {
const blockHeightString = `${blockHeight}`.padStart(12, '0');
return `${blockHeightString},${defaultArweaveMs},${sortingLast}`;
}
}

View File

@@ -1,8 +1,6 @@
import { EvalStateResult, GQLNodeInterface } from '@warp';
//export type StateCache<State> = Array<EvalStateResult<State>>;
export type StateCache<State> = EvalStateResult<State>;
export function canBeCached(tx: GQLNodeInterface): boolean {
// in case of using non-redstone gateway
if (tx.confirmationStatus === undefined) {

View File

@@ -1,4 +1,4 @@
import { GQLEdgeInterface, GQLTagInterface, LoggerFactory, SmartWeaveTags } from '@warp';
import { GQLNodeInterface, GQLTagInterface, LoggerFactory, SmartWeaveTags } from '@warp';
/**
* A class that is responsible for retrieving "input" tag from the interaction transaction.
@@ -12,20 +12,20 @@ import { GQLEdgeInterface, GQLTagInterface, LoggerFactory, SmartWeaveTags } from
export class TagsParser {
private readonly logger = LoggerFactory.INST.create('TagsParser');
getInputTag(interactionTransaction: GQLEdgeInterface, contractTxId: string): GQLTagInterface {
getInputTag(interactionTransaction: GQLNodeInterface, contractTxId: string): GQLTagInterface {
// this is the part to retain compatibility with https://github.com/ArweaveTeam/SmartWeave/pull/51
if (TagsParser.hasMultipleInteractions(interactionTransaction)) {
this.logger.debug('Interaction transaction is using multiple input tx tag format.');
const contractTagIndex = interactionTransaction.node.tags.findIndex(
const contractTagIndex = interactionTransaction.tags.findIndex(
(tag) => tag.name === SmartWeaveTags.CONTRACT_TX_ID && tag.value === contractTxId
);
// if "Contract" is the last tag
if (interactionTransaction.node.tags.length - 1 === contractTagIndex) {
if (interactionTransaction.tags.length - 1 === contractTagIndex) {
this.logger.warn("Wrong tags format: 'Contract' is the last tag");
return undefined;
}
// in this case the "Input" tag MUST be right after the "Contract" tag
const inputTag = interactionTransaction.node.tags[contractTagIndex + 1];
const inputTag = interactionTransaction.tags[contractTagIndex + 1];
// if the tag after "Contract" tag has wrong name
if (inputTag.name !== SmartWeaveTags.INPUT) {
this.logger.warn(`No 'Input' tag found after 'Contract' tag. Instead ${inputTag.name} was found`);
@@ -37,30 +37,28 @@ export class TagsParser {
// the "old way" - i.e. tags ordering does not matter,
// if there is at most one "Contract" tag
// - so returning the first occurrence of "Input" tag.
return interactionTransaction.node.tags.find((tag) => tag.name === SmartWeaveTags.INPUT);
return interactionTransaction.tags.find((tag) => tag.name === SmartWeaveTags.INPUT);
}
}
isInteractWrite(interactionTransaction: GQLEdgeInterface, contractTxId: string): boolean {
return interactionTransaction.node.tags.some(
isInteractWrite(interactionTransaction: GQLNodeInterface, contractTxId: string): boolean {
return interactionTransaction.tags.some(
(tag) => tag.name === SmartWeaveTags.INTERACT_WRITE && tag.value === contractTxId
);
}
getInteractWritesContracts(interactionTransaction: GQLEdgeInterface): string[] {
return interactionTransaction.node.tags
.filter((tag) => tag.name === SmartWeaveTags.INTERACT_WRITE)
.map((t) => t.value);
getInteractWritesContracts(interactionTransaction: GQLNodeInterface): string[] {
return interactionTransaction.tags.filter((tag) => tag.name === SmartWeaveTags.INTERACT_WRITE).map((t) => t.value);
}
getContractTag(interactionTransaction: GQLEdgeInterface): string {
return interactionTransaction.node.tags.find((tag) => tag.name === SmartWeaveTags.CONTRACT_TX_ID)?.value;
getContractTag(interactionTransaction: GQLNodeInterface): string {
return interactionTransaction.tags.find((tag) => tag.name === SmartWeaveTags.CONTRACT_TX_ID)?.value;
}
getContractsWithInputs(interactionTransaction: GQLEdgeInterface): Map<string, GQLTagInterface> {
getContractsWithInputs(interactionTransaction: GQLNodeInterface): Map<string, GQLTagInterface> {
const result = new Map<string, GQLTagInterface>();
const contractTags = interactionTransaction.node.tags.filter((tag) => tag.name === SmartWeaveTags.CONTRACT_TX_ID);
const contractTags = interactionTransaction.tags.filter((tag) => tag.name === SmartWeaveTags.CONTRACT_TX_ID);
contractTags.forEach((contractTag) => {
result.set(contractTag.value, this.getInputTag(interactionTransaction, contractTag.value));
@@ -69,7 +67,7 @@ export class TagsParser {
return result;
}
static hasMultipleInteractions(interactionTransaction): boolean {
return interactionTransaction.node.tags.filter((tag) => tag.name === SmartWeaveTags.CONTRACT_TX_ID).length > 1;
static hasMultipleInteractions(interactionTransaction: GQLNodeInterface): boolean {
return interactionTransaction.tags.filter((tag) => tag.name === SmartWeaveTags.CONTRACT_TX_ID).length > 1;
}
}

View File

@@ -1,7 +1,6 @@
import {
Benchmark,
EvaluationOptions,
GQLEdgeInterface,
GQLNodeInterface,
InteractionsLoader,
LoggerFactory,
@@ -60,11 +59,7 @@ export const enum SourceType {
*
* Passing no flag is the "backwards compatible" mode (ie. it will behave like the original Arweave GQL gateway endpoint).
* Note that this may result in returning corrupted and/or forked interactions
* - read more {@link https://github.com/redstone-finance/redstone-sw-gateway#corrupted-transactions}.
*
* Please note that currently caching (ie. {@link CacheableContractInteractionsLoader} is switched off
* for WarpGatewayInteractionsLoader due to the issue mentioned in the
* following comment {@link https://github.com/redstone-finance/warp/pull/62#issuecomment-995249264}
* - read more {@link https://github.com/warp-contracts/redstone-sw-gateway#corrupted-transactions}.
*/
export class WarpGatewayInteractionsLoader implements InteractionsLoader {
constructor(
@@ -81,14 +76,13 @@ export class WarpGatewayInteractionsLoader implements InteractionsLoader {
async load(
contractId: string,
fromBlockHeight: number,
toBlockHeight: number,
evaluationOptions?: EvaluationOptions,
upToTransactionId?: string
): Promise<GQLEdgeInterface[]> {
this.logger.debug('Loading interactions: for ', { contractId, fromBlockHeight, toBlockHeight });
fromSortKey?: string,
toSortKey?: string,
evaluationOptions?: EvaluationOptions
): Promise<GQLNodeInterface[]> {
this.logger.debug('Loading interactions: for ', { contractId, fromSortKey, toSortKey });
const interactions: GQLEdgeInterface[] = [];
const interactions: GQLNodeInterface[] = [];
let page = 0;
let totalPages = 0;
@@ -96,17 +90,15 @@ export class WarpGatewayInteractionsLoader implements InteractionsLoader {
do {
const benchmarkRequestTime = Benchmark.measure();
// to make caching in cloudfront possible
const url = `${this.baseUrl}/gateway/interactions-sort-key`;
const response = await fetch(
`${url}?${new URLSearchParams({
contractId: contractId,
from: fromBlockHeight.toString(),
to: toBlockHeight.toString(),
...(fromSortKey ? { from: fromSortKey } : ''),
...(toSortKey ? { to: toSortKey } : ''),
page: (++page).toString(),
minimize: 'true',
...(upToTransactionId ? { upToTransactionId } : ''),
...(this.confirmationStatus && this.confirmationStatus.confirmed ? { confirmationStatus: 'confirmed' } : ''),
...(this.confirmationStatus && this.confirmationStatus.notCorrupted
? { confirmationStatus: 'not_corrupted' }
@@ -129,24 +121,19 @@ export class WarpGatewayInteractionsLoader implements InteractionsLoader {
`Loading interactions: page ${page} of ${totalPages} loaded in ${benchmarkRequestTime.elapsed()}`
);
response.interactions.forEach((interaction) =>
response.interactions.forEach((interaction) => {
interactions.push({
cursor: '',
node: {
...interaction.interaction,
confirmationStatus: interaction.status
}
})
);
});
});
this.logger.debug(
`Loaded interactions length: ${interactions.length}, from: ${fromBlockHeight}, to: ${toBlockHeight}`
);
this.logger.debug(`Loaded interactions length: ${interactions.length}, from: ${fromSortKey}, to: ${toSortKey}`);
} while (page < totalPages);
this.logger.debug('All loaded interactions:', {
from: fromBlockHeight,
to: toBlockHeight,
from: fromSortKey,
to: toSortKey,
loaded: interactions.length,
time: benchmarkTotalTime.elapsed()
});

View File

@@ -38,7 +38,7 @@ export class WasmContractHandlerApi<State> implements HandlerApi<State> {
this.swGlobal._activeTx = interactionTx;
this.swGlobal.caller = interaction.caller; // either contract tx id (for internal writes) or transaction.owner
// TODO: this should be rather set on the HandlerFactory level..
// but currently no access evaluationOptions there
// but currently no access to evaluationOptions there
this.swGlobal.gasLimit = executionContext.evaluationOptions.gasLimit;
this.swGlobal.gasUsed = 0;
@@ -192,9 +192,9 @@ export class WasmContractHandlerApi<State> implements HandlerApi<State> {
const { stateEvaluator } = executionContext.warp;
const childContract = executionContext.warp.contract(contractTxId, executionContext.contract, interactionTx);
await stateEvaluator.onContractCall(interactionTx, executionContext, currentResult);
// await stateEvaluator.onContractCall(interactionTx, executionContext, currentResult);
const stateWithValidity = await childContract.readState(requestedHeight, [
const stateWithValidity = await childContract.readState(interactionTx.sortKey, [
...(currentTx || []),
{
contractTxId: this.contractDefinition.txId,
@@ -242,10 +242,8 @@ export class WasmContractHandlerApi<State> implements HandlerApi<State> {
this.logger.debug('Cache result?:', !this.swGlobal._activeTx.dry);
await executionContext.warp.stateEvaluator.onInternalWriteStateUpdate(this.swGlobal._activeTx, contractTxId, {
state: result.state as State,
validity: {
...result.originalValidity,
[this.swGlobal._activeTx.id]: result.type == 'ok'
}
validity: {},
errorMessages: {}
});
return result;

View File

@@ -1,113 +0,0 @@
import Arweave from 'arweave';
import {
ArweaveGatewayInteractionsLoader,
CacheableStateEvaluator,
ConfirmationStatus,
ContractDefinitionLoader,
EmptyInteractionsSorter,
HandlerExecutorFactory,
LexicographicalInteractionsSorter,
R_GW_URL,
WarpGatewayContractDefinitionLoader,
WarpGatewayInteractionsLoader,
Warp,
WarpBuilder,
WarpWebFactory
} from '@warp/core';
import { CacheableExecutorFactory, Evolve } from '@warp/plugins';
import { FileBlockHeightWarpCache, MemCache } from '@warp/cache';
import { Knex } from 'knex';
import { KnexStateCache } from '../../cache/impl/KnexStateCache';
/**
* A {@link Warp} factory that can be safely used only in Node.js env.
*/
export class WarpNodeFactory extends WarpWebFactory {
/**
* Returns a fully configured, memcached {@link Warp} that is suitable for tests with ArLocal
*/
static forTesting(arweave: Arweave): Warp {
return this.memCachedBased(arweave).useArweaveGateway().build();
}
/**
* Returns a fully configured {@link Warp} that is using file-based cache for {@link StateEvaluator} layer
* and mem cache for the rest.
*
* @param cacheBasePath - path where cache files will be stored
* @param maxStoredInMemoryBlockHeights - how many cache entries per contract will be stored in
* the underneath mem-cache
*
*/
static fileCached(arweave: Arweave, cacheBasePath?: string, maxStoredInMemoryBlockHeights = 10): Warp {
return this.fileCachedBased(arweave, cacheBasePath, maxStoredInMemoryBlockHeights).build();
}
/**
* Returns a preconfigured, fileCached {@link WarpBuilder}, that allows for customization of the Warp instance.
* Use {@link WarpBuilder.build()} to finish the configuration.
* @param cacheBasePath - see {@link fileCached.cacheBasePath}
* @param maxStoredInMemoryBlockHeights - see {@link fileCached.maxStoredInMemoryBlockHeights}
*
*/
static fileCachedBased(
arweave: Arweave,
cacheBasePath?: string,
maxStoredInMemoryBlockHeights = 10,
confirmationStatus: ConfirmationStatus = { notCorrupted: true }
): WarpBuilder {
const interactionsLoader = new WarpGatewayInteractionsLoader(R_GW_URL, confirmationStatus);
const definitionLoader = new WarpGatewayContractDefinitionLoader(R_GW_URL, arweave, new MemCache());
const executorFactory = new CacheableExecutorFactory(arweave, new HandlerExecutorFactory(arweave), new MemCache());
const stateEvaluator = new CacheableStateEvaluator(
arweave,
new FileBlockHeightWarpCache(cacheBasePath, maxStoredInMemoryBlockHeights),
[new Evolve(definitionLoader, executorFactory)]
);
const interactionsSorter = new EmptyInteractionsSorter();
return Warp.builder(arweave)
.setDefinitionLoader(definitionLoader)
.setInteractionsLoader(interactionsLoader)
.useWarpGwInfo()
.setInteractionsSorter(interactionsSorter)
.setExecutorFactory(executorFactory)
.setStateEvaluator(stateEvaluator);
}
static async knexCached(arweave: Arweave, dbConnection: Knex, maxStoredInMemoryBlockHeights = 10): Promise<Warp> {
return (await this.knexCachedBased(arweave, dbConnection, maxStoredInMemoryBlockHeights)).build();
}
/**
*/
static async knexCachedBased(
arweave: Arweave,
dbConnection: Knex,
maxStoredInMemoryBlockHeights = 10,
confirmationStatus: ConfirmationStatus = { notCorrupted: true }
): Promise<WarpBuilder> {
const interactionsLoader = new WarpGatewayInteractionsLoader(R_GW_URL, confirmationStatus);
const definitionLoader = new WarpGatewayContractDefinitionLoader(R_GW_URL, arweave, new MemCache());
const executorFactory = new CacheableExecutorFactory(arweave, new HandlerExecutorFactory(arweave), new MemCache());
const stateEvaluator = new CacheableStateEvaluator(
arweave,
await KnexStateCache.init(dbConnection, maxStoredInMemoryBlockHeights),
[new Evolve(definitionLoader, executorFactory)]
);
const interactionsSorter = new EmptyInteractionsSorter();
return Warp.builder(arweave)
.setDefinitionLoader(definitionLoader)
.setInteractionsLoader(interactionsLoader)
.useWarpGwInfo()
.setInteractionsSorter(interactionsSorter)
.setExecutorFactory(executorFactory)
.setStateEvaluator(stateEvaluator);
}
}

View File

@@ -1,60 +0,0 @@
import Arweave from 'arweave';
import { CacheableExecutorFactory, Evolve } from '@warp/plugins';
import {
CacheableStateEvaluator,
ConfirmationStatus,
EmptyInteractionsSorter,
HandlerExecutorFactory,
R_GW_URL,
WarpGatewayContractDefinitionLoader,
WarpGatewayInteractionsLoader,
Warp,
WarpBuilder,
StateCache
} from '@warp/core';
import { MemBlockHeightWarpCache, MemCache, RemoteBlockHeightCache } from '@warp/cache';
/**
* A factory that simplifies the process of creating different versions of {@link Warp}.
* All versions use the {@link Evolve} plugin.
* Warp instances created by this factory can be safely used in a web environment.
*/
export class WarpWebFactory {
/**
* Returns a fully configured {@link Warp} that is using mem cache for all layers.
*/
static memCached(arweave: Arweave, maxStoredBlockHeights = 10): Warp {
return this.memCachedBased(arweave, maxStoredBlockHeights).build();
}
/**
* Returns a preconfigured, memCached {@link WarpBuilder}, that allows for customization of the Warp instance.
* Use {@link WarpBuilder.build()} to finish the configuration.
*/
static memCachedBased(
arweave: Arweave,
maxStoredBlockHeights = 10,
confirmationStatus: ConfirmationStatus = { notCorrupted: true }
): WarpBuilder {
const interactionsLoader = new WarpGatewayInteractionsLoader(R_GW_URL, confirmationStatus);
const definitionLoader = new WarpGatewayContractDefinitionLoader(R_GW_URL, arweave, new MemCache());
const executorFactory = new CacheableExecutorFactory(arweave, new HandlerExecutorFactory(arweave), new MemCache());
const stateEvaluator = new CacheableStateEvaluator(
arweave,
new MemBlockHeightWarpCache<StateCache<unknown>>(maxStoredBlockHeights),
[new Evolve(definitionLoader, executorFactory)]
);
const interactionsSorter = new EmptyInteractionsSorter();
return Warp.builder(arweave)
.setDefinitionLoader(definitionLoader)
.setInteractionsLoader(interactionsLoader)
.useWarpGwInfo()
.setInteractionsSorter(interactionsSorter)
.setExecutorFactory(executorFactory)
.setStateEvaluator(stateEvaluator);
}
}

View File

@@ -4,7 +4,7 @@ import Transaction from 'arweave/node/lib/transaction';
import { CreateTransactionInterface } from 'arweave/node/common';
import { BlockData } from 'arweave/node/blocks';
export async function createTx(
export async function createInteractionTx(
arweave: Arweave,
signer: SigningFunction,
contractId: string,
@@ -12,7 +12,7 @@ export async function createTx(
tags: { name: string; value: string }[],
target = '',
winstonQty = '0',
bundle = false
dummy = false
): Promise<Transaction> {
const options: Partial<CreateTransactionInterface> = {
data: Math.random().toString().slice(-4)
@@ -29,7 +29,7 @@ export async function createTx(
// that are bundled. So to speed up the procees (and prevent the arweave-js
// from calling /tx_anchor and /price endpoints) - we're presetting theses
// values here
if (bundle) {
if (dummy) {
options.reward = '72600854';
options.last_tx = 'p7vc1iSP6bvH_fCeUFa9LqoV5qiyW-jdEKouAT0XMoSwrNraB9mgpi29Q10waEpO';
}

View File

@@ -62,9 +62,6 @@ export interface VrfData {
}
export interface GQLEdgeInterface {
// added dynamically by the LexicographicalInteractionsSorter
// or rewritten from GQLNodeInterface.sortKey (if added there by Warp Sequencer)
sortKey?: string;
cursor: string;
node: GQLNodeInterface;
}

View File

@@ -2,4 +2,4 @@ export * from './gqlResult';
export * from './smartweave-global';
export * from './errors';
export * from './utils';
export * from './create-tx';
export * from './create-interaction-tx';

View File

@@ -1,82 +0,0 @@
import {
Benchmark,
BlockHeightKey,
BlockHeightWarpCache,
EvaluationOptions,
GQLEdgeInterface,
InteractionsLoader,
LoggerFactory
} from '@warp';
/**
* This implementation of the {@link InteractionsLoader} tries to limit the amount of interactions
* with GraphQL endpoint. Additionally, it is downloading only the missing interactions
* (starting from the latest already cached) - to additionally limit the amount of "paging".
*/
export class CacheableContractInteractionsLoader implements InteractionsLoader {
private readonly logger = LoggerFactory.INST.create('CacheableContractInteractionsLoader');
constructor(
private readonly baseImplementation: InteractionsLoader,
private readonly cache: BlockHeightWarpCache<GQLEdgeInterface[]>
) {}
async load(
contractId: string,
fromBlockHeight: number,
toBlockHeight: number,
evaluationOptions?: EvaluationOptions
): Promise<GQLEdgeInterface[]> {
const benchmark = Benchmark.measure();
this.logger.debug('Loading interactions', {
contractId,
fromBlockHeight,
toBlockHeight
});
const { cachedHeight, cachedValue } = (await this.cache.getLast(contractId)) || {
cachedHeight: -1,
cachedValue: []
};
if (cachedHeight >= toBlockHeight) {
this.logger.debug('Reusing interactions cached at higher block height:', cachedHeight);
return cachedValue.filter(
(interaction: GQLEdgeInterface) =>
interaction.node.block.height >= fromBlockHeight && interaction.node.block.height <= toBlockHeight
);
}
this.logger.trace('Cached:', {
cachedHeight,
cachedValue
});
const missingInteractions = await this.baseImplementation.load(
contractId,
Math.max(cachedHeight + 1, fromBlockHeight),
toBlockHeight,
evaluationOptions
);
const result = cachedValue
.filter((interaction: GQLEdgeInterface) => interaction.node.block.height >= fromBlockHeight)
.concat(missingInteractions);
const valueToCache = cachedValue.concat(missingInteractions);
this.logger.debug('Interactions load result:', {
cached: cachedValue.length,
missing: missingInteractions.length,
total: valueToCache.length,
result: result.length
});
// note: interactions in cache should be always saved from the "0" block
// - that's why "result" variable is not used here
await this.cache.put(new BlockHeightKey(contractId, toBlockHeight), valueToCache);
this.logger.debug(`Interactions loaded in ${benchmark.elapsed()}`);
return result;
}
}

View File

@@ -1,38 +1,17 @@
import {
DefinitionLoader,
EvolveState,
ExecutionContext,
ExecutionContextModifier,
ExecutorFactory,
HandlerApi,
LoggerFactory,
SmartWeaveError,
SmartWeaveErrorType
} from '@warp';
/*
...I'm still not fully convinced to the whole "evolve" idea.
IMO It makes it very hard to audit what exactly the smart contract's code at given txId is doing (as it requires
to analyse its whole interactions history and verify if some of them do not modify original contract's source code).
IMO instead of using "evolve" feature - a new contract version should be deployed (with "output state"
from previous version set as "input state" for the new version).
Instead of using "evolve" feature - one could utilise the "contracts-registry" approach:
https://github.com/redstone-finance/redstone-smartweave-contracts/blob/main/src/contracts-registry/contracts-registry.contract.ts
https://viewblock.io/arweave/address/XQkGzXG6YknJyy-YbakEZvQKAWkW2_aPRhc3ShC8lyA?tab=state
- it keeps track of all the versions of the given contract and allows to retrieve the latest version by contract's "business" name -
without the need of hard-coding contract's txId in the client's source code.
This also makes it easier to audit given contract - as you keep all its versions in one place.
*/
function isEvolveCompatible(state: unknown): state is EvolveState {
if (!state) {
return false;
}
const settings = evalSettings(state);
return (state as EvolveState).evolve !== undefined || settings.has('evolve');
@@ -41,10 +20,7 @@ function isEvolveCompatible(state: unknown): state is EvolveState {
export class Evolve implements ExecutionContextModifier {
private readonly logger = LoggerFactory.INST.create('Evolve');
constructor(
private readonly definitionLoader: DefinitionLoader,
private readonly executorFactory: ExecutorFactory<HandlerApi<unknown>>
) {
constructor() {
this.modify = this.modify.bind(this);
}
@@ -52,11 +28,10 @@ export class Evolve implements ExecutionContextModifier {
state: State,
executionContext: ExecutionContext<State, HandlerApi<State>>
): Promise<ExecutionContext<State, HandlerApi<State>>> {
const { definitionLoader, executorFactory } = executionContext.warp;
const contractTxId = executionContext.contractDefinition.txId;
this.logger.debug(`trying to evolve for: ${contractTxId}`);
const evolvedSrcTxId = Evolve.evolvedSrcTxId(state);
const currentSrcTxId = executionContext.contractDefinition.srcTxId;
if (evolvedSrcTxId) {
@@ -70,8 +45,8 @@ export class Evolve implements ExecutionContextModifier {
// note: that's really nasty IMO - loading original contract definition,
// but forcing different sourceTxId...
this.logger.info('Evolving to: ', evolvedSrcTxId);
const newContractDefinition = await this.definitionLoader.load<State>(contractTxId, evolvedSrcTxId);
const newHandler = (await this.executorFactory.create<State>(
const newContractDefinition = await definitionLoader.load<State>(contractTxId, evolvedSrcTxId);
const newHandler = (await executorFactory.create<State>(
newContractDefinition,
executionContext.evaluationOptions
)) as HandlerApi<State>;

View File

@@ -1,4 +1,3 @@
export * from './CacheableContractInteractionsLoader';
export * from './CacheableExecutorFactory';
export * from './DebuggableExecutorFactor';
export * from './Evolve';

View File

@@ -1,6 +1,6 @@
import Arweave from 'arweave';
import { NetworkInfoInterface } from 'arweave/node/network';
import { GqlReqVariables, LoggerFactory, R_GW_URL } from '@warp';
import { GqlReqVariables, LoggerFactory, WARP_GW_URL } from '@warp';
import { AxiosResponse } from 'axios';
import Transaction from 'arweave/node/lib/transaction';
import { Buffer as isomorphicBuffer } from 'redstone-isomorphic';
@@ -16,7 +16,7 @@ export class ArweaveWrapper {
}
async rGwInfo(): Promise<NetworkInfoInterface> {
return await this.doFetchInfo(`${R_GW_URL}/gateway/arweave/info`);
return await this.doFetchInfo(`${WARP_GW_URL}/gateway/arweave/info`);
}
async info(): Promise<NetworkInfoInterface> {

View File

@@ -1,31 +0,0 @@
/* eslint-disable */
import { InteractionsLoader } from '../src/core/modules/InteractionsLoader';
import { GQLEdgeInterface } from '../src/legacy/gqlResult';
import * as fs from 'fs';
import {ArweaveGatewayInteractionsLoader, LoggerFactory} from '../src';
import { EvaluationOptions } from '../src/core/modules/StateEvaluator';
export class FromContractInteractionsLoader extends ArweaveGatewayInteractionsLoader {
private readonly logger = LoggerFactory.INST.create('FromContractInteractionsLoader');
private _contractTxId: string;
constructor(contractTxId: string) {
super();
this._contractTxId = contractTxId;
}
async load(
contractId: string,
fromBlockHeight: number,
toBlockHeight: number,
evaluationOptions: EvaluationOptions
): Promise<GQLEdgeInterface[]> {
return await super.load(this._contractTxId, fromBlockHeight, toBlockHeight, evaluationOptions);
}
set contractTxId(value: string) {
this._contractTxId = value;
}
}

View File

@@ -1,60 +0,0 @@
/* eslint-disable */
import { InteractionsLoader } from '../src/core/modules/InteractionsLoader';
import { GQLEdgeInterface } from '../src/legacy/gqlResult';
import * as fs from 'fs';
import { LoggerFactory } from '../src';
import { EvaluationOptions } from '../src/core/modules/StateEvaluator';
const brokenTransactions = [
'3O5Nvfbj72BDJT2bDC5EUm6gmkManJADsn93vKzQISU',
'6uNZj-IV5sDx2Rpe7E2Jh_8phHzmDwts771mwbbuZc4',
'oQt1SJz5dxNxyjYBMPCsthUR0OyhTLTwrnNH9rbcOE4',
't2LOZSWW8u4G8a8gQqIoN9MdczQb7mIflPuQG7MGgtU',
'v6bGNzNMTb7fj_q_KwRyLH2pSN6rSmPzHXUvrfDPYHs',
'vofahl_F506NkD6dP-1gYis-1N6sWQnfcXazDhoKaiQ',
'z2fZzeB_466S9kTikjA2RihwEuBVUUe9FAceYj_KKtA',
'nk1IIv4dM8ACzm9fwsxCKjngxWo4yMu6sqYr-Tqmp0I',
'k0789IzsSppZl3egmQxX_Slx8VmMig4fQJaxyztVSV8',
'lkjesyJ6Sr_flKak2FKd8As8FW-1k8wygRf8hjkTAfI',
'2aHIKrdEvu-cUfalvOdcdqq79oVb41PBSgiAXr7epoc',
'2QQxeYer5mranQLWBKLUGvbwhiqcGucAeB-puYB9hIM',
'2VHl88d-YQWngGGhyBrluF5VNxY273_uE30AJ0qI_hY',
'-3h01LpYQEd5bNXUfsSexYr-ak7G0ZPumLArZ-cuJ7I',
'3qVrnEcApWEeVn4BDzN-aIDrAFIrPPTsQKbXxDYnquc',
'4a9YiAXCavz22Gn0EFQ1_B9tNpRMUWvzsBeAarzR1c8',
'50DJFXPa0l0mbjZDgqpghM9mz7CxGKez7kebvI79NJA',
'7rqrFz3Jr8FZ5LYL2zSZbXrRvSXRwE8lZMpIOecKiag',
'7sRE3KSkyhUYuU2ZaX-D5Sk5FQ2sF9KucwWZFu877fw',
'8Fs-aLJgp8diQ5unp-hkli5oTBSDGnvQdIIrzfkCc0E',
'8Yzk29D2JzwqYmwdA91z_ZqfG1jW2hXX1lhh3HY9fxY',
'-b8gqnEsZp0AafO6tHTttTliGXu858vqolGs122dsaM',
'_GR5BE5kae1JkCMUcbecJBuryqNzuAzd8BIVLey4CJA',
'-k8bLMFysvyjKlakQaffbYyCSlZAGC7ZFq0KjhTVoKU',
'-Q8A_3JXH3yZms7awAhK2PFCinWfCzm1gvaa6ogi6O4'
];
export class FromFileInteractionsLoader implements InteractionsLoader {
private readonly logger = LoggerFactory.INST.create('FromFileInteractionsLoader');
private readonly transactions: GQLEdgeInterface[] = [];
constructor(filePath: string) {
const fileContent = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
this.transactions = (fileContent.cachedValue as GQLEdgeInterface[]).filter((e) => {
const skip = brokenTransactions.indexOf(e.node.id) >= 0;
if (skip) {
this.logger.debug('Skipping', e.node.id);
}
return !skip;
});
}
async load(
contractId: string,
fromBlockHeight: number,
toBlockHeight: number,
evaluationOptions: EvaluationOptions
): Promise<GQLEdgeInterface[]> {
return this.transactions;
}
}

View File

@@ -4,7 +4,7 @@ import { LoggerFactory, mapReplacer } from '../src';
import { TsLogFactory } from '../src/logging/node/TsLogFactory';
import fs from 'fs';
import path from 'path';
import { WarpWebFactory } from '../src/core/web/WarpWebFactory';
import { WarpFactory } from '../src/core/web/SmartWeaveWebFactory';
async function main() {
LoggerFactory.use(new TsLogFactory());
@@ -20,7 +20,7 @@ async function main() {
const contractTxId = 'LppT1p3wri4FCKzW5buohsjWxpJHC58_rgIO-rYTMB8';
const warp = WarpWebFactory.memCached(arweave);
const warp = WarpFactory.memCached(arweave);
const contract = warp.contract(contractTxId).setEvaluationOptions({
ignoreExceptions: false,

View File

@@ -1,6 +1,6 @@
/* eslint-disable */
import Arweave from 'arweave';
import {LoggerFactory, WarpNodeFactory} from '../src';
import {LoggerFactory, WarpFactory} from '../src';
import * as fs from 'fs';
import knex from 'knex';
import os from 'os';
@@ -11,21 +11,12 @@ const logger = LoggerFactory.INST.create('Contract');
//LoggerFactory.use(new TsLogFactory());
LoggerFactory.INST.logLevel('debug');
LoggerFactory.INST.logLevel('info', 'Contract');
//LoggerFactory.INST.logLevel('debug', 'DefaultStateEvaluator');
//LoggerFactory.INST.logLevel('debug', 'CacheableStateEvaluator');
async function main() {
printTestInfo();
const PIANITY_CONTRACT = 'SJ3l7474UHh3Dw6dWVT1bzsJ-8JvOewtGoDdOecWIZo';
const PIANITY_COMMUNITY_CONTRACT = 'n05LTiuWcAYjizXAu-ghegaWjL89anZ6VdvuHcU6dno';
const LOOT_CONTRACT = 'Daj-MNSnH55TDfxqC7v4eq0lKzVIwh98srUaWqyuZtY';
const KOI_CONTRACT = '38TR3D8BxlPTc89NOW67IkQQUPR8jDLaJNdYv-4wWfM';
const localC = "iwlOHr4oM37YGKyQOWxZ-CUiEUKNtiFEaRNwz8Pwx_k";
const CACHE_PATH = 'cache.sqlite.db';
const heapUsedBefore = Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) / 100;
const rssUsedBefore = Math.round((process.memoryUsage().rss / 1024 / 1024) * 100) / 100;
@@ -37,26 +28,18 @@ async function main() {
logging: false // Enable network request logging
});
if (fs.existsSync(CACHE_PATH)) {
fs.rmSync(CACHE_PATH);
}
const knexConfig = knex({
client: 'sqlite3',
connection: {
filename: `tools/data/smartweave_just_one.sqlite`
},
useNullAsDefault: true
});
const warp = (await WarpNodeFactory.knexCachedBased(arweave, knexConfig, 1))
.useRedStoneGateway().build();
const contract = warp.contract("3vAx5cIFhwMihrNJgGx3CoAeZTOjG7LeIs9tnbBfL14");
const warp = WarpFactory.forTesting(arweave);
try {
const contract = warp.contract("c8SFfRvGQ43BDeMBYTTaSuBFGu5hiqaAqWr1dh34ESs");
const {state, validity, errorMessages} = await contract.readState();
//console.log(errorMessages);
//console.log(state);
const resultString = stringify(state);
fs.writeFileSync(path.join(__dirname, 'DAJ.json'), resultString);
} catch (e) {
throw new Error("Gotcha!");
}
const heapUsedAfter = Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) / 100;
@@ -71,9 +54,6 @@ async function main() {
usedAfter: rssUsedAfter
});
//const result = contract.lastReadStateStats();
//logger.warn('total evaluation: ', result);
return;
}

View File

@@ -1,32 +1,38 @@
/* eslint-disable */
import Arweave from 'arweave';
import { Contract, LoggerFactory, Warp, WarpNodeFactory } from '../src';
import {
Contract,
LoggerFactory, Warp, WarpFactory
} from '../src';
import { TsLogFactory } from '../src/logging/node/TsLogFactory';
import fs from 'fs';
import path from 'path';
import ArLocal from 'arlocal';
import { JWKInterface } from 'arweave/node/lib/wallet';
import {mineBlock} from "../src/__tests__/integration/_helpers";
async function main() {
let callingContractSrc: string;
let calleeContractSrc: string;
let calleeInitialState: string;
let wallet: JWKInterface;
let walletAddress: string;
let warp: Warp;
let smartweave: Warp;
let calleeContract: Contract<any>;
let callingContract: Contract;
let calleeTxId;
LoggerFactory.use(new TsLogFactory());
LoggerFactory.INST.logLevel('debug');
//LoggerFactory.INST.logLevel('debug', 'HandlerBasedContract');
/*LoggerFactory.INST.logLevel('debug', 'DefaultStateEvaluator');
LoggerFactory.INST.logLevel('fatal');
LoggerFactory.INST.logLevel('debug', 'inner-write');
/*
LoggerFactory.INST.logLevel('trace', 'LevelDbCache');
LoggerFactory.INST.logLevel('debug', 'CacheableStateEvaluator');
LoggerFactory.INST.logLevel('debug', 'ContractHandler');
LoggerFactory.INST.logLevel('debug', 'MemBlockHeightWarpCache');*/
LoggerFactory.INST.logLevel('debug', 'HandlerBasedContract');
LoggerFactory.INST.logLevel('debug', 'DefaultStateEvaluator');
LoggerFactory.INST.logLevel('debug', 'ContractHandlerApi');
*/
const logger = LoggerFactory.INST.create('inner-write');
const arlocal = new ArLocal(1985, false);
@@ -37,11 +43,13 @@ async function main() {
protocol: 'http'
});
const cacheDir = './cache/tools/'
try {
warp = WarpNodeFactory.memCached(arweave);
smartweave = WarpFactory.forTesting(arweave);
wallet = await arweave.wallets.generate();
walletAddress = await arweave.wallets.jwkToAddress(wallet);
await arweave.api.get(`/mint/${walletAddress}/1000000000000000`);
callingContractSrc = fs.readFileSync(
path.join(__dirname, '../src/__tests__/integration/', 'data/writing-contract.js'),
@@ -53,69 +61,75 @@ async function main() {
);
// deploying contract using the new SDK.
calleeTxId = await warp.createContract.deploy({
calleeTxId = await smartweave.createContract.deploy({
wallet,
initState: JSON.stringify({ counter: 100 }),
initState: JSON.stringify({ counter: 0 }),
src: calleeContractSrc
});
const callingTxId = await warp.createContract.deploy({
const callingTxId = await smartweave.createContract.deploy({
wallet,
initState: JSON.stringify({ ticker: 'WRITING_CONTRACT' }),
src: callingContractSrc
});
calleeContract = warp.contract(calleeTxId).connect(wallet).setEvaluationOptions({
calleeContract = smartweave.contract(calleeTxId).connect(wallet).setEvaluationOptions({
ignoreExceptions: false,
internalWrites: true,
});
callingContract = warp.contract(callingTxId).connect(wallet).setEvaluationOptions({
callingContract = smartweave.contract(callingTxId).connect(wallet).setEvaluationOptions({
ignoreExceptions: false,
internalWrites: true
});
await mine();
await calleeContract.writeInteraction({ function: 'add' });
await callingContract.writeInteraction({ function: 'writeContract', contractId: calleeTxId, amount: 10 });
await mine(); // 113
/*logger.info('==== READ STATE 1 ====');
const result1 = await calleeContract.readState();
logger.info('Read state 1', result1.state);*/
await callingContract.writeInteraction({ function: 'writeContract', contractId: calleeTxId, amount: 10 });
await calleeContract.writeInteraction({ function: 'add' });
await mine(); //124
logger.debug("Cache dump 1", showCache(await calleeContract.dumpCache()));
await callingContract.writeInteraction({ function: 'writeContract', contractId: calleeTxId, amount: 10 });
logger.debug("Cache dump 2", showCache(await calleeContract.dumpCache()));
await mineBlock(arweave);
await calleeContract.writeInteraction({ function: 'add' });
logger.debug("Cache dump 3", showCache(await calleeContract.dumpCache()));
await callingContract.writeInteraction({ function: 'writeContract', contractId: calleeTxId, amount: 10 });
logger.debug("Cache dump 4", showCache(await calleeContract.dumpCache()));
await mineBlock(arweave);
await calleeContract.writeInteraction({ function: 'add' });
logger.debug("Cache dump 5", showCache(await calleeContract.dumpCache()));
await callingContract.writeInteraction({ function: 'writeContract', contractId: calleeTxId, amount: 10 });
logger.debug("Cache dump 6", showCache(await calleeContract.dumpCache()));
await mineBlock(arweave);
await calleeContract.writeInteraction({ function: 'add' });
logger.debug("Cache dump 7", showCache(await calleeContract.dumpCache()));
await callingContract.writeInteraction({ function: 'writeContract', contractId: calleeTxId, amount: 10 });
logger.debug("Cache dump 8", showCache(await calleeContract.dumpCache()));
await mineBlock(arweave);
logger.info('==== READ STATE 2 ====');
const result2 = await calleeContract.readState();
logger.error('Read state 2', result2.state);
logger.debug("Cache dump 9", showCache(await calleeContract.dumpCache()));
logger.info('Result (should be 44):', result2.state.counter);
/*await callingContract.writeInteraction({ function: 'writeContract', contractId: calleeTxId, amount: 10 });
await mine(); // 123
logger.info('==== READ STATE 2 ====');
const result2 = await calleeContract.readState();
logger.info('Read state 2', result2.state);
await calleeContract.writeInteraction({ function: 'add' });
await mine(); // 124
await callingContract.writeInteraction({ function: 'writeContract', contractId: calleeTxId, amount: 10 });
await callingContract.writeInteraction({ function: 'writeContract', contractId: calleeTxId, amount: 10 });
await calleeContract.writeInteraction({ function: 'add' });
await mine(); // 145
const result3 = await calleeContract.readState();
logger.info('Read state 3', result3.state);*/
/*const result = await callingContract.readState();
logger.debug("4", showCache(await callingContract.dumpCache()));
logger.info('Result (should be 21):', result.state);*/
} finally {
fs.rmSync(cacheDir, { recursive: true, force: true });
await arlocal.stop();
}
async function mine() {
await arweave.api.get('mine');
}
function showCache(dump: any) {
return dump/*.filter(i => i[0].includes(calleeTxId))*/
.map(i => i[0].includes(calleeTxId) ? `${i[0]}: ${i[1].state.counter}` : `${i[0]}: ${i[1].state.ticker}`);
}
}
main().catch((e) => console.error(e));

46
tools/leveldb.ts Normal file
View File

@@ -0,0 +1,46 @@
/* eslint-disable */
import {Level} from "level";
import { MemoryLevel } from 'memory-level';
// Create a database
async function test() {
const db = new Level<string, any>('./leveldb', {valueEncoding: 'json'});
const dbMem = new MemoryLevel({ valueEncoding: 'json' })
const contractA = dbMem.sublevel<string, any>('contract_a', {valueEncoding: 'json'});
const contractB = dbMem.sublevel<string, any>('contract_b', {valueEncoding: 'json'});
const contractC = dbMem.sublevel<string, any>('contract_c', {valueEncoding: 'json'});
contractA.put("01a", {state: "01a"});
contractA.put("01b", {state: "01b"});
contractA.put("02c", {state: "02c"});
contractA.put("03d", {state: "03d"});
contractB.put("01e", {state: "01e"});
contractB.put("01f", {state: "01f"});
contractB.put("02g", {state: "02g"});
contractB.put("03h", {state: "03h"});
for await (const value of contractA.values({lt: '02g'})) {
console.log(value)
}
console.log("state: " + (await contractB.get('03h')).state);
try {
(await contractB.get('06h'));
} catch (e: any) {
console.log(e.code);
}
const keys = await contractB.keys({reverse: true, limit: 1}).all();
console.log(keys.length);
console.log(keys);
}
test();

View File

@@ -1,32 +0,0 @@
/* eslint-disable */
import { RemoteBlockHeightCache } from '../src/cache/impl/RemoteBlockHeightCache';
async function main() {
const cache = new RemoteBlockHeightCache(
"STATE", "http://localhost:3000"
);
const get = await cache.get('txId', 557);
console.log('get result:', get);
const getLessOrEqual = await cache.getLessOrEqual('txId', 600);
console.log('getLessOrEqual result:', getLessOrEqual);
const contains = await cache.contains('txId');
console.log('contains result:', contains);
const getLast = await cache.getLast('txId');
console.log('getLast result:', getLast);
await cache.put({cacheKey: 'txId', blockHeight: 558}, {
"value": "toBeCached"
});
const getLastAfterPut = await cache.getLast('txId');
console.log('getLastAfterPut result:', getLastAfterPut);
}
main().catch((e) => {
console.log(e);
});

View File

@@ -34,7 +34,7 @@ async function main() {
const sorted = await lexSorting.sort([...interactions]);
logger.info("\n\nLexicographical");
sorted.forEach(v => {
logger.info(`${v.node.block.height}:${v.node.id}: [${v.sortKey}]`);
logger.info(`${v.node.block.height}:${v.node.id}: [${v.node.sortKey}]`);
});
}

1831
yarn.lock

File diff suppressed because it is too large Load Diff