diff --git a/AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md b/AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md new file mode 100644 index 00000000..b5a9d97c --- /dev/null +++ b/AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md @@ -0,0 +1,174 @@ +# Auto reconstruct by json schema +## Problem Solved: Could not decode contract state to class instance in early version of sdk +JS SDK decode contract as utf-8 and parse it as JSON, results in a JS Object. +One thing not intuitive is objects are recovered as Object, not class instance. For example, Assume an instance of this class is stored in contract state: +```typescript +Class Car { + name: string; + speed: number; + + run() { + // ... + } +} +``` +When load it back, the SDK gives us something like: +```json +{"name": "Audi", "speed": 200} +``` +However this is a JS Object, not an instance of Car Class, and therefore you cannot call run method on it. +This also applies to when user passes a JSON argument to a contract method. If the contract is written in TypeScript, although it may look like: +```typescript +add_a_car(car: Car) { + car.run(); // doesn't work + this.some_collection.set(car.name, car); +} +``` +But car.run() doesn't work, because SDK only know how to deserialize it as a plain object, not a Car instance. +This problem is particularly painful when class is nested, for example collection class instance LookupMap containing Car class instance. Currently SDK mitigate this problem by requires user to manually reconstruct the JS object to an instance of the original class. +## A method to decode string to class instance by json schema file +we just need to add static member in the class type. +```typescript +Class Car { + static schema = { + name: "string", + speed: "number", + }; + name: string; + speed: number; + + run() { + // ... + } +} +``` +After we add static member in the class type in our smart contract, it will auto reconstruct smart contract and it's member to class instance recursive by sdk. +And we can call class's functions directly after it deserialized. +```js +add_a_car(car: Car) { + car.run(); // it works! + this.some_collection.set(car.name, car); +} +``` +### The schema format +#### We support multiple type in schema: +* build-in non object types: `string`, `number`, `boolean` +* build-in object types: `Date`, `BigInt`. And we can skip those two build-in object types in schema info +* build-in collection types: `array`, `map` + * for `array` type, we need to declare it in the format of `{array: {value: valueType}}` + * for `map` type, we need to declare it in the format of `{map: {key: 'KeyType', value: 'valueType'}}` +* Custom Class types: `Car` or any class types +* Near collection types: `Vector`, `LookupMap`, `LookupSet`, `UnorderedMap`, `UnorderedSet` +We have a test example which contains all those types in one schema: [status-deserialize-class.js](./examples/src/status-deserialize-class.js) +```js +class StatusDeserializeClass { + static schema = { + is_inited: "boolean", + records: {map: {key: 'string', value: 'string'}}, + car: Car, + messages: {array: {value: 'string'}}, + efficient_recordes: {unordered_map: {value: 'string'}}, + nested_efficient_recordes: {unordered_map: {value: {unordered_map: {value: 'string'}}}}, + nested_lookup_recordes: {unordered_map: {value: {lookup_map: {value: 'string'}}}}, + vector_nested_group: {vector: {value: {lookup_map: {value: 'string'}}}}, + lookup_nest_vec: {lookup_map: {value: {vector: {value: 'string'}}}}, + unordered_set: {unordered_set: {value: 'string'}}, + user_car_map: {unordered_map: {value: Car}}, + big_num: 'bigint', + date: 'date' + }; + + constructor() { + this.is_inited = false; + this.records = {}; + this.car = new Car(); + this.messages = []; + // account_id -> message + this.efficient_recordes = new UnorderedMap("a"); + // id -> account_id -> message + this.nested_efficient_recordes = new UnorderedMap("b"); + // id -> account_id -> message + this.nested_lookup_recordes = new UnorderedMap("c"); + // index -> account_id -> message + this.vector_nested_group = new Vector("d"); + // account_id -> index -> message + this.lookup_nest_vec = new LookupMap("e"); + this.unordered_set = new UnorderedSet("f"); + this.user_car_map = new UnorderedMap("g"); + this.big_num = 1n; + this.date = new Date(); + } + // other methods +} +``` +#### Logic of auto reconstruct by json schema +The `_reconstruct` method in [near-bindgen.ts](./packages/near-sdk-js/src/near-bindgen.ts) will check whether there exit a schema in smart contract class, if there exist a static schema info, it will be decoded to class by invoking `decodeObj2class`, or it will fallback to previous behavior: +```typescript + static _reconstruct(classObject: object, plainObject: AnyObject): object { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (classObject.constructor.schema === undefined) { + for (const item in classObject) { + const reconstructor = classObject[item].constructor?.reconstruct; + + classObject[item] = reconstructor + ? reconstructor(plainObject[item]) + : plainObject[item]; + } + + return classObject; + } + + return decodeObj2class(classObject, plainObject); + } +``` +#### no need to announce GetOptions.reconstructor in decoding nested collections +In this other hand, after we set schema for the Near collections with nested collections, we don't need to announce `reconstructor` when we need to get and decode a nested collections because the data type info in the schema will tell sdk what the nested data type. +Before we set schema if we need to get a nested collection we need to set `reconstructor` in `GetOptions`: +```typescript +@NearBindgen({}) +export class Contract { + outerMap: UnorderedMap>; + + constructor() { + this.outerMap = new UnorderedMap("o"); + } + + @view({}) + get({id, accountId}: { id: string; accountId: string }) { + const innerMap = this.outerMap.get(id, { + reconstructor: UnorderedMap.reconstruct, // we need to announce reconstructor explicit + }); + if (innerMap === null) { + return null; + } + return innerMap.get(accountId); + } +} +``` +After we set schema info we don't need to set `reconstructor` in `GetOptions`, sdk can infer which reconstructor should be took by the schema: +```typescript +@NearBindgen({}) +export class Contract { + static schema = { + outerMap: {unordered_map: {value: { unordered_map: {value: 'string'}}}} + }; + + outerMap: UnorderedMap>; + + constructor() { + this.outerMap = new UnorderedMap("o"); + } + + @view({}) + get({id, accountId}: { id: string; accountId: string }) { + const innerMap = this.outerMap.get(id, { + reconstructor: UnorderedMap.reconstruct, // we need to announce reconstructor explicit, reconstructor can be infered from static schema + }); + if (innerMap === null) { + return null; + } + return innerMap.get(accountId); + } +} +``` diff --git a/examples/__tests__/test-status-deserialize-class.ava.js b/examples/__tests__/test-status-deserialize-class.ava.js new file mode 100644 index 00000000..f52d92d2 --- /dev/null +++ b/examples/__tests__/test-status-deserialize-class.ava.js @@ -0,0 +1,169 @@ +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; + + // Deploy the contract. + const statusMessage = await root.devDeploy("./build/status-deserialize-class.wasm"); + + await root.call(statusMessage, "init_contract", {}); + const result = await statusMessage.view("is_contract_inited", {}); + t.is(result, true); + + // Create test users + const ali = await root.createSubAccount("ali"); + const bob = await root.createSubAccount("bob"); + const carl = await root.createSubAccount("carl"); + + // Save state for test runs + t.context.worker = worker; + t.context.accounts = { root, statusMessage, ali, bob, carl }; +}); + +test.after.always(async (t) => { + await t.context.worker.tearDown().catch((error) => { + console.log("Failed to tear down the worker:", error); + }); +}); + +test("Ali sets then gets status", async (t) => { + const { ali, statusMessage } = t.context.accounts; + await ali.call(statusMessage, "set_record", { message: "hello" }); + + t.is( + await statusMessage.view("get_record", { account_id: ali.accountId }), + "hello" + ); +}); + +test("Ali set_truck_info and get_truck_info", async (t) => { + const { ali, statusMessage } = t.context.accounts; + let carName = "Mercedes-Benz"; + let speed = 240; + await ali.call(statusMessage, "set_truck_info", { name: carName, speed: speed }); + + await ali.call(statusMessage, "add_truck_load", { name: "alice", load: "a box" }); + await ali.call(statusMessage, "add_truck_load", { name: "bob", load: "a packet" }); + + t.is( + await statusMessage.view("get_truck_info", { }), + carName + " run with speed " + speed + " with loads length: 2" + ); + + t.is( + await statusMessage.view("get_user_car_info", { account_id: ali.accountId }), + carName + " run with speed " + speed + ); +}); + +test("Ali push_message and get_messages", async (t) => { + const { ali, statusMessage } = t.context.accounts; + let message1 = 'Hello'; + let message2 = 'World'; + await ali.call(statusMessage, "push_message", { message: message1 }); + await ali.call(statusMessage, "push_message", { message: message2 }); + + t.is( + await statusMessage.view("get_messages", { }), + 'Hello,World' + ); +}); + +test("Ali set_nested_efficient_recordes then get_nested_efficient_recordes text", async (t) => { + const { ali, bob, statusMessage } = t.context.accounts; + await ali.call(statusMessage, "set_nested_efficient_recordes", { id: "1", message: "hello" }, { gas: 35_000_000_000_000n }); + await bob.call(statusMessage, "set_nested_efficient_recordes", { id: "1", message: "hello" }, { gas: 35_000_000_000_000n }); + await bob.call(statusMessage, "set_nested_efficient_recordes", { id: "2", message: "world" }, { gas: 35_000_000_000_000n }); + + t.is( + await statusMessage.view("get_efficient_recordes", { account_id: ali.accountId }), + "hello" + ); + + t.is( + await statusMessage.view("get_nested_efficient_recordes", { id: "1", account_id: bob.accountId }), + "hello" + ); + + t.is( + await statusMessage.view("get_nested_efficient_recordes", { id: "2", account_id: bob.accountId }), + "world" + ); + + t.is( + await statusMessage.view("get_nested_lookup_recordes", { id: "1", account_id: bob.accountId }), + "hello" + ); + + t.is( + await statusMessage.view("get_nested_lookup_recordes", { id: "2", account_id: bob.accountId }), + "world" + ); + + t.is( + await statusMessage.view("get_vector_nested_group", { idx: 0, account_id: bob.accountId }), + "world" + ); + + t.is( + await statusMessage.view("get_lookup_nested_vec", { account_id: bob.accountId, idx: 1 }), + "world" + ); + + t.is( + await statusMessage.view("get_is_contains_user", { account_id: bob.accountId}), + true + ); +}); + +test("Ali set_big_num_and_date then gets", async (t) => { + const { ali, bob, statusMessage } = t.context.accounts; + await ali.call(statusMessage, "set_big_num_and_date", { bigint_num: `${10n}`, new_date: new Date('August 19, 2023 23:15:30 GMT+00:00') }); + + + const afterSetNum = await statusMessage.view("get_big_num", { }); + t.is(afterSetNum, `${10n}`); + const afterSetDate = await statusMessage.view("get_date", { }); + t.is(afterSetDate.toString(), '2023-08-19T23:15:30.000Z'); +}); + +test("Ali set_extra_data without schema defined then gets", async (t) => { + const { ali, statusMessage } = t.context.accounts; + await ali.call(statusMessage, "set_extra_data", { message: "Hello world!", number: 100 }); + + const messageWithoutSchemaDefined = await statusMessage.view("get_extra_msg", { }); + t.is(messageWithoutSchemaDefined, "Hello world!"); + const numberWithoutSchemaDefined = await statusMessage.view("get_extra_number", { }); + t.is(numberWithoutSchemaDefined, 100); +}); + +test("Ali set_extra_record without schema defined then gets", async (t) => { + const { ali, statusMessage } = t.context.accounts; + await ali.call(statusMessage, "set_extra_record", { message: "Hello world!"}); + + const recordWithoutSchemaDefined = await statusMessage.view("get_extra_record", { account_id: ali.accountId }); + t.is(recordWithoutSchemaDefined, "Hello world!"); +}); + +test("View get_subtype_of_efficient_recordes", async (t) => { + const { statusMessage } = t.context.accounts; + + t.is( + await statusMessage.view("get_subtype_of_efficient_recordes", { }), + 'string' + ); +}); + +test("View get_subtype_of_nested_efficient_recordes", async (t) => { + const { statusMessage } = t.context.accounts; + + t.is( + JSON.stringify(await statusMessage.view("get_subtype_of_nested_efficient_recordes", { })), + '{"collection":{"value":"string"}}' + ); +}); \ No newline at end of file diff --git a/examples/package.json b/examples/package.json index 9aa43ea6..be7e6309 100644 --- a/examples/package.json +++ b/examples/package.json @@ -32,6 +32,7 @@ "build:state-migration": "run-s build:state-migration:*", "build:state-migration:original": "near-sdk-js build src/state-migration-original.ts build/state-migration-original.wasm", "build:state-migration:new": "near-sdk-js build src/state-migration-new.ts build/state-migration-new.wasm", + "build:status-deserialize-class": "near-sdk-js build src/status-deserialize-class.js build/status-deserialize-class.wasm", "test": "ava && pnpm test:counter-lowlevel && pnpm test:counter-ts", "test:nft": "ava __tests__/standard-nft/*", "test:ft": "ava __tests__/standard-ft/*", @@ -53,7 +54,8 @@ "test:nested-collections": "ava __tests__/test-nested-collections.ava.js", "test:status-message-borsh": "ava __tests__/test-status-message-borsh.ava.js", "test:status-message-serialize-err": "ava __tests__/test-status-message-serialize-err.ava.js", - "test:status-message-deserialize-err": "ava __tests__/test-status-message-deserialize-err.ava.js" + "test:status-message-deserialize-err": "ava __tests__/test-status-message-deserialize-err.ava.js", + "test:status-deserialize-class": "ava __tests__/test-status-deserialize-class.ava.js" }, "author": "Near Inc ", "license": "Apache-2.0", diff --git a/examples/src/status-deserialize-class.js b/examples/src/status-deserialize-class.js new file mode 100644 index 00000000..270cd1b8 --- /dev/null +++ b/examples/src/status-deserialize-class.js @@ -0,0 +1,312 @@ +import { + NearBindgen, + call, + view, + near, + UnorderedMap, + LookupMap, + Vector, + UnorderedSet, +} from "near-sdk-js"; + +class Car { + static schema = { + name: "string", + speed: "number", + }; + constructor() { + this.name = ""; + this.speed = 0; + } + + info() { + return this.name + " run with speed " + this.speed.toString() + } +} + +class Truck { + static schema = { + name: "string", + speed: "number", + loads: {collection: {reconstructor: UnorderedMap.reconstruct, value: 'string'}} + }; + constructor() { + this.name = ""; + this.speed = 0; + this.loads = new UnorderedMap("tra"); + } + + info() { + return this.name + " run with speed " + this.speed.toString() + " with loads length: " + this.loads.toArray().length; + } +} + +@NearBindgen({}) +export class StatusDeserializeClass { + static schema = { + is_inited: "boolean", + records: {map: { key: 'string', value: 'string' }}, + truck: Truck, + messages: {array: {value: 'string'}}, + efficient_recordes: {collection: {reconstructor: UnorderedMap.reconstruct, value: 'string'}}, + nested_efficient_recordes: {collection: {reconstructor: UnorderedMap.reconstruct, value: { collection: {reconstructor: UnorderedMap.reconstruct, value: 'string'}}}}, + nested_lookup_recordes: {collection: {reconstructor: UnorderedMap.reconstruct, value: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}}}}, + vector_nested_group: {collection: {reconstructor: Vector.reconstruct, value: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}}}}, + lookup_nest_vec: {collection: {reconstructor: LookupMap.reconstruct, value: { collection: { reconstructor: Vector.reconstruct, value: 'string' }}}}, + unordered_set: {collection: {reconstructor: UnorderedSet.reconstruct, value: 'string'}}, + user_car_map: {collection: {reconstructor: UnorderedMap.reconstruct, value: Car }}, + big_num: 'bigint', + date: 'date' + }; + constructor() { + this.is_inited = false; + this.records = {}; + this.truck = new Truck(); + this.messages = []; + // account_id -> message + this.efficient_recordes = new UnorderedMap("a"); + // id -> account_id -> message + this.nested_efficient_recordes = new UnorderedMap("b"); + // id -> account_id -> message + this.nested_lookup_recordes = new UnorderedMap("c"); + // index -> account_id -> message + this.vector_nested_group = new Vector("d"); + // account_id -> index -> message + this.lookup_nest_vec = new LookupMap("e"); + this.unordered_set = new UnorderedSet("f"); + this.user_car_map = new UnorderedMap("g"); + this.big_num = 1n; + this.date = new Date(); + this.message_without_schema_defined = ""; + this.number_without_schema_defined = 0; + this.records_without_schema_defined = {}; + } + + @call({}) + init_contract({ }) { + if (this.is_inited) { + near.log(`message inited`); + return; + } + this.is_inited = true; + } + + @view({}) + is_contract_inited({}) { + near.log(`query is_contract_inited`); + return this.is_inited; + } + + @call({}) + set_record({ message }) { + let account_id = near.signerAccountId(); + near.log(`${account_id} set_status with message ${message}`); + this.records[account_id] = message; + } + + @view({}) + get_record({ account_id }) { + near.log(`get_record for account_id ${account_id}`); + return this.records[account_id] || null; + } + + + @call({}) + set_truck_info({ name, speed }) { + let account_id = near.signerAccountId(); + near.log(`${account_id} set_truck_info name ${name}, speed ${speed}`); + let truck = new Truck(); + truck.name = name; + truck.speed = speed; + truck.loads = this.truck.loads; + this.truck = truck; + let car = new Car(); + car.name = name; + car.speed = speed; + this.user_car_map.set(account_id, car); + } + + @call({}) + add_truck_load({ name, load }) { + let account_id = near.signerAccountId(); + near.log(`${account_id} add_truck_load name ${name}, load ${load}`); + this.truck.loads.set(name, load); + } + + @view({}) + get_truck_info({ }) { + near.log(`get_truck_info`); + return this.truck.info(); + } + + @view({}) + get_user_car_info({ account_id }) { + near.log(`get_user_car_info for account_id ${account_id}`); + let car = this.user_car_map.get(account_id); + if (car == null) { + return null; + } + return car.info(); + } + + @call({}) + push_message({ message }) { + let account_id = near.signerAccountId(); + near.log(`${account_id} push_message message ${message}`); + this.messages.push(message); + } + + @view({}) + get_messages({ }) { + near.log(`get_messages`); + return this.messages.join(','); + } + + @call({}) + set_nested_efficient_recordes({ message, id }) { + let account_id = near.signerAccountId(); + near.log(`${account_id} set_nested_efficient_recordes with message ${message},id ${id}`); + this.efficient_recordes.set(account_id, message); + const nestedMap = this.nested_efficient_recordes.get(id, { + defaultValue: new UnorderedMap("i_" + id + "_"), + }); + nestedMap.set(account_id, message); + this.nested_efficient_recordes.set(id, nestedMap); + + const nestedLookup = this.nested_lookup_recordes.get(id, { + defaultValue: new LookupMap("li_" + id + "_"), + }); + nestedLookup.set(account_id, message); + this.nested_lookup_recordes.set(id, nestedLookup); + + // vector_nested_group: {vector: {value: { lookup_map: {value: 'string'}}}}, + const vecNestedLookup = this.vector_nested_group.get(0, { + defaultValue: new LookupMap("di_0_"), + }); + vecNestedLookup.set(account_id, message); + if (this.vector_nested_group.isEmpty()) { + this.vector_nested_group.push(vecNestedLookup); + } else { + this.vector_nested_group.replace(0, vecNestedLookup); + } + + const lookupNestVec = this.lookup_nest_vec.get(account_id, { + defaultValue: new Vector("ei_" + account_id + "_"), + }); + lookupNestVec.push(message); + this.lookup_nest_vec.set(account_id, lookupNestVec); + + this.unordered_set.set(account_id); + } + + @call({}) + set_big_num_and_date({ bigint_num, new_date }) { + let account_id = near.signerAccountId(); + near.log(`${account_id} set_bigint_and_date bigint_num ${bigint_num}, new_date: ${new_date}`); + this.big_num = bigint_num; + this.date = new_date; + } + + @view({}) + get_big_num({ }) { + near.log(`get_big_num`); + return this.big_num; + } + + @view({}) + get_date({ }) { + near.log(`get_date`); + return this.date; + } + + @view({}) + get_efficient_recordes({ account_id }) { + near.log(`get_efficient_recordes for account_id ${account_id}`); + return this.efficient_recordes.get(account_id); + } + + @view({}) + get_nested_efficient_recordes({ account_id, id }) { + near.log(`get_nested_efficient_recordes for account_id ${account_id}, id ${id}`); + return this.nested_efficient_recordes.get(id, { + defaultValue: new UnorderedMap("i_" + id + "_"), + }).get(account_id); + } + + @view({}) + get_nested_lookup_recordes({ account_id, id }) { + near.log(`get_nested_lookup_recordes for account_id ${account_id}, id ${id}`); + return this.nested_lookup_recordes.get(id, { + defaultValue: new LookupMap("li_" + id + "_"), + }).get(account_id); + } + + @view({}) + get_vector_nested_group({ idx, account_id }) { + near.log(`get_vector_nested_group for idx ${idx}, account_id ${account_id}`); + return this.vector_nested_group.get(idx).get(account_id); + } + + @view({}) + get_lookup_nested_vec({ account_id, idx }) { + near.log(`get_looup_nested_vec for account_id ${account_id}, idx ${idx}`); + return this.lookup_nest_vec.get(account_id).get(idx); + } + + @view({}) + get_is_contains_user({ account_id }) { + near.log(`get_is_contains_user for account_id ${account_id}`); + return this.unordered_set.contains(account_id); + } + + @call({}) + set_extra_data({ message, number }) { + let account_id = near.signerAccountId(); + near.log(`${account_id} set_extra_data message ${message}, number: ${number}`); + this.message_without_schema_defined = message; + this.number_without_schema_defined = number; + } + + @view({}) + get_extra_msg({ }) { + near.log(`get_extra_msg`); + return this.message_without_schema_defined; + } + + @view({}) + get_extra_number({ }) { + near.log(`get_extra_number`); + return this.number_without_schema_defined; + } + + @call({}) + set_extra_record({ message }) { + let account_id = near.signerAccountId(); + near.log(`${account_id} set_extra_record with message ${message}`); + this.records_without_schema_defined[account_id] = message; + } + + @view({}) + get_extra_record({ account_id }) { + near.log(`get_extra_record for account_id ${account_id}`); + return this.records_without_schema_defined[account_id] || null; + } + + @view({}) + get_subtype_of_efficient_recordes({ }) { + near.log(`get_subtype_of_efficient_recordes`); + return this.efficient_recordes.subtype(); + } + + @view({}) + get_subtype_of_nested_efficient_recordes({ }) { + near.log(`get_subtype_of_nested_efficient_recordes`); + return this.nested_efficient_recordes.subtype(); + } + + @view({}) + get_subtype_of_nested_lookup_recordes({ }) { + near.log(`get_subtype_of_nested_lookup_recordes`); + return this.nested_lookup_recordes.subtype(); + } +} diff --git a/packages/near-contract-standards/lib/non_fungible_token/impl.js b/packages/near-contract-standards/lib/non_fungible_token/impl.js index 5f2634e1..043d6ce8 100644 --- a/packages/near-contract-standards/lib/non_fungible_token/impl.js +++ b/packages/near-contract-standards/lib/non_fungible_token/impl.js @@ -3,9 +3,9 @@ import { TokenMetadata } from "./metadata"; import { refund_storage_deposit, refund_deposit, refund_deposit_to_account, assert_at_least_one_yocto, assert_one_yocto, } from "./utils"; import { NftMint, NftTransfer } from "./events"; import { Token } from "./token"; -const GAS_FOR_RESOLVE_TRANSFER = 15000000000000n; +const GAS_FOR_RESOLVE_TRANSFER = 16000000000000n; const GAS_FOR_NFT_TRANSFER_CALL = 30000000000000n + GAS_FOR_RESOLVE_TRANSFER; -const GAS_FOR_NFT_APPROVE = 20000000000000n; +const GAS_FOR_NFT_APPROVE = 21000000000000n; function repeat(str, n) { return Array(n + 1).join(str); } diff --git a/packages/near-contract-standards/src/non_fungible_token/impl.ts b/packages/near-contract-standards/src/non_fungible_token/impl.ts index c66b7835..20302b7f 100644 --- a/packages/near-contract-standards/src/non_fungible_token/impl.ts +++ b/packages/near-contract-standards/src/non_fungible_token/impl.ts @@ -27,10 +27,10 @@ import { NonFungibleTokenCore } from "./core"; import { NonFungibleTokenApproval } from "./approval"; import { NonFungibleTokenEnumeration } from "./enumeration"; -const GAS_FOR_RESOLVE_TRANSFER = 15_000_000_000_000n; +const GAS_FOR_RESOLVE_TRANSFER = 16_000_000_000_000n; const GAS_FOR_NFT_TRANSFER_CALL = 30_000_000_000_000n + GAS_FOR_RESOLVE_TRANSFER; -const GAS_FOR_NFT_APPROVE = 20_000_000_000_000n; +const GAS_FOR_NFT_APPROVE = 21_000_000_000_000n; function repeat(str: string, n: number) { return Array(n + 1).join(str); diff --git a/packages/near-sdk-js/lib/collections/index.d.ts b/packages/near-sdk-js/lib/collections/index.d.ts index 75f12247..1e590180 100644 --- a/packages/near-sdk-js/lib/collections/index.d.ts +++ b/packages/near-sdk-js/lib/collections/index.d.ts @@ -3,3 +3,4 @@ export * from "./lookup-set"; export * from "./unordered-map"; export * from "./unordered-set"; export * from "./vector"; +export * from "./subtype"; diff --git a/packages/near-sdk-js/lib/collections/index.js b/packages/near-sdk-js/lib/collections/index.js index 75f12247..1e590180 100644 --- a/packages/near-sdk-js/lib/collections/index.js +++ b/packages/near-sdk-js/lib/collections/index.js @@ -3,3 +3,4 @@ export * from "./lookup-set"; export * from "./unordered-map"; export * from "./unordered-set"; export * from "./vector"; +export * from "./subtype"; diff --git a/packages/near-sdk-js/lib/collections/lookup-map.d.ts b/packages/near-sdk-js/lib/collections/lookup-map.d.ts index 4bd45f5e..36be477a 100644 --- a/packages/near-sdk-js/lib/collections/lookup-map.d.ts +++ b/packages/near-sdk-js/lib/collections/lookup-map.d.ts @@ -1,8 +1,9 @@ import { GetOptions } from "../types/collections"; +import { SubType } from "./subtype"; /** * A lookup map that stores data in NEAR storage. */ -export declare class LookupMap { +export declare class LookupMap extends SubType { readonly keyPrefix: string; /** * @param keyPrefix - The byte prefix to use when storing elements inside this collection. diff --git a/packages/near-sdk-js/lib/collections/lookup-map.js b/packages/near-sdk-js/lib/collections/lookup-map.js index 364049a6..0a5548eb 100644 --- a/packages/near-sdk-js/lib/collections/lookup-map.js +++ b/packages/near-sdk-js/lib/collections/lookup-map.js @@ -1,13 +1,15 @@ import * as near from "../api"; import { getValueWithOptions, serializeValueWithOptions, encode, } from "../utils"; +import { SubType } from "./subtype"; /** * A lookup map that stores data in NEAR storage. */ -export class LookupMap { +export class LookupMap extends SubType { /** * @param keyPrefix - The byte prefix to use when storing elements inside this collection. */ constructor(keyPrefix) { + super(); this.keyPrefix = keyPrefix; } /** @@ -28,7 +30,11 @@ export class LookupMap { get(key, options) { const storageKey = this.keyPrefix + key; const value = near.storageReadRaw(encode(storageKey)); - return getValueWithOptions(value, options); + if (options == undefined) { + options = {}; + } + options = this.set_reconstructor(options); + return getValueWithOptions(this.subtype(), value, options); } /** * Removes and retrieves the element with the provided key. @@ -42,7 +48,7 @@ export class LookupMap { return options?.defaultValue ?? null; } const value = near.storageGetEvictedRaw(); - return getValueWithOptions(value, options); + return getValueWithOptions(this.subtype(), value, options); } /** * Store a new value at the provided key. @@ -58,7 +64,7 @@ export class LookupMap { return options?.defaultValue ?? null; } const value = near.storageGetEvictedRaw(); - return getValueWithOptions(value, options); + return getValueWithOptions(this.subtype(), value, options); } /** * Extends the current collection with the passed in array of key-value pairs. diff --git a/packages/near-sdk-js/lib/collections/subtype.d.ts b/packages/near-sdk-js/lib/collections/subtype.d.ts new file mode 100644 index 00000000..b43bf1cf --- /dev/null +++ b/packages/near-sdk-js/lib/collections/subtype.d.ts @@ -0,0 +1,5 @@ +import { GetOptions } from "../types/collections"; +export declare abstract class SubType { + subtype(): any; + set_reconstructor(options?: Omit, "serializer">): Omit, "serializer">; +} diff --git a/packages/near-sdk-js/lib/collections/subtype.js b/packages/near-sdk-js/lib/collections/subtype.js new file mode 100644 index 00000000..af87b68c --- /dev/null +++ b/packages/near-sdk-js/lib/collections/subtype.js @@ -0,0 +1,22 @@ +export class SubType { + /* eslint-disable @typescript-eslint/no-explicit-any */ + /* eslint-disable @typescript-eslint/no-empty-function */ + subtype() { } + set_reconstructor(options) { + if (options == undefined) { + options = {}; + } + const subtype = this.subtype(); + if (options.reconstructor == undefined && + subtype != undefined && + // eslint-disable-next-line no-prototype-builtins + subtype.hasOwnProperty("collection") && + typeof this.subtype().collection.reconstructor === "function") { + // { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + options.reconstructor = this.subtype().collection.reconstructor; + } + return options; + } +} diff --git a/packages/near-sdk-js/lib/collections/unordered-map.d.ts b/packages/near-sdk-js/lib/collections/unordered-map.d.ts index af50e1ae..21e3293c 100644 --- a/packages/near-sdk-js/lib/collections/unordered-map.d.ts +++ b/packages/near-sdk-js/lib/collections/unordered-map.d.ts @@ -1,11 +1,12 @@ import { Vector } from "./vector"; import { LookupMap } from "./lookup-map"; import { GetOptions } from "../types/collections"; +import { SubType } from "./subtype"; declare type ValueAndIndex = [value: string, index: number]; /** * An unordered map that stores data in NEAR storage. */ -export declare class UnorderedMap { +export declare class UnorderedMap extends SubType { readonly prefix: string; readonly _keys: Vector; readonly values: LookupMap; @@ -95,6 +96,7 @@ declare class UnorderedMapIterator { * @param options - Options for retrieving and storing data. */ constructor(unorderedMap: UnorderedMap, options?: GetOptions); + subtype(): any; next(): { value: [string | null, DataType | null]; done: boolean; diff --git a/packages/near-sdk-js/lib/collections/unordered-map.js b/packages/near-sdk-js/lib/collections/unordered-map.js index f155eae2..19a57b77 100644 --- a/packages/near-sdk-js/lib/collections/unordered-map.js +++ b/packages/near-sdk-js/lib/collections/unordered-map.js @@ -1,14 +1,16 @@ import { assert, ERR_INCONSISTENT_STATE, getValueWithOptions, serializeValueWithOptions, encode, decode, } from "../utils"; import { Vector, VectorIterator } from "./vector"; import { LookupMap } from "./lookup-map"; +import { SubType } from "./subtype"; /** * An unordered map that stores data in NEAR storage. */ -export class UnorderedMap { +export class UnorderedMap extends SubType { /** * @param prefix - The byte prefix to use when storing elements inside this collection. */ constructor(prefix) { + super(); this.prefix = prefix; this._keys = new Vector(`${prefix}u`); // intentional different prefix with old UnorderedMap this.values = new LookupMap(`${prefix}m`); @@ -36,8 +38,9 @@ export class UnorderedMap { if (valueAndIndex === null) { return options?.defaultValue ?? null; } + options = this.set_reconstructor(options); const [value] = valueAndIndex; - return getValueWithOptions(encode(value), options); + return getValueWithOptions(this.subtype(), encode(value), options); } /** * Store a new value at the provided key. @@ -57,7 +60,7 @@ export class UnorderedMap { } const [oldValue, oldIndex] = valueAndIndex; this.values.set(key, [decode(serialized), oldIndex]); - return getValueWithOptions(encode(oldValue), options); + return getValueWithOptions(this.subtype(), encode(oldValue), options); } /** * Removes and retrieves the element with the provided key. @@ -80,7 +83,7 @@ export class UnorderedMap { assert(swappedValueAndIndex !== null, ERR_INCONSISTENT_STATE); this.values.set(swappedKey, [swappedValueAndIndex[0], index]); } - return getValueWithOptions(encode(value), options); + return getValueWithOptions(this.subtype(), encode(value), options); } /** * Remove all of the elements stored within the collection. @@ -176,7 +179,11 @@ class UnorderedMapIterator { this.options = options; this.keys = new VectorIterator(unorderedMap._keys); this.map = unorderedMap.values; + this.subtype = unorderedMap.subtype; } + /* eslint-disable @typescript-eslint/no-explicit-any */ + /* eslint-disable @typescript-eslint/no-empty-function */ + subtype() { } next() { const key = this.keys.next(); if (key.done) { @@ -188,7 +195,7 @@ class UnorderedMapIterator { done: key.done, value: [ key.value, - getValueWithOptions(encode(valueAndIndex[0]), this.options), + getValueWithOptions(this.subtype(), encode(valueAndIndex[0]), this.options), ], }; } diff --git a/packages/near-sdk-js/lib/collections/vector.d.ts b/packages/near-sdk-js/lib/collections/vector.d.ts index 5261f78d..ade162e6 100644 --- a/packages/near-sdk-js/lib/collections/vector.d.ts +++ b/packages/near-sdk-js/lib/collections/vector.d.ts @@ -1,9 +1,10 @@ import { GetOptions } from "../types/collections"; +import { SubType } from "./subtype"; /** * An iterable implementation of vector that stores its content on the trie. * Uses the following map: index -> element */ -export declare class Vector { +export declare class Vector extends SubType { readonly prefix: string; length: number; /** diff --git a/packages/near-sdk-js/lib/collections/vector.js b/packages/near-sdk-js/lib/collections/vector.js index 79bc8b85..80eb9a54 100644 --- a/packages/near-sdk-js/lib/collections/vector.js +++ b/packages/near-sdk-js/lib/collections/vector.js @@ -1,5 +1,6 @@ import * as near from "../api"; import { assert, getValueWithOptions, serializeValueWithOptions, ERR_INCONSISTENT_STATE, ERR_INDEX_OUT_OF_BOUNDS, str, bytes, } from "../utils"; +import { SubType } from "./subtype"; function indexToKey(prefix, index) { const data = new Uint32Array([index]); const array = new Uint8Array(data.buffer); @@ -10,12 +11,13 @@ 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 { +export class Vector extends SubType { /** * @param prefix - The byte prefix to use when storing elements inside this collection. * @param length - The initial length of the collection. By default 0. */ constructor(prefix, length = 0) { + super(); this.prefix = prefix; this.length = length; } @@ -37,7 +39,8 @@ export class Vector { } const storageKey = indexToKey(this.prefix, index); const value = near.storageReadRaw(bytes(storageKey)); - return getValueWithOptions(value, options); + options = this.set_reconstructor(options); + return getValueWithOptions(this.subtype(), value, options); } /** * Removes an element from the vector and returns it in serialized form. @@ -56,7 +59,8 @@ export class Vector { const last = this.pop(options); assert(near.storageWriteRaw(bytes(key), serializeValueWithOptions(last, options)), ERR_INCONSISTENT_STATE); const value = near.storageGetEvictedRaw(); - return getValueWithOptions(value, options); + options = this.set_reconstructor(options); + return getValueWithOptions(this.subtype(), value, options); } /** * Adds data to the collection. @@ -83,7 +87,7 @@ export class Vector { this.length -= 1; assert(near.storageRemoveRaw(bytes(lastKey)), ERR_INCONSISTENT_STATE); const value = near.storageGetEvictedRaw(); - return getValueWithOptions(value, options); + return getValueWithOptions(this.subtype(), value, options); } /** * Replaces the data stored at the provided index with the provided data and returns the previously stored data. @@ -97,7 +101,8 @@ export class Vector { const key = indexToKey(this.prefix, index); assert(near.storageWriteRaw(bytes(key), serializeValueWithOptions(element, options)), ERR_INCONSISTENT_STATE); const value = near.storageGetEvictedRaw(); - return getValueWithOptions(value, options); + options = this.set_reconstructor(options); + return getValueWithOptions(this.subtype(), value, options); } /** * Extends the current collection with the passed in array of elements. diff --git a/packages/near-sdk-js/lib/near-bindgen.js b/packages/near-sdk-js/lib/near-bindgen.js index a7b4792f..2cf8fc53 100644 --- a/packages/near-sdk-js/lib/near-bindgen.js +++ b/packages/near-sdk-js/lib/near-bindgen.js @@ -1,5 +1,5 @@ import * as near from "./api"; -import { deserialize, serialize, bytes, encode } from "./utils"; +import { deserialize, serialize, bytes, encode, decodeObj2class, } from "./utils"; /** * Tells the SDK to use this function as the migration function of the contract. * The migration function will ignore te existing state. @@ -101,13 +101,18 @@ export function NearBindgen({ requireInit = false, serializer = serialize, deser return deserializer(value); } static _reconstruct(classObject, plainObject) { - for (const item in classObject) { - const reconstructor = classObject[item].constructor?.reconstruct; - classObject[item] = reconstructor - ? reconstructor(plainObject[item]) - : plainObject[item]; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (classObject.constructor.schema === undefined) { + for (const item in classObject) { + const reconstructor = classObject[item].constructor?.reconstruct; + classObject[item] = reconstructor + ? reconstructor(plainObject[item]) + : plainObject[item]; + } + return classObject; } - return classObject; + return decodeObj2class(classObject, plainObject); } static _requireInit() { return requireInit; diff --git a/packages/near-sdk-js/lib/utils.d.ts b/packages/near-sdk-js/lib/utils.d.ts index 3c53e54d..805f92b7 100644 --- a/packages/near-sdk-js/lib/utils.d.ts +++ b/packages/near-sdk-js/lib/utils.d.ts @@ -39,10 +39,11 @@ export declare function assert(expression: unknown, message: string): asserts ex export declare type Mutable = { -readonly [P in keyof T]: T[P]; }; -export declare function getValueWithOptions(value: Uint8Array | null, options?: Omit, "serializer">): DataType | null; +export declare function getValueWithOptions(subDatatype: unknown, value: Uint8Array | null, options?: Omit, "serializer">): DataType | null; export declare function serializeValueWithOptions(value: DataType, { serializer }?: Pick, "serializer">): Uint8Array; export declare function serialize(valueToSerialize: unknown): Uint8Array; export declare function deserialize(valueToDeserialize: Uint8Array): unknown; +export declare function decodeObj2class(class_instance: any, obj: any): any; /** * Validates the Account ID according to the NEAR protocol * [Account ID rules](https://nomicon.io/DataStructures/Account#account-id-rules). diff --git a/packages/near-sdk-js/lib/utils.js b/packages/near-sdk-js/lib/utils.js index fd810fc7..45e34e83 100644 --- a/packages/near-sdk-js/lib/utils.js +++ b/packages/near-sdk-js/lib/utils.js @@ -1,3 +1,4 @@ +import { cloneDeep } from "lodash-es"; // make PromiseIndex a nominal typing var PromiseIndexBrand; (function (PromiseIndexBrand) { @@ -35,18 +36,69 @@ export function assert(expression, message) { throw new Error("assertion failed: " + message); } } -export function getValueWithOptions(value, options = { +export function getValueWithOptions(subDatatype, value, options = { deserializer: deserialize, }) { if (value === null) { return options?.defaultValue ?? null; } - const deserialized = deserialize(value); + // here is an obj + let deserialized = deserialize(value); if (deserialized === undefined || deserialized === null) { return options?.defaultValue ?? null; } if (options?.reconstructor) { - return options.reconstructor(deserialized); + // example: // { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}} + const collection = options.reconstructor(deserialized); + if (subDatatype !== undefined && + // eslint-disable-next-line no-prototype-builtins + subDatatype.hasOwnProperty("collection") && + // eslint-disable-next-line no-prototype-builtins + subDatatype["collection"].hasOwnProperty("value")) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + collection.subtype = function () { + // example: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}} + return subDatatype["collection"]["value"]; + }; + } + return collection; + } + // example: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}} + if (subDatatype !== undefined) { + // subtype info is a class constructor, Such as Car + if (typeof subDatatype === "function") { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + deserialized = decodeObj2class(new subDatatype(), deserialized); + } + else if (typeof subDatatype === "object") { + // normal collections of array, map; subtype will be: + // {map: { key: 'string', value: 'string' }} or {array: {value: 'string'}} .. + // eslint-disable-next-line no-prototype-builtins + if (subDatatype.hasOwnProperty("map")) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + for (const mkey in deserialized) { + if (subDatatype["map"]["value"] !== "string") { + deserialized[mkey] = decodeObj2class(new subDatatype["map"]["value"](), value[mkey]); + } + } + // eslint-disable-next-line no-prototype-builtins + } + else if (subDatatype.hasOwnProperty("array")) { + const new_vec = []; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + for (const k in deserialized) { + if (subDatatype["array"]["value"] !== "string") { + new_vec.push(decodeObj2class(new subDatatype["array"]["value"](), value[k])); + } + } + deserialized = new_vec; + // eslint-disable-next-line no-prototype-builtins + } + } } return deserialized; } @@ -90,6 +142,71 @@ export function deserialize(valueToDeserialize) { return value; }); } +export function decodeObj2class(class_instance, obj) { + if (typeof obj != "object" || + class_instance.constructor.schema === undefined) { + return obj; + } + let key; + for (key in obj) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const value = obj[key]; + if (typeof value == "object") { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const ty = class_instance.constructor.schema[key]; + // eslint-disable-next-line no-prototype-builtins + if (ty !== undefined && ty.hasOwnProperty("map")) { + for (const mkey in value) { + if (ty["map"]["value"] === "string") { + class_instance[key][mkey] = value[mkey]; + } + else { + class_instance[key][mkey] = decodeObj2class(new ty["map"]["value"](), value[mkey]); + } + } + // eslint-disable-next-line no-prototype-builtins + } + else if (ty !== undefined && ty.hasOwnProperty("array")) { + for (const k in value) { + if (ty["array"]["value"] === "string") { + class_instance[key].push(value[k]); + } + else { + class_instance[key].push(decodeObj2class(new ty["array"]["value"](), value[k])); + } + } + // eslint-disable-next-line no-prototype-builtins + } + else if (ty !== undefined && ty.hasOwnProperty("collection")) { + // nested_lookup_recordes: {collection: {reconstructor: UnorderedMap.reconstruct, value: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}}}}, + // {collection: {reconstructor: + class_instance[key] = ty["collection"]["reconstructor"](obj[key]); + const subtype_value = ty["collection"]["value"]; + class_instance[key].subtype = function () { + // example: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}} + return subtype_value; + }; + } + else { + // normal case with nested Class, such as field is truck: Truck, + class_instance[key] = decodeObj2class(class_instance[key], obj[key]); + } + } + else { + class_instance[key] = obj[key]; + } + } + const instance_tmp = cloneDeep(class_instance); + for (key in obj) { + if (typeof class_instance[key] == "object" && + !(class_instance[key] instanceof Date)) { + class_instance[key] = instance_tmp[key]; + } + } + return class_instance; +} /** * Validates the Account ID according to the NEAR protocol * [Account ID rules](https://nomicon.io/DataStructures/Account#account-id-rules). diff --git a/packages/near-sdk-js/package.json b/packages/near-sdk-js/package.json index 8cb25eae..3443c5df 100644 --- a/packages/near-sdk-js/package.json +++ b/packages/near-sdk-js/package.json @@ -46,6 +46,7 @@ "commander": "^9.4.1", "eslint": "^8.20.0", "json-schema": "0.4.0", + "lodash-es": "^4.17.21", "near-abi": "^0.1.0", "near-typescript-json-schema": "0.55.0", "rollup": "^2.61.1", diff --git a/packages/near-sdk-js/src/collections/index.ts b/packages/near-sdk-js/src/collections/index.ts index 75f12247..1e590180 100644 --- a/packages/near-sdk-js/src/collections/index.ts +++ b/packages/near-sdk-js/src/collections/index.ts @@ -3,3 +3,4 @@ export * from "./lookup-set"; export * from "./unordered-map"; export * from "./unordered-set"; export * from "./vector"; +export * from "./subtype"; diff --git a/packages/near-sdk-js/src/collections/lookup-map.ts b/packages/near-sdk-js/src/collections/lookup-map.ts index 8002449e..4bb653e3 100644 --- a/packages/near-sdk-js/src/collections/lookup-map.ts +++ b/packages/near-sdk-js/src/collections/lookup-map.ts @@ -5,15 +5,18 @@ import { serializeValueWithOptions, encode, } from "../utils"; +import { SubType } from "./subtype"; /** * A lookup map that stores data in NEAR storage. */ -export class LookupMap { +export class LookupMap extends SubType { /** * @param keyPrefix - The byte prefix to use when storing elements inside this collection. */ - constructor(readonly keyPrefix: string) {} + constructor(readonly keyPrefix: string) { + super(); + } /** * Checks whether the collection contains the value. @@ -37,8 +40,12 @@ export class LookupMap { ): DataType | null { const storageKey = this.keyPrefix + key; const value = near.storageReadRaw(encode(storageKey)); + if (options == undefined) { + options = {}; + } + options = this.set_reconstructor(options); - return getValueWithOptions(value, options); + return getValueWithOptions(this.subtype(), value, options); } /** @@ -59,7 +66,7 @@ export class LookupMap { const value = near.storageGetEvictedRaw(); - return getValueWithOptions(value, options); + return getValueWithOptions(this.subtype(), value, options); } /** @@ -83,7 +90,7 @@ export class LookupMap { const value = near.storageGetEvictedRaw(); - return getValueWithOptions(value, options); + return getValueWithOptions(this.subtype(), value, options); } /** diff --git a/packages/near-sdk-js/src/collections/subtype.ts b/packages/near-sdk-js/src/collections/subtype.ts new file mode 100644 index 00000000..1b180162 --- /dev/null +++ b/packages/near-sdk-js/src/collections/subtype.ts @@ -0,0 +1,29 @@ +import { GetOptions } from "../types/collections"; + +export abstract class SubType { + /* eslint-disable @typescript-eslint/no-explicit-any */ + /* eslint-disable @typescript-eslint/no-empty-function */ + subtype(): any {} + + set_reconstructor( + options?: Omit, "serializer"> + ): Omit, "serializer"> { + if (options == undefined) { + options = {}; + } + const subtype = this.subtype(); + if ( + options.reconstructor == undefined && + subtype != undefined && + // eslint-disable-next-line no-prototype-builtins + subtype.hasOwnProperty("collection") && + typeof this.subtype().collection.reconstructor === "function" + ) { + // { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + options.reconstructor = this.subtype().collection.reconstructor; + } + return options; + } +} diff --git a/packages/near-sdk-js/src/collections/unordered-map.ts b/packages/near-sdk-js/src/collections/unordered-map.ts index 810e1630..b482164a 100644 --- a/packages/near-sdk-js/src/collections/unordered-map.ts +++ b/packages/near-sdk-js/src/collections/unordered-map.ts @@ -10,13 +10,14 @@ import { import { Vector, VectorIterator } from "./vector"; import { LookupMap } from "./lookup-map"; import { GetOptions } from "../types/collections"; +import { SubType } from "./subtype"; type ValueAndIndex = [value: string, index: number]; /** * An unordered map that stores data in NEAR storage. */ -export class UnorderedMap { +export class UnorderedMap extends SubType { readonly _keys: Vector; readonly values: LookupMap; @@ -24,6 +25,7 @@ export class UnorderedMap { * @param prefix - The byte prefix to use when storing elements inside this collection. */ constructor(readonly prefix: string) { + super(); this._keys = new Vector(`${prefix}u`); // intentional different prefix with old UnorderedMap this.values = new LookupMap(`${prefix}m`); } @@ -57,10 +59,11 @@ export class UnorderedMap { if (valueAndIndex === null) { return options?.defaultValue ?? null; } + options = this.set_reconstructor(options); const [value] = valueAndIndex; - return getValueWithOptions(encode(value), options); + return getValueWithOptions(this.subtype(), encode(value), options); } /** @@ -90,7 +93,7 @@ export class UnorderedMap { const [oldValue, oldIndex] = valueAndIndex; this.values.set(key, [decode(serialized), oldIndex]); - return getValueWithOptions(encode(oldValue), options); + return getValueWithOptions(this.subtype(), encode(oldValue), options); } /** @@ -124,7 +127,7 @@ export class UnorderedMap { this.values.set(swappedKey, [swappedValueAndIndex[0], index]); } - return getValueWithOptions(encode(value), options); + return getValueWithOptions(this.subtype(), encode(value), options); } /** @@ -246,8 +249,13 @@ class UnorderedMapIterator { ) { this.keys = new VectorIterator(unorderedMap._keys); this.map = unorderedMap.values; + this.subtype = unorderedMap.subtype; } + /* eslint-disable @typescript-eslint/no-explicit-any */ + /* eslint-disable @typescript-eslint/no-empty-function */ + subtype(): any {} + next(): { value: [string | null, DataType | null]; done: boolean } { const key = this.keys.next(); @@ -263,7 +271,11 @@ class UnorderedMapIterator { done: key.done, value: [ key.value, - getValueWithOptions(encode(valueAndIndex[0]), this.options), + getValueWithOptions( + this.subtype(), + encode(valueAndIndex[0]), + this.options + ), ], }; } diff --git a/packages/near-sdk-js/src/collections/vector.ts b/packages/near-sdk-js/src/collections/vector.ts index 698613a3..9b8ca4ac 100644 --- a/packages/near-sdk-js/src/collections/vector.ts +++ b/packages/near-sdk-js/src/collections/vector.ts @@ -9,6 +9,7 @@ import { bytes, } from "../utils"; import { GetOptions } from "../types/collections"; +import { SubType } from "./subtype"; function indexToKey(prefix: string, index: number): string { const data = new Uint32Array([index]); @@ -22,12 +23,14 @@ function indexToKey(prefix: string, index: number): string { * An iterable implementation of vector that stores its content on the trie. * Uses the following map: index -> element */ -export class Vector { +export class Vector extends SubType { /** * @param prefix - The byte prefix to use when storing elements inside this collection. * @param length - The initial length of the collection. By default 0. */ - constructor(readonly prefix: string, public length = 0) {} + constructor(readonly prefix: string, public length = 0) { + super(); + } /** * Checks whether the collection is empty. @@ -52,8 +55,8 @@ export class Vector { const storageKey = indexToKey(this.prefix, index); const value = near.storageReadRaw(bytes(storageKey)); - - return getValueWithOptions(value, options); + options = this.set_reconstructor(options); + return getValueWithOptions(this.subtype(), value, options); } /** @@ -83,8 +86,9 @@ export class Vector { ); const value = near.storageGetEvictedRaw(); + options = this.set_reconstructor(options); - return getValueWithOptions(value, options); + return getValueWithOptions(this.subtype(), value, options); } /** @@ -124,7 +128,7 @@ export class Vector { const value = near.storageGetEvictedRaw(); - return getValueWithOptions(value, options); + return getValueWithOptions(this.subtype(), value, options); } /** @@ -151,8 +155,9 @@ export class Vector { ); const value = near.storageGetEvictedRaw(); + options = this.set_reconstructor(options); - return getValueWithOptions(value, options); + return getValueWithOptions(this.subtype(), value, options); } /** diff --git a/packages/near-sdk-js/src/near-bindgen.ts b/packages/near-sdk-js/src/near-bindgen.ts index 614f62b9..df1e04a2 100644 --- a/packages/near-sdk-js/src/near-bindgen.ts +++ b/packages/near-sdk-js/src/near-bindgen.ts @@ -1,5 +1,11 @@ import * as near from "./api"; -import { deserialize, serialize, bytes, encode } from "./utils"; +import { + deserialize, + serialize, + bytes, + encode, + decodeObj2class, +} from "./utils"; type EmptyParameterObject = Record; type AnyObject = Record; @@ -210,15 +216,21 @@ export function NearBindgen({ } static _reconstruct(classObject: object, plainObject: AnyObject): object { - for (const item in classObject) { - const reconstructor = classObject[item].constructor?.reconstruct; - - classObject[item] = reconstructor - ? reconstructor(plainObject[item]) - : plainObject[item]; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (classObject.constructor.schema === undefined) { + for (const item in classObject) { + const reconstructor = classObject[item].constructor?.reconstruct; + + classObject[item] = reconstructor + ? reconstructor(plainObject[item]) + : plainObject[item]; + } + + return classObject; } - return classObject; + return decodeObj2class(classObject, plainObject); } static _requireInit(): boolean { diff --git a/packages/near-sdk-js/src/utils.ts b/packages/near-sdk-js/src/utils.ts index ab5ebbe0..f611e0d2 100644 --- a/packages/near-sdk-js/src/utils.ts +++ b/packages/near-sdk-js/src/utils.ts @@ -1,4 +1,5 @@ import { GetOptions } from "./types/collections"; +import { cloneDeep } from "lodash-es"; export interface Env { uint8array_to_latin1_string(a: Uint8Array): string; @@ -70,6 +71,7 @@ export function assert( export type Mutable = { -readonly [P in keyof T]: T[P] }; export function getValueWithOptions( + subDatatype: unknown, value: Uint8Array | null, options: Omit, "serializer"> = { deserializer: deserialize, @@ -79,14 +81,71 @@ export function getValueWithOptions( return options?.defaultValue ?? null; } - const deserialized = deserialize(value); + // here is an obj + let deserialized = deserialize(value); if (deserialized === undefined || deserialized === null) { return options?.defaultValue ?? null; } if (options?.reconstructor) { - return options.reconstructor(deserialized); + // example: // { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}} + const collection = options.reconstructor(deserialized); + if ( + subDatatype !== undefined && + // eslint-disable-next-line no-prototype-builtins + subDatatype.hasOwnProperty("collection") && + // eslint-disable-next-line no-prototype-builtins + subDatatype["collection"].hasOwnProperty("value") + ) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + collection.subtype = function () { + // example: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}} + return subDatatype["collection"]["value"]; + }; + } + return collection; + } + + // example: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}} + if (subDatatype !== undefined) { + // subtype info is a class constructor, Such as Car + if (typeof subDatatype === "function") { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + deserialized = decodeObj2class(new subDatatype(), deserialized); + } else if (typeof subDatatype === "object") { + // normal collections of array, map; subtype will be: + // {map: { key: 'string', value: 'string' }} or {array: {value: 'string'}} .. + // eslint-disable-next-line no-prototype-builtins + if (subDatatype.hasOwnProperty("map")) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + for (const mkey in deserialized) { + if (subDatatype["map"]["value"] !== "string") { + deserialized[mkey] = decodeObj2class( + new subDatatype["map"]["value"](), + value[mkey] + ); + } + } + // eslint-disable-next-line no-prototype-builtins + } else if (subDatatype.hasOwnProperty("array")) { + const new_vec = []; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + for (const k in deserialized) { + if (subDatatype["array"]["value"] !== "string") { + new_vec.push( + decodeObj2class(new subDatatype["array"]["value"](), value[k]) + ); + } + } + deserialized = new_vec; + // eslint-disable-next-line no-prototype-builtins + } + } } return deserialized as DataType; @@ -147,6 +206,75 @@ export function deserialize(valueToDeserialize: Uint8Array): unknown { }); } +export function decodeObj2class(class_instance, obj) { + if ( + typeof obj != "object" || + class_instance.constructor.schema === undefined + ) { + return obj; + } + let key; + for (key in obj) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const value = obj[key]; + if (typeof value == "object") { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const ty = class_instance.constructor.schema[key]; + // eslint-disable-next-line no-prototype-builtins + if (ty !== undefined && ty.hasOwnProperty("map")) { + for (const mkey in value) { + if (ty["map"]["value"] === "string") { + class_instance[key][mkey] = value[mkey]; + } else { + class_instance[key][mkey] = decodeObj2class( + new ty["map"]["value"](), + value[mkey] + ); + } + } + // eslint-disable-next-line no-prototype-builtins + } else if (ty !== undefined && ty.hasOwnProperty("array")) { + for (const k in value) { + if (ty["array"]["value"] === "string") { + class_instance[key].push(value[k]); + } else { + class_instance[key].push( + decodeObj2class(new ty["array"]["value"](), value[k]) + ); + } + } + // eslint-disable-next-line no-prototype-builtins + } else if (ty !== undefined && ty.hasOwnProperty("collection")) { + // nested_lookup_recordes: {collection: {reconstructor: UnorderedMap.reconstruct, value: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}}}}, + // {collection: {reconstructor: + class_instance[key] = ty["collection"]["reconstructor"](obj[key]); + const subtype_value = ty["collection"]["value"]; + class_instance[key].subtype = function () { + // example: { collection: {reconstructor: LookupMap.reconstruct, value: 'string'}} + return subtype_value; + }; + } else { + // normal case with nested Class, such as field is truck: Truck, + class_instance[key] = decodeObj2class(class_instance[key], obj[key]); + } + } else { + class_instance[key] = obj[key]; + } + } + const instance_tmp = cloneDeep(class_instance); + for (key in obj) { + if ( + typeof class_instance[key] == "object" && + !(class_instance[key] instanceof Date) + ) { + class_instance[key] = instance_tmp[key]; + } + } + return class_instance; +} + /** * Validates the Account ID according to the NEAR protocol * [Account ID rules](https://nomicon.io/DataStructures/Account#account-id-rules). diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a94e0a46..744add28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + importers: .: @@ -121,6 +125,9 @@ importers: json-schema: specifier: 0.4.0 version: 0.4.0 + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 near-abi: specifier: ^0.1.0 version: 0.1.1 @@ -3949,7 +3956,3 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} dev: true - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false