0.0.6-alpha37 • Published 11 months ago

@compound-finance/quark v0.0.6-alpha37

Weekly downloads
-
License
-
Repository
-
Last release
11 months ago

Quark Client

The Quark client is a JavaScript library to quicky and easily build and invoke actions from Quark wallets. The goal of this library is to cover a large subset of actions one might want to take when using a Quark wallet, from simply wrapping Ethers transactions, to building compositions of built-in DeFi functions, to running Solidity code.

Getting Started

Start by installing this library:

npm install --save @compound-finance/quark

# yarn add @compound-finance/quark

Many commands will require a Web3 Provider, e.g. from Ethers. We will automatically detect the network and find the correct Quark Relayer. You can use this library to communicate with the Quark Relayer contract, e.g. to get your Quark wallet address:

import * as Quark from '@compound-finance/quark';

// Get the relayer (an Ethers contract)
let relayer = await Quark.getRelayer(provider);

// Note: Relayer function names end in nonce numbers to help differentiate function calls from transaction scripts

// quarkAddress25(address) returns your Quark wallet address
console.log(await relayer.quarkAddress25('0x...'))

You can also pass options when getting the relayer, which makes this synchronous e.g.

import * as Quark from '@compound-finance/quark';

// Get the relayer version 1 on arbitrum (note: only version 1 exists)
let relayer = Quark.Relayer(provider, 'arbitrum', 1);

Quark Scripts

The easiest way to get running is to simply run Quark Scripts, which are compiled Solidity scripts that ship with Quark. For instance, you can use send to wrap a standard Ethereum call. You can access these contracts directly, but usually you'll use a helper function for common actions.

Scripts come precompiled and do not require solc to run.

import * as Quark from '@compound-finance/quark';

let usdc = new ethers.Contract("0x...", [
  "function balanceOf(address owner) view returns (uint256)",
  "function decimals() view returns (uint8)",
  "function symbol() view returns (string)",
  "function transfer(address to, uint amount) returns (bool)",
  "function transferFrom(address spender, address recipient, uint amount) returns (bool)",
], provider);

let tx = await Quark.Scripts.send(provider, usdc, 'transfer', ['0x...', 100e6]);

You can also run multiple calls and you can use populateTransaction to pass in calls, e.g.

import * as Quark from '@compound-finance/quark';

let usdc = new ethers.Contract("0x...", [
  "function balanceOf(address owner) view returns (uint256)",
  "function decimals() view returns (uint8)",
  "function symbol() view returns (string)",
  "function transfer(address to, uint amount) returns (bool)",
  "function transferFrom(address spender, address recipient, uint amount) returns (bool)",
], provider);

