Skip to content

Commit

Permalink
Merge pull request #71 from near/unordered-set
Browse files Browse the repository at this point in the history
Add Unordered set
  • Loading branch information
volovyks authored Jun 1, 2022
2 parents 2416eab + e7a8d00 commit 50f2288
Show file tree
Hide file tree
Showing 7 changed files with 368 additions and 3 deletions.
4 changes: 3 additions & 1 deletion src/collections/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { LookupMap } from "./lookup-map";
import { Vector } from "./vector";
import { LookupSet } from "./lookup-set";
import { UnorderedMap } from "./unordered-map";
import { UnorderedSet } from "./unordered-set";

export {
LookupMap,
Vector,
LookupSet,
UnorderedMap
UnorderedMap,
UnorderedSet
}
108 changes: 108 additions & 0 deletions src/collections/unordered-set.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import * as near from '../api'
import {u8ArrayToString, stringToU8Array} from '../utils'
import { Vector } from './vector'

const ERR_INCONSISTENT_STATE = "The collection is an inconsistent state. Did previous smart contract execution terminate unexpectedly?"

export class UnorderedSet {
constructor(prefix) {
this.length = 0
this.elementIndexPrefix = prefix + 'i'
let elementsPrefix = prefix + 'e'
this.elements = new Vector(elementsPrefix)
}

len() {
return this.elements.len()
}

isEmpty() {
return this.elements.isEmpty()
}

serializeIndex(index) {
let data = new Uint32Array([index])
let array = new Uint8Array(data.buffer)
return u8ArrayToString(array)
}

deserializeIndex(rawIndex) {
let array = stringToU8Array(rawIndex)
let data = new Uint32Array(array.buffer)
return data[0]
}

contains(element) {
let indexLookup = this.elementIndexPrefix + element
return near.jsvmStorageHasKey(indexLookup)
}

set(element) {
let indexLookup = this.elementIndexPrefix + element
if (near.jsvmStorageRead(indexLookup)) {
return false
} else {
let nextIndex = this.len()
let nextIndexRaw = this.serializeIndex(nextIndex)
near.jsvmStorageWrite(indexLookup, nextIndexRaw)
this.elements.push(element)
return true
}
}

remove(element) {
let indexLookup = this.elementIndexPrefix + element
let indexRaw = near.jsvmStorageRead(indexLookup)
if (indexRaw) {
if (this.len() == 1) {
// If there is only one element then swap remove simply removes it without
// swapping with the last element.
near.jsvmStorageRemove(indexLookup)
} else {
// If there is more than one element then swap remove swaps it with the last
// element.
let lastElementRaw = this.elements.get(this.len() - 1)
if (!lastElementRaw) {
throw new Error(ERR_INCONSISTENT_STATE)
}
near.jsvmStorageRemove(indexLookup)
// If the removed element was the last element from keys, then we don't need to
// reinsert the lookup back.
if (lastElementRaw != element) {
let lastLookupElement = this.elementIndexPrefix + lastElementRaw
near.jsvmStorageWrite(lastLookupElement, indexRaw)
}
}
let index = this.deserializeIndex(indexRaw)
this.elements.swapRemove(index)
return true
}
return false
}

clear() {
for (let element of this.elements) {
let indexLookup = this.elementIndexPrefix + element
near.jsvmStorageRemove(indexLookup)
}
this.elements.clear()
}

toArray() {
let ret = []
for (let v of this) {
ret.push(v)
}
return ret
}

[Symbol.iterator]() {
return this.elements[Symbol.iterator]()
}

extend(elements) {
for (let element of elements) {
this.set(element)
}
}
}
6 changes: 4 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
LookupMap,
Vector,
LookupSet,
UnorderedMap
UnorderedMap,
UnorderedSet
} from './collections'

