Everything on GCScript are function calls. A main function call, and itโs nested function calls. Recursively.
In GCScript a function call is a JSON object with a type
property. Itโs value is the name of the function. All the other properties are argument names defined function declarations, and their values are the argument values passed to each function.
Example of a function call without arguments:
{
"type": "getCurrentAddress",
}
The getCurrentAddress function returns current wallet address and has no arguments.
Example of a function call with an argument named value
:
{
"type":"data",
"value":[
"foo",
"bar",
"baz",
true,
null,
{"nested":{"object":{"inside":"here"}}}
]
}
The data function returns the JSON type passed as value
argument, and allows you to define a constant.
In GCScript a container structure with nested function calls, a block of code, is defined by calling the script
function.
The body, the nested code, is a key-value map (or list) of function calls passed on the run
argument.
Example of a block of code:
{
"type": "script",
"run":{
"result01":{"type":"data", "value":"A"},
"result02":{"type":"data", "value":"B"},
"result03":{"type":"data", "value":"C"}
}
}
But this is the result of that code:
{
"exports": {}
}
Why exports
is empty? Have I done something wrong?
Locally, a block of code, (and descendants) has a cache, a local scope memory space where child function calls store their results.
This cache
object is isomorphic (unless instructed otherwise) with the code structure itself, it means it keeps the same structure as the block of code that populates it with results during runtime execution.
Globally, the entire script has a unique exports
object, and only what gets stored there is going to be returned back to dapp or caller agent.
Each block of code can export itโs cache
contents into this exports
object.
So in other terms, in order to return data from script execution back to a dapp for example, data needs to be exported from the local cache
s into the unique global exports
object. This can be done by passing an exportAs
property with a name for those exports, like this:
Example of a block of code with exports:
{
"type": "script",
"exportAs":"myFirstExport",
"run":{
"result01":{"type":"data", "value":"A"},
"result02":{"type":"data", "value":"B"},
"result03":{"type":"data", "value":"C"}
}
}
and now these are the expected results:
{
"exports": {
"myFirstExport":{
"result01":"A",
"result02":"B",
"result03":"C"
}
}
}
Finally, at root level there is always a script
function call containing all the others functions nested inside, like the usual main()
function in C language.
This root script
function call has some unique superpowers, for example it can instruct the wallet where to send the results back, this can be done by setting the returnURLPattern
property:
Example of a block of code exporting data back to a URL:
{
"type": "script",
"exportAs":"myFirstExport",
"returnURLPattern":"https://google.com/dappCallExecutionResults/{result}/view",
"run":{
"result01":{"type":"data", "value":"A"},
"result02":{"type":"data", "value":"B"},
"result03":{"type":"data", "value":"C"}
}
}
and then results will be packed and shared back to that dapp by redirecting the user to the produced URL
"https://google.com/dappCallExecutionResults/1-H4sIAAAAAAAAA6tWSq0oyC8qKVayqlbKrXTLLCoucQWLgASKUotLc0oMDJWslByVdGBcIyDXCcE1BnKdlWprawHx4ftoTAAAAA/view"
Important: this redirection only gets enabled if initial dapp->wallet communication was established from an https
origin.
We can understand isomorphism better by nesting some code blocks and check results.
Example of isomorphism with nested blocks of code:
{
"type": "script",
"exportAs":"myNestedExport",
"run":{
"groupA":{
"type": "script",
"run":{
"result01":{"type":"data", "value":"A"},
"result02":{"type":"data", "value":"B"},
"result03":{"type":"data", "value":"C"}
}
},
"groupB":{
"type": "script",
"run":{
"result01":{"type":"data", "value":"A"},
"result02":{"type":"data", "value":"B"},
"result03":{"type":"data", "value":"C"}
}
},
"groupC":{
"type": "script",
"run":{
"result01":{"type":"data", "value":"A"},
"result02":{"type":"data", "value":"B"},
"result03":{"type":"data", "value":"C"}
}
}
}
}
and the isomorphic results:
{
"exports": {
"myNestedExport": {
"groupA": {
"result01": "A",
"result02": "B",
"result03": "C"
},
"groupB": {
"result01": "A",
"result02": "B",
"result03": "C"
},
"groupC": {
"result01": "A",
"result02": "B",
"result03": "C"
}
}
}
}
Usually GCScript turns lists into key-value maps to make lists and maps equally valid.
Example of a block of code as a list:
{
"type": "script",
"exportAs":"myWalletData",
"run":[
{"type":"getName"},
{"type":"getCurrentAddress"},
{"type":"getCurrentSlot"}
]
}
๐ See also: script
The getName function returns current wallet name and has no arguments.
The getCurrentAddress function returns current wallet address and has no arguments.
The getCurrentSlot function returns current blockchain slot number and has no arguments.
Now because underneath lists are normalized into key-value maps with item indexes as map keys, we can do this:
Example of a block of code as a key-value map:
{
"type": "script",
"exportAs":"myWalletData",
"run":{
"0":{"type":"getName"},
"1":{"type":"getCurrentAddress"},
"2":{"type":"getCurrentSlot"}
}
}
๐ See also: getName, getCurrentAddress, getCurrentSlot, script
And in both cases, results will be exactly the same:
{
"exports": {
"myWalletData": {
"0": "Default",
"1": "addr_test1qqptd68g4yzkrtn38srqvkk78c3stkvw4wvvsqsmaanny7adqwj2u3djrag0mene2cm9elu5mdqmcz9zc2rzgq7c5g6q5rgrg5",
"2": "54762728"
}
}
}
GCScript normalize lists as key-value map, because among other reasons, key-value maps are the preferred structure on this language. Why you may ask?
Well because of the great self-documenting properties a block of code expressed as key-value map has to offer.
Letโs self document this code better:
Example of a self-documented block of code as key-value map:
{
"type": "script",
"exportAs":"myWalletData",
"run":{
"name": {"type":"getName"},
"address": {"type":"getCurrentAddress"},
"slot": {"type":"getCurrentSlot"}
}
}
๐ See also: getName, getCurrentAddress, getCurrentSlot, script
and this time results are, well, self-documented and isomorphic:
{
"exports": {
"myWalletData": {
"name": "Default",
"address": "addr_test1qqptd68g4yzkrtn38srqvkk78c3stkvw4wvvsqsmaanny7adqwj2u3djrag0mene2cm9elu5mdqmcz9zc2rzgq7c5g6q5rgrg5",
"slot": "54762728"
}
}
}
From last example, we can say that the object properties name
,address
and slot
are variables, and each one will store the result of each of itโs functions being called.
To be more specific GCScript is actually doing underneath something like this javascript code:
let exports={}
function script(exportAs){
let cache={}
cache["name" ]= getName();
cache["address" ]= getCurrentAddress();
cache["slot" ]= getCurrentSlot();
if(exportAs!==undefined)
exports[exportAs]=cache;
return cache
}
Results of each function call within a block (or descendants blocks ) will be stored on an internal cache
object, or local memory space.
Remember that only when you use the exportAs
property you are telling the interpreter to export this local state to the outer world.
Understanding this internal logic is important because this will be the key for you to access these variables/results for later reuse and to break the default isomorphism in order to customize your results.
โToday I woke up and decided that I hate your isomorphism and I want to cherry-pick my exports carefully, leaving behind some data. Can i break the default isomorphism?โ
Well yes, you can!
The solution is to set the mode in which each script
function returns itโs results from itโs own cache
memory. This can be done by setting the return
property.
The return
property can take these options and each one alters the code block results in different ways:
Option | Description |
---|---|
{"mode":"all"} |
Will return all it children code block results. This is the default isomorphic behavior |
{"mode":"none"} |
Will return undefined , and because is not a valid JSON value it will be purged. Like a function():void; in typescript |
{"mode":"first"} |
Will return the result of itโs first child code block. |
{"mode":"last"} |
Will return the result of itโs last child code block. |
{"mode":"one", "key":"<CHILD_KEY>"} |
Will return the result of one child code block, the one in the key name or position argument. |
{"mode":"some","keys":[<CHILD_KEY1>,<CHILD_KEY2>,...,<CHILD_KEY_N>]} |
Will return the result of some children code blocks, the ones in the keys name or position list argument. |
{"mode":"macro","exec":"<ISL>"} |
Will return the result of the execution of an inline scripting language macro. Useful for formatting, debugging results |
Lets explore all these on an example, but we will leave explanations for the macro
mode for later.
{
"type": "script",
"title": "Return modes example",
"description": "Learning all the return modes",
"return": {
"mode": "all"
},
"exportAs":"starfleetMuseum",
"run": {
"none": {
"type": "script",
"return": {
"mode": "none"
},
"run": {
"A": {
"type": "data",
"value": "USS Enterprise A"
},
"B": {
"type": "data",
"value": "USS Enterprise B"
},
"C": {
"type": "data",
"value": "USS Enterprise C"
},
"D": {
"type": "data",
"value": "USS Enterprise D"
},
"E": {
"type": "data",
"value": "USS Enterprise E"
}
}
},
"all": {
"type": "script",
"return": {
"mode": "all"
},
"run": {
"A": {
"type": "data",
"value": "USS Enterprise A"
},
"B": {
"type": "data",
"value": "USS Enterprise B"
},
"C": {
"type": "data",
"value": "USS Enterprise C"
},
"D": {
"type": "data",
"value": "USS Enterprise D"
},
"E": {
"type": "data",
"value": "USS Enterprise E"
}
}
},
"first": {
"type": "script",
"return": {
"mode": "first"
},
"run": {
"A": {
"type": "data",
"value": "USS Enterprise A"
},
"B": {
"type": "data",
"value": "USS Enterprise B"
},
"C": {
"type": "data",
"value": "USS Enterprise C"
},
"D": {
"type": "data",
"value": "USS Enterprise D"
},
"E": {
"type": "data",
"value": "USS Enterprise E"
}
}
},
"last": {
"type": "script",
"return": {
"mode": "last"
},
"run": {
"A": {
"type": "data",
"value": "USS Enterprise A"
},
"B": {
"type": "data",
"value": "USS Enterprise B"
},
"C": {
"type": "data",
"value": "USS Enterprise C"
},
"D": {
"type": "data",
"value": "USS Enterprise D"
},
"E": {
"type": "data",
"value": "USS Enterprise E"
}
}
},
"one": {
"type": "script",
"return": {
"mode": "one",
"key": "C"
},
"run": {
"A": {
"type": "data",
"value": "USS Enterprise A"
},
"B": {
"type": "data",
"value": "USS Enterprise B"
},
"C": {
"type": "data",
"value": "USS Enterprise C"
},
"D": {
"type": "data",
"value": "USS Enterprise D"
},
"E": {
"type": "data",
"value": "USS Enterprise E"
}
}
},
"some": {
"type": "script",
"return": {
"mode": "some",
"keys": [
"B",
"D"
]
},
"run": {
"A": {
"type": "data",
"value": "USS Enterprise A"
},
"B": {
"type": "data",
"value": "USS Enterprise B"
},
"C": {
"type": "data",
"value": "USS Enterprise C"
},
"D": {
"type": "data",
"value": "USS Enterprise D"
},
"E": {
"type": "data",
"value": "USS Enterprise E"
}
}
},
"isl": {
"type": "script",
"return": {
"mode": "macro",
"exec": "{get('cache.isl.C')}"
},
"run": {
"A": {
"type": "data",
"value": "USS Enterprise A"
},
"B": {
"type": "data",
"value": "USS Enterprise B"
},
"C": {
"type": "data",
"value": "USS Enterprise C"
},
"D": {
"type": "data",
"value": "USS Enterprise D"
},
"E": {
"type": "data",
"value": "USS Enterprise E"
}
}
}
}
}
and the results:
{
"exports": {
"starfleetMuseum": {
"all": {
"A": "USS Enterprise A",
"B": "USS Enterprise B",
"C": "USS Enterprise C",
"D": "USS Enterprise D",
"E": "USS Enterprise E"
},
"first": "USS Enterprise A",
"last": "USS Enterprise E",
"one": "USS Enterprise C",
"some": {
"B": "USS Enterprise B",
"D": "USS Enterprise D"
},
"isl": "USS Enterprise C"
}
}
}
Same like on other languages, you can pass arguments to a user-defined function or block of code and they will exist on that local scope context for the contained code to consume them.
Example of passing a single user-defined argument on a block of code:
{
"type": "script",
"args": "Hey, I'm an argument!",
"exportAs":"myWalletData",
"run":{
"name": {"type":"getName"},
"address": {"type":"getCurrentAddress"},
"slot": {"type":"getCurrentSlot"}
}
}
๐ See also: getName, getCurrentAddress, getCurrentSlot, script
also you can pass arguments differently by passing one argument to each children function call individually
Example of passing a user-defined argument per child function call on a block of code:
{
"type": "script",
"argsByKey": {
"name": "Hey, I'm name's argument!",
"address": "Hey, I'm address's argument!",
"slot": "Hey, I'm slot's argument!"
},
"exportAs":"myWalletData",
"run":{
"name": {"type":"getName"},
"address": {"type":"getCurrentAddress"},
"slot": {"type":"getCurrentSlot"}
}
}
๐ See also: getName, getCurrentAddress, getCurrentSlot, script
also both techniques work together if combined in such a way that if a child argument has not been provided to argsByKey
, args
value will be passed by default as a fallback
Example of passing a user-defined argument per child function call and a fallback on a block of code:
{
"type": "script",
"args": "Hey, I'm slot's argument!",
"argsByKey": {
"name": "Hey, I'm name's argument!",
"address": "Hey, I'm address's argument!"
},
"exportAs":"myWalletData",
"run":{
"name": {"type":"getName"},
"address": {"type":"getCurrentAddress"},
"slot": {"type":"getCurrentSlot"}
}
}
๐ See also: getName, getCurrentAddress, getCurrentSlot, script
How to access and use these arguments, the cache memory, and other topics will be addressed once you get to know how to code on GCScriptโs Inline Scripting Language (ISL).
โI canโt wait to build something on Cardano, give me all the available functions, now!โ
Ok, here you have them:
Network | API Documentation |
---|---|
Cardano Mainnet | HTML Docs |
Cardano Pre Production Testnet | HTML Docs |
Remember that GCScript DSL is a language defined by a JSON schema, so here are the latest auto-generated docs, self-hosted on the wallet itself. Schema between networks are the same but maybe some examples will change based on each one in the future.
Previous: Quick Start | Next: GCScript on Steroids: with Inline Scripting Language (ISL) |