-
Notifications
You must be signed in to change notification settings - Fork 70
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #71 from near/unordered-set
Add Unordered set
- Loading branch information
Showing
7 changed files
with
368 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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__`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
); | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.