When writing the Uniswap v3 wrapper, our goal was to provide the same user experience as the SDK. The wrapper provides feature-parity, and the "business logic" is the same. We also ported elements of the Uniswap SDK Core package, as necessary, to implement the v3 wrapper.
The best way to set up a Polywrap project is to start with one of the project templates available in the Polywrap CLI. The
w3 create command lets you bootstrap your project structure without effort.
The initial project setup includes a
mutation folder and a
query folder within
src, which correspond to the two types of modules a wrapper can have.
It also includes a
web3api.yaml manifest file, a
web3api.build.yaml build manifest file, and a
web3api.meta.yaml meta manaifest file. The
web3api.yaml manifest tells the Polywrap CLI what language your wrapper is in, where your module schemas are located, and more. Our
web3api.yaml looked like this:
The build manifest lets you customize the build process. The meta manifest lets you add meta-data to your project, like a description and a link to your repo.
For the Uniswap v3 wrapper, we left the
web3api.yaml manifest and the build manifest unchanged. We added detail to the meta manifest much later, when wrapper development was largely complete.
A wrapper can have two modules: a
query module and a
mutation module. Each module has its own schema that, along with a an optional
common schema for shared types, are combined at build time. The difference between mutations and queries is simple: mutations modify state--this typically means blockchain state in web3 applications--while queries do not. The Uniswap v3 SDK does not modify on-chain state, so all of its functionality was placed in the
query module schema.
The first draft of the Uniswap v3 wrapper's schema was written in just a few hours, though it was revised during development to fix mistakes and improve the user experience.
When porting an SDK, it's important to understand its project structure. The organization of the SDK's soure code can indicate how wrapper development should proceed. Development should allow for iterative changes and testing.
The Unsiwap v3 SDK can be mentally modularized into a roughly linear set of dependent components. We can start with the concept of a
Token, which is the component of a
CurrencyAmount and a
Route is a set of pools and currencies. A
Trade is constructed from two currency amounts and one or more routes. Based on this pattern, it made sense for us to start with
address, as well as two class methods:
Using the Polywrap CLI's
codegen command, we generated AssemblyScript classes corresponding to each type we defined in the schema. This was as simple as typing
codegen command simulatenously generates another flavor of AssemblyScript class: function inputs. An
Input_* class is generated for each function, where
* is the name of the function. The classes have properties corresponding to the arguments defined in the schema. These
Input_* classes are used as inputs to the functions declared in the GraphQL schema.
The Polywrap CLI places the generated files in directories named
w3, which can be found within each module folder (as declared in your
web3api.yaml manifest). From there you can implement and use them.
Once we generated the classes, we imported the generated types and implemented the functions just as we found them in the Uniswap SDK. The function signatures match the schema definitions we declared earlier.
After implementing the token functions, it was possible to build the project (after commenting out methods in the schema that had not yet been implemented) and write the first automated tests.
The Uniswap v3 wrapper imports external dependencies to help it with certain tasks. A wrapper can import other wrappers or plugins.
One of the most important dependencies we used is Polywrap's Ethereum plugin. The Ethereum plugin is based on the popular
ethers.js package. It can be used to prepare and send Ethereum transactions in much the same way.
ethers.js to encode calldata. We can do the same with the Ethereum plugin.
src/query/schema.graphql schema declares several imports at the top of the file. Among these is the Ethereum plugin, which is included in the Polywrap client by default.
Once imports are declared, we can run the
codegen command of the Polywrap CLI to generate imported modules and types. The imported module class includes all of the methods declared in its own GraphQL schema. If we want to know what's in it, we might look there first.
We used the Ethereum plugin's
encodeFunction method to encode calldata for Uniswap's Multicall smart contract.
Polywrap schemas support additional default types beyond those found in standard GraphQL. The
BigInt type is used in the Uniswap v3 wrapper to represent integers larger than 32 bits. Since Ethereum supports unsigned integers as large as 256 bits, we needed to support them as well.
BigInt type looks like a standard GraphQL type in the schema. In AssemblyScript, the type is received as an instance of the
BigInt class from in the as-bigint AssemblyScript package.
Other base schema types include
Map<T,U>. These types, along with
BigInt, can be imported directly into AssemblyScript modules from the
We adapted all of the tests in Uniswap's SDK to work with the wrapper. This ensured that the wrapper met at least the same standards of quality the Uniswap team expected of their SDK. The Uniswap team tested their SDK with artificial data that allowed them to calculate the expected results and compare those results to the outputs of their code. We used the same test cases and expected the same results from our wrapper.
We also wrote tests based on real-world data, using a fork of the Ethereum Mainnet network, to compare the results of our wrapper queries with results produced by the SDK. This helped us test the wrapper with input of greater complexity.
We wrote automated tests using two different testing frameworks:
as-pect is an AssemblyScript testing framework, and that is why we used it. Unit tests written in the native language of the wrapper can be used to test classes and functions that are written to support the main wrapper code. This reduces the layers of complexity that would be associated with testing only the functions declared in our GraphQL schema.
For example, we wrote a
PriorityQueue class to sort trades for the
bestTradeExactOut functions. We used
as-pect to test it. This simplified testing and debugging for
We wrote many of our other tests in
as-pect as well, in part because it was straightforward to copy and paste test scripts from the Uniswap v3 SDK repo and adapt the syntax.
One quirk with
as-pect is that the following must be added to its configuration file to get it working with Polywrap.
Not all tests can be written in the wrapper's native language, nor should they be. Code that depends on other wrappers or plugins must be tested by making calls to the Polywrap Client. The Client coordinates inter-API communication.
jest. Were we to write the Uniswap v3 wrapper again, we would actually use a lot less
as-pect and a lot more
One advantage of testing with
jest is that it requires developers to make calls in the same way users of their wrappers are likely to make them. A disadvantage is that it requires developers to set up the Polywrap client and a test environment, which is easy but takes more time.
The Polywrap CLI can automatically generate TypeScript types using the
w3 app command. The types mirror those declared in your GraphQL schema.
If you love brevity, you can write functions that "wrap" your wrapper calls. This can make your tests a bit easier to read.
As a final touch, we generated ample documentation for the Uniswap v3 wrapper.
Polywrap's GraphQL parser can read documentation comments (comments with triple quotes) from the wrapper's GraphQL schema. Using this capability, Polywrap built a tool to help developers create documentation for their wrappers.
The Polywrap CLI will soon be able to use GraphQL schemas to automatically generate markdown that is compatible with popular documentation tools like Docusaurus. We tested the tool to generate the reference documentation for the Uniswap v3 wrapper.