Solana OrcaのDevnetサンプル実装(Orca Typescript SDK)

Orca Typescript SDK のサンプル実装。トークンのSwap, Farm Deposit/Withdraw, Pool Deposit/Withdrawができる。

READMEのままで進められるが、一箇所だけyarnでmz(Modernize node.js to current ECMAScript standards)をインストールする部分があった。

実装手順

1. ts-node

TypeScriptなので、事前にts-nodeが実行できるようにしておく。

2. パッケージインストール

フォルダと空のTSファイルを用意。

% mkdir orca-typescript-sdk
% cd orca-typescript-sdk
% touch index.ts

READMEのとおり、以下をインストールする。

% yarn add @orca-so/sdk @solana/web3.js decimal.js

mzが使われているため、それもインストールする。

% yarn add @types/mz

3. TS記述

READMEのUSAGEとDevnet Testingを参考に、作成した空のTSファイル「index.ts」に以下を記述する。(内容はREADMEの丸ごとコピペで、Devnet設定箇所も指定どおりに書き換え済み)

import { readFile } from "mz/fs";
import { Connection, Keypair } from "@solana/web3.js";
import { getOrca, OrcaFarmConfig, OrcaPoolConfig, Network } from "@orca-so/sdk";
import Decimal from "decimal.js";

const main = async () => {
  /*** Setup ***/
  // 1. Read secret key file to get owner keypair
  const secretKeyString = await readFile("/Users/scuba/my-wallet/my-keypair.json", {
    encoding: "utf8",
  });
  const secretKey = Uint8Array.from(JSON.parse(secretKeyString));
  const owner = Keypair.fromSecretKey(secretKey);

  // 2. Initialzie Orca object with mainnet connection
  const connection = new Connection("https://api.devnet.solana.com", "singleGossip");
  const orca = getOrca(connection, Network.DEVNET);

  try {
    /*** Swap ***/
    // 3. We will be swapping 0.1 SOL for some ORCA
    const orcaSolPool = orca.getPool(OrcaPoolConfig.ORCA_SOL);
    const solToken = orcaSolPool.getTokenB();
    const solAmount = new Decimal(0.1);
    const quote = await orcaSolPool.getQuote(solToken, solAmount);
    const orcaAmount = quote.getMinOutputAmount();

    console.log(`Swap ${solAmount.toString()} SOL for at least ${orcaAmount.toNumber()} ORCA`);
    const swapPayload = await orcaSolPool.swap(owner, solToken, solAmount, orcaAmount);
    const swapTxId = await swapPayload.execute();
    console.log("Swapped:", swapTxId, "\n");

    /*** Pool Deposit ***/
    // 4. Deposit SOL and ORCA for LP token
    const { maxTokenAIn, maxTokenBIn, minPoolTokenAmountOut } = await orcaSolPool.getDepositQuote(
      orcaAmount,
      solAmount
    );

    console.log(
      `Deposit at most ${maxTokenBIn.toNumber()} SOL and ${maxTokenAIn.toNumber()} ORCA, for at least ${minPoolTokenAmountOut.toNumber()} LP tokens`
    );
    const poolDepositPayload = await orcaSolPool.deposit(
      owner,
      maxTokenAIn,
      maxTokenBIn,
      minPoolTokenAmountOut
    );
    const poolDepositTxId = await poolDepositPayload.execute();
    console.log("Pool deposited:", poolDepositTxId, "\n");

    /*** Farm Deposit ***/
    // 5. Deposit some ORCA_SOL LP token for farm token
    const lpBalance = await orcaSolPool.getLPBalance(owner.publicKey);
    const orcaSolFarm = orca.getFarm(OrcaFarmConfig.ORCA_SOL_AQ);
    const farmDepositPayload = await orcaSolFarm.deposit(owner, lpBalance);
    const farmDepositTxId = await farmDepositPayload.execute();
    console.log("Farm deposited:", farmDepositTxId, "\n");
    // Note 1: for double dip, repeat step 5 but with the double dip farm
    // Note 2: to harvest reward, orcaSolFarm.harvest(owner)
    // Note 3: to get harvestable reward amount, orcaSolFarm.getHarvestableAmount(owner.publicKey)

    /*** Farm Withdraw ***/
    // 6. Withdraw ORCA_SOL LP token, in exchange for farm token
    const farmBalance = await orcaSolFarm.getFarmBalance(owner.publicKey); // withdraw the entire balance
    const farmWithdrawPayload = await orcaSolFarm.withdraw(owner, farmBalance);
    const farmWithdrawTxId = await farmWithdrawPayload.execute();
    console.log("Farm withdrawn:", farmWithdrawTxId, "\n");

    /*** Pool Withdraw ***/
    // 6. Withdraw SOL and ORCA, in exchange for ORCA_SOL LP token
    const withdrawTokenAmount = await orcaSolPool.getLPBalance(owner.publicKey);
    const withdrawTokenMint = orcaSolPool.getPoolTokenMint();
    const { maxPoolTokenAmountIn, minTokenAOut, minTokenBOut } = await orcaSolPool.getWithdrawQuote(
      withdrawTokenAmount,
      withdrawTokenMint
    );

    console.log(
      `Withdraw at most ${maxPoolTokenAmountIn.toNumber()} ORCA_SOL LP token for at least ${minTokenAOut.toNumber()} ORCA and ${minTokenBOut.toNumber()} SOL`
    );
    const poolWithdrawPayload = await orcaSolPool.withdraw(
      owner,
      maxPoolTokenAmountIn,
      minTokenAOut,
      minTokenBOut
    );
    const poolWithdrawTxId = await poolWithdrawPayload.execute();
    console.log("Pool withdrawn:", poolWithdrawTxId, "\n");
  } catch (err) {
    console.warn(err);
  }
};

