The Polywrap CLI
Now that we have a basic understanding of the Polywrap Client, it's time to see how we can use the Polywrap CLI to create an amazing experience when working with Wraps.
Installation
For all possible ways to install the Polywrap CLI, please refer to its README.
There are two main ways to install the Polywrap CLI:
If you're developing in Javascript/Typescript and using Node, you can install the CLI globally:
npm i -g polywrap
Alternatively, you can use the standalone version by downloading and running its install script:
$ sh <(curl https://raw.githubusercontent.com/polywrap/cli/origin-dev/install.sh)
# Installs to `~/.polywrap`
# If polywrap is already installed, the script instead checks for updates
Overview
The Polywrap CLI allows us not only to build, test, and deploy Wraps, but also generate types for our applications which use the Polywrap Client.
This tutorial assumes that you installed polywrap
globally.
You can see all available commands by running:
polywrap help
Create a Polywrap-powered application
Polywrap allows you to integrate wraps into your app in a type-safe manner for supported languages.
Currently, Polywrap has type-safety support for:
- Typescript
- Rust
- Python
- Kotlin
- Swift
The Polywrap CLI allows you to create a template project in any of these languages with type safety built in.
Let's start with creating a new project using the Polywrap CLI:
- Typescript
- Rust
- Python
- Kotlin/Android
- Swift/iOS
polywrap create app typescript my-app
polywrap create app rust my-app
polywrap create app python my-app
polywrap create app android my-app
polywrap create app ios my-app
This will create a basic application in the language of your choice. There are two key files we want to take a look at, which define our Polywrap-powered project and allow us to perform code generation for type-safety. Let's take a look at them:
The Polywrap Manifest (polywrap.yaml
)
In order for the Polywrap CLI to know what kind of project it's working with, it needs a Polywrap Manifest file to obtain some basic information about your project. This is the polywrap.yaml
file.
It has a structure similar to this:
- Typescript
- Rust
- Python
- Kotlin/Android
- Swift/iOS
format: 0.5.0
project:
name: Sample
type: app/typescript
source:
schema: ./polywrap.graphql
format: 0.5.0
project:
name: Sample
type: app/rust
source:
schema: ./polywrap.graphql
format: 0.5.0
project:
name: Sample
type: app/python
source:
schema: ./polywrap.graphql
format: 0.5.0
project:
name: Sample
type: app/kotlin
source:
schema: ./polywrap.graphql
format: 0.5.0
project:
name: Sample
type: app/swift
source:
schema: ./polywrap.graphql
The format
property denotes the version of the Polywrap Manifest format. Under project
, you can set the name
field to the name of your application, while the type
field describes the project type, thus letting the CLI know how to interact with the application code.
Under the source
section, we have a schema
field with a path that leads to a Schema file.
The Schema File (polywrap.graphql
)
Every Polywrap project has a Schema file - it defines the types found within the project, what Wraps the project imports, and, in the context of a Wrap project, the methods that Wrap exposes.
In the context of an application project, the Schema file defines which Wraps our application imports and is used by the CLI to generate code with which we can invoke our Wraps in a type-safe manner.
Taking a look at the file, we can see import
statements:
- Typescript
- Rust
- Python
- Kotlin/Android
- Swift/iOS
#import * into Logging from "wrapscan.io/polywrap/logging@1"
#import * into Ethers from "wrapscan.io/polywrap/ethers@1.0"
#import * into Ipfs from "wrapscan.io/polywrap/ipfs-http-client@1.0"
#import * into EthersUtils from "wrapscan.io/polywrap/ethers-utils@1"
#import * into Logging from "wrapscan.io/polywrap/logger@1.0"
#import * into Ethereum from "wrapscan.io/polywrap/ethers@1.1.0"
#import * into Logging from "wrapscan.io/polywrap/logger@1.0"
#import * into Ethers from "wrapscan.io/polywrap/ethers@1.1"
An import
statement defines which Wraps we are importing, therefore using within our application.
Generating types (codegen
)
Now that we know how we can "import" Wraps into our application, we can use the codegen
command inside the Polywrap CLI to generate types that represent our Wraps which we can use within our application.
To generate types, all we need to do is run the codegen
command inside our project's root directory:
polywrap codegen
This will generate types inside a wrap
directory which you will be able to import within your application.
Introduce type-safety into your code
Now that we have our types generated, we can take a look at our sample application's main file.
Let's first take a look at some of the imports:
- Typescript
- Rust
- Python
- Kotlin/Android
- Swift/iOS
import { Ethers, Logging } from "./wrap";
use wrap::types::*;
from .wrap import EthersUtils
import wrap.Ethereum
import wrap.EthereumArgsEncodeParams
import wrap.Logging
import wrap.LoggingArgsLog
import wrap.LoggingLogLevel
// There is no need for imports in Swift, as all classes are in the app's namespace
Here we can see that we've imported Module types that represent our Wraps, according to their specified namespace. Using these types, we can invoke our Wraps in a type-safe manner, without having to repeatedly specify the Wrap URI or do any guesswork regarding invoke argument/return types:
- Typescript
- Rust
- Python
- Kotlin/Android
- Swift/iOS
async function main() {
console.log("Invoking: Logging.info(...)");
const logger = new Logging();
await logger.info({
message: "Hello there",
});
await logger.info({
message: "Hello again",
});
await logger.info({
message: "One last time...",
});
console.log("Invoking: Ethereum.encodeParams(...)");
const eth = new Ethers();
const result = await eth.encodeParams({
types: ["address", "uint256"],
values: ["0xB1B7586656116D546033e3bAFF69BFcD6592225E", "500"],
});
if (result.ok) {
console.log(`Ethers.encodeParams:\n${result.value}`);
} else {
console.log(`Error - Ethereum.encodeParams:\n${result.error}`);
}
}
let ipfs_provider = "https://ipfs.io";
let cid = "Qmc5gCcjYypU7y28oCALwfSvxCBskLuPKWpK4qpterKC7z";
let ipfs = Ipfs::new(None);
let data = ipfs.cat(&IpfsArgsCat{
cid: cid.to_string(),
ipfs_provider: ipfs_provider.to_string(),
timeout: None,
cat_options: None
}, None).unwrap();
if __name__ == "__main__":
eth = EthersUtils()
print("Invoking: EthersUtils.encodeParams(...)")
result = eth.encode_params({
"types": ["address", "uint256"],
"values": ["0xB1B7586656116D546033e3bAFF69BFcD6592225E", "500"]
})
print(f"EthersUtils.encodeParams:\n{result}")
class PolywrapDemoViewModel: ViewModel() {
// we can create a custom client
val loggerInterfaceUri = "wrapscan.io/polywrap/logger@1.0"
private val client = polywrapClient {
addDefaults()
setPackage("plugin/logger" to loggerPlugin(null))
addInterfaceImplementation(loggerInterfaceUri, "plugin/logger")
setRedirect(loggerInterfaceUri to "plugin/logger")
}
// and use the custom client to create an SDK class instance
private val logger = Logging(client)
// the client can be shared across SDK instances
private val ethereum = Ethereum(client)
// Because their lifetimes are tied to the client, SDK instances work well as extension properties
val PolywrapClient.eth
get() = ethereum
// or we can create an SDK class instance with a new client using default configuration
private val defaultEth = Ethereum()
fun polywrapDemo() = viewModelScope.launch {
Log.i("polywrapDemo","Invoking: Logging.info(...)")
logger.log(LoggingArgsLog(LoggingLogLevel.INFO, "Hello there")).getOrThrow()
logger.log(LoggingArgsLog(LoggingLogLevel.INFO, "Hello again")).getOrThrow()
logger.log(LoggingArgsLog(LoggingLogLevel.INFO, "One last time...")).getOrThrow()
Log.i("polywrapDemo","Invoking: Ethereum.encodeParams(...)")
val encodeArgs = EthereumArgsEncodeParams(
types = listOf("address", "uint256"),
values = listOf("0xB1B7586656116D546033e3bAFF69BFcD6592225E", "500")
)
val result = client.eth.encodeParams(encodeArgs)
if (result.isSuccess) {
println("Ethereum.encodeParams:\n${result.getOrThrow()}")
} else {
println("Error - Ethereum.encodeParams:\n${result.exceptionOrNull()}")
}
}
override fun onCleared() {
super.onCleared()
// remember to close clients to prevent memory leaks when you're done using them
client.close()
defaultEth.client.close()
}
}
func polywrapDemo() {
print("Invoking: Logging.info(...)")
let logger = Logging()
let logArgs = LoggingArgsLog(level: .INFO, message: "Hello there")
try? logger.log(args: logArgs)
// Ethers.encodeParams
print("Invoking: Ethers.encodeParams(...)")
let eth = Ethers()
let encodeArgs = EthersArgsEncodeParams(
types: ["address", "uint256"],
values: ["0xB1B7586656116D546033e3bAFF69BFcD6592225E", "500"]
)
do {
let encoded = try eth.encodeParams(args: encodeArgs)
print("Ethers.encodeParams:\n\(encoded)")
} catch {
let logArgs = LoggingArgsLog(level: .ERROR, message: "Error - Ethers.encodeParams: \(error)")
try? logger.log(args: logArgs)
return
}
}
This allows us to write all of our code in a type-safe manner, and allows for IDEs like VS Code to give us autocompletion suggestions via IntelliSense. Now we can explore our Wraps by simply importing them and trying them out!