どうも、初めましての投稿になります。imai@株式会社SaaSisです。
普段は新設の工場で、システム立ち上げのためのPMやトラブルシューティングなどを仕事にしています。
今回は仕事から離れて、Ethereum DeFiアプリケーション Uniswap V3をプライベートで使用していてちょっと不便に感じた部分を、簡単なプログラミングで効率化する方法をゆるい感じで紹介します。
なお、当記事ではUniswap自体の仕組み等については詳しく説明しません。
投資はくれぐれも自己責任でお願い致します🥺
動機
Uniswap V3でLP(流動性提供)を行うと、Unclaimed fees
(未請求の報酬)という手数料をGETできる。
参考URL(私の保有しているPoolではなくランダムのものです)
この値、流動性を提供している通貨ペアでSwap
(両替)がある度に増えるのだが、私の知る限り今現在の保有額しか確認できない。
この保有額の推移を自動で取得・記録し、グラフで表示できないかと考えた。
前提
- Uniswap V3でLPを行なっていること
- Googleアカウントを持っていること(無料のものでもOKと思われるが、未検証なので注意)
- GraphQL、Google Apps Script、Google Sheetsに関する知識があること
手順
SubgraphのPlaygroundページでクエリを試す
Uniswap V3ではクエリにGraphQLを使用するSubgraph APIが無料公開されている。
まずはこれを使って値の取得を試みる。
上記ページの下部に、GraphQLをクエリのみで簡単に試すことのできるPlayground画面がある。
ページに初めてアクセスしたときはデフォルトのExample query
になっているはずなので、中央ペイン右上の▶︎ボタンを押す。
クエリが実行され、その結果が中央ペインに出力されることを確認。
もちろん、この情報はなんの役にも立たないので、クエリを変更する必要がある。
LPに関する情報はPosition
パラメータで取ることができる。
(クエリのスキーマパラメータ詳細についてはあまりドキュメントが整備されていない為、ここで少しハマった)
{
position(id: {id}) { # {id}に自分が所持しているPoolのIDを入れる(ex. 128209)
token0{ # ペアとなる通貨(1つ目)
id
symbol # 通貨のシンボル(ex. USDC)
name
decimals # Unclaimed feesの元となる数値。後述の計算に使用する
}
token1{ # ペアとなる通貨(2つ目)
id
symbol
name
decimals
}
pool {
feeGrowthGlobal0X128 # 後述の計算に使用する
feeGrowthGlobal1X128
}
liquidity
feeGrowthInside0LastX128 # 後述の計算に使用する
feeGrowthInside1LastX128
tickLower {
feeGrowthOutside0X128 # 後述の計算に使用する
feeGrowthOutside1X128
}
tickUpper {
feeGrowthOutside0X128 # 後述の計算に使用する
feeGrowthOutside1X128
}
}
}
2行目の{id}
以外は、上記クエリを編集せずコピペで実行して構わない。
大きな数値がずらっと中央ペインに並べば、クエリは成功。
Unclaimed feesを計算するための数式を用意する
ここら辺は私も自信がないのだが、語義的に小数であるべきdecimalsの値が非常に大きな整数値になっている辺りを見ると、本来の小数値に変換する必要があるらしい。
詳しい説明は分からんので省き、こちらの回答を参考にする。
feetoken0 = ((feeGrowthGlobal0X128 - feeGrowthOutside0X128_lower - feeGrowthOutside0X128_upper - feeGrowthInside0LastX128)/(2**128))*liquidity/(1*10**decimals)
ただし、javascript上では2^53 - 1
を超える整数を扱うとエラーを吐いてしまうので、上記コードに加えてそれぞれの計算にBigInt
を使う。
GASにスクリプトを書く
幸い、BigInt
はGASでも使用可能。
コーディングの流れは:
-
UrlFetchApp.fetch(url, options)
でデータを取得 - それぞれの値をBigIntに変換し計算(BigIntはBigInt同士でないと四則演算ができないので注意)
- 日付とともに対象のスプレッドシートセルへ値を出力
下記、2つのPool
を取得してセルに出力するサンプルコード。
(かなりやっつけで書いてます。汚くて申し訳ないです🥺)
const ss = SpreadsheetApp.openById("YOUR_SHEET_ID");
// url, idsのベタ書きを避けたい場合: https://developers.google.com/apps-script/guides/properties
const url = "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3";
const ids = [
YOUR_POOL_ID1,
YOUR_POOL_ID2
];
function generateOptions(id) {
const queryString = `{
position(id: ${id}) {
token0{
id
symbol
name
decimals
}
token1{
id
symbol
name
decimals
}
pool {
feeGrowthGlobal0X128
feeGrowthGlobal1X128
}
liquidity
feeGrowthInside0LastX128
feeGrowthInside1LastX128
tickLower {
feeGrowthOutside0X128
feeGrowthOutside1X128
}
tickUpper {
feeGrowthOutside0X128
feeGrowthOutside1X128
}
}
}`;
const options = {
method: "POST",
contentType: "application/json",
payload: JSON.stringify({ query: queryString })
}
return options;
}
function generateDailyDataOptions(ids) {
const queryString = `{
tokenHourDatas(first:2, orderBy: periodStartUnix, orderDirection: desc, where: {token_in: ["${ids[0]}", "${ids[1]}"]}) {
periodStartUnix
id
priceUSD
token{
id
symbol
}
}
}`
const options = {
method: "POST",
contentType: "application/json",
payload: JSON.stringify({ query: queryString })
}
return options;
}
function main() {
for (const id of ids) {
const json = fetchDeFiData(url, generateOptions(id));
WriteConvertedValues(json);
}
}
function fetchDeFiData(url, options) {
try {
const res = UrlFetchApp.fetch(url, options);
const json = JSON.parse(res.getContentText());
return json;
} catch (e) {
}
}
function WriteConvertedValues(json) {
const position = json.data.position;
const feeGrowthGlobal0X128 = BigInt(position.pool.feeGrowthGlobal0X128);
const feeGrowthInside0LastX128 = BigInt(position.feeGrowthInside0LastX128);
const feeGrowthOutside0X128_lower = BigInt(position.tickLower.feeGrowthOutside0X128);
const feeGrowthOutside0X128_upper = BigInt(position.tickUpper.feeGrowthOutside0X128);
const liquidity = BigInt(position.liquidity);
const internalCalc0 = feeGrowthGlobal0X128 - feeGrowthOutside0X128_lower - feeGrowthOutside0X128_upper - feeGrowthInside0LastX128;
const decimals0 = position.token0.decimals;
const feetoken0 = parseFloat(internalCalc0 * liquidity / BigInt(2 ** 128)) / (1 * 10 ** decimals0);
const tokenName0 = position.token0.name;
const symbol0 = position.token0.symbol;
const id0 = position.token0.id;
const feeGrowthGlobal1X128 = BigInt(position.pool.feeGrowthGlobal1X128);
const feeGrowthInside1LastX128 = BigInt(position.feeGrowthInside1LastX128);
const feeGrowthOutside1X128_lower = BigInt(position.tickLower.feeGrowthOutside1X128);
const feeGrowthOutside1X128_upper = BigInt(position.tickUpper.feeGrowthOutside1X128);
const internalCalc1 = feeGrowthGlobal1X128 - feeGrowthOutside1X128_lower - feeGrowthOutside1X128_upper - feeGrowthInside1LastX128;
const decimals1 = position.token1.decimals;
const feetoken1 = parseFloat(internalCalc1 * liquidity / BigInt(2 ** 128)) / (1 * 10 ** decimals1);
const tokenName1 = position.token1.name;
const symbol1 = position.token1.symbol;
const id1 = position.token1.id;
const dailyJson = fetchDeFiData(url, generateDailyDataOptions([id0, id1]));
const tokenHourDatas = dailyJson.data.tokenHourDatas;
const priceUSD0 = tokenHourDatas[0].id === id0 ? tokenHourDatas[0].priceUSD : tokenHourDatas[1].priceUSD;
const priceUSD1 = tokenHourDatas[1].id === id1 ? tokenHOurDatas[1].priceUSD : tokenHourDatas[0].priceUSD;
const sheet = ss.getSheetByName('DeFi');
const targetRow = sheet.getRange(1, 1).getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow() + 1;
const values = [
[new Date().toLocaleDateString('ja-JP'), tokenName0, feetoken0, symbol0, priceUSD0, feetoken0 * priceUSD0],
[new Date().toLocaleDateString('ja-JP'), tokenName1, feetoken1, symbol1, priceUSD1, feetoken1 * priceUSD1]
]
const range = sheet.getRange(targetRow, 1, 2, 6);
range.setValues(values);
}
Google SheetsのQUERY関数やグラフを使ってデータを自動整形する & 定期実行のトリガーを設定する
割愛。
QUERY関数は濫用注意ではあるものの、すごく使えるのでものにしたい。
眺める
あぁ^〜癒されるんじゃぁ^〜😇
まとめ
途中で時間が足りなくなったので、中途半端な形ではありますが一旦公開します。
飽きたからではない
後々機会があれば更新します!
ほか、参考文献