Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add Unordered set #71

Merged
merged 7 commits into from
Jun 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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