From 10854b9f1388a6dbf270655e0a60d664610eb98d Mon Sep 17 00:00:00 2001 From: Bowen Wang Date: Tue, 25 Jun 2019 12:57:27 -0700 Subject: [PATCH 01/10] Basic contract metadata proposal --- text/0000-contract-metadata.md | 77 ++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 text/0000-contract-metadata.md diff --git a/text/0000-contract-metadata.md b/text/0000-contract-metadata.md new file mode 100644 index 000000000..5a68fbbe3 --- /dev/null +++ b/text/0000-contract-metadata.md @@ -0,0 +1,77 @@ +- Proposal Name: `contract_metadata` +- Start Date: (fill me in with today's date, YYYY-MM-DD) +- NEP PR: [nearprotocol/neps#0000](https://github.com/nearprotocol/neps/pull/0000) +- Issue(s): link to relevant issues in relevant repos (not required). + +# Summary +[summary]: #summary + +This NEP introduces contract metadata, which summarizes the content of a given contract in json format. +Contract metadata allows developers to easily list the methods in a contract and potentially display them to users to +provide transparency. + +# Motivation +[motivation]: #motivation + +Currently there is no direct way for a developer to programmatically list the methods of a contract that is deployed on chain +because the contract code stored on chain is the compiled wasm code, where the methods name and parameters are already mangled. +As a result, if developers want to display the methods of a contract to an user of the app to provide transparency, +especially in the case of financial apps, they have no choice but to hardcode them in the front end, which is suboptimal in many ways. +Furthermore, if developers want to get the list of methods in some contract for some downstream task like data analysis, +they have no way of doing so. Contract metadata aims to solve the aforementioned problems by providing a convenient way +for developers to list the methods of contracts. + +# Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +For developers, there will be two main changes: +- Instead of annotating view and change methods in the `initContract` function in `main.js`, +they will instead annotate the methods of a contract by decorators. +More specifically, every method is by default a change method, unless annotated by `@view_method`. +- Every contract will have a `metadata` method that returns a json that serializes the contract methods. For each method, +the json serialization is of the form `{"name": , "parameters": [{: , .. }], "returnType": }`. +The overall serialization is of the form `{"view_methods": [{: , .. }], "change_methods": [{: , .. }]}`. + +As an concrete example, suppose we have a contract where `main.ts` is as follows: + +```typescript +import { context, storage, near } from "./near"; + +@view_method +export function hello(): string { + return "Hello, world"; +} +``` + +Then the generated `metadata` method will return `{"view_methods": [{"hello": {"name": "hello", "parameters": [], "returnType": "string"}}], "change_methods": []}` + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +To implement this NEP, we just need to modify the binding generation to generate a method called `metadata` that returns +json serialization of contract metadata described in the previous section. This involves walking through the exported methods +in `main.ts`, get the metadata of each method, and serialize them in json. Metadata of a given method, including decorators, +are easily extractable from the assemblyscript IR and serialized into json format. + +# Drawbacks +[drawbacks]: #drawbacks + +The main drawback of this NEP is that it involves generating one more method during the binding generation phase, which +will in turn increase the size of each contract. + +# Rationale and alternatives +[rationale-and-alternatives]: #rationale-and-alternatives + +It is unclear to me what the alternative is. + +# Unresolved questions +[unresolved-questions]: #unresolved-questions + +* What other information, besides those mentioned in [guide-level-explanation], should we include in the metadata? +* Most of this NEP is concerned with contract metadata for assemblyscript, for the rust API, it is not yet unclear what +needs to be done given that it is not yet stabilized. + +# Future possibilities +[future-possibilities]: #future-possibilities + +TBD From 5992aea3901d7528305f9f8f6b9384828a11aeab Mon Sep 17 00:00:00 2001 From: Bowen Wang Date: Tue, 25 Jun 2019 15:04:18 -0700 Subject: [PATCH 02/10] Better example --- text/0000-contract-metadata.md | 56 +++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/text/0000-contract-metadata.md b/text/0000-contract-metadata.md index 5a68fbbe3..95fdd5490 100644 --- a/text/0000-contract-metadata.md +++ b/text/0000-contract-metadata.md @@ -32,18 +32,66 @@ More specifically, every method is by default a change method, unless annotated the json serialization is of the form `{"name": , "parameters": [{: , .. }], "returnType": }`. The overall serialization is of the form `{"view_methods": [{: , .. }], "change_methods": [{: , .. }]}`. -As an concrete example, suppose we have a contract where `main.ts` is as follows: +As an concrete example, suppose we have a contract that maintains a counter on chain: ```typescript import { context, storage, near } from "./near"; +export function incrementCounter(): void { + let newCounter = storage.get("counter") + 1; + storage.set("counter", newCounter) + near.log("Counter is now: " + newCounter.toString()); +} + +export function decrementCounter(): void { + let newCounter = storage.get("counter") - 1; + storage.set("counter", newCounter) + near.log("Counter is now: " + newCounter.toString()); +} + @view_method -export function hello(): string { - return "Hello, world"; +export function getCounter(): i32 { + return storage.get("counter"); } ``` -Then the generated `metadata` method will return `{"view_methods": [{"hello": {"name": "hello", "parameters": [], "returnType": "string"}}], "change_methods": []}` +This contract has two change methods, `incrementCounter` and `decrementCounter`, as well as one view method, `getCounter`. +In this case, the metadata we want looks like +```json +{ + "view_methods": [ + { + "getCounter": { + "name": "getCounter", + "parameters": [], + "returnType": "i32" + } + } + ], + "change_methods": [ + { + "incrementCounter": { + "name": "incrementCounter", + "parameters": [], + "returnType": "void" + } + }, + { + "decrementCounter": { + "name": "decrementCounter", + "parameters": [], + "returnType": "void" + } + } + ] +} +``` +and the generated `metadata` method looks like: +```typescript +export function metadata(): string { + return "{\"view_methods\": [{\"getCounter\": {\"name\": \"getCounter\", \"parameters\": [], \"returnType\": \"i32\"}],\"change_methods\": [{\"incrementCounter\": {\"name\": \"incrementCounter\", \"parameters\": [], \"returnType\": \"void\"}}, {\"decrementCounter\": {\"name\": \"decrementCounter\", \"parameters\": [], \"returnType\": \"void\"}}]}" +} +``` # Reference-level explanation [reference-level-explanation]: #reference-level-explanation From 757e9275f07fa20fd89848740a515355f421a0a6 Mon Sep 17 00:00:00 2001 From: Bowen Wang Date: Tue, 25 Jun 2019 15:09:08 -0700 Subject: [PATCH 03/10] update PR number --- text/0000-contract-metadata.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/text/0000-contract-metadata.md b/text/0000-contract-metadata.md index 95fdd5490..ecf74809c 100644 --- a/text/0000-contract-metadata.md +++ b/text/0000-contract-metadata.md @@ -1,7 +1,6 @@ - Proposal Name: `contract_metadata` - Start Date: (fill me in with today's date, YYYY-MM-DD) -- NEP PR: [nearprotocol/neps#0000](https://github.com/nearprotocol/neps/pull/0000) -- Issue(s): link to relevant issues in relevant repos (not required). +- NEP PR: [nearprotocol/neps#0003](https://github.com/nearprotocol/NEPs/pull/3) # Summary [summary]: #summary From b449eafad3d00c39b47a15c8295fbd001d6a6cf8 Mon Sep 17 00:00:00 2001 From: Bowen Wang Date: Wed, 26 Jun 2019 17:44:08 -0700 Subject: [PATCH 04/10] fix json syntax --- text/0000-contract-metadata.md | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/text/0000-contract-metadata.md b/text/0000-contract-metadata.md index ecf74809c..0b2f6d350 100644 --- a/text/0000-contract-metadata.md +++ b/text/0000-contract-metadata.md @@ -28,8 +28,8 @@ For developers, there will be two main changes: they will instead annotate the methods of a contract by decorators. More specifically, every method is by default a change method, unless annotated by `@view_method`. - Every contract will have a `metadata` method that returns a json that serializes the contract methods. For each method, -the json serialization is of the form `{"name": , "parameters": [{: , .. }], "returnType": }`. -The overall serialization is of the form `{"view_methods": [{: , .. }], "change_methods": [{: , .. }]}`. +the json serialization is of the form `{"name": , "parameters": {: , .. }, "returnType": }`. +The overall serialization is of the form `{"view_methods": {: , .. }, "change_methods": {: , .. }}`. As an concrete example, suppose we have a contract that maintains a counter on chain: @@ -58,37 +58,33 @@ This contract has two change methods, `incrementCounter` and `decrementCounter`, In this case, the metadata we want looks like ```json { - "view_methods": [ - { + "view_methods": + { "getCounter": { "name": "getCounter", "parameters": [], "returnType": "i32" } - } - ], - "change_methods": [ - { + }, + "change_methods": + { "incrementCounter": { "name": "incrementCounter", "parameters": [], "returnType": "void" - } - }, - { + }, "decrementCounter": { "name": "decrementCounter", "parameters": [], "returnType": "void" } - } - ] + } } ``` and the generated `metadata` method looks like: ```typescript export function metadata(): string { - return "{\"view_methods\": [{\"getCounter\": {\"name\": \"getCounter\", \"parameters\": [], \"returnType\": \"i32\"}],\"change_methods\": [{\"incrementCounter\": {\"name\": \"incrementCounter\", \"parameters\": [], \"returnType\": \"void\"}}, {\"decrementCounter\": {\"name\": \"decrementCounter\", \"parameters\": [], \"returnType\": \"void\"}}]}" + return "{\"view_methods\": {\"getCounter\": {\"name\": \"getCounter\", \"parameters\": {}, \"returnType\": \"i32\"},\"change_methods\": {\"incrementCounter\": {\"name\": \"incrementCounter\", \"parameters\": {}, \"returnType\": \"void\"}}, {\"decrementCounter\": {\"name\": \"decrementCounter\", \"parameters\": {}, \"returnType\": \"void\"}}}" } ``` From c178b4d1b6d80fbc0cc10c28c0b6354d8d3b7010 Mon Sep 17 00:00:00 2001 From: Bowen Wang Date: Wed, 10 Jul 2019 09:25:00 -0700 Subject: [PATCH 05/10] Update json format --- text/0000-contract-metadata.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/text/0000-contract-metadata.md b/text/0000-contract-metadata.md index 0b2f6d350..f3a32ec9e 100644 --- a/text/0000-contract-metadata.md +++ b/text/0000-contract-metadata.md @@ -28,7 +28,7 @@ For developers, there will be two main changes: they will instead annotate the methods of a contract by decorators. More specifically, every method is by default a change method, unless annotated by `@view_method`. - Every contract will have a `metadata` method that returns a json that serializes the contract methods. For each method, -the json serialization is of the form `{"name": , "parameters": {: , .. }, "returnType": }`. +the json serialization is of the form `{"parameters": {: , .. }, "returnType": }`. The overall serialization is of the form `{"view_methods": {: , .. }, "change_methods": {: , .. }}`. As an concrete example, suppose we have a contract that maintains a counter on chain: @@ -61,7 +61,6 @@ In this case, the metadata we want looks like "view_methods": { "getCounter": { - "name": "getCounter", "parameters": [], "returnType": "i32" } @@ -69,12 +68,10 @@ In this case, the metadata we want looks like "change_methods": { "incrementCounter": { - "name": "incrementCounter", "parameters": [], "returnType": "void" }, "decrementCounter": { - "name": "decrementCounter", "parameters": [], "returnType": "void" } @@ -84,7 +81,7 @@ In this case, the metadata we want looks like and the generated `metadata` method looks like: ```typescript export function metadata(): string { - return "{\"view_methods\": {\"getCounter\": {\"name\": \"getCounter\", \"parameters\": {}, \"returnType\": \"i32\"},\"change_methods\": {\"incrementCounter\": {\"name\": \"incrementCounter\", \"parameters\": {}, \"returnType\": \"void\"}}, {\"decrementCounter\": {\"name\": \"decrementCounter\", \"parameters\": {}, \"returnType\": \"void\"}}}" + return "{\"view_methods\": {\"getCounter\": {\"parameters\": {}, \"returnType\": \"i32\"},\"change_methods\": {\"incrementCounter\": {\"parameters\": {}, \"returnType\": \"void\"}}, {\"decrementCounter\": {\"parameters\": {}, \"returnType\": \"void\"}}}" } ``` @@ -117,4 +114,5 @@ needs to be done given that it is not yet stabilized. # Future possibilities [future-possibilities]: #future-possibilities -TBD +Under the framework proposed in this NEP, it is also not difficult to add annotations to methods in natural language. +We can also add contract-level annotation as part of the contract metadata json. From 17951dcb9ff8a021644c2cedde0c574c074d1f59 Mon Sep 17 00:00:00 2001 From: Bowen Wang Date: Wed, 10 Jul 2019 13:58:47 -0700 Subject: [PATCH 06/10] Add parameters in example --- text/0000-contract-metadata.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/text/0000-contract-metadata.md b/text/0000-contract-metadata.md index f3a32ec9e..1c77c4f82 100644 --- a/text/0000-contract-metadata.md +++ b/text/0000-contract-metadata.md @@ -29,21 +29,21 @@ they will instead annotate the methods of a contract by decorators. More specifically, every method is by default a change method, unless annotated by `@view_method`. - Every contract will have a `metadata` method that returns a json that serializes the contract methods. For each method, the json serialization is of the form `{"parameters": {: , .. }, "returnType": }`. -The overall serialization is of the form `{"view_methods": {: , .. }, "change_methods": {: , .. }}`. +The overall serialization is of the form `{"viewMethods": {: , .. }, "changeMethods": {: , .. }}`. As an concrete example, suppose we have a contract that maintains a counter on chain: ```typescript import { context, storage, near } from "./near"; -export function incrementCounter(): void { - let newCounter = storage.get("counter") + 1; +export function incrementCounterBy(amount: i32 = 1): void { + let newCounter = storage.get("counter") + amount; storage.set("counter", newCounter) near.log("Counter is now: " + newCounter.toString()); } -export function decrementCounter(): void { - let newCounter = storage.get("counter") - 1; +export function decrementCounterBy(amount: i32 = 1): void { + let newCounter = storage.get("counter") - amount; storage.set("counter", newCounter) near.log("Counter is now: " + newCounter.toString()); } @@ -58,21 +58,21 @@ This contract has two change methods, `incrementCounter` and `decrementCounter`, In this case, the metadata we want looks like ```json { - "view_methods": + "viewMethods": { "getCounter": { - "parameters": [], + "parameters": {"amount": "i32"}, "returnType": "i32" } }, - "change_methods": + "changeMethods": { - "incrementCounter": { - "parameters": [], + "incrementCounterBy": { + "parameters": {"amount": "i32"}, "returnType": "void" }, - "decrementCounter": { - "parameters": [], + "decrementCounterBy": { + "parameters": {}, "returnType": "void" } } @@ -81,7 +81,7 @@ In this case, the metadata we want looks like and the generated `metadata` method looks like: ```typescript export function metadata(): string { - return "{\"view_methods\": {\"getCounter\": {\"parameters\": {}, \"returnType\": \"i32\"},\"change_methods\": {\"incrementCounter\": {\"parameters\": {}, \"returnType\": \"void\"}}, {\"decrementCounter\": {\"parameters\": {}, \"returnType\": \"void\"}}}" + return "{\"viewMethods\": {\"getCounter\": {\"parameters\": {}, \"returnType\": \"i32\"},\"changeMethods\": {\"incrementCounterBy\": {\"parameters\": {\"amount\": \"i32\"}, \"returnType\": \"void\"}}, {\"decrementCounterBy\": {\"parameters\": {\"amount\": \"i32\"}, \"returnType\": \"void\"}}}" } ``` From e994817ce9ba7590d41b604dec892334f9dc110c Mon Sep 17 00:00:00 2001 From: Bowen Wang Date: Wed, 10 Jul 2019 16:43:14 -0700 Subject: [PATCH 07/10] change json format to use arrays --- text/0000-contract-metadata.md | 39 +++++++++++++++++----------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/text/0000-contract-metadata.md b/text/0000-contract-metadata.md index 1c77c4f82..0f2307068 100644 --- a/text/0000-contract-metadata.md +++ b/text/0000-contract-metadata.md @@ -27,9 +27,8 @@ For developers, there will be two main changes: - Instead of annotating view and change methods in the `initContract` function in `main.js`, they will instead annotate the methods of a contract by decorators. More specifically, every method is by default a change method, unless annotated by `@view_method`. -- Every contract will have a `metadata` method that returns a json that serializes the contract methods. For each method, -the json serialization is of the form `{"parameters": {: , .. }, "returnType": }`. -The overall serialization is of the form `{"viewMethods": {: , .. }, "changeMethods": {: , .. }}`. +- Every contract will have a `metadata` method that returns a json that serializes the contract methods in an array. +For each method, the json serialization is of the form `{"name": , "parameters": [{"name": , "type": , ..}, .. ], "returnType": }`. As an concrete example, suppose we have a contract that maintains a counter on chain: @@ -57,31 +56,31 @@ export function getCounter(): i32 { This contract has two change methods, `incrementCounter` and `decrementCounter`, as well as one view method, `getCounter`. In this case, the metadata we want looks like ```json -{ - "viewMethods": +[ { - "getCounter": { - "parameters": {"amount": "i32"}, - "returnType": "i32" - } + "name": "getCounter", + "parameters": [{"name": "amount", "type": "i32"}], + "returnType": "i32", + "methodType": "view" }, - "changeMethods": { - "incrementCounterBy": { - "parameters": {"amount": "i32"}, - "returnType": "void" - }, - "decrementCounterBy": { - "parameters": {}, - "returnType": "void" - } + "name": "incrementCounterBy", + "parameters": [{"name": "amount", "type": "i32"}], + "returnType": "i32", + "methodType": "change" + }, + { + "name": "decrementCounterBy", + "parameters": [{"name": "amount", "type": "i32"}], + "returnType": "void", + "methodType": "change" } -} +] ``` and the generated `metadata` method looks like: ```typescript export function metadata(): string { - return "{\"viewMethods\": {\"getCounter\": {\"parameters\": {}, \"returnType\": \"i32\"},\"changeMethods\": {\"incrementCounterBy\": {\"parameters\": {\"amount\": \"i32\"}, \"returnType\": \"void\"}}, {\"decrementCounterBy\": {\"parameters\": {\"amount\": \"i32\"}, \"returnType\": \"void\"}}}" + return '[{"name": "getCounter", "parameters": [{"name": "amount", "type": "i32"}], "returnType": "i32", "methodType": "view"}, {"name": "incrementCounterBy", "parameters": [{"name": "amount", "type": "i32"}], "returnType": "i32", "methodType": "change"}, {"name": "decrementCounterBy", "parameters": [{"name": "amount", "type": "i32"}], "returnType": "void", "returnType": "void"}]' } ``` From 00a005cae47c83cb56f6bd47f014d23c2d95091c Mon Sep 17 00:00:00 2001 From: Bowen Wang Date: Mon, 29 Jul 2019 09:44:42 -0700 Subject: [PATCH 08/10] change metadata format --- text/0000-contract-metadata.md | 169 ++++++++++++++++++++++++++------- 1 file changed, 135 insertions(+), 34 deletions(-) diff --git a/text/0000-contract-metadata.md b/text/0000-contract-metadata.md index 0f2307068..8446609f9 100644 --- a/text/0000-contract-metadata.md +++ b/text/0000-contract-metadata.md @@ -1,5 +1,5 @@ - Proposal Name: `contract_metadata` -- Start Date: (fill me in with today's date, YYYY-MM-DD) +- Start Date: 2019-06-25 - NEP PR: [nearprotocol/neps#0003](https://github.com/nearprotocol/NEPs/pull/3) # Summary @@ -7,32 +7,46 @@ This NEP introduces contract metadata, which summarizes the content of a given contract in json format. Contract metadata allows developers to easily list the methods in a contract and potentially display them to users to -provide transparency. +provide transparency. +It also provides the ability to generate bindings in different languages for the contract, thanks to the class information +also contained in the metadata. # Motivation [motivation]: #motivation -Currently there is no direct way for a developer to programmatically list the methods of a contract that is deployed on chain -because the contract code stored on chain is the compiled wasm code, where the methods name and parameters are already mangled. +Currently there is no convenient way for a developer to programmatically list the methods of a contract that is deployed on chain +because the contract code stored on chain is the compiled wasm code, where the parameters and type information are already mangled (even though +method name is not, it is still cumbersome to extract them from wasm binary). As a result, if developers want to display the methods of a contract to an user of the app to provide transparency, especially in the case of financial apps, they have no choice but to hardcode them in the front end, which is suboptimal in many ways. -Furthermore, if developers want to get the list of methods in some contract for some downstream task like data analysis, -they have no way of doing so. Contract metadata aims to solve the aforementioned problems by providing a convenient way +Furthermore, if developers want to get the list of methods in some contract for some downstream task like data analysis, or interacting +with other contracts, they have no way of doing so. +Contract metadata aims to solve the aforementioned problems by providing a convenient way for developers to list the methods of contracts. # Guide-level explanation [guide-level-explanation]: #guide-level-explanation -For developers, there will be two main changes: -- Instead of annotating view and change methods in the `initContract` function in `main.js`, -they will instead annotate the methods of a contract by decorators. -More specifically, every method is by default a change method, unless annotated by `@view_method`. -- Every contract will have a `metadata` method that returns a json that serializes the contract methods in an array. -For each method, the json serialization is of the form `{"name": , "parameters": [{"name": , "type": , ..}, .. ], "returnType": }`. +With contract metadata, developers, instead of annotating view and change methods in the `initContract` function in `main.js`, +will instead annotate the methods of a contract by decorators. We propose that view functions be annotated with the `@view` decorator +and by default functions can change the state. +Because functions might take non-primitive types and we want metadata to be easily usable by statically typed langauges, we +also provide, in addition to function annotations, class annotations, which mainly describe the name of fields in a class and their types. +Finally, since developers might want to have some contract-level description that provides an overview of what the contract does, +in metadata we also have contract-level annotation. + +More specifically, every contract will have a `metadata` method that returns a json that serializes the aforementioned information. +The overall format looks like `{"methods": [, ..], "classes": [, ..], "contract": }`. +For each method, the json serialization is of the form `{"name": , "parameters": [{"name": , "type": , ..}, .. ], "returnType": }` +where the `returnType` key will only be present for functions that have non-void return types. +For each class, the json serialization is of the form `{"name": , "fields": [{"name": ", "type": "}, ..]}`. + +## Simple Example As an concrete example, suppose we have a contract that maintains a counter on chain: ```typescript +// A contract that maintains a counter with the ability to increase and decrease by the given amount import { context, storage, near } from "./near"; export function incrementCounterBy(amount: i32 = 1): void { @@ -47,7 +61,7 @@ export function decrementCounterBy(amount: i32 = 1): void { near.log("Counter is now: " + newCounter.toString()); } -@view_method +@view export function getCounter(): i32 { return storage.get("counter"); } @@ -56,34 +70,121 @@ export function getCounter(): i32 { This contract has two change methods, `incrementCounter` and `decrementCounter`, as well as one view method, `getCounter`. In this case, the metadata we want looks like ```json -[ - { - "name": "getCounter", - "parameters": [{"name": "amount", "type": "i32"}], - "returnType": "i32", - "methodType": "view" - }, - { - "name": "incrementCounterBy", - "parameters": [{"name": "amount", "type": "i32"}], - "returnType": "i32", - "methodType": "change" - }, - { - "name": "decrementCounterBy", - "parameters": [{"name": "amount", "type": "i32"}], - "returnType": "void", - "methodType": "change" - } -] +{ + "methods": [ + { + "name": "getCounter", + "parameters": [{"name": "amount", "type": "i32"}], + "returnType": "i32", + "methodType": "view" + }, + { + "name": "incrementCounterBy", + "parameters": [{"name": "amount", "type": "i32"}], + "methodType": "change" + }, + { + "name": "decrementCounterBy", + "parameters": [{"name": "amount", "type": "i32"}], + "methodType": "change" + } + ], + "classes": [], + "contract": "A contract that maintains a counter with the ability to increase and decrease by the given amount" +} + ``` and the generated `metadata` method looks like: ```typescript export function metadata(): string { - return '[{"name": "getCounter", "parameters": [{"name": "amount", "type": "i32"}], "returnType": "i32", "methodType": "view"}, {"name": "incrementCounterBy", "parameters": [{"name": "amount", "type": "i32"}], "returnType": "i32", "methodType": "change"}, {"name": "decrementCounterBy", "parameters": [{"name": "amount", "type": "i32"}], "returnType": "void", "returnType": "void"}]' + return '{"methods": [{"name": "getCounter", "parameters": [{"name": "amount", "type": "i32"}], "returnType": "i32", "methodType": "view"}, {"name": "incrementCounterBy", "parameters": [{"name": "amount", "type": "i32"}], "methodType": "change"}, {"name": "decrementCounterBy", "parameters": [{"name": "amount", "type": "i32"}], "returnType": "void"}], "classes": [], "contract": "A contract that maintains a counter with the ability to increase and decrease by the given amount"' +} +``` + +## Real-world Example +Now let's consider a real-world example that involves more features such as class and arrays. +Suppose one wants to build a todo list app on blockchain, which is modeled as +```typescript +export class Todo { + id: string; + title: string; + completed: bool; +} +``` + +and the contract is as follows +```typescript +// a contract that implements a todo list on blockchain +import { context, storage, near, collections } from "./near"; + +import { Todo } from "./model.near"; + +// Map from string key ID to a Todo +// collections.map is a persistent collection. Any changes to it will +// be automatically saved in the storage. +// The parameter to the constructor needs to be unique across a single contract. +// It will be used as a prefix to all keys required to store data in the storage. +let todos = collections.map("todos"); + +export function setTodo(id: string, todo: Todo): void { + near.log("setTodo " + id); + todos.set(id, todo); +} + +@view +export function getTodo(id: string): Todo { + return todos.get(id); +} + +@view +export function getAllTodos(): Array { + // Map currently doesn't support getting all keys, so we use storage prefix. + let allKeys = storage.keys("todos::"); + near.log("allKeys: " + allKeys.join(", ")); + + let loaded = new Array(allKeys.length); + for (let i = 0; i < allKeys.length; i++) { + loaded[i] = Todo.decode(storage.getBytes(allKeys[i])); + } + return loaded; } ``` +For this contract, `setTodo` is a change method while `getTodo` and `getAllTodos` are view methods. +In this case, the metadata we want looks like +```json +{ + "methods": [ + { + "name": "setTodo", + "parameters": [{"name": "id", "type": "string"}, {"name": "todo", "type": "Todo"}], + "returnType": "void", + "methodType": "change" + }, + { + "name": "getTodo", + "parameters": [{"name": "id", "type": "string"}], + "returnType": "Todo", + "methodType": "view" + }, + { + "name": "getAllTodos", + "parameters": [], + "returnType": "Array", + "methodType": "view" + } + ], + "classes": [ + { + "name": "Todo", + "fields": [{"name": "id", "type": "string"}, {"name": "title", "type": "string"}, {"name": "completed", "type": "bool"}] + } + ], + "contract": "a contract that implements a todo list on blockchain" +} + +``` + # Reference-level explanation [reference-level-explanation]: #reference-level-explanation From 697f384fae7d44e1b8295d76d8b63f17c13d766b Mon Sep 17 00:00:00 2001 From: Bowen Wang Date: Mon, 16 Sep 2019 16:11:52 -0700 Subject: [PATCH 09/10] New design --- text/0000-contract-metadata.md | 142 ++++++++++++++++++++++++++------- 1 file changed, 115 insertions(+), 27 deletions(-) diff --git a/text/0000-contract-metadata.md b/text/0000-contract-metadata.md index 8446609f9..005198cc1 100644 --- a/text/0000-contract-metadata.md +++ b/text/0000-contract-metadata.md @@ -8,8 +8,10 @@ This NEP introduces contract metadata, which summarizes the content of a given contract in json format. Contract metadata allows developers to easily list the methods in a contract and potentially display them to users to provide transparency. -It also provides the ability to generate bindings in different languages for the contract, thanks to the class information -also contained in the metadata. +It also provides the ability to validate input data in frontend thanks to the signatures of contract methods provided +by the contract metadata. +Contract metadata is similar to contract abi in some other blockchains such as Ethereum or Substrate but is more geared +towards readability and developer friendliness. # Motivation [motivation]: #motivation @@ -21,6 +23,8 @@ As a result, if developers want to display the methods of a contract to an user especially in the case of financial apps, they have no choice but to hardcode them in the front end, which is suboptimal in many ways. Furthermore, if developers want to get the list of methods in some contract for some downstream task like data analysis, or interacting with other contracts, they have no way of doing so. +In addition, because of the lack of signature of the contract functions, there is no way that the frontend can validate +that input data is of the right format, which might lead to some spamming attacks. Contract metadata aims to solve the aforementioned problems by providing a convenient way for developers to list the methods of contracts. @@ -35,13 +39,75 @@ also provide, in addition to function annotations, class annotations, which main Finally, since developers might want to have some contract-level description that provides an overview of what the contract does, in metadata we also have contract-level annotation. -More specifically, every contract will have a `metadata` method that returns a json that serializes the aforementioned information. -The overall format looks like `{"methods": [, ..], "classes": [, ..], "contract": }`. -For each method, the json serialization is of the form `{"name": , "parameters": [{"name": , "type": , ..}, .. ], "returnType": }` -where the `returnType` key will only be present for functions that have non-void return types. -For each class, the json serialization is of the form `{"name": , "fields": [{"name": ", "type": "}, ..]}`. +The format of contract metadata is similar to [Ethereum json contract abi](https://solidity.readthedocs.io/en/v0.5.3/abi-spec.html#json). +For each function, the metadata is a json object with the fields: +* `name`: the name of the function; +* `parameters`: an array of objects, each of which contains: + * `name`: the name of the parameter + * `type`: the type of the parameter (more below) +* `returnType`: The return type of the function. If there is no return value, this field is omitted. +* `stateMutability`: a string that is either `view` or `change`. It specifies whether the function can mutate state. + +For types, we try to be as general and language agnostic as possible. Therefore we follow mostly the types that [serde](https://serde.rs/data-model.html) + uses. The following types are available: +* primitive types: + * `bool` + * `i8`, `i16`, `i32`, `i64`, `i128` + * `u8`, `u16`, `u32`, `u64`, `u128` + * `string` + +* `byteArray`: an array of bytes. In Rust this is `[u8]` whereas in AssemblyScript this is typedarrays. +* `Option`: Either none or some value. In Rust this is `Option` whereas in AssemblyScript this is `T | null` provided +that `T` is nullable. +* `Seq`: A variably sized homogeneous sequence of values, for example `Vec` or `HashSet` in Rust, `Array` in +AssemblyScript. +* tuple: A statically sized heterogeneous sequence of values, for example `(u8, string)`. This doesn't apply to AssemblyScript. +* `Map`: A variably sized heterogeneous key-value pairing, for example `BTreeMap` in Rust and `Map` in AssemblyScript. +* object: `struct` in Rust or `class` in AssemblyScript. The metadata for an object type is a json object with the fields: + * `name`: Name of the class + * `fields`: An array of objects each of which contains: + * `name`: the name of the parameter + * `type`: the type of the parameter + +The object types will not be fully unrolled in their json representation. For example, if we have the following classes +```rust +pub struct Person { + pub name: String, + pub address: Address, +} + +pub struct Address { + pub city: String, + pub street: String, + pub zip: u16, +} +``` + +The json representation for `Person` is +```json +{ + "name": "Person", + "fields": + [ + {"name": "name", "type": "string"}, + {"name": "address", "type": "Address"} + ] +} +``` + +The type `Address` is not unrolled to avoid repetitive information. Instead, the overall contract metadata also contains +metadata for each exported class. + +In addition to function metadata and class metadata, the contract metadata also includes information about the contract +as a whole. To provide this information, a developer can choose to implement a method called `description` in their contract +which returns a json string that contains `name`, the name of the contract, and `description`, annotation of what the +contract does as a whole. + +Contract metadata will also have a version associated with it so that it is clear to the developers when the format changes. +Overall, every contract will have a `metadata` method generated at compile time that returns a json that serializes all the aforementioned information. +The overall format looks like `{"methods": [, ..], "classes": [, ..], "contract": , "version": }`. -## Simple Example +## A Simple Example As an concrete example, suppose we have a contract that maintains a counter on chain: @@ -65,9 +131,15 @@ export function decrementCounterBy(amount: i32 = 1): void { export function getCounter(): i32 { return storage.get("counter"); } + +@view +export function description(): string { + return '{"name": "Counter", "description": " A contract that maintains a counter with the ability to increase and decrease by the given amount"}'; +} ``` -This contract has two change methods, `incrementCounter` and `decrementCounter`, as well as one view method, `getCounter`. +This contract has two change methods, `incrementCounter` and `decrementCounter`, as well as two view methods, `getCounter` +and `description`. In this case, the metadata we want looks like ```json { @@ -76,33 +148,39 @@ In this case, the metadata we want looks like "name": "getCounter", "parameters": [{"name": "amount", "type": "i32"}], "returnType": "i32", - "methodType": "view" + "stateMutability": "view" }, { "name": "incrementCounterBy", "parameters": [{"name": "amount", "type": "i32"}], - "methodType": "change" + "stateMutability": "change" }, { "name": "decrementCounterBy", "parameters": [{"name": "amount", "type": "i32"}], - "methodType": "change" + "stateMutability": "change" + }, + { + "name": "description", + "parameters": [], + "stateMutability": "view" } ], "classes": [], - "contract": "A contract that maintains a counter with the ability to increase and decrease by the given amount" + "contract": {"name": "Counter", "description": "A contract that maintains a counter with the ability to increase and decrease by the given amount"}, + "version": "1.0" } ``` and the generated `metadata` method looks like: ```typescript export function metadata(): string { - return '{"methods": [{"name": "getCounter", "parameters": [{"name": "amount", "type": "i32"}], "returnType": "i32", "methodType": "view"}, {"name": "incrementCounterBy", "parameters": [{"name": "amount", "type": "i32"}], "methodType": "change"}, {"name": "decrementCounterBy", "parameters": [{"name": "amount", "type": "i32"}], "returnType": "void"}], "classes": [], "contract": "A contract that maintains a counter with the ability to increase and decrease by the given amount"' + return '{"methods": [{"name": "getCounter", "parameters": [{"name": "amount", "type": "i32"}], "returnType": "i32", "stateMutability": "view"}, {"name": "incrementCounterBy", "parameters": [{"name": "amount", "type": "i32"}], "stateMutability": "change"}, {"name": "decrementCounterBy", "parameters": [{"name": "amount", "type": "i32"}]}], {"name": "description", "parameters": [], "returnType": "string", "stateMutability": "view"}, "classes": [], "contract": {"name": "Counter", "description": "A contract that maintains a counter with the ability to increase and decrease by the given amount"}, "version": "1.0"}' } ``` -## Real-world Example -Now let's consider a real-world example that involves more features such as class and arrays. +## A More Complex Example +Now let's consider a more complex example that involves more features such as class and arrays. Suppose one wants to build a todo list app on blockchain, which is modeled as ```typescript export class Todo { @@ -148,6 +226,11 @@ export function getAllTodos(): Array { } return loaded; } + +@view +export function description(): string { + return '{"name": "Todo list", "description": "A todo list on blockchain!"}' +} ``` For this contract, `setTodo` is a change method while `getTodo` and `getAllTodos` are view methods. @@ -159,19 +242,25 @@ In this case, the metadata we want looks like "name": "setTodo", "parameters": [{"name": "id", "type": "string"}, {"name": "todo", "type": "Todo"}], "returnType": "void", - "methodType": "change" + "stateMutability": "change" }, { "name": "getTodo", "parameters": [{"name": "id", "type": "string"}], "returnType": "Todo", - "methodType": "view" + "stateMutability": "view" }, { "name": "getAllTodos", "parameters": [], "returnType": "Array", - "methodType": "view" + "stateMutability": "view" + }, + { + "name": "description", + "parameters": [], + "returnType": "string", + "stateMutability": "view" } ], "classes": [ @@ -180,7 +269,8 @@ In this case, the metadata we want looks like "fields": [{"name": "id", "type": "string"}, {"name": "title", "type": "string"}, {"name": "completed", "type": "bool"}] } ], - "contract": "a contract that implements a todo list on blockchain" + "contract": {"name": "Todo list", "description": "A todo list on blockchain!"}, + "version": "1.0" } ``` @@ -188,10 +278,9 @@ In this case, the metadata we want looks like # Reference-level explanation [reference-level-explanation]: #reference-level-explanation -To implement this NEP, we just need to modify the binding generation to generate a method called `metadata` that returns -json serialization of contract metadata described in the previous section. This involves walking through the exported methods -in `main.ts`, get the metadata of each method, and serialize them in json. Metadata of a given method, including decorators, -are easily extractable from the assemblyscript IR and serialized into json format. +To implement this NEP, we need to modify the binding generation procedure to generate a method called `metadata` that returns +json serialization of contract metadata described in the previous section. This involves an AST walk to collect the relevant + information about functions and classes, as well as mapping types to the types used in metadata. # Drawbacks [drawbacks]: #drawbacks @@ -208,11 +297,10 @@ It is unclear to me what the alternative is. [unresolved-questions]: #unresolved-questions * What other information, besides those mentioned in [guide-level-explanation], should we include in the metadata? -* Most of this NEP is concerned with contract metadata for assemblyscript, for the rust API, it is not yet unclear what -needs to be done given that it is not yet stabilized. # Future possibilities [future-possibilities]: #future-possibilities Under the framework proposed in this NEP, it is also not difficult to add annotations to methods in natural language. -We can also add contract-level annotation as part of the contract metadata json. +Another interesting possibility is that for data validation, the contract can provide some predicate (in javascript for example) +to the frontend to validate input data. From 301eeb3f6c014f6d9d64ef62e3e6cd3b7078c14c Mon Sep 17 00:00:00 2001 From: Bowen Wang Date: Fri, 20 Sep 2019 16:15:47 -0700 Subject: [PATCH 10/10] Update typedarray --- text/0000-contract-metadata.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/text/0000-contract-metadata.md b/text/0000-contract-metadata.md index 005198cc1..b1845dd12 100644 --- a/text/0000-contract-metadata.md +++ b/text/0000-contract-metadata.md @@ -56,7 +56,9 @@ For types, we try to be as general and language agnostic as possible. Therefore * `u8`, `u16`, `u32`, `u64`, `u128` * `string` -* `byteArray`: an array of bytes. In Rust this is `[u8]` whereas in AssemblyScript this is typedarrays. +* `TypedArray`: Fixed length array. In Rust this is something like `[u8]` or `[u32]` whereas in AssemblyScript this is something like +`Uint8Array`. For the sake of convenience we use the Rust notation here. So `Uint8Array` would be represented as `[u8]` and +`Uint32Array` would be represented as `[u32]`, etc. * `Option`: Either none or some value. In Rust this is `Option` whereas in AssemblyScript this is `T | null` provided that `T` is nullable. * `Seq`: A variably sized homogeneous sequence of values, for example `Vec` or `HashSet` in Rust, `Array` in @@ -278,9 +280,10 @@ In this case, the metadata we want looks like # Reference-level explanation [reference-level-explanation]: #reference-level-explanation -To implement this NEP, we need to modify the binding generation procedure to generate a method called `metadata` that returns -json serialization of contract metadata described in the previous section. This involves an AST walk to collect the relevant - information about functions and classes, as well as mapping types to the types used in metadata. +To implement this NEP, we need to modify the binding generation procedure in the assemblyscript compiler to generate a method called `metadata` that returns +json serialization of contract metadata described in the previous section. In Rust we would need to augment the procedural macro to inject the metadata method. +Both involve an AST walk to collect the relevant information about functions and classes, as well as mapping types to the types used in metadata. + # Drawbacks [drawbacks]: #drawbacks