For detailed docs on phat_js , go here for the latest.
What Can You Do With Your Phat Contract?
In the README.mdlink, you learned how to generate a new default function template and execute the 3 separate ways to test and validate your the results of the function. Now we will dive into what you can do with your function to extend the capabilities.
What you will learn:
Available Capabilities of @phala/pink-env
Call into a contract (Phat Contract).
Invoke a delegate call on a contract code by a code hash (Phat Contract).
Send an HTTP request and returns the response as either a Uint8Array or a string.
First you will need to install the @phala/fn CLI tool using your node package manager (npm) or use node package execute (npx). In this tutorial we use npx.
Now create your first template with the CLI tool command:
npx@phala/fninitexample
We currently have only one template. Just press enter to see something similar to the example below:
npx @phala/fn init example
# ? Please select one of the templates for your "example" project: phala-oracle-consumer-contract. Polygon Consumer Contract for LensAPI Oracle
# Downloading the template: https://github.com/Phala-Network/phala-oracle-consumer-contract... ✔
# The project is created in ~/Projects/Phala/example
cd into the newly created template and ls the directory which will look similar to below.
Lastly, we will cd into ./src where the index.ts file resides. This file will be where we customize our function logic.
cdsrc
Available Capabilities of @phala/pink-env
In the GETTING_STARTED.md we introduced the basic functionality of making a single HTTP request to Lens API. The example code can be seen below:
functionfetchApiStats(lensApi:string, profileId:string):any {// profile_id should be like 0x0001let headers = {"Content-Type":"application/json","User-Agent":"phat-contract", };let query =JSON.stringify({ query:`query Profile { profile(request: { forProfileId: "0x01" }) { stats { followers following comments countOpenActions posts quotes mirrors publications reacted reactions } } }`, });let body =stringToHex(query);//// In Phat Function runtime, we not support async/await, you need use `pink.batchHttpRequest` to// send http request. The function will return an array of response.//let response =pink.batchHttpRequest( [ { url: lensApi, method:"POST", headers, body, returnTextBody:true, }, ],2000 )[0];if (response.statusCode !==200) {console.log(`Fail to read Lens api with status code: ${response.statusCode}, error: ${response.error ||response.body}}` );throwError.FailedToFetchData; }let respBody =response.body;if (typeof respBody !=="string") {throwError.FailedToDecode; }returnJSON.parse(respBody);}
Here we utilize the pink.batchHttpRequest() function, but we only use a single HTTP request. Before going any further, let's clarify what is available with @phala/pink-env.
pink.invokeContract() allows for a call to a specified address of a Phat contract deployed on Phala's Mainnet or PoC6 Testnet depending on where you deploy your function.
pink.invokeContractDelegate() is similar but instead the call on a Phat Contract is targeted by the code hash.
This is the low-level API for cross-contract call. If you have the contract metadata file, there is a script to help generate the high-level API for cross-contract call. For example run the following command:
Now you may need to call multiple APIs at once, this would require you to use the pink.batchHttpRequest() function to ensure you do not timeout (timeouts for Phat Contract is 10 seconds) on your response. The args and returned Object are the same as pink.httpRequest(), but instead you can create an array of HTTP requests within the function. Since we have an example above of how to use a pink.batchHttpRequest(), before an examples let's look at the syntax. You will have to define your array of args:
url: string – The URL to send the request to.
method?: string – (Optional) The HTTP method to use for the request (e.g. GET, POST, PUT). Defaults to GET.
headers?: Headers – (Optional) An map-like object containing the headers to send with the request.
body?: Uint8Array | string – (Optional) The body of the request, either as a Uint8Array or a string.
returnTextBody?: boolean – (Optional) A flag indicating whether the response body should be returned as a string (true) or a Uint8Array (false).
[x] - this value is what you will see below as [0] which points to index 0 in the array of HTTP requests.
timeout_ms?: number - (Optional) a number representing the number of milliseconds before the batch HTTP requests timeout. Returned is the Object response from the HTTP request containing the following fields:
{number} statusCode - The HTTP status code of the response.
{string} reasonPhrase - The reason phrase of the response.
{Headers} headers - An object containing the headers of the response.
{(Uint8Array|string)} body - The response body, either as a Uint8Array or a string depending on the value of args.returnTextBody.
error?: string - (Optional) The error string that will be mapped to the error corresponding to the index of the HTTP request in the batch HTTP requests.
[x] - this value is what you will see below as [0] which points to index 0 in the array of HTTP requests.
Let's create a unique example. In this example, we will:
Take response body of The Odds API query and send to a Telegram Group in a single pink.httpRequest()
constsportName='baseball_mlb'const odds_http_endpoint = `https://api.the-odds-api.com/v4/sports/${sportName}/scores/?apiKey=37af51c4c3d1823308ae2966bcfe7`;
constkvdb_http_endpoint=`https://kvdb.io/AwA4DS6fJN69q4erVyjKzY`;const tg_bot_http_endpoint = `https://api.telegram.org/bot4876363250:A1W7F0jeyMmvJAGd7K_12y_5qFjbXwPgpTQ/sendMessage?chat_id=-1001093498619&text=`;
// headers for the HTTP request argslet headers = {"Content-Type":"application/json","User-Agent":"phat-contract",};// Create body for updating kvdb.ioconstkvdbUpdate=JSON.stringify({"txn": [ {"set":"hello","value":"world"} ]});constbody2=stringToHex(kvdbUpdate);// Notice that depending on the number of queries, you will define and array of responses from the response.const [res1,res2] =pink.batchHttpRequest([ { url: odds_http_endpoint, method:"GET", headers, returnTextBody:true, }, { url:`${kvdb_http_endpoint}/hello`, method:"POST", headers: headers2, body: body2, returnTextBody:true, }]);// Notice that the single HTTP request uses the response data from the first HTTP request in the batchHttpRequest function.
constres3=pink.httpRequest({ url:`${tg_bot_http_endpoint}${res1.body}`, method:"POST", headers, returnTextBody:true,});
Here are the expected result of executing this:
KV DB on kvdb.io
Telegram Bot Updates Telegram Group
Pretty nifty, right? This is the power of the customized function with the ability to make single or batch HTTP requests. However, this example is missing some error handling which is our next topic.
Error Handling
To add some error handling to an HTTP request, you can check the default example with the query to Lens API above.
JS: Some error ocurred: TypeError: invalid value for field 'method'
pink.deriveSecret()
pink.deriveSecret() takes in a salt of either UInt8Array | string and generates a secret key response of type UInt8Array.
Let's build an example that will derive a secret from a salt howdy and update the Telegram group from above about the secret.
const tg_bot_http_endpoint = `https://api.telegram.org/bot4876363250:A1W7F0jeyMmvJAGd7K_12y_5qFjbXwPgpTQ/sendMessage?chat_id=-1001093498619&text=`;
// headers for the HTTP request argslet headers = {"Content-Type":"application/json","User-Agent":"phat-contract",};constres3=pink.httpRequest({ url:`${tg_bot_http_endpoint}shhhhhhh\nthis_is_a_secret:\n[${secret}]`, method:"POST", headers, returnTextBody:true,});
Here is the result 😜
pink.hash()
pink.hash() generates a hash based on the following params:
algorithm- the hash algorithm to use. Supported values are “blake2b128”, “blake2b256”, “sha256”, “keccak256”.
message – The message to hash, either as a Uint8Array or a string.
Let's create an example to hash the values of hello and world to store in the KVDB we used earlier. We can also send the mapping to Telegram group to show a pink.batchHttpRequest().
constkvdb_http_endpoint=`https://kvdb.io/AwA4DS6fJN69q4erVyjKzY`;const tg_bot_http_endpoint = `https://api.telegram.org/bot4876363250:A1W7F0jeyMmvJAGd7K_12y_5qFjbXwPgpTQ/sendMessage?chat_id=-1001093498619&text=`;
// headers for the HTTP request argslet headers = {"Content-Type":"application/json","User-Agent":"phat-contract",};// Generate a hash for each algorithm for helloconstblake2b128Hello=pink.hash('blake2b128','hello');constblake2b256Hello=pink.hash('blake2b256','hello');constsha256Hello=pink.hash('sha256','hello');constkeccak256Hello=pink.hash('keccak256','hello');consttgText=JSON.stringify({ blake2b128Hello: blake2b128Hello, blake2b256Hello: blake2b256Hello, sha256Hello: sha256Hello, keccak256Hello: keccak256Hello});// KV Update BodyconstkvdbUpdate=JSON.stringify({"txn": [ {"set":"blake2b128Hello","value":`${blake2b128Hello}`}, {"set":"blake2b256Hello","value":`${blake2b256Hello}`}, {"set":"sha256Hello","value":`${sha256Hello}`}, {"set":"keccak256Hello","value":`${keccak256Hello}`} ]});constbody2=stringToHex(kvdbUpdate);// Batch HTTP requestconst [res1,res2] =pink.batchHttpRequest([ { url:`${kvdb_http_endpoint}/hello`, method:"POST", headers: headers2, body: body2, returnTextBody:true, }, { url:`${tg_bot_http_endpoint}\n${tgText}`, method:"POST", headers, returnTextBody:true, }]);
Let's see how the results look.
KVDB hashes for hello
Telegram bot sends hashes for hello
SCALE Codec
Let’s introduce the details of the SCALE codec API which is not documented in the above link.
The SCALE codec API is mounted on the global object pink.SCALE which contains the following functions:
In the above example, we use the following type definition:
Hash=[u8;32]
Info={hash:Hash,size:u32}
where we define a type Hash which is an array of 32 bytes, and a type Info which is a struct containing a Hash and a u32.
The grammar is defined as follows:
Each entry is type definition, which is of the form name=type. Where name must be a valid identifier, and type is a valid type expression described below.
Type expression can be one of the following:
Type Expression
Description
Example
JS type
bool
Primitive type bool
true, false
u8, u16, u32, u64, u128, i8, i16, i32, i64, i128
Primitive number types
number or bigint
str
Primitive type str
string
[type;size]
Array type with element type type and size size.
[u8; 32]
Array of elements. (Uint8Array or 0x prefixed hex string is allowed for [u8; N])
[type]
Sequence type with element type type.
[u8]
Array of elements. (Uint8Array or 0x prefixed hex string is allowed for u8)
(type1, type2, ...)
Tuple type with elements of type type1, type2, …
(u8, str)
Array of value for inner type. (e.g. [42, 'foobar'])
{field1:type1, field2:type2, ...}
Struct type with fields and types.
{age:u32, name:str}
Object with field name as key
<variant1:type1, variant2:type2, ...>
Enum type with variants and types. if the variant is a unit variant, then the type expression can be omitted.
<Success:i32, Error:str>, <None,Some:u32>
Object with variant name as key. (e.g. {Some: 42})
@type
Compact number types. Only unsigned number types is supported
@u64
number or bigint
Generic Type Support
Generic parameters can be added to the type definition, for example:
Vec<T>=[T]
Option Type
The Option type is not a special type, but a vanilla enum type. It is needed to be defined by the user explicitly. Same for the Result type.
Option<T>=<None,Some:T>
Result<T,E>=<Ok:T,Err:E>
There is one special syntax for the Option type:
Option<T>=<_None,_Some:T>
If the Option type is defined in this way, then the None variant would be decoded as null instead of {None: null} and the Some variant would be decoded as the inner value directly instead of {Some: innerValue}. For example:
Note: @phala/ethers will be no longer be maintained in favor of native support of viem.
Why viem
viem is a TypeScript Interface for Ethereum that provides low-level stateless primitives for interacting with Ethereum. An alternative to ethers.js and web3.js with a focus on reliability, efficiency, and excellent developer experience.
Using native viem in Phat Contract 2.0 to handle EVM Smart Contract encoding/decoding helps reduce the friction of maintaining custom code. For more information on the viem's why, check out the link below.
For docs on how to use viem, check out the ABI section of the viem docs.
Note: @phala/ethers is no longer being maintained. Instead use the latest version of viem to handle the EVM Smart Contract ABI encoding/decoding.
In the index.ts file of your Phat Contract starter kit, there is an npm package available called @phala/ethers and your file will import Coders which has the following types available.
As a developer you can utilize these types in many ways. Here are some examples of how to handle each type with the TypeScript EncodeReply() function on the Phat Contract side and _onMessageReceived()on the Solidity Smart Contract side.
Static arrays can be created by defining a number > 0. as the length parameter in the Coders.ArrayCoder(coder: Coder, length: number, localName: string) function.
decodeRequest(abiParams, request) - decodes the action requestHexString that is passed into the main(request, secrets) entry function of the Phat Contract. You can find the expected encoded HexString in the OracleConsumerContract.solrequest(string calldata reqData) function where the action is encoded with a uint id and string reqData in the _pushMessage(abi.encode(id, reqData)) function.
encodeReply(abiParams, reply) - encodes the action reply to be sent back to the Consumer Contract on the EVM change. The Consumer Contract consumes the action reply via the _onMessageReceived(bytes calldata action) function where data encoded can be decoded and handled based on the Consumer Contract logic. The default example encodes a tuple of uint respType, uint id, and uint256 data that in turn gets decoded in OracleConsumerContract.sol_onMessageReceived(bytes calldata action) function with (uint respType, uint id, uint256 data) = abi.decode(action, (uint, uint, uint256).
Let's breakdown the example and step through the process.
encode action request in request(string calldata) of OracleConsumerContract.sol
decode action request when parsing the encoded action in the Phat Contract 2.0 main(request, secrets) entry function in the src/index.ts file.
encode the action reply in the Phat Contract to send to the OracleConsumerContract.sol
decode the action reply in the OracleConsumerContract.sol function _onMessageReceived(bytes calldata action)
Here is a snippet of the code to encode a tuple that includes the uint id representing the request id and string calldata reqData that is the request data string.
For the example, we will pass in:
id = 1
reqData = "0x01"
OracleConsumerContract.sol
functionrequest(stringcalldata reqData) public {// assemble the requestuint id = nextRequest; requests[id] = reqData;_pushMessage(abi.encode(id, reqData)); nextRequest +=1;}
This will produce a HexString0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000043078303100000000000000000000000000000000000000000000000000000000 representing (1, "0x01")
In the src/index.ts file, we handle the decoding by calling the decodeRequest(abiParams, request) to parse the expected variables of id equal to 1 and reqData equal to "0x01".
Notice that we have error handling to ensure that the request can be decoded or else the error will be sent back to the Consumer Contract with encodeReply(abiParams, reply).
Once the results are computed in the Phat Contract, the action reply is composed and sent via the encodeReply(abiParams, reply) function. In this example, we query the Lens v2 API to get profile id "0x01" and returns the total posts back as part of the encoded reply.
The expected posts count at the time of writing was 201 so the logs should print something similar to below where 0 equals TYPE_RESPONSE, 1 equals to the request id, and 201 is the total posts from profile "0x01"
The encoded action reply is lastly handled by the OracleconsumerContract.sol in the _onMessageReceived(bytes calldata action) with the expected value of action being 0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000c9 which is decoded to equal (0, 1, 201). This can be seen with the example below.
Congratulations! You now possess the power to extend the functionality of your functions in many unique ways. If this sparks some ideas that require some extensive functionality that is not supported in @phala/pink-env, jump in our discord, and we can help you learn a little rust to build some Phat Contracts with the Rust SDK then leverage the functions pink.invokeContract() & pink.invokeContractDelegate() to make calls to the Rust SDK deployed Phat Contracts.