export {
Expand All @@ -25,5 +26,6 @@ export {
LookupMap,
Vector,
LookupSet,
UnorderedMap
UnorderedMap,
UnorderedSet
}
14 changes: 14 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# NEAR-SDK-JS Tests

This tests the functionality of high level APIs of NEAR-SDK-JS. Currently, it directly tests all collections and indirectly tests all decorators, serialization/deserialization, utils, code generation and some important APIs. Majority of near-sdk-js can be seen as tested.

# Run tests
```
yarn
yarn build
yarn test
```

# Add a new test

Create a test contract that covers the API you want to test in `src/`. Add a build command in `build.sh`. Write ava test in `__tests__`.
175 changes: 175 additions & 0 deletions tests/__tests__/unordered-set.ava.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { Worker } from 'near-workspaces';
import { readFile } from 'fs/promises'
import test from 'ava';

function encodeCall(contract, method, args) {
return Buffer.concat([Buffer.from(contract), Buffer.from([0]), Buffer.from(method), Buffer.from([0]), Buffer.from(JSON.stringify(args))])
}

test.beforeEach(async t => {
// Init the worker and start a Sandbox server
const worker = await Worker.init();

// Prepare sandbox for tests, create accounts, deploy contracts, etx.
const root = worker.rootAccount;

// Deploy the jsvm contract.
const jsvm = await root.createAndDeploy(
root.getSubAccount('jsvm').accountId,
'../res/jsvm.wasm',
);

// Deploy test JS contract
const testContract = await root.createSubAccount('test-contract');
let contract_base64 = (await readFile('build/unordered-set.base64')).toString();
await testContract.call(jsvm, 'deploy_js_contract', Buffer.from(contract_base64, 'base64'), { attachedDeposit: '400000000000000000000000' });
await testContract.call(jsvm, 'call_js_contract', encodeCall(testContract.accountId, 'init', []), { attachedDeposit: '400000000000000000000000' });

// Test users
const ali = await root.createSubAccount('ali');
const bob = await root.createSubAccount('bob');
const carl = await root.createSubAccount('carl');

// Save state for test runs
t.context.worker = worker;
t.context.accounts = { root, jsvm, testContract, ali, bob, carl };
});

test.afterEach(async t => {
await t.context.worker.tearDown().catch(error => {
console.log('Failed to tear down the worker:', error);
});
});

test('UnorderedSet is empty by default', async t => {
const { root, jsvm, testContract } = t.context.accounts;
const result = await jsvm.view('view_js_contract', encodeCall(testContract.accountId, 'len', [root.accountId]));
t.is(result, 0);
t.is(
await jsvm.view('view_js_contract', encodeCall(testContract.accountId, 'isEmpty', [])),
true
);
});

test('UnorderedSet set() contains()', async t => {
const { ali, jsvm, testContract } = t.context.accounts;
t.is(
await jsvm.view('view_js_contract', encodeCall(testContract.accountId, 'contains', ['hello'])),
false
);

await ali.call(jsvm, 'call_js_contract', encodeCall(testContract.accountId, 'set', ['hello']), { attachedDeposit: '100000000000000000000000' });

t.is(
await jsvm.view('view_js_contract', encodeCall(testContract.accountId, 'contains', ['hello'])),
true
);
});


test('UnorderedSet insert, len and iterate', async t => {
const { ali, jsvm, testContract } = t.context.accounts;

t.is(
await jsvm.view('view_js_contract', encodeCall(testContract.accountId, 'len', [])),
0
);
t.deepEqual(
await jsvm.view('view_js_contract', encodeCall(testContract.accountId, 'toArray', [])),
[]
);

await ali.call(jsvm, 'call_js_contract', encodeCall(testContract.accountId, 'set', ['hello']), { attachedDeposit: '100000000000000000000000' });
t.is(
await jsvm.view('view_js_contract', encodeCall(testContract.accountId, 'len', [])),
1
);
await ali.call(jsvm, 'call_js_contract', encodeCall(testContract.accountId, 'set', ['hello1']), { attachedDeposit: '100000000000000000000000' });
t.is(
await jsvm.view('view_js_contract', encodeCall(testContract.accountId, 'len', [])),
2
);

// insert the same value, len shouldn't change
await ali.call(jsvm, 'call_js_contract', encodeCall(testContract.accountId, 'set', ['hello1']), { attachedDeposit: '100000000000000000000000' });
t.is(
await jsvm.view('view_js_contract', encodeCall(testContract.accountId, 'len', [])),
2
);

t.deepEqual(
await jsvm.view('view_js_contract', encodeCall(testContract.accountId, 'toArray', [])),
['hello', 'hello1']
);
});

test('UnorderedSet extend, remove, clear', async t => {
const { ali, jsvm, testContract } = t.context.accounts;

await ali.call(jsvm, 'call_js_contract', encodeCall(testContract.accountId, 'extend', [['hello', 'world', 'hello1']]), { attachedDeposit: '100000000000000000000000' });

t.deepEqual(
await jsvm.view('view_js_contract', encodeCall(testContract.accountId, 'toArray', [])),
['hello', 'world', 'hello1']
);

// remove non existing element should not error
await ali.call(jsvm, 'call_js_contract', encodeCall(testContract.accountId, 'remove', ['hello3']), { attachedDeposit: '100000000000000000000000' });
t.deepEqual(
await jsvm.view('view_js_contract', encodeCall(testContract.accountId, 'toArray', [])),
['hello', 'world', 'hello1']
);

// remove not the last one should work
await ali.call(jsvm, 'call_js_contract', encodeCall(testContract.accountId, 'remove', ['hello']), { attachedDeposit: '100000000000000000000000' });
t.deepEqual(
await jsvm.view('view_js_contract', encodeCall(testContract.accountId, 'toArray', [])),
['hello1', 'world']
);

// remove the last one should work
await ali.call(jsvm, 'call_js_contract', encodeCall(testContract.accountId, 'remove', ['world']), { attachedDeposit: '100000000000000000000000' });
t.deepEqual(
await jsvm.view('view_js_contract', encodeCall(testContract.accountId, 'toArray', [])),
['hello1']
);

// remove when length is 1 should work
t.is(
await jsvm.view('view_js_contract', encodeCall(testContract.accountId, 'len', [])),
1
);
t.is(
await jsvm.view('view_js_contract', encodeCall(testContract.accountId, 'isEmpty', [])),
false
);
await ali.call(jsvm, 'call_js_contract', encodeCall(testContract.accountId, 'remove', ['hello1']), { attachedDeposit: '100000000000000000000000' });
t.deepEqual(
await jsvm.view('view_js_contract', encodeCall(testContract.accountId, 'toArray', [])),
[]
);
t.is(
await jsvm.view('view_js_contract', encodeCall(testContract.accountId, 'isEmpty', [])),
true
);

await ali.call(jsvm, 'call_js_contract', encodeCall(testContract.accountId, 'extend', [['hello', 'world', 'hello1']]), { attachedDeposit: '100000000000000000000000' });
t.deepEqual(
await jsvm.view('view_js_contract', encodeCall(testContract.accountId, 'toArray', [])),
['hello', 'world', 'hello1']
);
t.is(
await jsvm.view('view_js_contract', encodeCall(testContract.accountId, 'isEmpty', [])),
false
);
// clear should work
await ali.call(jsvm, 'call_js_contract', encodeCall(testContract.accountId, 'clear', []), { attachedDeposit: '100000000000000000000000' });
t.deepEqual(
await jsvm.view('view_js_contract', encodeCall(testContract.accountId, 'toArray', [])),
[]
);
t.is(
await jsvm.view('view_js_contract', encodeCall(testContract.accountId, 'isEmpty', [])),
true
);
})
1 change: 1 addition & 0 deletions tests/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ near-sdk build src/unordered-map.js build/unordered-map.base64
near-sdk build src/vector.js build/vector.base64
near-sdk build src/lookup-map.js build/lookup-map.base64
near-sdk build src/lookup-set.js build/lookup-set.base64
near-sdk build src/unordered-set.js build/unordered-set.base64
Loading

0 comments on commit 50f2288

Please sign in to comment.