main()
  .then(() => {
    console.log("Done");
  })
  .catch((e) => {
    console.error(e);
  });

ただし、このままだとKeypairのパスがおかしいため、solana config getで正しい情報を取得して書き直す必要あり。

% solana config get
Config File: /Users/user/.config/solana/cli/config.yml
RPC URL: http://127.0.0.1:8899
WebSocket URL: ws://127.0.0.1:8900/ (computed)
Keypair Path: /Users/user/.config/solana/id.json
Commitment: confirmed

上記のKeypair Pathに書き換える。なお、チルダは使えなかった。
(こういう指定はできない → ~/.config/solana/id.json)

  const secretKeyString = await readFile("/Users/user/.config/solana/id.json", {

4. 実行

% ts-node index.ts
Swap 0.1 SOL for at least 0.011221 ORCA
Swapped: 3PMMF1wBiLZejcokG9Udj7HfSP1qttb5CWMo7aWYbzUWtfq6sZdTBwzStZ1J2YGyzuoL3NKV5VjYiibEUD6RJ4e

Deposit at most 0.1 SOL and 0.011221 ORCA, for at least 1.021566 LP tokens
Pool deposited: w8iS1Fz8QNxA4p7aQsV4x86NyRXqTxy1XvXA1ciipJp4hDJR1yJyfgT9UydngSSWee23zGGo2gBHq78fEqk3LPY

Farm deposited: TkGhzcYwCTdvvyC76FkX9SZP8fvAqigujPvFVbTBEXj2Yr1qyAvNN8aTyUc8adD1yeV5LATa2tuxibdXQT6ohqD

Farm withdrawn: 5n4XsMfTa3CD9RTjT4WcoUs7VsHJds1Um3bZwTQzKuPxD8wnKACCipz4SjogPWp5R7DvTv4MMd7ZZJjr32YBcBug

Withdraw at most 1.021566 ORCA_SOL LP token for at least 0.011198 ORCA and 0.099433553 SOL
Pool withdrawn: 2hSDz5zhkpWYhRWkXtUEvk1isNvtDKbDNXiMgRAGmkW4CpEoUuMojqyCTf4awEo5YGhrGf6cmdsRMvQFVN92drQ2

Done

エラー対応

mzエラー

mzが入っていないため発生するエラー。

% ts-node index.ts
/usr/local/lib/node_modules/ts-node/src/index.ts:750
    return new TSError(diagnosticText, diagnosticCodes);
           ^
TSError: ⨯ Unable to compile TypeScript:
index.ts:1:26 - error TS7016: Could not find a declaration file for module 'mz/fs'. '/Users/user/Desktop/blockchain/orca-typescript-sdk/node_modules/mz/fs.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/mz` if it exists or add a new declaration (.d.ts) file containing `declare module 'mz/fs';`

1 import { readFile } from "mz/fs";
                           ~~~~~~~

    at createTSError (/usr/local/lib/node_modules/ts-node/src/index.ts:750:12)
    at reportTSError (/usr/local/lib/node_modules/ts-node/src/index.ts:754:19)
    at getOutput (/usr/local/lib/node_modules/ts-node/src/index.ts:941:36)
    at Object.compile (/usr/local/lib/node_modules/ts-node/src/index.ts:1243:30)
    at Module.m._compile (/usr/local/lib/node_modules/ts-node/src/index.ts:1370:30)
    at Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
    at Object.require.extensions.<computed> [as .ts] (/usr/local/lib/node_modules/ts-node/src/index.ts:1374:12)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12) {
  diagnosticText: "\x1B[96mindex.ts\x1B[0m:\x1B[93m1\x1B[0m:\x1B[93m26\x1B[0m - \x1B[91merror\x1B[0m\x1B[90m TS7016: \x1B[0mCould not find a declaration file for module 'mz/fs'. '/Users/user/Desktop/blockchain/orca-typescript-sdk/node_modules/mz/fs.js' implicitly has an 'any' type.\n" +
    "  Try `npm i --save-dev @types/mz` if it exists or add a new declaration (.d.ts) file containing `declare module 'mz/fs';`\n" +
    '\n' +
    '\x1B[7m1\x1B[0m import { readFile } from "mz/fs";\n' +
    '\x1B[7m \x1B[0m \x1B[91m                         ~~~~~~~\x1B[0m\n',
  diagnosticCodes: [ 7016 ]
}

mzをインストールする。

% yarn add @types/mz

Keypairパスエラー

Keypairパスが見つからないというエラー。solana config getで正しいKeypairパスをTSファイルに記述する。

% ts-node index.ts
[Error: ENOENT: no such file or directory, open '/Users/scuba/my-wallet/my-keypair.json'] {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: '/Users/scuba/my-wallet/my-keypair.json'
}