diff --git a/examples/package.json b/examples/package.json index 7749e0904..071970010 100644 --- a/examples/package.json +++ b/examples/package.json @@ -38,6 +38,7 @@ "near-sdk-js": "file:../" }, "devDependencies": { + "@types/lodash-es": "^4.17.6", "ava": "^4.2.0", "near-workspaces": "3.2.1", "typescript": "^4.7.4" diff --git a/examples/src/clean-state.js b/examples/src/clean-state.js index 4257450ee..7021a6097 100644 --- a/examples/src/clean-state.js +++ b/examples/src/clean-state.js @@ -1,7 +1,7 @@ import { NearBindgen, call, view, near } from "near-sdk-js"; @NearBindgen({}) -class CleanState { +export class CleanState { @call({}) clean({ keys }) { keys.forEach((key) => near.storageRemove(key)); diff --git a/examples/src/counter.js b/examples/src/counter.js index 92dc70da5..6b9de29f2 100644 --- a/examples/src/counter.js +++ b/examples/src/counter.js @@ -1,8 +1,8 @@ -import { NearBindgen, near, call, view, initialize } from "near-sdk-js"; +import { NearBindgen, near, call, view } from "near-sdk-js"; import { isUndefined } from "lodash-es"; @NearBindgen({}) -class Counter { +export class Counter { constructor() { this.count = 0; } diff --git a/examples/src/counter.ts b/examples/src/counter.ts index 97830aea8..1a656ecd8 100644 --- a/examples/src/counter.ts +++ b/examples/src/counter.ts @@ -1,9 +1,9 @@ -import { NearBindgen, near, call, view, initialize } from "near-sdk-js"; +import { NearBindgen, near, call, view } from "near-sdk-js"; import { isUndefined } from "lodash-es"; import { log } from "./log"; @NearBindgen({}) -class Counter { +export class Counter { count = 0; @call({}) diff --git a/examples/src/cross-contract-call.js b/examples/src/cross-contract-call.js index 4e60aad91..2272e81fd 100644 --- a/examples/src/cross-contract-call.js +++ b/examples/src/cross-contract-call.js @@ -1,7 +1,7 @@ import { NearBindgen, call, view, initialize, near, bytes } from "near-sdk-js"; @NearBindgen({ requireInit: true }) -class OnCall { +export class OnCall { constructor() { this.personOnCall = ""; this.statusMessageContract = ""; diff --git a/examples/src/fungible-token-helper.js b/examples/src/fungible-token-helper.js index 05f420781..e0a6dbf17 100644 --- a/examples/src/fungible-token-helper.js +++ b/examples/src/fungible-token-helper.js @@ -1,7 +1,7 @@ import { NearBindgen, call, view } from "near-sdk-js"; @NearBindgen({}) -class FungibleTokenHelper { +export class FungibleTokenHelper { constructor() { this.data = ""; } diff --git a/examples/src/fungible-token-lockable.js b/examples/src/fungible-token-lockable.js index 9aa1ae399..1ecf32765 100644 --- a/examples/src/fungible-token-lockable.js +++ b/examples/src/fungible-token-lockable.js @@ -52,7 +52,7 @@ class Account { } @NearBindgen({ initRequired: true }) -class LockableFungibleToken { +export class LockableFungibleToken { constructor() { this.accounts = new LookupMap("a"); // Account ID -> Account mapping this.totalSupply = 0; // Total supply of the all tokens diff --git a/examples/src/fungible-token.js b/examples/src/fungible-token.js index 08cb587f1..c4462c4ae 100644 --- a/examples/src/fungible-token.js +++ b/examples/src/fungible-token.js @@ -9,38 +9,38 @@ import { } from "near-sdk-js"; @NearBindgen({ initRequired: true }) -class FungibleToken { +export class FungibleToken { constructor() { this.accounts = new LookupMap("a"); - this.totalSupply = 0; + this.totalSupply = 0n; } @initialize({}) init({ prefix, totalSupply }) { this.accounts = new LookupMap(prefix); - this.totalSupply = totalSupply; + this.totalSupply = BigInt(totalSupply); this.accounts.set(near.signerAccountId(), this.totalSupply); // In a real world Fungible Token contract, storage management is required to denfense drain-storage attack } internalDeposit({ accountId, amount }) { - let balance = this.accounts.get(accountId) || "0"; - let newBalance = BigInt(balance) + BigInt(amount); - this.accounts.set(accountId, newBalance.toString()); - this.totalSupply = (BigInt(this.totalSupply) + BigInt(amount)).toString(); + let balance = this.accounts.get(accountId, { defaultValue: 0n }); + let newBalance = balance + BigInt(amount); + this.accounts.set(accountId, newBalance); + this.totalSupply += BigInt(amount); } internalWithdraw({ accountId, amount }) { - let balance = this.accounts.get(accountId) || "0"; - let newBalance = BigInt(balance) - BigInt(amount); + let balance = this.accounts.get(accountId, { defaultValue: 0n }); + let newBalance = balance - BigInt(amount); assert(newBalance >= 0n, "The account doesn't have enough balance"); - this.accounts.set(accountId, newBalance.toString()); - let newSupply = BigInt(this.totalSupply) - BigInt(amount); + this.accounts.set(accountId, newBalance); + let newSupply = this.totalSupply - BigInt(amount); assert(newSupply >= 0n, "Total supply overflow"); - this.totalSupply = newSupply.toString(); + this.totalSupply = newSupply; } - internalTransfer({ senderId, receiverId, amount, memo }) { + internalTransfer({ senderId, receiverId, amount, memo: _ }) { assert(senderId != receiverId, "Sender and receiver should be different"); let amountInt = BigInt(amount); assert(amountInt > 0n, "The amount should be a positive number"); @@ -82,6 +82,6 @@ class FungibleToken { @view({}) ftBalanceOf({ accountId }) { - return this.accounts.get(accountId) || "0"; + return this.accounts.get(accountId, { defaultValue: 0n }); } } diff --git a/examples/src/log.ts b/examples/src/log.ts index 924c1e827..ec301208d 100644 --- a/examples/src/log.ts +++ b/examples/src/log.ts @@ -1,5 +1,5 @@ import { near } from "near-sdk-js"; -export function log(msg: any) { +export function log(msg: unknown) { near.log(msg); } diff --git a/examples/src/non-fungible-token-receiver.js b/examples/src/non-fungible-token-receiver.js index 7e2a29a2a..b752e87f8 100644 --- a/examples/src/non-fungible-token-receiver.js +++ b/examples/src/non-fungible-token-receiver.js @@ -1,7 +1,7 @@ import { NearBindgen, call, near, assert, initialize } from "near-sdk-js"; @NearBindgen({ requireInit: true }) -class NftContract { +export class NftContract { constructor() { this.nonFungibleTokenAccountId = ""; } diff --git a/examples/src/non-fungible-token.js b/examples/src/non-fungible-token.js index b0c6120da..c81637d10 100644 --- a/examples/src/non-fungible-token.js +++ b/examples/src/non-fungible-token.js @@ -17,7 +17,7 @@ class Token { } @NearBindgen({ requireInit: true }) -class NftContract { +export class NftContract { constructor() { this.owner_id = ""; this.owner_by_id = new LookupMap("a"); @@ -29,7 +29,13 @@ class NftContract { this.owner_by_id = new LookupMap(owner_by_id_prefix); } - internalTransfer({ sender_id, receiver_id, token_id, approval_id, memo }) { + internalTransfer({ + sender_id, + receiver_id, + token_id, + approval_id: _ai, + memo: _m, + }) { let owner_id = this.owner_by_id.get(token_id); assert(owner_id !== null, "Token not found"); @@ -125,7 +131,7 @@ class NftContract { } @call({}) - nftMint({ token_id, token_owner_id, token_metadata }) { + nftMint({ token_id, token_owner_id, token_metadata: _ }) { let sender_id = near.predecessorAccountId(); assert(sender_id === this.owner_id, "Unauthorized"); assert(this.owner_by_id.get(token_id) === null, "Token ID must be unique"); diff --git a/examples/src/parking-lot.ts b/examples/src/parking-lot.ts index c74010fb7..aa632b274 100644 --- a/examples/src/parking-lot.ts +++ b/examples/src/parking-lot.ts @@ -31,8 +31,9 @@ class Engine { } @NearBindgen({}) -class ParkingLot { +export class ParkingLot { cars: LookupMap; + constructor() { this.cars = new LookupMap("a"); } diff --git a/examples/src/status-message-collections.js b/examples/src/status-message-collections.js index 9c196223a..4a28cf6e8 100644 --- a/examples/src/status-message-collections.js +++ b/examples/src/status-message-collections.js @@ -8,7 +8,7 @@ import { } from "near-sdk-js"; @NearBindgen({}) -class StatusMessage { +export class StatusMessage { constructor() { this.records = new UnorderedMap("a"); this.uniqueValues = new LookupSet("b"); diff --git a/examples/src/status-message.js b/examples/src/status-message.js index a9c99d9e9..db77c0f03 100644 --- a/examples/src/status-message.js +++ b/examples/src/status-message.js @@ -1,7 +1,7 @@ import { NearBindgen, call, view, near } from "near-sdk-js"; @NearBindgen({}) -class StatusMessage { +export class StatusMessage { constructor() { this.records = {}; } diff --git a/examples/tsconfig.json b/examples/tsconfig.json index c68670ab7..7bebe8855 100644 --- a/examples/tsconfig.json +++ b/examples/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "experimentalDecorators": true, "target": "es2020", + "moduleResolution": "node", "noEmit": true }, "exclude": ["node_modules"] diff --git a/examples/yarn.lock b/examples/yarn.lock index 39e61403f..4ab21fa06 100644 --- a/examples/yarn.lock +++ b/examples/yarn.lock @@ -433,6 +433,18 @@ dependencies: "@types/node" "*" +"@types/lodash-es@^4.17.6": + version "4.17.6" + resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.6.tgz#c2ed4c8320ffa6f11b43eb89e9eaeec65966a0a0" + integrity sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.14.185" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.185.tgz#c9843f5a40703a8f5edfd53358a58ae729816908" + integrity sha512-evMDG1bC4rgQg4ku9tKpuMh5iBNEwNa3tf9zRHdP1qlv+1WUg44xat4IxCE14gIpZRGUUWAx2VhItCZc25NfMA== + "@types/node@*": version "18.0.0" resolved "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz" diff --git a/jsconfig.json b/jsconfig.json index f85e2a8db..43c5eea40 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,4 +1,4 @@ { "exclude": ["node_modules"], - "include": ["cli/*.js"] + "include": ["cli"] } diff --git a/lib/build-tools/near-bindgen-exporter.d.ts b/lib/build-tools/near-bindgen-exporter.d.ts index ac80da65c..2623a9660 100644 --- a/lib/build-tools/near-bindgen-exporter.d.ts +++ b/lib/build-tools/near-bindgen-exporter.d.ts @@ -1,5 +1,4 @@ export default function _default(): { - visitor: { - ClassDeclaration(path: any): void; - }; + /** @type {import('@babel/traverse').Visitor} */ + visitor: import('@babel/traverse').Visitor; }; diff --git a/lib/build-tools/near-bindgen-exporter.js b/lib/build-tools/near-bindgen-exporter.js index b777dd97c..b2f3aa76b 100644 --- a/lib/build-tools/near-bindgen-exporter.js +++ b/lib/build-tools/near-bindgen-exporter.js @@ -1,3 +1,4 @@ +"use strict"; import * as t from "@babel/types"; const methodTypes = ["call", "view", "initialize"]; function throwError(message) { @@ -53,64 +54,63 @@ function saveToStorage(classId, methodType) { } function executePromise(classId) { return t.ifStatement(t.binaryExpression("!==", t.identifier("_result"), t.identifier("undefined")), t.ifStatement(t.logicalExpression("&&", t.logicalExpression("&&", t.identifier("_result"), t.memberExpression(t.identifier("_result"), t.identifier("constructor"))), t.binaryExpression("===", t.memberExpression(t.memberExpression(t.identifier("_result"), t.identifier("constructor")), t.identifier("name")), t.stringLiteral("NearPromise"))), t.expressionStatement(t.callExpression(t.memberExpression(t.identifier("_result"), t.identifier("onReturn")), [])), t.expressionStatement(t.callExpression(t.memberExpression(t.identifier("env"), t.identifier("value_return")), [ - t.callExpression(t.memberExpression(classId, t.identifier("_serialize")), [t.identifier("_result")]), + t.callExpression(t.memberExpression(classId, t.identifier("_serialize")), [t.identifier("_result"), t.booleanLiteral(true)]), ])))); } +function createDeclaration(classId, methodName, methodType) { + return t.exportNamedDeclaration(t.functionDeclaration(t.identifier(methodName), [], t.blockStatement([ + // Read the state of the contract from storage. + // const _state = Counter._getState(); + readState(classId), + // Throw if initialized on any subsequent init function calls. + // if (_state) { throw new Error('Contract already initialized'); } + preventDoubleInit(methodType), + // Throw if NOT initialized on any non init function calls. + // if (!_state) { throw new Error('Contract must be initialized'); } + ensureInitBeforeCall(classId, methodType), + // Create instance of contract by calling _create function. + // let _contract = Counter._create(); + initializeContractClass(classId), + // Reconstruct the contract with the state if the state is valid. + // if (_state) { Counter._reconstruct(_contract, _state); } + reconstructState(classId, methodType), + // Collect the arguments sent to the function. + // const _args = Counter._getArgs(); + collectArguments(classId), + // Perform the actual function call to the appropriate contract method. + // const _result = _contract.method(args); + callContractMethod(methodName), + // If the method called is either an initialize or call method type, save the changes to storage. + // Counter._saveToStorage(_contract); + saveToStorage(classId, methodType), + // If a NearPromise is returned from the function call the onReturn method to execute the promise. + // if (_result !== undefined) + // if (_result && _result.constructor && _result.constructor.name === 'NearPromise') + // _result.onReturn(); + // else + // near.valueReturn(_contract._serialize(result)); + executePromise(classId), + ]))); +} export default function () { return { + /** @type {import('@babel/traverse').Visitor} */ visitor: { ClassDeclaration(path) { const classNode = path.node; if (classNode.decorators && classNode.decorators[0].expression.callee.name === "NearBindgen") { - const classId = classNode.id; - const contractMethods = {}; - for (let child of classNode.body.body) { + classNode.body.body.forEach((child) => { if (child.type === "ClassMethod" && child.kind === "method" && child.decorators) { const methodType = child.decorators[0].expression.callee.name; if (methodTypes.includes(methodType)) { - contractMethods[child.key.name] = methodType; + path.insertAfter(createDeclaration(classNode.id, child.key.name, methodType)); + console.log(`Babel ${child.key.name} method export done`); } } - } - for (let methodName of Object.keys(contractMethods)) { - path.insertAfter(t.exportNamedDeclaration(t.functionDeclaration(t.identifier(methodName), [], t.blockStatement([ - // Read the state of the contract from storage. - // const _state = Counter._getState(); - readState(classId), - // Throw if initialized on any subsequent init function calls. - // if (_state) { throw new Error('Contract already initialized'); } - preventDoubleInit(contractMethods[methodName]), - // Throw if NOT initialized on any non init function calls. - // if (!_state) { throw new Error('Contract must be initialized'); } - ensureInitBeforeCall(classId, contractMethods[methodName]), - // Create instance of contract by calling _create function. - // let _contract = Counter._create(); - initializeContractClass(classId), - // Reconstruct the contract with the state if the state is valid. - // if (_state) { Counter._reconstruct(_contract, _state); } - reconstructState(classId, contractMethods[methodName]), - // Collect the arguments sent to the function. - // const _args = Counter._getArgs(); - collectArguments(classId), - // Perform the actual function call to the appropriate contract method. - // const _result = _contract.method(args); - callContractMethod(methodName), - // If the method called is either an initialize or call method type, save the changes to storage. - // Counter._saveToStorage(_contract); - saveToStorage(classId, contractMethods[methodName]), - // If a NearPromise is returned from the function call the onReturn method to execute the promise. - // if (_result !== undefined) - // if (_result && _result.constructor && _result.constructor.name === 'NearPromise') - // _result.onReturn(); - // else - // near.valueReturn(_contract._serialize(result)); - executePromise(classId), - ])))); - console.log(`Babel ${methodName} method export done`); - } + }); } }, }, diff --git a/lib/collections/lookup-map.d.ts b/lib/collections/lookup-map.d.ts index 902801fa9..fe2050ab2 100644 --- a/lib/collections/lookup-map.d.ts +++ b/lib/collections/lookup-map.d.ts @@ -8,6 +8,6 @@ export declare class LookupMap { remove(key: Bytes, options?: GetOptions): DataType | null; set(key: Bytes, newValue: DataType, options?: GetOptions): DataType | null; extend(keyValuePairs: [Bytes, DataType][], options?: GetOptions): void; - serialize(): string; + serialize(options?: Pick, "serializer">): string; static reconstruct(data: LookupMap): LookupMap; } diff --git a/lib/collections/lookup-map.js b/lib/collections/lookup-map.js index f5039ba67..ad1d85421 100644 --- a/lib/collections/lookup-map.js +++ b/lib/collections/lookup-map.js @@ -1,5 +1,5 @@ import * as near from "../api"; -import { getValueWithOptions } from "../utils"; +import { getValueWithOptions, serializeValueWithOptions, } from "../utils"; export class LookupMap { constructor(keyPrefix) { this.keyPrefix = keyPrefix; @@ -10,7 +10,7 @@ export class LookupMap { } get(key, options) { const storageKey = this.keyPrefix + key; - const value = JSON.parse(near.storageRead(storageKey)); + const value = near.storageRead(storageKey); return getValueWithOptions(value, options); } remove(key, options) { @@ -18,16 +18,16 @@ export class LookupMap { if (!near.storageRemove(storageKey)) { return options?.defaultValue ?? null; } - const value = JSON.parse(near.storageGetEvicted()); + const value = near.storageGetEvicted(); return getValueWithOptions(value, options); } set(key, newValue, options) { const storageKey = this.keyPrefix + key; - const storageValue = JSON.stringify(newValue); + const storageValue = serializeValueWithOptions(newValue, options); if (!near.storageWrite(storageKey, storageValue)) { return options?.defaultValue ?? null; } - const value = JSON.parse(near.storageGetEvicted()); + const value = near.storageGetEvicted(); return getValueWithOptions(value, options); } extend(keyValuePairs, options) { @@ -35,8 +35,8 @@ export class LookupMap { this.set(key, value, options); } } - serialize() { - return JSON.stringify(this); + serialize(options) { + return serializeValueWithOptions(this, options); } // converting plain object to class object static reconstruct(data) { diff --git a/lib/collections/lookup-set.d.ts b/lib/collections/lookup-set.d.ts index 6e3b58898..9a147e004 100644 --- a/lib/collections/lookup-set.d.ts +++ b/lib/collections/lookup-set.d.ts @@ -1,11 +1,12 @@ +import { GetOptions } from "../types/collections"; import { Bytes } from "../utils"; export declare class LookupSet { readonly keyPrefix: Bytes; constructor(keyPrefix: Bytes); - contains(key: DataType): boolean; - remove(key: DataType): boolean; - set(key: DataType): boolean; - extend(keys: DataType[]): void; - serialize(): string; + contains(key: DataType, options?: Pick, "serializer">): boolean; + remove(key: DataType, options?: Pick, "serializer">): boolean; + set(key: DataType, options?: Pick, "serializer">): boolean; + extend(keys: DataType[], options?: Pick, "serializer">): void; + serialize(options?: Pick, "serializer">): string; static reconstruct(data: LookupSet): LookupSet; } diff --git a/lib/collections/lookup-set.js b/lib/collections/lookup-set.js index 4c6b329c0..efd2cbab5 100644 --- a/lib/collections/lookup-set.js +++ b/lib/collections/lookup-set.js @@ -1,28 +1,29 @@ import * as near from "../api"; +import { serializeValueWithOptions } from "../utils"; export class LookupSet { constructor(keyPrefix) { this.keyPrefix = keyPrefix; } - contains(key) { - const storageKey = this.keyPrefix + JSON.stringify(key); + contains(key, options) { + const storageKey = this.keyPrefix + serializeValueWithOptions(key, options); return near.storageHasKey(storageKey); } // Returns true if the element was present in the set. - remove(key) { - const storageKey = this.keyPrefix + JSON.stringify(key); + remove(key, options) { + const storageKey = this.keyPrefix + serializeValueWithOptions(key, options); return near.storageRemove(storageKey); } // If the set did not have this value present, `true` is returned. // If the set did have this value present, `false` is returned. - set(key) { - const storageKey = this.keyPrefix + JSON.stringify(key); + set(key, options) { + const storageKey = this.keyPrefix + serializeValueWithOptions(key, options); return !near.storageWrite(storageKey, ""); } - extend(keys) { - keys.forEach((key) => this.set(key)); + extend(keys, options) { + keys.forEach((key) => this.set(key, options)); } - serialize() { - return JSON.stringify(this); + serialize(options) { + return serializeValueWithOptions(this, options); } // converting plain object to class object static reconstruct(data) { diff --git a/lib/collections/unordered-map.d.ts b/lib/collections/unordered-map.d.ts index 0ec7074c5..3b5b497a8 100644 --- a/lib/collections/unordered-map.d.ts +++ b/lib/collections/unordered-map.d.ts @@ -2,11 +2,11 @@ import { Bytes } from "../utils"; import { Vector } from "./vector"; import { LookupMap } from "./lookup-map"; import { GetOptions } from "../types/collections"; -declare type ValueAndIndex = [value: DataType, index: number]; +declare type ValueAndIndex = [value: string, index: number]; export declare class UnorderedMap { readonly prefix: Bytes; readonly keys: Vector; - readonly values: LookupMap>; + readonly values: LookupMap; constructor(prefix: Bytes); get length(): number; isEmpty(): boolean; @@ -18,7 +18,7 @@ export declare class UnorderedMap { private createIteratorWithOptions; toArray(options?: GetOptions): [Bytes, DataType][]; extend(keyValuePairs: [Bytes, DataType][]): void; - serialize(): string; + serialize(options?: Pick, "serializer">): string; static reconstruct(data: UnorderedMap): UnorderedMap; } declare class UnorderedMapIterator { @@ -27,7 +27,7 @@ declare class UnorderedMapIterator { private map; constructor(unorderedMap: UnorderedMap, options?: GetOptions); next(): { - value: [unknown | null, unknown | null]; + value: [Bytes | null, DataType | null]; done: boolean; }; } diff --git a/lib/collections/unordered-map.js b/lib/collections/unordered-map.js index 1c0b63b6c..b63f5a2b8 100644 --- a/lib/collections/unordered-map.js +++ b/lib/collections/unordered-map.js @@ -1,7 +1,6 @@ -import { assert, getValueWithOptions } from "../utils"; +import { assert, ERR_INCONSISTENT_STATE, getValueWithOptions, serializeValueWithOptions, } from "../utils"; import { Vector, VectorIterator } from "./vector"; import { LookupMap } from "./lookup-map"; -const ERR_INCONSISTENT_STATE = "The collection is an inconsistent state. Did previous smart contract execution terminate unexpectedly?"; export class UnorderedMap { constructor(prefix) { this.prefix = prefix; @@ -24,15 +23,15 @@ export class UnorderedMap { } set(key, value, options) { const valueAndIndex = this.values.get(key); + const serialized = serializeValueWithOptions(value, options); if (valueAndIndex === null) { - const nextIndex = this.length; + const newElementIndex = this.length; this.keys.push(key); - this.values.set(key, [value, nextIndex]); + this.values.set(key, [serialized, newElementIndex]); return null; } - const [oldValue] = valueAndIndex; - valueAndIndex[0] = value; - this.values.set(key, valueAndIndex); + const [oldValue, oldIndex] = valueAndIndex; + this.values.set(key, [serialized, oldIndex]); return getValueWithOptions(oldValue, options); } remove(key, options) { @@ -48,10 +47,7 @@ export class UnorderedMap { const swappedKey = this.keys.get(index); const swappedValueAndIndex = this.values.get(swappedKey); assert(swappedValueAndIndex !== null, ERR_INCONSISTENT_STATE); - this.values.set(swappedKey, [ - getValueWithOptions(swappedValueAndIndex[0], options), - index, - ]); + this.values.set(swappedKey, [swappedValueAndIndex[0], index]); } return getValueWithOptions(value, options); } @@ -83,8 +79,8 @@ export class UnorderedMap { this.set(key, value); } } - serialize() { - return JSON.stringify(this); + serialize(options) { + return serializeValueWithOptions(this, options); } // converting plain object to class object static reconstruct(data) { @@ -108,11 +104,11 @@ class UnorderedMapIterator { if (key.done) { return { value: [key.value, null], done: key.done }; } - const [value] = this.map.get(key.value); - assert(value !== null, ERR_INCONSISTENT_STATE); + const valueAndIndex = this.map.get(key.value); + assert(valueAndIndex !== null, ERR_INCONSISTENT_STATE); return { done: key.done, - value: [key.value, getValueWithOptions(value, this.options)], + value: [key.value, getValueWithOptions(valueAndIndex[0], this.options)], }; } } diff --git a/lib/collections/unordered-set.d.ts b/lib/collections/unordered-set.d.ts index c98dbb7a4..f7a1971f8 100644 --- a/lib/collections/unordered-set.d.ts +++ b/lib/collections/unordered-set.d.ts @@ -8,14 +8,14 @@ export declare class UnorderedSet { constructor(prefix: Bytes); get length(): number; isEmpty(): boolean; - contains(element: DataType): boolean; - set(element: DataType): boolean; - remove(element: DataType): boolean; - clear(): void; + contains(element: DataType, options?: Pick, "serializer">): boolean; + set(element: DataType, options?: Pick, "serializer">): boolean; + remove(element: DataType, options?: GetOptions): boolean; + clear(options?: Pick, "serializer">): void; [Symbol.iterator](): VectorIterator; private createIteratorWithOptions; toArray(options?: GetOptions): DataType[]; extend(elements: DataType[]): void; - serialize(): string; + serialize(options?: Pick, "serializer">): string; static reconstruct(data: UnorderedSet): UnorderedSet; } diff --git a/lib/collections/unordered-set.js b/lib/collections/unordered-set.js index 1000e911b..bc0d3f2b6 100644 --- a/lib/collections/unordered-set.js +++ b/lib/collections/unordered-set.js @@ -1,7 +1,6 @@ import * as near from "../api"; -import { u8ArrayToBytes, bytesToU8Array, assert } from "../utils"; +import { u8ArrayToBytes, bytesToU8Array, assert, serializeValueWithOptions, ERR_INCONSISTENT_STATE, } from "../utils"; import { Vector, VectorIterator } from "./vector"; -const ERR_INCONSISTENT_STATE = "The collection is an inconsistent state. Did previous smart contract execution terminate unexpectedly?"; function serializeIndex(index) { const data = new Uint32Array([index]); const array = new Uint8Array(data.buffer); @@ -24,23 +23,23 @@ export class UnorderedSet { isEmpty() { return this.elements.isEmpty(); } - contains(element) { - const indexLookup = this.elementIndexPrefix + JSON.stringify(element); + contains(element, options) { + const indexLookup = this.elementIndexPrefix + serializeValueWithOptions(element, options); return near.storageHasKey(indexLookup); } - set(element) { - const indexLookup = this.elementIndexPrefix + JSON.stringify(element); - if (!near.storageRead(indexLookup)) { - const nextIndex = this.length; - const nextIndexRaw = serializeIndex(nextIndex); - near.storageWrite(indexLookup, nextIndexRaw); - this.elements.push(element); - return true; + set(element, options) { + const indexLookup = this.elementIndexPrefix + serializeValueWithOptions(element, options); + if (near.storageRead(indexLookup)) { + return false; } - return false; + const nextIndex = this.length; + const nextIndexRaw = serializeIndex(nextIndex); + near.storageWrite(indexLookup, nextIndexRaw); + this.elements.push(element, options); + return true; } - remove(element) { - const indexLookup = this.elementIndexPrefix + JSON.stringify(element); + remove(element, options) { + const indexLookup = this.elementIndexPrefix + serializeValueWithOptions(element, options); const indexRaw = near.storageRead(indexLookup); if (!indexRaw) { return false; @@ -55,22 +54,23 @@ export class UnorderedSet { } // If there is more than one element then swap remove swaps it with the last // element. - const lastElement = this.elements.get(this.length - 1); + const lastElement = this.elements.get(this.length - 1, options); assert(!!lastElement, ERR_INCONSISTENT_STATE); near.storageRemove(indexLookup); // If the removed element was the last element from keys, then we don't need to // reinsert the lookup back. if (lastElement !== element) { - const lastLookupElement = this.elementIndexPrefix + JSON.stringify(lastElement); + const lastLookupElement = this.elementIndexPrefix + + serializeValueWithOptions(lastElement, options); near.storageWrite(lastLookupElement, indexRaw); } const index = deserializeIndex(indexRaw); this.elements.swapRemove(index); return true; } - clear() { + clear(options) { for (const element of this.elements) { - const indexLookup = this.elementIndexPrefix + JSON.stringify(element); + const indexLookup = this.elementIndexPrefix + serializeValueWithOptions(element, options); near.storageRemove(indexLookup); } this.elements.clear(); @@ -96,8 +96,8 @@ export class UnorderedSet { this.set(element); } } - serialize() { - return JSON.stringify(this); + serialize(options) { + return serializeValueWithOptions(this, options); } // converting plain object to class object static reconstruct(data) { diff --git a/lib/collections/vector.d.ts b/lib/collections/vector.d.ts index af0101af2..ceb54ff4a 100644 --- a/lib/collections/vector.d.ts +++ b/lib/collections/vector.d.ts @@ -3,19 +3,19 @@ import { GetOptions } from "../types/collections"; export declare class Vector { readonly prefix: Bytes; length: number; - constructor(prefix: Bytes); + constructor(prefix: Bytes, length?: number); isEmpty(): boolean; - get(index: number, options?: GetOptions): DataType | null; + get(index: number, options?: Omit, "serializer">): DataType | null; swapRemove(index: number, options?: GetOptions): DataType | null; - push(element: DataType): void; - pop(options?: GetOptions): DataType | null; + push(element: DataType, options?: Pick, "serializer">): void; + pop(options?: Omit, "serializer">): DataType | null; replace(index: number, element: DataType, options?: GetOptions): DataType; extend(elements: DataType[]): void; [Symbol.iterator](): VectorIterator; private createIteratorWithOptions; toArray(options?: GetOptions): DataType[]; clear(): void; - serialize(): string; + serialize(options?: Pick, "serializer">): string; static reconstruct(data: Vector): Vector; } export declare class VectorIterator { diff --git a/lib/collections/vector.js b/lib/collections/vector.js index 3eaf2320c..f6516cb96 100644 --- a/lib/collections/vector.js +++ b/lib/collections/vector.js @@ -1,7 +1,5 @@ import * as near from "../api"; -import { assert, getValueWithOptions, u8ArrayToBytes } from "../utils"; -const ERR_INDEX_OUT_OF_BOUNDS = "Index out of bounds"; -const ERR_INCONSISTENT_STATE = "The collection is an inconsistent state. Did previous smart contract execution terminate unexpectedly?"; +import { assert, getValueWithOptions, u8ArrayToBytes, serializeValueWithOptions, ERR_INCONSISTENT_STATE, ERR_INDEX_OUT_OF_BOUNDS, } from "../utils"; function indexToKey(prefix, index) { const data = new Uint32Array([index]); const array = new Uint8Array(data.buffer); @@ -11,19 +9,19 @@ function indexToKey(prefix, index) { /// An iterable implementation of vector that stores its content on the trie. /// Uses the following map: index -> element export class Vector { - constructor(prefix) { + constructor(prefix, length = 0) { this.prefix = prefix; - this.length = 0; + this.length = length; } isEmpty() { return this.length === 0; } get(index, options) { if (index >= this.length) { - return null; + return options?.defaultValue ?? null; } const storageKey = indexToKey(this.prefix, index); - const value = JSON.parse(near.storageRead(storageKey)); + const value = near.storageRead(storageKey); return getValueWithOptions(value, options); } /// Removes an element from the vector and returns it in serialized form. @@ -35,15 +33,15 @@ export class Vector { return this.pop(options); } const key = indexToKey(this.prefix, index); - const last = this.pop(); - assert(near.storageWrite(key, JSON.stringify(last)), ERR_INCONSISTENT_STATE); - const value = JSON.parse(near.storageGetEvicted()); + const last = this.pop(options); + assert(near.storageWrite(key, serializeValueWithOptions(last, options)), ERR_INCONSISTENT_STATE); + const value = near.storageGetEvicted(); return getValueWithOptions(value, options); } - push(element) { + push(element, options) { const key = indexToKey(this.prefix, this.length); this.length += 1; - near.storageWrite(key, JSON.stringify(element)); + near.storageWrite(key, serializeValueWithOptions(element, options)); } pop(options) { if (this.isEmpty()) { @@ -53,14 +51,14 @@ export class Vector { const lastKey = indexToKey(this.prefix, lastIndex); this.length -= 1; assert(near.storageRemove(lastKey), ERR_INCONSISTENT_STATE); - const value = JSON.parse(near.storageGetEvicted()); + const value = near.storageGetEvicted(); return getValueWithOptions(value, options); } replace(index, element, options) { assert(index < this.length, ERR_INDEX_OUT_OF_BOUNDS); const key = indexToKey(this.prefix, index); - assert(near.storageWrite(key, JSON.stringify(element)), ERR_INCONSISTENT_STATE); - const value = JSON.parse(near.storageGetEvicted()); + assert(near.storageWrite(key, serializeValueWithOptions(element, options)), ERR_INCONSISTENT_STATE); + const value = near.storageGetEvicted(); return getValueWithOptions(value, options); } extend(elements) { @@ -79,8 +77,8 @@ export class Vector { toArray(options) { const array = []; const iterator = options ? this.createIteratorWithOptions(options) : this; - for (const v of iterator) { - array.push(v); + for (const value of iterator) { + array.push(value); } return array; } @@ -91,13 +89,12 @@ export class Vector { } this.length = 0; } - serialize() { - return JSON.stringify(this); + serialize(options) { + return serializeValueWithOptions(this, options); } // converting plain object to class object static reconstruct(data) { - const vector = new Vector(data.prefix); - vector.length = data.length; + const vector = new Vector(data.prefix, data.length); return vector; } } diff --git a/lib/index.d.ts b/lib/index.d.ts index 1ac8e8846..bbe7a805c 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -1,6 +1,6 @@ import { call, view, initialize, NearBindgen } from "./near-bindgen"; import * as near from "./api"; import { LookupMap, Vector, LookupSet, UnorderedMap, UnorderedSet } from "./collections"; -import { bytes, Bytes, assert } from "./utils"; +import { bytes, Bytes, assert, validateAccountId } from "./utils"; import { NearPromise, PromiseOrValue } from "./promise"; -export { call, view, initialize, NearBindgen, near, LookupMap, Vector, LookupSet, UnorderedMap, UnorderedSet, bytes, Bytes, assert, NearPromise, PromiseOrValue, }; +export { call, view, initialize, NearBindgen, near, LookupMap, Vector, LookupSet, UnorderedMap, UnorderedSet, bytes, Bytes, assert, validateAccountId, NearPromise, PromiseOrValue, }; diff --git a/lib/index.js b/lib/index.js index bd400b381..0d48ceeae 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,6 +1,6 @@ import { call, view, initialize, NearBindgen } from "./near-bindgen"; import * as near from "./api"; import { LookupMap, Vector, LookupSet, UnorderedMap, UnorderedSet, } from "./collections"; -import { bytes, assert } from "./utils"; +import { bytes, assert, validateAccountId } from "./utils"; import { NearPromise } from "./promise"; -export { call, view, initialize, NearBindgen, near, LookupMap, Vector, LookupSet, UnorderedMap, UnorderedSet, bytes, assert, NearPromise, }; +export { call, view, initialize, NearBindgen, near, LookupMap, Vector, LookupSet, UnorderedMap, UnorderedSet, bytes, assert, validateAccountId, NearPromise, }; diff --git a/lib/near-bindgen.d.ts b/lib/near-bindgen.d.ts index 8c8dc6d12..ac0f51985 100644 --- a/lib/near-bindgen.d.ts +++ b/lib/near-bindgen.d.ts @@ -1,23 +1,27 @@ declare type EmptyParameterObject = Record; -export declare function initialize(_empty: EmptyParameterObject): (_target: any, _key: string | symbol, _descriptor: TypedPropertyDescriptor) => void; +declare type AnyObject = Record; +declare type DecoratorFunction = any>(target: object, key: string | symbol, descriptor: TypedPropertyDescriptor) => void; +export declare function initialize(_empty: EmptyParameterObject): DecoratorFunction; +export declare function view(_empty: EmptyParameterObject): DecoratorFunction; export declare function call({ privateFunction, payableFunction, }: { privateFunction?: boolean; payableFunction?: boolean; -}): (_target: any, _key: string | symbol, descriptor: TypedPropertyDescriptor) => void; -export declare function view(_empty: EmptyParameterObject): (_target: any, _key: string | symbol, _descriptor: TypedPropertyDescriptor) => void; -export declare function NearBindgen({ requireInit, }: { +}): DecoratorFunction; +export declare function NearBindgen({ requireInit, serializer, deserializer, }: { requireInit?: boolean; + serializer?(value: unknown): string; + deserializer?(value: string): unknown; }): any>(target: T) => { new (...args: any[]): { [x: string]: any; }; _create(): any; - _getState(): any; - _saveToStorage(obj: Object): void; - _getArgs(): JSON; - _serialize(value: Object): string; - _deserialize(value: string): Object; - _reconstruct(classObject: any, plainObject: JSON): any; + _getState(): unknown | null; + _saveToStorage(objectToSave: unknown): void; + _getArgs(): unknown; + _serialize(value: unknown, forReturn?: boolean): string; + _deserialize(value: string): unknown; + _reconstruct(classObject: object, plainObject: AnyObject): object; _requireInit(): boolean; } & T; declare module "./" { diff --git a/lib/near-bindgen.js b/lib/near-bindgen.js index 81b56071b..7edeba649 100644 --- a/lib/near-bindgen.js +++ b/lib/near-bindgen.js @@ -1,38 +1,37 @@ import * as near from "./api"; -// type AnyObject = Record; -// type DecoratorFunction = ( -// target: AnyObject, -// key: string | symbol, -// descriptor: TypedPropertyDescriptor -// ) => void; +import { deserialize, serialize } from "./utils"; export function initialize(_empty) { - /* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/ban-types */ - return function (_target, _key, _descriptor) { }; - /* eslint-enable @typescript-eslint/no-empty-function, @typescript-eslint/ban-types */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return function (_target, _key, _descriptor + // eslint-disable-next-line @typescript-eslint/no-empty-function + ) { }; +} +export function view(_empty) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return function (_target, _key, _descriptor + // eslint-disable-next-line @typescript-eslint/no-empty-function + ) { }; } export function call({ privateFunction = false, payableFunction = false, }) { - /* eslint-disable @typescript-eslint/ban-types */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any return function (_target, _key, descriptor) { - /* eslint-enable @typescript-eslint/ban-types */ const originalMethod = descriptor.value; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore descriptor.value = function (...args) { if (privateFunction && near.predecessorAccountId() !== near.currentAccountId()) { - throw Error("Function is private"); + throw new Error("Function is private"); } - if (!payableFunction && near.attachedDeposit() > BigInt(0)) { - throw Error("Function is not payable"); + if (!payableFunction && near.attachedDeposit() > 0n) { + throw new Error("Function is not payable"); } return originalMethod.apply(this, args); }; }; } -export function view(_empty) { - /* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/ban-types */ - return function (_target, _key, _descriptor) { }; - /* eslint-enable @typescript-eslint/no-empty-function, @typescript-eslint/ban-types */ -} -export function NearBindgen({ requireInit = false, }) { +export function NearBindgen({ requireInit = false, serializer = serialize, deserializer = deserialize, }) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any return (target) => { return class extends target { static _create() { @@ -42,29 +41,27 @@ export function NearBindgen({ requireInit = false, }) { const rawState = near.storageRead("STATE"); return rawState ? this._deserialize(rawState) : null; } - /* eslint-disable-next-line @typescript-eslint/ban-types */ - static _saveToStorage(obj) { - near.storageWrite("STATE", this._serialize(obj)); + static _saveToStorage(objectToSave) { + near.storageWrite("STATE", this._serialize(objectToSave)); } static _getArgs() { return JSON.parse(near.input() || "{}"); } - /* eslint-disable-next-line @typescript-eslint/ban-types */ - static _serialize(value) { - return JSON.stringify(value); + static _serialize(value, forReturn = false) { + if (forReturn) { + return JSON.stringify(value, (_, value) => typeof value === "bigint" ? `${value}` : value); + } + return serializer(value); } - /* eslint-disable-next-line @typescript-eslint/ban-types */ static _deserialize(value) { - return JSON.parse(value); + return deserializer(value); } static _reconstruct(classObject, plainObject) { for (const item in classObject) { - if (classObject[item].constructor?.reconstruct !== undefined) { - classObject[item] = classObject[item].constructor.reconstruct(plainObject[item]); - } - else { - classObject[item] = plainObject[item]; - } + const reconstructor = classObject[item].constructor?.reconstruct; + classObject[item] = reconstructor + ? reconstructor(plainObject[item]) + : plainObject[item]; } return classObject; } diff --git a/lib/types/collections.d.ts b/lib/types/collections.d.ts index fafc4b1ae..f71af7696 100644 --- a/lib/types/collections.d.ts +++ b/lib/types/collections.d.ts @@ -1,4 +1,6 @@ export declare type GetOptions = { - reconstructor?: (value: unknown) => DataType; + reconstructor?(value: unknown): DataType; defaultValue?: DataType; + serializer?(valueToSerialize: unknown): string; + deserializer?(valueToDeserialize: string): unknown; }; diff --git a/lib/utils.d.ts b/lib/utils.d.ts index e43cc6b51..9829c4546 100644 --- a/lib/utils.d.ts +++ b/lib/utils.d.ts @@ -3,6 +3,8 @@ export declare type Bytes = string; export declare type PromiseIndex = number | bigint; export declare type NearAmount = number | bigint; export declare type Register = number | bigint; +export declare const ERR_INCONSISTENT_STATE = "The collection is an inconsistent state. Did previous smart contract execution terminate unexpectedly?"; +export declare const ERR_INDEX_OUT_OF_BOUNDS = "Index out of bounds"; export declare function u8ArrayToBytes(array: Uint8Array): Bytes; export declare function bytesToU8Array(bytes: Bytes): Uint8Array; export declare function bytes(stringOrU8Array: string | Uint8Array): Bytes; @@ -10,4 +12,15 @@ export declare function assert(expression: boolean, message: string): void; export declare type Mutable = { -readonly [P in keyof T]: T[P]; }; -export declare function getValueWithOptions(value: unknown, options?: GetOptions): DataType | null; +export declare function getValueWithOptions(value: string, options?: Omit, "serializer">): DataType | null; +export declare function serializeValueWithOptions(value: DataType, { serializer }?: Pick, "serializer">): string; +export declare function serialize(valueToSerialize: unknown): string; +export declare function deserialize(valueToDeserialize: string): unknown; +/** + * Validates the Account ID according to the NEAR protocol + * [Account ID rules](https://nomicon.io/DataStructures/Account#account-id-rules). + * + * @param accountId - The Account ID string you want to validate. + * @returns boolean + */ +export declare function validateAccountId(accountId: string): boolean; diff --git a/lib/utils.js b/lib/utils.js index c3b87e9cb..c3c72c4e0 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,3 +1,12 @@ +const TYPE_KEY = "typeInfo"; +var TypeBrand; +(function (TypeBrand) { + TypeBrand["BIGINT"] = "bigint"; + TypeBrand["DATE"] = "date"; +})(TypeBrand || (TypeBrand = {})); +export const ERR_INCONSISTENT_STATE = "The collection is an inconsistent state. Did previous smart contract execution terminate unexpectedly?"; +export const ERR_INDEX_OUT_OF_BOUNDS = "Index out of bounds"; +const ACCOUNT_ID_REGEX = /^(([a-z\d]+[-_])*[a-z\d]+\.)*([a-z\d]+[-_])*[a-z\d]+$/; export function u8ArrayToBytes(array) { return array.reduce((result, value) => `${result}${String.fromCharCode(value)}`, ""); } @@ -26,12 +35,67 @@ export function assert(expression, message) { throw Error("assertion failed: " + message); } } -export function getValueWithOptions(value, options) { - if (value === undefined || value === null) { +export function getValueWithOptions(value, options = { + deserializer: deserialize, +}) { + const deserialized = deserialize(value); + if (deserialized === undefined || deserialized === null) { return options?.defaultValue ?? null; } if (options?.reconstructor) { - return options.reconstructor(value); + return options.reconstructor(deserialized); } - return value; + return deserialized; +} +export function serializeValueWithOptions(value, { serializer } = { + serializer: serialize, +}) { + return serializer(value); +} +export function serialize(valueToSerialize) { + return JSON.stringify(valueToSerialize, function (key, value) { + if (typeof value === "bigint") { + return { + value: value.toString(), + [TYPE_KEY]: TypeBrand.BIGINT, + }; + } + if (typeof this[key] === "object" && + this[key] !== null && + this[key] instanceof Date) { + return { + value: this[key].toISOString(), + [TYPE_KEY]: TypeBrand.DATE, + }; + } + return value; + }); +} +export function deserialize(valueToDeserialize) { + return JSON.parse(valueToDeserialize, (_, value) => { + if (value !== null && + typeof value === "object" && + Object.keys(value).length === 2 && + Object.keys(value).every((key) => ["value", TYPE_KEY].includes(key))) { + switch (value[TYPE_KEY]) { + case TypeBrand.BIGINT: + return BigInt(value["value"]); + case TypeBrand.DATE: + return new Date(value["value"]); + } + } + return value; + }); +} +/** + * Validates the Account ID according to the NEAR protocol + * [Account ID rules](https://nomicon.io/DataStructures/Account#account-id-rules). + * + * @param accountId - The Account ID string you want to validate. + * @returns boolean + */ +export function validateAccountId(accountId) { + return (accountId.length >= 2 && + accountId.length <= 64 && + ACCOUNT_ID_REGEX.test(accountId)); } diff --git a/package.json b/package.json index 22c320917..f80c763ea 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ ], "devDependencies": { "@rollup/plugin-typescript": "^8.3.2", + "@types/babel__traverse": "^7.18.1", "@types/node": "^17.0.38", "@types/rollup": "^0.54.0", "@typescript-eslint/eslint-plugin": "^5.37.0", diff --git a/src/build-tools/near-bindgen-exporter.js b/src/build-tools/near-bindgen-exporter.js index 7770831ab..f2a730ce9 100644 --- a/src/build-tools/near-bindgen-exporter.js +++ b/src/build-tools/near-bindgen-exporter.js @@ -1,3 +1,4 @@ +"use strict"; import * as t from "@babel/types"; const methodTypes = ["call", "view", "initialize"]; @@ -156,7 +157,7 @@ function executePromise(classId) { [ t.callExpression( t.memberExpression(classId, t.identifier("_serialize")), - [t.identifier("_result")] + [t.identifier("_result"), t.booleanLiteral(true)] ), ] ) @@ -165,8 +166,51 @@ function executePromise(classId) { ); } +function createDeclaration(classId, methodName, methodType) { + return t.exportNamedDeclaration( + t.functionDeclaration( + t.identifier(methodName), + [], + t.blockStatement([ + // Read the state of the contract from storage. + // const _state = Counter._getState(); + readState(classId), + // Throw if initialized on any subsequent init function calls. + // if (_state) { throw new Error('Contract already initialized'); } + preventDoubleInit(methodType), + // Throw if NOT initialized on any non init function calls. + // if (!_state) { throw new Error('Contract must be initialized'); } + ensureInitBeforeCall(classId, methodType), + // Create instance of contract by calling _create function. + // let _contract = Counter._create(); + initializeContractClass(classId), + // Reconstruct the contract with the state if the state is valid. + // if (_state) { Counter._reconstruct(_contract, _state); } + reconstructState(classId, methodType), + // Collect the arguments sent to the function. + // const _args = Counter._getArgs(); + collectArguments(classId), + // Perform the actual function call to the appropriate contract method. + // const _result = _contract.method(args); + callContractMethod(methodName), + // If the method called is either an initialize or call method type, save the changes to storage. + // Counter._saveToStorage(_contract); + saveToStorage(classId, methodType), + // If a NearPromise is returned from the function call the onReturn method to execute the promise. + // if (_result !== undefined) + // if (_result && _result.constructor && _result.constructor.name === 'NearPromise') + // _result.onReturn(); + // else + // near.valueReturn(_contract._serialize(result)); + executePromise(classId), + ]) + ) + ); +} + export default function () { return { + /** @type {import('@babel/traverse').Visitor} */ visitor: { ClassDeclaration(path) { const classNode = path.node; @@ -175,10 +219,7 @@ export default function () { classNode.decorators && classNode.decorators[0].expression.callee.name === "NearBindgen" ) { - const classId = classNode.id; - const contractMethods = {}; - - for (let child of classNode.body.body) { + classNode.body.body.forEach((child) => { if ( child.type === "ClassMethod" && child.kind === "method" && @@ -187,55 +228,14 @@ export default function () { const methodType = child.decorators[0].expression.callee.name; if (methodTypes.includes(methodType)) { - contractMethods[child.key.name] = methodType; + path.insertAfter( + createDeclaration(classNode.id, child.key.name, methodType) + ); + + console.log(`Babel ${child.key.name} method export done`); } } - } - - for (let methodName of Object.keys(contractMethods)) { - path.insertAfter( - t.exportNamedDeclaration( - t.functionDeclaration( - t.identifier(methodName), - [], - t.blockStatement([ - // Read the state of the contract from storage. - // const _state = Counter._getState(); - readState(classId), - // Throw if initialized on any subsequent init function calls. - // if (_state) { throw new Error('Contract already initialized'); } - preventDoubleInit(contractMethods[methodName]), - // Throw if NOT initialized on any non init function calls. - // if (!_state) { throw new Error('Contract must be initialized'); } - ensureInitBeforeCall(classId, contractMethods[methodName]), - // Create instance of contract by calling _create function. - // let _contract = Counter._create(); - initializeContractClass(classId), - // Reconstruct the contract with the state if the state is valid. - // if (_state) { Counter._reconstruct(_contract, _state); } - reconstructState(classId, contractMethods[methodName]), - // Collect the arguments sent to the function. - // const _args = Counter._getArgs(); - collectArguments(classId), - // Perform the actual function call to the appropriate contract method. - // const _result = _contract.method(args); - callContractMethod(methodName), - // If the method called is either an initialize or call method type, save the changes to storage. - // Counter._saveToStorage(_contract); - saveToStorage(classId, contractMethods[methodName]), - // If a NearPromise is returned from the function call the onReturn method to execute the promise. - // if (_result !== undefined) - // if (_result && _result.constructor && _result.constructor.name === 'NearPromise') - // _result.onReturn(); - // else - // near.valueReturn(_contract._serialize(result)); - executePromise(classId), - ]) - ) - ) - ); - console.log(`Babel ${methodName} method export done`); - } + }); } }, }, diff --git a/src/collections/lookup-map.ts b/src/collections/lookup-map.ts index b6eb8aef8..bca619d21 100644 --- a/src/collections/lookup-map.ts +++ b/src/collections/lookup-map.ts @@ -1,6 +1,10 @@ import * as near from "../api"; import { GetOptions } from "../types/collections"; -import { Bytes, getValueWithOptions } from "../utils"; +import { + Bytes, + getValueWithOptions, + serializeValueWithOptions, +} from "../utils"; export class LookupMap { constructor(readonly keyPrefix: Bytes) {} @@ -12,7 +16,7 @@ export class LookupMap { get(key: Bytes, options?: GetOptions): DataType | null { const storageKey = this.keyPrefix + key; - const value = JSON.parse(near.storageRead(storageKey)); + const value = near.storageRead(storageKey); return getValueWithOptions(value, options); } @@ -24,7 +28,7 @@ export class LookupMap { return options?.defaultValue ?? null; } - const value = JSON.parse(near.storageGetEvicted()); + const value = near.storageGetEvicted(); return getValueWithOptions(value, options); } @@ -35,13 +39,13 @@ export class LookupMap { options?: GetOptions ): DataType | null { const storageKey = this.keyPrefix + key; - const storageValue = JSON.stringify(newValue); + const storageValue = serializeValueWithOptions(newValue, options); if (!near.storageWrite(storageKey, storageValue)) { return options?.defaultValue ?? null; } - const value = JSON.parse(near.storageGetEvicted()); + const value = near.storageGetEvicted(); return getValueWithOptions(value, options); } @@ -55,8 +59,8 @@ export class LookupMap { } } - serialize(): string { - return JSON.stringify(this); + serialize(options?: Pick, "serializer">): string { + return serializeValueWithOptions(this, options); } // converting plain object to class object diff --git a/src/collections/lookup-set.ts b/src/collections/lookup-set.ts index 299f4b2ed..a53e4d89d 100644 --- a/src/collections/lookup-set.ts +++ b/src/collections/lookup-set.ts @@ -1,33 +1,46 @@ import * as near from "../api"; -import { Bytes } from "../utils"; +import { GetOptions } from "../types/collections"; +import { Bytes, serializeValueWithOptions } from "../utils"; export class LookupSet { constructor(readonly keyPrefix: Bytes) {} - contains(key: DataType): boolean { - const storageKey = this.keyPrefix + JSON.stringify(key); + contains( + key: DataType, + options?: Pick, "serializer"> + ): boolean { + const storageKey = this.keyPrefix + serializeValueWithOptions(key, options); return near.storageHasKey(storageKey); } // Returns true if the element was present in the set. - remove(key: DataType): boolean { - const storageKey = this.keyPrefix + JSON.stringify(key); + remove( + key: DataType, + options?: Pick, "serializer"> + ): boolean { + const storageKey = this.keyPrefix + serializeValueWithOptions(key, options); return near.storageRemove(storageKey); } // If the set did not have this value present, `true` is returned. // If the set did have this value present, `false` is returned. - set(key: DataType): boolean { - const storageKey = this.keyPrefix + JSON.stringify(key); + set( + key: DataType, + options?: Pick, "serializer"> + ): boolean { + const storageKey = this.keyPrefix + serializeValueWithOptions(key, options); return !near.storageWrite(storageKey, ""); } - extend(keys: DataType[]): void { - keys.forEach((key) => this.set(key)); + extend( + keys: DataType[], + options?: Pick, "serializer"> + ): void { + keys.forEach((key) => this.set(key, options)); } - serialize(): string { - return JSON.stringify(this); + serialize(options?: Pick, "serializer">): string { + return serializeValueWithOptions(this, options); } // converting plain object to class object diff --git a/src/collections/unordered-map.ts b/src/collections/unordered-map.ts index a7e99cc33..0988bb457 100644 --- a/src/collections/unordered-map.ts +++ b/src/collections/unordered-map.ts @@ -1,20 +1,24 @@ -import { assert, Bytes, getValueWithOptions, Mutable } from "../utils"; +import { + assert, + Bytes, + ERR_INCONSISTENT_STATE, + getValueWithOptions, + Mutable, + serializeValueWithOptions, +} from "../utils"; import { Vector, VectorIterator } from "./vector"; import { LookupMap } from "./lookup-map"; import { GetOptions } from "../types/collections"; -const ERR_INCONSISTENT_STATE = - "The collection is an inconsistent state. Did previous smart contract execution terminate unexpectedly?"; - -type ValueAndIndex = [value: DataType, index: number]; +type ValueAndIndex = [value: string, index: number]; export class UnorderedMap { readonly keys: Vector; - readonly values: LookupMap>; + readonly values: LookupMap; constructor(readonly prefix: Bytes) { this.keys = new Vector(`${prefix}u`); // intentional different prefix with old UnorderedMap - this.values = new LookupMap>(`${prefix}m`); + this.values = new LookupMap(`${prefix}m`); } get length() { @@ -43,19 +47,19 @@ export class UnorderedMap { options?: GetOptions ): DataType | null { const valueAndIndex = this.values.get(key); + const serialized = serializeValueWithOptions(value, options); if (valueAndIndex === null) { - const nextIndex = this.length; + const newElementIndex = this.length; this.keys.push(key); - this.values.set(key, [value, nextIndex]); + this.values.set(key, [serialized, newElementIndex]); return null; } - const [oldValue] = valueAndIndex; - valueAndIndex[0] = value; - this.values.set(key, valueAndIndex); + const [oldValue, oldIndex] = valueAndIndex; + this.values.set(key, [serialized, oldIndex]); return getValueWithOptions(oldValue, options); } @@ -79,10 +83,7 @@ export class UnorderedMap { assert(swappedValueAndIndex !== null, ERR_INCONSISTENT_STATE); - this.values.set(swappedKey, [ - getValueWithOptions(swappedValueAndIndex[0], options), - index, - ]); + this.values.set(swappedKey, [swappedValueAndIndex[0], index]); } return getValueWithOptions(value, options); @@ -127,8 +128,8 @@ export class UnorderedMap { } } - serialize(): string { - return JSON.stringify(this); + serialize(options?: Pick, "serializer">): string { + return serializeValueWithOptions(this, options); } // converting plain object to class object @@ -151,7 +152,7 @@ export class UnorderedMap { class UnorderedMapIterator { private keys: VectorIterator; - private map: LookupMap>; + private map: LookupMap; constructor( unorderedMap: UnorderedMap, @@ -161,20 +162,20 @@ class UnorderedMapIterator { this.map = unorderedMap.values; } - next(): { value: [unknown | null, unknown | null]; done: boolean } { + next(): { value: [Bytes | null, DataType | null]; done: boolean } { const key = this.keys.next(); if (key.done) { return { value: [key.value, null], done: key.done }; } - const [value] = this.map.get(key.value); + const valueAndIndex = this.map.get(key.value); - assert(value !== null, ERR_INCONSISTENT_STATE); + assert(valueAndIndex !== null, ERR_INCONSISTENT_STATE); return { done: key.done, - value: [key.value, getValueWithOptions(value, this.options)], + value: [key.value, getValueWithOptions(valueAndIndex[0], this.options)], }; } } diff --git a/src/collections/unordered-set.ts b/src/collections/unordered-set.ts index 3cf535af2..0c687de60 100644 --- a/src/collections/unordered-set.ts +++ b/src/collections/unordered-set.ts @@ -1,12 +1,16 @@ import * as near from "../api"; -import { u8ArrayToBytes, bytesToU8Array, Bytes, assert } from "../utils"; +import { + u8ArrayToBytes, + bytesToU8Array, + Bytes, + assert, + serializeValueWithOptions, + ERR_INCONSISTENT_STATE, +} from "../utils"; import { Vector, VectorIterator } from "./vector"; import { Mutable } from "../utils"; import { GetOptions } from "../types/collections"; -const ERR_INCONSISTENT_STATE = - "The collection is an inconsistent state. Did previous smart contract execution terminate unexpectedly?"; - function serializeIndex(index: number) { const data = new Uint32Array([index]); const array = new Uint8Array(data.buffer); @@ -38,28 +42,37 @@ export class UnorderedSet { return this.elements.isEmpty(); } - contains(element: DataType): boolean { - const indexLookup = this.elementIndexPrefix + JSON.stringify(element); + contains( + element: DataType, + options?: Pick, "serializer"> + ): boolean { + const indexLookup = + this.elementIndexPrefix + serializeValueWithOptions(element, options); return near.storageHasKey(indexLookup); } - set(element: DataType): boolean { - const indexLookup = this.elementIndexPrefix + JSON.stringify(element); - - if (!near.storageRead(indexLookup)) { - const nextIndex = this.length; - const nextIndexRaw = serializeIndex(nextIndex); - near.storageWrite(indexLookup, nextIndexRaw); - this.elements.push(element); + set( + element: DataType, + options?: Pick, "serializer"> + ): boolean { + const indexLookup = + this.elementIndexPrefix + serializeValueWithOptions(element, options); - return true; + if (near.storageRead(indexLookup)) { + return false; } - return false; + const nextIndex = this.length; + const nextIndexRaw = serializeIndex(nextIndex); + near.storageWrite(indexLookup, nextIndexRaw); + this.elements.push(element, options); + + return true; } - remove(element: DataType): boolean { - const indexLookup = this.elementIndexPrefix + JSON.stringify(element); + remove(element: DataType, options?: GetOptions): boolean { + const indexLookup = + this.elementIndexPrefix + serializeValueWithOptions(element, options); const indexRaw = near.storageRead(indexLookup); if (!indexRaw) { @@ -79,7 +92,7 @@ export class UnorderedSet { // If there is more than one element then swap remove swaps it with the last // element. - const lastElement = this.elements.get(this.length - 1); + const lastElement = this.elements.get(this.length - 1, options); assert(!!lastElement, ERR_INCONSISTENT_STATE); @@ -89,7 +102,8 @@ export class UnorderedSet { // reinsert the lookup back. if (lastElement !== element) { const lastLookupElement = - this.elementIndexPrefix + JSON.stringify(lastElement); + this.elementIndexPrefix + + serializeValueWithOptions(lastElement, options); near.storageWrite(lastLookupElement, indexRaw); } @@ -99,9 +113,10 @@ export class UnorderedSet { return true; } - clear(): void { + clear(options?: Pick, "serializer">): void { for (const element of this.elements) { - const indexLookup = this.elementIndexPrefix + JSON.stringify(element); + const indexLookup = + this.elementIndexPrefix + serializeValueWithOptions(element, options); near.storageRemove(indexLookup); } @@ -138,8 +153,8 @@ export class UnorderedSet { } } - serialize(): string { - return JSON.stringify(this); + serialize(options?: Pick, "serializer">): string { + return serializeValueWithOptions(this, options); } // converting plain object to class object diff --git a/src/collections/vector.ts b/src/collections/vector.ts index 836b464f2..3b235e844 100644 --- a/src/collections/vector.ts +++ b/src/collections/vector.ts @@ -1,9 +1,14 @@ import * as near from "../api"; -import { assert, Bytes, getValueWithOptions, u8ArrayToBytes } from "../utils"; +import { + assert, + Bytes, + getValueWithOptions, + u8ArrayToBytes, + serializeValueWithOptions, + ERR_INCONSISTENT_STATE, + ERR_INDEX_OUT_OF_BOUNDS, +} from "../utils"; import { GetOptions } from "../types/collections"; -const ERR_INDEX_OUT_OF_BOUNDS = "Index out of bounds"; -const ERR_INCONSISTENT_STATE = - "The collection is an inconsistent state. Did previous smart contract execution terminate unexpectedly?"; function indexToKey(prefix: Bytes, index: number): Bytes { const data = new Uint32Array([index]); @@ -16,20 +21,22 @@ function indexToKey(prefix: Bytes, index: number): Bytes { /// An iterable implementation of vector that stores its content on the trie. /// Uses the following map: index -> element export class Vector { - length = 0; - - constructor(readonly prefix: Bytes) {} + constructor(readonly prefix: Bytes, public length = 0) {} isEmpty(): boolean { return this.length === 0; } - get(index: number, options?: GetOptions): DataType | null { + get( + index: number, + options?: Omit, "serializer"> + ): DataType | null { if (index >= this.length) { - return null; + return options?.defaultValue ?? null; } + const storageKey = indexToKey(this.prefix, index); - const value = JSON.parse(near.storageRead(storageKey)); + const value = near.storageRead(storageKey); return getValueWithOptions(value, options); } @@ -45,25 +52,29 @@ export class Vector { } const key = indexToKey(this.prefix, index); - const last = this.pop(); + const last = this.pop(options); assert( - near.storageWrite(key, JSON.stringify(last)), + near.storageWrite(key, serializeValueWithOptions(last, options)), ERR_INCONSISTENT_STATE ); - const value = JSON.parse(near.storageGetEvicted()); + const value = near.storageGetEvicted(); return getValueWithOptions(value, options); } - push(element: DataType) { + push( + element: DataType, + options?: Pick, "serializer"> + ): void { const key = indexToKey(this.prefix, this.length); this.length += 1; - near.storageWrite(key, JSON.stringify(element)); + + near.storageWrite(key, serializeValueWithOptions(element, options)); } - pop(options?: GetOptions): DataType | null { + pop(options?: Omit, "serializer">): DataType | null { if (this.isEmpty()) { return options?.defaultValue ?? null; } @@ -74,7 +85,7 @@ export class Vector { assert(near.storageRemove(lastKey), ERR_INCONSISTENT_STATE); - const value = JSON.parse(near.storageGetEvicted()); + const value = near.storageGetEvicted(); return getValueWithOptions(value, options); } @@ -88,11 +99,11 @@ export class Vector { const key = indexToKey(this.prefix, index); assert( - near.storageWrite(key, JSON.stringify(element)), + near.storageWrite(key, serializeValueWithOptions(element, options)), ERR_INCONSISTENT_STATE ); - const value = JSON.parse(near.storageGetEvicted()); + const value = near.storageGetEvicted(); return getValueWithOptions(value, options); } @@ -120,8 +131,8 @@ export class Vector { const iterator = options ? this.createIteratorWithOptions(options) : this; - for (const v of iterator) { - array.push(v); + for (const value of iterator) { + array.push(value); } return array; @@ -136,28 +147,25 @@ export class Vector { this.length = 0; } - serialize(): string { - return JSON.stringify(this); + serialize(options?: Pick, "serializer">): string { + return serializeValueWithOptions(this, options); } // converting plain object to class object static reconstruct(data: Vector): Vector { - const vector = new Vector(data.prefix); - vector.length = data.length; + const vector = new Vector(data.prefix, data.length); return vector; } } export class VectorIterator { - private current: number; + private current = 0; constructor( private vector: Vector, private readonly options?: GetOptions - ) { - this.current = 0; - } + ) {} next(): { value: DataType | null; diff --git a/src/index.ts b/src/index.ts index d8af4d0e6..54f875b87 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,7 @@ import { UnorderedSet, } from "./collections"; -import { bytes, Bytes, assert } from "./utils"; +import { bytes, Bytes, assert, validateAccountId } from "./utils"; import { NearPromise, PromiseOrValue } from "./promise"; @@ -27,6 +27,7 @@ export { bytes, Bytes, assert, + validateAccountId, NearPromise, PromiseOrValue, }; diff --git a/src/near-bindgen.ts b/src/near-bindgen.ts index 770c23017..9513a80d2 100644 --- a/src/near-bindgen.ts +++ b/src/near-bindgen.ts @@ -1,21 +1,33 @@ import * as near from "./api"; +import { deserialize, serialize } from "./utils"; type EmptyParameterObject = Record; -// type AnyObject = Record; -// type DecoratorFunction = ( -// target: AnyObject, -// key: string | symbol, -// descriptor: TypedPropertyDescriptor -// ) => void; - -export function initialize(_empty: EmptyParameterObject) { - /* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/ban-types */ - return function ( - _target: any, +type AnyObject = Record; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type DecoratorFunction = any>( + target: object, + key: string | symbol, + descriptor: TypedPropertyDescriptor +) => void; + +export function initialize(_empty: EmptyParameterObject): DecoratorFunction { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return function any>( + _target: object, + _key: string | symbol, + _descriptor: TypedPropertyDescriptor + // eslint-disable-next-line @typescript-eslint/no-empty-function + ): void {}; +} + +export function view(_empty: EmptyParameterObject): DecoratorFunction { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return function any>( + _target: object, _key: string | symbol, - _descriptor: TypedPropertyDescriptor + _descriptor: TypedPropertyDescriptor + // eslint-disable-next-line @typescript-eslint/no-empty-function ): void {}; - /* eslint-enable @typescript-eslint/no-empty-function, @typescript-eslint/ban-types */ } export function call({ @@ -24,86 +36,88 @@ export function call({ }: { privateFunction?: boolean; payableFunction?: boolean; -}) { - /* eslint-disable @typescript-eslint/ban-types */ - return function ( - _target: any, +}): DecoratorFunction { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return function any>( + _target: object, _key: string | symbol, - descriptor: TypedPropertyDescriptor + descriptor: TypedPropertyDescriptor ): void { - /* eslint-enable @typescript-eslint/ban-types */ const originalMethod = descriptor.value; - descriptor.value = function (...args: unknown[]) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + descriptor.value = function ( + ...args: Parameters + ): ReturnType { if ( privateFunction && near.predecessorAccountId() !== near.currentAccountId() ) { - throw Error("Function is private"); + throw new Error("Function is private"); } - if (!payableFunction && near.attachedDeposit() > BigInt(0)) { - throw Error("Function is not payable"); + + if (!payableFunction && near.attachedDeposit() > 0n) { + throw new Error("Function is not payable"); } + return originalMethod.apply(this, args); }; }; } -export function view(_empty: EmptyParameterObject) { - /* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/ban-types */ - return function ( - _target: any, - _key: string | symbol, - _descriptor: TypedPropertyDescriptor - ): void {}; - /* eslint-enable @typescript-eslint/no-empty-function, @typescript-eslint/ban-types */ -} - export function NearBindgen({ requireInit = false, + serializer = serialize, + deserializer = deserialize, }: { requireInit?: boolean; + serializer?(value: unknown): string; + deserializer?(value: string): unknown; }) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any return (target: T) => { return class extends target { static _create() { return new target(); } - static _getState(): any { + static _getState(): unknown | null { const rawState = near.storageRead("STATE"); return rawState ? this._deserialize(rawState) : null; } - /* eslint-disable-next-line @typescript-eslint/ban-types */ - static _saveToStorage(obj: Object): void { - near.storageWrite("STATE", this._serialize(obj)); + static _saveToStorage(objectToSave: unknown): void { + near.storageWrite("STATE", this._serialize(objectToSave)); } - static _getArgs(): JSON { + static _getArgs(): unknown { return JSON.parse(near.input() || "{}"); } - /* eslint-disable-next-line @typescript-eslint/ban-types */ - static _serialize(value: Object): string { - return JSON.stringify(value); + static _serialize(value: unknown, forReturn = false): string { + if (forReturn) { + return JSON.stringify(value, (_, value) => + typeof value === "bigint" ? `${value}` : value + ); + } + + return serializer(value); } - /* eslint-disable-next-line @typescript-eslint/ban-types */ - static _deserialize(value: string): Object { - return JSON.parse(value); + static _deserialize(value: string): unknown { + return deserializer(value); } - static _reconstruct(classObject: any, plainObject: JSON) { + static _reconstruct(classObject: object, plainObject: AnyObject): object { for (const item in classObject) { - if (classObject[item].constructor?.reconstruct !== undefined) { - classObject[item] = classObject[item].constructor.reconstruct( - plainObject[item] - ); - } else { - classObject[item] = plainObject[item]; - } + const reconstructor = classObject[item].constructor?.reconstruct; + + classObject[item] = reconstructor + ? reconstructor(plainObject[item]) + : plainObject[item]; } + return classObject; } diff --git a/src/types/collections.ts b/src/types/collections.ts index fa42684d4..1489088e4 100644 --- a/src/types/collections.ts +++ b/src/types/collections.ts @@ -1,4 +1,6 @@ export type GetOptions = { - reconstructor?: (value: unknown) => DataType; + reconstructor?(value: unknown): DataType; defaultValue?: DataType; + serializer?(valueToSerialize: unknown): string; + deserializer?(valueToDeserialize: string): unknown; }; diff --git a/src/utils.ts b/src/utils.ts index 6e6ca4d28..1e7d22a73 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,6 +5,19 @@ export type PromiseIndex = number | bigint; export type NearAmount = number | bigint; export type Register = number | bigint; +const TYPE_KEY = "typeInfo"; +enum TypeBrand { + BIGINT = "bigint", + DATE = "date", +} + +export const ERR_INCONSISTENT_STATE = + "The collection is an inconsistent state. Did previous smart contract execution terminate unexpectedly?"; +export const ERR_INDEX_OUT_OF_BOUNDS = "Index out of bounds"; + +const ACCOUNT_ID_REGEX = + /^(([a-z\d]+[-_])*[a-z\d]+\.)*([a-z\d]+[-_])*[a-z\d]+$/; + export function u8ArrayToBytes(array: Uint8Array): Bytes { return array.reduce( (result, value) => `${result}${String.fromCharCode(value)}`, @@ -50,16 +63,88 @@ export function assert(expression: boolean, message: string): void { export type Mutable = { -readonly [P in keyof T]: T[P] }; export function getValueWithOptions( - value: unknown, - options?: GetOptions + value: string, + options: Omit, "serializer"> = { + deserializer: deserialize, + } ): DataType | null { - if (value === undefined || value === null) { + const deserialized = deserialize(value); + + if (deserialized === undefined || deserialized === null) { return options?.defaultValue ?? null; } if (options?.reconstructor) { - return options.reconstructor(value); + return options.reconstructor(deserialized); } - return value as DataType; + return deserialized as DataType; +} + +export function serializeValueWithOptions( + value: DataType, + { serializer }: Pick, "serializer"> = { + serializer: serialize, + } +): string { + return serializer(value); +} + +export function serialize(valueToSerialize: unknown): string { + return JSON.stringify(valueToSerialize, function (key, value) { + if (typeof value === "bigint") { + return { + value: value.toString(), + [TYPE_KEY]: TypeBrand.BIGINT, + }; + } + + if ( + typeof this[key] === "object" && + this[key] !== null && + this[key] instanceof Date + ) { + return { + value: this[key].toISOString(), + [TYPE_KEY]: TypeBrand.DATE, + }; + } + + return value; + }); +} + +export function deserialize(valueToDeserialize: string): unknown { + return JSON.parse(valueToDeserialize, (_, value) => { + if ( + value !== null && + typeof value === "object" && + Object.keys(value).length === 2 && + Object.keys(value).every((key) => ["value", TYPE_KEY].includes(key)) + ) { + switch (value[TYPE_KEY]) { + case TypeBrand.BIGINT: + return BigInt(value["value"]); + case TypeBrand.DATE: + return new Date(value["value"]); + } + } + + return value; + }); +} + +/** + * Validates the Account ID according to the NEAR protocol + * [Account ID rules](https://nomicon.io/DataStructures/Account#account-id-rules). + * + * @param accountId - The Account ID string you want to validate. + * @returns boolean + */ +export function validateAccountId(accountId: string): boolean { + return ( + accountId.length >= 2 && + accountId.length <= 64 && + ACCOUNT_ID_REGEX.test(accountId) + ); } diff --git a/tests/__tests__/test-bigint-serialization.ava.js b/tests/__tests__/test-bigint-serialization.ava.js new file mode 100644 index 000000000..66a2ab593 --- /dev/null +++ b/tests/__tests__/test-bigint-serialization.ava.js @@ -0,0 +1,49 @@ +import { Worker } from "near-workspaces"; +import test from "ava"; + +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; + + // Create and deploy test contract + const bsContract = await root.devDeploy("build/bigint-serialization.wasm"); + + // Save state for test runs + t.context.worker = worker; + t.context.accounts = { root, bsContract }; +}); + +test.afterEach.always(async (t) => { + await t.context.worker.tearDown().catch((error) => { + console.log("Failed to tear down the worker:", error); + }); +}); + +test("get initial bigint field value", async (t) => { + const { bsContract } = t.context.accounts; + const bigintField = await bsContract.view("getBigintField"); + t.is(bigintField, `${1n}`); +}); + +test("get bigint field after increment", async (t) => { + const { bsContract } = t.context.accounts; + const bigintField = await bsContract.view("getBigintField"); + t.is(bigintField, `${1n}`); + + await bsContract.call(bsContract, "increment", ""); + const afterIncrement = await bsContract.view("getBigintField"); + t.is(afterIncrement, `${2n}`); +}); + +test("get bigint field after set", async (t) => { + const { bsContract } = t.context.accounts; + const bigintField = await bsContract.view("getBigintField"); + t.is(bigintField, `${1n}`); + + await bsContract.call(bsContract, "setBigintField", { bigintField: `${3n}` }); + const afterSet = await bsContract.view("getBigintField"); + t.is(afterSet, `${3n}`); +}); diff --git a/tests/__tests__/test-date-serialization.ava.js b/tests/__tests__/test-date-serialization.ava.js new file mode 100644 index 000000000..46eab5f01 --- /dev/null +++ b/tests/__tests__/test-date-serialization.ava.js @@ -0,0 +1,51 @@ +import { Worker } from "near-workspaces"; +import test from "ava"; + +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; + + // Create and deploy test contract + const dsContract = await root.devDeploy("build/date-serialization.wasm"); + + // Save state for test runs + t.context.worker = worker; + t.context.accounts = { root, dsContract }; +}); + +test.afterEach.always(async (t) => { + await t.context.worker.tearDown().catch((error) => { + console.log("Failed to tear down the worker:", error); + }); +}); + +test("get initial date field value", async (t) => { + const { dsContract } = t.context.accounts; + const dateField = await dsContract.view("getDateField"); + t.is(dateField, new Date(0).toISOString()); +}); + +test("get date field after set", async (t) => { + const { dsContract } = t.context.accounts; + const dateField = await dsContract.view("getDateField"); + t.is(dateField, new Date(0).toISOString()); + + const newDate = new Date(); + await dsContract.call(dsContract, "setDateField", { dateField: newDate }); + const afterSet = await dsContract.view("getDateField"); + t.is(afterSet, newDate.toISOString()); +}); + +test("get date field in milliseconds", async (t) => { + const { dsContract } = t.context.accounts; + const dateField = await dsContract.view("getDateFieldAsMilliseconds"); + t.is(dateField, new Date(0).getTime()); + + const newDate = new Date(); + await dsContract.call(dsContract, "setDateField", { dateField: newDate }); + const afterIncrement = await dsContract.view("getDateFieldAsMilliseconds"); + t.is(afterIncrement, newDate.getTime()); +}); diff --git a/tests/__tests__/test_promise_api.ava.js b/tests/__tests__/test_promise_api.ava.js index f67d28836..1cfb4202b 100644 --- a/tests/__tests__/test_promise_api.ava.js +++ b/tests/__tests__/test_promise_api.ava.js @@ -287,7 +287,7 @@ test("promise delete account", async (t) => { }); test("promise batch function call weight", async (t) => { - const { ali, caller2Contract, calleeContract } = t.context.accounts; + const { ali, caller2Contract } = t.context.accounts; let r = await ali.callRaw( caller2Contract, "test_promise_batch_call_weight", diff --git a/tests/__tests__/unordered-map.ava.js b/tests/__tests__/unordered-map.ava.js index 462bfd65f..24015e330 100644 --- a/tests/__tests__/unordered-map.ava.js +++ b/tests/__tests__/unordered-map.ava.js @@ -27,7 +27,7 @@ test.afterEach.always(async (t) => { }); test("UnorderedMap is empty by default", async (t) => { - const { root, unorderedMapContract } = t.context.accounts; + const { unorderedMapContract } = t.context.accounts; const result = await unorderedMapContract.view("len", {}); t.is(result, 0); }); diff --git a/tests/__tests__/unordered-set.ava.js b/tests/__tests__/unordered-set.ava.js index 89a341777..5f19a51e7 100644 --- a/tests/__tests__/unordered-set.ava.js +++ b/tests/__tests__/unordered-set.ava.js @@ -28,7 +28,7 @@ test.afterEach.always(async (t) => { }); test("UnorderedSet is empty by default", async (t) => { - const { root, unorderedSetContract } = t.context.accounts; + const { unorderedSetContract } = t.context.accounts; const result = await unorderedSetContract.view("len", {}); t.is(result, 0); t.is(await unorderedSetContract.view("isEmpty", {}), true); diff --git a/tests/__tests__/vector.ava.js b/tests/__tests__/vector.ava.js index 15fe4e2c2..48613077b 100644 --- a/tests/__tests__/vector.ava.js +++ b/tests/__tests__/vector.ava.js @@ -28,7 +28,7 @@ test.afterEach.always(async (t) => { }); test("Vector is empty by default", async (t) => { - const { root, vectorContract } = t.context.accounts; + const { vectorContract } = t.context.accounts; let result = await vectorContract.view("len", {}); t.is(result, 0); t.is(await vectorContract.view("isEmpty", {}), true); diff --git a/tests/package.json b/tests/package.json index 66b60d954..141d795ed 100644 --- a/tests/package.json +++ b/tests/package.json @@ -6,7 +6,7 @@ "type": "module", "scripts": { "postinstall": "cd .. && yarn link && cd tests && yarn link near-sdk-js", - "build": "yarn build:context-api && yarn build:math-api && yarn build:storage-api && yarn build:log-panic-api && yarn build:promise-api && yarn build:promise-batch-api && yarn build:function-params && yarn build:lookup-map && yarn build:lookup-set && yarn build:unordered-map && yarn build:unordered-set && yarn build:vector && yarn build:bytes && yarn build:typescript && yarn build:public-key && yarn build:near-bindgen && yarn build:payable && yarn build:private && yarn build:highlevel-promise", + "build": "yarn build:context-api && yarn build:math-api && yarn build:storage-api && yarn build:log-panic-api && yarn build:promise-api && yarn build:promise-batch-api && yarn build:function-params && yarn build:lookup-map && yarn build:lookup-set && yarn build:unordered-map && yarn build:unordered-set && yarn build:vector && yarn build:bytes && yarn build:typescript && yarn build:public-key && yarn build:near-bindgen && yarn build:payable && yarn build:private && yarn build:highlevel-promise && yarn build:bigint-serialization && yarn build:date-serialization", "build:context-api": "near-sdk-js build src/context_api.js build/context_api.wasm", "build:math-api": "near-sdk-js build src/math_api.js build/math_api.wasm", "build:storage-api": "near-sdk-js build src/storage_api.js build/storage_api.wasm", @@ -26,6 +26,8 @@ "build:near-bindgen": "near-sdk-js build src/decorators/require_init_true.ts build/require_init_true.wasm && near-sdk-js build src/decorators/require_init_false.ts build/require_init_false.wasm", "build:payable": "near-sdk-js build src/decorators/payable.ts build/payable.wasm", "build:private": "near-sdk-js build src/decorators/private.ts build/private.wasm", + "build:bigint-serialization": "near-sdk-js build src/bigint-serialization.ts build/bigint-serialization.wasm", + "build:date-serialization": "near-sdk-js build src/date-serialization.ts build/date-serialization.wasm", "test": "ava", "test:context-api": "ava __tests__/test_context_api.ava.js", "test:math-api": "ava __tests__/test_math_api.ava.js", @@ -44,7 +46,10 @@ "test:public-key": "ava __tests__/test-public-key.ava.js", "test:near-bindgen": "ava __tests__/decorators/near_bindgen.ava.js", "test:payable": "ava __tests__/decorators/payable.ava.js", - "test:private": "ava __tests__/decorators/private.ava.js" + "test:private": "ava __tests__/decorators/private.ava.js", + "test:bigint-serialization": "ava __tests__/test-bigint-serialization.ava.js", + "test:date-serialization": "ava __tests__/test-date-serialization.ava.js", + "test:serialization": "ava __tests__/test-serialization.ava.js" }, "author": "Near Inc ", "license": "Apache-2.0", diff --git a/tests/src/bigint-serialization.ts b/tests/src/bigint-serialization.ts new file mode 100644 index 000000000..c7f9c6038 --- /dev/null +++ b/tests/src/bigint-serialization.ts @@ -0,0 +1,29 @@ +import { near, NearBindgen, call, view } from "near-sdk-js"; + +@NearBindgen({}) +export class BigIntSerializationTest { + bigintField: bigint; + + constructor() { + this.bigintField = 1n; + } + + @view({}) + getBigintField(): bigint { + near.log(`getBigintField: ${this.bigintField}`); + return this.bigintField; + } + + @call({}) + setBigintField(args: { bigintField: bigint }): void { + const bigintField = BigInt(args.bigintField); + near.log(`setBigintField: ${bigintField}`); + this.bigintField = bigintField; + } + + @call({}) + increment(): void { + this.bigintField += 1n; + near.log(`increment: ${this.bigintField}`); + } +} diff --git a/tests/src/date-serialization.ts b/tests/src/date-serialization.ts new file mode 100644 index 000000000..aeb0e817c --- /dev/null +++ b/tests/src/date-serialization.ts @@ -0,0 +1,29 @@ +import { near, NearBindgen, call, view } from "near-sdk-js"; + +@NearBindgen({}) +export class DateSerializationTest { + dateField: Date; + + constructor() { + this.dateField = new Date(0); + } + + @view({}) + getDateField(): Date { + near.log(`getDateField: ${this.dateField}`); + return this.dateField; + } + + @call({}) + setDateField(args: { dateField: Date }): void { + const dateField = new Date(args.dateField); + near.log(`setDateField: ${dateField}`); + this.dateField = dateField; + } + + @view({}) + getDateFieldAsMilliseconds(): number { + near.log(`getDateFieldAsMilliseconds: ${this.dateField.getTime()}`); + return this.dateField.getTime(); + } +} diff --git a/tests/src/decorators/payable.ts b/tests/src/decorators/payable.ts index d87c18923..5400aaf4a 100644 --- a/tests/src/decorators/payable.ts +++ b/tests/src/decorators/payable.ts @@ -1,7 +1,7 @@ import { near, NearBindgen, call, view } from "near-sdk-js"; @NearBindgen({}) -class PayableTest { +export class PayableTest { value: string; constructor() { diff --git a/tests/src/decorators/private.ts b/tests/src/decorators/private.ts index a6875b15e..cad023dbe 100644 --- a/tests/src/decorators/private.ts +++ b/tests/src/decorators/private.ts @@ -1,7 +1,7 @@ import { near, NearBindgen, call, view } from "near-sdk-js"; @NearBindgen({}) -class PrivateTest { +export class PrivateTest { value: string; constructor() { diff --git a/tests/src/decorators/require_init_false.ts b/tests/src/decorators/require_init_false.ts index 181822cba..5c24c597e 100644 --- a/tests/src/decorators/require_init_false.ts +++ b/tests/src/decorators/require_init_false.ts @@ -1,7 +1,7 @@ import { near, NearBindgen, call, view, initialize } from "near-sdk-js"; @NearBindgen({ requireInit: false }) -class NBTest { +export class NBTest { status: string; constructor() { diff --git a/tests/src/decorators/require_init_true.ts b/tests/src/decorators/require_init_true.ts index 55ce0db83..6d813619b 100644 --- a/tests/src/decorators/require_init_true.ts +++ b/tests/src/decorators/require_init_true.ts @@ -1,7 +1,7 @@ import { near, NearBindgen, call, view, initialize } from "near-sdk-js"; @NearBindgen({ requireInit: true }) -class NBTest { +export class NBTest { status: string; constructor() { diff --git a/tests/src/function-params.js b/tests/src/function-params.js index 237c18c2d..18253ea1c 100644 --- a/tests/src/function-params.js +++ b/tests/src/function-params.js @@ -4,7 +4,7 @@ import { NearBindgen, call, view, near } from "near-sdk-js"; * Simple contract to test function parameters */ @NearBindgen({}) -class FunctionParamsTestContract { +export class FunctionParamsTestContract { constructor() { this.val1 = "default1"; this.val2 = "default2"; diff --git a/tests/src/highlevel-promise.js b/tests/src/highlevel-promise.js index 4b6dd1d97..0743a0c4b 100644 --- a/tests/src/highlevel-promise.js +++ b/tests/src/highlevel-promise.js @@ -1,4 +1,4 @@ -import { NearBindgen, call, view, NearPromise, near, bytes } from "near-sdk-js"; +import { NearBindgen, call, NearPromise, near, bytes } from "near-sdk-js"; import { PublicKey } from "near-sdk-js/lib/types"; function callingData() { @@ -15,7 +15,7 @@ function arrayN(n) { } @NearBindgen({}) -class HighlevelPromiseContract { +export class HighlevelPromiseContract { @call({}) test_promise_batch_stake() { let promise = NearPromise.new("highlevel-promise.test.near").stake( diff --git a/tests/src/lookup-map.js b/tests/src/lookup-map.js index af2367ce6..2f7cc382b 100644 --- a/tests/src/lookup-map.js +++ b/tests/src/lookup-map.js @@ -2,7 +2,7 @@ import { NearBindgen, call, view, LookupMap } from "near-sdk-js"; import { House, Room } from "./model.js"; @NearBindgen({}) -class LookupMapTestContract { +export class LookupMapTestContract { constructor() { this.lookupMap = new LookupMap("a"); } diff --git a/tests/src/lookup-set.js b/tests/src/lookup-set.js index 43f16ef09..f5dc9d2a9 100644 --- a/tests/src/lookup-set.js +++ b/tests/src/lookup-set.js @@ -2,7 +2,7 @@ import { NearBindgen, call, view, LookupSet } from "near-sdk-js"; import { House, Room } from "./model.js"; @NearBindgen({}) -class LookupSetTestContract { +export class LookupSetTestContract { constructor() { this.lookupSet = new LookupSet("a"); } diff --git a/tests/src/public-key.js b/tests/src/public-key.js index 46822f9ed..a62a6480a 100644 --- a/tests/src/public-key.js +++ b/tests/src/public-key.js @@ -1,4 +1,4 @@ -import { near, bytes, types } from "near-sdk-js"; +import { near, bytes } from "near-sdk-js"; import { CurveType, PublicKey } from "near-sdk-js/lib/types"; import { assert } from "near-sdk-js/lib/utils"; diff --git a/tests/src/typescript.ts b/tests/src/typescript.ts index b0347a53c..294fa1b97 100644 --- a/tests/src/typescript.ts +++ b/tests/src/typescript.ts @@ -1,7 +1,7 @@ import { NearBindgen, view } from "near-sdk-js"; @NearBindgen({}) -class TypeScriptTestContract { +export class TypeScriptTestContract { @view({}) bigint() { // JSON.stringify cannot seriaize a BigInt, need manually toString diff --git a/tests/src/unordered-map.js b/tests/src/unordered-map.js index b4528816e..8658d7955 100644 --- a/tests/src/unordered-map.js +++ b/tests/src/unordered-map.js @@ -1,8 +1,8 @@ -import { NearBindgen, call, view, UnorderedMap, near } from "near-sdk-js"; +import { NearBindgen, call, view, UnorderedMap } from "near-sdk-js"; import { House, Room } from "./model.js"; @NearBindgen({}) -class UnorderedMapTestContract { +export class UnorderedMapTestContract { constructor() { this.unorderedMap = new UnorderedMap("a"); } diff --git a/tests/src/unordered-set.js b/tests/src/unordered-set.js index 4392fff50..1637dc959 100644 --- a/tests/src/unordered-set.js +++ b/tests/src/unordered-set.js @@ -2,7 +2,7 @@ import { NearBindgen, call, view, UnorderedSet } from "near-sdk-js"; import { House, Room } from "./model.js"; @NearBindgen({}) -class UnorderedSetTestContract { +export class UnorderedSetTestContract { constructor() { this.unorderedSet = new UnorderedSet("a"); } diff --git a/tests/src/vector.js b/tests/src/vector.js index b46b94315..d1f873784 100644 --- a/tests/src/vector.js +++ b/tests/src/vector.js @@ -2,7 +2,7 @@ import { NearBindgen, call, view, Vector } from "near-sdk-js"; import { House, Room } from "./model.js"; @NearBindgen({}) -class VectorTestContract { +export class VectorTestContract { constructor() { this.vector = new Vector("a"); } diff --git a/yarn.lock b/yarn.lock index def95a287..5b1cfa904 100644 --- a/yarn.lock +++ b/yarn.lock @@ -262,6 +262,11 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-string-parser@^7.18.10": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz#181f22d28ebe1b3857fa575f5c290b1aaf659b56" + integrity sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw== + "@babel/helper-validator-identifier@^7.16.7": version "7.16.7" resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz" @@ -429,6 +434,15 @@ "@babel/helper-validator-identifier" "^7.18.6" to-fast-properties "^2.0.0" +"@babel/types@^7.3.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.19.0.tgz#75f21d73d73dc0351f3368d28db73465f4814600" + integrity sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA== + dependencies: + "@babel/helper-string-parser" "^7.18.10" + "@babel/helper-validator-identifier" "^7.18.6" + to-fast-properties "^2.0.0" + "@eslint/eslintrc@^1.3.2": version "1.3.2" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.2.tgz#58b69582f3b7271d8fa67fe5251767a5b38ea356" @@ -598,6 +612,13 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== +"@types/babel__traverse@^7.18.1": + version "7.18.1" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.1.tgz#ce5e2c8c272b99b7a9fd69fa39f0b4cd85028bd9" + integrity sha512-FSdLaZh2UxaMuLp9lixWaHq/golWTRWOnRsAXzDTDSDOQLuZb1nsdCt6pJSPWSEQt2eFZ2YVk3oYhn+1kLMeMA== + dependencies: + "@babel/types" "^7.3.0" + "@types/estree@*", "@types/estree@0.0.39": version "0.0.39" resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz"