feat: kv storage for contracts
This commit is contained in:
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
node-version: '18'
|
||||||
- name: Install modules
|
- name: Install modules
|
||||||
run: yarn
|
run: yarn
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
|
|||||||
@@ -21,5 +21,5 @@ module.exports = {
|
|||||||
'^.+\\.(ts|js)$': 'ts-jest'
|
'^.+\\.(ts|js)$': 'ts-jest'
|
||||||
},
|
},
|
||||||
|
|
||||||
silent: true
|
silent: false
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
"stream-buffers": false,
|
"stream-buffers": false,
|
||||||
"constants": false,
|
"constants": false,
|
||||||
"os": false,
|
"os": false,
|
||||||
"process": false
|
"process": false,
|
||||||
|
"url": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ describe('Testing WarpGatewayContractDefinitionLoader', () => {
|
|||||||
it('loads contract definition when cache is empty', async () => {
|
it('loads contract definition when cache is empty', async () => {
|
||||||
// Cache is empty
|
// Cache is empty
|
||||||
loader.getCache().delete(contract.txId());
|
loader.getCache().delete(contract.txId());
|
||||||
expect(await loader.getCache().get(contract.txId(), 'cd')).toBeFalsy();
|
expect(await loader.getCache().get({ key: contract.txId(), sortKey: 'cd'})).toBeFalsy();
|
||||||
|
|
||||||
// Load contract
|
// Load contract
|
||||||
const loaded = await loader.load(contract.txId());
|
const loaded = await loader.load(contract.txId());
|
||||||
@@ -86,12 +86,12 @@ describe('Testing WarpGatewayContractDefinitionLoader', () => {
|
|||||||
expect(loaded.src).toBe(contractSrc);
|
expect(loaded.src).toBe(contractSrc);
|
||||||
|
|
||||||
// Contract is in its cache
|
// Contract is in its cache
|
||||||
expect(await loader.getCache().get(loaded.txId, 'cd')).toBeTruthy();
|
expect(await loader.getCache().get({ key: loaded.txId, sortKey: 'cd'})).toBeTruthy();
|
||||||
expect(await loader.getSrcCache().get(loaded.txId, 'cd')).toBeFalsy();
|
expect(await loader.getSrcCache().get({ key: loaded.txId, sortKey: 'cd'})).toBeFalsy();
|
||||||
|
|
||||||
// Source is in its cache
|
// Source is in its cache
|
||||||
expect(await loader.getCache().get(loaded.srcTxId, 'src')).toBeFalsy();
|
expect(await loader.getCache().get({ key: loaded.srcTxId, sortKey: 'src'})).toBeFalsy();
|
||||||
expect(await loader.getSrcCache().get(loaded.srcTxId, 'src')).toBeTruthy();
|
expect(await loader.getSrcCache().get({ key: loaded.srcTxId, sortKey: 'src'})).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads contract definition when cache contains given definition', async () => {
|
it('loads contract definition when cache contains given definition', async () => {
|
||||||
@@ -99,10 +99,10 @@ describe('Testing WarpGatewayContractDefinitionLoader', () => {
|
|||||||
let loaded = await loader.load(contract.txId());
|
let loaded = await loader.load(contract.txId());
|
||||||
|
|
||||||
// Modify source in cache
|
// Modify source in cache
|
||||||
let source = await loader.getSrcCache().get(loaded.srcTxId, 'src');
|
let source = await loader.getSrcCache().get({ key: loaded.srcTxId, sortKey: 'src' });
|
||||||
expect(source).toBeTruthy();
|
expect(source).toBeTruthy();
|
||||||
source!.cachedValue.src = fs.readFileSync(path.join(__dirname, '../data/token-evolve.js'), 'utf8');
|
source!.cachedValue.src = fs.readFileSync(path.join(__dirname, '../data/token-evolve.js'), 'utf8');
|
||||||
await loader.getSrcCache().put({ contractTxId: loaded.srcTxId, sortKey: 'src' }, source!.cachedValue);
|
await loader.getSrcCache().put({ key: loaded.srcTxId, sortKey: 'src' }, source!.cachedValue);
|
||||||
|
|
||||||
// Load again, modified cache should be returned
|
// Load again, modified cache should be returned
|
||||||
loaded = await loader.load(contract.txId());
|
loaded = await loader.load(contract.txId());
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { PstState, PstContract } from '../../../contract/PstContract';
|
|||||||
import { Warp } from '../../../core/Warp';
|
import { Warp } from '../../../core/Warp';
|
||||||
import { WarpFactory } from '../../../core/WarpFactory';
|
import { WarpFactory } from '../../../core/WarpFactory';
|
||||||
import { LoggerFactory } from '../../../logging/LoggerFactory';
|
import { LoggerFactory } from '../../../logging/LoggerFactory';
|
||||||
import exp from 'constants';
|
|
||||||
|
|
||||||
describe('Testing unsafe client in nested contracts with "skip" option', () => {
|
describe('Testing unsafe client in nested contracts with "skip" option', () => {
|
||||||
let contractSrc: string;
|
let contractSrc: string;
|
||||||
|
|||||||
134
src/__tests__/integration/basic/pst-kv.test.ts
Normal file
134
src/__tests__/integration/basic/pst-kv.test.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
import ArLocal from 'arlocal';
|
||||||
|
import Arweave from 'arweave';
|
||||||
|
import { JWKInterface } from 'arweave/node/lib/wallet';
|
||||||
|
import path from 'path';
|
||||||
|
import { mineBlock } from '../_helpers';
|
||||||
|
import { PstState, PstContract } from '../../../contract/PstContract';
|
||||||
|
import { Warp } from '../../../core/Warp';
|
||||||
|
import { DEFAULT_LEVEL_DB_LOCATION, WarpFactory } from '../../../core/WarpFactory';
|
||||||
|
import { LoggerFactory } from '../../../logging/LoggerFactory';
|
||||||
|
|
||||||
|
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;
|
||||||
|
beforeAll(async () => {
|
||||||
|
// 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(2222, false);
|
||||||
|
await arlocal.start();
|
||||||
|
LoggerFactory.INST.logLevel('error');
|
||||||
|
|
||||||
|
warp = WarpFactory.forLocal(2222);
|
||||||
|
|
||||||
|
({ arweave } = warp);
|
||||||
|
({ jwk: wallet, address: walletAddress } = await warp.generateWallet());
|
||||||
|
|
||||||
|
contractSrc = fs.readFileSync(path.join(__dirname, '../data/kv-storage.js'), 'utf8');
|
||||||
|
const stateFromFile: PstState = JSON.parse(fs.readFileSync(path.join(__dirname, '../data/token-pst.json'), 'utf8'));
|
||||||
|
|
||||||
|
initialState = {
|
||||||
|
...stateFromFile,
|
||||||
|
...{
|
||||||
|
owner: walletAddress
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// deploying contract using the new SDK.
|
||||||
|
({ contractTxId } = await warp.deploy({
|
||||||
|
wallet,
|
||||||
|
initState: JSON.stringify(initialState),
|
||||||
|
src: contractSrc
|
||||||
|
}));
|
||||||
|
|
||||||
|
// connecting to the PST contract
|
||||||
|
pst = warp.pst(contractTxId).setEvaluationOptions({
|
||||||
|
useKVStorage: true
|
||||||
|
}) as PstContract;
|
||||||
|
pst.connect(wallet);
|
||||||
|
|
||||||
|
await mineBlock(warp);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await arlocal.stop();
|
||||||
|
fs.rmSync(`${DEFAULT_LEVEL_DB_LOCATION}/kv/ldb/${contractTxId}`, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize', async () => {
|
||||||
|
// this is done to "initialize" the state
|
||||||
|
await pst.writeInteraction({
|
||||||
|
function: 'mint',
|
||||||
|
target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M',
|
||||||
|
qty: 10000000
|
||||||
|
});
|
||||||
|
await mineBlock(warp);
|
||||||
|
|
||||||
|
await pst.writeInteraction({
|
||||||
|
function: 'mint',
|
||||||
|
target: '33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA',
|
||||||
|
qty: 23111222
|
||||||
|
});
|
||||||
|
await mineBlock(warp);
|
||||||
|
|
||||||
|
await pst.writeInteraction({
|
||||||
|
function: 'mint',
|
||||||
|
target: walletAddress,
|
||||||
|
qty: 555669
|
||||||
|
});
|
||||||
|
await mineBlock(warp);
|
||||||
|
});
|
||||||
|
|
||||||
|
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(warp);
|
||||||
|
|
||||||
|
expect((await pst.currentBalance(walletAddress)).balance).toEqual(555669 - 555);
|
||||||
|
expect((await pst.currentBalance('uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M')).balance).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 read storage value', async () => {
|
||||||
|
expect((await pst.getStorageValues([walletAddress])).cachedValue.get(walletAddress)).toEqual(555669 - 555);
|
||||||
|
expect(
|
||||||
|
(await pst.getStorageValues(['uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M'])).cachedValue.get(
|
||||||
|
'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M'
|
||||||
|
)
|
||||||
|
).toEqual(10000000 + 555);
|
||||||
|
expect(
|
||||||
|
(await pst.getStorageValues(['33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA'])).cachedValue.get(
|
||||||
|
'33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA'
|
||||||
|
)
|
||||||
|
).toEqual(23111222);
|
||||||
|
expect((await pst.getStorageValues(['foo'])).cachedValue.get('foo')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
71
src/__tests__/integration/data/kv-storage.js
Normal file
71
src/__tests__/integration/data/kv-storage.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
export async function handle(state, action) {
|
||||||
|
const input = action.input;
|
||||||
|
const caller = action.caller;
|
||||||
|
|
||||||
|
if (!state.kvOps) {
|
||||||
|
// state.kvOps = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.function === 'mint') {
|
||||||
|
console.log('mint', input.target, input.qty);
|
||||||
|
await SmartWeave.kv.put(input.target, input.qty);
|
||||||
|
// for debug or whatever
|
||||||
|
//state.kvOps[SmartWeave.transaction.id] = SmartWeave.kv.ops();
|
||||||
|
return {state};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.function === 'transfer') {
|
||||||
|
const target = input.target;
|
||||||
|
const qty = input.qty;
|
||||||
|
|
||||||
|
if (!Number.isInteger(qty)) {
|
||||||
|
throw new ContractError('Invalid value for "qty". Must be an integer');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
throw new ContractError('No target specified');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qty <= 0 || caller === target) {
|
||||||
|
throw new ContractError('Invalid token transfer');
|
||||||
|
}
|
||||||
|
|
||||||
|
let callerBalance = await SmartWeave.kv.get(caller);
|
||||||
|
callerBalance = callerBalance ? callerBalance : 0;
|
||||||
|
|
||||||
|
if (callerBalance < qty) {
|
||||||
|
throw new ContractError(`Caller balance not high enough to send ${qty} token(s)!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lower the token balance of the caller
|
||||||
|
callerBalance -= qty;
|
||||||
|
await SmartWeave.kv.put(caller, callerBalance);
|
||||||
|
|
||||||
|
let targetBalance = await SmartWeave.kv.get(target);
|
||||||
|
targetBalance = targetBalance ? targetBalance : 0;
|
||||||
|
|
||||||
|
targetBalance += qty;
|
||||||
|
await SmartWeave.kv.put(target, targetBalance);
|
||||||
|
|
||||||
|
// for debug or whatever
|
||||||
|
//state.kvOps[SmartWeave.transaction.id] = SmartWeave.kv.ops();
|
||||||
|
|
||||||
|
return {state};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.function === 'balance') {
|
||||||
|
const target = input.target;
|
||||||
|
const ticker = state.ticker;
|
||||||
|
|
||||||
|
if (typeof target !== 'string') {
|
||||||
|
throw new ContractError('Must specify target to get balance for');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await SmartWeave.kv.get(target);
|
||||||
|
console.log('balance', {target: input.target, balance: result});
|
||||||
|
|
||||||
|
return {result: {target, ticker, balance: result ? result : 0}};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ContractError(`No function supplied or function not recognised: "${input.function}"`);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { LevelDbCache } from '../../cache/impl/LevelDbCache';
|
import { LevelDbCache } from '../../cache/impl/LevelDbCache';
|
||||||
import { defaultCacheOptions, WarpFactory } from '../../core/WarpFactory';
|
import { defaultCacheOptions } from '../../core/WarpFactory';
|
||||||
|
import { CacheKey } from '../../cache/SortKeyCache';
|
||||||
|
|
||||||
const getContractId = (i: number) => `contract${i}`.padStart(43, '0');
|
const getContractId = (i: number) => `contract${i}`.padStart(43, '0');
|
||||||
const getSortKey = (j: number) =>
|
const getSortKey = (j: number) =>
|
||||||
@@ -13,7 +14,7 @@ describe('LevelDB cache prune', () => {
|
|||||||
for (let j = 0; j < numRepeatingEntries; j++) {
|
for (let j = 0; j < numRepeatingEntries; j++) {
|
||||||
await sut.put(
|
await sut.put(
|
||||||
{
|
{
|
||||||
contractTxId: getContractId(i),
|
key: getContractId(i),
|
||||||
sortKey: getSortKey(j)
|
sortKey: getSortKey(j)
|
||||||
},
|
},
|
||||||
{ result: `contract${i}:${j}` }
|
{ result: `contract${i}:${j}` }
|
||||||
@@ -73,12 +74,12 @@ describe('LevelDB cache prune', () => {
|
|||||||
for (let i = 0; i < contracts; i++) {
|
for (let i = 0; i < contracts; i++) {
|
||||||
// Check newest elements are present
|
// Check newest elements are present
|
||||||
for (let j = 0; j < toLeave; j++) {
|
for (let j = 0; j < toLeave; j++) {
|
||||||
expect(await sut.get(getContractId(i), getSortKey(entriesPerContract - j - 1))).toBeTruthy();
|
expect(await sut.get(new CacheKey(getContractId(i), getSortKey(entriesPerContract - j - 1)))).toBeTruthy();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check old elements are removed
|
// Check old elements are removed
|
||||||
for (let j = toLeave; j < entriesPerContract; j++) {
|
for (let j = toLeave; j < entriesPerContract; j++) {
|
||||||
expect(await sut.get(getContractId(i), getSortKey(entriesPerContract - j - 1))).toBeFalsy();
|
expect(await sut.get(new CacheKey(getContractId(i), getSortKey(entriesPerContract - j - 1)))).toBeFalsy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,13 +95,13 @@ describe('LevelDB cache prune', () => {
|
|||||||
|
|
||||||
// Removed elements
|
// Removed elements
|
||||||
for (let j = 0; j < entriesPerContract; j++) {
|
for (let j = 0; j < entriesPerContract; j++) {
|
||||||
expect(await sut.get(getContractId(0), getSortKey(j))).toBeFalsy();
|
expect(await sut.get(new CacheKey(getContractId(0), getSortKey(j)))).toBeFalsy();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remaining elements
|
// Remaining elements
|
||||||
for (let i = 1; i < contracts; i++) {
|
for (let i = 1; i < contracts; i++) {
|
||||||
for (let j = 0; j < entriesPerContract; j++) {
|
for (let j = 0; j < entriesPerContract; j++) {
|
||||||
expect(await sut.get(getContractId(i), getSortKey(j))).toBeTruthy();
|
expect(await sut.get(new CacheKey(getContractId(i), getSortKey(j)))).toBeTruthy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ describe('Evaluation options evaluator', () => {
|
|||||||
throwOnInternalWriteError: true,
|
throwOnInternalWriteError: true,
|
||||||
unsafeClient: 'throw',
|
unsafeClient: 'throw',
|
||||||
updateCacheForEachInteraction: false,
|
updateCacheForEachInteraction: false,
|
||||||
|
useKVStorage: false,
|
||||||
useVM2: false,
|
useVM2: false,
|
||||||
waitForConfirmation: false,
|
waitForConfirmation: false,
|
||||||
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/'
|
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/'
|
||||||
@@ -58,6 +59,7 @@ describe('Evaluation options evaluator', () => {
|
|||||||
throwOnInternalWriteError: true,
|
throwOnInternalWriteError: true,
|
||||||
unsafeClient: 'throw',
|
unsafeClient: 'throw',
|
||||||
updateCacheForEachInteraction: false,
|
updateCacheForEachInteraction: false,
|
||||||
|
useKVStorage: false,
|
||||||
useVM2: true,
|
useVM2: true,
|
||||||
waitForConfirmation: false,
|
waitForConfirmation: false,
|
||||||
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/'
|
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/'
|
||||||
@@ -87,6 +89,7 @@ describe('Evaluation options evaluator', () => {
|
|||||||
throwOnInternalWriteError: true,
|
throwOnInternalWriteError: true,
|
||||||
unsafeClient: 'allow',
|
unsafeClient: 'allow',
|
||||||
updateCacheForEachInteraction: false,
|
updateCacheForEachInteraction: false,
|
||||||
|
useKVStorage: false,
|
||||||
useVM2: true,
|
useVM2: true,
|
||||||
waitForConfirmation: false,
|
waitForConfirmation: false,
|
||||||
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/'
|
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/'
|
||||||
@@ -96,9 +99,7 @@ describe('Evaluation options evaluator', () => {
|
|||||||
const result = new EvaluationOptionsEvaluator(contract2.evaluationOptions(), {
|
const result = new EvaluationOptionsEvaluator(contract2.evaluationOptions(), {
|
||||||
useVM2: false
|
useVM2: false
|
||||||
}).rootOptions;
|
}).rootOptions;
|
||||||
}).toThrow(
|
}).toThrow('Option {useVM2} differs.');
|
||||||
'Option {useVM2} differs. EvaluationOptions: [true], manifest: [false]. Use contract.setEvaluationOptions({useVM2: false) to evaluate contract state.'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should properly set foreign evaluation options - unsafeClient - allow', async () => {
|
it('should properly set foreign evaluation options - unsafeClient - allow', async () => {
|
||||||
|
|||||||
53
src/__tests__/unit/smartwesve-global-level-db-cache.test.ts
Normal file
53
src/__tests__/unit/smartwesve-global-level-db-cache.test.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { DEFAULT_LEVEL_DB_LOCATION } from '../../core/WarpFactory';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { SmartWeaveGlobal } from '../../legacy/smartweave-global';
|
||||||
|
import Arweave from 'arweave';
|
||||||
|
import { DefaultEvaluationOptions } from '../../core/modules/StateEvaluator';
|
||||||
|
import { LevelDbCache } from '../../cache/impl/LevelDbCache';
|
||||||
|
import { GQLNodeInterface } from '../../legacy/gqlResult';
|
||||||
|
import { CacheKey } from '../../cache/SortKeyCache';
|
||||||
|
|
||||||
|
describe('KV database', () => {
|
||||||
|
describe('with the SmartWeave Global KV implementation', () => {
|
||||||
|
const arweave = Arweave.init({});
|
||||||
|
const db = new LevelDbCache({
|
||||||
|
inMemory: false,
|
||||||
|
dbLocation: `${DEFAULT_LEVEL_DB_LOCATION}/kv/KV_TRIE_TEST_SW_GLOBAL`
|
||||||
|
});
|
||||||
|
|
||||||
|
const sut = new SmartWeaveGlobal(arweave, { id: 'a', owner: '' }, new DefaultEvaluationOptions(), db);
|
||||||
|
|
||||||
|
it('should set values', async () => {
|
||||||
|
sut._activeTx = { sortKey: '123' } as GQLNodeInterface;
|
||||||
|
await sut.kv.put('foo', 'bar');
|
||||||
|
await sut.kv.put('one', [1]);
|
||||||
|
await sut.kv.put('one', { val: 1 });
|
||||||
|
await sut.kv.put('two', { val: 2 });
|
||||||
|
|
||||||
|
expect(await sut.kv.get('one')).toEqual({ val: 1 });
|
||||||
|
expect(await sut.kv.get('ninety')).toBeNull();
|
||||||
|
await sut.kv.commit();
|
||||||
|
expect(await sut.kv.get('one')).toEqual({ val: 1 });
|
||||||
|
|
||||||
|
sut._activeTx = { sortKey: '222' } as GQLNodeInterface;
|
||||||
|
await sut.kv.put('one', '1');
|
||||||
|
await sut.kv.put('three', 3);
|
||||||
|
await sut.kv.commit();
|
||||||
|
|
||||||
|
sut._activeTx = { sortKey: '330' } as GQLNodeInterface;
|
||||||
|
await sut.kv.put('one', { val: [1] });
|
||||||
|
await sut.kv.put('33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA', 23111222);
|
||||||
|
await sut.kv.commit();
|
||||||
|
|
||||||
|
expect(await sut.kv.get('foo')).toEqual('bar');
|
||||||
|
expect(await sut.kv.get('one')).toEqual({ val: [1] });
|
||||||
|
expect(await sut.kv.get('three')).toEqual(3);
|
||||||
|
expect(await sut.kv.get('33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA')).toEqual(23111222);
|
||||||
|
|
||||||
|
expect((await db.get(new CacheKey('foo', '123'))).cachedValue).toEqual('bar');
|
||||||
|
expect((await db.get(new CacheKey('one', '123'))).cachedValue).toEqual({ val: 1 });
|
||||||
|
expect((await db.get(new CacheKey('one', '222'))).cachedValue).toEqual('1');
|
||||||
|
expect((await db.get(new CacheKey('one', '330'))).cachedValue).toEqual({ val: [1] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
48
src/cache/SortKeyCache.ts
vendored
48
src/cache/SortKeyCache.ts
vendored
@@ -1,38 +1,45 @@
|
|||||||
/**
|
/**
|
||||||
* A cache that stores its values per contract tx id and sort key.
|
* A cache that stores its values per dedicated key and sort key.
|
||||||
* A sort key is a value that the SmartWeave protocol is using
|
* A sort key is a value that the SmartWeave protocol is using
|
||||||
* to sort contract transactions ({@link LexicographicalInteractionsSorter}.
|
* to sort contract transactions ({@link LexicographicalInteractionsSorter}.
|
||||||
*
|
*
|
||||||
* All values should be stored in a lexicographical order (per contract) -
|
* All values should be stored in a lexicographical order (per key) -
|
||||||
* sorted by the sort key.
|
* sorted by the sort key.
|
||||||
*/
|
*/
|
||||||
export interface SortKeyCache<V> {
|
export interface SortKeyCache<V> {
|
||||||
getLessOrEqual(key: string, sortKey: string): Promise<SortKeyCacheResult<V> | null>;
|
getLessOrEqual(key: string, sortKey: string): Promise<SortKeyCacheResult<V> | null>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* returns latest value stored for given contractTxId
|
* returns value stored for a given key and last sortKey
|
||||||
*/
|
*/
|
||||||
getLast(contractTxId: string): Promise<SortKeyCacheResult<V> | null>;
|
getLast(key: string): Promise<SortKeyCacheResult<V> | null>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* returns last cached sort key - takes all contracts into account
|
* returns last cached sort key - takes all keys into account
|
||||||
*/
|
*/
|
||||||
getLastSortKey(): Promise<string | null>;
|
getLastSortKey(): Promise<string | null>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* returns value for the key and exact blockHeight
|
* returns value for the key and exact sortKey
|
||||||
*/
|
*/
|
||||||
get(contractTxId: string, sortKey: string, returnDeepCopy?: boolean): Promise<SortKeyCacheResult<V> | null>;
|
get(cacheKey: CacheKey): Promise<SortKeyCacheResult<V> | null>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* puts new value in cache under given {@link CacheKey.key} and {@link CacheKey.blockHeight}.
|
* puts new value in cache under given {@link CacheKey.key} and {@link CacheKey.sortKey}.
|
||||||
*/
|
*/
|
||||||
put(cacheKey: CacheKey, value: V): Promise<void>;
|
put(cacheKey: CacheKey, value: V): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* removes contract's data
|
* removes all data stored under a specified key
|
||||||
*/
|
*/
|
||||||
delete(contractTxId: string): Promise<void>;
|
delete(key: string): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* executes a list of stacked operations
|
||||||
|
*/
|
||||||
|
batch(opStack: BatchDBOp<V>[]);
|
||||||
|
|
||||||
|
open(): Promise<void>;
|
||||||
|
|
||||||
close(): Promise<void>;
|
close(): Promise<void>;
|
||||||
|
|
||||||
@@ -43,9 +50,9 @@ export interface SortKeyCache<V> {
|
|||||||
dump(): Promise<any>;
|
dump(): Promise<any>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return all cached contracts.
|
* Return all cached keys.
|
||||||
*/
|
*/
|
||||||
allContracts(): Promise<string[]>;
|
keys(): Promise<string[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* returns underlying storage (LevelDB, LMDB, sqlite...)
|
* returns underlying storage (LevelDB, LMDB, sqlite...)
|
||||||
@@ -55,10 +62,10 @@ export interface SortKeyCache<V> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* leaves n-latest (i.e. with latest (in lexicographic order) sort keys)
|
* leaves n-latest (i.e. with latest (in lexicographic order) sort keys)
|
||||||
* entries for each cached contract
|
* entries for each cached key
|
||||||
*
|
*
|
||||||
* @param entriesStored - how many latest entries should be left
|
* @param entriesStored - how many latest entries should be left
|
||||||
* for each cached contract
|
* for each cached key
|
||||||
*
|
*
|
||||||
* @retun PruneStats if getting them doesn't introduce a delay, null otherwise
|
* @retun PruneStats if getting them doesn't introduce a delay, null otherwise
|
||||||
*/
|
*/
|
||||||
@@ -73,10 +80,21 @@ export interface PruneStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CacheKey {
|
export class CacheKey {
|
||||||
constructor(readonly contractTxId: string, readonly sortKey: string) {}
|
constructor(readonly key: string, readonly sortKey: string) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line:max-classes-per-file
|
// tslint:disable-next-line:max-classes-per-file
|
||||||
export class SortKeyCacheResult<V> {
|
export class SortKeyCacheResult<V> {
|
||||||
constructor(readonly sortKey: string, readonly cachedValue: V) {}
|
constructor(readonly sortKey: string, readonly cachedValue: V) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export declare type BatchDBOp<V> = PutBatch<V> | DelBatch;
|
||||||
|
export interface PutBatch<V> {
|
||||||
|
type: 'put';
|
||||||
|
key: CacheKey;
|
||||||
|
value: V;
|
||||||
|
}
|
||||||
|
export interface DelBatch {
|
||||||
|
type: 'del';
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|||||||
48
src/cache/impl/LevelDbCache.ts
vendored
48
src/cache/impl/LevelDbCache.ts
vendored
@@ -1,4 +1,4 @@
|
|||||||
import { SortKeyCache, CacheKey, SortKeyCacheResult, PruneStats } from '../SortKeyCache';
|
import { BatchDBOp, CacheKey, SortKeyCache, SortKeyCacheResult } from '../SortKeyCache';
|
||||||
import { Level } from 'level';
|
import { Level } from 'level';
|
||||||
import { MemoryLevel } from 'memory-level';
|
import { MemoryLevel } from 'memory-level';
|
||||||
import { CacheOptions } from '../../core/WarpFactory';
|
import { CacheOptions } from '../../core/WarpFactory';
|
||||||
@@ -43,15 +43,15 @@ export class LevelDbCache<V = any> implements SortKeyCache<V> {
|
|||||||
|
|
||||||
constructor(private readonly cacheOptions: CacheOptions) {}
|
constructor(private readonly cacheOptions: CacheOptions) {}
|
||||||
|
|
||||||
async get(contractTxId: string, sortKey: string, returnDeepCopy?: boolean): Promise<SortKeyCacheResult<V> | null> {
|
async get(cacheKey: CacheKey, returnDeepCopy?: boolean): Promise<SortKeyCacheResult<V> | null> {
|
||||||
const contractCache = this.db.sublevel<string, any>(contractTxId, { valueEncoding: 'json' });
|
const contractCache = this.db.sublevel<string, any>(cacheKey.key, { valueEncoding: 'json' });
|
||||||
// manually opening to fix https://github.com/Level/level/issues/221
|
// manually opening to fix https://github.com/Level/level/issues/221
|
||||||
await contractCache.open();
|
await contractCache.open();
|
||||||
try {
|
try {
|
||||||
const result = await contractCache.get(sortKey);
|
const result = await contractCache.get(cacheKey.sortKey);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sortKey: sortKey,
|
sortKey: cacheKey.sortKey,
|
||||||
cachedValue: result
|
cachedValue: result
|
||||||
};
|
};
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -63,8 +63,8 @@ export class LevelDbCache<V = any> implements SortKeyCache<V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLast(contractTxId: string): Promise<SortKeyCacheResult<V> | null> {
|
async getLast(key: string): Promise<SortKeyCacheResult<V> | null> {
|
||||||
const contractCache = this.db.sublevel<string, any>(contractTxId, { valueEncoding: 'json' });
|
const contractCache = this.db.sublevel<string, any>(key, { valueEncoding: 'json' });
|
||||||
// manually opening to fix https://github.com/Level/level/issues/221
|
// manually opening to fix https://github.com/Level/level/issues/221
|
||||||
await contractCache.open();
|
await contractCache.open();
|
||||||
const keys = await contractCache.keys({ reverse: true, limit: 1 }).all();
|
const keys = await contractCache.keys({ reverse: true, limit: 1 }).all();
|
||||||
@@ -78,8 +78,8 @@ export class LevelDbCache<V = any> implements SortKeyCache<V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLessOrEqual(contractTxId: string, sortKey: string): Promise<SortKeyCacheResult<V> | null> {
|
async getLessOrEqual(key: string, sortKey: string): Promise<SortKeyCacheResult<V> | null> {
|
||||||
const contractCache = this.db.sublevel<string, any>(contractTxId, { valueEncoding: 'json' });
|
const contractCache = this.db.sublevel<string, any>(key, { valueEncoding: 'json' });
|
||||||
// manually opening to fix https://github.com/Level/level/issues/221
|
// manually opening to fix https://github.com/Level/level/issues/221
|
||||||
await contractCache.open();
|
await contractCache.open();
|
||||||
const keys = await contractCache.keys({ reverse: true, lte: sortKey, limit: 1 }).all();
|
const keys = await contractCache.keys({ reverse: true, lte: sortKey, limit: 1 }).all();
|
||||||
@@ -94,20 +94,36 @@ export class LevelDbCache<V = any> implements SortKeyCache<V> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async put(stateCacheKey: CacheKey, value: V): Promise<void> {
|
async put(stateCacheKey: CacheKey, value: V): Promise<void> {
|
||||||
const contractCache = this.db.sublevel<string, any>(stateCacheKey.contractTxId, { valueEncoding: 'json' });
|
const contractCache = this.db.sublevel<string, any>(stateCacheKey.key, { valueEncoding: 'json' });
|
||||||
// manually opening to fix https://github.com/Level/level/issues/221
|
// manually opening to fix https://github.com/Level/level/issues/221
|
||||||
await contractCache.open();
|
await contractCache.open();
|
||||||
await contractCache.put(stateCacheKey.sortKey, value);
|
await contractCache.put(stateCacheKey.sortKey, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(contractTxId: string): Promise<void> {
|
async delete(key: string): Promise<void> {
|
||||||
const contractCache = this.db.sublevel<string, any>(contractTxId, { valueEncoding: 'json' });
|
const contractCache = this.db.sublevel<string, any>(key, { valueEncoding: 'json' });
|
||||||
await contractCache.open();
|
await contractCache.open();
|
||||||
await contractCache.clear();
|
await contractCache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
close(): Promise<void> {
|
async batch(opStack: BatchDBOp<V>[]) {
|
||||||
return this.db.close();
|
for (const op of opStack) {
|
||||||
|
if (op.type === 'put') {
|
||||||
|
await this.put(op.key, op.value);
|
||||||
|
} else if (op.type === 'del') {
|
||||||
|
await this.delete(op.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async open(): Promise<void> {
|
||||||
|
await this.db.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
if (this._db) {
|
||||||
|
await this._db.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async dump(): Promise<any> {
|
async dump(): Promise<any> {
|
||||||
@@ -134,7 +150,7 @@ export class LevelDbCache<V = any> implements SortKeyCache<V> {
|
|||||||
return lastSortKey == '' ? null : lastSortKey;
|
return lastSortKey == '' ? null : lastSortKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
async allContracts(): Promise<string[]> {
|
async keys(): Promise<string[]> {
|
||||||
await this.db.open();
|
await this.db.open();
|
||||||
const keys = await this.db.keys().all();
|
const keys = await this.db.keys().all();
|
||||||
|
|
||||||
@@ -170,7 +186,7 @@ export class LevelDbCache<V = any> implements SortKeyCache<V> {
|
|||||||
entriesStored = 1;
|
entriesStored = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const contracts = await this.allContracts();
|
const contracts = await this.keys();
|
||||||
for (let i = 0; i < contracts.length; i++) {
|
for (let i = 0; i < contracts.length; i++) {
|
||||||
const contractCache = this.db.sublevel<string, any>(contracts[i], { valueEncoding: 'json' });
|
const contractCache = this.db.sublevel<string, any>(contracts[i], { valueEncoding: 'json' });
|
||||||
|
|
||||||
|
|||||||
@@ -232,4 +232,6 @@ export interface Contract<State = unknown> {
|
|||||||
getEoEvaluator(): EvaluationOptionsEvaluator;
|
getEoEvaluator(): EvaluationOptionsEvaluator;
|
||||||
|
|
||||||
isRoot(): boolean;
|
isRoot(): boolean;
|
||||||
|
|
||||||
|
getStorageValues(keys: string[]): Promise<SortKeyCacheResult<Map<string, any>>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,8 @@ export class EvaluationOptionsEvaluator {
|
|||||||
allowBigInt: () => this.rootOptions['allowBigInt'], // not sure about this
|
allowBigInt: () => this.rootOptions['allowBigInt'], // not sure about this
|
||||||
walletBalanceUrl: () => this.rootOptions['walletBalanceUrl'],
|
walletBalanceUrl: () => this.rootOptions['walletBalanceUrl'],
|
||||||
mineArLocalBlocks: () => this.rootOptions['mineArLocalBlocks'],
|
mineArLocalBlocks: () => this.rootOptions['mineArLocalBlocks'],
|
||||||
cacheEveryNInteractions: () => this.rootOptions['cacheEveryNInteractions']
|
cacheEveryNInteractions: () => this.rootOptions['cacheEveryNInteractions'],
|
||||||
|
useKVStorage: (foreignOptions) => foreignOptions['useKVStorage']
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -114,9 +115,12 @@ export class EvaluationOptionsEvaluator {
|
|||||||
if (manifestOptions) {
|
if (manifestOptions) {
|
||||||
const errors = [];
|
const errors = [];
|
||||||
for (const k in manifestOptions) {
|
for (const k in manifestOptions) {
|
||||||
|
if (k === 'useKVStorage') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (userSetOptions[k] !== manifestOptions[k]) {
|
if (userSetOptions[k] !== manifestOptions[k]) {
|
||||||
errors.push(
|
errors.push(
|
||||||
`Option {${k}} differs. EvaluationOptions: [${userSetOptions[k]}], manifest: [${manifestOptions[k]}]. Use contract.setEvaluationOptions({${k}: ${manifestOptions[k]}) to evaluate contract state.`
|
`Option {${k}} differs. EvaluationOptions: [${userSetOptions[k]}], manifest: [${manifestOptions[k]}]. Use contract.setEvaluationOptions({${k}: ${manifestOptions[k]}}) to evaluate contract state.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,13 +73,16 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
|||||||
this._sorter = new LexicographicalInteractionsSorter(warp.arweave);
|
this._sorter = new LexicographicalInteractionsSorter(warp.arweave);
|
||||||
if (_parentContract != null) {
|
if (_parentContract != null) {
|
||||||
this._evaluationOptions = this.getRoot().evaluationOptions();
|
this._evaluationOptions = this.getRoot().evaluationOptions();
|
||||||
|
if (_parentContract.evaluationOptions().useKVStorage) {
|
||||||
|
throw new Error('Foreign writes or reads are forbidden for kv storage contracts');
|
||||||
|
}
|
||||||
this._callDepth = _parentContract.callDepth() + 1;
|
this._callDepth = _parentContract.callDepth() + 1;
|
||||||
const callingInteraction: InteractionCall = _parentContract
|
const callingInteraction: InteractionCall = _parentContract
|
||||||
.getCallStack()
|
.getCallStack()
|
||||||
.getInteraction(_innerCallData.callingInteraction.id);
|
.getInteraction(_innerCallData.callingInteraction.id);
|
||||||
|
|
||||||
if (this._callDepth > this._evaluationOptions.maxCallDepth) {
|
if (this._callDepth > this._evaluationOptions.maxCallDepth) {
|
||||||
throw Error(
|
throw new Error(
|
||||||
`Max call depth of ${this._evaluationOptions.maxCallDepth} has been exceeded for interaction ${JSON.stringify(
|
`Max call depth of ${this._evaluationOptions.maxCallDepth} has been exceeded for interaction ${JSON.stringify(
|
||||||
callingInteraction.interactionInput
|
callingInteraction.interactionInput
|
||||||
)}`
|
)}`
|
||||||
@@ -468,7 +471,6 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
|||||||
this.logger.debug('State fully cached, not loading interactions.');
|
this.logger.debug('State fully cached, not loading interactions.');
|
||||||
if (forceDefinitionLoad || evolvedSrcTxId || interactions?.length) {
|
if (forceDefinitionLoad || evolvedSrcTxId || interactions?.length) {
|
||||||
contractDefinition = await definitionLoader.load<State>(contractTxId, evolvedSrcTxId);
|
contractDefinition = await definitionLoader.load<State>(contractTxId, evolvedSrcTxId);
|
||||||
handler = await this.safeGetHandler(contractDefinition);
|
|
||||||
if (interactions?.length) {
|
if (interactions?.length) {
|
||||||
sortedInteractions = this._sorter.sort(interactions.map((i) => ({ node: i, cursor: null })));
|
sortedInteractions = this._sorter.sort(interactions.map((i) => ({ node: i, cursor: null })));
|
||||||
}
|
}
|
||||||
@@ -507,7 +509,6 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
|||||||
// - as no other contracts will be called.
|
// - as no other contracts will be called.
|
||||||
this._rootSortKey = sortedInteractions[sortedInteractions.length - 1].sortKey;
|
this._rootSortKey = sortedInteractions[sortedInteractions.length - 1].sortKey;
|
||||||
}
|
}
|
||||||
handler = await this.safeGetHandler(contractDefinition);
|
|
||||||
}
|
}
|
||||||
if (this.isRoot()) {
|
if (this.isRoot()) {
|
||||||
this._eoEvaluator = new EvaluationOptionsEvaluator(
|
this._eoEvaluator = new EvaluationOptionsEvaluator(
|
||||||
@@ -518,9 +519,19 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
|||||||
const contractEvaluationOptions = this.isRoot()
|
const contractEvaluationOptions = this.isRoot()
|
||||||
? this._eoEvaluator.rootOptions
|
? this._eoEvaluator.rootOptions
|
||||||
: this.getEoEvaluator().forForeignContract(contractDefinition.manifest?.evaluationOptions);
|
: this.getEoEvaluator().forForeignContract(contractDefinition.manifest?.evaluationOptions);
|
||||||
|
if (!this.isRoot() && contractEvaluationOptions.useKVStorage) {
|
||||||
|
throw new Error('Foreign read/writes cannot be performed on kv storage contracts');
|
||||||
|
}
|
||||||
this.ecLogger.debug(`Evaluation options ${contractTxId}:`, contractEvaluationOptions);
|
this.ecLogger.debug(`Evaluation options ${contractTxId}:`, contractEvaluationOptions);
|
||||||
|
|
||||||
|
if (contractDefinition) {
|
||||||
|
handler = (await this.warp.executorFactory.create(
|
||||||
|
contractDefinition,
|
||||||
|
contractEvaluationOptions,
|
||||||
|
this.warp
|
||||||
|
)) as HandlerApi<State>;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
warp: this.warp,
|
warp: this.warp,
|
||||||
contract: this,
|
contract: this,
|
||||||
@@ -533,11 +544,6 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async safeGetHandler(contractDefinition: ContractDefinition<any>): Promise<HandlerApi<State> | null> {
|
|
||||||
const { executorFactory } = this.warp;
|
|
||||||
return (await executorFactory.create(contractDefinition, this._evaluationOptions, this.warp)) as HandlerApi<State>;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getToSortKey(upToSortKey?: string) {
|
private getToSortKey(upToSortKey?: string) {
|
||||||
if (this._parentContract?.rootSortKey) {
|
if (this._parentContract?.rootSortKey) {
|
||||||
if (!upToSortKey) {
|
if (!upToSortKey) {
|
||||||
@@ -815,4 +821,30 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
|||||||
isRoot(): boolean {
|
isRoot(): boolean {
|
||||||
return this._parentContract == null;
|
return this._parentContract == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getStorageValues(keys: string[]): Promise<SortKeyCacheResult<Map<string, any>>> {
|
||||||
|
const lastCached = await this.warp.stateEvaluator.getCache().getLast(this.txId());
|
||||||
|
if (lastCached == null) {
|
||||||
|
return {
|
||||||
|
sortKey: null,
|
||||||
|
cachedValue: new Map()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const storage = this.warp.kvStorageFactory(this.txId());
|
||||||
|
const result: Map<string, any> = new Map();
|
||||||
|
try {
|
||||||
|
await storage.open();
|
||||||
|
for (const key of keys) {
|
||||||
|
const lastValue = await storage.getLessOrEqual(key, lastCached.sortKey);
|
||||||
|
result.set(key, lastValue == null ? null : lastValue.cachedValue);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
sortKey: lastCached.sortKey,
|
||||||
|
cachedValue: result
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
await storage.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,11 @@ import { ContractDefinition, SrcCache } from './ContractDefinition';
|
|||||||
import { CustomSignature } from '../contract/Signature';
|
import { CustomSignature } from '../contract/Signature';
|
||||||
import { SourceData } from '../contract/deploy/impl/SourceImpl';
|
import { SourceData } from '../contract/deploy/impl/SourceImpl';
|
||||||
import Transaction from 'arweave/node/lib/transaction';
|
import Transaction from 'arweave/node/lib/transaction';
|
||||||
|
import { DEFAULT_LEVEL_DB_LOCATION } from './WarpFactory';
|
||||||
|
import { LevelDbCache } from '../cache/impl/LevelDbCache';
|
||||||
|
|
||||||
export type WarpEnvironment = 'local' | 'testnet' | 'mainnet' | 'custom';
|
export type WarpEnvironment = 'local' | 'testnet' | 'mainnet' | 'custom';
|
||||||
|
export type KVStorageFactory = (contractTxId: string) => SortKeyCache<any>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Warp "motherboard" ;-).
|
* The Warp "motherboard" ;-).
|
||||||
@@ -49,6 +52,7 @@ export class Warp {
|
|||||||
*/
|
*/
|
||||||
readonly createContract: CreateContract;
|
readonly createContract: CreateContract;
|
||||||
readonly testing: Testing;
|
readonly testing: Testing;
|
||||||
|
kvStorageFactory: KVStorageFactory;
|
||||||
|
|
||||||
private readonly plugins: Map<WarpPluginType, WarpPlugin<unknown, unknown>> = new Map();
|
private readonly plugins: Map<WarpPluginType, WarpPlugin<unknown, unknown>> = new Map();
|
||||||
|
|
||||||
@@ -62,6 +66,12 @@ export class Warp {
|
|||||||
) {
|
) {
|
||||||
this.createContract = new DefaultCreateContract(arweave, this);
|
this.createContract = new DefaultCreateContract(arweave, this);
|
||||||
this.testing = new Testing(arweave);
|
this.testing = new Testing(arweave);
|
||||||
|
this.kvStorageFactory = (contractTxId: string) => {
|
||||||
|
return new LevelDbCache({
|
||||||
|
inMemory: false,
|
||||||
|
dbLocation: `${DEFAULT_LEVEL_DB_LOCATION}/kv/ldb/${contractTxId}`
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static builder(
|
static builder(
|
||||||
@@ -178,4 +188,9 @@ export class Warp {
|
|||||||
knownWarpPlugins.includes(value as WarpKnownPluginType) || knownWarpPluginsPartial.some((p) => value.match(p))
|
knownWarpPlugins.includes(value as WarpKnownPluginType) || knownWarpPluginsPartial.some((p) => value.match(p))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useKVStorageFactory(factory: KVStorageFactory): Warp {
|
||||||
|
this.kvStorageFactory = factory;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,8 +42,6 @@ export const defaultCacheOptions: CacheOptions = {
|
|||||||
* All versions use the {@link Evolve} plugin.
|
* All versions use the {@link Evolve} plugin.
|
||||||
*/
|
*/
|
||||||
export class WarpFactory {
|
export class WarpFactory {
|
||||||
private stateCache: SortKeyCache<EvalStateResult<unknown>>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* creates a Warp instance suitable for testing in a local environment
|
* creates a Warp instance suitable for testing in a local environment
|
||||||
* (e.g. usually using ArLocal)
|
* (e.g. usually using ArLocal)
|
||||||
@@ -61,7 +59,7 @@ export class WarpFactory {
|
|||||||
...defaultCacheOptions,
|
...defaultCacheOptions,
|
||||||
inMemory: true
|
inMemory: true
|
||||||
}
|
}
|
||||||
) {
|
): Warp {
|
||||||
return this.customArweaveGw(arweave, cacheOptions, 'local');
|
return this.customArweaveGw(arweave, cacheOptions, 'local');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +75,7 @@ export class WarpFactory {
|
|||||||
port: 443,
|
port: 443,
|
||||||
protocol: 'https'
|
protocol: 'https'
|
||||||
})
|
})
|
||||||
) {
|
): Warp {
|
||||||
if (useArweaveGw) {
|
if (useArweaveGw) {
|
||||||
return this.customArweaveGw(arweave, cacheOptions, 'testnet');
|
return this.customArweaveGw(arweave, cacheOptions, 'testnet');
|
||||||
} else {
|
} else {
|
||||||
@@ -106,7 +104,7 @@ export class WarpFactory {
|
|||||||
port: 443,
|
port: 443,
|
||||||
protocol: 'https'
|
protocol: 'https'
|
||||||
})
|
})
|
||||||
) {
|
): Warp {
|
||||||
if (useArweaveGw) {
|
if (useArweaveGw) {
|
||||||
return this.customArweaveGw(arweave, cacheOptions, 'mainnet');
|
return this.customArweaveGw(arweave, cacheOptions, 'mainnet');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -140,6 +140,8 @@ export class DefaultEvaluationOptions implements EvaluationOptions {
|
|||||||
throwOnInternalWriteError = true;
|
throwOnInternalWriteError = true;
|
||||||
|
|
||||||
cacheEveryNInteractions = -1;
|
cacheEveryNInteractions = -1;
|
||||||
|
|
||||||
|
useKVStorage = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// an interface for the contract EvaluationOptions - can be used to change the behaviour of some features.
|
// an interface for the contract EvaluationOptions - can be used to change the behaviour of some features.
|
||||||
@@ -217,4 +219,7 @@ export interface EvaluationOptions {
|
|||||||
// force SDK to cache the state after evaluating each N interactions
|
// force SDK to cache the state after evaluating each N interactions
|
||||||
// defaults to -1, which effectively turns off this feature
|
// defaults to -1, which effectively turns off this feature
|
||||||
cacheEveryNInteractions: number;
|
cacheEveryNInteractions: number;
|
||||||
|
|
||||||
|
// whether a separate key-value storage should be used for the contract
|
||||||
|
useKVStorage: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
|
|||||||
contractTxId: string,
|
contractTxId: string,
|
||||||
sortKey: string
|
sortKey: string
|
||||||
): Promise<SortKeyCacheResult<EvalStateResult<State>> | null> {
|
): Promise<SortKeyCacheResult<EvalStateResult<State>> | null> {
|
||||||
return (await this.cache.get(contractTxId, sortKey)) as SortKeyCacheResult<EvalStateResult<State>>;
|
return (await this.cache.get(new CacheKey(contractTxId, sortKey))) as SortKeyCacheResult<EvalStateResult<State>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async hasContractCached(contractTxId: string): Promise<boolean> {
|
async hasContractCached(contractTxId: string): Promise<boolean> {
|
||||||
@@ -232,7 +232,7 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async allCachedContracts(): Promise<string[]> {
|
async allCachedContracts(): Promise<string[]> {
|
||||||
return await this.cache.allContracts();
|
return await this.cache.keys();
|
||||||
}
|
}
|
||||||
|
|
||||||
setCache(cache: SortKeyCache<EvalStateResult<unknown>>): void {
|
setCache(cache: SortKeyCache<EvalStateResult<unknown>>): void {
|
||||||
|
|||||||
@@ -46,13 +46,20 @@ export class HandlerExecutorFactory implements ExecutorFactory<HandlerApi<unknow
|
|||||||
evaluationOptions: EvaluationOptions,
|
evaluationOptions: EvaluationOptions,
|
||||||
warp: Warp
|
warp: Warp
|
||||||
): Promise<HandlerApi<State>> {
|
): Promise<HandlerApi<State>> {
|
||||||
|
let kvStorage = null;
|
||||||
|
|
||||||
|
if (evaluationOptions.useKVStorage) {
|
||||||
|
kvStorage = warp.kvStorageFactory(contractDefinition.txId);
|
||||||
|
}
|
||||||
|
|
||||||
const swGlobal = new SmartWeaveGlobal(
|
const swGlobal = new SmartWeaveGlobal(
|
||||||
this.arweave,
|
this.arweave,
|
||||||
{
|
{
|
||||||
id: contractDefinition.txId,
|
id: contractDefinition.txId,
|
||||||
owner: contractDefinition.owner
|
owner: contractDefinition.owner
|
||||||
},
|
},
|
||||||
evaluationOptions
|
evaluationOptions,
|
||||||
|
kvStorage
|
||||||
);
|
);
|
||||||
|
|
||||||
const extensionPlugins = warp.matchPlugins(`^smartweave-extension-`);
|
const extensionPlugins = warp.matchPlugins(`^smartweave-extension-`);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const sortingFirst = ''.padEnd(64, '0');
|
|||||||
export const sortingLast = ''.padEnd(64, 'z');
|
export const sortingLast = ''.padEnd(64, 'z');
|
||||||
|
|
||||||
export const genesisSortKey = `${''.padStart(12, '0')},${firstSortKeyMs},${sortingFirst}`;
|
export const genesisSortKey = `${''.padStart(12, '0')},${firstSortKeyMs},${sortingFirst}`;
|
||||||
export const lastPossibleKey = `${''.padStart(12, '9')},${lastSortKeyMs},${sortingLast}`;
|
export const lastPossibleSortKey = `${''.padStart(12, '9')},${lastSortKeyMs},${sortingLast}`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* implementation that is based on current's SDK sorting alg.
|
* implementation that is based on current's SDK sorting alg.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { DefinitionLoader } from '../DefinitionLoader';
|
|||||||
import { WasmSrc } from './wasm/WasmSrc';
|
import { WasmSrc } from './wasm/WasmSrc';
|
||||||
import { WarpEnvironment } from '../../Warp';
|
import { WarpEnvironment } from '../../Warp';
|
||||||
import { TagsParser } from './TagsParser';
|
import { TagsParser } from './TagsParser';
|
||||||
import { SortKeyCache } from '../../../cache/SortKeyCache';
|
import { CacheKey, SortKeyCache } from '../../../cache/SortKeyCache';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An extension to {@link ContractDefinitionLoader} that makes use of
|
* An extension to {@link ContractDefinitionLoader} that makes use of
|
||||||
@@ -137,12 +137,12 @@ export class WarpGatewayContractDefinitionLoader implements DefinitionLoader {
|
|||||||
|
|
||||||
// Gets ContractDefinition and ContractSource from two caches and returns a combined structure
|
// Gets ContractDefinition and ContractSource from two caches and returns a combined structure
|
||||||
private async getFromCache(contractTxId: string, srcTxId?: string): Promise<ContractDefinition<any> | null> {
|
private async getFromCache(contractTxId: string, srcTxId?: string): Promise<ContractDefinition<any> | null> {
|
||||||
const contract = await this.definitionCache.get(contractTxId, 'cd');
|
const contract = await this.definitionCache.get(new CacheKey(contractTxId, 'cd'));
|
||||||
if (!contract) {
|
if (!contract) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const src = await this.srcCache.get(srcTxId || contract.cachedValue.srcTxId, 'src');
|
const src = await this.srcCache.get(new CacheKey(srcTxId || contract.cachedValue.srcTxId, 'src'));
|
||||||
if (!src) {
|
if (!src) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -153,7 +153,7 @@ export class WarpGatewayContractDefinitionLoader implements DefinitionLoader {
|
|||||||
private async putToCache(contractTxId: string, value: ContractDefinition<any>, srcTxId?: string): Promise<void> {
|
private async putToCache(contractTxId: string, value: ContractDefinition<any>, srcTxId?: string): Promise<void> {
|
||||||
const src = new SrcCache(value);
|
const src = new SrcCache(value);
|
||||||
const contract = new ContractCache(value);
|
const contract = new ContractCache(value);
|
||||||
await this.definitionCache.put({ contractTxId: contractTxId, sortKey: 'cd' }, contract);
|
await this.definitionCache.put({ key: contractTxId, sortKey: 'cd' }, contract);
|
||||||
await this.srcCache.put({ contractTxId: srcTxId || contract.srcTxId, sortKey: 'src' }, src);
|
await this.srcCache.put({ key: srcTxId || contract.srcTxId, sortKey: 'src' }, src);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { ContractDefinition } from '../../../../core/ContractDefinition';
|
|||||||
import { ExecutionContext } from '../../../../core/ExecutionContext';
|
import { ExecutionContext } from '../../../../core/ExecutionContext';
|
||||||
import { EvalStateResult } from '../../../../core/modules/StateEvaluator';
|
import { EvalStateResult } from '../../../../core/modules/StateEvaluator';
|
||||||
import { SmartWeaveGlobal } from '../../../../legacy/smartweave-global';
|
import { SmartWeaveGlobal } from '../../../../legacy/smartweave-global';
|
||||||
import { timeout, deepCopy } from '../../../../utils/utils';
|
import { deepCopy, timeout } from '../../../../utils/utils';
|
||||||
import { InteractionData, InteractionResult } from '../HandlerExecutorFactory';
|
import { InteractionData, InteractionResult } from '../HandlerExecutorFactory';
|
||||||
import { AbstractContractHandler } from './AbstractContractHandler';
|
import { AbstractContractHandler } from './AbstractContractHandler';
|
||||||
|
import { Level } from 'level';
|
||||||
|
import { DEFAULT_LEVEL_DB_LOCATION } from '../../../WarpFactory';
|
||||||
|
|
||||||
export class JsHandlerApi<State> extends AbstractContractHandler<State> {
|
export class JsHandlerApi<State> extends AbstractContractHandler<State> {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -39,9 +41,11 @@ export class JsHandlerApi<State> extends AbstractContractHandler<State> {
|
|||||||
|
|
||||||
const { warp } = executionContext;
|
const { warp } = executionContext;
|
||||||
|
|
||||||
|
await this.swGlobal.kv.open();
|
||||||
const handlerResult = await Promise.race([timeoutPromise, this.contractFunction(stateCopy, interaction)]);
|
const handlerResult = await Promise.race([timeoutPromise, this.contractFunction(stateCopy, interaction)]);
|
||||||
|
|
||||||
if (handlerResult && (handlerResult.state !== undefined || handlerResult.result !== undefined)) {
|
if (handlerResult && (handlerResult.state !== undefined || handlerResult.result !== undefined)) {
|
||||||
|
await this.swGlobal.kv.commit();
|
||||||
return {
|
return {
|
||||||
type: 'ok',
|
type: 'ok',
|
||||||
result: handlerResult.result,
|
result: handlerResult.result,
|
||||||
@@ -52,6 +56,7 @@ export class JsHandlerApi<State> extends AbstractContractHandler<State> {
|
|||||||
// Will be caught below as unexpected exception.
|
// Will be caught below as unexpected exception.
|
||||||
throw new Error(`Unexpected result from contract: ${JSON.stringify(handlerResult)}`);
|
throw new Error(`Unexpected result from contract: ${JSON.stringify(handlerResult)}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
await this.swGlobal.kv.rollback();
|
||||||
switch (err.name) {
|
switch (err.name) {
|
||||||
case 'ContractError':
|
case 'ContractError':
|
||||||
return {
|
return {
|
||||||
@@ -73,6 +78,7 @@ export class JsHandlerApi<State> extends AbstractContractHandler<State> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
await this.swGlobal.kv.close();
|
||||||
if (timeoutId !== null) {
|
if (timeoutId !== null) {
|
||||||
// it is important to clear the timeout promise
|
// it is important to clear the timeout promise
|
||||||
// - promise.race won't "cancel" it automatically if the "handler" promise "wins"
|
// - promise.race won't "cancel" it automatically if the "handler" promise "wins"
|
||||||
|
|||||||
@@ -33,8 +33,9 @@ export class WasmHandlerApi<State> extends AbstractContractHandler<State> {
|
|||||||
this.assignReadContractState<Input>(executionContext, currentTx, currentResult, interactionTx);
|
this.assignReadContractState<Input>(executionContext, currentTx, currentResult, interactionTx);
|
||||||
this.assignWrite(executionContext, currentTx);
|
this.assignWrite(executionContext, currentTx);
|
||||||
|
|
||||||
|
await this.swGlobal.kv.open();
|
||||||
const handlerResult = await this.doHandle(interaction);
|
const handlerResult = await this.doHandle(interaction);
|
||||||
|
await this.swGlobal.kv.commit();
|
||||||
return {
|
return {
|
||||||
type: 'ok',
|
type: 'ok',
|
||||||
result: handlerResult,
|
result: handlerResult,
|
||||||
@@ -42,6 +43,7 @@ export class WasmHandlerApi<State> extends AbstractContractHandler<State> {
|
|||||||
gasUsed: this.swGlobal.gasUsed
|
gasUsed: this.swGlobal.gasUsed
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
await this.swGlobal.kv.rollback();
|
||||||
// note: as exceptions handling in WASM is currently somewhat non-existent
|
// note: as exceptions handling in WASM is currently somewhat non-existent
|
||||||
// https://www.assemblyscript.org/status.html#exceptions
|
// https://www.assemblyscript.org/status.html#exceptions
|
||||||
// and since we have to somehow differentiate different types of exceptions
|
// and since we have to somehow differentiate different types of exceptions
|
||||||
@@ -70,6 +72,8 @@ export class WasmHandlerApi<State> extends AbstractContractHandler<State> {
|
|||||||
type: 'error'
|
type: 'error'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
await this.swGlobal.kv.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import Arweave from 'arweave';
|
import Arweave from 'arweave';
|
||||||
import { EvaluationOptions } from '../core/modules/StateEvaluator';
|
import { EvaluationOptions } from '../core/modules/StateEvaluator';
|
||||||
import { GQLNodeInterface, GQLTagInterface, VrfData } from './gqlResult';
|
import { GQLNodeInterface, GQLTagInterface, VrfData } from './gqlResult';
|
||||||
|
import { BatchDBOp, CacheKey, PutBatch, SortKeyCache } from '../cache/SortKeyCache';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -56,7 +57,14 @@ export class SmartWeaveGlobal {
|
|||||||
|
|
||||||
caller?: string;
|
caller?: string;
|
||||||
|
|
||||||
constructor(arweave: Arweave, contract: { id: string; owner: string }, evaluationOptions: EvaluationOptions) {
|
kv: KV;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
arweave: Arweave,
|
||||||
|
contract: { id: string; owner: string },
|
||||||
|
evaluationOptions: EvaluationOptions,
|
||||||
|
storage: SortKeyCache<any> | null
|
||||||
|
) {
|
||||||
this.gasUsed = 0;
|
this.gasUsed = 0;
|
||||||
this.gasLimit = Number.MAX_SAFE_INTEGER;
|
this.gasLimit = Number.MAX_SAFE_INTEGER;
|
||||||
this.unsafeClient = arweave;
|
this.unsafeClient = arweave;
|
||||||
@@ -95,6 +103,8 @@ export class SmartWeaveGlobal {
|
|||||||
this.getBalance = this.getBalance.bind(this);
|
this.getBalance = this.getBalance.bind(this);
|
||||||
|
|
||||||
this.extensions = {};
|
this.extensions = {};
|
||||||
|
|
||||||
|
this.kv = new KV(storage, this.transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
useGas(gas: number) {
|
useGas(gas: number) {
|
||||||
@@ -162,6 +172,13 @@ class Transaction {
|
|||||||
return this.smartWeaveGlobal._activeTx.tags;
|
return this.smartWeaveGlobal._activeTx.tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get sortKey(): string {
|
||||||
|
if (!this.smartWeaveGlobal._activeTx) {
|
||||||
|
throw new Error('No current Tx');
|
||||||
|
}
|
||||||
|
return this.smartWeaveGlobal._activeTx.sortKey;
|
||||||
|
}
|
||||||
|
|
||||||
get quantity() {
|
get quantity() {
|
||||||
if (!this.smartWeaveGlobal._activeTx) {
|
if (!this.smartWeaveGlobal._activeTx) {
|
||||||
throw new Error('No current Tx');
|
throw new Error('No current Tx');
|
||||||
@@ -229,3 +246,69 @@ class Vrf {
|
|||||||
return Number(result);
|
return Number(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class KV {
|
||||||
|
private _kvBatch: BatchDBOp<any>[] = [];
|
||||||
|
|
||||||
|
constructor(private readonly _storage: SortKeyCache<any> | null, private readonly _transaction: Transaction) {}
|
||||||
|
|
||||||
|
async put(key: string, value: any): Promise<void> {
|
||||||
|
this.checkStorageAvailable();
|
||||||
|
this._kvBatch.push({
|
||||||
|
type: 'put',
|
||||||
|
key: new CacheKey(key, this._transaction.sortKey),
|
||||||
|
value: value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async get<V>(key: string): Promise<V | null> {
|
||||||
|
this.checkStorageAvailable();
|
||||||
|
const sortKey = this._transaction.sortKey;
|
||||||
|
|
||||||
|
if (this._kvBatch.length > 0) {
|
||||||
|
const putBatches = this._kvBatch.filter((batchOp) => batchOp.type === 'put') as PutBatch<any>[];
|
||||||
|
const matchingPutBatch = putBatches.reverse().find((batchOp) => {
|
||||||
|
return batchOp.key.key === key && batchOp.key.sortKey === sortKey;
|
||||||
|
}) as PutBatch<V>;
|
||||||
|
if (matchingPutBatch !== undefined) {
|
||||||
|
return matchingPutBatch.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this._storage.getLessOrEqual(key, this._transaction.sortKey);
|
||||||
|
return result?.cachedValue || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async commit(): Promise<void> {
|
||||||
|
if (this._storage) {
|
||||||
|
await this._storage.batch(this._kvBatch);
|
||||||
|
this._kvBatch = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rollback(): void {
|
||||||
|
this._kvBatch = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
ops(): BatchDBOp<any>[] {
|
||||||
|
return structuredClone(this._kvBatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
open(): Promise<void> {
|
||||||
|
if (this._storage) {
|
||||||
|
return this._storage.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): Promise<void> {
|
||||||
|
if (this._storage) {
|
||||||
|
return this._storage.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkStorageAvailable() {
|
||||||
|
if (!this._storage) {
|
||||||
|
throw new Error('KV Storage not available');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
71
tools/data/js/kv-storage.js
Normal file
71
tools/data/js/kv-storage.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
export async function handle(state, action) {
|
||||||
|
const input = action.input;
|
||||||
|
const caller = action.caller;
|
||||||
|
|
||||||
|
if (!state.kvOps) {
|
||||||
|
state.kvOps = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.function === 'mint') {
|
||||||
|
console.log('mint', input.target, input.qty.toString());
|
||||||
|
await SmartWeave.kv.put(input.target, input.qty.toString());
|
||||||
|
// for debug or whatever
|
||||||
|
//state.kvOps[SmartWeave.transaction.id] = SmartWeave.kv.ops();
|
||||||
|
return {state};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.function === 'transfer') {
|
||||||
|
const target = input.target;
|
||||||
|
const qty = input.qty;
|
||||||
|
|
||||||
|
if (!Number.isInteger(qty)) {
|
||||||
|
throw new ContractError('Invalid value for "qty". Must be an integer');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
throw new ContractError('No target specified');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qty <= 0 || caller === target) {
|
||||||
|
throw new ContractError('Invalid token transfer');
|
||||||
|
}
|
||||||
|
|
||||||
|
let callerBalance = await SmartWeave.kv.get(caller);
|
||||||
|
callerBalance = callerBalance ? parseInt(callerBalance) : 0;
|
||||||
|
|
||||||
|
if (callerBalance < qty) {
|
||||||
|
throw new ContractError(`Caller balance not high enough to send ${qty} token(s)!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lower the token balance of the caller
|
||||||
|
callerBalance -= qty;
|
||||||
|
await SmartWeave.kv.put(caller, callerBalance.toString());
|
||||||
|
|
||||||
|
let targetBalance = await SmartWeave.kv.get(target);
|
||||||
|
targetBalance = targetBalance ? parseInt(targetBalance) : 0;
|
||||||
|
|
||||||
|
targetBalance += qty;
|
||||||
|
await SmartWeave.kv.put(target, targetBalance.toString());
|
||||||
|
|
||||||
|
// for debug or whatever
|
||||||
|
//state.kvOps[SmartWeave.transaction.id] = SmartWeave.kv.ops();
|
||||||
|
|
||||||
|
return {state};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.function === 'balance') {
|
||||||
|
const target = input.target;
|
||||||
|
const ticker = state.ticker;
|
||||||
|
|
||||||
|
if (typeof target !== 'string') {
|
||||||
|
throw new ContractError('Must specify target to get balance for');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('balance', input.target);
|
||||||
|
const result = await SmartWeave.kv.get(target);
|
||||||
|
|
||||||
|
return {result: {target, ticker, balance: result ? parseInt(result) : 0}};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ContractError(`No function supplied or function not recognised: "${input.function}"`);
|
||||||
|
}
|
||||||
@@ -18,10 +18,10 @@ async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const warp = WarpFactory
|
const warp = WarpFactory
|
||||||
.forMainnet({...defaultCacheOptions, inMemory: true});
|
.forMainnet({...defaultCacheOptions, inMemory: true});
|
||||||
|
|
||||||
const jsContractSrc = fs.readFileSync(path.join(__dirname, 'data/js/token-pst.js'), 'utf8');
|
const jsContractSrc = fs.readFileSync(path.join(__dirname, 'data/js/kv-storage.js'), 'utf8');
|
||||||
const initialState = fs.readFileSync(path.join(__dirname, 'data/js/token-pst.json'), 'utf8');
|
const initialState = fs.readFileSync(path.join(__dirname, 'data/js/token-pst.json'), 'utf8');
|
||||||
|
|
||||||
// case 1 - full deploy, js contract
|
// case 1 - full deploy, js contract
|
||||||
@@ -31,8 +31,7 @@ async function main() {
|
|||||||
src: jsContractSrc,
|
src: jsContractSrc,
|
||||||
evaluationManifest: {
|
evaluationManifest: {
|
||||||
evaluationOptions: {
|
evaluationOptions: {
|
||||||
unsafeClient: 'skip',
|
useKVStorage: true
|
||||||
internalWrites: true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -63,31 +62,31 @@ async function main() {
|
|||||||
});*/
|
});*/
|
||||||
|
|
||||||
const contract = warp.contract<any>(contractTxId)
|
const contract = warp.contract<any>(contractTxId)
|
||||||
.setEvaluationOptions({internalWrites: false, unsafeClient: 'throw', allowBigInt: true})
|
.setEvaluationOptions({})
|
||||||
.connect(wallet);
|
.connect(wallet);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
contract.writeInteraction<any>({
|
/*contract.writeInteraction<any>({
|
||||||
function: "transfer",
|
function: "transfer",
|
||||||
target: "M-mpNeJbg9h7mZ-uHaNsa5jwFFRAq0PsTkNWXJ-ojwI",
|
target: "M-mpNeJbg9h7mZ-uHaNsa5jwFFRAq0PsTkNWXJ-ojwI",
|
||||||
qty: 100
|
qty: 100
|
||||||
|
}),*/
|
||||||
|
contract.writeInteraction<any>({
|
||||||
|
function: "mint",
|
||||||
|
target: 'follows:0xe0',
|
||||||
|
qty: 100
|
||||||
}),
|
}),
|
||||||
contract.writeInteraction<any>({
|
/*contract.writeInteraction<any>({
|
||||||
function: "transfer",
|
function: "transfer",
|
||||||
target: "M-mpNeJbg9h7mZ-uHaNsa5jwFFRAq0PsTkNWXJ-ojwI",
|
target: "M-mpNeJbg9h7mZ-uHaNsa5jwFFRAq0PsTkNWXJ-ojwI",
|
||||||
qty: 100
|
qty: 100
|
||||||
}),
|
})*/
|
||||||
contract.writeInteraction<any>({
|
|
||||||
function: "transfer",
|
|
||||||
target: "M-mpNeJbg9h7mZ-uHaNsa5jwFFRAq0PsTkNWXJ-ojwI",
|
|
||||||
qty: 100
|
|
||||||
})
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const {cachedValue} = await contract.readState();
|
//const {cachedValue} = await contract.readState();
|
||||||
|
|
||||||
logger.info("Result");
|
//logger.info("Result", await contract.getStorageValue('33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA'));
|
||||||
console.dir(cachedValue.state);
|
//console.dir(cachedValue.state);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//logger.error(e)
|
//logger.error(e)
|
||||||
|
|||||||
@@ -1,32 +1,77 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import {Level} from "level";
|
import {Level} from "level";
|
||||||
|
|
||||||
import { MemoryLevel } from 'memory-level';
|
import {DEFAULT_LEVEL_DB_LOCATION, sleep, timeout} from "../src";
|
||||||
import fs from "fs";
|
|
||||||
import {WasmSrc} from "../src";
|
|
||||||
import {Buffer} from "buffer";
|
|
||||||
|
|
||||||
// Create a database
|
class SwGlobalMock {
|
||||||
|
db: Level;
|
||||||
|
|
||||||
async function test() {
|
|
||||||
//const db = new Level<string, any>('./leveldb', {valueEncoding: 'json'});
|
|
||||||
const db = new MemoryLevel<string, any>({ valueEncoding: 'json' });
|
|
||||||
const wasmSrc = fs.readFileSync('./tools/data/rust/rust-pst_bg.wasm');
|
|
||||||
|
|
||||||
const contractData = {
|
|
||||||
src: wasmSrc,
|
|
||||||
id: 'n05LTiuWcAYjizXAu-ghegaWjL89anZ6VdvuHcU6dno',
|
|
||||||
srcId: 'foobar'
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(contractData);
|
|
||||||
|
|
||||||
|
|
||||||
await db.put("n05LTiuWcAYjizXAu-ghegaWjL89anZ6VdvuHcU6dno", contractData);
|
|
||||||
const result = await db.get("n05LTiuWcAYjizXAu-ghegaWjL89anZ6VdvuHcU6dno");
|
|
||||||
console.log(result);
|
|
||||||
console.log(Buffer.from(result.src.data));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test();
|
async function test() {
|
||||||
|
const {handle: handleFn2, swGlobal} = await prepareHandle();
|
||||||
|
|
||||||
|
// simulates the code of the JsHandlerAPI.handle
|
||||||
|
await doHandle(swGlobal, handleFn2);
|
||||||
|
await doHandle(swGlobal, handleFn2);
|
||||||
|
await doHandle(swGlobal, handleFn2);
|
||||||
|
await doHandle(swGlobal, handleFn2);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prepareHandle() {
|
||||||
|
await sleep(10);
|
||||||
|
|
||||||
|
// simulates contract handle function
|
||||||
|
const swGlobal = new SwGlobalMock();
|
||||||
|
|
||||||
|
const handle = new Function(`
|
||||||
|
const [swGlobal] = arguments;
|
||||||
|
|
||||||
|
async function handle(state, input) {
|
||||||
|
await sleep(1000);
|
||||||
|
console.log('from handle:', await swGlobal.db.get('foo'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const sleep = (ms) => {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
return handle;
|
||||||
|
`)(swGlobal);
|
||||||
|
|
||||||
|
return {handle, swGlobal};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doHandle(swGlobal: SwGlobalMock, handleFn: Function) {
|
||||||
|
const {timeoutId, timeoutPromise} = timeout(10);
|
||||||
|
let now = new Date();
|
||||||
|
|
||||||
|
// the kv storage
|
||||||
|
const db = new Level(`${DEFAULT_LEVEL_DB_LOCATION}/kv/the_test_${now}`);
|
||||||
|
swGlobal.db = db;
|
||||||
|
try {
|
||||||
|
console.log('======== Connecting to LMDB KV');
|
||||||
|
await db.open();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.code) // 'LEVEL_DATABASE_NOT_OPEN'
|
||||||
|
if (err.cause && err.cause.code === 'LEVEL_LOCKED') {
|
||||||
|
console.error('LEVEL_LOCKED');
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.put('foo', 'bar');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// simulates calling single contract interaction
|
||||||
|
await Promise.race([timeoutPromise, handleFn()]);
|
||||||
|
|
||||||
|
// simulates 'commit'
|
||||||
|
console.log('======== Committing');
|
||||||
|
await swGlobal.db.batch([]);
|
||||||
|
} finally {
|
||||||
|
console.log('======== Disconnecting from LMDB KV');
|
||||||
|
await db.close()
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test().finally(() => {console.log('done')});
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ app.get('/:type/:key/:blockHeight', async function (req, res, next) {
|
|||||||
const { type, key } = req.params;
|
const { type, key } = req.params;
|
||||||
const blockHeight = parseInt(req.params.blockHeight);
|
const blockHeight = parseInt(req.params.blockHeight);
|
||||||
|
|
||||||
const result = await caches[type].get(key, blockHeight);
|
const result = await caches[type].get({ key: key, sortKey: blockHeight});
|
||||||
console.log('get', result);
|
console.log('get', result);
|
||||||
|
|
||||||
res.send(result);
|
res.send(result);
|
||||||
|
|||||||
9962
tools/web.bundle.js
9962
tools/web.bundle.js
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user