diff --git a/lib/build-tools/near-bindgen-exporter.js b/lib/build-tools/near-bindgen-exporter.js index b777dd97..c5882108 100644 --- a/lib/build-tools/near-bindgen-exporter.js +++ b/lib/build-tools/near-bindgen-exporter.js @@ -53,7 +53,7 @@ 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)]), ])))); } export default function () { diff --git a/lib/collections/lookup-map.js b/lib/collections/lookup-map.js index f5039ba6..5518e928 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 { deserialize, getValueWithOptions, serialize } 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 = deserialize(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 = deserialize(near.storageGetEvicted()); return getValueWithOptions(value, options); } set(key, newValue, options) { const storageKey = this.keyPrefix + key; - const storageValue = JSON.stringify(newValue); + const storageValue = serialize(newValue); if (!near.storageWrite(storageKey, storageValue)) { return options?.defaultValue ?? null; } - const value = JSON.parse(near.storageGetEvicted()); + const value = deserialize(near.storageGetEvicted()); return getValueWithOptions(value, options); } extend(keyValuePairs, options) { @@ -36,7 +36,7 @@ export class LookupMap { } } serialize() { - return JSON.stringify(this); + return serialize(this); } // converting plain object to class object static reconstruct(data) { diff --git a/lib/collections/lookup-set.js b/lib/collections/lookup-set.js index 4c6b329c..22915633 100644 --- a/lib/collections/lookup-set.js +++ b/lib/collections/lookup-set.js @@ -1,28 +1,29 @@ import * as near from "../api"; +import { serialize } from "../utils"; export class LookupSet { constructor(keyPrefix) { this.keyPrefix = keyPrefix; } contains(key) { - const storageKey = this.keyPrefix + JSON.stringify(key); + const storageKey = this.keyPrefix + serialize(key); return near.storageHasKey(storageKey); } // Returns true if the element was present in the set. remove(key) { - const storageKey = this.keyPrefix + JSON.stringify(key); + const storageKey = this.keyPrefix + serialize(key); 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); + const storageKey = this.keyPrefix + serialize(key); return !near.storageWrite(storageKey, ""); } extend(keys) { keys.forEach((key) => this.set(key)); } serialize() { - return JSON.stringify(this); + return serialize(this); } // converting plain object to class object static reconstruct(data) { diff --git a/lib/collections/unordered-map.js b/lib/collections/unordered-map.js index 1c0b63b6..85c07385 100644 --- a/lib/collections/unordered-map.js +++ b/lib/collections/unordered-map.js @@ -1,4 +1,4 @@ -import { assert, getValueWithOptions } from "../utils"; +import { assert, getValueWithOptions, serialize, } 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?"; @@ -84,7 +84,7 @@ export class UnorderedMap { } } serialize() { - return JSON.stringify(this); + return serialize(this); } // converting plain object to class object static reconstruct(data) { diff --git a/lib/collections/unordered-set.js b/lib/collections/unordered-set.js index 1000e911..a735b529 100644 --- a/lib/collections/unordered-set.js +++ b/lib/collections/unordered-set.js @@ -1,5 +1,5 @@ import * as near from "../api"; -import { u8ArrayToBytes, bytesToU8Array, assert } from "../utils"; +import { u8ArrayToBytes, bytesToU8Array, assert, serialize, } 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) { @@ -25,11 +25,11 @@ export class UnorderedSet { return this.elements.isEmpty(); } contains(element) { - const indexLookup = this.elementIndexPrefix + JSON.stringify(element); + const indexLookup = this.elementIndexPrefix + serialize(element); return near.storageHasKey(indexLookup); } set(element) { - const indexLookup = this.elementIndexPrefix + JSON.stringify(element); + const indexLookup = this.elementIndexPrefix + serialize(element); if (!near.storageRead(indexLookup)) { const nextIndex = this.length; const nextIndexRaw = serializeIndex(nextIndex); @@ -40,7 +40,7 @@ export class UnorderedSet { return false; } remove(element) { - const indexLookup = this.elementIndexPrefix + JSON.stringify(element); + const indexLookup = this.elementIndexPrefix + serialize(element); const indexRaw = near.storageRead(indexLookup); if (!indexRaw) { return false; @@ -61,7 +61,7 @@ export class UnorderedSet { // 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 + serialize(lastElement); near.storageWrite(lastLookupElement, indexRaw); } const index = deserializeIndex(indexRaw); @@ -70,7 +70,7 @@ export class UnorderedSet { } clear() { for (const element of this.elements) { - const indexLookup = this.elementIndexPrefix + JSON.stringify(element); + const indexLookup = this.elementIndexPrefix + serialize(element); near.storageRemove(indexLookup); } this.elements.clear(); @@ -97,7 +97,7 @@ export class UnorderedSet { } } serialize() { - return JSON.stringify(this); + return serialize(this); } // converting plain object to class object static reconstruct(data) { diff --git a/lib/collections/vector.js b/lib/collections/vector.js index 3eaf2320..c35dbc05 100644 --- a/lib/collections/vector.js +++ b/lib/collections/vector.js @@ -1,5 +1,5 @@ import * as near from "../api"; -import { assert, getValueWithOptions, u8ArrayToBytes } from "../utils"; +import { assert, deserialize, getValueWithOptions, serialize, 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?"; function indexToKey(prefix, index) { @@ -23,7 +23,7 @@ export class Vector { return null; } const storageKey = indexToKey(this.prefix, index); - const value = JSON.parse(near.storageRead(storageKey)); + const value = deserialize(near.storageRead(storageKey)); return getValueWithOptions(value, options); } /// Removes an element from the vector and returns it in serialized form. @@ -36,14 +36,14 @@ export class Vector { } 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()); + assert(near.storageWrite(key, serialize(last)), ERR_INCONSISTENT_STATE); + const value = deserialize(near.storageGetEvicted()); return getValueWithOptions(value, options); } push(element) { const key = indexToKey(this.prefix, this.length); this.length += 1; - near.storageWrite(key, JSON.stringify(element)); + near.storageWrite(key, serialize(element)); } pop(options) { if (this.isEmpty()) { @@ -53,14 +53,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 = deserialize(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, serialize(element)), ERR_INCONSISTENT_STATE); + const value = deserialize(near.storageGetEvicted()); return getValueWithOptions(value, options); } extend(elements) { diff --git a/lib/near-bindgen.d.ts b/lib/near-bindgen.d.ts index 8c8dc6d1..cb4dcea3 100644 --- a/lib/near-bindgen.d.ts +++ b/lib/near-bindgen.d.ts @@ -1,10 +1,12 @@ declare type EmptyParameterObject = Record; -export declare function initialize(_empty: EmptyParameterObject): (_target: any, _key: string | symbol, _descriptor: TypedPropertyDescriptor) => void; +declare type AnyObject = Record; +declare type AnyFunction = (...args: unknown[]) => unknown; +export declare function initialize(_empty: EmptyParameterObject): (_target: unknown, _key: string | symbol, _descriptor: TypedPropertyDescriptor) => void; 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; +}): (_target: unknown, _key: string | symbol, descriptor: TypedPropertyDescriptor) => void; +export declare function view(_empty: EmptyParameterObject): (_target: unknown, _key: string | symbol, _descriptor: TypedPropertyDescriptor) => void; export declare function NearBindgen({ requireInit, }: { requireInit?: boolean; }): any>(target: T) => { @@ -12,12 +14,12 @@ export declare function NearBindgen({ requireInit, }: { [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 81b56071..ffd8ee63 100644 --- a/lib/near-bindgen.js +++ b/lib/near-bindgen.js @@ -1,19 +1,17 @@ import * as near from "./api"; -// type AnyObject = Record; +import { deserialize, serialize } from "./utils"; // type DecoratorFunction = ( // target: AnyObject, // key: string | symbol, // descriptor: TypedPropertyDescriptor // ) => void; export function initialize(_empty) { - /* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/ban-types */ + /* eslint-disable @typescript-eslint/no-empty-function */ return function (_target, _key, _descriptor) { }; - /* eslint-enable @typescript-eslint/no-empty-function, @typescript-eslint/ban-types */ + /* eslint-enable @typescript-eslint/no-empty-function */ } export function call({ privateFunction = false, payableFunction = false, }) { - /* eslint-disable @typescript-eslint/ban-types */ return function (_target, _key, descriptor) { - /* eslint-enable @typescript-eslint/ban-types */ const originalMethod = descriptor.value; descriptor.value = function (...args) { if (privateFunction && @@ -28,11 +26,12 @@ export function call({ privateFunction = false, payableFunction = false, }) { }; } export function view(_empty) { - /* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/ban-types */ + /* eslint-disable @typescript-eslint/no-empty-function */ return function (_target, _key, _descriptor) { }; - /* eslint-enable @typescript-eslint/no-empty-function, @typescript-eslint/ban-types */ + /* eslint-enable @typescript-eslint/no-empty-function */ } export function NearBindgen({ requireInit = false, }) { + // 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 serialize(value); } - /* eslint-disable-next-line @typescript-eslint/ban-types */ static _deserialize(value) { - return JSON.parse(value); + return deserialize(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/utils.d.ts b/lib/utils.d.ts index e43cc6b5..259e28ae 100644 --- a/lib/utils.d.ts +++ b/lib/utils.d.ts @@ -11,3 +11,5 @@ export declare type Mutable = { -readonly [P in keyof T]: T[P]; }; export declare function getValueWithOptions(value: unknown, options?: GetOptions): DataType | null; +export declare function serialize(valueToSerialize: unknown): string; +export declare function deserialize(valueToDeserialize: string): unknown; diff --git a/lib/utils.js b/lib/utils.js index c3b87e9c..ff2f7637 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,3 +1,5 @@ +const BIGINT_KEY = "bigint"; +const BIGINT_BRAND = "tnigib"; export function u8ArrayToBytes(array) { return array.reduce((result, value) => `${result}${String.fromCharCode(value)}`, ""); } @@ -35,3 +37,25 @@ export function getValueWithOptions(value, options) { } return value; } +export function serialize(valueToSerialize) { + return JSON.stringify(valueToSerialize, (_, value) => { + if (typeof value === "bigint") { + return { + value: value.toString(), + [BIGINT_KEY]: BIGINT_BRAND, + }; + } + 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", BIGINT_KEY].includes(key))) { + return BigInt(value["value"]); + } + return value; + }); +} diff --git a/src/build-tools/near-bindgen-exporter.js b/src/build-tools/near-bindgen-exporter.js index 7770831a..42b3a680 100644 --- a/src/build-tools/near-bindgen-exporter.js +++ b/src/build-tools/near-bindgen-exporter.js @@ -156,7 +156,7 @@ function executePromise(classId) { [ t.callExpression( t.memberExpression(classId, t.identifier("_serialize")), - [t.identifier("_result")] + [t.identifier("_result"), t.booleanLiteral(true)] ), ] ) diff --git a/src/collections/lookup-map.ts b/src/collections/lookup-map.ts index b6eb8aef..dcbba40f 100644 --- a/src/collections/lookup-map.ts +++ b/src/collections/lookup-map.ts @@ -1,6 +1,6 @@ import * as near from "../api"; import { GetOptions } from "../types/collections"; -import { Bytes, getValueWithOptions } from "../utils"; +import { Bytes, deserialize, getValueWithOptions, serialize } from "../utils"; export class LookupMap { constructor(readonly keyPrefix: Bytes) {} @@ -12,7 +12,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 = deserialize(near.storageRead(storageKey)); return getValueWithOptions(value, options); } @@ -24,7 +24,7 @@ export class LookupMap { return options?.defaultValue ?? null; } - const value = JSON.parse(near.storageGetEvicted()); + const value = deserialize(near.storageGetEvicted()); return getValueWithOptions(value, options); } @@ -35,13 +35,13 @@ export class LookupMap { options?: GetOptions ): DataType | null { const storageKey = this.keyPrefix + key; - const storageValue = JSON.stringify(newValue); + const storageValue = serialize(newValue); if (!near.storageWrite(storageKey, storageValue)) { return options?.defaultValue ?? null; } - const value = JSON.parse(near.storageGetEvicted()); + const value = deserialize(near.storageGetEvicted()); return getValueWithOptions(value, options); } @@ -56,7 +56,7 @@ export class LookupMap { } serialize(): string { - return JSON.stringify(this); + return serialize(this); } // converting plain object to class object diff --git a/src/collections/lookup-set.ts b/src/collections/lookup-set.ts index 299f4b2e..baa07393 100644 --- a/src/collections/lookup-set.ts +++ b/src/collections/lookup-set.ts @@ -1,24 +1,24 @@ import * as near from "../api"; -import { Bytes } from "../utils"; +import { Bytes, serialize } from "../utils"; export class LookupSet { constructor(readonly keyPrefix: Bytes) {} contains(key: DataType): boolean { - const storageKey = this.keyPrefix + JSON.stringify(key); + const storageKey = this.keyPrefix + serialize(key); 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); + const storageKey = this.keyPrefix + serialize(key); 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); + const storageKey = this.keyPrefix + serialize(key); return !near.storageWrite(storageKey, ""); } @@ -27,7 +27,7 @@ export class LookupSet { } serialize(): string { - return JSON.stringify(this); + return serialize(this); } // converting plain object to class object diff --git a/src/collections/unordered-map.ts b/src/collections/unordered-map.ts index a7e99cc3..9f686fa6 100644 --- a/src/collections/unordered-map.ts +++ b/src/collections/unordered-map.ts @@ -1,4 +1,10 @@ -import { assert, Bytes, getValueWithOptions, Mutable } from "../utils"; +import { + assert, + Bytes, + getValueWithOptions, + Mutable, + serialize, +} from "../utils"; import { Vector, VectorIterator } from "./vector"; import { LookupMap } from "./lookup-map"; import { GetOptions } from "../types/collections"; @@ -128,7 +134,7 @@ export class UnorderedMap { } serialize(): string { - return JSON.stringify(this); + return serialize(this); } // converting plain object to class object diff --git a/src/collections/unordered-set.ts b/src/collections/unordered-set.ts index 3cf535af..edea3454 100644 --- a/src/collections/unordered-set.ts +++ b/src/collections/unordered-set.ts @@ -1,5 +1,11 @@ import * as near from "../api"; -import { u8ArrayToBytes, bytesToU8Array, Bytes, assert } from "../utils"; +import { + u8ArrayToBytes, + bytesToU8Array, + Bytes, + assert, + serialize, +} from "../utils"; import { Vector, VectorIterator } from "./vector"; import { Mutable } from "../utils"; import { GetOptions } from "../types/collections"; @@ -39,12 +45,12 @@ export class UnorderedSet { } contains(element: DataType): boolean { - const indexLookup = this.elementIndexPrefix + JSON.stringify(element); + const indexLookup = this.elementIndexPrefix + serialize(element); return near.storageHasKey(indexLookup); } set(element: DataType): boolean { - const indexLookup = this.elementIndexPrefix + JSON.stringify(element); + const indexLookup = this.elementIndexPrefix + serialize(element); if (!near.storageRead(indexLookup)) { const nextIndex = this.length; @@ -59,7 +65,7 @@ export class UnorderedSet { } remove(element: DataType): boolean { - const indexLookup = this.elementIndexPrefix + JSON.stringify(element); + const indexLookup = this.elementIndexPrefix + serialize(element); const indexRaw = near.storageRead(indexLookup); if (!indexRaw) { @@ -89,7 +95,7 @@ export class UnorderedSet { // reinsert the lookup back. if (lastElement !== element) { const lastLookupElement = - this.elementIndexPrefix + JSON.stringify(lastElement); + this.elementIndexPrefix + serialize(lastElement); near.storageWrite(lastLookupElement, indexRaw); } @@ -101,7 +107,7 @@ export class UnorderedSet { clear(): void { for (const element of this.elements) { - const indexLookup = this.elementIndexPrefix + JSON.stringify(element); + const indexLookup = this.elementIndexPrefix + serialize(element); near.storageRemove(indexLookup); } @@ -139,7 +145,7 @@ export class UnorderedSet { } serialize(): string { - return JSON.stringify(this); + return serialize(this); } // converting plain object to class object diff --git a/src/collections/vector.ts b/src/collections/vector.ts index 836b464f..135db35e 100644 --- a/src/collections/vector.ts +++ b/src/collections/vector.ts @@ -1,5 +1,12 @@ import * as near from "../api"; -import { assert, Bytes, getValueWithOptions, u8ArrayToBytes } from "../utils"; +import { + assert, + Bytes, + deserialize, + getValueWithOptions, + serialize, + u8ArrayToBytes, +} from "../utils"; import { GetOptions } from "../types/collections"; const ERR_INDEX_OUT_OF_BOUNDS = "Index out of bounds"; const ERR_INCONSISTENT_STATE = @@ -29,7 +36,7 @@ export class Vector { return null; } const storageKey = indexToKey(this.prefix, index); - const value = JSON.parse(near.storageRead(storageKey)); + const value = deserialize(near.storageRead(storageKey)); return getValueWithOptions(value, options); } @@ -47,12 +54,9 @@ export class Vector { const key = indexToKey(this.prefix, index); const last = this.pop(); - assert( - near.storageWrite(key, JSON.stringify(last)), - ERR_INCONSISTENT_STATE - ); + assert(near.storageWrite(key, serialize(last)), ERR_INCONSISTENT_STATE); - const value = JSON.parse(near.storageGetEvicted()); + const value = deserialize(near.storageGetEvicted()); return getValueWithOptions(value, options); } @@ -60,7 +64,7 @@ export class Vector { push(element: DataType) { const key = indexToKey(this.prefix, this.length); this.length += 1; - near.storageWrite(key, JSON.stringify(element)); + near.storageWrite(key, serialize(element)); } pop(options?: GetOptions): DataType | null { @@ -74,7 +78,7 @@ export class Vector { assert(near.storageRemove(lastKey), ERR_INCONSISTENT_STATE); - const value = JSON.parse(near.storageGetEvicted()); + const value = deserialize(near.storageGetEvicted()); return getValueWithOptions(value, options); } @@ -87,12 +91,9 @@ export class Vector { 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 - ); + assert(near.storageWrite(key, serialize(element)), ERR_INCONSISTENT_STATE); - const value = JSON.parse(near.storageGetEvicted()); + const value = deserialize(near.storageGetEvicted()); return getValueWithOptions(value, options); } diff --git a/src/near-bindgen.ts b/src/near-bindgen.ts index 770c2301..285a2370 100644 --- a/src/near-bindgen.ts +++ b/src/near-bindgen.ts @@ -1,7 +1,9 @@ import * as near from "./api"; +import { deserialize, serialize } from "./utils"; type EmptyParameterObject = Record; -// type AnyObject = Record; +type AnyObject = Record; +type AnyFunction = (...args: unknown[]) => unknown; // type DecoratorFunction = ( // target: AnyObject, // key: string | symbol, @@ -9,13 +11,13 @@ type EmptyParameterObject = Record; // ) => void; export function initialize(_empty: EmptyParameterObject) { - /* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/ban-types */ + /* eslint-disable @typescript-eslint/no-empty-function */ return function ( - _target: any, + _target: unknown, _key: string | symbol, - _descriptor: TypedPropertyDescriptor + _descriptor: TypedPropertyDescriptor ): void {}; - /* eslint-enable @typescript-eslint/no-empty-function, @typescript-eslint/ban-types */ + /* eslint-enable @typescript-eslint/no-empty-function */ } export function call({ @@ -25,13 +27,11 @@ export function call({ privateFunction?: boolean; payableFunction?: boolean; }) { - /* eslint-disable @typescript-eslint/ban-types */ return function ( - _target: any, + _target: unknown, _key: string | symbol, - descriptor: TypedPropertyDescriptor + descriptor: TypedPropertyDescriptor ): void { - /* eslint-enable @typescript-eslint/ban-types */ const originalMethod = descriptor.value; descriptor.value = function (...args: unknown[]) { @@ -50,13 +50,13 @@ export function call({ } export function view(_empty: EmptyParameterObject) { - /* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/ban-types */ + /* eslint-disable @typescript-eslint/no-empty-function */ return function ( - _target: any, + _target: unknown, _key: string | symbol, - _descriptor: TypedPropertyDescriptor + _descriptor: TypedPropertyDescriptor ): void {}; - /* eslint-enable @typescript-eslint/no-empty-function, @typescript-eslint/ban-types */ + /* eslint-enable @typescript-eslint/no-empty-function */ } export function NearBindgen({ @@ -64,46 +64,49 @@ export function NearBindgen({ }: { requireInit?: boolean; }) { + // 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 serialize(value); } - /* eslint-disable-next-line @typescript-eslint/ban-types */ - static _deserialize(value: string): Object { - return JSON.parse(value); + static _deserialize(value: string): unknown { + return deserialize(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/utils.ts b/src/utils.ts index 6e6ca4d2..09552a22 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,6 +5,9 @@ export type PromiseIndex = number | bigint; export type NearAmount = number | bigint; export type Register = number | bigint; +const BIGINT_KEY = "bigint"; +const BIGINT_BRAND = "tnigib"; + export function u8ArrayToBytes(array: Uint8Array): Bytes { return array.reduce( (result, value) => `${result}${String.fromCharCode(value)}`, @@ -63,3 +66,31 @@ export function getValueWithOptions( return value as DataType; } + +export function serialize(valueToSerialize: unknown): string { + return JSON.stringify(valueToSerialize, (_, value) => { + if (typeof value === "bigint") { + return { + value: value.toString(), + [BIGINT_KEY]: BIGINT_BRAND, + }; + } + + 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", BIGINT_KEY].includes(key)) + ) { + return BigInt(value["value"]); + } + + return value; + }); +} diff --git a/tests/__tests__/test-bigint-serialization.ava.js b/tests/__tests__/test-bigint-serialization.ava.js new file mode 100644 index 00000000..046e05bd --- /dev/null +++ b/tests/__tests__/test-bigint-serialization.ava.js @@ -0,0 +1,49 @@ +import { Worker } from "near-workspaces"; +import test from "ava"; + +test.before(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/package.json b/tests/package.json index 66b60d95..70e680ad 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", "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,7 @@ "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", "test": "ava", "test:context-api": "ava __tests__/test_context_api.ava.js", "test:math-api": "ava __tests__/test_math_api.ava.js", @@ -44,7 +45,8 @@ "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" }, "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 00000000..3f9b4794 --- /dev/null +++ b/tests/src/bigint-serialization.ts @@ -0,0 +1,29 @@ +import { near, NearBindgen, call, view, initialize } from "near-sdk-js"; + +@NearBindgen({}) +class BigIntSerializationTest { + bigintField: bigint; + + constructor() { + this.bigintField = 1n; + } + + @view({}) + getBigintField(): bigint { + near.log(`getStatus: ${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}`); + } +}