let tx = await Quark.Scripts.send(provider, [
  usdc.populateTransaction.transfer('0x...', 100e6),
  [usdc, 'transferFrom', ['0x...', 50e6],
]);

TODO: Think about searcher script TODO: Think about other good scripts

Quark Commands

Running Ethers function

While an Ethereum transaction is usually just data sent to a smart contract, Quark transactions are transaction scripts, that is: they run EVM code. This allows Quark scripts to be extremely powerful, but sometimes you want to just do the standard thing (send a simple function call to an existing smart contract). Quark lets you easily do that by wrapping the Ethers call in a simple transaction script:

import * as Quark from '@compound-finance/quark';
import * as solc from 'solc';

let usdc = new ethers.Contract("0x...", [
  "function balanceOf(address owner) view returns (uint256)",
  "function decimals() view returns (uint8)",
  "function symbol() view returns (string)",
  "function transfer(address to, uint amount) returns (bool)",
], provider);

let command = await Quark.wrap(
  usdc.populateTransaction.approve(...), solc.compile);

console.log(`Command: ${command.description}`);
console.log(`Command YUL: ${command.yul}`);
console.log(`Command Bytecode: ${command.bytecode}`);

let tx = await Quark.exec(provider, command);

If you have raw data, you can easily send that, as well, or pass in a custom relayer, e.g.:

import * as Quark from '@compound-finance/quark';
import * as solc from 'solc';

let command = await Quark.wrap(
  { to: '0x...', data: '0x...' }, solc.compile);

console.log(`Command: ${command.description}`);
console.log(`Command YUL: ${command.yul}`);
console.log(`Command Bytecode: ${command.bytecode}`);

let tx = await Quark.exec(relayer, command);

Pipelines and Built-ins

Quark becomes more interesting when you start to pipeline actions which will run atomically. While there's no limit to what Quark scripts can do, this library provides a simple way to pipeline actions, including passing data from one step in the pipeline to the next. Pipeline steps can be wrapped Ethers calls, or native Quark "built-ins" that provide more fine-grained access to DeFi functions.

For example, here's a simple pipeline to approve and supply using native built-ins. Notice that the approval amount and the supply amount are based on reading the exact Erc20 balance of the token.

import * as Quark from '@compound-finance/quark';
import * as Erc20 from '@compound-finance/quark/builtins/erc20/arbitrum';
import * as cUSDCv3 from '@compound-finance/quark/builtins/comet/arbitrum';
import * as solc from 'solc';

let action = Quark.pipeline([
  Erc20.approve(cUSDCv3.underlying, cUSDCv3.address, Erc20.balanceOf(cUSDCv3.underlying, cUSDCv3.address)),
  cUSDCv3.supply(cUSDCv3.underlying, Erc20.balanceOf(cUSDCv3.underlying, cUSDCv3.address)),
]);

let command = await Quark.prepare(action, solc.compile);

console.log(`Command: ${command.description}`);
console.log(`Command YUL: ${command.yul}`);
console.log(`Command Bytecode: ${command.bytecode}`);

let tx = await Quark.exec(provider, command);

Note: you can also pipe using the pipe command explicitly to prevent double-reading the balance, e.g.:

import * as Quark from '@compound-finance/quark';
import * as Erc20 from '@compound-finance/quark/builtins/erc20/arbitrum';
import * as cUSDCv3 from '@compound-finance/quark/builtins/comet/arbitrum';
import * as solc from 'solc';

let action = Quark.pipeline([
  pipe(Erc20.balanceOf(cUSDCv3.underlying, cUSDCv3.address), (bal) => [
    Erc20.approve(cUSDCv3.underlying, cUSDCv3.address, bal),
    cUSDCv3.supply(cUSDCv3.underlying, bal),
  ])
]);

let command = await Quark.prepare(action, solc.compile);

console.log(`Command: ${command.description}`);
console.log(`Command YUL: ${command.yul}`);
console.log(`Command Bytecode: ${command.bytecode}`);

let tx = await Quark.exec(provider, command);

You can also perform more complex actions, like combining Uniswap and Compound, e.g.

import * as Quark from '@compound-finance/quark';
import * as Erc20 from '@compound-finance/quark/builtins/tokens';
import * as cUSDCv3 from '@compound-finance/quark/builtins/comet/arbitrum';
import * as Uniswap from '@compound-finance/quark/builtins/uniswap/arbitrum';

let action = Quark.pipeline([
  Quark.pipe(Uniswap.singleSwap(cUSDCv3.underlying, Erc20.arbitrum.uni, new Quark.Uint256(1e18)), (swapAmount) => [
    Erc20.approve(Erc20.arbitrum.uni, cUSDCv3.address, swapAmount),
    cUSDCv3.supply(cUSDCv3.underlying, swapAmount),
  ]),
]);

You can also wrap Ethers calls as a pipeline action via invoke. Note: you cannot easily pipe values from Ethers calls to other Ethers calls. Thus, you should prefer to use builtins where possible as they compose better.

import * as Quark from '@compound-finance/quark';
import { invoke, readUint256 } from '@compound-finance/quark';
import * as cUSDCv3 from '@compound-finance/quark/builtins/comet/arbitrum';

let usdc = new ethers.Contract("0x...", [
  "function balanceOf(address owner) view returns (uint256)",
  "function decimals() view returns (uint8)",
  "function symbol() view returns (string)",
  "function transfer(address to, uint amount) returns (bool)",
  "function approve(address spender, uint amount) returns (bool)",
], provider);

let comet = new ethers.Contract("0x...", [
  "function supply(address asset, uint256 amount)"
], provider);

let action = pipeline([
  invoke(await usdc.populateTransaction.approve(cUSDCv3.address.get(), Quark.UINT256_MAX.get())),
  pipe(readUint256(usdc.balanceOf(cUSDCv3.address.get())), (bal) => [ // Read from Ethers call
    cUSDCv3.supply(cUSDCv3.underlying, bal) // Can pipe only to built-ins, not to Ethers calls
  ])
]);

Solidity

** Solidity Support is Experimental

There is also experimental support for running Solidity code directly as a Quark command.

import * as Quark from '@compound-finance/quark';
import * as solc from 'solc';

let command = await Quark.buildSol(`
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Fun {
  event FunTimes(uint256);

  function hello() external {
    emit FunTimes(55);
  }
}
`, solc.compile); // Pass solc compilation command

console.log(`Command: ${command.description}`);
console.log(`Command YUL: ${command.yul}`);
console.log(`Command Bytecode: ${command.bytecode}`);

let tx = await Quark.exec(provider, command);

There are a lot of potential limitations in these scripts as we have less control over the generated code. It's recommended you inspect the Yul code directly to make sure it does what you would expect.

Yul

You can also build a command directly from Yul.

import * as Quark from '@compound-finance/quark';
import * as solc from 'solc';

let command = await Quark.buildYul(`
object "Ping" {
  code {
    // Store a value (55) in memory
    mstore(0x80, 55)

    // ABI topic for \`Ping(uint256)\`
    let topic := 0x48257dc961b6f792c2b78a080dacfed693b660960a702de21cee364e20270e2f

    // emit Ping(55)
    log1(0x80, 0x20, topic)

    return(0, 0)
  }
}
`, solc.compile); // Pass solc compilation command

console.log(`Command: ${command.description}`);
console.log(`Command YUL: ${command.yul}`);
console.log(`Command Bytecode: ${command.bytecode}`);

let tx = await Quark.exec(provider, command);

Future Considerations

We could probably improve the ability to pipe data into Ethers invocations, but that is starting to get a little dicey. Leaving it as a note for now to address later.

We should make Solc/Yul compilation (optionally?) outside of the main thread.

License

Copyright Geoffrey Hayes, Compound Labs, Inc. 2023. All rights reserved.

This software is provided "as-is" with no warranty whatsoever. By using this software, you agree that you shall arise no claim against the author or their representatives from any usage of this software.

0.0.6-alpha37

11 months ago

0.0.6-alpha36

11 months ago

0.0.6-alpha35

11 months ago

0.0.6-alpha34

11 months ago

0.0.6-alpha33

11 months ago

0.0.6-alpha32

11 months ago

0.0.6-alpha31

11 months ago

0.0.6-alpha30

11 months ago

0.0.6-alpha29

11 months ago

0.0.6-alpha28

11 months ago

0.0.6-alpha27

11 months ago

0.0.6-alpha26

11 months ago

0.0.6-alpha25

11 months ago

0.0.6-alpha24

11 months ago

0.0.6-alpha23

11 months ago

0.0.6-alpha22

11 months ago

0.0.6-alpha21

11 months ago

0.0.6-alpha20

11 months ago

0.0.6-alpha19

11 months ago

0.0.6-alpha18

11 months ago

0.0.6-alpha17

11 months ago

0.0.6-alpha16

11 months ago

0.0.6-alpha15

11 months ago

0.0.6-alpha14

11 months ago

0.0.6-alpha13

11 months ago

0.0.6-alpha12

11 months ago

0.0.6-alpha11

11 months ago

0.0.6-alpha10

11 months ago

0.0.6-alpha9

11 months ago

0.0.6-alpha8

11 months ago

0.0.6-alpha7

11 months ago

0.0.6-alpha6

11 months ago

0.0.6-alpha5

11 months ago

0.0.6-alpha4

11 months ago

0.0.6-alpha3

11 months ago

0.0.6-alpha2

11 months ago

0.0.6-alpha1

11 months ago

0.0.5-alpha1

11 months ago

0.0.4-alpha1

11 months ago

0.0.3-alpha1

11 months ago

0.0.2-alpha1

11 months ago

0.0.1-alpha1

11 months ago