Custom types

Emit custom types from a Lambda or Patch

Summary

When you emit a variable from sim Lambdas or Patches, you typically pick from one of the standard available types: address, bool, bytes, bytes32, int256, int64, string, uint256, uint64. If you connect a persistence, we automatically ingest these into the table using DB types that are best suited for each. Sometimes, however, you may want to emit data that doesn't fit neatly within these standard types.

To define a custom type, open the Schema types tab within any Patch or Lambda component. This tab is global within a canvas, i.e., you can enter custom types in it within any component and use them within other components.

Once in the tab, define a custom type using a struct. For instance, you could define a type for a simple array of addresses as follows:

struct AddressArray {
    address[] addresses;
}

With this type created, you can now use it in your schemas by selecting it as a type. You can edit schemas either within the IDE or by editing the component directly in the canvas.

Finally, in any callback within the lambda, you can create and emit a variable of the custom type. An example:

AddressArray memory addrArray;
addrArray.addresses = new address[](1) ;
addrArray.addresses[0] = address(0);

simEmitToSchema_mySchema(
  SchemaMySchemaColumns({
    myArray: addrArray
  })
);

❗️

Prefix custom types by Sim. in Patches

When referencing simFunctions or custom types in a Patch component, you need to prefix with Sim.. Instead of AddressArray, write Sim.AddressArray.

If you connect a schema with custom types to a persistence, the custom type data will flow into the persistence as a JSON. Apache Pinot has extensive support for querying JSON data--you can read their documentation here.

Example: Indexing Uniswap X

We ran into a use case for custom types while trying to index UniswapX trades. In simple decentralized exchanges, each swap involves exactly two tokens--the user trade's one for another--so we simply construct columns with the data we need for each of the two tokens. With UniswapX, however, a single swap may involve more than one output token.

Given the number of output tokens is variable, what we really want to do is emit an array of the output tokens. And for each token, we want to emit various details. To accommodate this, we defined custom types as follows:

struct Tokens {
    Token[] tokens;
}

struct Token {
    address tokenAddress;
    int tokenUsd;
    uint8 tokenUsdDec;
    uint256 tokenAmt;
    string tokenName;
    string tokenSymbol;
    address recipient;
}

Then, we populated variables fromTokens and toTokens using these custom types. We only really needed this capability for toTokens given that there's only ever one fromToken, but we did fromTokens also for symmetry. Because we did this within a Patch component, we prefixed the custom types with Sim..

Sim.Tokens memory fromTokens;
fromTokens.tokens = new Sim.Token ;

Sim.Tokens memory toTokens;
toTokens.tokens = new Sim.Token[](resolvedOrder.outputs.length);

Sim.Token memory fromToken;
fromToken.tokenAddress = address(resolvedOrder.input.token);
fromToken.tokenAmt = resolvedOrder.input.amount;
fromToken.tokenName = this.getName(fromToken.tokenAddress);
fromToken.tokenSymbol = this.getSymbol(fromToken.tokenAddress);
if (block.number > 12864088) {
    (fromToken.tokenUsd, fromToken.tokenUsdDec) = getChainlinkDataFeedLatestAnswer(fromToken.tokenAddress);
}
fromTokens.tokens[0] = fromToken;

for (uint i = 0; i < resolvedOrder.outputs.length; i++) {
    Sim.Token memory toToken;
    toToken.tokenAddress = address(resolvedOrder.outputs[i].token);
    toToken.tokenAmt = resolvedOrder.outputs[i].amount;
    toToken.tokenName = this.getName(toToken.tokenAddress);
    toToken.tokenSymbol = this.getSymbol(toToken.tokenAddress);
    toToken.recipient = resolvedOrder.outputs[i].recipient;
    if (block.number > 12864088) {
        (toToken.tokenUsd, toToken.tokenUsdDec) = getChainlinkDataFeedLatestAnswer(toToken.tokenAddress);
    }

    toTokens.tokens[i] = toToken;
}

You can see the rest of the code used in the Patch in the canvas. When queried, from the persistence, records in to_tokens are JSON. Here's an example where to_tokens includes a single token.

{
  "tokens": [
    {
      "tokenAddress": "514910771af9ca656af840dff83e8264ecf986ca",
      "tokenUsdDec": 8,
      "tokenSymbol": "LINK",
      "tokenUsd": "29a9e4c0",
      "tokenName": "ChainLink Token",
      "recipient": "980e5dd186eb72c92565f93817cb858ae522633f",
      "tokenAmt": "c0ffd00ebc4c2ccecc"
    }
  ]
}